Skip to content

remorses/lintcn

Repository files navigation

lintcn

The shadcn for type-aware TypeScript lint rules. Powered by tsgolint.

Add rules by URL, own the source, customize freely. Rules are Go files that use the TypeScript type checker for deep analysis — things ESLint can't do.

Install

npm install -D lintcn

Usage

# Add a rule folder from tsgolint
npx lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises

# Add by file URL (auto-fetches the whole folder)
npx lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go

# Lint your project
npx lintcn lint

# Show warning rules for all files, not just updated/added files
npx lintcn lint --all-warnings

# Lint with a specific tsconfig
npx lintcn lint --tsconfig tsconfig.build.json

# List installed rules
npx lintcn list

# Remove a rule
npx lintcn remove no-floating-promises

# Clean cached tsgolint source + binaries
npx lintcn clean

Browse all 50+ available built-in rules in the tsgolint rules directory.

How it works

Each rule lives in its own subfolder under .lintcn/. You own the source — edit, customize, delete.

my-project/
├── .lintcn/
│   ├── .gitignore                          ← ignores generated Go files
│   ├── no_floating_promises/
│   │   ├── no_floating_promises.go         ← rule source (committed)
│   │   ├── no_floating_promises_test.go    ← tests (committed)
│   │   └── options.go                      ← rule options struct
│   ├── await_thenable/
│   │   ├── await_thenable.go
│   │   └── await_thenable_test.go
│   └── my_custom_rule/
│       └── my_custom_rule.go
├── src/
│   └── ...
├── tsconfig.json
└── package.json

When you run npx lintcn lint, the CLI:

  1. Scans .lintcn/*/ subfolders for rule definitions
  2. Generates a Go workspace with your custom rules
  3. Compiles a custom binary (cached — rebuilds only when rules change)
  4. Runs the binary against your project

You can run lintcn lint from any subdirectory — it walks up to find .lintcn/ and lints the cwd project.

Writing custom rules

To help AI agents write and modify rules, install the lintcn skill:

npx skills add remorses/lintcn

This gives your AI agent the full tsgolint rule API reference — AST visitors, type checker, reporting, fixes, and testing patterns.

Every rule lives in a subfolder under .lintcn/ with the package name matching the folder:

// .lintcn/no_unhandled_error/no_unhandled_error.go

// lintcn:name no-unhandled-error
// lintcn:description Disallow discarding Error-typed return values

package no_unhandled_error

import (
    "github.com/microsoft/typescript-go/shim/ast"
    "github.com/microsoft/typescript-go/shim/checker"
    "github.com/typescript-eslint/tsgolint/internal/rule"
    "github.com/typescript-eslint/tsgolint/internal/utils"
)

var NoUnhandledErrorRule = rule.Rule{
    Name: "no-unhandled-error",
    Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
        return rule.RuleListeners{
            ast.KindExpressionStatement: func(node *ast.Node) {
                expression := ast.SkipParentheses(node.AsExpressionStatement().Expression)

                if ast.IsVoidExpression(expression) {
                    return // void = intentional discard
                }

                innerExpr := expression
                if ast.IsAwaitExpression(innerExpr) {
                    innerExpr = ast.SkipParentheses(innerExpr.Expression())
                }
                if !ast.IsCallExpression(innerExpr) {
                    return
                }

                t := ctx.TypeChecker.GetTypeAtLocation(expression)

                if utils.IsTypeFlagSet(t, checker.TypeFlagsVoid|checker.TypeFlagsUndefined|checker.TypeFlagsNever) {
                    return
                }

                for _, part := range utils.UnionTypeParts(t) {
                    if utils.IsErrorLike(ctx.Program, ctx.TypeChecker, part) {
                        ctx.ReportNode(node, rule.RuleMessage{
                            Id:          "noUnhandledError",
                            Description: "Error-typed return value is not handled.",
                        })
                        return
                    }
                }
            },
        }
    },
}

This catches code like:

// error — result discarded, Error not handled
getUser("id"); // returns Error | User
await fetchData("/api"); // returns Promise<Error | Data>

// ok — result is checked
const user = getUser("id");
if (user instanceof Error) return user;

// ok — explicitly discarded
void getUser("id");

Warning severity

Rules can be configured as warnings instead of errors:

  • Don't fail CI — warnings produce exit code 0
  • Only shown for updated/added files by default — warning rules are limited to files in git diff plus untracked files, so unchanged files are silently skipped

This lets you adopt new rules gradually. In a large codebase, enabling a rule as an error means hundreds of violations at once. As a warning, you only see violations in files you're actively changing or adding — fixing issues in new code without blocking the build.

Configuring a rule as a warning

Add // lintcn:severity warn to the rule's Go file:

// lintcn:name no-unhandled-error
// lintcn:severity warn
// lintcn:description Disallow discarding Error-typed return values

Rules without // lintcn:severity default to error.

When warnings are shown

By default, lintcn lint runs git diff to find updated files and also includes untracked files you just added. Warning rules are only printed for files in that set:

# Warnings only for updated files plus newly added untracked files (default)
npx lintcn lint

# Warnings for ALL files, ignoring git diff
npx lintcn lint --all-warnings
Scenario Warnings shown?
File is updated in git diff Yes
File is newly added and untracked Yes
File is committed and unchanged No
--all-warnings flag is passed Yes, all files
Git is not installed or not a repo No warnings shown
Clean git tree (no changes, no new files) No warnings shown

Workflow

  1. Add a new rule with lintcn add
  2. Set it to // lintcn:severity warn in the Go source
  3. Run lintcn lint — only see warnings in files you're currently editing or adding
  4. Fix warnings as you touch files naturally
  5. Once the codebase is clean, change to // lintcn:severity error (or remove the directive) to enforce it

Version pinning

Pin lintcn in your package.json — do not use ^ or ~:

{
  "devDependencies": {
    "lintcn": "0.5.0"
  }
}

Each lintcn release bundles a specific tsgolint version. Updating lintcn can change the underlying tsgolint API, which may cause your rules to no longer compile. Always update consciously:

  1. Check the changelog for tsgolint version changes
  2. Run npx lintcn build after updating to verify your rules still compile
  3. Fix any compilation errors before committing

CI Setup

The first lintcn lint compiles a custom Go binary (~30s). Subsequent runs use the cached binary (<1s). Cache ~/.cache/lintcn/ and Go's build cache to keep CI fast.

# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Cache lintcn binary + Go build cache
        uses: actions/cache@v4
        with:
          path: |
            ~/.cache/lintcn
            ~/go/pkg
          key: lintcn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.lintcn/**/*.go') }}
          restore-keys: |
            lintcn-${{ runner.os }}-${{ runner.arch }}-

      - run: npm ci
      - run: npx lintcn lint

The cache key includes a hash of your rule files — when rules change, the binary is recompiled. The restore-keys fallback ensures Go's build cache is still used even when rules change, so recompilation takes ~1s instead of 30s.

Prerequisites

  • Node.js — for the CLI
  • Go — for compiling rules (go.dev/dl)

Go is only needed for lintcn lint / lintcn build. Adding and listing rules works without Go.

License

MIT

About

shadcn for type aware lint rules. Let agents write custom rules that follow your preferences. Prevent agents from adding 'as' and 'in' everywhere. Based on tsgolint & go

Resources

Stars

Watchers

Forks

Packages