@pyreon/lint 0.12.13 → 0.12.15
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/README.md +55 -2
- package/lib/analysis/cli.js.html +1 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/cli.js +960 -162
- package/lib/cli.js.map +1 -1
- package/lib/index.js +935 -161
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +96 -23
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +2 -1
- package/schema/pyreonlintrc.schema.json +64 -0
- package/src/cli.ts +44 -2
- package/src/config/presets.ts +13 -1
- package/src/index.ts +7 -0
- package/src/lint.ts +37 -6
- package/src/lsp/index.ts +15 -2
- package/src/rules/architecture/dev-guard-warnings.ts +172 -17
- package/src/rules/architecture/no-circular-import.ts +7 -0
- package/src/rules/architecture/no-process-dev-gate.ts +18 -45
- package/src/rules/architecture/require-browser-smoke-test.ts +227 -0
- package/src/rules/form/no-submit-without-validation.ts +9 -0
- package/src/rules/form/no-unregistered-field.ts +9 -0
- package/src/rules/hooks/no-raw-addeventlistener.ts +7 -0
- package/src/rules/hooks/no-raw-localstorage.ts +12 -1
- package/src/rules/hooks/no-raw-setinterval.ts +14 -0
- package/src/rules/index.ts +4 -1
- package/src/rules/jsx/no-props-destructure.ts +20 -6
- package/src/rules/lifecycle/no-dom-in-setup.ts +67 -7
- package/src/rules/reactivity/no-bare-signal-in-jsx.ts +12 -1
- package/src/rules/reactivity/no-unbatched-updates.ts +3 -0
- package/src/rules/router/no-imperative-navigate-in-render.ts +131 -35
- package/src/rules/ssr/no-window-in-ssr.ts +418 -35
- package/src/rules/store/no-duplicate-store-id.ts +11 -0
- package/src/rules/store/no-mutate-store-state.ts +11 -1
- package/src/rules/styling/no-dynamic-styled.ts +13 -24
- package/src/rules/styling/no-theme-outside-provider.ts +34 -2
- package/src/runner.ts +100 -10
- package/src/tests/runner.test.ts +1573 -21
- package/src/types.ts +74 -3
- package/src/utils/component-context.ts +106 -0
- package/src/utils/exempt-paths.ts +39 -0
- package/src/utils/file-roles.ts +32 -0
- package/src/utils/imports.ts +4 -1
- package/src/utils/validate-options.ts +68 -0
- package/src/watcher.ts +17 -0
package/lib/types/index.d.ts
CHANGED
|
@@ -21,19 +21,47 @@ interface Diagnostic {
|
|
|
21
21
|
fix?: Fix | undefined;
|
|
22
22
|
}
|
|
23
23
|
type RuleCategory = 'reactivity' | 'jsx' | 'lifecycle' | 'performance' | 'ssr' | 'architecture' | 'store' | 'form' | 'styling' | 'hooks' | 'accessibility' | 'router';
|
|
24
|
+
/**
|
|
25
|
+
* Declared type of an option slot. Minimal on purpose — sufficient for
|
|
26
|
+
* the exemption patterns we actually use. Extend when a rule needs more.
|
|
27
|
+
*/
|
|
28
|
+
type OptionType = 'string' | 'string[]' | 'number' | 'boolean';
|
|
29
|
+
/**
|
|
30
|
+
* Schema for a rule's options bag — keys are option names, values are
|
|
31
|
+
* their declared types. Unknown keys in user config emit a warning;
|
|
32
|
+
* wrong-typed values disable the rule and emit an error. Rules with no
|
|
33
|
+
* schema accept any options (no validation).
|
|
34
|
+
*/
|
|
35
|
+
type RuleOptionsSchema = Record<string, OptionType>;
|
|
24
36
|
interface RuleMeta {
|
|
25
37
|
id: string;
|
|
26
38
|
category: RuleCategory;
|
|
27
39
|
description: string;
|
|
28
40
|
severity: Severity;
|
|
29
41
|
fixable: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Declared options shape. Validated once when a config enables the rule;
|
|
44
|
+
* bad options either get reported (unknown key → warn, wrong type →
|
|
45
|
+
* error + rule disabled for that run).
|
|
46
|
+
*/
|
|
47
|
+
schema?: RuleOptionsSchema;
|
|
30
48
|
}
|
|
49
|
+
type RuleOptions = Record<string, unknown>;
|
|
31
50
|
interface RuleContext {
|
|
32
51
|
report(diagnostic: Omit<Diagnostic, 'ruleId' | 'severity' | 'loc'>): void;
|
|
33
52
|
getSourceText(): string;
|
|
34
53
|
getFilePath(): string;
|
|
54
|
+
/** Options passed via config (tuple form: `[severity, options]`). */
|
|
55
|
+
getOptions(): RuleOptions;
|
|
35
56
|
}
|
|
36
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Visitor callback. oxc's walker only passes the current node — it does NOT
|
|
59
|
+
* pass `parent`. Rules that need parent context must track it via
|
|
60
|
+
* enter/exit depth counters or pre-mark child nodes via WeakSet on the way
|
|
61
|
+
* in. An earlier `parent?: any` signature here was a false promise that
|
|
62
|
+
* silently disabled `parent.type === '…'` checks across multiple rules.
|
|
63
|
+
*/
|
|
64
|
+
type VisitorCallback = (node: any) => void;
|
|
37
65
|
interface VisitorCallbacks {
|
|
38
66
|
[nodeType: string]: VisitorCallback;
|
|
39
67
|
}
|
|
@@ -41,14 +69,23 @@ interface Rule {
|
|
|
41
69
|
meta: RuleMeta;
|
|
42
70
|
create(context: RuleContext): VisitorCallbacks;
|
|
43
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* A rule entry is either a bare severity (`"error"`, `"warn"`, `"info"`,
|
|
74
|
+
* `"off"`) or a tuple `[severity, options]`. The tuple form lets consumers
|
|
75
|
+
* pass per-rule options without a bespoke API per rule.
|
|
76
|
+
*
|
|
77
|
+
* "pyreon/no-window-in-ssr": "error"
|
|
78
|
+
* "pyreon/no-window-in-ssr": ["error", { "exemptPaths": ["packages/core/runtime-dom/"] }]
|
|
79
|
+
*/
|
|
80
|
+
type RuleEntry = Severity | readonly [Severity, RuleOptions];
|
|
44
81
|
interface LintConfig {
|
|
45
|
-
rules: Record<string,
|
|
82
|
+
rules: Record<string, RuleEntry>;
|
|
46
83
|
include?: string[] | undefined;
|
|
47
84
|
exclude?: string[] | undefined;
|
|
48
85
|
}
|
|
49
86
|
interface LintConfigFile {
|
|
50
87
|
preset?: PresetName | undefined;
|
|
51
|
-
rules?: Record<string,
|
|
88
|
+
rules?: Record<string, RuleEntry> | undefined;
|
|
52
89
|
include?: string[] | undefined;
|
|
53
90
|
exclude?: string[] | undefined;
|
|
54
91
|
}
|
|
@@ -58,11 +95,24 @@ interface LintFileResult {
|
|
|
58
95
|
diagnostics: Diagnostic[];
|
|
59
96
|
fixedSource?: string | undefined;
|
|
60
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Config-level diagnostic — emitted by `validateRuleOptions` when a rule's
|
|
100
|
+
* configured options don't match its declared `schema`. Not tied to a
|
|
101
|
+
* source file; lives on `LintResult.configDiagnostics` so programmatic
|
|
102
|
+
* consumers (CI, LSP, JSON reporters) surface them alongside file diags.
|
|
103
|
+
*/
|
|
104
|
+
interface ConfigDiagnostic {
|
|
105
|
+
ruleId: string;
|
|
106
|
+
severity: 'error' | 'warn';
|
|
107
|
+
message: string;
|
|
108
|
+
}
|
|
61
109
|
interface LintResult {
|
|
62
110
|
files: LintFileResult[];
|
|
63
111
|
totalErrors: number;
|
|
64
112
|
totalWarnings: number;
|
|
65
113
|
totalInfos: number;
|
|
114
|
+
/** Config-level diagnostics (malformed rule options, etc.). */
|
|
115
|
+
configDiagnostics: ConfigDiagnostic[];
|
|
66
116
|
}
|
|
67
117
|
interface LintOptions {
|
|
68
118
|
paths: string[];
|
|
@@ -70,6 +120,12 @@ interface LintOptions {
|
|
|
70
120
|
fix?: boolean | undefined;
|
|
71
121
|
quiet?: boolean | undefined;
|
|
72
122
|
ruleOverrides?: Record<string, Severity> | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Per-rule options overrides — typically populated from the
|
|
125
|
+
* `--rule-options id='{json}'` CLI flag. Merged on top of any
|
|
126
|
+
* options coming from the config file's tuple form.
|
|
127
|
+
*/
|
|
128
|
+
ruleOptionsOverrides?: Record<string, RuleOptions> | undefined;
|
|
73
129
|
config?: string | undefined;
|
|
74
130
|
ignore?: string | undefined;
|
|
75
131
|
}
|
|
@@ -212,24 +268,6 @@ declare function formatJSON(result: LintResult): string;
|
|
|
212
268
|
declare function formatCompact(result: LintResult): string;
|
|
213
269
|
//#endregion
|
|
214
270
|
//#region src/lsp/index.d.ts
|
|
215
|
-
/**
|
|
216
|
-
* Minimal LSP server for @pyreon/lint.
|
|
217
|
-
*
|
|
218
|
-
* Provides real-time Pyreon-specific diagnostics in editors that support
|
|
219
|
-
* the Language Server Protocol (VS Code, Neovim, etc.).
|
|
220
|
-
*
|
|
221
|
-
* Usage: pyreon-lint --lsp
|
|
222
|
-
*
|
|
223
|
-
* The server communicates via JSON-RPC over stdin/stdout following the
|
|
224
|
-
* LSP specification (https://microsoft.github.io/language-server-protocol/).
|
|
225
|
-
*
|
|
226
|
-
* Supported capabilities:
|
|
227
|
-
* - textDocument/didOpen — lint on open
|
|
228
|
-
* - textDocument/didSave — lint on save
|
|
229
|
-
* - textDocument/didChange — lint on change (debounced)
|
|
230
|
-
*
|
|
231
|
-
* @module
|
|
232
|
-
*/
|
|
233
271
|
/**
|
|
234
272
|
* Start the LSP server. Reads JSON-RPC messages from stdin,
|
|
235
273
|
* processes them, and writes responses to stdout.
|
|
@@ -249,13 +287,48 @@ declare const allRules: Rule[];
|
|
|
249
287
|
* for (const d of result.diagnostics) console.log(d.message)
|
|
250
288
|
* ```
|
|
251
289
|
*/
|
|
252
|
-
declare function lintFile(filePath: string, sourceText: string, rules: Rule[], config: LintConfig, cache?: AstCache | undefined
|
|
290
|
+
declare function lintFile(filePath: string, sourceText: string, rules: Rule[], config: LintConfig, cache?: AstCache | undefined,
|
|
291
|
+
/**
|
|
292
|
+
* Optional sink for config-level diagnostics (malformed rule options).
|
|
293
|
+
* When provided, diagnostics are appended to it instead of printed to
|
|
294
|
+
* stderr — `lint()` uses this to surface them on `LintResult`.
|
|
295
|
+
*/
|
|
296
|
+
|
|
297
|
+
configDiagnosticsSink?: ConfigDiagnostic[]): LintFileResult;
|
|
253
298
|
/**
|
|
254
299
|
* Apply all auto-fixes to a source text.
|
|
255
300
|
* Fixes are applied in reverse order to maintain correct offsets.
|
|
256
301
|
*/
|
|
257
302
|
declare function applyFixes(sourceText: string, diagnostics: Diagnostic[]): string;
|
|
258
303
|
//#endregion
|
|
304
|
+
//#region src/utils/exempt-paths.d.ts
|
|
305
|
+
declare function isPathExempt(ctx: RuleContext): boolean;
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/utils/file-roles.d.ts
|
|
308
|
+
/**
|
|
309
|
+
* Universal file-path classifiers for lint rules.
|
|
310
|
+
*
|
|
311
|
+
* What belongs here:
|
|
312
|
+
* - Conventions that exist in every project the linter runs on
|
|
313
|
+
* (test files, example directories — the `*.test.*` convention
|
|
314
|
+
* is not Pyreon-specific).
|
|
315
|
+
*
|
|
316
|
+
* What does NOT belong here:
|
|
317
|
+
* - Monorepo-specific paths like `packages/core/runtime-dom/` —
|
|
318
|
+
* those are implementation knowledge of one particular codebase
|
|
319
|
+
* and have no meaning in a user's app. Exemptions for such paths
|
|
320
|
+
* belong in the consuming project's lint config via the
|
|
321
|
+
* `exemptPaths: string[]` rule option — see `utils/exempt-paths.ts`
|
|
322
|
+
* and the Pyreon monorepo's `.pyreonlintrc.json` at repo root for
|
|
323
|
+
* reference.
|
|
324
|
+
*/
|
|
325
|
+
/**
|
|
326
|
+
* Matches files that are tests by convention. Universal — the
|
|
327
|
+
* `*.test.*` / `*.spec.*` / `/tests/` / `/__tests__/` conventions
|
|
328
|
+
* exist in every codebase this linter runs on, not just Pyreon.
|
|
329
|
+
*/
|
|
330
|
+
declare function isTestFile(filePath: string): boolean;
|
|
331
|
+
//#endregion
|
|
259
332
|
//#region src/utils/imports.d.ts
|
|
260
333
|
declare function isPyreonImport(source: string): boolean;
|
|
261
334
|
declare function isPyreonPackage(source: string): boolean;
|
|
@@ -281,5 +354,5 @@ declare function watchAndLint(options: LintOptions & {
|
|
|
281
354
|
format: string;
|
|
282
355
|
}): void;
|
|
283
356
|
//#endregion
|
|
284
|
-
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, startLspServer, watchAndLint };
|
|
357
|
+
export { AstCache, type ConfigDiagnostic, type Diagnostic, type Fix, type ImportInfo, LineIndex, type LintConfig, type LintConfigFile, type LintFileResult, type LintOptions, type LintResult, type OptionType, type PresetName, type Rule, type RuleCategory, type RuleContext, type RuleEntry, type RuleMeta, type RuleOptions, type RuleOptionsSchema, type Severity, type SourceLocation, type Span, type VisitorCallbacks, allRules, applyFixes, createIgnoreFilter, extractImportInfo, formatCompact, formatJSON, formatText, getLocalName, getPreset, importsName, isPathExempt, isPyreonImport, isPyreonPackage, isTestFile, lint, lintFile, listRules, loadConfig, loadConfigFromPath, startLspServer, watchAndLint };
|
|
285
358
|
//# sourceMappingURL=index2.d.ts.map
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +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/lsp/index.ts","../../../src/rules/index.ts","../../../src/runner.ts","../../../src/utils/imports.ts","../../../src/watcher.ts"],"mappings":";KAEY,QAAA;AAAA,UAEK,cAAA;EACf,IAAA;EACA,MAAA;AAAA;AAAA,UAGe,IAAA;EACf,KAAA;EACA,GAAA;AAAA;AAAA,UAGe,GAAA;EACf,IAAA,EAAM,IAAA;EACN,WAAA;AAAA;AAAA,UAGe,UAAA;EACf,MAAA;EACA,QAAA,EAAU,QAAA;EACV,OAAA;EACA,IAAA,EAAM,IAAA;EACN,GAAA,EAAK,cAAA;EACL,GAAA,GAAM,GAAA;AAAA;AAAA,KAKI,YAAA;AAAA,
|
|
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/lsp/index.ts","../../../src/rules/index.ts","../../../src/runner.ts","../../../src/utils/exempt-paths.ts","../../../src/utils/file-roles.ts","../../../src/utils/imports.ts","../../../src/watcher.ts"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/lint",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.15",
|
|
4
4
|
"description": "Pyreon-specific linter — 56 rules for signals, JSX, SSR, performance, router, and architecture",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/lint#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"lib",
|
|
20
20
|
"src",
|
|
21
|
+
"schema",
|
|
21
22
|
"README.md",
|
|
22
23
|
"LICENSE"
|
|
23
24
|
],
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://pyreon.dev/schemas/pyreonlintrc.json",
|
|
4
|
+
"title": "@pyreon/lint configuration",
|
|
5
|
+
"description": "Schema for .pyreonlintrc.json — configures the @pyreon/lint linter. Reference this in your config via `\"$schema\": \"./node_modules/@pyreon/lint/schema/pyreonlintrc.schema.json\"` for IDE autocomplete + validation.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"preset": {
|
|
11
|
+
"description": "Base preset to extend. Recommended is the safe default. Use `lib` for strict mode + architecture rules; `app` to disable lib-only rules.",
|
|
12
|
+
"enum": ["recommended", "strict", "app", "lib"]
|
|
13
|
+
},
|
|
14
|
+
"rules": {
|
|
15
|
+
"description": "Per-rule configuration. Each entry is either a bare severity (`\"error\"` / `\"warn\"` / `\"info\"` / `\"off\"`) or an `[severity, options]` tuple. Rules that support path-based exemption read `options.exemptPaths: string[]`.",
|
|
16
|
+
"type": "object",
|
|
17
|
+
"patternProperties": {
|
|
18
|
+
"^.+$": {
|
|
19
|
+
"oneOf": [
|
|
20
|
+
{ "$ref": "#/definitions/severity" },
|
|
21
|
+
{
|
|
22
|
+
"type": "array",
|
|
23
|
+
"minItems": 1,
|
|
24
|
+
"maxItems": 2,
|
|
25
|
+
"items": [
|
|
26
|
+
{ "$ref": "#/definitions/severity" },
|
|
27
|
+
{ "$ref": "#/definitions/ruleOptions" }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"additionalProperties": false
|
|
34
|
+
},
|
|
35
|
+
"include": {
|
|
36
|
+
"description": "Glob patterns of files to lint (relative to the config file).",
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": { "type": "string" }
|
|
39
|
+
},
|
|
40
|
+
"exclude": {
|
|
41
|
+
"description": "Glob patterns of files to skip (relative to the config file). `.pyreonlintignore` and `.gitignore` are honored automatically.",
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": { "type": "string" }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"definitions": {
|
|
47
|
+
"severity": {
|
|
48
|
+
"enum": ["error", "warn", "info", "off"],
|
|
49
|
+
"description": "Diagnostic severity. `off` disables the rule entirely."
|
|
50
|
+
},
|
|
51
|
+
"ruleOptions": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"description": "Per-rule options. Most rules support `exemptPaths: string[]` for path-based exemption (each entry is a substring matched against the file path).",
|
|
54
|
+
"properties": {
|
|
55
|
+
"exemptPaths": {
|
|
56
|
+
"description": "File-path substrings to exempt from this rule. Useful for foundation packages that legitimately implement what the rule recommends (e.g. a DOM renderer cannot use `useEventListener`).",
|
|
57
|
+
"type": "array",
|
|
58
|
+
"items": { "type": "string" }
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"additionalProperties": true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -18,7 +18,8 @@ function printUsage() {
|
|
|
18
18
|
--format <fmt> Output: text (default), json, compact
|
|
19
19
|
--quiet Only show errors
|
|
20
20
|
--list List all available rules
|
|
21
|
-
--rule <id>=<sev>
|
|
21
|
+
--rule <id>=<sev> Override rule severity (e.g. --rule pyreon/no-window-in-ssr=off)
|
|
22
|
+
--rule-options <id>=<json> Override rule options (e.g. --rule-options pyreon/no-window-in-ssr='{"exemptPaths":["src/foundation/"]}')
|
|
22
23
|
--config <path> Config file path
|
|
23
24
|
--ignore <path> Ignore file path
|
|
24
25
|
--watch Watch mode — re-lint on file changes
|
|
@@ -57,6 +58,8 @@ interface CliArgs {
|
|
|
57
58
|
configPath: string | undefined
|
|
58
59
|
ignorePath: string | undefined
|
|
59
60
|
ruleOverrides: Record<string, Severity>
|
|
61
|
+
/** Per-rule options parsed from `--rule-options id='{json}'`. */
|
|
62
|
+
ruleOptionsOverrides: Record<string, Record<string, unknown>>
|
|
60
63
|
paths: string[]
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -86,6 +89,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
86
89
|
configPath: undefined,
|
|
87
90
|
ignorePath: undefined,
|
|
88
91
|
ruleOverrides: {},
|
|
92
|
+
ruleOptionsOverrides: {},
|
|
89
93
|
paths: [],
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -127,6 +131,10 @@ function parseValueFlag(arg: string, nextArg: string | undefined, result: CliArg
|
|
|
127
131
|
parseRuleOverride(nextArg, result.ruleOverrides)
|
|
128
132
|
return 1
|
|
129
133
|
}
|
|
134
|
+
if (arg === '--rule-options') {
|
|
135
|
+
parseRuleOptionsOverride(nextArg, result.ruleOptionsOverrides)
|
|
136
|
+
return 1
|
|
137
|
+
}
|
|
130
138
|
if (arg) {
|
|
131
139
|
result.paths.push(arg)
|
|
132
140
|
}
|
|
@@ -142,6 +150,32 @@ function parseRuleOverride(val: string | undefined, overrides: Record<string, Se
|
|
|
142
150
|
overrides[ruleId] = severity
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
/** Exported for testing only. */
|
|
154
|
+
export function parseRuleOptionsOverride(
|
|
155
|
+
val: string | undefined,
|
|
156
|
+
overrides: Record<string, Record<string, unknown>>,
|
|
157
|
+
): void {
|
|
158
|
+
if (!val) return
|
|
159
|
+
const eqIdx = val.indexOf('=')
|
|
160
|
+
if (eqIdx === -1) return
|
|
161
|
+
const ruleId = val.slice(0, eqIdx)
|
|
162
|
+
const json = val.slice(eqIdx + 1)
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(json)
|
|
165
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
166
|
+
overrides[ruleId] = parsed as Record<string, unknown>
|
|
167
|
+
} else {
|
|
168
|
+
// oxlint-disable-next-line no-console
|
|
169
|
+
console.error(
|
|
170
|
+
`[pyreon-lint] --rule-options ${ruleId}: expected JSON object, got ${typeof parsed}`,
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
// oxlint-disable-next-line no-console
|
|
175
|
+
console.error(`[pyreon-lint] --rule-options ${ruleId}: invalid JSON — ${(err as Error).message}`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
145
179
|
function main() {
|
|
146
180
|
const args = parseArgs(process.argv.slice(2))
|
|
147
181
|
|
|
@@ -176,6 +210,7 @@ function main() {
|
|
|
176
210
|
fix: args.fix,
|
|
177
211
|
quiet: args.quiet,
|
|
178
212
|
ruleOverrides: args.ruleOverrides,
|
|
213
|
+
ruleOptionsOverrides: args.ruleOptionsOverrides,
|
|
179
214
|
config: args.configPath,
|
|
180
215
|
ignore: args.ignorePath,
|
|
181
216
|
format: args.format,
|
|
@@ -189,6 +224,7 @@ function main() {
|
|
|
189
224
|
fix: args.fix,
|
|
190
225
|
quiet: args.quiet,
|
|
191
226
|
ruleOverrides: args.ruleOverrides,
|
|
227
|
+
ruleOptionsOverrides: args.ruleOptionsOverrides,
|
|
192
228
|
config: args.configPath,
|
|
193
229
|
ignore: args.ignorePath,
|
|
194
230
|
})
|
|
@@ -207,4 +243,10 @@ function main() {
|
|
|
207
243
|
}
|
|
208
244
|
}
|
|
209
245
|
|
|
210
|
-
main()
|
|
246
|
+
// Only invoke `main()` when this module is the entry point. Importing
|
|
247
|
+
// CLI internals from tests must NOT trigger a real lint run +
|
|
248
|
+
// `process.exit`. `import.meta.main === true` under Bun when the file
|
|
249
|
+
// is the script; `undefined` / `false` under static imports.
|
|
250
|
+
if ((import.meta as { main?: boolean }).main === true) {
|
|
251
|
+
main()
|
|
252
|
+
}
|
package/src/config/presets.ts
CHANGED
|
@@ -10,11 +10,18 @@ function buildRecommended(): LintConfig {
|
|
|
10
10
|
return { rules }
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function severityOf(entry: LintConfig['rules'][string]): Severity {
|
|
14
|
+
// Presets are built from bare severities (no tuple form). If a future
|
|
15
|
+
// preset adds tuple form, extract the severity from the tuple.
|
|
16
|
+
return Array.isArray(entry) ? (entry[0] as Severity) : (entry as Severity)
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
/** Build a config where every warn is promoted to error. */
|
|
14
20
|
function buildStrict(): LintConfig {
|
|
15
21
|
const base = buildRecommended()
|
|
16
22
|
const rules: Record<string, Severity> = {}
|
|
17
|
-
for (const [id,
|
|
23
|
+
for (const [id, entry] of Object.entries(base.rules)) {
|
|
24
|
+
const sev = severityOf(entry)
|
|
18
25
|
rules[id] = sev === 'warn' ? 'error' : sev
|
|
19
26
|
}
|
|
20
27
|
return { rules }
|
|
@@ -30,6 +37,10 @@ function buildApp(): LintConfig {
|
|
|
30
37
|
'pyreon/no-error-without-prefix': 'off',
|
|
31
38
|
'pyreon/no-circular-import': 'off',
|
|
32
39
|
'pyreon/no-cross-layer-import': 'off',
|
|
40
|
+
// `require-browser-smoke-test` is a per-package contract that
|
|
41
|
+
// applies to published libraries — apps don't ship as packages
|
|
42
|
+
// with smoke obligations.
|
|
43
|
+
'pyreon/require-browser-smoke-test': 'off',
|
|
33
44
|
// `no-process-dev-gate` stays ON in `app` preset because the bug
|
|
34
45
|
// hits user-facing browser code regardless of whether it's a lib
|
|
35
46
|
// or an app.
|
|
@@ -48,6 +59,7 @@ function buildLib(): LintConfig {
|
|
|
48
59
|
'pyreon/dev-guard-warnings': 'error',
|
|
49
60
|
'pyreon/no-error-without-prefix': 'error',
|
|
50
61
|
'pyreon/no-process-dev-gate': 'error',
|
|
62
|
+
'pyreon/require-browser-smoke-test': 'error',
|
|
51
63
|
},
|
|
52
64
|
}
|
|
53
65
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { allRules } from './rules/index'
|
|
|
12
12
|
export { applyFixes, lintFile } from './runner'
|
|
13
13
|
// Types
|
|
14
14
|
export type {
|
|
15
|
+
ConfigDiagnostic,
|
|
15
16
|
Diagnostic,
|
|
16
17
|
Fix,
|
|
17
18
|
ImportInfo,
|
|
@@ -20,16 +21,22 @@ export type {
|
|
|
20
21
|
LintFileResult,
|
|
21
22
|
LintOptions,
|
|
22
23
|
LintResult,
|
|
24
|
+
OptionType,
|
|
23
25
|
PresetName,
|
|
24
26
|
Rule,
|
|
25
27
|
RuleCategory,
|
|
26
28
|
RuleContext,
|
|
29
|
+
RuleEntry,
|
|
27
30
|
RuleMeta,
|
|
31
|
+
RuleOptions,
|
|
32
|
+
RuleOptionsSchema,
|
|
28
33
|
Severity,
|
|
29
34
|
SourceLocation,
|
|
30
35
|
Span,
|
|
31
36
|
VisitorCallbacks,
|
|
32
37
|
} from './types'
|
|
38
|
+
export { isPathExempt } from './utils/exempt-paths'
|
|
39
|
+
export { isTestFile } from './utils/file-roles'
|
|
33
40
|
export {
|
|
34
41
|
extractImportInfo,
|
|
35
42
|
getLocalName,
|
package/src/lint.ts
CHANGED
|
@@ -6,7 +6,16 @@ import { loadConfig, loadConfigFromPath } from './config/loader'
|
|
|
6
6
|
import { getPreset } from './config/presets'
|
|
7
7
|
import { allRules } from './rules/index'
|
|
8
8
|
import { applyFixes, lintFile } from './runner'
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ConfigDiagnostic,
|
|
11
|
+
LintConfig,
|
|
12
|
+
LintFileResult,
|
|
13
|
+
LintOptions,
|
|
14
|
+
LintResult,
|
|
15
|
+
RuleMeta,
|
|
16
|
+
RuleOptions,
|
|
17
|
+
Severity,
|
|
18
|
+
} from './types'
|
|
10
19
|
import { hasJsExtension } from './utils/index'
|
|
11
20
|
|
|
12
21
|
function isHiddenOrVendor(entry: string): boolean {
|
|
@@ -96,20 +105,40 @@ function buildConfig(options: LintOptions): {
|
|
|
96
105
|
const presetName = options.preset ?? fileConfig?.preset ?? 'recommended'
|
|
97
106
|
const config = getPreset(presetName)
|
|
98
107
|
|
|
99
|
-
// Merge config file rule overrides
|
|
108
|
+
// Merge config file rule overrides. Entries can be a bare severity or a
|
|
109
|
+
// `[severity, options]` tuple — passed through verbatim; the runner
|
|
110
|
+
// normalizes at use-site.
|
|
100
111
|
if (fileConfig?.rules) {
|
|
101
|
-
for (const [id,
|
|
102
|
-
config.rules[id] =
|
|
112
|
+
for (const [id, entry] of Object.entries(fileConfig.rules)) {
|
|
113
|
+
config.rules[id] = entry
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
// CLI rule overrides (highest priority)
|
|
117
|
+
// CLI rule severity overrides (highest priority for severity).
|
|
107
118
|
if (options.ruleOverrides) {
|
|
108
119
|
for (const [id, severity] of Object.entries(options.ruleOverrides)) {
|
|
109
120
|
config.rules[id] = severity
|
|
110
121
|
}
|
|
111
122
|
}
|
|
112
123
|
|
|
124
|
+
// CLI rule options overrides (`--rule-options id='{json}'`). Merged
|
|
125
|
+
// on top of any options already configured for the rule. If the rule
|
|
126
|
+
// is currently bare-severity, we promote it to tuple form using its
|
|
127
|
+
// existing severity (or `recommended` default if not present).
|
|
128
|
+
if (options.ruleOptionsOverrides) {
|
|
129
|
+
for (const [id, optionOverrides] of Object.entries(options.ruleOptionsOverrides)) {
|
|
130
|
+
const existing = config.rules[id]
|
|
131
|
+
const [currentSeverity, currentOptions]: [Severity, RuleOptions] = Array.isArray(existing)
|
|
132
|
+
? [existing[0] as Severity, (existing[1] ?? {}) as RuleOptions]
|
|
133
|
+
: [(existing ?? 'off') as Severity, {}]
|
|
134
|
+
if (currentSeverity === 'off') continue
|
|
135
|
+
config.rules[id] = [
|
|
136
|
+
currentSeverity,
|
|
137
|
+
{ ...currentOptions, ...optionOverrides },
|
|
138
|
+
] as const
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
113
142
|
return {
|
|
114
143
|
config,
|
|
115
144
|
include: fileConfig?.include,
|
|
@@ -175,11 +204,13 @@ export function lint(options: LintOptions): LintResult {
|
|
|
175
204
|
const cache = new AstCache()
|
|
176
205
|
const files = gatherFiles(options.paths, isIgnored, include, exclude)
|
|
177
206
|
|
|
207
|
+
const configDiagnostics: ConfigDiagnostic[] = []
|
|
178
208
|
const results: LintResult = {
|
|
179
209
|
files: [],
|
|
180
210
|
totalErrors: 0,
|
|
181
211
|
totalWarnings: 0,
|
|
182
212
|
totalInfos: 0,
|
|
213
|
+
configDiagnostics,
|
|
183
214
|
}
|
|
184
215
|
|
|
185
216
|
for (const filePath of files) {
|
|
@@ -189,7 +220,7 @@ export function lint(options: LintOptions): LintResult {
|
|
|
189
220
|
} catch {
|
|
190
221
|
continue
|
|
191
222
|
}
|
|
192
|
-
const fileResult = lintFile(filePath, source, allRules, config, cache)
|
|
223
|
+
const fileResult = lintFile(filePath, source, allRules, config, cache, configDiagnostics)
|
|
193
224
|
if (options.fix) {
|
|
194
225
|
applyFixesToFile(fileResult, source)
|
|
195
226
|
}
|
package/src/lsp/index.ts
CHANGED
|
@@ -20,11 +20,24 @@
|
|
|
20
20
|
import { AstCache } from '../cache'
|
|
21
21
|
import { getPreset } from '../config/presets'
|
|
22
22
|
import { allRules } from '../rules/index'
|
|
23
|
-
import { lintFile } from '../runner'
|
|
23
|
+
import { _resetConfigDiagnosticsCache, lintFile } from '../runner'
|
|
24
24
|
import type { Diagnostic, LintConfig } from '../types'
|
|
25
25
|
|
|
26
26
|
const cache = new AstCache()
|
|
27
|
-
|
|
27
|
+
let config: LintConfig = getPreset('recommended')
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reload the LSP's lint config. Resets the runner's per-process
|
|
31
|
+
* validation cache so newly-configured options are re-validated against
|
|
32
|
+
* rule schemas on the next lint pass — needed when the user edits
|
|
33
|
+
* `.pyreonlintrc.json` mid-session. Future hookup point for an LSP
|
|
34
|
+
* `workspace/didChangeConfiguration` notification.
|
|
35
|
+
*/
|
|
36
|
+
export function _reloadConfig(next: LintConfig): void {
|
|
37
|
+
config = next
|
|
38
|
+
_resetConfigDiagnosticsCache()
|
|
39
|
+
cache.clear()
|
|
40
|
+
}
|
|
28
41
|
|
|
29
42
|
// ─── JSON-RPC message types ────────────────────────────────────────────────
|
|
30
43
|
|