oxlint-plugin-react-doctor 0.2.10 → 0.2.11-dev.f036b0f
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/dist/index.d.ts +9 -36
- package/dist/index.js +102 -203
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -82,10 +82,17 @@ interface ScopeAnalysis {
|
|
|
82
82
|
//#region src/plugin/utils/rule-context.d.ts
|
|
83
83
|
interface BaseRuleContext {
|
|
84
84
|
report: (descriptor: ReportDescriptor) => void;
|
|
85
|
-
|
|
85
|
+
readonly filename?: string;
|
|
86
|
+
/**
|
|
87
|
+
* @deprecated Rules use `context.filename`. Read only as a fallback by
|
|
88
|
+
* `wrapWithSemanticContext`; ESLint implements it as a `this`-bound class
|
|
89
|
+
* method, so it must be called on the host context, never a detached
|
|
90
|
+
* reference.
|
|
91
|
+
*/
|
|
92
|
+
getFilename?: () => string | undefined;
|
|
86
93
|
readonly settings?: Readonly<Record<string, unknown>>;
|
|
87
94
|
}
|
|
88
|
-
interface RuleContext extends BaseRuleContext {
|
|
95
|
+
interface RuleContext extends Omit<BaseRuleContext, "getFilename"> {
|
|
89
96
|
readonly scopes: ScopeAnalysis;
|
|
90
97
|
readonly cfg: ControlFlowAnalysis;
|
|
91
98
|
}
|
|
@@ -3954,23 +3961,6 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
3954
3961
|
readonly recommendation?: string;
|
|
3955
3962
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
3956
3963
|
};
|
|
3957
|
-
}, {
|
|
3958
|
-
readonly key: "react-doctor/react-compiler-destructure-method";
|
|
3959
|
-
readonly id: "react-compiler-destructure-method";
|
|
3960
|
-
readonly source: "react-doctor";
|
|
3961
|
-
readonly originallyExternal: false;
|
|
3962
|
-
readonly rule: {
|
|
3963
|
-
readonly framework: "global";
|
|
3964
|
-
readonly category: "Architecture";
|
|
3965
|
-
readonly id: string;
|
|
3966
|
-
readonly severity: RuleSeverity;
|
|
3967
|
-
readonly requires?: ReadonlyArray<string>;
|
|
3968
|
-
readonly disabledBy?: ReadonlyArray<string>;
|
|
3969
|
-
readonly tags?: ReadonlyArray<string>;
|
|
3970
|
-
readonly defaultEnabled?: boolean;
|
|
3971
|
-
readonly recommendation?: string;
|
|
3972
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
3973
|
-
};
|
|
3974
3964
|
}, {
|
|
3975
3965
|
readonly key: "react-doctor/react-compiler-no-manual-memoization";
|
|
3976
3966
|
readonly id: "react-compiler-no-manual-memoization";
|
|
@@ -9239,23 +9229,6 @@ declare const RULES: readonly [{
|
|
|
9239
9229
|
readonly recommendation?: string;
|
|
9240
9230
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
9241
9231
|
};
|
|
9242
|
-
}, {
|
|
9243
|
-
readonly key: "react-doctor/react-compiler-destructure-method";
|
|
9244
|
-
readonly id: "react-compiler-destructure-method";
|
|
9245
|
-
readonly source: "react-doctor";
|
|
9246
|
-
readonly originallyExternal: false;
|
|
9247
|
-
readonly rule: {
|
|
9248
|
-
readonly framework: "global";
|
|
9249
|
-
readonly category: "Architecture";
|
|
9250
|
-
readonly id: string;
|
|
9251
|
-
readonly severity: RuleSeverity;
|
|
9252
|
-
readonly requires?: ReadonlyArray<string>;
|
|
9253
|
-
readonly disabledBy?: ReadonlyArray<string>;
|
|
9254
|
-
readonly tags?: ReadonlyArray<string>;
|
|
9255
|
-
readonly defaultEnabled?: boolean;
|
|
9256
|
-
readonly recommendation?: string;
|
|
9257
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
9258
|
-
};
|
|
9259
9232
|
}, {
|
|
9260
9233
|
readonly key: "react-doctor/react-compiler-no-manual-memoization";
|
|
9261
9234
|
readonly id: "react-compiler-no-manual-memoization";
|
package/dist/index.js
CHANGED
|
@@ -223,7 +223,7 @@ const jsxAttributeIsNonReactDialectMarker = (openingNode) => {
|
|
|
223
223
|
//#endregion
|
|
224
224
|
//#region src/plugin/utils/define-rule.ts
|
|
225
225
|
const wrapCreateForTestNoise = (create) => ((context) => {
|
|
226
|
-
if (isTestlikeFilename(context.
|
|
226
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
227
227
|
return create(context);
|
|
228
228
|
});
|
|
229
229
|
const VISITOR_NODE_NAME_PATTERN = /^[A-Z]/;
|
|
@@ -813,6 +813,12 @@ const getElementType = (openingElement, settings) => {
|
|
|
813
813
|
return baseName;
|
|
814
814
|
};
|
|
815
815
|
//#endregion
|
|
816
|
+
//#region src/plugin/utils/is-nextjs-metadata-image-route-filename.ts
|
|
817
|
+
const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
818
|
+
if (!rawFilename) return false;
|
|
819
|
+
return /^(opengraph-image|twitter-image|icon|apple-icon)\d*\.(jsx?|tsx?)$/.test(path.basename(rawFilename));
|
|
820
|
+
};
|
|
821
|
+
//#endregion
|
|
816
822
|
//#region src/plugin/utils/is-hidden-from-screen-reader.ts
|
|
817
823
|
const isHiddenFromScreenReader = (openingElement, settings) => {
|
|
818
824
|
if (getElementType(openingElement, settings).toLowerCase() === "input") {
|
|
@@ -983,6 +989,7 @@ const altText = defineRule({
|
|
|
983
989
|
recommendation: "Provide `alt` (or aria-label / aria-labelledby) for non-decorative images.",
|
|
984
990
|
category: "Accessibility",
|
|
985
991
|
create: (context) => {
|
|
992
|
+
if (isNextjsMetadataImageRouteFilename(context.filename)) return {};
|
|
986
993
|
const settings = resolveSettings$53(context.settings);
|
|
987
994
|
const checkImg = !settings.elements || settings.elements.includes("img");
|
|
988
995
|
const checkObject = !settings.elements || settings.elements.includes("object");
|
|
@@ -2712,6 +2719,17 @@ const asyncAwaitInLoop = defineRule({
|
|
|
2712
2719
|
}
|
|
2713
2720
|
});
|
|
2714
2721
|
//#endregion
|
|
2722
|
+
//#region src/plugin/constants/ts-type-position-keys.ts
|
|
2723
|
+
const TYPE_POSITION_CHILD_KEYS = new Set([
|
|
2724
|
+
"implements",
|
|
2725
|
+
"returnType",
|
|
2726
|
+
"superTypeArguments",
|
|
2727
|
+
"superTypeParameters",
|
|
2728
|
+
"typeAnnotation",
|
|
2729
|
+
"typeArguments",
|
|
2730
|
+
"typeParameters"
|
|
2731
|
+
]);
|
|
2732
|
+
//#endregion
|
|
2715
2733
|
//#region src/plugin/utils/collect-pattern-names.ts
|
|
2716
2734
|
const collectPatternNames = (pattern, into) => {
|
|
2717
2735
|
if (!pattern) return;
|
|
@@ -2741,14 +2759,6 @@ const collectPatternNames = (pattern, into) => {
|
|
|
2741
2759
|
};
|
|
2742
2760
|
//#endregion
|
|
2743
2761
|
//#region src/plugin/utils/collect-reference-identifier-names.ts
|
|
2744
|
-
const TYPE_POSITION_KEYS = new Set([
|
|
2745
|
-
"typeAnnotation",
|
|
2746
|
-
"typeParameters",
|
|
2747
|
-
"typeArguments",
|
|
2748
|
-
"returnType",
|
|
2749
|
-
"superTypeArguments",
|
|
2750
|
-
"superTypeParameters"
|
|
2751
|
-
]);
|
|
2752
2762
|
const collectScopedReferencesInPattern = (pattern, into, shadowed) => {
|
|
2753
2763
|
if (!pattern) return;
|
|
2754
2764
|
if (isNodeOfType(pattern, "Identifier")) return;
|
|
@@ -2809,7 +2819,7 @@ const collectScopedReferenceIdentifierNames = (node, into, shadowed) => {
|
|
|
2809
2819
|
if (typeof node.type === "string" && node.type.startsWith("TS")) return;
|
|
2810
2820
|
for (const [key, child] of Object.entries(node)) {
|
|
2811
2821
|
if (key === "parent") continue;
|
|
2812
|
-
if (
|
|
2822
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
2813
2823
|
if (Array.isArray(child)) {
|
|
2814
2824
|
for (const item of child) if (isAstNode(item)) collectScopedReferenceIdentifierNames(item, into, shadowed);
|
|
2815
2825
|
} else if (isAstNode(child)) collectScopedReferenceIdentifierNames(child, into, shadowed);
|
|
@@ -3127,7 +3137,7 @@ const asyncParallel = defineRule({
|
|
|
3127
3137
|
severity: "warn",
|
|
3128
3138
|
recommendation: "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
|
|
3129
3139
|
create: (context) => {
|
|
3130
|
-
const filename = normalizeFilename$1(context.
|
|
3140
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
3131
3141
|
const isBrowserTestFile = BROWSER_TEST_FILE_PATTERN.test(filename);
|
|
3132
3142
|
let hasTestLibraryImport = false;
|
|
3133
3143
|
const shouldSkipFile = () => isBrowserTestFile || hasTestLibraryImport;
|
|
@@ -3330,7 +3340,7 @@ const buttonHasType = defineRule({
|
|
|
3330
3340
|
recommendation: "Set `type=\"button\"` (or `\"submit\"` / `\"reset\"`) explicitly on every `<button>`.",
|
|
3331
3341
|
create: (context) => {
|
|
3332
3342
|
const settings = resolveSettings$48(context.settings);
|
|
3333
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3343
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3334
3344
|
return {
|
|
3335
3345
|
JSXOpeningElement(node) {
|
|
3336
3346
|
if (isTestlikeFile) return;
|
|
@@ -3530,7 +3540,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
3530
3540
|
recommendation: "Pair `onClick` with `onKeyUp` / `onKeyDown` / `onKeyPress` for keyboard users.",
|
|
3531
3541
|
category: "Accessibility",
|
|
3532
3542
|
create: (context) => {
|
|
3533
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3543
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3534
3544
|
return { JSXOpeningElement(node) {
|
|
3535
3545
|
if (isTestlikeFile) return;
|
|
3536
3546
|
const tag = getElementType(node, context.settings);
|
|
@@ -3791,7 +3801,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3791
3801
|
category: "Accessibility",
|
|
3792
3802
|
create: (context) => {
|
|
3793
3803
|
const settings = resolveSettings$46(context.settings);
|
|
3794
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3804
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3795
3805
|
return { JSXElement(node) {
|
|
3796
3806
|
if (isTestlikeFile) return;
|
|
3797
3807
|
const opening = node.openingElement;
|
|
@@ -4928,6 +4938,33 @@ const inferReferenceFlag = (identifier) => {
|
|
|
4928
4938
|
const setNodeScope = (node, state) => {
|
|
4929
4939
|
state.nodeScope.set(node, state.currentScope);
|
|
4930
4940
|
};
|
|
4941
|
+
const walkParameterReferences = (pattern, state) => {
|
|
4942
|
+
if (isNodeOfType(pattern, "AssignmentPattern")) {
|
|
4943
|
+
walkParameterReferences(pattern.left, state);
|
|
4944
|
+
const defaultValue = pattern.right ?? null;
|
|
4945
|
+
if (defaultValue) walk(defaultValue, state);
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
if (isNodeOfType(pattern, "ObjectPattern")) {
|
|
4949
|
+
for (const property of pattern.properties) {
|
|
4950
|
+
const propertyNode = property;
|
|
4951
|
+
if (isNodeOfType(propertyNode, "RestElement")) {
|
|
4952
|
+
walkParameterReferences(propertyNode.argument, state);
|
|
4953
|
+
continue;
|
|
4954
|
+
}
|
|
4955
|
+
if (!isNodeOfType(propertyNode, "Property")) continue;
|
|
4956
|
+
const propertyDetail = propertyNode;
|
|
4957
|
+
if (propertyDetail.computed) walk(propertyDetail.key, state);
|
|
4958
|
+
walkParameterReferences(propertyDetail.value, state);
|
|
4959
|
+
}
|
|
4960
|
+
return;
|
|
4961
|
+
}
|
|
4962
|
+
if (isNodeOfType(pattern, "ArrayPattern")) {
|
|
4963
|
+
for (const element of pattern.elements) if (element) walkParameterReferences(element, state);
|
|
4964
|
+
return;
|
|
4965
|
+
}
|
|
4966
|
+
if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
|
|
4967
|
+
};
|
|
4931
4968
|
const walk = (node, state) => {
|
|
4932
4969
|
if (isFunctionLike$1(node)) {
|
|
4933
4970
|
if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
|
|
@@ -4943,7 +4980,9 @@ const walk = (node, state) => {
|
|
|
4943
4980
|
});
|
|
4944
4981
|
tagAsBinding(state, node.id);
|
|
4945
4982
|
}
|
|
4946
|
-
|
|
4983
|
+
const functionParams = node.params ?? [];
|
|
4984
|
+
handleFunctionParameters(functionParams, fnScope, state);
|
|
4985
|
+
for (const param of functionParams) walkParameterReferences(param, state);
|
|
4947
4986
|
const body = node.body;
|
|
4948
4987
|
if (body) walk(body, state);
|
|
4949
4988
|
popScope(state);
|
|
@@ -4985,6 +5024,7 @@ const walk = (node, state) => {
|
|
|
4985
5024
|
const nodeRecord = node;
|
|
4986
5025
|
for (const key of Object.keys(nodeRecord)) {
|
|
4987
5026
|
if (key === "parent") continue;
|
|
5027
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
4988
5028
|
const child = nodeRecord[key];
|
|
4989
5029
|
if (Array.isArray(child)) {
|
|
4990
5030
|
for (const item of child) if (isAstNode(item)) walk(item, state);
|
|
@@ -5047,6 +5087,7 @@ const walk = (node, state) => {
|
|
|
5047
5087
|
const nodeRecord = node;
|
|
5048
5088
|
for (const key of Object.keys(nodeRecord)) {
|
|
5049
5089
|
if (key === "parent") continue;
|
|
5090
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
5050
5091
|
const child = nodeRecord[key];
|
|
5051
5092
|
if (Array.isArray(child)) {
|
|
5052
5093
|
for (const item of child) if (isAstNode(item)) walk(item, state);
|
|
@@ -5166,14 +5207,6 @@ const isAstDescendant = (inner, outer) => {
|
|
|
5166
5207
|
};
|
|
5167
5208
|
//#endregion
|
|
5168
5209
|
//#region src/plugin/semantic/closure-captures.ts
|
|
5169
|
-
const TYPE_ONLY_CHILD_KEYS = new Set([
|
|
5170
|
-
"implements",
|
|
5171
|
-
"returnType",
|
|
5172
|
-
"superTypeArguments",
|
|
5173
|
-
"typeAnnotation",
|
|
5174
|
-
"typeArguments",
|
|
5175
|
-
"typeParameters"
|
|
5176
|
-
]);
|
|
5177
5210
|
const closureCaptures = (functionNode, scopes) => {
|
|
5178
5211
|
const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
|
|
5179
5212
|
const out = [];
|
|
@@ -5201,7 +5234,7 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
5201
5234
|
const record = node;
|
|
5202
5235
|
for (const key of Object.keys(record)) {
|
|
5203
5236
|
if (key === "parent") continue;
|
|
5204
|
-
if (
|
|
5237
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
5205
5238
|
const child = record[key];
|
|
5206
5239
|
if (Array.isArray(child)) {
|
|
5207
5240
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
@@ -5564,33 +5597,6 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
5564
5597
|
if (!depKey) continue;
|
|
5565
5598
|
keys.add(depKey);
|
|
5566
5599
|
}
|
|
5567
|
-
const functionParams = callback.params ?? [];
|
|
5568
|
-
for (const param of functionParams) {
|
|
5569
|
-
if (!isNodeOfType(param, "AssignmentPattern")) continue;
|
|
5570
|
-
const visitDefaultValue = (node) => {
|
|
5571
|
-
if (isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression")) {
|
|
5572
|
-
const depKey = stringifyMemberChain(node);
|
|
5573
|
-
if (depKey) keys.add(depKey);
|
|
5574
|
-
}
|
|
5575
|
-
const reference = scopes.referenceFor(node);
|
|
5576
|
-
if (reference?.resolvedSymbol) {
|
|
5577
|
-
const symbol = reference.resolvedSymbol;
|
|
5578
|
-
if (!isOutsideAllFunctions(symbol)) {
|
|
5579
|
-
const depKey = computeDepKey(reference);
|
|
5580
|
-
if (depKey) keys.add(depKey);
|
|
5581
|
-
}
|
|
5582
|
-
}
|
|
5583
|
-
const record = node;
|
|
5584
|
-
for (const key of Object.keys(record)) {
|
|
5585
|
-
if (key === "parent") continue;
|
|
5586
|
-
const child = record[key];
|
|
5587
|
-
if (Array.isArray(child)) {
|
|
5588
|
-
for (const item of child) if (isAstNode(item)) visitDefaultValue(item);
|
|
5589
|
-
} else if (isAstNode(child)) visitDefaultValue(child);
|
|
5590
|
-
}
|
|
5591
|
-
};
|
|
5592
|
-
visitDefaultValue(param.right);
|
|
5593
|
-
}
|
|
5594
5600
|
return {
|
|
5595
5601
|
keys,
|
|
5596
5602
|
stableCapturedNames
|
|
@@ -8423,6 +8429,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
8423
8429
|
id: "js-tosorted-immutable",
|
|
8424
8430
|
tags: ["test-noise"],
|
|
8425
8431
|
severity: "warn",
|
|
8432
|
+
disabledBy: ["react-native"],
|
|
8426
8433
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` for immutable sorting without the spread allocation",
|
|
8427
8434
|
create: (context) => ({ CallExpression(node) {
|
|
8428
8435
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
@@ -8711,7 +8718,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
8711
8718
|
const settings = resolveSettings$34(context.settings);
|
|
8712
8719
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
8713
8720
|
const allowedList = [...allowedExtensions].map((extension) => `.${extension}`).join(", ");
|
|
8714
|
-
const filename =
|
|
8721
|
+
const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
|
|
8715
8722
|
const extensionOnly = path.extname(filename).slice(1);
|
|
8716
8723
|
const fileHasAllowedExtension = allowedExtensions.has(extensionOnly);
|
|
8717
8724
|
let didReportMismatch = false;
|
|
@@ -9495,7 +9502,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
9495
9502
|
recommendation: "Memoize the context value (`useMemo`) or hoist it outside the render.",
|
|
9496
9503
|
category: "Performance",
|
|
9497
9504
|
create: (context) => {
|
|
9498
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
9505
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9499
9506
|
return { JSXOpeningElement(node) {
|
|
9500
9507
|
if (isTestlikeFile) return;
|
|
9501
9508
|
if (!isProviderName(node.name)) return;
|
|
@@ -9841,7 +9848,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
9841
9848
|
recommendation: "Hoist the inner JSX outside the render or memoize via `useMemo`.",
|
|
9842
9849
|
category: "Performance",
|
|
9843
9850
|
create: (context) => {
|
|
9844
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
9851
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9845
9852
|
let memoRegistry = null;
|
|
9846
9853
|
return {
|
|
9847
9854
|
Program(node) {
|
|
@@ -10212,7 +10219,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
10212
10219
|
recommendation: "Memoize the array (`useMemo`) or hoist it outside the component.",
|
|
10213
10220
|
category: "Performance",
|
|
10214
10221
|
create: (context) => {
|
|
10215
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10222
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10216
10223
|
let memoRegistry = null;
|
|
10217
10224
|
return {
|
|
10218
10225
|
Program(node) {
|
|
@@ -10674,7 +10681,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
10674
10681
|
recommendation: "Memoize the callback (`useCallback`) or hoist it outside the component.",
|
|
10675
10682
|
category: "Performance",
|
|
10676
10683
|
create: (context) => {
|
|
10677
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10684
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10678
10685
|
let memoRegistry = null;
|
|
10679
10686
|
return {
|
|
10680
10687
|
Program(node) {
|
|
@@ -10980,7 +10987,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10980
10987
|
recommendation: "Memoize the object (`useMemo`) or hoist it outside the component.",
|
|
10981
10988
|
category: "Performance",
|
|
10982
10989
|
create: (context) => {
|
|
10983
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10990
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10984
10991
|
let memoRegistry = null;
|
|
10985
10992
|
return {
|
|
10986
10993
|
Program(node) {
|
|
@@ -11785,7 +11792,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
11785
11792
|
category: "Accessibility",
|
|
11786
11793
|
create: (context) => {
|
|
11787
11794
|
const settings = resolveSettings$24(context.settings);
|
|
11788
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
11795
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
11789
11796
|
return { JSXElement(node) {
|
|
11790
11797
|
if (isTestlikeFile) return;
|
|
11791
11798
|
const opening = node.openingElement;
|
|
@@ -12323,7 +12330,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
12323
12330
|
severity: "warn",
|
|
12324
12331
|
recommendation: "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
|
|
12325
12332
|
create: (context) => ({ Program(programNode) {
|
|
12326
|
-
const filename = normalizeFilename$1(context.
|
|
12333
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12327
12334
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
12328
12335
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
12329
12336
|
if (!programNode.body?.some((statement) => {
|
|
@@ -12388,7 +12395,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
12388
12395
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
12389
12396
|
const callback = getEffectCallback(node);
|
|
12390
12397
|
if (!callback || !containsFetchCall(callback)) return;
|
|
12391
|
-
const filename = normalizeFilename$1(context.
|
|
12398
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12392
12399
|
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || PAGES_DIRECTORY_PATTERN.test(filename)) context.report({
|
|
12393
12400
|
node,
|
|
12394
12401
|
message: "useEffect + fetch in a page/layout — fetch data server-side with a server component instead"
|
|
@@ -12421,7 +12428,7 @@ const nextjsNoClientSideRedirect = defineRule({
|
|
|
12421
12428
|
severity: "warn",
|
|
12422
12429
|
recommendation: "Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)",
|
|
12423
12430
|
create: (context) => {
|
|
12424
|
-
const filename = normalizeFilename$1(context.
|
|
12431
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12425
12432
|
const isPagesRouterFile = PAGES_DIRECTORY_PATTERN.test(filename);
|
|
12426
12433
|
return { CallExpression(node) {
|
|
12427
12434
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
@@ -12490,7 +12497,7 @@ const nextjsNoHeadImport = defineRule({
|
|
|
12490
12497
|
recommendation: "Use the Metadata API instead: `export const metadata = { title: '...' }` or `export async function generateMetadata()`",
|
|
12491
12498
|
create: (context) => ({ ImportDeclaration(node) {
|
|
12492
12499
|
if (node.source?.value !== "next/head") return;
|
|
12493
|
-
const filename = normalizeFilename$1(context.
|
|
12500
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12494
12501
|
if (!APP_DIRECTORY_PATTERN.test(filename)) return;
|
|
12495
12502
|
context.report({
|
|
12496
12503
|
node,
|
|
@@ -12507,7 +12514,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
12507
12514
|
severity: "warn",
|
|
12508
12515
|
recommendation: "`import Image from 'next/image'` — provides automatic WebP/AVIF, lazy loading, and responsive srcset",
|
|
12509
12516
|
create: (context) => {
|
|
12510
|
-
const filename = normalizeFilename$1(context.
|
|
12517
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12511
12518
|
const isOgRoute = OG_ROUTE_PATTERN.test(filename);
|
|
12512
12519
|
return { JSXOpeningElement(node) {
|
|
12513
12520
|
if (isOgRoute) return;
|
|
@@ -12861,7 +12868,7 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
12861
12868
|
resolveBinding = buildProgramBindingLookup(node);
|
|
12862
12869
|
},
|
|
12863
12870
|
ExportNamedDeclaration(node) {
|
|
12864
|
-
const filename = normalizeFilename$1(context.
|
|
12871
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12865
12872
|
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
12866
12873
|
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
12867
12874
|
if (!isExportedGetHandler(node)) return;
|
|
@@ -13405,9 +13412,9 @@ const findContainingNode = (analysis, node) => {
|
|
|
13405
13412
|
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
13406
13413
|
const noAdjustStateOnPropChange = defineRule({
|
|
13407
13414
|
id: "no-adjust-state-on-prop-change",
|
|
13408
|
-
severity: "
|
|
13415
|
+
severity: "error",
|
|
13409
13416
|
tags: ["test-noise"],
|
|
13410
|
-
recommendation: "Adjust the state inline during render
|
|
13417
|
+
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
13411
13418
|
create: (context) => ({ CallExpression(node) {
|
|
13412
13419
|
if (!isUseEffect(node)) return;
|
|
13413
13420
|
const analysis = getProgramAnalysis(node);
|
|
@@ -13426,7 +13433,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
13426
13433
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
|
|
13427
13434
|
context.report({
|
|
13428
13435
|
node: callExpr,
|
|
13429
|
-
message: "
|
|
13436
|
+
message: "State adjusted in a useEffect when a prop changes — forces an extra render with a stale UI between the two commits. Adjust the state during render with a `prev`-prop comparison instead, or refactor to remove the duplicated state."
|
|
13430
13437
|
});
|
|
13431
13438
|
}
|
|
13432
13439
|
} })
|
|
@@ -14093,7 +14100,7 @@ const noAutofocus = defineRule({
|
|
|
14093
14100
|
category: "Accessibility",
|
|
14094
14101
|
create: (context) => {
|
|
14095
14102
|
const settings = resolveSettings$21(context.settings);
|
|
14096
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
14103
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
14097
14104
|
return { JSXOpeningElement(node) {
|
|
14098
14105
|
if (isTestlikeFile) return;
|
|
14099
14106
|
const autoFocusAttribute = node.attributes.find((attribute) => {
|
|
@@ -14467,7 +14474,7 @@ const noBarrelImport = defineRule({
|
|
|
14467
14474
|
if (didReportForFile) return;
|
|
14468
14475
|
const source = node.source?.value;
|
|
14469
14476
|
if (typeof source !== "string" || !source.startsWith(".")) return;
|
|
14470
|
-
const filename = normalizeFilename$1(context.
|
|
14477
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
14471
14478
|
if (!filename) return;
|
|
14472
14479
|
const importRequests = getRuntimeImportRequests(node);
|
|
14473
14480
|
if (importRequests.length === 0) return;
|
|
@@ -18246,7 +18253,7 @@ const noMultiComp = defineRule({
|
|
|
18246
18253
|
category: "Architecture",
|
|
18247
18254
|
create: (context) => {
|
|
18248
18255
|
const settings = resolveSettings$16(context.settings);
|
|
18249
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
18256
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
18250
18257
|
return { Program(node) {
|
|
18251
18258
|
if (isTestlikeFile) return;
|
|
18252
18259
|
const visitContext = {
|
|
@@ -20193,7 +20200,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
20193
20200
|
severity: "warn",
|
|
20194
20201
|
recommendation: "Move secrets to server-only code. Public client environment variables are bundled into browser code and must not contain secrets",
|
|
20195
20202
|
create: (context) => {
|
|
20196
|
-
const filename = normalizeFilename$1(context.
|
|
20203
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
20197
20204
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
20198
20205
|
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
20199
20206
|
let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
|
|
@@ -20448,7 +20455,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
20448
20455
|
category: "Accessibility",
|
|
20449
20456
|
create: (context) => {
|
|
20450
20457
|
const settings = resolveSettings$12(context.settings);
|
|
20451
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
20458
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
20452
20459
|
return { JSXOpeningElement(node) {
|
|
20453
20460
|
if (isTestlikeFile) return;
|
|
20454
20461
|
let hasNonBlockerHandler = false;
|
|
@@ -20525,7 +20532,7 @@ const noStringRefs = defineRule({
|
|
|
20525
20532
|
recommendation: "Use a callback ref (`ref={(node) => { this.foo = node }}`) or `useRef` instead of string refs.",
|
|
20526
20533
|
create: (context) => {
|
|
20527
20534
|
const { noTemplateLiterals = false } = resolveSettings$11(context.settings);
|
|
20528
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
20535
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
20529
20536
|
return {
|
|
20530
20537
|
JSXAttribute(node) {
|
|
20531
20538
|
if (isTestlikeFile) return;
|
|
@@ -22660,7 +22667,7 @@ const isFileNameAllowed = (filename, checkJS) => {
|
|
|
22660
22667
|
};
|
|
22661
22668
|
const onlyExportComponents = defineRule({
|
|
22662
22669
|
id: "only-export-components",
|
|
22663
|
-
severity: "
|
|
22670
|
+
severity: "warn",
|
|
22664
22671
|
recommendation: "Move non-component exports out of files that export components.",
|
|
22665
22672
|
category: "Architecture",
|
|
22666
22673
|
create: (context) => {
|
|
@@ -22671,7 +22678,7 @@ const onlyExportComponents = defineRule({
|
|
|
22671
22678
|
allowConstantExport: settings.allowConstantExport
|
|
22672
22679
|
};
|
|
22673
22680
|
return { Program(node) {
|
|
22674
|
-
if (!isFileNameAllowed(
|
|
22681
|
+
if (!isFileNameAllowed(normalizeFilename$1(context.filename ?? ""), settings.checkJS)) return;
|
|
22675
22682
|
const allNodes = collectAllNodes(node);
|
|
22676
22683
|
const exports = [];
|
|
22677
22684
|
let hasReactExport = false;
|
|
@@ -23717,106 +23724,6 @@ const queryStableQueryClient = defineRule({
|
|
|
23717
23724
|
}
|
|
23718
23725
|
});
|
|
23719
23726
|
//#endregion
|
|
23720
|
-
//#region src/plugin/rules/architecture/react-compiler-destructure-method.ts
|
|
23721
|
-
const HOOK_OBJECTS_WITH_METHODS = new Map([
|
|
23722
|
-
["useRouter", new Set([
|
|
23723
|
-
"push",
|
|
23724
|
-
"replace",
|
|
23725
|
-
"back",
|
|
23726
|
-
"forward",
|
|
23727
|
-
"refresh",
|
|
23728
|
-
"prefetch"
|
|
23729
|
-
])],
|
|
23730
|
-
["useNavigation", new Set([
|
|
23731
|
-
"navigate",
|
|
23732
|
-
"push",
|
|
23733
|
-
"goBack",
|
|
23734
|
-
"popToTop",
|
|
23735
|
-
"reset",
|
|
23736
|
-
"replace",
|
|
23737
|
-
"dispatch"
|
|
23738
|
-
])],
|
|
23739
|
-
["useSearchParams", new Set([
|
|
23740
|
-
"get",
|
|
23741
|
-
"getAll",
|
|
23742
|
-
"has",
|
|
23743
|
-
"set"
|
|
23744
|
-
])]
|
|
23745
|
-
]);
|
|
23746
|
-
const HOOK_IMPORT_SOURCES_WITH_UNSAFE_METHOD_DESTRUCTURING = new Map([["useNavigation", new Set(["@react-navigation/native", "@react-navigation/core"])]]);
|
|
23747
|
-
const isUnsafeMethodDestructureHookImport = (node, hookSource) => {
|
|
23748
|
-
const moduleSources = HOOK_IMPORT_SOURCES_WITH_UNSAFE_METHOD_DESTRUCTURING.get(hookSource);
|
|
23749
|
-
if (!moduleSources) return false;
|
|
23750
|
-
for (const moduleSource of moduleSources) if (isImportedFromModule(node, hookSource, moduleSource)) return true;
|
|
23751
|
-
return false;
|
|
23752
|
-
};
|
|
23753
|
-
const buildHookBindingMap = (componentBody) => {
|
|
23754
|
-
const result = /* @__PURE__ */ new Map();
|
|
23755
|
-
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return result;
|
|
23756
|
-
for (const statement of componentBody.body ?? []) {
|
|
23757
|
-
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
23758
|
-
for (const declarator of statement.declarations ?? []) {
|
|
23759
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
23760
|
-
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
23761
|
-
const callee = declarator.init.callee;
|
|
23762
|
-
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
23763
|
-
result.set(declarator.id.name, callee.name);
|
|
23764
|
-
}
|
|
23765
|
-
}
|
|
23766
|
-
return result;
|
|
23767
|
-
};
|
|
23768
|
-
const reactCompilerDestructureMethod = defineRule({
|
|
23769
|
-
id: "react-compiler-destructure-method",
|
|
23770
|
-
tags: ["test-noise"],
|
|
23771
|
-
severity: "warn",
|
|
23772
|
-
recommendation: "Destructure the method up front: `const { push } = useRouter()` then call `push(...)` directly — clearer dependency graph and easier for React Compiler to memoize",
|
|
23773
|
-
create: (context) => {
|
|
23774
|
-
const hookBindingMapStack = [];
|
|
23775
|
-
const isComponent = (node) => {
|
|
23776
|
-
if (isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.id?.name && isUppercaseName(node.id.name));
|
|
23777
|
-
if (isNodeOfType(node, "VariableDeclarator")) return isComponentAssignment(node);
|
|
23778
|
-
return false;
|
|
23779
|
-
};
|
|
23780
|
-
const enter = (node) => {
|
|
23781
|
-
if (!isComponent(node)) return;
|
|
23782
|
-
let body;
|
|
23783
|
-
if (isNodeOfType(node, "FunctionDeclaration")) body = node.body;
|
|
23784
|
-
else if (isNodeOfType(node, "VariableDeclarator")) {
|
|
23785
|
-
const initializer = node.init;
|
|
23786
|
-
body = isInlineFunctionExpression(initializer) ? initializer.body : null;
|
|
23787
|
-
}
|
|
23788
|
-
hookBindingMapStack.push(buildHookBindingMap(body));
|
|
23789
|
-
};
|
|
23790
|
-
const exit = (node) => {
|
|
23791
|
-
if (isComponent(node)) hookBindingMapStack.pop();
|
|
23792
|
-
};
|
|
23793
|
-
return {
|
|
23794
|
-
FunctionDeclaration: enter,
|
|
23795
|
-
"FunctionDeclaration:exit": exit,
|
|
23796
|
-
VariableDeclarator: enter,
|
|
23797
|
-
"VariableDeclarator:exit": exit,
|
|
23798
|
-
MemberExpression(node) {
|
|
23799
|
-
if (hookBindingMapStack.length === 0) return;
|
|
23800
|
-
if (node.computed) return;
|
|
23801
|
-
if (!isNodeOfType(node.object, "Identifier")) return;
|
|
23802
|
-
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
23803
|
-
const bindingName = node.object.name;
|
|
23804
|
-
const methodName = node.property.name;
|
|
23805
|
-
const hookSource = hookBindingMapStack[hookBindingMapStack.length - 1].get(bindingName);
|
|
23806
|
-
if (!hookSource) return;
|
|
23807
|
-
const allowedMethods = HOOK_OBJECTS_WITH_METHODS.get(hookSource);
|
|
23808
|
-
if (!allowedMethods || !allowedMethods.has(methodName)) return;
|
|
23809
|
-
if (isUnsafeMethodDestructureHookImport(node, hookSource)) return;
|
|
23810
|
-
if (!isNodeOfType(node.parent, "CallExpression") || node.parent.callee !== node) return;
|
|
23811
|
-
context.report({
|
|
23812
|
-
node,
|
|
23813
|
-
message: `Destructure for clarity: \`const { ${methodName} } = ${hookSource}()\` then call \`${methodName}(...)\` directly — easier for React Compiler to memoize and clearer about which methods this component depends on`
|
|
23814
|
-
});
|
|
23815
|
-
}
|
|
23816
|
-
};
|
|
23817
|
-
}
|
|
23818
|
-
});
|
|
23819
|
-
//#endregion
|
|
23820
23727
|
//#region src/plugin/rules/architecture/react-compiler-no-manual-memoization.ts
|
|
23821
23728
|
const REMOVAL_MESSAGE_BY_REACT_API_NAME = new Map([
|
|
23822
23729
|
["useMemo", "Remove `useMemo` — React Compiler auto-memoizes every value in this component. Manual `useMemo` adds noise without improving performance."],
|
|
@@ -24192,7 +24099,7 @@ const renderingSvgPrecision = defineRule({
|
|
|
24192
24099
|
category: "Performance",
|
|
24193
24100
|
recommendation: "Truncate path/points/transform decimals to 1–2 digits — sub-pixel precision adds bytes with no visible difference",
|
|
24194
24101
|
create: (context) => {
|
|
24195
|
-
const filename = context.
|
|
24102
|
+
const filename = context.filename;
|
|
24196
24103
|
const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename$1(filename) : void 0);
|
|
24197
24104
|
return { JSXAttribute(node) {
|
|
24198
24105
|
if (isAutoGenerated) return;
|
|
@@ -25524,7 +25431,8 @@ const classifyPackagePlatform = (filename) => {
|
|
|
25524
25431
|
//#endregion
|
|
25525
25432
|
//#region src/plugin/utils/is-expo-managed-file.ts
|
|
25526
25433
|
const isExpoManagedFileActive = (context) => {
|
|
25527
|
-
const
|
|
25434
|
+
const rawFilename = context.filename;
|
|
25435
|
+
const filename = rawFilename ? normalizeFilename$1(rawFilename) : void 0;
|
|
25528
25436
|
if (filename) {
|
|
25529
25437
|
const packagePlatform = classifyPackagePlatform(filename);
|
|
25530
25438
|
if (packagePlatform === "expo") return true;
|
|
@@ -30692,7 +30600,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
30692
30600
|
let isServerSideFile = false;
|
|
30693
30601
|
return {
|
|
30694
30602
|
Program(node) {
|
|
30695
|
-
const filename = normalizeFilename$1(context.
|
|
30603
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30696
30604
|
if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
|
|
30697
30605
|
isServerSideFile = false;
|
|
30698
30606
|
return;
|
|
@@ -30805,7 +30713,7 @@ const serverHoistStaticIo = defineRule({
|
|
|
30805
30713
|
inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectIdentifierParams(declaration.params ?? []));
|
|
30806
30714
|
},
|
|
30807
30715
|
ExportDefaultDeclaration(node) {
|
|
30808
|
-
const filename = normalizeFilename$1(context.
|
|
30716
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30809
30717
|
if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
|
|
30810
30718
|
const declaration = node.declaration;
|
|
30811
30719
|
if (!declaration || !isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "FunctionExpression") && !isNodeOfType(declaration, "ArrowFunctionExpression")) return;
|
|
@@ -31356,7 +31264,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
31356
31264
|
};
|
|
31357
31265
|
return {
|
|
31358
31266
|
Program(node) {
|
|
31359
|
-
const filename = context.
|
|
31267
|
+
const filename = context.filename ?? "";
|
|
31360
31268
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31361
31269
|
const statements = node.body ?? [];
|
|
31362
31270
|
for (const statement of statements) collectImportBindings(statement);
|
|
@@ -31366,17 +31274,17 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
31366
31274
|
}
|
|
31367
31275
|
},
|
|
31368
31276
|
ImportDeclaration(node) {
|
|
31369
|
-
const filename = context.
|
|
31277
|
+
const filename = context.filename ?? "";
|
|
31370
31278
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31371
31279
|
collectImportBindings(node);
|
|
31372
31280
|
},
|
|
31373
31281
|
VariableDeclarator(node) {
|
|
31374
|
-
const filename = context.
|
|
31282
|
+
const filename = context.filename ?? "";
|
|
31375
31283
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31376
31284
|
collectVariableAlias(node);
|
|
31377
31285
|
},
|
|
31378
31286
|
JSXOpeningElement(node) {
|
|
31379
|
-
const filename = normalizeFilename$1(context.
|
|
31287
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31380
31288
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31381
31289
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
31382
31290
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
@@ -31391,7 +31299,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
31391
31299
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
31392
31300
|
},
|
|
31393
31301
|
"Program:exit"(programNode) {
|
|
31394
|
-
const filename = normalizeFilename$1(context.
|
|
31302
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31395
31303
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31396
31304
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
31397
31305
|
node: programNode,
|
|
@@ -31410,7 +31318,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
31410
31318
|
severity: "warn",
|
|
31411
31319
|
recommendation: "`import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
|
|
31412
31320
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31413
|
-
const filename = normalizeFilename$1(context.
|
|
31321
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31414
31322
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31415
31323
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
31416
31324
|
const hrefAttribute = (node.attributes ?? []).find((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "href");
|
|
@@ -31484,7 +31392,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
31484
31392
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && typeof node.name.name === "string" && node.name.name.startsWith("on") && UPPERCASE_PATTERN.test(node.name.name.charAt(2));
|
|
31485
31393
|
return {
|
|
31486
31394
|
CallExpression(node) {
|
|
31487
|
-
const filename = normalizeFilename$1(context.
|
|
31395
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31488
31396
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31489
31397
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
31490
31398
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
@@ -31494,17 +31402,17 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
31494
31402
|
});
|
|
31495
31403
|
},
|
|
31496
31404
|
"CallExpression:exit"(node) {
|
|
31497
|
-
const filename = normalizeFilename$1(context.
|
|
31405
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31498
31406
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31499
31407
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
31500
31408
|
},
|
|
31501
31409
|
JSXAttribute(node) {
|
|
31502
|
-
const filename = normalizeFilename$1(context.
|
|
31410
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31503
31411
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31504
31412
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
31505
31413
|
},
|
|
31506
31414
|
"JSXAttribute:exit"(node) {
|
|
31507
|
-
const filename = normalizeFilename$1(context.
|
|
31415
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31508
31416
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31509
31417
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
31510
31418
|
}
|
|
@@ -31585,7 +31493,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
31585
31493
|
severity: "warn",
|
|
31586
31494
|
recommendation: "Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfalls",
|
|
31587
31495
|
create: (context) => ({ CallExpression(node) {
|
|
31588
|
-
const filename = normalizeFilename$1(context.
|
|
31496
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
31589
31497
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
31590
31498
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
31591
31499
|
const callback = node.arguments?.[0];
|
|
@@ -34308,17 +34216,6 @@ const reactDoctorRules = [
|
|
|
34308
34216
|
category: "TanStack Query"
|
|
34309
34217
|
}
|
|
34310
34218
|
},
|
|
34311
|
-
{
|
|
34312
|
-
key: "react-doctor/react-compiler-destructure-method",
|
|
34313
|
-
id: "react-compiler-destructure-method",
|
|
34314
|
-
source: "react-doctor",
|
|
34315
|
-
originallyExternal: false,
|
|
34316
|
-
rule: {
|
|
34317
|
-
...reactCompilerDestructureMethod,
|
|
34318
|
-
framework: "global",
|
|
34319
|
-
category: "Architecture"
|
|
34320
|
-
}
|
|
34321
|
-
},
|
|
34322
34219
|
{
|
|
34323
34220
|
key: "react-doctor/react-compiler-no-manual-memoization",
|
|
34324
34221
|
id: "react-compiler-no-manual-memoization",
|
|
@@ -35254,7 +35151,7 @@ const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id,
|
|
|
35254
35151
|
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
35255
35152
|
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
35256
35153
|
const isReactNativeFileActive = (context) => {
|
|
35257
|
-
const rawFilename = context.
|
|
35154
|
+
const rawFilename = context.filename;
|
|
35258
35155
|
if (!rawFilename) return true;
|
|
35259
35156
|
const filename = normalizeFilename$1(rawFilename);
|
|
35260
35157
|
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
|
|
@@ -35742,7 +35639,9 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
35742
35639
|
};
|
|
35743
35640
|
const enrichedContext = {
|
|
35744
35641
|
report: baseContext.report,
|
|
35745
|
-
|
|
35642
|
+
get filename() {
|
|
35643
|
+
return baseContext.filename ?? baseContext.getFilename?.();
|
|
35644
|
+
},
|
|
35746
35645
|
settings: baseContext.settings,
|
|
35747
35646
|
get scopes() {
|
|
35748
35647
|
return getScopes();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-react-doctor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11-dev.f036b0f",
|
|
4
4
|
"description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|