oxlint-plugin-react-doctor 0.2.10 → 0.2.11-dev.d917f62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +43 -36
  2. package/dist/index.js +215 -248
  3. 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
- getFilename?: () => string;
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
  }
@@ -3104,6 +3111,23 @@ declare const REACT_DOCTOR_RULES: readonly [{
3104
3111
  readonly recommendation?: string;
3105
3112
  readonly create: (context: RuleContext) => RuleVisitors;
3106
3113
  };
3114
+ }, {
3115
+ readonly key: "react-doctor/no-prop-types";
3116
+ readonly id: "no-prop-types";
3117
+ readonly source: "react-doctor";
3118
+ readonly originallyExternal: false;
3119
+ readonly rule: {
3120
+ readonly framework: "global";
3121
+ readonly category: "Architecture";
3122
+ readonly id: string;
3123
+ readonly severity: RuleSeverity;
3124
+ readonly requires?: ReadonlyArray<string>;
3125
+ readonly disabledBy?: ReadonlyArray<string>;
3126
+ readonly tags?: ReadonlyArray<string>;
3127
+ readonly defaultEnabled?: boolean;
3128
+ readonly recommendation?: string;
3129
+ readonly create: (context: RuleContext) => RuleVisitors;
3130
+ };
3107
3131
  }, {
3108
3132
  readonly key: "react-doctor/no-pure-black-background";
3109
3133
  readonly id: "no-pure-black-background";
@@ -3954,23 +3978,6 @@ declare const REACT_DOCTOR_RULES: readonly [{
3954
3978
  readonly recommendation?: string;
3955
3979
  readonly create: (context: RuleContext) => RuleVisitors;
3956
3980
  };
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
3981
  }, {
3975
3982
  readonly key: "react-doctor/react-compiler-no-manual-memoization";
3976
3983
  readonly id: "react-compiler-no-manual-memoization";
@@ -8389,6 +8396,23 @@ declare const RULES: readonly [{
8389
8396
  readonly recommendation?: string;
8390
8397
  readonly create: (context: RuleContext) => RuleVisitors;
8391
8398
  };
8399
+ }, {
8400
+ readonly key: "react-doctor/no-prop-types";
8401
+ readonly id: "no-prop-types";
8402
+ readonly source: "react-doctor";
8403
+ readonly originallyExternal: false;
8404
+ readonly rule: {
8405
+ readonly framework: "global";
8406
+ readonly category: "Architecture";
8407
+ readonly id: string;
8408
+ readonly severity: RuleSeverity;
8409
+ readonly requires?: ReadonlyArray<string>;
8410
+ readonly disabledBy?: ReadonlyArray<string>;
8411
+ readonly tags?: ReadonlyArray<string>;
8412
+ readonly defaultEnabled?: boolean;
8413
+ readonly recommendation?: string;
8414
+ readonly create: (context: RuleContext) => RuleVisitors;
8415
+ };
8392
8416
  }, {
8393
8417
  readonly key: "react-doctor/no-pure-black-background";
8394
8418
  readonly id: "no-pure-black-background";
@@ -9239,23 +9263,6 @@ declare const RULES: readonly [{
9239
9263
  readonly recommendation?: string;
9240
9264
  readonly create: (context: RuleContext) => RuleVisitors;
9241
9265
  };
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
9266
  }, {
9260
9267
  readonly key: "react-doctor/react-compiler-no-manual-memoization";
9261
9268
  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.getFilename?.())) return {};
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");
@@ -1019,7 +1026,7 @@ const altText = defineRule({
1019
1026
  });
1020
1027
  //#endregion
1021
1028
  //#region src/plugin/rules/a11y/anchor-ambiguous-text.ts
1022
- const buildMessage$28 = (text) => `\`${text}\` is ambiguous link text — describe the destination instead (e.g. "View pricing details").`;
1029
+ const buildMessage$29 = (text) => `\`${text}\` is ambiguous link text — describe the destination instead (e.g. "View pricing details").`;
1023
1030
  const DEFAULT_AMBIGUOUS = [
1024
1031
  "click here",
1025
1032
  "here",
@@ -1076,7 +1083,7 @@ const anchorAmbiguousText = defineRule({
1076
1083
  const normalized = normalizeText(accessibleText);
1077
1084
  if (ambiguousSet.has(normalized)) context.report({
1078
1085
  node: node.openingElement.name,
1079
- message: buildMessage$28(normalized)
1086
+ message: buildMessage$29(normalized)
1080
1087
  });
1081
1088
  } };
1082
1089
  }
@@ -1655,7 +1662,7 @@ const ARIA_PROPERTIES = new Map([
1655
1662
  const isValidAriaProperty = (name) => ARIA_PROPERTIES.has(name);
1656
1663
  //#endregion
1657
1664
  //#region src/plugin/rules/a11y/aria-props.ts
1658
- const buildMessage$27 = (name) => `\`${name}\` is not a valid ARIA property — check WAI-ARIA spec.`;
1665
+ const buildMessage$28 = (name) => `\`${name}\` is not a valid ARIA property — check WAI-ARIA spec.`;
1659
1666
  const ariaProps = defineRule({
1660
1667
  id: "aria-props",
1661
1668
  tags: ["react-jsx-only"],
@@ -1668,7 +1675,7 @@ const ariaProps = defineRule({
1668
1675
  if (!name || !name.startsWith("aria-")) return;
1669
1676
  if (!isValidAriaProperty(name)) context.report({
1670
1677
  node: node.name,
1671
- message: buildMessage$27(name)
1678
+ message: buildMessage$28(name)
1672
1679
  });
1673
1680
  } })
1674
1681
  });
@@ -1819,7 +1826,7 @@ const buildExpectedDescription = (propType) => {
1819
1826
  case "token-list": return `a space-separated list of: ${propType.tokens.join(", ")}`;
1820
1827
  }
1821
1828
  };
1822
- const buildMessage$26 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
1829
+ const buildMessage$27 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
1823
1830
  const allowNoneValue = (propType) => {
1824
1831
  switch (propType.kind) {
1825
1832
  case "boolean":
@@ -1952,13 +1959,13 @@ const ariaProptypes = defineRule({
1952
1959
  if (!node.value) {
1953
1960
  if (!allowNoneValue(propType)) context.report({
1954
1961
  node,
1955
- message: buildMessage$26(propName, propType)
1962
+ message: buildMessage$27(propName, propType)
1956
1963
  });
1957
1964
  return;
1958
1965
  }
1959
1966
  if (!isValidValueForType(propType, node.value)) context.report({
1960
1967
  node,
1961
- message: buildMessage$26(propName, propType)
1968
+ message: buildMessage$27(propName, propType)
1962
1969
  });
1963
1970
  } })
1964
1971
  });
@@ -2270,7 +2277,7 @@ const ariaRole = defineRule({
2270
2277
  });
2271
2278
  //#endregion
2272
2279
  //#region src/plugin/rules/a11y/aria-unsupported-elements.ts
2273
- const buildMessage$25 = (tag, attribute) => `\`<${tag}>\` does not support \`${attribute}\` — reserved HTML elements don't accept ARIA attributes.`;
2280
+ const buildMessage$26 = (tag, attribute) => `\`<${tag}>\` does not support \`${attribute}\` — reserved HTML elements don't accept ARIA attributes.`;
2274
2281
  const ariaUnsupportedElements = defineRule({
2275
2282
  id: "aria-unsupported-elements",
2276
2283
  tags: ["react-jsx-only"],
@@ -2287,7 +2294,7 @@ const ariaUnsupportedElements = defineRule({
2287
2294
  if (!attrName) continue;
2288
2295
  if (attrName.startsWith("aria-") || attrName === "role") context.report({
2289
2296
  node: attribute,
2290
- message: buildMessage$25(tag, attrName)
2297
+ message: buildMessage$26(tag, attrName)
2291
2298
  });
2292
2299
  }
2293
2300
  } })
@@ -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 (TYPE_POSITION_KEYS.has(key)) continue;
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.getFilename?.() ?? "");
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;
@@ -3154,7 +3164,7 @@ const asyncParallel = defineRule({
3154
3164
  });
