oxlint-plugin-react-doctor 0.4.2-dev.7cf52d8 → 0.4.2-dev.98066c2

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.js CHANGED
@@ -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 /^(opengraph-image|twitter-image|icon|apple-icon)\d*\.(jsx?|tsx?)$/.test(path.basename(rawFilename));
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") {
@@ -887,7 +1422,7 @@ const PREFER_ALT = "Screen readers skip a decorative image more reliably with `a
887
1422
  const MESSAGE_OBJECT = "Blind users can't use this `<object>` because screen readers can't 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$53 = (settings) => {
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 (isNextjsMetadataImageRouteFilename(normalizeFilename$1(context.filename ?? ""))) return {};
1007
- const settings = resolveSettings$53(context.settings);
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$52 = (settings) => {
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$52(context.settings);
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$51 = (settings) => {
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$51(context.settings);
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;
@@ -2213,7 +2749,7 @@ const PRESENTATION_ROLES$2 = new Set(["presentation", "none"]);
2213
2749
  //#endregion
2214
2750
  //#region src/plugin/rules/a11y/aria-role.ts
2215
2751
  const buildBaseMessage = (suffix) => `Screen reader users get no help from this \`role\` because it isn't a valid ARIA role, so use a real, non-abstract role.${suffix}`;
2216
- const resolveSettings$50 = (settings) => {
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 {
@@ -2229,7 +2765,7 @@ const ariaRole = defineRule({
2229
2765
  recommendation: "Use a real, non-abstract ARIA role.",
2230
2766
  category: "Accessibility",
2231
2767
  create: (context) => {
2232
- const settings = resolveSettings$50(context.settings);
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$1 = new Set([
2869
+ const FUNCTION_LIKE_TYPES = new Set([
2334
2870
  "FunctionDeclaration",
2335
2871
  "FunctionExpression",
2336
2872
  "ArrowFunctionExpression"
@@ -3255,7 +3791,7 @@ const FORM_CONTROL_TAGS = new Set([
3255
3791
  "select",
3256
3792
  "form"
3257
3793
  ]);
3258
- const resolveSettings$49 = (settings) => {
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
  };
@@ -3267,7 +3803,7 @@ const autocompleteValid = defineRule({
3267
3803
  recommendation: "Use a valid autofill token in `autoComplete`.",
3268
3804
  category: "Accessibility",
3269
3805
  create: (context) => {
3270
- const settings = resolveSettings$49(context.settings);
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;
@@ -3320,7 +3856,7 @@ const isCreateElementCall = (node) => {
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
3858
  const INVALID_MESSAGE = "This button's `type` is invalid.";
3323
- const resolveSettings$48 = (settings) => {
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 {
@@ -3362,7 +3898,7 @@ const buttonHasType = defineRule({
3362
3898
  severity: "warn",
3363
3899
  recommendation: "Always set a `type` on a `<button>`: `type=\"button\"`, `\"submit\"`, or `\"reset\"`.",
3364
3900
  create: (context) => {
3365
- const settings = resolveSettings$48(context.settings);
3901
+ const settings = resolveSettings$47(context.settings);
3366
3902
  const isTestlikeFile = isTestlikeFilename(context.filename);
3367
3903
  return {
3368
3904
  JSXOpeningElement(node) {
@@ -3431,7 +3967,7 @@ const buttonHasType = defineRule({
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
3969
  const EXCLUSIVE_MESSAGE = "This input behaves unpredictably with both `checked` & `defaultChecked` set.";
3434
- const resolveSettings$47 = (settings) => {
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 {
@@ -3484,7 +4020,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
3484
4020
  recommendation: "Add `onChange` (controlled) or `readOnly` (display-only), or use `defaultChecked` for an uncontrolled checkbox.",
3485
4021
  category: "Correctness",
3486
4022
  create: (context) => {
3487
- const settings = resolveSettings$47(context.settings);
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$46 = (settings) => {
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$46(context.settings);
4339
+ const settings = resolveSettings$45(context.settings);
3832
4340
  const isTestlikeFile = isTestlikeFilename(context.filename);
3833
4341
  return { JSXElement(node) {
3834
4342
  if (isTestlikeFile) return;
@@ -4199,7 +4707,7 @@ const DEFAULT_ADDITIONAL_HOCS = [
4199
4707
  "lazy",
4200
4708
  "withTracking"
4201
4709
  ];
4202
- const resolveSettings$45 = (settings) => {
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$45(context.settings);
4902
+ const settings = resolveSettings$44(context.settings);
4395
4903
  const ignoreNamed = settings.ignoreTranspilerName ? false : true;
4396
4904
  const reportAt = (node) => {
4397
4905
  context.report({
@@ -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$44 = (settings) => {
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,9 +6809,9 @@ 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$1 = (name) => {
6812
+ const flattenJsxName = (name) => {
6305
6813
  if (isNodeOfType(name, "JSXIdentifier")) return name.name;
6306
- if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$1(name.object)}.${name.property.name}`;
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
  };
@@ -6317,11 +6825,11 @@ const forbidComponentProps = defineRule({
6317
6825
  recommendation: "List the props you want to block per component in the `forbidComponentProps.forbid` setting.",
6318
6826
  category: "Architecture",
6319
6827
  create: (context) => {
6320
- const entries = resolveSettings$44(context.settings).forbid.map(normalizeEntry);
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$1(node.name);
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) {
@@ -6346,7 +6854,7 @@ const forbidComponentProps = defineRule({
6346
6854
  //#endregion
6347
6855
  //#region src/plugin/rules/react-builtins/forbid-dom-props.ts
6348
6856
  const buildMessage$23 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags.`;
6349
- const resolveSettings$43 = (settings) => {
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();
@@ -6368,7 +6876,7 @@ const forbidDomProps = defineRule({
6368
6876
  recommendation: "List the HTML props you want to block in the `forbidDomProps.forbid` setting.",
6369
6877
  category: "Architecture",
6370
6878
  create: (context) => {
6371
- const forbidMap = resolveSettings$43(context.settings);
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) => {
@@ -6455,7 +6935,7 @@ const isReactFunctionCall = (node, expectedCall) => {
6455
6935
  //#endregion
6456
6936
  //#region src/plugin/rules/react-builtins/forbid-elements.ts
6457
6937
  const buildMessage$22 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here.`;
6458
- const resolveSettings$42 = (settings) => {
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();
@@ -6471,11 +6951,11 @@ const forbidElements = defineRule({
6471
6951
  recommendation: "List the element names you want to block in the `forbidElements.forbid` setting.",
6472
6952
  category: "Architecture",
6473
6953
  create: (context) => {
6474
- const forbidMap = resolveSettings$42(context.settings);
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,
@@ -6542,7 +7022,7 @@ const DEFAULT_HEADING_TAGS = [
6542
7022
  "h5",
6543
7023
  "h6"
6544
7024
  ];
6545
- const resolveSettings$41 = (settings) => {
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$41(context.settings);
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;
@@ -6575,7 +7055,7 @@ const headingHasContent = defineRule({
6575
7055
  //#region src/plugin/rules/react-builtins/hook-use-state.ts
6576
7056
  const REQUIRE_DESTRUCTURE_MESSAGE = "This `useState` result is hard to follow.";
6577
7057
  const NAMING_CONVENTION_MESSAGE = "The setter here is hard to spot.";
6578
- const resolveSettings$40 = (settings) => {
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
  };
@@ -6602,7 +7082,7 @@ const hookUseState = defineRule({
6602
7082
  recommendation: "Destructure useState as `const [thing, setThing] = useState(…)`.",
6603
7083
  category: "Architecture",
6604
7084
  create: (context) => {
6605
- const { allowDestructuredState } = resolveSettings$40(context.settings);
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;
@@ -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$39 = (settings) => {
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$39(context.settings);
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);
@@ -7138,7 +7618,7 @@ const DEFAULT_REDUNDANT_WORDS = [
7138
7618
  "photo",
7139
7619
  "picture"
7140
7620
  ];
7141
- const resolveSettings$38 = (settings) => {
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
- const settings = resolveSettings$38(context.settings);
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$37 = (settings) => {
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
  };
@@ -7287,7 +7769,7 @@ const interactiveSupportsFocus = defineRule({
7287
7769
  recommendation: "Add `tabIndex` to elements with interactive roles and handlers.",
7288
7770
  category: "Accessibility",
7289
7771
  create: (context) => {
7290
- const settings = resolveSettings$37(context.settings);
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;
@@ -8585,7 +8985,7 @@ const jsTosortedImmutable = defineRule({
8585
8985
  const NEVER_MESSAGE$2 = () => `This prop is written inconsistently.`;
8586
8986
  const ALWAYS_MESSAGE$2 = () => `This prop is written inconsistently.`;
8587
8987
  const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing.`;
8588
- const resolveSettings$36 = (settings) => {
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 {
@@ -8603,7 +9003,7 @@ const jsxBooleanValue = defineRule({
8603
9003
  recommendation: "Pick a single boolean-attribute style across the codebase (default: omit `={true}`).",
8604
9004
  category: "Architecture",
8605
9005
  create: (context) => {
8606
- const settings = resolveSettings$36(context.settings);
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) {
@@ -8657,7 +9057,7 @@ const jsxBooleanValue = defineRule({
8657
9057
  const UNNECESSARY_BRACES_MESSAGE = "These curly braces do nothing here.";
8658
9058
  const REQUIRED_BRACES_MESSAGE = "This value needs curly braces `{ }` to read as an expression.";
8659
9059
  const isAllowedMode = (value) => value === "always" || value === "never" || value === "ignore";
8660
- const resolveSettings$35 = (settings) => {
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 {
@@ -8753,7 +9153,7 @@ const jsxCurlyBracePresence = defineRule({
8753
9153
  recommendation: "Pick a consistent quoting style for JSX literal values.",
8754
9154
  category: "Architecture",
8755
9155
  create: (context) => {
8756
- const settings = resolveSettings$35(context.settings);
9156
+ const settings = resolveSettings$34(context.settings);
8757
9157
  return {
8758
9158
  JSXAttribute(node) {
8759
9159
  const value = node.value;
@@ -8836,7 +9236,7 @@ const containsJsxElement = (root) => {
8836
9236
  //#region src/plugin/rules/react-builtins/jsx-filename-extension.ts
8837
9237
  const JSX_NOT_ALLOWED = (extension) => `This file has JSX but a \`${extension}\` name.`;
8838
9238
  const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are meant for JSX, but this one has none.`;
8839
- const resolveSettings$34 = (settings) => {
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 {
@@ -8858,7 +9258,7 @@ const jsxFilenameExtension = defineRule({
8858
9258
  recommendation: "Name files with JSX `.jsx` or `.tsx`, or whatever extension your project uses.",
8859
9259
  category: "Architecture",
8860
9260
  create: (context) => {
8861
- const settings = resolveSettings$34(context.settings);
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);
@@ -8912,7 +9312,7 @@ const isJsxFragmentElement = (node) => {
8912
9312
  //#region src/plugin/rules/react-builtins/jsx-fragments.ts
8913
9313
  const SYNTAX_MESSAGE = "This fragment is written inconsistently.";
8914
9314
  const ELEMENT_MESSAGE = "This fragment is written inconsistently.";
8915
- const resolveSettings$33 = (settings) => {
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
  };
@@ -8924,7 +9324,7 @@ const jsxFragments = defineRule({
8924
9324
  recommendation: "Pick one fragment style across the codebase.",
8925
9325
  category: "Architecture",
8926
9326
  create: (context) => {
8927
- const { mode } = resolveSettings$33(context.settings);
9327
+ const { mode } = resolveSettings$32(context.settings);
8928
9328
  return {
8929
9329
  JSXElement(node) {
8930
9330
  if (mode !== "syntax") return;
@@ -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$32 = (settings) => {
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;
@@ -9044,7 +9444,7 @@ const jsxHandlerNames = defineRule({
9044
9444
  recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers.",
9045
9445
  category: "Architecture",
9046
9446
  create: (context) => {
9047
- const settings = resolveSettings$32(context.settings);
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$31 = (settings) => {
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 {
@@ -9277,7 +9677,7 @@ const jsxKey = defineRule({
9277
9677
  severity: "error",
9278
9678
  recommendation: "Add a `key={...}` prop to each element produced inside `.map` / array literal.",
9279
9679
  create: (context) => {
9280
- const settings = resolveSettings$31(context.settings);
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$30 = (settings) => {
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$30(context.settings);
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());
@@ -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$29 = (settings) => {
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 ?? {};
@@ -11295,7 +11511,7 @@ const jsxNoScriptUrl = defineRule({
11295
11511
  recommendation: "Replace `javascript:` URLs with `onClick` or `onSubmit` handlers. React 19 blocks them anyway.",
11296
11512
  category: "Security",
11297
11513
  create: (context) => {
11298
- const options = resolveSettings$29(context.settings);
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([
@@ -11925,7 +11860,7 @@ const jsxPropsNoSpreading = defineRule({
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;
@@ -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",
@@ -12952,11 +12843,9 @@ const nextjsNoImgElement = defineRule({
12952
12843
  severity: "warn",
12953
12844
  recommendation: "`import Image from 'next/image'` for automatic WebP/AVIF, lazy loading, and responsive srcset",
12954
12845
  create: (context) => {
12955
- const filename = normalizeFilename$1(context.filename ?? "");
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 (isOgRoute || isMetadataImageRoute) return;
12848
+ if (isGeneratedImageRenderContext(context, node)) return;
12960
12849
  if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "img") context.report({
12961
12850
  node,
12962
12851
  message: "Plain <img> ships unoptimized, oversized images to your users."
@@ -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
  }
@@ -23799,7 +23714,7 @@ const noUnknownProperty = defineRule({
23799
23714
  create: (context) => {
23800
23715
  const { ignore = [], requireDataLowercase = false } = resolveSettings$10(context.settings);
23801
23716
  const ignoreSet = new Set(ignore);
23802
- if (isNextjsMetadataImageRouteFilename(context.filename)) ignoreSet.add("tw");
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({
@@ -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$1.has(item.type)) continue;
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$1.has(child.type)) continue;
25379
+ if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
25464
25380
  visit(child);
25465
25381
  }
25466
25382
  }
@@ -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$1.has(item.type)) continue;
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$1.has(child.type)) continue;
26806
+ if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
26891
26807
  visit(child);
26892
26808
  }
26893
26809
  }
@@ -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$1.has(node.type)) return;
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)) {
@@ -33566,7 +33482,7 @@ const objectExpressionHasNextRevalidate = (objectExpression) => {
33566
33482
  }
33567
33483
  return false;
33568
33484
  };
33569
- const APP_ROUTER_FILE_PATTERN = /\/app\/(?:[^/]+\/)*(?:route|page|layout|template|loading|error|default)\.(?:tsx?|jsx?)$/;
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",
@@ -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",