@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.
- 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/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
2
|
+
import path, { dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { Visitor, parseSync } from "oxc-parser";
|
|
4
4
|
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error("Calling `require` for \"" + x + "\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
5
12
|
//#region src/cache.ts
|
|
6
13
|
/**
|
|
7
14
|
* Simple in-memory cache for parsed ASTs keyed by file content hash.
|
|
@@ -248,7 +255,6 @@ const BROWSER_GLOBALS = new Set([
|
|
|
248
255
|
"localStorage",
|
|
249
256
|
"sessionStorage",
|
|
250
257
|
"indexedDB",
|
|
251
|
-
"fetch",
|
|
252
258
|
"XMLHttpRequest",
|
|
253
259
|
"WebSocket",
|
|
254
260
|
"requestAnimationFrame",
|
|
@@ -450,6 +456,44 @@ const toastA11y = {
|
|
|
450
456
|
}
|
|
451
457
|
};
|
|
452
458
|
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/utils/exempt-paths.ts
|
|
461
|
+
function isPathExempt(ctx) {
|
|
462
|
+
const raw = ctx.getOptions().exemptPaths;
|
|
463
|
+
if (!Array.isArray(raw) || raw.length === 0) return false;
|
|
464
|
+
const filePath = ctx.getFilePath();
|
|
465
|
+
for (const entry of raw) if (typeof entry === "string" && entry.length > 0 && filePath.includes(entry)) return true;
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/utils/file-roles.ts
|
|
471
|
+
/**
|
|
472
|
+
* Universal file-path classifiers for lint rules.
|
|
473
|
+
*
|
|
474
|
+
* What belongs here:
|
|
475
|
+
* - Conventions that exist in every project the linter runs on
|
|
476
|
+
* (test files, example directories — the `*.test.*` convention
|
|
477
|
+
* is not Pyreon-specific).
|
|
478
|
+
*
|
|
479
|
+
* What does NOT belong here:
|
|
480
|
+
* - Monorepo-specific paths like `packages/core/runtime-dom/` —
|
|
481
|
+
* those are implementation knowledge of one particular codebase
|
|
482
|
+
* and have no meaning in a user's app. Exemptions for such paths
|
|
483
|
+
* belong in the consuming project's lint config via the
|
|
484
|
+
* `exemptPaths: string[]` rule option — see `utils/exempt-paths.ts`
|
|
485
|
+
* and the Pyreon monorepo's `.pyreonlintrc.json` at repo root for
|
|
486
|
+
* reference.
|
|
487
|
+
*/
|
|
488
|
+
/**
|
|
489
|
+
* Matches files that are tests by convention. Universal — the
|
|
490
|
+
* `*.test.*` / `*.spec.*` / `/tests/` / `/__tests__/` conventions
|
|
491
|
+
* exist in every codebase this linter runs on, not just Pyreon.
|
|
492
|
+
*/
|
|
493
|
+
function isTestFile(filePath) {
|
|
494
|
+
return filePath.includes("/tests/") || filePath.includes("/test/") || filePath.includes("/__tests__/") || filePath.includes(".test.") || filePath.includes(".spec.");
|
|
495
|
+
}
|
|
496
|
+
|
|
453
497
|
//#endregion
|
|
454
498
|
//#region src/rules/architecture/dev-guard-warnings.ts
|
|
455
499
|
const devGuardWarnings = {
|
|
@@ -458,26 +502,122 @@ const devGuardWarnings = {
|
|
|
458
502
|
category: "architecture",
|
|
459
503
|
description: "Require console.warn/error calls to be wrapped in `if (__DEV__)` guards.",
|
|
460
504
|
severity: "error",
|
|
461
|
-
fixable: false
|
|
505
|
+
fixable: false,
|
|
506
|
+
schema: {
|
|
507
|
+
exemptPaths: "string[]",
|
|
508
|
+
devFlagNames: "string[]"
|
|
509
|
+
}
|
|
462
510
|
},
|
|
463
511
|
create(context) {
|
|
464
|
-
|
|
465
|
-
if (
|
|
512
|
+
if (isTestFile(context.getFilePath())) return {};
|
|
513
|
+
if (isPathExempt(context)) return {};
|
|
514
|
+
const userFlagNames = context.getOptions().devFlagNames;
|
|
515
|
+
const extraFlagNames = Array.isArray(userFlagNames) ? userFlagNames.filter((n) => typeof n === "string") : [];
|
|
516
|
+
const devFlagBoundConsts = /* @__PURE__ */ new Set();
|
|
517
|
+
function exprResolvesToDevFlag(expr) {
|
|
518
|
+
if (!expr) return false;
|
|
519
|
+
if (expr.type === "ChainExpression") return exprResolvesToDevFlag(expr.expression);
|
|
520
|
+
if (isDevFlag(expr)) return true;
|
|
521
|
+
if (expr.type === "BinaryExpression" && (expr.operator === "===" || expr.operator === "==")) return exprResolvesToDevFlag(expr.left) || exprResolvesToDevFlag(expr.right);
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
const DEV_FLAG_NAMES = new Set([
|
|
525
|
+
"__DEV__",
|
|
526
|
+
"IS_DEV",
|
|
527
|
+
"IS_DEVELOPMENT",
|
|
528
|
+
"isDev",
|
|
529
|
+
...extraFlagNames
|
|
530
|
+
]);
|
|
531
|
+
function isDevFlag(node) {
|
|
532
|
+
if (!node) return false;
|
|
533
|
+
if (node.type === "ChainExpression") return isDevFlag(node.expression);
|
|
534
|
+
if (node.type === "Identifier" && DEV_FLAG_NAMES.has(node.name)) return true;
|
|
535
|
+
if (node.type === "Identifier" && devFlagBoundConsts.has(node.name)) return true;
|
|
536
|
+
if (node.type === "MemberExpression" && node.property?.type === "Identifier" && node.property.name === "DEV") {
|
|
537
|
+
const obj = node.object;
|
|
538
|
+
if (obj?.type === "MemberExpression" && obj.property?.type === "Identifier" && obj.property.name === "env" && obj.object?.type === "MetaProperty") return true;
|
|
539
|
+
}
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
function containsDevGuard(test) {
|
|
543
|
+
if (!test) return false;
|
|
544
|
+
if (isDevFlag(test)) return true;
|
|
545
|
+
if (test.type === "LogicalExpression" && test.operator === "&&") return containsDevGuard(test.left) || containsDevGuard(test.right);
|
|
546
|
+
if (test.type === "BinaryExpression" && (test.operator === "===" || test.operator === "==")) return isDevFlag(test.left) || isDevFlag(test.right);
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
function isEarlyReturnDevGuard(node) {
|
|
550
|
+
if (!node || node.type !== "IfStatement") return false;
|
|
551
|
+
const t = node.test;
|
|
552
|
+
const arg = t?.type === "UnaryExpression" && t.operator === "!" ? t.argument : null;
|
|
553
|
+
if (!arg) return false;
|
|
554
|
+
if (!isDevFlag(arg)) return false;
|
|
555
|
+
const c = node.consequent;
|
|
556
|
+
if (c?.type === "ReturnStatement") return true;
|
|
557
|
+
if (c?.type === "BlockStatement" && c.body.length === 1 && c.body[0]?.type === "ReturnStatement") return true;
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
466
560
|
let devGuardDepth = 0;
|
|
561
|
+
let catchDepth = 0;
|
|
562
|
+
const funcGuardStack = [];
|
|
563
|
+
function enterFunction(node) {
|
|
564
|
+
const body = node?.body;
|
|
565
|
+
const stmts = body?.type === "BlockStatement" ? body.body : null;
|
|
566
|
+
let guarded = 0;
|
|
567
|
+
if (stmts && stmts.length > 0 && isEarlyReturnDevGuard(stmts[0])) {
|
|
568
|
+
guarded = 1;
|
|
569
|
+
devGuardDepth++;
|
|
570
|
+
}
|
|
571
|
+
funcGuardStack.push(guarded);
|
|
572
|
+
}
|
|
573
|
+
function exitFunction() {
|
|
574
|
+
const g = funcGuardStack.pop() ?? 0;
|
|
575
|
+
if (g > 0) devGuardDepth -= g;
|
|
576
|
+
}
|
|
467
577
|
return {
|
|
578
|
+
VariableDeclaration(node) {
|
|
579
|
+
for (const decl of node.declarations ?? []) if (decl.id?.type === "Identifier" && exprResolvesToDevFlag(decl.init)) devFlagBoundConsts.add(decl.id.name);
|
|
580
|
+
},
|
|
581
|
+
FunctionDeclaration: enterFunction,
|
|
582
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
583
|
+
FunctionExpression: enterFunction,
|
|
584
|
+
"FunctionExpression:exit": exitFunction,
|
|
585
|
+
ArrowFunctionExpression: enterFunction,
|
|
586
|
+
"ArrowFunctionExpression:exit": exitFunction,
|
|
468
587
|
IfStatement(node) {
|
|
469
|
-
if (node.test
|
|
588
|
+
if (containsDevGuard(node.test)) devGuardDepth++;
|
|
470
589
|
},
|
|
471
590
|
"IfStatement:exit"(node) {
|
|
472
|
-
if (node.test
|
|
591
|
+
if (containsDevGuard(node.test)) devGuardDepth--;
|
|
592
|
+
},
|
|
593
|
+
LogicalExpression(node) {
|
|
594
|
+
if (node.operator === "&&" && containsDevGuard(node.left)) devGuardDepth++;
|
|
595
|
+
},
|
|
596
|
+
"LogicalExpression:exit"(node) {
|
|
597
|
+
if (node.operator === "&&" && containsDevGuard(node.left)) devGuardDepth--;
|
|
598
|
+
},
|
|
599
|
+
ConditionalExpression(node) {
|
|
600
|
+
if (containsDevGuard(node.test)) devGuardDepth++;
|
|
601
|
+
},
|
|
602
|
+
"ConditionalExpression:exit"(node) {
|
|
603
|
+
if (containsDevGuard(node.test)) devGuardDepth--;
|
|
604
|
+
},
|
|
605
|
+
CatchClause() {
|
|
606
|
+
catchDepth++;
|
|
607
|
+
},
|
|
608
|
+
"CatchClause:exit"() {
|
|
609
|
+
catchDepth--;
|
|
473
610
|
},
|
|
474
611
|
CallExpression(node) {
|
|
475
612
|
if (devGuardDepth > 0) return;
|
|
476
613
|
const callee = node.callee;
|
|
477
|
-
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "console" && callee.property?.type === "Identifier" && (callee.property.name === "warn" || callee.property.name === "error"))
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
614
|
+
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "console" && callee.property?.type === "Identifier" && (callee.property.name === "warn" || callee.property.name === "error")) {
|
|
615
|
+
if (callee.property.name === "error" && catchDepth > 0) return;
|
|
616
|
+
context.report({
|
|
617
|
+
message: `\`console.${callee.property.name}()\` without \`__DEV__\` guard — dev warnings must be tree-shakeable in production. Wrap in \`if (__DEV__) { ... }\` (or \`__DEV__ && ...\`). Production error logging in \`catch\` blocks is exempt for \`console.error\`.`,
|
|
618
|
+
span: getSpan(node)
|
|
619
|
+
});
|
|
620
|
+
}
|
|
481
621
|
}
|
|
482
622
|
};
|
|
483
623
|
}
|
|
@@ -514,7 +654,9 @@ const noCircularImport = {
|
|
|
514
654
|
fixable: false
|
|
515
655
|
},
|
|
516
656
|
create(context) {
|
|
517
|
-
const
|
|
657
|
+
const filePath = context.getFilePath();
|
|
658
|
+
if (isTestFile(filePath)) return {};
|
|
659
|
+
const fileLayer = getFileLayer(filePath);
|
|
518
660
|
if (fileLayer === null) return {};
|
|
519
661
|
return { ImportDeclaration(node) {
|
|
520
662
|
const source = node.source?.value;
|
|
@@ -704,34 +846,19 @@ const noErrorWithoutPrefix = {
|
|
|
704
846
|
* Does NOT delete the const declaration — that has to happen by hand because
|
|
705
847
|
* the variable name and downstream usages may need updating in callers.
|
|
706
848
|
*
|
|
707
|
-
* **Server-
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
849
|
+
* **Server-only exemption**: projects configure `exemptPaths` per-file for
|
|
850
|
+
* server-only code (Node environments where `process` is always defined and
|
|
851
|
+
* the pattern is correct). Configure in `.pyreonlintrc.json`:
|
|
852
|
+
*
|
|
853
|
+
* {
|
|
854
|
+
* "rules": {
|
|
855
|
+
* "pyreon/no-process-dev-gate": [
|
|
856
|
+
* "error",
|
|
857
|
+
* { "exemptPaths": ["packages/zero/", "packages/core/server/"] }
|
|
858
|
+
* ]
|
|
859
|
+
* }
|
|
860
|
+
* }
|
|
719
861
|
*/
|
|
720
|
-
const SERVER_PACKAGE_PATTERNS = [
|
|
721
|
-
"packages/zero/",
|
|
722
|
-
"packages/core/server/",
|
|
723
|
-
"packages/core/runtime-server/",
|
|
724
|
-
"packages/tools/vite-plugin/",
|
|
725
|
-
"packages/tools/cli/",
|
|
726
|
-
"packages/tools/lint/",
|
|
727
|
-
"packages/tools/mcp/",
|
|
728
|
-
"packages/tools/storybook/",
|
|
729
|
-
"packages/tools/typescript/",
|
|
730
|
-
"scripts/"
|
|
731
|
-
];
|
|
732
|
-
function isServerOnlyFile(filePath) {
|
|
733
|
-
return SERVER_PACKAGE_PATTERNS.some((pat) => filePath.includes(pat));
|
|
734
|
-
}
|
|
735
862
|
const noProcessDevGate = {
|
|
736
863
|
meta: {
|
|
737
864
|
id: "pyreon/no-process-dev-gate",
|
|
@@ -741,9 +868,8 @@ const noProcessDevGate = {
|
|
|
741
868
|
fixable: true
|
|
742
869
|
},
|
|
743
870
|
create(context) {
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
if (isServerOnlyFile(filePath)) return {};
|
|
871
|
+
if (isTestFile(context.getFilePath())) return {};
|
|
872
|
+
if (isPathExempt(context)) return {};
|
|
747
873
|
/**
|
|
748
874
|
* Match the broken pattern at the AST level. We're looking for any
|
|
749
875
|
* `LogicalExpression` whose two sides are:
|
|
@@ -800,6 +926,185 @@ const noProcessDevGate = {
|
|
|
800
926
|
}
|
|
801
927
|
};
|
|
802
928
|
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/rules/architecture/require-browser-smoke-test.ts
|
|
931
|
+
/**
|
|
932
|
+
* `pyreon/require-browser-smoke-test` — every browser-categorized package
|
|
933
|
+
* must ship at least one `*.browser.test.{ts,tsx}` file under `src/`.
|
|
934
|
+
*
|
|
935
|
+
* Locks in the durability of the T1.1 browser smoke harness (PRs #224,
|
|
936
|
+
* #227, #229, #231). Without this rule, any new browser-running package
|
|
937
|
+
* can quietly ship without a real-browser smoke test and we drift back
|
|
938
|
+
* to the world before T1.1 — where happy-dom silently masks
|
|
939
|
+
* environment-divergence bugs (PR #197 mock-vnode metadata drop, PR
|
|
940
|
+
* #200 `typeof process` dead code, the multi-word event delegation bug
|
|
941
|
+
* fixed alongside PR #231).
|
|
942
|
+
*
|
|
943
|
+
* **What it checks**: when linting a package's `src/index.ts`, the rule
|
|
944
|
+
* looks at the package directory for any file matching
|
|
945
|
+
* `**\/*.browser.test.{ts,tsx}`. If none are found AND the package's
|
|
946
|
+
* name appears in the browser-categorized list, the rule reports an
|
|
947
|
+
* error on `src/index.ts`.
|
|
948
|
+
*
|
|
949
|
+
* **Why src/index.ts only**: the rule needs to fire exactly once per
|
|
950
|
+
* package, not per file. `src/index.ts` is a stable per-package entry
|
|
951
|
+
* point. Files inside the package are not browser-test files
|
|
952
|
+
* themselves, so they get skipped via the path check.
|
|
953
|
+
*
|
|
954
|
+
* **Default browser packages list**: matches the categorization in
|
|
955
|
+
* `.claude/rules/test-environment-parity.md`. Override via the
|
|
956
|
+
* `additionalPackages` option to opt in new packages, or via
|
|
957
|
+
* `exemptPaths` to opt out (e.g. for a brand-new package still under
|
|
958
|
+
* construction).
|
|
959
|
+
*
|
|
960
|
+
* @example Configuration in `.pyreonlintrc.json`
|
|
961
|
+
* ```json
|
|
962
|
+
* {
|
|
963
|
+
* "rules": {
|
|
964
|
+
* "pyreon/require-browser-smoke-test": [
|
|
965
|
+
* "error",
|
|
966
|
+
* {
|
|
967
|
+
* "additionalPackages": ["@my-org/my-browser-pkg"],
|
|
968
|
+
* "exemptPaths": ["packages/experimental/"]
|
|
969
|
+
* }
|
|
970
|
+
* ]
|
|
971
|
+
* }
|
|
972
|
+
* }
|
|
973
|
+
* ```
|
|
974
|
+
*
|
|
975
|
+
* **Known limitation — file existence, not test quality.** The rule only
|
|
976
|
+
* checks that at least one `*.browser.test.*` file exists under `src/`;
|
|
977
|
+
* it cannot assess whether the test is meaningful. A package could ship
|
|
978
|
+
* `sanity.browser.test.ts` with `expect(1).toBe(1)` and satisfy the
|
|
979
|
+
* rule. That's accepted by design — the rule is a *gate* against
|
|
980
|
+
* packages shipping with zero smoke coverage, not a quality check.
|
|
981
|
+
* Review the actual test contents on PR. If drive-by one-liner tests
|
|
982
|
+
* become a pattern, add a per-package coverage threshold or a
|
|
983
|
+
* complementary rule that inspects test file contents.
|
|
984
|
+
*/
|
|
985
|
+
/**
|
|
986
|
+
* Single source of truth for browser-categorized packages lives at
|
|
987
|
+
* `.claude/rules/browser-packages.json`. Loading it lazily here means:
|
|
988
|
+
*
|
|
989
|
+
* 1. Updating the list never requires re-publishing `@pyreon/lint`.
|
|
990
|
+
* 2. The script `scripts/check-browser-smoke.ts` + the human-readable
|
|
991
|
+
* `.claude/rules/test-environment-parity.md` share the same source,
|
|
992
|
+
* so they can't drift out of sync silently.
|
|
993
|
+
*
|
|
994
|
+
* The JSON is searched for by walking up from the linted file's directory
|
|
995
|
+
* to the first ancestor containing `.claude/rules/browser-packages.json`.
|
|
996
|
+
* If not found (rule running in a consumer repo that doesn't ship the
|
|
997
|
+
* JSON), the rule falls back to an empty list — `additionalPackages`
|
|
998
|
+
* becomes the only signal and the rule stays opt-in, not a footgun.
|
|
999
|
+
*
|
|
1000
|
+
* Cached globally because the list is tiny and lint runs lint thousands
|
|
1001
|
+
* of files per invocation.
|
|
1002
|
+
*/
|
|
1003
|
+
let _cachedBrowserPackages = null;
|
|
1004
|
+
function loadBrowserPackages(fromFile) {
|
|
1005
|
+
if (_cachedBrowserPackages) return _cachedBrowserPackages;
|
|
1006
|
+
let dir = path.dirname(fromFile);
|
|
1007
|
+
for (let i = 0; i < 30; i++) {
|
|
1008
|
+
const candidate = path.join(dir, ".claude", "rules", "browser-packages.json");
|
|
1009
|
+
if (existsSync(candidate)) {
|
|
1010
|
+
try {
|
|
1011
|
+
const fs = __require("node:fs");
|
|
1012
|
+
const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
1013
|
+
if (Array.isArray(parsed.packages)) {
|
|
1014
|
+
_cachedBrowserPackages = new Set(parsed.packages.filter((p) => typeof p === "string"));
|
|
1015
|
+
return _cachedBrowserPackages;
|
|
1016
|
+
}
|
|
1017
|
+
} catch {}
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
const parent = path.dirname(dir);
|
|
1021
|
+
if (parent === dir) break;
|
|
1022
|
+
dir = parent;
|
|
1023
|
+
}
|
|
1024
|
+
_cachedBrowserPackages = /* @__PURE__ */ new Set();
|
|
1025
|
+
return _cachedBrowserPackages;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Walk a directory looking for `*.browser.test.{ts,tsx}` files. Bails
|
|
1029
|
+
* on the first match — we only need to know `at least one exists`,
|
|
1030
|
+
* not enumerate them. Skips `node_modules`, `lib`, `dist`, and dot
|
|
1031
|
+
* directories so a package's own dependencies don't pollute the check.
|
|
1032
|
+
*/
|
|
1033
|
+
function hasBrowserTest(dir) {
|
|
1034
|
+
let entries;
|
|
1035
|
+
try {
|
|
1036
|
+
entries = readdirSync(dir);
|
|
1037
|
+
} catch {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
for (const name of entries) {
|
|
1041
|
+
if (name.startsWith(".") || name === "node_modules" || name === "lib" || name === "dist") continue;
|
|
1042
|
+
const full = path.join(dir, name);
|
|
1043
|
+
let isDir = false;
|
|
1044
|
+
try {
|
|
1045
|
+
isDir = statSync(full).isDirectory();
|
|
1046
|
+
} catch {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (isDir) {
|
|
1050
|
+
if (hasBrowserTest(full)) return true;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (/\.browser\.test\.(?:ts|tsx)$/.test(name)) return true;
|
|
1054
|
+
}
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Read the package.json `name` field for the directory containing the
|
|
1059
|
+
* given src/index.ts file. Returns null if not found.
|
|
1060
|
+
*/
|
|
1061
|
+
function readPackageName(srcIndexPath) {
|
|
1062
|
+
const pkgPath = path.resolve(path.dirname(srcIndexPath), "..", "package.json");
|
|
1063
|
+
if (!existsSync(pkgPath)) return null;
|
|
1064
|
+
try {
|
|
1065
|
+
const text = __require("node:fs").readFileSync(pkgPath, "utf8");
|
|
1066
|
+
const parsed = JSON.parse(text);
|
|
1067
|
+
return typeof parsed.name === "string" ? parsed.name : null;
|
|
1068
|
+
} catch {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const requireBrowserSmokeTest = {
|
|
1073
|
+
meta: {
|
|
1074
|
+
id: "pyreon/require-browser-smoke-test",
|
|
1075
|
+
category: "architecture",
|
|
1076
|
+
description: "Every browser-categorized package must ship at least one `*.browser.test.{ts,tsx}` file under `src/`. Locks in the T1.1 browser smoke harness.",
|
|
1077
|
+
severity: "error",
|
|
1078
|
+
fixable: false,
|
|
1079
|
+
schema: {
|
|
1080
|
+
additionalPackages: "string[]",
|
|
1081
|
+
exemptPaths: "string[]"
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
create(context) {
|
|
1085
|
+
const filePath = context.getFilePath();
|
|
1086
|
+
if (!filePath.endsWith("/src/index.ts") && !filePath.endsWith("/src/index.tsx")) return {};
|
|
1087
|
+
if (isPathExempt(context)) return {};
|
|
1088
|
+
const pkgName = readPackageName(filePath);
|
|
1089
|
+
if (pkgName == null) return {};
|
|
1090
|
+
const options = context.getOptions();
|
|
1091
|
+
const additional = Array.isArray(options.additionalPackages) ? options.additionalPackages.filter((s) => typeof s === "string") : [];
|
|
1092
|
+
const browserPackages = new Set(loadBrowserPackages(filePath));
|
|
1093
|
+
for (const p of additional) browserPackages.add(p);
|
|
1094
|
+
if (!browserPackages.has(pkgName)) return {};
|
|
1095
|
+
if (hasBrowserTest(path.dirname(path.dirname(filePath)))) return {};
|
|
1096
|
+
return { "Program:exit"(node) {
|
|
1097
|
+
context.report({
|
|
1098
|
+
message: `[Pyreon] Browser-categorized package "${pkgName}" has no \`*.browser.test.{ts,tsx}\` file. Add at least one real-browser smoke test under \`src/\` to catch environment-divergence bugs that happy-dom hides (typeof process dead code, real pointer events, computed styles, etc.). See .claude/rules/test-environment-parity.md for the recipe.`,
|
|
1099
|
+
span: {
|
|
1100
|
+
start: node.start ?? 0,
|
|
1101
|
+
end: node.end ?? 0
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
} };
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
803
1108
|
//#endregion
|
|
804
1109
|
//#region src/rules/form/no-submit-without-validation.ts
|
|
805
1110
|
const noSubmitWithoutValidation = {
|
|
@@ -811,6 +1116,7 @@ const noSubmitWithoutValidation = {
|
|
|
811
1116
|
fixable: false
|
|
812
1117
|
},
|
|
813
1118
|
create(context) {
|
|
1119
|
+
if (isTestFile(context.getFilePath())) return {};
|
|
814
1120
|
return { CallExpression(node) {
|
|
815
1121
|
if (!isCallTo(node, "useForm")) return;
|
|
816
1122
|
const args = node.arguments;
|
|
@@ -846,6 +1152,7 @@ const noUnregisteredField = {
|
|
|
846
1152
|
fixable: false
|
|
847
1153
|
},
|
|
848
1154
|
create(context) {
|
|
1155
|
+
if (isTestFile(context.getFilePath())) return {};
|
|
849
1156
|
const fieldDecls = /* @__PURE__ */ new Map();
|
|
850
1157
|
const registeredNames = /* @__PURE__ */ new Set();
|
|
851
1158
|
return {
|
|
@@ -911,9 +1218,11 @@ const noRawAddEventListener = {
|
|
|
911
1218
|
category: "hooks",
|
|
912
1219
|
description: "Suggest useEventListener() instead of raw .addEventListener() calls.",
|
|
913
1220
|
severity: "info",
|
|
914
|
-
fixable: false
|
|
1221
|
+
fixable: false,
|
|
1222
|
+
schema: { exemptPaths: "string[]" }
|
|
915
1223
|
},
|
|
916
1224
|
create(context) {
|
|
1225
|
+
if (isPathExempt(context)) return {};
|
|
917
1226
|
return { CallExpression(node) {
|
|
918
1227
|
const callee = node.callee;
|
|
919
1228
|
if (!callee || callee.type !== "MemberExpression") return;
|
|
@@ -926,6 +1235,41 @@ const noRawAddEventListener = {
|
|
|
926
1235
|
}
|
|
927
1236
|
};
|
|
928
1237
|
|
|
1238
|
+
//#endregion
|
|
1239
|
+
//#region src/utils/component-context.ts
|
|
1240
|
+
const COMPONENT_NAME = /^[A-Z]/;
|
|
1241
|
+
const HOOK_NAME$1 = /^use[A-Z]/;
|
|
1242
|
+
function isComponentOrHookName(name) {
|
|
1243
|
+
if (!name) return false;
|
|
1244
|
+
return COMPONENT_NAME.test(name) || HOOK_NAME$1.test(name);
|
|
1245
|
+
}
|
|
1246
|
+
function createComponentContextTracker() {
|
|
1247
|
+
let depth = 0;
|
|
1248
|
+
function declaratorIsComponentOrHook(node) {
|
|
1249
|
+
if (node?.id?.type !== "Identifier") return false;
|
|
1250
|
+
const init = node.init;
|
|
1251
|
+
if (init?.type !== "ArrowFunctionExpression" && init?.type !== "FunctionExpression") return false;
|
|
1252
|
+
return isComponentOrHookName(node.id.name);
|
|
1253
|
+
}
|
|
1254
|
+
return {
|
|
1255
|
+
isInComponentOrHook: () => depth > 0,
|
|
1256
|
+
callbacks: {
|
|
1257
|
+
FunctionDeclaration(node) {
|
|
1258
|
+
if (isComponentOrHookName(node.id?.name)) depth++;
|
|
1259
|
+
},
|
|
1260
|
+
"FunctionDeclaration:exit"(node) {
|
|
1261
|
+
if (isComponentOrHookName(node.id?.name)) depth--;
|
|
1262
|
+
},
|
|
1263
|
+
VariableDeclarator(node) {
|
|
1264
|
+
if (declaratorIsComponentOrHook(node)) depth++;
|
|
1265
|
+
},
|
|
1266
|
+
"VariableDeclarator:exit"(node) {
|
|
1267
|
+
if (declaratorIsComponentOrHook(node)) depth--;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
929
1273
|
//#endregion
|
|
930
1274
|
//#region src/rules/hooks/no-raw-localstorage.ts
|
|
931
1275
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
@@ -938,19 +1282,24 @@ const noRawLocalStorage = {
|
|
|
938
1282
|
meta: {
|
|
939
1283
|
id: "pyreon/no-raw-localstorage",
|
|
940
1284
|
category: "hooks",
|
|
941
|
-
description: "Suggest useStorage() instead of raw localStorage/sessionStorage
|
|
1285
|
+
description: "Suggest useStorage() instead of raw localStorage/sessionStorage inside a component or hook.",
|
|
942
1286
|
severity: "info",
|
|
943
1287
|
fixable: false
|
|
944
1288
|
},
|
|
945
1289
|
create(context) {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1290
|
+
const ctx = createComponentContextTracker();
|
|
1291
|
+
return {
|
|
1292
|
+
...ctx.callbacks,
|
|
1293
|
+
CallExpression(node) {
|
|
1294
|
+
if (!ctx.isInComponentOrHook()) return;
|
|
1295
|
+
const callee = node.callee;
|
|
1296
|
+
if (!callee || callee.type !== "MemberExpression") return;
|
|
1297
|
+
if (callee.object?.type === "Identifier" && STORAGE_OBJECTS.has(callee.object.name) && callee.property?.type === "Identifier" && STORAGE_METHODS.has(callee.property.name)) context.report({
|
|
1298
|
+
message: `Raw \`${callee.object.name}.${callee.property.name}()\` — consider using \`useStorage()\` from \`@pyreon/storage\` for reactive, cross-tab synced storage.`,
|
|
1299
|
+
span: getSpan(node)
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
954
1303
|
}
|
|
955
1304
|
};
|
|
956
1305
|
|
|
@@ -963,14 +1312,19 @@ const noRawSetInterval = {
|
|
|
963
1312
|
category: "hooks",
|
|
964
1313
|
description: "Suggest wrapping setInterval/setTimeout in onMount for automatic cleanup.",
|
|
965
1314
|
severity: "info",
|
|
966
|
-
fixable: false
|
|
1315
|
+
fixable: false,
|
|
1316
|
+
schema: { exemptPaths: "string[]" }
|
|
967
1317
|
},
|
|
968
1318
|
create(context) {
|
|
1319
|
+
if (isPathExempt(context)) return {};
|
|
1320
|
+
const ctx = createComponentContextTracker();
|
|
969
1321
|
let mountDepth = 0;
|
|
970
1322
|
return {
|
|
1323
|
+
...ctx.callbacks,
|
|
971
1324
|
CallExpression(node) {
|
|
972
1325
|
if (isCallTo(node, "onMount")) mountDepth++;
|
|
973
1326
|
if (mountDepth > 0) return;
|
|
1327
|
+
if (!ctx.isInComponentOrHook()) return;
|
|
974
1328
|
const callee = node.callee;
|
|
975
1329
|
if (!callee || callee.type !== "Identifier") return;
|
|
976
1330
|
if (TIMER_FNS.has(callee.name)) context.report({
|
|
@@ -1280,24 +1634,28 @@ const noPropsDestructure = {
|
|
|
1280
1634
|
},
|
|
1281
1635
|
create(context) {
|
|
1282
1636
|
let functionDepth = 0;
|
|
1637
|
+
const callArgFns = /* @__PURE__ */ new WeakSet();
|
|
1283
1638
|
return {
|
|
1639
|
+
CallExpression(node) {
|
|
1640
|
+
for (const arg of node.arguments ?? []) if (arg?.type === "ArrowFunctionExpression" || arg?.type === "FunctionExpression" || arg?.type === "FunctionDeclaration") callArgFns.add(arg);
|
|
1641
|
+
},
|
|
1284
1642
|
ArrowFunctionExpression(node) {
|
|
1285
1643
|
functionDepth++;
|
|
1286
|
-
checkFunction(node, context, functionDepth);
|
|
1644
|
+
checkFunction(node, context, functionDepth, callArgFns);
|
|
1287
1645
|
},
|
|
1288
1646
|
"ArrowFunctionExpression:exit"() {
|
|
1289
1647
|
functionDepth--;
|
|
1290
1648
|
},
|
|
1291
1649
|
FunctionDeclaration(node) {
|
|
1292
1650
|
functionDepth++;
|
|
1293
|
-
checkFunction(node, context, functionDepth);
|
|
1651
|
+
checkFunction(node, context, functionDepth, callArgFns);
|
|
1294
1652
|
},
|
|
1295
1653
|
"FunctionDeclaration:exit"() {
|
|
1296
1654
|
functionDepth--;
|
|
1297
1655
|
},
|
|
1298
1656
|
FunctionExpression(node) {
|
|
1299
1657
|
functionDepth++;
|
|
1300
|
-
checkFunction(node, context, functionDepth);
|
|
1658
|
+
checkFunction(node, context, functionDepth, callArgFns);
|
|
1301
1659
|
},
|
|
1302
1660
|
"FunctionExpression:exit"() {
|
|
1303
1661
|
functionDepth--;
|
|
@@ -1305,14 +1663,13 @@ const noPropsDestructure = {
|
|
|
1305
1663
|
};
|
|
1306
1664
|
}
|
|
1307
1665
|
};
|
|
1308
|
-
function checkFunction(node, context, depth) {
|
|
1666
|
+
function checkFunction(node, context, depth, callArgFns) {
|
|
1309
1667
|
const params = node.params;
|
|
1310
1668
|
if (!params || params.length === 0) return;
|
|
1311
1669
|
const firstParam = params[0];
|
|
1312
1670
|
if (!isDestructuring(firstParam)) return;
|
|
1313
1671
|
if (depth > 1) return;
|
|
1314
|
-
|
|
1315
|
-
if (parent?.type === "CallExpression" && parent.arguments?.includes(node)) return;
|
|
1672
|
+
if (callArgFns.has(node)) return;
|
|
1316
1673
|
const body = node.body;
|
|
1317
1674
|
if (!body) return;
|
|
1318
1675
|
if (containsJSXReturn(body)) {
|
|
@@ -1409,9 +1766,47 @@ const noDomInSetup = {
|
|
|
1409
1766
|
},
|
|
1410
1767
|
create(context) {
|
|
1411
1768
|
let safeDepth = 0;
|
|
1769
|
+
function isSafeContextCall(node) {
|
|
1770
|
+
return isCallTo(node, "onMount") || isCallTo(node, "onUnmount") || isCallTo(node, "onCleanup") || isCallTo(node, "effect") || isCallTo(node, "renderEffect") || isCallTo(node, "requestAnimationFrame");
|
|
1771
|
+
}
|
|
1772
|
+
function isNegatedTypeofDocument(test) {
|
|
1773
|
+
if (!test) return false;
|
|
1774
|
+
if (test.type === "BinaryExpression" && (test.operator === "===" || test.operator === "==") && test.left?.type === "UnaryExpression" && test.left.operator === "typeof" && test.left.argument?.type === "Identifier" && (test.left.argument.name === "document" || test.left.argument.name === "window")) return true;
|
|
1775
|
+
return false;
|
|
1776
|
+
}
|
|
1777
|
+
function isEarlyReturnDocumentGuard(stmt) {
|
|
1778
|
+
if (!stmt || stmt.type !== "IfStatement") return false;
|
|
1779
|
+
if (!isNegatedTypeofDocument(stmt.test)) return false;
|
|
1780
|
+
const c = stmt.consequent;
|
|
1781
|
+
const isTerminator = (s) => s?.type === "ReturnStatement" || s?.type === "ThrowStatement";
|
|
1782
|
+
if (isTerminator(c)) return true;
|
|
1783
|
+
if (c?.type === "BlockStatement" && c.body.length === 1 && isTerminator(c.body[0])) return true;
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
const earlyReturnStack = [];
|
|
1787
|
+
function pushFunctionScope(node) {
|
|
1788
|
+
const body = node?.body;
|
|
1789
|
+
const stmts = body?.type === "BlockStatement" ? body.body : null;
|
|
1790
|
+
let bumps = 0;
|
|
1791
|
+
if (stmts && stmts.length > 0 && isEarlyReturnDocumentGuard(stmts[0])) {
|
|
1792
|
+
bumps = 1;
|
|
1793
|
+
safeDepth++;
|
|
1794
|
+
}
|
|
1795
|
+
earlyReturnStack.push(bumps);
|
|
1796
|
+
}
|
|
1797
|
+
function popFunctionScope() {
|
|
1798
|
+
const bumps = earlyReturnStack.pop() ?? 0;
|
|
1799
|
+
if (bumps > 0) safeDepth -= bumps;
|
|
1800
|
+
}
|
|
1412
1801
|
return {
|
|
1802
|
+
FunctionDeclaration: pushFunctionScope,
|
|
1803
|
+
"FunctionDeclaration:exit": popFunctionScope,
|
|
1804
|
+
FunctionExpression: pushFunctionScope,
|
|
1805
|
+
"FunctionExpression:exit": popFunctionScope,
|
|
1806
|
+
ArrowFunctionExpression: pushFunctionScope,
|
|
1807
|
+
"ArrowFunctionExpression:exit": popFunctionScope,
|
|
1413
1808
|
CallExpression(node) {
|
|
1414
|
-
if (
|
|
1809
|
+
if (isSafeContextCall(node)) safeDepth++;
|
|
1415
1810
|
if (safeDepth > 0) return;
|
|
1416
1811
|
const callee = node.callee;
|
|
1417
1812
|
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "document" && callee.property?.type === "Identifier" && DOM_METHODS.has(callee.property.name)) context.report({
|
|
@@ -1420,7 +1815,7 @@ const noDomInSetup = {
|
|
|
1420
1815
|
});
|
|
1421
1816
|
},
|
|
1422
1817
|
"CallExpression:exit"(node) {
|
|
1423
|
-
if (
|
|
1818
|
+
if (isSafeContextCall(node)) safeDepth--;
|
|
1424
1819
|
}
|
|
1425
1820
|
};
|
|
1426
1821
|
}
|
|
@@ -1642,6 +2037,11 @@ const preferShowOverDisplay = {
|
|
|
1642
2037
|
|
|
1643
2038
|
//#endregion
|
|
1644
2039
|
//#region src/rules/reactivity/no-bare-signal-in-jsx.ts
|
|
2040
|
+
const SKIP_NAMES = new Set([
|
|
2041
|
+
"render",
|
|
2042
|
+
"h",
|
|
2043
|
+
"cloneVNode"
|
|
2044
|
+
]);
|
|
1645
2045
|
const SKIP_PREFIXES = /^(use|get|is|has|[A-Z])/;
|
|
1646
2046
|
const noBareSignalInJsx = {
|
|
1647
2047
|
meta: {
|
|
@@ -1673,7 +2073,7 @@ const noBareSignalInJsx = {
|
|
|
1673
2073
|
const callee = expr.callee;
|
|
1674
2074
|
if (!callee || callee.type !== "Identifier") return;
|
|
1675
2075
|
const name = callee.name;
|
|
1676
|
-
if (SKIP_PREFIXES.test(name)) return;
|
|
2076
|
+
if (SKIP_NAMES.has(name) || SKIP_PREFIXES.test(name)) return;
|
|
1677
2077
|
const span = getSpan(node);
|
|
1678
2078
|
const fixed = `{() => ${context.getSourceText().slice(span.start, span.end).slice(1, -1)}}`;
|
|
1679
2079
|
context.report({
|
|
@@ -1974,9 +2374,11 @@ const noUnbatchedUpdates = {
|
|
|
1974
2374
|
category: "reactivity",
|
|
1975
2375
|
description: "Warn when 3+ .set() calls occur in the same function without batch().",
|
|
1976
2376
|
severity: "warn",
|
|
1977
|
-
fixable: false
|
|
2377
|
+
fixable: false,
|
|
2378
|
+
schema: { exemptPaths: "string[]" }
|
|
1978
2379
|
},
|
|
1979
2380
|
create(context) {
|
|
2381
|
+
if (isPathExempt(context)) return {};
|
|
1980
2382
|
const scopeStack = [];
|
|
1981
2383
|
let batchDepth = 0;
|
|
1982
2384
|
function enterScope(node) {
|
|
@@ -2125,46 +2527,94 @@ const noImperativeNavigateInRender = {
|
|
|
2125
2527
|
},
|
|
2126
2528
|
create(context) {
|
|
2127
2529
|
let componentBodyDepth = 0;
|
|
2128
|
-
let
|
|
2129
|
-
|
|
2530
|
+
let nestedFnDepth = 0;
|
|
2531
|
+
const componentInits = /* @__PURE__ */ new WeakSet();
|
|
2532
|
+
const dangerousBindings = [];
|
|
2533
|
+
const nestedFnStack = [];
|
|
2534
|
+
function isComponentFunctionDecl(node) {
|
|
2535
|
+
return /^[A-Z]/.test(node.id?.name ?? "");
|
|
2536
|
+
}
|
|
2537
|
+
function enterNestedFn(node, bindingName) {
|
|
2538
|
+
nestedFnDepth++;
|
|
2539
|
+
nestedFnStack.push({
|
|
2540
|
+
containsNavigate: false,
|
|
2541
|
+
bindingName
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
function exitNestedFn() {
|
|
2545
|
+
nestedFnDepth--;
|
|
2546
|
+
const frame = nestedFnStack.pop();
|
|
2547
|
+
if (!frame) return;
|
|
2548
|
+
if (frame.containsNavigate && frame.bindingName && dangerousBindings.length > 0) dangerousBindings[dangerousBindings.length - 1].add(frame.bindingName);
|
|
2549
|
+
}
|
|
2550
|
+
function isNavigateCall(node) {
|
|
2551
|
+
return isCallTo(node, "navigate") || isMemberCallTo(node, "router", "push");
|
|
2552
|
+
}
|
|
2553
|
+
const callbacks = {
|
|
2130
2554
|
FunctionDeclaration(node) {
|
|
2131
|
-
|
|
2132
|
-
|
|
2555
|
+
if (isComponentFunctionDecl(node)) {
|
|
2556
|
+
componentBodyDepth++;
|
|
2557
|
+
dangerousBindings.push(/* @__PURE__ */ new Set());
|
|
2558
|
+
} else if (componentBodyDepth > 0) enterNestedFn(node, node.id?.type === "Identifier" ? node.id.name : null);
|
|
2133
2559
|
},
|
|
2134
2560
|
"FunctionDeclaration:exit"(node) {
|
|
2135
|
-
|
|
2136
|
-
|
|
2561
|
+
if (isComponentFunctionDecl(node)) {
|
|
2562
|
+
componentBodyDepth--;
|
|
2563
|
+
dangerousBindings.pop();
|
|
2564
|
+
} else if (componentBodyDepth > 0) exitNestedFn();
|
|
2137
2565
|
},
|
|
2138
2566
|
VariableDeclarator(node) {
|
|
2139
|
-
|
|
2140
|
-
|
|
2567
|
+
if (/^[A-Z]/.test(node.id?.name ?? "") && (node.init?.type === "ArrowFunctionExpression" || node.init?.type === "FunctionExpression")) {
|
|
2568
|
+
componentBodyDepth++;
|
|
2569
|
+
dangerousBindings.push(/* @__PURE__ */ new Set());
|
|
2570
|
+
componentInits.add(node.init);
|
|
2571
|
+
}
|
|
2141
2572
|
},
|
|
2142
2573
|
"VariableDeclarator:exit"(node) {
|
|
2143
|
-
|
|
2144
|
-
|
|
2574
|
+
if (/^[A-Z]/.test(node.id?.name ?? "") && (node.init?.type === "ArrowFunctionExpression" || node.init?.type === "FunctionExpression")) {
|
|
2575
|
+
componentBodyDepth--;
|
|
2576
|
+
dangerousBindings.pop();
|
|
2577
|
+
}
|
|
2578
|
+
},
|
|
2579
|
+
ArrowFunctionExpression(node) {
|
|
2580
|
+
if (componentInits.has(node)) return;
|
|
2581
|
+
if (componentBodyDepth > 0) enterNestedFn(node, bindingAssignmentNames.get(node) ?? null);
|
|
2582
|
+
},
|
|
2583
|
+
"ArrowFunctionExpression:exit"(node) {
|
|
2584
|
+
if (componentInits.has(node)) return;
|
|
2585
|
+
if (componentBodyDepth > 0) exitNestedFn();
|
|
2586
|
+
},
|
|
2587
|
+
FunctionExpression(node) {
|
|
2588
|
+
if (componentInits.has(node)) return;
|
|
2589
|
+
if (componentBodyDepth > 0) enterNestedFn(node, bindingAssignmentNames.get(node) ?? null);
|
|
2590
|
+
},
|
|
2591
|
+
"FunctionExpression:exit"(node) {
|
|
2592
|
+
if (componentInits.has(node)) return;
|
|
2593
|
+
if (componentBodyDepth > 0) exitNestedFn();
|
|
2145
2594
|
},
|
|
2146
2595
|
CallExpression(node) {
|
|
2147
2596
|
if (componentBodyDepth <= 0) return;
|
|
2148
|
-
if (
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2597
|
+
if (isNavigateCall(node)) {
|
|
2598
|
+
if (nestedFnDepth === 0) context.report({
|
|
2599
|
+
message: "Imperative navigation at the top level of a component — this runs on every render and causes infinite loops. Move inside `onMount`, `effect`, or an event handler.",
|
|
2600
|
+
span: getSpan(node)
|
|
2601
|
+
});
|
|
2602
|
+
else if (nestedFnStack.length > 0) nestedFnStack[nestedFnStack.length - 1].containsNavigate = true;
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
if (nestedFnDepth === 0 && node.callee?.type === "Identifier" && dangerousBindings.length > 0 && dangerousBindings[dangerousBindings.length - 1].has(node.callee.name)) context.report({
|
|
2606
|
+
message: "Synchronous call of a nested function that performs imperative navigation — this runs during render and causes infinite loops. Move the call inside `onMount`, `effect`, or an event handler.",
|
|
2152
2607
|
span: getSpan(node)
|
|
2153
2608
|
});
|
|
2154
|
-
},
|
|
2155
|
-
"CallExpression:exit"(node) {
|
|
2156
|
-
if (componentBodyDepth <= 0) return;
|
|
2157
|
-
if (isSafeWrapperCall(node)) safeDepth--;
|
|
2158
2609
|
}
|
|
2159
2610
|
};
|
|
2611
|
+
const bindingAssignmentNames = /* @__PURE__ */ new WeakMap();
|
|
2612
|
+
callbacks.VariableDeclaration = (node) => {
|
|
2613
|
+
for (const decl of node.declarations ?? []) if (decl.id?.type === "Identifier" && (decl.init?.type === "ArrowFunctionExpression" || decl.init?.type === "FunctionExpression")) bindingAssignmentNames.set(decl.init, decl.id.name);
|
|
2614
|
+
};
|
|
2615
|
+
return callbacks;
|
|
2160
2616
|
}
|
|
2161
2617
|
};
|
|
2162
|
-
function isSafeWrapperCall(node) {
|
|
2163
|
-
const callee = node.callee;
|
|
2164
|
-
if (!callee || callee.type !== "Identifier") return false;
|
|
2165
|
-
const name = callee.name;
|
|
2166
|
-
return name === "onMount" || name === "effect" || name === "onUnmount";
|
|
2167
|
-
}
|
|
2168
2618
|
|
|
2169
2619
|
//#endregion
|
|
2170
2620
|
//#region src/rules/router/no-missing-fallback.ts
|
|
@@ -2311,32 +2761,236 @@ const noWindowInSsr = {
|
|
|
2311
2761
|
category: "ssr",
|
|
2312
2762
|
description: "Disallow browser globals outside onMount/effect/typeof guards — they break SSR.",
|
|
2313
2763
|
severity: "error",
|
|
2314
|
-
fixable: false
|
|
2764
|
+
fixable: false,
|
|
2765
|
+
schema: { exemptPaths: "string[]" }
|
|
2315
2766
|
},
|
|
2316
2767
|
create(context) {
|
|
2768
|
+
if (isPathExempt(context)) return {};
|
|
2317
2769
|
let safeDepth = 0;
|
|
2318
2770
|
let typeofGuardDepth = 0;
|
|
2771
|
+
let inTypeofExpr = 0;
|
|
2772
|
+
const skipPropertyNodes = /* @__PURE__ */ new WeakSet();
|
|
2773
|
+
let inTsTypePos = 0;
|
|
2774
|
+
const typeofBoundConsts = /* @__PURE__ */ new Set();
|
|
2775
|
+
function isPositiveTypeofCheck(expr) {
|
|
2776
|
+
if (!expr) return false;
|
|
2777
|
+
if (expr.type === "BinaryExpression" && (expr.operator === "!==" || expr.operator === "!=") && expr.left?.type === "UnaryExpression" && expr.left.operator === "typeof") return true;
|
|
2778
|
+
if (expr.type === "UnaryExpression" && expr.operator === "typeof") return true;
|
|
2779
|
+
return false;
|
|
2780
|
+
}
|
|
2781
|
+
/** Used by VariableDeclaration to decide whether to bind a const. */
|
|
2782
|
+
function isTypeofCheckForBinding(expr) {
|
|
2783
|
+
if (!expr) return false;
|
|
2784
|
+
if (expr.type === "BinaryExpression" && expr.left?.type === "UnaryExpression" && expr.left.operator === "typeof") return true;
|
|
2785
|
+
if (expr.type === "UnaryExpression" && expr.operator === "typeof") return true;
|
|
2786
|
+
if (expr.type === "LogicalExpression" && expr.operator === "&&") return isTypeofCheckForBinding(expr.left) || isTypeofCheckForBinding(expr.right);
|
|
2787
|
+
if (expr.type === "ConditionalExpression") return isTypeofCheckForBinding(expr.test);
|
|
2788
|
+
if (expr.type === "Identifier" && typeofBoundConsts.has(expr.name)) return true;
|
|
2789
|
+
return false;
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* `if (test) { … }` — does the test indicate the body is the
|
|
2793
|
+
* BROWSER-SAFE branch? Only positive forms qualify here. Negated
|
|
2794
|
+
* forms (`typeof X === 'undefined'`, `!isBrowser`) are early-return
|
|
2795
|
+
* guards handled separately.
|
|
2796
|
+
*/
|
|
2797
|
+
function testIsTypeofGuard(test) {
|
|
2798
|
+
if (!test) return false;
|
|
2799
|
+
if (isPositiveTypeofCheck(test)) return true;
|
|
2800
|
+
if (test.type === "Identifier" && typeofBoundConsts.has(test.name)) return true;
|
|
2801
|
+
if (test.type === "CallExpression" && test.callee?.type === "Identifier" && typeofGuardFunctions.has(test.callee.name)) return true;
|
|
2802
|
+
if (test.type === "LogicalExpression" && test.operator === "&&") return testIsTypeofGuard(test.left) || testIsTypeofGuard(test.right);
|
|
2803
|
+
return false;
|
|
2804
|
+
}
|
|
2805
|
+
const typeofGuardFunctions = new Set([
|
|
2806
|
+
"isBrowser",
|
|
2807
|
+
"isClient",
|
|
2808
|
+
"isServer",
|
|
2809
|
+
"isSSR"
|
|
2810
|
+
]);
|
|
2811
|
+
function bodyIsTypeofGuard(body) {
|
|
2812
|
+
if (!body) return false;
|
|
2813
|
+
if (body.type !== "BlockStatement") return isReturnedTypeofExpr(body);
|
|
2814
|
+
const stmts = body.body ?? [];
|
|
2815
|
+
if (stmts.length !== 1) return false;
|
|
2816
|
+
const stmt = stmts[0];
|
|
2817
|
+
if (stmt?.type !== "ReturnStatement") return false;
|
|
2818
|
+
return isReturnedTypeofExpr(stmt.argument);
|
|
2819
|
+
}
|
|
2820
|
+
function isReturnedTypeofExpr(expr) {
|
|
2821
|
+
if (!expr) return false;
|
|
2822
|
+
if (isPositiveTypeofCheck(expr)) return true;
|
|
2823
|
+
if (expr.type === "LogicalExpression" && expr.operator === "&&") return isReturnedTypeofExpr(expr.left) && isReturnedTypeofExpr(expr.right);
|
|
2824
|
+
if (expr.type === "Identifier" && typeofBoundConsts.has(expr.name)) return true;
|
|
2825
|
+
return false;
|
|
2826
|
+
}
|
|
2827
|
+
function isNegatedTypeofExpr(test) {
|
|
2828
|
+
if (!test) return false;
|
|
2829
|
+
if (test.type === "BinaryExpression" && (test.operator === "===" || test.operator === "==") && test.left?.type === "UnaryExpression" && test.left.operator === "typeof") return true;
|
|
2830
|
+
if (test.type === "UnaryExpression" && test.operator === "!" && test.argument?.type === "Identifier" && typeofBoundConsts.has(test.argument.name)) return true;
|
|
2831
|
+
if (test.type === "UnaryExpression" && test.operator === "!" && test.argument?.type === "CallExpression" && test.argument.callee?.type === "Identifier" && typeofGuardFunctions.has(test.argument.callee.name)) return true;
|
|
2832
|
+
if (test.type === "LogicalExpression" && test.operator === "||") return isNegatedTypeofExpr(test.left) && isNegatedTypeofExpr(test.right);
|
|
2833
|
+
return false;
|
|
2834
|
+
}
|
|
2835
|
+
function isEarlyReturnTypeofGuard(stmt) {
|
|
2836
|
+
if (!stmt || stmt.type !== "IfStatement") return false;
|
|
2837
|
+
if (!isNegatedTypeofExpr(stmt.test)) return false;
|
|
2838
|
+
const c = stmt.consequent;
|
|
2839
|
+
const isTerminator = (s) => s?.type === "ReturnStatement" || s?.type === "ThrowStatement";
|
|
2840
|
+
if (isTerminator(c)) return true;
|
|
2841
|
+
if (c?.type === "BlockStatement" && c.body.length === 1 && isTerminator(c.body[0])) return true;
|
|
2842
|
+
return false;
|
|
2843
|
+
}
|
|
2844
|
+
const earlyReturnStack = [];
|
|
2845
|
+
const watchCallbackNodes = /* @__PURE__ */ new WeakSet();
|
|
2846
|
+
const watchCallbackSafeDepthStack = [];
|
|
2847
|
+
const shadowedNamesStack = [];
|
|
2848
|
+
const importShadowedNames = /* @__PURE__ */ new Set();
|
|
2849
|
+
function collectParamNames(params) {
|
|
2850
|
+
const names = /* @__PURE__ */ new Set();
|
|
2851
|
+
const walk = (p) => {
|
|
2852
|
+
if (!p) return;
|
|
2853
|
+
if (p.type === "Identifier" && BROWSER_GLOBALS.has(p.name)) names.add(p.name);
|
|
2854
|
+
else if (p.type === "AssignmentPattern") walk(p.left);
|
|
2855
|
+
else if (p.type === "RestElement") walk(p.argument);
|
|
2856
|
+
else if (p.type === "ArrayPattern") for (const el of p.elements ?? []) walk(el);
|
|
2857
|
+
else if (p.type === "ObjectPattern") for (const prop of p.properties ?? []) if (prop.type === "RestElement") walk(prop.argument);
|
|
2858
|
+
else walk(prop.value);
|
|
2859
|
+
};
|
|
2860
|
+
for (const p of params ?? []) walk(p);
|
|
2861
|
+
return names;
|
|
2862
|
+
}
|
|
2863
|
+
function isNameShadowed(name) {
|
|
2864
|
+
for (let i = shadowedNamesStack.length - 1; i >= 0; i--) if (shadowedNamesStack[i].has(name)) return true;
|
|
2865
|
+
return false;
|
|
2866
|
+
}
|
|
2867
|
+
function pushFunctionScope(node) {
|
|
2868
|
+
earlyReturnStack.push(0);
|
|
2869
|
+
shadowedNamesStack.push(node ? collectParamNames(node.params ?? []) : /* @__PURE__ */ new Set());
|
|
2870
|
+
if (node && watchCallbackNodes.has(node)) {
|
|
2871
|
+
safeDepth++;
|
|
2872
|
+
watchCallbackSafeDepthStack.push(1);
|
|
2873
|
+
} else watchCallbackSafeDepthStack.push(0);
|
|
2874
|
+
}
|
|
2875
|
+
function popFunctionScope() {
|
|
2876
|
+
const bumps = earlyReturnStack.pop() ?? 0;
|
|
2877
|
+
typeofGuardDepth -= bumps;
|
|
2878
|
+
shadowedNamesStack.pop();
|
|
2879
|
+
const watchBump = watchCallbackSafeDepthStack.pop() ?? 0;
|
|
2880
|
+
if (watchBump > 0) safeDepth -= watchBump;
|
|
2881
|
+
}
|
|
2882
|
+
function noteEarlyReturnGuardVisit() {
|
|
2883
|
+
typeofGuardDepth++;
|
|
2884
|
+
if (earlyReturnStack.length > 0) earlyReturnStack[earlyReturnStack.length - 1]++;
|
|
2885
|
+
}
|
|
2319
2886
|
return {
|
|
2887
|
+
VariableDeclaration(node) {
|
|
2888
|
+
for (const decl of node.declarations ?? []) {
|
|
2889
|
+
if (decl.id?.type !== "Identifier") continue;
|
|
2890
|
+
if (isTypeofCheckForBinding(decl.init)) typeofBoundConsts.add(decl.id.name);
|
|
2891
|
+
if ((decl.init?.type === "ArrowFunctionExpression" || decl.init?.type === "FunctionExpression") && bodyIsTypeofGuard(decl.init.body)) typeofGuardFunctions.add(decl.id.name);
|
|
2892
|
+
}
|
|
2893
|
+
},
|
|
2894
|
+
FunctionDeclaration(node) {
|
|
2895
|
+
if (node.id?.type === "Identifier" && bodyIsTypeofGuard(node.body)) typeofGuardFunctions.add(node.id.name);
|
|
2896
|
+
pushFunctionScope(node);
|
|
2897
|
+
},
|
|
2898
|
+
"FunctionDeclaration:exit": popFunctionScope,
|
|
2899
|
+
FunctionExpression: pushFunctionScope,
|
|
2900
|
+
"FunctionExpression:exit": popFunctionScope,
|
|
2901
|
+
ArrowFunctionExpression: pushFunctionScope,
|
|
2902
|
+
"ArrowFunctionExpression:exit": popFunctionScope,
|
|
2320
2903
|
CallExpression(node) {
|
|
2321
|
-
if (isCallTo(node, "onMount") || isCallTo(node, "effect"))
|
|
2904
|
+
if (isCallTo(node, "onMount") || isCallTo(node, "onUnmount") || isCallTo(node, "onCleanup") || isCallTo(node, "effect") || isCallTo(node, "renderEffect") || isCallTo(node, "requestAnimationFrame")) {
|
|
2905
|
+
safeDepth++;
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
if (isCallTo(node, "watch")) {
|
|
2909
|
+
const cb = node.arguments?.[1];
|
|
2910
|
+
if (cb?.type === "ArrowFunctionExpression" || cb?.type === "FunctionExpression" || cb?.type === "FunctionDeclaration") watchCallbackNodes.add(cb);
|
|
2911
|
+
}
|
|
2322
2912
|
},
|
|
2323
2913
|
"CallExpression:exit"(node) {
|
|
2324
|
-
if (isCallTo(node, "onMount") || isCallTo(node, "effect")) safeDepth--;
|
|
2914
|
+
if (isCallTo(node, "onMount") || isCallTo(node, "onUnmount") || isCallTo(node, "onCleanup") || isCallTo(node, "effect") || isCallTo(node, "renderEffect") || isCallTo(node, "requestAnimationFrame")) safeDepth--;
|
|
2325
2915
|
},
|
|
2326
2916
|
IfStatement(node) {
|
|
2327
|
-
|
|
2328
|
-
if (
|
|
2917
|
+
if (testIsTypeofGuard(node.test)) typeofGuardDepth++;
|
|
2918
|
+
else if (isEarlyReturnTypeofGuard(node)) noteEarlyReturnGuardVisit();
|
|
2329
2919
|
},
|
|
2330
2920
|
"IfStatement:exit"(node) {
|
|
2331
|
-
|
|
2332
|
-
|
|
2921
|
+
if (testIsTypeofGuard(node.test)) typeofGuardDepth--;
|
|
2922
|
+
},
|
|
2923
|
+
ConditionalExpression(node) {
|
|
2924
|
+
if (testIsTypeofGuard(node.test)) typeofGuardDepth++;
|
|
2925
|
+
},
|
|
2926
|
+
"ConditionalExpression:exit"(node) {
|
|
2927
|
+
if (testIsTypeofGuard(node.test)) typeofGuardDepth--;
|
|
2928
|
+
},
|
|
2929
|
+
UnaryExpression(node) {
|
|
2930
|
+
if (node.operator === "typeof") inTypeofExpr++;
|
|
2931
|
+
},
|
|
2932
|
+
"UnaryExpression:exit"(node) {
|
|
2933
|
+
if (node.operator === "typeof") inTypeofExpr--;
|
|
2934
|
+
},
|
|
2935
|
+
TSTypeAnnotation(_n) {
|
|
2936
|
+
inTsTypePos++;
|
|
2937
|
+
},
|
|
2938
|
+
"TSTypeAnnotation:exit"(_n) {
|
|
2939
|
+
inTsTypePos--;
|
|
2940
|
+
},
|
|
2941
|
+
TSTypeReference(_n) {
|
|
2942
|
+
inTsTypePos++;
|
|
2943
|
+
},
|
|
2944
|
+
"TSTypeReference:exit"(_n) {
|
|
2945
|
+
inTsTypePos--;
|
|
2946
|
+
},
|
|
2947
|
+
TSTypeAliasDeclaration(_n) {
|
|
2948
|
+
inTsTypePos++;
|
|
2949
|
+
},
|
|
2950
|
+
"TSTypeAliasDeclaration:exit"(_n) {
|
|
2951
|
+
inTsTypePos--;
|
|
2952
|
+
},
|
|
2953
|
+
TSInterfaceDeclaration(_n) {
|
|
2954
|
+
inTsTypePos++;
|
|
2955
|
+
},
|
|
2956
|
+
"TSInterfaceDeclaration:exit"(_n) {
|
|
2957
|
+
inTsTypePos--;
|
|
2958
|
+
},
|
|
2959
|
+
TSTypeParameter(_n) {
|
|
2960
|
+
inTsTypePos++;
|
|
2961
|
+
},
|
|
2962
|
+
"TSTypeParameter:exit"(_n) {
|
|
2963
|
+
inTsTypePos--;
|
|
2964
|
+
},
|
|
2965
|
+
MemberExpression(node) {
|
|
2966
|
+
if (!node.computed && node.property?.type === "Identifier") skipPropertyNodes.add(node.property);
|
|
2967
|
+
},
|
|
2968
|
+
Property(node) {
|
|
2969
|
+
if (!node.computed && node.key?.type === "Identifier") skipPropertyNodes.add(node.key);
|
|
2970
|
+
},
|
|
2971
|
+
ImportSpecifier(node) {
|
|
2972
|
+
if (node.imported?.type === "Identifier") skipPropertyNodes.add(node.imported);
|
|
2973
|
+
if (node.local?.type === "Identifier" && node.local !== node.imported) skipPropertyNodes.add(node.local);
|
|
2974
|
+
if (node.local?.type === "Identifier" && BROWSER_GLOBALS.has(node.local.name)) importShadowedNames.add(node.local.name);
|
|
2975
|
+
},
|
|
2976
|
+
ImportDefaultSpecifier(node) {
|
|
2977
|
+
if (node.local?.type === "Identifier") {
|
|
2978
|
+
skipPropertyNodes.add(node.local);
|
|
2979
|
+
if (BROWSER_GLOBALS.has(node.local.name)) importShadowedNames.add(node.local.name);
|
|
2980
|
+
}
|
|
2981
|
+
},
|
|
2982
|
+
ImportNamespaceSpecifier(node) {
|
|
2983
|
+
if (node.local?.type === "Identifier") {
|
|
2984
|
+
skipPropertyNodes.add(node.local);
|
|
2985
|
+
if (BROWSER_GLOBALS.has(node.local.name)) importShadowedNames.add(node.local.name);
|
|
2986
|
+
}
|
|
2333
2987
|
},
|
|
2334
|
-
Identifier(node
|
|
2335
|
-
if (safeDepth > 0 || typeofGuardDepth > 0) return;
|
|
2988
|
+
Identifier(node) {
|
|
2989
|
+
if (safeDepth > 0 || typeofGuardDepth > 0 || inTypeofExpr > 0 || inTsTypePos > 0) return;
|
|
2990
|
+
if (skipPropertyNodes.has(node)) return;
|
|
2336
2991
|
if (!BROWSER_GLOBALS.has(node.name)) return;
|
|
2337
|
-
if (
|
|
2338
|
-
if (
|
|
2339
|
-
if (parent?.type === "MemberExpression" && parent.property === node && !parent.computed) return;
|
|
2992
|
+
if (isNameShadowed(node.name)) return;
|
|
2993
|
+
if (importShadowedNames.has(node.name)) return;
|
|
2340
2994
|
context.report({
|
|
2341
2995
|
message: `Browser global \`${node.name}\` used outside \`onMount\`/\`effect\`/typeof guard — this will fail during SSR. Wrap in \`onMount(() => { ... })\`.`,
|
|
2342
2996
|
span: getSpan(node)
|
|
@@ -2404,6 +3058,7 @@ const noDuplicateStoreId = {
|
|
|
2404
3058
|
fixable: false
|
|
2405
3059
|
},
|
|
2406
3060
|
create(context) {
|
|
3061
|
+
if (isTestFile(context.getFilePath())) return {};
|
|
2407
3062
|
const storeIds = /* @__PURE__ */ new Map();
|
|
2408
3063
|
return { CallExpression(node) {
|
|
2409
3064
|
if (!isCallTo(node, "defineStore")) return;
|
|
@@ -2429,25 +3084,30 @@ const noMutateStoreState = {
|
|
|
2429
3084
|
meta: {
|
|
2430
3085
|
id: "pyreon/no-mutate-store-state",
|
|
2431
3086
|
category: "store",
|
|
2432
|
-
description: "Warn when
|
|
3087
|
+
description: "Warn when calling .set() on store signals from a component or hook — use store actions instead.",
|
|
2433
3088
|
severity: "warn",
|
|
2434
3089
|
fixable: false
|
|
2435
3090
|
},
|
|
2436
3091
|
create(context) {
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
3092
|
+
const ctx = createComponentContextTracker();
|
|
3093
|
+
return {
|
|
3094
|
+
...ctx.callbacks,
|
|
3095
|
+
CallExpression(node) {
|
|
3096
|
+
if (!ctx.isInComponentOrHook()) return;
|
|
3097
|
+
const callee = node.callee;
|
|
3098
|
+
if (!callee || callee.type !== "MemberExpression") return;
|
|
3099
|
+
if (callee.property?.type !== "Identifier" || callee.property.name !== "set") return;
|
|
3100
|
+
const obj = callee.object;
|
|
3101
|
+
if (!obj || obj.type !== "MemberExpression") return;
|
|
3102
|
+
const outerObj = obj.object;
|
|
3103
|
+
if (!outerObj || outerObj.type !== "Identifier") return;
|
|
3104
|
+
const name = outerObj.name;
|
|
3105
|
+
if (name.toLowerCase().includes("store")) context.report({
|
|
3106
|
+
message: `Direct \`.set()\` on store state \`${name}\` — use store actions to mutate state for better traceability.`,
|
|
3107
|
+
span: getSpan(node)
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
};
|
|
2451
3111
|
}
|
|
2452
3112
|
};
|
|
2453
3113
|
|
|
@@ -2498,44 +3158,27 @@ const noDynamicStyled = {
|
|
|
2498
3158
|
meta: {
|
|
2499
3159
|
id: "pyreon/no-dynamic-styled",
|
|
2500
3160
|
category: "styling",
|
|
2501
|
-
description: "Warn when styled() is called inside a
|
|
3161
|
+
description: "Warn when styled() is called inside a component or hook — it creates new CSS on every render.",
|
|
2502
3162
|
severity: "warn",
|
|
2503
3163
|
fixable: false
|
|
2504
3164
|
},
|
|
2505
3165
|
create(context) {
|
|
2506
|
-
|
|
3166
|
+
const ctx = createComponentContextTracker();
|
|
2507
3167
|
return {
|
|
2508
|
-
|
|
2509
|
-
functionDepth++;
|
|
2510
|
-
},
|
|
2511
|
-
"FunctionDeclaration:exit"() {
|
|
2512
|
-
functionDepth--;
|
|
2513
|
-
},
|
|
2514
|
-
FunctionExpression() {
|
|
2515
|
-
functionDepth++;
|
|
2516
|
-
},
|
|
2517
|
-
"FunctionExpression:exit"() {
|
|
2518
|
-
functionDepth--;
|
|
2519
|
-
},
|
|
2520
|
-
ArrowFunctionExpression() {
|
|
2521
|
-
functionDepth++;
|
|
2522
|
-
},
|
|
2523
|
-
"ArrowFunctionExpression:exit"() {
|
|
2524
|
-
functionDepth--;
|
|
2525
|
-
},
|
|
3168
|
+
...ctx.callbacks,
|
|
2526
3169
|
CallExpression(node) {
|
|
2527
|
-
if (
|
|
3170
|
+
if (!ctx.isInComponentOrHook()) return;
|
|
2528
3171
|
if (isCallTo(node, "styled")) context.report({
|
|
2529
|
-
message: "`styled()` inside a
|
|
3172
|
+
message: "`styled()` inside a component or hook — this creates new CSS rules on every render. Move `styled()` to module scope.",
|
|
2530
3173
|
span: getSpan(node)
|
|
2531
3174
|
});
|
|
2532
3175
|
},
|
|
2533
3176
|
TaggedTemplateExpression(node) {
|
|
2534
|
-
if (
|
|
3177
|
+
if (!ctx.isInComponentOrHook()) return;
|
|
2535
3178
|
const tag = node.tag;
|
|
2536
3179
|
if (!tag) return;
|
|
2537
3180
|
if (tag.type === "CallExpression" && isCallTo(tag, "styled")) context.report({
|
|
2538
|
-
message: "`styled()` tagged template inside a
|
|
3181
|
+
message: "`styled()` tagged template inside a component or hook — this creates new CSS rules on every render. Move to module scope.",
|
|
2539
3182
|
span: getSpan(node)
|
|
2540
3183
|
});
|
|
2541
3184
|
}
|
|
@@ -2568,6 +3211,7 @@ const noInlineStyleObject = {
|
|
|
2568
3211
|
|
|
2569
3212
|
//#endregion
|
|
2570
3213
|
//#region src/rules/styling/no-theme-outside-provider.ts
|
|
3214
|
+
const HOOK_NAME = /^use[A-Z]/;
|
|
2571
3215
|
const noThemeOutsideProvider = {
|
|
2572
3216
|
meta: {
|
|
2573
3217
|
id: "pyreon/no-theme-outside-provider",
|
|
@@ -2578,22 +3222,47 @@ const noThemeOutsideProvider = {
|
|
|
2578
3222
|
},
|
|
2579
3223
|
create(context) {
|
|
2580
3224
|
let hasProviderImport = false;
|
|
3225
|
+
let hookDepth = 0;
|
|
2581
3226
|
const themeCalls = [];
|
|
3227
|
+
function declaratorIsHook(node) {
|
|
3228
|
+
if (node?.id?.type !== "Identifier") return false;
|
|
3229
|
+
const init = node.init;
|
|
3230
|
+
if (init?.type !== "ArrowFunctionExpression" && init?.type !== "FunctionExpression") return false;
|
|
3231
|
+
return HOOK_NAME.test(node.id.name);
|
|
3232
|
+
}
|
|
2582
3233
|
return {
|
|
3234
|
+
FunctionDeclaration(node) {
|
|
3235
|
+
if (node.id?.name && HOOK_NAME.test(node.id.name)) hookDepth++;
|
|
3236
|
+
},
|
|
3237
|
+
"FunctionDeclaration:exit"(node) {
|
|
3238
|
+
if (node.id?.name && HOOK_NAME.test(node.id.name)) hookDepth--;
|
|
3239
|
+
},
|
|
3240
|
+
VariableDeclarator(node) {
|
|
3241
|
+
if (declaratorIsHook(node)) hookDepth++;
|
|
3242
|
+
},
|
|
3243
|
+
"VariableDeclarator:exit"(node) {
|
|
3244
|
+
if (declaratorIsHook(node)) hookDepth--;
|
|
3245
|
+
},
|
|
2583
3246
|
ImportDeclaration(node) {
|
|
2584
3247
|
const info = extractImportInfo(node);
|
|
2585
3248
|
if (!info) return;
|
|
2586
3249
|
if (info.specifiers.some((s) => s.imported === "PyreonUI" || s.imported === "ThemeProvider")) hasProviderImport = true;
|
|
2587
3250
|
},
|
|
2588
3251
|
CallExpression(node) {
|
|
2589
|
-
if (isCallTo(node, "useTheme")) themeCalls.push({
|
|
3252
|
+
if (isCallTo(node, "useTheme")) themeCalls.push({
|
|
3253
|
+
span: getSpan(node),
|
|
3254
|
+
insideHook: hookDepth > 0
|
|
3255
|
+
});
|
|
2590
3256
|
},
|
|
2591
3257
|
"Program:exit"() {
|
|
2592
3258
|
if (hasProviderImport) return;
|
|
2593
|
-
for (const call of themeCalls)
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
3259
|
+
for (const call of themeCalls) {
|
|
3260
|
+
if (call.insideHook) continue;
|
|
3261
|
+
context.report({
|
|
3262
|
+
message: "`useTheme()` without a `PyreonUI` or `ThemeProvider` import — the theme context may not be available.",
|
|
3263
|
+
span: call.span
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
2597
3266
|
}
|
|
2598
3267
|
};
|
|
2599
3268
|
}
|
|
@@ -2672,6 +3341,7 @@ const allRules = [
|
|
|
2672
3341
|
devGuardWarnings,
|
|
2673
3342
|
noErrorWithoutPrefix,
|
|
2674
3343
|
noProcessDevGate,
|
|
3344
|
+
requireBrowserSmokeTest,
|
|
2675
3345
|
noStoreOutsideProvider,
|
|
2676
3346
|
noMutateStoreState,
|
|
2677
3347
|
noDuplicateStoreId,
|
|
@@ -2702,11 +3372,17 @@ function buildRecommended() {
|
|
|
2702
3372
|
for (const rule of allRules) rules[rule.meta.id] = rule.meta.severity;
|
|
2703
3373
|
return { rules };
|
|
2704
3374
|
}
|
|
3375
|
+
function severityOf(entry) {
|
|
3376
|
+
return Array.isArray(entry) ? entry[0] : entry;
|
|
3377
|
+
}
|
|
2705
3378
|
/** Build a config where every warn is promoted to error. */
|
|
2706
3379
|
function buildStrict() {
|
|
2707
3380
|
const base = buildRecommended();
|
|
2708
3381
|
const rules = {};
|
|
2709
|
-
for (const [id,
|
|
3382
|
+
for (const [id, entry] of Object.entries(base.rules)) {
|
|
3383
|
+
const sev = severityOf(entry);
|
|
3384
|
+
rules[id] = sev === "warn" ? "error" : sev;
|
|
3385
|
+
}
|
|
2710
3386
|
return { rules };
|
|
2711
3387
|
}
|
|
2712
3388
|
/** Build app config — recommended but disable library-only rules. */
|
|
@@ -2716,7 +3392,8 @@ function buildApp() {
|
|
|
2716
3392
|
"pyreon/dev-guard-warnings": "off",
|
|
2717
3393
|
"pyreon/no-error-without-prefix": "off",
|
|
2718
3394
|
"pyreon/no-circular-import": "off",
|
|
2719
|
-
"pyreon/no-cross-layer-import": "off"
|
|
3395
|
+
"pyreon/no-cross-layer-import": "off",
|
|
3396
|
+
"pyreon/require-browser-smoke-test": "off"
|
|
2720
3397
|
} };
|
|
2721
3398
|
}
|
|
2722
3399
|
/** Build lib config — strict + all architecture rules as error. */
|
|
@@ -2727,7 +3404,8 @@ function buildLib() {
|
|
|
2727
3404
|
"pyreon/no-cross-layer-import": "error",
|
|
2728
3405
|
"pyreon/dev-guard-warnings": "error",
|
|
2729
3406
|
"pyreon/no-error-without-prefix": "error",
|
|
2730
|
-
"pyreon/no-process-dev-gate": "error"
|
|
3407
|
+
"pyreon/no-process-dev-gate": "error",
|
|
3408
|
+
"pyreon/require-browser-smoke-test": "error"
|
|
2731
3409
|
} };
|
|
2732
3410
|
}
|
|
2733
3411
|
const presetBuilders = {
|
|
@@ -2785,8 +3463,46 @@ function hasJsExtension(filePath) {
|
|
|
2785
3463
|
return JS_EXTENSIONS.has(ext);
|
|
2786
3464
|
}
|
|
2787
3465
|
|
|
3466
|
+
//#endregion
|
|
3467
|
+
//#region src/utils/validate-options.ts
|
|
3468
|
+
function validateRuleOptions(rule, options) {
|
|
3469
|
+
const schema = rule.meta.schema;
|
|
3470
|
+
const errors = [];
|
|
3471
|
+
const warnings = [];
|
|
3472
|
+
if (!schema) return {
|
|
3473
|
+
errors,
|
|
3474
|
+
warnings
|
|
3475
|
+
};
|
|
3476
|
+
for (const [key, value] of Object.entries(options)) {
|
|
3477
|
+
const expected = schema[key];
|
|
3478
|
+
if (expected === void 0) {
|
|
3479
|
+
warnings.push(`[${rule.meta.id}] unknown option "${key}" — allowed options: ${Object.keys(schema).join(", ") || "(none)"}`);
|
|
3480
|
+
continue;
|
|
3481
|
+
}
|
|
3482
|
+
if (!matchesType(value, expected)) errors.push(`[${rule.meta.id}] option "${key}" must be ${expected}, got ${describe(value)}`);
|
|
3483
|
+
}
|
|
3484
|
+
return {
|
|
3485
|
+
errors,
|
|
3486
|
+
warnings
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
function matchesType(value, type) {
|
|
3490
|
+
switch (type) {
|
|
3491
|
+
case "string": return typeof value === "string";
|
|
3492
|
+
case "string[]": return Array.isArray(value) && value.every((x) => typeof x === "string");
|
|
3493
|
+
case "number": return typeof value === "number" && Number.isFinite(value);
|
|
3494
|
+
case "boolean": return typeof value === "boolean";
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
function describe(value) {
|
|
3498
|
+
if (value === null) return "null";
|
|
3499
|
+
if (Array.isArray(value)) return `Array<${[...new Set(value.map((x) => x === null ? "null" : typeof x))].join(" | ") || "empty"}>`;
|
|
3500
|
+
return typeof value;
|
|
3501
|
+
}
|
|
3502
|
+
|
|
2788
3503
|
//#endregion
|
|
2789
3504
|
//#region src/runner.ts
|
|
3505
|
+
const VALIDATION_CACHE = /* @__PURE__ */ new Map();
|
|
2790
3506
|
function getExtension(filePath) {
|
|
2791
3507
|
const lastDot = filePath.lastIndexOf(".");
|
|
2792
3508
|
return lastDot === -1 ? "" : filePath.slice(lastDot);
|
|
@@ -2796,7 +3512,7 @@ function getLang(ext) {
|
|
|
2796
3512
|
if (ext === ".ts" || ext === ".mts") return "ts";
|
|
2797
3513
|
return "js";
|
|
2798
3514
|
}
|
|
2799
|
-
function createRuleContext(rule, severity, diagnostics, lineIndex, sourceText, filePath) {
|
|
3515
|
+
function createRuleContext(rule, severity, options, diagnostics, lineIndex, sourceText, filePath) {
|
|
2800
3516
|
return {
|
|
2801
3517
|
report(partial) {
|
|
2802
3518
|
diagnostics.push({
|
|
@@ -2813,6 +3529,9 @@ function createRuleContext(rule, severity, diagnostics, lineIndex, sourceText, f
|
|
|
2813
3529
|
},
|
|
2814
3530
|
getFilePath() {
|
|
2815
3531
|
return filePath;
|
|
3532
|
+
},
|
|
3533
|
+
getOptions() {
|
|
3534
|
+
return options;
|
|
2816
3535
|
}
|
|
2817
3536
|
};
|
|
2818
3537
|
}
|
|
@@ -2842,7 +3561,7 @@ function mergeCallbacks(allCallbacks) {
|
|
|
2842
3561
|
* for (const d of result.diagnostics) console.log(d.message)
|
|
2843
3562
|
* ```
|
|
2844
3563
|
*/
|
|
2845
|
-
function lintFile(filePath, sourceText, rules, config, cache) {
|
|
3564
|
+
function lintFile(filePath, sourceText, rules, config, cache, configDiagnosticsSink) {
|
|
2846
3565
|
const ext = getExtension(filePath);
|
|
2847
3566
|
if (!JS_EXTENSIONS.has(ext)) return {
|
|
2848
3567
|
filePath,
|
|
@@ -2875,20 +3594,50 @@ function lintFile(filePath, sourceText, rules, config, cache) {
|
|
|
2875
3594
|
const diagnostics = [];
|
|
2876
3595
|
const allCallbacks = [];
|
|
2877
3596
|
for (const rule of rules) {
|
|
2878
|
-
const
|
|
2879
|
-
if (
|
|
2880
|
-
const
|
|
3597
|
+
const entry = config.rules[rule.meta.id];
|
|
3598
|
+
if (entry === void 0) continue;
|
|
3599
|
+
const [severity, options] = Array.isArray(entry) ? [entry[0], entry[1] ?? {}] : [entry, {}];
|
|
3600
|
+
if (severity === "off") continue;
|
|
3601
|
+
const cacheKey = `${rule.meta.id}::${JSON.stringify(options)}`;
|
|
3602
|
+
let cached = VALIDATION_CACHE.get(cacheKey);
|
|
3603
|
+
if (!cached) {
|
|
3604
|
+
const { errors, warnings } = validateRuleOptions(rule, options);
|
|
3605
|
+
const configDiags = [];
|
|
3606
|
+
for (const message of warnings) configDiags.push({
|
|
3607
|
+
ruleId: rule.meta.id,
|
|
3608
|
+
severity: "warn",
|
|
3609
|
+
message
|
|
3610
|
+
});
|
|
3611
|
+
for (const message of errors) configDiags.push({
|
|
3612
|
+
ruleId: rule.meta.id,
|
|
3613
|
+
severity: "error",
|
|
3614
|
+
message
|
|
3615
|
+
});
|
|
3616
|
+
cached = {
|
|
3617
|
+
ok: errors.length === 0,
|
|
3618
|
+
diagnostics: configDiags
|
|
3619
|
+
};
|
|
3620
|
+
VALIDATION_CACHE.set(cacheKey, cached);
|
|
3621
|
+
}
|
|
3622
|
+
if (cached.diagnostics.length > 0) if (configDiagnosticsSink) {
|
|
3623
|
+
for (const d of cached.diagnostics) if (!configDiagnosticsSink.some((x) => x.ruleId === d.ruleId && x.message === d.message)) configDiagnosticsSink.push(d);
|
|
3624
|
+
} else for (const d of cached.diagnostics) (d.severity === "error" ? console.error : console.warn)(`[pyreon-lint] ${d.message}`);
|
|
3625
|
+
if (!cached.ok) continue;
|
|
3626
|
+
const ctx = createRuleContext(rule, severity, options, diagnostics, lineIndex, sourceText, filePath);
|
|
2881
3627
|
allCallbacks.push(rule.create(ctx));
|
|
2882
3628
|
}
|
|
2883
3629
|
new Visitor(mergeCallbacks(allCallbacks)).visit(program);
|
|
2884
3630
|
const lines = sourceText.split("\n");
|
|
3631
|
+
const SUPPRESS_RE = /^\/\/\s*pyreon-lint-(?:ignore|disable-next-line)(?:\s+(\S+))?\s*$/;
|
|
2885
3632
|
const filtered = diagnostics.filter((d) => {
|
|
2886
3633
|
const prevLineIdx = d.loc.line - 2;
|
|
2887
3634
|
if (prevLineIdx < 0) return true;
|
|
2888
|
-
const prevLine = lines[prevLineIdx]?.trim();
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3635
|
+
const prevLine = lines[prevLineIdx]?.trim() ?? "";
|
|
3636
|
+
const match = SUPPRESS_RE.exec(prevLine);
|
|
3637
|
+
if (!match) return true;
|
|
3638
|
+
const ruleId = match[1];
|
|
3639
|
+
if (!ruleId) return false;
|
|
3640
|
+
return ruleId !== d.ruleId;
|
|
2892
3641
|
});
|
|
2893
3642
|
filtered.sort((a, b) => a.span.start - b.span.start);
|
|
2894
3643
|
return {
|
|
@@ -2966,8 +3715,17 @@ function buildConfig(options) {
|
|
|
2966
3715
|
const cwd = resolve(".");
|
|
2967
3716
|
const fileConfig = options.config ? loadConfigFromPath(options.config) : loadConfig(cwd);
|
|
2968
3717
|
const config = getPreset(options.preset ?? fileConfig?.preset ?? "recommended");
|
|
2969
|
-
if (fileConfig?.rules) for (const [id,
|
|
3718
|
+
if (fileConfig?.rules) for (const [id, entry] of Object.entries(fileConfig.rules)) config.rules[id] = entry;
|
|
2970
3719
|
if (options.ruleOverrides) for (const [id, severity] of Object.entries(options.ruleOverrides)) config.rules[id] = severity;
|
|
3720
|
+
if (options.ruleOptionsOverrides) for (const [id, optionOverrides] of Object.entries(options.ruleOptionsOverrides)) {
|
|
3721
|
+
const existing = config.rules[id];
|
|
3722
|
+
const [currentSeverity, currentOptions] = Array.isArray(existing) ? [existing[0], existing[1] ?? {}] : [existing ?? "off", {}];
|
|
3723
|
+
if (currentSeverity === "off") continue;
|
|
3724
|
+
config.rules[id] = [currentSeverity, {
|
|
3725
|
+
...currentOptions,
|
|
3726
|
+
...optionOverrides
|
|
3727
|
+
}];
|
|
3728
|
+
}
|
|
2971
3729
|
return {
|
|
2972
3730
|
config,
|
|
2973
3731
|
include: fileConfig?.include,
|
|
@@ -3017,11 +3775,13 @@ function lint(options) {
|
|
|
3017
3775
|
const { config, include, exclude, isIgnored } = buildConfig(options);
|
|
3018
3776
|
const cache = new AstCache();
|
|
3019
3777
|
const files = gatherFiles(options.paths, isIgnored, include, exclude);
|
|
3778
|
+
const configDiagnostics = [];
|
|
3020
3779
|
const results = {
|
|
3021
3780
|
files: [],
|
|
3022
3781
|
totalErrors: 0,
|
|
3023
3782
|
totalWarnings: 0,
|
|
3024
|
-
totalInfos: 0
|
|
3783
|
+
totalInfos: 0,
|
|
3784
|
+
configDiagnostics
|
|
3025
3785
|
};
|
|
3026
3786
|
for (const filePath of files) {
|
|
3027
3787
|
let source;
|
|
@@ -3030,7 +3790,7 @@ function lint(options) {
|
|
|
3030
3790
|
} catch {
|
|
3031
3791
|
continue;
|
|
3032
3792
|
}
|
|
3033
|
-
const fileResult = lintFile(filePath, source, allRules, config, cache);
|
|
3793
|
+
const fileResult = lintFile(filePath, source, allRules, config, cache, configDiagnostics);
|
|
3034
3794
|
if (options.fix) applyFixesToFile(fileResult, source);
|
|
3035
3795
|
if (options.quiet) fileResult.diagnostics = fileResult.diagnostics.filter((d) => d.severity === "error");
|
|
3036
3796
|
countDiagnostics(fileResult, results);
|
|
@@ -3137,7 +3897,7 @@ function formatCompact(result) {
|
|
|
3137
3897
|
* @module
|
|
3138
3898
|
*/
|
|
3139
3899
|
const cache = new AstCache();
|
|
3140
|
-
|
|
3900
|
+
let config = getPreset("recommended");
|
|
3141
3901
|
function toLspDiagnostics(diagnostics) {
|
|
3142
3902
|
return diagnostics.map((d) => ({
|
|
3143
3903
|
range: {
|
|
@@ -3312,6 +4072,7 @@ function watchAndLint(options) {
|
|
|
3312
4072
|
const cache = new AstCache();
|
|
3313
4073
|
const config = getPreset(options.preset ?? "recommended");
|
|
3314
4074
|
applyOverrides(config, options.ruleOverrides);
|
|
4075
|
+
applyOptionsOverrides(config, options.ruleOptionsOverrides);
|
|
3315
4076
|
const isIgnored = createIgnoreFilter(resolve("."), options.ignore);
|
|
3316
4077
|
const pending = /* @__PURE__ */ new Map();
|
|
3317
4078
|
console.log(`\x1b[2m[pyreon-lint] Watching for changes...\x1b[0m\n`);
|
|
@@ -3338,6 +4099,18 @@ function applyOverrides(config, overrides) {
|
|
|
3338
4099
|
if (!overrides) return;
|
|
3339
4100
|
for (const [id, severity] of Object.entries(overrides)) config.rules[id] = severity;
|
|
3340
4101
|
}
|
|
4102
|
+
function applyOptionsOverrides(config, overrides) {
|
|
4103
|
+
if (!overrides) return;
|
|
4104
|
+
for (const [id, opts] of Object.entries(overrides)) {
|
|
4105
|
+
const existing = config.rules[id];
|
|
4106
|
+
const [severity, current] = Array.isArray(existing) ? [existing[0], existing[1] ?? {}] : [existing ?? "off", {}];
|
|
4107
|
+
if (severity === "off") continue;
|
|
4108
|
+
config.rules[id] = [severity, {
|
|
4109
|
+
...current,
|
|
4110
|
+
...opts
|
|
4111
|
+
}];
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
3341
4114
|
function relintFile(filePath, config, cache, format) {
|
|
3342
4115
|
let source;
|
|
3343
4116
|
try {
|
|
@@ -3351,7 +4124,8 @@ function relintFile(filePath, config, cache, format) {
|
|
|
3351
4124
|
files: [fileResult],
|
|
3352
4125
|
totalErrors: 0,
|
|
3353
4126
|
totalWarnings: 0,
|
|
3354
|
-
totalInfos: 0
|
|
4127
|
+
totalInfos: 0,
|
|
4128
|
+
configDiagnostics: []
|
|
3355
4129
|
};
|
|
3356
4130
|
for (const d of fileResult.diagnostics) if (d.severity === "error") result.totalErrors++;
|
|
3357
4131
|
else if (d.severity === "warn") result.totalWarnings++;
|
|
@@ -3361,5 +4135,5 @@ function relintFile(filePath, config, cache, format) {
|
|
|
3361
4135
|
}
|
|
3362
4136
|
|
|
3363
4137
|
//#endregion
|
|
3364
|
-
export { AstCache, LineIndex, allRules, applyFixes, createIgnoreFilter, extractImportInfo, formatCompact, formatJSON, formatText, getLocalName, getPreset, importsName, isPyreonImport, isPyreonPackage, lint, lintFile, listRules, loadConfig, loadConfigFromPath, startLspServer, watchAndLint };
|
|
4138
|
+
export { AstCache, LineIndex, allRules, applyFixes, createIgnoreFilter, extractImportInfo, formatCompact, formatJSON, formatText, getLocalName, getPreset, importsName, isPathExempt, isPyreonImport, isPyreonPackage, isTestFile, lint, lintFile, listRules, loadConfig, loadConfigFromPath, startLspServer, watchAndLint };
|
|
3365
4139
|
//# sourceMappingURL=index.js.map
|