3155
3165
  //#endregion
3156
3166
  //#region src/plugin/rules/a11y/autocomplete-valid.ts
3157
- const buildMessage$24 = (value) => `\`autoComplete\` value \`${value}\` is not a known HTML autofill token.`;
3167
+ const buildMessage$25 = (value) => `\`autoComplete\` value \`${value}\` is not a known HTML autofill token.`;
3158
3168
  const AUTOFILL_TOKENS = new Set([
3159
3169
  "off",
3160
3170
  "on",
@@ -3242,7 +3252,7 @@ const autocompleteValid = defineRule({
3242
3252
  if (!AUTOFILL_TOKENS.has(token)) {
3243
3253
  context.report({
3244
3254
  node: attribute,
3245
- message: buildMessage$24(value)
3255
+ message: buildMessage$25(value)
3246
3256
  });
3247
3257
  return;
3248
3258
  }
@@ -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.getFilename?.());
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.getFilename?.());
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.getFilename?.());
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
- handleFunctionParameters(node.params ?? [], fnScope, state);
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 (TYPE_ONLY_CHILD_KEYS.has(key)) continue;
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
@@ -6200,7 +6206,7 @@ const flattenJsxName$1 = (name) => {
6200
6206
  return "";
6201
6207
  };
6202
6208
  const isSupportedJsxName = (name) => isNodeOfType(name, "JSXIdentifier") || isNodeOfType(name, "JSXMemberExpression");
6203
- const buildMessage$23 = (propName, message) => message ?? `Prop \`${propName}\` is forbidden on this component.`;
6209
+ const buildMessage$24 = (propName, message) => message ?? `Prop \`${propName}\` is forbidden on this component.`;
6204
6210
  const forbidComponentProps = defineRule({
6205
6211
  id: "forbid-component-props",
6206
6212
  severity: "warn",
@@ -6226,7 +6232,7 @@ const forbidComponentProps = defineRule({
6226
6232
  if (!isForbiddenForTag(entry, tag)) continue;
6227
6233
  context.report({
6228
6234
  node: attribute,
6229
- message: buildMessage$23(propName, entry.message)
6235
+ message: buildMessage$24(propName, entry.message)
6230
6236
  });
6231
6237
  break;
6232
6238
  }
@@ -6236,7 +6242,7 @@ const forbidComponentProps = defineRule({
6236
6242
  });
6237
6243
  //#endregion
6238
6244
  //#region src/plugin/rules/react-builtins/forbid-dom-props.ts
6239
- const buildMessage$22 = (propName, customMessage) => customMessage ?? `Prop \`${propName}\` is forbidden on DOM nodes.`;
6245
+ const buildMessage$23 = (propName, customMessage) => customMessage ?? `Prop \`${propName}\` is forbidden on DOM nodes.`;
6240
6246
  const resolveSettings$43 = (settings) => {
6241
6247
  const reactDoctor = settings?.["react-doctor"];
6242
6248
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
@@ -6274,7 +6280,7 @@ const forbidDomProps = defineRule({
6274
6280
  if (disallowedFor && disallowedFor.size > 0 && !disallowedFor.has(elementName)) continue;
6275
6281
  context.report({
6276
6282
  node: attribute.name,
6277
- message: buildMessage$22(propName, descriptor.message)
6283
+ message: buildMessage$23(propName, descriptor.message)
6278
6284
  });
6279
6285
  }
6280
6286
  } };
@@ -6344,7 +6350,7 @@ const isReactFunctionCall = (node, expectedCall) => {
6344
6350
  };
6345
6351
  //#endregion
6346
6352
  //#region src/plugin/rules/react-builtins/forbid-elements.ts
6347
- const buildMessage$21 = (element, customHelp) => customHelp ? `<${element}> is forbidden — ${customHelp}` : `<${element}> is forbidden.`;
6353
+ const buildMessage$22 = (element, customHelp) => customHelp ? `<${element}> is forbidden — ${customHelp}` : `<${element}> is forbidden.`;
6348
6354
  const resolveSettings$42 = (settings) => {
6349
6355
  const reactDoctor = settings?.["react-doctor"];
6350
6356
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
@@ -6368,7 +6374,7 @@ const forbidElements = defineRule({
6368
6374
  if (!fullName || !forbidMap.has(fullName)) return;
6369
6375
  context.report({
6370
6376
  node: node.name,
6371
- message: buildMessage$21(fullName, forbidMap.get(fullName))
6377
+ message: buildMessage$22(fullName, forbidMap.get(fullName))
6372
6378
  });
6373
6379
  },
6374
6380
  CallExpression(node) {
@@ -6388,7 +6394,7 @@ const forbidElements = defineRule({
6388
6394
  if (!elementName || !forbidMap.has(elementName)) return;
6389
6395
  context.report({
6390
6396
  node: firstArgument,
6391
- message: buildMessage$21(elementName, forbidMap.get(elementName))
6397
+ message: buildMessage$22(elementName, forbidMap.get(elementName))
6392
6398
  });
6393
6399
  }
6394
6400
  };
@@ -6690,7 +6696,7 @@ const BLOCK_LEVEL_ELEMENTS = new Set([
6690
6696
  "table",
6691
6697
  "ul"
6692
6698
  ]);
6693
- const buildMessage$20 = (childTagName) => `Block-level \`<${childTagName}>\` cannot appear inside a \`<p>\` — the HTML parser auto-closes the paragraph at the start of \`<${childTagName}>\`, splitting your DOM in ways the renderer never expressed and triggering hydration mismatches.`;
6699
+ const buildMessage$21 = (childTagName) => `Block-level \`<${childTagName}>\` cannot appear inside a \`<p>\` — the HTML parser auto-closes the paragraph at the start of \`<${childTagName}>\`, splitting your DOM in ways the renderer never expressed and triggering hydration mismatches.`;
6694
6700
  const isParagraphElement = (candidate) => {
6695
6701
  if (!isNodeOfType(candidate, "JSXElement")) return false;
6696
6702
  const opening = candidate.openingElement;
@@ -6718,7 +6724,7 @@ const htmlNoInvalidParagraphChild = defineRule({
6718
6724
  if (!findEnclosingParagraph(node)) return;
6719
6725
  context.report({
6720
6726
  node: node.name,
6721
- message: buildMessage$20(childTagName)
6727
+ message: buildMessage$21(childTagName)
6722
6728
  });
6723
6729
  } })
6724
6730
  });
@@ -6738,7 +6744,7 @@ const ROW_GROUPS = new Set([
6738
6744
  "tbody",
6739
6745
  "tfoot"
6740
6746
  ]);
6741
- const buildMessage$19 = (childTag, expectedParent, actualParent) => `Improper table nesting — \`<${childTag}>\` must be a direct child of ${expectedParent}, but its nearest host ancestor is \`<${actualParent}>\`. Browsers auto-rewrite invalid table structure, producing a DOM that doesn't match the JSX (broken hydration, broken \`>\` selectors, broken accessibility tree).`;
6747
+ const buildMessage$20 = (childTag, expectedParent, actualParent) => `Improper table nesting — \`<${childTag}>\` must be a direct child of ${expectedParent}, but its nearest host ancestor is \`<${actualParent}>\`. Browsers auto-rewrite invalid table structure, producing a DOM that doesn't match the JSX (broken hydration, broken \`>\` selectors, broken accessibility tree).`;
6742
6748
  const buildNestedTableMessage = () => "Improper table nesting — `<table>` cannot be a direct descendant of another table element. Tables can only nest inside a `<td>` or `<th>` cell of an outer table.";
6743
6749
  const getHostTagName = (jsxElement) => {
6744
6750
  if (!isNodeOfType(jsxElement, "JSXElement")) return null;
@@ -6806,28 +6812,28 @@ const htmlNoInvalidTableNesting = defineRule({
6806
6812
  if (ROW_GROUPS.has(tagName)) {
6807
6813
  if (actualParent !== "table") context.report({
6808
6814
  node: node.openingElement.name,
6809
- message: buildMessage$19(tagName, "`<table>`", actualParent)
6815
+ message: buildMessage$20(tagName, "`<table>`", actualParent)
6810
6816
  });
6811
6817
  return;
6812
6818
  }
6813
6819
  if (tagName === "tr") {
6814
6820
  if (!ROW_GROUPS.has(actualParent) && actualParent !== "table") context.report({
6815
6821
  node: node.openingElement.name,
6816
- message: buildMessage$19(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
6822
+ message: buildMessage$20(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
6817
6823
  });
6818
6824
  return;
6819
6825
  }
6820
6826
  if (tagName === "td" || tagName === "th") {
6821
6827
  if (actualParent !== "tr") context.report({
6822
6828
  node: node.openingElement.name,
6823
- message: buildMessage$19(tagName, "`<tr>`", actualParent)
6829
+ message: buildMessage$20(tagName, "`<tr>`", actualParent)
6824
6830
  });
6825
6831
  }
6826
6832
  } })
6827
6833
  });
6828
6834
  //#endregion
6829
6835
  //#region src/plugin/rules/correctness/html-no-nested-interactive.ts
6830
- const buildMessage$18 = (tagName) => `Improper nesting of \`<${tagName}>\` inside another \`<${tagName}>\` — the HTML parser auto-closes the outer element, splitting your DOM in ways the renderer never expressed and breaking event delegation, focus, and accessibility.`;
6836
+ const buildMessage$19 = (tagName) => `Improper nesting of \`<${tagName}>\` inside another \`<${tagName}>\` — the HTML parser auto-closes the outer element, splitting your DOM in ways the renderer never expressed and breaking event delegation, focus, and accessibility.`;
6831
6837
  const isJsxElementWithTagName = (candidate, tagName) => {
6832
6838
  if (!isNodeOfType(candidate, "JSXElement")) return false;
6833
6839
  const opening = candidate.openingElement;
@@ -6855,7 +6861,7 @@ const htmlNoNestedInteractive = defineRule({
6855
6861
  if (!findEnclosingSameTag(node, tagName)) return;
6856
6862
  context.report({
6857
6863
  node: node.name,
6858
- message: buildMessage$18(tagName)
6864
+ message: buildMessage$19(tagName)
6859
6865
  });
6860
6866
  } })
6861
6867
  });
@@ -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 = context.getFilename ? normalizeFilename$1(context.getFilename()) : "fixture.tsx";
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;
@@ -9337,7 +9344,7 @@ const findVariableInitializer = (referenceNode, bindingName) => {
9337
9344
  };
9338
9345
  //#endregion
9339
9346
  //#region src/plugin/rules/react-builtins/jsx-max-depth.ts
9340
- const buildMessage$17 = (depth, max) => `JSX nesting depth ${depth} exceeds maximum ${max}.`;
9347
+ const buildMessage$18 = (depth, max) => `JSX nesting depth ${depth} exceeds maximum ${max}.`;
9341
9348
  const DEFAULT_MAX_DEPTH = 14;
9342
9349
  const resolveSettings$30 = (settings) => {
9343
9350
  const reactDoctor = settings?.["react-doctor"];
@@ -9404,7 +9411,7 @@ const jsxMaxDepth = defineRule({
9404
9411
  const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
9405
9412
  if (total > max) context.report({
9406
9413
  node,
9407
- message: buildMessage$17(total, max)
9414
+ message: buildMessage$18(total, max)
9408
9415
  });
9409
9416
  };
9410
9417
  return {
@@ -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.getFilename?.());
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.getFilename?.());
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.getFilename?.());
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.getFilename?.());
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.getFilename?.());
10990
+ const isTestlikeFile = isTestlikeFilename(context.filename);
10984
10991
  let memoRegistry = null;
10985
10992
  return {
10986
10993
  Program(node) {
@@ -11340,7 +11347,7 @@ const jsxNoTargetBlank = defineRule({
11340
11347
  });
11341
11348
  //#endregion
11342
11349
  //#region src/plugin/rules/react-builtins/jsx-no-undef.ts
11343
- const buildMessage$16 = (name) => `\`${name}\` is not defined in this scope.`;
11350
+ const buildMessage$17 = (name) => `\`${name}\` is not defined in this scope.`;
11344
11351
  const KNOWN_GLOBALS = new Set([
11345
11352
  "globalThis",
11346
11353
  "window",
@@ -11375,7 +11382,7 @@ const jsxNoUndef = defineRule({
11375
11382
  if (findVariableInitializer(node, rootIdentifier)) return;
11376
11383
  context.report({
11377
11384
  node: node.name,
11378
- message: buildMessage$16(rootIdentifier)
11385
+ message: buildMessage$17(rootIdentifier)
11379
11386
  });
11380
11387
  } })
11381
11388
  });
@@ -11474,7 +11481,7 @@ const jsxNoUselessFragment = defineRule({
11474
11481
  });
11475
11482
  //#endregion
11476
11483
  //#region src/plugin/rules/react-builtins/jsx-pascal-case.ts
11477
- const buildMessage$15 = (componentName, allowAllCaps) => allowAllCaps ? `JSX component \`${componentName}\` must be in PascalCase or SCREAMING_SNAKE_CASE.` : `JSX component \`${componentName}\` must be in PascalCase.`;
11484
+ const buildMessage$16 = (componentName, allowAllCaps) => allowAllCaps ? `JSX component \`${componentName}\` must be in PascalCase or SCREAMING_SNAKE_CASE.` : `JSX component \`${componentName}\` must be in PascalCase.`;
11478
11485
  const resolveSettings$26 = (settings) => {
11479
11486
  const reactDoctor = settings?.["react-doctor"];
11480
11487
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPascalCase ?? {} : {};
@@ -11590,7 +11597,7 @@ const jsxPascalCase = defineRule({
11590
11597
  if (!isPascal && !isAllCaps) {
11591
11598
  context.report({
11592
11599
  node,
11593
- message: buildMessage$15(segment, settings.allowAllCaps)
11600
+ message: buildMessage$16(segment, settings.allowAllCaps)
11594
11601
  });
11595
11602
  return;
11596
11603
  }
@@ -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.getFilename?.());
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.getFilename?.() ?? "");
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.getFilename?.() ?? "");
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.getFilename?.() ?? "");
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.getFilename?.() ?? "");
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.getFilename?.() ?? "");
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.getFilename?.() ?? "");
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: "warn",
13415
+ severity: "error",
13409
13416
  tags: ["test-noise"],
