oxlint-plugin-react-doctor 0.4.2-dev.829655c → 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 +592 -395
- 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
|
|
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 (
|
|
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
|
|
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
|
|
6812
|
+
const flattenJsxName = (name) => {
|
|
6305
6813
|
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
6306
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName
|
|
6814
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName(name.object)}.${name.property.name}`;
|
|
6307
6815
|
if (name.type === "ThisExpression" || name.type === "JSXThisExpression") return "this";
|
|
6308
6816
|
return "";
|
|
6309
6817
|
};
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
3
|
+
"version": "0.4.2-dev.98066c2",
|
|
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",
|