lintcn 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,111 @@
1
+ ## 0.7.0
2
+
3
+ 1. **`lintcn lint --fix`** — automatically apply fixes in-place. Collects diagnostics per file, applies fixes via the Go runner, and only reports what couldn't be auto-fixed:
4
+
5
+ ```bash
6
+ lintcn lint --fix
7
+ ```
8
+
9
+ 2. **Warning severity system** — rules can now declare `// lintcn:severity warn`. Warnings don't fail CI (exit 0) and are filtered to git-changed files by default so they don't flood large codebases:
10
+
11
+ ```go
12
+ // lintcn:severity warn
13
+ ```
14
+
15
+ Two new flags:
16
+ - `--all-warnings` — show warnings for all files, not just changed ones
17
+ - `lintcn list` now shows a `(warn)` suffix on warning-severity rules
18
+
19
+ 3. **New rule: `no-type-assertion` (warn)** — flags every `as X` / `<X>expr` and includes the actual expression type so agents know what they're working with:
20
+
21
+ ```
22
+ warning: Type assertion to `User ({ name: string; age: number })` from `unknown`
23
+ ```
24
+
25
+ User-defined types show their structural form in parentheses. Standard library types (Array, Map, Promise, etc.) are not expanded. Assertion chains (`x as unknown as Foo`) walk back to the original source type. Casting from `any` is silently allowed (standard untyped-API pattern).
26
+
27
+ 4. **New rule: `no-in-operator` (warn)** — warns on every `in` expression and shows the expanded type of the right-hand operand. When the right side is a union and the property exists in some members but not others, it names which members have it and suggests using a discriminant property instead:
28
+
29
+ ```
30
+ warning: Avoid the `in` operator on `Cat | Dog`. Property `meow` exists in Cat but not Dog.
31
+ Consider using a discriminant property (e.g. `kind`) instead of `in`.
32
+ ```
33
+
34
+ 5. **New rule: `no-redundant-in-check` (error)** — flags `"y" in x` when the type already has `y` as a required non-optional property in all union members. The check can never be false — it's dead code:
35
+
36
+ ```ts
37
+ interface User { name: string; age: number }
38
+ if ('name' in user) { ... } // error: redundant — User always has 'name'
39
+ ```
40
+
41
+ 6. **New built-in rules**: `jsx-no-leaked-render`, `no-unhandled-error`, `no-useless-coalescing` — available via `lintcn add` or by pointing at the `.lintcn/` folder URL.
42
+
43
+ 7. **`lintcn add` with whole repo URL** — download all rules from a repo's `.lintcn/` folder in one shot. Merge semantics: remote rule folders overwrite local ones; local-only rules are preserved:
44
+
45
+ ```bash
46
+ # bare repo URL — fetches all rules from .lintcn/ at repo root
47
+ lintcn add https://github.com/remorses/lintcn
48
+
49
+ # tree URL pointing at a .lintcn collection
50
+ lintcn add https://github.com/remorses/lintcn/tree/main/.lintcn
51
+ ```
52
+
53
+ 8. **Fixed `await-thenable` false positive on overloaded functions** — when a function has multiple call signatures (overloads or intersection-of-callable-types), the rule now checks if any overload returns a thenable before reporting. Fixes false positives like `await extract({...})` from the `tar` package.
54
+
55
+ 9. **Brighter error underline** — error highlights changed from ANSI 256-color 160 (muted red) to 196 (pure bright red). Run `lintcn clean` to clear the old cached binary.
56
+
57
+ ## 0.6.0
58
+
59
+ 1. **Rules now live in subfolders** — each rule is its own Go package under `.lintcn/{rule_name}/`, replacing the old flat `.lintcn/*.go` layout. This eliminates the need to rename `options.go` and `schema.json` companions — they stay in the subfolder with their original names, and the Go package name matches the folder. `lintcn add` now fetches the entire rule folder automatically.
60
+
61
+ ```
62
+ .lintcn/
63
+ no_floating_promises/
64
+ no_floating_promises.go
65
+ no_floating_promises_test.go
66
+ options.go ← original name, no renaming
67
+ schema.json
68
+ my_custom_rule/
69
+ my_custom_rule.go
70
+ ```
71
+
72
+ 2. **`lintcn add` fetches whole folders** — both folder URLs (`/tree/`) and file URLs (`/blob/`) now fetch every `.go` and `.json` file in the rule's directory. Passing a file URL auto-detects the parent folder:
73
+
74
+ ```bash
75
+ # folder URL
76
+ lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises
77
+
78
+ # file URL — auto-fetches the whole folder
79
+ lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go
80
+ ```
81
+
82
+ 3. **Error for flat `.go` files in `.lintcn/`** — if leftover flat files from older versions are detected, lintcn now prints a clear migration error with instructions instead of silently ignoring them.
83
+
84
+ 4. **Reproducible builds with `-trimpath`** — the Go binary is now built with `-trimpath`, stripping absolute paths from the output. Binaries are identical across machines for the same rule content + tsgolint version + platform.
85
+
86
+ 5. **Faster cache hits** — Go version removed from the content hash. The compiled binary is a standalone executable with no Go runtime dependency, so the Go version used to build it doesn't affect correctness. Also excludes `_test.go` files from the hash since tests don't affect the binary.
87
+
88
+ 6. **Go compilation output is live** — `go build` now inherits stdio, so compilation progress and errors stream directly to the terminal instead of being silently captured.
89
+
90
+ 7. **First-build guidance** — on first compile (cold Go cache), lintcn explains the one-time 30s cost and shows which directories to cache in CI:
91
+ ```
92
+ Compiling custom tsgolint binary (first build — may take 30s+ to compile dependencies)...
93
+ Subsequent builds will be fast (~1s). In CI, cache ~/.cache/lintcn/ and GOCACHE (run `go env GOCACHE`).
94
+ ```
95
+
96
+ 8. **GitHub Actions example** — README now includes a copy-paste workflow that caches the compiled binary. Subsequent CI runs take ~12s (vs ~4min cold):
97
+
98
+ ```yaml
99
+ - name: Cache lintcn binary + Go build cache
100
+ uses: actions/cache@v4
101
+ with:
102
+ path: |
103
+ ~/.cache/lintcn
104
+ ~/go/pkg
105
+ key: lintcn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.lintcn/**/*.go') }}
106
+ restore-keys: lintcn-${{ runner.os }}-${{ runner.arch }}-
107
+ ```
108
+
1
109
  ## 0.5.0