13410
- recommendation: "Adjust the state inline during render instead of via a useEffect, or refactor the state to avoid the need entirely. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
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: "Avoid adjusting state when a prop changes. Instead, adjust the state directly during render, or refactor your state to avoid this need entirely."
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.getFilename?.());
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.getFilename?.() ?? "");
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;
@@ -15597,7 +15604,7 @@ const noDisabledZoom = defineRule({
15597
15604
  });
15598
15605
  //#endregion
15599
15606
  //#region src/plugin/rules/a11y/no-distracting-elements.ts
15600
- const buildMessage$14 = (tag) => `\`<${tag}>\` is distracting and should not be used — replace with semantic, accessible markup.`;
15607
+ const buildMessage$15 = (tag) => `\`<${tag}>\` is distracting and should not be used — replace with semantic, accessible markup.`;
15601
15608
  const DEFAULT_DISTRACTING = ["marquee", "blink"];
15602
15609
  const resolveSettings$18 = (settings) => {
15603
15610
  const reactDoctor = settings?.["react-doctor"];
@@ -15617,7 +15624,7 @@ const noDistractingElements = defineRule({
15617
15624
  const tag = getElementType(node, context.settings);
15618
15625
  if (distractingTags.has(tag)) context.report({
15619
15626
  node: node.name,
15620
- message: buildMessage$14(tag)
15627
+ message: buildMessage$15(tag)
15621
15628
  });
15622
15629
  } };
15623
15630
  }
