lintcn 0.2.0 → 0.3.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 +21 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +82 -41
- package/dist/codegen.d.ts +7 -1
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +64 -498
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +29 -16
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +2 -5
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +3 -4
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +2 -5
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +8 -1
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +6 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +23 -1
- package/package.json +2 -1
- package/src/cache.ts +93 -46
- package/src/codegen.ts +74 -498
- package/src/commands/add.ts +34 -19
- package/src/commands/lint.ts +2 -5
- package/src/commands/list.ts +3 -4
- package/src/commands/remove.ts +2 -6
- package/src/discover.ts +10 -1
- package/src/hash.ts +7 -2
- package/src/index.ts +1 -0
- package/src/paths.ts +25 -1
package/src/commands/add.ts
CHANGED
|
@@ -8,36 +8,54 @@ import { getLintcnDir } from '../paths.ts'
|
|
|
8
8
|
import { generateEditorGoFiles } from '../codegen.ts'
|
|
9
9
|
import { ensureTsgolintSource, DEFAULT_TSGOLINT_VERSION } from '../cache.ts'
|
|
10
10
|
|
|
11
|
+
/** Convert GitHub blob URLs to raw.githubusercontent.com.
|
|
12
|
+
* Handles branch names containing slashes (e.g. feature/x) by splitting
|
|
13
|
+
* on /blob/ then finding the file path from the end (must end in .go). */
|
|
11
14
|
function normalizeGithubUrl(url: string): string {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
)
|
|
16
|
-
if (blobMatch) {
|
|
17
|
-
const [, owner, repo, branch, filePath] = blobMatch
|
|
18
|
-
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filePath}`
|
|
15
|
+
const blobSplit = url.match(/^(https?:\/\/github\.com\/[^/]+\/[^/]+)\/blob\/(.+)$/)
|
|
16
|
+
if (!blobSplit) {
|
|
17
|
+
return url
|
|
19
18
|
}
|
|
20
|
-
|
|
19
|
+
|
|
20
|
+
const [, repoUrl, refAndPath] = blobSplit
|
|
21
|
+
// repoUrl = "https://github.com/owner/repo"
|
|
22
|
+
// refAndPath = "feature/x/rules/my_rule.go" or "main/rules/my_rule.go"
|
|
23
|
+
|
|
24
|
+
// Extract owner/repo from repoUrl
|
|
25
|
+
const repoMatch = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)$/)
|
|
26
|
+
if (!repoMatch) {
|
|
27
|
+
return url
|
|
28
|
+
}
|
|
29
|
+
const [, owner, repo] = repoMatch
|
|
30
|
+
|
|
31
|
+
// For raw.githubusercontent.com, the format is owner/repo/ref/path.
|
|
32
|
+
// We can pass refAndPath directly since GitHub resolves it.
|
|
33
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${refAndPath}`
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
function deriveTestUrl(rawUrl: string): string {
|
|
24
37
|
return rawUrl.replace(/\.go$/, '_test.go')
|
|
25
38
|
}
|
|
26
39
|
|
|
27
|
-
async function fetchFile(url: string): Promise<string
|
|
40
|
+
async function fetchFile(url: string): Promise<string> {
|
|
41
|
+
const response = await fetch(url)
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`)
|
|
44
|
+
}
|
|
45
|
+
return response.text()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function tryFetchFile(url: string): Promise<string | null> {
|
|
28
49
|
try {
|
|
29
|
-
|
|
30
|
-
if (!response.ok) {
|
|
31
|
-
return null
|
|
32
|
-
}
|
|
33
|
-
return await response.text()
|
|
50
|
+
return await fetchFile(url)
|
|
34
51
|
} catch {
|
|
35
52
|
return null
|
|
36
53
|
}
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
function rewritePackageName(content: string): string {
|
|
40
|
-
// Rewrite first package declaration to package lintcn
|
|
57
|
+
// Rewrite first package declaration to package lintcn.
|
|
58
|
+
// Only matches before the first import or func to avoid touching comments.
|
|
41
59
|
return content.replace(/^package\s+\w+/m, 'package lintcn')
|
|
42
60
|
}
|
|
43
61
|
|
|
@@ -64,9 +82,6 @@ export async function addRule(url: string): Promise<void> {
|
|
|
64
82
|
|
|
65
83
|
console.log(`Fetching ${rawUrl}...`)
|
|
66
84
|
const content = await fetchFile(rawUrl)
|
|
67
|
-
if (!content) {
|
|
68
|
-
throw new Error(`Could not fetch rule from ${rawUrl}`)
|
|
69
|
-
}
|
|
70
85
|
|
|
71
86
|
// validate it looks like a Go file with a rule
|
|
72
87
|
if (!content.includes('rule.Rule')) {
|
|
@@ -96,7 +111,7 @@ export async function addRule(url: string): Promise<void> {
|
|
|
96
111
|
|
|
97
112
|
// try to fetch matching test file
|
|
98
113
|
const testUrl = deriveTestUrl(rawUrl)
|
|
99
|
-
const testContent = await
|
|
114
|
+
const testContent = await tryFetchFile(testUrl)
|
|
100
115
|
if (testContent) {
|
|
101
116
|
const testFileName = fileName.replace(/\.go$/, '_test.go')
|
|
102
117
|
const testProcessed = rewritePackageName(testContent)
|
package/src/commands/lint.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import fs from 'node:fs'
|
|
5
5
|
import { spawn } from 'node:child_process'
|
|
6
|
-
import {
|
|
6
|
+
import { requireLintcnDir } from '../paths.ts'
|
|
7
7
|
import { discoverRules } from '../discover.ts'
|
|
8
8
|
import { generateBuildWorkspace } from '../codegen.ts'
|
|
9
9
|
import { ensureTsgolintSource, DEFAULT_TSGOLINT_VERSION, cachedBinaryExists, getBinaryPath, getBuildDir, getBinDir } from '../cache.ts'
|
|
@@ -30,10 +30,7 @@ export async function buildBinary({
|
|
|
30
30
|
}): Promise<string> {
|
|
31
31
|
await checkGoInstalled()
|
|
32
32
|
|
|
33
|
-
const lintcnDir =
|
|
34
|
-
if (!fs.existsSync(lintcnDir)) {
|
|
35
|
-
throw new Error('No .lintcn/ directory found. Run `lintcn add <url>` first.')
|
|
36
|
-
}
|
|
33
|
+
const lintcnDir = requireLintcnDir()
|
|
37
34
|
|
|
38
35
|
const rules = discoverRules(lintcnDir)
|
|
39
36
|
if (rules.length === 0) {
|
package/src/commands/list.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// lintcn list — list installed rules with metadata from .lintcn/
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { getLintcnDir } from '../paths.ts'
|
|
3
|
+
import { findLintcnDir } from '../paths.ts'
|
|
5
4
|
import { discoverRules } from '../discover.ts'
|
|
6
5
|
|
|
7
6
|
export function listRules(): void {
|
|
8
|
-
const lintcnDir =
|
|
7
|
+
const lintcnDir = findLintcnDir()
|
|
9
8
|
|
|
10
|
-
if (!
|
|
9
|
+
if (!lintcnDir) {
|
|
11
10
|
console.log('No .lintcn/ directory found. Run `lintcn add <url>` to add rules.')
|
|
12
11
|
return
|
|
13
12
|
}
|
package/src/commands/remove.ts
CHANGED
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
-
import {
|
|
5
|
+
import { requireLintcnDir } from '../paths.ts'
|
|
6
6
|
import { discoverRules } from '../discover.ts'
|
|
7
7
|
|
|
8
8
|
export function removeRule(name: string): void {
|
|
9
|
-
const lintcnDir =
|
|
10
|
-
|
|
11
|
-
if (!fs.existsSync(lintcnDir)) {
|
|
12
|
-
throw new Error('No .lintcn/ directory found.')
|
|
13
|
-
}
|
|
9
|
+
const lintcnDir = requireLintcnDir()
|
|
14
10
|
|
|
15
11
|
// match by lintcn:name metadata or by filename
|
|
16
12
|
const rules = discoverRules(lintcnDir)
|
package/src/discover.ts
CHANGED
|
@@ -17,7 +17,9 @@ export interface RuleMetadata {
|
|
|
17
17
|
fileName: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Matches `var XxxRule = rule.Rule{` with optional leading whitespace
|
|
21
|
+
// and optional import alias (e.g. `r.Rule{` if imported as `r "...rule"`)
|
|
22
|
+
const RULE_VAR_RE = /^\s*var\s+(\w+)\s*=\s*\w*\.?Rule\s*\{/m
|
|
21
23
|
const METADATA_RE = /^\/\/\s*lintcn:(\w+)\s+(.+)$/gm
|
|
22
24
|
|
|
23
25
|
export function parseMetadata(content: string): Record<string, string> {
|
|
@@ -50,6 +52,13 @@ export function discoverRules(lintcnDir: string): RuleMetadata[] {
|
|
|
50
52
|
|
|
51
53
|
const varName = parseRuleVar(content)
|
|
52
54
|
if (!varName) {
|
|
55
|
+
// warn if file contains rule.Rule but we couldn't parse the var name
|
|
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
|
+
}
|
|
53
62
|
continue
|
|
54
63
|
}
|
|
55
64
|
|
package/src/hash.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// Content hash for binary caching.
|
|
2
|
-
// Combines tsgolint version, rule file contents,
|
|
3
|
-
// into a single SHA-256 hash
|
|
2
|
+
// Combines cache schema version, tsgolint version, rule file contents,
|
|
3
|
+
// Go version, and platform into a single SHA-256 hash.
|
|
4
|
+
// Bump CACHE_SCHEMA_VERSION when codegen logic changes to invalidate
|
|
5
|
+
// stale binaries built by older lintcn versions.
|
|
4
6
|
|
|
5
7
|
import crypto from 'node:crypto'
|
|
6
8
|
import fs from 'node:fs'
|
|
7
9
|
import path from 'node:path'
|
|
8
10
|
import { execAsync } from './exec.ts'
|
|
9
11
|
|
|
12
|
+
const CACHE_SCHEMA_VERSION = '2'
|
|
13
|
+
|
|
10
14
|
export async function computeContentHash({
|
|
11
15
|
lintcnDir,
|
|
12
16
|
tsgolintVersion,
|
|
@@ -16,6 +20,7 @@ export async function computeContentHash({
|
|
|
16
20
|
}): Promise<string> {
|
|
17
21
|
const hash = crypto.createHash('sha256')
|
|
18
22
|
|
|
23
|
+
hash.update(`cache-schema:${CACHE_SCHEMA_VERSION}\n`)
|
|
19
24
|
hash.update(`tsgolint:${tsgolintVersion}\n`)
|
|
20
25
|
hash.update(`platform:${process.platform}-${process.arch}\n`)
|
|
21
26
|
|
package/src/index.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { lint, buildBinary } from './commands/lint.ts'
|
|
|
5
5
|
export { listRules } from './commands/list.ts'
|
|
6
6
|
export { removeRule } from './commands/remove.ts'
|
|
7
7
|
export { DEFAULT_TSGOLINT_VERSION } from './cache.ts'
|
|
8
|
+
export { findLintcnDir, getLintcnDir, requireLintcnDir } from './paths.ts'
|
package/src/paths.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
// Resolve the .lintcn/ directory
|
|
1
|
+
// Resolve the .lintcn/ directory by walking up from cwd.
|
|
2
|
+
// This lets users run `lintcn lint` from any subdirectory of their project.
|
|
2
3
|
|
|
4
|
+
import { findUpSync } from 'find-up'
|
|
3
5
|
import path from 'node:path'
|
|
4
6
|
|
|
7
|
+
/** Find the nearest .lintcn/ directory by walking up from cwd.
|
|
8
|
+
* Returns the absolute path to the directory, or null if not found. */
|
|
9
|
+
export function findLintcnDir(): string | null {
|
|
10
|
+
const found = findUpSync('.lintcn', { type: 'directory' })
|
|
11
|
+
return found ?? null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Find .lintcn/ or throw with a helpful error. */
|
|
5
15
|
export function getLintcnDir(): string {
|
|
16
|
+
const dir = findLintcnDir()
|
|
17
|
+
if (dir) {
|
|
18
|
+
return dir
|
|
19
|
+
}
|
|
20
|
+
// fall back to cwd/.lintcn for `lintcn add` (creates the directory)
|
|
6
21
|
return path.resolve(process.cwd(), '.lintcn')
|
|
7
22
|
}
|
|
23
|
+
|
|
24
|
+
/** Find .lintcn/ or throw — for commands that require it to exist. */
|
|
25
|
+
export function requireLintcnDir(): string {
|
|
26
|
+
const dir = findLintcnDir()
|
|
27
|
+
if (!dir) {
|
|
28
|
+
throw new Error('No .lintcn/ directory found in current or parent directories. Run `lintcn add <url>` first.')
|
|
29
|
+
}
|
|
30
|
+
return dir
|
|
31
|
+
}
|