oxlint-plugin-react-doctor 0.4.2-dev.7cf52d8 → 0.4.2-dev.98066c2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -38
- package/dist/index.js +639 -734
- 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") {
|
|
@@ -887,7 +1422,7 @@ const PREFER_ALT = "Screen readers skip a decorative image more reliably with `a
|
|
|
887
1422
|
const MESSAGE_OBJECT = "Blind users can't use this `<object>` because screen readers can't describe it, so add `alt`, `aria-label`, `aria-labelledby`, `title`, or inner fallback text.";
|
|
888
1423
|
const MESSAGE_AREA = "Blind users can't use this `<area>` of the image map because screen readers can't describe it, so add `alt`, `aria-label`, or `aria-labelledby`.";
|
|
889
1424
|
const MESSAGE_INPUT_IMAGE = "Blind users can't use this image button because screen readers can't describe it, so add `alt`, `aria-label`, or `aria-labelledby`.";
|
|
890
|
-
const resolveSettings$
|
|
1425
|
+
const resolveSettings$52 = (settings) => {
|
|
891
1426
|
const reactDoctor = settings?.["react-doctor"];
|
|
892
1427
|
return typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.altText ?? {} : {};
|
|
893
1428
|
};
|
|
@@ -1003,8 +1538,8 @@ const altText = defineRule({
|
|
|
1003
1538
|
recommendation: "Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",
|
|
1004
1539
|
category: "Accessibility",
|
|
1005
1540
|
create: (context) => {
|
|
1006
|
-
if (
|
|
1007
|
-
const settings = resolveSettings$
|
|
1541
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
1542
|
+
const settings = resolveSettings$52(context.settings);
|
|
1008
1543
|
const checkImg = !settings.elements || settings.elements.includes("img");
|
|
1009
1544
|
const checkObject = !settings.elements || settings.elements.includes("object");
|
|
1010
1545
|
const checkArea = !settings.elements || settings.elements.includes("area");
|
|
@@ -1014,6 +1549,7 @@ const altText = defineRule({
|
|
|
1014
1549
|
const areaAliases = new Set(settings.area ?? []);
|
|
1015
1550
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
1016
1551
|
return { JSXOpeningElement(node) {
|
|
1552
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
1017
1553
|
const tag = getElementType(node, context.settings);
|
|
1018
1554
|
if (checkImg && (tag === "img" || imgAliases.has(tag))) {
|
|
1019
1555
|
imgRule(node, node, context);
|
|
@@ -1048,7 +1584,7 @@ const DEFAULT_AMBIGUOUS = [
|
|
|
1048
1584
|
"a link",
|
|
1049
1585
|
"learn more"
|
|
1050
1586
|
];
|
|
1051
|
-
const resolveSettings$
|
|
1587
|
+
const resolveSettings$51 = (settings) => {
|
|
1052
1588
|
const reactDoctor = settings?.["react-doctor"];
|
|
1053
1589
|
return { words: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorAmbiguousText ?? {} : {}).words ?? DEFAULT_AMBIGUOUS };
|
|
1054
1590
|
};
|
|
@@ -1089,7 +1625,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1089
1625
|
recommendation: "Name where a link goes. Avoid 'click here', 'learn more', and 'link'.",
|
|
1090
1626
|
category: "Accessibility",
|
|
1091
1627
|
create: (context) => {
|
|
1092
|
-
const settings = resolveSettings$
|
|
1628
|
+
const settings = resolveSettings$51(context.settings);
|
|
1093
1629
|
const ambiguousSet = new Set(settings.words.map((word) => word.toLowerCase()));
|
|
1094
1630
|
return { JSXElement(node) {
|
|
1095
1631
|
if (getElementType(node.openingElement, context.settings) !== "a") return;
|
|
@@ -1139,7 +1675,7 @@ const getStaticTemplateLiteralValue = (templateLiteral) => {
|
|
|
1139
1675
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
1140
1676
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
1141
1677
|
const MESSAGE_CANT_BE_ANCHOR = "Keyboard users can't trigger this link because it's a click handler with no real `href`, so use `<button>` instead.";
|
|
1142
|
-
const resolveSettings$
|
|
1678
|
+
const resolveSettings$50 = (settings) => {
|
|
1143
1679
|
const reactDoctor = settings?.["react-doctor"];
|
|
1144
1680
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorIsValid ?? {} : {};
|
|
1145
1681
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
@@ -1178,7 +1714,7 @@ const anchorIsValid = defineRule({
|
|
|
1178
1714
|
recommendation: "Give links a real destination. Use `<button>` for in-page actions.",
|
|
1179
1715
|
category: "Accessibility",
|
|
1180
1716
|
create: (context) => {
|
|
1181
|
-
const settings = resolveSettings$
|
|
1717
|
+
const settings = resolveSettings$50(context.settings);
|
|
1182
1718
|
return { JSXOpeningElement(node) {
|
|
1183
1719
|
if (getElementType(node, context.settings) !== "a") return;
|
|
1184
1720
|
let hrefAttribute;
|
|
@@ -2213,7 +2749,7 @@ const PRESENTATION_ROLES$2 = new Set(["presentation", "none"]);
|
|
|
2213
2749
|
//#endregion
|
|
2214
2750
|
//#region src/plugin/rules/a11y/aria-role.ts
|
|
2215
2751
|
const buildBaseMessage = (suffix) => `Screen reader users get no help from this \`role\` because it isn't a valid ARIA role, so use a real, non-abstract role.${suffix}`;
|
|
2216
|
-
const resolveSettings$
|
|
2752
|
+
const resolveSettings$49 = (settings) => {
|
|
2217
2753
|
const reactDoctor = settings?.["react-doctor"];
|
|
2218
2754
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.ariaRole ?? {} : {};
|
|
2219
2755
|
return {
|
|
@@ -2229,7 +2765,7 @@ const ariaRole = defineRule({
|
|
|
2229
2765
|
recommendation: "Use a real, non-abstract ARIA role.",
|
|
2230
2766
|
category: "Accessibility",
|
|
2231
2767
|
create: (context) => {
|
|
2232
|
-
const settings = resolveSettings$
|
|
2768
|
+
const settings = resolveSettings$49(context.settings);
|
|
2233
2769
|
return { JSXOpeningElement(node) {
|
|
2234
2770
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
2235
2771
|
if (!roleAttribute) return;
|
|
@@ -2330,7 +2866,7 @@ const LOOP_TYPES = [
|
|
|
2330
2866
|
"WhileStatement",
|
|
2331
2867
|
"DoWhileStatement"
|
|
2332
2868
|
];
|
|
2333
|
-
const FUNCTION_LIKE_TYPES
|
|
2869
|
+
const FUNCTION_LIKE_TYPES = new Set([
|
|
2334
2870
|
"FunctionDeclaration",
|
|
2335
2871
|
"FunctionExpression",
|
|
2336
2872
|
"ArrowFunctionExpression"
|
|
@@ -3255,7 +3791,7 @@ const FORM_CONTROL_TAGS = new Set([
|
|
|
3255
3791
|
"select",
|
|
3256
3792
|
"form"
|
|
3257
3793
|
]);
|
|
3258
|
-
const resolveSettings$
|
|
3794
|
+
const resolveSettings$48 = (settings) => {
|
|
3259
3795
|
const reactDoctor = settings?.["react-doctor"];
|
|
3260
3796
|
return { inputComponents: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {}).inputComponents ?? [] };
|
|
3261
3797
|
};
|
|
@@ -3267,7 +3803,7 @@ const autocompleteValid = defineRule({
|
|
|
3267
3803
|
recommendation: "Use a valid autofill token in `autoComplete`.",
|
|
3268
3804
|
category: "Accessibility",
|
|
3269
3805
|
create: (context) => {
|
|
3270
|
-
const settings = resolveSettings$
|
|
3806
|
+
const settings = resolveSettings$48(context.settings);
|
|
3271
3807
|
return { JSXOpeningElement: (node) => {
|
|
3272
3808
|
const tag = getElementType(node, context.settings);
|
|
3273
3809
|
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.includes(tag)) return;
|
|
@@ -3320,7 +3856,7 @@ const isCreateElementCall = (node) => {
|
|
|
3320
3856
|
//#region src/plugin/rules/react-builtins/button-has-type.ts
|
|
3321
3857
|
const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
|
|
3322
3858
|
const INVALID_MESSAGE = "This button's `type` is invalid.";
|
|
3323
|
-
const resolveSettings$
|
|
3859
|
+
const resolveSettings$47 = (settings) => {
|
|
3324
3860
|
const reactDoctor = settings?.["react-doctor"];
|
|
3325
3861
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.buttonHasType ?? {} : {};
|
|
3326
3862
|
return {
|
|
@@ -3362,7 +3898,7 @@ const buttonHasType = defineRule({
|
|
|
3362
3898
|
severity: "warn",
|
|
3363
3899
|
recommendation: "Always set a `type` on a `<button>`: `type=\"button\"`, `\"submit\"`, or `\"reset\"`.",
|
|
3364
3900
|
create: (context) => {
|
|
3365
|
-
const settings = resolveSettings$
|
|
3901
|
+
const settings = resolveSettings$47(context.settings);
|
|
3366
3902
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3367
3903
|
return {
|
|
3368
3904
|
JSXOpeningElement(node) {
|
|
@@ -3431,7 +3967,7 @@ const buttonHasType = defineRule({
|
|
|
3431
3967
|
//#region src/plugin/rules/react-builtins/checked-requires-onchange-or-readonly.ts
|
|
3432
3968
|
const MISSING_MESSAGE$1 = "Your users can't toggle this input because `checked` has no `onChange`.";
|
|
3433
3969
|
const EXCLUSIVE_MESSAGE = "This input behaves unpredictably with both `checked` & `defaultChecked` set.";
|
|
3434
|
-
const resolveSettings$
|
|
3970
|
+
const resolveSettings$46 = (settings) => {
|
|
3435
3971
|
const reactDoctor = settings?.["react-doctor"];
|
|
3436
3972
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.checkedRequiresOnchangeOrReadonly ?? {} : {};
|
|
3437
3973
|
return {
|
|
@@ -3484,7 +4020,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
3484
4020
|
recommendation: "Add `onChange` (controlled) or `readOnly` (display-only), or use `defaultChecked` for an uncontrolled checkbox.",
|
|
3485
4021
|
category: "Correctness",
|
|
3486
4022
|
create: (context) => {
|
|
3487
|
-
const settings = resolveSettings$
|
|
4023
|
+
const settings = resolveSettings$46(context.settings);
|
|
3488
4024
|
const reportFromPresence = (presence) => {
|
|
3489
4025
|
if (presence.checkedNode && presence.defaultCheckedNode && !settings.ignoreExclusiveCheckedAttribute) context.report({
|
|
3490
4026
|
node: presence.checkedNode,
|
|
@@ -3628,9 +4164,6 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
3628
4164
|
} })
|
|
3629
4165
|
});
|
|
3630
4166
|
//#endregion
|
|
3631
|
-
//#region src/plugin/utils/is-member-property.ts
|
|
3632
|
-
const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === propertyName);
|
|
3633
|
-
//#endregion
|
|
3634
4167
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
3635
4168
|
const clientPassiveEventListeners = defineRule({
|
|
3636
4169
|
id: "client-passive-event-listeners",
|
|
@@ -3670,31 +4203,6 @@ const isReactComponentName = (name) => {
|
|
|
3670
4203
|
return firstCharacter >= 65 && firstCharacter <= 90;
|
|
3671
4204
|
};
|
|
3672
4205
|
//#endregion
|
|
3673
|
-
//#region src/plugin/utils/strip-paren-expression.ts
|
|
3674
|
-
const TS_WRAPPER_TYPES = new Set([
|
|
3675
|
-
"ParenthesizedExpression",
|
|
3676
|
-
"TSAsExpression",
|
|
3677
|
-
"TSSatisfiesExpression",
|
|
3678
|
-
"TSTypeAssertion",
|
|
3679
|
-
"TSNonNullExpression",
|
|
3680
|
-
"TSInstantiationExpression"
|
|
3681
|
-
]);
|
|
3682
|
-
const stripParenExpression = (node) => {
|
|
3683
|
-
let current = node;
|
|
3684
|
-
while (true) {
|
|
3685
|
-
if (TS_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) {
|
|
3686
|
-
current = current.expression;
|
|
3687
|
-
continue;
|
|
3688
|
-
}
|
|
3689
|
-
if (isNodeOfType(current, "ChainExpression") && current.expression) {
|
|
3690
|
-
current = current.expression;
|
|
3691
|
-
continue;
|
|
3692
|
-
}
|
|
3693
|
-
break;
|
|
3694
|
-
}
|
|
3695
|
-
return current;
|
|
3696
|
-
};
|
|
3697
|
-
//#endregion
|
|
3698
4206
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
3699
4207
|
const MESSAGE$48 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
3700
4208
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
@@ -3708,7 +4216,7 @@ const HTML_FOR_ATTRIBUTE = "htmlFor";
|
|
|
3708
4216
|
const LABEL_ELEMENT = "label";
|
|
3709
4217
|
const DEFAULT_DEPTH = 5;
|
|
3710
4218
|
const MAX_DEPTH = 25;
|
|
3711
|
-
const resolveSettings$
|
|
4219
|
+
const resolveSettings$45 = (settings) => {
|
|
3712
4220
|
const reactDoctor = settings?.["react-doctor"];
|
|
3713
4221
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.controlHasAssociatedLabel ?? {} : {};
|
|
3714
4222
|
return {
|
|
@@ -3828,7 +4336,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3828
4336
|
recommendation: "Give every interactive control a label screen readers can read.",
|
|
3829
4337
|
category: "Accessibility",
|
|
3830
4338
|
create: (context) => {
|
|
3831
|
-
const settings = resolveSettings$
|
|
4339
|
+
const settings = resolveSettings$45(context.settings);
|
|
3832
4340
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3833
4341
|
return { JSXElement(node) {
|
|
3834
4342
|
if (isTestlikeFile) return;
|
|
@@ -4199,7 +4707,7 @@ const DEFAULT_ADDITIONAL_HOCS = [
|
|
|
4199
4707
|
"lazy",
|
|
4200
4708
|
"withTracking"
|
|
4201
4709
|
];
|
|
4202
|
-
const resolveSettings$
|
|
4710
|
+
const resolveSettings$44 = (settings) => {
|
|
4203
4711
|
const reactDoctor = settings?.["react-doctor"];
|
|
4204
4712
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
|
|
4205
4713
|
const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
|
|
@@ -4391,7 +4899,7 @@ const displayName = defineRule({
|
|
|
4391
4899
|
recommendation: "Give each component a `displayName` so DevTools shows a clear name.",
|
|
4392
4900
|
category: "Architecture",
|
|
4393
4901
|
create: (context) => {
|
|
4394
|
-
const settings = resolveSettings$
|
|
4902
|
+
const settings = resolveSettings$44(context.settings);
|
|
4395
4903
|
const ignoreNamed = settings.ignoreTranspilerName ? false : true;
|
|
4396
4904
|
const reportAt = (node) => {
|
|
4397
4905
|
context.report({
|
|
@@ -6263,7 +6771,7 @@ const compileGlob = (pattern) => {
|
|
|
6263
6771
|
//#endregion
|
|
6264
6772
|
//#region src/plugin/rules/react-builtins/forbid-component-props.ts
|
|
6265
6773
|
const DEFAULT_FORBID_PROPS = ["className", "style"];
|
|
6266
|
-
const resolveSettings$
|
|
6774
|
+
const resolveSettings$43 = (settings) => {
|
|
6267
6775
|
const reactDoctor = settings?.["react-doctor"];
|
|
6268
6776
|
const forbid = (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidComponentProps ?? {} : {}).forbid;
|
|
6269
6777
|
if (!forbid || forbid.length === 0) return { forbid: DEFAULT_FORBID_PROPS };
|
|
@@ -6301,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
|
};
|
|
@@ -6317,11 +6825,11 @@ const forbidComponentProps = defineRule({
|
|
|
6317
6825
|
recommendation: "List the props you want to block per component in the `forbidComponentProps.forbid` setting.",
|
|
6318
6826
|
category: "Architecture",
|
|
6319
6827
|
create: (context) => {
|
|
6320
|
-
const entries = resolveSettings$
|
|
6828
|
+
const entries = resolveSettings$43(context.settings).forbid.map(normalizeEntry);
|
|
6321
6829
|
return { JSXOpeningElement(node) {
|
|
6322
6830
|
if (entries.length === 0) return;
|
|
6323
6831
|
if (!isSupportedJsxName(node.name)) return;
|
|
6324
|
-
const tag = flattenJsxName
|
|
6832
|
+
const tag = flattenJsxName(node.name);
|
|
6325
6833
|
if (!tag) return;
|
|
6326
6834
|
if (!(isReactComponentName(tag.split(".")[0]) || tag.includes("."))) return;
|
|
6327
6835
|
for (const attribute of node.attributes) {
|
|
@@ -6346,7 +6854,7 @@ const forbidComponentProps = defineRule({
|
|
|
6346
6854
|
//#endregion
|
|
6347
6855
|
//#region src/plugin/rules/react-builtins/forbid-dom-props.ts
|
|
6348
6856
|
const buildMessage$23 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags.`;
|
|
6349
|
-
const resolveSettings$
|
|
6857
|
+
const resolveSettings$42 = (settings) => {
|
|
6350
6858
|
const reactDoctor = settings?.["react-doctor"];
|
|
6351
6859
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
|
|
6352
6860
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -6368,7 +6876,7 @@ const forbidDomProps = defineRule({
|
|
|
6368
6876
|
recommendation: "List the HTML props you want to block in the `forbidDomProps.forbid` setting.",
|
|
6369
6877
|
category: "Architecture",
|
|
6370
6878
|
create: (context) => {
|
|
6371
|
-
const forbidMap = resolveSettings$
|
|
6879
|
+
const forbidMap = resolveSettings$42(context.settings);
|
|
6372
6880
|
return { JSXOpeningElement(node) {
|
|
6373
6881
|
if (forbidMap.size === 0) return;
|
|
6374
6882
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -6416,34 +6924,6 @@ const flattenCalleeName = (callee) => {
|
|
|
6416
6924
|
return null;
|
|
6417
6925
|
};
|
|
6418
6926
|
//#endregion
|
|
6419
|
-
//#region src/plugin/utils/flatten-jsx-name.ts
|
|
6420
|
-
/**
|
|
6421
|
-
* Flattens a JSX opener / member-expression chain into a dotted name.
|
|
6422
|
-
*
|
|
6423
|
-
* `<Foo />` → `"Foo"`
|
|
6424
|
-
* `<Namespace.Foo />` → `"Namespace.Foo"`
|
|
6425
|
-
* `<a.b.c />` → `"a.b.c"`
|
|
6426
|
-
*
|
|
6427
|
-
* Returns `null` when the chain root isn't a `JSXIdentifier` (e.g.
|
|
6428
|
-
* the rare-but-valid `<this.x />` case, which JSX surfaces as a
|
|
6429
|
-
* `JSXThisExpression` root).
|
|
6430
|
-
*
|
|
6431
|
-
* Used by `forbid-elements` and `jsx-props-no-spreading`. Two other
|
|
6432
|
-
* rules (`forbid-component-props`, `utils/get-element-type`) keep
|
|
6433
|
-
* their own extended variants because they handle additional node
|
|
6434
|
-
* types (ThisExpression / JSXNamespacedName) and return a non-
|
|
6435
|
-
* nullable string with a sentinel value — semantically distinct.
|
|
6436
|
-
*/
|
|
6437
|
-
const flattenJsxName = (node) => {
|
|
6438
|
-
if (isNodeOfType(node, "JSXIdentifier")) return node.name;
|
|
6439
|
-
if (isNodeOfType(node, "JSXMemberExpression")) {
|
|
6440
|
-
const objectName = flattenJsxName(node.object);
|
|
6441
|
-
if (!objectName) return null;
|
|
6442
|
-
return `${objectName}.${node.property.name}`;
|
|
6443
|
-
}
|
|
6444
|
-
return null;
|
|
6445
|
-
};
|
|
6446
|
-
//#endregion
|
|
6447
6927
|
//#region src/plugin/utils/is-react-function-call.ts
|
|
6448
6928
|
const PRAGMA = "React";
|
|
6449
6929
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
@@ -6455,7 +6935,7 @@ const isReactFunctionCall = (node, expectedCall) => {
|
|
|
6455
6935
|
//#endregion
|
|
6456
6936
|
//#region src/plugin/rules/react-builtins/forbid-elements.ts
|
|
6457
6937
|
const buildMessage$22 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here.`;
|
|
6458
|
-
const resolveSettings$
|
|
6938
|
+
const resolveSettings$41 = (settings) => {
|
|
6459
6939
|
const reactDoctor = settings?.["react-doctor"];
|
|
6460
6940
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
|
|
6461
6941
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -6471,11 +6951,11 @@ const forbidElements = defineRule({
|
|
|
6471
6951
|
recommendation: "List the element names you want to block in the `forbidElements.forbid` setting.",
|
|
6472
6952
|
category: "Architecture",
|
|
6473
6953
|
create: (context) => {
|
|
6474
|
-
const forbidMap = resolveSettings$
|
|
6954
|
+
const forbidMap = resolveSettings$41(context.settings);
|
|
6475
6955
|
return {
|
|
6476
6956
|
JSXOpeningElement(node) {
|
|
6477
6957
|
if (forbidMap.size === 0) return;
|
|
6478
|
-
const fullName = flattenJsxName(node.name);
|
|
6958
|
+
const fullName = flattenJsxName$1(node.name);
|
|
6479
6959
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
6480
6960
|
context.report({
|
|
6481
6961
|
node: node.name,
|
|
@@ -6542,7 +7022,7 @@ const DEFAULT_HEADING_TAGS = [
|
|
|
6542
7022
|
"h5",
|
|
6543
7023
|
"h6"
|
|
6544
7024
|
];
|
|
6545
|
-
const resolveSettings$
|
|
7025
|
+
const resolveSettings$40 = (settings) => {
|
|
6546
7026
|
const reactDoctor = settings?.["react-doctor"];
|
|
6547
7027
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
|
|
6548
7028
|
return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
|
|
@@ -6555,7 +7035,7 @@ const headingHasContent = defineRule({
|
|
|
6555
7035
|
recommendation: "Put readable text in every heading.",
|
|
6556
7036
|
category: "Accessibility",
|
|
6557
7037
|
create: (context) => {
|
|
6558
|
-
const settings = resolveSettings$
|
|
7038
|
+
const settings = resolveSettings$40(context.settings);
|
|
6559
7039
|
return { JSXOpeningElement(node) {
|
|
6560
7040
|
const elementType = getElementType(node, context.settings);
|
|
6561
7041
|
if (!settings.headingTags.includes(elementType)) return;
|
|
@@ -6575,7 +7055,7 @@ const headingHasContent = defineRule({
|
|
|
6575
7055
|
//#region src/plugin/rules/react-builtins/hook-use-state.ts
|
|
6576
7056
|
const REQUIRE_DESTRUCTURE_MESSAGE = "This `useState` result is hard to follow.";
|
|
6577
7057
|
const NAMING_CONVENTION_MESSAGE = "The setter here is hard to spot.";
|
|
6578
|
-
const resolveSettings$
|
|
7058
|
+
const resolveSettings$39 = (settings) => {
|
|
6579
7059
|
const reactDoctor = settings?.["react-doctor"];
|
|
6580
7060
|
return { allowDestructuredState: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.hookUseState ?? {} : {}).allowDestructuredState ?? false };
|
|
6581
7061
|
};
|
|
@@ -6602,7 +7082,7 @@ const hookUseState = defineRule({
|
|
|
6602
7082
|
recommendation: "Destructure useState as `const [thing, setThing] = useState(…)`.",
|
|
6603
7083
|
category: "Architecture",
|
|
6604
7084
|
create: (context) => {
|
|
6605
|
-
const { allowDestructuredState } = resolveSettings$
|
|
7085
|
+
const { allowDestructuredState } = resolveSettings$39(context.settings);
|
|
6606
7086
|
return { CallExpression(node) {
|
|
6607
7087
|
if (!isReactFunctionCall(node, "useState")) return;
|
|
6608
7088
|
const parent = node.parent;
|
|
@@ -6705,7 +7185,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
6705
7185
|
//#endregion
|
|
6706
7186
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
6707
7187
|
const MESSAGE$44 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
6708
|
-
const resolveSettings$
|
|
7188
|
+
const resolveSettings$38 = (settings) => {
|
|
6709
7189
|
const reactDoctor = settings?.["react-doctor"];
|
|
6710
7190
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
6711
7191
|
};
|
|
@@ -6742,7 +7222,7 @@ const htmlHasLang = defineRule({
|
|
|
6742
7222
|
recommendation: "Set `<html lang=\"…\">` so screen readers know the page language.",
|
|
6743
7223
|
category: "Accessibility",
|
|
6744
7224
|
create: (context) => {
|
|
6745
|
-
const settings = resolveSettings$
|
|
7225
|
+
const settings = resolveSettings$38(context.settings);
|
|
6746
7226
|
const tagSet = new Set(settings.htmlTags);
|
|
6747
7227
|
return { JSXOpeningElement(node) {
|
|
6748
7228
|
const tag = getElementType(node, context.settings);
|
|
@@ -7138,7 +7618,7 @@ const DEFAULT_REDUNDANT_WORDS = [
|
|
|
7138
7618
|
"photo",
|
|
7139
7619
|
"picture"
|
|
7140
7620
|
];
|
|
7141
|
-
const resolveSettings$
|
|
7621
|
+
const resolveSettings$37 = (settings) => {
|
|
7142
7622
|
const reactDoctor = settings?.["react-doctor"];
|
|
7143
7623
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.imgRedundantAlt ?? {} : {};
|
|
7144
7624
|
return {
|
|
@@ -7185,8 +7665,10 @@ const imgRedundantAlt = defineRule({
|
|
|
7185
7665
|
recommendation: "Do not put 'image' or 'photo' in alt text. Describe what is shown.",
|
|
7186
7666
|
category: "Accessibility",
|
|
7187
7667
|
create: (context) => {
|
|
7188
|
-
|
|
7668
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
7669
|
+
const settings = resolveSettings$37(context.settings);
|
|
7189
7670
|
return { JSXOpeningElement(node) {
|
|
7671
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
7190
7672
|
const tag = getElementType(node, context.settings);
|
|
7191
7673
|
if (!settings.components.includes(tag)) return;
|
|
7192
7674
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
@@ -7275,7 +7757,7 @@ const DEFAULT_TABBABLE_ROLES = [
|
|
|
7275
7757
|
"switch",
|
|
7276
7758
|
"textbox"
|
|
7277
7759
|
];
|
|
7278
|
-
const resolveSettings$
|
|
7760
|
+
const resolveSettings$36 = (settings) => {
|
|
7279
7761
|
const reactDoctor = settings?.["react-doctor"];
|
|
7280
7762
|
return { tabbable: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.interactiveSupportsFocus ?? {} : {}).tabbable ?? DEFAULT_TABBABLE_ROLES };
|
|
7281
7763
|
};
|
|
@@ -7287,7 +7769,7 @@ const interactiveSupportsFocus = defineRule({
|
|
|
7287
7769
|
recommendation: "Add `tabIndex` to elements with interactive roles and handlers.",
|
|
7288
7770
|
category: "Accessibility",
|
|
7289
7771
|
create: (context) => {
|
|
7290
|
-
const settings = resolveSettings$
|
|
7772
|
+
const settings = resolveSettings$36(context.settings);
|
|
7291
7773
|
const tabbableSet = new Set(settings.tabbable);
|
|
7292
7774
|
return { JSXOpeningElement(node) {
|
|
7293
7775
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -7308,88 +7790,6 @@ const interactiveSupportsFocus = defineRule({
|
|
|
7308
7790
|
}
|
|
7309
7791
|
});
|
|
7310
7792
|
//#endregion
|
|
7311
|
-
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
7312
|
-
const collectFromProgram = (programRoot) => {
|
|
7313
|
-
const lookup = /* @__PURE__ */ new Map();
|
|
7314
|
-
const visit = (node) => {
|
|
7315
|
-
if (node.type === "ImportDeclaration" && "source" in node && node.source) {
|
|
7316
|
-
const source = node.source.value;
|
|
7317
|
-
if (typeof source !== "string") return;
|
|
7318
|
-
if ("specifiers" in node && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
|
|
7319
|
-
if (!("local" in specifier) || !specifier.local) continue;
|
|
7320
|
-
const local = specifier.local;
|
|
7321
|
-
if (typeof local.name !== "string") continue;
|
|
7322
|
-
if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
|
|
7323
|
-
source,
|
|
7324
|
-
imported: null,
|
|
7325
|
-
isDefault: true,
|
|
7326
|
-
isNamespace: false
|
|
7327
|
-
});
|
|
7328
|
-
else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
|
|
7329
|
-
source,
|
|
7330
|
-
imported: null,
|
|
7331
|
-
isDefault: false,
|
|
7332
|
-
isNamespace: true
|
|
7333
|
-
});
|
|
7334
|
-
else if (specifier.type === "ImportSpecifier") {
|
|
7335
|
-
const importedNode = specifier.imported;
|
|
7336
|
-
const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
|
|
7337
|
-
lookup.set(local.name, {
|
|
7338
|
-
source,
|
|
7339
|
-
imported: importedName,
|
|
7340
|
-
isDefault: false,
|
|
7341
|
-
isNamespace: false
|
|
7342
|
-
});
|
|
7343
|
-
}
|
|
7344
|
-
}
|
|
7345
|
-
return;
|
|
7346
|
-
}
|
|
7347
|
-
const nodeRecord = node;
|
|
7348
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
7349
|
-
if (key === "parent") continue;
|
|
7350
|
-
const child = nodeRecord[key];
|
|
7351
|
-
if (Array.isArray(child)) {
|
|
7352
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
7353
|
-
} else if (isAstNode(child)) visit(child);
|
|
7354
|
-
}
|
|
7355
|
-
};
|
|
7356
|
-
visit(programRoot);
|
|
7357
|
-
return lookup;
|
|
7358
|
-
};
|
|
7359
|
-
const importLookupCache = /* @__PURE__ */ new WeakMap();
|
|
7360
|
-
const getImportLookup = (node) => {
|
|
7361
|
-
const programRoot = findProgramRoot(node);
|
|
7362
|
-
if (!programRoot) return null;
|
|
7363
|
-
let cached = importLookupCache.get(programRoot);
|
|
7364
|
-
if (!cached) {
|
|
7365
|
-
cached = collectFromProgram(programRoot);
|
|
7366
|
-
importLookupCache.set(programRoot, cached);
|
|
7367
|
-
}
|
|
7368
|
-
return cached;
|
|
7369
|
-
};
|
|
7370
|
-
const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7371
|
-
const lookup = getImportLookup(contextNode);
|
|
7372
|
-
if (!lookup) return false;
|
|
7373
|
-
const info = lookup.get(localIdentifierName);
|
|
7374
|
-
if (!info) return false;
|
|
7375
|
-
return info.source === moduleSource;
|
|
7376
|
-
};
|
|
7377
|
-
const isNamespaceImportFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7378
|
-
const lookup = getImportLookup(contextNode);
|
|
7379
|
-
if (!lookup) return false;
|
|
7380
|
-
const info = lookup.get(localIdentifierName);
|
|
7381
|
-
if (!info) return false;
|
|
7382
|
-
return info.isNamespace && info.source === moduleSource;
|
|
7383
|
-
};
|
|
7384
|
-
const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7385
|
-
const lookup = getImportLookup(contextNode);
|
|
7386
|
-
if (!lookup) return null;
|
|
7387
|
-
const info = lookup.get(localIdentifierName);
|
|
7388
|
-
if (!info) return null;
|
|
7389
|
-
if (info.source !== moduleSource) return null;
|
|
7390
|
-
return info.imported;
|
|
7391
|
-
};
|
|
7392
|
-
//#endregion
|
|
7393
7793
|
//#region src/plugin/rules/jotai/jotai-derived-atom-returns-fresh-object.ts
|
|
7394
7794
|
const isAtomFromJotai = (callExpression) => {
|
|
7395
7795
|
if (!isNodeOfType(callExpression.callee, "Identifier")) return false;
|
|
@@ -8585,7 +8985,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
8585
8985
|
const NEVER_MESSAGE$2 = () => `This prop is written inconsistently.`;
|
|
8586
8986
|
const ALWAYS_MESSAGE$2 = () => `This prop is written inconsistently.`;
|
|
8587
8987
|
const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing.`;
|
|
8588
|
-
const resolveSettings$
|
|
8988
|
+
const resolveSettings$35 = (settings) => {
|
|
8589
8989
|
const reactDoctor = settings?.["react-doctor"];
|
|
8590
8990
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxBooleanValue ?? {} : {};
|
|
8591
8991
|
return {
|
|
@@ -8603,7 +9003,7 @@ const jsxBooleanValue = defineRule({
|
|
|
8603
9003
|
recommendation: "Pick a single boolean-attribute style across the codebase (default: omit `={true}`).",
|
|
8604
9004
|
category: "Architecture",
|
|
8605
9005
|
create: (context) => {
|
|
8606
|
-
const settings = resolveSettings$
|
|
9006
|
+
const settings = resolveSettings$35(context.settings);
|
|
8607
9007
|
const alwaysSet = new Set(settings.always);
|
|
8608
9008
|
const neverSet = new Set(settings.never);
|
|
8609
9009
|
return { JSXAttribute(node) {
|
|
@@ -8657,7 +9057,7 @@ const jsxBooleanValue = defineRule({
|
|
|
8657
9057
|
const UNNECESSARY_BRACES_MESSAGE = "These curly braces do nothing here.";
|
|
8658
9058
|
const REQUIRED_BRACES_MESSAGE = "This value needs curly braces `{ }` to read as an expression.";
|
|
8659
9059
|
const isAllowedMode = (value) => value === "always" || value === "never" || value === "ignore";
|
|
8660
|
-
const resolveSettings$
|
|
9060
|
+
const resolveSettings$34 = (settings) => {
|
|
8661
9061
|
const reactDoctor = settings?.["react-doctor"];
|
|
8662
9062
|
const ruleSettingsRaw = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxCurlyBracePresence : void 0;
|
|
8663
9063
|
if (isAllowedMode(ruleSettingsRaw)) return {
|
|
@@ -8753,7 +9153,7 @@ const jsxCurlyBracePresence = defineRule({
|
|
|
8753
9153
|
recommendation: "Pick a consistent quoting style for JSX literal values.",
|
|
8754
9154
|
category: "Architecture",
|
|
8755
9155
|
create: (context) => {
|
|
8756
|
-
const settings = resolveSettings$
|
|
9156
|
+
const settings = resolveSettings$34(context.settings);
|
|
8757
9157
|
return {
|
|
8758
9158
|
JSXAttribute(node) {
|
|
8759
9159
|
const value = node.value;
|
|
@@ -8836,7 +9236,7 @@ const containsJsxElement = (root) => {
|
|
|
8836
9236
|
//#region src/plugin/rules/react-builtins/jsx-filename-extension.ts
|
|
8837
9237
|
const JSX_NOT_ALLOWED = (extension) => `This file has JSX but a \`${extension}\` name.`;
|
|
8838
9238
|
const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are meant for JSX, but this one has none.`;
|
|
8839
|
-
const resolveSettings$
|
|
9239
|
+
const resolveSettings$33 = (settings) => {
|
|
8840
9240
|
const reactDoctor = settings?.["react-doctor"];
|
|
8841
9241
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFilenameExtension ?? {} : {};
|
|
8842
9242
|
return {
|
|
@@ -8858,7 +9258,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
8858
9258
|
recommendation: "Name files with JSX `.jsx` or `.tsx`, or whatever extension your project uses.",
|
|
8859
9259
|
category: "Architecture",
|
|
8860
9260
|
create: (context) => {
|
|
8861
|
-
const settings = resolveSettings$
|
|
9261
|
+
const settings = resolveSettings$33(context.settings);
|
|
8862
9262
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
8863
9263
|
const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
|
|
8864
9264
|
const extensionOnly = path.extname(filename).slice(1);
|
|
@@ -8912,7 +9312,7 @@ const isJsxFragmentElement = (node) => {
|
|
|
8912
9312
|
//#region src/plugin/rules/react-builtins/jsx-fragments.ts
|
|
8913
9313
|
const SYNTAX_MESSAGE = "This fragment is written inconsistently.";
|
|
8914
9314
|
const ELEMENT_MESSAGE = "This fragment is written inconsistently.";
|
|
8915
|
-
const resolveSettings$
|
|
9315
|
+
const resolveSettings$32 = (settings) => {
|
|
8916
9316
|
const reactDoctor = settings?.["react-doctor"];
|
|
8917
9317
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFragments ?? {} : {}).mode ?? "syntax" };
|
|
8918
9318
|
};
|
|
@@ -8924,7 +9324,7 @@ const jsxFragments = defineRule({
|
|
|
8924
9324
|
recommendation: "Pick one fragment style across the codebase.",
|
|
8925
9325
|
category: "Architecture",
|
|
8926
9326
|
create: (context) => {
|
|
8927
|
-
const { mode } = resolveSettings$
|
|
9327
|
+
const { mode } = resolveSettings$32(context.settings);
|
|
8928
9328
|
return {
|
|
8929
9329
|
JSXElement(node) {
|
|
8930
9330
|
if (mode !== "syntax") return;
|
|
@@ -8968,7 +9368,7 @@ const buildPropRegex = (handlerPropPrefix) => {
|
|
|
8968
9368
|
const escaped = tokens.map((token) => token.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
8969
9369
|
return new RegExp(`^(${escaped})[A-Z].*$`);
|
|
8970
9370
|
};
|
|
8971
|
-
const resolveSettings$
|
|
9371
|
+
const resolveSettings$31 = (settings) => {
|
|
8972
9372
|
const reactDoctor = settings?.["react-doctor"];
|
|
8973
9373
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxHandlerNames ?? {} : {};
|
|
8974
9374
|
let handlerPrefix = DEFAULT_HANDLER_PREFIX;
|
|
@@ -9044,7 +9444,7 @@ const jsxHandlerNames = defineRule({
|
|
|
9044
9444
|
recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers.",
|
|
9045
9445
|
category: "Architecture",
|
|
9046
9446
|
create: (context) => {
|
|
9047
|
-
const settings = resolveSettings$
|
|
9447
|
+
const settings = resolveSettings$31(context.settings);
|
|
9048
9448
|
return { JSXAttribute(node) {
|
|
9049
9449
|
if (settings.ignoreComponentNames.length > 0) {
|
|
9050
9450
|
const opening = node.parent;
|
|
@@ -9129,7 +9529,7 @@ const MISSING_KEY_ARRAY = "Your users can see the wrong data when this array reo
|
|
|
9129
9529
|
const MISSING_KEY_ITERATOR = "Your users can see the wrong data when this list reorders.";
|
|
9130
9530
|
const KEY_BEFORE_SPREAD = "The `{...spread}` can overwrite this `key` & break React's tracking.";
|
|
9131
9531
|
const DUPLICATE_KEY = (keyValue) => `Your users can see the wrong data because two elements share the key "${keyValue}".`;
|
|
9132
|
-
const resolveSettings$
|
|
9532
|
+
const resolveSettings$30 = (settings) => {
|
|
9133
9533
|
const reactDoctor = settings?.["react-doctor"];
|
|
9134
9534
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxKey ?? {} : {};
|
|
9135
9535
|
return {
|
|
@@ -9277,7 +9677,7 @@ const jsxKey = defineRule({
|
|
|
9277
9677
|
severity: "error",
|
|
9278
9678
|
recommendation: "Add a `key={...}` prop to each element produced inside `.map` / array literal.",
|
|
9279
9679
|
create: (context) => {
|
|
9280
|
-
const settings = resolveSettings$
|
|
9680
|
+
const settings = resolveSettings$30(context.settings);
|
|
9281
9681
|
return {
|
|
9282
9682
|
JSXElement(node) {
|
|
9283
9683
|
const openingElement = node.openingElement;
|
|
@@ -9327,194 +9727,10 @@ const jsxKey = defineRule({
|
|
|
9327
9727
|
}
|
|
9328
9728
|
});
|
|
9329
9729
|
//#endregion
|
|
9330
|
-
//#region src/plugin/utils/find-variable-initializer.ts
|
|
9331
|
-
const FUNCTION_LIKE_TYPES = new Set([
|
|
9332
|
-
"FunctionDeclaration",
|
|
9333
|
-
"FunctionExpression",
|
|
9334
|
-
"ArrowFunctionExpression",
|
|
9335
|
-
"MethodDefinition",
|
|
9336
|
-
"Program"
|
|
9337
|
-
]);
|
|
9338
|
-
const findScopeOwner = (node) => {
|
|
9339
|
-
let ancestor = node;
|
|
9340
|
-
while (ancestor) {
|
|
9341
|
-
if (FUNCTION_LIKE_TYPES.has(ancestor.type)) return ancestor;
|
|
9342
|
-
ancestor = ancestor.parent ?? null;
|
|
9343
|
-
}
|
|
9344
|
-
return null;
|
|
9345
|
-
};
|
|
9346
|
-
const findBlockScopeOwner = (declaratorNode, declarationKind) => {
|
|
9347
|
-
if (declarationKind !== "let" && declarationKind !== "const") return findScopeOwner(declaratorNode);
|
|
9348
|
-
let ancestor = declaratorNode.parent;
|
|
9349
|
-
while (ancestor) {
|
|
9350
|
-
if (ancestor.type === "BlockStatement") {
|
|
9351
|
-
const blockParent = ancestor.parent;
|
|
9352
|
-
if (blockParent && (blockParent.type === "FunctionDeclaration" || blockParent.type === "FunctionExpression" || blockParent.type === "ArrowFunctionExpression" || blockParent.type === "MethodDefinition")) return findScopeOwner(declaratorNode);
|
|
9353
|
-
return ancestor;
|
|
9354
|
-
}
|
|
9355
|
-
if (FUNCTION_LIKE_TYPES.has(ancestor.type)) return ancestor;
|
|
9356
|
-
ancestor = ancestor.parent ?? null;
|
|
9357
|
-
}
|
|
9358
|
-
return null;
|
|
9359
|
-
};
|
|
9360
|
-
const collectFromBindingPattern = (pattern, initializer, scopeOwner, out) => {
|
|
9361
|
-
if (isNodeOfType(pattern, "Identifier")) {
|
|
9362
|
-
const list = out.get(pattern.name) ?? [];
|
|
9363
|
-
list.push({
|
|
9364
|
-
bindingIdentifier: pattern,
|
|
9365
|
-
initializer,
|
|
9366
|
-
scopeOwner
|
|
9367
|
-
});
|
|
9368
|
-
out.set(pattern.name, list);
|
|
9369
|
-
return;
|
|
9370
|
-
}
|
|
9371
|
-
if (isNodeOfType(pattern, "ObjectPattern")) {
|
|
9372
|
-
for (const property of pattern.properties) if (isNodeOfType(property, "Property")) {
|
|
9373
|
-
const valueNode = property.value;
|
|
9374
|
-
collectFromBindingPattern(valueNode, isNodeOfType(valueNode, "AssignmentPattern") ? valueNode.right : null, scopeOwner, out);
|
|
9375
|
-
} else if (isNodeOfType(property, "RestElement")) collectFromBindingPattern(property.argument, null, scopeOwner, out);
|
|
9376
|
-
return;
|
|
9377
|
-
}
|
|
9378
|
-
if (isNodeOfType(pattern, "ArrayPattern")) {
|
|
9379
|
-
for (const element of pattern.elements) {
|
|
9380
|
-
if (!element) continue;
|
|
9381
|
-
collectFromBindingPattern(element, isNodeOfType(element, "AssignmentPattern") ? element.right ?? null : null, scopeOwner, out);
|
|
9382
|
-
}
|
|
9383
|
-
return;
|
|
9384
|
-
}
|
|
9385
|
-
if (isNodeOfType(pattern, "AssignmentPattern")) {
|
|
9386
|
-
collectFromBindingPattern(pattern.left, pattern.right ?? null, scopeOwner, out);
|
|
9387
|
-
return;
|
|
9388
|
-
}
|
|
9389
|
-
if (isNodeOfType(pattern, "RestElement")) collectFromBindingPattern(pattern.argument, null, scopeOwner, out);
|
|
9390
|
-
};
|
|
9391
|
-
const buildBindingIndex = (root) => {
|
|
9392
|
-
const out = /* @__PURE__ */ new Map();
|
|
9393
|
-
const visit = (node) => {
|
|
9394
|
-
if (isNodeOfType(node, "VariableDeclarator")) {
|
|
9395
|
-
const declaration = node.parent;
|
|
9396
|
-
const scopeOwner = findBlockScopeOwner(node, declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : void 0);
|
|
9397
|
-
if (scopeOwner) collectFromBindingPattern(node.id, node.init ?? null, scopeOwner, out);
|
|
9398
|
-
}
|
|
9399
|
-
if ((isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression")) && node.id) {
|
|
9400
|
-
const enclosing = node.parent ? findScopeOwner(node.parent) : null;
|
|
9401
|
-
if (enclosing) {
|
|
9402
|
-
const list = out.get(node.id.name) ?? [];
|
|
9403
|
-
list.push({
|
|
9404
|
-
bindingIdentifier: node.id,
|
|
9405
|
-
initializer: node,
|
|
9406
|
-
scopeOwner: enclosing
|
|
9407
|
-
});
|
|
9408
|
-
out.set(node.id.name, list);
|
|
9409
|
-
}
|
|
9410
|
-
}
|
|
9411
|
-
if ((isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression")) && node.id) {
|
|
9412
|
-
const enclosing = node.parent ? findScopeOwner(node.parent) : null;
|
|
9413
|
-
if (enclosing) {
|
|
9414
|
-
const list = out.get(node.id.name) ?? [];
|
|
9415
|
-
list.push({
|
|
9416
|
-
bindingIdentifier: node.id,
|
|
9417
|
-
initializer: node,
|
|
9418
|
-
scopeOwner: enclosing
|
|
9419
|
-
});
|
|
9420
|
-
out.set(node.id.name, list);
|
|
9421
|
-
}
|
|
9422
|
-
}
|
|
9423
|
-
if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")) {
|
|
9424
|
-
if (Array.isArray(node.params)) for (const param of node.params) {
|
|
9425
|
-
if (!param) continue;
|
|
9426
|
-
collectFromBindingPattern(param, null, node, out);
|
|
9427
|
-
if (isNodeOfType(param, "AssignmentPattern")) collectFromBindingPattern(param.left ?? null, param.right ?? null, node, out);
|
|
9428
|
-
}
|
|
9429
|
-
}
|
|
9430
|
-
if (isNodeOfType(node, "ImportDeclaration")) {
|
|
9431
|
-
const scopeOwner = findScopeOwner(node);
|
|
9432
|
-
if (scopeOwner && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
|
|
9433
|
-
const local = specifier.local;
|
|
9434
|
-
if (local && isNodeOfType(local, "Identifier")) {
|
|
9435
|
-
const list = out.get(local.name) ?? [];
|
|
9436
|
-
list.push({
|
|
9437
|
-
bindingIdentifier: local,
|
|
9438
|
-
initializer: specifier,
|
|
9439
|
-
scopeOwner
|
|
9440
|
-
});
|
|
9441
|
-
out.set(local.name, list);
|
|
9442
|
-
}
|
|
9443
|
-
}
|
|
9444
|
-
}
|
|
9445
|
-
if (node.type === "TSImportEqualsDeclaration" || node.type === "TSEnumDeclaration" || node.type === "TSModuleDeclaration") {
|
|
9446
|
-
const idNode = node.id;
|
|
9447
|
-
if (idNode && idNode.type === "Identifier") {
|
|
9448
|
-
const idObject = idNode;
|
|
9449
|
-
const scopeOwner = findScopeOwner(node);
|
|
9450
|
-
if (scopeOwner && typeof idObject.name === "string") {
|
|
9451
|
-
const list = out.get(idObject.name) ?? [];
|
|
9452
|
-
list.push({
|
|
9453
|
-
bindingIdentifier: idNode,
|
|
9454
|
-
initializer: null,
|
|
9455
|
-
scopeOwner
|
|
9456
|
-
});
|
|
9457
|
-
out.set(idObject.name, list);
|
|
9458
|
-
}
|
|
9459
|
-
}
|
|
9460
|
-
}
|
|
9461
|
-
const nodeRecord = node;
|
|
9462
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
9463
|
-
if (key === "parent") continue;
|
|
9464
|
-
const child = nodeRecord[key];
|
|
9465
|
-
if (Array.isArray(child)) {
|
|
9466
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
9467
|
-
} else if (isAstNode(child)) visit(child);
|
|
9468
|
-
}
|
|
9469
|
-
};
|
|
9470
|
-
visit(root);
|
|
9471
|
-
return out;
|
|
9472
|
-
};
|
|
9473
|
-
const programRootCache = /* @__PURE__ */ new WeakMap();
|
|
9474
|
-
const getBindingIndex = (referenceNode) => {
|
|
9475
|
-
const programRoot = findProgramRoot(referenceNode);
|
|
9476
|
-
if (!programRoot) return null;
|
|
9477
|
-
let index = programRootCache.get(programRoot);
|
|
9478
|
-
if (!index) {
|
|
9479
|
-
index = buildBindingIndex(programRoot);
|
|
9480
|
-
programRootCache.set(programRoot, index);
|
|
9481
|
-
}
|
|
9482
|
-
return index;
|
|
9483
|
-
};
|
|
9484
|
-
const findVariableInitializer = (referenceNode, bindingName) => {
|
|
9485
|
-
const index = getBindingIndex(referenceNode);
|
|
9486
|
-
if (!index) return null;
|
|
9487
|
-
const candidates = index.get(bindingName);
|
|
9488
|
-
if (!candidates || candidates.length === 0) return null;
|
|
9489
|
-
const referenceAncestors = /* @__PURE__ */ new Set();
|
|
9490
|
-
let walker = referenceNode;
|
|
9491
|
-
while (walker) {
|
|
9492
|
-
referenceAncestors.add(walker);
|
|
9493
|
-
walker = walker.parent ?? null;
|
|
9494
|
-
}
|
|
9495
|
-
let best = null;
|
|
9496
|
-
for (const candidate of candidates) {
|
|
9497
|
-
if (!referenceAncestors.has(candidate.scopeOwner)) continue;
|
|
9498
|
-
if (best === null) {
|
|
9499
|
-
best = candidate;
|
|
9500
|
-
continue;
|
|
9501
|
-
}
|
|
9502
|
-
let cursor = candidate.scopeOwner;
|
|
9503
|
-
while (cursor) {
|
|
9504
|
-
if (cursor === best.scopeOwner) {
|
|
9505
|
-
best = candidate;
|
|
9506
|
-
break;
|
|
9507
|
-
}
|
|
9508
|
-
cursor = cursor.parent ?? null;
|
|
9509
|
-
}
|
|
9510
|
-
}
|
|
9511
|
-
return best;
|
|
9512
|
-
};
|
|
9513
|
-
//#endregion
|
|
9514
9730
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
9515
9731
|
const buildMessage$18 = (depth, max) => `This JSX is hard to read at ${depth} levels deep, past the limit of ${max}.`;
|
|
9516
9732
|
const DEFAULT_MAX_DEPTH = 14;
|
|
9517
|
-
const resolveSettings$
|
|
9733
|
+
const resolveSettings$29 = (settings) => {
|
|
9518
9734
|
const reactDoctor = settings?.["react-doctor"];
|
|
9519
9735
|
return { max: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxMaxDepth ?? {} : {}).max ?? DEFAULT_MAX_DEPTH };
|
|
9520
9736
|
};
|
|
@@ -9574,7 +9790,7 @@ const jsxMaxDepth = defineRule({
|
|
|
9574
9790
|
recommendation: "Pull deeply nested JSX into smaller components so it's easier to read.",
|
|
9575
9791
|
category: "Architecture",
|
|
9576
9792
|
create: (context) => {
|
|
9577
|
-
const { max } = resolveSettings$
|
|
9793
|
+
const { max } = resolveSettings$29(context.settings);
|
|
9578
9794
|
const checkNode = (node) => {
|
|
9579
9795
|
if (!isLeafJsxNode(node)) return;
|
|
9580
9796
|
const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
|
|
@@ -11269,7 +11485,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
11269
11485
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
11270
11486
|
const MESSAGE$35 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
11271
11487
|
const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
11272
|
-
const resolveSettings$
|
|
11488
|
+
const resolveSettings$28 = (settings) => {
|
|
11273
11489
|
const reactDoctor = settings?.["react-doctor"];
|
|
11274
11490
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
11275
11491
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
@@ -11295,7 +11511,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
11295
11511
|
recommendation: "Replace `javascript:` URLs with `onClick` or `onSubmit` handlers. React 19 blocks them anyway.",
|
|
11296
11512
|
category: "Security",
|
|
11297
11513
|
create: (context) => {
|
|
11298
|
-
const options = resolveSettings$
|
|
11514
|
+
const options = resolveSettings$28(context.settings);
|
|
11299
11515
|
return { JSXOpeningElement(node) {
|
|
11300
11516
|
const elementName = getElementName(node);
|
|
11301
11517
|
if (!elementName) return;
|
|
@@ -11315,287 +11531,6 @@ const jsxNoScriptUrl = defineRule({
|
|
|
11315
11531
|
}
|
|
11316
11532
|
});
|
|
11317
11533
|
//#endregion
|
|
11318
|
-
//#region src/plugin/rules/react-builtins/jsx-no-target-blank.ts
|
|
11319
|
-
const NOREFERRER_MESSAGE = "`target=\"_blank\"` without `rel=\"noreferrer\"` lets the linked page hijack your tab to a phishing site.";
|
|
11320
|
-
const NOOPENER_MESSAGE = "`target=\"_blank\"` without `rel` lets the linked page hijack your tab to a phishing site.";
|
|
11321
|
-
const SPREAD_MESSAGE = "A spread here can add `target=\"_blank\"`, letting the linked page hijack your tab to a phishing site.";
|
|
11322
|
-
const resolveSettings$28 = (settings) => {
|
|
11323
|
-
const reactDoctor = settings?.["react-doctor"];
|
|
11324
|
-
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxNoTargetBlank ?? {} : {};
|
|
11325
|
-
const reactSettings = typeof settings?.react === "object" && settings.react !== null ? settings.react : {};
|
|
11326
|
-
const linkComponents = /* @__PURE__ */ new Map();
|
|
11327
|
-
for (const entry of reactSettings.linkComponents ?? []) if (typeof entry === "string") linkComponents.set(entry, ["href"]);
|
|
11328
|
-
else if (typeof entry === "object" && entry !== null) {
|
|
11329
|
-
const linkAttribute = entry.linkAttribute ?? "href";
|
|
11330
|
-
linkComponents.set(entry.name, Array.isArray(linkAttribute) ? linkAttribute : [linkAttribute]);
|
|
11331
|
-
}
|
|
11332
|
-
const formComponents = /* @__PURE__ */ new Map();
|
|
11333
|
-
for (const entry of reactSettings.formComponents ?? []) if (typeof entry === "string") formComponents.set(entry, ["action"]);
|
|
11334
|
-
else if (typeof entry === "object" && entry !== null) {
|
|
11335
|
-
const formAttribute = entry.formAttribute ?? "action";
|
|
11336
|
-
formComponents.set(entry.name, Array.isArray(formAttribute) ? formAttribute : [formAttribute]);
|
|
11337
|
-
}
|
|
11338
|
-
return {
|
|
11339
|
-
enforceDynamicLinks: ruleSettings.enforceDynamicLinks ?? "always",
|
|
11340
|
-
warnOnSpreadAttributes: ruleSettings.warnOnSpreadAttributes ?? false,
|
|
11341
|
-
allowReferrer: ruleSettings.allowReferrer ?? false,
|
|
11342
|
-
links: ruleSettings.links ?? true,
|
|
11343
|
-
forms: ruleSettings.forms ?? false,
|
|
11344
|
-
linkComponents,
|
|
11345
|
-
formComponents
|
|
11346
|
-
};
|
|
11347
|
-
};
|
|
11348
|
-
const isExternalLink = (href) => href.includes("//");
|
|
11349
|
-
const matchHrefExpression = (expression, state) => {
|
|
11350
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") {
|
|
11351
|
-
if (isExternalLink(expression.value)) state.isExternal = true;
|
|
11352
|
-
return;
|
|
11353
|
-
}
|
|
11354
|
-
if (isNodeOfType(expression, "Identifier")) {
|
|
11355
|
-
state.isDynamic = true;
|
|
11356
|
-
return;
|
|
11357
|
-
}
|
|
11358
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
11359
|
-
matchHrefExpression(expression.consequent, state);
|
|
11360
|
-
matchHrefExpression(expression.alternate, state);
|
|
11361
|
-
}
|
|
11362
|
-
};
|
|
11363
|
-
const checkHref = (attributeValue, enforceDynamicLinks) => {
|
|
11364
|
-
const state = {
|
|
11365
|
-
isExternal: false,
|
|
11366
|
-
isDynamic: false
|
|
11367
|
-
};
|
|
11368
|
-
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") state.isExternal = isExternalLink(attributeValue.value);
|
|
11369
|
-
else if (isNodeOfType(attributeValue, "JSXExpressionContainer")) matchHrefExpression(attributeValue.expression, state);
|
|
11370
|
-
if (enforceDynamicLinks === "never") return !state.isExternal || state.isDynamic;
|
|
11371
|
-
return !(state.isExternal || state.isDynamic);
|
|
11372
|
-
};
|
|
11373
|
-
const checkRelValue = (text, allowReferrer) => {
|
|
11374
|
-
const tokens = text.split(/\s+/);
|
|
11375
|
-
if (allowReferrer) return tokens.includes("noopener") || tokens.includes("noreferrer");
|
|
11376
|
-
return tokens.some((token) => token.toLowerCase() === "noreferrer");
|
|
11377
|
-
};
|
|
11378
|
-
const matchRelExpression = (expression, allowReferrer) => {
|
|
11379
|
-
const empty = {
|
|
11380
|
-
combined: false,
|
|
11381
|
-
testName: "",
|
|
11382
|
-
consequent: false,
|
|
11383
|
-
alternate: false
|
|
11384
|
-
};
|
|
11385
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
|
|
11386
|
-
combined: checkRelValue(expression.value, allowReferrer),
|
|
11387
|
-
testName: "",
|
|
11388
|
-
consequent: false,
|
|
11389
|
-
alternate: false
|
|
11390
|
-
};
|
|
11391
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
11392
|
-
const consequent = matchRelExpression(expression.consequent, allowReferrer);
|
|
11393
|
-
const alternate = matchRelExpression(expression.alternate, allowReferrer);
|
|
11394
|
-
const test = expression.test;
|
|
11395
|
-
if (isNodeOfType(test, "Identifier")) return {
|
|
11396
|
-
combined: consequent.combined && alternate.combined,
|
|
11397
|
-
testName: test.name,
|
|
11398
|
-
consequent: consequent.combined,
|
|
11399
|
-
alternate: alternate.combined
|
|
11400
|
-
};
|
|
11401
|
-
return {
|
|
11402
|
-
combined: consequent.combined && alternate.combined,
|
|
11403
|
-
testName: "",
|
|
11404
|
-
consequent: consequent.combined,
|
|
11405
|
-
alternate: alternate.combined
|
|
11406
|
-
};
|
|
11407
|
-
}
|
|
11408
|
-
return empty;
|
|
11409
|
-
};
|
|
11410
|
-
const checkRel = (attributeValue, allowReferrer) => {
|
|
11411
|
-
const empty = {
|
|
11412
|
-
combined: false,
|
|
11413
|
-
testName: "",
|
|
11414
|
-
consequent: false,
|
|
11415
|
-
alternate: false
|
|
11416
|
-
};
|
|
11417
|
-
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
|
|
11418
|
-
combined: checkRelValue(attributeValue.value, allowReferrer),
|
|
11419
|
-
testName: "",
|
|
11420
|
-
consequent: false,
|
|
11421
|
-
alternate: false
|
|
11422
|
-
};
|
|
11423
|
-
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
11424
|
-
const expression = attributeValue.expression;
|
|
11425
|
-
if (expression.type === "JSXEmptyExpression") return empty;
|
|
11426
|
-
return matchRelExpression(expression, allowReferrer);
|
|
11427
|
-
}
|
|
11428
|
-
return empty;
|
|
11429
|
-
};
|
|
11430
|
-
const matchTargetExpression = (expression) => {
|
|
11431
|
-
const empty = {
|
|
11432
|
-
combined: false,
|
|
11433
|
-
testName: "",
|
|
11434
|
-
consequent: false,
|
|
11435
|
-
alternate: false
|
|
11436
|
-
};
|
|
11437
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
|
|
11438
|
-
combined: expression.value.toLowerCase() === "_blank",
|
|
11439
|
-
testName: "",
|
|
11440
|
-
consequent: false,
|
|
11441
|
-
alternate: false
|
|
11442
|
-
};
|
|
11443
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
11444
|
-
const consequent = matchTargetExpression(expression.consequent);
|
|
11445
|
-
const alternate = matchTargetExpression(expression.alternate);
|
|
11446
|
-
const test = expression.test;
|
|
11447
|
-
const combined = consequent.combined || alternate.combined;
|
|
11448
|
-
if (isNodeOfType(test, "Identifier")) return {
|
|
11449
|
-
combined,
|
|
11450
|
-
testName: test.name,
|
|
11451
|
-
consequent: consequent.combined,
|
|
11452
|
-
alternate: alternate.combined
|
|
11453
|
-
};
|
|
11454
|
-
return {
|
|
11455
|
-
combined,
|
|
11456
|
-
testName: "",
|
|
11457
|
-
consequent: consequent.combined,
|
|
11458
|
-
alternate: alternate.combined
|
|
11459
|
-
};
|
|
11460
|
-
}
|
|
11461
|
-
return empty;
|
|
11462
|
-
};
|
|
11463
|
-
const checkTarget = (attributeValue) => {
|
|
11464
|
-
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
|
|
11465
|
-
combined: attributeValue.value.toLowerCase() === "_blank",
|
|
11466
|
-
testName: "",
|
|
11467
|
-
consequent: false,
|
|
11468
|
-
alternate: false
|
|
11469
|
-
};
|
|
11470
|
-
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
11471
|
-
const expression = attributeValue.expression;
|
|
11472
|
-
if (expression.type === "JSXEmptyExpression") return {
|
|
11473
|
-
combined: false,
|
|
11474
|
-
testName: "",
|
|
11475
|
-
consequent: false,
|
|
11476
|
-
alternate: false
|
|
11477
|
-
};
|
|
11478
|
-
return matchTargetExpression(expression);
|
|
11479
|
-
}
|
|
11480
|
-
return {
|
|
11481
|
-
combined: false,
|
|
11482
|
-
testName: "",
|
|
11483
|
-
consequent: false,
|
|
11484
|
-
alternate: false
|
|
11485
|
-
};
|
|
11486
|
-
};
|
|
11487
|
-
const getOpeningElementName = (node) => {
|
|
11488
|
-
const name = node.name;
|
|
11489
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
11490
|
-
return null;
|
|
11491
|
-
};
|
|
11492
|
-
const jsxNoTargetBlank = defineRule({
|
|
11493
|
-
id: "jsx-no-target-blank",
|
|
11494
|
-
title: "Unsafe target=_blank link",
|
|
11495
|
-
severity: "warn",
|
|
11496
|
-
recommendation: "Add `rel=\"noreferrer\"` (or `\"noopener\"`) when using `target=\"_blank\"`.",
|
|
11497
|
-
category: "Security",
|
|
11498
|
-
create: (context) => {
|
|
11499
|
-
const settings = resolveSettings$28(context.settings);
|
|
11500
|
-
const isLink = (tagName) => {
|
|
11501
|
-
if (!settings.links) return false;
|
|
11502
|
-
if (tagName === "a") return true;
|
|
11503
|
-
return settings.linkComponents.has(tagName);
|
|
11504
|
-
};
|
|
11505
|
-
const isForm = (tagName) => {
|
|
11506
|
-
if (!settings.forms) return false;
|
|
11507
|
-
if (tagName === "form") return true;
|
|
11508
|
-
return settings.formComponents.has(tagName);
|
|
11509
|
-
};
|
|
11510
|
-
return { JSXOpeningElement(node) {
|
|
11511
|
-
const tagName = getOpeningElementName(node);
|
|
11512
|
-
if (!tagName) return;
|
|
11513
|
-
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
11514
|
-
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
11515
|
-
const formAttributeNames = settings.formComponents.get(tagName) ?? ["action"];
|
|
11516
|
-
let targetTuple = {
|
|
11517
|
-
combined: false,
|
|
11518
|
-
testName: "",
|
|
11519
|
-
consequent: false,
|
|
11520
|
-
alternate: false
|
|
11521
|
-
};
|
|
11522
|
-
let relTuple = {
|
|
11523
|
-
combined: false,
|
|
11524
|
-
testName: "",
|
|
11525
|
-
consequent: false,
|
|
11526
|
-
alternate: false
|
|
11527
|
-
};
|
|
11528
|
-
let isHrefValid = true;
|
|
11529
|
-
let hasHrefValue = false;
|
|
11530
|
-
let warnSpread = false;
|
|
11531
|
-
let targetReportNode = node.name;
|
|
11532
|
-
let spreadReportNode = null;
|
|
11533
|
-
for (const attribute of node.attributes) {
|
|
11534
|
-
if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
|
|
11535
|
-
if (settings.warnOnSpreadAttributes) {
|
|
11536
|
-
warnSpread = true;
|
|
11537
|
-
spreadReportNode = attribute;
|
|
11538
|
-
targetTuple = {
|
|
11539
|
-
combined: false,
|
|
11540
|
-
testName: "",
|
|
11541
|
-
consequent: false,
|
|
11542
|
-
alternate: false
|
|
11543
|
-
};
|
|
11544
|
-
relTuple = {
|
|
11545
|
-
combined: false,
|
|
11546
|
-
testName: "",
|
|
11547
|
-
consequent: false,
|
|
11548
|
-
alternate: false
|
|
11549
|
-
};
|
|
11550
|
-
isHrefValid = false;
|
|
11551
|
-
hasHrefValue = true;
|
|
11552
|
-
}
|
|
11553
|
-
continue;
|
|
11554
|
-
}
|
|
11555
|
-
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
11556
|
-
const attributeName = attribute.name;
|
|
11557
|
-
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
11558
|
-
const propName = attributeName.name;
|
|
11559
|
-
const value = attribute.value;
|
|
11560
|
-
if (propName === "target") {
|
|
11561
|
-
if (value) {
|
|
11562
|
-
targetTuple = checkTarget(value);
|
|
11563
|
-
targetReportNode = value;
|
|
11564
|
-
}
|
|
11565
|
-
} else if (propName === "href" || propName === "action" || linkAttributeNames.includes(propName) || formAttributeNames.includes(propName)) {
|
|
11566
|
-
if (value) {
|
|
11567
|
-
hasHrefValue = true;
|
|
11568
|
-
isHrefValid = checkHref(value, settings.enforceDynamicLinks);
|
|
11569
|
-
}
|
|
11570
|
-
} else if (propName === "rel" && value) relTuple = checkRel(value, settings.allowReferrer);
|
|
11571
|
-
}
|
|
11572
|
-
if (warnSpread) {
|
|
11573
|
-
if (hasHrefValue && isHrefValid || relTuple.combined) return;
|
|
11574
|
-
context.report({
|
|
11575
|
-
node: spreadReportNode ?? node,
|
|
11576
|
-
message: SPREAD_MESSAGE
|
|
11577
|
-
});
|
|
11578
|
-
return;
|
|
11579
|
-
}
|
|
11580
|
-
if (!isHrefValid) {
|
|
11581
|
-
if (targetTuple.testName !== "" && targetTuple.testName === relTuple.testName) {
|
|
11582
|
-
const consequentBad = targetTuple.consequent && !relTuple.consequent;
|
|
11583
|
-
const alternateBad = targetTuple.alternate && !relTuple.alternate;
|
|
11584
|
-
if (consequentBad || alternateBad) context.report({
|
|
11585
|
-
node: targetReportNode,
|
|
11586
|
-
message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
|
|
11587
|
-
});
|
|
11588
|
-
return;
|
|
11589
|
-
}
|
|
11590
|
-
if (targetTuple.combined && !relTuple.combined) context.report({
|
|
11591
|
-
node: targetReportNode,
|
|
11592
|
-
message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
|
|
11593
|
-
});
|
|
11594
|
-
}
|
|
11595
|
-
} };
|
|
11596
|
-
}
|
|
11597
|
-
});
|
|
11598
|
-
//#endregion
|
|
11599
11534
|
//#region src/plugin/rules/react-builtins/jsx-no-undef.ts
|
|
11600
11535
|
const buildMessage$17 = (name) => `\`${name}\` crashes at runtime because it isn't defined here.`;
|
|
11601
11536
|
const KNOWN_GLOBALS = new Set([
|
|
@@ -11925,7 +11860,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
11925
11860
|
create: (context) => {
|
|
11926
11861
|
const settings = resolveSettings$25(context.settings);
|
|
11927
11862
|
return { JSXOpeningElement(node) {
|
|
11928
|
-
const tagName = flattenJsxName(node.name);
|
|
11863
|
+
const tagName = flattenJsxName$1(node.name);
|
|
11929
11864
|
if (!tagName) return;
|
|
11930
11865
|
const isCustom = isReactComponentName(tagName) || tagName.includes(".");
|
|
11931
11866
|
const isHtml = !isCustom;
|
|
@@ -12511,50 +12446,6 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
12511
12446
|
}
|
|
12512
12447
|
});
|
|
12513
12448
|
//#endregion
|
|
12514
|
-
//#region src/plugin/constants/nextjs.ts
|
|
12515
|
-
const PAGE_FILE_PATTERN = /\/page\.(tsx?|jsx?)$/;
|
|
12516
|
-
const PAGE_OR_LAYOUT_FILE_PATTERN = /\/(page|layout)\.(tsx?|jsx?)$/;
|
|
12517
|
-
const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
|
|
12518
|
-
const OG_ROUTE_PATTERN = /\/og\b/i;
|
|
12519
|
-
const PAGES_DIRECTORY_PATTERN = /\/pages\//;
|
|
12520
|
-
const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
|
|
12521
|
-
"redirect",
|
|
12522
|
-
"permanentRedirect",
|
|
12523
|
-
"notFound",
|
|
12524
|
-
"forbidden",
|
|
12525
|
-
"unauthorized"
|
|
12526
|
-
]);
|
|
12527
|
-
const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
|
|
12528
|
-
const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
|
|
12529
|
-
const APP_DIRECTORY_PATTERN = /\/app\//;
|
|
12530
|
-
const ROUTE_HANDLER_FILE_PATTERN = /\/route\.(tsx?|jsx?)$/;
|
|
12531
|
-
const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
|
|
12532
|
-
const MUTATING_ROUTE_SEGMENTS = new Set([
|
|
12533
|
-
"logout",
|
|
12534
|
-
"log-out",
|
|
12535
|
-
"signout",
|
|
12536
|
-
"sign-out",
|
|
12537
|
-
"unsubscribe",
|
|
12538
|
-
"delete",
|
|
12539
|
-
"remove",
|
|
12540
|
-
"revoke",
|
|
12541
|
-
"cancel",
|
|
12542
|
-
"deactivate"
|
|
12543
|
-
]);
|
|
12544
|
-
const ERROR_BOUNDARY_FILE_PATTERN = /\/(error|global-error)\.(tsx?|jsx?)$/;
|
|
12545
|
-
const GLOBAL_ERROR_FILE_PATTERN = /\/global-error\.(tsx?|jsx?)$/;
|
|
12546
|
-
const ROUTE_HANDLER_HTTP_METHODS = new Set([
|
|
12547
|
-
"GET",
|
|
12548
|
-
"POST",
|
|
12549
|
-
"PUT",
|
|
12550
|
-
"PATCH",
|
|
12551
|
-
"DELETE",
|
|
12552
|
-
"OPTIONS",
|
|
12553
|
-
"HEAD"
|
|
12554
|
-
]);
|
|
12555
|
-
const GOOGLE_ANALYTICS_SCRIPT_PATTERN = /google-analytics\.com|googletagmanager\.com\/gtag/;
|
|
12556
|
-
const OG_IMAGE_FILE_PATTERN = /\/(opengraph-image|twitter-image)\d*\.(tsx?|jsx?)$/;
|
|
12557
|
-
//#endregion
|
|
12558
12449
|
//#region src/plugin/rules/nextjs/nextjs-error-boundary-missing-use-client.ts
|
|
12559
12450
|
const nextjsErrorBoundaryMissingUseClient = defineRule({
|
|
12560
12451
|
id: "nextjs-error-boundary-missing-use-client",
|
|
@@ -12952,11 +12843,9 @@ const nextjsNoImgElement = defineRule({
|
|
|
12952
12843
|
severity: "warn",
|
|
12953
12844
|
recommendation: "`import Image from 'next/image'` for automatic WebP/AVIF, lazy loading, and responsive srcset",
|
|
12954
12845
|
create: (context) => {
|
|
12955
|
-
|
|
12956
|
-
const isOgRoute = OG_ROUTE_PATTERN.test(filename);
|
|
12957
|
-
const isMetadataImageRoute = isNextjsMetadataImageRouteFilename(filename);
|
|
12846
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
12958
12847
|
return { JSXOpeningElement(node) {
|
|
12959
|
-
if (
|
|
12848
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
12960
12849
|
if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "img") context.report({
|
|
12961
12850
|
node,
|
|
12962
12851
|
message: "Plain <img> ships unoptimized, oversized images to your users."
|
|
@@ -21586,12 +21475,22 @@ const PUBLIC_CLIENT_KEY_PATTERNS = [
|
|
|
21586
21475
|
/^public-token-(?:live|test)-/,
|
|
21587
21476
|
/^pk\.eyJ/
|
|
21588
21477
|
];
|
|
21478
|
+
const SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS = [
|
|
21479
|
+
/^[\s._\-*\u2022xX]{8,}$/,
|
|
21480
|
+
/(?:\.{3,}|\u2026|[*\u2022]{3,})/,
|
|
21481
|
+
/(?:^|[_\-\s])(?:your|redacted|masked|placeholder|replace[_\-\s]?me|changeme)(?:$|[_\-\s])/i,
|
|
21482
|
+
/<[^>]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^>]*>/i,
|
|
21483
|
+
/\[[^\]]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^\]]*\]/i,
|
|
21484
|
+
/\{[^}]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^}]*\}/i
|
|
21485
|
+
];
|
|
21486
|
+
const SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS = [/(?:^|[_\-\s])(?:example|sample|dummy)(?:$|[_\-\s])/i];
|
|
21487
|
+
const SECRET_PLACEHOLDER_CONTEXT_PATTERN = /(?:placeholder|example|sample|dummy|masked|redacted|mask)/i;
|
|
21589
21488
|
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
|
|
21590
21489
|
const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
|
|
21591
21490
|
const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
|
|
21592
21491
|
const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
|
|
21593
21492
|
const SECRET_SERVER_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.server\.[cm]?[jt]sx?$/;
|
|
21594
|
-
const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|route)\.[cm]?[jt]sx?$/;
|
|
21493
|
+
const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|proxy|route)\.[cm]?[jt]sx?$/;
|
|
21595
21494
|
const SECRET_NEXT_PAGES_API_FILE_PATTERN = /(?:^|\/)pages\/api\/.+\.[cm]?[jt]sx?$/;
|
|
21596
21495
|
const SECRET_CLIENT_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.(?:client|browser|web)\.[cm]?[jt]sx?$/;
|
|
21597
21496
|
const SECRET_CLIENT_ENTRY_FILE_PATTERN = /(?:^|\/)(?:src\/)?(?:main|index|[Aa]pp|client)\.[cm]?[jt]sx?$/;
|
|
@@ -21871,6 +21770,15 @@ const isInsideServerOnlyScope = (node) => {
|
|
|
21871
21770
|
return false;
|
|
21872
21771
|
};
|
|
21873
21772
|
//#endregion
|
|
21773
|
+
//#region src/plugin/utils/is-placeholder-secret-value.ts
|
|
21774
|
+
const isPlaceholderSecretValue = (literalValue, options) => {
|
|
21775
|
+
const trimmedValue = literalValue.trim();
|
|
21776
|
+
if (trimmedValue.length === 0) return false;
|
|
21777
|
+
if (SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS.some((pattern) => pattern.test(trimmedValue))) return true;
|
|
21778
|
+
if (!options.allowContextualExamples) return false;
|
|
21779
|
+
return SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS.some((pattern) => pattern.test(trimmedValue));
|
|
21780
|
+
};
|
|
21781
|
+
//#endregion
|
|
21874
21782
|
//#region src/plugin/rules/security/no-secrets-in-client-code.ts
|
|
21875
21783
|
const noSecretsInClientCode = defineRule({
|
|
21876
21784
|
id: "no-secrets-in-client-code",
|
|
@@ -21899,21 +21807,28 @@ const noSecretsInClientCode = defineRule({
|
|
|
21899
21807
|
if (!isNodeOfType(node.init, "Literal") || typeof node.init.value !== "string") return;
|
|
21900
21808
|
const variableName = node.id.name;
|
|
21901
21809
|
const literalValue = node.init.value;
|
|
21810
|
+
const componentOrHookName = enclosingComponentOrHookName(node);
|
|
21811
|
+
const hasPlaceholderContext = SECRET_PLACEHOLDER_CONTEXT_PATTERN.test(variableName) || componentOrHookName !== null && SECRET_PLACEHOLDER_CONTEXT_PATTERN.test(componentOrHookName);
|
|
21812
|
+
const isUnambiguousPlaceholderValue = isPlaceholderSecretValue(literalValue, { allowContextualExamples: false });
|
|
21813
|
+
const isPlaceholderValueForVariableHeuristic = isPlaceholderSecretValue(literalValue, { allowContextualExamples: hasPlaceholderContext });
|
|
21902
21814
|
if (PUBLIC_CLIENT_KEY_PATTERNS.some((pattern) => pattern.test(literalValue))) return;
|
|
21815
|
+
if (SECRET_PATTERNS.some((pattern) => pattern.test(literalValue))) {
|
|
21816
|
+
if (!isUnambiguousPlaceholderValue) context.report({
|
|
21817
|
+
node,
|
|
21818
|
+
message: "This hardcoded secret is a security vulnerability: it ships to the browser where anyone can read it."
|
|
21819
|
+
});
|
|
21820
|
+
return;
|
|
21821
|
+
}
|
|
21903
21822
|
const isServerOnlyScope = isInsideServerOnlyScope(node);
|
|
21904
21823
|
const trailingSuffix = getIdentifierTrailingWord(variableName);
|
|
21905
21824
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
21906
|
-
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && literalValue.length > 24) {
|
|
21825
|
+
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPlaceholderValueForVariableHeuristic && literalValue.length > 24) {
|
|
21907
21826
|
context.report({
|
|
21908
21827
|
node,
|
|
21909
21828
|
message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
|
|
21910
21829
|
});
|
|
21911
21830
|
return;
|
|
21912
21831
|
}
|
|
21913
|
-
if (SECRET_PATTERNS.some((pattern) => pattern.test(literalValue))) context.report({
|
|
21914
|
-
node,
|
|
21915
|
-
message: "This hardcoded secret is a security vulnerability: it ships to the browser where anyone can read it."
|
|
21916
|
-
});
|
|
21917
21832
|
}
|
|
21918
21833
|
};
|
|
21919
21834
|
}
|
|
@@ -23799,7 +23714,7 @@ const noUnknownProperty = defineRule({
|
|
|
23799
23714
|
create: (context) => {
|
|
23800
23715
|
const { ignore = [], requireDataLowercase = false } = resolveSettings$10(context.settings);
|
|
23801
23716
|
const ignoreSet = new Set(ignore);
|
|
23802
|
-
if (
|
|
23717
|
+
if (isGeneratedImageRenderContext(context)) ignoreSet.add("tw");
|
|
23803
23718
|
let fileIsNonReactJsx = false;
|
|
23804
23719
|
return {
|
|
23805
23720
|
Program(node) {
|
|
@@ -23834,6 +23749,7 @@ const noUnknownProperty = defineRule({
|
|
|
23834
23749
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23835
23750
|
const actualName = getJsxAttributeName(attribute.name);
|
|
23836
23751
|
if (!actualName) continue;
|
|
23752
|
+
if (actualName === "tw" && isGeneratedImageRenderContext(context, node)) continue;
|
|
23837
23753
|
if (ignoreSet.has(actualName)) continue;
|
|
23838
23754
|
if (isValidDataAttribute(actualName)) {
|
|
23839
23755
|
if (requireDataLowercase && hasUppercaseChar(actualName)) context.report({
|
|
@@ -25455,12 +25371,12 @@ const doesFunctionReturnsObjectLiteral = (functionNode) => {
|
|
|
25455
25371
|
const child = nodeRecord[key];
|
|
25456
25372
|
if (Array.isArray(child)) for (const item of child) {
|
|
25457
25373
|
if (!isAstNode(item)) continue;
|
|
25458
|
-
if (FUNCTION_LIKE_TYPES
|
|
25374
|
+
if (FUNCTION_LIKE_TYPES.has(item.type)) continue;
|
|
25459
25375
|
visit(item);
|
|
25460
25376
|
if (returnsObject) return;
|
|
25461
25377
|
}
|
|
25462
25378
|
else if (isAstNode(child)) {
|
|
25463
|
-
if (FUNCTION_LIKE_TYPES
|
|
25379
|
+
if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
|
|
25464
25380
|
visit(child);
|
|
25465
25381
|
}
|
|
25466
25382
|
}
|
|
@@ -26882,12 +26798,12 @@ const functionBodyHasReturnWithValue = (functionNode) => {
|
|
|
26882
26798
|
const child = nodeRecord[key];
|
|
26883
26799
|
if (Array.isArray(child)) for (const item of child) {
|
|
26884
26800
|
if (!isAstNode(item)) continue;
|
|
26885
|
-
if (FUNCTION_LIKE_TYPES
|
|
26801
|
+
if (FUNCTION_LIKE_TYPES.has(item.type)) continue;
|
|
26886
26802
|
visit(item);
|
|
26887
26803
|
if (didFindReturn) return;
|
|
26888
26804
|
}
|
|
26889
26805
|
else if (isAstNode(child)) {
|
|
26890
|
-
if (FUNCTION_LIKE_TYPES
|
|
26806
|
+
if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
|
|
26891
26807
|
visit(child);
|
|
26892
26808
|
}
|
|
26893
26809
|
}
|
|
@@ -28637,7 +28553,7 @@ const isExpoUiNamespaceImport = (contextNode, localName) => {
|
|
|
28637
28553
|
};
|
|
28638
28554
|
const isExpoUiComponentElement = (openingElement, contextNode, componentName) => {
|
|
28639
28555
|
if (!openingElement.name) return false;
|
|
28640
|
-
const dottedName = flattenJsxName(openingElement.name);
|
|
28556
|
+
const dottedName = flattenJsxName$1(openingElement.name);
|
|
28641
28557
|
if (!dottedName) return false;
|
|
28642
28558
|
const [rootLocalName, secondName] = dottedName.split(".");
|
|
28643
28559
|
if (isNamedImportOf(contextNode, rootLocalName, componentName)) return true;
|
|
@@ -28727,7 +28643,7 @@ const collectTopLevelReturnExpressions = (functionNode) => {
|
|
|
28727
28643
|
if (!block || !isNodeOfType(block, "BlockStatement")) return [];
|
|
28728
28644
|
const returnExpressions = [];
|
|
28729
28645
|
const visit = (node) => {
|
|
28730
|
-
if (FUNCTION_LIKE_TYPES
|
|
28646
|
+
if (FUNCTION_LIKE_TYPES.has(node.type)) return;
|
|
28731
28647
|
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnExpressions.push(node.argument);
|
|
28732
28648
|
const nodeRecord = node;
|
|
28733
28649
|
for (const fieldName of Object.keys(nodeRecord)) {
|
|
@@ -33566,7 +33482,7 @@ const objectExpressionHasNextRevalidate = (objectExpression) => {
|
|
|
33566
33482
|
}
|
|
33567
33483
|
return false;
|
|
33568
33484
|
};
|
|
33569
|
-
const APP_ROUTER_FILE_PATTERN =
|
|
33485
|
+
const APP_ROUTER_FILE_PATTERN = new RegExp(`/app/(?:[^/]+/)*(?:route|page|layout|template|loading|error|default)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
33570
33486
|
const NON_PROJECT_PATH_PATTERN = /\/(?:node_modules|dist|build|\.next)\//;
|
|
33571
33487
|
const serverFetchWithoutRevalidate = defineRule({
|
|
33572
33488
|
id: "server-fetch-without-revalidate",
|
|
@@ -35999,17 +35915,6 @@ const reactDoctorRules = [
|
|
|
35999
35915
|
category: "Security"
|
|
36000
35916
|
}
|
|
36001
35917
|
},
|
|
36002
|
-
{
|
|
36003
|
-
key: "react-doctor/jsx-no-target-blank",
|
|
36004
|
-
id: "jsx-no-target-blank",
|
|
36005
|
-
source: "react-doctor",
|
|
36006
|
-
originallyExternal: true,
|
|
36007
|
-
rule: {
|
|
36008
|
-
...jsxNoTargetBlank,
|
|
36009
|
-
framework: "global",
|
|
36010
|
-
category: "Security"
|
|
36011
|
-
}
|
|
36012
|
-
},
|
|
36013
35918
|
{
|
|
36014
35919
|
key: "react-doctor/jsx-no-undef",
|
|
36015
35920
|
id: "jsx-no-undef",
|