@@ -17458,7 +17465,7 @@ const noInlinePropOnMemoComponent = defineRule({
17458
17465
  });
17459
17466
  //#endregion
17460
17467
  //#region src/plugin/rules/a11y/no-interactive-element-to-noninteractive-role.ts
17461
- const buildMessage$13 = (tag, role) => `Interactive element \`<${tag}>\` cannot have non-interactive role \`${role}\`.`;
17468
+ const buildMessage$14 = (tag, role) => `Interactive element \`<${tag}>\` cannot have non-interactive role \`${role}\`.`;
17462
17469
  const PRESENTATION_ROLES = ["presentation", "none"];
17463
17470
  const DEFAULT_ALLOWED_ROLES$1 = {
17464
17471
  tr: ["none", "presentation"],
@@ -17502,7 +17509,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
17502
17509
  if (!isNonInteractiveRole(firstRole) && !PRESENTATION_ROLES.includes(firstRole)) return;
17503
17510
  context.report({
17504
17511
  node: roleAttribute,
17505
- message: buildMessage$13(elementType, firstRole)
17512
+ message: buildMessage$14(elementType, firstRole)
17506
17513
  });
17507
17514
  } };
17508
17515
  }
@@ -17946,7 +17953,7 @@ const noMoment = defineRule({
17946
17953
  });
