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/src/commands/lint.ts
CHANGED
|
@@ -5,7 +5,7 @@ import fs from 'node:fs'
|
|
|
5
5
|
import { spawn } from 'node:child_process'
|
|
6
6
|
import { requireLintcnDir } from '../paths.ts'
|
|
7
7
|
import { discoverRules } from '../discover.ts'
|
|
8
|
-
import { generateBuildWorkspace } from '../codegen.ts'
|
|
8
|
+
import { generateBuildWorkspace, generateEditorGoFiles } from '../codegen.ts'
|
|
9
9
|
import { ensureTsgolintSource, validateVersion, cachedBinaryExists, getBinaryPath, getBuildDir, getBinDir } from '../cache.ts'
|
|
10
10
|
import { computeContentHash } from '../hash.ts'
|
|
11
11
|
import { execAsync } from '../exec.ts'
|
|
@@ -44,7 +44,7 @@ export async function buildBinary({
|
|
|
44
44
|
const tsgolintDir = await ensureTsgolintSource(tsgolintVersion)
|
|
45
45
|
|
|
46
46
|
// compute content hash
|
|
47
|
-
const contentHash = await computeContentHash({
|
|
47
|
+
const { short: contentHash } = await computeContentHash({
|
|
48
48
|
lintcnDir,
|
|
49
49
|
tsgolintVersion,
|
|
50
50
|
})
|
|
@@ -55,6 +55,9 @@ export async function buildBinary({
|
|
|
55
55
|
return getBinaryPath(contentHash)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// ensure .lintcn/go.mod exists (gitignored, needed by the build workspace symlink)
|
|
59
|
+
generateEditorGoFiles(lintcnDir)
|
|
60
|
+
|
|
58
61
|
// generate build workspace (per-hash dir to avoid races between concurrent processes)
|
|
59
62
|
const buildDir = getBuildDir(contentHash)
|
|
60
63
|
console.log('Generating build workspace...')
|
|
@@ -70,10 +73,23 @@ export async function buildBinary({
|
|
|
70
73
|
fs.mkdirSync(binDir, { recursive: true })
|
|
71
74
|
const binaryPath = getBinaryPath(contentHash)
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
// Check if any lintcn binary has been built before — if not, this is a cold
|
|
77
|
+
// build that compiles the full tsgolint + typescript-go dependency tree.
|
|
78
|
+
const existingBins = fs.existsSync(binDir) ? fs.readdirSync(binDir) : []
|
|
79
|
+
if (existingBins.length === 0) {
|
|
80
|
+
console.log('Compiling custom tsgolint binary (first build — may take 30s+ to compile dependencies)...')
|
|
81
|
+
console.log('Subsequent builds will be fast (~1s). In CI, cache ~/.cache/lintcn/ and GOCACHE (run `go env GOCACHE`).')
|
|
82
|
+
} else {
|
|
83
|
+
console.log('Compiling custom tsgolint binary...')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { exitCode: buildExitCode } = await execAsync('go', ['build', '-trimpath', '-o', binaryPath, './wrapper'], {
|
|
75
87
|
cwd: buildDir,
|
|
88
|
+
stdio: 'inherit',
|
|
76
89
|
})
|
|
90
|
+
if (buildExitCode !== 0) {
|
|
91
|
+
throw new Error(`Go compilation failed (exit code ${buildExitCode})`)
|
|
92
|
+
}
|
|
77
93
|
|
|
78
94
|
console.log('Build complete')
|
|
79
95
|
return binaryPath
|
package/src/commands/remove.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// lintcn remove <name> — delete a rule
|
|
1
|
+
// lintcn remove <name> — delete a rule subfolder from .lintcn/
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
@@ -7,13 +7,11 @@ import { discoverRules } from '../discover.ts'
|
|
|
7
7
|
|
|
8
8
|
export function removeRule(name: string): void {
|
|
9
9
|
const lintcnDir = requireLintcnDir()
|
|
10
|
-
|
|
11
|
-
// match by lintcn:name metadata or by filename
|
|
12
10
|
const rules = discoverRules(lintcnDir)
|
|
13
11
|
const normalizedName = name.replace(/-/g, '_')
|
|
14
12
|
|
|
15
13
|
const match = rules.find((r) => {
|
|
16
|
-
return r.name === name || r.
|
|
14
|
+
return r.name === name || r.packageName === normalizedName
|
|
17
15
|
})
|
|
18
16
|
|
|
19
17
|
if (!match) {
|
|
@@ -22,16 +20,8 @@ export function removeRule(name: string): void {
|
|
|
22
20
|
)
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
fs.rmSync(
|
|
28
|
-
console.log(`Removed ${match.
|
|
29
|
-
|
|
30
|
-
// delete test file if exists
|
|
31
|
-
const testFileName = match.fileName.replace(/\.go$/, '_test.go')
|
|
32
|
-
const testPath = path.join(lintcnDir, testFileName)
|
|
33
|
-
if (fs.existsSync(testPath)) {
|
|
34
|
-
fs.rmSync(testPath)
|
|
35
|
-
console.log(`Removed ${testFileName}`)
|
|
36
|
-
}
|
|
23
|
+
// Remove the entire subfolder
|
|
24
|
+
const ruleDir = path.join(lintcnDir, match.packageName)
|
|
25
|
+
fs.rmSync(ruleDir, { recursive: true })
|
|
26
|
+
console.log(`Removed ${match.packageName}/`)
|
|
37
27
|
}
|
package/src/discover.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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
|
|
10
11
|
/** one-line description from // lintcn:description */
|
|
11
12
|
description: string
|
|
@@ -13,8 +14,8 @@ export interface RuleMetadata {
|
|
|
13
14
|
source: string
|
|
14
15
|
/** exported Go variable name like NoFloatingPromisesRule */
|
|
15
16
|
varName: string
|
|
16
|
-
/**
|
|
17
|
-
|
|
17
|
+
/** Go package name (= subfolder name, e.g. no_floating_promises) */
|
|
18
|
+
packageName: string
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// Matches `var XxxRule = rule.Rule{` with optional leading whitespace
|
|
@@ -40,38 +41,46 @@ export function discoverRules(lintcnDir: string): RuleMetadata[] {
|
|
|
40
41
|
return []
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
const
|
|
44
|
-
|
|
44
|
+
const rules: RuleMetadata[] = []
|
|
45
|
+
const entries = fs.readdirSync(lintcnDir, { withFileTypes: true })
|
|
46
|
+
|
|
47
|
+
// Warn about flat .go files that should be in subfolders
|
|
48
|
+
const flatGoFiles = entries.filter((e) => {
|
|
49
|
+
return e.isFile() && e.name.endsWith('.go')
|
|
45
50
|
})
|
|
51
|
+
for (const file of flatGoFiles) {
|
|
52
|
+
const baseName = file.name.replace(/(_test)?\.go$/, '')
|
|
53
|
+
console.error(
|
|
54
|
+
`Error: ${file.name} is a flat file in .lintcn/ — rules must be in subfolders.\n` +
|
|
55
|
+
` Move it to .lintcn/${baseName}/${file.name} and change the package to "${baseName}".\n`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
46
58
|
|
|
47
|
-
const
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
|
48
61
|
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
62
|
+
const subDir = path.join(lintcnDir, entry.name)
|
|
63
|
+
const goFiles = fs.readdirSync(subDir).filter((f) => {
|
|
64
|
+
return f.endsWith('.go') && !f.endsWith('_test.go')
|
|
65
|
+
})
|
|
52
66
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (content.includes('rule.Rule')) {
|
|
57
|
-
console.warn(
|
|
58
|
-
`Warning: ${fileName} contains rule.Rule but no exported var was found. ` +
|
|
59
|
-
`Expected pattern: var XxxRule = rule.Rule{`,
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
67
|
+
for (const fileName of goFiles) {
|
|
68
|
+
const filePath = path.join(subDir, fileName)
|
|
69
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
const varName = parseRuleVar(content)
|
|
72
|
+
if (!varName) continue
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const meta = parseMetadata(content)
|
|
75
|
+
|
|
76
|
+
rules.push({
|
|
77
|
+
name: meta.name || entry.name.replace(/_/g, '-'),
|
|
78
|
+
description: meta.description || '',
|
|
79
|
+
source: meta.source || '',
|
|
80
|
+
varName,
|
|
81
|
+
packageName: entry.name,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
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
|
+
|