oxlint-plugin-react-doctor 0.4.2-dev.25cc69b → 0.4.2-dev.48e61bc
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 +0 -38
- package/dist/index.js +902 -997
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -719,7 +719,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
719
719
|
if (totalEffects === 0) return;
|
|
720
720
|
context.report({
|
|
721
721
|
node: openingElement,
|
|
722
|
-
message: `Every hide
|
|
722
|
+
message: `Every hide and show rebuilds ${effectfulChildren.join(", ")} from scratch because <Activity> wraps components with ${totalEffects} effect hook${totalEffects === 1 ? "" : "s"}.`
|
|
723
723
|
});
|
|
724
724
|
}
|
|
725
725
|
};
|
|
@@ -823,15 +823,550 @@ const getElementType = (openingElement, settings) => {
|
|
|
823
823
|
return baseName;
|
|
824
824
|
};
|
|
825
825
|
//#endregion
|
|
826
|
+
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
827
|
+
const collectFromProgram = (programRoot) => {
|
|
828
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
829
|
+
const visit = (node) => {
|
|
830
|
+
if (node.type === "ImportDeclaration" && "source" in node && node.source) {
|
|
831
|
+
const source = node.source.value;
|
|
832
|
+
if (typeof source !== "string") return;
|
|
833
|
+
if ("specifiers" in node && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
|
|
834
|
+
if (!("local" in specifier) || !specifier.local) continue;
|
|
835
|
+
const local = specifier.local;
|
|
836
|
+
if (typeof local.name !== "string") continue;
|
|
837
|
+
if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
|
|
838
|
+
source,
|
|
839
|
+
imported: null,
|
|
840
|
+
isDefault: true,
|
|
841
|
+
isNamespace: false
|
|
842
|
+
});
|
|
843
|
+
else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
|
|
844
|
+
source,
|
|
845
|
+
imported: null,
|
|
846
|
+
isDefault: false,
|
|
847
|
+
isNamespace: true
|
|
848
|
+
});
|
|
849
|
+
else if (specifier.type === "ImportSpecifier") {
|
|
850
|
+
const importedNode = specifier.imported;
|
|
851
|
+
const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
|
|
852
|
+
lookup.set(local.name, {
|
|
853
|
+
source,
|
|
854
|
+
imported: importedName,
|
|
855
|
+
isDefault: false,
|
|
856
|
+
isNamespace: false
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const nodeRecord = node;
|
|
863
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
864
|
+
if (key === "parent") continue;
|
|
865
|
+
const child = nodeRecord[key];
|
|
866
|
+
if (Array.isArray(child)) {
|
|
867
|
+
for (const item of child) if (isAstNode(item)) visit(item);
|
|
868
|
+
} else if (isAstNode(child)) visit(child);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
visit(programRoot);
|
|
872
|
+
return lookup;
|
|
873
|
+
};
|
|
874
|
+
const importLookupCache = /* @__PURE__ */ new WeakMap();
|
|
875
|
+
const getImportLookup = (node) => {
|
|
876
|
+
const programRoot = findProgramRoot(node);
|
|
877
|
+
if (!programRoot) return null;
|
|
878
|
+
let cached = importLookupCache.get(programRoot);
|
|
879
|
+
if (!cached) {
|
|
880
|
+
cached = collectFromProgram(programRoot);
|
|
881
|
+
importLookupCache.set(programRoot, cached);
|
|
882
|
+
}
|
|
883
|
+
return cached;
|
|
884
|
+
};
|
|
885
|
+
const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
886
|
+
const lookup = getImportLookup(contextNode);
|
|
887
|
+
if (!lookup) return false;
|
|
888
|
+
const info = lookup.get(localIdentifierName);
|
|
889
|
+
if (!info) return false;
|
|
890
|
+
return info.source === moduleSource;
|
|
891
|
+
};
|
|
892
|
+
const isNamespaceImportFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
893
|
+
const lookup = getImportLookup(contextNode);
|
|
894
|
+
if (!lookup) return false;
|
|
895
|
+
const info = lookup.get(localIdentifierName);
|
|
896
|
+
if (!info) return false;
|
|
897
|
+
return info.isNamespace && info.source === moduleSource;
|
|
898
|
+
};
|
|
899
|
+
const isDefaultImportFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
900
|
+
const lookup = getImportLookup(contextNode);
|
|
901
|
+
if (!lookup) return false;
|
|
902
|
+
const info = lookup.get(localIdentifierName);
|
|
903
|
+
if (!info) return false;
|
|
904
|
+
return info.isDefault && info.source === moduleSource;
|
|
905
|
+
};
|
|
906
|
+
const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
907
|
+
const lookup = getImportLookup(contextNode);
|
|
908
|
+
if (!lookup) return null;
|
|
909
|
+
const info = lookup.get(localIdentifierName);
|
|
910
|
+
if (!info) return null;
|
|
911
|
+
if (info.source !== moduleSource) return null;
|
|
912
|
+
return info.imported;
|
|
913
|
+
};
|
|
914
|
+
//#endregion
|
|
915
|
+
//#region src/plugin/utils/find-variable-initializer.ts
|
|
916
|
+
const FUNCTION_LIKE_TYPES$1 = new Set([
|
|
917
|
+
"FunctionDeclaration",
|
|
918
|
+
"FunctionExpression",
|
|
919
|
+
"ArrowFunctionExpression",
|
|
920
|
+
"MethodDefinition",
|
|
921
|
+
"Program"
|
|
922
|
+
]);
|
|
923
|
+
const findScopeOwner = (node) => {
|
|
924
|
+
let ancestor = node;
|
|
925
|
+
while (ancestor) {
|
|
926
|
+
if (FUNCTION_LIKE_TYPES$1.has(ancestor.type)) return ancestor;
|
|
927
|
+
ancestor = ancestor.parent ?? null;
|
|
928
|
+
}
|
|
929
|
+
return null;
|
|
930
|
+
};
|
|
931
|
+
const findBlockScopeOwner = (declaratorNode, declarationKind) => {
|
|
932
|
+
if (declarationKind !== "let" && declarationKind !== "const") return findScopeOwner(declaratorNode);
|
|
933
|
+
let ancestor = declaratorNode.parent;
|
|
934
|
+
while (ancestor) {
|
|
935
|
+
if (ancestor.type === "BlockStatement") {
|
|
936
|
+
const blockParent = ancestor.parent;
|
|
937
|
+
if (blockParent && (blockParent.type === "FunctionDeclaration" || blockParent.type === "FunctionExpression" || blockParent.type === "ArrowFunctionExpression" || blockParent.type === "MethodDefinition")) return findScopeOwner(declaratorNode);
|
|
938
|
+
return ancestor;
|
|
939
|
+
}
|
|
940
|
+
if (FUNCTION_LIKE_TYPES$1.has(ancestor.type)) return ancestor;
|
|
941
|
+
ancestor = ancestor.parent ?? null;
|
|
942
|
+
}
|
|
943
|
+
return null;
|
|
944
|
+
};
|
|
945
|
+
const collectFromBindingPattern = (pattern, initializer, scopeOwner, out) => {
|
|
946
|
+
if (isNodeOfType(pattern, "Identifier")) {
|
|
947
|
+
const list = out.get(pattern.name) ?? [];
|
|
948
|
+
list.push({
|
|
949
|
+
bindingIdentifier: pattern,
|
|
950
|
+
initializer,
|
|
951
|
+
scopeOwner
|
|
952
|
+
});
|
|
953
|
+
out.set(pattern.name, list);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (isNodeOfType(pattern, "ObjectPattern")) {
|
|
957
|
+
for (const property of pattern.properties) if (isNodeOfType(property, "Property")) {
|
|
958
|
+
const valueNode = property.value;
|
|
959
|
+
collectFromBindingPattern(valueNode, isNodeOfType(valueNode, "AssignmentPattern") ? valueNode.right : null, scopeOwner, out);
|
|
960
|
+
} else if (isNodeOfType(property, "RestElement")) collectFromBindingPattern(property.argument, null, scopeOwner, out);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (isNodeOfType(pattern, "ArrayPattern")) {
|
|
964
|
+
for (const element of pattern.elements) {
|
|
965
|
+
if (!element) continue;
|
|
966
|
+
collectFromBindingPattern(element, isNodeOfType(element, "AssignmentPattern") ? element.right ?? null : null, scopeOwner, out);
|
|
967
|
+
}
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (isNodeOfType(pattern, "AssignmentPattern")) {
|
|
971
|
+
collectFromBindingPattern(pattern.left, pattern.right ?? null, scopeOwner, out);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (isNodeOfType(pattern, "RestElement")) collectFromBindingPattern(pattern.argument, null, scopeOwner, out);
|
|
975
|
+
};
|
|
976
|
+
const buildBindingIndex = (root) => {
|
|
977
|
+
const out = /* @__PURE__ */ new Map();
|
|
978
|
+
const visit = (node) => {
|
|
979
|
+
if (isNodeOfType(node, "VariableDeclarator")) {
|
|
980
|
+
const declaration = node.parent;
|
|
981
|
+
const scopeOwner = findBlockScopeOwner(node, declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : void 0);
|
|
982
|
+
if (scopeOwner) collectFromBindingPattern(node.id, node.init ?? null, scopeOwner, out);
|
|
983
|
+
}
|
|
984
|
+
if ((isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression")) && node.id) {
|
|
985
|
+
const enclosing = node.parent ? findScopeOwner(node.parent) : null;
|
|
986
|
+
if (enclosing) {
|
|
987
|
+
const list = out.get(node.id.name) ?? [];
|
|
988
|
+
list.push({
|
|
989
|
+
bindingIdentifier: node.id,
|
|
990
|
+
initializer: node,
|
|
991
|
+
scopeOwner: enclosing
|
|
992
|
+
});
|
|
993
|
+
out.set(node.id.name, list);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if ((isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression")) && node.id) {
|
|
997
|
+
const enclosing = node.parent ? findScopeOwner(node.parent) : null;
|
|
998
|
+
if (enclosing) {
|
|
999
|
+
const list = out.get(node.id.name) ?? [];
|
|
1000
|
+
list.push({
|
|
1001
|
+
bindingIdentifier: node.id,
|
|
1002
|
+
initializer: node,
|
|
1003
|
+
scopeOwner: enclosing
|
|
1004
|
+
});
|
|
1005
|
+
out.set(node.id.name, list);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")) {
|
|
1009
|
+
if (Array.isArray(node.params)) for (const param of node.params) {
|
|
1010
|
+
if (!param) continue;
|
|
1011
|
+
collectFromBindingPattern(param, null, node, out);
|
|
1012
|
+
if (isNodeOfType(param, "AssignmentPattern")) collectFromBindingPattern(param.left ?? null, param.right ?? null, node, out);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (isNodeOfType(node, "ImportDeclaration")) {
|
|
1016
|
+
const scopeOwner = findScopeOwner(node);
|
|
1017
|
+
if (scopeOwner && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
|
|
1018
|
+
const local = specifier.local;
|
|
1019
|
+
if (local && isNodeOfType(local, "Identifier")) {
|
|
1020
|
+
const list = out.get(local.name) ?? [];
|
|
1021
|
+
list.push({
|
|
1022
|
+
bindingIdentifier: local,
|
|
1023
|
+
initializer: specifier,
|
|
1024
|
+
scopeOwner
|
|
1025
|
+
});
|
|
1026
|
+
out.set(local.name, list);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (node.type === "TSImportEqualsDeclaration" || node.type === "TSEnumDeclaration" || node.type === "TSModuleDeclaration") {
|
|
1031
|
+
const idNode = node.id;
|
|
1032
|
+
if (idNode && idNode.type === "Identifier") {
|
|
1033
|
+
const idObject = idNode;
|
|
1034
|
+
const scopeOwner = findScopeOwner(node);
|
|
1035
|
+
if (scopeOwner && typeof idObject.name === "string") {
|
|
1036
|
+
const list = out.get(idObject.name) ?? [];
|
|
1037
|
+
list.push({
|
|
1038
|
+
bindingIdentifier: idNode,
|
|
1039
|
+
initializer: null,
|
|
1040
|
+
scopeOwner
|
|
1041
|
+
});
|
|
1042
|
+
out.set(idObject.name, list);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const nodeRecord = node;
|
|
1047
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
1048
|
+
if (key === "parent") continue;
|
|
1049
|
+
const child = nodeRecord[key];
|
|
1050
|
+
if (Array.isArray(child)) {
|
|
1051
|
+
for (const item of child) if (isAstNode(item)) visit(item);
|
|
1052
|
+
} else if (isAstNode(child)) visit(child);
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
visit(root);
|
|
1056
|
+
return out;
|
|
1057
|
+
};
|
|
1058
|
+
const programRootCache = /* @__PURE__ */ new WeakMap();
|
|
1059
|
+
const getBindingIndex = (referenceNode) => {
|
|
1060
|
+
const programRoot = findProgramRoot(referenceNode);
|
|
1061
|
+
if (!programRoot) return null;
|
|
1062
|
+
let index = programRootCache.get(programRoot);
|
|
1063
|
+
if (!index) {
|
|
1064
|
+
index = buildBindingIndex(programRoot);
|
|
1065
|
+
programRootCache.set(programRoot, index);
|
|
1066
|
+
}
|
|
1067
|
+
return index;
|
|
1068
|
+
};
|
|
1069
|
+
const findVariableInitializer = (referenceNode, bindingName) => {
|
|
1070
|
+
const index = getBindingIndex(referenceNode);
|
|
1071
|
+
if (!index) return null;
|
|
1072
|
+
const candidates = index.get(bindingName);
|
|
1073
|
+
if (!candidates || candidates.length === 0) return null;
|
|
1074
|
+
const referenceAncestors = /* @__PURE__ */ new Set();
|
|
1075
|
+
let walker = referenceNode;
|
|
1076
|
+
while (walker) {
|
|
1077
|
+
referenceAncestors.add(walker);
|
|
1078
|
+
walker = walker.parent ?? null;
|
|
1079
|
+
}
|
|
1080
|
+
let best = null;
|
|
1081
|
+
for (const candidate of candidates) {
|
|
1082
|
+
if (!referenceAncestors.has(candidate.scopeOwner)) continue;
|
|
1083
|
+
if (best === null) {
|
|
1084
|
+
best = candidate;
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
let cursor = candidate.scopeOwner;
|
|
1088
|
+
while (cursor) {
|
|
1089
|
+
if (cursor === best.scopeOwner) {
|
|
1090
|
+
best = candidate;
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
cursor = cursor.parent ?? null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return best;
|
|
1097
|
+
};
|
|
1098
|
+
//#endregion
|
|
1099
|
+
//#region src/plugin/utils/flatten-jsx-name.ts
|
|
1100
|
+
/**
|
|
1101
|
+
* Flattens a JSX opener / member-expression chain into a dotted name.
|
|
1102
|
+
*
|
|
1103
|
+
* `<Foo />` → `"Foo"`
|
|
1104
|
+
* `<Namespace.Foo />` → `"Namespace.Foo"`
|
|
1105
|
+
* `<a.b.c />` → `"a.b.c"`
|
|
1106
|
+
*
|
|
1107
|
+
* Returns `null` when the chain root isn't a `JSXIdentifier` (e.g.
|
|
1108
|
+
* the rare-but-valid `<this.x />` case, which JSX surfaces as a
|
|
1109
|
+
* `JSXThisExpression` root).
|
|
1110
|
+
*
|
|
1111
|
+
* Used by `forbid-elements` and `jsx-props-no-spreading`. Two other
|
|
1112
|
+
* rules (`forbid-component-props`, `utils/get-element-type`) keep
|
|
1113
|
+
* their own extended variants because they handle additional node
|
|
1114
|
+
* types (ThisExpression / JSXNamespacedName) and return a non-
|
|
1115
|
+
* nullable string with a sentinel value — semantically distinct.
|
|
1116
|
+
*/
|
|
1117
|
+
const flattenJsxName$1 = (node) => {
|
|
1118
|
+
if (isNodeOfType(node, "JSXIdentifier")) return node.name;
|
|
1119
|
+
if (isNodeOfType(node, "JSXMemberExpression")) {
|
|
1120
|
+
const objectName = flattenJsxName$1(node.object);
|
|
1121
|
+
if (!objectName) return null;
|
|
1122
|
+
return `${objectName}.${node.property.name}`;
|
|
1123
|
+
}
|
|
1124
|
+
return null;
|
|
1125
|
+
};
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/plugin/utils/is-member-property.ts
|
|
1128
|
+
const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === propertyName);
|
|
1129
|
+
//#endregion
|
|
1130
|
+
//#region src/plugin/constants/nextjs.ts
|
|
1131
|
+
const NEXTJS_SOURCE_FILE_EXTENSION_GROUP = "(?:tsx?|jsx?|mts|mjs)";
|
|
1132
|
+
const PAGE_FILE_PATTERN = new RegExp(`/page\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1133
|
+
const PAGE_OR_LAYOUT_FILE_PATTERN = new RegExp(`/(page|layout)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1134
|
+
const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
|
|
1135
|
+
const PAGES_DIRECTORY_PATTERN = /\/pages\//;
|
|
1136
|
+
const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
|
|
1137
|
+
"redirect",
|
|
1138
|
+
"permanentRedirect",
|
|
1139
|
+
"notFound",
|
|
1140
|
+
"forbidden",
|
|
1141
|
+
"unauthorized"
|
|
1142
|
+
]);
|
|
1143
|
+
const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
|
|
1144
|
+
const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
|
|
1145
|
+
const APP_DIRECTORY_PATTERN = /\/app\//;
|
|
1146
|
+
const ROUTE_HANDLER_FILE_PATTERN = new RegExp(`/route\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1147
|
+
const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
|
|
1148
|
+
const MUTATING_ROUTE_SEGMENTS = new Set([
|
|
1149
|
+
"logout",
|
|
1150
|
+
"log-out",
|
|
1151
|
+
"signout",
|
|
1152
|
+
"sign-out",
|
|
1153
|
+
"unsubscribe",
|
|
1154
|
+
"delete",
|
|
1155
|
+
"remove",
|
|
1156
|
+
"revoke",
|
|
1157
|
+
"cancel",
|
|
1158
|
+
"deactivate"
|
|
1159
|
+
]);
|
|
1160
|
+
const ERROR_BOUNDARY_FILE_PATTERN = new RegExp(`/(error|global-error)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1161
|
+
const GLOBAL_ERROR_FILE_PATTERN = new RegExp(`/global-error\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1162
|
+
const ROUTE_HANDLER_HTTP_METHODS = new Set([
|
|
1163
|
+
"GET",
|
|
1164
|
+
"POST",
|
|
1165
|
+
"PUT",
|
|
1166
|
+
"PATCH",
|
|
1167
|
+
"DELETE",
|
|
1168
|
+
"OPTIONS",
|
|
1169
|
+
"HEAD"
|
|
1170
|
+
]);
|
|
1171
|
+
const GOOGLE_ANALYTICS_SCRIPT_PATTERN = /google-analytics\.com|googletagmanager\.com\/gtag/;
|
|
1172
|
+
const OG_IMAGE_FILE_PATTERN = new RegExp(`/(opengraph-image|twitter-image)\\d*\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
1173
|
+
//#endregion
|
|
826
1174
|
//#region src/plugin/utils/is-nextjs-metadata-image-route-filename.ts
|
|
1175
|
+
const METADATA_IMAGE_ROUTE_FILE_PATTERN = new RegExp(`^(opengraph-image|twitter-image|icon|apple-icon)\\d*\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
827
1176
|
const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
828
1177
|
if (!rawFilename) return false;
|
|
829
|
-
return
|
|
1178
|
+
return METADATA_IMAGE_ROUTE_FILE_PATTERN.test(path.basename(rawFilename));
|
|
830
1179
|
};
|
|
831
1180
|
//#endregion
|
|
832
1181
|
//#region src/plugin/utils/normalize-filename.ts
|
|
833
1182
|
const normalizeFilename$1 = (filename) => filename.replaceAll("\\", "/");
|
|
834
1183
|
//#endregion
|
|
1184
|
+
//#region src/plugin/utils/strip-paren-expression.ts
|
|
1185
|
+
const TS_WRAPPER_TYPES = new Set([
|
|
1186
|
+
"ParenthesizedExpression",
|
|
1187
|
+
"TSAsExpression",
|
|
1188
|
+
"TSSatisfiesExpression",
|
|
1189
|
+
"TSTypeAssertion",
|
|
1190
|
+
"TSNonNullExpression",
|
|
1191
|
+
"TSInstantiationExpression"
|
|
1192
|
+
]);
|
|
1193
|
+
const stripParenExpression = (node) => {
|
|
1194
|
+
let current = node;
|
|
1195
|
+
while (true) {
|
|
1196
|
+
if (TS_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) {
|
|
1197
|
+
current = current.expression;
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
if (isNodeOfType(current, "ChainExpression") && current.expression) {
|
|
1201
|
+
current = current.expression;
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
return current;
|
|
1207
|
+
};
|
|
1208
|
+
//#endregion
|
|
1209
|
+
//#region src/plugin/utils/is-generated-image-render-context.ts
|
|
1210
|
+
const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
|
|
1211
|
+
const SATORI_MODULE = "satori";
|
|
1212
|
+
const generatedImageJsxCache = /* @__PURE__ */ new WeakMap();
|
|
1213
|
+
const isGeneratedImageRenderFilename = (rawFilename) => {
|
|
1214
|
+
if (!rawFilename) return false;
|
|
1215
|
+
return isNextjsMetadataImageRouteFilename(normalizeFilename$1(rawFilename));
|
|
1216
|
+
};
|
|
1217
|
+
const isImageResponseCallee = (contextNode, callee) => {
|
|
1218
|
+
if (isNodeOfType(callee, "Identifier")) return IMAGE_RESPONSE_MODULES.some((moduleSource) => getImportedNameFromModule(contextNode, callee.name, moduleSource) === "ImageResponse");
|
|
1219
|
+
if (!isMemberProperty(callee, "ImageResponse")) return false;
|
|
1220
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
1221
|
+
const namespaceIdentifierName = callee.object.name;
|
|
1222
|
+
return IMAGE_RESPONSE_MODULES.some((moduleSource) => isNamespaceImportFromModule(contextNode, namespaceIdentifierName, moduleSource));
|
|
1223
|
+
};
|
|
1224
|
+
const isSatoriCallee = (contextNode, callee) => {
|
|
1225
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
1226
|
+
if (getImportedNameFromModule(contextNode, callee.name, SATORI_MODULE) === "satori") return true;
|
|
1227
|
+
return isDefaultImportFromModule(contextNode, callee.name, SATORI_MODULE);
|
|
1228
|
+
};
|
|
1229
|
+
const isGeneratedImageRendererCall = (node) => {
|
|
1230
|
+
if (!isNodeOfType(node, "CallExpression") && !isNodeOfType(node, "NewExpression")) return false;
|
|
1231
|
+
if (!isNodeOfType(node.callee, "Identifier") && !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
1232
|
+
return isImageResponseCallee(node, node.callee) || isSatoriCallee(node, node.callee);
|
|
1233
|
+
};
|
|
1234
|
+
const isComponentIdentifierName = (name) => {
|
|
1235
|
+
const firstCharacter = name[0];
|
|
1236
|
+
return Boolean(firstCharacter && firstCharacter === firstCharacter.toUpperCase());
|
|
1237
|
+
};
|
|
1238
|
+
const isFunctionLike$3 = (node) => Boolean(node && (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")));
|
|
1239
|
+
const markFunctionReturnJsx = (functionNode, programRoot, generatedImageJsxNodes, visitedComponentNames) => {
|
|
1240
|
+
if (!isFunctionLike$3(functionNode)) return;
|
|
1241
|
+
if (isNodeOfType(functionNode, "ArrowFunctionExpression")) {
|
|
1242
|
+
const body = stripParenExpression(functionNode.body);
|
|
1243
|
+
if (!isNodeOfType(body, "BlockStatement")) {
|
|
1244
|
+
markGeneratedImageExpression(body, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
const body = functionNode.body;
|
|
1249
|
+
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
1250
|
+
walkAst(body, (descendantNode) => {
|
|
1251
|
+
if (descendantNode !== body && isFunctionLike$3(descendantNode)) return false;
|
|
1252
|
+
if (!isNodeOfType(descendantNode, "ReturnStatement")) return;
|
|
1253
|
+
if (!descendantNode.argument) return;
|
|
1254
|
+
markGeneratedImageExpression(stripParenExpression(descendantNode.argument), programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1255
|
+
});
|
|
1256
|
+
};
|
|
1257
|
+
const hasNormalJsxUsage = (programRoot, componentName, generatedImageJsxNodes) => {
|
|
1258
|
+
let hasNormalUsage = false;
|
|
1259
|
+
walkAst(programRoot, (descendantNode) => {
|
|
1260
|
+
if (hasNormalUsage) return false;
|
|
1261
|
+
if (!isNodeOfType(descendantNode, "JSXOpeningElement")) return;
|
|
1262
|
+
if (generatedImageJsxNodes.has(descendantNode)) return;
|
|
1263
|
+
if (flattenJsxName$1(descendantNode.name) !== componentName) return;
|
|
1264
|
+
hasNormalUsage = true;
|
|
1265
|
+
return false;
|
|
1266
|
+
});
|
|
1267
|
+
return hasNormalUsage;
|
|
1268
|
+
};
|
|
1269
|
+
const markComponentRenderJsx = (programRoot, openingElement, generatedImageJsxNodes, visitedComponentNames) => {
|
|
1270
|
+
const tagName = flattenJsxName$1(openingElement.name);
|
|
1271
|
+
if (!tagName || tagName.includes(".") || !isComponentIdentifierName(tagName)) return;
|
|
1272
|
+
if (visitedComponentNames.has(tagName)) return;
|
|
1273
|
+
if (hasNormalJsxUsage(programRoot, tagName, generatedImageJsxNodes)) return;
|
|
1274
|
+
const binding = findVariableInitializer(openingElement, tagName);
|
|
1275
|
+
if (!binding?.initializer) return;
|
|
1276
|
+
visitedComponentNames.add(tagName);
|
|
1277
|
+
markGeneratedImageExpression(stripParenExpression(binding.initializer), programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1278
|
+
};
|
|
1279
|
+
const isInsideGeneratedImageRendererArgument = (node) => {
|
|
1280
|
+
let cursor = node.parent;
|
|
1281
|
+
while (cursor) {
|
|
1282
|
+
if (isGeneratedImageRendererCall(cursor)) return true;
|
|
1283
|
+
cursor = cursor.parent ?? null;
|
|
1284
|
+
}
|
|
1285
|
+
return false;
|
|
1286
|
+
};
|
|
1287
|
+
const hasNormalFunctionCallUsage = (programRoot, functionName) => {
|
|
1288
|
+
let hasNormalUsage = false;
|
|
1289
|
+
walkAst(programRoot, (descendantNode) => {
|
|
1290
|
+
if (hasNormalUsage) return false;
|
|
1291
|
+
if (!isNodeOfType(descendantNode, "CallExpression")) return;
|
|
1292
|
+
if (!isNodeOfType(descendantNode.callee, "Identifier")) return;
|
|
1293
|
+
if (descendantNode.callee.name !== functionName) return;
|
|
1294
|
+
if (isInsideGeneratedImageRendererArgument(descendantNode)) return;
|
|
1295
|
+
hasNormalUsage = true;
|
|
1296
|
+
return false;
|
|
1297
|
+
});
|
|
1298
|
+
return hasNormalUsage;
|
|
1299
|
+
};
|
|
1300
|
+
const markJsxSubtree = (node, programRoot, generatedImageJsxNodes, visitedComponentNames) => {
|
|
1301
|
+
walkAst(node, (descendantNode) => {
|
|
1302
|
+
if (!isNodeOfType(descendantNode, "JSXOpeningElement")) return;
|
|
1303
|
+
generatedImageJsxNodes.add(descendantNode);
|
|
1304
|
+
markComponentRenderJsx(programRoot, descendantNode, generatedImageJsxNodes, visitedComponentNames);
|
|
1305
|
+
});
|
|
1306
|
+
};
|
|
1307
|
+
const markGeneratedImageExpression = (expression, programRoot, generatedImageJsxNodes, visitedComponentNames) => {
|
|
1308
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
1309
|
+
if (isNodeOfType(unwrappedExpression, "JSXElement") || isNodeOfType(unwrappedExpression, "JSXFragment")) {
|
|
1310
|
+
markJsxSubtree(unwrappedExpression, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (isFunctionLike$3(unwrappedExpression)) {
|
|
1314
|
+
markFunctionReturnJsx(unwrappedExpression, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
|
|
1318
|
+
markGeneratedImageExpression(unwrappedExpression.consequent, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1319
|
+
markGeneratedImageExpression(unwrappedExpression.alternate, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
1323
|
+
markGeneratedImageExpression(unwrappedExpression.left, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1324
|
+
markGeneratedImageExpression(unwrappedExpression.right, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
1328
|
+
const callee = unwrappedExpression.callee;
|
|
1329
|
+
if (isFunctionLike$3(callee)) {
|
|
1330
|
+
markFunctionReturnJsx(callee, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (!isNodeOfType(callee, "Identifier")) return;
|
|
1334
|
+
if (visitedComponentNames.has(callee.name)) return;
|
|
1335
|
+
if (hasNormalJsxUsage(programRoot, callee.name, generatedImageJsxNodes)) return;
|
|
1336
|
+
if (hasNormalFunctionCallUsage(programRoot, callee.name)) return;
|
|
1337
|
+
const binding = findVariableInitializer(callee, callee.name);
|
|
1338
|
+
if (!binding?.initializer || !isFunctionLike$3(stripParenExpression(binding.initializer))) return;
|
|
1339
|
+
visitedComponentNames.add(callee.name);
|
|
1340
|
+
markFunctionReturnJsx(stripParenExpression(binding.initializer), programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
1344
|
+
if (visitedComponentNames.has(unwrappedExpression.name)) return;
|
|
1345
|
+
visitedComponentNames.add(unwrappedExpression.name);
|
|
1346
|
+
const binding = findVariableInitializer(unwrappedExpression, unwrappedExpression.name);
|
|
1347
|
+
if (!binding?.initializer) return;
|
|
1348
|
+
markGeneratedImageExpression(stripParenExpression(binding.initializer), programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
const collectGeneratedImageJsxNodes = (programRoot) => {
|
|
1352
|
+
const cached = generatedImageJsxCache.get(programRoot);
|
|
1353
|
+
if (cached) return cached;
|
|
1354
|
+
const generatedImageJsxNodes = /* @__PURE__ */ new WeakSet();
|
|
1355
|
+
walkAst(programRoot, (descendantNode) => {
|
|
1356
|
+
if (!isGeneratedImageRendererCall(descendantNode)) return;
|
|
1357
|
+
for (const argument of descendantNode.arguments) markGeneratedImageExpression(argument, programRoot, generatedImageJsxNodes, /* @__PURE__ */ new Set());
|
|
1358
|
+
});
|
|
1359
|
+
generatedImageJsxCache.set(programRoot, generatedImageJsxNodes);
|
|
1360
|
+
return generatedImageJsxNodes;
|
|
1361
|
+
};
|
|
1362
|
+
const isGeneratedImageRenderContext = (context, node) => {
|
|
1363
|
+
if (isGeneratedImageRenderFilename(context.filename)) return true;
|
|
1364
|
+
if (!node) return false;
|
|
1365
|
+
const programRoot = findProgramRoot(node);
|
|
1366
|
+
if (!programRoot) return false;
|
|
1367
|
+
return collectGeneratedImageJsxNodes(programRoot).has(node);
|
|
1368
|
+
};
|
|
1369
|
+
//#endregion
|
|
835
1370
|
//#region src/plugin/utils/is-hidden-from-screen-reader.ts
|
|
836
1371
|
const isHiddenFromScreenReader = (openingElement, settings) => {
|
|
837
1372
|
if (getElementType(openingElement, settings).toLowerCase() === "input") {
|
|
@@ -879,15 +1414,15 @@ const objectHasAccessibleChild = (jsxElement, settings) => {
|
|
|
879
1414
|
};
|
|
880
1415
|
//#endregion
|
|
881
1416
|
//#region src/plugin/rules/a11y/alt-text.ts
|
|
882
|
-
const MISSING_ALT_PROP = "
|
|
883
|
-
const MISSING_ALT_VALUE = "
|
|
884
|
-
const ARIA_LABEL_VALUE = "
|
|
885
|
-
const ARIA_LABELLEDBY_VALUE = "
|
|
1417
|
+
const MISSING_ALT_PROP = "Screen reader users cannot access this image without `alt`. Add `alt=\"image_description\"`, or `alt=\"\"` if it is decorative.";
|
|
1418
|
+
const MISSING_ALT_VALUE = "Screen reader users cannot access this image because its `alt` is empty or invalid. Add a short description, or `alt=\"\"` if it is decorative.";
|
|
1419
|
+
const ARIA_LABEL_VALUE = "Screen reader users hear nothing here because `aria-label` has no value, so give it a short description.";
|
|
1420
|
+
const ARIA_LABELLEDBY_VALUE = "Screen reader users hear nothing here because `aria-labelledby` has no value, so point it at the id of the text that labels this.";
|
|
886
1421
|
const PREFER_ALT = "Screen readers skip a decorative image more reliably with `alt=\"\"` than `role=\"presentation\"`, so use `alt=\"\"` instead.";
|
|
887
|
-
const MESSAGE_OBJECT = "
|
|
1422
|
+
const MESSAGE_OBJECT = "Screen reader users cannot use this `<object>` because assistive tech cannot describe it, so add `alt`, `aria-label`, `aria-labelledby`, `title`, or inner fallback text.";
|
|
888
1423
|
const MESSAGE_AREA = "Blind users can't use this `<area>` of the image map because screen readers can't describe it, so add `alt`, `aria-label`, or `aria-labelledby`.";
|
|
889
1424
|
const MESSAGE_INPUT_IMAGE = "Blind users can't use this image button because screen readers can't describe it, so add `alt`, `aria-label`, or `aria-labelledby`.";
|
|
890
|
-
const resolveSettings$
|
|
1425
|
+
const resolveSettings$52 = (settings) => {
|
|
891
1426
|
const reactDoctor = settings?.["react-doctor"];
|
|
892
1427
|
return typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.altText ?? {} : {};
|
|
893
1428
|
};
|
|
@@ -1003,8 +1538,8 @@ const altText = defineRule({
|
|
|
1003
1538
|
recommendation: "Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",
|
|
1004
1539
|
category: "Accessibility",
|
|
1005
1540
|
create: (context) => {
|
|
1006
|
-
if (
|
|
1007
|
-
const settings = resolveSettings$
|
|
1541
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
1542
|
+
const settings = resolveSettings$52(context.settings);
|
|
1008
1543
|
const checkImg = !settings.elements || settings.elements.includes("img");
|
|
1009
1544
|
const checkObject = !settings.elements || settings.elements.includes("object");
|
|
1010
1545
|
const checkArea = !settings.elements || settings.elements.includes("area");
|
|
@@ -1014,6 +1549,7 @@ const altText = defineRule({
|
|
|
1014
1549
|
const areaAliases = new Set(settings.area ?? []);
|
|
1015
1550
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
1016
1551
|
return { JSXOpeningElement(node) {
|
|
1552
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
1017
1553
|
const tag = getElementType(node, context.settings);
|
|
1018
1554
|
if (checkImg && (tag === "img" || imgAliases.has(tag))) {
|
|
1019
1555
|
imgRule(node, node, context);
|
|
@@ -1048,7 +1584,7 @@ const DEFAULT_AMBIGUOUS = [
|
|
|
1048
1584
|
"a link",
|
|
1049
1585
|
"learn more"
|
|
1050
1586
|
];
|
|
1051
|
-
const resolveSettings$
|
|
1587
|
+
const resolveSettings$51 = (settings) => {
|
|
1052
1588
|
const reactDoctor = settings?.["react-doctor"];
|
|
1053
1589
|
return { words: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorAmbiguousText ?? {} : {}).words ?? DEFAULT_AMBIGUOUS };
|
|
1054
1590
|
};
|
|
@@ -1089,7 +1625,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1089
1625
|
recommendation: "Name where a link goes. Avoid 'click here', 'learn more', and 'link'.",
|
|
1090
1626
|
category: "Accessibility",
|
|
1091
1627
|
create: (context) => {
|
|
1092
|
-
const settings = resolveSettings$
|
|
1628
|
+
const settings = resolveSettings$51(context.settings);
|
|
1093
1629
|
const ambiguousSet = new Set(settings.words.map((word) => word.toLowerCase()));
|
|
1094
1630
|
return { JSXElement(node) {
|
|
1095
1631
|
if (getElementType(node.openingElement, context.settings) !== "a") return;
|
|
@@ -1139,7 +1675,7 @@ const getStaticTemplateLiteralValue = (templateLiteral) => {
|
|
|
1139
1675
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
1140
1676
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
1141
1677
|
const MESSAGE_CANT_BE_ANCHOR = "Keyboard users can't trigger this link because it's a click handler with no real `href`, so use `<button>` instead.";
|
|
1142
|
-
const resolveSettings$
|
|
1678
|
+
const resolveSettings$50 = (settings) => {
|
|
1143
1679
|
const reactDoctor = settings?.["react-doctor"];
|
|
1144
1680
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorIsValid ?? {} : {};
|
|
1145
1681
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
@@ -1178,7 +1714,7 @@ const anchorIsValid = defineRule({
|
|
|
1178
1714
|
recommendation: "Give links a real destination. Use `<button>` for in-page actions.",
|
|
1179
1715
|
category: "Accessibility",
|
|
1180
1716
|
create: (context) => {
|
|
1181
|
-
const settings = resolveSettings$
|
|
1717
|
+
const settings = resolveSettings$50(context.settings);
|
|
1182
1718
|
return { JSXOpeningElement(node) {
|
|
1183
1719
|
if (getElementType(node, context.settings) !== "a") return;
|
|
1184
1720
|
let hrefAttribute;
|
|
@@ -2212,8 +2748,8 @@ const ABSTRACT_ROLES = new Set([
|
|
|
2212
2748
|
const PRESENTATION_ROLES$2 = new Set(["presentation", "none"]);
|
|
2213
2749
|
//#endregion
|
|
2214
2750
|
//#region src/plugin/rules/a11y/aria-role.ts
|
|
2215
|
-
const buildBaseMessage = (suffix) => `
|
|
2216
|
-
const resolveSettings$
|
|
2751
|
+
const buildBaseMessage = (suffix) => `This \`role\` is not a valid ARIA role, so assistive tech cannot expose it correctly. Use a real, non-abstract role.${suffix}`;
|
|
2752
|
+
const resolveSettings$49 = (settings) => {
|
|
2217
2753
|
const reactDoctor = settings?.["react-doctor"];
|
|
2218
2754
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.ariaRole ?? {} : {};
|
|
2219
2755
|
return {
|
|
@@ -2226,10 +2762,10 @@ const ariaRole = defineRule({
|
|
|
2226
2762
|
title: "Invalid ARIA role",
|
|
2227
2763
|
tags: ["react-jsx-only"],
|
|
2228
2764
|
severity: "error",
|
|
2229
|
-
recommendation: "Use a real, non-abstract ARIA role.",
|
|
2765
|
+
recommendation: "Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.",
|
|
2230
2766
|
category: "Accessibility",
|
|
2231
2767
|
create: (context) => {
|
|
2232
|
-
const settings = resolveSettings$
|
|
2768
|
+
const settings = resolveSettings$49(context.settings);
|
|
2233
2769
|
return { JSXOpeningElement(node) {
|
|
2234
2770
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
2235
2771
|
if (!roleAttribute) return;
|
|
@@ -2330,7 +2866,7 @@ const LOOP_TYPES = [
|
|
|
2330
2866
|
"WhileStatement",
|
|
2331
2867
|
"DoWhileStatement"
|
|
2332
2868
|
];
|
|
2333
|
-
const FUNCTION_LIKE_TYPES
|
|
2869
|
+
const FUNCTION_LIKE_TYPES = new Set([
|
|
2334
2870
|
"FunctionDeclaration",
|
|
2335
2871
|
"FunctionExpression",
|
|
2336
2872
|
"ArrowFunctionExpression"
|
|
@@ -3021,7 +3557,7 @@ const asyncDeferAwait = defineRule({
|
|
|
3021
3557
|
title: "await before an early-return guard",
|
|
3022
3558
|
severity: "warn",
|
|
3023
3559
|
tags: ["test-noise"],
|
|
3024
|
-
recommendation: "Move the `await` below the early-return guard so the skip path stays fast",
|
|
3560
|
+
recommendation: "Move the `await` below the early-return guard so the skip path stays fast and avoids unnecessary async work.",
|
|
3025
3561
|
create: (context) => {
|
|
3026
3562
|
const inspectStatements = (statements) => {
|
|
3027
3563
|
for (let statementIndex = 0; statementIndex < statements.length - 1; statementIndex++) {
|
|
@@ -3255,7 +3791,7 @@ const FORM_CONTROL_TAGS = new Set([
|
|
|
3255
3791
|
"select",
|
|
3256
3792
|
"form"
|
|
3257
3793
|
]);
|
|
3258
|
-
const resolveSettings$
|
|
3794
|
+
const resolveSettings$48 = (settings) => {
|
|
3259
3795
|
const reactDoctor = settings?.["react-doctor"];
|
|
3260
3796
|
return { inputComponents: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {}).inputComponents ?? [] };
|
|
3261
3797
|
};
|
|
@@ -3264,10 +3800,10 @@ const autocompleteValid = defineRule({
|
|
|
3264
3800
|
title: "Invalid autocomplete value",
|
|
3265
3801
|
tags: ["react-jsx-only"],
|
|
3266
3802
|
severity: "warn",
|
|
3267
|
-
recommendation: "Use a valid autofill token in `autoComplete
|
|
3803
|
+
recommendation: "Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",
|
|
3268
3804
|
category: "Accessibility",
|
|
3269
3805
|
create: (context) => {
|
|
3270
|
-
const settings = resolveSettings$
|
|
3806
|
+
const settings = resolveSettings$48(context.settings);
|
|
3271
3807
|
return { JSXOpeningElement: (node) => {
|
|
3272
3808
|
const tag = getElementType(node, context.settings);
|
|
3273
3809
|
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.includes(tag)) return;
|
|
@@ -3319,8 +3855,8 @@ const isCreateElementCall = (node) => {
|
|
|
3319
3855
|
//#endregion
|
|
3320
3856
|
//#region src/plugin/rules/react-builtins/button-has-type.ts
|
|
3321
3857
|
const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
|
|
3322
|
-
const INVALID_MESSAGE = "This button
|
|
3323
|
-
const resolveSettings$
|
|
3858
|
+
const INVALID_MESSAGE = "This button has an invalid `type`, so the browser may treat it like a submit button.";
|
|
3859
|
+
const resolveSettings$47 = (settings) => {
|
|
3324
3860
|
const reactDoctor = settings?.["react-doctor"];
|
|
3325
3861
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.buttonHasType ?? {} : {};
|
|
3326
3862
|
return {
|
|
@@ -3360,9 +3896,9 @@ const buttonHasType = defineRule({
|
|
|
3360
3896
|
id: "button-has-type",
|
|
3361
3897
|
title: "Button missing explicit type",
|
|
3362
3898
|
severity: "warn",
|
|
3363
|
-
recommendation: "
|
|
3899
|
+
recommendation: "Set an explicit button `type` so plain buttons do not submit forms by accident: `type=\"button\"`, `\"submit\"`, or `\"reset\"`.",
|
|
3364
3900
|
create: (context) => {
|
|
3365
|
-
const settings = resolveSettings$
|
|
3901
|
+
const settings = resolveSettings$47(context.settings);
|
|
3366
3902
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3367
3903
|
return {
|
|
3368
3904
|
JSXOpeningElement(node) {
|
|
@@ -3430,8 +3966,8 @@ const buttonHasType = defineRule({
|
|
|
3430
3966
|
//#endregion
|
|
3431
3967
|
//#region src/plugin/rules/react-builtins/checked-requires-onchange-or-readonly.ts
|
|
3432
3968
|
const MISSING_MESSAGE$1 = "Your users can't toggle this input because `checked` has no `onChange`.";
|
|
3433
|
-
const EXCLUSIVE_MESSAGE = "This input
|
|
3434
|
-
const resolveSettings$
|
|
3969
|
+
const EXCLUSIVE_MESSAGE = "This input mixes `checked` with `defaultChecked`, so React can't tell whether it is controlled or uncontrolled.";
|
|
3970
|
+
const resolveSettings$46 = (settings) => {
|
|
3435
3971
|
const reactDoctor = settings?.["react-doctor"];
|
|
3436
3972
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.checkedRequiresOnchangeOrReadonly ?? {} : {};
|
|
3437
3973
|
return {
|
|
@@ -3481,10 +4017,10 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
3481
4017
|
id: "checked-requires-onchange-or-readonly",
|
|
3482
4018
|
title: "Checked input without onChange",
|
|
3483
4019
|
severity: "warn",
|
|
3484
|
-
recommendation: "Add `onChange`
|
|
4020
|
+
recommendation: "Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",
|
|
3485
4021
|
category: "Correctness",
|
|
3486
4022
|
create: (context) => {
|
|
3487
|
-
const settings = resolveSettings$
|
|
4023
|
+
const settings = resolveSettings$46(context.settings);
|
|
3488
4024
|
const reportFromPresence = (presence) => {
|
|
3489
4025
|
if (presence.checkedNode && presence.defaultCheckedNode && !settings.ignoreExclusiveCheckedAttribute) context.report({
|
|
3490
4026
|
node: presence.checkedNode,
|
|
@@ -3628,9 +4164,6 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
3628
4164
|
} })
|
|
3629
4165
|
});
|
|
3630
4166
|
//#endregion
|
|
3631
|
-
//#region src/plugin/utils/is-member-property.ts
|
|
3632
|
-
const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === propertyName);
|
|
3633
|
-
//#endregion
|
|
3634
4167
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
3635
4168
|
const clientPassiveEventListeners = defineRule({
|
|
3636
4169
|
id: "client-passive-event-listeners",
|
|
@@ -3670,31 +4203,6 @@ const isReactComponentName = (name) => {
|
|
|
3670
4203
|
return firstCharacter >= 65 && firstCharacter <= 90;
|
|
3671
4204
|
};
|
|
3672
4205
|
//#endregion
|
|
3673
|
-
//#region src/plugin/utils/strip-paren-expression.ts
|
|
3674
|
-
const TS_WRAPPER_TYPES = new Set([
|
|
3675
|
-
"ParenthesizedExpression",
|
|
3676
|
-
"TSAsExpression",
|
|
3677
|
-
"TSSatisfiesExpression",
|
|
3678
|
-
"TSTypeAssertion",
|
|
3679
|
-
"TSNonNullExpression",
|
|
3680
|
-
"TSInstantiationExpression"
|
|
3681
|
-
]);
|
|
3682
|
-
const stripParenExpression = (node) => {
|
|
3683
|
-
let current = node;
|
|
3684
|
-
while (true) {
|
|
3685
|
-
if (TS_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) {
|
|
3686
|
-
current = current.expression;
|
|
3687
|
-
continue;
|
|
3688
|
-
}
|
|
3689
|
-
if (isNodeOfType(current, "ChainExpression") && current.expression) {
|
|
3690
|
-
current = current.expression;
|
|
3691
|
-
continue;
|
|
3692
|
-
}
|
|
3693
|
-
break;
|
|
3694
|
-
}
|
|
3695
|
-
return current;
|
|
3696
|
-
};
|
|
3697
|
-
//#endregion
|
|
3698
4206
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
3699
4207
|
const MESSAGE$48 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
3700
4208
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
@@ -3708,7 +4216,7 @@ const HTML_FOR_ATTRIBUTE = "htmlFor";
|
|
|
3708
4216
|
const LABEL_ELEMENT = "label";
|
|
3709
4217
|
const DEFAULT_DEPTH = 5;
|
|
3710
4218
|
const MAX_DEPTH = 25;
|
|
3711
|
-
const resolveSettings$
|
|
4219
|
+
const resolveSettings$45 = (settings) => {
|
|
3712
4220
|
const reactDoctor = settings?.["react-doctor"];
|
|
3713
4221
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.controlHasAssociatedLabel ?? {} : {};
|
|
3714
4222
|
return {
|
|
@@ -3828,7 +4336,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3828
4336
|
recommendation: "Give every interactive control a label screen readers can read.",
|
|
3829
4337
|
category: "Accessibility",
|
|
3830
4338
|
create: (context) => {
|
|
3831
|
-
const settings = resolveSettings$
|
|
4339
|
+
const settings = resolveSettings$45(context.settings);
|
|
3832
4340
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3833
4341
|
return { JSXElement(node) {
|
|
3834
4342
|
if (isTestlikeFile) return;
|
|
@@ -4003,7 +4511,7 @@ const noRedundantPaddingAxes = defineRule({
|
|
|
4003
4511
|
severity: "warn",
|
|
4004
4512
|
defaultEnabled: false,
|
|
4005
4513
|
category: "Architecture",
|
|
4006
|
-
recommendation: "Collapse
|
|
4514
|
+
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.",
|
|
4007
4515
|
create: (context) => ({ JSXAttribute(jsxAttribute) {
|
|
4008
4516
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
4009
4517
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
@@ -4013,7 +4521,7 @@ const noRedundantPaddingAxes = defineRule({
|
|
|
4013
4521
|
if (matchedPairs.length === 0) return;
|
|
4014
4522
|
for (const matchedPair of matchedPairs) context.report({
|
|
4015
4523
|
node: jsxAttribute,
|
|
4016
|
-
message: `px-${matchedPair.value}
|
|
4524
|
+
message: `px-${matchedPair.value} and py-${matchedPair.value} duplicate p-${matchedPair.value}, so the class list is noisier without changing spacing.`
|
|
4017
4525
|
});
|
|
4018
4526
|
} })
|
|
4019
4527
|
});
|
|
@@ -4027,7 +4535,7 @@ const noRedundantSizeAxes = defineRule({
|
|
|
4027
4535
|
severity: "warn",
|
|
4028
4536
|
defaultEnabled: false,
|
|
4029
4537
|
category: "Architecture",
|
|
4030
|
-
recommendation: "Collapse
|
|
4538
|
+
recommendation: "Collapse matching width and height to `size-N` so duplicated classes do not make layout harder to scan.",
|
|
4031
4539
|
create: (context) => ({ JSXAttribute(jsxAttribute) {
|
|
4032
4540
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
4033
4541
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
@@ -4037,7 +4545,7 @@ const noRedundantSizeAxes = defineRule({
|
|
|
4037
4545
|
if (matchedPairs.length === 0) return;
|
|
4038
4546
|
for (const matchedPair of matchedPairs) context.report({
|
|
4039
4547
|
node: jsxAttribute,
|
|
4040
|
-
message: `w-${matchedPair.value}
|
|
4548
|
+
message: `w-${matchedPair.value} and h-${matchedPair.value} duplicate size-${matchedPair.value}, so the class list is noisier without changing layout.`
|
|
4041
4549
|
});
|
|
4042
4550
|
} })
|
|
4043
4551
|
});
|
|
@@ -4072,7 +4580,7 @@ const noSpaceOnFlexChildren = defineRule({
|
|
|
4072
4580
|
const spaceValue = spaceMatch[2];
|
|
4073
4581
|
context.report({
|
|
4074
4582
|
node: jsxAttribute,
|
|
4075
|
-
message: `space-${spaceAxis}-${spaceValue} on a flex or grid parent
|
|
4583
|
+
message: `space-${spaceAxis}-${spaceValue} on a flex or grid parent can leave uneven gaps when children hide, wrap, or render in RTL layouts.`
|
|
4076
4584
|
});
|
|
4077
4585
|
} })
|
|
4078
4586
|
});
|
|
@@ -4085,14 +4593,14 @@ const noThreePeriodEllipsis = defineRule({
|
|
|
4085
4593
|
severity: "warn",
|
|
4086
4594
|
defaultEnabled: false,
|
|
4087
4595
|
category: "Architecture",
|
|
4088
|
-
recommendation: "Use the real ellipsis \"…\"
|
|
4596
|
+
recommendation: "Use the real ellipsis character (\"…\") so UI labels look polished and consistent instead of like three separate periods.",
|
|
4089
4597
|
create: (context) => ({ JSXText(jsxTextNode) {
|
|
4090
4598
|
const textValue = typeof jsxTextNode.value === "string" ? jsxTextNode.value : "";
|
|
4091
4599
|
if (!TRAILING_THREE_PERIOD_ELLIPSIS_PATTERN.test(textValue)) return;
|
|
4092
4600
|
if (isInsideExcludedTypographyAncestor(jsxTextNode)) return;
|
|
4093
4601
|
context.report({
|
|
4094
4602
|
node: jsxTextNode,
|
|
4095
|
-
message: "
|
|
4603
|
+
message: "Use the real ellipsis character (\"…\") instead of three period characters."
|
|
4096
4604
|
});
|
|
4097
4605
|
} })
|
|
4098
4606
|
});
|
|
@@ -4154,7 +4662,7 @@ const noVagueButtonLabel = defineRule({
|
|
|
4154
4662
|
if (!VAGUE_BUTTON_LABELS.has(normalizedLabel)) return;
|
|
4155
4663
|
context.report({
|
|
4156
4664
|
node: jsxElementNode.openingElement ?? jsxElementNode,
|
|
4157
|
-
message: `Screen reader
|
|
4665
|
+
message: `Screen reader users may not know what "${labelText}" does. Use a specific action label.`
|
|
4158
4666
|
});
|
|
4159
4667
|
} })
|
|
4160
4668
|
});
|
|
@@ -4199,7 +4707,7 @@ const DEFAULT_ADDITIONAL_HOCS = [
|
|
|
4199
4707
|
"lazy",
|
|
4200
4708
|
"withTracking"
|
|
4201
4709
|
];
|
|
4202
|
-
const resolveSettings$
|
|
4710
|
+
const resolveSettings$44 = (settings) => {
|
|
4203
4711
|
const reactDoctor = settings?.["react-doctor"];
|
|
4204
4712
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
|
|
4205
4713
|
const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
|
|
@@ -4391,7 +4899,7 @@ const displayName = defineRule({
|
|
|
4391
4899
|
recommendation: "Give each component a `displayName` so DevTools shows a clear name.",
|
|
4392
4900
|
category: "Architecture",
|
|
4393
4901
|
create: (context) => {
|
|
4394
|
-
const settings = resolveSettings$
|
|
4902
|
+
const settings = resolveSettings$44(context.settings);
|
|
4395
4903
|
const ignoreNamed = settings.ignoreTranspilerName ? false : true;
|
|
4396
4904
|
const reportAt = (node) => {
|
|
4397
4905
|
context.report({
|
|
@@ -4740,10 +5248,10 @@ const effectNeedsCleanup = defineRule({
|
|
|
4740
5248
|
if (usages.length === 0) return;
|
|
4741
5249
|
if (effectHasCleanupReturn(callback, usages)) return;
|
|
4742
5250
|
const firstUsage = usages[0];
|
|
4743
|
-
const
|
|
5251
|
+
const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
|
|
4744
5252
|
context.report({
|
|
4745
5253
|
node,
|
|
4746
|
-
message: `\`${firstUsage.resourceName}
|
|
5254
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
4747
5255
|
});
|
|
4748
5256
|
} })
|
|
4749
5257
|
});
|
|
@@ -5391,21 +5899,21 @@ const isOutsideAllFunctions = (symbol) => {
|
|
|
5391
5899
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-messages.ts
|
|
5392
5900
|
const buildMissingDepMessage = (hookName, depName) => `\`${hookName}\` can run with a stale \`${depName}\` & show your users old data.`;
|
|
5393
5901
|
const buildUnnecessaryDepMessage = (hookName, depName) => `\`${hookName}\` re-runs whenever \`${depName}\` changes even though it never uses it.`;
|
|
5394
|
-
const buildDuplicateDepMessage = (hookName, depName) => `\`${hookName}\` lists \`${depName}\` twice
|
|
5395
|
-
const buildLiteralDepMessage = (hookName) => `A literal in \`${hookName}\`'s dependency array never changes
|
|
5902
|
+
const buildDuplicateDepMessage = (hookName, depName) => `\`${hookName}\` lists \`${depName}\` twice, adding dependency-array noise without changing when it runs.`;
|
|
5903
|
+
const buildLiteralDepMessage = (hookName) => `A literal in \`${hookName}\`'s dependency array never changes, so it adds noise without protecting against stale values.`;
|
|
5396
5904
|
const buildRefCurrentDepMessage = (hookName, depName) => `\`${hookName}\` won't re-run when \`${depName}\` changes, since a ref never triggers a redraw.`;
|
|
5397
|
-
const buildNonArrayDepsMessage = (hookName) => `\`${hookName}\`'s dependencies can't be checked because its second argument isn't an inline array.`;
|
|
5905
|
+
const buildNonArrayDepsMessage = (hookName) => `\`${hookName}\`'s dependencies can't be checked because its second argument isn't an inline array, so stale values can slip through.`;
|
|
5398
5906
|
const buildMissingDepArrayMessage = (hookName) => `\`${hookName}\` re-runs on every render with no dependency array.`;
|
|
5399
5907
|
const buildMissingCallbackMessage = (hookName) => `\`${hookName}\` crashes without a function as its first argument.`;
|
|
5400
|
-
const buildEffectEventDepMessage = () => `A function from \`useEffectEvent\` is stable
|
|
5401
|
-
const buildSpreadDepMessage = (hookName) =>
|
|
5402
|
-
const buildComplexDepMessage = (hookName) =>
|
|
5908
|
+
const buildEffectEventDepMessage = () => `A function from \`useEffectEvent\` is stable, so listing it adds noise and defeats the event/dependency split.`;
|
|
5909
|
+
const buildSpreadDepMessage = (hookName) => `A spread in \`${hookName}\`'s dependency array hides the actual deps, so stale values can slip through.`;
|
|
5910
|
+
const buildComplexDepMessage = (hookName) => `A complex expression in \`${hookName}\`'s dependency array hides the real value, so stale values can slip through.`;
|
|
5403
5911
|
const buildAsyncEffectMessage = (hookName) => `\`${hookName}\` was given an async function, so its cleanup breaks.`;
|
|
5404
|
-
const buildUnknownCallbackMessage = (hookName) => `\`${hookName}\`'s dependencies can't be checked
|
|
5912
|
+
const buildUnknownCallbackMessage = (hookName) => `\`${hookName}\`'s callback is defined elsewhere, so dependencies can't be checked and stale values can slip through.`;
|
|
5405
5913
|
const buildUnstableDepMessage = (hookName, depName) => `\`${depName}\` is rebuilt every render, so \`${hookName}\` runs every time.`;
|
|
5406
5914
|
const buildSetStateWithoutDepsMessage = (hookName, setterName) => `\`${hookName}\` calls \`${setterName}\` with no dependency array, so it can loop forever & freeze the component.`;
|
|
5407
5915
|
const buildRefCleanupMessage = (depName) => `Your cleanup may read the wrong node since the ref \`${depName}\` can change before it runs.`;
|
|
5408
|
-
const buildAssignmentMessage = (name) => `Assigning to \`${name}\` inside a hook is thrown away after each render.`;
|
|
5916
|
+
const buildAssignmentMessage = (name) => `Assigning to \`${name}\` inside a hook is thrown away after each render, so the next render reads the old value.`;
|
|
5409
5917
|
//#endregion
|
|
5410
5918
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-settings.ts
|
|
5411
5919
|
const resolveExhaustiveDepsSettings = (settings) => {
|
|
@@ -6263,7 +6771,7 @@ const compileGlob = (pattern) => {
|
|
|
6263
6771
|
//#endregion
|
|
6264
6772
|
//#region src/plugin/rules/react-builtins/forbid-component-props.ts
|
|
6265
6773
|
const DEFAULT_FORBID_PROPS = ["className", "style"];
|
|
6266
|
-
const resolveSettings$
|
|
6774
|
+
const resolveSettings$43 = (settings) => {
|
|
6267
6775
|
const reactDoctor = settings?.["react-doctor"];
|
|
6268
6776
|
const forbid = (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidComponentProps ?? {} : {}).forbid;
|
|
6269
6777
|
if (!forbid || forbid.length === 0) return { forbid: DEFAULT_FORBID_PROPS };
|
|
@@ -6301,27 +6809,27 @@ const isForbiddenForTag = (entry, tag) => {
|
|
|
6301
6809
|
if (entry.allowedForPatterns.some((regex) => regex.test(tag))) return false;
|
|
6302
6810
|
return true;
|
|
6303
6811
|
};
|
|
6304
|
-
const flattenJsxName
|
|
6812
|
+
const flattenJsxName = (name) => {
|
|
6305
6813
|
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
6306
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName
|
|
6814
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName(name.object)}.${name.property.name}`;
|
|
6307
6815
|
if (name.type === "ThisExpression" || name.type === "JSXThisExpression") return "this";
|
|
6308
6816
|
return "";
|
|
6309
6817
|
};
|
|
6310
6818
|
const isSupportedJsxName = (name) => isNodeOfType(name, "JSXIdentifier") || isNodeOfType(name, "JSXMemberExpression");
|
|
6311
|
-
const buildMessage$24 = (propName, message) => message ?? `Your project blocks the \`${propName}\` prop on this component.`;
|
|
6819
|
+
const buildMessage$24 = (propName, message) => message ?? `Your project blocks the \`${propName}\` prop on this component, so this bypasses the component API contract.`;
|
|
6312
6820
|
const forbidComponentProps = defineRule({
|
|
6313
6821
|
id: "forbid-component-props",
|
|
6314
|
-
title: "
|
|
6822
|
+
title: "Blocked component prop bypasses API contract",
|
|
6315
6823
|
severity: "warn",
|
|
6316
6824
|
defaultEnabled: false,
|
|
6317
|
-
recommendation: "
|
|
6825
|
+
recommendation: "Configure blocked component props so callers cannot bypass the component API contract.",
|
|
6318
6826
|
category: "Architecture",
|
|
6319
6827
|
create: (context) => {
|
|
6320
|
-
const entries = resolveSettings$
|
|
6828
|
+
const entries = resolveSettings$43(context.settings).forbid.map(normalizeEntry);
|
|
6321
6829
|
return { JSXOpeningElement(node) {
|
|
6322
6830
|
if (entries.length === 0) return;
|
|
6323
6831
|
if (!isSupportedJsxName(node.name)) return;
|
|
6324
|
-
const tag = flattenJsxName
|
|
6832
|
+
const tag = flattenJsxName(node.name);
|
|
6325
6833
|
if (!tag) return;
|
|
6326
6834
|
if (!(isReactComponentName(tag.split(".")[0]) || tag.includes("."))) return;
|
|
6327
6835
|
for (const attribute of node.attributes) {
|
|
@@ -6345,8 +6853,8 @@ const forbidComponentProps = defineRule({
|
|
|
6345
6853
|
});
|
|
6346
6854
|
//#endregion
|
|
6347
6855
|
//#region src/plugin/rules/react-builtins/forbid-dom-props.ts
|
|
6348
|
-
const buildMessage$23 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags.`;
|
|
6349
|
-
const resolveSettings$
|
|
6856
|
+
const buildMessage$23 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags, so this bypasses the agreed DOM API contract.`;
|
|
6857
|
+
const resolveSettings$42 = (settings) => {
|
|
6350
6858
|
const reactDoctor = settings?.["react-doctor"];
|
|
6351
6859
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
|
|
6352
6860
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -6363,12 +6871,12 @@ const resolveSettings$43 = (settings) => {
|
|
|
6363
6871
|
};
|
|
6364
6872
|
const forbidDomProps = defineRule({
|
|
6365
6873
|
id: "forbid-dom-props",
|
|
6366
|
-
title: "
|
|
6874
|
+
title: "Blocked DOM prop bypasses project contract",
|
|
6367
6875
|
severity: "warn",
|
|
6368
|
-
recommendation: "
|
|
6876
|
+
recommendation: "Configure blocked DOM props so plain HTML tags stay on the agreed DOM API surface.",
|
|
6369
6877
|
category: "Architecture",
|
|
6370
6878
|
create: (context) => {
|
|
6371
|
-
const forbidMap = resolveSettings$
|
|
6879
|
+
const forbidMap = resolveSettings$42(context.settings);
|
|
6372
6880
|
return { JSXOpeningElement(node) {
|
|
6373
6881
|
if (forbidMap.size === 0) return;
|
|
6374
6882
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -6416,34 +6924,6 @@ const flattenCalleeName = (callee) => {
|
|
|
6416
6924
|
return null;
|
|
6417
6925
|
};
|
|
6418
6926
|
//#endregion
|
|
6419
|
-
//#region src/plugin/utils/flatten-jsx-name.ts
|
|
6420
|
-
/**
|
|
6421
|
-
* Flattens a JSX opener / member-expression chain into a dotted name.
|
|
6422
|
-
*
|
|
6423
|
-
* `<Foo />` → `"Foo"`
|
|
6424
|
-
* `<Namespace.Foo />` → `"Namespace.Foo"`
|
|
6425
|
-
* `<a.b.c />` → `"a.b.c"`
|
|
6426
|
-
*
|
|
6427
|
-
* Returns `null` when the chain root isn't a `JSXIdentifier` (e.g.
|
|
6428
|
-
* the rare-but-valid `<this.x />` case, which JSX surfaces as a
|
|
6429
|
-
* `JSXThisExpression` root).
|
|
6430
|
-
*
|
|
6431
|
-
* Used by `forbid-elements` and `jsx-props-no-spreading`. Two other
|
|
6432
|
-
* rules (`forbid-component-props`, `utils/get-element-type`) keep
|
|
6433
|
-
* their own extended variants because they handle additional node
|
|
6434
|
-
* types (ThisExpression / JSXNamespacedName) and return a non-
|
|
6435
|
-
* nullable string with a sentinel value — semantically distinct.
|
|
6436
|
-
*/
|
|
6437
|
-
const flattenJsxName = (node) => {
|
|
6438
|
-
if (isNodeOfType(node, "JSXIdentifier")) return node.name;
|
|
6439
|
-
if (isNodeOfType(node, "JSXMemberExpression")) {
|
|
6440
|
-
const objectName = flattenJsxName(node.object);
|
|
6441
|
-
if (!objectName) return null;
|
|
6442
|
-
return `${objectName}.${node.property.name}`;
|
|
6443
|
-
}
|
|
6444
|
-
return null;
|
|
6445
|
-
};
|
|
6446
|
-
//#endregion
|
|
6447
6927
|
//#region src/plugin/utils/is-react-function-call.ts
|
|
6448
6928
|
const PRAGMA = "React";
|
|
6449
6929
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
@@ -6454,8 +6934,8 @@ const isReactFunctionCall = (node, expectedCall) => {
|
|
|
6454
6934
|
};
|
|
6455
6935
|
//#endregion
|
|
6456
6936
|
//#region src/plugin/rules/react-builtins/forbid-elements.ts
|
|
6457
|
-
const buildMessage$22 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here.`;
|
|
6458
|
-
const resolveSettings$
|
|
6937
|
+
const buildMessage$22 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here, so code stays on the approved UI surface.`;
|
|
6938
|
+
const resolveSettings$41 = (settings) => {
|
|
6459
6939
|
const reactDoctor = settings?.["react-doctor"];
|
|
6460
6940
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
|
|
6461
6941
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -6466,16 +6946,16 @@ const resolveSettings$42 = (settings) => {
|
|
|
6466
6946
|
const flattenMemberName$1 = flattenCalleeName;
|
|
6467
6947
|
const forbidElements = defineRule({
|
|
6468
6948
|
id: "forbid-elements",
|
|
6469
|
-
title: "
|
|
6949
|
+
title: "Blocked element bypasses approved UI primitives",
|
|
6470
6950
|
severity: "warn",
|
|
6471
|
-
recommendation: "
|
|
6951
|
+
recommendation: "Configure blocked elements so code stays on the approved UI primitives.",
|
|
6472
6952
|
category: "Architecture",
|
|
6473
6953
|
create: (context) => {
|
|
6474
|
-
const forbidMap = resolveSettings$
|
|
6954
|
+
const forbidMap = resolveSettings$41(context.settings);
|
|
6475
6955
|
return {
|
|
6476
6956
|
JSXOpeningElement(node) {
|
|
6477
6957
|
if (forbidMap.size === 0) return;
|
|
6478
|
-
const fullName = flattenJsxName(node.name);
|
|
6958
|
+
const fullName = flattenJsxName$1(node.name);
|
|
6479
6959
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
6480
6960
|
context.report({
|
|
6481
6961
|
node: node.name,
|
|
@@ -6512,7 +6992,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
6512
6992
|
id: "forward-ref-uses-ref",
|
|
6513
6993
|
title: "forwardRef without ref parameter",
|
|
6514
6994
|
severity: "warn",
|
|
6515
|
-
recommendation: "
|
|
6995
|
+
recommendation: "Accept the `ref` parameter or drop `forwardRef` so parents are not promised a ref that never reaches the node.",
|
|
6516
6996
|
category: "Architecture",
|
|
6517
6997
|
create: (context) => ({ CallExpression(node) {
|
|
6518
6998
|
if (getCalleeName$1(node) !== "forwardRef") return;
|
|
@@ -6542,7 +7022,7 @@ const DEFAULT_HEADING_TAGS = [
|
|
|
6542
7022
|
"h5",
|
|
6543
7023
|
"h6"
|
|
6544
7024
|
];
|
|
6545
|
-
const resolveSettings$
|
|
7025
|
+
const resolveSettings$40 = (settings) => {
|
|
6546
7026
|
const reactDoctor = settings?.["react-doctor"];
|
|
6547
7027
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
|
|
6548
7028
|
return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
|
|
@@ -6555,7 +7035,7 @@ const headingHasContent = defineRule({
|
|
|
6555
7035
|
recommendation: "Put readable text in every heading.",
|
|
6556
7036
|
category: "Accessibility",
|
|
6557
7037
|
create: (context) => {
|
|
6558
|
-
const settings = resolveSettings$
|
|
7038
|
+
const settings = resolveSettings$40(context.settings);
|
|
6559
7039
|
return { JSXOpeningElement(node) {
|
|
6560
7040
|
const elementType = getElementType(node, context.settings);
|
|
6561
7041
|
if (!settings.headingTags.includes(elementType)) return;
|
|
@@ -6573,9 +7053,9 @@ const headingHasContent = defineRule({
|
|
|
6573
7053
|
});
|
|
6574
7054
|
//#endregion
|
|
6575
7055
|
//#region src/plugin/rules/react-builtins/hook-use-state.ts
|
|
6576
|
-
const REQUIRE_DESTRUCTURE_MESSAGE = "
|
|
6577
|
-
const NAMING_CONVENTION_MESSAGE = "
|
|
6578
|
-
const resolveSettings$
|
|
7056
|
+
const REQUIRE_DESTRUCTURE_MESSAGE = "`useState` should be destructured as `[value, setValue]` so readers can see the state value and setter together.";
|
|
7057
|
+
const NAMING_CONVENTION_MESSAGE = "This `useState` setter does not match its value name, so updates are harder to trace.";
|
|
7058
|
+
const resolveSettings$39 = (settings) => {
|
|
6579
7059
|
const reactDoctor = settings?.["react-doctor"];
|
|
6580
7060
|
return { allowDestructuredState: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.hookUseState ?? {} : {}).allowDestructuredState ?? false };
|
|
6581
7061
|
};
|
|
@@ -6599,10 +7079,10 @@ const hookUseState = defineRule({
|
|
|
6599
7079
|
title: "useState not destructured",
|
|
6600
7080
|
severity: "warn",
|
|
6601
7081
|
defaultEnabled: false,
|
|
6602
|
-
recommendation: "Destructure useState as `const [thing, setThing] = useState(…)
|
|
7082
|
+
recommendation: "Destructure `useState` as `const [thing, setThing] = useState(…)` so state reads and writes stay visible together.",
|
|
6603
7083
|
category: "Architecture",
|
|
6604
7084
|
create: (context) => {
|
|
6605
|
-
const { allowDestructuredState } = resolveSettings$
|
|
7085
|
+
const { allowDestructuredState } = resolveSettings$39(context.settings);
|
|
6606
7086
|
return { CallExpression(node) {
|
|
6607
7087
|
if (!isReactFunctionCall(node, "useState")) return;
|
|
6608
7088
|
const parent = node.parent;
|
|
@@ -6677,7 +7157,7 @@ const HOOKS_WITH_DEP_ARRAY = new Set([
|
|
|
6677
7157
|
"useMemo",
|
|
6678
7158
|
"useImperativeHandle"
|
|
6679
7159
|
]);
|
|
6680
|
-
const NAN_MESSAGE = "`NaN` in a dependency array
|
|
7160
|
+
const NAN_MESSAGE = "`NaN` in a dependency array never compares as changed with `Object.is`, so normalize the value before passing it as a dependency.";
|
|
6681
7161
|
const isNanLiteral = (node) => {
|
|
6682
7162
|
if (isNodeOfType(node, "Identifier") && node.name === "NaN") return true;
|
|
6683
7163
|
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "Number" && isNodeOfType(node.property, "Identifier") && node.property.name === "NaN") return true;
|
|
@@ -6705,7 +7185,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
6705
7185
|
//#endregion
|
|
6706
7186
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
6707
7187
|
const MESSAGE$44 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
6708
|
-
const resolveSettings$
|
|
7188
|
+
const resolveSettings$38 = (settings) => {
|
|
6709
7189
|
const reactDoctor = settings?.["react-doctor"];
|
|
6710
7190
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
6711
7191
|
};
|
|
@@ -6742,7 +7222,7 @@ const htmlHasLang = defineRule({
|
|
|
6742
7222
|
recommendation: "Set `<html lang=\"…\">` so screen readers know the page language.",
|
|
6743
7223
|
category: "Accessibility",
|
|
6744
7224
|
create: (context) => {
|
|
6745
|
-
const settings = resolveSettings$
|
|
7225
|
+
const settings = resolveSettings$38(context.settings);
|
|
6746
7226
|
const tagSet = new Set(settings.htmlTags);
|
|
6747
7227
|
return { JSXOpeningElement(node) {
|
|
6748
7228
|
const tag = getElementType(node, context.settings);
|
|
@@ -6980,7 +7460,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
6980
7460
|
});
|
|
6981
7461
|
//#endregion
|
|
6982
7462
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
6983
|
-
const MESSAGE$43 = "
|
|
7463
|
+
const MESSAGE$43 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
6984
7464
|
const evaluateTitleValue = (value) => {
|
|
6985
7465
|
if (!value) return "missing";
|
|
6986
7466
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -7010,7 +7490,7 @@ const iframeHasTitle = defineRule({
|
|
|
7010
7490
|
title: "iframe missing title",
|
|
7011
7491
|
tags: ["react-jsx-only"],
|
|
7012
7492
|
severity: "warn",
|
|
7013
|
-
recommendation: "Add a descriptive `title`
|
|
7493
|
+
recommendation: "Add a descriptive `title` so screen reader users know what the embedded frame contains.",
|
|
7014
7494
|
category: "Accessibility",
|
|
7015
7495
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
7016
7496
|
const tag = getElementType(node, context.settings);
|
|
@@ -7078,7 +7558,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
7078
7558
|
id: "iframe-missing-sandbox",
|
|
7079
7559
|
title: "iframe missing sandbox attribute",
|
|
7080
7560
|
severity: "warn",
|
|
7081
|
-
recommendation: "Add `sandbox=\"\"`
|
|
7561
|
+
recommendation: "Add `sandbox=\"\"` or a curated value so embedded pages cannot get full access to your site by default.",
|
|
7082
7562
|
category: "Security",
|
|
7083
7563
|
create: (context) => ({
|
|
7084
7564
|
JSXOpeningElement(node) {
|
|
@@ -7138,7 +7618,7 @@ const DEFAULT_REDUNDANT_WORDS = [
|
|
|
7138
7618
|
"photo",
|
|
7139
7619
|
"picture"
|
|
7140
7620
|
];
|
|
7141
|
-
const resolveSettings$
|
|
7621
|
+
const resolveSettings$37 = (settings) => {
|
|
7142
7622
|
const reactDoctor = settings?.["react-doctor"];
|
|
7143
7623
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.imgRedundantAlt ?? {} : {};
|
|
7144
7624
|
return {
|
|
@@ -7185,8 +7665,10 @@ const imgRedundantAlt = defineRule({
|
|
|
7185
7665
|
recommendation: "Do not put 'image' or 'photo' in alt text. Describe what is shown.",
|
|
7186
7666
|
category: "Accessibility",
|
|
7187
7667
|
create: (context) => {
|
|
7188
|
-
|
|
7668
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
7669
|
+
const settings = resolveSettings$37(context.settings);
|
|
7189
7670
|
return { JSXOpeningElement(node) {
|
|
7671
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
7190
7672
|
const tag = getElementType(node, context.settings);
|
|
7191
7673
|
if (!settings.components.includes(tag)) return;
|
|
7192
7674
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
@@ -7275,7 +7757,7 @@ const DEFAULT_TABBABLE_ROLES = [
|
|
|
7275
7757
|
"switch",
|
|
7276
7758
|
"textbox"
|
|
7277
7759
|
];
|
|
7278
|
-
const resolveSettings$
|
|
7760
|
+
const resolveSettings$36 = (settings) => {
|
|
7279
7761
|
const reactDoctor = settings?.["react-doctor"];
|
|
7280
7762
|
return { tabbable: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.interactiveSupportsFocus ?? {} : {}).tabbable ?? DEFAULT_TABBABLE_ROLES };
|
|
7281
7763
|
};
|
|
@@ -7284,10 +7766,10 @@ const interactiveSupportsFocus = defineRule({
|
|
|
7284
7766
|
title: "Interactive element not focusable",
|
|
7285
7767
|
tags: ["react-jsx-only"],
|
|
7286
7768
|
severity: "warn",
|
|
7287
|
-
recommendation: "Add
|
|
7769
|
+
recommendation: "Add keyboard focus support so users can reach interactive elements without a pointer.",
|
|
7288
7770
|
category: "Accessibility",
|
|
7289
7771
|
create: (context) => {
|
|
7290
|
-
const settings = resolveSettings$
|
|
7772
|
+
const settings = resolveSettings$36(context.settings);
|
|
7291
7773
|
const tabbableSet = new Set(settings.tabbable);
|
|
7292
7774
|
return { JSXOpeningElement(node) {
|
|
7293
7775
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -7308,88 +7790,6 @@ const interactiveSupportsFocus = defineRule({
|
|
|
7308
7790
|
}
|
|
7309
7791
|
});
|
|
7310
7792
|
//#endregion
|
|
7311
|
-
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
7312
|
-
const collectFromProgram = (programRoot) => {
|
|
7313
|
-
const lookup = /* @__PURE__ */ new Map();
|
|
7314
|
-
const visit = (node) => {
|
|
7315
|
-
if (node.type === "ImportDeclaration" && "source" in node && node.source) {
|
|
7316
|
-
const source = node.source.value;
|
|
7317
|
-
if (typeof source !== "string") return;
|
|
7318
|
-
if ("specifiers" in node && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
|
|
7319
|
-
if (!("local" in specifier) || !specifier.local) continue;
|
|
7320
|
-
const local = specifier.local;
|
|
7321
|
-
if (typeof local.name !== "string") continue;
|
|
7322
|
-
if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
|
|
7323
|
-
source,
|
|
7324
|
-
imported: null,
|
|
7325
|
-
isDefault: true,
|
|
7326
|
-
isNamespace: false
|
|
7327
|
-
});
|
|
7328
|
-
else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
|
|
7329
|
-
source,
|
|
7330
|
-
imported: null,
|
|
7331
|
-
isDefault: false,
|
|
7332
|
-
isNamespace: true
|
|
7333
|
-
});
|
|
7334
|
-
else if (specifier.type === "ImportSpecifier") {
|
|
7335
|
-
const importedNode = specifier.imported;
|
|
7336
|
-
const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
|
|
7337
|
-
lookup.set(local.name, {
|
|
7338
|
-
source,
|
|
7339
|
-
imported: importedName,
|
|
7340
|
-
isDefault: false,
|
|
7341
|
-
isNamespace: false
|
|
7342
|
-
});
|
|
7343
|
-
}
|
|
7344
|
-
}
|
|
7345
|
-
return;
|
|
7346
|
-
}
|
|
7347
|
-
const nodeRecord = node;
|
|
7348
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
7349
|
-
if (key === "parent") continue;
|
|
7350
|
-
const child = nodeRecord[key];
|
|
7351
|
-
if (Array.isArray(child)) {
|
|
7352
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
7353
|
-
} else if (isAstNode(child)) visit(child);
|
|
7354
|
-
}
|
|
7355
|
-
};
|
|
7356
|
-
visit(programRoot);
|
|
7357
|
-
return lookup;
|
|
7358
|
-
};
|
|
7359
|
-
const importLookupCache = /* @__PURE__ */ new WeakMap();
|
|
7360
|
-
const getImportLookup = (node) => {
|
|
7361
|
-
const programRoot = findProgramRoot(node);
|
|
7362
|
-
if (!programRoot) return null;
|
|
7363
|
-
let cached = importLookupCache.get(programRoot);
|
|
7364
|
-
if (!cached) {
|
|
7365
|
-
cached = collectFromProgram(programRoot);
|
|
7366
|
-
importLookupCache.set(programRoot, cached);
|
|
7367
|
-
}
|
|
7368
|
-
return cached;
|
|
7369
|
-
};
|
|
7370
|
-
const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7371
|
-
const lookup = getImportLookup(contextNode);
|
|
7372
|
-
if (!lookup) return false;
|
|
7373
|
-
const info = lookup.get(localIdentifierName);
|
|
7374
|
-
if (!info) return false;
|
|
7375
|
-
return info.source === moduleSource;
|
|
7376
|
-
};
|
|
7377
|
-
const isNamespaceImportFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7378
|
-
const lookup = getImportLookup(contextNode);
|
|
7379
|
-
if (!lookup) return false;
|
|
7380
|
-
const info = lookup.get(localIdentifierName);
|
|
7381
|
-
if (!info) return false;
|
|
7382
|
-
return info.isNamespace && info.source === moduleSource;
|
|
7383
|
-
};
|
|
7384
|
-
const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7385
|
-
const lookup = getImportLookup(contextNode);
|
|
7386
|
-
if (!lookup) return null;
|
|
7387
|
-
const info = lookup.get(localIdentifierName);
|
|
7388
|
-
if (!info) return null;
|
|
7389
|
-
if (info.source !== moduleSource) return null;
|
|
7390
|
-
return info.imported;
|
|
7391
|
-
};
|
|
7392
|
-
//#endregion
|
|
7393
7793
|
//#region src/plugin/rules/jotai/jotai-derived-atom-returns-fresh-object.ts
|
|
7394
7794
|
const isAtomFromJotai = (callExpression) => {
|
|
7395
7795
|
if (!isNodeOfType(callExpression.callee, "Identifier")) return false;
|
|
@@ -8184,7 +8584,7 @@ const jsHoistRegexp = defineRule({
|
|
|
8184
8584
|
create: (context) => createLoopAwareVisitors({ NewExpression(node) {
|
|
8185
8585
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "RegExp") context.report({
|
|
8186
8586
|
node,
|
|
8187
|
-
message: "
|
|
8587
|
+
message: "`new RegExp()` rebuilds the pattern on every loop pass. Move it to a constant outside the loop."
|
|
8188
8588
|
});
|
|
8189
8589
|
} })
|
|
8190
8590
|
});
|
|
@@ -8558,7 +8958,7 @@ const jsSetMapLookups = defineRule({
|
|
|
8558
8958
|
if (isIndexedArrayElementWithStringArgument(node.callee.object, node.arguments?.[0])) return;
|
|
8559
8959
|
context.report({
|
|
8560
8960
|
node,
|
|
8561
|
-
message: `This
|
|
8961
|
+
message: `This scales poorly because \`array.${methodName}()\` inside a loop scans the whole list every time. Use a Set for constant-time lookups.`
|
|
8562
8962
|
});
|
|
8563
8963
|
} })
|
|
8564
8964
|
});
|
|
@@ -8569,7 +8969,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
8569
8969
|
title: "Spread copy before sort()",
|
|
8570
8970
|
tags: ["test-noise"],
|
|
8571
8971
|
severity: "warn",
|
|
8572
|
-
disabledBy: ["react-native"],
|
|
8972
|
+
disabledBy: ["react-native", "pre-es2023"],
|
|
8573
8973
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
8574
8974
|
create: (context) => ({ CallExpression(node) {
|
|
8575
8975
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
@@ -8582,10 +8982,10 @@ const jsTosortedImmutable = defineRule({
|
|
|
8582
8982
|
});
|
|
8583
8983
|
//#endregion
|
|
8584
8984
|
//#region src/plugin/rules/react-builtins/jsx-boolean-value.ts
|
|
8585
|
-
const NEVER_MESSAGE$2 = () =>
|
|
8586
|
-
const ALWAYS_MESSAGE$2 = () =>
|
|
8587
|
-
const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing.`;
|
|
8588
|
-
const resolveSettings$
|
|
8985
|
+
const NEVER_MESSAGE$2 = () => "This boolean prop style disagrees with the project setting, so equivalent true props are harder to scan consistently.";
|
|
8986
|
+
const ALWAYS_MESSAGE$2 = () => "This boolean prop style disagrees with the project setting, so equivalent true props are harder to scan consistently.";
|
|
8987
|
+
const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing, so the explicit false value adds noise without changing output.`;
|
|
8988
|
+
const resolveSettings$35 = (settings) => {
|
|
8589
8989
|
const reactDoctor = settings?.["react-doctor"];
|
|
8590
8990
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxBooleanValue ?? {} : {};
|
|
8591
8991
|
return {
|
|
@@ -8600,10 +9000,10 @@ const jsxBooleanValue = defineRule({
|
|
|
8600
9000
|
title: "Inconsistent boolean prop notation",
|
|
8601
9001
|
severity: "warn",
|
|
8602
9002
|
defaultEnabled: false,
|
|
8603
|
-
recommendation: "
|
|
9003
|
+
recommendation: "Use one boolean-attribute style so equivalent true props scan the same across the codebase.",
|
|
8604
9004
|
category: "Architecture",
|
|
8605
9005
|
create: (context) => {
|
|
8606
|
-
const settings = resolveSettings$
|
|
9006
|
+
const settings = resolveSettings$35(context.settings);
|
|
8607
9007
|
const alwaysSet = new Set(settings.always);
|
|
8608
9008
|
const neverSet = new Set(settings.never);
|
|
8609
9009
|
return { JSXAttribute(node) {
|
|
@@ -8654,10 +9054,10 @@ const jsxBooleanValue = defineRule({
|
|
|
8654
9054
|
});
|
|
8655
9055
|
//#endregion
|
|
8656
9056
|
//#region src/plugin/rules/react-builtins/jsx-curly-brace-presence.ts
|
|
8657
|
-
const UNNECESSARY_BRACES_MESSAGE = "These curly braces
|
|
8658
|
-
const REQUIRED_BRACES_MESSAGE = "This value needs
|
|
9057
|
+
const UNNECESSARY_BRACES_MESSAGE = "These curly braces wrap a literal value, so they add JSX noise without changing output.";
|
|
9058
|
+
const REQUIRED_BRACES_MESSAGE = "This JSX value needs `{ }` so React reads it as an expression instead of text.";
|
|
8659
9059
|
const isAllowedMode = (value) => value === "always" || value === "never" || value === "ignore";
|
|
8660
|
-
const resolveSettings$
|
|
9060
|
+
const resolveSettings$34 = (settings) => {
|
|
8661
9061
|
const reactDoctor = settings?.["react-doctor"];
|
|
8662
9062
|
const ruleSettingsRaw = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxCurlyBracePresence : void 0;
|
|
8663
9063
|
if (isAllowedMode(ruleSettingsRaw)) return {
|
|
@@ -8750,10 +9150,10 @@ const jsxCurlyBracePresence = defineRule({
|
|
|
8750
9150
|
title: "Unnecessary curly braces in JSX",
|
|
8751
9151
|
severity: "warn",
|
|
8752
9152
|
defaultEnabled: false,
|
|
8753
|
-
recommendation: "
|
|
9153
|
+
recommendation: "Use one JSX literal style so equivalent markup scans the same without extra JSX noise.",
|
|
8754
9154
|
category: "Architecture",
|
|
8755
9155
|
create: (context) => {
|
|
8756
|
-
const settings = resolveSettings$
|
|
9156
|
+
const settings = resolveSettings$34(context.settings);
|
|
8757
9157
|
return {
|
|
8758
9158
|
JSXAttribute(node) {
|
|
8759
9159
|
const value = node.value;
|
|
@@ -8834,9 +9234,9 @@ const containsJsxElement = (root) => {
|
|
|
8834
9234
|
};
|
|
8835
9235
|
//#endregion
|
|
8836
9236
|
//#region src/plugin/rules/react-builtins/jsx-filename-extension.ts
|
|
8837
|
-
const JSX_NOT_ALLOWED = (extension) => `This file
|
|
8838
|
-
const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are
|
|
8839
|
-
const resolveSettings$
|
|
9237
|
+
const JSX_NOT_ALLOWED = (extension) => `This file contains JSX but uses a \`${extension}\` name, so the filename no longer signals JSX to readers or tooling conventions.`;
|
|
9238
|
+
const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are reserved for JSX here, so using that extension without JSX makes file-type conventions less useful.`;
|
|
9239
|
+
const resolveSettings$33 = (settings) => {
|
|
8840
9240
|
const reactDoctor = settings?.["react-doctor"];
|
|
8841
9241
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFilenameExtension ?? {} : {};
|
|
8842
9242
|
return {
|
|
@@ -8855,10 +9255,10 @@ const jsxFilenameExtension = defineRule({
|
|
|
8855
9255
|
title: "JSX in disallowed file extension",
|
|
8856
9256
|
severity: "warn",
|
|
8857
9257
|
defaultEnabled: false,
|
|
8858
|
-
recommendation: "Name files with
|
|
9258
|
+
recommendation: "Name JSX files with the configured extension so file names accurately signal which files contain JSX.",
|
|
8859
9259
|
category: "Architecture",
|
|
8860
9260
|
create: (context) => {
|
|
8861
|
-
const settings = resolveSettings$
|
|
9261
|
+
const settings = resolveSettings$33(context.settings);
|
|
8862
9262
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
8863
9263
|
const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
|
|
8864
9264
|
const extensionOnly = path.extname(filename).slice(1);
|
|
@@ -8910,9 +9310,9 @@ const isJsxFragmentElement = (node) => {
|
|
|
8910
9310
|
};
|
|
8911
9311
|
//#endregion
|
|
8912
9312
|
//#region src/plugin/rules/react-builtins/jsx-fragments.ts
|
|
8913
|
-
const SYNTAX_MESSAGE = "
|
|
8914
|
-
const ELEMENT_MESSAGE = "
|
|
8915
|
-
const resolveSettings$
|
|
9313
|
+
const SYNTAX_MESSAGE = "`<React.Fragment>` is used where shorthand fragments are configured, so similar wrappers look different across the codebase.";
|
|
9314
|
+
const ELEMENT_MESSAGE = "Fragment shorthand is used where explicit fragments are configured, so similar wrappers look different across the codebase.";
|
|
9315
|
+
const resolveSettings$32 = (settings) => {
|
|
8916
9316
|
const reactDoctor = settings?.["react-doctor"];
|
|
8917
9317
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFragments ?? {} : {}).mode ?? "syntax" };
|
|
8918
9318
|
};
|
|
@@ -8921,10 +9321,10 @@ const jsxFragments = defineRule({
|
|
|
8921
9321
|
title: "Inconsistent fragment syntax",
|
|
8922
9322
|
severity: "warn",
|
|
8923
9323
|
defaultEnabled: false,
|
|
8924
|
-
recommendation: "
|
|
9324
|
+
recommendation: "Use one fragment style so identical wrappers do not look different across files.",
|
|
8925
9325
|
category: "Architecture",
|
|
8926
9326
|
create: (context) => {
|
|
8927
|
-
const { mode } = resolveSettings$
|
|
9327
|
+
const { mode } = resolveSettings$32(context.settings);
|
|
8928
9328
|
return {
|
|
8929
9329
|
JSXElement(node) {
|
|
8930
9330
|
if (mode !== "syntax") return;
|
|
@@ -8949,8 +9349,8 @@ const jsxFragments = defineRule({
|
|
|
8949
9349
|
});
|
|
8950
9350
|
//#endregion
|
|
8951
9351
|
//#region src/plugin/rules/react-builtins/jsx-handler-names.ts
|
|
8952
|
-
const buildHandlerNameMessage = (handlerName, propKey) => `The handler "${handlerName}"
|
|
8953
|
-
const buildHandlerPropMessage = (propKey, propValue) => `The prop "${propKey}" passes
|
|
9352
|
+
const buildHandlerNameMessage = (handlerName, propKey) => `The handler "${handlerName}" does not match the "${propKey}" event prop convention, so readers cannot trace event flow quickly.`;
|
|
9353
|
+
const buildHandlerPropMessage = (propKey, propValue) => `The prop "${propKey}" passes handler "${propValue}" but is not named like an event prop, so callers cannot tell it fires an event.`;
|
|
8954
9354
|
const DEFAULT_HANDLER_PREFIX = "handle";
|
|
8955
9355
|
const DEFAULT_HANDLER_PROP_PREFIX = "on";
|
|
8956
9356
|
const splitPrefixes = (prefixes) => prefixes.split("|").map((token) => token.trim()).filter((token) => token.length > 0);
|
|
@@ -8968,7 +9368,7 @@ const buildPropRegex = (handlerPropPrefix) => {
|
|
|
8968
9368
|
const escaped = tokens.map((token) => token.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
8969
9369
|
return new RegExp(`^(${escaped})[A-Z].*$`);
|
|
8970
9370
|
};
|
|
8971
|
-
const resolveSettings$
|
|
9371
|
+
const resolveSettings$31 = (settings) => {
|
|
8972
9372
|
const reactDoctor = settings?.["react-doctor"];
|
|
8973
9373
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxHandlerNames ?? {} : {};
|
|
8974
9374
|
let handlerPrefix = DEFAULT_HANDLER_PREFIX;
|
|
@@ -9041,10 +9441,10 @@ const jsxHandlerNames = defineRule({
|
|
|
9041
9441
|
title: "Inconsistent event handler names",
|
|
9042
9442
|
severity: "warn",
|
|
9043
9443
|
defaultEnabled: false,
|
|
9044
|
-
recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers.",
|
|
9444
|
+
recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers so readers can trace event flow.",
|
|
9045
9445
|
category: "Architecture",
|
|
9046
9446
|
create: (context) => {
|
|
9047
|
-
const settings = resolveSettings$
|
|
9447
|
+
const settings = resolveSettings$31(context.settings);
|
|
9048
9448
|
return { JSXAttribute(node) {
|
|
9049
9449
|
if (settings.ignoreComponentNames.length > 0) {
|
|
9050
9450
|
const opening = node.parent;
|
|
@@ -9129,7 +9529,7 @@ const MISSING_KEY_ARRAY = "Your users can see the wrong data when this array reo
|
|
|
9129
9529
|
const MISSING_KEY_ITERATOR = "Your users can see the wrong data when this list reorders.";
|
|
9130
9530
|
const KEY_BEFORE_SPREAD = "The `{...spread}` can overwrite this `key` & break React's tracking.";
|
|
9131
9531
|
const DUPLICATE_KEY = (keyValue) => `Your users can see the wrong data because two elements share the key "${keyValue}".`;
|
|
9132
|
-
const resolveSettings$
|
|
9532
|
+
const resolveSettings$30 = (settings) => {
|
|
9133
9533
|
const reactDoctor = settings?.["react-doctor"];
|
|
9134
9534
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxKey ?? {} : {};
|
|
9135
9535
|
return {
|
|
@@ -9275,9 +9675,9 @@ const jsxKey = defineRule({
|
|
|
9275
9675
|
id: "jsx-key",
|
|
9276
9676
|
title: "Missing key in list",
|
|
9277
9677
|
severity: "error",
|
|
9278
|
-
recommendation: "Add a `key
|
|
9678
|
+
recommendation: "Add a stable `key` prop so React can keep list items matched to the right data when the list changes.",
|
|
9279
9679
|
create: (context) => {
|
|
9280
|
-
const settings = resolveSettings$
|
|
9680
|
+
const settings = resolveSettings$30(context.settings);
|
|
9281
9681
|
return {
|
|
9282
9682
|
JSXElement(node) {
|
|
9283
9683
|
const openingElement = node.openingElement;
|
|
@@ -9327,194 +9727,10 @@ const jsxKey = defineRule({
|
|
|
9327
9727
|
}
|
|
9328
9728
|
});
|
|
9329
9729
|
//#endregion
|
|
9330
|
-
//#region src/plugin/utils/find-variable-initializer.ts
|
|
9331
|
-
const FUNCTION_LIKE_TYPES = new Set([
|
|
9332
|
-
"FunctionDeclaration",
|
|
9333
|
-
"FunctionExpression",
|
|
9334
|
-
"ArrowFunctionExpression",
|
|
9335
|
-
"MethodDefinition",
|
|
9336
|
-
"Program"
|
|
9337
|
-
]);
|
|
9338
|
-
const findScopeOwner = (node) => {
|
|
9339
|
-
let ancestor = node;
|
|
9340
|
-
while (ancestor) {
|
|
9341
|
-
if (FUNCTION_LIKE_TYPES.has(ancestor.type)) return ancestor;
|
|
9342
|
-
ancestor = ancestor.parent ?? null;
|
|
9343
|
-
}
|
|
9344
|
-
return null;
|
|
9345
|
-
};
|
|
9346
|
-
const findBlockScopeOwner = (declaratorNode, declarationKind) => {
|
|
9347
|
-
if (declarationKind !== "let" && declarationKind !== "const") return findScopeOwner(declaratorNode);
|
|
9348
|
-
let ancestor = declaratorNode.parent;
|
|
9349
|
-
while (ancestor) {
|
|
9350
|
-
if (ancestor.type === "BlockStatement") {
|
|
9351
|
-
const blockParent = ancestor.parent;
|
|
9352
|
-
if (blockParent && (blockParent.type === "FunctionDeclaration" || blockParent.type === "FunctionExpression" || blockParent.type === "ArrowFunctionExpression" || blockParent.type === "MethodDefinition")) return findScopeOwner(declaratorNode);
|
|
9353
|
-
return ancestor;
|
|
9354
|
-
}
|
|
9355
|
-
if (FUNCTION_LIKE_TYPES.has(ancestor.type)) return ancestor;
|
|
9356
|
-
ancestor = ancestor.parent ?? null;
|
|
9357
|
-
}
|
|
9358
|
-
return null;
|
|
9359
|
-
};
|
|
9360
|
-
const collectFromBindingPattern = (pattern, initializer, scopeOwner, out) => {
|
|
9361
|
-
if (isNodeOfType(pattern, "Identifier")) {
|
|
9362
|
-
const list = out.get(pattern.name) ?? [];
|
|
9363
|
-
list.push({
|
|
9364
|
-
bindingIdentifier: pattern,
|
|
9365
|
-
initializer,
|
|
9366
|
-
scopeOwner
|
|
9367
|
-
});
|
|
9368
|
-
out.set(pattern.name, list);
|
|
9369
|
-
return;
|
|
9370
|
-
}
|
|
9371
|
-
if (isNodeOfType(pattern, "ObjectPattern")) {
|
|
9372
|
-
for (const property of pattern.properties) if (isNodeOfType(property, "Property")) {
|
|
9373
|
-
const valueNode = property.value;
|
|
9374
|
-
collectFromBindingPattern(valueNode, isNodeOfType(valueNode, "AssignmentPattern") ? valueNode.right : null, scopeOwner, out);
|
|
9375
|
-
} else if (isNodeOfType(property, "RestElement")) collectFromBindingPattern(property.argument, null, scopeOwner, out);
|
|
9376
|
-
return;
|
|
9377
|
-
}
|
|
9378
|
-
if (isNodeOfType(pattern, "ArrayPattern")) {
|
|
9379
|
-
for (const element of pattern.elements) {
|
|
9380
|
-
if (!element) continue;
|
|
9381
|
-
collectFromBindingPattern(element, isNodeOfType(element, "AssignmentPattern") ? element.right ?? null : null, scopeOwner, out);
|
|
9382
|
-
}
|
|
9383
|
-
return;
|
|
9384
|
-
}
|
|
9385
|
-
if (isNodeOfType(pattern, "AssignmentPattern")) {
|
|
9386
|
-
collectFromBindingPattern(pattern.left, pattern.right ?? null, scopeOwner, out);
|
|
9387
|
-
return;
|
|
9388
|
-
}
|
|
9389
|
-
if (isNodeOfType(pattern, "RestElement")) collectFromBindingPattern(pattern.argument, null, scopeOwner, out);
|
|
9390
|
-
};
|
|
9391
|
-
const buildBindingIndex = (root) => {
|
|
9392
|
-
const out = /* @__PURE__ */ new Map();
|
|
9393
|
-
const visit = (node) => {
|
|
9394
|
-
if (isNodeOfType(node, "VariableDeclarator")) {
|
|
9395
|
-
const declaration = node.parent;
|
|
9396
|
-
const scopeOwner = findBlockScopeOwner(node, declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : void 0);
|
|
9397
|
-
if (scopeOwner) collectFromBindingPattern(node.id, node.init ?? null, scopeOwner, out);
|
|
9398
|
-
}
|
|
9399
|
-
if ((isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression")) && node.id) {
|
|
9400
|
-
const enclosing = node.parent ? findScopeOwner(node.parent) : null;
|
|
9401
|
-
if (enclosing) {
|
|
9402
|
-
const list = out.get(node.id.name) ?? [];
|
|
9403
|
-
list.push({
|
|
9404
|
-
bindingIdentifier: node.id,
|
|
9405
|
-
initializer: node,
|
|
9406
|
-
scopeOwner: enclosing
|
|
9407
|
-
});
|
|
9408
|
-
out.set(node.id.name, list);
|
|
9409
|
-
}
|
|
9410
|
-
}
|
|
9411
|
-
if ((isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression")) && node.id) {
|
|
9412
|
-
const enclosing = node.parent ? findScopeOwner(node.parent) : null;
|
|
9413
|
-
if (enclosing) {
|
|
9414
|
-
const list = out.get(node.id.name) ?? [];
|
|
9415
|
-
list.push({
|
|
9416
|
-
bindingIdentifier: node.id,
|
|
9417
|
-
initializer: node,
|
|
9418
|
-
scopeOwner: enclosing
|
|
9419
|
-
});
|
|
9420
|
-
out.set(node.id.name, list);
|
|
9421
|
-
}
|
|
9422
|
-
}
|
|
9423
|
-
if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")) {
|
|
9424
|
-
if (Array.isArray(node.params)) for (const param of node.params) {
|
|
9425
|
-
if (!param) continue;
|
|
9426
|
-
collectFromBindingPattern(param, null, node, out);
|
|
9427
|
-
if (isNodeOfType(param, "AssignmentPattern")) collectFromBindingPattern(param.left ?? null, param.right ?? null, node, out);
|
|
9428
|
-
}
|
|
9429
|
-
}
|
|
9430
|
-
if (isNodeOfType(node, "ImportDeclaration")) {
|
|
9431
|
-
const scopeOwner = findScopeOwner(node);
|
|
9432
|
-
if (scopeOwner && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
|
|
9433
|
-
const local = specifier.local;
|
|
9434
|
-
if (local && isNodeOfType(local, "Identifier")) {
|
|
9435
|
-
const list = out.get(local.name) ?? [];
|
|
9436
|
-
list.push({
|
|
9437
|
-
bindingIdentifier: local,
|
|
9438
|
-
initializer: specifier,
|
|
9439
|
-
scopeOwner
|
|
9440
|
-
});
|
|
9441
|
-
out.set(local.name, list);
|
|
9442
|
-
}
|
|
9443
|
-
}
|
|
9444
|
-
}
|
|
9445
|
-
if (node.type === "TSImportEqualsDeclaration" || node.type === "TSEnumDeclaration" || node.type === "TSModuleDeclaration") {
|
|
9446
|
-
const idNode = node.id;
|
|
9447
|
-
if (idNode && idNode.type === "Identifier") {
|
|
9448
|
-
const idObject = idNode;
|
|
9449
|
-
const scopeOwner = findScopeOwner(node);
|
|
9450
|
-
if (scopeOwner && typeof idObject.name === "string") {
|
|
9451
|
-
const list = out.get(idObject.name) ?? [];
|
|
9452
|
-
list.push({
|
|
9453
|
-
bindingIdentifier: idNode,
|
|
9454
|
-
initializer: null,
|
|
9455
|
-
scopeOwner
|
|
9456
|
-
});
|
|
9457
|
-
out.set(idObject.name, list);
|
|
9458
|
-
}
|
|
9459
|
-
}
|
|
9460
|
-
}
|
|
9461
|
-
const nodeRecord = node;
|
|
9462
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
9463
|
-
if (key === "parent") continue;
|
|
9464
|
-
const child = nodeRecord[key];
|
|
9465
|
-
if (Array.isArray(child)) {
|
|
9466
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
9467
|
-
} else if (isAstNode(child)) visit(child);
|
|
9468
|
-
}
|
|
9469
|
-
};
|
|
9470
|
-
visit(root);
|
|
9471
|
-
return out;
|
|
9472
|
-
};
|
|
9473
|
-
const programRootCache = /* @__PURE__ */ new WeakMap();
|
|
9474
|
-
const getBindingIndex = (referenceNode) => {
|
|
9475
|
-
const programRoot = findProgramRoot(referenceNode);
|
|
9476
|
-
if (!programRoot) return null;
|
|
9477
|
-
let index = programRootCache.get(programRoot);
|
|
9478
|
-
if (!index) {
|
|
9479
|
-
index = buildBindingIndex(programRoot);
|
|
9480
|
-
programRootCache.set(programRoot, index);
|
|
9481
|
-
}
|
|
9482
|
-
return index;
|
|
9483
|
-
};
|
|
9484
|
-
const findVariableInitializer = (referenceNode, bindingName) => {
|
|
9485
|
-
const index = getBindingIndex(referenceNode);
|
|
9486
|
-
if (!index) return null;
|
|
9487
|
-
const candidates = index.get(bindingName);
|
|
9488
|
-
if (!candidates || candidates.length === 0) return null;
|
|
9489
|
-
const referenceAncestors = /* @__PURE__ */ new Set();
|
|
9490
|
-
let walker = referenceNode;
|
|
9491
|
-
while (walker) {
|
|
9492
|
-
referenceAncestors.add(walker);
|
|
9493
|
-
walker = walker.parent ?? null;
|
|
9494
|
-
}
|
|
9495
|
-
let best = null;
|
|
9496
|
-
for (const candidate of candidates) {
|
|
9497
|
-
if (!referenceAncestors.has(candidate.scopeOwner)) continue;
|
|
9498
|
-
if (best === null) {
|
|
9499
|
-
best = candidate;
|
|
9500
|
-
continue;
|
|
9501
|
-
}
|
|
9502
|
-
let cursor = candidate.scopeOwner;
|
|
9503
|
-
while (cursor) {
|
|
9504
|
-
if (cursor === best.scopeOwner) {
|
|
9505
|
-
best = candidate;
|
|
9506
|
-
break;
|
|
9507
|
-
}
|
|
9508
|
-
cursor = cursor.parent ?? null;
|
|
9509
|
-
}
|
|
9510
|
-
}
|
|
9511
|
-
return best;
|
|
9512
|
-
};
|
|
9513
|
-
//#endregion
|
|
9514
9730
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
9515
9731
|
const buildMessage$18 = (depth, max) => `This JSX is hard to read at ${depth} levels deep, past the limit of ${max}.`;
|
|
9516
9732
|
const DEFAULT_MAX_DEPTH = 14;
|
|
9517
|
-
const resolveSettings$
|
|
9733
|
+
const resolveSettings$29 = (settings) => {
|
|
9518
9734
|
const reactDoctor = settings?.["react-doctor"];
|
|
9519
9735
|
return { max: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxMaxDepth ?? {} : {}).max ?? DEFAULT_MAX_DEPTH };
|
|
9520
9736
|
};
|
|
@@ -9574,7 +9790,7 @@ const jsxMaxDepth = defineRule({
|
|
|
9574
9790
|
recommendation: "Pull deeply nested JSX into smaller components so it's easier to read.",
|
|
9575
9791
|
category: "Architecture",
|
|
9576
9792
|
create: (context) => {
|
|
9577
|
-
const { max } = resolveSettings$
|
|
9793
|
+
const { max } = resolveSettings$29(context.settings);
|
|
9578
9794
|
const checkNode = (node) => {
|
|
9579
9795
|
if (!isLeafJsxNode(node)) return;
|
|
9580
9796
|
const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
|
|
@@ -9625,7 +9841,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
9625
9841
|
id: "jsx-no-comment-textnodes",
|
|
9626
9842
|
title: "Comment rendered as JSX text",
|
|
9627
9843
|
severity: "warn",
|
|
9628
|
-
recommendation: "Wrap JSX comments in `{/* … */}` so
|
|
9844
|
+
recommendation: "Wrap JSX comments in `{/* … */}` so users do not see comment text rendered as children.",
|
|
9629
9845
|
create: (context) => ({ JSXText(node) {
|
|
9630
9846
|
if (!hasCommentLikePattern(node.value)) return;
|
|
9631
9847
|
if (isInsideLiteralTextTag(node)) return;
|
|
@@ -9732,7 +9948,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
9732
9948
|
tags: ["react-jsx-only"],
|
|
9733
9949
|
severity: "warn",
|
|
9734
9950
|
disabledBy: ["react-compiler"],
|
|
9735
|
-
recommendation: "Wrap the context value in `useMemo
|
|
9951
|
+
recommendation: "Wrap the context value in `useMemo` or move it outside the component so consumers do not redraw every render.",
|
|
9736
9952
|
category: "Performance",
|
|
9737
9953
|
create: (context) => {
|
|
9738
9954
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -9773,7 +9989,7 @@ const jsxNoDuplicateProps = defineRule({
|
|
|
9773
9989
|
id: "jsx-no-duplicate-props",
|
|
9774
9990
|
title: "Duplicate props on element",
|
|
9775
9991
|
severity: "error",
|
|
9776
|
-
recommendation: "Remove or rename one of the duplicate props.",
|
|
9992
|
+
recommendation: "Remove or rename one of the duplicate props so the later value does not silently override the earlier one.",
|
|
9777
9993
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
9778
9994
|
const seenPropNames = /* @__PURE__ */ new Set();
|
|
9779
9995
|
for (const attribute of node.attributes) {
|
|
@@ -10090,7 +10306,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
10090
10306
|
tags: ["react-jsx-only"],
|
|
10091
10307
|
severity: "warn",
|
|
10092
10308
|
disabledBy: ["react-compiler"],
|
|
10093
|
-
recommendation: "Move the JSX outside the component
|
|
10309
|
+
recommendation: "Move the JSX outside the component or wrap it in `useMemo` so memoized children do not redraw every render.",
|
|
10094
10310
|
category: "Performance",
|
|
10095
10311
|
create: (context) => {
|
|
10096
10312
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -10462,7 +10678,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
10462
10678
|
tags: ["react-jsx-only"],
|
|
10463
10679
|
severity: "warn",
|
|
10464
10680
|
disabledBy: ["react-compiler"],
|
|
10465
|
-
recommendation: "Wrap the array in `useMemo
|
|
10681
|
+
recommendation: "Wrap the array in `useMemo` or move it outside the component so memoized children do not redraw every render.",
|
|
10466
10682
|
category: "Performance",
|
|
10467
10683
|
create: (context) => {
|
|
10468
10684
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -10925,7 +11141,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
10925
11141
|
tags: ["react-jsx-only"],
|
|
10926
11142
|
severity: "warn",
|
|
10927
11143
|
disabledBy: ["react-compiler"],
|
|
10928
|
-
recommendation: "Wrap the callback in `useCallback
|
|
11144
|
+
recommendation: "Wrap the callback in `useCallback` or move it outside the component so memoized children do not redraw every render.",
|
|
10929
11145
|
category: "Performance",
|
|
10930
11146
|
create: (context) => {
|
|
10931
11147
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -11232,7 +11448,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
11232
11448
|
tags: ["react-jsx-only"],
|
|
11233
11449
|
severity: "warn",
|
|
11234
11450
|
disabledBy: ["react-compiler"],
|
|
11235
|
-
recommendation: "Wrap the object in `useMemo
|
|
11451
|
+
recommendation: "Wrap the object in `useMemo` or move it outside the component so memoized children do not redraw every render.",
|
|
11236
11452
|
category: "Performance",
|
|
11237
11453
|
create: (context) => {
|
|
11238
11454
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -11269,7 +11485,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
11269
11485
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
11270
11486
|
const MESSAGE$35 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
11271
11487
|
const JAVASCRIPT_URL_PATTERN = /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;
|
|
11272
|
-
const resolveSettings$
|
|
11488
|
+
const resolveSettings$28 = (settings) => {
|
|
11273
11489
|
const reactDoctor = settings?.["react-doctor"];
|
|
11274
11490
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
11275
11491
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
@@ -11292,10 +11508,10 @@ const jsxNoScriptUrl = defineRule({
|
|
|
11292
11508
|
id: "jsx-no-script-url",
|
|
11293
11509
|
title: "javascript: URL in JSX",
|
|
11294
11510
|
severity: "error",
|
|
11295
|
-
recommendation: "
|
|
11511
|
+
recommendation: "Use real event handlers instead of `javascript:` URLs so injected URL text cannot execute as code.",
|
|
11296
11512
|
category: "Security",
|
|
11297
11513
|
create: (context) => {
|
|
11298
|
-
const options = resolveSettings$
|
|
11514
|
+
const options = resolveSettings$28(context.settings);
|
|
11299
11515
|
return { JSXOpeningElement(node) {
|
|
11300
11516
|
const elementName = getElementName(node);
|
|
11301
11517
|
if (!elementName) return;
|
|
@@ -11315,287 +11531,6 @@ const jsxNoScriptUrl = defineRule({
|
|
|
11315
11531
|
}
|
|
11316
11532
|
});
|
|
11317
11533
|
//#endregion
|
|
11318
|
-
//#region src/plugin/rules/react-builtins/jsx-no-target-blank.ts
|
|
11319
|
-
const NOREFERRER_MESSAGE = "`target=\"_blank\"` without `rel=\"noreferrer\"` lets the linked page hijack your tab to a phishing site.";
|
|
11320
|
-
const NOOPENER_MESSAGE = "`target=\"_blank\"` without `rel` lets the linked page hijack your tab to a phishing site.";
|
|
11321
|
-
const SPREAD_MESSAGE = "A spread here can add `target=\"_blank\"`, letting the linked page hijack your tab to a phishing site.";
|
|
11322
|
-
const resolveSettings$28 = (settings) => {
|
|
11323
|
-
const reactDoctor = settings?.["react-doctor"];
|
|
11324
|
-
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxNoTargetBlank ?? {} : {};
|
|
11325
|
-
const reactSettings = typeof settings?.react === "object" && settings.react !== null ? settings.react : {};
|
|
11326
|
-
const linkComponents = /* @__PURE__ */ new Map();
|
|
11327
|
-
for (const entry of reactSettings.linkComponents ?? []) if (typeof entry === "string") linkComponents.set(entry, ["href"]);
|
|
11328
|
-
else if (typeof entry === "object" && entry !== null) {
|
|
11329
|
-
const linkAttribute = entry.linkAttribute ?? "href";
|
|
11330
|
-
linkComponents.set(entry.name, Array.isArray(linkAttribute) ? linkAttribute : [linkAttribute]);
|
|
11331
|
-
}
|
|
11332
|
-
const formComponents = /* @__PURE__ */ new Map();
|
|
11333
|
-
for (const entry of reactSettings.formComponents ?? []) if (typeof entry === "string") formComponents.set(entry, ["action"]);
|
|
11334
|
-
else if (typeof entry === "object" && entry !== null) {
|
|
11335
|
-
const formAttribute = entry.formAttribute ?? "action";
|
|
11336
|
-
formComponents.set(entry.name, Array.isArray(formAttribute) ? formAttribute : [formAttribute]);
|
|
11337
|
-
}
|
|
11338
|
-
return {
|
|
11339
|
-
enforceDynamicLinks: ruleSettings.enforceDynamicLinks ?? "always",
|
|
11340
|
-
warnOnSpreadAttributes: ruleSettings.warnOnSpreadAttributes ?? false,
|
|
11341
|
-
allowReferrer: ruleSettings.allowReferrer ?? false,
|
|
11342
|
-
links: ruleSettings.links ?? true,
|
|
11343
|
-
forms: ruleSettings.forms ?? false,
|
|
11344
|
-
linkComponents,
|
|
11345
|
-
formComponents
|
|
11346
|
-
};
|
|
11347
|
-
};
|
|
11348
|
-
const isExternalLink = (href) => href.includes("//");
|
|
11349
|
-
const matchHrefExpression = (expression, state) => {
|
|
11350
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") {
|
|
11351
|
-
if (isExternalLink(expression.value)) state.isExternal = true;
|
|
11352
|
-
return;
|
|
11353
|
-
}
|
|
11354
|
-
if (isNodeOfType(expression, "Identifier")) {
|
|
11355
|
-
state.isDynamic = true;
|
|
11356
|
-
return;
|
|
11357
|
-
}
|
|
11358
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
11359
|
-
matchHrefExpression(expression.consequent, state);
|
|
11360
|
-
matchHrefExpression(expression.alternate, state);
|
|
11361
|
-
}
|
|
11362
|
-
};
|
|
11363
|
-
const checkHref = (attributeValue, enforceDynamicLinks) => {
|
|
11364
|
-
const state = {
|
|
11365
|
-
isExternal: false,
|
|
11366
|
-
isDynamic: false
|
|
11367
|
-
};
|
|
11368
|
-
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") state.isExternal = isExternalLink(attributeValue.value);
|
|
11369
|
-
else if (isNodeOfType(attributeValue, "JSXExpressionContainer")) matchHrefExpression(attributeValue.expression, state);
|
|
11370
|
-
if (enforceDynamicLinks === "never") return !state.isExternal || state.isDynamic;
|
|
11371
|
-
return !(state.isExternal || state.isDynamic);
|
|
11372
|
-
};
|
|
11373
|
-
const checkRelValue = (text, allowReferrer) => {
|
|
11374
|
-
const tokens = text.split(/\s+/);
|
|
11375
|
-
if (allowReferrer) return tokens.includes("noopener") || tokens.includes("noreferrer");
|
|
11376
|
-
return tokens.some((token) => token.toLowerCase() === "noreferrer");
|
|
11377
|
-
};
|
|
11378
|
-
const matchRelExpression = (expression, allowReferrer) => {
|
|
11379
|
-
const empty = {
|
|
11380
|
-
combined: false,
|
|
11381
|
-
testName: "",
|
|
11382
|
-
consequent: false,
|
|
11383
|
-
alternate: false
|
|
11384
|
-
};
|
|
11385
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
|
|
11386
|
-
combined: checkRelValue(expression.value, allowReferrer),
|
|
11387
|
-
testName: "",
|
|
11388
|
-
consequent: false,
|
|
11389
|
-
alternate: false
|
|
11390
|
-
};
|
|
11391
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
11392
|
-
const consequent = matchRelExpression(expression.consequent, allowReferrer);
|
|
11393
|
-
const alternate = matchRelExpression(expression.alternate, allowReferrer);
|
|
11394
|
-
const test = expression.test;
|
|
11395
|
-
if (isNodeOfType(test, "Identifier")) return {
|
|
11396
|
-
combined: consequent.combined && alternate.combined,
|
|
11397
|
-
testName: test.name,
|
|
11398
|
-
consequent: consequent.combined,
|
|
11399
|
-
alternate: alternate.combined
|
|
11400
|
-
};
|
|
11401
|
-
return {
|
|
11402
|
-
combined: consequent.combined && alternate.combined,
|
|
11403
|
-
testName: "",
|
|
11404
|
-
consequent: consequent.combined,
|
|
11405
|
-
alternate: alternate.combined
|
|
11406
|
-
};
|
|
11407
|
-
}
|
|
11408
|
-
return empty;
|
|
11409
|
-
};
|
|
11410
|
-
const checkRel = (attributeValue, allowReferrer) => {
|
|
11411
|
-
const empty = {
|
|
11412
|
-
combined: false,
|
|
11413
|
-
testName: "",
|
|
11414
|
-
consequent: false,
|
|
11415
|
-
alternate: false
|
|
11416
|
-
};
|
|
11417
|
-
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
|
|
11418
|
-
combined: checkRelValue(attributeValue.value, allowReferrer),
|
|
11419
|
-
testName: "",
|
|
11420
|
-
consequent: false,
|
|
11421
|
-
alternate: false
|
|
11422
|
-
};
|
|
11423
|
-
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
11424
|
-
const expression = attributeValue.expression;
|
|
11425
|
-
if (expression.type === "JSXEmptyExpression") return empty;
|
|
11426
|
-
return matchRelExpression(expression, allowReferrer);
|
|
11427
|
-
}
|
|
11428
|
-
return empty;
|
|
11429
|
-
};
|
|
11430
|
-
const matchTargetExpression = (expression) => {
|
|
11431
|
-
const empty = {
|
|
11432
|
-
combined: false,
|
|
11433
|
-
testName: "",
|
|
11434
|
-
consequent: false,
|
|
11435
|
-
alternate: false
|
|
11436
|
-
};
|
|
11437
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
|
|
11438
|
-
combined: expression.value.toLowerCase() === "_blank",
|
|
11439
|
-
testName: "",
|
|
11440
|
-
consequent: false,
|
|
11441
|
-
alternate: false
|
|
11442
|
-
};
|
|
11443
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
11444
|
-
const consequent = matchTargetExpression(expression.consequent);
|
|
11445
|
-
const alternate = matchTargetExpression(expression.alternate);
|
|
11446
|
-
const test = expression.test;
|
|
11447
|
-
const combined = consequent.combined || alternate.combined;
|
|
11448
|
-
if (isNodeOfType(test, "Identifier")) return {
|
|
11449
|
-
combined,
|
|
11450
|
-
testName: test.name,
|
|
11451
|
-
consequent: consequent.combined,
|
|
11452
|
-
alternate: alternate.combined
|
|
11453
|
-
};
|
|
11454
|
-
return {
|
|
11455
|
-
combined,
|
|
11456
|
-
testName: "",
|
|
11457
|
-
consequent: consequent.combined,
|
|
11458
|
-
alternate: alternate.combined
|
|
11459
|
-
};
|
|
11460
|
-
}
|
|
11461
|
-
return empty;
|
|
11462
|
-
};
|
|
11463
|
-
const checkTarget = (attributeValue) => {
|
|
11464
|
-
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
|
|
11465
|
-
combined: attributeValue.value.toLowerCase() === "_blank",
|
|
11466
|
-
testName: "",
|
|
11467
|
-
consequent: false,
|
|
11468
|
-
alternate: false
|
|
11469
|
-
};
|
|
11470
|
-
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
11471
|
-
const expression = attributeValue.expression;
|
|
11472
|
-
if (expression.type === "JSXEmptyExpression") return {
|
|
11473
|
-
combined: false,
|
|
11474
|
-
testName: "",
|
|
11475
|
-
consequent: false,
|
|
11476
|
-
alternate: false
|
|
11477
|
-
};
|
|
11478
|
-
return matchTargetExpression(expression);
|
|
11479
|
-
}
|
|
11480
|
-
return {
|
|
11481
|
-
combined: false,
|
|
11482
|
-
testName: "",
|
|
11483
|
-
consequent: false,
|
|
11484
|
-
alternate: false
|
|
11485
|
-
};
|
|
11486
|
-
};
|
|
11487
|
-
const getOpeningElementName = (node) => {
|
|
11488
|
-
const name = node.name;
|
|
11489
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
11490
|
-
return null;
|
|
11491
|
-
};
|
|
11492
|
-
const jsxNoTargetBlank = defineRule({
|
|
11493
|
-
id: "jsx-no-target-blank",
|
|
11494
|
-
title: "Unsafe target=_blank link",
|
|
11495
|
-
severity: "warn",
|
|
11496
|
-
recommendation: "Add `rel=\"noreferrer\"` (or `\"noopener\"`) when using `target=\"_blank\"`.",
|
|
11497
|
-
category: "Security",
|
|
11498
|
-
create: (context) => {
|
|
11499
|
-
const settings = resolveSettings$28(context.settings);
|
|
11500
|
-
const isLink = (tagName) => {
|
|
11501
|
-
if (!settings.links) return false;
|
|
11502
|
-
if (tagName === "a") return true;
|
|
11503
|
-
return settings.linkComponents.has(tagName);
|
|
11504
|
-
};
|
|
11505
|
-
const isForm = (tagName) => {
|
|
11506
|
-
if (!settings.forms) return false;
|
|
11507
|
-
if (tagName === "form") return true;
|
|
11508
|
-
return settings.formComponents.has(tagName);
|
|
11509
|
-
};
|
|
11510
|
-
return { JSXOpeningElement(node) {
|
|
11511
|
-
const tagName = getOpeningElementName(node);
|
|
11512
|
-
if (!tagName) return;
|
|
11513
|
-
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
11514
|
-
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
11515
|
-
const formAttributeNames = settings.formComponents.get(tagName) ?? ["action"];
|
|
11516
|
-
let targetTuple = {
|
|
11517
|
-
combined: false,
|
|
11518
|
-
testName: "",
|
|
11519
|
-
consequent: false,
|
|
11520
|
-
alternate: false
|
|
11521
|
-
};
|
|
11522
|
-
let relTuple = {
|
|
11523
|
-
combined: false,
|
|
11524
|
-
testName: "",
|
|
11525
|
-
consequent: false,
|
|
11526
|
-
alternate: false
|
|
11527
|
-
};
|
|
11528
|
-
let isHrefValid = true;
|
|
11529
|
-
let hasHrefValue = false;
|
|
11530
|
-
let warnSpread = false;
|
|
11531
|
-
let targetReportNode = node.name;
|
|
11532
|
-
let spreadReportNode = null;
|
|
11533
|
-
for (const attribute of node.attributes) {
|
|
11534
|
-
if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
|
|
11535
|
-
if (settings.warnOnSpreadAttributes) {
|
|
11536
|
-
warnSpread = true;
|
|
11537
|
-
spreadReportNode = attribute;
|
|
11538
|
-
targetTuple = {
|
|
11539
|
-
combined: false,
|
|
11540
|
-
testName: "",
|
|
11541
|
-
consequent: false,
|
|
11542
|
-
alternate: false
|
|
11543
|
-
};
|
|
11544
|
-
relTuple = {
|
|
11545
|
-
combined: false,
|
|
11546
|
-
testName: "",
|
|
11547
|
-
consequent: false,
|
|
11548
|
-
alternate: false
|
|
11549
|
-
};
|
|
11550
|
-
isHrefValid = false;
|
|
11551
|
-
hasHrefValue = true;
|
|
11552
|
-
}
|
|
11553
|
-
continue;
|
|
11554
|
-
}
|
|
11555
|
-
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
11556
|
-
const attributeName = attribute.name;
|
|
11557
|
-
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
11558
|
-
const propName = attributeName.name;
|
|
11559
|
-
const value = attribute.value;
|
|
11560
|
-
if (propName === "target") {
|
|
11561
|
-
if (value) {
|
|
11562
|
-
targetTuple = checkTarget(value);
|
|
11563
|
-
targetReportNode = value;
|
|
11564
|
-
}
|
|
11565
|
-
} else if (propName === "href" || propName === "action" || linkAttributeNames.includes(propName) || formAttributeNames.includes(propName)) {
|
|
11566
|
-
if (value) {
|
|
11567
|
-
hasHrefValue = true;
|
|
11568
|
-
isHrefValid = checkHref(value, settings.enforceDynamicLinks);
|
|
11569
|
-
}
|
|
11570
|
-
} else if (propName === "rel" && value) relTuple = checkRel(value, settings.allowReferrer);
|
|
11571
|
-
}
|
|
11572
|
-
if (warnSpread) {
|
|
11573
|
-
if (hasHrefValue && isHrefValid || relTuple.combined) return;
|
|
11574
|
-
context.report({
|
|
11575
|
-
node: spreadReportNode ?? node,
|
|
11576
|
-
message: SPREAD_MESSAGE
|
|
11577
|
-
});
|
|
11578
|
-
return;
|
|
11579
|
-
}
|
|
11580
|
-
if (!isHrefValid) {
|
|
11581
|
-
if (targetTuple.testName !== "" && targetTuple.testName === relTuple.testName) {
|
|
11582
|
-
const consequentBad = targetTuple.consequent && !relTuple.consequent;
|
|
11583
|
-
const alternateBad = targetTuple.alternate && !relTuple.alternate;
|
|
11584
|
-
if (consequentBad || alternateBad) context.report({
|
|
11585
|
-
node: targetReportNode,
|
|
11586
|
-
message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
|
|
11587
|
-
});
|
|
11588
|
-
return;
|
|
11589
|
-
}
|
|
11590
|
-
if (targetTuple.combined && !relTuple.combined) context.report({
|
|
11591
|
-
node: targetReportNode,
|
|
11592
|
-
message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
|
|
11593
|
-
});
|
|
11594
|
-
}
|
|
11595
|
-
} };
|
|
11596
|
-
}
|
|
11597
|
-
});
|
|
11598
|
-
//#endregion
|
|
11599
11534
|
//#region src/plugin/rules/react-builtins/jsx-no-undef.ts
|
|
11600
11535
|
const buildMessage$17 = (name) => `\`${name}\` crashes at runtime because it isn't defined here.`;
|
|
11601
11536
|
const KNOWN_GLOBALS = new Set([
|
|
@@ -11624,7 +11559,7 @@ const jsxNoUndef = defineRule({
|
|
|
11624
11559
|
id: "jsx-no-undef",
|
|
11625
11560
|
title: "Undefined JSX component",
|
|
11626
11561
|
severity: "error",
|
|
11627
|
-
recommendation: "Import the component or
|
|
11562
|
+
recommendation: "Import the component or fix the typo so React can resolve the JSX identifier at runtime.",
|
|
11628
11563
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
11629
11564
|
const rootIdentifier = getRootIdentifier(node.name);
|
|
11630
11565
|
if (!rootIdentifier) return;
|
|
@@ -11816,7 +11751,7 @@ const jsxPascalCase = defineRule({
|
|
|
11816
11751
|
severity: "warn",
|
|
11817
11752
|
defaultEnabled: false,
|
|
11818
11753
|
tags: ["test-noise"],
|
|
11819
|
-
recommendation: "Rename custom JSX components to PascalCase.",
|
|
11754
|
+
recommendation: "Rename custom JSX components to PascalCase so React treats them as components, not intrinsic DOM tags.",
|
|
11820
11755
|
category: "Architecture",
|
|
11821
11756
|
create: (context) => {
|
|
11822
11757
|
const settings = resolveSettings$26(context.settings);
|
|
@@ -11880,7 +11815,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
11880
11815
|
id: "jsx-props-no-spread-multi",
|
|
11881
11816
|
title: "Same prop spread multiple times",
|
|
11882
11817
|
severity: "warn",
|
|
11883
|
-
recommendation: "Spread
|
|
11818
|
+
recommendation: "Spread each value at most once so later props cannot silently override earlier props from the same object.",
|
|
11884
11819
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
11885
11820
|
const seenNames = /* @__PURE__ */ new Map();
|
|
11886
11821
|
const reportedNames = /* @__PURE__ */ new Set();
|
|
@@ -11920,12 +11855,12 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
11920
11855
|
title: "Props spread onto element",
|
|
11921
11856
|
severity: "warn",
|
|
11922
11857
|
defaultEnabled: false,
|
|
11923
|
-
recommendation: "List each prop explicitly so consumers can see
|
|
11858
|
+
recommendation: "List each prop explicitly so consumers can see the component API instead of receiving hidden spread props.",
|
|
11924
11859
|
category: "Architecture",
|
|
11925
11860
|
create: (context) => {
|
|
11926
11861
|
const settings = resolveSettings$25(context.settings);
|
|
11927
11862
|
return { JSXOpeningElement(node) {
|
|
11928
|
-
const tagName = flattenJsxName(node.name);
|
|
11863
|
+
const tagName = flattenJsxName$1(node.name);
|
|
11929
11864
|
if (!tagName) return;
|
|
11930
11865
|
const isCustom = isReactComponentName(tagName) || tagName.includes(".");
|
|
11931
11866
|
const isHtml = !isCustom;
|
|
@@ -12301,7 +12236,7 @@ const lang = defineRule({
|
|
|
12301
12236
|
title: "Invalid lang attribute value",
|
|
12302
12237
|
tags: ["react-jsx-only"],
|
|
12303
12238
|
severity: "warn",
|
|
12304
|
-
recommendation: "Use a valid language code
|
|
12239
|
+
recommendation: "Use a valid language code like `en` or `en-US` so screen readers choose the right pronunciation rules.",
|
|
12305
12240
|
category: "Accessibility",
|
|
12306
12241
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12307
12242
|
if (getElementType(node, context.settings) !== "html") return;
|
|
@@ -12328,7 +12263,7 @@ const lang = defineRule({
|
|
|
12328
12263
|
});
|
|
12329
12264
|
//#endregion
|
|
12330
12265
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
12331
|
-
const MESSAGE$32 = "Deaf
|
|
12266
|
+
const MESSAGE$32 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
12332
12267
|
const DEFAULT_AUDIO = ["audio"];
|
|
12333
12268
|
const DEFAULT_VIDEO = ["video"];
|
|
12334
12269
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -12511,50 +12446,6 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
12511
12446
|
}
|
|
12512
12447
|
});
|
|
12513
12448
|
//#endregion
|
|
12514
|
-
//#region src/plugin/constants/nextjs.ts
|
|
12515
|
-
const PAGE_FILE_PATTERN = /\/page\.(tsx?|jsx?)$/;
|
|
12516
|
-
const PAGE_OR_LAYOUT_FILE_PATTERN = /\/(page|layout)\.(tsx?|jsx?)$/;
|
|
12517
|
-
const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
|
|
12518
|
-
const OG_ROUTE_PATTERN = /\/og\b/i;
|
|
12519
|
-
const PAGES_DIRECTORY_PATTERN = /\/pages\//;
|
|
12520
|
-
const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
|
|
12521
|
-
"redirect",
|
|
12522
|
-
"permanentRedirect",
|
|
12523
|
-
"notFound",
|
|
12524
|
-
"forbidden",
|
|
12525
|
-
"unauthorized"
|
|
12526
|
-
]);
|
|
12527
|
-
const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
|
|
12528
|
-
const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
|
|
12529
|
-
const APP_DIRECTORY_PATTERN = /\/app\//;
|
|
12530
|
-
const ROUTE_HANDLER_FILE_PATTERN = /\/route\.(tsx?|jsx?)$/;
|
|
12531
|
-
const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
|
|
12532
|
-
const MUTATING_ROUTE_SEGMENTS = new Set([
|
|
12533
|
-
"logout",
|
|
12534
|
-
"log-out",
|
|
12535
|
-
"signout",
|
|
12536
|
-
"sign-out",
|
|
12537
|
-
"unsubscribe",
|
|
12538
|
-
"delete",
|
|
12539
|
-
"remove",
|
|
12540
|
-
"revoke",
|
|
12541
|
-
"cancel",
|
|
12542
|
-
"deactivate"
|
|
12543
|
-
]);
|
|
12544
|
-
const ERROR_BOUNDARY_FILE_PATTERN = /\/(error|global-error)\.(tsx?|jsx?)$/;
|
|
12545
|
-
const GLOBAL_ERROR_FILE_PATTERN = /\/global-error\.(tsx?|jsx?)$/;
|
|
12546
|
-
const ROUTE_HANDLER_HTTP_METHODS = new Set([
|
|
12547
|
-
"GET",
|
|
12548
|
-
"POST",
|
|
12549
|
-
"PUT",
|
|
12550
|
-
"PATCH",
|
|
12551
|
-
"DELETE",
|
|
12552
|
-
"OPTIONS",
|
|
12553
|
-
"HEAD"
|
|
12554
|
-
]);
|
|
12555
|
-
const GOOGLE_ANALYTICS_SCRIPT_PATTERN = /google-analytics\.com|googletagmanager\.com\/gtag/;
|
|
12556
|
-
const OG_IMAGE_FILE_PATTERN = /\/(opengraph-image|twitter-image)\d*\.(tsx?|jsx?)$/;
|
|
12557
|
-
//#endregion
|
|
12558
12449
|
//#region src/plugin/rules/nextjs/nextjs-error-boundary-missing-use-client.ts
|
|
12559
12450
|
const nextjsErrorBoundaryMissingUseClient = defineRule({
|
|
12560
12451
|
id: "nextjs-error-boundary-missing-use-client",
|
|
@@ -12614,11 +12505,11 @@ const hasJsxAttribute = (attributes, attributeName) => Boolean(findJsxAttribute(
|
|
|
12614
12505
|
//#region src/plugin/rules/nextjs/nextjs-image-missing-sizes.ts
|
|
12615
12506
|
const nextjsImageMissingSizes = defineRule({
|
|
12616
12507
|
id: "nextjs-image-missing-sizes",
|
|
12617
|
-
title: "
|
|
12508
|
+
title: "next/image fill image is missing sizes",
|
|
12618
12509
|
tags: ["test-noise"],
|
|
12619
12510
|
requires: ["nextjs"],
|
|
12620
12511
|
severity: "warn",
|
|
12621
|
-
recommendation: "Add sizes
|
|
12512
|
+
recommendation: "Add `sizes` matching your layout so `next/image` does not assume the largest candidate and make users download oversized images.",
|
|
12622
12513
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12623
12514
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "Image") return;
|
|
12624
12515
|
const attributes = node.attributes ?? [];
|
|
@@ -12654,11 +12545,11 @@ const nextjsInlineScriptMissingId = defineRule({
|
|
|
12654
12545
|
//#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
|
|
12655
12546
|
const nextjsMissingMetadata = defineRule({
|
|
12656
12547
|
id: "nextjs-missing-metadata",
|
|
12657
|
-
title: "Page missing metadata",
|
|
12548
|
+
title: "Page missing metadata for search previews",
|
|
12658
12549
|
tags: ["test-noise"],
|
|
12659
12550
|
requires: ["nextjs"],
|
|
12660
12551
|
severity: "warn",
|
|
12661
|
-
recommendation: "Add
|
|
12552
|
+
recommendation: "Add metadata or `generateMetadata()` so search engines and social previews get a title and description.",
|
|
12662
12553
|
create: (context) => ({ Program(programNode) {
|
|
12663
12554
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
12664
12555
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
@@ -12671,7 +12562,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
12671
12562
|
return false;
|
|
12672
12563
|
})) context.report({
|
|
12673
12564
|
node: programNode,
|
|
12674
|
-
message: "This page has no metadata, so search engines
|
|
12565
|
+
message: "This page has no metadata, so search engines and social previews get no title or description."
|
|
12675
12566
|
});
|
|
12676
12567
|
} })
|
|
12677
12568
|
});
|
|
@@ -12679,7 +12570,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
12679
12570
|
//#region src/plugin/rules/nextjs/nextjs-no-a-element.ts
|
|
12680
12571
|
const nextjsNoAElement = defineRule({
|
|
12681
12572
|
id: "nextjs-no-a-element",
|
|
12682
|
-
title: "Plain anchor
|
|
12573
|
+
title: "Plain anchor reloads internal Next.js links",
|
|
12683
12574
|
tags: ["test-noise"],
|
|
12684
12575
|
requires: ["nextjs"],
|
|
12685
12576
|
severity: "warn",
|
|
@@ -12693,7 +12584,7 @@ const nextjsNoAElement = defineRule({
|
|
|
12693
12584
|
else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
|
|
12694
12585
|
if (typeof hrefValue === "string" && hrefValue.startsWith("/")) context.report({
|
|
12695
12586
|
node,
|
|
12696
|
-
message: "Plain <a> reloads the whole page
|
|
12587
|
+
message: "Plain <a> reloads the whole page for internal links, so Next.js loses client-side navigation and prefetching."
|
|
12697
12588
|
});
|
|
12698
12589
|
} })
|
|
12699
12590
|
});
|
|
@@ -12778,11 +12669,11 @@ const nextjsNoClientSideRedirect = defineRule({
|
|
|
12778
12669
|
//#region src/plugin/rules/nextjs/nextjs-no-css-link.ts
|
|
12779
12670
|
const nextjsNoCssLink = defineRule({
|
|
12780
12671
|
id: "nextjs-no-css-link",
|
|
12781
|
-
title: "
|
|
12672
|
+
title: "Linked stylesheet bypasses Next.js CSS optimization",
|
|
12782
12673
|
tags: ["test-noise"],
|
|
12783
12674
|
requires: ["nextjs"],
|
|
12784
12675
|
severity: "warn",
|
|
12785
|
-
recommendation: "Import CSS directly
|
|
12676
|
+
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
12786
12677
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12787
12678
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
12788
12679
|
const attributes = node.attributes ?? [];
|
|
@@ -12795,7 +12686,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
12795
12686
|
if (typeof hrefValue === "string" && GOOGLE_FONTS_PATTERN.test(hrefValue)) return;
|
|
12796
12687
|
context.report({
|
|
12797
12688
|
node,
|
|
12798
|
-
message: "This <link rel=\"stylesheet\"> loads unbundled
|
|
12689
|
+
message: "This <link rel=\"stylesheet\"> bypasses Next.js CSS handling, so the CSS loads unbundled and unoptimized."
|
|
12799
12690
|
});
|
|
12800
12691
|
} })
|
|
12801
12692
|
});
|
|
@@ -12819,7 +12710,7 @@ const nextjsNoDefaultExportInRouteHandler = defineRule({
|
|
|
12819
12710
|
tags: ["test-noise"],
|
|
12820
12711
|
requires: ["nextjs"],
|
|
12821
12712
|
severity: "error",
|
|
12822
|
-
recommendation: "Replace `export default` with named HTTP method exports
|
|
12713
|
+
recommendation: "Replace `export default` with named HTTP method exports because Next.js ignores default exports in `route.ts`.",
|
|
12823
12714
|
create: (context) => {
|
|
12824
12715
|
let isAppRouteHandler = false;
|
|
12825
12716
|
let programNode = null;
|
|
@@ -12906,11 +12797,11 @@ const nextjsNoFontLink = defineRule({
|
|
|
12906
12797
|
//#region src/plugin/rules/nextjs/nextjs-no-google-analytics-script.ts
|
|
12907
12798
|
const nextjsNoGoogleAnalyticsScript = defineRule({
|
|
12908
12799
|
id: "nextjs-no-google-analytics-script",
|
|
12909
|
-
title: "Manual Google Analytics script",
|
|
12800
|
+
title: "Manual Google Analytics script blocks optimized loading",
|
|
12910
12801
|
tags: ["test-noise"],
|
|
12911
12802
|
requires: ["nextjs"],
|
|
12912
12803
|
severity: "warn",
|
|
12913
|
-
recommendation: "Use `import { GoogleAnalytics } from '@next/third-parties/google'` for automatic optimization
|
|
12804
|
+
recommendation: "Use `import { GoogleAnalytics } from '@next/third-parties/google'` for automatic optimization and smaller bundles.",
|
|
12914
12805
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12915
12806
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12916
12807
|
if (node.name.name !== "script" && node.name.name !== "Script") return;
|
|
@@ -12919,7 +12810,7 @@ const nextjsNoGoogleAnalyticsScript = defineRule({
|
|
|
12919
12810
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
12920
12811
|
if (typeof srcValue === "string" && GOOGLE_ANALYTICS_SCRIPT_PATTERN.test(srcValue)) context.report({
|
|
12921
12812
|
node,
|
|
12922
|
-
message: "Manual Google Analytics
|
|
12813
|
+
message: "Manual Google Analytics scripts block rendering without Next.js' optimized loading strategy."
|
|
12923
12814
|
});
|
|
12924
12815
|
} })
|
|
12925
12816
|
});
|
|
@@ -12931,7 +12822,7 @@ const nextjsNoHeadImport = defineRule({
|
|
|
12931
12822
|
tags: ["test-noise"],
|
|
12932
12823
|
requires: ["nextjs"],
|
|
12933
12824
|
severity: "error",
|
|
12934
|
-
recommendation: "Use the Metadata API
|
|
12825
|
+
recommendation: "Use the Metadata API because `next/head` is ignored in the App Router and meta tags will not render.",
|
|
12935
12826
|
create: (context) => ({ ImportDeclaration(node) {
|
|
12936
12827
|
if (node.source?.value !== "next/head") return;
|
|
12937
12828
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
@@ -12946,20 +12837,18 @@ const nextjsNoHeadImport = defineRule({
|
|
|
12946
12837
|
//#region src/plugin/rules/nextjs/nextjs-no-img-element.ts
|
|
12947
12838
|
const nextjsNoImgElement = defineRule({
|
|
12948
12839
|
id: "nextjs-no-img-element",
|
|
12949
|
-
title: "Plain img
|
|
12840
|
+
title: "Plain img ships unoptimized images",
|
|
12950
12841
|
tags: ["test-noise"],
|
|
12951
12842
|
requires: ["nextjs"],
|
|
12952
12843
|
severity: "warn",
|
|
12953
|
-
recommendation: "`
|
|
12844
|
+
recommendation: "Use `next/image` so users get optimized formats, responsive srcsets, and lazy loading instead of oversized image downloads.",
|
|
12954
12845
|
create: (context) => {
|
|
12955
|
-
|
|
12956
|
-
const isOgRoute = OG_ROUTE_PATTERN.test(filename);
|
|
12957
|
-
const isMetadataImageRoute = isNextjsMetadataImageRouteFilename(filename);
|
|
12846
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
12958
12847
|
return { JSXOpeningElement(node) {
|
|
12959
|
-
if (
|
|
12848
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
12960
12849
|
if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "img") context.report({
|
|
12961
12850
|
node,
|
|
12962
|
-
message: "Plain <img> ships unoptimized, oversized images
|
|
12851
|
+
message: "Plain <img> ships unoptimized, oversized images."
|
|
12963
12852
|
});
|
|
12964
12853
|
} };
|
|
12965
12854
|
}
|
|
@@ -12968,11 +12857,11 @@ const nextjsNoImgElement = defineRule({
|
|
|
12968
12857
|
//#region src/plugin/rules/nextjs/nextjs-no-native-script.ts
|
|
12969
12858
|
const nextjsNoNativeScript = defineRule({
|
|
12970
12859
|
id: "nextjs-no-native-script",
|
|
12971
|
-
title: "Plain script
|
|
12860
|
+
title: "Plain script can block Next.js rendering",
|
|
12972
12861
|
tags: ["test-noise"],
|
|
12973
12862
|
requires: ["nextjs"],
|
|
12974
12863
|
severity: "warn",
|
|
12975
|
-
recommendation: "`
|
|
12864
|
+
recommendation: "Use `next/script` with `strategy=\"afterInteractive\"` or `\"lazyOnload\"` so third-party scripts do not block rendering.",
|
|
12976
12865
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12977
12866
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
|
|
12978
12867
|
const typeAttribute = findJsxAttribute(node.attributes ?? [], "type");
|
|
@@ -12980,7 +12869,7 @@ const nextjsNoNativeScript = defineRule({
|
|
|
12980
12869
|
if (typeof typeValue === "string" && !EXECUTABLE_SCRIPT_TYPES.has(typeValue)) return;
|
|
12981
12870
|
context.report({
|
|
12982
12871
|
node,
|
|
12983
|
-
message: "Plain <script>
|
|
12872
|
+
message: "Plain <script> has no Next.js loading strategy, so it can block rendering."
|
|
12984
12873
|
});
|
|
12985
12874
|
} })
|
|
12986
12875
|
});
|
|
@@ -13013,7 +12902,7 @@ const nextjsNoRedirectInTryCatch = defineRule({
|
|
|
13013
12902
|
tags: ["test-noise"],
|
|
13014
12903
|
requires: ["nextjs"],
|
|
13015
12904
|
severity: "warn",
|
|
13016
|
-
recommendation: "Move
|
|
12905
|
+
recommendation: "Move `redirect()` or `notFound()` outside the try block, or rethrow in `catch`, because these APIs throw control-flow errors that catch blocks swallow.",
|
|
13017
12906
|
create: (context) => {
|
|
13018
12907
|
let tryCatchDepth = 0;
|
|
13019
12908
|
return {
|
|
@@ -14164,7 +14053,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
14164
14053
|
tags: ["test-noise"],
|
|
14165
14054
|
requires: ["nextjs"],
|
|
14166
14055
|
severity: "warn",
|
|
14167
|
-
recommendation: "Wrap the component using useSearchParams
|
|
14056
|
+
recommendation: "Wrap the component using `useSearchParams` in `<Suspense>` so the rest of the page can stay statically rendered.",
|
|
14168
14057
|
create: (context) => {
|
|
14169
14058
|
let isPageOrLayoutFile = false;
|
|
14170
14059
|
let hasAncestorLayoutSuspense = false;
|
|
@@ -14202,7 +14091,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
14202
14091
|
if (!componentBody || !astContainsUseSearchParams(componentBody)) return;
|
|
14203
14092
|
context.report({
|
|
14204
14093
|
node,
|
|
14205
|
-
message: `<${node.name.name}> uses useSearchParams()
|
|
14094
|
+
message: `<${node.name.name}> uses useSearchParams() outside <Suspense>, so this page falls back to client-side rendering.`
|
|
14206
14095
|
});
|
|
14207
14096
|
}
|
|
14208
14097
|
};
|
|
@@ -14216,7 +14105,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
14216
14105
|
tags: ["test-noise"],
|
|
14217
14106
|
requires: ["nextjs"],
|
|
14218
14107
|
severity: "warn",
|
|
14219
|
-
recommendation: "Use `import { ImageResponse } from \"next/og\"
|
|
14108
|
+
recommendation: "Use `import { ImageResponse } from \"next/og\"`; do not import `@vercel/og` directly because Next.js already bundles it.",
|
|
14220
14109
|
create: (context) => ({ ImportDeclaration(node) {
|
|
14221
14110
|
if (node.source?.value !== "@vercel/og") return;
|
|
14222
14111
|
context.report({
|
|
@@ -14730,7 +14619,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
14730
14619
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
|
|
14731
14620
|
context.report({
|
|
14732
14621
|
node: callExpr,
|
|
14733
|
-
message: "
|
|
14622
|
+
message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
|
|
14734
14623
|
});
|
|
14735
14624
|
}
|
|
14736
14625
|
} })
|
|
@@ -14743,7 +14632,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
14743
14632
|
title: "aria-hidden on focusable element",
|
|
14744
14633
|
tags: ["react-jsx-only"],
|
|
14745
14634
|
severity: "warn",
|
|
14746
|
-
recommendation: "Remove `aria-hidden` from focusable elements, or stop them being focusable.",
|
|
14635
|
+
recommendation: "Remove `aria-hidden` from focusable elements, or stop them being focusable, so keyboard users do not land on content screen readers hide.",
|
|
14747
14636
|
category: "Accessibility",
|
|
14748
14637
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
14749
14638
|
const ariaHidden = hasJsxPropIgnoreCase(node.attributes, "aria-hidden");
|
|
@@ -15287,7 +15176,7 @@ const noArrayIndexKey = defineRule({
|
|
|
15287
15176
|
title: "Array index used as a key",
|
|
15288
15177
|
severity: "warn",
|
|
15289
15178
|
defaultEnabled: false,
|
|
15290
|
-
recommendation: "Use a stable `key` from your data
|
|
15179
|
+
recommendation: "Use a stable `key` from your data so reordered items keep the right state and DOM.",
|
|
15291
15180
|
category: "Performance",
|
|
15292
15181
|
create: (context) => ({
|
|
15293
15182
|
JSXOpeningElement(node) {
|
|
@@ -15364,7 +15253,7 @@ const noArrayIndexKey = defineRule({
|
|
|
15364
15253
|
});
|
|
15365
15254
|
//#endregion
|
|
15366
15255
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
15367
|
-
const MESSAGE$28 = "
|
|
15256
|
+
const MESSAGE$28 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
15368
15257
|
const resolveSettings$21 = (settings) => {
|
|
15369
15258
|
const reactDoctor = settings?.["react-doctor"];
|
|
15370
15259
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -15598,7 +15487,7 @@ const noCascadingSetState = defineRule({
|
|
|
15598
15487
|
title: "Multiple setState calls in one effect",
|
|
15599
15488
|
severity: "warn",
|
|
15600
15489
|
tags: ["test-noise"],
|
|
15601
|
-
recommendation: "Combine
|
|
15490
|
+
recommendation: "Combine related updates in `useReducer` so one effect does not redraw the screen once per `setState` call.",
|
|
15602
15491
|
create: (context) => ({ CallExpression(node) {
|
|
15603
15492
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
15604
15493
|
if (isInitOnlyEffect(node)) return;
|
|
@@ -15644,12 +15533,12 @@ const noChainStateUpdates = defineRule({
|
|
|
15644
15533
|
});
|
|
15645
15534
|
//#endregion
|
|
15646
15535
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
15647
|
-
const MESSAGE$27 = "
|
|
15536
|
+
const MESSAGE$27 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
15648
15537
|
const noChildrenProp = defineRule({
|
|
15649
15538
|
id: "no-children-prop",
|
|
15650
15539
|
title: "Children passed as a prop",
|
|
15651
15540
|
severity: "warn",
|
|
15652
|
-
recommendation: "Nest children between the tags
|
|
15541
|
+
recommendation: "Nest children between the tags so the rendered content is visible in JSX and cannot be hidden inside a props object.",
|
|
15653
15542
|
create: (context) => ({
|
|
15654
15543
|
JSXAttribute(node) {
|
|
15655
15544
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -15677,13 +15566,13 @@ const noChildrenProp = defineRule({
|
|
|
15677
15566
|
});
|
|
15678
15567
|
//#endregion
|
|
15679
15568
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
15680
|
-
const MESSAGE$26 = "`React.cloneElement`
|
|
15569
|
+
const MESSAGE$26 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
15681
15570
|
const noCloneElement = defineRule({
|
|
15682
15571
|
id: "no-clone-element",
|
|
15683
|
-
title: "
|
|
15572
|
+
title: "cloneElement makes child props fragile",
|
|
15684
15573
|
severity: "warn",
|
|
15685
15574
|
defaultEnabled: false,
|
|
15686
|
-
recommendation: "Pass children
|
|
15575
|
+
recommendation: "Pass children or render props instead so parent code does not depend on fragile cloned child props.",
|
|
15687
15576
|
category: "Architecture",
|
|
15688
15577
|
create: (context) => ({ CallExpression(node) {
|
|
15689
15578
|
const callee = stripParenExpression(node.callee);
|
|
@@ -15915,7 +15804,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
15915
15804
|
title: "Store created during render",
|
|
15916
15805
|
severity: "error",
|
|
15917
15806
|
category: "Correctness",
|
|
15918
|
-
recommendation: "Create
|
|
15807
|
+
recommendation: "Create stores at module scope so subscribers are not cut off and saved state does not reset every render.",
|
|
15919
15808
|
create: (context) => ({ CallExpression(node) {
|
|
15920
15809
|
const factory = resolveStoreFactoryForCallee(node.callee);
|
|
15921
15810
|
if (!factory) return;
|
|
@@ -15931,10 +15820,10 @@ const noCreateStoreInRender = defineRule({
|
|
|
15931
15820
|
const MESSAGE$24 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
15932
15821
|
const noDanger = defineRule({
|
|
15933
15822
|
id: "no-danger",
|
|
15934
|
-
title: "
|
|
15823
|
+
title: "Raw HTML injection can run unsafe markup",
|
|
15935
15824
|
severity: "warn",
|
|
15936
15825
|
category: "Security",
|
|
15937
|
-
recommendation: "Render trusted content as React children
|
|
15826
|
+
recommendation: "Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.",
|
|
15938
15827
|
create: (context) => ({
|
|
15939
15828
|
JSXOpeningElement(node) {
|
|
15940
15829
|
const propAttribute = hasJsxProp(node.attributes, "dangerouslySetInnerHTML");
|
|
@@ -16020,7 +15909,7 @@ const noDangerWithChildren = defineRule({
|
|
|
16020
15909
|
id: "no-danger-with-children",
|
|
16021
15910
|
title: "dangerouslySetInnerHTML with children",
|
|
16022
15911
|
severity: "error",
|
|
16023
|
-
recommendation: "Use either `children` or `dangerouslySetInnerHTML
|
|
15912
|
+
recommendation: "Use either `children` or `dangerouslySetInnerHTML` so React does not ignore one source of content.",
|
|
16024
15913
|
category: "Correctness",
|
|
16025
15914
|
create: (context) => ({
|
|
16026
15915
|
JSXElement(node) {
|
|
@@ -16179,7 +16068,7 @@ const noDarkModeGlow = defineRule({
|
|
|
16179
16068
|
if (!hasDarkBackground || !shadowValue || !shadowProperty) return;
|
|
16180
16069
|
if (hasColoredGlowShadow(shadowValue)) context.report({
|
|
16181
16070
|
node: shadowProperty,
|
|
16182
|
-
message: "
|
|
16071
|
+
message: "A strong colored glow on a dark background can feel heavy. Use a subtle, neutral shadow instead."
|
|
16183
16072
|
});
|
|
16184
16073
|
} })
|
|
16185
16074
|
});
|
|
@@ -16555,7 +16444,7 @@ const noDerivedUseState = defineRule({
|
|
|
16555
16444
|
title: "Prop derived into useState",
|
|
16556
16445
|
tags: ["test-noise"],
|
|
16557
16446
|
severity: "warn",
|
|
16558
|
-
recommendation: "
|
|
16447
|
+
recommendation: "Compute the value inline so prop changes do not leave `useState` holding a stale copy.",
|
|
16559
16448
|
create: (context) => {
|
|
16560
16449
|
const propStackTracker = createComponentPropStackTracker();
|
|
16561
16450
|
return {
|
|
@@ -16647,7 +16536,7 @@ const noDidMountSetState = defineRule({
|
|
|
16647
16536
|
//#endregion
|
|
16648
16537
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
16649
16538
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
16650
|
-
const MESSAGE$21 = "
|
|
16539
|
+
const MESSAGE$21 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
16651
16540
|
const resolveSettings$19 = (settings) => {
|
|
16652
16541
|
const reactDoctor = settings?.["react-doctor"];
|
|
16653
16542
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -16887,7 +16776,7 @@ const noDistractingElements = defineRule({
|
|
|
16887
16776
|
title: "Distracting marquee or blink element",
|
|
16888
16777
|
tags: ["react-jsx-only"],
|
|
16889
16778
|
severity: "error",
|
|
16890
|
-
recommendation: "Replace `<marquee>` and `<blink>` with normal
|
|
16779
|
+
recommendation: "Replace `<marquee>` and `<blink>` with normal markup so motion does not distract or disorient users.",
|
|
16891
16780
|
category: "Accessibility",
|
|
16892
16781
|
create: (context) => {
|
|
16893
16782
|
const { distractingTags } = resolveSettings$18(context.settings);
|
|
@@ -16916,7 +16805,7 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
16916
16805
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
16917
16806
|
context.report({
|
|
16918
16807
|
node,
|
|
16919
|
-
message: "
|
|
16808
|
+
message: "Calling `document.startViewTransition()` directly can bypass React's `<ViewTransition>` animation lifecycle."
|
|
16920
16809
|
});
|
|
16921
16810
|
} })
|
|
16922
16811
|
});
|
|
@@ -16934,13 +16823,13 @@ const noDynamicImportPath = defineRule({
|
|
|
16934
16823
|
if (source && !isNodeOfType(source, "Literal") && !isNodeOfType(source, "TemplateLiteral")) {
|
|
16935
16824
|
context.report({
|
|
16936
16825
|
node,
|
|
16937
|
-
message: "This
|
|
16826
|
+
message: "This can stay in the main bundle because the bundler cannot code-split a dynamic import path. Use a plain string path instead."
|
|
16938
16827
|
});
|
|
16939
16828
|
return;
|
|
16940
16829
|
}
|
|
16941
16830
|
if (isNodeOfType(source, "TemplateLiteral") && (source.expressions?.length ?? 0) > 0) context.report({
|
|
16942
16831
|
node,
|
|
16943
|
-
message: "This
|
|
16832
|
+
message: "This can stay in the main bundle because the bundler cannot code-split a dynamic import path with `${dynamic_path}`. Use a plain string path instead."
|
|
16944
16833
|
});
|
|
16945
16834
|
},
|
|
16946
16835
|
CallExpression(node) {
|
|
@@ -17231,7 +17120,7 @@ const noEffectEventHandler = defineRule({
|
|
|
17231
17120
|
title: "Effect used as an event handler",
|
|
17232
17121
|
tags: ["test-noise"],
|
|
17233
17122
|
severity: "warn",
|
|
17234
|
-
recommendation: "Move
|
|
17123
|
+
recommendation: "Move event logic into the handler that starts it so the side effect does not run late after an extra render.",
|
|
17235
17124
|
create: (context) => {
|
|
17236
17125
|
const propStackTracker = createComponentPropStackTracker();
|
|
17237
17126
|
return {
|
|
@@ -17408,7 +17297,7 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
17408
17297
|
//#region src/plugin/rules/security/no-eval.ts
|
|
17409
17298
|
const noEval = defineRule({
|
|
17410
17299
|
id: "no-eval",
|
|
17411
|
-
title: "
|
|
17300
|
+
title: "eval() runs untrusted code strings",
|
|
17412
17301
|
severity: "error",
|
|
17413
17302
|
recommendation: "Use `JSON.parse` for data, or rewrite the code so it doesn't build and run code from strings.",
|
|
17414
17303
|
create: (context) => ({
|
|
@@ -18281,14 +18170,14 @@ const noFetchInEffect = defineRule({
|
|
|
18281
18170
|
id: "no-fetch-in-effect",
|
|
18282
18171
|
title: "Data fetching inside an effect",
|
|
18283
18172
|
severity: "warn",
|
|
18284
|
-
recommendation: "Use
|
|
18173
|
+
recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
|
|
18285
18174
|
create: (context) => ({ CallExpression(node) {
|
|
18286
18175
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
18287
18176
|
const callback = getEffectCallback(node);
|
|
18288
18177
|
if (!callback) return;
|
|
18289
18178
|
if (containsFetchCall(callback)) context.report({
|
|
18290
18179
|
node,
|
|
18291
|
-
message: "fetch() inside useEffect
|
|
18180
|
+
message: "fetch() inside useEffect can race, double-fire, or leak. Use a data-fetching layer or Server Component instead."
|
|
18292
18181
|
});
|
|
18293
18182
|
} })
|
|
18294
18183
|
});
|
|
@@ -18302,9 +18191,9 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
18302
18191
|
const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
18303
18192
|
const noFindDomNode = defineRule({
|
|
18304
18193
|
id: "no-find-dom-node",
|
|
18305
|
-
title: "
|
|
18194
|
+
title: "findDOMNode breaks component encapsulation",
|
|
18306
18195
|
severity: "warn",
|
|
18307
|
-
recommendation: "Use a ref
|
|
18196
|
+
recommendation: "Use a ref to reach DOM nodes because `findDOMNode` was removed in React 19 and can crash the app.",
|
|
18308
18197
|
create: (context) => ({ CallExpression(node) {
|
|
18309
18198
|
const callee = node.callee;
|
|
18310
18199
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
@@ -18341,7 +18230,7 @@ const noFlushSync = defineRule({
|
|
|
18341
18230
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
18342
18231
|
if (getImportedName$1(specifier) === "flushSync") context.report({
|
|
18343
18232
|
node: specifier,
|
|
18344
|
-
message: "
|
|
18233
|
+
message: "`flushSync` forces an immediate update, which skips View Transitions and concurrent rendering."
|
|
18345
18234
|
});
|
|
18346
18235
|
}
|
|
18347
18236
|
} })
|
|
@@ -18445,10 +18334,10 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => containsRend
|
|
|
18445
18334
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
18446
18335
|
const noGiantComponent = defineRule({
|
|
18447
18336
|
id: "no-giant-component",
|
|
18448
|
-
title: "
|
|
18337
|
+
title: "Large component is hard to read and change",
|
|
18449
18338
|
severity: "warn",
|
|
18450
18339
|
tags: ["test-noise", "react-jsx-only"],
|
|
18451
|
-
recommendation: "Pull each section into its own component
|
|
18340
|
+
recommendation: "Pull each section into its own component so the parent is easier to read, test, and change.",
|
|
18452
18341
|
create: (context) => {
|
|
18453
18342
|
const getOversizedComponentLineCount = (bodyNode) => {
|
|
18454
18343
|
if (!bodyNode.loc) return null;
|
|
@@ -18686,11 +18575,11 @@ const noInlineBounceEasing = defineRule({
|
|
|
18686
18575
|
if (!value) continue;
|
|
18687
18576
|
if ((key === "transition" || key === "transitionTimingFunction" || key === "animation" || key === "animationTimingFunction") && isOvershootCubicBezier(value)) context.report({
|
|
18688
18577
|
node: property,
|
|
18689
|
-
message: "
|
|
18578
|
+
message: "This bouncy easing can feel distracting. Use ease-out or cubic-bezier(0.16, 1, 0.3, 1) for a smoother finish."
|
|
18690
18579
|
});
|
|
18691
18580
|
if ((key === "animation" || key === "animationName") && hasBounceAnimationName(value)) context.report({
|
|
18692
18581
|
node: property,
|
|
18693
|
-
message: "
|
|
18582
|
+
message: "This bounce animation can feel distracting. Use a smooth ease-out, like ease-out-quart or expo, for a natural finish."
|
|
18694
18583
|
});
|
|
18695
18584
|
}
|
|
18696
18585
|
},
|
|
@@ -18708,7 +18597,7 @@ const noInlineBounceEasing = defineRule({
|
|
|
18708
18597
|
//#region src/plugin/rules/design/no-inline-exhaustive-style.ts
|
|
18709
18598
|
const noInlineExhaustiveStyle = defineRule({
|
|
18710
18599
|
id: "no-inline-exhaustive-style",
|
|
18711
|
-
title: "
|
|
18600
|
+
title: "Large inline style object rebuilds every render",
|
|
18712
18601
|
severity: "warn",
|
|
18713
18602
|
tags: ["test-noise", "react-jsx-only"],
|
|
18714
18603
|
recommendation: "Move the styles to a CSS class, CSS module, Tailwind utilities, or a styled component. Big inline objects are hard to read and rebuild on every update.",
|
|
@@ -18828,7 +18717,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
|
|
|
18828
18717
|
//#region src/plugin/rules/react-builtins/no-is-mounted.ts
|
|
18829
18718
|
const noIsMounted = defineRule({
|
|
18830
18719
|
id: "no-is-mounted",
|
|
18831
|
-
title: "
|
|
18720
|
+
title: "isMounted lets async callbacks update after unmount",
|
|
18832
18721
|
severity: "warn",
|
|
18833
18722
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
18834
18723
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -18840,7 +18729,7 @@ const noIsMounted = defineRule({
|
|
|
18840
18729
|
if (ancestor.type === "MethodDefinition" || ancestor.type === "Property") {
|
|
18841
18730
|
context.report({
|
|
18842
18731
|
node,
|
|
18843
|
-
message: "`isMounted` is unreliable in modern React
|
|
18732
|
+
message: "`isMounted` is unreliable in modern React, so async callbacks can update state after unmount."
|
|
18844
18733
|
});
|
|
18845
18734
|
return;
|
|
18846
18735
|
}
|
|
@@ -18945,7 +18834,7 @@ const noLargeAnimatedBlur = defineRule({
|
|
|
18945
18834
|
const blurRadius = Number.parseFloat(match[1]);
|
|
18946
18835
|
if (blurRadius > 10) context.report({
|
|
18947
18836
|
node: property,
|
|
18948
|
-
message: `
|
|
18837
|
+
message: `Large animated blurs can use significant GPU memory on phones because blur(${blurRadius}px) gets heavier as the blur and element grow. Use a smaller blur or a smaller element.`
|
|
18949
18838
|
});
|
|
18950
18839
|
}
|
|
18951
18840
|
} })
|
|
@@ -19189,10 +19078,10 @@ const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
|
19189
19078
|
};
|
|
19190
19079
|
const noManyBooleanProps = defineRule({
|
|
19191
19080
|
id: "no-many-boolean-props",
|
|
19192
|
-
title: "
|
|
19081
|
+
title: "Boolean prop combinations are hard to test",
|
|
19193
19082
|
severity: "warn",
|
|
19194
19083
|
tags: ["test-noise", "react-jsx-only"],
|
|
19195
|
-
recommendation: "Split into smaller components or named variants
|
|
19084
|
+
recommendation: "Split boolean-heavy APIs into smaller components or named variants so combinations stay testable.",
|
|
19196
19085
|
create: (context) => {
|
|
19197
19086
|
const reportIfMany = (booleanLikePropNames, componentName, reportNode) => {
|
|
19198
19087
|
if (booleanLikePropNames.length >= 4) context.report({
|
|
@@ -19323,7 +19212,7 @@ const noMoment = defineRule({
|
|
|
19323
19212
|
});
|
|
19324
19213
|
//#endregion
|
|
19325
19214
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
19326
|
-
const MESSAGE$17 = "This file is harder to
|
|
19215
|
+
const MESSAGE$17 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
19327
19216
|
const resolveSettings$16 = (settings) => {
|
|
19328
19217
|
const reactDoctor = settings?.["react-doctor"];
|
|
19329
19218
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -19620,7 +19509,7 @@ const noMultiComp = defineRule({
|
|
|
19620
19509
|
id: "no-multi-comp",
|
|
19621
19510
|
title: "Multiple components in one file",
|
|
19622
19511
|
severity: "warn",
|
|
19623
|
-
recommendation: "Move secondary components into their own files.",
|
|
19512
|
+
recommendation: "Move secondary components into their own files so each component stays easier to find, test, and change.",
|
|
19624
19513
|
category: "Architecture",
|
|
19625
19514
|
create: (context) => {
|
|
19626
19515
|
const settings = resolveSettings$16(context.settings);
|
|
@@ -19700,11 +19589,11 @@ const noMutableInDeps = defineRule({
|
|
|
19700
19589
|
if (!issue) continue;
|
|
19701
19590
|
if (issue.kind === "ref-current") context.report({
|
|
19702
19591
|
node: element,
|
|
19703
|
-
message: `
|
|
19592
|
+
message: `Changing "${issue.rootName}.current" does not re-render the component, so this dependency will not make the effect run again.`
|
|
19704
19593
|
});
|
|
19705
19594
|
else context.report({
|
|
19706
19595
|
node: element,
|
|
19707
|
-
message: `
|
|
19596
|
+
message: `Values like "${issue.rootName}.*" can change without re-rendering the component, so this dependency will not make the effect run again.`
|
|
19708
19597
|
});
|
|
19709
19598
|
}
|
|
19710
19599
|
});
|
|
@@ -20135,7 +20024,7 @@ const noNamespace = defineRule({
|
|
|
20135
20024
|
id: "no-namespace",
|
|
20136
20025
|
title: "Namespaced JSX element",
|
|
20137
20026
|
severity: "warn",
|
|
20138
|
-
recommendation: "
|
|
20027
|
+
recommendation: "Use a plain component or DOM tag because React cannot render JSX namespaced names like `ns:Foo`.",
|
|
20139
20028
|
create: (context) => ({
|
|
20140
20029
|
JSXOpeningElement(node) {
|
|
20141
20030
|
if (!isNodeOfType(node.name, "JSXNamespacedName")) return;
|
|
@@ -20167,7 +20056,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
20167
20056
|
tags: ["test-noise", "react-jsx-only"],
|
|
20168
20057
|
severity: "error",
|
|
20169
20058
|
category: "Correctness",
|
|
20170
|
-
recommendation: "Move to a separate file
|
|
20059
|
+
recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
|
|
20171
20060
|
create: (context) => {
|
|
20172
20061
|
const componentStack = [];
|
|
20173
20062
|
return {
|
|
@@ -20232,7 +20121,7 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
20232
20121
|
});
|
|
20233
20122
|
//#endregion
|
|
20234
20123
|
//#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
|
|
20235
|
-
const buildMessage$11 = (tag, role) => `
|
|
20124
|
+
const buildMessage$11 = (tag, role) => `Role \`${role}\` gives \`<${tag}>\` interactive semantics even though the element is noninteractive, so screen reader users get the wrong controls.`;
|
|
20236
20125
|
const DEFAULT_ALLOWED_ROLES = {
|
|
20237
20126
|
ul: [
|
|
20238
20127
|
"menu",
|
|
@@ -21071,13 +20960,13 @@ const noRandomKey = defineRule({
|
|
|
21071
20960
|
if (!freshDescription) return;
|
|
21072
20961
|
context.report({
|
|
21073
20962
|
node: node.value,
|
|
21074
|
-
message: `
|
|
20963
|
+
message: `A changing key makes React rebuild each item, which can reset typed input, focus, and scroll position. Use a stable id from the item instead of \`key={${freshDescription}}\`.`
|
|
21075
20964
|
});
|
|
21076
20965
|
} })
|
|
21077
20966
|
});
|
|
21078
20967
|
//#endregion
|
|
21079
20968
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
21080
|
-
const MESSAGE$14 = "`React.Children`
|
|
20969
|
+
const MESSAGE$14 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
21081
20970
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
21082
20971
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
21083
20972
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -21091,10 +20980,10 @@ const isReactNamespaceMember = (node, contextNode) => {
|
|
|
21091
20980
|
};
|
|
21092
20981
|
const noReactChildren = defineRule({
|
|
21093
20982
|
id: "no-react-children",
|
|
21094
|
-
title: "
|
|
20983
|
+
title: "React.Children is fragile when child shape changes",
|
|
21095
20984
|
severity: "warn",
|
|
21096
20985
|
defaultEnabled: false,
|
|
21097
|
-
recommendation: "Pass children as props or render them directly
|
|
20986
|
+
recommendation: "Pass children as explicit props or render them directly so child shape changes do not break traversal logic.",
|
|
21098
20987
|
category: "Architecture",
|
|
21099
20988
|
create: (context) => ({ CallExpression(node) {
|
|
21100
20989
|
const calleeOuter = stripParenExpression(node.callee);
|
|
@@ -21192,7 +21081,7 @@ const reportTestUtilsImports = (node, context) => {
|
|
|
21192
21081
|
};
|
|
21193
21082
|
const noReactDomDeprecatedApis = defineRule({
|
|
21194
21083
|
id: "no-react-dom-deprecated-apis",
|
|
21195
|
-
title: "Deprecated react-dom APIs",
|
|
21084
|
+
title: "Deprecated react-dom APIs break in React 19",
|
|
21196
21085
|
requires: ["react:18"],
|
|
21197
21086
|
tags: ["test-noise", "migration-hint"],
|
|
21198
21087
|
severity: "warn",
|
|
@@ -21209,7 +21098,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
21209
21098
|
});
|
|
21210
21099
|
const noReact19DeprecatedApis = defineRule({
|
|
21211
21100
|
id: "no-react19-deprecated-apis",
|
|
21212
|
-
title: "
|
|
21101
|
+
title: "React 19 API migration can break callers",
|
|
21213
21102
|
requires: ["react:19"],
|
|
21214
21103
|
tags: ["test-noise", "migration-hint"],
|
|
21215
21104
|
severity: "warn",
|
|
@@ -21317,7 +21206,7 @@ const noRedundantRoles = defineRule({
|
|
|
21317
21206
|
title: "Redundant ARIA role",
|
|
21318
21207
|
tags: ["react-jsx-only"],
|
|
21319
21208
|
severity: "warn",
|
|
21320
|
-
recommendation: "Remove `role` attributes
|
|
21209
|
+
recommendation: "Remove redundant `role` attributes so assistive tech reads the element's native semantics without extra noise.",
|
|
21321
21210
|
category: "Accessibility",
|
|
21322
21211
|
create: (context) => {
|
|
21323
21212
|
const settings = resolveSettings$13(context.settings);
|
|
@@ -21387,7 +21276,7 @@ const noRenderInRender = defineRule({
|
|
|
21387
21276
|
title: "Component rendered by inline function call",
|
|
21388
21277
|
severity: "warn",
|
|
21389
21278
|
tags: ["test-noise"],
|
|
21390
|
-
recommendation: "Make it a named component
|
|
21279
|
+
recommendation: "Make it a named component so React preserves its identity and does not remount its state.",
|
|
21391
21280
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
21392
21281
|
const expression = node.expression;
|
|
21393
21282
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
@@ -21406,7 +21295,7 @@ const noRenderInRender = defineRule({
|
|
|
21406
21295
|
const RENDER_PROP_PATTERN = /^render[A-Z]/;
|
|
21407
21296
|
const noRenderPropChildren = defineRule({
|
|
21408
21297
|
id: "no-render-prop-children",
|
|
21409
|
-
title: "
|
|
21298
|
+
title: "Render-prop slots make this component hard to extend",
|
|
21410
21299
|
tags: ["test-noise"],
|
|
21411
21300
|
severity: "warn",
|
|
21412
21301
|
recommendation: "Swap `renderXxx` props for child components like `<Modal.Header>` or plain `children`, so the parent doesn't control every slot.",
|
|
@@ -21586,12 +21475,22 @@ const PUBLIC_CLIENT_KEY_PATTERNS = [
|
|
|
21586
21475
|
/^public-token-(?:live|test)-/,
|
|
21587
21476
|
/^pk\.eyJ/
|
|
21588
21477
|
];
|
|
21478
|
+
const SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS = [
|
|
21479
|
+
/^[\s._\-*\u2022xX]{8,}$/,
|
|
21480
|
+
/(?:\.{3,}|\u2026|[*\u2022]{3,})/,
|
|
21481
|
+
/(?:^|[_\-\s])(?:your|redacted|masked|placeholder|replace[_\-\s]?me|changeme)(?:$|[_\-\s])/i,
|
|
21482
|
+
/<[^>]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^>]*>/i,
|
|
21483
|
+
/\[[^\]]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^\]]*\]/i,
|
|
21484
|
+
/\{[^}]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^}]*\}/i
|
|
21485
|
+
];
|
|
21486
|
+
const SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS = [/(?:^|[_\-\s])(?:example|sample|dummy)(?:$|[_\-\s])/i];
|
|
21487
|
+
const SECRET_PLACEHOLDER_CONTEXT_PATTERN = /(?:placeholder|example|sample|dummy|masked|redacted|mask)/i;
|
|
21589
21488
|
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
|
|
21590
21489
|
const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
|
|
21591
21490
|
const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
|
|
21592
21491
|
const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
|
|
21593
21492
|
const SECRET_SERVER_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.server\.[cm]?[jt]sx?$/;
|
|
21594
|
-
const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|route)\.[cm]?[jt]sx?$/;
|
|
21493
|
+
const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|proxy|route)\.[cm]?[jt]sx?$/;
|
|
21595
21494
|
const SECRET_NEXT_PAGES_API_FILE_PATTERN = /(?:^|\/)pages\/api\/.+\.[cm]?[jt]sx?$/;
|
|
21596
21495
|
const SECRET_CLIENT_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.(?:client|browser|web)\.[cm]?[jt]sx?$/;
|
|
21597
21496
|
const SECRET_CLIENT_ENTRY_FILE_PATTERN = /(?:^|\/)(?:src\/)?(?:main|index|[Aa]pp|client)\.[cm]?[jt]sx?$/;
|
|
@@ -21871,6 +21770,15 @@ const isInsideServerOnlyScope = (node) => {
|
|
|
21871
21770
|
return false;
|
|
21872
21771
|
};
|
|
21873
21772
|
//#endregion
|
|
21773
|
+
//#region src/plugin/utils/is-placeholder-secret-value.ts
|
|
21774
|
+
const isPlaceholderSecretValue = (literalValue, options) => {
|
|
21775
|
+
const trimmedValue = literalValue.trim();
|
|
21776
|
+
if (trimmedValue.length === 0) return false;
|
|
21777
|
+
if (SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS.some((pattern) => pattern.test(trimmedValue))) return true;
|
|
21778
|
+
if (!options.allowContextualExamples) return false;
|
|
21779
|
+
return SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS.some((pattern) => pattern.test(trimmedValue));
|
|
21780
|
+
};
|
|
21781
|
+
//#endregion
|
|
21874
21782
|
//#region src/plugin/rules/security/no-secrets-in-client-code.ts
|
|
21875
21783
|
const noSecretsInClientCode = defineRule({
|
|
21876
21784
|
id: "no-secrets-in-client-code",
|
|
@@ -21899,21 +21807,28 @@ const noSecretsInClientCode = defineRule({
|
|
|
21899
21807
|
if (!isNodeOfType(node.init, "Literal") || typeof node.init.value !== "string") return;
|
|
21900
21808
|
const variableName = node.id.name;
|
|
21901
21809
|
const literalValue = node.init.value;
|
|
21810
|
+
const componentOrHookName = enclosingComponentOrHookName(node);
|
|
21811
|
+
const hasPlaceholderContext = SECRET_PLACEHOLDER_CONTEXT_PATTERN.test(variableName) || componentOrHookName !== null && SECRET_PLACEHOLDER_CONTEXT_PATTERN.test(componentOrHookName);
|
|
21812
|
+
const isUnambiguousPlaceholderValue = isPlaceholderSecretValue(literalValue, { allowContextualExamples: false });
|
|
21813
|
+
const isPlaceholderValueForVariableHeuristic = isPlaceholderSecretValue(literalValue, { allowContextualExamples: hasPlaceholderContext });
|
|
21902
21814
|
if (PUBLIC_CLIENT_KEY_PATTERNS.some((pattern) => pattern.test(literalValue))) return;
|
|
21815
|
+
if (SECRET_PATTERNS.some((pattern) => pattern.test(literalValue))) {
|
|
21816
|
+
if (!isUnambiguousPlaceholderValue) context.report({
|
|
21817
|
+
node,
|
|
21818
|
+
message: "This hardcoded secret is a security vulnerability: it ships to the browser where anyone can read it."
|
|
21819
|
+
});
|
|
21820
|
+
return;
|
|
21821
|
+
}
|
|
21903
21822
|
const isServerOnlyScope = isInsideServerOnlyScope(node);
|
|
21904
21823
|
const trailingSuffix = getIdentifierTrailingWord(variableName);
|
|
21905
21824
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
21906
|
-
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && literalValue.length > 24) {
|
|
21825
|
+
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPlaceholderValueForVariableHeuristic && literalValue.length > 24) {
|
|
21907
21826
|
context.report({
|
|
21908
21827
|
node,
|
|
21909
21828
|
message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
|
|
21910
21829
|
});
|
|
21911
21830
|
return;
|
|
21912
21831
|
}
|
|
21913
|
-
if (SECRET_PATTERNS.some((pattern) => pattern.test(literalValue))) context.report({
|
|
21914
|
-
node,
|
|
21915
|
-
message: "This hardcoded secret is a security vulnerability: it ships to the browser where anyone can read it."
|
|
21916
|
-
});
|
|
21917
21832
|
}
|
|
21918
21833
|
};
|
|
21919
21834
|
}
|
|
@@ -22256,7 +22171,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
22256
22171
|
reportedStateNames.add(stateName);
|
|
22257
22172
|
context.report({
|
|
22258
22173
|
node: setterCall,
|
|
22259
|
-
message: `${setterCall.callee.name}()
|
|
22174
|
+
message: `${setterCall.callee.name}() updates \`${stateName}\`, which is also in this effect's dependency list. Guard the update or move the derivation out of the effect.`
|
|
22260
22175
|
});
|
|
22261
22176
|
}
|
|
22262
22177
|
}
|
|
@@ -22288,13 +22203,13 @@ const getParentComponent = (node) => {
|
|
|
22288
22203
|
};
|
|
22289
22204
|
//#endregion
|
|
22290
22205
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
22291
|
-
const MESSAGE$12 = "
|
|
22206
|
+
const MESSAGE$12 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
22292
22207
|
const noSetState = defineRule({
|
|
22293
22208
|
id: "no-set-state",
|
|
22294
|
-
title: "
|
|
22209
|
+
title: "Local class state forbidden",
|
|
22295
22210
|
severity: "warn",
|
|
22296
22211
|
defaultEnabled: false,
|
|
22297
|
-
recommendation: "Lift state up or use an external store
|
|
22212
|
+
recommendation: "Lift state up or use an external store so class-local state does not hide ownership.",
|
|
22298
22213
|
category: "Architecture",
|
|
22299
22214
|
create: (context) => ({ CallExpression(node) {
|
|
22300
22215
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
@@ -22336,7 +22251,7 @@ const noSetStateInRender = defineRule({
|
|
|
22336
22251
|
const setterIdentifierName = setterCall.callee.name;
|
|
22337
22252
|
context.report({
|
|
22338
22253
|
node: setterCall,
|
|
22339
|
-
message: `${setterIdentifierName}()
|
|
22254
|
+
message: `${setterIdentifierName}() triggers another render while rendering. Move it to an effect or event handler, or compute the value during render.`
|
|
22340
22255
|
});
|
|
22341
22256
|
}
|
|
22342
22257
|
};
|
|
@@ -22570,9 +22485,9 @@ const resolveSettings$11 = (settings) => {
|
|
|
22570
22485
|
};
|
|
22571
22486
|
const noStringRefs = defineRule({
|
|
22572
22487
|
id: "no-string-refs",
|
|
22573
|
-
title: "
|
|
22488
|
+
title: "String refs are legacy and fragile",
|
|
22574
22489
|
severity: "warn",
|
|
22575
|
-
recommendation: "Use a callback ref
|
|
22490
|
+
recommendation: "Use a callback ref or `useRef` so ref ownership is explicit and not tied to legacy string lookup.",
|
|
22576
22491
|
create: (context) => {
|
|
22577
22492
|
const { noTemplateLiterals = false } = resolveSettings$11(context.settings);
|
|
22578
22493
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -22651,7 +22566,7 @@ const noThisInSfc = defineRule({
|
|
|
22651
22566
|
id: "no-this-in-sfc",
|
|
22652
22567
|
title: "this used in function component",
|
|
22653
22568
|
severity: "warn",
|
|
22654
|
-
recommendation: "Read from the `props` argument
|
|
22569
|
+
recommendation: "Read from the `props` argument because function components do not have a React instance `this`.",
|
|
22655
22570
|
create: (context) => {
|
|
22656
22571
|
const configured = (context.settings?.react)?.createClass;
|
|
22657
22572
|
const customClassFactoryNames = /* @__PURE__ */ new Set();
|
|
@@ -22795,10 +22710,10 @@ const noUncontrolledInput = defineRule({
|
|
|
22795
22710
|
const hasAllowedPartner = VALUE_PARTNER_ATTRIBUTES.some((partnerAttributeName) => findJsxAttribute(attributes, partnerAttributeName));
|
|
22796
22711
|
if (isNodeOfType(valueAttribute.value, "JSXExpressionContainer") && isNodeOfType(valueAttribute.value.expression, "Identifier") && undefinedInitialStateNames.has(valueAttribute.value.expression.name)) {
|
|
22797
22712
|
const stateName = valueAttribute.value.expression.name;
|
|
22798
|
-
const partnerHint = hasAllowedPartner ? "Give useState a starting value" : "Give useState a starting value
|
|
22713
|
+
const partnerHint = hasAllowedPartner ? "Give useState a starting value" : "Give useState a starting value and add onChange (or readOnly)";
|
|
22799
22714
|
context.report({
|
|
22800
22715
|
node: child,
|
|
22801
|
-
message: `
|
|
22716
|
+
message: `This can trigger a console warning and reset the field because "${stateName}" starts undefined, so <${tagName} value={${stateName}}> flips from uncontrolled to controlled. ${partnerHint} (e.g. \`useState("")\`).`
|
|
22802
22717
|
});
|
|
22803
22718
|
return;
|
|
22804
22719
|
}
|
|
@@ -22860,7 +22775,7 @@ const noUnescapedEntities = defineRule({
|
|
|
22860
22775
|
title: "Unescaped entities in JSX",
|
|
22861
22776
|
severity: "warn",
|
|
22862
22777
|
defaultEnabled: false,
|
|
22863
|
-
recommendation: "Replace bare `'` / `\"` / `>` / `}` characters
|
|
22778
|
+
recommendation: "Replace bare `'` / `\"` / `>` / `}` characters with HTML entities so literal UI text is encoded consistently.",
|
|
22864
22779
|
create: (context) => ({ JSXText(node) {
|
|
22865
22780
|
const value = node.value;
|
|
22866
22781
|
for (const character of value) if (character in ESCAPED_VERSIONS) {
|
|
@@ -23795,11 +23710,11 @@ const noUnknownProperty = defineRule({
|
|
|
23795
23710
|
id: "no-unknown-property",
|
|
23796
23711
|
title: "Unknown DOM property",
|
|
23797
23712
|
severity: "warn",
|
|
23798
|
-
recommendation: "Use the prop name React expects, like `className`, `htmlFor`, or `tabIndex
|
|
23713
|
+
recommendation: "Use the prop name React expects, like `className`, `htmlFor`, or `tabIndex`, so the attribute is applied correctly.",
|
|
23799
23714
|
create: (context) => {
|
|
23800
23715
|
const { ignore = [], requireDataLowercase = false } = resolveSettings$10(context.settings);
|
|
23801
23716
|
const ignoreSet = new Set(ignore);
|
|
23802
|
-
if (
|
|
23717
|
+
if (isGeneratedImageRenderContext(context)) ignoreSet.add("tw");
|
|
23803
23718
|
let fileIsNonReactJsx = false;
|
|
23804
23719
|
return {
|
|
23805
23720
|
Program(node) {
|
|
@@ -23834,6 +23749,7 @@ const noUnknownProperty = defineRule({
|
|
|
23834
23749
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23835
23750
|
const actualName = getJsxAttributeName(attribute.name);
|
|
23836
23751
|
if (!actualName) continue;
|
|
23752
|
+
if (actualName === "tw" && isGeneratedImageRenderContext(context, node)) continue;
|
|
23837
23753
|
if (ignoreSet.has(actualName)) continue;
|
|
23838
23754
|
if (isValidDataAttribute(actualName)) {
|
|
23839
23755
|
if (requireDataLowercase && hasUppercaseChar(actualName)) context.report({
|
|
@@ -23880,7 +23796,7 @@ const UNSAFE_ALIASES = new Set([
|
|
|
23880
23796
|
"componentWillReceiveProps",
|
|
23881
23797
|
"componentWillUpdate"
|
|
23882
23798
|
]);
|
|
23883
|
-
const buildMessage$6 = (methodName) => `\`${methodName}\`
|
|
23799
|
+
const buildMessage$6 = (methodName) => `\`${methodName}\` runs during unsafe legacy render timing and is deprecated, so React may double-invoke or remove it.`;
|
|
23884
23800
|
const resolveSettings$9 = (settings) => {
|
|
23885
23801
|
const reactDoctor = settings?.["react-doctor"];
|
|
23886
23802
|
return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
|
|
@@ -23913,7 +23829,7 @@ const noUnsafe = defineRule({
|
|
|
23913
23829
|
id: "no-unsafe",
|
|
23914
23830
|
title: "Unsafe legacy lifecycle method",
|
|
23915
23831
|
severity: "warn",
|
|
23916
|
-
recommendation: "
|
|
23832
|
+
recommendation: "Move setup to `constructor` or `componentDidMount`, prop-derived state to `getDerivedStateFromProps`, and update side effects to `componentDidUpdate` so React does not rely on deprecated unsafe lifecycles.",
|
|
23917
23833
|
create: (context) => {
|
|
23918
23834
|
const { checkAliases } = resolveSettings$9(context.settings);
|
|
23919
23835
|
const flagsUnsafePrefix = isReactVersionAtLeast(getConfiguredReactMajorMinor(context.settings), 16, 3);
|
|
@@ -24169,7 +24085,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
24169
24085
|
id: "no-unstable-nested-components",
|
|
24170
24086
|
title: "Component defined inside a component",
|
|
24171
24087
|
severity: "warn",
|
|
24172
|
-
recommendation: "Move nested components to
|
|
24088
|
+
recommendation: "Move nested components to module scope so React does not remount them and lose state on every render.",
|
|
24173
24089
|
category: "Performance",
|
|
24174
24090
|
create: (context) => {
|
|
24175
24091
|
const settings = resolveSettings$8(context.settings);
|
|
@@ -24343,7 +24259,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
24343
24259
|
//#endregion
|
|
24344
24260
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
24345
24261
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
24346
|
-
const MESSAGE$9 = "
|
|
24262
|
+
const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
24347
24263
|
const resolveSettings$7 = (settings) => {
|
|
24348
24264
|
const reactDoctor = settings?.["react-doctor"];
|
|
24349
24265
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -24366,7 +24282,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
24366
24282
|
id: "no-will-update-set-state",
|
|
24367
24283
|
title: "setState in componentWillUpdate",
|
|
24368
24284
|
severity: "warn",
|
|
24369
|
-
recommendation: "
|
|
24285
|
+
recommendation: "Avoid setState in componentWillUpdate because it can loop forever; derive state before render or move guarded updates to componentDidUpdate.",
|
|
24370
24286
|
create: (context) => {
|
|
24371
24287
|
const { mode } = resolveSettings$7(context.settings);
|
|
24372
24288
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
@@ -24400,7 +24316,7 @@ const noZIndex9999 = defineRule({
|
|
|
24400
24316
|
const zValue = getStylePropertyNumberValue(property);
|
|
24401
24317
|
if (zValue !== null && Math.abs(zValue) >= 1e3) context.report({
|
|
24402
24318
|
node: property,
|
|
24403
|
-
message: `z-index ${zValue} is
|
|
24319
|
+
message: `z-index ${zValue} is unusually high and can hide a layering bug instead of fixing it. Use a small set scale, like 1 to 50.`
|
|
24404
24320
|
});
|
|
24405
24321
|
}
|
|
24406
24322
|
},
|
|
@@ -24583,12 +24499,12 @@ const UTILITY_FILE_BASENAMES = new Set([
|
|
|
24583
24499
|
]);
|
|
24584
24500
|
//#endregion
|
|
24585
24501
|
//#region src/plugin/rules/react-builtins/only-export-components.ts
|
|
24586
|
-
const NAMED_EXPORT_MESSAGE = "Fast Refresh
|
|
24587
|
-
const ANONYMOUS_MESSAGE = "Fast Refresh can't track
|
|
24588
|
-
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh
|
|
24589
|
-
const REACT_CONTEXT_MESSAGE = "
|
|
24590
|
-
const LOCAL_COMPONENT_MESSAGE = "Fast Refresh skips
|
|
24591
|
-
const NO_EXPORT_MESSAGE = "Fast Refresh can't track
|
|
24502
|
+
const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh can't safely preserve component state.";
|
|
24503
|
+
const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
|
|
24504
|
+
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
|
|
24505
|
+
const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
|
|
24506
|
+
const LOCAL_COMPONENT_MESSAGE = "This component is not exported, so Fast Refresh skips it and local edits can full-reload.";
|
|
24507
|
+
const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
|
|
24592
24508
|
const DEFAULT_REACT_HOCS = [
|
|
24593
24509
|
"memo",
|
|
24594
24510
|
"forwardRef",
|
|
@@ -24741,7 +24657,7 @@ const onlyExportComponents = defineRule({
|
|
|
24741
24657
|
id: "only-export-components",
|
|
24742
24658
|
title: "Non-component export in component file",
|
|
24743
24659
|
severity: "warn",
|
|
24744
|
-
recommendation: "Move non-component exports out of files
|
|
24660
|
+
recommendation: "Move non-component exports out of component files so Fast Refresh can preserve component state instead of full-reloading.",
|
|
24745
24661
|
category: "Architecture",
|
|
24746
24662
|
create: (context) => {
|
|
24747
24663
|
const settings = resolveSettings$6(context.settings);
|
|
@@ -24962,10 +24878,10 @@ const isChildrenMemberExpression = (node) => {
|
|
|
24962
24878
|
};
|
|
24963
24879
|
const preactNoChildrenLength = defineRule({
|
|
24964
24880
|
id: "preact-no-children-length",
|
|
24965
|
-
title: "Array methods on
|
|
24881
|
+
title: "Array methods on Preact children can crash",
|
|
24966
24882
|
requires: ["preact"],
|
|
24967
24883
|
severity: "warn",
|
|
24968
|
-
recommendation: "Wrap with `toChildArray(children)`
|
|
24884
|
+
recommendation: "Wrap with `toChildArray(children)` because Preact's `props.children` is not always an array and array methods can crash.",
|
|
24969
24885
|
create: (context) => ({ MemberExpression(node) {
|
|
24970
24886
|
if (node.computed) return;
|
|
24971
24887
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
@@ -24999,10 +24915,10 @@ const REACT_HOOK_NAMES = new Set([
|
|
|
24999
24915
|
const buildMessage$4 = (importedNames) => `Your users hit \`__H\` undefined errors because importing ${importedNames.map((innerName) => `\`${innerName}\``).join(", ")} from \`react\` in a pure-Preact project loads a second copy of the hook state, so import from \`preact/hooks\` (or \`preact/compat\`) instead.`;
|
|
25000
24916
|
const preactNoReactHooksImport = defineRule({
|
|
25001
24917
|
id: "preact-no-react-hooks-import",
|
|
25002
|
-
title: "
|
|
24918
|
+
title: "React hook imports break pure Preact hook state",
|
|
25003
24919
|
requires: ["pure-preact"],
|
|
25004
24920
|
severity: "warn",
|
|
25005
|
-
recommendation: "
|
|
24921
|
+
recommendation: "Import hooks from `preact/hooks` so they share Preact's renderer state instead of loading a second hook implementation.",
|
|
25006
24922
|
create: (context) => ({ ImportDeclaration(node) {
|
|
25007
24923
|
const source = node.source;
|
|
25008
24924
|
if (!isNodeOfType(source, "Literal") || source.value !== "react") return;
|
|
@@ -25062,7 +24978,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
25062
24978
|
title: "render() reads props from arguments",
|
|
25063
24979
|
requires: ["preact"],
|
|
25064
24980
|
severity: "warn",
|
|
25065
|
-
recommendation: "Read
|
|
24981
|
+
recommendation: "Read from `this.props` and `this.state` because `preact/compat` uses React's parameterless `render()` and positional props/state become undefined.",
|
|
25066
24982
|
create: (context) => ({ MethodDefinition(node) {
|
|
25067
24983
|
if (!isInstanceMethodNamedRender(node)) return;
|
|
25068
24984
|
if (!isInsideEs6Component$1(node)) return;
|
|
@@ -25084,7 +25000,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
25084
25000
|
title: "onDoubleClick instead of onDblClick",
|
|
25085
25001
|
requires: ["pure-preact"],
|
|
25086
25002
|
severity: "warn",
|
|
25087
|
-
recommendation: "Rename
|
|
25003
|
+
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
25088
25004
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
25089
25005
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
25090
25006
|
const tagName = node.name.name;
|
|
@@ -25156,8 +25072,8 @@ const preferDynamicImport = defineRule({
|
|
|
25156
25072
|
});
|
|
25157
25073
|
//#endregion
|
|
25158
25074
|
//#region src/plugin/rules/react-builtins/prefer-es6-class.ts
|
|
25159
|
-
const ALWAYS_MESSAGE$1 = "`createReactClass` is legacy
|
|
25160
|
-
const NEVER_MESSAGE$1 = "This component is
|
|
25075
|
+
const ALWAYS_MESSAGE$1 = "`createReactClass` is legacy and adds a dependency, so this component diverges from modern React class syntax.";
|
|
25076
|
+
const NEVER_MESSAGE$1 = "This component uses an ES6 class where `createReactClass` is configured, so component style is inconsistent across the codebase.";
|
|
25161
25077
|
const resolveSettings$5 = (settings) => {
|
|
25162
25078
|
const reactDoctor = settings?.["react-doctor"];
|
|
25163
25079
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
@@ -25168,7 +25084,7 @@ const preferEs6Class = defineRule({
|
|
|
25168
25084
|
title: "createClass instead of ES6 class",
|
|
25169
25085
|
severity: "warn",
|
|
25170
25086
|
defaultEnabled: false,
|
|
25171
|
-
recommendation: "Pick one component style
|
|
25087
|
+
recommendation: "Pick one component style so readers do not have to switch between legacy `createReactClass` patterns and modern class components.",
|
|
25172
25088
|
category: "Architecture",
|
|
25173
25089
|
create: (context) => {
|
|
25174
25090
|
const { mode = "always" } = resolveSettings$5(context.settings);
|
|
@@ -25327,7 +25243,7 @@ const preferExplicitVariants = defineRule({
|
|
|
25327
25243
|
});
|
|
25328
25244
|
//#endregion
|
|
25329
25245
|
//#region src/plugin/rules/react-builtins/prefer-function-component.ts
|
|
25330
|
-
const MESSAGE$7 = "This class component
|
|
25246
|
+
const MESSAGE$7 = "This class component keeps behavior in lifecycle methods, so state and effects are harder to follow than in a hook-based function component.";
|
|
25331
25247
|
const resolveSettings$4 = (settings) => {
|
|
25332
25248
|
const reactDoctor = settings?.["react-doctor"];
|
|
25333
25249
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.preferFunctionComponent ?? {} : {};
|
|
@@ -25353,7 +25269,7 @@ const preferFunctionComponent = defineRule({
|
|
|
25353
25269
|
title: "Class component instead of function",
|
|
25354
25270
|
severity: "warn",
|
|
25355
25271
|
defaultEnabled: false,
|
|
25356
|
-
recommendation: "
|
|
25272
|
+
recommendation: "Rewrite the class component as a function component so state and effects use modern hook patterns instead of class lifecycles.",
|
|
25357
25273
|
category: "Architecture",
|
|
25358
25274
|
create: (context) => {
|
|
25359
25275
|
const settings = resolveSettings$4(context.settings);
|
|
@@ -25455,12 +25371,12 @@ const doesFunctionReturnsObjectLiteral = (functionNode) => {
|
|
|
25455
25371
|
const child = nodeRecord[key];
|
|
25456
25372
|
if (Array.isArray(child)) for (const item of child) {
|
|
25457
25373
|
if (!isAstNode(item)) continue;
|
|
25458
|
-
if (FUNCTION_LIKE_TYPES
|
|
25374
|
+
if (FUNCTION_LIKE_TYPES.has(item.type)) continue;
|
|
25459
25375
|
visit(item);
|
|
25460
25376
|
if (returnsObject) return;
|
|
25461
25377
|
}
|
|
25462
25378
|
else if (isAstNode(child)) {
|
|
25463
|
-
if (FUNCTION_LIKE_TYPES
|
|
25379
|
+
if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
|
|
25464
25380
|
visit(child);
|
|
25465
25381
|
}
|
|
25466
25382
|
}
|
|
@@ -25705,7 +25621,7 @@ const preferTagOverRole = defineRule({
|
|
|
25705
25621
|
title: "Role used instead of HTML tag",
|
|
25706
25622
|
tags: ["react-jsx-only"],
|
|
25707
25623
|
severity: "warn",
|
|
25708
|
-
recommendation: "
|
|
25624
|
+
recommendation: "Use the matching HTML element when one exists so browsers and assistive tech get native semantics.",
|
|
25709
25625
|
category: "Accessibility",
|
|
25710
25626
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
25711
25627
|
const tag = getElementType(node, context.settings);
|
|
@@ -26011,7 +25927,7 @@ const preferUseReducer = defineRule({
|
|
|
26011
25927
|
title: "Many related useState calls",
|
|
26012
25928
|
tags: ["test-noise"],
|
|
26013
25929
|
severity: "warn",
|
|
26014
|
-
recommendation: "Group related state
|
|
25930
|
+
recommendation: "Group related state in `useReducer` so one logical update does not fan out into separate renders.",
|
|
26015
25931
|
create: (context) => {
|
|
26016
25932
|
const reportExcessiveUseState = (body, componentName) => {
|
|
26017
25933
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
@@ -26043,7 +25959,7 @@ const preferUseReducer = defineRule({
|
|
|
26043
25959
|
//#region src/plugin/rules/tanstack-query/query-destructure-result.ts
|
|
26044
25960
|
const queryDestructureResult = defineRule({
|
|
26045
25961
|
id: "query-destructure-result",
|
|
26046
|
-
title: "
|
|
25962
|
+
title: "Whole query result subscribes to every field",
|
|
26047
25963
|
tags: ["test-noise"],
|
|
26048
25964
|
requires: ["tanstack-query"],
|
|
26049
25965
|
severity: "error",
|
|
@@ -26096,7 +26012,7 @@ const queryNoQueryInEffect = defineRule({
|
|
|
26096
26012
|
tags: ["test-noise"],
|
|
26097
26013
|
requires: ["tanstack-query"],
|
|
26098
26014
|
severity: "warn",
|
|
26099
|
-
recommendation: "
|
|
26015
|
+
recommendation: "Use `queryKey` changes or `enabled` so React Query schedules the fetch once instead of refetching again from `useEffect`.",
|
|
26100
26016
|
create: (context) => ({ CallExpression(node) {
|
|
26101
26017
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
26102
26018
|
const callback = getEffectCallback(node);
|
|
@@ -26419,7 +26335,7 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
26419
26335
|
severity: "warn",
|
|
26420
26336
|
category: "Performance",
|
|
26421
26337
|
disabledBy: ["react-compiler"],
|
|
26422
|
-
recommendation: "Select the raw slice and
|
|
26338
|
+
recommendation: "Select the raw slice and memoize derivation so Redux actions do not rebuild a collection and redraw this component.",
|
|
26423
26339
|
create: (context) => {
|
|
26424
26340
|
let aliases = /* @__PURE__ */ new Set();
|
|
26425
26341
|
return {
|
|
@@ -26479,7 +26395,7 @@ const reduxUseselectorReturnsNewCollection = defineRule({
|
|
|
26479
26395
|
severity: "warn",
|
|
26480
26396
|
category: "Performance",
|
|
26481
26397
|
disabledBy: ["react-compiler"],
|
|
26482
|
-
recommendation: "Return a
|
|
26398
|
+
recommendation: "Return a stable selected value, split selectors, or pass `shallowEqual` so every Redux action does not redraw this component.",
|
|
26483
26399
|
create: (context) => {
|
|
26484
26400
|
let aliases = /* @__PURE__ */ new Set();
|
|
26485
26401
|
return {
|
|
@@ -26505,7 +26421,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
26505
26421
|
title: "Animating an SVG directly",
|
|
26506
26422
|
tags: ["test-noise"],
|
|
26507
26423
|
severity: "warn",
|
|
26508
|
-
recommendation: "Wrap the SVG
|
|
26424
|
+
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
26509
26425
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
26510
26426
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
|
|
26511
26427
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
@@ -26655,7 +26571,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
26655
26571
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
26656
26572
|
context.report({
|
|
26657
26573
|
node,
|
|
26658
|
-
message: `This
|
|
26574
|
+
message: `This can cause a hydration mismatch because ${matched.display} in JSX gives a different value on the server than in the browser. Move it into useEffect+useState to run only in the browser, or add suppressHydrationWarning to the parent if it's on purpose.`
|
|
26659
26575
|
});
|
|
26660
26576
|
return;
|
|
26661
26577
|
}
|
|
@@ -26664,7 +26580,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
26664
26580
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
26665
26581
|
context.report({
|
|
26666
26582
|
node: child,
|
|
26667
|
-
message: `This
|
|
26583
|
+
message: `This can cause a hydration mismatch because ${pattern.display} reached from JSX gives a different value on the server than in the browser. Move it into useEffect+useState to run only in the browser, or add suppressHydrationWarning to the parent if it's on purpose.`
|
|
26668
26584
|
});
|
|
26669
26585
|
return;
|
|
26670
26586
|
}
|
|
@@ -26882,12 +26798,12 @@ const functionBodyHasReturnWithValue = (functionNode) => {
|
|
|
26882
26798
|
const child = nodeRecord[key];
|
|
26883
26799
|
if (Array.isArray(child)) for (const item of child) {
|
|
26884
26800
|
if (!isAstNode(item)) continue;
|
|
26885
|
-
if (FUNCTION_LIKE_TYPES
|
|
26801
|
+
if (FUNCTION_LIKE_TYPES.has(item.type)) continue;
|
|
26886
26802
|
visit(item);
|
|
26887
26803
|
if (didFindReturn) return;
|
|
26888
26804
|
}
|
|
26889
26805
|
else if (isAstNode(child)) {
|
|
26890
|
-
if (FUNCTION_LIKE_TYPES
|
|
26806
|
+
if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
|
|
26891
26807
|
visit(child);
|
|
26892
26808
|
}
|
|
26893
26809
|
}
|
|
@@ -26943,7 +26859,7 @@ const requireRenderReturn = defineRule({
|
|
|
26943
26859
|
id: "require-render-return",
|
|
26944
26860
|
title: "Render method does not return",
|
|
26945
26861
|
severity: "error",
|
|
26946
|
-
recommendation: "Return JSX
|
|
26862
|
+
recommendation: "Return JSX or `null` from `render` so the component intentionally shows something or nothing.",
|
|
26947
26863
|
create: (context) => {
|
|
26948
26864
|
const checkFunction = (functionNode) => {
|
|
26949
26865
|
const host = resolveRenderHost(functionNode);
|
|
@@ -27265,7 +27181,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
27265
27181
|
tags: ["test-noise"],
|
|
27266
27182
|
severity: "warn",
|
|
27267
27183
|
category: "Performance",
|
|
27268
|
-
recommendation: "
|
|
27184
|
+
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
27269
27185
|
create: (context) => ({ CallExpression(node) {
|
|
27270
27186
|
if (!isHookCall$1(node, "useRef") || !node.arguments?.length) return;
|
|
27271
27187
|
const initializer = node.arguments[0];
|
|
@@ -27292,7 +27208,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
27292
27208
|
tags: ["test-noise"],
|
|
27293
27209
|
severity: "warn",
|
|
27294
27210
|
category: "Performance",
|
|
27295
|
-
recommendation: "Wrap in an arrow function so
|
|
27211
|
+
recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
|
|
27296
27212
|
create: (context) => ({ CallExpression(node) {
|
|
27297
27213
|
if (!isHookCall$1(node, "useState") || !node.arguments?.length) return;
|
|
27298
27214
|
const initializer = node.arguments[0];
|
|
@@ -27575,7 +27491,7 @@ const rerenderTransitionsScroll = defineRule({
|
|
|
27575
27491
|
}
|
|
27576
27492
|
context.report({
|
|
27577
27493
|
node: setStateCall,
|
|
27578
|
-
message: `This
|
|
27494
|
+
message: `This can make scrolling stutter because setState in a "${eventName}" handler redraws on every event. Wrap it in startTransition, use useDeferredValue, or keep the value in a ref and throttle with requestAnimationFrame.`
|
|
27579
27495
|
});
|
|
27580
27496
|
} })
|
|
27581
27497
|
});
|
|
@@ -27595,7 +27511,7 @@ const rnAnimateLayoutProperty = defineRetiredRule({
|
|
|
27595
27511
|
tags: ["test-noise"],
|
|
27596
27512
|
requires: ["react-native"],
|
|
27597
27513
|
severity: "warn",
|
|
27598
|
-
recommendation: "Reanimated useAnimatedStyle
|
|
27514
|
+
recommendation: "Retired: Reanimated `useAnimatedStyle` can safely drive layout-affecting properties on the UI thread, so this pattern should not be flagged."
|
|
27599
27515
|
});
|
|
27600
27516
|
//#endregion
|
|
27601
27517
|
//#region src/plugin/rules/react-native/rn-animation-reaction-as-derived.ts
|
|
@@ -27626,7 +27542,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
27626
27542
|
if (!isValueAssignment && !isSetCall) return;
|
|
27627
27543
|
context.report({
|
|
27628
27544
|
node,
|
|
27629
|
-
message: "
|
|
27545
|
+
message: "This useAnimatedReaction only copies one shared value into another, so it can miss Reanimated's derived-value dependency tracking."
|
|
27630
27546
|
});
|
|
27631
27547
|
} })
|
|
27632
27548
|
});
|
|
@@ -27643,17 +27559,17 @@ const JS_BOTTOM_SHEET_PACKAGES = new Set([
|
|
|
27643
27559
|
]);
|
|
27644
27560
|
const rnBottomSheetPreferNative = defineRule({
|
|
27645
27561
|
id: "rn-bottom-sheet-prefer-native",
|
|
27646
|
-
title: "JS bottom sheet
|
|
27562
|
+
title: "JS bottom sheet misses native sheet behavior",
|
|
27647
27563
|
tags: ["test-noise"],
|
|
27648
27564
|
requires: ["react-native"],
|
|
27649
27565
|
severity: "warn",
|
|
27650
|
-
recommendation: "On RN v7+, use `<Modal presentationStyle=\"formSheet\">`
|
|
27566
|
+
recommendation: "On RN v7+, use `<Modal presentationStyle=\"formSheet\">` so the sheet uses platform-native gestures, detents, accessibility, and presentation behavior.",
|
|
27651
27567
|
create: (context) => ({ ImportDeclaration(node) {
|
|
27652
27568
|
const source = node.source?.value;
|
|
27653
27569
|
if (typeof source !== "string" || !JS_BOTTOM_SHEET_PACKAGES.has(source)) return;
|
|
27654
27570
|
context.report({
|
|
27655
27571
|
node,
|
|
27656
|
-
message: `
|
|
27572
|
+
message: `Users get JS-driven sheet gestures and presentation with ${source}, instead of the platform-native formSheet behavior.`
|
|
27657
27573
|
});
|
|
27658
27574
|
} })
|
|
27659
27575
|
});
|
|
@@ -27739,13 +27655,13 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
27739
27655
|
if (root.calleeName === "waitFor") {
|
|
27740
27656
|
context.report({
|
|
27741
27657
|
node,
|
|
27742
|
-
message: "This Detox `waitFor
|
|
27658
|
+
message: "This Detox `waitFor` chain isn't awaited, so the test can continue before the condition settles. Prepend `await`."
|
|
27743
27659
|
});
|
|
27744
27660
|
return;
|
|
27745
27661
|
}
|
|
27746
27662
|
if (root.calleeName === "expect" && isDetoxExpectSubject(root.rootCall)) context.report({
|
|
27747
27663
|
node,
|
|
27748
|
-
message: "This Detox `expect(element
|
|
27664
|
+
message: "This Detox `expect(element)` assertion isn't awaited, so the test can pass or fail before the assertion settles. Prepend `await`."
|
|
27749
27665
|
});
|
|
27750
27666
|
} };
|
|
27751
27667
|
}
|
|
@@ -28376,7 +28292,7 @@ const rnNoLegacyExpoPackages = defineRule({
|
|
|
28376
28292
|
tags: ["test-noise"],
|
|
28377
28293
|
requires: ["react-native"],
|
|
28378
28294
|
severity: "warn",
|
|
28379
|
-
recommendation: "
|
|
28295
|
+
recommendation: "Switch to the maintained replacement package so users are not stuck with unfixed bugs in deprecated Expo packages.",
|
|
28380
28296
|
create: (context) => ({ ImportDeclaration(node) {
|
|
28381
28297
|
const source = node.source?.value;
|
|
28382
28298
|
if (typeof source !== "string") return;
|
|
@@ -28458,7 +28374,7 @@ const rnNoNonNativeNavigator = defineRule({
|
|
|
28458
28374
|
if (!NON_NATIVE_NAVIGATOR_PACKAGES.get(source)) return;
|
|
28459
28375
|
context.report({
|
|
28460
28376
|
node,
|
|
28461
|
-
message: `
|
|
28377
|
+
message: `Users get JS-driven transitions and gestures from ${source}, instead of platform-native navigation behavior.`
|
|
28462
28378
|
});
|
|
28463
28379
|
} })
|
|
28464
28380
|
});
|
|
@@ -28637,7 +28553,7 @@ const isExpoUiNamespaceImport = (contextNode, localName) => {
|
|
|
28637
28553
|
};
|
|
28638
28554
|
const isExpoUiComponentElement = (openingElement, contextNode, componentName) => {
|
|
28639
28555
|
if (!openingElement.name) return false;
|
|
28640
|
-
const dottedName = flattenJsxName(openingElement.name);
|
|
28556
|
+
const dottedName = flattenJsxName$1(openingElement.name);
|
|
28641
28557
|
if (!dottedName) return false;
|
|
28642
28558
|
const [rootLocalName, secondName] = dottedName.split(".");
|
|
28643
28559
|
if (isNamedImportOf(contextNode, rootLocalName, componentName)) return true;
|
|
@@ -28727,7 +28643,7 @@ const collectTopLevelReturnExpressions = (functionNode) => {
|
|
|
28727
28643
|
if (!block || !isNodeOfType(block, "BlockStatement")) return [];
|
|
28728
28644
|
const returnExpressions = [];
|
|
28729
28645
|
const visit = (node) => {
|
|
28730
|
-
if (FUNCTION_LIKE_TYPES
|
|
28646
|
+
if (FUNCTION_LIKE_TYPES.has(node.type)) return;
|
|
28731
28647
|
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnExpressions.push(node.argument);
|
|
28732
28648
|
const nodeRecord = node;
|
|
28733
28649
|
for (const fieldName of Object.keys(nodeRecord)) {
|
|
@@ -28764,7 +28680,7 @@ const collectReturnedJsxElements = (expression) => {
|
|
|
28764
28680
|
};
|
|
28765
28681
|
const rnNoRenderitemKey = defineRule({
|
|
28766
28682
|
id: "rn-no-renderitem-key",
|
|
28767
|
-
title: "
|
|
28683
|
+
title: "renderItem key is ignored by React Native lists",
|
|
28768
28684
|
tags: ["test-noise"],
|
|
28769
28685
|
requires: ["react-native"],
|
|
28770
28686
|
severity: "warn",
|
|
@@ -28905,7 +28821,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
28905
28821
|
//#region src/plugin/rules/react-native/rn-no-single-element-style-array.ts
|
|
28906
28822
|
const rnNoSingleElementStyleArray = defineRule({
|
|
28907
28823
|
id: "rn-no-single-element-style-array",
|
|
28908
|
-
title: "Single-element style array",
|
|
28824
|
+
title: "Single-element style array adds wasted allocation",
|
|
28909
28825
|
tags: ["test-noise"],
|
|
28910
28826
|
requires: ["react-native"],
|
|
28911
28827
|
severity: "warn",
|
|
@@ -28928,11 +28844,11 @@ const rnNoSingleElementStyleArray = defineRule({
|
|
|
28928
28844
|
//#region src/plugin/rules/react-native/rn-prefer-content-inset-adjustment.ts
|
|
28929
28845
|
const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
28930
28846
|
id: "rn-prefer-content-inset-adjustment",
|
|
28931
|
-
title: "Manual safe-area
|
|
28847
|
+
title: "Manual safe-area insets can duplicate offsets",
|
|
28932
28848
|
tags: ["test-noise"],
|
|
28933
28849
|
requires: ["react-native"],
|
|
28934
28850
|
severity: "warn",
|
|
28935
|
-
recommendation: "
|
|
28851
|
+
recommendation: "Retired: SafeAreaView wrappers are valid; prefer native content inset adjustment only when manual inset plumbing causes scroll jumps or duplicated safe-area offsets."
|
|
28936
28852
|
});
|
|
28937
28853
|
//#endregion
|
|
28938
28854
|
//#region src/react-native-dependency-names.ts
|
|
@@ -29117,7 +29033,7 @@ const rnPreferPressable = defineRule({
|
|
|
29117
29033
|
tags: ["test-noise"],
|
|
29118
29034
|
requires: ["react-native"],
|
|
29119
29035
|
severity: "warn",
|
|
29120
|
-
recommendation: "Use `<Pressable>`
|
|
29036
|
+
recommendation: "Use `<Pressable>` because Touchable* components are frozen and lack Pressable's state-based feedback and accessibility behavior.",
|
|
29121
29037
|
create: (context) => ({ ImportDeclaration(node) {
|
|
29122
29038
|
const source = node.source?.value;
|
|
29123
29039
|
if (typeof source !== "string" || !TOUCHABLE_SOURCES.has(source)) return;
|
|
@@ -29553,7 +29469,7 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
29553
29469
|
title: "Role missing required ARIA props",
|
|
29554
29470
|
tags: ["react-jsx-only"],
|
|
29555
29471
|
severity: "error",
|
|
29556
|
-
recommendation: "Add every required `aria-*` attribute
|
|
29472
|
+
recommendation: "Add every required `aria-*` attribute so assistive tech can expose the role's state correctly.",
|
|
29557
29473
|
category: "Accessibility",
|
|
29558
29474
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
29559
29475
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -32799,16 +32715,16 @@ const roleSupportsAriaProps = defineRule({
|
|
|
32799
32715
|
});
|
|
32800
32716
|
//#endregion
|
|
32801
32717
|
//#region src/plugin/rules/react-builtins/rules-of-hooks.ts
|
|
32802
|
-
const buildTopLevelMessage = (hookName) => `\`${hookName}\`
|
|
32803
|
-
const buildNonComponentMessage = (hookName, functionName) => `\`${hookName}\`
|
|
32804
|
-
const buildConditionalMessage = (hookName) => `\`${hookName}\`
|
|
32805
|
-
const buildLoopMessage = (hookName) => `\`${hookName}\`
|
|
32806
|
-
const buildAsyncMessage = (hookName) => `\`${hookName}\`
|
|
32807
|
-
const buildClassComponentMessage = (hookName) => `\`${hookName}\`
|
|
32808
|
-
const buildTryMessage = (hookName) => `\`${hookName}\`
|
|
32718
|
+
const buildTopLevelMessage = (hookName) => `\`${hookName}\` can only run inside a React component or custom Hook because React needs that render scope to track Hook state.`;
|
|
32719
|
+
const buildNonComponentMessage = (hookName, functionName) => `\`${hookName}\` runs inside \`${functionName}\`, which is not a component or Hook, so React cannot attach Hook state to a render.`;
|
|
32720
|
+
const buildConditionalMessage = (hookName) => `\`${hookName}\` changes Hook order between renders when called conditionally, so React can attach state to the wrong Hook.`;
|
|
32721
|
+
const buildLoopMessage = (hookName) => `\`${hookName}\` can run a different number of times inside a loop, so React can attach state to the wrong Hook.`;
|
|
32722
|
+
const buildAsyncMessage = (hookName) => `\`${hookName}\` runs inside an async function, so React cannot guarantee the same Hook order during render.`;
|
|
32723
|
+
const buildClassComponentMessage = (hookName) => `\`${hookName}\` cannot run in a class component because Hooks require a function component or custom Hook render scope.`;
|
|
32724
|
+
const buildTryMessage = (hookName) => `\`${hookName}\` can be skipped by try/catch/finally control flow, so React can attach state to the wrong Hook.`;
|
|
32809
32725
|
const buildEffectEventCallMessage = (bindingName) => `\`${bindingName}\` comes from useEffectEvent, so it only works when called from Effects in the same component.`;
|
|
32810
32726
|
const buildEffectEventAssignmentMessage = (bindingName) => `${buildEffectEventCallMessage(bindingName)} It also breaks if saved in a variable or passed around.`;
|
|
32811
|
-
const buildEffectEventPassedDownMessage = () => `A function from useEffectEvent breaks
|
|
32727
|
+
const buildEffectEventPassedDownMessage = () => `A function from useEffectEvent only works inside Effects in the same component, so passing it around breaks the event/dependency split.`;
|
|
32812
32728
|
const ASCII_UPPERCASE_A = 65;
|
|
32813
32729
|
const ASCII_UPPERCASE_Z = 90;
|
|
32814
32730
|
const EFFECT_HOOK_NAMES = new Set([
|
|
@@ -33061,7 +32977,7 @@ const rulesOfHooks = defineRule({
|
|
|
33061
32977
|
title: "Hook called conditionally",
|
|
33062
32978
|
severity: "error",
|
|
33063
32979
|
tags: ["test-noise"],
|
|
33064
|
-
recommendation: "Call hooks at the top level of a React function component or
|
|
32980
|
+
recommendation: "Call hooks at the top level of a React function component or custom Hook so React sees the same hook order on every render.",
|
|
33065
32981
|
category: "Correctness",
|
|
33066
32982
|
create: (context) => {
|
|
33067
32983
|
const settings = resolveSettings$3(context.settings);
|
|
@@ -33189,13 +33105,13 @@ const rulesOfHooks = defineRule({
|
|
|
33189
33105
|
});
|
|
33190
33106
|
//#endregion
|
|
33191
33107
|
//#region src/plugin/rules/a11y/scope.ts
|
|
33192
|
-
const MESSAGE$3 = "
|
|
33108
|
+
const MESSAGE$3 = "The `scope` attribute only works on `<th>` cells, so screen readers get no table-header help from it here.";
|
|
33193
33109
|
const scope = defineRule({
|
|
33194
33110
|
id: "scope",
|
|
33195
33111
|
title: "scope attribute on non-th element",
|
|
33196
33112
|
tags: ["react-jsx-only"],
|
|
33197
33113
|
severity: "warn",
|
|
33198
|
-
recommendation: "
|
|
33114
|
+
recommendation: "Remove `scope` from this element or move it to the related `<th>` cell.",
|
|
33199
33115
|
category: "Accessibility",
|
|
33200
33116
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
33201
33117
|
const scopeAttribute = hasJsxProp(node.attributes, "scope");
|
|
@@ -33210,7 +33126,7 @@ const scope = defineRule({
|
|
|
33210
33126
|
});
|
|
33211
33127
|
//#endregion
|
|
33212
33128
|
//#region src/plugin/rules/react-builtins/self-closing-comp.ts
|
|
33213
|
-
const MESSAGE$2 = "This tag has no children.";
|
|
33129
|
+
const MESSAGE$2 = "This tag has no children, so the closing tag adds noise without changing output.";
|
|
33214
33130
|
const resolveSettings$2 = (settings) => {
|
|
33215
33131
|
const reactDoctor = settings?.["react-doctor"];
|
|
33216
33132
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.selfClosingComp ?? {} : {};
|
|
@@ -33229,7 +33145,7 @@ const selfClosingComp = defineRule({
|
|
|
33229
33145
|
title: "Element not self-closing",
|
|
33230
33146
|
severity: "warn",
|
|
33231
33147
|
defaultEnabled: false,
|
|
33232
|
-
recommendation: "Use
|
|
33148
|
+
recommendation: "Use `<X />` for childless elements so empty closing tags do not add noise.",
|
|
33233
33149
|
category: "Architecture",
|
|
33234
33150
|
create: (context) => {
|
|
33235
33151
|
const settings = resolveSettings$2(context.settings);
|
|
@@ -33426,9 +33342,9 @@ const getCandidateFromDefaultDeclaration = (node) => {
|
|
|
33426
33342
|
};
|
|
33427
33343
|
const serverAuthActions = defineRule({
|
|
33428
33344
|
id: "server-auth-actions",
|
|
33429
|
-
title: "
|
|
33345
|
+
title: "Unauthenticated server action can be called directly",
|
|
33430
33346
|
severity: "error",
|
|
33431
|
-
recommendation: "
|
|
33347
|
+
recommendation: "Check auth before touching data because exported server actions can be called directly by unauthenticated clients.",
|
|
33432
33348
|
create: (context) => {
|
|
33433
33349
|
let fileHasUseServerDirective = false;
|
|
33434
33350
|
const customAuthFunctionNames = getReactDoctorStringArraySetting(context.settings, "serverAuthFunctionNames");
|
|
@@ -33566,7 +33482,7 @@ const objectExpressionHasNextRevalidate = (objectExpression) => {
|
|
|
33566
33482
|
}
|
|
33567
33483
|
return false;
|
|
33568
33484
|
};
|
|
33569
|
-
const APP_ROUTER_FILE_PATTERN =
|
|
33485
|
+
const APP_ROUTER_FILE_PATTERN = new RegExp(`/app/(?:[^/]+/)*(?:route|page|layout|template|loading|error|default)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
33570
33486
|
const NON_PROJECT_PATH_PATTERN = /\/(?:node_modules|dist|build|\.next)\//;
|
|
33571
33487
|
const serverFetchWithoutRevalidate = defineRule({
|
|
33572
33488
|
id: "server-fetch-without-revalidate",
|
|
@@ -33809,8 +33725,8 @@ const serverSequentialIndependentAwait = defineRule({
|
|
|
33809
33725
|
});
|
|
33810
33726
|
//#endregion
|
|
33811
33727
|
//#region src/plugin/rules/react-builtins/state-in-constructor.ts
|
|
33812
|
-
const ALWAYS_MESSAGE = "This
|
|
33813
|
-
const NEVER_MESSAGE = "This
|
|
33728
|
+
const ALWAYS_MESSAGE = "This class uses a state field instead of the configured constructor pattern, so state setup is inconsistent across the codebase.";
|
|
33729
|
+
const NEVER_MESSAGE = "This class sets state in the constructor instead of the configured class-field pattern, so state setup is inconsistent across the codebase.";
|
|
33814
33730
|
const resolveSettings$1 = (settings) => {
|
|
33815
33731
|
const reactDoctor = settings?.["react-doctor"];
|
|
33816
33732
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.stateInConstructor ?? {} : {}).mode ?? "always" };
|
|
@@ -33842,7 +33758,7 @@ const stateInConstructor = defineRule({
|
|
|
33842
33758
|
title: "State initialized in constructor",
|
|
33843
33759
|
severity: "warn",
|
|
33844
33760
|
defaultEnabled: false,
|
|
33845
|
-
recommendation: "
|
|
33761
|
+
recommendation: "Use one class-state setup pattern so readers know where initial state lives.",
|
|
33846
33762
|
category: "Architecture",
|
|
33847
33763
|
create: (context) => {
|
|
33848
33764
|
const { mode } = resolveSettings$1(context.settings);
|
|
@@ -33945,7 +33861,7 @@ const stylePropObject = defineRule({
|
|
|
33945
33861
|
id: "style-prop-object",
|
|
33946
33862
|
title: "Style prop is not an object",
|
|
33947
33863
|
severity: "warn",
|
|
33948
|
-
recommendation: "Pass
|
|
33864
|
+
recommendation: "Pass `style` as an object so React can apply CSS properties instead of ignoring a string style value.",
|
|
33949
33865
|
category: "Correctness",
|
|
33950
33866
|
create: (context) => {
|
|
33951
33867
|
const { allow } = resolveSettings(context.settings);
|
|
@@ -34291,11 +34207,11 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
34291
34207
|
//#region src/plugin/rules/tanstack-start/tanstack-start-no-anchor-element.ts
|
|
34292
34208
|
const tanstackStartNoAnchorElement = defineRule({
|
|
34293
34209
|
id: "tanstack-start-no-anchor-element",
|
|
34294
|
-
title: "Plain anchor
|
|
34210
|
+
title: "Plain anchor reloads TanStack Router navigation",
|
|
34295
34211
|
tags: ["test-noise"],
|
|
34296
34212
|
requires: ["tanstack-start"],
|
|
34297
34213
|
severity: "warn",
|
|
34298
|
-
recommendation: "`
|
|
34214
|
+
recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
|
|
34299
34215
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
34300
34216
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34301
34217
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
@@ -34308,7 +34224,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
34308
34224
|
else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
|
|
34309
34225
|
if (typeof hrefValue === "string" && hrefValue.startsWith("/")) context.report({
|
|
34310
34226
|
node,
|
|
34311
|
-
message: "Plain <a> reloads the whole page
|
|
34227
|
+
message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
|
|
34312
34228
|
});
|
|
34313
34229
|
} })
|
|
34314
34230
|
});
|
|
@@ -34380,7 +34296,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
34380
34296
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
34381
34297
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) context.report({
|
|
34382
34298
|
node,
|
|
34383
|
-
message: "navigate() during render
|
|
34299
|
+
message: "navigate() runs during render here, so server and browser output can diverge during hydration."
|
|
34384
34300
|
});
|
|
34385
34301
|
},
|
|
34386
34302
|
"CallExpression:exit"(node) {
|
|
@@ -34464,7 +34380,7 @@ const tanstackStartNoUseServerInHandler = defineRule({
|
|
|
34464
34380
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
34465
34381
|
if (body.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && (statement.directive === "use server" || isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use server"))) context.report({
|
|
34466
34382
|
node: handlerFunction,
|
|
34467
|
-
message: "\"use server\" inside a createServerFn handler
|
|
34383
|
+
message: "\"use server\" inside a createServerFn handler duplicates TanStack Start's server boundary, so the route can fail to compile."
|
|
34468
34384
|
});
|
|
34469
34385
|
} })
|
|
34470
34386
|
});
|
|
@@ -34538,11 +34454,11 @@ const tanstackStartRedirectInTryCatch = defineRule({
|
|
|
34538
34454
|
//#region src/plugin/rules/tanstack-start/tanstack-start-route-property-order.ts
|
|
34539
34455
|
const tanstackStartRoutePropertyOrder = defineRule({
|
|
34540
34456
|
id: "tanstack-start-route-property-order",
|
|
34541
|
-
title: "
|
|
34457
|
+
title: "Route property order breaks type inference",
|
|
34542
34458
|
tags: ["test-noise"],
|
|
34543
34459
|
requires: ["tanstack-start"],
|
|
34544
34460
|
severity: "error",
|
|
34545
|
-
recommendation: "Follow the order
|
|
34461
|
+
recommendation: "Follow the route property order because TanStack Router's type inference depends on earlier properties feeding later ones.",
|
|
34546
34462
|
create: (context) => ({ CallExpression(node) {
|
|
34547
34463
|
const optionsObject = getRouteOptionsObject(node);
|
|
34548
34464
|
if (!optionsObject) return;
|
|
@@ -34572,7 +34488,7 @@ const tanstackStartRoutePropertyOrder = defineRule({
|
|
|
34572
34488
|
//#region src/plugin/rules/tanstack-start/tanstack-start-server-fn-method-order.ts
|
|
34573
34489
|
const tanstackStartServerFnMethodOrder = defineRule({
|
|
34574
34490
|
id: "tanstack-start-server-fn-method-order",
|
|
34575
|
-
title: "
|
|
34491
|
+
title: "Server function method order breaks type inference",
|
|
34576
34492
|
tags: ["test-noise"],
|
|
34577
34493
|
requires: ["tanstack-start"],
|
|
34578
34494
|
severity: "error",
|
|
@@ -34653,7 +34569,7 @@ const useLazyMotion = defineRule({
|
|
|
34653
34569
|
return getImportedName$1(specifier) === "motion";
|
|
34654
34570
|
})) context.report({
|
|
34655
34571
|
node,
|
|
34656
|
-
message: "Importing \"motion\" ships about 30 kb of extra code
|
|
34572
|
+
message: "Importing \"motion\" ships about 30 kb of extra code and slows page load. Use \"m\" with LazyMotion instead."
|
|
34657
34573
|
});
|
|
34658
34574
|
} })
|
|
34659
34575
|
});
|
|
@@ -34690,7 +34606,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
34690
34606
|
id: "void-dom-elements-no-children",
|
|
34691
34607
|
title: "Children on a void element",
|
|
34692
34608
|
severity: "warn",
|
|
34693
|
-
recommendation: "Remove the children
|
|
34609
|
+
recommendation: "Remove the children or use a non-void tag so React does not drop content the element cannot render.",
|
|
34694
34610
|
create: (context) => ({
|
|
34695
34611
|
JSXElement(node) {
|
|
34696
34612
|
const openingElement = node.openingElement;
|
|
@@ -34838,7 +34754,7 @@ const DEPRECATED_ZOD_ERROR_MEMBERS = new Set([
|
|
|
34838
34754
|
"formErrors",
|
|
34839
34755
|
"format"
|
|
34840
34756
|
]);
|
|
34841
|
-
const ZOD_ERROR_API_MESSAGE = "
|
|
34757
|
+
const ZOD_ERROR_API_MESSAGE = "This ZodError API was removed in Zod 4, so error handling can break during the upgrade.";
|
|
34842
34758
|
const isZodErrorReference = (node) => {
|
|
34843
34759
|
const inner = stripParenExpression(node);
|
|
34844
34760
|
if (isNodeOfType(inner, "Identifier")) return getZodNamedImport(inner) === "ZodError";
|
|
@@ -34870,7 +34786,7 @@ const isReceiverOfDeprecatedZodErrorMember = (callExpression) => {
|
|
|
34870
34786
|
};
|
|
34871
34787
|
const zodV4NoDeprecatedErrorApis = defineRule({
|
|
34872
34788
|
id: "zod-v4-no-deprecated-error-apis",
|
|
34873
|
-
title: "
|
|
34789
|
+
title: "Zod 3 error API breaks in Zod 4",
|
|
34874
34790
|
requires: ["zod:4"],
|
|
34875
34791
|
tags: ["migration-hint"],
|
|
34876
34792
|
severity: "warn",
|
|
@@ -34959,7 +34875,7 @@ const parseCallUsesErrorMap = (callExpression) => {
|
|
|
34959
34875
|
};
|
|
34960
34876
|
const zodV4NoDeprecatedErrorCustomization = defineRule({
|
|
34961
34877
|
id: "zod-v4-no-deprecated-error-customization",
|
|
34962
|
-
title: "
|
|
34878
|
+
title: "Zod 3 error customization breaks in Zod 4",
|
|
34963
34879
|
requires: ["zod:4"],
|
|
34964
34880
|
tags: ["migration-hint"],
|
|
34965
34881
|
severity: "warn",
|
|
@@ -34968,7 +34884,7 @@ const zodV4NoDeprecatedErrorCustomization = defineRule({
|
|
|
34968
34884
|
if (!factoryUsesDeprecatedErrorParameter(node) && !parseCallUsesErrorMap(node)) return;
|
|
34969
34885
|
context.report({
|
|
34970
34886
|
node,
|
|
34971
|
-
message: "Zod
|
|
34887
|
+
message: "This Zod 3 error-customization form is not compatible with Zod 4, so custom messages can stop applying during the upgrade."
|
|
34972
34888
|
});
|
|
34973
34889
|
} })
|
|
34974
34890
|
});
|
|
@@ -35028,7 +34944,7 @@ const LITERAL_FACTORY = new Set(["literal"]);
|
|
|
35028
34944
|
const reportSchemaMigration = (context, node) => {
|
|
35029
34945
|
context.report({
|
|
35030
34946
|
node,
|
|
35031
|
-
message: "Zod
|
|
34947
|
+
message: "This Zod 3 schema API changed in Zod 4, so this schema can fail after the upgrade."
|
|
35032
34948
|
});
|
|
35033
34949
|
};
|
|
35034
34950
|
const isCallToDeprecatedTopLevelFactory = (callExpression) => isZodFactoryCall(callExpression, DEPRECATED_TOP_LEVEL_FACTORIES);
|
|
@@ -35080,7 +34996,7 @@ const isZodNamespaceImportMemberCreate = (memberExpression) => {
|
|
|
35080
34996
|
};
|
|
35081
34997
|
const zodV4NoDeprecatedSchemaApis = defineRule({
|
|
35082
34998
|
id: "zod-v4-no-deprecated-schema-apis",
|
|
35083
|
-
title: "
|
|
34999
|
+
title: "Zod 3 schema API breaks in Zod 4",
|
|
35084
35000
|
requires: ["zod:4"],
|
|
35085
35001
|
tags: ["migration-hint"],
|
|
35086
35002
|
severity: "warn",
|
|
@@ -35133,7 +35049,7 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
35133
35049
|
if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
|
|
35134
35050
|
context.report({
|
|
35135
35051
|
node,
|
|
35136
|
-
message: "
|
|
35052
|
+
message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
|
|
35137
35053
|
});
|
|
35138
35054
|
} })
|
|
35139
35055
|
});
|
|
@@ -35999,17 +35915,6 @@ const reactDoctorRules = [
|
|
|
35999
35915
|
category: "Security"
|
|
36000
35916
|
}
|
|
36001
35917
|
},
|
|
36002
|
-
{
|
|
36003
|
-
key: "react-doctor/jsx-no-target-blank",
|
|
36004
|
-
id: "jsx-no-target-blank",
|
|
36005
|
-
source: "react-doctor",
|
|
36006
|
-
originallyExternal: true,
|
|
36007
|
-
rule: {
|
|
36008
|
-
...jsxNoTargetBlank,
|
|
36009
|
-
framework: "global",
|
|
36010
|
-
category: "Security"
|
|
36011
|
-
}
|
|
36012
|
-
},
|
|
36013
35918
|
{
|
|
36014
35919
|
key: "react-doctor/jsx-no-undef",
|
|
36015
35920
|
id: "jsx-no-undef",
|