17947
17954
  //#endregion
17948
17955
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
17949
- const buildMessage$12 = (componentName) => `Declare only one React component per file. Found extra component: ${componentName}.`;
17956
+ const buildMessage$13 = (componentName) => `Declare only one React component per file. Found extra component: ${componentName}.`;
17950
17957
  const resolveSettings$16 = (settings) => {
17951
17958
  const reactDoctor = settings?.["react-doctor"];
17952
17959
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -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.getFilename?.());
18256
+ const isTestlikeFile = isTestlikeFilename(context.filename);
18250
18257
  return { Program(node) {
18251
18258
  if (isTestlikeFile) return;
18252
18259
  const visitContext = {
@@ -18267,7 +18274,7 @@ const noMultiComp = defineRule({
18267
18274
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
18268
18275
  for (const component of flagged.slice(1)) context.report({
18269
18276
  node: component.reportNode,
18270
- message: buildMessage$12(component.name)
18277
+ message: buildMessage$13(component.name)
18271
18278
  });
18272
18279
  } };
18273
18280
  }
@@ -18628,7 +18635,7 @@ const noMutatingReducerState = defineRule({
18628
18635
  });
18629
18636
  //#endregion
18630
18637
  //#region src/plugin/rules/react-builtins/no-namespace.ts
18631
- const buildMessage$11 = (componentName) => `React component \`${componentName}\` must not be in a namespace — React doesn't support them.`;
18638
+ const buildMessage$12 = (componentName) => `React component \`${componentName}\` must not be in a namespace — React doesn't support them.`;
18632
18639
  const noNamespace = defineRule({
18633
18640
  id: "no-namespace",
18634
18641
  severity: "warn",
@@ -18640,7 +18647,7 @@ const noNamespace = defineRule({
18640
18647
  const fullName = `${namespaced.namespace.name}:${namespaced.name.name}`;
18641
18648
  context.report({
18642
18649
  node: namespaced,
18643
- message: buildMessage$11(fullName)
18650
+ message: buildMessage$12(fullName)
18644
18651
  });
18645
18652
  },
18646
18653
  CallExpression(node) {
@@ -18651,7 +18658,7 @@ const noNamespace = defineRule({
18651
18658
  if (!firstArgument.value.includes(":")) return;
18652
18659
  context.report({
18653
18660
  node: firstArgument,
18654
- message: buildMessage$11(firstArgument.value)
18661
+ message: buildMessage$12(firstArgument.value)
18655
18662
  });
18656
18663
  }
18657
18664
  })
@@ -18695,7 +18702,7 @@ const noNestedComponentDefinition = defineRule({
18695
18702
  });
18696
18703
  //#endregion
18697
18704
  //#region src/plugin/rules/a11y/no-noninteractive-element-interactions.ts
18698
- const buildMessage$10 = (tag) => `Non-interactive element \`<${tag}>\` should not have interactive event handlers — convert to a semantic interactive element or add an interactive role.`;
18705
+ const buildMessage$11 = (tag) => `Non-interactive element \`<${tag}>\` should not have interactive event handlers — convert to a semantic interactive element or add an interactive role.`;
18699
18706
  const INTERACTIVE_HANDLERS = [
18700
18707
  "onClick",
18701
18708
  "onMouseDown",
@@ -18721,13 +18728,13 @@ const noNoninteractiveElementInteractions = defineRule({
18721
18728
  }
18722
18729
  context.report({
18723
18730
  node: node.name,
18724
- message: buildMessage$10(tag)
18731
+ message: buildMessage$11(tag)
18725
18732
  });
18726
18733
  } })
18727
18734
  });
18728
18735
  //#endregion
18729
18736
  //#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
18730
- const buildMessage$9 = (tag, role) => `Non-interactive element \`<${tag}>\` cannot have interactive role \`${role}\` — use a semantic interactive element instead.`;
18737
+ const buildMessage$10 = (tag, role) => `Non-interactive element \`<${tag}>\` cannot have interactive role \`${role}\` — use a semantic interactive element instead.`;
18731
18738
  const DEFAULT_ALLOWED_ROLES = {
18732
18739
  ul: [
18733
18740
  "menu",
@@ -18791,7 +18798,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
18791
18798
  if (!isInteractiveRole(firstRole)) return;
18792
18799
  context.report({
18793
18800
  node: roleAttribute,
18794
- message: buildMessage$9(elementType, firstRole)
18801
+ message: buildMessage$10(elementType, firstRole)
18795
18802
  });
18796
18803
  } };
18797
18804
  }
@@ -19381,6 +19388,63 @@ const noPropCallbackInEffect = defineRule({
19381
19388
  }
19382
19389
  });
