lintcn
v0.7.0
Published
The shadcn for type-aware TypeScript lint rules. Browse, pick, and copy rules into your project.
Maintainers
Readme
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 lintcnUsage
# 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
# 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 cleanBrowse 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.jsonWhen you run npx lintcn lint, the CLI:
- Scans
.lintcn/*/subfolders for rule definitions - Generates a Go workspace with your custom rules
- Compiles a custom binary (cached — rebuilds only when rules change)
- 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/lintcnThis 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 git-changed files — warnings for 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 — 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 valuesRules without // lintcn:severity default to error.
When warnings are shown
By default, lintcn lint runs git diff to find changed and untracked files. Warnings are only printed for files in that list:
# Warnings only for files in git diff (default)
npx lintcn lint
# Warnings for ALL files, ignoring git diff
npx lintcn lint --all-warnings| Scenario | Warnings shown? |
| ---------------------------------- | ----------------- |
| File is in git diff or 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 warnings shown |
Workflow
- Add a new rule with
lintcn add - Set it to
// lintcn:severity warnin the Go source - Run
lintcn lint— only see warnings in files you're currently editing - Fix warnings as you touch files naturally
- 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:
- Check the changelog for tsgolint version changes
- Run
npx lintcn buildafter updating to verify your rules still compile - 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 lintThe 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