2
110
 
3
111
  1. **Security fix — path traversal in `--tsgolint-version`** — the version flag is now validated against a strict pattern. Previously a value like `../../etc` could escape the cache directory.
package/README.md CHANGED
@@ -13,8 +13,11 @@ npm install -D lintcn
13
13
  ## Usage
14
14
 
15
15
  ```bash
16
- # Add a rule by URL
17
- npx lintcn add https://github.com/user/repo/blob/main/rules/no_unhandled_error.go
16
+ # Add a rule folder from tsgolint
17
+ npx lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises
18
+
19
+ # Add by file URL (auto-fetches the whole folder)
20
+ npx lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go
18
21
 
19
22
  # Lint your project
20
23
  npx lintcn lint
@@ -26,21 +29,32 @@ npx lintcn lint --tsconfig tsconfig.build.json
26
29
  npx lintcn list
27
30
 
28
31
  # Remove a rule
29
- npx lintcn remove no-unhandled-error
32
+ npx lintcn remove no-floating-promises
33
+
34
+ # Clean cached tsgolint source + binaries
35
+ npx lintcn clean
30
36
  ```
31
37
 
38
+ Browse all 50+ available built-in rules in the [tsgolint rules directory](https://github.com/oxc-project/tsgolint/tree/main/internal/rules).
39
+
32
40
  ## How it works
33
41
 
34
- Rules live as `.go` files in `.lintcn/` at your project root. You own the source — edit, customize, delete.
42
+ Each rule lives in its own subfolder under `.lintcn/`. You own the source — edit, customize, delete.
35
43
 
36
44
  ```
37
45
  my-project/
38
46
  ├── .lintcn/
39
- │ ├── .gitignore ← ignores generated Go files
40
- │ ├── no_unhandled_error.go ← your rule (committed)
41
- └── no_unhandled_error_test.go its tests (committed)
47
+ │ ├── .gitignore ← ignores generated Go files
48
+ │ ├── no_floating_promises/
49
+ │ ├── no_floating_promises.go rule source (committed)
50
+ │ │ ├── no_floating_promises_test.go ← tests (committed)
51
+ │ │ └── options.go ← rule options struct
52
+ │ ├── await_thenable/
53
+ │ │ ├── await_thenable.go
54
+ │ │ └── await_thenable_test.go
55
+ │ └── my_custom_rule/
56
+ │ └── my_custom_rule.go
42
57
  ├── src/
43
- │ ├── index.ts
44
58
  │ └── ...
45
59
  ├── tsconfig.json
46
60
  └── package.json
@@ -48,24 +62,32 @@ my-project/
48
62
 
49
63
  When you run `npx lintcn lint`, the CLI:
50
64
 
51
- 1. Scans `.lintcn/*.go` for rule definitions
65
+ 1. Scans `.lintcn/*/` subfolders for rule definitions
52
66
  2. Generates a Go workspace with your custom rules
53
67
  3. Compiles a custom binary (cached — rebuilds only when rules change)
54
68
  4. Runs the binary against your project
55
69
 
56
70
  You can run `lintcn lint` from any subdirectory — it walks up to find `.lintcn/` and lints the cwd project.
57
71
 
58
- ## Writing a rule
72
+ ## Writing custom rules
59
73
 
60
- Every rule is a Go file with `package lintcn` that exports a `rule.Rule` variable.
74
+ To help AI agents write and modify rules, install the lintcn skill:
61
75
 
62
- Here's a rule that errors when you discard the return value of a function that returns `Error | T` — enforcing the [errore](https://errore.org) pattern:
76
+ ```bash
77
+ npx skills add remorses/lintcn
78
+ ```
79
+
80
+ This gives your AI agent the full tsgolint rule API reference — AST visitors, type checker, reporting, fixes, and testing patterns.
81
+
82
+ Every rule lives in a subfolder under `.lintcn/` with the package name matching the folder:
63
83
 
64
84
  ```go
85
+ // .lintcn/no_unhandled_error/no_unhandled_error.go
86
+
65
87
  // lintcn:name no-unhandled-error
66
88
  // lintcn:description Disallow discarding Error-typed return values
67
89
 
68
- package lintcn
90
+ package no_unhandled_error
69
91
 
70
92
  import (
71
93
  "github.com/microsoft/typescript-go/shim/ast"
@@ -118,17 +140,66 @@ This catches code like:
118
140
 
119
141
  ```typescript
120
142
  // error — result discarded, Error not handled
121
- getUser("id") // returns Error | User
122
- await fetchData("/api") // returns Promise<Error | Data>
143
+ getUser("id"); // returns Error | User
144
+ await fetchData("/api"); // returns Promise<Error | Data>
123
145
 
124
146
  // ok — result is checked
125
- const user = getUser("id")
126
- if (user instanceof Error) return user
147
+ const user = getUser("id");
148
+ if (user instanceof Error) return user;
127
149
 
128
150
  // ok — explicitly discarded
129
- void getUser("id")
151
+ void getUser("id");
130
152
  ```
131
153
 
154
+ ## Warning severity
155
+
156
+ Rules can be configured as **warnings** instead of errors:
157
+
158
+ - **Don't fail CI** — warnings produce exit code 0
159
+ - **Only shown for git-changed files** — warnings for unchanged files are silently skipped
160
+
161
+ 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.
162
+
163
+ ### Configuring a rule as a warning
164
+
165
+ Add `// lintcn:severity warn` to the rule's Go file:
166
+
167
+ ```go
168
+ // lintcn:name no-unhandled-error
169
+ // lintcn:severity warn
170
+ // lintcn:description Disallow discarding Error-typed return values
171
+ ```
172
+
173
+ Rules without `// lintcn:severity` default to `error`.
174
+
175
+ ### When warnings are shown
176
+
177
+ By default, `lintcn lint` runs `git diff` to find changed and untracked files. Warnings are only printed for files in that list:
178
+
179
+ ```bash
180
+ # Warnings only for files in git diff (default)
181
+ npx lintcn lint
182
+
183
+ # Warnings for ALL files, ignoring git diff
184
+ npx lintcn lint --all-warnings
185
+ ```
186
+
187
+ | Scenario | Warnings shown? |
188
+ | ---------------------------------- | ----------------- |
189
+ | File is in `git diff` or untracked | Yes |
190
+ | File is committed and unchanged | No |
191
+ | `--all-warnings` flag is passed | Yes, all files |
192
+ | Git is not installed or not a repo | No warnings shown |
193
+ | Clean git tree (no changes) | No warnings shown |
194
+
195
+ ### Workflow
196
+
197
+ 1. Add a new rule with `lintcn add`
198
+ 2. Set it to `// lintcn:severity warn` in the Go source
199
+ 3. Run `lintcn lint` — only see warnings in files you're currently editing
200
+ 4. Fix warnings as you touch files naturally
201
+ 5. Once the codebase is clean, change to `// lintcn:severity error` (or remove the directive) to enforce it
202
+
132
203
  ## Version pinning
133
204
 
134
205
  **Pin lintcn in your `package.json`** — do not use `^` or `~`:
@@ -136,7 +207,7 @@ void getUser("id")
136
207
  ```json
137
208
  {
138
209
  "devDependencies": {
139
- "lintcn": "0.4.0"
210
+ "lintcn": "0.5.0"
140
211
  }
141
212
  }
142
213
  ```
@@ -147,12 +218,41 @@ Each lintcn release bundles a specific tsgolint version. Updating lintcn can cha
147
218
  2. Run `npx lintcn build` after updating to verify your rules still compile
148
219
  3. Fix any compilation errors before committing
149
220
 
150
- You can test against an unreleased tsgolint version without updating lintcn:
151
-
152
- ```bash
153
- npx lintcn lint --tsgolint-version v0.10.0
221
+ ## CI Setup
222
+
223
+ 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.
224
+
225
+ ```yaml
226
+ # .github/workflows/lint.yml
227
+ name: Lint
228
+ on: [push, pull_request]
229
+
230
+ jobs:
231
+ lint:
232
+ runs-on: ubuntu-latest
233
+ steps:
234
+ - uses: actions/checkout@v4
235
+
236
+ - uses: actions/setup-node@v4
237
+ with:
238
+ node-version: 22
239
+
240
+ - name: Cache lintcn binary + Go build cache
241
+ uses: actions/cache@v4
242
+ with:
243
+ path: |
244
+ ~/.cache/lintcn
245
+ ~/go/pkg
246
+ key: lintcn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.lintcn/**/*.go') }}
247
+ restore-keys: |
248
+ lintcn-${{ runner.os }}-${{ runner.arch }}-
249
+
250
+ - run: npm ci
251
+ - run: npx lintcn lint
154
252
  ```
155
253
 
254
+ 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.
255
+
156
256
  ## Prerequisites
157
257
 
158
258
  - **Node.js** — for the CLI
package/dist/cache.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const DEFAULT_TSGOLINT_VERSION = "e945641eabec22993eda3e7c101692e80417e0ea";
1
+ export declare const DEFAULT_TSGOLINT_VERSION = "23190a08a6315eba8ef11818fc1c38d7b01c9e10";
2
2
  /** Validate version string to prevent path traversal attacks.
3
3
  * Only allows alphanumeric chars, dots, underscores, and hyphens. */
4
4
  export declare function validateVersion(version: string): void;
package/dist/cache.js CHANGED
@@ -16,7 +16,7 @@ import { execAsync } from "./exec.js";
16
16
  // Pinned tsgolint fork commit — updated with each lintcn release.
17
17
  // Uses remorses/tsgolint fork which adds internal/runner.Run().
18
18
  // Only 1 commit on top of upstream — zero modifications to existing files.
19
- export const DEFAULT_TSGOLINT_VERSION = 'e945641eabec22993eda3e7c101692e80417e0ea';
19
+ export const DEFAULT_TSGOLINT_VERSION = '23190a08a6315eba8ef11818fc1c38d7b01c9e10';
20
20
  // Pinned typescript-go base commit from microsoft/typescript-go (before patches).
21
21
  // Patches from tsgolint/patches/ are applied on top during setup.
22
22
  // Must be updated when DEFAULT_TSGOLINT_VERSION changes.
package/dist/cli.js CHANGED
@@ -7,16 +7,19 @@ import { addRule } from "./commands/add.js";
7
7
  import { lint, buildBinary } from "./commands/lint.js";
8
8
  import { listRules } from "./commands/list.js";
9
9
  import { removeRule } from "./commands/remove.js";
10
+ import { clean } from "./commands/clean.js";
10
11
  import { DEFAULT_TSGOLINT_VERSION } from "./cache.js";
11
12
  const require = createRequire(import.meta.url);
12
13
  const packageJson = require('../package.json');
13
14
  const cli = goke('lintcn');
14
15
  cli
15
- .command('add <url>', 'Add a rule by URL. Fetches the .go file and copies it into .lintcn/')
16
- .example('# Add a rule from GitHub')
17
- .example('lintcn add https://github.com/user/repo/blob/main/rules/no_floating_promises.go')
18
- .example('# Add from raw URL')
19
- .example('lintcn add https://raw.githubusercontent.com/user/repo/main/rules/no_unused_result.go')
16
+ .command('add <url>', 'Add rules by GitHub URL. Supports single rule folders, .lintcn/ directories, or full repo URLs.')
17
+ .example('# Add a single rule folder')
18
+ .example('lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises')
19
+ .example('# Add by file URL (auto-fetches the whole folder)')
20
+ .example('lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go')
21
+ .example('# Add all rules from a repo (downloads .lintcn/ folder)')
22
+ .example('lintcn add https://github.com/someone/their-project')
20
23
  .action(async (url) => {
21
24
  await addRule(url);
22
25
  });
@@ -34,12 +37,17 @@ cli
34
37
  cli
35
38
  .command('lint', 'Build custom tsgolint binary and run it against the project')
36
39
  .option('--rebuild', 'Force rebuild even if cached binary exists')
40
+ .option('--fix', 'Automatically fix violations')
37
41
  .option('--tsconfig <path>', 'Path to tsconfig.json')
38
42
  .option('--list-files', 'List matched files')
43
+ .option('--all-warnings', 'Show warnings for all files, not just git-changed ones')
39
44
  .option('--tsgolint-version [version]', 'Override the pinned tsgolint version (tag or commit). For testing unreleased tsgolint versions.')
40
45
  .action(async (options) => {
41
46
  const tsgolintVersion = options.tsgolintVersion || DEFAULT_TSGOLINT_VERSION;
42
47
  const passthroughArgs = [];
48
+ if (options.fix) {
49
+ passthroughArgs.push('--fix');
50
+ }
43
51
  if (options.tsconfig) {
44
52
  passthroughArgs.push('--tsconfig', options.tsconfig);
45
53
  }
@@ -55,6 +63,7 @@ cli
55
63
  rebuild: !!options.rebuild,
56
64
  tsgolintVersion,
57
65
  passthroughArgs,
66
+ allWarnings: !!options.allWarnings,
58
67
  });
59
68
  process.exit(exitCode);
60
69
  });
@@ -67,6 +76,11 @@ cli
67
76
  const binaryPath = await buildBinary({ rebuild: !!options.rebuild, tsgolintVersion });
68
77
  console.log(binaryPath);
69
78
  });
79
+ cli
80
+ .command('clean', 'Remove cached tsgolint source and compiled binaries to free disk space')
81
+ .action(() => {
82
+ clean();
83
+ });
70
84
  cli.help();
71
85
  cli.version(packageJson.version);
72
86
  cli.parse();
package/dist/codegen.js CHANGED
@@ -102,10 +102,27 @@ go 1.26
102
102
  const mainGo = generateMainGo(rules);
103
103
  fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo);
104
104
  }
105
- /** Generate main.go that imports user rules and calls internal/runner.Run(). */
105
+ /** Sanitize a package name into a valid Go identifier for use as an import alias.
106
+ * Replaces hyphens/dots with underscores, prepends _ if starts with a digit. */
107
+ function toGoAlias(pkg) {
108
+ let alias = pkg.replace(/[^a-zA-Z0-9_]/g, '_');
109
+ if (/^[0-9]/.test(alias)) {
110
+ alias = '_' + alias;
111
+ }
112
+ return alias;
113
+ }
114
+ /** Generate main.go that imports user rules and calls internal/runner.Run().
115
+ * Each rule subfolder is its own Go package, imported by package name. */
106
116
  function generateMainGo(rules) {
117
+ // Deduplicate imports by package name (in case a subfolder has multiple rules)
118
+ const uniquePackages = [...new Set(rules.map((r) => { return r.packageName; }))];
119
+ const imports = uniquePackages.map((pkg) => {
120
+ const alias = toGoAlias(pkg);
121
+ return `\t${alias} "${TSGOLINT_MODULE}/lintcn-rules/${pkg}"`;
122
+ }).join('\n');
107
123
  const ruleEntries = rules.map((r) => {
108
- return `\t\tlintcn.${r.varName},`;
124
+ const alias = toGoAlias(r.packageName);
125
+ return `\t\t${alias}.${r.varName},`;
109
126
  }).join('\n');
110
127
  return `// Code generated by lintcn. DO NOT EDIT.
111
128
  package main
@@ -115,7 +132,7 @@ import (
115
132
 
116
133
  \t"${TSGOLINT_MODULE}/internal/rule"
117
134
  \t"${TSGOLINT_MODULE}/internal/runner"
118
- \tlintcn "${TSGOLINT_MODULE}/lintcn-rules"
135
+ ${imports}
119
136
  )
120
137
 
121
138
  func main() {
@@ -1 +1 @@
1
- {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AA+EA,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2DxD"}
1
+ {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAqQA,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0ExD"}