19383
19390
  //#endregion
19391
+ //#region src/plugin/rules/architecture/no-prop-types.ts
19392
+ const PROP_TYPES_PROPERTY = "propTypes";
19393
+ const isPropTypesKey = (key, computed) => {
19394
+ if (!key) return false;
19395
+ if (computed) return isNodeOfType(key, "Literal") && key.value === PROP_TYPES_PROPERTY;
19396
+ return isNodeOfType(key, "Identifier") && key.name === PROP_TYPES_PROPERTY;
19397
+ };
19398
+ const getComponentNameFromPropTypesAssignment = (left) => {
19399
+ if (!isNodeOfType(left, "MemberExpression")) return null;
19400
+ if (!isPropTypesKey(left.property, Boolean(left.computed))) return null;
19401
+ if (!isNodeOfType(left.object, "Identifier")) return null;
19402
+ if (!isUppercaseName(left.object.name)) return null;
19403
+ return left.object.name;
19404
+ };
19405
+ const getComponentNameFromClassProperty = (node) => {
19406
+ if (!node.static) return null;
19407
+ if (!isPropTypesKey(node.key, Boolean(node.computed))) return null;
19408
+ const classBody = node.parent;
19409
+ if (!isNodeOfType(classBody, "ClassBody")) return null;
19410
+ const classNode = classBody.parent;
19411
+ if (!classNode) return null;
19412
+ if ((isNodeOfType(classNode, "ClassDeclaration") || isNodeOfType(classNode, "ClassExpression")) && classNode.id?.name && isUppercaseName(classNode.id.name)) return classNode.id.name;
19413
+ if (!isNodeOfType(classNode, "ClassExpression")) return null;
19414
+ const declarator = classNode.parent;
19415
+ if (!isNodeOfType(declarator, "VariableDeclarator")) return null;
19416
+ if (!isNodeOfType(declarator.id, "Identifier")) return null;
19417
+ if (!isUppercaseName(declarator.id.name)) return null;
19418
+ return declarator.id.name;
19419
+ };
19420
+ const buildMessage$9 = (componentName) => `${componentName}.propTypes — React 19 no longer runs \`propTypes\` checks, so invalid props pass silently. Move the prop contract to TypeScript types and add explicit runtime validation only where data can actually be invalid`;
19421
+ const noPropTypes = defineRule({
19422
+ id: "no-prop-types",
19423
+ requires: ["react:19"],
19424
+ tags: ["test-noise"],
19425
+ severity: "warn",
19426
+ recommendation: "React 19 removed runtime `propTypes` validation — React no longer reads `Component.propTypes`, so invalid props pass silently. Describe props with TypeScript types and move any required runtime validation to explicit checks (or schema parsing) in component code. Only enabled on projects detected as React 19+.",
19427
+ create: (context) => ({
19428
+ AssignmentExpression(node) {
19429
+ if (node.operator !== "=") return;
19430
+ const componentName = getComponentNameFromPropTypesAssignment(node.left);
19431
+ if (!componentName) return;
19432
+ context.report({
19433
+ node: node.left,
19434
+ message: buildMessage$9(componentName)
19435
+ });
19436
+ },
19437
+ PropertyDefinition(node) {
19438
+ const componentName = getComponentNameFromClassProperty(node);
19439
+ if (!componentName) return;
19440
+ context.report({
19441
+ node: node.key,
19442
+ message: buildMessage$9(componentName)
19443
+ });
19444
+ }
19445
+ })
19446
+ });
19447
+ //#endregion
19384
19448
  //#region src/plugin/rules/design/no-pure-black-background.ts
