oxlint-plugin-react-doctor 0.4.2-dev.6bfc03a → 0.4.2-dev.772385f
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 +560 -683
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -823,6 +823,310 @@ 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
|
|
826
1130
|
//#region src/plugin/utils/is-nextjs-metadata-image-route-filename.ts
|
|
827
1131
|
const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
828
1132
|
if (!rawFilename) return false;
|
|
@@ -832,6 +1136,192 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
|
832
1136
|
//#region src/plugin/utils/normalize-filename.ts
|
|
833
1137
|
const normalizeFilename$1 = (filename) => filename.replaceAll("\\", "/");
|
|
834
1138
|
//#endregion
|
|
1139
|
+
//#region src/plugin/utils/strip-paren-expression.ts
|
|
1140
|
+
const TS_WRAPPER_TYPES = new Set([
|
|
1141
|
+
"ParenthesizedExpression",
|
|
1142
|
+
"TSAsExpression",
|
|
1143
|
+
"TSSatisfiesExpression",
|
|
1144
|
+
"TSTypeAssertion",
|
|
1145
|
+
"TSNonNullExpression",
|
|
1146
|
+
"TSInstantiationExpression"
|
|
1147
|
+
]);
|
|
1148
|
+
const stripParenExpression = (node) => {
|
|
1149
|
+
let current = node;
|
|
1150
|
+
while (true) {
|
|
1151
|
+
if (TS_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) {
|
|
1152
|
+
current = current.expression;
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
if (isNodeOfType(current, "ChainExpression") && current.expression) {
|
|
1156
|
+
current = current.expression;
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
return current;
|
|
1162
|
+
};
|
|
1163
|
+
//#endregion
|
|
1164
|
+
//#region src/plugin/utils/is-generated-image-render-context.ts
|
|
1165
|
+
const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
|
|
1166
|
+
const SATORI_MODULE = "satori";
|
|
1167
|
+
const generatedImageJsxCache = /* @__PURE__ */ new WeakMap();
|
|
1168
|
+
const isGeneratedImageRenderFilename = (rawFilename) => {
|
|
1169
|
+
if (!rawFilename) return false;
|
|
1170
|
+
return isNextjsMetadataImageRouteFilename(normalizeFilename$1(rawFilename));
|
|
1171
|
+
};
|
|
1172
|
+
const isImageResponseCallee = (contextNode, callee) => {
|
|
1173
|
+
if (isNodeOfType(callee, "Identifier")) return IMAGE_RESPONSE_MODULES.some((moduleSource) => getImportedNameFromModule(contextNode, callee.name, moduleSource) === "ImageResponse");
|
|
1174
|
+
if (!isMemberProperty(callee, "ImageResponse")) return false;
|
|
1175
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
1176
|
+
const namespaceIdentifierName = callee.object.name;
|
|
1177
|
+
return IMAGE_RESPONSE_MODULES.some((moduleSource) => isNamespaceImportFromModule(contextNode, namespaceIdentifierName, moduleSource));
|
|
1178
|
+
};
|
|
1179
|
+
const isSatoriCallee = (contextNode, callee) => {
|
|
1180
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
1181
|
+
if (getImportedNameFromModule(contextNode, callee.name, SATORI_MODULE) === "satori") return true;
|
|
1182
|
+
return isDefaultImportFromModule(contextNode, callee.name, SATORI_MODULE);
|
|
1183
|
+
};
|
|
1184
|
+
const isGeneratedImageRendererCall = (node) => {
|
|
1185
|
+
if (!isNodeOfType(node, "CallExpression") && !isNodeOfType(node, "NewExpression")) return false;
|
|
1186
|
+
if (!isNodeOfType(node.callee, "Identifier") && !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
1187
|
+
return isImageResponseCallee(node, node.callee) || isSatoriCallee(node, node.callee);
|
|
1188
|
+
};
|
|
1189
|
+
const isComponentIdentifierName = (name) => {
|
|
1190
|
+
const firstCharacter = name[0];
|
|
1191
|
+
return Boolean(firstCharacter && firstCharacter === firstCharacter.toUpperCase());
|
|
1192
|
+
};
|
|
1193
|
+
const isFunctionLike$3 = (node) => Boolean(node && (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")));
|
|
1194
|
+
const markFunctionReturnJsx = (functionNode, programRoot, generatedImageJsxNodes, visitedComponentNames) => {
|
|
1195
|
+
if (!isFunctionLike$3(functionNode)) return;
|
|
1196
|
+
if (isNodeOfType(functionNode, "ArrowFunctionExpression")) {
|
|
1197
|
+
const body = stripParenExpression(functionNode.body);
|
|
1198
|
+
if (!isNodeOfType(body, "BlockStatement")) {
|
|
1199
|
+
markGeneratedImageExpression(body, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const body = functionNode.body;
|
|
1204
|
+
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
1205
|
+
walkAst(body, (descendantNode) => {
|
|
1206
|
+
if (descendantNode !== body && isFunctionLike$3(descendantNode)) return false;
|
|
1207
|
+
if (!isNodeOfType(descendantNode, "ReturnStatement")) return;
|
|
1208
|
+
if (!descendantNode.argument) return;
|
|
1209
|
+
markGeneratedImageExpression(stripParenExpression(descendantNode.argument), programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1210
|
+
});
|
|
1211
|
+
};
|
|
1212
|
+
const hasNormalJsxUsage = (programRoot, componentName, generatedImageJsxNodes) => {
|
|
1213
|
+
let hasNormalUsage = false;
|
|
1214
|
+
walkAst(programRoot, (descendantNode) => {
|
|
1215
|
+
if (hasNormalUsage) return false;
|
|
1216
|
+
if (!isNodeOfType(descendantNode, "JSXOpeningElement")) return;
|
|
1217
|
+
if (generatedImageJsxNodes.has(descendantNode)) return;
|
|
1218
|
+
if (flattenJsxName$1(descendantNode.name) !== componentName) return;
|
|
1219
|
+
hasNormalUsage = true;
|
|
1220
|
+
return false;
|
|
1221
|
+
});
|
|
1222
|
+
return hasNormalUsage;
|
|
1223
|
+
};
|
|
1224
|
+
const markComponentRenderJsx = (programRoot, openingElement, generatedImageJsxNodes, visitedComponentNames) => {
|
|
1225
|
+
const tagName = flattenJsxName$1(openingElement.name);
|
|
1226
|
+
if (!tagName || tagName.includes(".") || !isComponentIdentifierName(tagName)) return;
|
|
1227
|
+
if (visitedComponentNames.has(tagName)) return;
|
|
1228
|
+
if (hasNormalJsxUsage(programRoot, tagName, generatedImageJsxNodes)) return;
|
|
1229
|
+
const binding = findVariableInitializer(openingElement, tagName);
|
|
1230
|
+
if (!binding?.initializer) return;
|
|
1231
|
+
visitedComponentNames.add(tagName);
|
|
1232
|
+
markGeneratedImageExpression(stripParenExpression(binding.initializer), programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1233
|
+
};
|
|
1234
|
+
const isInsideGeneratedImageRendererArgument = (node) => {
|
|
1235
|
+
let cursor = node.parent;
|
|
1236
|
+
while (cursor) {
|
|
1237
|
+
if (isGeneratedImageRendererCall(cursor)) return true;
|
|
1238
|
+
cursor = cursor.parent ?? null;
|
|
1239
|
+
}
|
|
1240
|
+
return false;
|
|
1241
|
+
};
|
|
1242
|
+
const hasNormalFunctionCallUsage = (programRoot, functionName) => {
|
|
1243
|
+
let hasNormalUsage = false;
|
|
1244
|
+
walkAst(programRoot, (descendantNode) => {
|
|
1245
|
+
if (hasNormalUsage) return false;
|
|
1246
|
+
if (!isNodeOfType(descendantNode, "CallExpression")) return;
|
|
1247
|
+
if (!isNodeOfType(descendantNode.callee, "Identifier")) return;
|
|
1248
|
+
if (descendantNode.callee.name !== functionName) return;
|
|
1249
|
+
if (isInsideGeneratedImageRendererArgument(descendantNode)) return;
|
|
1250
|
+
hasNormalUsage = true;
|
|
1251
|
+
return false;
|
|
1252
|
+
});
|
|
1253
|
+
return hasNormalUsage;
|
|
1254
|
+
};
|
|
1255
|
+
const markJsxSubtree = (node, programRoot, generatedImageJsxNodes, visitedComponentNames) => {
|
|
1256
|
+
walkAst(node, (descendantNode) => {
|
|
1257
|
+
if (!isNodeOfType(descendantNode, "JSXOpeningElement")) return;
|
|
1258
|
+
generatedImageJsxNodes.add(descendantNode);
|
|
1259
|
+
markComponentRenderJsx(programRoot, descendantNode, generatedImageJsxNodes, visitedComponentNames);
|
|
1260
|
+
});
|
|
1261
|
+
};
|
|
1262
|
+
const markGeneratedImageExpression = (expression, programRoot, generatedImageJsxNodes, visitedComponentNames) => {
|
|
1263
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
1264
|
+
if (isNodeOfType(unwrappedExpression, "JSXElement") || isNodeOfType(unwrappedExpression, "JSXFragment")) {
|
|
1265
|
+
markJsxSubtree(unwrappedExpression, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (isFunctionLike$3(unwrappedExpression)) {
|
|
1269
|
+
markFunctionReturnJsx(unwrappedExpression, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
|
|
1273
|
+
markGeneratedImageExpression(unwrappedExpression.consequent, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1274
|
+
markGeneratedImageExpression(unwrappedExpression.alternate, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
1278
|
+
markGeneratedImageExpression(unwrappedExpression.left, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1279
|
+
markGeneratedImageExpression(unwrappedExpression.right, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
1283
|
+
const callee = unwrappedExpression.callee;
|
|
1284
|
+
if (isFunctionLike$3(callee)) {
|
|
1285
|
+
markFunctionReturnJsx(callee, programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (!isNodeOfType(callee, "Identifier")) return;
|
|
1289
|
+
if (visitedComponentNames.has(callee.name)) return;
|
|
1290
|
+
if (hasNormalJsxUsage(programRoot, callee.name, generatedImageJsxNodes)) return;
|
|
1291
|
+
if (hasNormalFunctionCallUsage(programRoot, callee.name)) return;
|
|
1292
|
+
const binding = findVariableInitializer(callee, callee.name);
|
|
1293
|
+
if (!binding?.initializer || !isFunctionLike$3(stripParenExpression(binding.initializer))) return;
|
|
1294
|
+
visitedComponentNames.add(callee.name);
|
|
1295
|
+
markFunctionReturnJsx(stripParenExpression(binding.initializer), programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
1299
|
+
if (visitedComponentNames.has(unwrappedExpression.name)) return;
|
|
1300
|
+
visitedComponentNames.add(unwrappedExpression.name);
|
|
1301
|
+
const binding = findVariableInitializer(unwrappedExpression, unwrappedExpression.name);
|
|
1302
|
+
if (!binding?.initializer) return;
|
|
1303
|
+
markGeneratedImageExpression(stripParenExpression(binding.initializer), programRoot, generatedImageJsxNodes, visitedComponentNames);
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
const collectGeneratedImageJsxNodes = (programRoot) => {
|
|
1307
|
+
const cached = generatedImageJsxCache.get(programRoot);
|
|
1308
|
+
if (cached) return cached;
|
|
1309
|
+
const generatedImageJsxNodes = /* @__PURE__ */ new WeakSet();
|
|
1310
|
+
walkAst(programRoot, (descendantNode) => {
|
|
1311
|
+
if (!isGeneratedImageRendererCall(descendantNode)) return;
|
|
1312
|
+
for (const argument of descendantNode.arguments) markGeneratedImageExpression(argument, programRoot, generatedImageJsxNodes, /* @__PURE__ */ new Set());
|
|
1313
|
+
});
|
|
1314
|
+
generatedImageJsxCache.set(programRoot, generatedImageJsxNodes);
|
|
1315
|
+
return generatedImageJsxNodes;
|
|
1316
|
+
};
|
|
1317
|
+
const isGeneratedImageRenderContext = (context, node) => {
|
|
1318
|
+
if (isGeneratedImageRenderFilename(context.filename)) return true;
|
|
1319
|
+
if (!node) return false;
|
|
1320
|
+
const programRoot = findProgramRoot(node);
|
|
1321
|
+
if (!programRoot) return false;
|
|
1322
|
+
return collectGeneratedImageJsxNodes(programRoot).has(node);
|
|
1323
|
+
};
|
|
1324
|
+
//#endregion
|
|
835
1325
|
//#region src/plugin/utils/is-hidden-from-screen-reader.ts
|
|
836
1326
|
const isHiddenFromScreenReader = (openingElement, settings) => {
|
|
837
1327
|
if (getElementType(openingElement, settings).toLowerCase() === "input") {
|
|
@@ -887,7 +1377,7 @@ const PREFER_ALT = "Screen readers skip a decorative image more reliably with `a
|
|
|
887
1377
|
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
1378
|
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
1379
|
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$
|
|
1380
|
+
const resolveSettings$52 = (settings) => {
|
|
891
1381
|
const reactDoctor = settings?.["react-doctor"];
|
|
892
1382
|
return typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.altText ?? {} : {};
|
|
893
1383
|
};
|
|
@@ -1003,8 +1493,8 @@ const altText = defineRule({
|
|
|
1003
1493
|
recommendation: "Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",
|
|
1004
1494
|
category: "Accessibility",
|
|
1005
1495
|
create: (context) => {
|
|
1006
|
-
if (
|
|
1007
|
-
const settings = resolveSettings$
|
|
1496
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
1497
|
+
const settings = resolveSettings$52(context.settings);
|
|
1008
1498
|
const checkImg = !settings.elements || settings.elements.includes("img");
|
|
1009
1499
|
const checkObject = !settings.elements || settings.elements.includes("object");
|
|
1010
1500
|
const checkArea = !settings.elements || settings.elements.includes("area");
|
|
@@ -1014,6 +1504,7 @@ const altText = defineRule({
|
|
|
1014
1504
|
const areaAliases = new Set(settings.area ?? []);
|
|
1015
1505
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
1016
1506
|
return { JSXOpeningElement(node) {
|
|
1507
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
1017
1508
|
const tag = getElementType(node, context.settings);
|
|
1018
1509
|
if (checkImg && (tag === "img" || imgAliases.has(tag))) {
|
|
1019
1510
|
imgRule(node, node, context);
|
|
@@ -1048,7 +1539,7 @@ const DEFAULT_AMBIGUOUS = [
|
|
|
1048
1539
|
"a link",
|
|
1049
1540
|
"learn more"
|
|
1050
1541
|
];
|
|
1051
|
-
const resolveSettings$
|
|
1542
|
+
const resolveSettings$51 = (settings) => {
|
|
1052
1543
|
const reactDoctor = settings?.["react-doctor"];
|
|
1053
1544
|
return { words: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorAmbiguousText ?? {} : {}).words ?? DEFAULT_AMBIGUOUS };
|
|
1054
1545
|
};
|
|
@@ -1089,7 +1580,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1089
1580
|
recommendation: "Name where a link goes. Avoid 'click here', 'learn more', and 'link'.",
|
|
1090
1581
|
category: "Accessibility",
|
|
1091
1582
|
create: (context) => {
|
|
1092
|
-
const settings = resolveSettings$
|
|
1583
|
+
const settings = resolveSettings$51(context.settings);
|
|
1093
1584
|
const ambiguousSet = new Set(settings.words.map((word) => word.toLowerCase()));
|
|
1094
1585
|
return { JSXElement(node) {
|
|
1095
1586
|
if (getElementType(node.openingElement, context.settings) !== "a") return;
|
|
@@ -1139,7 +1630,7 @@ const getStaticTemplateLiteralValue = (templateLiteral) => {
|
|
|
1139
1630
|
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
1631
|
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
1632
|
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$
|
|
1633
|
+
const resolveSettings$50 = (settings) => {
|
|
1143
1634
|
const reactDoctor = settings?.["react-doctor"];
|
|
1144
1635
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorIsValid ?? {} : {};
|
|
1145
1636
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
@@ -1178,7 +1669,7 @@ const anchorIsValid = defineRule({
|
|
|
1178
1669
|
recommendation: "Give links a real destination. Use `<button>` for in-page actions.",
|
|
1179
1670
|
category: "Accessibility",
|
|
1180
1671
|
create: (context) => {
|
|
1181
|
-
const settings = resolveSettings$
|
|
1672
|
+
const settings = resolveSettings$50(context.settings);
|
|
1182
1673
|
return { JSXOpeningElement(node) {
|
|
1183
1674
|
if (getElementType(node, context.settings) !== "a") return;
|
|
1184
1675
|
let hrefAttribute;
|
|
@@ -2213,7 +2704,7 @@ const PRESENTATION_ROLES$2 = new Set(["presentation", "none"]);
|
|
|
2213
2704
|
//#endregion
|
|
2214
2705
|
//#region src/plugin/rules/a11y/aria-role.ts
|
|
2215
2706
|
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$
|
|
2707
|
+
const resolveSettings$49 = (settings) => {
|
|
2217
2708
|
const reactDoctor = settings?.["react-doctor"];
|
|
2218
2709
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.ariaRole ?? {} : {};
|
|
2219
2710
|
return {
|
|
@@ -2229,7 +2720,7 @@ const ariaRole = defineRule({
|
|
|
2229
2720
|
recommendation: "Use a real, non-abstract ARIA role.",
|
|
2230
2721
|
category: "Accessibility",
|
|
2231
2722
|
create: (context) => {
|
|
2232
|
-
const settings = resolveSettings$
|
|
2723
|
+
const settings = resolveSettings$49(context.settings);
|
|
2233
2724
|
return { JSXOpeningElement(node) {
|
|
2234
2725
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
2235
2726
|
if (!roleAttribute) return;
|
|
@@ -2330,7 +2821,7 @@ const LOOP_TYPES = [
|
|
|
2330
2821
|
"WhileStatement",
|
|
2331
2822
|
"DoWhileStatement"
|
|
2332
2823
|
];
|
|
2333
|
-
const FUNCTION_LIKE_TYPES
|
|
2824
|
+
const FUNCTION_LIKE_TYPES = new Set([
|
|
2334
2825
|
"FunctionDeclaration",
|
|
2335
2826
|
"FunctionExpression",
|
|
2336
2827
|
"ArrowFunctionExpression"
|
|
@@ -3255,7 +3746,7 @@ const FORM_CONTROL_TAGS = new Set([
|
|
|
3255
3746
|
"select",
|
|
3256
3747
|
"form"
|
|
3257
3748
|
]);
|
|
3258
|
-
const resolveSettings$
|
|
3749
|
+
const resolveSettings$48 = (settings) => {
|
|
3259
3750
|
const reactDoctor = settings?.["react-doctor"];
|
|
3260
3751
|
return { inputComponents: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {}).inputComponents ?? [] };
|
|
3261
3752
|
};
|
|
@@ -3267,7 +3758,7 @@ const autocompleteValid = defineRule({
|
|
|
3267
3758
|
recommendation: "Use a valid autofill token in `autoComplete`.",
|
|
3268
3759
|
category: "Accessibility",
|
|
3269
3760
|
create: (context) => {
|
|
3270
|
-
const settings = resolveSettings$
|
|
3761
|
+
const settings = resolveSettings$48(context.settings);
|
|
3271
3762
|
return { JSXOpeningElement: (node) => {
|
|
3272
3763
|
const tag = getElementType(node, context.settings);
|
|
3273
3764
|
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.includes(tag)) return;
|
|
@@ -3320,7 +3811,7 @@ const isCreateElementCall = (node) => {
|
|
|
3320
3811
|
//#region src/plugin/rules/react-builtins/button-has-type.ts
|
|
3321
3812
|
const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
|
|
3322
3813
|
const INVALID_MESSAGE = "This button's `type` is invalid.";
|
|
3323
|
-
const resolveSettings$
|
|
3814
|
+
const resolveSettings$47 = (settings) => {
|
|
3324
3815
|
const reactDoctor = settings?.["react-doctor"];
|
|
3325
3816
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.buttonHasType ?? {} : {};
|
|
3326
3817
|
return {
|
|
@@ -3362,7 +3853,7 @@ const buttonHasType = defineRule({
|
|
|
3362
3853
|
severity: "warn",
|
|
3363
3854
|
recommendation: "Always set a `type` on a `<button>`: `type=\"button\"`, `\"submit\"`, or `\"reset\"`.",
|
|
3364
3855
|
create: (context) => {
|
|
3365
|
-
const settings = resolveSettings$
|
|
3856
|
+
const settings = resolveSettings$47(context.settings);
|
|
3366
3857
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3367
3858
|
return {
|
|
3368
3859
|
JSXOpeningElement(node) {
|
|
@@ -3431,7 +3922,7 @@ const buttonHasType = defineRule({
|
|
|
3431
3922
|
//#region src/plugin/rules/react-builtins/checked-requires-onchange-or-readonly.ts
|
|
3432
3923
|
const MISSING_MESSAGE$1 = "Your users can't toggle this input because `checked` has no `onChange`.";
|
|
3433
3924
|
const EXCLUSIVE_MESSAGE = "This input behaves unpredictably with both `checked` & `defaultChecked` set.";
|
|
3434
|
-
const resolveSettings$
|
|
3925
|
+
const resolveSettings$46 = (settings) => {
|
|
3435
3926
|
const reactDoctor = settings?.["react-doctor"];
|
|
3436
3927
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.checkedRequiresOnchangeOrReadonly ?? {} : {};
|
|
3437
3928
|
return {
|
|
@@ -3484,7 +3975,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
3484
3975
|
recommendation: "Add `onChange` (controlled) or `readOnly` (display-only), or use `defaultChecked` for an uncontrolled checkbox.",
|
|
3485
3976
|
category: "Correctness",
|
|
3486
3977
|
create: (context) => {
|
|
3487
|
-
const settings = resolveSettings$
|
|
3978
|
+
const settings = resolveSettings$46(context.settings);
|
|
3488
3979
|
const reportFromPresence = (presence) => {
|
|
3489
3980
|
if (presence.checkedNode && presence.defaultCheckedNode && !settings.ignoreExclusiveCheckedAttribute) context.report({
|
|
3490
3981
|
node: presence.checkedNode,
|
|
@@ -3628,9 +4119,6 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
3628
4119
|
} })
|
|
3629
4120
|
});
|
|
3630
4121
|
//#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
4122
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
3635
4123
|
const clientPassiveEventListeners = defineRule({
|
|
3636
4124
|
id: "client-passive-event-listeners",
|
|
@@ -3670,31 +4158,6 @@ const isReactComponentName = (name) => {
|
|
|
3670
4158
|
return firstCharacter >= 65 && firstCharacter <= 90;
|
|
3671
4159
|
};
|
|
3672
4160
|
//#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
4161
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
3699
4162
|
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
4163
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
@@ -3708,7 +4171,7 @@ const HTML_FOR_ATTRIBUTE = "htmlFor";
|
|
|
3708
4171
|
const LABEL_ELEMENT = "label";
|
|
3709
4172
|
const DEFAULT_DEPTH = 5;
|
|
3710
4173
|
const MAX_DEPTH = 25;
|
|
3711
|
-
const resolveSettings$
|
|
4174
|
+
const resolveSettings$45 = (settings) => {
|
|
3712
4175
|
const reactDoctor = settings?.["react-doctor"];
|
|
3713
4176
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.controlHasAssociatedLabel ?? {} : {};
|
|
3714
4177
|
return {
|
|
@@ -3828,7 +4291,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3828
4291
|
recommendation: "Give every interactive control a label screen readers can read.",
|
|
3829
4292
|
category: "Accessibility",
|
|
3830
4293
|
create: (context) => {
|
|
3831
|
-
const settings = resolveSettings$
|
|
4294
|
+
const settings = resolveSettings$45(context.settings);
|
|
3832
4295
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3833
4296
|
return { JSXElement(node) {
|
|
3834
4297
|
if (isTestlikeFile) return;
|
|
@@ -4199,7 +4662,7 @@ const DEFAULT_ADDITIONAL_HOCS = [
|
|
|
4199
4662
|
"lazy",
|
|
4200
4663
|
"withTracking"
|
|
4201
4664
|
];
|
|
4202
|
-
const resolveSettings$
|
|
4665
|
+
const resolveSettings$44 = (settings) => {
|
|
4203
4666
|
const reactDoctor = settings?.["react-doctor"];
|
|
4204
4667
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
|
|
4205
4668
|
const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
|
|
@@ -4391,7 +4854,7 @@ const displayName = defineRule({
|
|
|
4391
4854
|
recommendation: "Give each component a `displayName` so DevTools shows a clear name.",
|
|
4392
4855
|
category: "Architecture",
|
|
4393
4856
|
create: (context) => {
|
|
4394
|
-
const settings = resolveSettings$
|
|
4857
|
+
const settings = resolveSettings$44(context.settings);
|
|
4395
4858
|
const ignoreNamed = settings.ignoreTranspilerName ? false : true;
|
|
4396
4859
|
const reportAt = (node) => {
|
|
4397
4860
|
context.report({
|
|
@@ -6263,7 +6726,7 @@ const compileGlob = (pattern) => {
|
|
|
6263
6726
|
//#endregion
|
|
6264
6727
|
//#region src/plugin/rules/react-builtins/forbid-component-props.ts
|
|
6265
6728
|
const DEFAULT_FORBID_PROPS = ["className", "style"];
|
|
6266
|
-
const resolveSettings$
|
|
6729
|
+
const resolveSettings$43 = (settings) => {
|
|
6267
6730
|
const reactDoctor = settings?.["react-doctor"];
|
|
6268
6731
|
const forbid = (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidComponentProps ?? {} : {}).forbid;
|
|
6269
6732
|
if (!forbid || forbid.length === 0) return { forbid: DEFAULT_FORBID_PROPS };
|
|
@@ -6301,9 +6764,9 @@ const isForbiddenForTag = (entry, tag) => {
|
|
|
6301
6764
|
if (entry.allowedForPatterns.some((regex) => regex.test(tag))) return false;
|
|
6302
6765
|
return true;
|
|
6303
6766
|
};
|
|
6304
|
-
const flattenJsxName
|
|
6767
|
+
const flattenJsxName = (name) => {
|
|
6305
6768
|
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
6306
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName
|
|
6769
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName(name.object)}.${name.property.name}`;
|
|
6307
6770
|
if (name.type === "ThisExpression" || name.type === "JSXThisExpression") return "this";
|
|
6308
6771
|
return "";
|
|
6309
6772
|
};
|
|
@@ -6317,11 +6780,11 @@ const forbidComponentProps = defineRule({
|
|
|
6317
6780
|
recommendation: "List the props you want to block per component in the `forbidComponentProps.forbid` setting.",
|
|
6318
6781
|
category: "Architecture",
|
|
6319
6782
|
create: (context) => {
|
|
6320
|
-
const entries = resolveSettings$
|
|
6783
|
+
const entries = resolveSettings$43(context.settings).forbid.map(normalizeEntry);
|
|
6321
6784
|
return { JSXOpeningElement(node) {
|
|
6322
6785
|
if (entries.length === 0) return;
|
|
6323
6786
|
if (!isSupportedJsxName(node.name)) return;
|
|
6324
|
-
const tag = flattenJsxName
|
|
6787
|
+
const tag = flattenJsxName(node.name);
|
|
6325
6788
|
if (!tag) return;
|
|
6326
6789
|
if (!(isReactComponentName(tag.split(".")[0]) || tag.includes("."))) return;
|
|
6327
6790
|
for (const attribute of node.attributes) {
|
|
@@ -6346,7 +6809,7 @@ const forbidComponentProps = defineRule({
|
|
|
6346
6809
|
//#endregion
|
|
6347
6810
|
//#region src/plugin/rules/react-builtins/forbid-dom-props.ts
|
|
6348
6811
|
const buildMessage$23 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags.`;
|
|
6349
|
-
const resolveSettings$
|
|
6812
|
+
const resolveSettings$42 = (settings) => {
|
|
6350
6813
|
const reactDoctor = settings?.["react-doctor"];
|
|
6351
6814
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
|
|
6352
6815
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -6368,7 +6831,7 @@ const forbidDomProps = defineRule({
|
|
|
6368
6831
|
recommendation: "List the HTML props you want to block in the `forbidDomProps.forbid` setting.",
|
|
6369
6832
|
category: "Architecture",
|
|
6370
6833
|
create: (context) => {
|
|
6371
|
-
const forbidMap = resolveSettings$
|
|
6834
|
+
const forbidMap = resolveSettings$42(context.settings);
|
|
6372
6835
|
return { JSXOpeningElement(node) {
|
|
6373
6836
|
if (forbidMap.size === 0) return;
|
|
6374
6837
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -6416,34 +6879,6 @@ const flattenCalleeName = (callee) => {
|
|
|
6416
6879
|
return null;
|
|
6417
6880
|
};
|
|
6418
6881
|
//#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
6882
|
//#region src/plugin/utils/is-react-function-call.ts
|
|
6448
6883
|
const PRAGMA = "React";
|
|
6449
6884
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
@@ -6455,7 +6890,7 @@ const isReactFunctionCall = (node, expectedCall) => {
|
|
|
6455
6890
|
//#endregion
|
|
6456
6891
|
//#region src/plugin/rules/react-builtins/forbid-elements.ts
|
|
6457
6892
|
const buildMessage$22 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here.`;
|
|
6458
|
-
const resolveSettings$
|
|
6893
|
+
const resolveSettings$41 = (settings) => {
|
|
6459
6894
|
const reactDoctor = settings?.["react-doctor"];
|
|
6460
6895
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
|
|
6461
6896
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -6471,11 +6906,11 @@ const forbidElements = defineRule({
|
|
|
6471
6906
|
recommendation: "List the element names you want to block in the `forbidElements.forbid` setting.",
|
|
6472
6907
|
category: "Architecture",
|
|
6473
6908
|
create: (context) => {
|
|
6474
|
-
const forbidMap = resolveSettings$
|
|
6909
|
+
const forbidMap = resolveSettings$41(context.settings);
|
|
6475
6910
|
return {
|
|
6476
6911
|
JSXOpeningElement(node) {
|
|
6477
6912
|
if (forbidMap.size === 0) return;
|
|
6478
|
-
const fullName = flattenJsxName(node.name);
|
|
6913
|
+
const fullName = flattenJsxName$1(node.name);
|
|
6479
6914
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
6480
6915
|
context.report({
|
|
6481
6916
|
node: node.name,
|
|
@@ -6542,7 +6977,7 @@ const DEFAULT_HEADING_TAGS = [
|
|
|
6542
6977
|
"h5",
|
|
6543
6978
|
"h6"
|
|
6544
6979
|
];
|
|
6545
|
-
const resolveSettings$
|
|
6980
|
+
const resolveSettings$40 = (settings) => {
|
|
6546
6981
|
const reactDoctor = settings?.["react-doctor"];
|
|
6547
6982
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
|
|
6548
6983
|
return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
|
|
@@ -6555,7 +6990,7 @@ const headingHasContent = defineRule({
|
|
|
6555
6990
|
recommendation: "Put readable text in every heading.",
|
|
6556
6991
|
category: "Accessibility",
|
|
6557
6992
|
create: (context) => {
|
|
6558
|
-
const settings = resolveSettings$
|
|
6993
|
+
const settings = resolveSettings$40(context.settings);
|
|
6559
6994
|
return { JSXOpeningElement(node) {
|
|
6560
6995
|
const elementType = getElementType(node, context.settings);
|
|
6561
6996
|
if (!settings.headingTags.includes(elementType)) return;
|
|
@@ -6575,7 +7010,7 @@ const headingHasContent = defineRule({
|
|
|
6575
7010
|
//#region src/plugin/rules/react-builtins/hook-use-state.ts
|
|
6576
7011
|
const REQUIRE_DESTRUCTURE_MESSAGE = "This `useState` result is hard to follow.";
|
|
6577
7012
|
const NAMING_CONVENTION_MESSAGE = "The setter here is hard to spot.";
|
|
6578
|
-
const resolveSettings$
|
|
7013
|
+
const resolveSettings$39 = (settings) => {
|
|
6579
7014
|
const reactDoctor = settings?.["react-doctor"];
|
|
6580
7015
|
return { allowDestructuredState: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.hookUseState ?? {} : {}).allowDestructuredState ?? false };
|
|
6581
7016
|
};
|
|
@@ -6602,7 +7037,7 @@ const hookUseState = defineRule({
|
|
|
6602
7037
|
recommendation: "Destructure useState as `const [thing, setThing] = useState(…)`.",
|
|
6603
7038
|
category: "Architecture",
|
|
6604
7039
|
create: (context) => {
|
|
6605
|
-
const { allowDestructuredState } = resolveSettings$
|
|
7040
|
+
const { allowDestructuredState } = resolveSettings$39(context.settings);
|
|
6606
7041
|
return { CallExpression(node) {
|
|
6607
7042
|
if (!isReactFunctionCall(node, "useState")) return;
|
|
6608
7043
|
const parent = node.parent;
|
|
@@ -6705,7 +7140,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
6705
7140
|
//#endregion
|
|
6706
7141
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
6707
7142
|
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$
|
|
7143
|
+
const resolveSettings$38 = (settings) => {
|
|
6709
7144
|
const reactDoctor = settings?.["react-doctor"];
|
|
6710
7145
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
6711
7146
|
};
|
|
@@ -6742,7 +7177,7 @@ const htmlHasLang = defineRule({
|
|
|
6742
7177
|
recommendation: "Set `<html lang=\"…\">` so screen readers know the page language.",
|
|
6743
7178
|
category: "Accessibility",
|
|
6744
7179
|
create: (context) => {
|
|
6745
|
-
const settings = resolveSettings$
|
|
7180
|
+
const settings = resolveSettings$38(context.settings);
|
|
6746
7181
|
const tagSet = new Set(settings.htmlTags);
|
|
6747
7182
|
return { JSXOpeningElement(node) {
|
|
6748
7183
|
const tag = getElementType(node, context.settings);
|
|
@@ -7138,7 +7573,7 @@ const DEFAULT_REDUNDANT_WORDS = [
|
|
|
7138
7573
|
"photo",
|
|
7139
7574
|
"picture"
|
|
7140
7575
|
];
|
|
7141
|
-
const resolveSettings$
|
|
7576
|
+
const resolveSettings$37 = (settings) => {
|
|
7142
7577
|
const reactDoctor = settings?.["react-doctor"];
|
|
7143
7578
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.imgRedundantAlt ?? {} : {};
|
|
7144
7579
|
return {
|
|
@@ -7185,8 +7620,10 @@ const imgRedundantAlt = defineRule({
|
|
|
7185
7620
|
recommendation: "Do not put 'image' or 'photo' in alt text. Describe what is shown.",
|
|
7186
7621
|
category: "Accessibility",
|
|
7187
7622
|
create: (context) => {
|
|
7188
|
-
|
|
7623
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
7624
|
+
const settings = resolveSettings$37(context.settings);
|
|
7189
7625
|
return { JSXOpeningElement(node) {
|
|
7626
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
7190
7627
|
const tag = getElementType(node, context.settings);
|
|
7191
7628
|
if (!settings.components.includes(tag)) return;
|
|
7192
7629
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
@@ -7275,7 +7712,7 @@ const DEFAULT_TABBABLE_ROLES = [
|
|
|
7275
7712
|
"switch",
|
|
7276
7713
|
"textbox"
|
|
7277
7714
|
];
|
|
7278
|
-
const resolveSettings$
|
|
7715
|
+
const resolveSettings$36 = (settings) => {
|
|
7279
7716
|
const reactDoctor = settings?.["react-doctor"];
|
|
7280
7717
|
return { tabbable: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.interactiveSupportsFocus ?? {} : {}).tabbable ?? DEFAULT_TABBABLE_ROLES };
|
|
7281
7718
|
};
|
|
@@ -7287,7 +7724,7 @@ const interactiveSupportsFocus = defineRule({
|
|
|
7287
7724
|
recommendation: "Add `tabIndex` to elements with interactive roles and handlers.",
|
|
7288
7725
|
category: "Accessibility",
|
|
7289
7726
|
create: (context) => {
|
|
7290
|
-
const settings = resolveSettings$
|
|
7727
|
+
const settings = resolveSettings$36(context.settings);
|
|
7291
7728
|
const tabbableSet = new Set(settings.tabbable);
|
|
7292
7729
|
return { JSXOpeningElement(node) {
|
|
7293
7730
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -7308,88 +7745,6 @@ const interactiveSupportsFocus = defineRule({
|
|
|
7308
7745
|
}
|
|
7309
7746
|
});
|
|
7310
7747
|
//#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
7748
|
//#region src/plugin/rules/jotai/jotai-derived-atom-returns-fresh-object.ts
|
|
7394
7749
|
const isAtomFromJotai = (callExpression) => {
|
|
7395
7750
|
if (!isNodeOfType(callExpression.callee, "Identifier")) return false;
|
|
@@ -8585,7 +8940,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
8585
8940
|
const NEVER_MESSAGE$2 = () => `This prop is written inconsistently.`;
|
|
8586
8941
|
const ALWAYS_MESSAGE$2 = () => `This prop is written inconsistently.`;
|
|
8587
8942
|
const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing.`;
|
|
8588
|
-
const resolveSettings$
|
|
8943
|
+
const resolveSettings$35 = (settings) => {
|
|
8589
8944
|
const reactDoctor = settings?.["react-doctor"];
|
|
8590
8945
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxBooleanValue ?? {} : {};
|
|
8591
8946
|
return {
|
|
@@ -8603,7 +8958,7 @@ const jsxBooleanValue = defineRule({
|
|
|
8603
8958
|
recommendation: "Pick a single boolean-attribute style across the codebase (default: omit `={true}`).",
|
|
8604
8959
|
category: "Architecture",
|
|
8605
8960
|
create: (context) => {
|
|
8606
|
-
const settings = resolveSettings$
|
|
8961
|
+
const settings = resolveSettings$35(context.settings);
|
|
8607
8962
|
const alwaysSet = new Set(settings.always);
|
|
8608
8963
|
const neverSet = new Set(settings.never);
|
|
8609
8964
|
return { JSXAttribute(node) {
|
|
@@ -8657,7 +9012,7 @@ const jsxBooleanValue = defineRule({
|
|
|
8657
9012
|
const UNNECESSARY_BRACES_MESSAGE = "These curly braces do nothing here.";
|
|
8658
9013
|
const REQUIRED_BRACES_MESSAGE = "This value needs curly braces `{ }` to read as an expression.";
|
|
8659
9014
|
const isAllowedMode = (value) => value === "always" || value === "never" || value === "ignore";
|
|
8660
|
-
const resolveSettings$
|
|
9015
|
+
const resolveSettings$34 = (settings) => {
|
|
8661
9016
|
const reactDoctor = settings?.["react-doctor"];
|
|
8662
9017
|
const ruleSettingsRaw = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxCurlyBracePresence : void 0;
|
|
8663
9018
|
if (isAllowedMode(ruleSettingsRaw)) return {
|
|
@@ -8753,7 +9108,7 @@ const jsxCurlyBracePresence = defineRule({
|
|
|
8753
9108
|
recommendation: "Pick a consistent quoting style for JSX literal values.",
|
|
8754
9109
|
category: "Architecture",
|
|
8755
9110
|
create: (context) => {
|
|
8756
|
-
const settings = resolveSettings$
|
|
9111
|
+
const settings = resolveSettings$34(context.settings);
|
|
8757
9112
|
return {
|
|
8758
9113
|
JSXAttribute(node) {
|
|
8759
9114
|
const value = node.value;
|
|
@@ -8836,7 +9191,7 @@ const containsJsxElement = (root) => {
|
|
|
8836
9191
|
//#region src/plugin/rules/react-builtins/jsx-filename-extension.ts
|
|
8837
9192
|
const JSX_NOT_ALLOWED = (extension) => `This file has JSX but a \`${extension}\` name.`;
|
|
8838
9193
|
const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are meant for JSX, but this one has none.`;
|
|
8839
|
-
const resolveSettings$
|
|
9194
|
+
const resolveSettings$33 = (settings) => {
|
|
8840
9195
|
const reactDoctor = settings?.["react-doctor"];
|
|
8841
9196
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFilenameExtension ?? {} : {};
|
|
8842
9197
|
return {
|
|
@@ -8858,7 +9213,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
8858
9213
|
recommendation: "Name files with JSX `.jsx` or `.tsx`, or whatever extension your project uses.",
|
|
8859
9214
|
category: "Architecture",
|
|
8860
9215
|
create: (context) => {
|
|
8861
|
-
const settings = resolveSettings$
|
|
9216
|
+
const settings = resolveSettings$33(context.settings);
|
|
8862
9217
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
8863
9218
|
const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
|
|
8864
9219
|
const extensionOnly = path.extname(filename).slice(1);
|
|
@@ -8912,7 +9267,7 @@ const isJsxFragmentElement = (node) => {
|
|
|
8912
9267
|
//#region src/plugin/rules/react-builtins/jsx-fragments.ts
|
|
8913
9268
|
const SYNTAX_MESSAGE = "This fragment is written inconsistently.";
|
|
8914
9269
|
const ELEMENT_MESSAGE = "This fragment is written inconsistently.";
|
|
8915
|
-
const resolveSettings$
|
|
9270
|
+
const resolveSettings$32 = (settings) => {
|
|
8916
9271
|
const reactDoctor = settings?.["react-doctor"];
|
|
8917
9272
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFragments ?? {} : {}).mode ?? "syntax" };
|
|
8918
9273
|
};
|
|
@@ -8924,7 +9279,7 @@ const jsxFragments = defineRule({
|
|
|
8924
9279
|
recommendation: "Pick one fragment style across the codebase.",
|
|
8925
9280
|
category: "Architecture",
|
|
8926
9281
|
create: (context) => {
|
|
8927
|
-
const { mode } = resolveSettings$
|
|
9282
|
+
const { mode } = resolveSettings$32(context.settings);
|
|
8928
9283
|
return {
|
|
8929
9284
|
JSXElement(node) {
|
|
8930
9285
|
if (mode !== "syntax") return;
|
|
@@ -8968,7 +9323,7 @@ const buildPropRegex = (handlerPropPrefix) => {
|
|
|
8968
9323
|
const escaped = tokens.map((token) => token.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
8969
9324
|
return new RegExp(`^(${escaped})[A-Z].*$`);
|
|
8970
9325
|
};
|
|
8971
|
-
const resolveSettings$
|
|
9326
|
+
const resolveSettings$31 = (settings) => {
|
|
8972
9327
|
const reactDoctor = settings?.["react-doctor"];
|
|
8973
9328
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxHandlerNames ?? {} : {};
|
|
8974
9329
|
let handlerPrefix = DEFAULT_HANDLER_PREFIX;
|
|
@@ -9044,7 +9399,7 @@ const jsxHandlerNames = defineRule({
|
|
|
9044
9399
|
recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers.",
|
|
9045
9400
|
category: "Architecture",
|
|
9046
9401
|
create: (context) => {
|
|
9047
|
-
const settings = resolveSettings$
|
|
9402
|
+
const settings = resolveSettings$31(context.settings);
|
|
9048
9403
|
return { JSXAttribute(node) {
|
|
9049
9404
|
if (settings.ignoreComponentNames.length > 0) {
|
|
9050
9405
|
const opening = node.parent;
|
|
@@ -9129,7 +9484,7 @@ const MISSING_KEY_ARRAY = "Your users can see the wrong data when this array reo
|
|
|
9129
9484
|
const MISSING_KEY_ITERATOR = "Your users can see the wrong data when this list reorders.";
|
|
9130
9485
|
const KEY_BEFORE_SPREAD = "The `{...spread}` can overwrite this `key` & break React's tracking.";
|
|
9131
9486
|
const DUPLICATE_KEY = (keyValue) => `Your users can see the wrong data because two elements share the key "${keyValue}".`;
|
|
9132
|
-
const resolveSettings$
|
|
9487
|
+
const resolveSettings$30 = (settings) => {
|
|
9133
9488
|
const reactDoctor = settings?.["react-doctor"];
|
|
9134
9489
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxKey ?? {} : {};
|
|
9135
9490
|
return {
|
|
@@ -9277,7 +9632,7 @@ const jsxKey = defineRule({
|
|
|
9277
9632
|
severity: "error",
|
|
9278
9633
|
recommendation: "Add a `key={...}` prop to each element produced inside `.map` / array literal.",
|
|
9279
9634
|
create: (context) => {
|
|
9280
|
-
const settings = resolveSettings$
|
|
9635
|
+
const settings = resolveSettings$30(context.settings);
|
|
9281
9636
|
return {
|
|
9282
9637
|
JSXElement(node) {
|
|
9283
9638
|
const openingElement = node.openingElement;
|
|
@@ -9327,194 +9682,10 @@ const jsxKey = defineRule({
|
|
|
9327
9682
|
}
|
|
9328
9683
|
});
|
|
9329
9684
|
//#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
9685
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
9515
9686
|
const buildMessage$18 = (depth, max) => `This JSX is hard to read at ${depth} levels deep, past the limit of ${max}.`;
|
|
9516
9687
|
const DEFAULT_MAX_DEPTH = 14;
|
|
9517
|
-
const resolveSettings$
|
|
9688
|
+
const resolveSettings$29 = (settings) => {
|
|
9518
9689
|
const reactDoctor = settings?.["react-doctor"];
|
|
9519
9690
|
return { max: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxMaxDepth ?? {} : {}).max ?? DEFAULT_MAX_DEPTH };
|
|
9520
9691
|
};
|
|
@@ -9574,7 +9745,7 @@ const jsxMaxDepth = defineRule({
|
|
|
9574
9745
|
recommendation: "Pull deeply nested JSX into smaller components so it's easier to read.",
|
|
9575
9746
|
category: "Architecture",
|
|
9576
9747
|
create: (context) => {
|
|
9577
|
-
const { max } = resolveSettings$
|
|
9748
|
+
const { max } = resolveSettings$29(context.settings);
|
|
9578
9749
|
const checkNode = (node) => {
|
|
9579
9750
|
if (!isLeafJsxNode(node)) return;
|
|
9580
9751
|
const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
|
|
@@ -11269,7 +11440,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
11269
11440
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
11270
11441
|
const MESSAGE$35 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
11271
11442
|
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$
|
|
11443
|
+
const resolveSettings$28 = (settings) => {
|
|
11273
11444
|
const reactDoctor = settings?.["react-doctor"];
|
|
11274
11445
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
11275
11446
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
@@ -11295,7 +11466,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
11295
11466
|
recommendation: "Replace `javascript:` URLs with `onClick` or `onSubmit` handlers. React 19 blocks them anyway.",
|
|
11296
11467
|
category: "Security",
|
|
11297
11468
|
create: (context) => {
|
|
11298
|
-
const options = resolveSettings$
|
|
11469
|
+
const options = resolveSettings$28(context.settings);
|
|
11299
11470
|
return { JSXOpeningElement(node) {
|
|
11300
11471
|
const elementName = getElementName(node);
|
|
11301
11472
|
if (!elementName) return;
|
|
@@ -11315,287 +11486,6 @@ const jsxNoScriptUrl = defineRule({
|
|
|
11315
11486
|
}
|
|
11316
11487
|
});
|
|
11317
11488
|
//#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
11489
|
//#region src/plugin/rules/react-builtins/jsx-no-undef.ts
|
|
11600
11490
|
const buildMessage$17 = (name) => `\`${name}\` crashes at runtime because it isn't defined here.`;
|
|
11601
11491
|
const KNOWN_GLOBALS = new Set([
|
|
@@ -11925,7 +11815,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
11925
11815
|
create: (context) => {
|
|
11926
11816
|
const settings = resolveSettings$25(context.settings);
|
|
11927
11817
|
return { JSXOpeningElement(node) {
|
|
11928
|
-
const tagName = flattenJsxName(node.name);
|
|
11818
|
+
const tagName = flattenJsxName$1(node.name);
|
|
11929
11819
|
if (!tagName) return;
|
|
11930
11820
|
const isCustom = isReactComponentName(tagName) || tagName.includes(".");
|
|
11931
11821
|
const isHtml = !isCustom;
|
|
@@ -12515,7 +12405,6 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
12515
12405
|
const PAGE_FILE_PATTERN = /\/page\.(tsx?|jsx?)$/;
|
|
12516
12406
|
const PAGE_OR_LAYOUT_FILE_PATTERN = /\/(page|layout)\.(tsx?|jsx?)$/;
|
|
12517
12407
|
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
12408
|
const PAGES_DIRECTORY_PATTERN = /\/pages\//;
|
|
12520
12409
|
const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
|
|
12521
12410
|
"redirect",
|
|
@@ -12952,11 +12841,9 @@ const nextjsNoImgElement = defineRule({
|
|
|
12952
12841
|
severity: "warn",
|
|
12953
12842
|
recommendation: "`import Image from 'next/image'` for automatic WebP/AVIF, lazy loading, and responsive srcset",
|
|
12954
12843
|
create: (context) => {
|
|
12955
|
-
|
|
12956
|
-
const isOgRoute = OG_ROUTE_PATTERN.test(filename);
|
|
12957
|
-
const isMetadataImageRoute = isNextjsMetadataImageRouteFilename(filename);
|
|
12844
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
12958
12845
|
return { JSXOpeningElement(node) {
|
|
12959
|
-
if (
|
|
12846
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
12960
12847
|
if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "img") context.report({
|
|
12961
12848
|
node,
|
|
12962
12849
|
message: "Plain <img> ships unoptimized, oversized images to your users."
|
|
@@ -23799,7 +23686,7 @@ const noUnknownProperty = defineRule({
|
|
|
23799
23686
|
create: (context) => {
|
|
23800
23687
|
const { ignore = [], requireDataLowercase = false } = resolveSettings$10(context.settings);
|
|
23801
23688
|
const ignoreSet = new Set(ignore);
|
|
23802
|
-
if (
|
|
23689
|
+
if (isGeneratedImageRenderContext(context)) ignoreSet.add("tw");
|
|
23803
23690
|
let fileIsNonReactJsx = false;
|
|
23804
23691
|
return {
|
|
23805
23692
|
Program(node) {
|
|
@@ -23834,6 +23721,7 @@ const noUnknownProperty = defineRule({
|
|
|
23834
23721
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23835
23722
|
const actualName = getJsxAttributeName(attribute.name);
|
|
23836
23723
|
if (!actualName) continue;
|
|
23724
|
+
if (actualName === "tw" && isGeneratedImageRenderContext(context, node)) continue;
|
|
23837
23725
|
if (ignoreSet.has(actualName)) continue;
|
|
23838
23726
|
if (isValidDataAttribute(actualName)) {
|
|
23839
23727
|
if (requireDataLowercase && hasUppercaseChar(actualName)) context.report({
|
|
@@ -25455,12 +25343,12 @@ const doesFunctionReturnsObjectLiteral = (functionNode) => {
|
|
|
25455
25343
|
const child = nodeRecord[key];
|
|
25456
25344
|
if (Array.isArray(child)) for (const item of child) {
|
|
25457
25345
|
if (!isAstNode(item)) continue;
|
|
25458
|
-
if (FUNCTION_LIKE_TYPES
|
|
25346
|
+
if (FUNCTION_LIKE_TYPES.has(item.type)) continue;
|
|
25459
25347
|
visit(item);
|
|
25460
25348
|
if (returnsObject) return;
|
|
25461
25349
|
}
|
|
25462
25350
|
else if (isAstNode(child)) {
|
|
25463
|
-
if (FUNCTION_LIKE_TYPES
|
|
25351
|
+
if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
|
|
25464
25352
|
visit(child);
|
|
25465
25353
|
}
|
|
25466
25354
|
}
|
|
@@ -26882,12 +26770,12 @@ const functionBodyHasReturnWithValue = (functionNode) => {
|
|
|
26882
26770
|
const child = nodeRecord[key];
|
|
26883
26771
|
if (Array.isArray(child)) for (const item of child) {
|
|
26884
26772
|
if (!isAstNode(item)) continue;
|
|
26885
|
-
if (FUNCTION_LIKE_TYPES
|
|
26773
|
+
if (FUNCTION_LIKE_TYPES.has(item.type)) continue;
|
|
26886
26774
|
visit(item);
|
|
26887
26775
|
if (didFindReturn) return;
|
|
26888
26776
|
}
|
|
26889
26777
|
else if (isAstNode(child)) {
|
|
26890
|
-
if (FUNCTION_LIKE_TYPES
|
|
26778
|
+
if (FUNCTION_LIKE_TYPES.has(child.type)) continue;
|
|
26891
26779
|
visit(child);
|
|
26892
26780
|
}
|
|
26893
26781
|
}
|
|
@@ -28637,7 +28525,7 @@ const isExpoUiNamespaceImport = (contextNode, localName) => {
|
|
|
28637
28525
|
};
|
|
28638
28526
|
const isExpoUiComponentElement = (openingElement, contextNode, componentName) => {
|
|
28639
28527
|
if (!openingElement.name) return false;
|
|
28640
|
-
const dottedName = flattenJsxName(openingElement.name);
|
|
28528
|
+
const dottedName = flattenJsxName$1(openingElement.name);
|
|
28641
28529
|
if (!dottedName) return false;
|
|
28642
28530
|
const [rootLocalName, secondName] = dottedName.split(".");
|
|
28643
28531
|
if (isNamedImportOf(contextNode, rootLocalName, componentName)) return true;
|
|
@@ -28727,7 +28615,7 @@ const collectTopLevelReturnExpressions = (functionNode) => {
|
|
|
28727
28615
|
if (!block || !isNodeOfType(block, "BlockStatement")) return [];
|
|
28728
28616
|
const returnExpressions = [];
|
|
28729
28617
|
const visit = (node) => {
|
|
28730
|
-
if (FUNCTION_LIKE_TYPES
|
|
28618
|
+
if (FUNCTION_LIKE_TYPES.has(node.type)) return;
|
|
28731
28619
|
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnExpressions.push(node.argument);
|
|
28732
28620
|
const nodeRecord = node;
|
|
28733
28621
|
for (const fieldName of Object.keys(nodeRecord)) {
|
|
@@ -35999,17 +35887,6 @@ const reactDoctorRules = [
|
|
|
35999
35887
|
category: "Security"
|
|
36000
35888
|
}
|
|
36001
35889
|
},
|
|
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
35890
|
{
|
|
36014
35891
|
key: "react-doctor/jsx-no-undef",
|
|
36015
35892
|
id: "jsx-no-undef",
|