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.
@@ -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
- // Convert github.com/user/repo/blob/branch/path to raw.githubusercontent.com
13
- const blobMatch = url.match(
14
- /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/,
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
- return url
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 | null> {
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
- const response = await fetch(url)
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 fetchFile(testUrl)
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)
@@ -3,7 +3,7 @@
3
3
 
4
4
  import fs from 'node:fs'
5
5
  import { spawn } from 'node:child_process'
6
- import { getLintcnDir } from '../paths.ts'
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 = getLintcnDir()
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) {
@@ -1,13 +1,12 @@
1
1
  // lintcn list — list installed rules with metadata from .lintcn/
2
2
 
3
- import fs from 'node:fs'
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 = getLintcnDir()
7
+ const lintcnDir = findLintcnDir()
9
8
 
10
- if (!fs.existsSync(lintcnDir)) {
9
+ if (!lintcnDir) {
11
10
  console.log('No .lintcn/ directory found. Run `lintcn add <url>` to add rules.')
12
11
  return
13
12
  }
@@ -2,15 +2,11 @@
2
2
 
3
3
  import fs from 'node:fs'
4
4
  import path from 'node:path'
5
- import { getLintcnDir } from '../paths.ts'
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 = getLintcnDir()
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
- const RULE_VAR_RE = /^var\s+(\w+)\s*=\s*rule\.Rule\s*\{/m
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, Go version, and platform
3
- // into a single SHA-256 hash used as the cached binary filename.
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 path relative to cwd.
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
+ }