@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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/lib/analysis/index.js.html +5406 -0
  4. package/lib/index.js +2955 -0
  5. package/lib/index.js.map +1 -0
  6. package/lib/types/index.d.ts +260 -0
  7. package/lib/types/index.d.ts.map +1 -0
  8. package/package.json +56 -0
  9. package/src/cache.ts +51 -0
  10. package/src/cli.ts +199 -0
  11. package/src/config/ignore.ts +159 -0
  12. package/src/config/loader.ts +72 -0
  13. package/src/config/presets.ts +62 -0
  14. package/src/index.ts +40 -0
  15. package/src/lint.ts +226 -0
  16. package/src/reporter.ts +85 -0
  17. package/src/rules/accessibility/dialog-a11y.ts +32 -0
  18. package/src/rules/accessibility/overlay-a11y.ts +33 -0
  19. package/src/rules/accessibility/toast-a11y.ts +38 -0
  20. package/src/rules/architecture/dev-guard-warnings.ts +57 -0
  21. package/src/rules/architecture/no-circular-import.ts +59 -0
  22. package/src/rules/architecture/no-cross-layer-import.ts +75 -0
  23. package/src/rules/architecture/no-deep-import.ts +32 -0
  24. package/src/rules/architecture/no-error-without-prefix.ts +75 -0
  25. package/src/rules/form/no-submit-without-validation.ts +45 -0
  26. package/src/rules/form/no-unregistered-field.ts +45 -0
  27. package/src/rules/form/prefer-field-array.ts +41 -0
  28. package/src/rules/hooks/no-raw-addeventlistener.ts +28 -0
  29. package/src/rules/hooks/no-raw-localstorage.ts +35 -0
  30. package/src/rules/hooks/no-raw-setinterval.ts +41 -0
  31. package/src/rules/index.ts +208 -0
  32. package/src/rules/jsx/no-and-conditional.ts +32 -0
  33. package/src/rules/jsx/no-children-access.ts +44 -0
  34. package/src/rules/jsx/no-classname.ts +27 -0
  35. package/src/rules/jsx/no-htmlfor.ts +27 -0
  36. package/src/rules/jsx/no-index-as-by.ts +70 -0
  37. package/src/rules/jsx/no-map-in-jsx.ts +43 -0
  38. package/src/rules/jsx/no-missing-for-by.ts +27 -0
  39. package/src/rules/jsx/no-onchange.ts +46 -0
  40. package/src/rules/jsx/no-props-destructure.ts +64 -0
  41. package/src/rules/jsx/no-ternary-conditional.ts +32 -0
  42. package/src/rules/jsx/use-by-not-key.ts +33 -0
  43. package/src/rules/lifecycle/no-dom-in-setup.ts +53 -0
  44. package/src/rules/lifecycle/no-effect-in-mount.ts +36 -0
  45. package/src/rules/lifecycle/no-missing-cleanup.ts +80 -0
  46. package/src/rules/lifecycle/no-mount-in-effect.ts +35 -0
  47. package/src/rules/performance/no-eager-import.ts +28 -0
  48. package/src/rules/performance/no-effect-in-for.ts +41 -0
  49. package/src/rules/performance/no-large-for-without-by.ts +28 -0
  50. package/src/rules/performance/prefer-show-over-display.ts +47 -0
  51. package/src/rules/reactivity/no-bare-signal-in-jsx.ts +56 -0
  52. package/src/rules/reactivity/no-effect-assignment.ts +65 -0
  53. package/src/rules/reactivity/no-nested-effect.ts +33 -0
  54. package/src/rules/reactivity/no-peek-in-tracked.ts +35 -0
  55. package/src/rules/reactivity/no-signal-in-loop.ts +59 -0
  56. package/src/rules/reactivity/no-signal-leak.ts +58 -0
  57. package/src/rules/reactivity/no-unbatched-updates.ts +77 -0
  58. package/src/rules/reactivity/prefer-computed.ts +56 -0
  59. package/src/rules/router/index.ts +4 -0
  60. package/src/rules/router/no-href-navigation.ts +51 -0
  61. package/src/rules/router/no-imperative-navigate-in-render.ts +83 -0
  62. package/src/rules/router/no-missing-fallback.ts +87 -0
  63. package/src/rules/router/prefer-use-is-active.ts +45 -0
  64. package/src/rules/ssr/no-mismatch-risk.ts +47 -0
  65. package/src/rules/ssr/no-window-in-ssr.ts +76 -0
  66. package/src/rules/ssr/prefer-request-context.ts +56 -0
  67. package/src/rules/store/no-duplicate-store-id.ts +43 -0
  68. package/src/rules/store/no-mutate-store-state.ts +37 -0
  69. package/src/rules/store/no-store-outside-provider.ts +59 -0
  70. package/src/rules/styling/no-dynamic-styled.ts +60 -0
  71. package/src/rules/styling/no-inline-style-object.ts +30 -0
  72. package/src/rules/styling/no-theme-outside-provider.ts +45 -0
  73. package/src/rules/styling/prefer-cx.ts +44 -0
  74. package/src/runner.ts +170 -0
  75. package/src/tests/runner.test.ts +1043 -0
  76. package/src/types.ts +125 -0
  77. package/src/utils/ast.ts +192 -0
  78. package/src/utils/imports.ts +122 -0
  79. package/src/utils/index.ts +39 -0
  80. package/src/utils/source.ts +36 -0
  81. 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()