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.
@@ -1 +1 @@
1
- {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAA;CACjB;AAOD,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,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,EAAE,CAwC/D"}
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/*.go files for rule.Rule variables and lintcn: metadata comments.
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
- for (const fileName of files) {
29
- const filePath = path.join(lintcnDir, fileName);
30
- const content = fs.readFileSync(filePath, 'utf-8');
31
- const varName = parseRuleVar(content);
32
- if (!varName) {
33
- // warn if file contains rule.Rule but we couldn't parse the var name
34
- if (content.includes('rule.Rule')) {
35
- console.warn(`Warning: ${fileName} contains rule.Rule but no exported var was found. ` +
36
- `Expected pattern: var XxxRule = rule.Rule{`);
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 meta = parseMetadata(content);
41
- const baseName = fileName.replace(/\.go$/, '');
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<string>;
7
+ }): Promise<{
8
+ short: string;
9
+ full: string;
10
+ }>;
5
11
  //# sourceMappingURL=hash.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAaA,wBAAsB,kBAAkB,CAAC,EACvC,SAAS,EACT,eAAe,GAChB,EAAE;IACD,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BlB"}
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, rule file contents,
3
- // Go version, and platform into a single SHA-256 hash.
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
- import { execAsync } from "./exec.js";
10
- const CACHE_SCHEMA_VERSION = '2';
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
- // add Go version
17
- try {
18
- const { stdout } = await execAsync('go', ['version']);
19
- hash.update(`go:${stdout.trim()}\n`);
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
- catch {
22
- hash.update('go:unknown\n');
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.5.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 = 'e945641eabec22993eda3e7c101692e80417e0ea'
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 a rule by URL. Fetches the .go file and copies it into .lintcn/')
21
- .example('# Add a rule from GitHub')
22
- .example('lintcn add https://github.com/user/repo/blob/main/rules/no_floating_promises.go')
23
- .example('# Add from raw URL')
24
- .example('lintcn add https://raw.githubusercontent.com/user/repo/main/rules/no_unused_result.go')
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
- /** Generate main.go that imports user rules and calls internal/runner.Run(). */
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
- return `\t\tlintcn.${r.varName},`
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
- \tlintcn "${TSGOLINT_MODULE}/lintcn-rules"
165
+ ${imports}
147
166
  )
148
167
 
149
168
  func main() {