@pyreon/lint 0.11.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/LICENSE +21 -0
- package/README.md +214 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +2955 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +260 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +56 -0
- package/src/cache.ts +51 -0
- package/src/cli.ts +199 -0
- package/src/config/ignore.ts +159 -0
- package/src/config/loader.ts +72 -0
- package/src/config/presets.ts +62 -0
- package/src/index.ts +40 -0
- package/src/lint.ts +226 -0
- package/src/reporter.ts +85 -0
- package/src/rules/accessibility/dialog-a11y.ts +32 -0
- package/src/rules/accessibility/overlay-a11y.ts +33 -0
- package/src/rules/accessibility/toast-a11y.ts +38 -0
- package/src/rules/architecture/dev-guard-warnings.ts +57 -0
- package/src/rules/architecture/no-circular-import.ts +59 -0
- package/src/rules/architecture/no-cross-layer-import.ts +75 -0
- package/src/rules/architecture/no-deep-import.ts +32 -0
- package/src/rules/architecture/no-error-without-prefix.ts +75 -0
- package/src/rules/form/no-submit-without-validation.ts +45 -0
- package/src/rules/form/no-unregistered-field.ts +45 -0
- package/src/rules/form/prefer-field-array.ts +41 -0
- package/src/rules/hooks/no-raw-addeventlistener.ts +28 -0
- package/src/rules/hooks/no-raw-localstorage.ts +35 -0
- package/src/rules/hooks/no-raw-setinterval.ts +41 -0
- package/src/rules/index.ts +208 -0
- package/src/rules/jsx/no-and-conditional.ts +32 -0
- package/src/rules/jsx/no-children-access.ts +44 -0
- package/src/rules/jsx/no-classname.ts +27 -0
- package/src/rules/jsx/no-htmlfor.ts +27 -0
- package/src/rules/jsx/no-index-as-by.ts +70 -0
- package/src/rules/jsx/no-map-in-jsx.ts +43 -0
- package/src/rules/jsx/no-missing-for-by.ts +27 -0
- package/src/rules/jsx/no-onchange.ts +46 -0
- package/src/rules/jsx/no-props-destructure.ts +64 -0
- package/src/rules/jsx/no-ternary-conditional.ts +32 -0
- package/src/rules/jsx/use-by-not-key.ts +33 -0
- package/src/rules/lifecycle/no-dom-in-setup.ts +53 -0
- package/src/rules/lifecycle/no-effect-in-mount.ts +36 -0
- package/src/rules/lifecycle/no-missing-cleanup.ts +80 -0
- package/src/rules/lifecycle/no-mount-in-effect.ts +35 -0
- package/src/rules/performance/no-eager-import.ts +28 -0
- package/src/rules/performance/no-effect-in-for.ts +41 -0
- package/src/rules/performance/no-large-for-without-by.ts +28 -0
- package/src/rules/performance/prefer-show-over-display.ts +47 -0
- package/src/rules/reactivity/no-bare-signal-in-jsx.ts +56 -0
- package/src/rules/reactivity/no-effect-assignment.ts +65 -0
- package/src/rules/reactivity/no-nested-effect.ts +33 -0
- package/src/rules/reactivity/no-peek-in-tracked.ts +35 -0
- package/src/rules/reactivity/no-signal-in-loop.ts +59 -0
- package/src/rules/reactivity/no-signal-leak.ts +58 -0
- package/src/rules/reactivity/no-unbatched-updates.ts +77 -0
- package/src/rules/reactivity/prefer-computed.ts +56 -0
- package/src/rules/router/index.ts +4 -0
- package/src/rules/router/no-href-navigation.ts +51 -0
- package/src/rules/router/no-imperative-navigate-in-render.ts +83 -0
- package/src/rules/router/no-missing-fallback.ts +87 -0
- package/src/rules/router/prefer-use-is-active.ts +45 -0
- package/src/rules/ssr/no-mismatch-risk.ts +47 -0
- package/src/rules/ssr/no-window-in-ssr.ts +76 -0
- package/src/rules/ssr/prefer-request-context.ts +56 -0
- package/src/rules/store/no-duplicate-store-id.ts +43 -0
- package/src/rules/store/no-mutate-store-state.ts +37 -0
- package/src/rules/store/no-store-outside-provider.ts +59 -0
- package/src/rules/styling/no-dynamic-styled.ts +60 -0
- package/src/rules/styling/no-inline-style-object.ts +30 -0
- package/src/rules/styling/no-theme-outside-provider.ts +45 -0
- package/src/rules/styling/prefer-cx.ts +44 -0
- package/src/runner.ts +170 -0
- package/src/tests/runner.test.ts +1043 -0
- package/src/types.ts +125 -0
- package/src/utils/ast.ts +192 -0
- package/src/utils/imports.ts +122 -0
- package/src/utils/index.ts +39 -0
- package/src/utils/source.ts +36 -0
- package/src/watcher.ts +118 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type Severity = "error" | "warn" | "info" | "off";
|
|
3
|
+
interface SourceLocation {
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
}
|
|
7
|
+
interface Span {
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
}
|
|
11
|
+
interface Fix {
|
|
12
|
+
span: Span;
|
|
13
|
+
replacement: string;
|
|
14
|
+
}
|
|
15
|
+
interface Diagnostic {
|
|
16
|
+
ruleId: string;
|
|
17
|
+
severity: Severity;
|
|
18
|
+
message: string;
|
|
19
|
+
span: Span;
|
|
20
|
+
loc: SourceLocation;
|
|
21
|
+
fix?: Fix | undefined;
|
|
22
|
+
}
|
|
23
|
+
type RuleCategory = "reactivity" | "jsx" | "lifecycle" | "performance" | "ssr" | "architecture" | "store" | "form" | "styling" | "hooks" | "accessibility" | "router";
|
|
24
|
+
interface RuleMeta {
|
|
25
|
+
id: string;
|
|
26
|
+
category: RuleCategory;
|
|
27
|
+
description: string;
|
|
28
|
+
severity: Severity;
|
|
29
|
+
fixable: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface RuleContext {
|
|
32
|
+
report(diagnostic: Omit<Diagnostic, "ruleId" | "severity" | "loc">): void;
|
|
33
|
+
getSourceText(): string;
|
|
34
|
+
getFilePath(): string;
|
|
35
|
+
}
|
|
36
|
+
type VisitorCallback = (node: any, parent?: any) => void;
|
|
37
|
+
interface VisitorCallbacks {
|
|
38
|
+
[nodeType: string]: VisitorCallback;
|
|
39
|
+
}
|
|
40
|
+
interface Rule {
|
|
41
|
+
meta: RuleMeta;
|
|
42
|
+
create(context: RuleContext): VisitorCallbacks;
|
|
43
|
+
}
|
|
44
|
+
interface LintConfig {
|
|
45
|
+
rules: Record<string, Severity>;
|
|
46
|
+
include?: string[] | undefined;
|
|
47
|
+
exclude?: string[] | undefined;
|
|
48
|
+
}
|
|
49
|
+
interface LintConfigFile {
|
|
50
|
+
preset?: PresetName | undefined;
|
|
51
|
+
rules?: Record<string, Severity> | undefined;
|
|
52
|
+
include?: string[] | undefined;
|
|
53
|
+
exclude?: string[] | undefined;
|
|
54
|
+
}
|
|
55
|
+
type PresetName = "recommended" | "strict" | "app" | "lib";
|
|
56
|
+
interface LintFileResult {
|
|
57
|
+
filePath: string;
|
|
58
|
+
diagnostics: Diagnostic[];
|
|
59
|
+
fixedSource?: string | undefined;
|
|
60
|
+
}
|
|
61
|
+
interface LintResult {
|
|
62
|
+
files: LintFileResult[];
|
|
63
|
+
totalErrors: number;
|
|
64
|
+
totalWarnings: number;
|
|
65
|
+
totalInfos: number;
|
|
66
|
+
}
|
|
67
|
+
interface LintOptions {
|
|
68
|
+
paths: string[];
|
|
69
|
+
preset?: PresetName | undefined;
|
|
70
|
+
fix?: boolean | undefined;
|
|
71
|
+
quiet?: boolean | undefined;
|
|
72
|
+
ruleOverrides?: Record<string, Severity> | undefined;
|
|
73
|
+
config?: string | undefined;
|
|
74
|
+
ignore?: string | undefined;
|
|
75
|
+
}
|
|
76
|
+
interface ImportInfo {
|
|
77
|
+
source: string;
|
|
78
|
+
specifiers: Array<{
|
|
79
|
+
imported: string;
|
|
80
|
+
local: string;
|
|
81
|
+
}>;
|
|
82
|
+
isDefault: boolean;
|
|
83
|
+
isNamespace: boolean;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/utils/source.d.ts
|
|
87
|
+
/**
|
|
88
|
+
* Fast offset→line/column conversion using binary search over precomputed line starts.
|
|
89
|
+
*/
|
|
90
|
+
declare class LineIndex {
|
|
91
|
+
private lineStarts;
|
|
92
|
+
constructor(sourceText: string);
|
|
93
|
+
/** Convert a byte offset to a 1-based line and 0-based column. */
|
|
94
|
+
locate(offset: number): SourceLocation;
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/cache.d.ts
|
|
98
|
+
/**
|
|
99
|
+
* Simple in-memory cache for parsed ASTs keyed by file content hash.
|
|
100
|
+
*
|
|
101
|
+
* Uses FNV-1a hash of source text as cache key for fast lookups
|
|
102
|
+
* during repeat runs (e.g., watch mode).
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { AstCache } from "@pyreon/lint"
|
|
107
|
+
*
|
|
108
|
+
* const cache = new AstCache()
|
|
109
|
+
* const cached = cache.get(sourceText)
|
|
110
|
+
* if (!cached) {
|
|
111
|
+
* const parsed = parse(sourceText)
|
|
112
|
+
* cache.set(sourceText, parsed)
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare class AstCache {
|
|
117
|
+
private cache;
|
|
118
|
+
get(sourceText: string): {
|
|
119
|
+
program: any;
|
|
120
|
+
lineIndex: LineIndex;
|
|
121
|
+
} | undefined;
|
|
122
|
+
set(sourceText: string, value: {
|
|
123
|
+
program: any;
|
|
124
|
+
lineIndex: LineIndex;
|
|
125
|
+
}): void;
|
|
126
|
+
clear(): void;
|
|
127
|
+
get size(): number;
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/config/ignore.d.ts
|
|
131
|
+
/**
|
|
132
|
+
* Create a filter function that returns true if a file path should be ignored.
|
|
133
|
+
*
|
|
134
|
+
* Loads patterns from `.pyreonlintignore` and `.gitignore` in the given directory.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* import { createIgnoreFilter } from "@pyreon/lint"
|
|
139
|
+
*
|
|
140
|
+
* const isIgnored = createIgnoreFilter(process.cwd())
|
|
141
|
+
* if (!isIgnored("src/app.tsx")) lintFile(...)
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
declare function createIgnoreFilter(cwd: string, extraIgnore?: string | undefined): (filePath: string) => boolean;
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/config/loader.d.ts
|
|
147
|
+
/**
|
|
148
|
+
* Load a lint config file from the given directory or its parents.
|
|
149
|
+
*
|
|
150
|
+
* Search order:
|
|
151
|
+
* 1. `.pyreonlintrc.json`
|
|
152
|
+
* 2. `.pyreonlintrc`
|
|
153
|
+
* 3. `pyreonlint.config.json`
|
|
154
|
+
* 4. `package.json` `"pyreonlint"` field
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* import { loadConfig } from "@pyreon/lint"
|
|
159
|
+
*
|
|
160
|
+
* const config = loadConfig(process.cwd())
|
|
161
|
+
* if (config) console.log(config.preset)
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
declare function loadConfig(cwd: string): LintConfigFile | null;
|
|
165
|
+
/**
|
|
166
|
+
* Load a config file from a specific path.
|
|
167
|
+
*/
|
|
168
|
+
declare function loadConfigFromPath(filePath: string): LintConfigFile | null;
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/config/presets.d.ts
|
|
171
|
+
declare function getPreset(name: PresetName): LintConfig;
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/lint.d.ts
|
|
174
|
+
/**
|
|
175
|
+
* Lint files and return results.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* import { lint } from "@pyreon/lint"
|
|
180
|
+
*
|
|
181
|
+
* const result = lint({ paths: ["src/"], preset: "recommended" })
|
|
182
|
+
* console.log(result.totalErrors) // 0
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
declare function lint(options: LintOptions): LintResult;
|
|
186
|
+
/**
|
|
187
|
+
* List all available rules with their metadata.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* import { listRules } from "@pyreon/lint"
|
|
192
|
+
*
|
|
193
|
+
* for (const rule of listRules()) {
|
|
194
|
+
* console.log(`${rule.id} (${rule.severity}): ${rule.description}`)
|
|
195
|
+
* }
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
declare function listRules(): RuleMeta[];
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/reporter.d.ts
|
|
201
|
+
/**
|
|
202
|
+
* Format results as human-readable colored text.
|
|
203
|
+
*/
|
|
204
|
+
declare function formatText(result: LintResult): string;
|
|
205
|
+
/**
|
|
206
|
+
* Format results as JSON.
|
|
207
|
+
*/
|
|
208
|
+
declare function formatJSON(result: LintResult): string;
|
|
209
|
+
/**
|
|
210
|
+
* Format results as compact single-line-per-diagnostic output.
|
|
211
|
+
*/
|
|
212
|
+
declare function formatCompact(result: LintResult): string;
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/rules/index.d.ts
|
|
215
|
+
declare const allRules: Rule[];
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/runner.d.ts
|
|
218
|
+
/**
|
|
219
|
+
* Lint a single file and return diagnostics.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const result = lintFile("app.tsx", source, allRules, getPreset("recommended"))
|
|
224
|
+
* for (const d of result.diagnostics) console.log(d.message)
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
declare function lintFile(filePath: string, sourceText: string, rules: Rule[], config: LintConfig, cache?: AstCache | undefined): LintFileResult;
|
|
228
|
+
/**
|
|
229
|
+
* Apply all auto-fixes to a source text.
|
|
230
|
+
* Fixes are applied in reverse order to maintain correct offsets.
|
|
231
|
+
*/
|
|
232
|
+
declare function applyFixes(sourceText: string, diagnostics: Diagnostic[]): string;
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/utils/imports.d.ts
|
|
235
|
+
declare function isPyreonImport(source: string): boolean;
|
|
236
|
+
declare function isPyreonPackage(source: string): boolean;
|
|
237
|
+
declare function extractImportInfo(node: any): ImportInfo | null;
|
|
238
|
+
declare function importsName(imports: ImportInfo[], name: string, fromPackage?: string): boolean;
|
|
239
|
+
declare function getLocalName(imports: ImportInfo[], name: string, fromPackage?: string): string | null;
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/watcher.d.ts
|
|
242
|
+
/**
|
|
243
|
+
* Watch directories and re-lint changed files.
|
|
244
|
+
*
|
|
245
|
+
* Uses `fs.watch` (recursive) with 100ms debounce.
|
|
246
|
+
* Caches ASTs for unchanged files.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* import { watchAndLint } from "@pyreon/lint"
|
|
251
|
+
*
|
|
252
|
+
* watchAndLint({ paths: ["src/"], preset: "recommended", format: "text" })
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
declare function watchAndLint(options: LintOptions & {
|
|
256
|
+
format: string;
|
|
257
|
+
}): void;
|
|
258
|
+
//#endregion
|
|
259
|
+
export { AstCache, type Diagnostic, type Fix, type ImportInfo, LineIndex, type LintConfig, type LintConfigFile, type LintFileResult, type LintOptions, type LintResult, type PresetName, type Rule, type RuleCategory, type RuleContext, type RuleMeta, type Severity, type SourceLocation, type Span, type VisitorCallbacks, allRules, applyFixes, createIgnoreFilter, extractImportInfo, formatCompact, formatJSON, formatText, getLocalName, getPreset, importsName, isPyreonImport, isPyreonPackage, lint, lintFile, listRules, loadConfig, loadConfigFromPath, watchAndLint };
|
|
260
|
+
//# sourceMappingURL=index2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/utils/source.ts","../../../src/cache.ts","../../../src/config/ignore.ts","../../../src/config/loader.ts","../../../src/config/presets.ts","../../../src/lint.ts","../../../src/reporter.ts","../../../src/rules/index.ts","../../../src/runner.ts","../../../src/utils/imports.ts","../../../src/watcher.ts"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pyreon/lint",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Pyreon-specific linter — 55 rules for signals, JSX, SSR, performance, router, and architecture",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/pyreon/pyreon.git",
|
|
9
|
+
"directory": "packages/tools/lint"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/lint#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/pyreon/pyreon/issues"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./lib/index.js",
|
|
24
|
+
"module": "./lib/index.js",
|
|
25
|
+
"types": "./lib/types/index.d.ts",
|
|
26
|
+
"bin": {
|
|
27
|
+
"pyreon-lint": "./lib/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"bun": "./src/index.ts",
|
|
32
|
+
"import": "./lib/index.js",
|
|
33
|
+
"types": "./lib/types/index.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "vl_rolldown_build",
|
|
39
|
+
"dev": "vl_rolldown_build-watch",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"lint": "biome check .",
|
|
43
|
+
"prepublishOnly": "bun run build"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"oxc-parser": "^0.121.0",
|
|
47
|
+
"@oxc-project/types": "^0.121.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"oxc-parser": "^0.121.0",
|
|
51
|
+
"@oxc-project/types": "^0.121.0"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { LineIndex } from "./utils/source"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple in-memory cache for parsed ASTs keyed by file content hash.
|
|
5
|
+
*
|
|
6
|
+
* Uses FNV-1a hash of source text as cache key for fast lookups
|
|
7
|
+
* during repeat runs (e.g., watch mode).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { AstCache } from "@pyreon/lint"
|
|
12
|
+
*
|
|
13
|
+
* const cache = new AstCache()
|
|
14
|
+
* const cached = cache.get(sourceText)
|
|
15
|
+
* if (!cached) {
|
|
16
|
+
* const parsed = parse(sourceText)
|
|
17
|
+
* cache.set(sourceText, parsed)
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class AstCache {
|
|
22
|
+
private cache = new Map<string, { program: any; lineIndex: LineIndex }>()
|
|
23
|
+
|
|
24
|
+
get(sourceText: string): { program: any; lineIndex: LineIndex } | undefined {
|
|
25
|
+
const key = fnv1aHash(sourceText)
|
|
26
|
+
return this.cache.get(key)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(sourceText: string, value: { program: any; lineIndex: LineIndex }): void {
|
|
30
|
+
const key = fnv1aHash(sourceText)
|
|
31
|
+
this.cache.set(key, value)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
clear(): void {
|
|
35
|
+
this.cache.clear()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get size(): number {
|
|
39
|
+
return this.cache.size
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** FNV-1a hash — fast, non-cryptographic, low collision rate. */
|
|
44
|
+
function fnv1aHash(str: string): string {
|
|
45
|
+
let hash = 0x811c9dc5 // FNV offset basis
|
|
46
|
+
for (let i = 0; i < str.length; i++) {
|
|
47
|
+
hash ^= str.charCodeAt(i)
|
|
48
|
+
hash = (hash * 0x01000193) | 0 // FNV prime, keep 32-bit
|
|
49
|
+
}
|
|
50
|
+
return (hash >>> 0).toString(36)
|
|
51
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { lint, listRules } from "./lint"
|
|
3
|
+
import { formatCompact, formatJSON, formatText } from "./reporter"
|
|
4
|
+
import type { PresetName, Severity } from "./types"
|
|
5
|
+
import { watchAndLint } from "./watcher"
|
|
6
|
+
|
|
7
|
+
const VERSION = "0.10.0"
|
|
8
|
+
|
|
9
|
+
function printUsage() {
|
|
10
|
+
console.log(`
|
|
11
|
+
pyreon-lint [options] [path...]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--preset <name> Preset: recommended (default), strict, app, lib
|
|
15
|
+
--fix Auto-fix fixable issues
|
|
16
|
+
--format <fmt> Output: text (default), json, compact
|
|
17
|
+
--quiet Only show errors
|
|
18
|
+
--list List all available rules
|
|
19
|
+
--rule <id>=<sev> Override rule severity
|
|
20
|
+
--config <path> Config file path
|
|
21
|
+
--ignore <path> Ignore file path
|
|
22
|
+
--watch Watch mode — re-lint on file changes
|
|
23
|
+
--help, -h Show this help
|
|
24
|
+
--version, -v Show version
|
|
25
|
+
`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function printList() {
|
|
29
|
+
const rules = listRules()
|
|
30
|
+
const maxId = Math.max(...rules.map((r) => r.id.length))
|
|
31
|
+
const maxCat = Math.max(...rules.map((r) => r.category.length))
|
|
32
|
+
|
|
33
|
+
for (const rule of rules) {
|
|
34
|
+
const fixLabel = rule.fixable ? " [fixable]" : ""
|
|
35
|
+
const id = rule.id.padEnd(maxId)
|
|
36
|
+
const cat = rule.category.padEnd(maxCat)
|
|
37
|
+
const sev = rule.severity.padEnd(5)
|
|
38
|
+
console.log(` ${id} ${cat} ${sev} ${rule.description}${fixLabel}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`\n ${rules.length} rules total`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CliArgs {
|
|
45
|
+
preset: PresetName
|
|
46
|
+
fix: boolean
|
|
47
|
+
format: "text" | "json" | "compact"
|
|
48
|
+
quiet: boolean
|
|
49
|
+
showList: boolean
|
|
50
|
+
showHelp: boolean
|
|
51
|
+
showVersion: boolean
|
|
52
|
+
watchMode: boolean
|
|
53
|
+
configPath: string | undefined
|
|
54
|
+
ignorePath: string | undefined
|
|
55
|
+
ruleOverrides: Record<string, Severity>
|
|
56
|
+
paths: string[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const BOOLEAN_FLAGS: Record<string, keyof CliArgs> = {
|
|
60
|
+
"--help": "showHelp",
|
|
61
|
+
"-h": "showHelp",
|
|
62
|
+
"--version": "showVersion",
|
|
63
|
+
"-v": "showVersion",
|
|
64
|
+
"--list": "showList",
|
|
65
|
+
"--fix": "fix",
|
|
66
|
+
"--quiet": "quiet",
|
|
67
|
+
"--watch": "watchMode",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseArgs(argv: string[]): CliArgs {
|
|
71
|
+
const result: CliArgs = {
|
|
72
|
+
preset: "recommended",
|
|
73
|
+
fix: false,
|
|
74
|
+
format: "text",
|
|
75
|
+
quiet: false,
|
|
76
|
+
showList: false,
|
|
77
|
+
showHelp: false,
|
|
78
|
+
showVersion: false,
|
|
79
|
+
watchMode: false,
|
|
80
|
+
configPath: undefined,
|
|
81
|
+
ignorePath: undefined,
|
|
82
|
+
ruleOverrides: {},
|
|
83
|
+
paths: [],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < argv.length; i++) {
|
|
87
|
+
const arg = argv[i] as string
|
|
88
|
+
const boolKey = BOOLEAN_FLAGS[arg]
|
|
89
|
+
|
|
90
|
+
if (boolKey) {
|
|
91
|
+
;(result as unknown as Record<string, unknown>)[boolKey] = true
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const consumed = parseValueFlag(arg, argv[i + 1], result)
|
|
96
|
+
i += consumed
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Returns number of extra args consumed (0 or 1). */
|
|
103
|
+
function parseValueFlag(arg: string, nextArg: string | undefined, result: CliArgs): number {
|
|
104
|
+
if (arg === "--preset") {
|
|
105
|
+
result.preset = (nextArg ?? "recommended") as PresetName
|
|
106
|
+
return 1
|
|
107
|
+
}
|
|
108
|
+
if (arg === "--format") {
|
|
109
|
+
result.format = (nextArg ?? "text") as "text" | "json" | "compact"
|
|
110
|
+
return 1
|
|
111
|
+
}
|
|
112
|
+
if (arg === "--config") {
|
|
113
|
+
result.configPath = nextArg
|
|
114
|
+
return 1
|
|
115
|
+
}
|
|
116
|
+
if (arg === "--ignore") {
|
|
117
|
+
result.ignorePath = nextArg
|
|
118
|
+
return 1
|
|
119
|
+
}
|
|
120
|
+
if (arg === "--rule") {
|
|
121
|
+
parseRuleOverride(nextArg, result.ruleOverrides)
|
|
122
|
+
return 1
|
|
123
|
+
}
|
|
124
|
+
if (arg) {
|
|
125
|
+
result.paths.push(arg)
|
|
126
|
+
}
|
|
127
|
+
return 0
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseRuleOverride(val: string | undefined, overrides: Record<string, Severity>): void {
|
|
131
|
+
if (!val) return
|
|
132
|
+
const eqIdx = val.lastIndexOf("=")
|
|
133
|
+
if (eqIdx === -1) return
|
|
134
|
+
const ruleId = val.slice(0, eqIdx)
|
|
135
|
+
const severity = val.slice(eqIdx + 1) as Severity
|
|
136
|
+
overrides[ruleId] = severity
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function main() {
|
|
140
|
+
const args = parseArgs(process.argv.slice(2))
|
|
141
|
+
|
|
142
|
+
if (args.showHelp) {
|
|
143
|
+
printUsage()
|
|
144
|
+
process.exit(0)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (args.showVersion) {
|
|
148
|
+
console.log(`pyreon-lint v${VERSION}`)
|
|
149
|
+
process.exit(0)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (args.showList) {
|
|
153
|
+
printList()
|
|
154
|
+
process.exit(0)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (args.paths.length === 0) {
|
|
158
|
+
args.paths.push(".")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (args.watchMode) {
|
|
162
|
+
watchAndLint({
|
|
163
|
+
paths: args.paths,
|
|
164
|
+
preset: args.preset,
|
|
165
|
+
fix: args.fix,
|
|
166
|
+
quiet: args.quiet,
|
|
167
|
+
ruleOverrides: args.ruleOverrides,
|
|
168
|
+
config: args.configPath,
|
|
169
|
+
ignore: args.ignorePath,
|
|
170
|
+
format: args.format,
|
|
171
|
+
})
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const result = lint({
|
|
176
|
+
paths: args.paths,
|
|
177
|
+
preset: args.preset,
|
|
178
|
+
fix: args.fix,
|
|
179
|
+
quiet: args.quiet,
|
|
180
|
+
ruleOverrides: args.ruleOverrides,
|
|
181
|
+
config: args.configPath,
|
|
182
|
+
ignore: args.ignorePath,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
if (args.format === "json") {
|
|
186
|
+
console.log(formatJSON(result))
|
|
187
|
+
} else if (args.format === "compact") {
|
|
188
|
+
console.log(formatCompact(result))
|
|
189
|
+
} else {
|
|
190
|
+
const output = formatText(result)
|
|
191
|
+
if (output) console.log(output)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (result.totalErrors > 0) {
|
|
195
|
+
process.exit(1)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
main()
|