@pyreon/lint 0.12.12 → 0.12.14

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 (45) hide show
  1. package/README.md +55 -2
  2. package/lib/analysis/cli.js.html +1 -1
  3. package/lib/analysis/index.js.html +1 -1
  4. package/lib/cli.js +960 -162
  5. package/lib/cli.js.map +1 -1
  6. package/lib/index.js +935 -161
  7. package/lib/index.js.map +1 -1
  8. package/lib/types/index.d.ts +96 -23
  9. package/lib/types/index.d.ts.map +1 -1
  10. package/package.json +2 -1
  11. package/schema/pyreonlintrc.schema.json +64 -0
  12. package/src/cli.ts +44 -2
  13. package/src/config/presets.ts +13 -1
  14. package/src/index.ts +7 -0
  15. package/src/lint.ts +37 -6
  16. package/src/lsp/index.ts +15 -2
  17. package/src/rules/architecture/dev-guard-warnings.ts +172 -17
  18. package/src/rules/architecture/no-circular-import.ts +7 -0
  19. package/src/rules/architecture/no-process-dev-gate.ts +18 -45
  20. package/src/rules/architecture/require-browser-smoke-test.ts +227 -0
  21. package/src/rules/form/no-submit-without-validation.ts +9 -0
  22. package/src/rules/form/no-unregistered-field.ts +9 -0
  23. package/src/rules/hooks/no-raw-addeventlistener.ts +7 -0
  24. package/src/rules/hooks/no-raw-localstorage.ts +12 -1
  25. package/src/rules/hooks/no-raw-setinterval.ts +14 -0
  26. package/src/rules/index.ts +4 -1
  27. package/src/rules/jsx/no-props-destructure.ts +20 -6
  28. package/src/rules/lifecycle/no-dom-in-setup.ts +67 -7
  29. package/src/rules/reactivity/no-bare-signal-in-jsx.ts +12 -1
  30. package/src/rules/reactivity/no-unbatched-updates.ts +3 -0
  31. package/src/rules/router/no-imperative-navigate-in-render.ts +131 -35
  32. package/src/rules/ssr/no-window-in-ssr.ts +418 -35
  33. package/src/rules/store/no-duplicate-store-id.ts +11 -0
  34. package/src/rules/store/no-mutate-store-state.ts +11 -1
  35. package/src/rules/styling/no-dynamic-styled.ts +13 -24
  36. package/src/rules/styling/no-theme-outside-provider.ts +34 -2
  37. package/src/runner.ts +100 -10
  38. package/src/tests/runner.test.ts +1573 -21
  39. package/src/types.ts +74 -3
  40. package/src/utils/component-context.ts +106 -0
  41. package/src/utils/exempt-paths.ts +39 -0
  42. package/src/utils/file-roles.ts +32 -0
  43. package/src/utils/imports.ts +4 -1
  44. package/src/utils/validate-options.ts +68 -0
  45. package/src/watcher.ts +17 -0
@@ -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
- type VisitorCallback = (node: any, parent?: any) => void;
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, Severity>;
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, Severity> | undefined;
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): LintFileResult;
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
@@ -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":"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"}
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.12",
3
+ "version": "0.12.14",
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> Override rule severity
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
+ }
@@ -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, sev] of Object.entries(base.rules)) {
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 { LintConfig, LintFileResult, LintOptions, LintResult, RuleMeta } from './types'
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, severity] of Object.entries(fileConfig.rules)) {
102
- config.rules[id] = severity
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
- const config: LintConfig = getPreset('recommended')
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