19385
19449
  const noPureBlackBackground = defineRule({
19386
19450
  id: "no-pure-black-background",
@@ -20193,7 +20257,7 @@ const noSecretsInClientCode = defineRule({
20193
20257
  severity: "warn",
20194
20258
  recommendation: "Move secrets to server-only code. Public client environment variables are bundled into browser code and must not contain secrets",
20195
20259
  create: (context) => {
20196
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
20260
+ const filename = normalizeFilename$1(context.filename ?? "");
20197
20261
  const framework = getReactDoctorStringSetting(context.settings, "framework");
20198
20262
  const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
20199
20263
  let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
@@ -20448,7 +20512,7 @@ const noStaticElementInteractions = defineRule({
20448
20512
  category: "Accessibility",
20449
20513
  create: (context) => {
20450
20514
  const settings = resolveSettings$12(context.settings);
20451
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
20515
+ const isTestlikeFile = isTestlikeFilename(context.filename);
20452
20516
  return { JSXOpeningElement(node) {
20453
20517
  if (isTestlikeFile) return;
20454
20518
  let hasNonBlockerHandler = false;
@@ -20525,7 +20589,7 @@ const noStringRefs = defineRule({
20525
20589
  recommendation: "Use a callback ref (`ref={(node) => { this.foo = node }}`) or `useRef` instead of string refs.",
20526
20590
  create: (context) => {
20527
20591
  const { noTemplateLiterals = false } = resolveSettings$11(context.settings);
20528
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
20592
+ const isTestlikeFile = isTestlikeFilename(context.filename);
20529
20593
  return {
20530
20594
  JSXAttribute(node) {
20531
20595
  if (isTestlikeFile) return;
@@ -22660,7 +22724,7 @@ const isFileNameAllowed = (filename, checkJS) => {
22660
22724
  };
22661
22725
  const onlyExportComponents = defineRule({
22662
22726
  id: "only-export-components",
22663
- severity: "error",
22727
+ severity: "warn",
22664
22728
  recommendation: "Move non-component exports out of files that export components.",
22665
22729
  category: "Architecture",
22666
22730
  create: (context) => {
@@ -22671,7 +22735,7 @@ const onlyExportComponents = defineRule({
22671
22735
  allowConstantExport: settings.allowConstantExport
22672
22736
  };
22673
22737
  return { Program(node) {
22674
- if (!isFileNameAllowed(context.getFilename ? normalizeFilename$1(context.getFilename()) : void 0, settings.checkJS)) return;
22738
+ if (!isFileNameAllowed(normalizeFilename$1(context.filename ?? ""), settings.checkJS)) return;
22675
22739
  const allNodes = collectAllNodes(node);
22676
22740
  const exports = [];
22677
22741
  let hasReactExport = false;
@@ -23717,106 +23781,6 @@ const queryStableQueryClient = defineRule({
23717
23781
  }
23718
23782
  });
23719
23783
  //#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
23784
  //#region src/plugin/rules/architecture/react-compiler-no-manual-memoization.ts
23821
23785
  const REMOVAL_MESSAGE_BY_REACT_API_NAME = new Map([
23822
23786
  ["useMemo", "Remove `useMemo` — React Compiler auto-memoizes every value in this component. Manual `useMemo` adds noise without improving performance."],
@@ -24192,7 +24156,7 @@ const renderingSvgPrecision = defineRule({
24192
24156
  category: "Performance",
24193
24157
  recommendation: "Truncate path/points/transform decimals to 1–2 digits — sub-pixel precision adds bytes with no visible difference",
24194
24158
  create: (context) => {
24195
- const filename = context.getFilename?.();
24159
+ const filename = context.filename;
24196
24160
  const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename$1(filename) : void 0);
24197
24161
  return { JSXAttribute(node) {
24198
24162
  if (isAutoGenerated) return;
@@ -25524,7 +25488,8 @@ const classifyPackagePlatform = (filename) => {
25524
25488
  //#endregion
25525
25489
  //#region src/plugin/utils/is-expo-managed-file.ts
25526
25490
  const isExpoManagedFileActive = (context) => {
25527
- const filename = context.getFilename?.() ? normalizeFilename$1(context.getFilename()) : void 0;
25491
+ const rawFilename = context.filename;
25492
+ const filename = rawFilename ? normalizeFilename$1(rawFilename) : void 0;
25528
25493
  if (filename) {
25529
25494
  const packagePlatform = classifyPackagePlatform(filename);
25530
25495
  if (packagePlatform === "expo") return true;
@@ -30692,7 +30657,7 @@ const serverFetchWithoutRevalidate = defineRule({
30692
30657
  let isServerSideFile = false;
30693
30658
  return {
30694
30659
  Program(node) {
30695
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
30660
+ const filename = normalizeFilename$1(context.filename ?? "");
30696
30661
  if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
30697
30662
  isServerSideFile = false;
30698
30663
  return;
@@ -30805,7 +30770,7 @@ const serverHoistStaticIo = defineRule({
30805
30770
  inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectIdentifierParams(declaration.params ?? []));
30806
30771
  },
30807
30772
  ExportDefaultDeclaration(node) {
30808
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
30773
+ const filename = normalizeFilename$1(context.filename ?? "");
30809
30774
  if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
30810
30775
  const declaration = node.declaration;
30811
30776
  if (!declaration || !isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "FunctionExpression") && !isNodeOfType(declaration, "ArrowFunctionExpression")) return;
@@ -31356,7 +31321,7 @@ const tanstackStartMissingHeadContent = defineRule({
31356
31321
  };
31357
31322
  return {
31358
31323
  Program(node) {
31359
- const filename = context.getFilename?.() ?? "";
31324
+ const filename = context.filename ?? "";
31360
31325
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
31361
31326
  const statements = node.body ?? [];
31362
31327
  for (const statement of statements) collectImportBindings(statement);
@@ -31366,17 +31331,17 @@ const tanstackStartMissingHeadContent = defineRule({
31366
31331
  }
31367
31332
  },
31368
31333
  ImportDeclaration(node) {
31369
- const filename = context.getFilename?.() ?? "";
31334
+ const filename = context.filename ?? "";
31370
31335
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
31371
31336
  collectImportBindings(node);
31372
31337
  },
31373
31338
  VariableDeclarator(node) {
31374
- const filename = context.getFilename?.() ?? "";
31339
+ const filename = context.filename ?? "";
31375
31340
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
31376
31341
  collectVariableAlias(node);
31377
31342
  },
31378
31343
  JSXOpeningElement(node) {
31379
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
31344
+ const filename = normalizeFilename$1(context.filename ?? "");
31380
31345
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
31381
31346
  if (isNodeOfType(node.name, "JSXIdentifier")) {
31382
31347
  if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
@@ -31391,7 +31356,7 @@ const tanstackStartMissingHeadContent = defineRule({
31391
31356
  if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
31392
31357
  },
31393
31358
  "Program:exit"(programNode) {
31394
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
31359
+ const filename = normalizeFilename$1(context.filename ?? "");
31395
31360
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
31396
31361
  if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
31397
31362
  node: programNode,
@@ -31410,7 +31375,7 @@ const tanstackStartNoAnchorElement = defineRule({
31410
31375
  severity: "warn",
31411
31376
  recommendation: "`import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
31412
31377
  create: (context) => ({ JSXOpeningElement(node) {
31413
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
31378
+ const filename = normalizeFilename$1(context.filename ?? "");
31414
31379
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31415
31380
  if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
31416
31381
  const hrefAttribute = (node.attributes ?? []).find((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "href");
@@ -31484,7 +31449,7 @@ const tanstackStartNoNavigateInRender = defineRule({
31484
31449
  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
31450
  return {
31486
31451
  CallExpression(node) {
31487
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
31452
+ const filename = normalizeFilename$1(context.filename ?? "");
31488
31453
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31489
31454
  if (isDeferredHookCall(node)) deferredCallbackDepth++;
31490
31455
  if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
@@ -31494,17 +31459,17 @@ const tanstackStartNoNavigateInRender = defineRule({
31494
31459
  });
31495
31460
  },
31496
31461
  "CallExpression:exit"(node) {
31497
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
31462
+ const filename = normalizeFilename$1(context.filename ?? "");
31498
31463
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31499
31464
  if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
31500
31465
  },
31501
31466
  JSXAttribute(node) {
31502
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
31467
+ const filename = normalizeFilename$1(context.filename ?? "");
31503
31468
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31504
31469
  if (isEventHandlerAttribute(node)) eventHandlerDepth++;
31505
31470
  },
31506
31471
  "JSXAttribute:exit"(node) {
31507
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
31472
+ const filename = normalizeFilename$1(context.filename ?? "");
31508
31473
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31509
31474
  if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
31510
31475
  }
@@ -31585,7 +31550,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
31585
31550
  severity: "warn",
31586
31551
  recommendation: "Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfalls",
31587
31552
  create: (context) => ({ CallExpression(node) {
31588
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
31553
+ const filename = normalizeFilename$1(context.filename ?? "");
31589
31554
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31590
31555
  if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
31591
31556
  const callback = node.arguments?.[0];
@@ -33758,6 +33723,17 @@ const reactDoctorRules = [
33758
33723
  category: "State & Effects"
33759
33724
  }
33760
33725
  },
33726
+ {
33727
+ key: "react-doctor/no-prop-types",
33728
+ id: "no-prop-types",
33729
+ source: "react-doctor",
33730
+ originallyExternal: false,
33731
+ rule: {
33732
+ ...noPropTypes,
33733
+ framework: "global",
33734
+ category: "Architecture"
33735
+ }
33736
+ },
33761
33737
  {
33762
33738
  key: "react-doctor/no-pure-black-background",
33763
33739
  id: "no-pure-black-background",
@@ -34308,17 +34284,6 @@ const reactDoctorRules = [
34308
34284
  category: "TanStack Query"
34309
34285
  }
34310
34286
  },
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
34287
  {
34323
34288
  key: "react-doctor/react-compiler-no-manual-memoization",
34324
34289
  id: "react-compiler-no-manual-memoization",
@@ -35254,7 +35219,7 @@ const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id,
35254
35219
  const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
35255
35220
  const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
35256
35221
  const isReactNativeFileActive = (context) => {
35257
- const rawFilename = context.getFilename?.();
35222
+ const rawFilename = context.filename;
35258
35223
  if (!rawFilename) return true;
35259
35224
  const filename = normalizeFilename$1(rawFilename);
35260
35225
  if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
@@ -35742,7 +35707,9 @@ const wrapWithSemanticContext = (rule) => ({
35742
35707
  };
35743
35708
  const enrichedContext = {
35744
35709
  report: baseContext.report,
35745
- getFilename: baseContext.getFilename,
35710
+ get filename() {
35711
+ return baseContext.filename ?? baseContext.getFilename?.();
35712
+ },
35746
35713
  settings: baseContext.settings,
35747
35714
  get scopes() {
35748
35715
  return getScopes();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.2.10",
3
+ "version": "0.2.11-dev.d917f62",
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",