oxlint-plugin-react-doctor 0.5.4 → 0.5.5-dev.bac7c82
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 +747 -0
- package/dist/index.js +34 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -323,9 +323,18 @@ const BROWSER_ARTIFACT_PATH_PATTERNS = [
|
|
|
323
323
|
const AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN = /\b(?:exec|execSync|spawn|child_process|eval|new Function|vm\.run|readFile|writeFile|fs\.read|fs\.write|fetch|axios|http\.request|sandbox|runCode|executeCode)\b/;
|
|
324
324
|
//#endregion
|
|
325
325
|
//#region src/plugin/rules/security-scan/utils/is-browser-artifact-path.ts
|
|
326
|
-
const
|
|
326
|
+
const SERVER_BUILD_ROOT_SEGMENTS = new Set([".next", ".output"]);
|
|
327
|
+
const isNonShippedBuildArtifactPath = (relativePath) => {
|
|
328
|
+
const segments = relativePath.split("/");
|
|
329
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
330
|
+
if (!SERVER_BUILD_ROOT_SEGMENTS.has(segments[index])) continue;
|
|
331
|
+
if (segments[index] === ".next" && segments[index + 1] === "dev") return true;
|
|
332
|
+
if (segments[index + 1] === "server" && index + 2 < segments.length) return true;
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
};
|
|
327
336
|
const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
|
|
328
|
-
if (
|
|
337
|
+
if (isNonShippedBuildArtifactPath(relativePath)) return false;
|
|
329
338
|
if (isGeneratedBundle) return true;
|
|
330
339
|
if (relativePath.endsWith(".map")) return true;
|
|
331
340
|
return BROWSER_ARTIFACT_PATH_PATTERNS.some((pattern) => pattern.test(relativePath));
|
|
@@ -1108,6 +1117,11 @@ const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSourc
|
|
|
1108
1117
|
if (info.source !== moduleSource) return null;
|
|
1109
1118
|
return info.imported;
|
|
1110
1119
|
};
|
|
1120
|
+
const getImportSourceForName = (contextNode, localIdentifierName) => {
|
|
1121
|
+
const lookup = getImportLookup(contextNode);
|
|
1122
|
+
if (!lookup) return null;
|
|
1123
|
+
return lookup.get(localIdentifierName)?.source ?? null;
|
|
1124
|
+
};
|
|
1111
1125
|
//#endregion
|
|
1112
1126
|
//#region src/plugin/utils/find-variable-initializer.ts
|
|
1113
1127
|
const FUNCTION_LIKE_TYPES$1 = new Set([
|
|
@@ -12705,6 +12719,7 @@ const keyLifecycleRisk = defineRule({
|
|
|
12705
12719
|
id: "key-lifecycle-risk",
|
|
12706
12720
|
title: "Long-lived key material in repository",
|
|
12707
12721
|
severity: "error",
|
|
12722
|
+
committedFilesOnly: true,
|
|
12708
12723
|
recommendation: "Remove private keys from source, rotate exposed credentials, prefer short-lived deploy credentials, and document revocation/expiry for release keys.",
|
|
12709
12724
|
scan: scanByPattern({
|
|
12710
12725
|
shouldScan: (file) => !TEST_CONTEXT_PATTERN.test(file.relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath),
|
|
@@ -22559,9 +22574,10 @@ const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
|
|
|
22559
22574
|
"createRootRouteWithContext"
|
|
22560
22575
|
]);
|
|
22561
22576
|
const TANSTACK_SERVER_FN_NAMES = new Set(["createServerFn"]);
|
|
22577
|
+
const TANSTACK_INPUT_VALIDATOR_METHOD_NAMES = new Set(["validator", "inputValidator"]);
|
|
22562
22578
|
const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
|
|
22563
22579
|
"middleware",
|
|
22564
|
-
"
|
|
22580
|
+
"validator",
|
|
22565
22581
|
"client",
|
|
22566
22582
|
"server",
|
|
22567
22583
|
"handler"
|
|
@@ -27034,6 +27050,8 @@ const publicEnvSecretName = defineRule({
|
|
|
27034
27050
|
});
|
|
27035
27051
|
//#endregion
|
|
27036
27052
|
//#region src/plugin/rules/tanstack-query/query-destructure-result.ts
|
|
27053
|
+
const TANSTACK_QUERY_PACKAGE_PATTERN = /^@tanstack\/[\w-]*query[\w-]*$/;
|
|
27054
|
+
const isTanstackQuerySource = (source) => TANSTACK_QUERY_PACKAGE_PATTERN.test(source) || source === "react-query";
|
|
27037
27055
|
const queryDestructureResult = defineRule({
|
|
27038
27056
|
id: "query-destructure-result",
|
|
27039
27057
|
title: "Whole query result subscribes to every field",
|
|
@@ -27046,6 +27064,8 @@ const queryDestructureResult = defineRule({
|
|
|
27046
27064
|
if (!node.init || !isNodeOfType(node.init, "CallExpression")) return;
|
|
27047
27065
|
const calleeName = isNodeOfType(node.init.callee, "Identifier") ? node.init.callee.name : null;
|
|
27048
27066
|
if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
|
|
27067
|
+
const importSource = getImportSourceForName(node, calleeName);
|
|
27068
|
+
if (importSource !== null && !isTanstackQuerySource(importSource)) return;
|
|
27049
27069
|
context.report({
|
|
27050
27070
|
node: node.id,
|
|
27051
27071
|
message: `Destructure ${calleeName}() results instead of assigning the whole query object, so TanStack Query only subscribes to the fields you use.`
|
|
@@ -27888,6 +27908,7 @@ const repositorySecretFile = defineRule({
|
|
|
27888
27908
|
id: "repository-secret-file",
|
|
27889
27909
|
title: "Secret file checked into repository",
|
|
27890
27910
|
severity: "error",
|
|
27911
|
+
committedFilesOnly: true,
|
|
27891
27912
|
recommendation: "Remove committed env files, service-account credentials, npm auth tokens, and webhook URLs; rotate exposed values and keep only redacted examples in source.",
|
|
27892
27913
|
scan: (file) => {
|
|
27893
27914
|
if (!isRepositorySecretFilePath(file.relativePath)) return [];
|
|
@@ -35200,7 +35221,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
35200
35221
|
const result = {
|
|
35201
35222
|
isServerFnChain: false,
|
|
35202
35223
|
specifiedMethod: null,
|
|
35203
|
-
|
|
35224
|
+
hasInputValidation: false
|
|
35204
35225
|
};
|
|
35205
35226
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
35206
35227
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
@@ -35214,7 +35235,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
35214
35235
|
for (const property of optionsArgument.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && property.key.name === "method" && isNodeOfType(property.value, "Literal") && typeof property.value.value === "string") result.specifiedMethod = property.value.value;
|
|
35215
35236
|
}
|
|
35216
35237
|
}
|
|
35217
|
-
if (calleeName
|
|
35238
|
+
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
35218
35239
|
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
35219
35240
|
else break;
|
|
35220
35241
|
}
|
|
@@ -35768,13 +35789,14 @@ const tanstackStartRoutePropertyOrder = defineRule({
|
|
|
35768
35789
|
});
|
|
35769
35790
|
//#endregion
|
|
35770
35791
|
//#region src/plugin/rules/tanstack-start/tanstack-start-server-fn-method-order.ts
|
|
35792
|
+
const toMethodOrderToken = (methodName) => TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(methodName) ? "validator" : methodName;
|
|
35771
35793
|
const tanstackStartServerFnMethodOrder = defineRule({
|
|
35772
35794
|
id: "tanstack-start-server-fn-method-order",
|
|
35773
35795
|
title: "Server function method order breaks type inference",
|
|
35774
35796
|
tags: ["test-noise"],
|
|
35775
35797
|
requires: ["tanstack-start"],
|
|
35776
35798
|
severity: "error",
|
|
35777
|
-
recommendation: "Chain methods in order: .middleware() → .
|
|
35799
|
+
recommendation: "Chain methods in order: .middleware() → .validator() → .client() → .server() → .handler(). Types depend on this sequence.",
|
|
35778
35800
|
create: (context) => ({ CallExpression(node) {
|
|
35779
35801
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
35780
35802
|
const methodNames = [];
|
|
@@ -35789,10 +35811,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
35789
35811
|
} else return;
|
|
35790
35812
|
const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
35791
35813
|
if (methodNames[methodNames.length - 1] !== ownMethodName) return;
|
|
35792
|
-
const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_ORDER.includes(name));
|
|
35814
|
+
const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_ORDER.includes(toMethodOrderToken(name)));
|
|
35793
35815
|
let lastIndex = -1;
|
|
35794
35816
|
for (const methodName of orderSensitiveMethods) {
|
|
35795
|
-
const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(methodName);
|
|
35817
|
+
const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(toMethodOrderToken(methodName));
|
|
35796
35818
|
if (currentIndex < lastIndex) {
|
|
35797
35819
|
const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
|
|
35798
35820
|
context.report({
|
|
@@ -35813,7 +35835,7 @@ const tanstackStartServerFnValidateInput = defineRule({
|
|
|
35813
35835
|
tags: ["test-noise"],
|
|
35814
35836
|
requires: ["tanstack-start"],
|
|
35815
35837
|
severity: "warn",
|
|
35816
|
-
recommendation: "Add `.
|
|
35838
|
+
recommendation: "Add `.validator(schema)` before `.handler()`. This data crosses the network and must be validated at runtime.",
|
|
35817
35839
|
create: (context) => ({ CallExpression(node) {
|
|
35818
35840
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
35819
35841
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
@@ -35827,9 +35849,9 @@ const tanstackStartServerFnValidateInput = defineRule({
|
|
|
35827
35849
|
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.property, "Identifier") && child.property.name === "data") accessesData = true;
|
|
35828
35850
|
if (isNodeOfType(child, "ObjectPattern") && child.properties?.some((property) => isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && property.key.name === "data")) accessesData = true;
|
|
35829
35851
|
});
|
|
35830
|
-
if (accessesData && !chainInfo.
|
|
35852
|
+
if (accessesData && !chainInfo.hasInputValidation) context.report({
|
|
35831
35853
|
node,
|
|
35832
|
-
message: "This server function reads network data with no
|
|
35854
|
+
message: "This server function reads network data with no validator(), so anyone can send unvalidated input."
|
|
35833
35855
|
});
|
|
35834
35856
|
} })
|
|
35835
35857
|
});
|
|
@@ -36001,7 +36023,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
36001
36023
|
//#region src/plugin/rules/security-scan/webhook-signature-risk.ts
|
|
36002
36024
|
const WEBHOOK_HANDLER_PATTERN = /(?:^|\/)[^/]*webhook[^/]*\/|(?:^|\/)[^/]*webhook[^/]*\.[cm]?[jt]s$|\bwebhook\b/i;
|
|
36003
36025
|
const WEBHOOK_ENTRYPOINT_PATTERN = /\b(?:export\s+(?:async\s+)?function\s+POST|export\s+const\s+(?:POST|handler|webhook)|webhookHandler|webhookRoute)\b/i;
|
|
36004
|
-
const WEBHOOK_SIGNATURE_VERIFICATION_PATTERN = /verifySignature|verify.*signature|verify\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\.webhooks|["'][\w-]*signature["']
|
|
36026
|
+
const WEBHOOK_SIGNATURE_VERIFICATION_PATTERN = new RegExp(`${/verifySignature|verify.*signature|verify\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\.webhooks|["'][\w-]*signature["']/.source}|${/\b[A-Za-z]{0,40}(?:verif|valid|check|assert|authenticat|compare|guard)[A-Za-z]{0,40}(?:secret|signature|hmac|webhook|digest)[A-Za-z]{0,40}\s*\(/.source}`, "i");
|
|
36005
36027
|
const OUTBOUND_WEBHOOK_URL_MENTION_PATTERN = /webhook[\s_-]?ur[il]\w*/gi;
|
|
36006
36028
|
const OUTBOUND_WEBHOOK_CONFIG_PATTERN = /process\.env\.\w*WEBHOOK_URL|\b(?:send|post|dispatch|publish|notify)\w*Webhook/;
|
|
36007
36029
|
const REQUEST_READ_PATTERN = /\b(?:req|request)\b/;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-react-doctor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5-dev.bac7c82",
|
|
4
4
|
"description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|