oxlint-plugin-react-doctor 0.8.1-dev.7c9dbed → 0.8.1-dev.7eca0ee
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 +740 -4
- package/dist/index.js +2553 -302
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1953,13 +1953,13 @@ const getImportDeclaration = (node) => {
|
|
|
1953
1953
|
}
|
|
1954
1954
|
return null;
|
|
1955
1955
|
};
|
|
1956
|
-
const getImportSource = (declaration) => typeof declaration.source.value === "string" ? declaration.source.value : null;
|
|
1956
|
+
const getImportSource$1 = (declaration) => typeof declaration.source.value === "string" ? declaration.source.value : null;
|
|
1957
1957
|
const isNamedImport = (symbol, importedName, moduleSources) => {
|
|
1958
1958
|
if (symbol.kind !== "import") return false;
|
|
1959
1959
|
const declaration = symbol.declarationNode;
|
|
1960
1960
|
if (!isNodeOfType(declaration, "ImportSpecifier")) return false;
|
|
1961
1961
|
const importDeclaration = getImportDeclaration(declaration);
|
|
1962
|
-
const source = importDeclaration ? getImportSource(importDeclaration) : null;
|
|
1962
|
+
const source = importDeclaration ? getImportSource$1(importDeclaration) : null;
|
|
1963
1963
|
if (!source || !moduleSources.has(source)) return false;
|
|
1964
1964
|
const imported = declaration.imported;
|
|
1965
1965
|
return isNodeOfType(imported, "Identifier") && imported.name === importedName || isNodeOfType(imported, "Literal") && imported.value === importedName;
|
|
@@ -1968,7 +1968,7 @@ const isSatoriImport = (symbol) => {
|
|
|
1968
1968
|
if (symbol.kind !== "import") return false;
|
|
1969
1969
|
const declaration = symbol.declarationNode;
|
|
1970
1970
|
const importDeclaration = getImportDeclaration(declaration);
|
|
1971
|
-
if (!importDeclaration || getImportSource(importDeclaration) !== "satori") return false;
|
|
1971
|
+
if (!importDeclaration || getImportSource$1(importDeclaration) !== "satori") return false;
|
|
1972
1972
|
if (isNodeOfType(declaration, "ImportDefaultSpecifier")) return true;
|
|
1973
1973
|
if (!isNodeOfType(declaration, "ImportSpecifier")) return false;
|
|
1974
1974
|
const imported = declaration.imported;
|
|
@@ -1989,7 +1989,7 @@ const isGeneratedImageRendererCall = (node, scopes) => {
|
|
|
1989
1989
|
const declaration = symbol.declarationNode;
|
|
1990
1990
|
if (!isNodeOfType(declaration, "ImportNamespaceSpecifier")) return false;
|
|
1991
1991
|
const importDeclaration = getImportDeclaration(declaration);
|
|
1992
|
-
const source = importDeclaration ? getImportSource(importDeclaration) : null;
|
|
1992
|
+
const source = importDeclaration ? getImportSource$1(importDeclaration) : null;
|
|
1993
1993
|
return Boolean(source && IMAGE_RESPONSE_MODULES.has(source));
|
|
1994
1994
|
};
|
|
1995
1995
|
//#endregion
|
|
@@ -3173,7 +3173,7 @@ const getJsxPropStaticStringValues = (attribute, scopes) => getJsxPropStaticStri
|
|
|
3173
3173
|
const getJsxPropExhaustiveStaticStringValues = (attribute, scopes) => getJsxPropStaticStringValuesWithMode(attribute, scopes, null, true);
|
|
3174
3174
|
//#endregion
|
|
3175
3175
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
3176
|
-
const MESSAGE$
|
|
3176
|
+
const MESSAGE$66 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
3177
3177
|
const hasLinkRole = (roleValue) => roleValue.trim().split(/\s+/).find((roleToken) => VALID_ARIA_ROLES.has(roleToken)) === "link";
|
|
3178
3178
|
const isTransComponentsTemplate = (node) => {
|
|
3179
3179
|
let current = node.parent;
|
|
@@ -3215,7 +3215,7 @@ const anchorHasContent = defineRule({
|
|
|
3215
3215
|
if (isTransComponentsTemplate(node)) return;
|
|
3216
3216
|
context.report({
|
|
3217
3217
|
node: opening.name,
|
|
3218
|
-
message: MESSAGE$
|
|
3218
|
+
message: MESSAGE$66
|
|
3219
3219
|
});
|
|
3220
3220
|
} };
|
|
3221
3221
|
}
|
|
@@ -3703,7 +3703,7 @@ const parseJsxValue = (value) => {
|
|
|
3703
3703
|
};
|
|
3704
3704
|
//#endregion
|
|
3705
3705
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
3706
|
-
const MESSAGE$
|
|
3706
|
+
const MESSAGE$65 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
3707
3707
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
3708
3708
|
id: "aria-activedescendant-has-tabindex",
|
|
3709
3709
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -3721,7 +3721,7 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
3721
3721
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
3722
3722
|
context.report({
|
|
3723
3723
|
node: node.name,
|
|
3724
|
-
message: MESSAGE$
|
|
3724
|
+
message: MESSAGE$65
|
|
3725
3725
|
});
|
|
3726
3726
|
return;
|
|
3727
3727
|
}
|
|
@@ -3729,7 +3729,7 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
3729
3729
|
if (canContentEditableBeTabbable(node, context.scopes, context.settings)) return;
|
|
3730
3730
|
context.report({
|
|
3731
3731
|
node: node.name,
|
|
3732
|
-
message: MESSAGE$
|
|
3732
|
+
message: MESSAGE$65
|
|
3733
3733
|
});
|
|
3734
3734
|
} })
|
|
3735
3735
|
});
|
|
@@ -6827,7 +6827,7 @@ const stripThisParameter = (parameters) => {
|
|
|
6827
6827
|
};
|
|
6828
6828
|
//#endregion
|
|
6829
6829
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
6830
|
-
const MESSAGE$
|
|
6830
|
+
const MESSAGE$64 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
6831
6831
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
6832
6832
|
const STORAGE_GLOBALS = new Set([
|
|
6833
6833
|
"window",
|
|
@@ -6999,7 +6999,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
6999
6999
|
})) return;
|
|
7000
7000
|
context.report({
|
|
7001
7001
|
node,
|
|
7002
|
-
message: MESSAGE$
|
|
7002
|
+
message: MESSAGE$64
|
|
7003
7003
|
});
|
|
7004
7004
|
},
|
|
7005
7005
|
AssignmentExpression(node) {
|
|
@@ -7010,7 +7010,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
7010
7010
|
if (!propertyName || !isAuthCredentialKey(propertyName)) return;
|
|
7011
7011
|
context.report({
|
|
7012
7012
|
node: target,
|
|
7013
|
-
message: MESSAGE$
|
|
7013
|
+
message: MESSAGE$64
|
|
7014
7014
|
});
|
|
7015
7015
|
}
|
|
7016
7016
|
}))
|
|
@@ -7149,7 +7149,7 @@ const CI_INSTALL_NEAR_SECRET_PATTERN = /(?:npm|pnpm|yarn|bun)\s+(?:install|ci)\b
|
|
|
7149
7149
|
const INSTALL_COMMAND_PATTERN = /(?:npm|pnpm|yarn|bun)\s+(?:install|ci)\b/i;
|
|
7150
7150
|
const SECRET_REFERENCE_PATTERN = /\bsecrets\.[A-Z0-9_]+/;
|
|
7151
7151
|
const IGNORE_SCRIPTS_FLAG_PATTERN = /--ignore-scripts\b/;
|
|
7152
|
-
const MESSAGE$
|
|
7152
|
+
const MESSAGE$63 = "The build or install pipeline can execute package lifecycle code while CI secrets may be present.";
|
|
7153
7153
|
const isWorkflowPath = (relativePath) => /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(relativePath);
|
|
7154
7154
|
const scanWorkflowContent = (content) => {
|
|
7155
7155
|
const lines = content.split("\n");
|
|
@@ -7194,7 +7194,7 @@ const scanWorkflowContent = (content) => {
|
|
|
7194
7194
|
const installLineOffset = Math.max(step.lines.findIndex((stepLine) => INSTALL_COMMAND_PATTERN.test(stepLine)), 0);
|
|
7195
7195
|
const installColumnIndex = step.lines[installLineOffset].search(INSTALL_COMMAND_PATTERN);
|
|
7196
7196
|
return [{
|
|
7197
|
-
message: MESSAGE$
|
|
7197
|
+
message: MESSAGE$63,
|
|
7198
7198
|
line: step.startLineIndex + installLineOffset + 1,
|
|
7199
7199
|
column: (installColumnIndex === -1 ? 0 : installColumnIndex) + 1
|
|
7200
7200
|
}];
|
|
@@ -7204,7 +7204,7 @@ const scanWorkflowContent = (content) => {
|
|
|
7204
7204
|
const scanNonWorkflowConfig = scanByPattern({
|
|
7205
7205
|
shouldScan: (file) => isConfigOrCiPath(file.relativePath) && !file.relativePath.endsWith("package.json") && !isWorkflowPath(file.relativePath),
|
|
7206
7206
|
pattern: CI_INSTALL_NEAR_SECRET_PATTERN,
|
|
7207
|
-
message: MESSAGE$
|
|
7207
|
+
message: MESSAGE$63
|
|
7208
7208
|
});
|
|
7209
7209
|
const scan = (file) => {
|
|
7210
7210
|
if (isWorkflowPath(file.relativePath)) return scanWorkflowContent(file.content);
|
|
@@ -7954,7 +7954,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
7954
7954
|
};
|
|
7955
7955
|
//#endregion
|
|
7956
7956
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
7957
|
-
const MESSAGE$
|
|
7957
|
+
const MESSAGE$62 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
7958
7958
|
const KEY_HANDLERS = [
|
|
7959
7959
|
"onKeyUp",
|
|
7960
7960
|
"onKeyDown",
|
|
@@ -8165,7 +8165,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
8165
8165
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler) || spreadEventValues.has(handler.toLowerCase()))) return;
|
|
8166
8166
|
context.report({
|
|
8167
8167
|
node: node.name,
|
|
8168
|
-
message: MESSAGE$
|
|
8168
|
+
message: MESSAGE$62
|
|
8169
8169
|
});
|
|
8170
8170
|
} };
|
|
8171
8171
|
}
|
|
@@ -10243,21 +10243,19 @@ const isReactComponentName = (name) => {
|
|
|
10243
10243
|
return firstCharacter >= 65 && firstCharacter <= 90;
|
|
10244
10244
|
};
|
|
10245
10245
|
//#endregion
|
|
10246
|
-
//#region src/plugin/
|
|
10247
|
-
const
|
|
10248
|
-
if (!isNodeOfType(
|
|
10249
|
-
if (
|
|
10250
|
-
if (isNodeOfType(
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
if (isNodeOfType(expression, "TemplateLiteral") && expression.quasis?.length === 1) return expression.quasis[0].value?.raw ?? null;
|
|
10255
|
-
}
|
|
10246
|
+
//#region src/plugin/utils/get-jsx-attribute-static-string.ts
|
|
10247
|
+
const getJsxAttributeStaticString = (attribute) => {
|
|
10248
|
+
if (!isNodeOfType(attribute, "JSXAttribute") || !attribute.value) return null;
|
|
10249
|
+
if (isNodeOfType(attribute.value, "Literal") && typeof attribute.value.value === "string") return attribute.value.value;
|
|
10250
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
|
|
10251
|
+
const expression = attribute.value.expression;
|
|
10252
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
|
|
10253
|
+
if (isNodeOfType(expression, "TemplateLiteral") && expression.expressions.length === 0 && expression.quasis.length === 1) return expression.quasis[0].value.raw;
|
|
10256
10254
|
return null;
|
|
10257
10255
|
};
|
|
10258
10256
|
//#endregion
|
|
10259
10257
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
10260
|
-
const MESSAGE$
|
|
10258
|
+
const MESSAGE$61 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
10261
10259
|
const NON_OPERABLE_ELEMENTS = new Set([
|
|
10262
10260
|
"td",
|
|
10263
10261
|
"th",
|
|
@@ -10315,7 +10313,7 @@ const collectStaticTemplateClassTokens = (templateLiteral) => {
|
|
|
10315
10313
|
const hasDisplayNoneClass = (opening) => {
|
|
10316
10314
|
const classAttribute = hasJsxPropIgnoreCase(opening.attributes, "className") ?? hasJsxPropIgnoreCase(opening.attributes, "class");
|
|
10317
10315
|
if (!classAttribute) return false;
|
|
10318
|
-
const literalValue =
|
|
10316
|
+
const literalValue = getJsxAttributeStaticString(classAttribute);
|
|
10319
10317
|
if (literalValue !== null) return literalValue.split(/\s+/).some(isDisplayNoneClassToken);
|
|
10320
10318
|
if (classAttribute.value && isNodeOfType(classAttribute.value, "JSXExpressionContainer") && isNodeOfType(classAttribute.value.expression, "TemplateLiteral")) return collectStaticTemplateClassTokens(classAttribute.value.expression).some(isDisplayNoneClassToken);
|
|
10321
10319
|
return false;
|
|
@@ -10787,7 +10785,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
10787
10785
|
if (candidate.enclosingBindingName !== null && labelEmbeddedNames.has(candidate.enclosingBindingName)) continue;
|
|
10788
10786
|
context.report({
|
|
10789
10787
|
node: candidate.opening,
|
|
10790
|
-
message: MESSAGE$
|
|
10788
|
+
message: MESSAGE$61
|
|
10791
10789
|
});
|
|
10792
10790
|
}
|
|
10793
10791
|
}
|
|
@@ -12431,7 +12429,7 @@ const noRedundantPaddingAxes = defineRule({
|
|
|
12431
12429
|
recommendation: "Collapse matching padding axes to `p-N` so duplicated classes do not make spacing harder to scan; keep split axes only when breakpoints differ.",
|
|
12432
12430
|
create: (context) => ({ JSXAttribute(jsxAttribute) {
|
|
12433
12431
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
12434
|
-
const classNameLiteral =
|
|
12432
|
+
const classNameLiteral = getJsxAttributeStaticString(jsxAttribute);
|
|
12435
12433
|
if (!classNameLiteral) return;
|
|
12436
12434
|
if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
|
|
12437
12435
|
if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
|
|
@@ -12459,7 +12457,7 @@ const noRedundantSizeAxes = defineRule({
|
|
|
12459
12457
|
return { JSXAttribute(jsxAttribute) {
|
|
12460
12458
|
if (didReportInFile) return;
|
|
12461
12459
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
12462
|
-
const classNameLiteral =
|
|
12460
|
+
const classNameLiteral = getJsxAttributeStaticString(jsxAttribute);
|
|
12463
12461
|
if (!classNameLiteral) return;
|
|
12464
12462
|
if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
|
|
12465
12463
|
if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
|
|
@@ -12487,7 +12485,7 @@ const noSpaceOnFlexChildren = defineRule({
|
|
|
12487
12485
|
recommendation: "Use `gap-*` on the flex or grid parent. `space-x-*` and `space-y-*` leave gaps when a child is hidden, miss spacing on wrapped lines, and don't flip in right-to-left layouts.",
|
|
12488
12486
|
create: (context) => ({ JSXAttribute(jsxAttribute) {
|
|
12489
12487
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
12490
|
-
const classNameLiteral =
|
|
12488
|
+
const classNameLiteral = getJsxAttributeStaticString(jsxAttribute);
|
|
12491
12489
|
if (!classNameLiteral) return;
|
|
12492
12490
|
if (!classNameLiteral.includes("space-")) return;
|
|
12493
12491
|
const tokens = tokenizeClassName(classNameLiteral);
|
|
@@ -12594,7 +12592,7 @@ const noVagueButtonLabel = defineRule({
|
|
|
12594
12592
|
});
|
|
12595
12593
|
//#endregion
|
|
12596
12594
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
12597
|
-
const MESSAGE$
|
|
12595
|
+
const MESSAGE$60 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
12598
12596
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
12599
12597
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
12600
12598
|
"aria-label",
|
|
@@ -12619,7 +12617,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
12619
12617
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
12620
12618
|
context.report({
|
|
12621
12619
|
node: node.name,
|
|
12622
|
-
message: MESSAGE$
|
|
12620
|
+
message: MESSAGE$60
|
|
12623
12621
|
});
|
|
12624
12622
|
} };
|
|
12625
12623
|
}
|
|
@@ -12659,7 +12657,7 @@ const isEs6Component = (node) => {
|
|
|
12659
12657
|
};
|
|
12660
12658
|
//#endregion
|
|
12661
12659
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
12662
|
-
const MESSAGE$
|
|
12660
|
+
const MESSAGE$59 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
12663
12661
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
12664
12662
|
"observer",
|
|
12665
12663
|
"lazy",
|
|
@@ -12852,7 +12850,7 @@ const displayName = defineRule({
|
|
|
12852
12850
|
const reportAt = (node) => {
|
|
12853
12851
|
context.report({
|
|
12854
12852
|
node,
|
|
12855
|
-
message: MESSAGE$
|
|
12853
|
+
message: MESSAGE$59
|
|
12856
12854
|
});
|
|
12857
12855
|
};
|
|
12858
12856
|
return {
|
|
@@ -13803,17 +13801,18 @@ const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
|
|
|
13803
13801
|
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
13804
13802
|
return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
|
|
13805
13803
|
};
|
|
13806
|
-
const executesDuringRender = (
|
|
13807
|
-
const
|
|
13804
|
+
const executesDuringRender = (callbackExpression, scopes) => {
|
|
13805
|
+
const callbackRoot = findTransparentExpressionRoot(callbackExpression);
|
|
13806
|
+
const parent = callbackRoot.parent;
|
|
13808
13807
|
if (isNodeOfType(parent, "NewExpression")) {
|
|
13809
13808
|
const callee = stripParenExpression(parent.callee);
|
|
13810
|
-
return Boolean(scopes && parent.arguments?.[0] ===
|
|
13809
|
+
return Boolean(scopes && parent.arguments?.[0] === callbackRoot && isNodeOfType(callee, "Identifier") && callee.name === "Promise" && scopes.isGlobalReference(callee));
|
|
13811
13810
|
}
|
|
13812
13811
|
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
13813
|
-
if (parent.callee ===
|
|
13814
|
-
if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] ===
|
|
13815
|
-
if (scopes && parent.arguments?.[1] ===
|
|
13816
|
-
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] ===
|
|
13812
|
+
if (parent.callee === callbackRoot) return true;
|
|
13813
|
+
if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === callbackRoot) return true;
|
|
13814
|
+
if (scopes && parent.arguments?.[1] === callbackRoot && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
|
|
13815
|
+
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === callbackRoot;
|
|
13817
13816
|
};
|
|
13818
13817
|
//#endregion
|
|
13819
13818
|
//#region src/plugin/utils/find-render-phase-component-or-hook.ts
|
|
@@ -13940,9 +13939,10 @@ const doNodesCoverEveryPathFromFunctionEntry = (owner, matchingNodes, context, o
|
|
|
13940
13939
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
13941
13940
|
const getFunctionBindingIdentifier$1 = (functionNode) => {
|
|
13942
13941
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
13943
|
-
const
|
|
13942
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
13943
|
+
const parent = functionRoot.parent;
|
|
13944
13944
|
if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
13945
|
-
if (isNodeOfType(parent, "AssignmentExpression") && parent.right ===
|
|
13945
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionRoot && isNodeOfType(parent.left, "Identifier")) return parent.left;
|
|
13946
13946
|
if (isNodeOfType(parent, "CallExpression")) {
|
|
13947
13947
|
const callParent = parent.parent;
|
|
13948
13948
|
if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
|
|
@@ -19264,7 +19264,7 @@ const forbidElements = defineRule({
|
|
|
19264
19264
|
});
|
|
19265
19265
|
//#endregion
|
|
19266
19266
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
19267
|
-
const MESSAGE$
|
|
19267
|
+
const MESSAGE$58 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
19268
19268
|
const forwardRefUsesRef = defineRule({
|
|
19269
19269
|
id: "forward-ref-uses-ref",
|
|
19270
19270
|
title: "forwardRef without ref parameter",
|
|
@@ -19287,7 +19287,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
19287
19287
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
19288
19288
|
context.report({
|
|
19289
19289
|
node: inner,
|
|
19290
|
-
message: MESSAGE$
|
|
19290
|
+
message: MESSAGE$58
|
|
19291
19291
|
});
|
|
19292
19292
|
} })
|
|
19293
19293
|
});
|
|
@@ -19328,7 +19328,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
19328
19328
|
});
|
|
19329
19329
|
//#endregion
|
|
19330
19330
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
19331
|
-
const MESSAGE$
|
|
19331
|
+
const MESSAGE$57 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
19332
19332
|
const DEFAULT_HEADING_TAGS = [
|
|
19333
19333
|
"h1",
|
|
19334
19334
|
"h2",
|
|
@@ -19362,7 +19362,7 @@ const headingHasContent = defineRule({
|
|
|
19362
19362
|
for (const attribute of ["aria-label", "aria-labelledby"]) if (hasJsxPropIgnoreCase(node.attributes, attribute)) return;
|
|
19363
19363
|
context.report({
|
|
19364
19364
|
node,
|
|
19365
|
-
message: MESSAGE$
|
|
19365
|
+
message: MESSAGE$57
|
|
19366
19366
|
});
|
|
19367
19367
|
} };
|
|
19368
19368
|
}
|
|
@@ -19530,7 +19530,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
19530
19530
|
});
|
|
19531
19531
|
//#endregion
|
|
19532
19532
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
19533
|
-
const MESSAGE$
|
|
19533
|
+
const MESSAGE$56 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
19534
19534
|
const resolveSettings$38 = (settings) => {
|
|
19535
19535
|
const reactDoctor = settings?.["react-doctor"];
|
|
19536
19536
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -19577,13 +19577,13 @@ const htmlHasLang = defineRule({
|
|
|
19577
19577
|
if (!lang) {
|
|
19578
19578
|
context.report({
|
|
19579
19579
|
node: node.name,
|
|
19580
|
-
message: MESSAGE$
|
|
19580
|
+
message: MESSAGE$56
|
|
19581
19581
|
});
|
|
19582
19582
|
return;
|
|
19583
19583
|
}
|
|
19584
19584
|
if (evaluateLang(lang.value) === "empty") context.report({
|
|
19585
19585
|
node: lang,
|
|
19586
|
-
message: MESSAGE$
|
|
19586
|
+
message: MESSAGE$56
|
|
19587
19587
|
});
|
|
19588
19588
|
} };
|
|
19589
19589
|
}
|
|
@@ -19856,7 +19856,7 @@ const isJsxFragmentElement = (node, scopes) => {
|
|
|
19856
19856
|
};
|
|
19857
19857
|
//#endregion
|
|
19858
19858
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
19859
|
-
const MESSAGE$
|
|
19859
|
+
const MESSAGE$55 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
19860
19860
|
const isStaticallyAriaHidden = (openingElement) => {
|
|
19861
19861
|
const ariaHiddenAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-hidden", false);
|
|
19862
19862
|
if (!ariaHiddenAttribute) return false;
|
|
@@ -19945,14 +19945,14 @@ const iframeHasTitle = defineRule({
|
|
|
19945
19945
|
if (!titleAttr) {
|
|
19946
19946
|
if (hasSpread || tag === "iframe") context.report({
|
|
19947
19947
|
node: node.name,
|
|
19948
|
-
message: MESSAGE$
|
|
19948
|
+
message: MESSAGE$55
|
|
19949
19949
|
});
|
|
19950
19950
|
return;
|
|
19951
19951
|
}
|
|
19952
19952
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
19953
19953
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
19954
19954
|
node: titleAttr,
|
|
19955
|
-
message: MESSAGE$
|
|
19955
|
+
message: MESSAGE$55
|
|
19956
19956
|
});
|
|
19957
19957
|
} })
|
|
19958
19958
|
});
|
|
@@ -20077,7 +20077,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
20077
20077
|
});
|
|
20078
20078
|
//#endregion
|
|
20079
20079
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
20080
|
-
const MESSAGE$
|
|
20080
|
+
const MESSAGE$54 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
20081
20081
|
const DEFAULT_COMPONENTS = ["img"];
|
|
20082
20082
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
20083
20083
|
"image",
|
|
@@ -20142,7 +20142,7 @@ const imgRedundantAlt = defineRule({
|
|
|
20142
20142
|
if (!altAttribute) return;
|
|
20143
20143
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
20144
20144
|
node: altAttribute,
|
|
20145
|
-
message: MESSAGE$
|
|
20145
|
+
message: MESSAGE$54
|
|
20146
20146
|
});
|
|
20147
20147
|
} };
|
|
20148
20148
|
}
|
|
@@ -20674,7 +20674,7 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
20674
20674
|
//#endregion
|
|
20675
20675
|
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
20676
20676
|
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
20677
|
-
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
20677
|
+
const COMPONENT_NAME_PATTERN$1 = /^[A-Z]/;
|
|
20678
20678
|
const isImportedSelectAtom = (callExpression) => {
|
|
20679
20679
|
if (!isNodeOfType(callExpression.callee, "Identifier")) return false;
|
|
20680
20680
|
const localName = callExpression.callee.name;
|
|
@@ -20710,14 +20710,14 @@ const getHandlerNamedBindingName = (functionNode) => {
|
|
|
20710
20710
|
const containingFunctionIsComponentOrHook = (functionNode) => {
|
|
20711
20711
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
|
|
20712
20712
|
const declaredName = functionNode.id.name;
|
|
20713
|
-
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$3.test(declaredName);
|
|
20713
|
+
return COMPONENT_NAME_PATTERN$1.test(declaredName) || HOOK_NAME_PATTERN$3.test(declaredName);
|
|
20714
20714
|
}
|
|
20715
20715
|
let cursor = functionNode.parent ?? null;
|
|
20716
20716
|
while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
20717
20717
|
if (!cursor) return false;
|
|
20718
20718
|
if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
|
|
20719
20719
|
if (!isNodeOfType(cursor.id, "Identifier")) return false;
|
|
20720
|
-
return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN$3.test(cursor.id.name);
|
|
20720
|
+
return COMPONENT_NAME_PATTERN$1.test(cursor.id.name) || HOOK_NAME_PATTERN$3.test(cursor.id.name);
|
|
20721
20721
|
};
|
|
20722
20722
|
const isBindingInvokedOnRenderPath = (root, bindingName) => {
|
|
20723
20723
|
let didFindRenderPathInvocation = false;
|
|
@@ -21795,7 +21795,7 @@ const isDiscardedProbeInsideTry = (node) => {
|
|
|
21795
21795
|
return false;
|
|
21796
21796
|
};
|
|
21797
21797
|
const isComponentOrHookName = (name) => /^[A-Z]/.test(name) || /^use[A-Z0-9]/.test(name);
|
|
21798
|
-
const getFunctionName = (functionNode) => {
|
|
21798
|
+
const getFunctionName$1 = (functionNode) => {
|
|
21799
21799
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id.name;
|
|
21800
21800
|
const holder = functionNode.parent;
|
|
21801
21801
|
if (holder && isNodeOfType(holder, "VariableDeclarator") && isNodeOfType(holder.id, "Identifier")) return holder.id.name;
|
|
@@ -21804,7 +21804,7 @@ const getFunctionName = (functionNode) => {
|
|
|
21804
21804
|
const isUncacheableOptionsMergeUtility = (node) => {
|
|
21805
21805
|
const enclosingFunction = findEnclosingFunction$1(node);
|
|
21806
21806
|
if (!enclosingFunction || !isFunctionLike$1(enclosingFunction)) return false;
|
|
21807
|
-
const functionName = getFunctionName(enclosingFunction);
|
|
21807
|
+
const functionName = getFunctionName$1(enclosingFunction);
|
|
21808
21808
|
if (!functionName || isComponentOrHookName(functionName)) return false;
|
|
21809
21809
|
const parameterNames = /* @__PURE__ */ new Set();
|
|
21810
21810
|
for (const parameter of enclosingFunction.params ?? []) collectPatternNames(parameter, parameterNames);
|
|
@@ -22106,7 +22106,7 @@ const getHoistableRegExpConstructionKind = (node, context) => {
|
|
|
22106
22106
|
if (!STATEFUL_REGEXP_FLAGS_PATTERN.test(effectiveFlags)) return "stateless";
|
|
22107
22107
|
return isSafeStatefulReplaceAllSearch(node, effectiveFlags, context) ? "statefulReplaceAll" : null;
|
|
22108
22108
|
};
|
|
22109
|
-
const MESSAGE$
|
|
22109
|
+
const MESSAGE$53 = "`new RegExp()` rebuilds the pattern on every loop pass. Move it to a constant outside the loop.";
|
|
22110
22110
|
const jsHoistRegexp = defineRule({
|
|
22111
22111
|
id: "js-hoist-regexp",
|
|
22112
22112
|
title: "RegExp built inside a loop",
|
|
@@ -22123,7 +22123,7 @@ const jsHoistRegexp = defineRule({
|
|
|
22123
22123
|
if (constructionKind === "statefulReplaceAll" && cachedEnvironmentHazard === "replaceAllIntegrityLost") return;
|
|
22124
22124
|
context.report({
|
|
22125
22125
|
node,
|
|
22126
|
-
message: MESSAGE$
|
|
22126
|
+
message: MESSAGE$53
|
|
22127
22127
|
});
|
|
22128
22128
|
};
|
|
22129
22129
|
return createLoopAwareVisitors({
|
|
@@ -24890,7 +24890,7 @@ const jsxMaxDepth = defineRule({
|
|
|
24890
24890
|
});
|
|
24891
24891
|
//#endregion
|
|
24892
24892
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
24893
|
-
const MESSAGE$
|
|
24893
|
+
const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
24894
24894
|
const LITERAL_TEXT_TAGS = new Set([
|
|
24895
24895
|
"code",
|
|
24896
24896
|
"pre",
|
|
@@ -24960,7 +24960,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
24960
24960
|
if (isDeliberateStyledCommentToken(node)) return;
|
|
24961
24961
|
context.report({
|
|
24962
24962
|
node,
|
|
24963
|
-
message: MESSAGE$
|
|
24963
|
+
message: MESSAGE$52
|
|
24964
24964
|
});
|
|
24965
24965
|
} })
|
|
24966
24966
|
});
|
|
@@ -24991,7 +24991,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
24991
24991
|
};
|
|
24992
24992
|
//#endregion
|
|
24993
24993
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
24994
|
-
const MESSAGE$
|
|
24994
|
+
const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
24995
24995
|
const CONTEXT_MODULES$1 = [
|
|
24996
24996
|
"react",
|
|
24997
24997
|
"use-context-selector",
|
|
@@ -25094,7 +25094,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
25094
25094
|
if (!isConstructedValue(innerExpression)) continue;
|
|
25095
25095
|
context.report({
|
|
25096
25096
|
node: attribute,
|
|
25097
|
-
message: MESSAGE$
|
|
25097
|
+
message: MESSAGE$51
|
|
25098
25098
|
});
|
|
25099
25099
|
}
|
|
25100
25100
|
}
|
|
@@ -25960,7 +25960,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
25960
25960
|
];
|
|
25961
25961
|
//#endregion
|
|
25962
25962
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
25963
|
-
const MESSAGE$
|
|
25963
|
+
const MESSAGE$50 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
25964
25964
|
const isDataArrayPropName = (propName) => {
|
|
25965
25965
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
25966
25966
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -26047,7 +26047,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
26047
26047
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
26048
26048
|
context.report({
|
|
26049
26049
|
node,
|
|
26050
|
-
message: MESSAGE$
|
|
26050
|
+
message: MESSAGE$50
|
|
26051
26051
|
});
|
|
26052
26052
|
}
|
|
26053
26053
|
};
|
|
@@ -26305,7 +26305,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
26305
26305
|
]);
|
|
26306
26306
|
//#endregion
|
|
26307
26307
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
26308
|
-
const MESSAGE$
|
|
26308
|
+
const MESSAGE$49 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
26309
26309
|
const isAccessorPredicateName = (propName) => {
|
|
26310
26310
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
26311
26311
|
if (propName.length <= prefix.length) continue;
|
|
@@ -26512,7 +26512,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
26512
26512
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
26513
26513
|
context.report({
|
|
26514
26514
|
node,
|
|
26515
|
-
message: MESSAGE$
|
|
26515
|
+
message: MESSAGE$49
|
|
26516
26516
|
});
|
|
26517
26517
|
}
|
|
26518
26518
|
};
|
|
@@ -26732,7 +26732,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
26732
26732
|
];
|
|
26733
26733
|
//#endregion
|
|
26734
26734
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
26735
|
-
const MESSAGE$
|
|
26735
|
+
const MESSAGE$48 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
26736
26736
|
const isConfigObjectPropName = (propName) => {
|
|
26737
26737
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
26738
26738
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -26821,7 +26821,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
26821
26821
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
26822
26822
|
context.report({
|
|
26823
26823
|
node,
|
|
26824
|
-
message: MESSAGE$
|
|
26824
|
+
message: MESSAGE$48
|
|
26825
26825
|
});
|
|
26826
26826
|
}
|
|
26827
26827
|
};
|
|
@@ -26829,7 +26829,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
26829
26829
|
});
|
|
26830
26830
|
//#endregion
|
|
26831
26831
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
26832
|
-
const MESSAGE$
|
|
26832
|
+
const MESSAGE$47 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
26833
26833
|
const JAVASCRIPT_URL_PATTERN = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
26834
26834
|
const resolveSettings$28 = (settings) => {
|
|
26835
26835
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -26867,7 +26867,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
26867
26867
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
26868
26868
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
26869
26869
|
node: attribute,
|
|
26870
|
-
message: MESSAGE$
|
|
26870
|
+
message: MESSAGE$47
|
|
26871
26871
|
});
|
|
26872
26872
|
}
|
|
26873
26873
|
} };
|
|
@@ -27210,7 +27210,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
27210
27210
|
});
|
|
27211
27211
|
//#endregion
|
|
27212
27212
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
27213
|
-
const MESSAGE$
|
|
27213
|
+
const MESSAGE$46 = "You can't tell what props reach this element when you spread them.";
|
|
27214
27214
|
const resolveSettings$25 = (settings) => {
|
|
27215
27215
|
const reactDoctor = settings?.["react-doctor"];
|
|
27216
27216
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -27253,7 +27253,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
27253
27253
|
}
|
|
27254
27254
|
context.report({
|
|
27255
27255
|
node: attribute,
|
|
27256
|
-
message: MESSAGE$
|
|
27256
|
+
message: MESSAGE$46
|
|
27257
27257
|
});
|
|
27258
27258
|
didReportInFile = true;
|
|
27259
27259
|
return;
|
|
@@ -27529,7 +27529,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
27529
27529
|
});
|
|
27530
27530
|
//#endregion
|
|
27531
27531
|
//#region src/plugin/rules/a11y/lang.ts
|
|
27532
|
-
const MESSAGE$
|
|
27532
|
+
const MESSAGE$45 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
|
|
27533
27533
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
27534
27534
|
"aa",
|
|
27535
27535
|
"ab",
|
|
@@ -28009,7 +28009,7 @@ const lang = defineRule({
|
|
|
28009
28009
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
28010
28010
|
context.report({
|
|
28011
28011
|
node: langAttr,
|
|
28012
|
-
message: MESSAGE$
|
|
28012
|
+
message: MESSAGE$45
|
|
28013
28013
|
});
|
|
28014
28014
|
return;
|
|
28015
28015
|
}
|
|
@@ -28018,7 +28018,7 @@ const lang = defineRule({
|
|
|
28018
28018
|
if (value === null) return;
|
|
28019
28019
|
if (!isValidLangTag(value)) context.report({
|
|
28020
28020
|
node: langAttr,
|
|
28021
|
-
message: MESSAGE$
|
|
28021
|
+
message: MESSAGE$45
|
|
28022
28022
|
});
|
|
28023
28023
|
} })
|
|
28024
28024
|
});
|
|
@@ -28063,13 +28063,13 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
28063
28063
|
recommendation: "Use a constrained compiler for untrusted content, disable expressions/raw HTML, sandbox renderers, and avoid caching attacker-controlled output across tenants.",
|
|
28064
28064
|
scan: scanByPattern({
|
|
28065
28065
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
28066
|
-
pattern: /(?:@mdx-js\/mdx|next-mdx-remote|\b(?:MDXRemote|compileMDX|evaluateMdx)\b)[\s\S]{0,700}\b(?:repo|customer|tenant|user[-_]?(?:content|markdown|mdx|input|provided|generated|submitted)|untrusted|searchParams|req\.|request\.|fetch\s*\(|prisma\.|db\.|database|rehypeRaw|allowDangerousHtml)/i,
|
|
28066
|
+
pattern: /(?:@mdx-js\/mdx|next-mdx-remote|\b(?:MDXRemote|compileMDX|evaluateMdx)\b)[\s\S]{0,700}\b(?:repo\b|customer|tenant|user[-_]?(?:content|markdown|mdx|input|provided|generated|submitted)|untrusted|searchParams|req\.|request\.|fetch\s*\(|prisma\.|db\.|database|rehypeRaw|allowDangerousHtml)/i,
|
|
28067
28067
|
message: "MDX/markdown rendering code may evaluate user or repository content during SSR or static generation."
|
|
28068
28068
|
})
|
|
28069
28069
|
});
|
|
28070
28070
|
//#endregion
|
|
28071
28071
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
28072
|
-
const MESSAGE$
|
|
28072
|
+
const MESSAGE$44 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
28073
28073
|
const DEFAULT_AUDIO = ["audio"];
|
|
28074
28074
|
const DEFAULT_VIDEO = ["video"];
|
|
28075
28075
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -28159,7 +28159,7 @@ const mediaHasCaption = defineRule({
|
|
|
28159
28159
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
28160
28160
|
context.report({
|
|
28161
28161
|
node: node.name,
|
|
28162
|
-
message: MESSAGE$
|
|
28162
|
+
message: MESSAGE$44
|
|
28163
28163
|
});
|
|
28164
28164
|
return;
|
|
28165
28165
|
}
|
|
@@ -28178,12 +28178,553 @@ const mediaHasCaption = defineRule({
|
|
|
28178
28178
|
return kindValue.value.toLowerCase() === "captions";
|
|
28179
28179
|
})) context.report({
|
|
28180
28180
|
node: node.name,
|
|
28181
|
-
message: MESSAGE$
|
|
28181
|
+
message: MESSAGE$44
|
|
28182
28182
|
});
|
|
28183
28183
|
} };
|
|
28184
28184
|
}
|
|
28185
28185
|
});
|
|
28186
28186
|
//#endregion
|
|
28187
|
+
//#region src/plugin/utils/get-class-binding-symbol.ts
|
|
28188
|
+
const getClassBindingSymbol = (classNode, scopes) => {
|
|
28189
|
+
if (isNodeOfType(classNode.id, "Identifier")) return scopes.symbolFor(classNode.id);
|
|
28190
|
+
const parent = classNode.parent;
|
|
28191
|
+
return isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier") ? scopes.symbolFor(parent.id) : null;
|
|
28192
|
+
};
|
|
28193
|
+
//#endregion
|
|
28194
|
+
//#region src/plugin/utils/mobx-rule-gates.ts
|
|
28195
|
+
const MOBX_RULE_GATES = {
|
|
28196
|
+
"mobx-reaction-disposer-discarded": { requires: ["mobx:4"] },
|
|
28197
|
+
"mobx-no-make-auto-observable-in-inheritance": { requires: ["mobx:6"] },
|
|
28198
|
+
"mobx-no-computed-side-effects": { requires: ["mobx:4"] },
|
|
28199
|
+
"mobx-async-action-requires-action": { requires: ["mobx:4"] },
|
|
28200
|
+
"mobx-no-observer-wrapped-memo": { requires: [
|
|
28201
|
+
"mobx:4",
|
|
28202
|
+
"mobx-react-binding",
|
|
28203
|
+
"react"
|
|
28204
|
+
] },
|
|
28205
|
+
"mobx-make-observable-unconditional": { requires: ["mobx:6"] },
|
|
28206
|
+
"mobx-legacy-decorator-needs-make-observable": { requires: ["mobx:6"] },
|
|
28207
|
+
"mobx-initialize-before-make-auto-observable": { requires: ["mobx:6"] },
|
|
28208
|
+
"mobx-observable-read-needs-observer": {
|
|
28209
|
+
requires: [
|
|
28210
|
+
"mobx:4",
|
|
28211
|
+
"mobx-react-binding",
|
|
28212
|
+
"react"
|
|
28213
|
+
],
|
|
28214
|
+
disabledWhen: ["mobx-react-observer"]
|
|
28215
|
+
},
|
|
28216
|
+
"mobx-observer-before-inject": { requires: [
|
|
28217
|
+
"mobx:4",
|
|
28218
|
+
"mobx-react",
|
|
28219
|
+
"react"
|
|
28220
|
+
] },
|
|
28221
|
+
"mobx-reaction-requires-observable": { requires: ["mobx:4"] },
|
|
28222
|
+
"mobx-no-invalid-observable-override": { requires: ["mobx:6"] },
|
|
28223
|
+
"mobx-no-observable-prop-to-untracked-child": { requires: [
|
|
28224
|
+
"mobx:4",
|
|
28225
|
+
"mobx-react-binding",
|
|
28226
|
+
"react"
|
|
28227
|
+
] },
|
|
28228
|
+
"mobx-no-stale-observable-snapshot-after-await": { requires: ["mobx:4"] },
|
|
28229
|
+
"mobx-no-reaction-comparison-value-mutation": { requires: ["mobx:4"] },
|
|
28230
|
+
"mobx-observer-class-no-should-component-update": { requires: [
|
|
28231
|
+
"mobx:4",
|
|
28232
|
+
"mobx-react",
|
|
28233
|
+
"react"
|
|
28234
|
+
] },
|
|
28235
|
+
"mobx-enable-static-rendering-for-ssr": { requires: [
|
|
28236
|
+
"mobx:4",
|
|
28237
|
+
"mobx-react-binding",
|
|
28238
|
+
"react",
|
|
28239
|
+
"ssr"
|
|
28240
|
+
] },
|
|
28241
|
+
"mobx-no-rest-destructure-observable": { requires: [
|
|
28242
|
+
"mobx:4",
|
|
28243
|
+
"mobx-react-binding",
|
|
28244
|
+
"react"
|
|
28245
|
+
] },
|
|
28246
|
+
"mobx-computed-depends-on-non-observable": { requires: ["mobx:4"] },
|
|
28247
|
+
"mobx-no-keepalive-computed-without-disposal": { requires: ["mobx:4"] }
|
|
28248
|
+
};
|
|
28249
|
+
//#endregion
|
|
28250
|
+
//#region src/plugin/utils/resolve-imported-api-reference.ts
|
|
28251
|
+
const resolveImportSymbol = (symbol) => {
|
|
28252
|
+
const importDeclaration = getImportDeclarationForSymbol(symbol);
|
|
28253
|
+
if (!importDeclaration || typeof importDeclaration.source.value !== "string") return null;
|
|
28254
|
+
if (isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")) return {
|
|
28255
|
+
source: importDeclaration.source.value,
|
|
28256
|
+
importedName: null,
|
|
28257
|
+
isNamespace: true
|
|
28258
|
+
};
|
|
28259
|
+
if (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier")) return {
|
|
28260
|
+
source: importDeclaration.source.value,
|
|
28261
|
+
importedName: "default",
|
|
28262
|
+
isNamespace: false
|
|
28263
|
+
};
|
|
28264
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
28265
|
+
return importedName ? {
|
|
28266
|
+
source: importDeclaration.source.value,
|
|
28267
|
+
importedName,
|
|
28268
|
+
isNamespace: false
|
|
28269
|
+
} : null;
|
|
28270
|
+
};
|
|
28271
|
+
const resolveIdentifierReference = (identifier, scopes, visitedSymbolIds) => {
|
|
28272
|
+
const symbol = scopes.symbolFor(identifier);
|
|
28273
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return null;
|
|
28274
|
+
if (symbol.kind === "import") return resolveImportSymbol(symbol);
|
|
28275
|
+
if (symbol.kind !== "const" || !symbol.initializer) return null;
|
|
28276
|
+
visitedSymbolIds.add(symbol.id);
|
|
28277
|
+
if (isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) {
|
|
28278
|
+
const receiver = resolveImportedApiReference(symbol.initializer, scopes, visitedSymbolIds);
|
|
28279
|
+
if (!receiver || !receiver.isNamespace && receiver.importedName !== "default") return null;
|
|
28280
|
+
for (const property of symbol.declarationNode.id.properties) {
|
|
28281
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
28282
|
+
if ((isNodeOfType(property.value, "AssignmentPattern") ? property.value.left : property.value) !== symbol.bindingIdentifier) continue;
|
|
28283
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
28284
|
+
return propertyName ? {
|
|
28285
|
+
source: receiver.source,
|
|
28286
|
+
importedName: propertyName,
|
|
28287
|
+
isNamespace: false
|
|
28288
|
+
} : null;
|
|
28289
|
+
}
|
|
28290
|
+
return null;
|
|
28291
|
+
}
|
|
28292
|
+
return resolveImportedApiReference(symbol.initializer, scopes, visitedSymbolIds);
|
|
28293
|
+
};
|
|
28294
|
+
const resolveImportedApiReference = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
28295
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
28296
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) return resolveIdentifierReference(unwrappedExpression, scopes, visitedSymbolIds);
|
|
28297
|
+
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
28298
|
+
const propertyName = getStaticPropertyName(unwrappedExpression);
|
|
28299
|
+
if (!propertyName) return null;
|
|
28300
|
+
const receiver = resolveImportedApiReference(unwrappedExpression.object, scopes, visitedSymbolIds);
|
|
28301
|
+
if (!receiver || !receiver.isNamespace && receiver.importedName !== "default") return null;
|
|
28302
|
+
return {
|
|
28303
|
+
source: receiver.source,
|
|
28304
|
+
importedName: propertyName,
|
|
28305
|
+
isNamespace: false
|
|
28306
|
+
};
|
|
28307
|
+
};
|
|
28308
|
+
//#endregion
|
|
28309
|
+
//#region src/plugin/rules/mobx/mobx-no-make-auto-observable-in-inheritance.ts
|
|
28310
|
+
const MESSAGE$43 = "MobX does not support `makeAutoObservable(this)` in inherited classes. Use composition or explicit `makeObservable` annotations.";
|
|
28311
|
+
const getEnclosingConstructorClass = (node) => {
|
|
28312
|
+
let ancestor = node.parent;
|
|
28313
|
+
while (ancestor) {
|
|
28314
|
+
if (!isFunctionLike$1(ancestor)) {
|
|
28315
|
+
ancestor = ancestor.parent ?? null;
|
|
28316
|
+
continue;
|
|
28317
|
+
}
|
|
28318
|
+
const methodDefinition = ancestor.parent;
|
|
28319
|
+
if (!isNodeOfType(methodDefinition, "MethodDefinition") || methodDefinition.kind !== "constructor") return null;
|
|
28320
|
+
const classNode = methodDefinition.parent?.parent;
|
|
28321
|
+
return isNodeOfType(classNode, "ClassDeclaration") || isNodeOfType(classNode, "ClassExpression") ? classNode : null;
|
|
28322
|
+
}
|
|
28323
|
+
return null;
|
|
28324
|
+
};
|
|
28325
|
+
const isNonNullSuperclass = (classNode) => {
|
|
28326
|
+
if (!classNode.superClass) return false;
|
|
28327
|
+
const superClass = stripParenExpression(classNode.superClass);
|
|
28328
|
+
return !(isNodeOfType(superClass, "Literal") && superClass.value === null);
|
|
28329
|
+
};
|
|
28330
|
+
const subclassedClassSymbolIdsByAnalysis = /* @__PURE__ */ new WeakMap();
|
|
28331
|
+
const getSubclassedClassSymbolIds = (node, scopes) => {
|
|
28332
|
+
const cached = subclassedClassSymbolIdsByAnalysis.get(scopes);
|
|
28333
|
+
if (cached) return cached;
|
|
28334
|
+
const symbolIds = /* @__PURE__ */ new Set();
|
|
28335
|
+
const program = findProgramRoot(node);
|
|
28336
|
+
if (program) walkAst(program, (candidate) => {
|
|
28337
|
+
if (!isNodeOfType(candidate, "ClassDeclaration") && !isNodeOfType(candidate, "ClassExpression") || !isNonNullSuperclass(candidate)) return;
|
|
28338
|
+
const superClassExpression = candidate.superClass;
|
|
28339
|
+
if (!superClassExpression) return;
|
|
28340
|
+
const superClass = stripParenExpression(superClassExpression);
|
|
28341
|
+
if (!isNodeOfType(superClass, "Identifier")) return;
|
|
28342
|
+
const symbol = resolveConstIdentifierAlias(superClass, scopes);
|
|
28343
|
+
if (symbol?.kind === "class" || symbol?.kind === "const" && isNodeOfType(symbol.initializer, "ClassExpression")) symbolIds.add(symbol.id);
|
|
28344
|
+
});
|
|
28345
|
+
subclassedClassSymbolIdsByAnalysis.set(scopes, symbolIds);
|
|
28346
|
+
return symbolIds;
|
|
28347
|
+
};
|
|
28348
|
+
const isMakeAutoObservableCall = (callExpression, scopes) => {
|
|
28349
|
+
const reference = resolveImportedApiReference(callExpression.callee, scopes);
|
|
28350
|
+
return reference?.source === "mobx" && reference.importedName === "makeAutoObservable";
|
|
28351
|
+
};
|
|
28352
|
+
const mobxNoMakeAutoObservableInInheritance = defineRule({
|
|
28353
|
+
id: "mobx-no-make-auto-observable-in-inheritance",
|
|
28354
|
+
title: "Unsupported MobX auto-observable inheritance",
|
|
28355
|
+
severity: "error",
|
|
28356
|
+
category: "Bugs",
|
|
28357
|
+
requires: MOBX_RULE_GATES["mobx-no-make-auto-observable-in-inheritance"].requires,
|
|
28358
|
+
recommendation: "Replace inheritance with composition, or annotate inherited members explicitly with `makeObservable`.",
|
|
28359
|
+
create: (context) => ({ CallExpression(callExpression) {
|
|
28360
|
+
if (!isMakeAutoObservableCall(callExpression, context.scopes)) return;
|
|
28361
|
+
const target = callExpression.arguments[0];
|
|
28362
|
+
if (!target || !isNodeOfType(stripParenExpression(target), "ThisExpression")) return;
|
|
28363
|
+
const classNode = getEnclosingConstructorClass(callExpression);
|
|
28364
|
+
if (!classNode) return;
|
|
28365
|
+
const classSymbol = getClassBindingSymbol(classNode, context.scopes);
|
|
28366
|
+
const isSubclassed = Boolean(classSymbol && getSubclassedClassSymbolIds(callExpression, context.scopes).has(classSymbol.id));
|
|
28367
|
+
if (!isNonNullSuperclass(classNode) && !isSubclassed) return;
|
|
28368
|
+
context.report({
|
|
28369
|
+
node: callExpression,
|
|
28370
|
+
message: MESSAGE$43
|
|
28371
|
+
});
|
|
28372
|
+
} })
|
|
28373
|
+
});
|
|
28374
|
+
//#endregion
|
|
28375
|
+
//#region src/plugin/rules/mobx/mobx-no-observer-wrapped-memo.ts
|
|
28376
|
+
const OBSERVER_MODULES = new Set(["mobx-react", "mobx-react-lite"]);
|
|
28377
|
+
const MESSAGE$42 = "`observer` cannot wrap an already memoized or observed component. Apply `observer` first, then place `memo` outside only if needed.";
|
|
28378
|
+
const isObserverCall = (callExpression, scopes) => {
|
|
28379
|
+
const reference = resolveImportedApiReference(callExpression.callee, scopes);
|
|
28380
|
+
return Boolean(reference?.importedName === "observer" && OBSERVER_MODULES.has(reference.source));
|
|
28381
|
+
};
|
|
28382
|
+
const hasInvalidInnerWrapper = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
28383
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
28384
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
28385
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
28386
|
+
if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
28387
|
+
visitedSymbolIds.add(symbol.id);
|
|
28388
|
+
return hasInvalidInnerWrapper(symbol.initializer, scopes, visitedSymbolIds);
|
|
28389
|
+
}
|
|
28390
|
+
if (!isNodeOfType(unwrappedExpression, "CallExpression")) return false;
|
|
28391
|
+
const reference = resolveImportedApiReference(unwrappedExpression.callee, scopes);
|
|
28392
|
+
if (reference?.importedName === "observer" && OBSERVER_MODULES.has(reference.source)) return true;
|
|
28393
|
+
return reference?.source === "react" && reference.importedName === "memo";
|
|
28394
|
+
};
|
|
28395
|
+
const mobxNoObserverWrappedMemo = defineRule({
|
|
28396
|
+
id: "mobx-no-observer-wrapped-memo",
|
|
28397
|
+
title: "Invalid MobX observer wrapper order",
|
|
28398
|
+
severity: "error",
|
|
28399
|
+
category: "Bugs",
|
|
28400
|
+
requires: MOBX_RULE_GATES["mobx-no-observer-wrapped-memo"].requires,
|
|
28401
|
+
recommendation: "Pass the component directly to `observer`, or apply React `memo` outside the resulting observer component.",
|
|
28402
|
+
create: (context) => ({ CallExpression(callExpression) {
|
|
28403
|
+
if (!isObserverCall(callExpression, context.scopes)) return;
|
|
28404
|
+
const componentArgument = callExpression.arguments[0];
|
|
28405
|
+
if (!componentArgument) return;
|
|
28406
|
+
if (!hasInvalidInnerWrapper(componentArgument, context.scopes)) return;
|
|
28407
|
+
context.report({
|
|
28408
|
+
node: callExpression,
|
|
28409
|
+
message: MESSAGE$42
|
|
28410
|
+
});
|
|
28411
|
+
} })
|
|
28412
|
+
});
|
|
28413
|
+
//#endregion
|
|
28414
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
28415
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
28416
|
+
let node = callExpression;
|
|
28417
|
+
let parent = node.parent;
|
|
28418
|
+
while (parent) {
|
|
28419
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
28420
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
28421
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
28422
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
28423
|
+
node = parent;
|
|
28424
|
+
parent = node.parent;
|
|
28425
|
+
continue;
|
|
28426
|
+
}
|
|
28427
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
28428
|
+
node = parent;
|
|
28429
|
+
parent = node.parent;
|
|
28430
|
+
continue;
|
|
28431
|
+
}
|
|
28432
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
28433
|
+
node = parent;
|
|
28434
|
+
parent = node.parent;
|
|
28435
|
+
continue;
|
|
28436
|
+
}
|
|
28437
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
28438
|
+
const expressions = parent.expressions ?? [];
|
|
28439
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
28440
|
+
node = parent;
|
|
28441
|
+
parent = node.parent;
|
|
28442
|
+
continue;
|
|
28443
|
+
}
|
|
28444
|
+
return false;
|
|
28445
|
+
}
|
|
28446
|
+
return false;
|
|
28447
|
+
};
|
|
28448
|
+
//#endregion
|
|
28449
|
+
//#region src/plugin/utils/resolve-stable-options-object.ts
|
|
28450
|
+
const resolveStableOptionsObject = (expression, observedPropertyNames, scopes, referenceNode = expression, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
28451
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
28452
|
+
if (isNodeOfType(unwrappedExpression, "ObjectExpression")) return unwrappedExpression;
|
|
28453
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
28454
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
28455
|
+
if (!symbol?.initializer || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, referenceNode, scopes) || observedPropertyNames.some((propertyName) => hasPossibleStaticPropertyWriteBefore(unwrappedExpression, propertyName, referenceNode, scopes))) return null;
|
|
28456
|
+
visitedSymbolIds.add(symbol.id);
|
|
28457
|
+
return resolveStableOptionsObject(symbol.initializer, observedPropertyNames, scopes, referenceNode, visitedSymbolIds);
|
|
28458
|
+
};
|
|
28459
|
+
//#endregion
|
|
28460
|
+
//#region src/plugin/rules/mobx/mobx-reaction-disposer-discarded.ts
|
|
28461
|
+
const MESSAGE$41 = "This MobX reaction discards its disposer and can outlive its owner. Store and dispose it during teardown, or provide an AbortSignal.";
|
|
28462
|
+
const LEAKING_SUBSCRIPTION_NAMES = new Set(["reaction", "autorun"]);
|
|
28463
|
+
const OPTIONS_ARGUMENT_INDEX = {
|
|
28464
|
+
autorun: 1,
|
|
28465
|
+
reaction: 2
|
|
28466
|
+
};
|
|
28467
|
+
const DISPOSER_COERCION_NAMES = new Set([
|
|
28468
|
+
"Boolean",
|
|
28469
|
+
"Number",
|
|
28470
|
+
"String"
|
|
28471
|
+
]);
|
|
28472
|
+
const NON_OBSERVABLE_GLOBAL_RECEIVER_NAMES = new Set([
|
|
28473
|
+
"Array",
|
|
28474
|
+
"Boolean",
|
|
28475
|
+
"JSON",
|
|
28476
|
+
"Math",
|
|
28477
|
+
"Number",
|
|
28478
|
+
"Object",
|
|
28479
|
+
"String",
|
|
28480
|
+
"console"
|
|
28481
|
+
]);
|
|
28482
|
+
const NON_OBSERVING_IMPORTED_METHOD_NAMES = new Set([
|
|
28483
|
+
"add",
|
|
28484
|
+
"clear",
|
|
28485
|
+
"delete",
|
|
28486
|
+
"remove",
|
|
28487
|
+
"save",
|
|
28488
|
+
"set",
|
|
28489
|
+
"update",
|
|
28490
|
+
"write"
|
|
28491
|
+
]);
|
|
28492
|
+
const REACTION_PARAMETER_INDEX = {
|
|
28493
|
+
autorun: 0,
|
|
28494
|
+
reaction: 2
|
|
28495
|
+
};
|
|
28496
|
+
const REACTION_CALLBACK_INDEX = {
|
|
28497
|
+
autorun: 0,
|
|
28498
|
+
reaction: 1
|
|
28499
|
+
};
|
|
28500
|
+
const OBSERVATION_CALLBACK_INDEX = {
|
|
28501
|
+
autorun: 0,
|
|
28502
|
+
reaction: 0
|
|
28503
|
+
};
|
|
28504
|
+
const PROCESS_LIFETIME_WIRING_NAME_PATTERN = /^(?:register.*(?:reactions?|autoruns?)|init.*(?:stores?|reactions?|autoruns?)|setup.*(?:stores?|reactions?|autoruns?)|bootstrap(?:app(?:lication)?|stores?|reactions?|autoruns?))$/i;
|
|
28505
|
+
const resolveLeakingSubscriptionName = (callExpression, scopes) => {
|
|
28506
|
+
const reference = resolveImportedApiReference(callExpression.callee, scopes);
|
|
28507
|
+
if (reference?.source !== "mobx" || !reference.importedName || !LEAKING_SUBSCRIPTION_NAMES.has(reference.importedName)) return null;
|
|
28508
|
+
return reference.importedName;
|
|
28509
|
+
};
|
|
28510
|
+
const isEvaluatedAtModuleScope = (node) => {
|
|
28511
|
+
let ancestor = node.parent;
|
|
28512
|
+
while (ancestor) {
|
|
28513
|
+
if (isNodeOfType(ancestor, "PropertyDefinition") || isNodeOfType(ancestor, "AccessorProperty")) {
|
|
28514
|
+
if (!ancestor.static) return false;
|
|
28515
|
+
ancestor = ancestor.parent ?? null;
|
|
28516
|
+
continue;
|
|
28517
|
+
}
|
|
28518
|
+
if (isNodeOfType(ancestor, "StaticBlock")) {
|
|
28519
|
+
ancestor = ancestor.parent ?? null;
|
|
28520
|
+
continue;
|
|
28521
|
+
}
|
|
28522
|
+
if (isFunctionLike$1(ancestor)) {
|
|
28523
|
+
const functionRoot = findTransparentExpressionRoot(ancestor);
|
|
28524
|
+
const invocation = functionRoot.parent;
|
|
28525
|
+
if (isNodeOfType(invocation, "CallExpression") && stripParenExpression(invocation.callee) === functionRoot) {
|
|
28526
|
+
ancestor = invocation.parent ?? null;
|
|
28527
|
+
continue;
|
|
28528
|
+
}
|
|
28529
|
+
return false;
|
|
28530
|
+
}
|
|
28531
|
+
ancestor = ancestor.parent ?? null;
|
|
28532
|
+
}
|
|
28533
|
+
return true;
|
|
28534
|
+
};
|
|
28535
|
+
const getFunctionName = (functionNode) => {
|
|
28536
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id.name;
|
|
28537
|
+
const parent = functionNode.parent;
|
|
28538
|
+
return isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier") ? parent.id.name : null;
|
|
28539
|
+
};
|
|
28540
|
+
const isModuleScopedFunction = (functionNode) => {
|
|
28541
|
+
let ancestor = functionNode.parent;
|
|
28542
|
+
while (ancestor) {
|
|
28543
|
+
if (isFunctionLike$1(ancestor) || isNodeOfType(ancestor, "ClassBody")) return false;
|
|
28544
|
+
if (isNodeOfType(ancestor, "Program")) return true;
|
|
28545
|
+
ancestor = ancestor.parent ?? null;
|
|
28546
|
+
}
|
|
28547
|
+
return false;
|
|
28548
|
+
};
|
|
28549
|
+
const getDirectCalls = (functionNode, scopes) => {
|
|
28550
|
+
const bindingIdentifier = isNodeOfType(functionNode, "FunctionDeclaration") ? functionNode.id : isNodeOfType(functionNode.parent, "VariableDeclarator") ? functionNode.parent.id : null;
|
|
28551
|
+
if (!bindingIdentifier || !isNodeOfType(bindingIdentifier, "Identifier")) return [];
|
|
28552
|
+
const symbol = scopes.scopeFor(functionNode).symbolsByName.get(bindingIdentifier.name);
|
|
28553
|
+
if (!symbol) return [];
|
|
28554
|
+
const calls = [];
|
|
28555
|
+
const program = findProgramRoot(functionNode);
|
|
28556
|
+
if (!program) return calls;
|
|
28557
|
+
walkAst(program, (candidate) => {
|
|
28558
|
+
if (!isNodeOfType(candidate, "CallExpression")) return;
|
|
28559
|
+
const callee = stripParenExpression(candidate.callee);
|
|
28560
|
+
if (!isNodeOfType(callee, "Identifier")) return;
|
|
28561
|
+
if (resolveConstIdentifierAlias(callee, scopes)?.id === symbol.id) calls.push(candidate);
|
|
28562
|
+
});
|
|
28563
|
+
return calls;
|
|
28564
|
+
};
|
|
28565
|
+
const processLifetimeClassSymbolIdsByAnalysis = /* @__PURE__ */ new WeakMap();
|
|
28566
|
+
const getProcessLifetimeClassSymbolIds = (node, scopes) => {
|
|
28567
|
+
const cached = processLifetimeClassSymbolIdsByAnalysis.get(scopes);
|
|
28568
|
+
if (cached) return cached;
|
|
28569
|
+
const instantiationScopeBySymbolId = /* @__PURE__ */ new Map();
|
|
28570
|
+
const program = findProgramRoot(node);
|
|
28571
|
+
if (program) walkAst(program, (candidate) => {
|
|
28572
|
+
if (!isNodeOfType(candidate, "NewExpression")) return;
|
|
28573
|
+
const callee = stripParenExpression(candidate.callee);
|
|
28574
|
+
const symbol = isNodeOfType(callee, "Identifier") ? resolveConstIdentifierAlias(callee, scopes) : null;
|
|
28575
|
+
if (!symbol) return;
|
|
28576
|
+
const wasModuleOnly = instantiationScopeBySymbolId.get(symbol.id) ?? true;
|
|
28577
|
+
instantiationScopeBySymbolId.set(symbol.id, wasModuleOnly && isEvaluatedAtModuleScope(candidate));
|
|
28578
|
+
});
|
|
28579
|
+
const processLifetimeSymbolIds = /* @__PURE__ */ new Set();
|
|
28580
|
+
for (const [symbolId, isModuleOnly] of instantiationScopeBySymbolId) if (isModuleOnly) processLifetimeSymbolIds.add(symbolId);
|
|
28581
|
+
processLifetimeClassSymbolIdsByAnalysis.set(scopes, processLifetimeSymbolIds);
|
|
28582
|
+
return processLifetimeSymbolIds;
|
|
28583
|
+
};
|
|
28584
|
+
const isProcessLifetimeWiring = (node, scopes) => {
|
|
28585
|
+
let ancestor = node.parent;
|
|
28586
|
+
while (ancestor) {
|
|
28587
|
+
if (!isFunctionLike$1(ancestor)) {
|
|
28588
|
+
ancestor = ancestor.parent ?? null;
|
|
28589
|
+
continue;
|
|
28590
|
+
}
|
|
28591
|
+
const functionName = getFunctionName(ancestor);
|
|
28592
|
+
if (functionName && PROCESS_LIFETIME_WIRING_NAME_PATTERN.test(functionName) && isModuleScopedFunction(ancestor)) {
|
|
28593
|
+
const calls = getDirectCalls(ancestor, scopes);
|
|
28594
|
+
return calls.length > 0 && calls.every(isEvaluatedAtModuleScope);
|
|
28595
|
+
}
|
|
28596
|
+
const methodDefinition = ancestor.parent;
|
|
28597
|
+
if (isNodeOfType(methodDefinition, "MethodDefinition") && methodDefinition.kind === "constructor") {
|
|
28598
|
+
let classNode = methodDefinition.parent?.parent;
|
|
28599
|
+
if (isNodeOfType(classNode, "ClassDeclaration") || isNodeOfType(classNode, "ClassExpression")) {
|
|
28600
|
+
const classRoot = findTransparentExpressionRoot(classNode);
|
|
28601
|
+
const classInstantiation = classRoot.parent;
|
|
28602
|
+
if (isNodeOfType(classNode, "ClassExpression") && isNodeOfType(classInstantiation, "NewExpression") && stripParenExpression(classInstantiation.callee) === classRoot) return isEvaluatedAtModuleScope(classInstantiation);
|
|
28603
|
+
const classSymbol = getClassBindingSymbol(classNode, scopes);
|
|
28604
|
+
return Boolean(classSymbol && getProcessLifetimeClassSymbolIds(node, scopes).has(classSymbol.id));
|
|
28605
|
+
}
|
|
28606
|
+
}
|
|
28607
|
+
return false;
|
|
28608
|
+
}
|
|
28609
|
+
return false;
|
|
28610
|
+
};
|
|
28611
|
+
const mayCarryAbortSignal = (optionsArgument, scopes) => {
|
|
28612
|
+
if (!optionsArgument) return false;
|
|
28613
|
+
const options = resolveStableOptionsObject(optionsArgument, ["signal"], scopes);
|
|
28614
|
+
if (!options) return true;
|
|
28615
|
+
return options.properties.some((property) => {
|
|
28616
|
+
if (!isNodeOfType(property, "Property")) return true;
|
|
28617
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
28618
|
+
if (propertyName === null) return true;
|
|
28619
|
+
if (propertyName !== "signal") return false;
|
|
28620
|
+
const value = property.value;
|
|
28621
|
+
if (isNodeOfType(value, "Identifier") && value.name === "undefined") return false;
|
|
28622
|
+
if (isNodeOfType(value, "Literal") && value.value == null) return false;
|
|
28623
|
+
return !(isNodeOfType(value, "UnaryExpression") && value.operator === "void");
|
|
28624
|
+
});
|
|
28625
|
+
};
|
|
28626
|
+
const callbackDisposesReaction = (callExpression, subscriptionName, scopes) => {
|
|
28627
|
+
const callbackArgument = callExpression.arguments[REACTION_CALLBACK_INDEX[subscriptionName]];
|
|
28628
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return false;
|
|
28629
|
+
const callback = resolveExactLocalFunction(callbackArgument, scopes);
|
|
28630
|
+
if (!callback || !isFunctionLike$1(callback)) return false;
|
|
28631
|
+
const reactionParameter = callback.params?.[REACTION_PARAMETER_INDEX[subscriptionName]];
|
|
28632
|
+
if (!reactionParameter || !isNodeOfType(reactionParameter, "Identifier")) return false;
|
|
28633
|
+
const reactionSymbol = scopes.symbolFor(reactionParameter);
|
|
28634
|
+
if (!reactionSymbol) return false;
|
|
28635
|
+
let doesDisposeReaction = false;
|
|
28636
|
+
walkAst(callback, (candidate) => {
|
|
28637
|
+
if (doesDisposeReaction || !isNodeOfType(candidate, "CallExpression")) return;
|
|
28638
|
+
const callee = stripParenExpression(candidate.callee);
|
|
28639
|
+
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
28640
|
+
const receiver = stripParenExpression(callee.object);
|
|
28641
|
+
if (isNodeOfType(receiver, "Identifier") && scopes.symbolFor(receiver)?.id === reactionSymbol.id && getStaticPropertyKeyName(callee, { allowComputedString: true }) === "dispose") doesDisposeReaction = true;
|
|
28642
|
+
});
|
|
28643
|
+
return doesDisposeReaction;
|
|
28644
|
+
};
|
|
28645
|
+
const getMemberReceiverRoot = (memberExpression) => {
|
|
28646
|
+
let receiver = stripParenExpression(memberExpression);
|
|
28647
|
+
while (isNodeOfType(receiver, "MemberExpression")) receiver = stripParenExpression(receiver.object);
|
|
28648
|
+
return receiver;
|
|
28649
|
+
};
|
|
28650
|
+
const observesOnlyInstanceRootedState = (callExpression, subscriptionName, scopes) => {
|
|
28651
|
+
const callbackArgument = callExpression.arguments[OBSERVATION_CALLBACK_INDEX[subscriptionName]];
|
|
28652
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return false;
|
|
28653
|
+
const callback = resolveExactLocalFunction(callbackArgument, scopes);
|
|
28654
|
+
if (!callback || !isFunctionLike$1(callback)) return false;
|
|
28655
|
+
let observesExternalState = false;
|
|
28656
|
+
walkAst(callback, (candidate) => {
|
|
28657
|
+
if (candidate !== callback && isFunctionLike$1(candidate)) return false;
|
|
28658
|
+
if (observesExternalState || !isNodeOfType(candidate, "MemberExpression")) return;
|
|
28659
|
+
const receiver = getMemberReceiverRoot(candidate);
|
|
28660
|
+
const candidateRoot = findTransparentExpressionRoot(candidate);
|
|
28661
|
+
const parent = candidateRoot.parent;
|
|
28662
|
+
const isDirectMethodCall = isNodeOfType(parent, "CallExpression") && parent.callee === candidateRoot;
|
|
28663
|
+
if (isNodeOfType(receiver, "ThisExpression")) {
|
|
28664
|
+
if (isDirectMethodCall) observesExternalState = true;
|
|
28665
|
+
return;
|
|
28666
|
+
}
|
|
28667
|
+
if (!isNodeOfType(receiver, "Identifier")) {
|
|
28668
|
+
observesExternalState = true;
|
|
28669
|
+
return;
|
|
28670
|
+
}
|
|
28671
|
+
const receiverSymbol = scopes.symbolFor(receiver);
|
|
28672
|
+
if (receiverSymbol && isAstDescendant(receiverSymbol.declarationNode, callback) || scopes.isGlobalReference(receiver) && NON_OBSERVABLE_GLOBAL_RECEIVER_NAMES.has(receiver.name)) return;
|
|
28673
|
+
if (receiverSymbol?.kind === "import" && isDirectMethodCall && NON_OBSERVING_IMPORTED_METHOD_NAMES.has(getStaticPropertyKeyName(candidate, { allowComputedString: true }) ?? "")) return;
|
|
28674
|
+
observesExternalState = true;
|
|
28675
|
+
});
|
|
28676
|
+
return !observesExternalState;
|
|
28677
|
+
};
|
|
28678
|
+
const isForwardedFromConciseArrow = (callExpression) => {
|
|
28679
|
+
let expressionRoot = findTransparentExpressionRoot(callExpression);
|
|
28680
|
+
let parent = expressionRoot.parent;
|
|
28681
|
+
while (parent) {
|
|
28682
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === expressionRoot) return true;
|
|
28683
|
+
if (isNodeOfType(parent, "LogicalExpression") && (parent.right === expressionRoot || parent.left === expressionRoot && parent.operator !== "&&") || isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === expressionRoot || parent.alternate === expressionRoot) || isNodeOfType(parent, "SequenceExpression") && parent.expressions[parent.expressions.length - 1] === expressionRoot) {
|
|
28684
|
+
expressionRoot = findTransparentExpressionRoot(parent);
|
|
28685
|
+
parent = expressionRoot.parent;
|
|
28686
|
+
continue;
|
|
28687
|
+
}
|
|
28688
|
+
return false;
|
|
28689
|
+
}
|
|
28690
|
+
return false;
|
|
28691
|
+
};
|
|
28692
|
+
const isDisposerDiscarded = (callExpression) => {
|
|
28693
|
+
if (isForwardedFromConciseArrow(callExpression)) return false;
|
|
28694
|
+
const expressionRoot = findTransparentExpressionRoot(callExpression);
|
|
28695
|
+
if (isResultDiscardedCall(callExpression)) return true;
|
|
28696
|
+
const parent = expressionRoot.parent;
|
|
28697
|
+
if (!parent) return false;
|
|
28698
|
+
if (isNodeOfType(parent, "UnaryExpression") || isNodeOfType(parent, "BinaryExpression")) return true;
|
|
28699
|
+
if ((isNodeOfType(parent, "IfStatement") || isNodeOfType(parent, "WhileStatement") || isNodeOfType(parent, "DoWhileStatement") || isNodeOfType(parent, "ForStatement")) && parent.test === expressionRoot) return true;
|
|
28700
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.test === expressionRoot || isNodeOfType(parent, "SwitchStatement") && parent.discriminant === expressionRoot) return true;
|
|
28701
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.left === expressionRoot) return parent.operator === "&&" || isResultDiscardedCall(parent);
|
|
28702
|
+
const callee = isNodeOfType(parent, "CallExpression") ? stripParenExpression(parent.callee) : null;
|
|
28703
|
+
return Boolean(isNodeOfType(parent, "CallExpression") && parent.arguments.some((argument) => argument === expressionRoot) && isNodeOfType(callee, "Identifier") && DISPOSER_COERCION_NAMES.has(callee.name));
|
|
28704
|
+
};
|
|
28705
|
+
const mobxReactionDisposerDiscarded = defineRule({
|
|
28706
|
+
id: "mobx-reaction-disposer-discarded",
|
|
28707
|
+
title: "MobX reaction disposer discarded",
|
|
28708
|
+
severity: "warn",
|
|
28709
|
+
category: "Bugs",
|
|
28710
|
+
requires: MOBX_RULE_GATES["mobx-reaction-disposer-discarded"].requires,
|
|
28711
|
+
recommendation: "Keep the disposer returned by `reaction` or `autorun` and invoke it during teardown, or provide an AbortSignal.",
|
|
28712
|
+
create: (context) => ({ CallExpression(callExpression) {
|
|
28713
|
+
const subscriptionName = resolveLeakingSubscriptionName(callExpression, context.scopes);
|
|
28714
|
+
if (!subscriptionName || !isDisposerDiscarded(callExpression)) return;
|
|
28715
|
+
if (isEvaluatedAtModuleScope(callExpression)) return;
|
|
28716
|
+
if (isProcessLifetimeWiring(callExpression, context.scopes)) return;
|
|
28717
|
+
if (callbackDisposesReaction(callExpression, subscriptionName, context.scopes)) return;
|
|
28718
|
+
if (observesOnlyInstanceRootedState(callExpression, subscriptionName, context.scopes)) return;
|
|
28719
|
+
const optionsArgument = callExpression.arguments[OPTIONS_ARGUMENT_INDEX[subscriptionName]];
|
|
28720
|
+
if (mayCarryAbortSignal(optionsArgument, context.scopes)) return;
|
|
28721
|
+
context.report({
|
|
28722
|
+
node: callExpression,
|
|
28723
|
+
message: MESSAGE$41
|
|
28724
|
+
});
|
|
28725
|
+
} })
|
|
28726
|
+
});
|
|
28727
|
+
//#endregion
|
|
28187
28728
|
//#region src/plugin/utils/has-jsx-prop.ts
|
|
28188
28729
|
const hasJsxProp = (attributes, targetProp) => {
|
|
28189
28730
|
for (const attribute of attributes) {
|
|
@@ -28974,11 +29515,11 @@ const hasEmailTemplateImport = (programRoot) => {
|
|
|
28974
29515
|
return found;
|
|
28975
29516
|
};
|
|
28976
29517
|
//#endregion
|
|
28977
|
-
//#region src/plugin/utils/build-
|
|
28978
|
-
const
|
|
28979
|
-
const
|
|
28980
|
-
const
|
|
28981
|
-
const
|
|
29518
|
+
//#region src/plugin/utils/build-source-project-index.ts
|
|
29519
|
+
const SOURCE_PROJECT_FILE_PATTERN = /\.[cm]?[jt]sx?$/i;
|
|
29520
|
+
const SOURCE_PROJECT_DECLARATION_FILE_PATTERN = /\.d\.[cm]?[jt]s$/i;
|
|
29521
|
+
const SOURCE_PROJECT_MDX_FILE_PATTERN = /\.mdx$/i;
|
|
29522
|
+
const SOURCE_PROJECT_IGNORED_DIRECTORY_NAMES = new Set([
|
|
28982
29523
|
".angular",
|
|
28983
29524
|
".astro",
|
|
28984
29525
|
".cache",
|
|
@@ -28999,12 +29540,12 @@ const GENERATED_IMAGE_IGNORED_DIRECTORY_NAMES = new Set([
|
|
|
28999
29540
|
"out",
|
|
29000
29541
|
"storybook-static"
|
|
29001
29542
|
]);
|
|
29002
|
-
const
|
|
29003
|
-
const
|
|
29004
|
-
const cachedScopes =
|
|
29543
|
+
const sourceProjectScopeCache = /* @__PURE__ */ new WeakMap();
|
|
29544
|
+
const getSourceProjectModuleScopes = (programNode) => {
|
|
29545
|
+
const cachedScopes = sourceProjectScopeCache.get(programNode);
|
|
29005
29546
|
if (cachedScopes) return cachedScopes;
|
|
29006
29547
|
const scopes = analyzeScopes(programNode);
|
|
29007
|
-
|
|
29548
|
+
sourceProjectScopeCache.set(programNode, scopes);
|
|
29008
29549
|
return scopes;
|
|
29009
29550
|
};
|
|
29010
29551
|
const listProductionSourceFiles = (rootDirectory) => {
|
|
@@ -29022,7 +29563,7 @@ const listProductionSourceFiles = (rootDirectory) => {
|
|
|
29022
29563
|
}
|
|
29023
29564
|
for (const entry of entries) {
|
|
29024
29565
|
const absolutePath = path.join(currentDirectory, entry.name);
|
|
29025
|
-
const isIgnoredDirectoryName =
|
|
29566
|
+
const isIgnoredDirectoryName = SOURCE_PROJECT_IGNORED_DIRECTORY_NAMES.has(entry.name) || entry.name.startsWith(".") && entry.name !== ".dumi" && entry.name !== ".storybook";
|
|
29026
29567
|
if (entry.isSymbolicLink() && isIgnoredDirectoryName) continue;
|
|
29027
29568
|
if (entry.isSymbolicLink()) return null;
|
|
29028
29569
|
if (entry.isDirectory()) {
|
|
@@ -29031,12 +29572,12 @@ const listProductionSourceFiles = (rootDirectory) => {
|
|
|
29031
29572
|
continue;
|
|
29032
29573
|
}
|
|
29033
29574
|
if (!entry.isFile() || isTestlikeFilename(absolutePath)) continue;
|
|
29034
|
-
if (
|
|
29575
|
+
if (SOURCE_PROJECT_MDX_FILE_PATTERN.test(entry.name)) {
|
|
29035
29576
|
hasOpaqueMdxConsumerSurface = true;
|
|
29036
29577
|
continue;
|
|
29037
29578
|
}
|
|
29038
|
-
if (!
|
|
29039
|
-
if (
|
|
29579
|
+
if (!SOURCE_PROJECT_FILE_PATTERN.test(entry.name)) continue;
|
|
29580
|
+
if (SOURCE_PROJECT_DECLARATION_FILE_PATTERN.test(entry.name)) continue;
|
|
29040
29581
|
sourceFilePaths.push(normalizeFilename(absolutePath));
|
|
29041
29582
|
}
|
|
29042
29583
|
}
|
|
@@ -29081,7 +29622,7 @@ const indexModuleSources = (module, consumerModulesByFilePath, resolvedSourcePat
|
|
|
29081
29622
|
consumerModulesByFilePath.set(normalizedSourcePath, consumerModules);
|
|
29082
29623
|
});
|
|
29083
29624
|
};
|
|
29084
|
-
const
|
|
29625
|
+
const buildSourceProjectIndex = (rootDirectory, currentFilePath, currentProgramNode, currentScopes) => {
|
|
29085
29626
|
const productionSourceFiles = listProductionSourceFiles(rootDirectory);
|
|
29086
29627
|
if (!productionSourceFiles) return null;
|
|
29087
29628
|
const modulesByFilePath = /* @__PURE__ */ new Map();
|
|
@@ -29099,7 +29640,7 @@ const buildGeneratedImageProjectIndex = (rootDirectory, currentFilePath, current
|
|
|
29099
29640
|
modulesByFilePath.set(filePath, {
|
|
29100
29641
|
filePath,
|
|
29101
29642
|
programNode: parsedProgram,
|
|
29102
|
-
scopes:
|
|
29643
|
+
scopes: getSourceProjectModuleScopes(parsedProgram)
|
|
29103
29644
|
});
|
|
29104
29645
|
}
|
|
29105
29646
|
const consumerModulesByFilePath = /* @__PURE__ */ new Map();
|
|
@@ -29115,6 +29656,51 @@ const buildGeneratedImageProjectIndex = (rootDirectory, currentFilePath, current
|
|
|
29115
29656
|
};
|
|
29116
29657
|
};
|
|
29117
29658
|
//#endregion
|
|
29659
|
+
//#region src/plugin/utils/get-direct-function-binding-identifier.ts
|
|
29660
|
+
const getDirectFunctionBindingIdentifier = (functionNode) => {
|
|
29661
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
29662
|
+
const functionValueRoot = findTransparentExpressionRoot(functionNode);
|
|
29663
|
+
const parent = functionValueRoot.parent;
|
|
29664
|
+
return isNodeOfType(parent, "VariableDeclarator") && parent.init === functionValueRoot && isNodeOfType(parent.id, "Identifier") ? parent.id : null;
|
|
29665
|
+
};
|
|
29666
|
+
//#endregion
|
|
29667
|
+
//#region src/plugin/utils/get-function-export-names.ts
|
|
29668
|
+
const getExportedSpecifierName$1 = (specifier) => {
|
|
29669
|
+
const exported = specifier.exported;
|
|
29670
|
+
if (isNodeOfType(exported, "Identifier")) return exported.name;
|
|
29671
|
+
return isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
|
|
29672
|
+
};
|
|
29673
|
+
const getLocalSpecifierName = (specifier) => {
|
|
29674
|
+
const local = specifier.local;
|
|
29675
|
+
if (isNodeOfType(local, "Identifier")) return local.name;
|
|
29676
|
+
return isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
|
|
29677
|
+
};
|
|
29678
|
+
const getFunctionExportNames = (programNode, functionNode) => {
|
|
29679
|
+
const functionValueRoot = findTransparentExpressionRoot(functionNode);
|
|
29680
|
+
const bindingName = getDirectFunctionBindingIdentifier(functionNode)?.name ?? null;
|
|
29681
|
+
const exportedNames = /* @__PURE__ */ new Set();
|
|
29682
|
+
for (const statement of programNode.body) {
|
|
29683
|
+
if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
|
|
29684
|
+
if (statement.declaration === functionValueRoot || bindingName && isNodeOfType(statement.declaration, "Identifier") && statement.declaration.name === bindingName) exportedNames.add("default");
|
|
29685
|
+
continue;
|
|
29686
|
+
}
|
|
29687
|
+
if (!isNodeOfType(statement, "ExportNamedDeclaration")) continue;
|
|
29688
|
+
const declaration = statement.declaration;
|
|
29689
|
+
if (declaration === functionValueRoot && bindingName) exportedNames.add(bindingName);
|
|
29690
|
+
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
29691
|
+
for (const declarator of declaration.declarations) if (declarator.init === functionValueRoot && isNodeOfType(declarator.id, "Identifier")) exportedNames.add(declarator.id.name);
|
|
29692
|
+
}
|
|
29693
|
+
if (!bindingName || statement.source) continue;
|
|
29694
|
+
for (const specifier of statement.specifiers) {
|
|
29695
|
+
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
29696
|
+
if (getLocalSpecifierName(specifier) !== bindingName) continue;
|
|
29697
|
+
const exportedName = getExportedSpecifierName$1(specifier);
|
|
29698
|
+
if (exportedName) exportedNames.add(exportedName);
|
|
29699
|
+
}
|
|
29700
|
+
}
|
|
29701
|
+
return [...exportedNames];
|
|
29702
|
+
};
|
|
29703
|
+
//#endregion
|
|
29118
29704
|
//#region src/plugin/utils/read-nearest-package-manifest.ts
|
|
29119
29705
|
const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
|
|
29120
29706
|
const cachedManifestByPackageDirectory = /* @__PURE__ */ new Map();
|
|
@@ -29199,37 +29785,6 @@ const getImportSpecifierName = (specifier) => {
|
|
|
29199
29785
|
if (isNodeOfType(imported, "Identifier")) return imported.name;
|
|
29200
29786
|
return isNodeOfType(imported, "Literal") && typeof imported.value === "string" ? imported.value : null;
|
|
29201
29787
|
};
|
|
29202
|
-
const getDirectFunctionBindingIdentifier = (functionNode) => {
|
|
29203
|
-
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
29204
|
-
const functionValueRoot = findTransparentExpressionRoot(functionNode);
|
|
29205
|
-
const parent = functionValueRoot.parent;
|
|
29206
|
-
return isNodeOfType(parent, "VariableDeclarator") && parent.init === functionValueRoot && isNodeOfType(parent.id, "Identifier") ? parent.id : null;
|
|
29207
|
-
};
|
|
29208
|
-
const getExportNamesForFunction = (programNode, functionNode) => {
|
|
29209
|
-
const functionValueRoot = findTransparentExpressionRoot(functionNode);
|
|
29210
|
-
const bindingName = getDirectFunctionBindingIdentifier(functionNode)?.name ?? null;
|
|
29211
|
-
const exportedNames = /* @__PURE__ */ new Set();
|
|
29212
|
-
for (const statement of programNode.body) {
|
|
29213
|
-
if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
|
|
29214
|
-
if (statement.declaration === functionValueRoot || bindingName && isNodeOfType(statement.declaration, "Identifier") && statement.declaration.name === bindingName) exportedNames.add("default");
|
|
29215
|
-
continue;
|
|
29216
|
-
}
|
|
29217
|
-
if (!isNodeOfType(statement, "ExportNamedDeclaration")) continue;
|
|
29218
|
-
const declaration = statement.declaration;
|
|
29219
|
-
if (declaration === functionValueRoot && bindingName) exportedNames.add(bindingName);
|
|
29220
|
-
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
29221
|
-
for (const declarator of declaration.declarations) if (declarator.init === functionValueRoot && isNodeOfType(declarator.id, "Identifier")) exportedNames.add(declarator.id.name);
|
|
29222
|
-
}
|
|
29223
|
-
if (!bindingName || statement.source) continue;
|
|
29224
|
-
for (const specifier of statement.specifiers) {
|
|
29225
|
-
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
29226
|
-
if (getImportedSpecifierName(specifier) !== bindingName) continue;
|
|
29227
|
-
const exportedName = getExportedSpecifierName(specifier);
|
|
29228
|
-
if (exportedName) exportedNames.add(exportedName);
|
|
29229
|
-
}
|
|
29230
|
-
}
|
|
29231
|
-
return [...exportedNames];
|
|
29232
|
-
};
|
|
29233
29788
|
const isTransparentGeneratedImageValueFlow = (expression, target) => {
|
|
29234
29789
|
let current = findTransparentExpressionRoot(expression);
|
|
29235
29790
|
while (current !== target) {
|
|
@@ -29288,7 +29843,7 @@ const classifyInvokedExpression = (module, expression, state) => {
|
|
|
29288
29843
|
}
|
|
29289
29844
|
const forwardingFunction = getForwardingFunction(expression);
|
|
29290
29845
|
if (!forwardingFunction) return false;
|
|
29291
|
-
const exportedNames =
|
|
29846
|
+
const exportedNames = getFunctionExportNames(module.programNode, forwardingFunction);
|
|
29292
29847
|
if (exportedNames.length === 0) return false;
|
|
29293
29848
|
for (const exportedName of exportedNames) enqueueExport(state, module.filePath, exportedName);
|
|
29294
29849
|
return true;
|
|
@@ -29421,9 +29976,9 @@ const createExportedJsxGeneratedImageOwnershipAnalyzer = (context) => {
|
|
|
29421
29976
|
const programNode = findProgramRoot(jsxNode);
|
|
29422
29977
|
const enclosingFunction = findEnclosingFunction$1(jsxNode);
|
|
29423
29978
|
if (!programNode || !enclosingFunction) return false;
|
|
29424
|
-
const initialExportNames =
|
|
29979
|
+
const initialExportNames = getFunctionExportNames(programNode, enclosingFunction);
|
|
29425
29980
|
if (initialExportNames.length === 0) return false;
|
|
29426
|
-
if (projectIndex === void 0) projectIndex =
|
|
29981
|
+
if (projectIndex === void 0) projectIndex = buildSourceProjectIndex(rootDirectory, filename, programNode, context.scopes);
|
|
29427
29982
|
if (!projectIndex || projectIndex.hasOpaqueMdxConsumerSurface) return false;
|
|
29428
29983
|
const state = {
|
|
29429
29984
|
projectIndex,
|
|
@@ -30231,7 +30786,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
30231
30786
|
});
|
|
30232
30787
|
//#endregion
|
|
30233
30788
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
30234
|
-
const MESSAGE$
|
|
30789
|
+
const MESSAGE$40 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
30235
30790
|
const isUndefinedIdentifier$1 = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
30236
30791
|
const noAccessKey = defineRule({
|
|
30237
30792
|
id: "no-access-key",
|
|
@@ -30250,7 +30805,7 @@ const noAccessKey = defineRule({
|
|
|
30250
30805
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
30251
30806
|
context.report({
|
|
30252
30807
|
node: accessKey,
|
|
30253
|
-
message: MESSAGE$
|
|
30808
|
+
message: MESSAGE$40
|
|
30254
30809
|
});
|
|
30255
30810
|
return;
|
|
30256
30811
|
}
|
|
@@ -30260,7 +30815,7 @@ const noAccessKey = defineRule({
|
|
|
30260
30815
|
if (isUndefinedIdentifier$1(expression)) return;
|
|
30261
30816
|
context.report({
|
|
30262
30817
|
node: accessKey,
|
|
30263
|
-
message: MESSAGE$
|
|
30818
|
+
message: MESSAGE$40
|
|
30264
30819
|
});
|
|
30265
30820
|
}
|
|
30266
30821
|
} };
|
|
@@ -32603,7 +33158,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
32603
33158
|
});
|
|
32604
33159
|
//#endregion
|
|
32605
33160
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
32606
|
-
const MESSAGE$
|
|
33161
|
+
const MESSAGE$39 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
|
|
32607
33162
|
const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
32608
33163
|
"button",
|
|
32609
33164
|
"embed",
|
|
@@ -32715,7 +33270,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
32715
33270
|
const isImplicitlyFocusable = isNativelyFocusable(tag, node);
|
|
32716
33271
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
32717
33272
|
node: ariaHidden,
|
|
32718
|
-
message: MESSAGE$
|
|
33273
|
+
message: MESSAGE$39
|
|
32719
33274
|
});
|
|
32720
33275
|
} })
|
|
32721
33276
|
});
|
|
@@ -33762,7 +34317,7 @@ const isAllLiteralArrayExpression = (node) => {
|
|
|
33762
34317
|
};
|
|
33763
34318
|
//#endregion
|
|
33764
34319
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
33765
|
-
const MESSAGE$
|
|
34320
|
+
const MESSAGE$38 = "Your users can see & submit the wrong data when this list reorders.";
|
|
33766
34321
|
const SECOND_INDEX_METHODS = new Set([
|
|
33767
34322
|
"every",
|
|
33768
34323
|
"filter",
|
|
@@ -33881,14 +34436,14 @@ const noArrayIndexKey = defineRule({
|
|
|
33881
34436
|
if (propName !== "key") continue;
|
|
33882
34437
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
33883
34438
|
node: property,
|
|
33884
|
-
message: MESSAGE$
|
|
34439
|
+
message: MESSAGE$38
|
|
33885
34440
|
});
|
|
33886
34441
|
}
|
|
33887
34442
|
} })
|
|
33888
34443
|
});
|
|
33889
34444
|
//#endregion
|
|
33890
34445
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
33891
|
-
const MESSAGE$
|
|
34446
|
+
const MESSAGE$37 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
33892
34447
|
const noAsyncEffectCallback = defineRule({
|
|
33893
34448
|
id: "no-async-effect-callback",
|
|
33894
34449
|
title: "Async effect callback",
|
|
@@ -33906,13 +34461,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
33906
34461
|
if (!callback.async) return;
|
|
33907
34462
|
context.report({
|
|
33908
34463
|
node: callback,
|
|
33909
|
-
message: MESSAGE$
|
|
34464
|
+
message: MESSAGE$37
|
|
33910
34465
|
});
|
|
33911
34466
|
} })
|
|
33912
34467
|
});
|
|
33913
34468
|
//#endregion
|
|
33914
34469
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
33915
|
-
const MESSAGE$
|
|
34470
|
+
const MESSAGE$36 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
33916
34471
|
const resolveSettings$21 = (settings) => {
|
|
33917
34472
|
const reactDoctor = settings?.["react-doctor"];
|
|
33918
34473
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -34009,7 +34564,7 @@ const noAutofocus = defineRule({
|
|
|
34009
34564
|
if (isConditionallyRendered(node)) return;
|
|
34010
34565
|
context.report({
|
|
34011
34566
|
node: autoFocusAttribute,
|
|
34012
|
-
message: MESSAGE$
|
|
34567
|
+
message: MESSAGE$36
|
|
34013
34568
|
});
|
|
34014
34569
|
} };
|
|
34015
34570
|
}
|
|
@@ -35338,7 +35893,7 @@ const noChainStateUpdates = defineRule({
|
|
|
35338
35893
|
});
|
|
35339
35894
|
//#endregion
|
|
35340
35895
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
35341
|
-
const MESSAGE$
|
|
35896
|
+
const MESSAGE$35 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
35342
35897
|
const noChildrenProp = defineRule({
|
|
35343
35898
|
id: "no-children-prop",
|
|
35344
35899
|
title: "Children passed as a prop",
|
|
@@ -35350,7 +35905,7 @@ const noChildrenProp = defineRule({
|
|
|
35350
35905
|
if (node.name.name !== "children") return;
|
|
35351
35906
|
context.report({
|
|
35352
35907
|
node: node.name,
|
|
35353
|
-
message: MESSAGE$
|
|
35908
|
+
message: MESSAGE$35
|
|
35354
35909
|
});
|
|
35355
35910
|
},
|
|
35356
35911
|
CallExpression(node) {
|
|
@@ -35363,7 +35918,7 @@ const noChildrenProp = defineRule({
|
|
|
35363
35918
|
const propertyKey = property.key;
|
|
35364
35919
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
35365
35920
|
node: propertyKey,
|
|
35366
|
-
message: MESSAGE$
|
|
35921
|
+
message: MESSAGE$35
|
|
35367
35922
|
});
|
|
35368
35923
|
}
|
|
35369
35924
|
}
|
|
@@ -35371,7 +35926,7 @@ const noChildrenProp = defineRule({
|
|
|
35371
35926
|
});
|
|
35372
35927
|
//#endregion
|
|
35373
35928
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
35374
|
-
const MESSAGE$
|
|
35929
|
+
const MESSAGE$34 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
35375
35930
|
const noCloneElement = defineRule({
|
|
35376
35931
|
id: "no-clone-element",
|
|
35377
35932
|
title: "cloneElement makes child props fragile",
|
|
@@ -35384,7 +35939,7 @@ const noCloneElement = defineRule({
|
|
|
35384
35939
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
35385
35940
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
35386
35941
|
node: callee,
|
|
35387
|
-
message: MESSAGE$
|
|
35942
|
+
message: MESSAGE$34
|
|
35388
35943
|
});
|
|
35389
35944
|
return;
|
|
35390
35945
|
}
|
|
@@ -35397,7 +35952,7 @@ const noCloneElement = defineRule({
|
|
|
35397
35952
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
35398
35953
|
context.report({
|
|
35399
35954
|
node: callee,
|
|
35400
|
-
message: MESSAGE$
|
|
35955
|
+
message: MESSAGE$34
|
|
35401
35956
|
});
|
|
35402
35957
|
}
|
|
35403
35958
|
} })
|
|
@@ -35417,7 +35972,7 @@ const getCallMethodName = (callee) => {
|
|
|
35417
35972
|
};
|
|
35418
35973
|
//#endregion
|
|
35419
35974
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
35420
|
-
const MESSAGE$
|
|
35975
|
+
const MESSAGE$33 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
35421
35976
|
const CONTEXT_MODULES = [
|
|
35422
35977
|
"react",
|
|
35423
35978
|
"use-context-selector",
|
|
@@ -35465,7 +36020,7 @@ const noCreateContextInRender = defineRule({
|
|
|
35465
36020
|
if (!componentOrHookName) return;
|
|
35466
36021
|
context.report({
|
|
35467
36022
|
node,
|
|
35468
|
-
message: `${MESSAGE$
|
|
36023
|
+
message: `${MESSAGE$33} (called inside "${componentOrHookName}")`
|
|
35469
36024
|
});
|
|
35470
36025
|
} })
|
|
35471
36026
|
});
|
|
@@ -36212,7 +36767,7 @@ const isProvenOneShotTestingLibraryComponent = (functionNode, filename, scopes)
|
|
|
36212
36767
|
};
|
|
36213
36768
|
//#endregion
|
|
36214
36769
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
36215
|
-
const MESSAGE$
|
|
36770
|
+
const MESSAGE$32 = "`createRef()` may escape or be observed beyond the render that created it, so a later render can replace the ref object and detach the observed one. Hoist a `useRef()` call to the component's unconditional top level instead.";
|
|
36216
36771
|
const isUseMemoCallbackArgument = (functionNode, scopes) => {
|
|
36217
36772
|
const parent = functionNode.parent;
|
|
36218
36773
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
@@ -36266,7 +36821,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
36266
36821
|
if (isCreateRefResultWriteOnly(node, context.filename, context.scopes)) return;
|
|
36267
36822
|
context.report({
|
|
36268
36823
|
node,
|
|
36269
|
-
message: MESSAGE$
|
|
36824
|
+
message: MESSAGE$32
|
|
36270
36825
|
});
|
|
36271
36826
|
} })
|
|
36272
36827
|
});
|
|
@@ -36406,7 +36961,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
36406
36961
|
});
|
|
36407
36962
|
//#endregion
|
|
36408
36963
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
36409
|
-
const MESSAGE$
|
|
36964
|
+
const MESSAGE$31 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
36410
36965
|
const noDanger = defineRule({
|
|
36411
36966
|
id: "no-danger",
|
|
36412
36967
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -36420,7 +36975,7 @@ const noDanger = defineRule({
|
|
|
36420
36975
|
if (!propAttribute) return;
|
|
36421
36976
|
context.report({
|
|
36422
36977
|
node: propAttribute.name,
|
|
36423
|
-
message: MESSAGE$
|
|
36978
|
+
message: MESSAGE$31
|
|
36424
36979
|
});
|
|
36425
36980
|
},
|
|
36426
36981
|
CallExpression(node) {
|
|
@@ -36432,7 +36987,7 @@ const noDanger = defineRule({
|
|
|
36432
36987
|
const propertyKey = property.key;
|
|
36433
36988
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
36434
36989
|
node: propertyKey,
|
|
36435
|
-
message: MESSAGE$
|
|
36990
|
+
message: MESSAGE$31
|
|
36436
36991
|
});
|
|
36437
36992
|
}
|
|
36438
36993
|
}
|
|
@@ -36455,7 +37010,7 @@ const isMeaningfulJsxChild = (child) => {
|
|
|
36455
37010
|
};
|
|
36456
37011
|
//#endregion
|
|
36457
37012
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
36458
|
-
const MESSAGE$
|
|
37013
|
+
const MESSAGE$30 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
36459
37014
|
const mergePropsShape = (target, source) => {
|
|
36460
37015
|
target.hasDangerously ||= source.hasDangerously;
|
|
36461
37016
|
target.hasChildren ||= source.hasChildren;
|
|
@@ -36530,7 +37085,7 @@ const noDangerWithChildren = defineRule({
|
|
|
36530
37085
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
36531
37086
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
36532
37087
|
node: opening,
|
|
36533
|
-
message: MESSAGE$
|
|
37088
|
+
message: MESSAGE$30
|
|
36534
37089
|
});
|
|
36535
37090
|
},
|
|
36536
37091
|
CallExpression(node) {
|
|
@@ -36543,7 +37098,7 @@ const noDangerWithChildren = defineRule({
|
|
|
36543
37098
|
const positionalChildren = node.arguments.slice(2);
|
|
36544
37099
|
if (positionalChildren.length > 1 || positionalChildren.some((argument) => !isNullishExpression(argument)) || propsShape.hasChildren) context.report({
|
|
36545
37100
|
node,
|
|
36546
|
-
message: MESSAGE$
|
|
37101
|
+
message: MESSAGE$30
|
|
36547
37102
|
});
|
|
36548
37103
|
}
|
|
36549
37104
|
})
|
|
@@ -37518,7 +38073,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
37518
38073
|
//#endregion
|
|
37519
38074
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
37520
38075
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
37521
|
-
const MESSAGE$
|
|
38076
|
+
const MESSAGE$29 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
37522
38077
|
const getEnclosingLifecycleFunction = (setStateCall) => {
|
|
37523
38078
|
let ancestor = setStateCall.parent;
|
|
37524
38079
|
while (ancestor) {
|
|
@@ -37645,7 +38200,7 @@ const noDidMountSetState = defineRule({
|
|
|
37645
38200
|
}
|
|
37646
38201
|
context.report({
|
|
37647
38202
|
node: node.callee,
|
|
37648
|
-
message: MESSAGE$
|
|
38203
|
+
message: MESSAGE$29
|
|
37649
38204
|
});
|
|
37650
38205
|
} };
|
|
37651
38206
|
}
|
|
@@ -37663,7 +38218,7 @@ const findEnclosingClass = (node) => {
|
|
|
37663
38218
|
//#endregion
|
|
37664
38219
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
37665
38220
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
37666
|
-
const MESSAGE$
|
|
38221
|
+
const MESSAGE$28 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
37667
38222
|
const DIFFERENCE_OPERATORS = new Set(["!=", "!=="]);
|
|
37668
38223
|
const EQUALITY_OPERATORS = new Set([
|
|
37669
38224
|
"==",
|
|
@@ -38057,7 +38612,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
38057
38612
|
if (isInsideDiffGuard(node, context.scopes)) return;
|
|
38058
38613
|
context.report({
|
|
38059
38614
|
node: node.callee,
|
|
38060
|
-
message: MESSAGE$
|
|
38615
|
+
message: MESSAGE$28
|
|
38061
38616
|
});
|
|
38062
38617
|
} };
|
|
38063
38618
|
}
|
|
@@ -38080,7 +38635,7 @@ const isStateMemberExpression = (node) => {
|
|
|
38080
38635
|
};
|
|
38081
38636
|
//#endregion
|
|
38082
38637
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
38083
|
-
const MESSAGE$
|
|
38638
|
+
const MESSAGE$27 = "Mutating `this.state` by hand never triggers a redraw on its own & a later setState can overwrite it, so use `this.setState` instead.";
|
|
38084
38639
|
const shouldIgnoreMutation = (node) => {
|
|
38085
38640
|
let isConstructor = false;
|
|
38086
38641
|
let isInsideCallExpression = false;
|
|
@@ -38102,7 +38657,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
38102
38657
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
38103
38658
|
context.report({
|
|
38104
38659
|
node: reportNode,
|
|
38105
|
-
message: MESSAGE$
|
|
38660
|
+
message: MESSAGE$27
|
|
38106
38661
|
});
|
|
38107
38662
|
};
|
|
38108
38663
|
const noDirectMutationState = defineRule({
|
|
@@ -38453,7 +39008,7 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
38453
39008
|
});
|
|
38454
39009
|
//#endregion
|
|
38455
39010
|
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
38456
|
-
const MESSAGE$
|
|
39011
|
+
const MESSAGE$26 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
38457
39012
|
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
38458
39013
|
const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
|
|
38459
39014
|
const noDocumentWrite = defineRule({
|
|
@@ -38470,7 +39025,7 @@ const noDocumentWrite = defineRule({
|
|
|
38470
39025
|
if (methodName === null || !WRITE_METHODS.has(methodName)) return;
|
|
38471
39026
|
context.report({
|
|
38472
39027
|
node,
|
|
38473
|
-
message: MESSAGE$
|
|
39028
|
+
message: MESSAGE$26
|
|
38474
39029
|
});
|
|
38475
39030
|
} })
|
|
38476
39031
|
});
|
|
@@ -41518,7 +42073,7 @@ const IMPORT_INITIALIZER_TYPES = new Set([
|
|
|
41518
42073
|
"ImportNamespaceSpecifier"
|
|
41519
42074
|
]);
|
|
41520
42075
|
const CANCELLATION_FLAG_NAME_PATTERN = /cancel|ignore/i;
|
|
41521
|
-
const PROMISE_CONTINUATION_METHOD_NAMES = new Set([
|
|
42076
|
+
const PROMISE_CONTINUATION_METHOD_NAMES$1 = new Set([
|
|
41522
42077
|
"then",
|
|
41523
42078
|
"catch",
|
|
41524
42079
|
"finally"
|
|
@@ -41731,18 +42286,10 @@ const isCompletionSinkAfterCancellationEarlyExit = (completionSink, cancellation
|
|
|
41731
42286
|
return false;
|
|
41732
42287
|
};
|
|
41733
42288
|
const isCompletionSinkGuardedByCancellationFlag = (completionSink, cancellationFlagKey, context) => isCompletionSinkInsideCancellationGuard(completionSink, cancellationFlagKey, context) || isCompletionSinkAfterCancellationEarlyExit(completionSink, cancellationFlagKey, context);
|
|
41734
|
-
const isDescendantOf = (node, ancestor) => {
|
|
41735
|
-
let currentNode = node;
|
|
41736
|
-
while (currentNode) {
|
|
41737
|
-
if (currentNode === ancestor) return true;
|
|
41738
|
-
currentNode = currentNode.parent ?? null;
|
|
41739
|
-
}
|
|
41740
|
-
return false;
|
|
41741
|
-
};
|
|
41742
42289
|
const isPromiseContinuationForRequest = (functionNode, request) => {
|
|
41743
42290
|
const callNode = functionNode.parent;
|
|
41744
|
-
if (!isNodeOfType(callNode, "CallExpression") || !callNode.arguments?.some((callArgument) => callArgument === functionNode) || !isNodeOfType(callNode.callee, "MemberExpression") || callNode.callee.computed || !isNodeOfType(callNode.callee.property, "Identifier") || !PROMISE_CONTINUATION_METHOD_NAMES.has(callNode.callee.property.name)) return false;
|
|
41745
|
-
return
|
|
42291
|
+
if (!isNodeOfType(callNode, "CallExpression") || !callNode.arguments?.some((callArgument) => callArgument === functionNode) || !isNodeOfType(callNode.callee, "MemberExpression") || callNode.callee.computed || !isNodeOfType(callNode.callee.property, "Identifier") || !PROMISE_CONTINUATION_METHOD_NAMES$1.has(callNode.callee.property.name)) return false;
|
|
42292
|
+
return isAstDescendant(request, callNode.callee.object);
|
|
41746
42293
|
};
|
|
41747
42294
|
const isAwaitedInFunction = (request, functionNode) => {
|
|
41748
42295
|
let currentNode = request.parent;
|
|
@@ -41810,7 +42357,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
41810
42357
|
"ReactDOM",
|
|
41811
42358
|
"ReactDom"
|
|
41812
42359
|
]);
|
|
41813
|
-
const MESSAGE$
|
|
42360
|
+
const MESSAGE$25 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
41814
42361
|
const noFindDomNode = defineRule({
|
|
41815
42362
|
id: "no-find-dom-node",
|
|
41816
42363
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -41821,7 +42368,7 @@ const noFindDomNode = defineRule({
|
|
|
41821
42368
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
41822
42369
|
if (isImportedFromModule(node, callee.name, "react-dom")) context.report({
|
|
41823
42370
|
node: callee,
|
|
41824
|
-
message: MESSAGE$
|
|
42371
|
+
message: MESSAGE$25
|
|
41825
42372
|
});
|
|
41826
42373
|
return;
|
|
41827
42374
|
}
|
|
@@ -41832,7 +42379,7 @@ const noFindDomNode = defineRule({
|
|
|
41832
42379
|
if (callee.property.name !== "findDOMNode") return;
|
|
41833
42380
|
context.report({
|
|
41834
42381
|
node: callee.property,
|
|
41835
|
-
message: MESSAGE$
|
|
42382
|
+
message: MESSAGE$25
|
|
41836
42383
|
});
|
|
41837
42384
|
}
|
|
41838
42385
|
} })
|
|
@@ -42970,7 +43517,7 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
|
42970
43517
|
});
|
|
42971
43518
|
//#endregion
|
|
42972
43519
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
42973
|
-
const MESSAGE$
|
|
43520
|
+
const MESSAGE$24 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
42974
43521
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
42975
43522
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
42976
43523
|
title: "Lazy image with high fetchPriority",
|
|
@@ -42984,7 +43531,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
42984
43531
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
42985
43532
|
context.report({
|
|
42986
43533
|
node: node.name,
|
|
42987
|
-
message: MESSAGE$
|
|
43534
|
+
message: MESSAGE$24
|
|
42988
43535
|
});
|
|
42989
43536
|
} })
|
|
42990
43537
|
});
|
|
@@ -43173,7 +43720,7 @@ const noImpureStateUpdater = defineRule({
|
|
|
43173
43720
|
});
|
|
43174
43721
|
//#endregion
|
|
43175
43722
|
//#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
|
|
43176
|
-
const MESSAGE$
|
|
43723
|
+
const MESSAGE$23 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
|
|
43177
43724
|
const REACT_USE_REF_OPTIONS = {
|
|
43178
43725
|
allowGlobalReactNamespace: false,
|
|
43179
43726
|
allowUnboundBareCalls: false
|
|
@@ -43274,7 +43821,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
43274
43821
|
if (!inputTypeValues || !inputTypeValues.every((inputTypeValue) => inputTypeValue.toLowerCase() === "checkbox")) return;
|
|
43275
43822
|
context.report({
|
|
43276
43823
|
node: indeterminateAttribute,
|
|
43277
|
-
message: MESSAGE$
|
|
43824
|
+
message: MESSAGE$23
|
|
43278
43825
|
});
|
|
43279
43826
|
},
|
|
43280
43827
|
CallExpression(node) {
|
|
@@ -43283,7 +43830,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
43283
43830
|
if (!isProvenHtmlInputElement(receiver, context.scopes)) return;
|
|
43284
43831
|
context.report({
|
|
43285
43832
|
node,
|
|
43286
|
-
message: MESSAGE$
|
|
43833
|
+
message: MESSAGE$23
|
|
43287
43834
|
});
|
|
43288
43835
|
}
|
|
43289
43836
|
};
|
|
@@ -43571,7 +44118,7 @@ const noIsMounted = defineRule({
|
|
|
43571
44118
|
});
|
|
43572
44119
|
//#endregion
|
|
43573
44120
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
43574
|
-
const MESSAGE$
|
|
44121
|
+
const MESSAGE$22 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
43575
44122
|
const isJsonMethodCall = (node, method) => {
|
|
43576
44123
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
43577
44124
|
const callee = node.callee;
|
|
@@ -43635,13 +44182,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
43635
44182
|
if (isCatchParameterRoundTrip(firstArgument)) return;
|
|
43636
44183
|
context.report({
|
|
43637
44184
|
node,
|
|
43638
|
-
message: MESSAGE$
|
|
44185
|
+
message: MESSAGE$22
|
|
43639
44186
|
});
|
|
43640
44187
|
} })
|
|
43641
44188
|
});
|
|
43642
44189
|
//#endregion
|
|
43643
44190
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
43644
|
-
const MESSAGE$
|
|
44191
|
+
const MESSAGE$21 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
43645
44192
|
const isJsxElementTypeReference = (node) => {
|
|
43646
44193
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
43647
44194
|
const typeName = node.typeName;
|
|
@@ -43693,7 +44240,7 @@ const noJsxElementType = defineRule({
|
|
|
43693
44240
|
if (isJsxImported) return;
|
|
43694
44241
|
for (const typeAnnotation of flaggedAnnotations) context.report({
|
|
43695
44242
|
node: typeAnnotation,
|
|
43696
|
-
message: MESSAGE$
|
|
44243
|
+
message: MESSAGE$21
|
|
43697
44244
|
});
|
|
43698
44245
|
}
|
|
43699
44246
|
};
|
|
@@ -44974,7 +45521,7 @@ const noMoment = defineRule({
|
|
|
44974
45521
|
});
|
|
44975
45522
|
//#endregion
|
|
44976
45523
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
44977
|
-
const MESSAGE$
|
|
45524
|
+
const MESSAGE$20 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
44978
45525
|
const resolveSettings$16 = (settings) => {
|
|
44979
45526
|
const reactDoctor = settings?.["react-doctor"];
|
|
44980
45527
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -45326,7 +45873,7 @@ const noMultiComp = defineRule({
|
|
|
45326
45873
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
45327
45874
|
for (const component of flagged.slice(1)) context.report({
|
|
45328
45875
|
node: component.reportNode,
|
|
45329
|
-
message: MESSAGE$
|
|
45876
|
+
message: MESSAGE$20
|
|
45330
45877
|
});
|
|
45331
45878
|
} };
|
|
45332
45879
|
}
|
|
@@ -45414,41 +45961,6 @@ const noMutableInDeps = defineRule({
|
|
|
45414
45961
|
}
|
|
45415
45962
|
});
|
|
45416
45963
|
//#endregion
|
|
45417
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
45418
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
45419
|
-
let node = callExpression;
|
|
45420
|
-
let parent = node.parent;
|
|
45421
|
-
while (parent) {
|
|
45422
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
45423
|
-
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
45424
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
45425
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
45426
|
-
node = parent;
|
|
45427
|
-
parent = node.parent;
|
|
45428
|
-
continue;
|
|
45429
|
-
}
|
|
45430
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
45431
|
-
node = parent;
|
|
45432
|
-
parent = node.parent;
|
|
45433
|
-
continue;
|
|
45434
|
-
}
|
|
45435
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
45436
|
-
node = parent;
|
|
45437
|
-
parent = node.parent;
|
|
45438
|
-
continue;
|
|
45439
|
-
}
|
|
45440
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
45441
|
-
const expressions = parent.expressions ?? [];
|
|
45442
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
45443
|
-
node = parent;
|
|
45444
|
-
parent = node.parent;
|
|
45445
|
-
continue;
|
|
45446
|
-
}
|
|
45447
|
-
return false;
|
|
45448
|
-
}
|
|
45449
|
-
return false;
|
|
45450
|
-
};
|
|
45451
|
-
//#endregion
|
|
45452
45964
|
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
45453
45965
|
const LODASH_MUTATOR_NAMES = new Set([
|
|
45454
45966
|
"set",
|
|
@@ -45539,7 +46051,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
45539
46051
|
};
|
|
45540
46052
|
//#endregion
|
|
45541
46053
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
45542
|
-
const MESSAGE$
|
|
46054
|
+
const MESSAGE$19 = "This reducer changes state in place, so your update is silently skipped.";
|
|
45543
46055
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
45544
46056
|
"copyWithin",
|
|
45545
46057
|
"fill",
|
|
@@ -45743,7 +46255,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
45743
46255
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
45744
46256
|
context.report({
|
|
45745
46257
|
node: options.crossFileConsumerCallSite,
|
|
45746
|
-
message: `${MESSAGE$
|
|
46258
|
+
message: `${MESSAGE$19} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
45747
46259
|
});
|
|
45748
46260
|
return;
|
|
45749
46261
|
}
|
|
@@ -45752,7 +46264,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
45752
46264
|
reportedNodes.add(mutation.node);
|
|
45753
46265
|
context.report({
|
|
45754
46266
|
node: mutation.node,
|
|
45755
|
-
message: MESSAGE$
|
|
46267
|
+
message: MESSAGE$19
|
|
45756
46268
|
});
|
|
45757
46269
|
}
|
|
45758
46270
|
};
|
|
@@ -46159,7 +46671,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
46159
46671
|
});
|
|
46160
46672
|
//#endregion
|
|
46161
46673
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
46162
|
-
const MESSAGE$
|
|
46674
|
+
const MESSAGE$18 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
46163
46675
|
const KEYBOARD_HANDLER_PROP_NAMES = [
|
|
46164
46676
|
"onKeyDown",
|
|
46165
46677
|
"onKeyUp",
|
|
@@ -46278,7 +46790,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
46278
46790
|
if (numeric === null) {
|
|
46279
46791
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues && !isKeyboardOperable(node) && !isFocusOperable(node) && !hasJsxSpreadAttribute$1(node.attributes)) context.report({
|
|
46280
46792
|
node: tabIndex,
|
|
46281
|
-
message: MESSAGE$
|
|
46793
|
+
message: MESSAGE$18
|
|
46282
46794
|
});
|
|
46283
46795
|
return;
|
|
46284
46796
|
}
|
|
@@ -46300,7 +46812,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
46300
46812
|
if (!roleAttribute) {
|
|
46301
46813
|
context.report({
|
|
46302
46814
|
node: tabIndex,
|
|
46303
|
-
message: MESSAGE$
|
|
46815
|
+
message: MESSAGE$18
|
|
46304
46816
|
});
|
|
46305
46817
|
return;
|
|
46306
46818
|
}
|
|
@@ -46314,7 +46826,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
46314
46826
|
}
|
|
46315
46827
|
context.report({
|
|
46316
46828
|
node: tabIndex,
|
|
46317
|
-
message: MESSAGE$
|
|
46829
|
+
message: MESSAGE$18
|
|
46318
46830
|
});
|
|
46319
46831
|
} };
|
|
46320
46832
|
}
|
|
@@ -48520,7 +49032,7 @@ const noRandomKey = defineRule({
|
|
|
48520
49032
|
});
|
|
48521
49033
|
//#endregion
|
|
48522
49034
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
48523
|
-
const MESSAGE$
|
|
49035
|
+
const MESSAGE$17 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
48524
49036
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
48525
49037
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
48526
49038
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -48546,13 +49058,13 @@ const noReactChildren = defineRule({
|
|
|
48546
49058
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
48547
49059
|
context.report({
|
|
48548
49060
|
node: calleeOuter,
|
|
48549
|
-
message: MESSAGE$
|
|
49061
|
+
message: MESSAGE$17
|
|
48550
49062
|
});
|
|
48551
49063
|
return;
|
|
48552
49064
|
}
|
|
48553
49065
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
48554
49066
|
node: calleeOuter,
|
|
48555
|
-
message: MESSAGE$
|
|
49067
|
+
message: MESSAGE$17
|
|
48556
49068
|
});
|
|
48557
49069
|
} })
|
|
48558
49070
|
});
|
|
@@ -49434,7 +49946,7 @@ const noRenderPropChildren = defineRule({
|
|
|
49434
49946
|
});
|
|
49435
49947
|
//#endregion
|
|
49436
49948
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
49437
|
-
const MESSAGE$
|
|
49949
|
+
const MESSAGE$16 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
49438
49950
|
const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
|
|
49439
49951
|
const isUsedAsReturnValue = (parent) => {
|
|
49440
49952
|
if (!parent) return false;
|
|
@@ -49452,7 +49964,7 @@ const noRenderReturnValue = defineRule({
|
|
|
49452
49964
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
49453
49965
|
context.report({
|
|
49454
49966
|
node: node.callee,
|
|
49455
|
-
message: MESSAGE$
|
|
49967
|
+
message: MESSAGE$16
|
|
49456
49968
|
});
|
|
49457
49969
|
} })
|
|
49458
49970
|
});
|
|
@@ -50879,7 +51391,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
50879
51391
|
});
|
|
50880
51392
|
//#endregion
|
|
50881
51393
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
50882
|
-
const MESSAGE$
|
|
51394
|
+
const MESSAGE$15 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
50883
51395
|
const noSetState = defineRule({
|
|
50884
51396
|
id: "no-set-state",
|
|
50885
51397
|
title: "Local class state forbidden",
|
|
@@ -50894,7 +51406,7 @@ const noSetState = defineRule({
|
|
|
50894
51406
|
if (!getParentComponent(node)) return;
|
|
50895
51407
|
context.report({
|
|
50896
51408
|
node: node.callee,
|
|
50897
|
-
message: MESSAGE$
|
|
51409
|
+
message: MESSAGE$15
|
|
50898
51410
|
});
|
|
50899
51411
|
} })
|
|
50900
51412
|
});
|
|
@@ -51264,7 +51776,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
51264
51776
|
};
|
|
51265
51777
|
//#endregion
|
|
51266
51778
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
51267
|
-
const MESSAGE$
|
|
51779
|
+
const MESSAGE$14 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
|
|
51268
51780
|
const DEFAULT_HANDLERS = [
|
|
51269
51781
|
"onClick",
|
|
51270
51782
|
"onMouseDown",
|
|
@@ -51379,7 +51891,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51379
51891
|
if (!roleAttribute || !roleAttribute.value) {
|
|
51380
51892
|
context.report({
|
|
51381
51893
|
node: node.name,
|
|
51382
|
-
message: MESSAGE$
|
|
51894
|
+
message: MESSAGE$14
|
|
51383
51895
|
});
|
|
51384
51896
|
return;
|
|
51385
51897
|
}
|
|
@@ -51389,7 +51901,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51389
51901
|
if (isRecognizedRoleString(attributeValue.value)) return;
|
|
51390
51902
|
context.report({
|
|
51391
51903
|
node: node.name,
|
|
51392
|
-
message: MESSAGE$
|
|
51904
|
+
message: MESSAGE$14
|
|
51393
51905
|
});
|
|
51394
51906
|
return;
|
|
51395
51907
|
}
|
|
@@ -51397,7 +51909,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51397
51909
|
if (!settings.allowExpressionValues) {
|
|
51398
51910
|
context.report({
|
|
51399
51911
|
node: node.name,
|
|
51400
|
-
message: MESSAGE$
|
|
51912
|
+
message: MESSAGE$14
|
|
51401
51913
|
});
|
|
51402
51914
|
return;
|
|
51403
51915
|
}
|
|
@@ -51405,7 +51917,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51405
51917
|
if (isStaticNullishExpression(expression)) {
|
|
51406
51918
|
context.report({
|
|
51407
51919
|
node: node.name,
|
|
51408
|
-
message: MESSAGE$
|
|
51920
|
+
message: MESSAGE$14
|
|
51409
51921
|
});
|
|
51410
51922
|
return;
|
|
51411
51923
|
}
|
|
@@ -51415,7 +51927,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51415
51927
|
if (branches.some((branch) => branch.role !== null && isRecognizedRoleString(branch.role))) return;
|
|
51416
51928
|
context.report({
|
|
51417
51929
|
node: node.name,
|
|
51418
|
-
message: MESSAGE$
|
|
51930
|
+
message: MESSAGE$14
|
|
51419
51931
|
});
|
|
51420
51932
|
return;
|
|
51421
51933
|
}
|
|
@@ -51423,7 +51935,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51423
51935
|
}
|
|
51424
51936
|
context.report({
|
|
51425
51937
|
node: node.name,
|
|
51426
|
-
message: MESSAGE$
|
|
51938
|
+
message: MESSAGE$14
|
|
51427
51939
|
});
|
|
51428
51940
|
} };
|
|
51429
51941
|
}
|
|
@@ -51529,7 +52041,7 @@ const noStringRefs = defineRule({
|
|
|
51529
52041
|
});
|
|
51530
52042
|
//#endregion
|
|
51531
52043
|
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
51532
|
-
const MESSAGE$
|
|
52044
|
+
const MESSAGE$13 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
|
|
51533
52045
|
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
51534
52046
|
const PUBLIC_ASSET_PATH_PATTERN = /(?:^|\/)public\//i;
|
|
51535
52047
|
const noSyncXhr = defineRule({
|
|
@@ -51550,7 +52062,7 @@ const noSyncXhr = defineRule({
|
|
|
51550
52062
|
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
51551
52063
|
context.report({
|
|
51552
52064
|
node,
|
|
51553
|
-
message: MESSAGE$
|
|
52065
|
+
message: MESSAGE$13
|
|
51554
52066
|
});
|
|
51555
52067
|
};
|
|
51556
52068
|
return {
|
|
@@ -51575,7 +52087,7 @@ const noSyncXhr = defineRule({
|
|
|
51575
52087
|
});
|
|
51576
52088
|
//#endregion
|
|
51577
52089
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
51578
|
-
const MESSAGE$
|
|
52090
|
+
const MESSAGE$12 = "This value is `undefined` because function components have no `this`.";
|
|
51579
52091
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
51580
52092
|
let ancestor = node.parent;
|
|
51581
52093
|
while (ancestor) {
|
|
@@ -51663,7 +52175,7 @@ const noThisInSfc = defineRule({
|
|
|
51663
52175
|
if (functionHasOwnThisMemberWrite(enclosingFunction)) return;
|
|
51664
52176
|
context.report({
|
|
51665
52177
|
node,
|
|
51666
|
-
message: MESSAGE$
|
|
52178
|
+
message: MESSAGE$12
|
|
51667
52179
|
});
|
|
51668
52180
|
} };
|
|
51669
52181
|
}
|
|
@@ -53686,7 +54198,7 @@ const isElementTypeJsxAttribute = (node) => {
|
|
|
53686
54198
|
const attributeName = node.name.name;
|
|
53687
54199
|
return ELEMENT_TYPE_PROP_NAMES.has(attributeName.toLowerCase()) || attributeName.endsWith("Component");
|
|
53688
54200
|
};
|
|
53689
|
-
const isReactUseMemoCallback = (call, valueNode, scopes) => call.arguments[0] === valueNode && isReactApiCall(call, "useMemo", scopes, {
|
|
54201
|
+
const isReactUseMemoCallback$1 = (call, valueNode, scopes) => call.arguments[0] === valueNode && isReactApiCall(call, "useMemo", scopes, {
|
|
53690
54202
|
allowGlobalReactNamespace: true,
|
|
53691
54203
|
resolveNamedAliases: true
|
|
53692
54204
|
});
|
|
@@ -53710,7 +54222,7 @@ const isRenderFlowingReadReference = (identifier, scopes, visitedSymbols = /* @_
|
|
|
53710
54222
|
case "ArrowFunctionExpression": return false;
|
|
53711
54223
|
case "CallExpression":
|
|
53712
54224
|
if (parent.callee === valueNode) return false;
|
|
53713
|
-
if (isReactUseMemoCallback(parent, valueNode, scopes)) return false;
|
|
54225
|
+
if (isReactUseMemoCallback$1(parent, valueNode, scopes)) return false;
|
|
53714
54226
|
if (isReactCreateElementCall(parent, scopes)) return true;
|
|
53715
54227
|
valueNode = parent;
|
|
53716
54228
|
parent = parent.parent;
|
|
@@ -54033,7 +54545,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
54033
54545
|
//#endregion
|
|
54034
54546
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
54035
54547
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
54036
|
-
const MESSAGE$
|
|
54548
|
+
const MESSAGE$11 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
54037
54549
|
const resolveSettings$7 = (settings) => {
|
|
54038
54550
|
const reactDoctor = settings?.["react-doctor"];
|
|
54039
54551
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -54067,7 +54579,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
54067
54579
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
54068
54580
|
context.report({
|
|
54069
54581
|
node: node.callee,
|
|
54070
|
-
message: MESSAGE$
|
|
54582
|
+
message: MESSAGE$11
|
|
54071
54583
|
});
|
|
54072
54584
|
} };
|
|
54073
54585
|
}
|
|
@@ -55867,7 +56379,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
55867
56379
|
});
|
|
55868
56380
|
//#endregion
|
|
55869
56381
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
55870
|
-
const MESSAGE$
|
|
56382
|
+
const MESSAGE$10 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
|
|
55871
56383
|
const preactPreferOndblclick = defineRule({
|
|
55872
56384
|
id: "preact-prefer-ondblclick",
|
|
55873
56385
|
title: "onDoubleClick instead of onDblClick",
|
|
@@ -55882,7 +56394,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
55882
56394
|
if (!onDoubleClickAttribute) return;
|
|
55883
56395
|
context.report({
|
|
55884
56396
|
node: onDoubleClickAttribute,
|
|
55885
|
-
message: MESSAGE$
|
|
56397
|
+
message: MESSAGE$10
|
|
55886
56398
|
});
|
|
55887
56399
|
} })
|
|
55888
56400
|
});
|
|
@@ -56122,7 +56634,7 @@ const preferExplicitVariants = defineRule({
|
|
|
56122
56634
|
});
|
|
56123
56635
|
//#endregion
|
|
56124
56636
|
//#region src/plugin/rules/react-builtins/prefer-function-component.ts
|
|
56125
|
-
const MESSAGE$
|
|
56637
|
+
const MESSAGE$9 = "This class component keeps behavior in lifecycle methods, so state and effects are harder to follow than in a hook-based function component.";
|
|
56126
56638
|
const resolveSettings$4 = (settings) => {
|
|
56127
56639
|
const reactDoctor = settings?.["react-doctor"];
|
|
56128
56640
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.preferFunctionComponent ?? {} : {};
|
|
@@ -56161,7 +56673,7 @@ const preferFunctionComponent = defineRule({
|
|
|
56161
56673
|
const reportNode = node.id ?? node;
|
|
56162
56674
|
context.report({
|
|
56163
56675
|
node: reportNode,
|
|
56164
|
-
message: MESSAGE$
|
|
56676
|
+
message: MESSAGE$9
|
|
56165
56677
|
});
|
|
56166
56678
|
};
|
|
56167
56679
|
return {
|
|
@@ -57482,7 +57994,8 @@ const preferUseReducer = defineRule({
|
|
|
57482
57994
|
//#endregion
|
|
57483
57995
|
//#region src/plugin/rules/security-scan/utils/is-public-debug-artifact-path.ts
|
|
57484
57996
|
const LOCALE_DIRECTORY_PATTERN = /(?:^|\/)(?:locales?|i18n|lang|langs|translations?)\//i;
|
|
57485
|
-
const
|
|
57997
|
+
const PUBLIC_DEBUG_ARTIFACT_PATH_PATTERN = /(?:^|\/)(?:\.env(?:\.[^/]*)?|(?:debug|crash|trace|stack[-_]?trace|report|dump|phpinfo)(?:[-_.][^/]*)?\.(?:txt|log|json|html?)|[^/]+\.log)$/i;
|
|
57998
|
+
const isPublicDebugArtifactPath = (relativePath) => isBrowserArtifactPath(relativePath, GENERATED_BUNDLE_FILE_PATTERN.test(relativePath)) && !LOCALE_DIRECTORY_PATTERN.test(relativePath) && PUBLIC_DEBUG_ARTIFACT_PATH_PATTERN.test(relativePath);
|
|
57486
57999
|
//#endregion
|
|
57487
58000
|
//#region src/plugin/rules/security-scan/public-debug-artifact.ts
|
|
57488
58001
|
const publicDebugArtifact = defineRule({
|
|
@@ -58361,7 +58874,7 @@ const reactCompilerNoManualMemoization = defineRule({
|
|
|
58361
58874
|
});
|
|
58362
58875
|
//#endregion
|
|
58363
58876
|
//#region src/plugin/rules/react-builtins/react-in-jsx-scope.ts
|
|
58364
|
-
const MESSAGE$
|
|
58877
|
+
const MESSAGE$8 = "This JSX crashes because `React` isn't in scope.";
|
|
58365
58878
|
const reactInJsxScope = defineRule({
|
|
58366
58879
|
id: "react-in-jsx-scope",
|
|
58367
58880
|
title: "React not in scope for JSX",
|
|
@@ -58373,21 +58886,21 @@ const reactInJsxScope = defineRule({
|
|
|
58373
58886
|
if (findVariableInitializer(node, "React")) return;
|
|
58374
58887
|
context.report({
|
|
58375
58888
|
node: node.name,
|
|
58376
|
-
message: MESSAGE$
|
|
58889
|
+
message: MESSAGE$8
|
|
58377
58890
|
});
|
|
58378
58891
|
},
|
|
58379
58892
|
JSXFragment(node) {
|
|
58380
58893
|
if (findVariableInitializer(node, "React")) return;
|
|
58381
58894
|
context.report({
|
|
58382
58895
|
node: node.openingFragment,
|
|
58383
|
-
message: MESSAGE$
|
|
58896
|
+
message: MESSAGE$8
|
|
58384
58897
|
});
|
|
58385
58898
|
}
|
|
58386
58899
|
})
|
|
58387
58900
|
});
|
|
58388
58901
|
//#endregion
|
|
58389
58902
|
//#region src/plugin/rules/security/react-markdown-unsanitized-raw-html.ts
|
|
58390
|
-
const MESSAGE$
|
|
58903
|
+
const MESSAGE$7 = "React Markdown parses dynamic raw HTML when `rehype-raw` is enabled. Add `rehype-sanitize` to `rehypePlugins` or sanitize the markdown before rendering it.";
|
|
58391
58904
|
const REACT_MARKDOWN_MODULE = "react-markdown";
|
|
58392
58905
|
const REHYPE_RAW_MODULE = "rehype-raw";
|
|
58393
58906
|
const REHYPE_SANITIZE_MODULE = "rehype-sanitize";
|
|
@@ -58539,7 +59052,7 @@ const reactMarkdownUnsanitizedRawHtml = defineRule({
|
|
|
58539
59052
|
if (pluginEntries.some((entry) => isPluginFromModule(entry, REHYPE_SANITIZE_MODULE, context.scopes, /* @__PURE__ */ new Set())) || !hasDynamicUnsanitizedChildren(node, context.scopes)) return;
|
|
58540
59053
|
context.report({
|
|
58541
59054
|
node,
|
|
58542
|
-
message: MESSAGE$
|
|
59055
|
+
message: MESSAGE$7
|
|
58543
59056
|
});
|
|
58544
59057
|
} }))
|
|
58545
59058
|
});
|
|
@@ -58595,7 +59108,7 @@ const inlineUseSelectorFunction = (callNode, aliases) => {
|
|
|
58595
59108
|
};
|
|
58596
59109
|
//#endregion
|
|
58597
59110
|
//#region src/plugin/rules/state-and-effects/redux-useselector-inline-derivation.ts
|
|
58598
|
-
const ALLOCATING_ARRAY_METHODS = new Set([
|
|
59111
|
+
const ALLOCATING_ARRAY_METHODS$1 = new Set([
|
|
58599
59112
|
"filter",
|
|
58600
59113
|
"map",
|
|
58601
59114
|
"flatMap",
|
|
@@ -58631,7 +59144,7 @@ const getAllocatingCallSiteDescription = (expression) => {
|
|
|
58631
59144
|
method: methodName
|
|
58632
59145
|
};
|
|
58633
59146
|
}
|
|
58634
|
-
if (ALLOCATING_ARRAY_METHODS.has(methodName)) return {
|
|
59147
|
+
if (ALLOCATING_ARRAY_METHODS$1.has(methodName)) return {
|
|
58635
59148
|
kind: "method",
|
|
58636
59149
|
method: methodName
|
|
58637
59150
|
};
|
|
@@ -58694,7 +59207,7 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
58694
59207
|
});
|
|
58695
59208
|
//#endregion
|
|
58696
59209
|
//#region src/plugin/rules/state-and-effects/redux-useselector-returns-new-collection.ts
|
|
58697
|
-
const MESSAGE$
|
|
59210
|
+
const MESSAGE$6 = "Your component re-renders on every dispatched action when useSelector returns a new object or array.";
|
|
58698
59211
|
const isConciseBodyReturningCollection = (functionNode) => {
|
|
58699
59212
|
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
58700
59213
|
const rawBody = functionNode.body;
|
|
@@ -58729,13 +59242,567 @@ const reduxUseselectorReturnsNewCollection = defineRule({
|
|
|
58729
59242
|
if (!isConciseBodyReturningCollection(selectorArgument)) return;
|
|
58730
59243
|
context.report({
|
|
58731
59244
|
node,
|
|
58732
|
-
message: MESSAGE$
|
|
59245
|
+
message: MESSAGE$6
|
|
58733
59246
|
});
|
|
58734
59247
|
}
|
|
58735
59248
|
};
|
|
58736
59249
|
}
|
|
58737
59250
|
});
|
|
58738
59251
|
//#endregion
|
|
59252
|
+
//#region src/plugin/utils/is-remotion-module-source.ts
|
|
59253
|
+
const isRemotionModuleSource = (moduleSource) => moduleSource === "remotion" || moduleSource.startsWith("@remotion/");
|
|
59254
|
+
//#endregion
|
|
59255
|
+
//#region src/plugin/utils/resolve-remotion-api.ts
|
|
59256
|
+
const resolveRemotionApi = (referenceNode, scopes) => {
|
|
59257
|
+
const candidate = stripParenExpression(referenceNode);
|
|
59258
|
+
if (isNodeOfType(candidate, "Identifier") || isNodeOfType(candidate, "JSXIdentifier")) {
|
|
59259
|
+
const symbol = scopes.symbolFor(candidate);
|
|
59260
|
+
if (symbol?.kind !== "import") return null;
|
|
59261
|
+
const importBinding = getImportBindingForName(candidate, symbol.name);
|
|
59262
|
+
if (!importBinding || importBinding.isNamespace || importBinding.exportedName === null || !isRemotionModuleSource(importBinding.source)) return null;
|
|
59263
|
+
return {
|
|
59264
|
+
apiName: importBinding.exportedName,
|
|
59265
|
+
moduleSource: importBinding.source
|
|
59266
|
+
};
|
|
59267
|
+
}
|
|
59268
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
59269
|
+
const apiName = getStaticPropertyKeyName(candidate, { allowComputedString: true });
|
|
59270
|
+
const namespaceObject = stripParenExpression(candidate.object);
|
|
59271
|
+
if (!apiName || !isNodeOfType(namespaceObject, "Identifier")) return null;
|
|
59272
|
+
const symbol = scopes.symbolFor(namespaceObject);
|
|
59273
|
+
if (symbol?.kind !== "import") return null;
|
|
59274
|
+
const importBinding = getImportBindingForName(namespaceObject, symbol.name);
|
|
59275
|
+
if (!importBinding?.isNamespace || !isRemotionModuleSource(importBinding.source)) return null;
|
|
59276
|
+
return {
|
|
59277
|
+
apiName,
|
|
59278
|
+
moduleSource: importBinding.source
|
|
59279
|
+
};
|
|
59280
|
+
}
|
|
59281
|
+
if (!isNodeOfType(candidate, "JSXMemberExpression") || !isNodeOfType(candidate.object, "JSXIdentifier") || !isNodeOfType(candidate.property, "JSXIdentifier")) return null;
|
|
59282
|
+
const symbol = scopes.symbolFor(candidate.object);
|
|
59283
|
+
if (symbol?.kind !== "import") return null;
|
|
59284
|
+
const importBinding = getImportBindingForName(candidate.object, symbol.name);
|
|
59285
|
+
if (!importBinding?.isNamespace || !isRemotionModuleSource(importBinding.source)) return null;
|
|
59286
|
+
return {
|
|
59287
|
+
apiName: candidate.property.name,
|
|
59288
|
+
moduleSource: importBinding.source
|
|
59289
|
+
};
|
|
59290
|
+
};
|
|
59291
|
+
//#endregion
|
|
59292
|
+
//#region src/plugin/utils/create-remotion-composition-ownership-analyzer.ts
|
|
59293
|
+
const compositionAttributeFunctionCacheBySettings = /* @__PURE__ */ new WeakMap();
|
|
59294
|
+
const getFunctionExportKeys = (filePath, programNode, functionNode) => getFunctionExportNames(programNode, functionNode).map((exportedName) => `${normalizeFilename(filePath)}\0${exportedName}`);
|
|
59295
|
+
const resolveImportedCompositionFunction = (expression, module) => {
|
|
59296
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
59297
|
+
let importReference;
|
|
59298
|
+
let exportedName;
|
|
59299
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
59300
|
+
const symbol = module.scopes.symbolFor(unwrappedExpression);
|
|
59301
|
+
if (symbol?.kind !== "import") return null;
|
|
59302
|
+
importReference = unwrappedExpression;
|
|
59303
|
+
exportedName = getImportBindingForName(unwrappedExpression, symbol.name)?.exportedName ?? null;
|
|
59304
|
+
} else if (isNodeOfType(unwrappedExpression, "MemberExpression") && isNodeOfType(stripParenExpression(unwrappedExpression.object), "Identifier")) {
|
|
59305
|
+
const namespaceObject = stripParenExpression(unwrappedExpression.object);
|
|
59306
|
+
if (!isNodeOfType(namespaceObject, "Identifier")) return null;
|
|
59307
|
+
const symbol = module.scopes.symbolFor(namespaceObject);
|
|
59308
|
+
if (symbol?.kind !== "import") return null;
|
|
59309
|
+
if (!getImportBindingForName(namespaceObject, symbol.name)?.isNamespace) return null;
|
|
59310
|
+
importReference = namespaceObject;
|
|
59311
|
+
exportedName = getStaticPropertyKeyName(unwrappedExpression, { allowComputedString: true });
|
|
59312
|
+
} else return null;
|
|
59313
|
+
if (!exportedName) return null;
|
|
59314
|
+
const importBinding = getImportBindingForName(importReference, importReference.name);
|
|
59315
|
+
if (!importBinding) return null;
|
|
59316
|
+
return resolveCrossFileFunctionExportWithFilePath(module.filePath, importBinding.source, exportedName);
|
|
59317
|
+
};
|
|
59318
|
+
const collectCompositionAttributeFunctionKeys = (context, currentProgram, attributeName) => {
|
|
59319
|
+
const filename = context.filename ? normalizeFilename(context.filename) : "";
|
|
59320
|
+
const rootDirectorySetting = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
59321
|
+
const rootDirectory = rootDirectorySetting ? normalizeFilename(rootDirectorySetting).replace(/\/$/, "") : "";
|
|
59322
|
+
if (!filename || !rootDirectory || filename !== rootDirectory && !filename.startsWith(`${rootDirectory}/`)) return null;
|
|
59323
|
+
const projectIndex = buildSourceProjectIndex(rootDirectory, filename, currentProgram, context.scopes);
|
|
59324
|
+
if (!projectIndex) return null;
|
|
59325
|
+
const functionKeys = /* @__PURE__ */ new Set();
|
|
59326
|
+
for (const module of projectIndex.modulesByFilePath.values()) walkAst(module.programNode, (candidate) => {
|
|
59327
|
+
if (!isNodeOfType(candidate, "JSXOpeningElement")) return;
|
|
59328
|
+
const apiBinding = resolveRemotionApi(candidate.name, module.scopes);
|
|
59329
|
+
if (apiBinding?.apiName !== "Composition" || apiBinding.moduleSource !== "remotion") return;
|
|
59330
|
+
const functionAttribute = findJsxAttribute(candidate.attributes, attributeName);
|
|
59331
|
+
if (!functionAttribute?.value || !isNodeOfType(functionAttribute.value, "JSXExpressionContainer") || !functionAttribute.value.expression) return;
|
|
59332
|
+
const resolvedFunction = resolveImportedCompositionFunction(functionAttribute.value.expression, module);
|
|
59333
|
+
if (!resolvedFunction || !isNodeOfType(resolvedFunction.programNode, "Program")) return;
|
|
59334
|
+
for (const functionKey of getFunctionExportKeys(resolvedFunction.filePath, resolvedFunction.programNode, resolvedFunction.functionNode)) functionKeys.add(functionKey);
|
|
59335
|
+
});
|
|
59336
|
+
return functionKeys;
|
|
59337
|
+
};
|
|
59338
|
+
const createRemotionCompositionAttributeOwnershipAnalyzer = (context, attributeName) => {
|
|
59339
|
+
const settings = context.settings;
|
|
59340
|
+
return (functionNode) => {
|
|
59341
|
+
const currentProgram = findProgramRoot(functionNode);
|
|
59342
|
+
if (!currentProgram || !isNodeOfType(currentProgram, "Program") || !context.filename || !settings) return false;
|
|
59343
|
+
const currentFunctionKeys = getFunctionExportKeys(context.filename, currentProgram, functionNode);
|
|
59344
|
+
if (currentFunctionKeys.length === 0) return false;
|
|
59345
|
+
let cacheByAttributeName = compositionAttributeFunctionCacheBySettings.get(settings);
|
|
59346
|
+
if (!cacheByAttributeName) {
|
|
59347
|
+
cacheByAttributeName = /* @__PURE__ */ new Map();
|
|
59348
|
+
compositionAttributeFunctionCacheBySettings.set(settings, cacheByAttributeName);
|
|
59349
|
+
}
|
|
59350
|
+
let cache = cacheByAttributeName.get(attributeName);
|
|
59351
|
+
if (!cache) {
|
|
59352
|
+
cache = { failedFilenames: /* @__PURE__ */ new Set() };
|
|
59353
|
+
cacheByAttributeName.set(attributeName, cache);
|
|
59354
|
+
}
|
|
59355
|
+
let functionKeys = cache.functionKeys;
|
|
59356
|
+
if (!functionKeys) {
|
|
59357
|
+
const filename = normalizeFilename(context.filename);
|
|
59358
|
+
if (cache.failedFilenames.has(filename)) return false;
|
|
59359
|
+
const collectedFunctionKeys = collectCompositionAttributeFunctionKeys(context, currentProgram, attributeName);
|
|
59360
|
+
if (!collectedFunctionKeys) {
|
|
59361
|
+
cache.failedFilenames.add(filename);
|
|
59362
|
+
return false;
|
|
59363
|
+
}
|
|
59364
|
+
functionKeys = collectedFunctionKeys;
|
|
59365
|
+
cache.functionKeys = collectedFunctionKeys;
|
|
59366
|
+
}
|
|
59367
|
+
return currentFunctionKeys.some((functionKey) => functionKeys.has(functionKey));
|
|
59368
|
+
};
|
|
59369
|
+
};
|
|
59370
|
+
const createRemotionCompositionOwnershipAnalyzer = (context) => createRemotionCompositionAttributeOwnershipAnalyzer(context, "component");
|
|
59371
|
+
const createRemotionMetadataOwnershipAnalyzer = (context) => createRemotionCompositionAttributeOwnershipAnalyzer(context, "calculateMetadata");
|
|
59372
|
+
//#endregion
|
|
59373
|
+
//#region src/plugin/rules/correctness/remotion-calculate-metadata-fetch-signal.ts
|
|
59374
|
+
const getParameterIdentifier = (parameter) => {
|
|
59375
|
+
if (isNodeOfType(parameter, "Identifier")) return parameter;
|
|
59376
|
+
if (isNodeOfType(parameter, "AssignmentPattern") && isNodeOfType(parameter.left, "Identifier")) return parameter.left;
|
|
59377
|
+
return null;
|
|
59378
|
+
};
|
|
59379
|
+
const getAbortSignalBinding = (functionNode) => {
|
|
59380
|
+
if (!isFunctionLike$1(functionNode)) return {
|
|
59381
|
+
parameterIdentifier: null,
|
|
59382
|
+
signalIdentifier: null
|
|
59383
|
+
};
|
|
59384
|
+
const firstParameter = functionNode.params[0];
|
|
59385
|
+
const parameterIdentifier = getParameterIdentifier(firstParameter);
|
|
59386
|
+
if (parameterIdentifier) return {
|
|
59387
|
+
parameterIdentifier,
|
|
59388
|
+
signalIdentifier: null
|
|
59389
|
+
};
|
|
59390
|
+
if (!isNodeOfType(firstParameter, "ObjectPattern")) return {
|
|
59391
|
+
parameterIdentifier: null,
|
|
59392
|
+
signalIdentifier: null
|
|
59393
|
+
};
|
|
59394
|
+
for (const property of firstParameter.properties) {
|
|
59395
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
59396
|
+
if (getStaticPropertyKeyName(property) !== "abortSignal") continue;
|
|
59397
|
+
const signalIdentifier = getParameterIdentifier(property.value);
|
|
59398
|
+
if (signalIdentifier) return {
|
|
59399
|
+
parameterIdentifier: null,
|
|
59400
|
+
signalIdentifier
|
|
59401
|
+
};
|
|
59402
|
+
}
|
|
59403
|
+
return {
|
|
59404
|
+
parameterIdentifier: null,
|
|
59405
|
+
signalIdentifier: null
|
|
59406
|
+
};
|
|
59407
|
+
};
|
|
59408
|
+
const identifiersResolveToSameSymbol = (leftIdentifier, rightIdentifier, scopes) => {
|
|
59409
|
+
if (!isNodeOfType(leftIdentifier, "Identifier") || !isNodeOfType(rightIdentifier, "Identifier")) return false;
|
|
59410
|
+
const leftSymbol = scopes.symbolFor(leftIdentifier);
|
|
59411
|
+
return Boolean(leftSymbol && leftSymbol === scopes.symbolFor(rightIdentifier));
|
|
59412
|
+
};
|
|
59413
|
+
const isMetadataAbortSignal = (expression, binding, scopes) => {
|
|
59414
|
+
const candidate = stripParenExpression(expression);
|
|
59415
|
+
if (binding.signalIdentifier && identifiersResolveToSameSymbol(candidate, binding.signalIdentifier, scopes)) return true;
|
|
59416
|
+
return Boolean(binding.parameterIdentifier && isNodeOfType(candidate, "MemberExpression") && getStaticPropertyKeyName(candidate, { allowComputedString: true }) === "abortSignal" && identifiersResolveToSameSymbol(candidate.object, binding.parameterIdentifier, scopes));
|
|
59417
|
+
};
|
|
59418
|
+
const fetchUsesMetadataAbortSignal = (fetchCall, binding, scopes) => {
|
|
59419
|
+
const optionsArgument = fetchCall.arguments[1];
|
|
59420
|
+
if (!optionsArgument) return false;
|
|
59421
|
+
const options = stripParenExpression(optionsArgument);
|
|
59422
|
+
if (!isNodeOfType(options, "ObjectExpression")) return null;
|
|
59423
|
+
if (options.properties.some((property) => isNodeOfType(property, "SpreadElement"))) return null;
|
|
59424
|
+
for (let propertyIndex = options.properties.length - 1; propertyIndex >= 0; propertyIndex -= 1) {
|
|
59425
|
+
const property = options.properties[propertyIndex];
|
|
59426
|
+
if (isNodeOfType(property, "Property") && getStaticPropertyKeyName(property, { allowComputedString: true }) === "signal") return isMetadataAbortSignal(property.value, binding, scopes);
|
|
59427
|
+
}
|
|
59428
|
+
return false;
|
|
59429
|
+
};
|
|
59430
|
+
const reportFetchesWithoutAbortSignal = (functionNode, context) => {
|
|
59431
|
+
const binding = getAbortSignalBinding(functionNode);
|
|
59432
|
+
walkAst(functionNode, (candidate) => {
|
|
59433
|
+
if (candidate !== functionNode && isFunctionLike$1(candidate)) return false;
|
|
59434
|
+
if (!isNodeOfType(candidate, "CallExpression") || !isNodeOfType(candidate.callee, "Identifier") || candidate.callee.name !== "fetch" || !context.scopes.isGlobalReference(candidate.callee) || fetchUsesMetadataAbortSignal(candidate, binding, context.scopes) !== false) return;
|
|
59435
|
+
context.report({
|
|
59436
|
+
node: candidate,
|
|
59437
|
+
message: "Pass Remotion's `abortSignal` to this fetch with `{signal: abortSignal}` so superseded metadata requests are cancelled."
|
|
59438
|
+
});
|
|
59439
|
+
});
|
|
59440
|
+
};
|
|
59441
|
+
const isCalculateMetadataFunctionType = (variableDeclarator) => {
|
|
59442
|
+
if (!isNodeOfType(variableDeclarator.id, "Identifier")) return false;
|
|
59443
|
+
const annotation = variableDeclarator.id.typeAnnotation;
|
|
59444
|
+
if (!isNodeOfType(annotation, "TSTypeAnnotation") || !isNodeOfType(annotation.typeAnnotation, "TSTypeReference")) return false;
|
|
59445
|
+
const typeName = annotation.typeAnnotation.typeName;
|
|
59446
|
+
if (!isNodeOfType(typeName, "Identifier")) return false;
|
|
59447
|
+
const apiBinding = getImportBindingForName(typeName, typeName.name);
|
|
59448
|
+
return Boolean(apiBinding?.exportedName === "CalculateMetadataFunction" && apiBinding.source === "remotion");
|
|
59449
|
+
};
|
|
59450
|
+
const remotionCalculateMetadataFetchSignal = defineRule({
|
|
59451
|
+
id: "remotion-calculate-metadata-fetch-signal",
|
|
59452
|
+
title: "calculateMetadata fetch ignores abortSignal",
|
|
59453
|
+
tags: ["react-jsx-only"],
|
|
59454
|
+
requires: ["remotion:4"],
|
|
59455
|
+
severity: "error",
|
|
59456
|
+
recommendation: "Destructure `abortSignal` from the calculateMetadata argument and pass it to direct fetch calls as `{signal: abortSignal}`.",
|
|
59457
|
+
create: (context) => {
|
|
59458
|
+
const analyzedFunctions = /* @__PURE__ */ new WeakSet();
|
|
59459
|
+
const isOwnedByCalculateMetadata = createRemotionMetadataOwnershipAnalyzer(context);
|
|
59460
|
+
const analyzeFunction = (functionNode) => {
|
|
59461
|
+
if (!functionNode || analyzedFunctions.has(functionNode)) return;
|
|
59462
|
+
analyzedFunctions.add(functionNode);
|
|
59463
|
+
reportFetchesWithoutAbortSignal(functionNode, context);
|
|
59464
|
+
};
|
|
59465
|
+
return {
|
|
59466
|
+
CallExpression(node) {
|
|
59467
|
+
if (!isNodeOfType(node.callee, "Identifier") || node.callee.name !== "fetch" || !context.scopes.isGlobalReference(node.callee)) return;
|
|
59468
|
+
const functionNode = findEnclosingFunction$1(node);
|
|
59469
|
+
if (functionNode && isOwnedByCalculateMetadata(functionNode)) analyzeFunction(functionNode);
|
|
59470
|
+
},
|
|
59471
|
+
JSXOpeningElement(node) {
|
|
59472
|
+
const apiBinding = resolveRemotionApi(node.name, context.scopes);
|
|
59473
|
+
if (apiBinding?.apiName !== "Composition" || apiBinding.moduleSource !== "remotion") return;
|
|
59474
|
+
const calculateMetadataAttribute = findJsxAttribute(node.attributes, "calculateMetadata");
|
|
59475
|
+
if (!calculateMetadataAttribute?.value || !isNodeOfType(calculateMetadataAttribute.value, "JSXExpressionContainer") || !calculateMetadataAttribute.value.expression) return;
|
|
59476
|
+
analyzeFunction(resolveExactLocalFunction(calculateMetadataAttribute.value.expression, context.scopes));
|
|
59477
|
+
},
|
|
59478
|
+
VariableDeclarator(node) {
|
|
59479
|
+
if (!node.init || !isCalculateMetadataFunctionType(node)) return;
|
|
59480
|
+
analyzeFunction(resolveExactLocalFunction(node.init, context.scopes));
|
|
59481
|
+
}
|
|
59482
|
+
};
|
|
59483
|
+
}
|
|
59484
|
+
});
|
|
59485
|
+
//#endregion
|
|
59486
|
+
//#region src/plugin/utils/create-remotion-render-evidence-checker.ts
|
|
59487
|
+
const REMOTION_RENDER_CALL_NAMES = new Set([
|
|
59488
|
+
"continueRender",
|
|
59489
|
+
"delayRender",
|
|
59490
|
+
"getInputProps",
|
|
59491
|
+
"random",
|
|
59492
|
+
"spring",
|
|
59493
|
+
"useCurrentFrame",
|
|
59494
|
+
"useDelayRender",
|
|
59495
|
+
"useVideoConfig"
|
|
59496
|
+
]);
|
|
59497
|
+
const REMOTION_RENDER_COMPONENT_MODULE_BY_NAME = new Map([
|
|
59498
|
+
["Audio", "@remotion/media"],
|
|
59499
|
+
["Freeze", "remotion"],
|
|
59500
|
+
["IFrame", "remotion"],
|
|
59501
|
+
["Img", "remotion"],
|
|
59502
|
+
["Loop", "remotion"],
|
|
59503
|
+
["OffthreadVideo", "remotion"],
|
|
59504
|
+
["Sequence", "remotion"],
|
|
59505
|
+
["Series", "remotion"],
|
|
59506
|
+
["Video", "@remotion/media"]
|
|
59507
|
+
]);
|
|
59508
|
+
const createRemotionRenderEvidenceChecker = (context) => {
|
|
59509
|
+
const scopes = context.scopes;
|
|
59510
|
+
const evidenceByFunction = /* @__PURE__ */ new WeakMap();
|
|
59511
|
+
const registeredCompositionFunctions = /* @__PURE__ */ new WeakSet();
|
|
59512
|
+
const inspectedPrograms = /* @__PURE__ */ new WeakSet();
|
|
59513
|
+
const isOwnedByRegisteredComposition = createRemotionCompositionOwnershipAnalyzer(context);
|
|
59514
|
+
const collectRegisteredCompositionFunctions = (functionNode) => {
|
|
59515
|
+
const program = findProgramRoot(functionNode);
|
|
59516
|
+
if (!program || inspectedPrograms.has(program)) return;
|
|
59517
|
+
inspectedPrograms.add(program);
|
|
59518
|
+
walkAst(program, (candidate) => {
|
|
59519
|
+
if (!isNodeOfType(candidate, "JSXOpeningElement")) return;
|
|
59520
|
+
const apiBinding = resolveRemotionApi(candidate.name, scopes);
|
|
59521
|
+
if (apiBinding?.apiName !== "Composition" || apiBinding.moduleSource !== "remotion") return;
|
|
59522
|
+
const componentAttribute = findJsxAttribute(candidate.attributes, "component");
|
|
59523
|
+
if (!componentAttribute?.value || !isNodeOfType(componentAttribute.value, "JSXExpressionContainer") || !componentAttribute.value.expression) return;
|
|
59524
|
+
const registeredFunction = resolveExactLocalFunction(componentAttribute.value.expression, scopes);
|
|
59525
|
+
if (registeredFunction) registeredCompositionFunctions.add(registeredFunction);
|
|
59526
|
+
});
|
|
59527
|
+
};
|
|
59528
|
+
const functionUsesRemotionRenderApi = (functionNode) => {
|
|
59529
|
+
let hasEvidence = false;
|
|
59530
|
+
walkAst(functionNode, (candidate) => {
|
|
59531
|
+
if (hasEvidence) return false;
|
|
59532
|
+
if (!isNodeOfType(candidate, "CallExpression") && !isNodeOfType(candidate, "JSXOpeningElement")) return;
|
|
59533
|
+
const apiBinding = resolveRemotionApi(isNodeOfType(candidate, "CallExpression") ? candidate.callee : candidate.name, scopes);
|
|
59534
|
+
if (isNodeOfType(candidate, "CallExpression") && apiBinding?.moduleSource === "remotion" && REMOTION_RENDER_CALL_NAMES.has(apiBinding.apiName) || isNodeOfType(candidate, "JSXOpeningElement") && apiBinding !== null && REMOTION_RENDER_COMPONENT_MODULE_BY_NAME.get(apiBinding.apiName) === apiBinding.moduleSource) {
|
|
59535
|
+
hasEvidence = true;
|
|
59536
|
+
return false;
|
|
59537
|
+
}
|
|
59538
|
+
});
|
|
59539
|
+
return hasEvidence;
|
|
59540
|
+
};
|
|
59541
|
+
return { functionHasEvidence: (functionNode) => {
|
|
59542
|
+
const cachedEvidence = evidenceByFunction.get(functionNode);
|
|
59543
|
+
if (cachedEvidence !== void 0) return cachedEvidence;
|
|
59544
|
+
collectRegisteredCompositionFunctions(functionNode);
|
|
59545
|
+
const hasEvidence = registeredCompositionFunctions.has(functionNode) || functionUsesRemotionRenderApi(functionNode) || isOwnedByRegisteredComposition(functionNode);
|
|
59546
|
+
evidenceByFunction.set(functionNode, hasEvidence);
|
|
59547
|
+
return hasEvidence;
|
|
59548
|
+
} };
|
|
59549
|
+
};
|
|
59550
|
+
//#endregion
|
|
59551
|
+
//#region src/plugin/rules/correctness/remotion-deterministic-randomness.ts
|
|
59552
|
+
const isGlobalMathObject = (node, scopes) => {
|
|
59553
|
+
const candidate = stripParenExpression(node);
|
|
59554
|
+
if (isNodeOfType(candidate, "Identifier")) return candidate.name === "Math" && scopes.isGlobalReference(candidate);
|
|
59555
|
+
return Boolean(isNodeOfType(candidate, "MemberExpression") && getStaticPropertyKeyName(candidate, { allowComputedString: true }) === "Math" && isNodeOfType(candidate.object, "Identifier") && candidate.object.name === "globalThis" && scopes.isGlobalReference(candidate.object));
|
|
59556
|
+
};
|
|
59557
|
+
const remotionDeterministicRandomness = defineRule({
|
|
59558
|
+
id: "remotion-deterministic-randomness",
|
|
59559
|
+
title: "Randomness changes between rendered frames",
|
|
59560
|
+
tags: ["react-jsx-only"],
|
|
59561
|
+
requires: ["remotion:4"],
|
|
59562
|
+
severity: "error",
|
|
59563
|
+
recommendation: "Use Remotion's seeded `random(seed)` helper so the same frame produces the same value in every render tab.",
|
|
59564
|
+
create: (context) => {
|
|
59565
|
+
const renderEvidence = createRemotionRenderEvidenceChecker(context);
|
|
59566
|
+
return { CallExpression(node) {
|
|
59567
|
+
const callee = stripParenExpression(node.callee);
|
|
59568
|
+
if (!isNodeOfType(callee, "MemberExpression") || getStaticPropertyKeyName(callee, { allowComputedString: true }) !== "random" || !isGlobalMathObject(callee.object, context.scopes)) return;
|
|
59569
|
+
const componentOrHook = findRenderPhaseComponentOrHook(node, context.scopes) ?? findEnclosingFunction$1(node);
|
|
59570
|
+
if (!componentOrHook || !renderEvidence.functionHasEvidence(componentOrHook)) return;
|
|
59571
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHook);
|
|
59572
|
+
if (displayName && !isReactHookName(displayName) && !functionHasReactComponentEvidence(componentOrHook, context.scopes, context.cfg)) return;
|
|
59573
|
+
context.report({
|
|
59574
|
+
node,
|
|
59575
|
+
message: "`Math.random()` can return a different value in each parallel Remotion render tab, so the same frame is not deterministic. Use `random(seed)` from `remotion` instead."
|
|
59576
|
+
});
|
|
59577
|
+
} };
|
|
59578
|
+
}
|
|
59579
|
+
});
|
|
59580
|
+
//#endregion
|
|
59581
|
+
//#region src/plugin/utils/create-remotion-css-time-rule-visitors.ts
|
|
59582
|
+
const createRemotionCssTimeRuleVisitors = (context, options) => {
|
|
59583
|
+
const renderEvidence = createRemotionRenderEvidenceChecker(context);
|
|
59584
|
+
return {
|
|
59585
|
+
Property(node) {
|
|
59586
|
+
if (!options.stylePropertyNames.has(getStaticPropertyKeyName(node) ?? "")) return;
|
|
59587
|
+
const styleObject = node.parent;
|
|
59588
|
+
const expressionContainer = styleObject?.parent;
|
|
59589
|
+
const styleAttribute = expressionContainer?.parent;
|
|
59590
|
+
if (!isNodeOfType(styleObject, "ObjectExpression") || !isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== styleObject || !isNodeOfType(styleAttribute, "JSXAttribute") || !isNodeOfType(styleAttribute.name, "JSXIdentifier") || styleAttribute.name.name !== "style") return;
|
|
59591
|
+
const renderFunction = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
59592
|
+
if (!renderFunction || !renderEvidence.functionHasEvidence(renderFunction)) return;
|
|
59593
|
+
context.report({
|
|
59594
|
+
node,
|
|
59595
|
+
message: options.styleMessage
|
|
59596
|
+
});
|
|
59597
|
+
},
|
|
59598
|
+
JSXAttribute(node) {
|
|
59599
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "className") return;
|
|
59600
|
+
const renderFunction = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
59601
|
+
if (!renderFunction || !renderEvidence.functionHasEvidence(renderFunction)) return;
|
|
59602
|
+
const className = getJsxAttributeStaticString(node);
|
|
59603
|
+
if (!className) return;
|
|
59604
|
+
if (className.split(/\s+/).filter(Boolean).some((classToken) => options.classTokenIsForbidden(classToken.split(":").at(-1) ?? ""))) context.report({
|
|
59605
|
+
node,
|
|
59606
|
+
message: options.classMessage
|
|
59607
|
+
});
|
|
59608
|
+
}
|
|
59609
|
+
};
|
|
59610
|
+
};
|
|
59611
|
+
//#endregion
|
|
59612
|
+
//#region src/plugin/rules/correctness/remotion-no-css-animation.ts
|
|
59613
|
+
const ANIMATION_STYLE_PROPERTY_NAMES = new Set(["animation", "animationName"]);
|
|
59614
|
+
const remotionNoCssAnimation = defineRule({
|
|
59615
|
+
id: "remotion-no-css-animation",
|
|
59616
|
+
title: "CSS animation is not frame-driven",
|
|
59617
|
+
tags: ["react-jsx-only"],
|
|
59618
|
+
requires: ["remotion:4"],
|
|
59619
|
+
severity: "error",
|
|
59620
|
+
recommendation: "Drive the property from `useCurrentFrame()` with `interpolate()` so every rendered frame is deterministic.",
|
|
59621
|
+
create: (context) => createRemotionCssTimeRuleVisitors(context, {
|
|
59622
|
+
classTokenIsForbidden: (classToken) => classToken.startsWith("animate-") && classToken !== "animate-none",
|
|
59623
|
+
classMessage: "Tailwind animations advance on browser time, so Remotion can capture inconsistent frames. Drive the property from `useCurrentFrame()` instead.",
|
|
59624
|
+
styleMessage: "CSS animations advance on browser time, so Remotion can capture inconsistent frames. Drive the property from `useCurrentFrame()` instead.",
|
|
59625
|
+
stylePropertyNames: ANIMATION_STYLE_PROPERTY_NAMES
|
|
59626
|
+
})
|
|
59627
|
+
});
|
|
59628
|
+
//#endregion
|
|
59629
|
+
//#region src/plugin/rules/correctness/remotion-no-css-transition.ts
|
|
59630
|
+
const TRANSITION_STYLE_PROPERTY_NAMES = new Set(["transition", "transitionProperty"]);
|
|
59631
|
+
const remotionNoCssTransition = defineRule({
|
|
59632
|
+
id: "remotion-no-css-transition",
|
|
59633
|
+
title: "CSS transition is not frame-driven",
|
|
59634
|
+
tags: ["react-jsx-only"],
|
|
59635
|
+
requires: ["remotion:4"],
|
|
59636
|
+
severity: "error",
|
|
59637
|
+
recommendation: "Drive the property from `useCurrentFrame()` with `interpolate()` so every rendered frame is deterministic.",
|
|
59638
|
+
create: (context) => createRemotionCssTimeRuleVisitors(context, {
|
|
59639
|
+
classTokenIsForbidden: (classToken) => (classToken === "transition" || classToken.startsWith("transition-")) && classToken !== "transition-none",
|
|
59640
|
+
classMessage: "Tailwind transitions advance on browser time, so Remotion can capture inconsistent frames. Drive the property from `useCurrentFrame()` instead.",
|
|
59641
|
+
styleMessage: "CSS transitions advance on browser time, so Remotion can capture inconsistent frames. Drive the property from `useCurrentFrame()` instead.",
|
|
59642
|
+
stylePropertyNames: TRANSITION_STYLE_PROPERTY_NAMES
|
|
59643
|
+
})
|
|
59644
|
+
});
|
|
59645
|
+
//#endregion
|
|
59646
|
+
//#region src/plugin/rules/correctness/remotion-no-css-url-assets.ts
|
|
59647
|
+
const CSS_URL_ASSET_PROPERTY_NAMES = new Set([
|
|
59648
|
+
"backgroundImage",
|
|
59649
|
+
"maskImage",
|
|
59650
|
+
"WebkitMaskImage"
|
|
59651
|
+
]);
|
|
59652
|
+
const CSS_URL_PATTERN = /\burl\(\s*(["']?)([^"')]+)\1\s*\)/i;
|
|
59653
|
+
const isEmbeddedAssetSource = (assetSource) => assetSource.startsWith("data:") || assetSource.startsWith("#");
|
|
59654
|
+
const getStaticStringExpression = (node) => {
|
|
59655
|
+
if (isNodeOfType(node, "Literal") && typeof node.value === "string") return node.value;
|
|
59656
|
+
if (isNodeOfType(node, "TemplateLiteral") && node.expressions.length === 0 && node.quasis.length === 1) return node.quasis[0].value.raw;
|
|
59657
|
+
return null;
|
|
59658
|
+
};
|
|
59659
|
+
const isReactUseMemoCallback = (node, scopes) => {
|
|
59660
|
+
const parent = node.parent;
|
|
59661
|
+
return Boolean(isNodeOfType(parent, "CallExpression") && parent.arguments[0] === node && isReactApiCall(parent, "useMemo", scopes, { resolveNamedAliases: true }));
|
|
59662
|
+
};
|
|
59663
|
+
const componentPreloadsStaticImage = (componentNode, assetSource, scopes) => {
|
|
59664
|
+
let hasPreload = false;
|
|
59665
|
+
walkAst(componentNode, (child) => {
|
|
59666
|
+
if (hasPreload) return false;
|
|
59667
|
+
if (child !== componentNode && isFunctionLike$1(child) && !isReactUseMemoCallback(child, scopes)) return false;
|
|
59668
|
+
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
59669
|
+
const apiBinding = resolveRemotionApi(child.name, scopes);
|
|
59670
|
+
if (apiBinding?.apiName !== "Img" || apiBinding.moduleSource !== "remotion") return;
|
|
59671
|
+
const sourceAttribute = findJsxAttribute(child.attributes, "src");
|
|
59672
|
+
if (sourceAttribute && getJsxAttributeStaticString(sourceAttribute) === assetSource) {
|
|
59673
|
+
hasPreload = true;
|
|
59674
|
+
return false;
|
|
59675
|
+
}
|
|
59676
|
+
});
|
|
59677
|
+
return hasPreload;
|
|
59678
|
+
};
|
|
59679
|
+
const remotionNoCssUrlAssets = defineRule({
|
|
59680
|
+
id: "remotion-no-css-url-assets",
|
|
59681
|
+
title: "CSS URL asset can flicker in Remotion",
|
|
59682
|
+
tags: ["react-jsx-only"],
|
|
59683
|
+
requires: ["remotion:4"],
|
|
59684
|
+
severity: "error",
|
|
59685
|
+
recommendation: "Render the asset with `Img` inside an `AbsoluteFill`, or preload the same source with a hidden `Img` when a CSS mask is required.",
|
|
59686
|
+
create: (context) => {
|
|
59687
|
+
const renderEvidence = createRemotionRenderEvidenceChecker(context);
|
|
59688
|
+
return { Property(node) {
|
|
59689
|
+
if (!CSS_URL_ASSET_PROPERTY_NAMES.has(getStaticPropertyKeyName(node) ?? "")) return;
|
|
59690
|
+
const styleObject = node.parent;
|
|
59691
|
+
const expressionContainer = styleObject?.parent;
|
|
59692
|
+
const styleAttribute = expressionContainer?.parent;
|
|
59693
|
+
if (!isNodeOfType(styleObject, "ObjectExpression") || !isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== styleObject || !isNodeOfType(styleAttribute, "JSXAttribute") || !isNodeOfType(styleAttribute.name, "JSXIdentifier") || styleAttribute.name.name !== "style") return;
|
|
59694
|
+
const cssValue = getStaticStringExpression(node.value);
|
|
59695
|
+
const urlMatch = cssValue ? CSS_URL_PATTERN.exec(cssValue) : null;
|
|
59696
|
+
if (!urlMatch) return;
|
|
59697
|
+
const assetSource = urlMatch[2].trim();
|
|
59698
|
+
if (isEmbeddedAssetSource(assetSource)) return;
|
|
59699
|
+
const componentNode = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
59700
|
+
if (!componentNode || !renderEvidence.functionHasEvidence(componentNode) || componentPreloadsStaticImage(componentNode, assetSource, context.scopes)) return;
|
|
59701
|
+
context.report({
|
|
59702
|
+
node,
|
|
59703
|
+
message: "Remotion cannot detect when a CSS `url()` asset has loaded, so the rendered frame can flicker. Render or preload the source with <Img> instead."
|
|
59704
|
+
});
|
|
59705
|
+
} };
|
|
59706
|
+
}
|
|
59707
|
+
});
|
|
59708
|
+
//#endregion
|
|
59709
|
+
//#region src/plugin/rules/correctness/remotion-no-module-scope-delay-render.ts
|
|
59710
|
+
const remotionNoModuleScopeDelayRender = defineRule({
|
|
59711
|
+
id: "remotion-no-module-scope-delay-render",
|
|
59712
|
+
title: "Module-scoped delayRender blocks every composition",
|
|
59713
|
+
requires: ["remotion:4"],
|
|
59714
|
+
severity: "error",
|
|
59715
|
+
recommendation: "Create the handle once inside the component. Use `useDelayRender()` on Remotion 4.0.342 or newer, or lazy `useState(() => delayRender())` on earlier versions.",
|
|
59716
|
+
create: (context) => ({ CallExpression(node) {
|
|
59717
|
+
const apiBinding = resolveRemotionApi(node.callee, context.scopes);
|
|
59718
|
+
if (apiBinding?.apiName !== "delayRender" || apiBinding.moduleSource !== "remotion" || findEnclosingFunction$1(node)) return;
|
|
59719
|
+
context.report({
|
|
59720
|
+
node,
|
|
59721
|
+
message: "A module-scoped `delayRender()` handle blocks all compositions and composition discovery. Move it inside the component and create it once with `useDelayRender()` or a lazy `useState` initializer."
|
|
59722
|
+
});
|
|
59723
|
+
} })
|
|
59724
|
+
});
|
|
59725
|
+
//#endregion
|
|
59726
|
+
//#region src/plugin/rules/correctness/remotion-no-native-media-elements.ts
|
|
59727
|
+
const REMOTION_MEDIA_REPLACEMENT_BY_TAG = new Map([
|
|
59728
|
+
["audio", "`Audio` from `@remotion/media`"],
|
|
59729
|
+
["iframe", "`IFrame` from `remotion`"],
|
|
59730
|
+
["img", "`Img` from `remotion`"],
|
|
59731
|
+
["video", "`Video` from `@remotion/media`"]
|
|
59732
|
+
]);
|
|
59733
|
+
const remotionNoNativeMediaElements = defineRule({
|
|
59734
|
+
id: "remotion-no-native-media-elements",
|
|
59735
|
+
title: "Native media element bypasses Remotion loading",
|
|
59736
|
+
tags: ["react-jsx-only"],
|
|
59737
|
+
requires: ["remotion:4"],
|
|
59738
|
+
severity: "error",
|
|
59739
|
+
recommendation: "Use Remotion's media components so rendering waits for assets and seeks media to the requested frame.",
|
|
59740
|
+
create: (context) => {
|
|
59741
|
+
const renderEvidence = createRemotionRenderEvidenceChecker(context);
|
|
59742
|
+
return { JSXOpeningElement(node) {
|
|
59743
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
59744
|
+
const renderFunction = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
59745
|
+
if (!renderFunction || !renderEvidence.functionHasEvidence(renderFunction)) return;
|
|
59746
|
+
const replacement = REMOTION_MEDIA_REPLACEMENT_BY_TAG.get(node.name.name);
|
|
59747
|
+
if (!replacement) return;
|
|
59748
|
+
context.report({
|
|
59749
|
+
node,
|
|
59750
|
+
message: `Native <${node.name.name}> does not let Remotion reliably wait for and synchronize the asset. Use ${replacement} instead.`
|
|
59751
|
+
});
|
|
59752
|
+
} };
|
|
59753
|
+
}
|
|
59754
|
+
});
|
|
59755
|
+
//#endregion
|
|
59756
|
+
//#region src/plugin/rules/correctness/remotion-no-next-image.ts
|
|
59757
|
+
const remotionNoNextImage = defineRule({
|
|
59758
|
+
id: "remotion-no-next-image",
|
|
59759
|
+
title: "Next.js Image can flicker in Remotion",
|
|
59760
|
+
tags: ["react-jsx-only"],
|
|
59761
|
+
requires: ["remotion:4"],
|
|
59762
|
+
severity: "error",
|
|
59763
|
+
recommendation: "Use `Img` from `remotion`, which delays rendering until the image is loaded.",
|
|
59764
|
+
create: (context) => {
|
|
59765
|
+
const renderEvidence = createRemotionRenderEvidenceChecker(context);
|
|
59766
|
+
return { JSXOpeningElement(node) {
|
|
59767
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
59768
|
+
const renderFunction = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
59769
|
+
if (!renderFunction || !renderEvidence.functionHasEvidence(renderFunction)) return;
|
|
59770
|
+
const symbol = context.scopes.symbolFor(node.name);
|
|
59771
|
+
if (symbol?.kind !== "import") return;
|
|
59772
|
+
const importBinding = getImportBindingForName(node.name, symbol.name);
|
|
59773
|
+
if (importBinding?.source !== "next/image" || importBinding.exportedName !== "default") return;
|
|
59774
|
+
context.report({
|
|
59775
|
+
node,
|
|
59776
|
+
message: "Next.js <Image> does not expose a reliable loading signal to Remotion, so rendered frames can flicker. Use <Img> from `remotion` instead."
|
|
59777
|
+
});
|
|
59778
|
+
} };
|
|
59779
|
+
}
|
|
59780
|
+
});
|
|
59781
|
+
//#endregion
|
|
59782
|
+
//#region src/plugin/rules/correctness/remotion-stable-delay-render-handle.ts
|
|
59783
|
+
const isUseStateLazyInitializer = (node, scopes) => {
|
|
59784
|
+
const enclosingFunction = findEnclosingFunction$1(node);
|
|
59785
|
+
if (!enclosingFunction) return false;
|
|
59786
|
+
const parent = enclosingFunction.parent;
|
|
59787
|
+
return Boolean(isNodeOfType(parent, "CallExpression") && parent.arguments[0] === enclosingFunction && isReactApiCall(parent, "useState", scopes, { resolveNamedAliases: true }));
|
|
59788
|
+
};
|
|
59789
|
+
const remotionStableDelayRenderHandle = defineRule({
|
|
59790
|
+
id: "remotion-stable-delay-render-handle",
|
|
59791
|
+
title: "delayRender handle is recreated during render",
|
|
59792
|
+
tags: ["react-jsx-only"],
|
|
59793
|
+
requires: ["remotion:4"],
|
|
59794
|
+
severity: "error",
|
|
59795
|
+
recommendation: "Prefer `useDelayRender()`, or initialize `delayRender()` once with `useState(() => delayRender())`.",
|
|
59796
|
+
create: (context) => ({ CallExpression(node) {
|
|
59797
|
+
const apiBinding = resolveRemotionApi(node.callee, context.scopes);
|
|
59798
|
+
if (apiBinding?.apiName !== "delayRender" || apiBinding.moduleSource !== "remotion" || !findRenderPhaseComponentOrHook(node, context.scopes) || isUseStateLazyInitializer(node, context.scopes)) return;
|
|
59799
|
+
context.report({
|
|
59800
|
+
node,
|
|
59801
|
+
message: "Calling `delayRender()` during every component render creates another outstanding handle and can make rendering time out. Use `useDelayRender()` or a lazy `useState` initializer."
|
|
59802
|
+
});
|
|
59803
|
+
} })
|
|
59804
|
+
});
|
|
59805
|
+
//#endregion
|
|
58739
59806
|
//#region src/plugin/rules/performance/rendering-animate-svg-wrapper.ts
|
|
58740
59807
|
const renderingAnimateSvgWrapper = defineRule({
|
|
58741
59808
|
id: "rendering-animate-svg-wrapper",
|
|
@@ -59856,7 +60923,7 @@ const functionBodyHasReturnWithValue = (functionNode) => {
|
|
|
59856
60923
|
//#endregion
|
|
59857
60924
|
//#region src/plugin/rules/react-builtins/require-render-return.ts
|
|
59858
60925
|
const RENDER_METHOD_NAME = "render";
|
|
59859
|
-
const MESSAGE$
|
|
60926
|
+
const MESSAGE$5 = "Your users see nothing because this `render` method returns nothing.";
|
|
59860
60927
|
const isStaticRenderKey = (key) => {
|
|
59861
60928
|
if (isNodeOfType(key, "Identifier")) return key.name === RENDER_METHOD_NAME;
|
|
59862
60929
|
if (isNodeOfType(key, "Literal")) return key.value === RENDER_METHOD_NAME;
|
|
@@ -59916,7 +60983,7 @@ const requireRenderReturn = defineRule({
|
|
|
59916
60983
|
if (functionBodyHasReturnWithValue(functionNode)) return;
|
|
59917
60984
|
context.report({
|
|
59918
60985
|
node: host.reportNode,
|
|
59919
|
-
message: MESSAGE$
|
|
60986
|
+
message: MESSAGE$5
|
|
59920
60987
|
});
|
|
59921
60988
|
};
|
|
59922
60989
|
return {
|
|
@@ -68468,6 +69535,7 @@ const countOwnScopeHookCalls = (functionNode, scopes, settings, countedFunctionN
|
|
|
68468
69535
|
};
|
|
68469
69536
|
const MIN_HOOK_CALLS_FOR_RENDER_SCOPE = 2;
|
|
68470
69537
|
const RENDER_SCOPE_FACTORY_NAME_PATTERN = /^_?(?:init|create|make|build)(?:[A-Z0-9_]|$)/;
|
|
69538
|
+
const HOOK_NAMESPACE_NAME_PATTERN = /Hooks?$/;
|
|
68471
69539
|
const isLocalNonHookFunctionCallee = (call, scopes, settings, countedFunctionNodes) => {
|
|
68472
69540
|
const callee = call.callee;
|
|
68473
69541
|
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
@@ -68492,6 +69560,15 @@ const isPackageImportedNonReactHookCallee = (call) => {
|
|
|
68492
69560
|
if (PATH_ALIAS_IMPORT_PATTERN.test(importSource)) return false;
|
|
68493
69561
|
return !isReactEcosystemImportSource(importSource);
|
|
68494
69562
|
};
|
|
69563
|
+
const isDefaultImportedClassApiMemberCallee = (call) => {
|
|
69564
|
+
const callee = call.callee;
|
|
69565
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
69566
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
69567
|
+
if (HOOK_NAMESPACE_NAME_PATTERN.test(callee.object.name)) return false;
|
|
69568
|
+
const importBinding = getImportBindingForName(call, callee.object.name);
|
|
69569
|
+
if (!importBinding || importBinding.exportedName !== "default") return false;
|
|
69570
|
+
return !REACT_RUNTIME_MODULE_SOURCES.has(importBinding.source);
|
|
69571
|
+
};
|
|
68495
69572
|
const isProjectOwnedMdxComponentsGetter = (call) => {
|
|
68496
69573
|
const callee = call.callee;
|
|
68497
69574
|
if (!isNodeOfType(callee, "Identifier") || callee.name !== "useMDXComponents") return false;
|
|
@@ -68641,6 +69718,7 @@ const rulesOfHooks = defineRule({
|
|
|
68641
69718
|
return;
|
|
68642
69719
|
}
|
|
68643
69720
|
if (isInsideClassComponent(node)) {
|
|
69721
|
+
if (isPackageImportedNonReactHookCallee(node) || isDefaultImportedClassApiMemberCallee(node)) return;
|
|
68644
69722
|
context.report({
|
|
68645
69723
|
node: node.callee,
|
|
68646
69724
|
message: buildClassComponentMessage(hookName)
|
|
@@ -68747,7 +69825,7 @@ const rulesOfHooks = defineRule({
|
|
|
68747
69825
|
});
|
|
68748
69826
|
//#endregion
|
|
68749
69827
|
//#region src/plugin/rules/a11y/scope.ts
|
|
68750
|
-
const MESSAGE$
|
|
69828
|
+
const MESSAGE$4 = "The `scope` attribute only works on `<th>` cells, so screen readers get no table-header help from it here.";
|
|
68751
69829
|
const scope = defineRule({
|
|
68752
69830
|
id: "scope",
|
|
68753
69831
|
title: "scope attribute on non-th element",
|
|
@@ -68762,7 +69840,7 @@ const scope = defineRule({
|
|
|
68762
69840
|
if (!HTML_TAGS.has(tag)) return;
|
|
68763
69841
|
if (tag !== "th") context.report({
|
|
68764
69842
|
node: scopeAttribute,
|
|
68765
|
-
message: MESSAGE$
|
|
69843
|
+
message: MESSAGE$4
|
|
68766
69844
|
});
|
|
68767
69845
|
} })
|
|
68768
69846
|
});
|
|
@@ -68773,13 +69851,13 @@ const secretInFallback = defineRule({
|
|
|
68773
69851
|
recommendation: "Remove the literal fallback and fail closed (throw when the variable is unset). The hardcoded value is a committed secret, and the `??`/`||` default makes the app run with it in any environment that forgot to set the var.",
|
|
68774
69852
|
scan: scanByPattern({
|
|
68775
69853
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
68776
|
-
pattern: /\bprocess\.env\.(?!(?:(?:(?:NEXT|EXPO|GATSBY|NUXT|REACT_APP|VITE)_)?PUBLIC_[A-Z0-9_]*(?<!_SECRET)(?<!_PRIVATE_KEY)(?<!_PASSWORD)(?<!_PASSWD)|[A-Z0-9_]*(?:PUBLISHABLE|ANON)_KEY)(?![A-Z0-9_]))[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|API_?KEY|APIKEY|ACCESS_KEY|CLIENT_SECRET|CREDENTIAL|SIGNING_KEY|ENCRYPTION_KEY|WEBHOOK_SECRET|SERVICE_ROLE)[A-Z0-9_]*(?<!_(?:NAME|HEADER|ENDPOINT|URL|URI|ID|PREFIX|SUFFIX|PARAM|PARAMS|FIELD|ISSUER|AUDIENCE|ALGORITHM|ALG|REGION|BUCKET|HOST|HOSTNAME|PORT|PATH|VERSION|SCOPE|TYPE|FORMAT|EXPIRY|TTL))\s*(?:\?\?|\|\|)\s*(["'`])(?![0-9]+["'`])(?![a-z][a-z0-9]*(?:[_-][a-z0-9]+)*[_-](?:token|secret|key|password|passwd|credential)s?["'`])(?![\w-]{0,12}0{8,}["'`])(?!(?:changeme|change[_-]?me|placeholder|your[_-]|example|sample|dummy|development|local|todo|replace[_-]?me|https?:\/\/|x{3,}|\*{3,}))[^"'`\n]{8,}\1/i,
|
|
69854
|
+
pattern: /\bprocess\.env\.(?!(?:(?:(?:NEXT|EXPO|GATSBY|NUXT|REACT_APP|VITE)_)?PUBLIC_[A-Z0-9_]*(?<!_SECRET)(?<!_PRIVATE_KEY)(?<!_PASSWORD)(?<!_PASSWD)|[A-Z0-9_]*(?:PUBLISHABLE|ANON)_KEY)(?![A-Z0-9_]))[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|API_?KEY|APIKEY|ACCESS_KEY|CLIENT_SECRET|CREDENTIAL|SIGNING_KEY|ENCRYPTION_KEY|WEBHOOK_SECRET|SERVICE_ROLE)[A-Z0-9_]*(?<!_(?:NAME|HEADER|ENDPOINT|URL|URI|ID|PREFIX|SUFFIX|PARAM|PARAMS|FIELD|ISSUER|AUDIENCE|ALGORITHM|ALG|REGION|BUCKET|HOST|HOSTNAME|PORT|PATH|VERSION|SCOPE|TYPE|FORMAT|EXPIRY|TTL))\s*(?:\?\?|\|\|)\s*(["'`])(?![0-9]+["'`])(?![a-z][a-z0-9]*(?:[_-][a-z0-9]+)*[_-](?:token|secret|key|password|passwd|credential)s?["'`])(?![\w-]{0,12}0{8,}["'`])(?!test test test test test test test test test test test junk["'`])(?!(?:changeme|change[_-]?me|placeholder|your[_-]|example|sample|dummy|development|local|todo|replace[_-]?me|https?:\/\/|x{3,}|\*{3,}))[^"'`\n]{8,}\1/i,
|
|
68777
69855
|
message: "A secret env var has a hardcoded string fallback: the literal is a committed secret and the app fails open (uses it) when the variable is unset."
|
|
68778
69856
|
})
|
|
68779
69857
|
});
|
|
68780
69858
|
//#endregion
|
|
68781
69859
|
//#region src/plugin/rules/react-builtins/self-closing-comp.ts
|
|
68782
|
-
const MESSAGE$
|
|
69860
|
+
const MESSAGE$3 = "This tag has no children, so the closing tag adds noise without changing output.";
|
|
68783
69861
|
const resolveSettings$2 = (settings) => {
|
|
68784
69862
|
const reactDoctor = settings?.["react-doctor"];
|
|
68785
69863
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.selfClosingComp ?? {} : {};
|
|
@@ -68818,7 +69896,7 @@ const selfClosingComp = defineRule({
|
|
|
68818
69896
|
if (isHtmlTag && !settings.html) return;
|
|
68819
69897
|
context.report({
|
|
68820
69898
|
node: node.openingElement,
|
|
68821
|
-
message: MESSAGE$
|
|
69899
|
+
message: MESSAGE$3
|
|
68822
69900
|
});
|
|
68823
69901
|
} };
|
|
68824
69902
|
}
|
|
@@ -69043,7 +70121,30 @@ const isAuthGuardName = (calleeName) => {
|
|
|
69043
70121
|
//#endregion
|
|
69044
70122
|
//#region src/plugin/utils/is-non-privileged-server-action.ts
|
|
69045
70123
|
const NON_DATA_EFFECT_FUNCTION_NAMES = new Set([...CACHE_REVALIDATION_FUNCTION_NAMES, ...NEXTJS_NAVIGATION_FUNCTIONS]);
|
|
69046
|
-
const
|
|
70124
|
+
const LOCALE_PREFERENCE_VERB_TOKENS = new Set(["set", "update"]);
|
|
70125
|
+
const LOCALE_PREFERENCE_NOUN_TOKENS = new Set(["language", "locale"]);
|
|
70126
|
+
const COOKIE_DELETION_METHOD_NAME = "delete";
|
|
70127
|
+
const isCacheOrNavigationCall = (node) => {
|
|
70128
|
+
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "Identifier")) return false;
|
|
70129
|
+
return NON_DATA_EFFECT_FUNCTION_NAMES.has(node.callee.name) && getImportSourceForName(node, node.callee.name) !== null;
|
|
70130
|
+
};
|
|
70131
|
+
const isCallerScopedLocalePreferenceCall = (node) => {
|
|
70132
|
+
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "Identifier") || node.arguments?.length !== 1) return false;
|
|
70133
|
+
const nameTokens = tokenizeIdentifierWords(node.callee.name);
|
|
70134
|
+
if (!nameTokens.some((token) => LOCALE_PREFERENCE_VERB_TOKENS.has(token))) return false;
|
|
70135
|
+
if (!nameTokens.some((token) => LOCALE_PREFERENCE_NOUN_TOKENS.has(token))) return false;
|
|
70136
|
+
const importSource = getImportSourceForName(node, node.callee.name)?.toLowerCase();
|
|
70137
|
+
return Boolean(importSource?.includes("i18n") || importSource?.includes("locale"));
|
|
70138
|
+
};
|
|
70139
|
+
const isCallerScopedCookieCall = (node, cookieBindingNames) => {
|
|
70140
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
70141
|
+
if (isNodeOfType(node.callee, "Identifier")) return node.callee.name === "cookies" && getImportSourceForName(node, node.callee.name) === "next/headers";
|
|
70142
|
+
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== COOKIE_DELETION_METHOD_NAME) return false;
|
|
70143
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
70144
|
+
if (isNodeOfType(receiver, "Identifier")) return cookieBindingNames.has(receiver.name);
|
|
70145
|
+
return isCookiesOrAwaitedCookiesCall(receiver) && getImportSourceForName(receiver, "cookies") === "next/headers";
|
|
70146
|
+
};
|
|
70147
|
+
const isKnownNonDataEffectCall = (node, cookieBindingNames) => isCacheOrNavigationCall(node) || isCallerScopedLocalePreferenceCall(node) || isCallerScopedCookieCall(node, cookieBindingNames);
|
|
69047
70148
|
const unwrapExpression = (node) => {
|
|
69048
70149
|
let current = node;
|
|
69049
70150
|
while (current) {
|
|
@@ -69059,10 +70160,10 @@ const unwrapExpression = (node) => {
|
|
|
69059
70160
|
}
|
|
69060
70161
|
return null;
|
|
69061
70162
|
};
|
|
69062
|
-
const isDataExposingValue = (node) => {
|
|
70163
|
+
const isDataExposingValue = (node, cookieBindingNames) => {
|
|
69063
70164
|
const value = unwrapExpression(node);
|
|
69064
70165
|
if (!value) return false;
|
|
69065
|
-
if (
|
|
70166
|
+
if (isKnownNonDataEffectCall(value, cookieBindingNames)) return false;
|
|
69066
70167
|
return !isLiteralOnlyExpression(value);
|
|
69067
70168
|
};
|
|
69068
70169
|
const isLiteralOnlyExpression = (node) => {
|
|
@@ -69080,22 +70181,23 @@ const getReturnedOrThrownArgument = (node) => {
|
|
|
69080
70181
|
if (isNodeOfType(node, "ThrowStatement")) return node.argument ?? null;
|
|
69081
70182
|
return null;
|
|
69082
70183
|
};
|
|
69083
|
-
const isDataExposingReturnOrThrow = (node) => isDataExposingValue(getReturnedOrThrownArgument(node));
|
|
69084
|
-
const isPrivilegedEffect = (node) => isNodeOfType(node, "CallExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "UnaryExpression") && node.operator === "delete" || isDataExposingReturnOrThrow(node);
|
|
70184
|
+
const isDataExposingReturnOrThrow = (node, cookieBindingNames) => isDataExposingValue(getReturnedOrThrownArgument(node), cookieBindingNames);
|
|
70185
|
+
const isPrivilegedEffect = (node, cookieBindingNames) => isNodeOfType(node, "CallExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "UnaryExpression") && node.operator === "delete" || isDataExposingReturnOrThrow(node, cookieBindingNames);
|
|
69085
70186
|
const isNonPrivilegedServerAction = (functionNode) => {
|
|
69086
70187
|
const functionBody = functionNode.body;
|
|
69087
70188
|
if (!functionBody) return false;
|
|
69088
|
-
|
|
70189
|
+
const cookieBindingNames = getImportSourceForName(functionBody, "cookies") === "next/headers" ? collectLocallyScopedCookieBindings(functionBody) : /* @__PURE__ */ new Set();
|
|
70190
|
+
if (!isNodeOfType(functionBody, "BlockStatement") && isDataExposingValue(functionBody, cookieBindingNames)) return false;
|
|
69089
70191
|
let hasNonDataEffectCall = false;
|
|
69090
70192
|
let hasPrivilegedEffect = false;
|
|
69091
70193
|
walkAst(functionBody, (child) => {
|
|
69092
70194
|
if (hasPrivilegedEffect) return false;
|
|
69093
70195
|
if (child !== functionBody && isFunctionLike$1(child)) return false;
|
|
69094
|
-
if (
|
|
70196
|
+
if (isKnownNonDataEffectCall(child, cookieBindingNames)) {
|
|
69095
70197
|
hasNonDataEffectCall = true;
|
|
69096
70198
|
return;
|
|
69097
70199
|
}
|
|
69098
|
-
if (isPrivilegedEffect(child)) {
|
|
70200
|
+
if (isPrivilegedEffect(child, cookieBindingNames)) {
|
|
69099
70201
|
hasPrivilegedEffect = true;
|
|
69100
70202
|
return false;
|
|
69101
70203
|
}
|
|
@@ -69174,6 +70276,34 @@ const getAuthScanRoots = (functionNode) => {
|
|
|
69174
70276
|
if (isNodeOfType(bodyNode, "BlockStatement")) return (bodyNode.body ?? []).slice(0, 10);
|
|
69175
70277
|
return [bodyNode];
|
|
69176
70278
|
};
|
|
70279
|
+
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
70280
|
+
const TEST_APP_SOURCE_PATH_PATTERN = /(?:^|[/\\])test[/\\](?:app|src)[/\\]/;
|
|
70281
|
+
const containsJsxOutsideNestedFunctions = (rootNode) => {
|
|
70282
|
+
let containsJsx = false;
|
|
70283
|
+
walkAst(rootNode, (child) => {
|
|
70284
|
+
if (containsJsx) return false;
|
|
70285
|
+
if (child !== rootNode && isFunctionLike$1(child)) return false;
|
|
70286
|
+
if (isNodeOfType(child, "JSXElement") || isNodeOfType(child, "JSXFragment")) {
|
|
70287
|
+
containsJsx = true;
|
|
70288
|
+
return false;
|
|
70289
|
+
}
|
|
70290
|
+
});
|
|
70291
|
+
return containsJsx;
|
|
70292
|
+
};
|
|
70293
|
+
const isComponentLikeServerExport = (candidate) => {
|
|
70294
|
+
if (!COMPONENT_NAME_PATTERN.test(candidate.displayName)) return false;
|
|
70295
|
+
const functionBody = candidate.functionNode.body;
|
|
70296
|
+
if (!isNodeOfType(functionBody, "BlockStatement")) return containsJsxOutsideNestedFunctions(functionBody);
|
|
70297
|
+
let hasReturnedJsx = false;
|
|
70298
|
+
walkAst(functionBody, (child) => {
|
|
70299
|
+
if (hasReturnedJsx) return false;
|
|
70300
|
+
if (child !== functionBody && isFunctionLike$1(child)) return false;
|
|
70301
|
+
if (!isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
70302
|
+
hasReturnedJsx = containsJsxOutsideNestedFunctions(child.argument);
|
|
70303
|
+
return false;
|
|
70304
|
+
});
|
|
70305
|
+
return hasReturnedJsx;
|
|
70306
|
+
};
|
|
69177
70307
|
const CREDENTIAL_MERGE_TAIL_TOKENS = {
|
|
69178
70308
|
sign: new Set([
|
|
69179
70309
|
"in",
|
|
@@ -69238,6 +70368,7 @@ const isCredentialEstablishingActionName = (actionName) => {
|
|
|
69238
70368
|
const hasPublicNameToken = (actionName) => tokenizeIdentifierWords(actionName).includes("public");
|
|
69239
70369
|
const inspectServerAction = (candidate, fileHasUseServerDirective, allowedFunctionNames, context) => {
|
|
69240
70370
|
if (!(fileHasUseServerDirective || hasUseServerDirective(candidate.functionNode))) return;
|
|
70371
|
+
if (isComponentLikeServerExport(candidate)) return;
|
|
69241
70372
|
if (isCredentialEstablishingActionName(candidate.displayName)) return;
|
|
69242
70373
|
if (hasPublicNameToken(candidate.displayName)) return;
|
|
69243
70374
|
if (containsAuthCheck(getAuthScanRoots(candidate.functionNode), allowedFunctionNames, GENERIC_AUTH_METHOD_NAMES)) return;
|
|
@@ -69275,18 +70406,42 @@ const serverAuthActions = defineRule({
|
|
|
69275
70406
|
title: "Unauthenticated server action can be called directly",
|
|
69276
70407
|
severity: "error",
|
|
69277
70408
|
recommendation: "Check auth before touching data because exported server actions can be called directly by unauthenticated clients.",
|
|
69278
|
-
create: (context) => {
|
|
70409
|
+
create: skipNonProductionFiles((context) => {
|
|
70410
|
+
const shouldSkipTestAppSource = Boolean(context.filename && TEST_APP_SOURCE_PATH_PATTERN.test(context.filename));
|
|
69279
70411
|
let fileHasUseServerDirective = false;
|
|
70412
|
+
let programNode = null;
|
|
70413
|
+
const inspectedFunctions = /* @__PURE__ */ new Set();
|
|
69280
70414
|
const customAuthFunctionNames = getReactDoctorStringArraySetting(context.settings, "serverAuthFunctionNames");
|
|
69281
70415
|
const allowedFunctionNames = customAuthFunctionNames.length > 0 ? new Set([...AUTH_FUNCTION_NAMES, ...customAuthFunctionNames]) : AUTH_FUNCTION_NAMES;
|
|
69282
|
-
const inspect = (candidate) =>
|
|
70416
|
+
const inspect = (candidate) => {
|
|
70417
|
+
if (inspectedFunctions.has(candidate.functionNode)) return;
|
|
70418
|
+
inspectedFunctions.add(candidate.functionNode);
|
|
70419
|
+
inspectServerAction(candidate, fileHasUseServerDirective, allowedFunctionNames, context);
|
|
70420
|
+
};
|
|
69283
70421
|
return {
|
|
69284
|
-
Program(
|
|
69285
|
-
|
|
70422
|
+
Program(currentProgramNode) {
|
|
70423
|
+
programNode = currentProgramNode;
|
|
70424
|
+
fileHasUseServerDirective = hasDirective(currentProgramNode, "use server");
|
|
69286
70425
|
},
|
|
69287
70426
|
ExportNamedDeclaration(node) {
|
|
70427
|
+
if (shouldSkipTestAppSource) return;
|
|
69288
70428
|
const declaration = node.declaration;
|
|
69289
|
-
if (!declaration)
|
|
70429
|
+
if (!declaration) {
|
|
70430
|
+
if (!programNode || node.source || node.exportKind === "type") return;
|
|
70431
|
+
for (const specifier of node.specifiers ?? []) {
|
|
70432
|
+
if (!isNodeOfType(specifier, "ExportSpecifier") || specifier.exportKind === "type") continue;
|
|
70433
|
+
const exportedName = isNodeOfType(specifier.exported, "Identifier") ? specifier.exported.name : isNodeOfType(specifier.exported, "Literal") && typeof specifier.exported.value === "string" ? specifier.exported.value : null;
|
|
70434
|
+
if (!exportedName) continue;
|
|
70435
|
+
const exportedValue = findExportedValue(programNode, exportedName);
|
|
70436
|
+
if (!isAsyncFunctionLikeNode(exportedValue)) continue;
|
|
70437
|
+
inspect({
|
|
70438
|
+
functionNode: exportedValue,
|
|
70439
|
+
displayName: isNodeOfType(specifier.local, "Identifier") ? specifier.local.name : exportedName,
|
|
70440
|
+
reportNode: specifier.local ?? specifier
|
|
70441
|
+
});
|
|
70442
|
+
}
|
|
70443
|
+
return;
|
|
70444
|
+
}
|
|
69290
70445
|
if (isAsyncFunctionLikeNode(declaration)) {
|
|
69291
70446
|
if (!isNodeOfType(declaration, "FunctionDeclaration")) return;
|
|
69292
70447
|
inspect({
|
|
@@ -69299,11 +70454,23 @@ const serverAuthActions = defineRule({
|
|
|
69299
70454
|
if (isNodeOfType(declaration, "VariableDeclaration")) for (const candidate of collectCandidatesFromVariableDeclaration(declaration)) inspect(candidate);
|
|
69300
70455
|
},
|
|
69301
70456
|
ExportDefaultDeclaration(node) {
|
|
69302
|
-
|
|
70457
|
+
if (shouldSkipTestAppSource) return;
|
|
70458
|
+
const directCandidate = getCandidateFromDefaultDeclaration(node);
|
|
70459
|
+
if (directCandidate) {
|
|
70460
|
+
inspect(directCandidate);
|
|
70461
|
+
return;
|
|
70462
|
+
}
|
|
70463
|
+
if (!programNode) return;
|
|
70464
|
+
const resolvedDefaultExport = findExportedValue(programNode, "default");
|
|
70465
|
+
const candidate = isAsyncFunctionLikeNode(resolvedDefaultExport) ? {
|
|
70466
|
+
functionNode: resolvedDefaultExport,
|
|
70467
|
+
displayName: isNodeOfType(node.declaration, "Identifier") ? node.declaration.name : "default",
|
|
70468
|
+
reportNode: node.declaration ?? node
|
|
70469
|
+
} : null;
|
|
69303
70470
|
if (candidate) inspect(candidate);
|
|
69304
70471
|
}
|
|
69305
70472
|
};
|
|
69306
|
-
}
|
|
70473
|
+
})
|
|
69307
70474
|
});
|
|
69308
70475
|
//#endregion
|
|
69309
70476
|
//#region src/plugin/rules/server/server-cache-with-object-literal.ts
|
|
@@ -69383,6 +70550,7 @@ const serverDedupProps = defineRule({
|
|
|
69383
70550
|
for (const attr of node.attributes ?? []) {
|
|
69384
70551
|
if (!isNodeOfType(attr, "JSXAttribute")) continue;
|
|
69385
70552
|
if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
|
|
70553
|
+
if (attr.name.name === "key") continue;
|
|
69386
70554
|
if (!isNodeOfType(attr.value, "JSXExpressionContainer")) continue;
|
|
69387
70555
|
const expression = attr.value.expression;
|
|
69388
70556
|
if (!expression) continue;
|
|
@@ -70112,7 +71280,7 @@ const collectJsxRuntimeImports = (program) => {
|
|
|
70112
71280
|
};
|
|
70113
71281
|
//#endregion
|
|
70114
71282
|
//#region src/plugin/rules/react-builtins/style-prop-object.ts
|
|
70115
|
-
const MESSAGE$
|
|
71283
|
+
const MESSAGE$2 = "Your styles don't render because you passed the `style` prop a string instead of an object.";
|
|
70116
71284
|
const resolveSettings = (settings) => {
|
|
70117
71285
|
const reactDoctor = settings?.["react-doctor"];
|
|
70118
71286
|
return { allow: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.stylePropObject ?? {} : {}).allow ?? [] };
|
|
@@ -70219,7 +71387,7 @@ const stylePropObject = defineRule({
|
|
|
70219
71387
|
if (isNodeOfType(value, "Literal") && typeof value.value === "string") {
|
|
70220
71388
|
context.report({
|
|
70221
71389
|
node: attribute,
|
|
70222
|
-
message: MESSAGE$
|
|
71390
|
+
message: MESSAGE$2
|
|
70223
71391
|
});
|
|
70224
71392
|
return;
|
|
70225
71393
|
}
|
|
@@ -70227,7 +71395,7 @@ const stylePropObject = defineRule({
|
|
|
70227
71395
|
const innerExpression = value.expression;
|
|
70228
71396
|
if (innerExpression && innerExpression.type !== "JSXEmptyExpression" && isInvalidStyleExpression(innerExpression)) context.report({
|
|
70229
71397
|
node: attribute,
|
|
70230
|
-
message: MESSAGE$
|
|
71398
|
+
message: MESSAGE$2
|
|
70231
71399
|
});
|
|
70232
71400
|
}
|
|
70233
71401
|
}
|
|
@@ -70250,7 +71418,7 @@ const stylePropObject = defineRule({
|
|
|
70250
71418
|
if (!(isNodeOfType(key, "Identifier") && key.name === "style" || isNodeOfType(key, "Literal") && key.value === "style")) continue;
|
|
70251
71419
|
if (isInvalidStyleExpression(property.value)) context.report({
|
|
70252
71420
|
node: property,
|
|
70253
|
-
message: MESSAGE$
|
|
71421
|
+
message: MESSAGE$2
|
|
70254
71422
|
});
|
|
70255
71423
|
}
|
|
70256
71424
|
}
|
|
@@ -70586,7 +71754,7 @@ const svgFilterClickjackingRisk = defineRule({
|
|
|
70586
71754
|
});
|
|
70587
71755
|
//#endregion
|
|
70588
71756
|
//#region src/plugin/rules/a11y/tabindex-no-positive.ts
|
|
70589
|
-
const MESSAGE = "Keyboard users get jumped out of the normal order by a positive `tabIndex`, so use `0` or `-1`.";
|
|
71757
|
+
const MESSAGE$1 = "Keyboard users get jumped out of the normal order by a positive `tabIndex`, so use `0` or `-1`.";
|
|
70590
71758
|
const tabindexNoPositive = defineRule({
|
|
70591
71759
|
id: "tabindex-no-positive",
|
|
70592
71760
|
title: "Positive tabindex value",
|
|
@@ -70606,7 +71774,7 @@ const tabindexNoPositive = defineRule({
|
|
|
70606
71774
|
} else if (tabIndex.value && isNodeOfType(tabIndex.value, "JSXExpressionContainer")) numericValue = parseJsxValue(tabIndex.value);
|
|
70607
71775
|
if (numericValue !== null && numericValue > 0) context.report({
|
|
70608
71776
|
node: tabIndex,
|
|
70609
|
-
message: MESSAGE
|
|
71777
|
+
message: MESSAGE$1
|
|
70610
71778
|
});
|
|
70611
71779
|
} })
|
|
70612
71780
|
});
|
|
@@ -71512,6 +72680,445 @@ const useLazyMotion = defineRule({
|
|
|
71512
72680
|
} })
|
|
71513
72681
|
});
|
|
71514
72682
|
//#endregion
|
|
72683
|
+
//#region src/plugin/rules/valtio/valtio-no-proxy-read-in-render.ts
|
|
72684
|
+
const VALTIO_REACT_MODULE_SOURCES$1 = new Set(["valtio", "valtio/react"]);
|
|
72685
|
+
const REACT_DEPENDENCY_ARRAY_HOOK_NAMES = new Set([
|
|
72686
|
+
"useCallback",
|
|
72687
|
+
"useEffect",
|
|
72688
|
+
"useInsertionEffect",
|
|
72689
|
+
"useLayoutEffect",
|
|
72690
|
+
"useMemo"
|
|
72691
|
+
]);
|
|
72692
|
+
const MESSAGE = "Read this Valtio value from the useSnapshot result during render. Keep proxy reads for callbacks so render uses the tracked, consistent snapshot.";
|
|
72693
|
+
const isValtioImportSymbol = (symbol) => {
|
|
72694
|
+
if (symbol.kind !== "import") return false;
|
|
72695
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
72696
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && VALTIO_REACT_MODULE_SOURCES$1.has(importDeclaration.source.value));
|
|
72697
|
+
};
|
|
72698
|
+
const isValtioUseSnapshotCall = (node, scopes) => {
|
|
72699
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
72700
|
+
const callee = stripParenExpression(node.callee);
|
|
72701
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
72702
|
+
const symbol = resolveConstIdentifierAlias(callee, scopes);
|
|
72703
|
+
return Boolean(symbol && isValtioImportSymbol(symbol) && getImportedName(symbol.declarationNode) === "useSnapshot");
|
|
72704
|
+
}
|
|
72705
|
+
if (!isNodeOfType(callee, "MemberExpression") || getStaticPropertyName(callee) !== "useSnapshot") return false;
|
|
72706
|
+
const receiver = stripParenExpression(callee.object);
|
|
72707
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
72708
|
+
const namespaceSymbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
72709
|
+
return Boolean(namespaceSymbol && isValtioImportSymbol(namespaceSymbol) && isNodeOfType(namespaceSymbol.declarationNode, "ImportNamespaceSpecifier"));
|
|
72710
|
+
};
|
|
72711
|
+
const collectPatternBindingIdentifiers = (pattern) => {
|
|
72712
|
+
if (isNodeOfType(pattern, "Identifier")) return [pattern];
|
|
72713
|
+
if (isNodeOfType(pattern, "AssignmentPattern")) return collectPatternBindingIdentifiers(pattern.left);
|
|
72714
|
+
if (isNodeOfType(pattern, "RestElement")) return collectPatternBindingIdentifiers(pattern.argument);
|
|
72715
|
+
if (isNodeOfType(pattern, "ArrayPattern")) return pattern.elements.flatMap((element) => element ? collectPatternBindingIdentifiers(element) : []);
|
|
72716
|
+
if (isNodeOfType(pattern, "ObjectPattern")) return pattern.properties.flatMap((property) => {
|
|
72717
|
+
if (isNodeOfType(property, "Property")) return collectPatternBindingIdentifiers(property.value);
|
|
72718
|
+
return isNodeOfType(property, "RestElement") ? collectPatternBindingIdentifiers(property.argument) : [];
|
|
72719
|
+
});
|
|
72720
|
+
return [];
|
|
72721
|
+
};
|
|
72722
|
+
const collectAssignmentWriteTargets = (target) => {
|
|
72723
|
+
const candidate = stripParenExpression(target);
|
|
72724
|
+
if (isNodeOfType(candidate, "AssignmentPattern")) return collectAssignmentWriteTargets(candidate.left);
|
|
72725
|
+
if (isNodeOfType(candidate, "RestElement")) return collectAssignmentWriteTargets(candidate.argument);
|
|
72726
|
+
if (isNodeOfType(candidate, "ArrayPattern")) return candidate.elements.flatMap((element) => element ? collectAssignmentWriteTargets(element) : []);
|
|
72727
|
+
if (isNodeOfType(candidate, "ObjectPattern")) return candidate.properties.flatMap((property) => isNodeOfType(property, "Property") ? collectAssignmentWriteTargets(property.value) : collectAssignmentWriteTargets(property.argument));
|
|
72728
|
+
return [candidate];
|
|
72729
|
+
};
|
|
72730
|
+
const findPatternPathToBinding = (pattern, bindingIdentifier) => {
|
|
72731
|
+
if (pattern === bindingIdentifier) return [];
|
|
72732
|
+
if (isNodeOfType(pattern, "AssignmentPattern")) return findPatternPathToBinding(pattern.left, bindingIdentifier);
|
|
72733
|
+
if (isNodeOfType(pattern, "RestElement")) return null;
|
|
72734
|
+
if (isNodeOfType(pattern, "ArrayPattern")) {
|
|
72735
|
+
for (const [elementIndex, element] of pattern.elements.entries()) {
|
|
72736
|
+
if (!element) continue;
|
|
72737
|
+
const nestedPath = findPatternPathToBinding(element, bindingIdentifier);
|
|
72738
|
+
if (nestedPath) return [String(elementIndex), ...nestedPath];
|
|
72739
|
+
}
|
|
72740
|
+
return null;
|
|
72741
|
+
}
|
|
72742
|
+
if (!isNodeOfType(pattern, "ObjectPattern")) return null;
|
|
72743
|
+
for (const property of pattern.properties) {
|
|
72744
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
72745
|
+
const nestedPath = findPatternPathToBinding(property.value, bindingIdentifier);
|
|
72746
|
+
if (!nestedPath) continue;
|
|
72747
|
+
const propertyName = getStaticPropertyKeyName(property);
|
|
72748
|
+
return propertyName === null ? null : [propertyName, ...nestedPath];
|
|
72749
|
+
}
|
|
72750
|
+
return null;
|
|
72751
|
+
};
|
|
72752
|
+
const resolveProxyPath = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set(), allowedAliasSymbolIds = null, collectedAliasCaptures = null) => {
|
|
72753
|
+
const candidate = stripParenExpression(expression);
|
|
72754
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
72755
|
+
const receiverPath = resolveProxyPath(candidate.object, scopes, visitedSymbolIds, allowedAliasSymbolIds, collectedAliasCaptures);
|
|
72756
|
+
if (!receiverPath) return null;
|
|
72757
|
+
return {
|
|
72758
|
+
...receiverPath,
|
|
72759
|
+
properties: [...receiverPath.properties, getStaticPropertyName(candidate)]
|
|
72760
|
+
};
|
|
72761
|
+
}
|
|
72762
|
+
if (!isNodeOfType(candidate, "Identifier")) return null;
|
|
72763
|
+
const symbol = scopes.symbolFor(candidate);
|
|
72764
|
+
if (!symbol) return scopes.isGlobalReference(candidate) ? {
|
|
72765
|
+
rootKey: `global:${candidate.name}`,
|
|
72766
|
+
properties: []
|
|
72767
|
+
} : null;
|
|
72768
|
+
if (visitedSymbolIds.has(symbol.id)) return null;
|
|
72769
|
+
if (symbol.kind === "const" && symbol.initializer && isNodeOfType(symbol.declarationNode, "VariableDeclarator")) {
|
|
72770
|
+
const directAlias = symbol.declarationNode.id === symbol.bindingIdentifier;
|
|
72771
|
+
const initializerNode = directAlias ? symbol.initializer : symbol.declarationNode.init;
|
|
72772
|
+
if (!initializerNode) return {
|
|
72773
|
+
rootKey: `symbol:${symbol.id}`,
|
|
72774
|
+
properties: []
|
|
72775
|
+
};
|
|
72776
|
+
const initializer = stripParenExpression(initializerNode);
|
|
72777
|
+
const canResolveDirectAlias = directAlias && (isNodeOfType(initializer, "Identifier") || isNodeOfType(initializer, "MemberExpression"));
|
|
72778
|
+
const patternPath = directAlias ? null : findPatternPathToBinding(symbol.declarationNode.id, symbol.bindingIdentifier);
|
|
72779
|
+
if ((canResolveDirectAlias || patternPath) && (allowedAliasSymbolIds === null || allowedAliasSymbolIds.has(symbol.id))) {
|
|
72780
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
72781
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
72782
|
+
const initializerPath = resolveProxyPath(initializer, scopes, nextVisitedSymbolIds, allowedAliasSymbolIds, collectedAliasCaptures);
|
|
72783
|
+
if (initializerPath) {
|
|
72784
|
+
const aliasPath = {
|
|
72785
|
+
...initializerPath,
|
|
72786
|
+
properties: [...initializerPath.properties, ...patternPath ?? []]
|
|
72787
|
+
};
|
|
72788
|
+
const declarationEnd = symbol.declarationNode.range?.[1];
|
|
72789
|
+
if (typeof declarationEnd === "number") collectedAliasCaptures?.set(symbol.id, {
|
|
72790
|
+
declarationEnd,
|
|
72791
|
+
path: aliasPath
|
|
72792
|
+
});
|
|
72793
|
+
return aliasPath;
|
|
72794
|
+
}
|
|
72795
|
+
}
|
|
72796
|
+
}
|
|
72797
|
+
return {
|
|
72798
|
+
rootKey: `symbol:${symbol.id}`,
|
|
72799
|
+
properties: []
|
|
72800
|
+
};
|
|
72801
|
+
};
|
|
72802
|
+
const isScopeWithin = (candidateScope, ancestorScope) => {
|
|
72803
|
+
let currentScope = candidateScope;
|
|
72804
|
+
while (currentScope) {
|
|
72805
|
+
if (currentScope === ancestorScope) return true;
|
|
72806
|
+
currentScope = currentScope.parent;
|
|
72807
|
+
}
|
|
72808
|
+
return false;
|
|
72809
|
+
};
|
|
72810
|
+
const isPathPrefix = (prefix, candidate) => {
|
|
72811
|
+
if (prefix.rootKey !== candidate.rootKey || prefix.properties.length > candidate.properties.length) return false;
|
|
72812
|
+
return prefix.properties.every((propertyName, propertyIndex) => propertyName !== null && propertyName === candidate.properties[propertyIndex]);
|
|
72813
|
+
};
|
|
72814
|
+
const findOutermostMemberRead = (identifier) => {
|
|
72815
|
+
let currentExpression = findTransparentExpressionRoot(identifier);
|
|
72816
|
+
for (;;) {
|
|
72817
|
+
const parent = currentExpression.parent;
|
|
72818
|
+
if (!isNodeOfType(parent, "MemberExpression") || stripParenExpression(parent.object) !== stripParenExpression(currentExpression)) return currentExpression;
|
|
72819
|
+
currentExpression = findTransparentExpressionRoot(parent);
|
|
72820
|
+
}
|
|
72821
|
+
};
|
|
72822
|
+
const isReadPositionWithinAssignmentTarget = (expression) => {
|
|
72823
|
+
let currentNode = expression;
|
|
72824
|
+
let parentNode = currentNode.parent;
|
|
72825
|
+
while (parentNode) {
|
|
72826
|
+
if (isNodeOfType(parentNode, "MemberExpression") && parentNode.computed && parentNode.property === currentNode || isNodeOfType(parentNode, "Property") && parentNode.computed && parentNode.key === currentNode || isNodeOfType(parentNode, "AssignmentPattern") && parentNode.right === currentNode) return true;
|
|
72827
|
+
if (isNodeOfType(parentNode, "AssignmentExpression") || isNodeOfType(parentNode, "UpdateExpression") || isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "delete" || isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return false;
|
|
72828
|
+
currentNode = parentNode;
|
|
72829
|
+
parentNode = currentNode.parent;
|
|
72830
|
+
}
|
|
72831
|
+
return false;
|
|
72832
|
+
};
|
|
72833
|
+
const isSnapshotArgument = (expression, scopes) => {
|
|
72834
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
72835
|
+
const parent = expressionRoot.parent;
|
|
72836
|
+
return Boolean(parent && isNodeOfType(parent, "CallExpression") && isValtioUseSnapshotCall(parent, scopes) && parent.arguments[0] === expressionRoot);
|
|
72837
|
+
};
|
|
72838
|
+
const isStableProxyDependency = (expression, readPath, matchingTarget, scopes) => {
|
|
72839
|
+
if (readPath.properties.length !== matchingTarget.path.properties.length || !isPathPrefix(matchingTarget.path, readPath)) return false;
|
|
72840
|
+
const dependencyArray = findTransparentExpressionRoot(expression).parent;
|
|
72841
|
+
if (!isNodeOfType(dependencyArray, "ArrayExpression")) return false;
|
|
72842
|
+
const hookCall = dependencyArray.parent;
|
|
72843
|
+
return Boolean(isNodeOfType(hookCall, "CallExpression") && hookCall.arguments[1] === dependencyArray && isReactApiCall(hookCall, REACT_DEPENDENCY_ARRAY_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }));
|
|
72844
|
+
};
|
|
72845
|
+
const getSnapshotTarget = (callExpression, context) => {
|
|
72846
|
+
if (!isValtioUseSnapshotCall(callExpression, context.scopes)) return null;
|
|
72847
|
+
const proxyArgument = callExpression.arguments[0];
|
|
72848
|
+
if (!proxyArgument || isNodeOfType(proxyArgument, "SpreadElement")) return null;
|
|
72849
|
+
const path = resolveProxyPath(proxyArgument, context.scopes);
|
|
72850
|
+
if (!path || path.properties.some((propertyName) => propertyName === null)) return null;
|
|
72851
|
+
const callExpressionRoot = findTransparentExpressionRoot(callExpression);
|
|
72852
|
+
const declarator = callExpressionRoot.parent;
|
|
72853
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== callExpressionRoot || !isNodeOfType(declarator.parent, "VariableDeclaration")) return null;
|
|
72854
|
+
const ownerFunction = findRenderPhaseComponentOrHook(callExpression, context.scopes);
|
|
72855
|
+
if (!ownerFunction) return null;
|
|
72856
|
+
const snapshotBindingScopes = collectPatternBindingIdentifiers(declarator.id).flatMap((bindingIdentifier) => {
|
|
72857
|
+
const bindingSymbol = context.scopes.symbolFor(bindingIdentifier);
|
|
72858
|
+
return bindingSymbol ? [bindingSymbol.scope] : [];
|
|
72859
|
+
});
|
|
72860
|
+
const declarationEnd = declarator.range?.[1];
|
|
72861
|
+
if (snapshotBindingScopes.length === 0 || typeof declarationEnd !== "number") return null;
|
|
72862
|
+
return {
|
|
72863
|
+
declarationEnd,
|
|
72864
|
+
ownerFunction,
|
|
72865
|
+
path,
|
|
72866
|
+
snapshotBindingScopes
|
|
72867
|
+
};
|
|
72868
|
+
};
|
|
72869
|
+
const wasTargetReplacedBeforeRead = (target, readPosition, writeTargets, readAliasCaptures, context) => writeTargets.some((writeTarget) => {
|
|
72870
|
+
const writeNode = findTransparentExpressionRoot(writeTarget).parent;
|
|
72871
|
+
const writePosition = writeNode?.range?.[0];
|
|
72872
|
+
if (!writeNode || typeof writePosition !== "number" || writePosition <= target.declarationEnd || writePosition >= readPosition || findRenderPhaseComponentOrHook(writeNode, context.scopes) !== target.ownerFunction) return false;
|
|
72873
|
+
const writePath = resolveProxyPath(writeTarget, context.scopes, /* @__PURE__ */ new Set(), null);
|
|
72874
|
+
if (!writePath || !isPathPrefix(writePath, target.path)) return false;
|
|
72875
|
+
return ![...readAliasCaptures.values()].some((aliasCapture) => aliasCapture.declarationEnd < writePosition && isPathPrefix(writePath, aliasCapture.path));
|
|
72876
|
+
});
|
|
72877
|
+
const valtioNoProxyReadInRender = defineRule({
|
|
72878
|
+
id: "valtio-no-proxy-read-in-render",
|
|
72879
|
+
title: "Valtio proxy read during render",
|
|
72880
|
+
severity: "warn",
|
|
72881
|
+
recommendation: "Read reactive render values from useSnapshot. Reserve the mutable proxy for event handlers, effects, and other callbacks that need the latest value.",
|
|
72882
|
+
requires: ["valtio:1"],
|
|
72883
|
+
create: (context) => {
|
|
72884
|
+
const snapshotTargets = [];
|
|
72885
|
+
const identifierCandidates = [];
|
|
72886
|
+
const writeTargets = [];
|
|
72887
|
+
return {
|
|
72888
|
+
CallExpression(node) {
|
|
72889
|
+
const snapshotTarget = getSnapshotTarget(node, context);
|
|
72890
|
+
if (snapshotTarget) snapshotTargets.push(snapshotTarget);
|
|
72891
|
+
},
|
|
72892
|
+
Identifier(node) {
|
|
72893
|
+
if (context.scopes.referenceFor(node)) identifierCandidates.push(node);
|
|
72894
|
+
},
|
|
72895
|
+
AssignmentExpression(node) {
|
|
72896
|
+
writeTargets.push(...collectAssignmentWriteTargets(node.left));
|
|
72897
|
+
},
|
|
72898
|
+
UpdateExpression(node) {
|
|
72899
|
+
writeTargets.push(node.argument);
|
|
72900
|
+
},
|
|
72901
|
+
"Program:exit"() {
|
|
72902
|
+
const reportedExpressions = /* @__PURE__ */ new Set();
|
|
72903
|
+
for (const identifier of identifierCandidates) {
|
|
72904
|
+
const readExpression = findOutermostMemberRead(identifier);
|
|
72905
|
+
if (reportedExpressions.has(readExpression) || isWithinAssignmentTarget(readExpression) && !isReadPositionWithinAssignmentTarget(readExpression) || isSnapshotArgument(readExpression, context.scopes)) continue;
|
|
72906
|
+
const readAliasCaptures = /* @__PURE__ */ new Map();
|
|
72907
|
+
const readPath = resolveProxyPath(readExpression, context.scopes, /* @__PURE__ */ new Set(), null, readAliasCaptures);
|
|
72908
|
+
const readPosition = readExpression.range?.[0];
|
|
72909
|
+
const ownerFunction = findRenderPhaseComponentOrHook(readExpression, context.scopes);
|
|
72910
|
+
if (!readPath || typeof readPosition !== "number" || !ownerFunction) continue;
|
|
72911
|
+
const readScope = context.scopes.scopeFor(readExpression);
|
|
72912
|
+
const matchingTarget = snapshotTargets.find((target) => target.ownerFunction === ownerFunction && target.declarationEnd < readPosition && target.snapshotBindingScopes.some((bindingScope) => isScopeWithin(readScope, bindingScope)) && isPathPrefix(target.path, readPath) && (readAliasCaptures.size === 0 || [...readAliasCaptures.values()].some((aliasCapture) => isPathPrefix(aliasCapture.path, target.path) || isPathPrefix(target.path, aliasCapture.path) && readPath.properties.length > aliasCapture.path.properties.length)) && !wasTargetReplacedBeforeRead(target, readPosition, writeTargets, readAliasCaptures, context));
|
|
72913
|
+
if (!matchingTarget || isStableProxyDependency(readExpression, readPath, matchingTarget, context.scopes)) continue;
|
|
72914
|
+
reportedExpressions.add(readExpression);
|
|
72915
|
+
context.report({
|
|
72916
|
+
node: readExpression,
|
|
72917
|
+
message: MESSAGE
|
|
72918
|
+
});
|
|
72919
|
+
}
|
|
72920
|
+
}
|
|
72921
|
+
};
|
|
72922
|
+
}
|
|
72923
|
+
});
|
|
72924
|
+
//#endregion
|
|
72925
|
+
//#region src/plugin/rules/valtio/valtio-no-snapshot-in-callback.ts
|
|
72926
|
+
const VALTIO_REACT_MODULE_SOURCES = new Set(["valtio", "valtio/react"]);
|
|
72927
|
+
const DEFERRED_REACT_HOOK_NAMES = new Set([
|
|
72928
|
+
"useEffect",
|
|
72929
|
+
"useInsertionEffect",
|
|
72930
|
+
"useLayoutEffect"
|
|
72931
|
+
]);
|
|
72932
|
+
const PROMISE_CONTINUATION_METHOD_NAMES = new Set([
|
|
72933
|
+
"catch",
|
|
72934
|
+
"finally",
|
|
72935
|
+
"then"
|
|
72936
|
+
]);
|
|
72937
|
+
const DEFERRED_CONSTRUCTOR_NAMES = new Set([
|
|
72938
|
+
"IntersectionObserver",
|
|
72939
|
+
"MutationObserver",
|
|
72940
|
+
"PerformanceObserver",
|
|
72941
|
+
"ResizeObserver"
|
|
72942
|
+
]);
|
|
72943
|
+
const isValtioImport = (symbol) => {
|
|
72944
|
+
const source = getImportDeclarationForSymbol(symbol)?.source.value;
|
|
72945
|
+
return typeof source === "string" && VALTIO_REACT_MODULE_SOURCES.has(source);
|
|
72946
|
+
};
|
|
72947
|
+
const resolvesToValtioNamespace = (expression, scopes, visitedSymbolIds) => {
|
|
72948
|
+
const candidate = stripParenExpression(expression);
|
|
72949
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
72950
|
+
const symbol = scopes.symbolFor(candidate);
|
|
72951
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
72952
|
+
if (symbol.kind === "import" && isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") && isValtioImport(symbol)) return true;
|
|
72953
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
72954
|
+
if (!initializer) return false;
|
|
72955
|
+
visitedSymbolIds.add(symbol.id);
|
|
72956
|
+
return resolvesToValtioNamespace(initializer, scopes, visitedSymbolIds);
|
|
72957
|
+
};
|
|
72958
|
+
const isValtioUseSnapshotCallee = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
72959
|
+
const candidate = stripParenExpression(expression);
|
|
72960
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
72961
|
+
const symbol = scopes.symbolFor(candidate);
|
|
72962
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
72963
|
+
if (symbol.kind === "import" && isValtioImport(symbol) && getImportedName(symbol.declarationNode) === "useSnapshot") return true;
|
|
72964
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
72965
|
+
if (!initializer) return false;
|
|
72966
|
+
visitedSymbolIds.add(symbol.id);
|
|
72967
|
+
return isValtioUseSnapshotCallee(initializer, scopes, visitedSymbolIds);
|
|
72968
|
+
}
|
|
72969
|
+
if (!isNodeOfType(candidate, "MemberExpression") || getStaticPropertyName(candidate) !== "useSnapshot") return false;
|
|
72970
|
+
return resolvesToValtioNamespace(candidate.object, scopes, visitedSymbolIds);
|
|
72971
|
+
};
|
|
72972
|
+
const getSnapshotOriginCall = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
72973
|
+
const candidate = stripParenExpression(expression);
|
|
72974
|
+
if (isNodeOfType(candidate, "CallExpression")) return isValtioUseSnapshotCallee(candidate.callee, context.scopes) ? candidate : null;
|
|
72975
|
+
if (!isNodeOfType(candidate, "Identifier")) return null;
|
|
72976
|
+
const symbol = context.scopes.symbolFor(candidate);
|
|
72977
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return null;
|
|
72978
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
72979
|
+
if (!initializer) return null;
|
|
72980
|
+
visitedSymbolIds.add(symbol.id);
|
|
72981
|
+
return getSnapshotOriginCall(initializer, context, visitedSymbolIds);
|
|
72982
|
+
};
|
|
72983
|
+
const isJsxEventHandlerValue = (expression) => {
|
|
72984
|
+
const expressionContainer = expression.parent;
|
|
72985
|
+
if (!isNodeOfType(expressionContainer, "JSXExpressionContainer")) return false;
|
|
72986
|
+
const attribute = expressionContainer.parent;
|
|
72987
|
+
return Boolean(isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && REACT_HANDLER_PROP_PATTERN.test(attribute.name.name));
|
|
72988
|
+
};
|
|
72989
|
+
const isGlobalDeferredFunctionCall = (callExpression, context) => {
|
|
72990
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
72991
|
+
if (isNodeOfType(callee, "Identifier") && TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES.has(callee.name)) return context.scopes.isGlobalReference(callee);
|
|
72992
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
72993
|
+
const methodName = getStaticPropertyName(callee);
|
|
72994
|
+
if (!methodName || !TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES.has(methodName)) return false;
|
|
72995
|
+
const receiver = stripParenExpression(callee.object);
|
|
72996
|
+
return Boolean(isNodeOfType(receiver, "Identifier") && (receiver.name === "globalThis" || receiver.name === "window") && context.scopes.isGlobalReference(receiver));
|
|
72997
|
+
};
|
|
72998
|
+
const isDeferredCallbackArgument = (expression, context) => {
|
|
72999
|
+
const parent = expression.parent;
|
|
73000
|
+
if (isNodeOfType(parent, "NewExpression")) {
|
|
73001
|
+
const callee = stripParenExpression(parent.callee);
|
|
73002
|
+
return Boolean(parent.arguments?.[0] === expression && isNodeOfType(callee, "Identifier") && DEFERRED_CONSTRUCTOR_NAMES.has(callee.name) && context.scopes.isGlobalReference(callee));
|
|
73003
|
+
}
|
|
73004
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
73005
|
+
if (!(parent.arguments ?? []).some((argument) => argument === expression)) return false;
|
|
73006
|
+
if (parent.arguments?.[0] === expression && isReactApiCall(parent, DEFERRED_REACT_HOOK_NAMES, context.scopes, {
|
|
73007
|
+
allowGlobalReactNamespace: true,
|
|
73008
|
+
resolveNamedAliases: true
|
|
73009
|
+
})) return true;
|
|
73010
|
+
if (parent.arguments?.[0] === expression && isGlobalDeferredFunctionCall(parent, context)) return true;
|
|
73011
|
+
const callee = stripParenExpression(parent.callee);
|
|
73012
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
73013
|
+
const methodName = getStaticPropertyName(callee);
|
|
73014
|
+
return Boolean(methodName && (SUBSCRIPTION_METHOD_NAMES.has(methodName) || PROMISE_CONTINUATION_METHOD_NAMES.has(methodName)));
|
|
73015
|
+
};
|
|
73016
|
+
const isReactEffectCallbackValue = (expression, context) => {
|
|
73017
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
73018
|
+
const parent = expressionRoot.parent;
|
|
73019
|
+
return Boolean(isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === expressionRoot && isReactApiCall(parent, DEFERRED_REACT_HOOK_NAMES, context.scopes, {
|
|
73020
|
+
allowGlobalReactNamespace: true,
|
|
73021
|
+
resolveNamedAliases: true
|
|
73022
|
+
}));
|
|
73023
|
+
};
|
|
73024
|
+
const isSymbolUsedAsReactEffectCallback = (symbol, context, visitedSymbolIds) => {
|
|
73025
|
+
if (visitedSymbolIds.has(symbol.id)) return false;
|
|
73026
|
+
visitedSymbolIds.add(symbol.id);
|
|
73027
|
+
return symbol.references.some((reference) => {
|
|
73028
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
73029
|
+
if (isReactEffectCallbackValue(referenceRoot, context)) return true;
|
|
73030
|
+
const aliasSymbol = getConstAliasSymbol(referenceRoot, context);
|
|
73031
|
+
return Boolean(aliasSymbol && isSymbolUsedAsReactEffectCallback(aliasSymbol, context, visitedSymbolIds));
|
|
73032
|
+
});
|
|
73033
|
+
};
|
|
73034
|
+
const isFunctionUsedAsReactEffectCallback = (functionNode, context) => {
|
|
73035
|
+
if (isReactEffectCallbackValue(functionNode, context)) return true;
|
|
73036
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
73037
|
+
const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
|
|
73038
|
+
return Boolean(symbol && isSymbolUsedAsReactEffectCallback(symbol, context, /* @__PURE__ */ new Set()));
|
|
73039
|
+
};
|
|
73040
|
+
const isReactEffectCleanupValue = (expression, context) => {
|
|
73041
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
73042
|
+
const returnStatement = expressionRoot.parent;
|
|
73043
|
+
if (isNodeOfType(returnStatement, "ArrowFunctionExpression") && returnStatement.body === expressionRoot) return isFunctionUsedAsReactEffectCallback(returnStatement, context);
|
|
73044
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement") || returnStatement.argument !== expressionRoot) return false;
|
|
73045
|
+
const effectCallback = findEnclosingFunction$1(returnStatement);
|
|
73046
|
+
return Boolean(effectCallback && isFunctionUsedAsReactEffectCallback(effectCallback, context));
|
|
73047
|
+
};
|
|
73048
|
+
const isDeferredCallbackValue = (expression, context) => isJsxEventHandlerValue(expression) || isDeferredCallbackArgument(expression, context) || isReactEffectCleanupValue(expression, context);
|
|
73049
|
+
const getConstAliasSymbol = (expression, context) => {
|
|
73050
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
73051
|
+
const declarator = expressionRoot.parent;
|
|
73052
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== expressionRoot || !isNodeOfType(declarator.id, "Identifier")) return null;
|
|
73053
|
+
const aliasSymbol = context.scopes.symbolFor(declarator.id);
|
|
73054
|
+
return aliasSymbol?.kind === "const" ? aliasSymbol : null;
|
|
73055
|
+
};
|
|
73056
|
+
const isCallTarget = (expression) => {
|
|
73057
|
+
const parent = expression.parent;
|
|
73058
|
+
return isNodeOfType(parent, "CallExpression") && parent.callee === expression ? parent : null;
|
|
73059
|
+
};
|
|
73060
|
+
const getSynchronousCallbackInvocation = (expression, context) => {
|
|
73061
|
+
if (!executesDuringRender(expression, context.scopes)) return null;
|
|
73062
|
+
const parent = expression.parent;
|
|
73063
|
+
return isNodeOfType(parent, "CallExpression") || isNodeOfType(parent, "NewExpression") ? parent : null;
|
|
73064
|
+
};
|
|
73065
|
+
const isNodeExecutedFromDeferredCallback = (node, snapshotOwner, context, visitedFunctionSymbolIds) => {
|
|
73066
|
+
const enclosingFunction = findEnclosingFunction$1(node);
|
|
73067
|
+
if (!enclosingFunction || enclosingFunction === snapshotOwner) return false;
|
|
73068
|
+
return isFunctionExecutedAsDeferredCallback(enclosingFunction, snapshotOwner, context, visitedFunctionSymbolIds);
|
|
73069
|
+
};
|
|
73070
|
+
const isSymbolExecutedAsDeferredCallback = (symbol, snapshotOwner, context, visitedFunctionSymbolIds) => {
|
|
73071
|
+
if (visitedFunctionSymbolIds.has(symbol.id)) return false;
|
|
73072
|
+
visitedFunctionSymbolIds.add(symbol.id);
|
|
73073
|
+
return symbol.references.some((reference) => {
|
|
73074
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
73075
|
+
if (isDeferredCallbackValue(referenceRoot, context)) return true;
|
|
73076
|
+
const aliasSymbol = getConstAliasSymbol(referenceRoot, context);
|
|
73077
|
+
if (aliasSymbol && isSymbolExecutedAsDeferredCallback(aliasSymbol, snapshotOwner, context, visitedFunctionSymbolIds)) return true;
|
|
73078
|
+
const callTarget = isCallTarget(referenceRoot);
|
|
73079
|
+
if (callTarget) return isNodeExecutedFromDeferredCallback(callTarget, snapshotOwner, context, visitedFunctionSymbolIds);
|
|
73080
|
+
const callbackInvocation = getSynchronousCallbackInvocation(referenceRoot, context);
|
|
73081
|
+
return Boolean(callbackInvocation && isNodeExecutedFromDeferredCallback(callbackInvocation, snapshotOwner, context, visitedFunctionSymbolIds));
|
|
73082
|
+
});
|
|
73083
|
+
};
|
|
73084
|
+
const isFunctionExecutedAsDeferredCallback = (functionNode, snapshotOwner, context, visitedFunctionSymbolIds) => {
|
|
73085
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
73086
|
+
if (isDeferredCallbackValue(functionRoot, context)) return true;
|
|
73087
|
+
const synchronousInvocation = getSynchronousCallbackInvocation(functionRoot, context);
|
|
73088
|
+
if (synchronousInvocation) return isNodeExecutedFromDeferredCallback(synchronousInvocation, snapshotOwner, context, visitedFunctionSymbolIds);
|
|
73089
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
73090
|
+
if (!bindingIdentifier) return false;
|
|
73091
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
73092
|
+
return Boolean(symbol && isSymbolExecutedAsDeferredCallback(symbol, snapshotOwner, context, visitedFunctionSymbolIds));
|
|
73093
|
+
};
|
|
73094
|
+
const isDirectSnapshotAliasInitializer = (identifier) => {
|
|
73095
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
73096
|
+
const parent = expressionRoot.parent;
|
|
73097
|
+
return Boolean(isNodeOfType(parent, "VariableDeclarator") && parent.init === expressionRoot && isNodeOfType(parent.id, "Identifier"));
|
|
73098
|
+
};
|
|
73099
|
+
const valtioNoSnapshotInCallback = defineRule({
|
|
73100
|
+
id: "valtio-no-snapshot-in-callback",
|
|
73101
|
+
title: "Valtio snapshot read in a callback",
|
|
73102
|
+
severity: "warn",
|
|
73103
|
+
requires: ["valtio", "valtio:1"],
|
|
73104
|
+
recommendation: "Read from the original Valtio proxy inside callbacks. `useSnapshot()` results are for render reads; callback reads can become tracked render dependencies and cause extra re-renders.",
|
|
73105
|
+
create: (context) => ({ Identifier(node) {
|
|
73106
|
+
const reference = context.scopes.referenceFor(node);
|
|
73107
|
+
if (!reference || reference.flag === "write") return;
|
|
73108
|
+
const snapshotOriginCall = getSnapshotOriginCall(node, context);
|
|
73109
|
+
if (!snapshotOriginCall || isDirectSnapshotAliasInitializer(node)) return;
|
|
73110
|
+
const snapshotOwner = findEnclosingFunction$1(snapshotOriginCall);
|
|
73111
|
+
if (!snapshotOwner) return;
|
|
73112
|
+
const referenceOwner = findEnclosingFunction$1(node);
|
|
73113
|
+
if (!referenceOwner || referenceOwner === snapshotOwner) return;
|
|
73114
|
+
if (!isFunctionExecutedAsDeferredCallback(referenceOwner, snapshotOwner, context, /* @__PURE__ */ new Set())) return;
|
|
73115
|
+
context.report({
|
|
73116
|
+
node,
|
|
73117
|
+
message: "This callback reads a Valtio snapshot. Read the original proxy instead so callback-only fields do not become tracked render dependencies."
|
|
73118
|
+
});
|
|
73119
|
+
} })
|
|
73120
|
+
});
|
|
73121
|
+
//#endregion
|
|
71515
73122
|
//#region src/plugin/rules/react-builtins/void-dom-elements-no-children.ts
|
|
71516
73123
|
const VOID_DOM_ELEMENTS = new Set([
|
|
71517
73124
|
"area",
|
|
@@ -72045,6 +73652,453 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
72045
73652
|
})
|
|
72046
73653
|
});
|
|
72047
73654
|
//#endregion
|
|
73655
|
+
//#region src/plugin/utils/function-returns-collection-at-path.ts
|
|
73656
|
+
const resolveImmutableInitializer = (expression, scopes, visitedSymbolIds) => {
|
|
73657
|
+
const candidate = stripParenExpression(expression);
|
|
73658
|
+
if (!isNodeOfType(candidate, "Identifier")) return candidate;
|
|
73659
|
+
const symbol = scopes.symbolFor(candidate);
|
|
73660
|
+
if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read")) return candidate;
|
|
73661
|
+
visitedSymbolIds.add(symbol.id);
|
|
73662
|
+
return resolveImmutableInitializer(symbol.initializer, scopes, visitedSymbolIds);
|
|
73663
|
+
};
|
|
73664
|
+
const expressionAtPropertyPath = (expression, propertyPath, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
73665
|
+
const candidate = resolveImmutableInitializer(expression, scopes, visitedSymbolIds);
|
|
73666
|
+
const [propertyName, ...remainingPath] = propertyPath;
|
|
73667
|
+
if (!propertyName) return candidate;
|
|
73668
|
+
if (!isNodeOfType(candidate, "ObjectExpression")) return null;
|
|
73669
|
+
for (const property of candidate.properties) {
|
|
73670
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
73671
|
+
if (getStaticPropertyKeyName(property) !== propertyName) continue;
|
|
73672
|
+
return expressionAtPropertyPath(property.value, remainingPath, scopes, visitedSymbolIds);
|
|
73673
|
+
}
|
|
73674
|
+
return null;
|
|
73675
|
+
};
|
|
73676
|
+
const isGlobalConstructor = (expression, constructorNames, scopes) => {
|
|
73677
|
+
const candidate = stripParenExpression(expression);
|
|
73678
|
+
return isNodeOfType(candidate, "Identifier") && constructorNames.has(candidate.name) && scopes.isGlobalReference(candidate);
|
|
73679
|
+
};
|
|
73680
|
+
const isCollectionExpression = (expression, collectionKind, scopes) => {
|
|
73681
|
+
const candidate = stripParenExpression(expression);
|
|
73682
|
+
if (collectionKind === "array" && isNodeOfType(candidate, "ArrayExpression")) return true;
|
|
73683
|
+
if (!isNodeOfType(candidate, "NewExpression")) return false;
|
|
73684
|
+
return isGlobalConstructor(candidate.callee, collectionKind === "array" ? new Set(["Array"]) : new Set(["Map", "Set"]), scopes);
|
|
73685
|
+
};
|
|
73686
|
+
const functionReturnsCollectionAtPath = ({ collectionKind, functionNode, propertyPath, scopes }) => {
|
|
73687
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
73688
|
+
const returnedExpressions = isNodeOfType(functionNode.body, "BlockStatement") ? collectFunctionReturnStatements(functionNode).flatMap((returnStatement) => returnStatement.argument ? [returnStatement.argument] : []) : [functionNode.body];
|
|
73689
|
+
return returnedExpressions.length > 0 && returnedExpressions.every((returnedExpression) => {
|
|
73690
|
+
const collectionExpression = expressionAtPropertyPath(returnedExpression, propertyPath, scopes);
|
|
73691
|
+
return Boolean(collectionExpression && isCollectionExpression(collectionExpression, collectionKind, scopes));
|
|
73692
|
+
});
|
|
73693
|
+
};
|
|
73694
|
+
//#endregion
|
|
73695
|
+
//#region src/plugin/utils/resolve-zustand-api.ts
|
|
73696
|
+
const ZUSTAND_APIS_BY_MODULE = new Map([
|
|
73697
|
+
["zustand", new Set([
|
|
73698
|
+
"create",
|
|
73699
|
+
"createStore",
|
|
73700
|
+
"useStore"
|
|
73701
|
+
])],
|
|
73702
|
+
["zustand/vanilla", new Set(["createStore"])],
|
|
73703
|
+
["zustand/react", new Set(["useStore"])],
|
|
73704
|
+
["zustand/traditional", new Set(["createWithEqualityFn", "useStoreWithEqualityFn"])],
|
|
73705
|
+
["zustand/shallow", new Set(["shallow", "useShallow"])],
|
|
73706
|
+
["zustand/react/shallow", new Set(["useShallow"])],
|
|
73707
|
+
["zustand/middleware", new Set([
|
|
73708
|
+
"combine",
|
|
73709
|
+
"createJSONStorage",
|
|
73710
|
+
"devtools",
|
|
73711
|
+
"persist",
|
|
73712
|
+
"redux",
|
|
73713
|
+
"subscribeWithSelector"
|
|
73714
|
+
])],
|
|
73715
|
+
["zustand/middleware/immer", new Set(["immer"])]
|
|
73716
|
+
]);
|
|
73717
|
+
const STATE_CREATOR_MIDDLEWARE_ARGUMENT_INDEX = new Map([
|
|
73718
|
+
["combine", 1],
|
|
73719
|
+
["devtools", 0],
|
|
73720
|
+
["immer", 0],
|
|
73721
|
+
["persist", 0],
|
|
73722
|
+
["subscribeWithSelector", 0]
|
|
73723
|
+
]);
|
|
73724
|
+
const toZustandApiName = (value) => {
|
|
73725
|
+
switch (value) {
|
|
73726
|
+
case "combine":
|
|
73727
|
+
case "create":
|
|
73728
|
+
case "createJSONStorage":
|
|
73729
|
+
case "createStore":
|
|
73730
|
+
case "createWithEqualityFn":
|
|
73731
|
+
case "devtools":
|
|
73732
|
+
case "immer":
|
|
73733
|
+
case "persist":
|
|
73734
|
+
case "redux":
|
|
73735
|
+
case "shallow":
|
|
73736
|
+
case "subscribeWithSelector":
|
|
73737
|
+
case "useShallow":
|
|
73738
|
+
case "useStore":
|
|
73739
|
+
case "useStoreWithEqualityFn": return value;
|
|
73740
|
+
default: return null;
|
|
73741
|
+
}
|
|
73742
|
+
};
|
|
73743
|
+
const getImportSource = (symbol) => {
|
|
73744
|
+
const declaration = getImportDeclarationForSymbol(symbol);
|
|
73745
|
+
if (!declaration || isTypeOnlyImport(declaration) || isNodeOfType(symbol.declarationNode, "ImportSpecifier") && symbol.declarationNode.importKind === "type") return null;
|
|
73746
|
+
return typeof declaration.source.value === "string" ? declaration.source.value : null;
|
|
73747
|
+
};
|
|
73748
|
+
const bindingFromImportSymbol = (symbol) => {
|
|
73749
|
+
const moduleSource = getImportSource(symbol);
|
|
73750
|
+
const supportedApiNames = moduleSource ? ZUSTAND_APIS_BY_MODULE.get(moduleSource) : null;
|
|
73751
|
+
if (!moduleSource || !supportedApiNames) return null;
|
|
73752
|
+
if (moduleSource === "zustand" && isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier")) return {
|
|
73753
|
+
apiName: "create",
|
|
73754
|
+
moduleSource
|
|
73755
|
+
};
|
|
73756
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
73757
|
+
const apiName = importedName ? toZustandApiName(importedName) : null;
|
|
73758
|
+
return apiName && supportedApiNames.has(apiName) ? {
|
|
73759
|
+
apiName,
|
|
73760
|
+
moduleSource
|
|
73761
|
+
} : null;
|
|
73762
|
+
};
|
|
73763
|
+
const namespaceImportSource = (expression, scopes) => {
|
|
73764
|
+
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
73765
|
+
const symbol = resolveConstIdentifierAlias(expression, scopes);
|
|
73766
|
+
if (!symbol || !isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")) return null;
|
|
73767
|
+
return getImportSource(symbol);
|
|
73768
|
+
};
|
|
73769
|
+
const resolveZustandApiBinding = (expression, scopes) => {
|
|
73770
|
+
const candidate = stripParenExpression(expression);
|
|
73771
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
73772
|
+
const symbol = resolveConstIdentifierAlias(candidate, scopes);
|
|
73773
|
+
if (!symbol) return null;
|
|
73774
|
+
if (symbol.kind === "import") return bindingFromImportSymbol(symbol);
|
|
73775
|
+
if (symbol.kind !== "const" || !symbol.initializer) return null;
|
|
73776
|
+
return resolveZustandApiBinding(symbol.initializer, scopes);
|
|
73777
|
+
}
|
|
73778
|
+
if (!isNodeOfType(candidate, "MemberExpression")) return null;
|
|
73779
|
+
const propertyName = getStaticPropertyName(candidate);
|
|
73780
|
+
const moduleSource = namespaceImportSource(stripParenExpression(candidate.object), scopes);
|
|
73781
|
+
const supportedApiNames = moduleSource ? ZUSTAND_APIS_BY_MODULE.get(moduleSource) : null;
|
|
73782
|
+
const apiName = propertyName ? toZustandApiName(propertyName) : null;
|
|
73783
|
+
return apiName && moduleSource && supportedApiNames?.has(apiName) ? {
|
|
73784
|
+
apiName,
|
|
73785
|
+
moduleSource
|
|
73786
|
+
} : null;
|
|
73787
|
+
};
|
|
73788
|
+
const resolveStateCreator = (expression, scopes, middlewareNames, visitedExpressions = /* @__PURE__ */ new Set()) => {
|
|
73789
|
+
const candidate = stripParenExpression(expression);
|
|
73790
|
+
if (visitedExpressions.has(candidate)) return null;
|
|
73791
|
+
visitedExpressions.add(candidate);
|
|
73792
|
+
const creatorFunction = resolveExactLocalFunction(expression, scopes);
|
|
73793
|
+
if (isFunctionLike$1(creatorFunction)) return creatorFunction;
|
|
73794
|
+
if (!isNodeOfType(candidate, "CallExpression")) return null;
|
|
73795
|
+
const middleware = resolveZustandApiBinding(candidate.callee, scopes);
|
|
73796
|
+
const creatorArgumentIndex = middleware ? STATE_CREATOR_MIDDLEWARE_ARGUMENT_INDEX.get(middleware.apiName) : null;
|
|
73797
|
+
if (!middleware || creatorArgumentIndex === void 0 || creatorArgumentIndex === null) return null;
|
|
73798
|
+
const creatorArgument = candidate.arguments[creatorArgumentIndex];
|
|
73799
|
+
if (!creatorArgument || isNodeOfType(creatorArgument, "SpreadElement")) return null;
|
|
73800
|
+
middlewareNames.add(middleware.apiName);
|
|
73801
|
+
return resolveStateCreator(creatorArgument, scopes, middlewareNames, visitedExpressions);
|
|
73802
|
+
};
|
|
73803
|
+
const isStoreFactoryApi = (binding) => binding?.apiName === "create" || binding?.apiName === "createStore" || binding?.apiName === "createWithEqualityFn";
|
|
73804
|
+
const resolveCurriedStoreFactoryBinding = (expression, scopes) => {
|
|
73805
|
+
const candidate = stripParenExpression(expression);
|
|
73806
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
73807
|
+
const symbol = resolveConstIdentifierAlias(candidate, scopes);
|
|
73808
|
+
if (symbol?.kind !== "const" || !symbol.initializer) return null;
|
|
73809
|
+
return resolveCurriedStoreFactoryBinding(symbol.initializer, scopes);
|
|
73810
|
+
}
|
|
73811
|
+
if (!isNodeOfType(candidate, "CallExpression") || candidate.arguments.length > 0) return null;
|
|
73812
|
+
const factoryBinding = resolveZustandApiBinding(candidate.callee, scopes);
|
|
73813
|
+
return isStoreFactoryApi(factoryBinding) ? factoryBinding : null;
|
|
73814
|
+
};
|
|
73815
|
+
const resolveZustandStoreCreator = (callExpression, scopes) => {
|
|
73816
|
+
const factoryCall = resolveZustandStoreFactoryCall(callExpression, scopes);
|
|
73817
|
+
if (!factoryCall) return null;
|
|
73818
|
+
const middlewareNames = /* @__PURE__ */ new Set();
|
|
73819
|
+
const creatorFunction = resolveStateCreator(factoryCall.creatorArgument, scopes, middlewareNames);
|
|
73820
|
+
if (!creatorFunction) return null;
|
|
73821
|
+
return {
|
|
73822
|
+
creatorFunction,
|
|
73823
|
+
factoryApiName: factoryCall.factoryApiName,
|
|
73824
|
+
middlewareNames
|
|
73825
|
+
};
|
|
73826
|
+
};
|
|
73827
|
+
const resolveZustandStoreFactoryCall = (callExpression, scopes) => {
|
|
73828
|
+
let factoryBinding = resolveZustandApiBinding(callExpression.callee, scopes);
|
|
73829
|
+
if (!isStoreFactoryApi(factoryBinding)) {
|
|
73830
|
+
factoryBinding = resolveCurriedStoreFactoryBinding(callExpression.callee, scopes);
|
|
73831
|
+
if (!isStoreFactoryApi(factoryBinding)) return null;
|
|
73832
|
+
}
|
|
73833
|
+
const creatorArgument = callExpression.arguments[0];
|
|
73834
|
+
if (!creatorArgument || isNodeOfType(creatorArgument, "SpreadElement")) return null;
|
|
73835
|
+
return {
|
|
73836
|
+
callExpression,
|
|
73837
|
+
creatorArgument,
|
|
73838
|
+
factoryApiName: factoryBinding.apiName
|
|
73839
|
+
};
|
|
73840
|
+
};
|
|
73841
|
+
//#endregion
|
|
73842
|
+
//#region src/plugin/rules/state-and-effects/zustand-no-fresh-selector-result.ts
|
|
73843
|
+
const ALLOCATING_ARRAY_METHODS = new Set([
|
|
73844
|
+
"filter",
|
|
73845
|
+
"flat",
|
|
73846
|
+
"flatMap",
|
|
73847
|
+
"map",
|
|
73848
|
+
"toReversed",
|
|
73849
|
+
"toSorted",
|
|
73850
|
+
"toSpliced",
|
|
73851
|
+
"with"
|
|
73852
|
+
]);
|
|
73853
|
+
const SAME_REFERENCE_ARRAY_METHODS = new Set(["reverse", "sort"]);
|
|
73854
|
+
const ALLOCATING_NAMESPACE_METHOD_KINDS = new Map([["Array", new Map([["from", "array"], ["of", "array"]])], ["Object", new Map([
|
|
73855
|
+
["create", "object"],
|
|
73856
|
+
["entries", "array"],
|
|
73857
|
+
["fromEntries", "object"],
|
|
73858
|
+
["keys", "array"],
|
|
73859
|
+
["values", "array"]
|
|
73860
|
+
])]]);
|
|
73861
|
+
const isNullishEqualityArgument = (argument, scopes) => {
|
|
73862
|
+
const candidate = stripParenExpression(argument);
|
|
73863
|
+
return isNodeOfType(candidate, "Identifier") && candidate.name === "undefined" && scopes.isGlobalReference(candidate) || isNodeOfType(candidate, "Literal") && candidate.value === null || isNodeOfType(candidate, "UnaryExpression") && candidate.operator === "void";
|
|
73864
|
+
};
|
|
73865
|
+
const hasExplicitEqualityArgument = (argumentsList, equalityArgumentIndex, scopes) => {
|
|
73866
|
+
const equalityArgument = argumentsList[equalityArgumentIndex];
|
|
73867
|
+
if (!equalityArgument) return false;
|
|
73868
|
+
if (isNodeOfType(equalityArgument, "SpreadElement")) return true;
|
|
73869
|
+
return !isNullishEqualityArgument(equalityArgument, scopes);
|
|
73870
|
+
};
|
|
73871
|
+
const resolveZustandStoreCreation = (expression, scopes) => {
|
|
73872
|
+
const candidate = stripParenExpression(expression);
|
|
73873
|
+
if (!isNodeOfType(candidate, "CallExpression")) return null;
|
|
73874
|
+
const factoryCall = resolveZustandStoreFactoryCall(candidate, scopes);
|
|
73875
|
+
if (!factoryCall || factoryCall.factoryApiName !== "create" && factoryCall.factoryApiName !== "createWithEqualityFn") return null;
|
|
73876
|
+
return {
|
|
73877
|
+
creatorFunction: resolveZustandStoreCreator(candidate, scopes)?.creatorFunction ?? null,
|
|
73878
|
+
hasDefaultEquality: factoryCall.factoryApiName === "createWithEqualityFn" && hasExplicitEqualityArgument(candidate.arguments, 1, scopes),
|
|
73879
|
+
supportsEqualityArgument: factoryCall.factoryApiName === "createWithEqualityFn"
|
|
73880
|
+
};
|
|
73881
|
+
};
|
|
73882
|
+
const resolveZustandBoundStore = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
73883
|
+
const candidate = stripParenExpression(expression);
|
|
73884
|
+
if (!isNodeOfType(candidate, "Identifier")) return null;
|
|
73885
|
+
const symbol = scopes.symbolFor(candidate);
|
|
73886
|
+
if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
73887
|
+
visitedSymbolIds.add(symbol.id);
|
|
73888
|
+
return resolveZustandStoreCreation(symbol.initializer, scopes) ?? resolveZustandBoundStore(symbol.initializer, scopes, visitedSymbolIds);
|
|
73889
|
+
};
|
|
73890
|
+
const getZustandSelectorCall = (callExpression, scopes) => {
|
|
73891
|
+
const apiBinding = resolveZustandApiBinding(callExpression.callee, scopes);
|
|
73892
|
+
if (apiBinding?.apiName === "useStore" || apiBinding?.apiName === "useStoreWithEqualityFn") {
|
|
73893
|
+
const selector = callExpression.arguments[1];
|
|
73894
|
+
if (!selector || isNodeOfType(selector, "SpreadElement")) return null;
|
|
73895
|
+
if (apiBinding.apiName === "useStoreWithEqualityFn" && hasExplicitEqualityArgument(callExpression.arguments, 2, scopes)) return null;
|
|
73896
|
+
return {
|
|
73897
|
+
selector,
|
|
73898
|
+
storeCreatorFunction: null
|
|
73899
|
+
};
|
|
73900
|
+
}
|
|
73901
|
+
const boundStore = resolveZustandBoundStore(callExpression.callee, scopes);
|
|
73902
|
+
const selector = callExpression.arguments[0];
|
|
73903
|
+
if (!boundStore || !selector || isNodeOfType(selector, "SpreadElement")) return null;
|
|
73904
|
+
if (boundStore.hasDefaultEquality || boundStore.supportsEqualityArgument && hasExplicitEqualityArgument(callExpression.arguments, 1, scopes)) return null;
|
|
73905
|
+
return {
|
|
73906
|
+
selector,
|
|
73907
|
+
storeCreatorFunction: boundStore.creatorFunction
|
|
73908
|
+
};
|
|
73909
|
+
};
|
|
73910
|
+
const isUseShallowCall = (callExpression, scopes) => resolveZustandApiBinding(callExpression.callee, scopes)?.apiName === "useShallow";
|
|
73911
|
+
const resolveSelectorFunction = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
73912
|
+
const candidate = stripParenExpression(expression);
|
|
73913
|
+
if (isFunctionLike$1(candidate)) return candidate;
|
|
73914
|
+
if (isNodeOfType(candidate, "CallExpression")) {
|
|
73915
|
+
if (isUseShallowCall(candidate, scopes)) return null;
|
|
73916
|
+
if (!isReactApiCall(candidate, "useCallback", scopes, {
|
|
73917
|
+
allowGlobalReactNamespace: true,
|
|
73918
|
+
resolveNamedAliases: true
|
|
73919
|
+
})) return null;
|
|
73920
|
+
const callback = candidate.arguments[0];
|
|
73921
|
+
if (!callback || isNodeOfType(callback, "SpreadElement")) return null;
|
|
73922
|
+
return resolveSelectorFunction(callback, scopes, visitedSymbolIds);
|
|
73923
|
+
}
|
|
73924
|
+
if (!isNodeOfType(candidate, "Identifier")) return null;
|
|
73925
|
+
const symbol = scopes.symbolFor(candidate);
|
|
73926
|
+
if (!symbol || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
73927
|
+
if (symbol.kind === "function" && isFunctionLike$1(symbol.declarationNode)) return symbol.declarationNode;
|
|
73928
|
+
if (symbol.kind !== "const" || !symbol.initializer) return null;
|
|
73929
|
+
visitedSymbolIds.add(symbol.id);
|
|
73930
|
+
return resolveSelectorFunction(symbol.initializer, scopes, visitedSymbolIds);
|
|
73931
|
+
};
|
|
73932
|
+
const selectorStatePropertyPath = (expression, analysis, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
73933
|
+
const candidate = stripParenExpression(expression);
|
|
73934
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
73935
|
+
const symbol = analysis.scopes.symbolFor(candidate);
|
|
73936
|
+
const selectorParameter = isFunctionLike$1(analysis.selectorFunction) ? analysis.selectorFunction.params[0] : null;
|
|
73937
|
+
if (selectorParameter && isNodeOfType(selectorParameter, "Identifier") && symbol?.id === analysis.scopes.symbolFor(selectorParameter)?.id) return [];
|
|
73938
|
+
if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
73939
|
+
visitedSymbolIds.add(symbol.id);
|
|
73940
|
+
return selectorStatePropertyPath(symbol.initializer, analysis, visitedSymbolIds);
|
|
73941
|
+
}
|
|
73942
|
+
if (!isNodeOfType(candidate, "MemberExpression")) return null;
|
|
73943
|
+
const propertyName = getStaticPropertyName(candidate);
|
|
73944
|
+
const objectPath = selectorStatePropertyPath(candidate.object, analysis, visitedSymbolIds);
|
|
73945
|
+
return propertyName && objectPath ? [...objectPath, propertyName] : null;
|
|
73946
|
+
};
|
|
73947
|
+
const freshResultFromAllocatingCall = (expression, analysis, visitedSymbolIds) => {
|
|
73948
|
+
const callee = stripParenExpression(expression.callee);
|
|
73949
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
73950
|
+
const methodName = getStaticPropertyName(callee);
|
|
73951
|
+
if (!methodName) return null;
|
|
73952
|
+
const receiver = stripParenExpression(callee.object);
|
|
73953
|
+
if (isNodeOfType(receiver, "Identifier") && analysis.scopes.isGlobalReference(receiver)) {
|
|
73954
|
+
const resultKind = ALLOCATING_NAMESPACE_METHOD_KINDS.get(receiver.name)?.get(methodName);
|
|
73955
|
+
if (resultKind) return {
|
|
73956
|
+
kind: resultKind,
|
|
73957
|
+
node: expression
|
|
73958
|
+
};
|
|
73959
|
+
if (receiver.name === "Object" && methodName === "assign") {
|
|
73960
|
+
const target = expression.arguments[0];
|
|
73961
|
+
if (!target || isNodeOfType(target, "SpreadElement")) return null;
|
|
73962
|
+
const freshTarget = resolveFreshSelectorResult(target, analysis, new Set(visitedSymbolIds));
|
|
73963
|
+
return freshTarget ? {
|
|
73964
|
+
kind: freshTarget.kind,
|
|
73965
|
+
node: expression
|
|
73966
|
+
} : null;
|
|
73967
|
+
}
|
|
73968
|
+
}
|
|
73969
|
+
if (ALLOCATING_ARRAY_METHODS.has(methodName)) {
|
|
73970
|
+
if (resolveFreshSelectorResult(receiver, analysis, new Set(visitedSymbolIds))?.kind === "array") return {
|
|
73971
|
+
kind: "array",
|
|
73972
|
+
node: expression
|
|
73973
|
+
};
|
|
73974
|
+
const receiverPath = selectorStatePropertyPath(receiver, analysis);
|
|
73975
|
+
if (receiverPath && analysis.storeCreatorFunction && functionReturnsCollectionAtPath({
|
|
73976
|
+
collectionKind: "array",
|
|
73977
|
+
functionNode: analysis.storeCreatorFunction,
|
|
73978
|
+
propertyPath: receiverPath,
|
|
73979
|
+
scopes: analysis.scopes
|
|
73980
|
+
})) return {
|
|
73981
|
+
kind: "array",
|
|
73982
|
+
node: expression
|
|
73983
|
+
};
|
|
73984
|
+
return null;
|
|
73985
|
+
}
|
|
73986
|
+
if (!SAME_REFERENCE_ARRAY_METHODS.has(methodName)) return null;
|
|
73987
|
+
return resolveFreshSelectorResult(receiver, analysis, new Set(visitedSymbolIds)) ? {
|
|
73988
|
+
kind: "array",
|
|
73989
|
+
node: expression
|
|
73990
|
+
} : null;
|
|
73991
|
+
};
|
|
73992
|
+
const resolveFreshSelectorResult = (expression, analysis, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
73993
|
+
const freshRenderValue = resolveFreshRenderValue(expression, analysis.scopes);
|
|
73994
|
+
if (freshRenderValue?.kind === "array" || freshRenderValue?.kind === "function" || freshRenderValue?.kind === "instance" || freshRenderValue?.kind === "object") return {
|
|
73995
|
+
kind: freshRenderValue.kind,
|
|
73996
|
+
node: expression
|
|
73997
|
+
};
|
|
73998
|
+
const candidate = stripParenExpression(expression);
|
|
73999
|
+
if (isNodeOfType(candidate, "CallExpression")) return freshResultFromAllocatingCall(candidate, analysis, visitedSymbolIds);
|
|
74000
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return resolveFreshSelectorResult(candidate.consequent, analysis, new Set(visitedSymbolIds)) ?? resolveFreshSelectorResult(candidate.alternate, analysis, new Set(visitedSymbolIds));
|
|
74001
|
+
if (isNodeOfType(candidate, "LogicalExpression")) {
|
|
74002
|
+
if (candidate.operator === "&&") return resolveFreshSelectorResult(candidate.right, analysis, visitedSymbolIds);
|
|
74003
|
+
return resolveFreshSelectorResult(candidate.left, analysis, new Set(visitedSymbolIds)) ?? resolveFreshSelectorResult(candidate.right, analysis, new Set(visitedSymbolIds));
|
|
74004
|
+
}
|
|
74005
|
+
if (isNodeOfType(candidate, "SequenceExpression")) {
|
|
74006
|
+
const returnedExpression = candidate.expressions[candidate.expressions.length - 1];
|
|
74007
|
+
return returnedExpression ? resolveFreshSelectorResult(returnedExpression, analysis, visitedSymbolIds) : null;
|
|
74008
|
+
}
|
|
74009
|
+
if (!isNodeOfType(candidate, "Identifier")) return null;
|
|
74010
|
+
const symbol = analysis.scopes.symbolFor(candidate);
|
|
74011
|
+
if (symbol?.kind !== "const" || symbol.scope.kind === "module" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
74012
|
+
visitedSymbolIds.add(symbol.id);
|
|
74013
|
+
return resolveFreshSelectorResult(symbol.initializer, analysis, visitedSymbolIds);
|
|
74014
|
+
};
|
|
74015
|
+
const findFreshSelectorReturn = (selectorFunction, scopes, storeCreatorFunction) => {
|
|
74016
|
+
if (!isFunctionLike$1(selectorFunction) || !selectorFunction.body) return null;
|
|
74017
|
+
const analysis = {
|
|
74018
|
+
scopes,
|
|
74019
|
+
selectorFunction,
|
|
74020
|
+
storeCreatorFunction
|
|
74021
|
+
};
|
|
74022
|
+
if (!isNodeOfType(selectorFunction.body, "BlockStatement")) return resolveFreshSelectorResult(selectorFunction.body, analysis);
|
|
74023
|
+
let freshResult = null;
|
|
74024
|
+
walkAst(selectorFunction.body, (candidate) => {
|
|
74025
|
+
if (freshResult) return false;
|
|
74026
|
+
if (candidate !== selectorFunction.body && isFunctionLike$1(candidate)) return false;
|
|
74027
|
+
if (!isNodeOfType(candidate, "ReturnStatement") || !candidate.argument) return;
|
|
74028
|
+
freshResult = resolveFreshSelectorResult(candidate.argument, analysis);
|
|
74029
|
+
return freshResult ? false : void 0;
|
|
74030
|
+
});
|
|
74031
|
+
return freshResult;
|
|
74032
|
+
};
|
|
74033
|
+
const zustandNoFreshSelectorResult = defineRule({
|
|
74034
|
+
id: "zustand-no-fresh-selector-result",
|
|
74035
|
+
title: "Zustand selector returns a fresh value",
|
|
74036
|
+
severity: "error",
|
|
74037
|
+
category: "Performance",
|
|
74038
|
+
requires: ["zustand", "zustand:5"],
|
|
74039
|
+
recommendation: "Select a stable store field, split the selector, or wrap a collection selector with `useShallow`.",
|
|
74040
|
+
create: (context) => ({ CallExpression(node) {
|
|
74041
|
+
const selectorCall = getZustandSelectorCall(node, context.scopes);
|
|
74042
|
+
if (!selectorCall) return;
|
|
74043
|
+
const selectorFunction = resolveSelectorFunction(selectorCall.selector, context.scopes);
|
|
74044
|
+
if (!selectorFunction) return;
|
|
74045
|
+
const freshResult = findFreshSelectorReturn(selectorFunction, context.scopes, selectorCall.storeCreatorFunction);
|
|
74046
|
+
if (!freshResult) return;
|
|
74047
|
+
context.report({
|
|
74048
|
+
node: freshResult.node,
|
|
74049
|
+
message: "This Zustand selector creates a new reference whenever the store is read, so Object.is never sees a stable snapshot and Zustand v5 can repeatedly render or hit maximum update depth. Select a stable field or use `useShallow`."
|
|
74050
|
+
});
|
|
74051
|
+
} })
|
|
74052
|
+
});
|
|
74053
|
+
//#endregion
|
|
74054
|
+
//#region src/plugin/rules/state-and-effects/zustand-no-whole-store-destructure.ts
|
|
74055
|
+
const BOUND_STORE_FACTORY_APIS = new Set(["create", "createWithEqualityFn"]);
|
|
74056
|
+
const VANILLA_STORE_FACTORY_APIS = new Set(["createStore"]);
|
|
74057
|
+
const isStoreFactoryValue = (rawValue, factoryApiNames, scopes) => {
|
|
74058
|
+
const value = stripParenExpression(rawValue);
|
|
74059
|
+
if (!isNodeOfType(value, "CallExpression")) return false;
|
|
74060
|
+
const factoryCall = resolveZustandStoreFactoryCall(value, scopes);
|
|
74061
|
+
return Boolean(factoryCall && factoryApiNames.has(factoryCall.factoryApiName));
|
|
74062
|
+
};
|
|
74063
|
+
const isStoreValue = (rawValue, factoryApiNames, scopes) => {
|
|
74064
|
+
const value = stripParenExpression(rawValue);
|
|
74065
|
+
if (isStoreFactoryValue(value, factoryApiNames, scopes)) return true;
|
|
74066
|
+
if (!isNodeOfType(value, "Identifier")) return false;
|
|
74067
|
+
const symbol = resolveConstIdentifierAlias(value, scopes);
|
|
74068
|
+
return Boolean(symbol?.kind === "const" && symbol.initializer && isStoreFactoryValue(symbol.initializer, factoryApiNames, scopes));
|
|
74069
|
+
};
|
|
74070
|
+
const isBoundStoreCall = (node, scopes) => {
|
|
74071
|
+
const callee = stripParenExpression(node.callee);
|
|
74072
|
+
return isNodeOfType(callee, "Identifier") && isStoreValue(callee, BOUND_STORE_FACTORY_APIS, scopes);
|
|
74073
|
+
};
|
|
74074
|
+
const isVanillaStoreHookCall = (node, scopes) => {
|
|
74075
|
+
if (node.arguments.length !== 1) return false;
|
|
74076
|
+
const hook = resolveZustandApiBinding(node.callee, scopes);
|
|
74077
|
+
if (hook?.apiName !== "useStore" && hook?.apiName !== "useStoreWithEqualityFn") return false;
|
|
74078
|
+
return isStoreValue(node.arguments[0], VANILLA_STORE_FACTORY_APIS, scopes);
|
|
74079
|
+
};
|
|
74080
|
+
const isDirectReactRenderCall = (node, context) => {
|
|
74081
|
+
const renderFunction = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
74082
|
+
if (!renderFunction || findEnclosingFunction$1(node) !== renderFunction) return false;
|
|
74083
|
+
const displayName = componentOrHookDisplayNameForFunction(renderFunction);
|
|
74084
|
+
return Boolean(displayName && (isReactHookName(displayName) || functionHasReactComponentEvidence(renderFunction, context.scopes, context.cfg)));
|
|
74085
|
+
};
|
|
74086
|
+
const zustandNoWholeStoreDestructure = defineRule({
|
|
74087
|
+
id: "zustand-no-whole-store-destructure",
|
|
74088
|
+
title: "Whole Zustand store subscribed during render",
|
|
74089
|
+
severity: "warn",
|
|
74090
|
+
requires: ["zustand:1"],
|
|
74091
|
+
recommendation: "Pass a selector to the Zustand hook so this component rerenders only when the state it reads changes.",
|
|
74092
|
+
create: (context) => ({ CallExpression(node) {
|
|
74093
|
+
if (!isDirectReactRenderCall(node, context)) return;
|
|
74094
|
+
if (!(node.arguments.length === 0 && isBoundStoreCall(node, context.scopes)) && !isVanillaStoreHookCall(node, context.scopes)) return;
|
|
74095
|
+
context.report({
|
|
74096
|
+
node,
|
|
74097
|
+
message: "This hook subscribes to the whole Zustand store, so every store update rerenders this component. Pass a selector for the state it reads."
|
|
74098
|
+
});
|
|
74099
|
+
} })
|
|
74100
|
+
});
|
|
74101
|
+
//#endregion
|
|
72048
74102
|
//#region src/plugin/rule-registry.ts
|
|
72049
74103
|
const reactDoctorRules = [
|
|
72050
74104
|
{
|
|
@@ -73359,6 +75413,39 @@ const reactDoctorRules = [
|
|
|
73359
75413
|
requires: [...new Set(["react", ...mediaHasCaption.requires ?? []])]
|
|
73360
75414
|
}
|
|
73361
75415
|
},
|
|
75416
|
+
{
|
|
75417
|
+
key: "react-doctor/mobx-no-make-auto-observable-in-inheritance",
|
|
75418
|
+
id: "mobx-no-make-auto-observable-in-inheritance",
|
|
75419
|
+
source: "react-doctor",
|
|
75420
|
+
originallyExternal: false,
|
|
75421
|
+
rule: {
|
|
75422
|
+
...mobxNoMakeAutoObservableInInheritance,
|
|
75423
|
+
framework: "global",
|
|
75424
|
+
category: "Bugs"
|
|
75425
|
+
}
|
|
75426
|
+
},
|
|
75427
|
+
{
|
|
75428
|
+
key: "react-doctor/mobx-no-observer-wrapped-memo",
|
|
75429
|
+
id: "mobx-no-observer-wrapped-memo",
|
|
75430
|
+
source: "react-doctor",
|
|
75431
|
+
originallyExternal: false,
|
|
75432
|
+
rule: {
|
|
75433
|
+
...mobxNoObserverWrappedMemo,
|
|
75434
|
+
framework: "global",
|
|
75435
|
+
category: "Bugs"
|
|
75436
|
+
}
|
|
75437
|
+
},
|
|
75438
|
+
{
|
|
75439
|
+
key: "react-doctor/mobx-reaction-disposer-discarded",
|
|
75440
|
+
id: "mobx-reaction-disposer-discarded",
|
|
75441
|
+
source: "react-doctor",
|
|
75442
|
+
originallyExternal: false,
|
|
75443
|
+
rule: {
|
|
75444
|
+
...mobxReactionDisposerDiscarded,
|
|
75445
|
+
framework: "global",
|
|
75446
|
+
category: "Bugs"
|
|
75447
|
+
}
|
|
75448
|
+
},
|
|
73362
75449
|
{
|
|
73363
75450
|
key: "react-doctor/mouse-events-have-key-events",
|
|
73364
75451
|
id: "mouse-events-have-key-events",
|
|
@@ -75541,6 +77628,105 @@ const reactDoctorRules = [
|
|
|
75541
77628
|
requires: [...new Set(["react", ...reduxUseselectorReturnsNewCollection.requires ?? []])]
|
|
75542
77629
|
}
|
|
75543
77630
|
},
|
|
77631
|
+
{
|
|
77632
|
+
key: "react-doctor/remotion-calculate-metadata-fetch-signal",
|
|
77633
|
+
id: "remotion-calculate-metadata-fetch-signal",
|
|
77634
|
+
source: "react-doctor",
|
|
77635
|
+
originallyExternal: false,
|
|
77636
|
+
rule: {
|
|
77637
|
+
...remotionCalculateMetadataFetchSignal,
|
|
77638
|
+
framework: "global",
|
|
77639
|
+
category: "Bugs"
|
|
77640
|
+
}
|
|
77641
|
+
},
|
|
77642
|
+
{
|
|
77643
|
+
key: "react-doctor/remotion-deterministic-randomness",
|
|
77644
|
+
id: "remotion-deterministic-randomness",
|
|
77645
|
+
source: "react-doctor",
|
|
77646
|
+
originallyExternal: false,
|
|
77647
|
+
rule: {
|
|
77648
|
+
...remotionDeterministicRandomness,
|
|
77649
|
+
framework: "global",
|
|
77650
|
+
category: "Bugs"
|
|
77651
|
+
}
|
|
77652
|
+
},
|
|
77653
|
+
{
|
|
77654
|
+
key: "react-doctor/remotion-no-css-animation",
|
|
77655
|
+
id: "remotion-no-css-animation",
|
|
77656
|
+
source: "react-doctor",
|
|
77657
|
+
originallyExternal: false,
|
|
77658
|
+
rule: {
|
|
77659
|
+
...remotionNoCssAnimation,
|
|
77660
|
+
framework: "global",
|
|
77661
|
+
category: "Bugs"
|
|
77662
|
+
}
|
|
77663
|
+
},
|
|
77664
|
+
{
|
|
77665
|
+
key: "react-doctor/remotion-no-css-transition",
|
|
77666
|
+
id: "remotion-no-css-transition",
|
|
77667
|
+
source: "react-doctor",
|
|
77668
|
+
originallyExternal: false,
|
|
77669
|
+
rule: {
|
|
77670
|
+
...remotionNoCssTransition,
|
|
77671
|
+
framework: "global",
|
|
77672
|
+
category: "Bugs"
|
|
77673
|
+
}
|
|
77674
|
+
},
|
|
77675
|
+
{
|
|
77676
|
+
key: "react-doctor/remotion-no-css-url-assets",
|
|
77677
|
+
id: "remotion-no-css-url-assets",
|
|
77678
|
+
source: "react-doctor",
|
|
77679
|
+
originallyExternal: false,
|
|
77680
|
+
rule: {
|
|
77681
|
+
...remotionNoCssUrlAssets,
|
|
77682
|
+
framework: "global",
|
|
77683
|
+
category: "Bugs"
|
|
77684
|
+
}
|
|
77685
|
+
},
|
|
77686
|
+
{
|
|
77687
|
+
key: "react-doctor/remotion-no-module-scope-delay-render",
|
|
77688
|
+
id: "remotion-no-module-scope-delay-render",
|
|
77689
|
+
source: "react-doctor",
|
|
77690
|
+
originallyExternal: false,
|
|
77691
|
+
rule: {
|
|
77692
|
+
...remotionNoModuleScopeDelayRender,
|
|
77693
|
+
framework: "global",
|
|
77694
|
+
category: "Bugs"
|
|
77695
|
+
}
|
|
77696
|
+
},
|
|
77697
|
+
{
|
|
77698
|
+
key: "react-doctor/remotion-no-native-media-elements",
|
|
77699
|
+
id: "remotion-no-native-media-elements",
|
|
77700
|
+
source: "react-doctor",
|
|
77701
|
+
originallyExternal: false,
|
|
77702
|
+
rule: {
|
|
77703
|
+
...remotionNoNativeMediaElements,
|
|
77704
|
+
framework: "global",
|
|
77705
|
+
category: "Bugs"
|
|
77706
|
+
}
|
|
77707
|
+
},
|
|
77708
|
+
{
|
|
77709
|
+
key: "react-doctor/remotion-no-next-image",
|
|
77710
|
+
id: "remotion-no-next-image",
|
|
77711
|
+
source: "react-doctor",
|
|
77712
|
+
originallyExternal: false,
|
|
77713
|
+
rule: {
|
|
77714
|
+
...remotionNoNextImage,
|
|
77715
|
+
framework: "global",
|
|
77716
|
+
category: "Bugs"
|
|
77717
|
+
}
|
|
77718
|
+
},
|
|
77719
|
+
{
|
|
77720
|
+
key: "react-doctor/remotion-stable-delay-render-handle",
|
|
77721
|
+
id: "remotion-stable-delay-render-handle",
|
|
77722
|
+
source: "react-doctor",
|
|
77723
|
+
originallyExternal: false,
|
|
77724
|
+
rule: {
|
|
77725
|
+
...remotionStableDelayRenderHandle,
|
|
77726
|
+
framework: "global",
|
|
77727
|
+
category: "Bugs"
|
|
77728
|
+
}
|
|
77729
|
+
},
|
|
75544
77730
|
{
|
|
75545
77731
|
key: "react-doctor/rendering-animate-svg-wrapper",
|
|
75546
77732
|
id: "rendering-animate-svg-wrapper",
|
|
@@ -76664,6 +78850,30 @@ const reactDoctorRules = [
|
|
|
76664
78850
|
category: "Performance"
|
|
76665
78851
|
}
|
|
76666
78852
|
},
|
|
78853
|
+
{
|
|
78854
|
+
key: "react-doctor/valtio-no-proxy-read-in-render",
|
|
78855
|
+
id: "valtio-no-proxy-read-in-render",
|
|
78856
|
+
source: "react-doctor",
|
|
78857
|
+
originallyExternal: false,
|
|
78858
|
+
rule: {
|
|
78859
|
+
...valtioNoProxyReadInRender,
|
|
78860
|
+
framework: "global",
|
|
78861
|
+
category: "Bugs",
|
|
78862
|
+
requires: [...new Set(["react", ...valtioNoProxyReadInRender.requires ?? []])]
|
|
78863
|
+
}
|
|
78864
|
+
},
|
|
78865
|
+
{
|
|
78866
|
+
key: "react-doctor/valtio-no-snapshot-in-callback",
|
|
78867
|
+
id: "valtio-no-snapshot-in-callback",
|
|
78868
|
+
source: "react-doctor",
|
|
78869
|
+
originallyExternal: false,
|
|
78870
|
+
rule: {
|
|
78871
|
+
...valtioNoSnapshotInCallback,
|
|
78872
|
+
framework: "global",
|
|
78873
|
+
category: "Bugs",
|
|
78874
|
+
requires: [...new Set(["react", ...valtioNoSnapshotInCallback.requires ?? []])]
|
|
78875
|
+
}
|
|
78876
|
+
},
|
|
76667
78877
|
{
|
|
76668
78878
|
key: "react-doctor/void-dom-elements-no-children",
|
|
76669
78879
|
id: "void-dom-elements-no-children",
|
|
@@ -76731,6 +78941,30 @@ const reactDoctorRules = [
|
|
|
76731
78941
|
framework: "global",
|
|
76732
78942
|
category: "Maintainability"
|
|
76733
78943
|
}
|
|
78944
|
+
},
|
|
78945
|
+
{
|
|
78946
|
+
key: "react-doctor/zustand-no-fresh-selector-result",
|
|
78947
|
+
id: "zustand-no-fresh-selector-result",
|
|
78948
|
+
source: "react-doctor",
|
|
78949
|
+
originallyExternal: false,
|
|
78950
|
+
rule: {
|
|
78951
|
+
...zustandNoFreshSelectorResult,
|
|
78952
|
+
framework: "global",
|
|
78953
|
+
category: "Performance",
|
|
78954
|
+
requires: [...new Set(["react", ...zustandNoFreshSelectorResult.requires ?? []])]
|
|
78955
|
+
}
|
|
78956
|
+
},
|
|
78957
|
+
{
|
|
78958
|
+
key: "react-doctor/zustand-no-whole-store-destructure",
|
|
78959
|
+
id: "zustand-no-whole-store-destructure",
|
|
78960
|
+
source: "react-doctor",
|
|
78961
|
+
originallyExternal: false,
|
|
78962
|
+
rule: {
|
|
78963
|
+
...zustandNoWholeStoreDestructure,
|
|
78964
|
+
framework: "global",
|
|
78965
|
+
category: "Bugs",
|
|
78966
|
+
requires: [...new Set(["react", ...zustandNoWholeStoreDestructure.requires ?? []])]
|
|
78967
|
+
}
|
|
76734
78968
|
}
|
|
76735
78969
|
];
|
|
76736
78970
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
@@ -77000,6 +79234,13 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
77000
79234
|
"no-unguarded-browser-global-in-render-or-hook-init",
|
|
77001
79235
|
"prefer-dynamic-import",
|
|
77002
79236
|
"rendering-hydration-mismatch-time",
|
|
79237
|
+
"remotion-calculate-metadata-fetch-signal",
|
|
79238
|
+
"remotion-deterministic-randomness",
|
|
79239
|
+
"remotion-no-css-animation",
|
|
79240
|
+
"remotion-no-css-transition",
|
|
79241
|
+
"remotion-no-css-url-assets",
|
|
79242
|
+
"remotion-no-native-media-elements",
|
|
79243
|
+
"remotion-no-next-image",
|
|
77003
79244
|
"rerender-memo-with-default-value",
|
|
77004
79245
|
"rn-no-legacy-shadow-styles",
|
|
77005
79246
|
"rn-no-raw-text",
|
|
@@ -77217,7 +79458,17 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
77217
79458
|
* `CROSS_FILE_DEPENDENCY_COLLECTORS` (the core guard test enforces the
|
|
77218
79459
|
* partition), forcing a conscious classification.
|
|
77219
79460
|
*/
|
|
77220
|
-
const UNBOUNDED_CROSS_FILE_RULE_IDS = new Set([
|
|
79461
|
+
const UNBOUNDED_CROSS_FILE_RULE_IDS = new Set([
|
|
79462
|
+
"nextjs-no-img-element",
|
|
79463
|
+
"only-export-components",
|
|
79464
|
+
"remotion-calculate-metadata-fetch-signal",
|
|
79465
|
+
"remotion-deterministic-randomness",
|
|
79466
|
+
"remotion-no-css-animation",
|
|
79467
|
+
"remotion-no-css-transition",
|
|
79468
|
+
"remotion-no-css-url-assets",
|
|
79469
|
+
"remotion-no-native-media-elements",
|
|
79470
|
+
"remotion-no-next-image"
|
|
79471
|
+
]);
|
|
77221
79472
|
/**
|
|
77222
79473
|
* Runs the collectors for `ruleIds` over one file and returns every
|
|
77223
79474
|
* filesystem probe they made — the file's cross-file dependency set.
|