lintcn 0.5.0 → 0.6.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 +52 -0
- package/README.md +70 -18
- package/dist/cli.js +11 -5
- package/dist/codegen.js +20 -3
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +106 -75
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +44 -0
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +19 -4
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +6 -14
- package/dist/discover.d.ts +3 -3
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +32 -23
- package/dist/hash.d.ts +7 -1
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +28 -25
- package/package.json +1 -1
- package/src/cli.ts +12 -5
- package/src/codegen.ts +22 -3
- package/src/commands/add.ts +129 -71
- package/src/commands/clean.ts +48 -0
- package/src/commands/lint.ts +20 -4
- package/src/commands/remove.ts +6 -16
- package/src/discover.ts +39 -30
- package/src/hash.ts +33 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
+
## 0.6.0
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
.lintcn/
|
|
7
|
+
no_floating_promises/
|
|
8
|
+
no_floating_promises.go
|
|
9
|
+
no_floating_promises_test.go
|
|
10
|
+
options.go ← original name, no renaming
|
|
11
|
+
schema.json
|
|
12
|
+
my_custom_rule/
|
|
13
|
+
my_custom_rule.go
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
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:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# folder URL
|
|
20
|
+
lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises
|
|
21
|
+
|
|
22
|
+
# file URL — auto-fetches the whole folder
|
|
23
|
+
lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
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.
|
|
33
|
+
|
|
34
|
+
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:
|
|
35
|
+
```
|
|
36
|
+
Compiling custom tsgolint binary (first build — may take 30s+ to compile dependencies)...
|
|
37
|
+
Subsequent builds will be fast (~1s). In CI, cache ~/.cache/lintcn/ and GOCACHE (run `go env GOCACHE`).
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
8. **GitHub Actions example** — README now includes a copy-paste workflow that caches the compiled binary. Subsequent CI runs take ~12s (vs ~4min cold):
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
- name: Cache lintcn binary + Go build cache
|
|
44
|
+
uses: actions/cache@v4
|
|
45
|
+
with:
|
|
46
|
+
path: |
|
|
47
|
+
~/.cache/lintcn
|
|
48
|
+
~/go/pkg
|
|
49
|
+
key: lintcn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.lintcn/**/*.go') }}
|
|
50
|
+
restore-keys: lintcn-${{ runner.os }}-${{ runner.arch }}-
|
|
51
|
+
```
|
|
52
|
+
|
|
1
53
|
## 0.5.0
|
|
2
54
|
|
|
3
55
|
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
|
|
17
|
-
npx lintcn add https://github.com/
|
|
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,33 @@ npx lintcn lint --tsconfig tsconfig.build.json
|
|
|
26
29
|
npx lintcn list
|
|
27
30
|
|
|
28
31
|
# Remove a rule
|
|
29
|
-
npx lintcn remove no-
|
|
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
|
-
|
|
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
|
|
40
|
-
│ ├──
|
|
41
|
-
│
|
|
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
|
+
│ │ └── schema.json ← options schema
|
|
53
|
+
│ ├── await_thenable/
|
|
54
|
+
│ │ ├── await_thenable.go
|
|
55
|
+
│ │ └── await_thenable_test.go
|
|
56
|
+
│ └── my_custom_rule/
|
|
57
|
+
│ └── my_custom_rule.go
|
|
42
58
|
├── src/
|
|
43
|
-
│ ├── index.ts
|
|
44
59
|
│ └── ...
|
|
45
60
|
├── tsconfig.json
|
|
46
61
|
└── package.json
|
|
@@ -48,24 +63,32 @@ my-project/
|
|
|
48
63
|
|
|
49
64
|
When you run `npx lintcn lint`, the CLI:
|
|
50
65
|
|
|
51
|
-
1. Scans `.lintcn
|
|
66
|
+
1. Scans `.lintcn/*/` subfolders for rule definitions
|
|
52
67
|
2. Generates a Go workspace with your custom rules
|
|
53
68
|
3. Compiles a custom binary (cached — rebuilds only when rules change)
|
|
54
69
|
4. Runs the binary against your project
|
|
55
70
|
|
|
56
71
|
You can run `lintcn lint` from any subdirectory — it walks up to find `.lintcn/` and lints the cwd project.
|
|
57
72
|
|
|
58
|
-
## Writing
|
|
73
|
+
## Writing custom rules
|
|
59
74
|
|
|
60
|
-
|
|
75
|
+
To help AI agents write and modify rules, install the lintcn skill:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx skills add remorses/lintcn
|
|
79
|
+
```
|
|
61
80
|
|
|
62
|
-
|
|
81
|
+
This gives your AI agent the full tsgolint rule API reference — AST visitors, type checker, reporting, fixes, and testing patterns.
|
|
82
|
+
|
|
83
|
+
Every rule lives in a subfolder under `.lintcn/` with the package name matching the folder:
|
|
63
84
|
|
|
64
85
|
```go
|
|
86
|
+
// .lintcn/no_unhandled_error/no_unhandled_error.go
|
|
87
|
+
|
|
65
88
|
// lintcn:name no-unhandled-error
|
|
66
89
|
// lintcn:description Disallow discarding Error-typed return values
|
|
67
90
|
|
|
68
|
-
package
|
|
91
|
+
package no_unhandled_error
|
|
69
92
|
|
|
70
93
|
import (
|
|
71
94
|
"github.com/microsoft/typescript-go/shim/ast"
|
|
@@ -136,7 +159,7 @@ void getUser("id")
|
|
|
136
159
|
```json
|
|
137
160
|
{
|
|
138
161
|
"devDependencies": {
|
|
139
|
-
"lintcn": "0.
|
|
162
|
+
"lintcn": "0.5.0"
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
165
|
```
|
|
@@ -147,12 +170,41 @@ Each lintcn release bundles a specific tsgolint version. Updating lintcn can cha
|
|
|
147
170
|
2. Run `npx lintcn build` after updating to verify your rules still compile
|
|
148
171
|
3. Fix any compilation errors before committing
|
|
149
172
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
173
|
+
## CI Setup
|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
# .github/workflows/lint.yml
|
|
179
|
+
name: Lint
|
|
180
|
+
on: [push, pull_request]
|
|
181
|
+
|
|
182
|
+
jobs:
|
|
183
|
+
lint:
|
|
184
|
+
runs-on: ubuntu-latest
|
|
185
|
+
steps:
|
|
186
|
+
- uses: actions/checkout@v4
|
|
187
|
+
|
|
188
|
+
- uses: actions/setup-node@v4
|
|
189
|
+
with:
|
|
190
|
+
node-version: 22
|
|
191
|
+
|
|
192
|
+
- name: Cache lintcn binary + Go build cache
|
|
193
|
+
uses: actions/cache@v4
|
|
194
|
+
with:
|
|
195
|
+
path: |
|
|
196
|
+
~/.cache/lintcn
|
|
197
|
+
~/go/pkg
|
|
198
|
+
key: lintcn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.lintcn/**/*.go') }}
|
|
199
|
+
restore-keys: |
|
|
200
|
+
lintcn-${{ runner.os }}-${{ runner.arch }}-
|
|
201
|
+
|
|
202
|
+
- run: npm ci
|
|
203
|
+
- run: npx lintcn lint
|
|
154
204
|
```
|
|
155
205
|
|
|
206
|
+
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.
|
|
207
|
+
|
|
156
208
|
## Prerequisites
|
|
157
209
|
|
|
158
210
|
- **Node.js** — for the CLI
|
package/dist/cli.js
CHANGED
|
@@ -7,16 +7,17 @@ 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
|
|
16
|
-
.example('# Add a rule
|
|
17
|
-
.example('lintcn add https://github.com/
|
|
18
|
-
.example('# Add
|
|
19
|
-
.example('lintcn add https://
|
|
16
|
+
.command('add <url>', 'Add a rule by GitHub URL. Fetches the whole folder into .lintcn/{rule}/')
|
|
17
|
+
.example('# Add a 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')
|
|
20
21
|
.action(async (url) => {
|
|
21
22
|
await addRule(url);
|
|
22
23
|
});
|
|
@@ -67,6 +68,11 @@ cli
|
|
|
67
68
|
const binaryPath = await buildBinary({ rebuild: !!options.rebuild, tsgolintVersion });
|
|
68
69
|
console.log(binaryPath);
|
|
69
70
|
});
|
|
71
|
+
cli
|
|
72
|
+
.command('clean', 'Remove cached tsgolint source and compiled binaries to free disk space')
|
|
73
|
+
.action(() => {
|
|
74
|
+
clean();
|
|
75
|
+
});
|
|
70
76
|
cli.help();
|
|
71
77
|
cli.version(packageJson.version);
|
|
72
78
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAwHA,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4ExD"}
|
package/dist/commands/add.js
CHANGED
|
@@ -1,34 +1,70 @@
|
|
|
1
|
-
// lintcn add <url> — fetch a
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// lintcn add <url> — fetch a rule folder by URL and copy into .lintcn/{rule_name}/
|
|
2
|
+
// Supports GitHub folder URLs (/tree/) and file URLs (/blob/).
|
|
3
|
+
// For file URLs, auto-detects the parent folder and fetches all sibling files.
|
|
4
|
+
// Uses GitHub API to list folder contents.
|
|
4
5
|
import fs from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
6
8
|
import { getLintcnDir } from "../paths.js";
|
|
7
9
|
import { generateEditorGoFiles } from "../codegen.js";
|
|
8
10
|
import { ensureTsgolintSource, DEFAULT_TSGOLINT_VERSION } from "../cache.js";
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
11
|
+
/** Parse GitHub blob/tree/raw URLs into components.
|
|
12
|
+
* Ref is assumed to be the first path component after blob/tree —
|
|
13
|
+
* branch names with slashes (e.g. feature/foo) are not supported. */
|
|
14
|
+
function parseGitHubUrl(url) {
|
|
15
|
+
// GitHub blob URLs: github.com/owner/repo/blob/ref/path/to/file.go
|
|
16
|
+
let match = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
|
|
17
|
+
if (match) {
|
|
18
|
+
const [, owner, repo, ref, filePath] = match;
|
|
19
|
+
return { owner, repo, ref, dirPath: path.posix.dirname(filePath), fileName: path.posix.basename(filePath) };
|
|
20
|
+
}
|
|
21
|
+
// GitHub tree URLs: github.com/owner/repo/tree/ref/path/to/folder
|
|
22
|
+
match = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)$/);
|
|
23
|
+
if (match) {
|
|
24
|
+
const [, owner, repo, ref, dirPath] = match;
|
|
25
|
+
return { owner, repo, ref, dirPath };
|
|
26
|
+
}
|
|
27
|
+
// raw.githubusercontent.com URLs
|
|
28
|
+
match = url.match(/^https?:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
29
|
+
if (match) {
|
|
30
|
+
const [, owner, repo, ref, filePath] = match;
|
|
31
|
+
return { owner, repo, ref, dirPath: path.posix.dirname(filePath), fileName: path.posix.basename(filePath) };
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** Get a GitHub auth token from gh CLI, GITHUB_TOKEN env, or return undefined. */
|
|
36
|
+
function getGitHubToken() {
|
|
37
|
+
if (process.env.GITHUB_TOKEN) {
|
|
38
|
+
return process.env.GITHUB_TOKEN;
|
|
39
|
+
}
|
|
40
|
+
// Try gh CLI token (synchronous to keep it simple)
|
|
41
|
+
try {
|
|
42
|
+
return execSync('gh auth token', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() || undefined;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
29
47
|
}
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
/** List files in a GitHub directory via the Contents API. */
|
|
49
|
+
async function listGitHubFolder(owner, repo, dirPath, ref) {
|
|
50
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${dirPath}?ref=${ref}`;
|
|
51
|
+
const headers = {
|
|
52
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
53
|
+
'User-Agent': 'lintcn',
|
|
54
|
+
};
|
|
55
|
+
const token = getGitHubToken();
|
|
56
|
+
if (token) {
|
|
57
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
58
|
+
}
|
|
59
|
+
const response = await fetch(apiUrl, { headers });
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`GitHub API error: ${response.status} ${response.statusText}\n ${apiUrl}`);
|
|
62
|
+
}
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
if (!Array.isArray(data)) {
|
|
65
|
+
throw new Error(`Expected a directory listing from GitHub API but got a single file.\n ${apiUrl}`);
|
|
66
|
+
}
|
|
67
|
+
return data;
|
|
32
68
|
}
|
|
33
69
|
async function fetchFile(url) {
|
|
34
70
|
const response = await fetch(url);
|
|
@@ -37,24 +73,11 @@ async function fetchFile(url) {
|
|
|
37
73
|
}
|
|
38
74
|
return response.text();
|
|
39
75
|
}
|
|
40
|
-
async function tryFetchFile(url) {
|
|
41
|
-
try {
|
|
42
|
-
return await fetchFile(url);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function rewritePackageName(content) {
|
|
49
|
-
// Rewrite first package declaration to package lintcn.
|
|
50
|
-
// Only matches before the first import or func to avoid touching comments.
|
|
51
|
-
return content.replace(/^package\s+\w+/m, 'package lintcn');
|
|
52
|
-
}
|
|
53
76
|
function ensureSourceComment(content, sourceUrl) {
|
|
54
77
|
if (content.includes('// lintcn:source')) {
|
|
55
78
|
return content;
|
|
56
79
|
}
|
|
57
|
-
// Insert source comment after
|
|
80
|
+
// Insert source comment after any existing lintcn: comment block, or at the very top
|
|
58
81
|
const lines = content.split('\n');
|
|
59
82
|
let insertIndex = 0;
|
|
60
83
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -69,51 +92,59 @@ function ensureSourceComment(content, sourceUrl) {
|
|
|
69
92
|
return lines.join('\n');
|
|
70
93
|
}
|
|
71
94
|
export async function addRule(url) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
const parsed = parseGitHubUrl(url);
|
|
96
|
+
if (!parsed) {
|
|
97
|
+
throw new Error('Only GitHub URLs are supported. Pass a /blob/ (file) or /tree/ (folder) URL.\n' +
|
|
98
|
+
'Example: lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises');
|
|
99
|
+
}
|
|
100
|
+
const { owner, repo, ref, dirPath } = parsed;
|
|
101
|
+
const folderName = path.posix.basename(dirPath);
|
|
102
|
+
console.log(`Fetching ${owner}/${repo}/${dirPath}...`);
|
|
103
|
+
const items = await listGitHubFolder(owner, repo, dirPath, ref);
|
|
104
|
+
// Filter for .go and .json files
|
|
105
|
+
const filesToFetch = items.filter((item) => {
|
|
106
|
+
return item.type === 'file' && item.download_url && (item.name.endsWith('.go') || item.name.endsWith('.json'));
|
|
107
|
+
});
|
|
108
|
+
if (filesToFetch.length === 0) {
|
|
109
|
+
throw new Error(`No .go files found in ${dirPath}. Is this a rule folder?`);
|
|
110
|
+
}
|
|
111
|
+
// Warn if this doesn't look like a single-rule folder (too many main .go files)
|
|
112
|
+
const mainGoFiles = filesToFetch.filter((f) => {
|
|
113
|
+
return f.name.endsWith('.go') && !f.name.endsWith('_test.go') && f.name !== 'options.go';
|
|
114
|
+
});
|
|
115
|
+
if (mainGoFiles.length > 3) {
|
|
116
|
+
console.warn(`Warning: folder has ${mainGoFiles.length} non-test .go files. ` +
|
|
117
|
+
`This may be a directory of multiple rules — consider using a more specific URL.`);
|
|
84
118
|
}
|
|
85
119
|
const lintcnDir = getLintcnDir();
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.log(`Overwriting existing ${
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// ensure .tsgolint source is available and generate editor support files
|
|
120
|
+
const ruleDir = path.join(lintcnDir, folderName);
|
|
121
|
+
// Clean existing rule folder if it exists
|
|
122
|
+
if (fs.existsSync(ruleDir)) {
|
|
123
|
+
fs.rmSync(ruleDir, { recursive: true });
|
|
124
|
+
console.log(`Overwriting existing ${folderName}/`);
|
|
125
|
+
}
|
|
126
|
+
fs.mkdirSync(ruleDir, { recursive: true });
|
|
127
|
+
// Fetch and write all files
|
|
128
|
+
for (const item of filesToFetch) {
|
|
129
|
+
let content = await fetchFile(item.download_url);
|
|
130
|
+
// Add lintcn:source comment to the main rule file (same name as folder)
|
|
131
|
+
if (item.name === `${folderName}.go`) {
|
|
132
|
+
content = ensureSourceComment(content, url);
|
|
133
|
+
}
|
|
134
|
+
fs.writeFileSync(path.join(ruleDir, item.name), content);
|
|
135
|
+
console.log(` ${item.name}`);
|
|
136
|
+
}
|
|
137
|
+
console.log(`Added ${folderName}/ (${filesToFetch.length} files)`);
|
|
138
|
+
// Ensure tsgolint source is available
|
|
106
139
|
const tsgolintDir = await ensureTsgolintSource(DEFAULT_TSGOLINT_VERSION);
|
|
107
|
-
//
|
|
108
|
-
// Use lstatSync to detect broken symlinks (existsSync returns false for broken links)
|
|
140
|
+
// Create/refresh .tsgolint symlink for gopls
|
|
109
141
|
const tsgolintLink = path.join(lintcnDir, '.tsgolint');
|
|
110
142
|
try {
|
|
111
143
|
fs.lstatSync(tsgolintLink);
|
|
112
|
-
// exists (possibly broken) — remove and recreate
|
|
113
144
|
fs.rmSync(tsgolintLink, { force: true });
|
|
114
145
|
}
|
|
115
146
|
catch {
|
|
116
|
-
// doesn't exist
|
|
147
|
+
// doesn't exist
|
|
117
148
|
}
|
|
118
149
|
fs.symlinkSync(tsgolintDir, tsgolintLink);
|
|
119
150
|
generateEditorGoFiles(lintcnDir);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clean.d.ts","sourceRoot":"","sources":["../../src/commands/clean.ts"],"names":[],"mappings":"AAMA,wBAAgB,KAAK,IAAI,IAAI,CAW5B"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// lintcn clean — remove cached tsgolint source and compiled binaries.
|
|
2
|
+
// Frees disk space from old versions that accumulate over time.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { getCacheDir } from "../cache.js";
|
|
5
|
+
export function clean() {
|
|
6
|
+
const cacheDir = getCacheDir();
|
|
7
|
+
if (!fs.existsSync(cacheDir)) {
|
|
8
|
+
console.log('No cache to clean');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const stats = getCacheStats(cacheDir);
|
|
12
|
+
fs.rmSync(cacheDir, { recursive: true });
|
|
13
|
+
console.log(`Removed ${cacheDir} (${formatBytes(stats.totalBytes)})`);
|
|
14
|
+
}
|
|
15
|
+
function getCacheStats(dir) {
|
|
16
|
+
let totalBytes = 0;
|
|
17
|
+
const walk = (d) => {
|
|
18
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
19
|
+
const fullPath = `${d}/${entry.name}`;
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
walk(fullPath);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
totalBytes += fs.statSync(fullPath).size;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
walk(dir);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// ignore errors during stat
|
|
33
|
+
}
|
|
34
|
+
return { totalBytes };
|
|
35
|
+
}
|
|
36
|
+
function formatBytes(bytes) {
|
|
37
|
+
if (bytes < 1024) {
|
|
38
|
+
return `${bytes}B`;
|
|
39
|
+
}
|
|
40
|
+
if (bytes < 1024 * 1024) {
|
|
41
|
+
return `${(bytes / 1024).toFixed(0)}KB`;
|
|
42
|
+
}
|
|
43
|
+
return `${(bytes / (1024 * 1024)).toFixed(0)}MB`;
|
|
44
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/commands/lint.ts"],"names":[],"mappings":"AAuBA,wBAAsB,WAAW,CAAC,EAChC,OAAO,EACP,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/commands/lint.ts"],"names":[],"mappings":"AAuBA,wBAAsB,WAAW,CAAC,EAChC,OAAO,EACP,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkElB;AAED,wBAAsB,IAAI,CAAC,EACzB,OAAO,EACP,eAAe,EACf,eAAe,GAChB,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,EAAE,CAAA;CAC1B,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBlB"}
|
package/dist/commands/lint.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { requireLintcnDir } from "../paths.js";
|
|
6
6
|
import { discoverRules } from "../discover.js";
|
|
7
|
-
import { generateBuildWorkspace } from "../codegen.js";
|
|
7
|
+
import { generateBuildWorkspace, generateEditorGoFiles } from "../codegen.js";
|
|
8
8
|
import { ensureTsgolintSource, validateVersion, cachedBinaryExists, getBinaryPath, getBuildDir, getBinDir } from "../cache.js";
|
|
9
9
|
import { computeContentHash } from "../hash.js";
|
|
10
10
|
import { execAsync } from "../exec.js";
|
|
@@ -29,7 +29,7 @@ export async function buildBinary({ rebuild, tsgolintVersion, }) {
|
|
|
29
29
|
// ensure tsgolint source
|
|
30
30
|
const tsgolintDir = await ensureTsgolintSource(tsgolintVersion);
|
|
31
31
|
// compute content hash
|
|
32
|
-
const contentHash = await computeContentHash({
|
|
32
|
+
const { short: contentHash } = await computeContentHash({
|
|
33
33
|
lintcnDir,
|
|
34
34
|
tsgolintVersion,
|
|
35
35
|
});
|
|
@@ -38,6 +38,8 @@ export async function buildBinary({ rebuild, tsgolintVersion, }) {
|
|
|
38
38
|
console.log('Using cached binary');
|
|
39
39
|
return getBinaryPath(contentHash);
|
|
40
40
|
}
|
|
41
|
+
// ensure .lintcn/go.mod exists (gitignored, needed by the build workspace symlink)
|
|
42
|
+
generateEditorGoFiles(lintcnDir);
|
|
41
43
|
// generate build workspace (per-hash dir to avoid races between concurrent processes)
|
|
42
44
|
const buildDir = getBuildDir(contentHash);
|
|
43
45
|
console.log('Generating build workspace...');
|
|
@@ -51,10 +53,23 @@ export async function buildBinary({ rebuild, tsgolintVersion, }) {
|
|
|
51
53
|
const binDir = getBinDir();
|
|
52
54
|
fs.mkdirSync(binDir, { recursive: true });
|
|
53
55
|
const binaryPath = getBinaryPath(contentHash);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
// Check if any lintcn binary has been built before — if not, this is a cold
|
|
57
|
+
// build that compiles the full tsgolint + typescript-go dependency tree.
|
|
58
|
+
const existingBins = fs.existsSync(binDir) ? fs.readdirSync(binDir) : [];
|
|
59
|
+
if (existingBins.length === 0) {
|
|
60
|
+
console.log('Compiling custom tsgolint binary (first build — may take 30s+ to compile dependencies)...');
|
|
61
|
+
console.log('Subsequent builds will be fast (~1s). In CI, cache ~/.cache/lintcn/ and GOCACHE (run `go env GOCACHE`).');
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log('Compiling custom tsgolint binary...');
|
|
65
|
+
}
|
|
66
|
+
const { exitCode: buildExitCode } = await execAsync('go', ['build', '-trimpath', '-o', binaryPath, './wrapper'], {
|
|
56
67
|
cwd: buildDir,
|
|
68
|
+
stdio: 'inherit',
|
|
57
69
|
});
|
|
70
|
+
if (buildExitCode !== 0) {
|
|
71
|
+
throw new Error(`Go compilation failed (exit code ${buildExitCode})`);
|
|
72
|
+
}
|
|
58
73
|
console.log('Build complete');
|
|
59
74
|
return binaryPath;
|
|
60
75
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remove.d.ts","sourceRoot":"","sources":["../../src/commands/remove.ts"],"names":[],"mappings":"AAOA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"remove.d.ts","sourceRoot":"","sources":["../../src/commands/remove.ts"],"names":[],"mappings":"AAOA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAmB7C"}
|