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 +108 -0
- package/README.md +123 -23
- package/dist/cache.d.ts +1 -1
- package/dist/cache.js +1 -1
- package/dist/cli.js +19 -5
- package/dist/codegen.js +20 -3
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +223 -77
- 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 +2 -1
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +93 -7
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +5 -2
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +6 -14
- package/dist/discover.d.ts +13 -3
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +49 -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 +3 -2
- package/src/cache.ts +1 -1
- package/src/cli.ts +20 -5
- package/src/codegen.ts +22 -3
- package/src/commands/add.ts +270 -73
- package/src/commands/clean.ts +48 -0
- package/src/commands/lint.ts +100 -7
- package/src/commands/list.ts +5 -2
- package/src/commands/remove.ts +6 -16
- package/src/discover.ts +66 -31
- package/src/hash.ts +33 -27
package/dist/discover.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAA;IACZ;;qEAEiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAA;IACd;kFAC8E;IAC9E,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAA;IAC1B,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAA;CACpB;AAaD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMrE;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGhE;AAED;4EAC4E;AAC5E,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGpF;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,EAAE,CAsD/D"}
|
package/dist/discover.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Scan .lintcn
|
|
1
|
+
// Scan .lintcn/*/ subfolders for rule.Rule variables and lintcn: metadata comments.
|
|
2
|
+
// Each subfolder is a Go package containing one rule with its companions.
|
|
2
3
|
// Returns structured info about each discovered rule for codegen and list display.
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
@@ -6,6 +7,12 @@ import path from 'node:path';
|
|
|
6
7
|
// and optional import alias (e.g. `r.Rule{` if imported as `r "...rule"`)
|
|
7
8
|
const RULE_VAR_RE = /^\s*var\s+(\w+)\s*=\s*\w*\.?Rule\s*\{/m;
|
|
8
9
|
const METADATA_RE = /^\/\/\s*lintcn:(\w+)\s+(.+)$/gm;
|
|
10
|
+
// buildGoRuleNameRe creates a regex scoped to a specific rule variable's
|
|
11
|
+
// struct literal, e.g. `var FooRule = rule.Rule{ ... Name: "foo" ... }`.
|
|
12
|
+
// This avoids matching a Name field in an unrelated struct earlier in the file.
|
|
13
|
+
function buildGoRuleNameRe(varName) {
|
|
14
|
+
return new RegExp(`var\\s+${varName}\\s*=\\s*\\w*\\.?Rule\\s*\\{[\\s\\S]*?Name:\\s*"([^"]+)"`);
|
|
15
|
+
}
|
|
9
16
|
export function parseMetadata(content) {
|
|
10
17
|
const meta = {};
|
|
11
18
|
for (const match of content.matchAll(METADATA_RE)) {
|
|
@@ -17,35 +24,54 @@ export function parseRuleVar(content) {
|
|
|
17
24
|
const match = content.match(RULE_VAR_RE);
|
|
18
25
|
return match?.[1];
|
|
19
26
|
}
|
|
27
|
+
/** Extract the Name field from a specific rule.Rule variable's struct literal.
|
|
28
|
+
* Scoped to varName to avoid matching Name fields in unrelated structs. */
|
|
29
|
+
export function parseGoRuleName(content, varName) {
|
|
30
|
+
const match = content.match(buildGoRuleNameRe(varName));
|
|
31
|
+
return match?.[1];
|
|
32
|
+
}
|
|
20
33
|
export function discoverRules(lintcnDir) {
|
|
21
34
|
if (!fs.existsSync(lintcnDir)) {
|
|
22
35
|
return [];
|
|
23
36
|
}
|
|
24
|
-
const files = fs.readdirSync(lintcnDir).filter((f) => {
|
|
25
|
-
return f.endsWith('.go') && !f.endsWith('_test.go');
|
|
26
|
-
});
|
|
27
37
|
const rules = [];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
const entries = fs.readdirSync(lintcnDir, { withFileTypes: true });
|
|
39
|
+
// Warn about flat .go files that should be in subfolders
|
|
40
|
+
const flatGoFiles = entries.filter((e) => {
|
|
41
|
+
return e.isFile() && e.name.endsWith('.go');
|
|
42
|
+
});
|
|
43
|
+
for (const file of flatGoFiles) {
|
|
44
|
+
const baseName = file.name.replace(/(_test)?\.go$/, '');
|
|
45
|
+
console.error(`Error: ${file.name} is a flat file in .lintcn/ — rules must be in subfolders.\n` +
|
|
46
|
+
` Move it to .lintcn/${baseName}/${file.name} and change the package to "${baseName}".\n`);
|
|
47
|
+
}
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
38
50
|
continue;
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
rules.push({
|
|
43
|
-
name: meta.name || baseName.replace(/_/g, '-'),
|
|
44
|
-
description: meta.description || '',
|
|
45
|
-
source: meta.source || '',
|
|
46
|
-
varName,
|
|
47
|
-
fileName,
|
|
51
|
+
const subDir = path.join(lintcnDir, entry.name);
|
|
52
|
+
const goFiles = fs.readdirSync(subDir).filter((f) => {
|
|
53
|
+
return f.endsWith('.go') && !f.endsWith('_test.go');
|
|
48
54
|
});
|
|
55
|
+
for (const fileName of goFiles) {
|
|
56
|
+
const filePath = path.join(subDir, fileName);
|
|
57
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
58
|
+
const varName = parseRuleVar(content);
|
|
59
|
+
if (!varName)
|
|
60
|
+
continue;
|
|
61
|
+
const meta = parseMetadata(content);
|
|
62
|
+
const severity = meta.severity === 'warn' ? 'warn' : 'error';
|
|
63
|
+
const displayName = meta.name || entry.name.replace(/_/g, '-');
|
|
64
|
+
const goRuleName = parseGoRuleName(content, varName) || displayName;
|
|
65
|
+
rules.push({
|
|
66
|
+
name: displayName,
|
|
67
|
+
goRuleName,
|
|
68
|
+
description: meta.description,
|
|
69
|
+
source: meta.source,
|
|
70
|
+
severity,
|
|
71
|
+
varName,
|
|
72
|
+
packageName: entry.name,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
49
75
|
}
|
|
50
76
|
return rules;
|
|
51
77
|
}
|
package/dist/hash.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
/** Compute a deterministic content hash for binary caching.
|
|
2
|
+
* Returns: { short: "a1b2c3d4..." (16 hex), full: "a1b2c3d4..." (64 hex) }
|
|
3
|
+
* The short hash is used for local cache paths, the full hash for remote cache keys. */
|
|
1
4
|
export declare function computeContentHash({ lintcnDir, tsgolintVersion, }: {
|
|
2
5
|
lintcnDir: string;
|
|
3
6
|
tsgolintVersion: string;
|
|
4
|
-
}): Promise<
|
|
7
|
+
}): Promise<{
|
|
8
|
+
short: string;
|
|
9
|
+
full: string;
|
|
10
|
+
}>;
|
|
5
11
|
//# sourceMappingURL=hash.d.ts.map
|
package/dist/hash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAiBA;;yFAEyF;AACzF,wBAAsB,kBAAkB,CAAC,EACvC,SAAS,EACT,eAAe,GAChB,EAAE;IACD,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA2B3C"}
|
package/dist/hash.js
CHANGED
|
@@ -1,37 +1,40 @@
|
|
|
1
|
-
// Content hash for binary caching.
|
|
2
|
-
// Combines cache schema version, tsgolint version,
|
|
3
|
-
//
|
|
1
|
+
// Content hash for binary caching (local + remote).
|
|
2
|
+
// Combines cache schema version, tsgolint version, platform triplet,
|
|
3
|
+
// and non-test rule file contents into a SHA-256 hash.
|
|
4
|
+
//
|
|
5
|
+
// The hash is deterministic across machines — same rules + same tsgolint
|
|
6
|
+
// version + same platform = same hash. Go version is NOT included because
|
|
7
|
+
// the compiled binary is standalone (no Go runtime dependency).
|
|
8
|
+
//
|
|
4
9
|
// Bump CACHE_SCHEMA_VERSION when codegen logic changes to invalidate
|
|
5
10
|
// stale binaries built by older lintcn versions.
|
|
6
11
|
import crypto from 'node:crypto';
|
|
7
12
|
import fs from 'node:fs';
|
|
8
13
|
import path from 'node:path';
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
const CACHE_SCHEMA_VERSION = '4';
|
|
15
|
+
/** Compute a deterministic content hash for binary caching.
|
|
16
|
+
* Returns: { short: "a1b2c3d4..." (16 hex), full: "a1b2c3d4..." (64 hex) }
|
|
17
|
+
* The short hash is used for local cache paths, the full hash for remote cache keys. */
|
|
11
18
|
export async function computeContentHash({ lintcnDir, tsgolintVersion, }) {
|
|
12
19
|
const hash = crypto.createHash('sha256');
|
|
13
20
|
hash.update(`cache-schema:${CACHE_SCHEMA_VERSION}\n`);
|
|
14
21
|
hash.update(`tsgolint:${tsgolintVersion}\n`);
|
|
15
22
|
hash.update(`platform:${process.platform}-${process.arch}\n`);
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
// walk rule subfolders for non-test .go files in sorted order
|
|
24
|
+
const entries = fs.readdirSync(lintcnDir, { withFileTypes: true })
|
|
25
|
+
.filter((e) => { return e.isDirectory() && !e.name.startsWith('.'); })
|
|
26
|
+
.sort((a, b) => { return a.name.localeCompare(b.name); });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const subDir = path.join(lintcnDir, entry.name);
|
|
29
|
+
const goFiles = fs.readdirSync(subDir)
|
|
30
|
+
.filter((f) => { return f.endsWith('.go') && !f.endsWith('_test.go'); })
|
|
31
|
+
.sort();
|
|
32
|
+
for (const file of goFiles) {
|
|
33
|
+
const content = fs.readFileSync(path.join(subDir, file), 'utf-8');
|
|
34
|
+
hash.update(`file:${entry.name}/${file}\n`);
|
|
35
|
+
hash.update(content);
|
|
36
|
+
}
|
|
20
37
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
// add all rule file contents in sorted order
|
|
25
|
-
const files = fs
|
|
26
|
-
.readdirSync(lintcnDir)
|
|
27
|
-
.filter((f) => {
|
|
28
|
-
return f.endsWith('.go');
|
|
29
|
-
})
|
|
30
|
-
.sort();
|
|
31
|
-
for (const file of files) {
|
|
32
|
-
const content = fs.readFileSync(path.join(lintcnDir, file), 'utf-8');
|
|
33
|
-
hash.update(`file:${file}\n`);
|
|
34
|
-
hash.update(content);
|
|
35
|
-
}
|
|
36
|
-
return hash.digest('hex').slice(0, 16);
|
|
38
|
+
const full = hash.digest('hex');
|
|
39
|
+
return { short: full.slice(0, 16), full };
|
|
37
40
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lintcn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The shadcn for type-aware TypeScript lint rules. Browse, pick, and copy rules into your project.",
|
|
6
6
|
"bin": "dist/cli.js",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"tar": "^7.5.12"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
|
-
"build": "rm -rf dist *.tsbuildinfo && tsc && chmod +x dist/cli.js"
|
|
63
|
+
"build": "rm -rf dist *.tsbuildinfo && tsc && chmod +x dist/cli.js",
|
|
64
|
+
"cli": "tsx src/cli"
|
|
64
65
|
}
|
|
65
66
|
}
|
package/src/cache.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { execAsync } from './exec.ts'
|
|
|
18
18
|
// Pinned tsgolint fork commit — updated with each lintcn release.
|
|
19
19
|
// Uses remorses/tsgolint fork which adds internal/runner.Run().
|
|
20
20
|
// Only 1 commit on top of upstream — zero modifications to existing files.
|
|
21
|
-
export const DEFAULT_TSGOLINT_VERSION = '
|
|
21
|
+
export const DEFAULT_TSGOLINT_VERSION = '23190a08a6315eba8ef11818fc1c38d7b01c9e10'
|
|
22
22
|
|
|
23
23
|
// Pinned typescript-go base commit from microsoft/typescript-go (before patches).
|
|
24
24
|
// Patches from tsgolint/patches/ are applied on top during setup.
|
package/src/cli.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { addRule } from './commands/add.ts'
|
|
|
9
9
|
import { lint, buildBinary } from './commands/lint.ts'
|
|
10
10
|
import { listRules } from './commands/list.ts'
|
|
11
11
|
import { removeRule } from './commands/remove.ts'
|
|
12
|
+
import { clean } from './commands/clean.ts'
|
|
12
13
|
import { DEFAULT_TSGOLINT_VERSION } from './cache.ts'
|
|
13
14
|
|
|
14
15
|
const require = createRequire(import.meta.url)
|
|
@@ -17,11 +18,13 @@ const packageJson = require('../package.json') as { version: string }
|
|
|
17
18
|
const cli = goke('lintcn')
|
|
18
19
|
|
|
19
20
|
cli
|
|
20
|
-
.command('add <url>', 'Add
|
|
21
|
-
.example('# Add a rule
|
|
22
|
-
.example('lintcn add https://github.com/
|
|
23
|
-
.example('# Add
|
|
24
|
-
.example('lintcn add https://
|
|
21
|
+
.command('add <url>', 'Add rules by GitHub URL. Supports single rule folders, .lintcn/ directories, or full repo URLs.')
|
|
22
|
+
.example('# Add a single rule folder')
|
|
23
|
+
.example('lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises')
|
|
24
|
+
.example('# Add by file URL (auto-fetches the whole folder)')
|
|
25
|
+
.example('lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go')
|
|
26
|
+
.example('# Add all rules from a repo (downloads .lintcn/ folder)')
|
|
27
|
+
.example('lintcn add https://github.com/someone/their-project')
|
|
25
28
|
.action(async (url) => {
|
|
26
29
|
await addRule(url)
|
|
27
30
|
})
|
|
@@ -42,12 +45,17 @@ cli
|
|
|
42
45
|
cli
|
|
43
46
|
.command('lint', 'Build custom tsgolint binary and run it against the project')
|
|
44
47
|
.option('--rebuild', 'Force rebuild even if cached binary exists')
|
|
48
|
+
.option('--fix', 'Automatically fix violations')
|
|
45
49
|
.option('--tsconfig <path>', 'Path to tsconfig.json')
|
|
46
50
|
.option('--list-files', 'List matched files')
|
|
51
|
+
.option('--all-warnings', 'Show warnings for all files, not just git-changed ones')
|
|
47
52
|
.option('--tsgolint-version [version]', 'Override the pinned tsgolint version (tag or commit). For testing unreleased tsgolint versions.')
|
|
48
53
|
.action(async (options) => {
|
|
49
54
|
const tsgolintVersion = (options.tsgolintVersion as string) || DEFAULT_TSGOLINT_VERSION
|
|
50
55
|
const passthroughArgs: string[] = []
|
|
56
|
+
if (options.fix) {
|
|
57
|
+
passthroughArgs.push('--fix')
|
|
58
|
+
}
|
|
51
59
|
if (options.tsconfig) {
|
|
52
60
|
passthroughArgs.push('--tsconfig', options.tsconfig as string)
|
|
53
61
|
}
|
|
@@ -63,6 +71,7 @@ cli
|
|
|
63
71
|
rebuild: !!options.rebuild,
|
|
64
72
|
tsgolintVersion,
|
|
65
73
|
passthroughArgs,
|
|
74
|
+
allWarnings: !!options.allWarnings,
|
|
66
75
|
})
|
|
67
76
|
process.exit(exitCode)
|
|
68
77
|
})
|
|
@@ -77,6 +86,12 @@ cli
|
|
|
77
86
|
console.log(binaryPath)
|
|
78
87
|
})
|
|
79
88
|
|
|
89
|
+
cli
|
|
90
|
+
.command('clean', 'Remove cached tsgolint source and compiled binaries to free disk space')
|
|
91
|
+
.action(() => {
|
|
92
|
+
clean()
|
|
93
|
+
})
|
|
94
|
+
|
|
80
95
|
cli.help()
|
|
81
96
|
cli.version(packageJson.version)
|
|
82
97
|
cli.parse()
|
package/src/codegen.ts
CHANGED
|
@@ -129,10 +129,29 @@ go 1.26
|
|
|
129
129
|
fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo)
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
/**
|
|
132
|
+
/** Sanitize a package name into a valid Go identifier for use as an import alias.
|
|
133
|
+
* Replaces hyphens/dots with underscores, prepends _ if starts with a digit. */
|
|
134
|
+
function toGoAlias(pkg: string): string {
|
|
135
|
+
let alias = pkg.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
136
|
+
if (/^[0-9]/.test(alias)) {
|
|
137
|
+
alias = '_' + alias
|
|
138
|
+
}
|
|
139
|
+
return alias
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Generate main.go that imports user rules and calls internal/runner.Run().
|
|
143
|
+
* Each rule subfolder is its own Go package, imported by package name. */
|
|
133
144
|
function generateMainGo(rules: RuleMetadata[]): string {
|
|
145
|
+
// Deduplicate imports by package name (in case a subfolder has multiple rules)
|
|
146
|
+
const uniquePackages = [...new Set(rules.map((r) => { return r.packageName }))]
|
|
147
|
+
const imports = uniquePackages.map((pkg) => {
|
|
148
|
+
const alias = toGoAlias(pkg)
|
|
149
|
+
return `\t${alias} "${TSGOLINT_MODULE}/lintcn-rules/${pkg}"`
|
|
150
|
+
}).join('\n')
|
|
151
|
+
|
|
134
152
|
const ruleEntries = rules.map((r) => {
|
|
135
|
-
|
|
153
|
+
const alias = toGoAlias(r.packageName)
|
|
154
|
+
return `\t\t${alias}.${r.varName},`
|
|
136
155
|
}).join('\n')
|
|
137
156
|
|
|
138
157
|
return `// Code generated by lintcn. DO NOT EDIT.
|
|
@@ -143,7 +162,7 @@ import (
|
|
|
143
162
|
|
|
144
163
|
\t"${TSGOLINT_MODULE}/internal/rule"
|
|
145
164
|
\t"${TSGOLINT_MODULE}/internal/runner"
|
|
146
|
-
|
|
165
|
+
${imports}
|
|
147
166
|
)
|
|
148
167
|
|
|
149
168
|
func main() {
|