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/src/discover.ts
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
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
|
|
|
4
5
|
import fs from 'node:fs'
|
|
5
6
|
import path from 'node:path'
|
|
6
7
|
|
|
7
8
|
export interface RuleMetadata {
|
|
8
|
-
/** kebab-case rule name from // lintcn:name or derived from
|
|
9
|
+
/** kebab-case rule name from // lintcn:name or derived from folder name */
|
|
9
10
|
name: string
|
|
11
|
+
/** runtime rule name parsed from Go `rule.Rule{Name: "..."}`. This is
|
|
12
|
+
* the name tsgolint uses in diagnostics and must match --warn flags.
|
|
13
|
+
* Falls back to `name` if the Go Name field can't be parsed. */
|
|
14
|
+
goRuleName: string
|
|
10
15
|
/** one-line description from // lintcn:description */
|
|
11
16
|
description: string
|
|
12
17
|
/** original source URL from // lintcn:source */
|
|
13
18
|
source: string
|
|
19
|
+
/** severity from // lintcn:severity — 'error' (default) or 'warn'.
|
|
20
|
+
* Warnings are displayed with yellow styling and don't cause exit code 1. */
|
|
21
|
+
severity: 'error' | 'warn'
|
|
14
22
|
/** exported Go variable name like NoFloatingPromisesRule */
|
|
15
23
|
varName: string
|
|
16
|
-
/**
|
|
17
|
-
|
|
24
|
+
/** Go package name (= subfolder name, e.g. no_floating_promises) */
|
|
25
|
+
packageName: string
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
// Matches `var XxxRule = rule.Rule{` with optional leading whitespace
|
|
21
29
|
// and optional import alias (e.g. `r.Rule{` if imported as `r "...rule"`)
|
|
22
30
|
const RULE_VAR_RE = /^\s*var\s+(\w+)\s*=\s*\w*\.?Rule\s*\{/m
|
|
23
31
|
const METADATA_RE = /^\/\/\s*lintcn:(\w+)\s+(.+)$/gm
|
|
32
|
+
// buildGoRuleNameRe creates a regex scoped to a specific rule variable's
|
|
33
|
+
// struct literal, e.g. `var FooRule = rule.Rule{ ... Name: "foo" ... }`.
|
|
34
|
+
// This avoids matching a Name field in an unrelated struct earlier in the file.
|
|
35
|
+
function buildGoRuleNameRe(varName: string): RegExp {
|
|
36
|
+
return new RegExp(`var\\s+${varName}\\s*=\\s*\\w*\\.?Rule\\s*\\{[\\s\\S]*?Name:\\s*"([^"]+)"`)
|
|
37
|
+
}
|
|
24
38
|
|
|
25
39
|
export function parseMetadata(content: string): Record<string, string> {
|
|
26
40
|
const meta: Record<string, string> = {}
|
|
@@ -35,43 +49,64 @@ export function parseRuleVar(content: string): string | undefined {
|
|
|
35
49
|
return match?.[1]
|
|
36
50
|
}
|
|
37
51
|
|
|
52
|
+
/** Extract the Name field from a specific rule.Rule variable's struct literal.
|
|
53
|
+
* Scoped to varName to avoid matching Name fields in unrelated structs. */
|
|
54
|
+
export function parseGoRuleName(content: string, varName: string): string | undefined {
|
|
55
|
+
const match = content.match(buildGoRuleNameRe(varName))
|
|
56
|
+
return match?.[1]
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
export function discoverRules(lintcnDir: string): RuleMetadata[] {
|
|
39
60
|
if (!fs.existsSync(lintcnDir)) {
|
|
40
61
|
return []
|
|
41
62
|
}
|
|
42
63
|
|
|
43
|
-
const files = fs.readdirSync(lintcnDir).filter((f) => {
|
|
44
|
-
return f.endsWith('.go') && !f.endsWith('_test.go')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
64
|
const rules: RuleMetadata[] = []
|
|
65
|
+
const entries = fs.readdirSync(lintcnDir, { withFileTypes: true })
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
67
|
+
// Warn about flat .go files that should be in subfolders
|
|
68
|
+
const flatGoFiles = entries.filter((e) => {
|
|
69
|
+
return e.isFile() && e.name.endsWith('.go')
|
|
70
|
+
})
|
|
71
|
+
for (const file of flatGoFiles) {
|
|
72
|
+
const baseName = file.name.replace(/(_test)?\.go$/, '')
|
|
73
|
+
console.error(
|
|
74
|
+
`Error: ${file.name} is a flat file in .lintcn/ — rules must be in subfolders.\n` +
|
|
75
|
+
` Move it to .lintcn/${baseName}/${file.name} and change the package to "${baseName}".\n`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
64
78
|
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
|
67
81
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
source: meta.source || '',
|
|
72
|
-
varName,
|
|
73
|
-
fileName,
|
|
82
|
+
const subDir = path.join(lintcnDir, entry.name)
|
|
83
|
+
const goFiles = fs.readdirSync(subDir).filter((f) => {
|
|
84
|
+
return f.endsWith('.go') && !f.endsWith('_test.go')
|
|
74
85
|
})
|
|
86
|
+
|
|
87
|
+
for (const fileName of goFiles) {
|
|
88
|
+
const filePath = path.join(subDir, fileName)
|
|
89
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
90
|
+
|
|
91
|
+
const varName = parseRuleVar(content)
|
|
92
|
+
if (!varName) continue
|
|
93
|
+
|
|
94
|
+
const meta = parseMetadata(content)
|
|
95
|
+
|
|
96
|
+
const severity = meta.severity === 'warn' ? 'warn' as const : 'error' as const
|
|
97
|
+
const displayName = meta.name || entry.name.replace(/_/g, '-')
|
|
98
|
+
const goRuleName = parseGoRuleName(content, varName) || displayName
|
|
99
|
+
|
|
100
|
+
rules.push({
|
|
101
|
+
name: displayName,
|
|
102
|
+
goRuleName,
|
|
103
|
+
description: meta.description,
|
|
104
|
+
source: meta.source,
|
|
105
|
+
severity,
|
|
106
|
+
varName,
|
|
107
|
+
packageName: entry.name,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
75
110
|
}
|
|
76
111
|
|
|
77
112
|
return rules
|
package/src/hash.ts
CHANGED
|
@@ -1,50 +1,56 @@
|
|
|
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
|
|
|
7
12
|
import crypto from 'node:crypto'
|
|
8
13
|
import fs from 'node:fs'
|
|
9
14
|
import path from 'node:path'
|
|
10
|
-
import { execAsync } from './exec.ts'
|
|
11
15
|
|
|
12
|
-
const CACHE_SCHEMA_VERSION = '
|
|
16
|
+
const CACHE_SCHEMA_VERSION = '4'
|
|
13
17
|
|
|
18
|
+
/** Compute a deterministic content hash for binary caching.
|
|
19
|
+
* Returns: { short: "a1b2c3d4..." (16 hex), full: "a1b2c3d4..." (64 hex) }
|
|
20
|
+
* The short hash is used for local cache paths, the full hash for remote cache keys. */
|
|
14
21
|
export async function computeContentHash({
|
|
15
22
|
lintcnDir,
|
|
16
23
|
tsgolintVersion,
|
|
17
24
|
}: {
|
|
18
25
|
lintcnDir: string
|
|
19
26
|
tsgolintVersion: string
|
|
20
|
-
}): Promise<string> {
|
|
27
|
+
}): Promise<{ short: string; full: string }> {
|
|
21
28
|
const hash = crypto.createHash('sha256')
|
|
22
29
|
|
|
23
30
|
hash.update(`cache-schema:${CACHE_SCHEMA_VERSION}\n`)
|
|
24
31
|
hash.update(`tsgolint:${tsgolintVersion}\n`)
|
|
25
32
|
hash.update(`platform:${process.platform}-${process.arch}\n`)
|
|
26
33
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
// walk rule subfolders for non-test .go files in sorted order
|
|
35
|
+
const entries = fs.readdirSync(lintcnDir, { withFileTypes: true })
|
|
36
|
+
.filter((e) => { return e.isDirectory() && !e.name.startsWith('.') })
|
|
37
|
+
.sort((a, b) => { return a.name.localeCompare(b.name) })
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const subDir = path.join(lintcnDir, entry.name)
|
|
41
|
+
const goFiles = fs.readdirSync(subDir)
|
|
42
|
+
.filter((f) => { return f.endsWith('.go') && !f.endsWith('_test.go') })
|
|
43
|
+
.sort()
|
|
44
|
+
|
|
45
|
+
for (const file of goFiles) {
|
|
46
|
+
const content = fs.readFileSync(path.join(subDir, file), 'utf-8')
|
|
47
|
+
hash.update(`file:${entry.name}/${file}\n`)
|
|
48
|
+
hash.update(content)
|
|
49
|
+
}
|
|
33
50
|
}
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.readdirSync(lintcnDir)
|
|
38
|
-
.filter((f) => {
|
|
39
|
-
return f.endsWith('.go')
|
|
40
|
-
})
|
|
41
|
-
.sort()
|
|
42
|
-
|
|
43
|
-
for (const file of files) {
|
|
44
|
-
const content = fs.readFileSync(path.join(lintcnDir, file), 'utf-8')
|
|
45
|
-
hash.update(`file:${file}\n`)
|
|
46
|
-
hash.update(content)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return hash.digest('hex').slice(0, 16)
|
|
52
|
+
const full = hash.digest('hex')
|
|
53
|
+
return { short: full.slice(0, 16), full }
|
|
50
54
|
}
|
|
55
|
+
|
|
56
|
+
|