oxlint-plugin-react-doctor 0.4.2-dev.829655c → 0.4.2-dev.a59499b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +592 -395
  2. package/package.json +1 -1
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") {
@@ -1003,7 +1538,7 @@ 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 {};
1541
+ if (isGeneratedImageRenderContext(context)) return {};
1007
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");
@@ -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);
@@ -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"
@@ -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"];
@@ -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
  };
@@ -6321,7 +6829,7 @@ const forbidComponentProps = defineRule({
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) {
@@ -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) => {
@@ -6475,7 +6955,7 @@ const forbidElements = defineRule({
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,
@@ -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) => {
7668
+ if (isGeneratedImageRenderContext(context)) return {};
7188
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;
@@ -7303,92 +7785,10 @@ const interactiveSupportsFocus = defineRule({
7303
7785
  context.report({
7304
7786
  node,
7305
7787
  message
7306
- });
7307
- } };
7308
- }
7309
- });
7310
- //#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);
7788
+ });
7789
+ } };
7367
7790
  }
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
- };
7791
+ });
7392
7792
  //#endregion
7393
7793
  //#region src/plugin/rules/jotai/jotai-derived-atom-returns-fresh-object.ts
7394
7794
  const isAtomFromJotai = (callExpression) => {
@@ -9327,190 +9727,6 @@ 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;
@@ -11644,7 +11860,7 @@ const jsxPropsNoSpreading = defineRule({
11644
11860
  create: (context) => {
11645
11861
  const settings = resolveSettings$25(context.settings);
11646
11862
  return { JSXOpeningElement(node) {
11647
- const tagName = flattenJsxName(node.name);
11863
+ const tagName = flattenJsxName$1(node.name);
11648
11864
  if (!tagName) return;
11649
11865
  const isCustom = isReactComponentName(tagName) || tagName.includes(".");
11650
11866
  const isHtml = !isCustom;
@@ -12230,50 +12446,6 @@ const nextjsAsyncClientComponent = defineRule({
12230
12446
  }
12231
12447
  });
12232
12448
  //#endregion
12233
- //#region src/plugin/constants/nextjs.ts
12234
- const PAGE_FILE_PATTERN = /\/page\.(tsx?|jsx?)$/;
12235
- const PAGE_OR_LAYOUT_FILE_PATTERN = /\/(page|layout)\.(tsx?|jsx?)$/;
12236
- 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;
12237
- const OG_ROUTE_PATTERN = /\/og\b/i;
12238
- const PAGES_DIRECTORY_PATTERN = /\/pages\//;
12239
- const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
12240
- "redirect",
12241
- "permanentRedirect",
12242
- "notFound",
12243
- "forbidden",
12244
- "unauthorized"
12245
- ]);
12246
- const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
12247
- const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
12248
- const APP_DIRECTORY_PATTERN = /\/app\//;
12249
- const ROUTE_HANDLER_FILE_PATTERN = /\/route\.(tsx?|jsx?)$/;
12250
- const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
12251
- const MUTATING_ROUTE_SEGMENTS = new Set([
12252
- "logout",
12253
- "log-out",
12254
- "signout",
12255
- "sign-out",
12256
- "unsubscribe",
12257
- "delete",
12258
- "remove",
12259
- "revoke",
12260
- "cancel",
12261
- "deactivate"
12262
- ]);
12263
- const ERROR_BOUNDARY_FILE_PATTERN = /\/(error|global-error)\.(tsx?|jsx?)$/;
12264
- const GLOBAL_ERROR_FILE_PATTERN = /\/global-error\.(tsx?|jsx?)$/;
12265
- const ROUTE_HANDLER_HTTP_METHODS = new Set([
12266
- "GET",
12267
- "POST",
12268
- "PUT",
12269
- "PATCH",
12270
- "DELETE",
12271
- "OPTIONS",
12272
- "HEAD"
12273
- ]);
12274
- const GOOGLE_ANALYTICS_SCRIPT_PATTERN = /google-analytics\.com|googletagmanager\.com\/gtag/;
12275
- const OG_IMAGE_FILE_PATTERN = /\/(opengraph-image|twitter-image)\d*\.(tsx?|jsx?)$/;
12276
- //#endregion
12277
12449
  //#region src/plugin/rules/nextjs/nextjs-error-boundary-missing-use-client.ts
12278
12450
  const nextjsErrorBoundaryMissingUseClient = defineRule({
12279
12451
  id: "nextjs-error-boundary-missing-use-client",
@@ -12671,11 +12843,9 @@ const nextjsNoImgElement = defineRule({
12671
12843
  severity: "warn",
12672
12844
  recommendation: "`import Image from 'next/image'` for automatic WebP/AVIF, lazy loading, and responsive srcset",
12673
12845
  create: (context) => {
12674
- const filename = normalizeFilename$1(context.filename ?? "");
12675
- const isOgRoute = OG_ROUTE_PATTERN.test(filename);
12676
- const isMetadataImageRoute = isNextjsMetadataImageRouteFilename(filename);
12846
+ if (isGeneratedImageRenderContext(context)) return {};
12677
12847
  return { JSXOpeningElement(node) {
12678
- if (isOgRoute || isMetadataImageRoute) return;
12848
+ if (isGeneratedImageRenderContext(context, node)) return;
12679
12849
  if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "img") context.report({
12680
12850
  node,
12681
12851
  message: "Plain <img> ships unoptimized, oversized images to your users."
@@ -21305,12 +21475,22 @@ const PUBLIC_CLIENT_KEY_PATTERNS = [
21305
21475
  /^public-token-(?:live|test)-/,
21306
21476
  /^pk\.eyJ/
21307
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;
21308
21488
  const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
21309
21489
  const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
21310
21490
  const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
21311
21491
  const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
21312
21492
  const SECRET_SERVER_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.server\.[cm]?[jt]sx?$/;
21313
- const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|route)\.[cm]?[jt]sx?$/;
21493
+ const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|proxy|route)\.[cm]?[jt]sx?$/;
21314
21494
  const SECRET_NEXT_PAGES_API_FILE_PATTERN = /(?:^|\/)pages\/api\/.+\.[cm]?[jt]sx?$/;
21315
21495
  const SECRET_CLIENT_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.(?:client|browser|web)\.[cm]?[jt]sx?$/;
21316
21496
  const SECRET_CLIENT_ENTRY_FILE_PATTERN = /(?:^|\/)(?:src\/)?(?:main|index|[Aa]pp|client)\.[cm]?[jt]sx?$/;
@@ -21590,6 +21770,15 @@ const isInsideServerOnlyScope = (node) => {
21590
21770
  return false;
21591
21771
  };
21592
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
21593
21782
  //#region src/plugin/rules/security/no-secrets-in-client-code.ts
21594
21783
  const noSecretsInClientCode = defineRule({
21595
21784
  id: "no-secrets-in-client-code",
@@ -21618,21 +21807,28 @@ const noSecretsInClientCode = defineRule({
21618
21807
  if (!isNodeOfType(node.init, "Literal") || typeof node.init.value !== "string") return;
21619
21808
  const variableName = node.id.name;
21620
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 });
21621
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
+ }
21622
21822
  const isServerOnlyScope = isInsideServerOnlyScope(node);
21623
21823
  const trailingSuffix = getIdentifierTrailingWord(variableName);
21624
21824
  const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
21625
- 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) {
21626
21826
  context.report({
21627
21827
  node,
21628
21828
  message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
21629
21829
  });
21630
21830
  return;
21631
21831
  }
21632
- if (SECRET_PATTERNS.some((pattern) => pattern.test(literalValue))) context.report({
21633
- node,
21634
- message: "This hardcoded secret is a security vulnerability: it ships to the browser where anyone can read it."
21635
- });
21636
21832
  }
21637
21833
  };
21638
21834
  }
@@ -23518,7 +23714,7 @@ const noUnknownProperty = defineRule({
23518
23714
  create: (context) => {
23519
23715
  const { ignore = [], requireDataLowercase = false } = resolveSettings$10(context.settings);
23520
23716
  const ignoreSet = new Set(ignore);
23521
- if (isNextjsMetadataImageRouteFilename(context.filename)) ignoreSet.add("tw");
23717
+ if (isGeneratedImageRenderContext(context)) ignoreSet.add("tw");
23522
23718
  let fileIsNonReactJsx = false;
23523
23719
  return {
23524
23720
  Program(node) {
@@ -23553,6 +23749,7 @@ const noUnknownProperty = defineRule({
23553
23749
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
23554
23750
  const actualName = getJsxAttributeName(attribute.name);
23555
23751
  if (!actualName) continue;
23752
+ if (actualName === "tw" && isGeneratedImageRenderContext(context, node)) continue;
23556
23753
  if (ignoreSet.has(actualName)) continue;
23557
23754
  if (isValidDataAttribute(actualName)) {
23558
23755
  if (requireDataLowercase && hasUppercaseChar(actualName)) context.report({
@@ -25174,12 +25371,12 @@ const doesFunctionReturnsObjectLiteral = (functionNode) => {
25174
25371
  const child = nodeRecord[key];
25175
25372
  if (Array.isArray(child)) for (const item of child) {
25176
25373
  if (!isAstNode(item)) continue;
25177
- if (FUNCTION_LIKE_TYPES$1.has(item.type)) continue;
25374
+ if (FUNCTION_LIKE_TYPES.has(item.type)) continue;
25178
25375
  visit(item);
25179
25376
  if (returnsObject) return;
25180
25377
  }
25181
25378
  else if (isAstNode(child)) {
25182
- if (FUNCTION_LIKE_TYPES$1.has(child.type)) continue;
25379
+ if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
25183
25380
  visit(child);
25184
25381
  }
25185
25382
  }
@@ -26601,12 +26798,12 @@ const functionBodyHasReturnWithValue = (functionNode) => {
26601
26798
  const child = nodeRecord[key];
26602
26799
  if (Array.isArray(child)) for (const item of child) {
26603
26800
  if (!isAstNode(item)) continue;
26604
- if (FUNCTION_LIKE_TYPES$1.has(item.type)) continue;
26801
+ if (FUNCTION_LIKE_TYPES.has(item.type)) continue;
26605
26802
  visit(item);
26606
26803
  if (didFindReturn) return;
26607
26804
  }
26608
26805
  else if (isAstNode(child)) {
26609
- if (FUNCTION_LIKE_TYPES$1.has(child.type)) continue;
26806
+ if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
26610
26807
  visit(child);
26611
26808
  }
26612
26809
  }
@@ -28356,7 +28553,7 @@ const isExpoUiNamespaceImport = (contextNode, localName) => {
28356
28553
  };
28357
28554
  const isExpoUiComponentElement = (openingElement, contextNode, componentName) => {
28358
28555
  if (!openingElement.name) return false;
28359
- const dottedName = flattenJsxName(openingElement.name);
28556
+ const dottedName = flattenJsxName$1(openingElement.name);
28360
28557
  if (!dottedName) return false;
28361
28558
  const [rootLocalName, secondName] = dottedName.split(".");
28362
28559
  if (isNamedImportOf(contextNode, rootLocalName, componentName)) return true;
@@ -28446,7 +28643,7 @@ const collectTopLevelReturnExpressions = (functionNode) => {
28446
28643
  if (!block || !isNodeOfType(block, "BlockStatement")) return [];
28447
28644
  const returnExpressions = [];
28448
28645
  const visit = (node) => {
28449
- if (FUNCTION_LIKE_TYPES$1.has(node.type)) return;
28646
+ if (FUNCTION_LIKE_TYPES.has(node.type)) return;
28450
28647
  if (isNodeOfType(node, "ReturnStatement") && node.argument) returnExpressions.push(node.argument);
28451
28648
  const nodeRecord = node;
28452
28649
  for (const fieldName of Object.keys(nodeRecord)) {
@@ -33285,7 +33482,7 @@ const objectExpressionHasNextRevalidate = (objectExpression) => {
33285
33482
  }
33286
33483
  return false;
33287
33484
  };
33288
- 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}$`);
33289
33486
  const NON_PROJECT_PATH_PATTERN = /\/(?:node_modules|dist|build|\.next)\//;
33290
33487
  const serverFetchWithoutRevalidate = defineRule({
33291
33488
  id: "server-fetch-without-revalidate",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.4.2-dev.829655c",
3
+ "version": "0.4.2-dev.a59499b",
4
4
  "description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",