oxlint-plugin-react-doctor 0.7.9-dev.621be9b → 0.7.9-dev.6c5c91b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +138 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27251,7 +27251,11 @@ const mouseEventsHaveKeyEvents = defineRule({
|
|
|
27251
27251
|
//#region src/plugin/utils/has-directive.ts
|
|
27252
27252
|
const hasDirective = (programNode, directive) => {
|
|
27253
27253
|
if (!isNodeOfType(programNode, "Program")) return false;
|
|
27254
|
-
|
|
27254
|
+
for (const statement of programNode.body) {
|
|
27255
|
+
if (!isNodeOfType(statement, "ExpressionStatement") || statement.directive === void 0) return false;
|
|
27256
|
+
if (statement.directive === directive) return true;
|
|
27257
|
+
}
|
|
27258
|
+
return false;
|
|
27255
27259
|
};
|
|
27256
27260
|
//#endregion
|
|
27257
27261
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
@@ -46866,7 +46870,7 @@ const doConditionsImplyFormula = (conditions, target) => {
|
|
|
46866
46870
|
}
|
|
46867
46871
|
return facts.didConflict || evaluateBooleanFormula$1(target, facts.assignments) === true;
|
|
46868
46872
|
};
|
|
46869
|
-
const getFunctionBindingSymbol = (functionNode, scopes) => {
|
|
46873
|
+
const getFunctionBindingSymbol$1 = (functionNode, scopes) => {
|
|
46870
46874
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.symbolFor(functionNode.id);
|
|
46871
46875
|
const parent = functionNode.parent;
|
|
46872
46876
|
if ((isNodeOfType(functionNode, "ArrowFunctionExpression") || isNodeOfType(functionNode, "FunctionExpression")) && isNodeOfType(parent, "VariableDeclarator") && parent.init === functionNode && isNodeOfType(parent.id, "Identifier")) return scopes.symbolFor(parent.id);
|
|
@@ -46899,7 +46903,7 @@ const isNodeEvaluatedDuringRender = (node, componentNode, scopes, visitedFunctio
|
|
|
46899
46903
|
const synchronousCallbackCall = getSynchronousCallbackCall(functionNode);
|
|
46900
46904
|
if (synchronousCallbackCall) return isNodeEvaluatedDuringRender(synchronousCallbackCall, componentNode, scopes, visitedFunctionSymbolIds);
|
|
46901
46905
|
if (executesDuringRender(functionNode, scopes)) return isNodeEvaluatedDuringRender(functionNode.parent ?? functionNode, componentNode, scopes, visitedFunctionSymbolIds);
|
|
46902
|
-
const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
|
|
46906
|
+
const functionSymbol = getFunctionBindingSymbol$1(functionNode, scopes);
|
|
46903
46907
|
if (!functionSymbol || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
|
|
46904
46908
|
visitedFunctionSymbolIds.add(functionSymbol.id);
|
|
46905
46909
|
let callCount = 0;
|
|
@@ -46948,7 +46952,7 @@ const collectExposureConditions = (analysis, context, node, componentNode, prote
|
|
|
46948
46952
|
parent = synchronousCallbackCall.parent;
|
|
46949
46953
|
continue;
|
|
46950
46954
|
}
|
|
46951
|
-
const functionSymbol = getFunctionBindingSymbol(parent, context.scopes);
|
|
46955
|
+
const functionSymbol = getFunctionBindingSymbol$1(parent, context.scopes);
|
|
46952
46956
|
if (functionSymbol?.references.length === 1) {
|
|
46953
46957
|
const callExpression = isReferenceDirectlyCalled(functionSymbol.references[0].identifier);
|
|
46954
46958
|
if (callExpression) {
|
|
@@ -47158,7 +47162,7 @@ const getSetterExposureConditions = (analysis, context, setterReference, compone
|
|
|
47158
47162
|
const functionNode = findEnclosingFunction$1(setterReference.identifier);
|
|
47159
47163
|
if (!functionNode) return null;
|
|
47160
47164
|
if (isInlineJsxCallback(functionNode)) return [collectExposureConditions(analysis, context, functionNode, componentNode, protectedSymbolIds)];
|
|
47161
|
-
const functionSymbol = getFunctionBindingSymbol(functionNode, context.scopes);
|
|
47165
|
+
const functionSymbol = getFunctionBindingSymbol$1(functionNode, context.scopes);
|
|
47162
47166
|
if (!functionSymbol || functionSymbol.references.length === 0) return null;
|
|
47163
47167
|
const conditionsByReference = [];
|
|
47164
47168
|
for (const reference of functionSymbol.references) {
|
|
@@ -47546,7 +47550,7 @@ const isSelfReferentialSentinelValue = (variableName, literalValue) => literalVa
|
|
|
47546
47550
|
const isIdentifierLikeKeyNameValue = (literalValue) => {
|
|
47547
47551
|
const wordSegments = literalValue.replace(/^[_$\s]+|[_$\s]+$/g, "").split(/[_\-:./$]+/).filter((segment) => segment.length > 0);
|
|
47548
47552
|
if (wordSegments.length < 2) return false;
|
|
47549
|
-
return wordSegments.every((segment) => /^[a-z]
|
|
47553
|
+
return wordSegments.every((segment) => /^[a-z]+(?:[A-Z][a-z]+)*$/.test(segment));
|
|
47550
47554
|
};
|
|
47551
47555
|
const FRAMEWORK_ENV_ADVICE = [
|
|
47552
47556
|
[
|
|
@@ -47620,7 +47624,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
47620
47624
|
const isServerOnlyScope = isInsideServerOnlyScope(node);
|
|
47621
47625
|
const trailingSuffix = getIdentifierTrailingWord(variableName);
|
|
47622
47626
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
47623
|
-
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) &&
|
|
47627
|
+
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
|
|
47624
47628
|
context.report({
|
|
47625
47629
|
node,
|
|
47626
47630
|
message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
|
|
@@ -64945,6 +64949,109 @@ const isDeferrableSideEffectCall = (objectName, methodName) => {
|
|
|
64945
64949
|
if (ANALYTICS_DEFERRABLE_OBJECTS.has(objectName)) return ANALYTICS_DEFERRABLE_METHODS.has(methodName);
|
|
64946
64950
|
return false;
|
|
64947
64951
|
};
|
|
64952
|
+
const NEXT_SERVER_SOURCE = "next/server";
|
|
64953
|
+
const NEXT_AFTER_EXPORT_NAMES = new Set(["after", "unstable_after"]);
|
|
64954
|
+
const isNextAfterImportSymbol = (symbol, contextNode) => {
|
|
64955
|
+
if (symbol.kind !== "import") return false;
|
|
64956
|
+
const importBinding = getImportBindingForName(contextNode, symbol.name);
|
|
64957
|
+
return Boolean(importBinding && importBinding.source === NEXT_SERVER_SOURCE && !importBinding.isNamespace && importBinding.exportedName && NEXT_AFTER_EXPORT_NAMES.has(importBinding.exportedName));
|
|
64958
|
+
};
|
|
64959
|
+
const isDirectObjectPatternBinding = (symbol) => {
|
|
64960
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
64961
|
+
if (!isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
|
|
64962
|
+
let bindingNode = symbol.bindingIdentifier;
|
|
64963
|
+
if (isNodeOfType(bindingNode.parent, "AssignmentPattern") && bindingNode.parent.left === bindingNode) bindingNode = bindingNode.parent;
|
|
64964
|
+
const property = bindingNode.parent;
|
|
64965
|
+
return Boolean(isNodeOfType(property, "Property") && property.value === bindingNode && property.parent === symbol.declarationNode.id);
|
|
64966
|
+
};
|
|
64967
|
+
const isNextServerNamespace = (expression, contextNode, scopes) => {
|
|
64968
|
+
let candidate = stripParenExpression(expression);
|
|
64969
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
64970
|
+
while (isNodeOfType(candidate, "Identifier")) {
|
|
64971
|
+
const symbol = scopes.symbolFor(candidate);
|
|
64972
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
64973
|
+
if (symbol.kind === "import") {
|
|
64974
|
+
const importBinding = getImportBindingForName(contextNode, symbol.name);
|
|
64975
|
+
return Boolean(importBinding?.source === NEXT_SERVER_SOURCE && importBinding.isNamespace);
|
|
64976
|
+
}
|
|
64977
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
|
|
64978
|
+
visitedSymbolIds.add(symbol.id);
|
|
64979
|
+
candidate = stripParenExpression(symbol.initializer);
|
|
64980
|
+
}
|
|
64981
|
+
return false;
|
|
64982
|
+
};
|
|
64983
|
+
const isNextAfterCallee = (callee, contextNode, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
64984
|
+
const candidate = stripParenExpression(callee);
|
|
64985
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
64986
|
+
const propertyName = getStaticPropertyKeyName(candidate, { allowComputedString: true });
|
|
64987
|
+
return Boolean(propertyName && NEXT_AFTER_EXPORT_NAMES.has(propertyName) && isNextServerNamespace(candidate.object, contextNode, scopes));
|
|
64988
|
+
}
|
|
64989
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
64990
|
+
const symbol = scopes.symbolFor(candidate);
|
|
64991
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
64992
|
+
if (isNextAfterImportSymbol(symbol, contextNode)) return true;
|
|
64993
|
+
const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
|
|
64994
|
+
if (symbol.kind === "const" && symbol.initializer && isDirectObjectPatternBinding(symbol) && destructuredPropertyName && NEXT_AFTER_EXPORT_NAMES.has(destructuredPropertyName)) return isNextServerNamespace(symbol.initializer, contextNode, scopes);
|
|
64995
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
|
|
64996
|
+
visitedSymbolIds.add(symbol.id);
|
|
64997
|
+
return isNextAfterCallee(symbol.initializer, contextNode, scopes, visitedSymbolIds);
|
|
64998
|
+
};
|
|
64999
|
+
const getDirectArgumentCall = (expression) => {
|
|
65000
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
65001
|
+
const parent = expressionRoot.parent;
|
|
65002
|
+
if (!isNodeOfType(parent, "CallExpression")) return null;
|
|
65003
|
+
return parent.arguments[0] === expressionRoot ? parent : null;
|
|
65004
|
+
};
|
|
65005
|
+
const isScheduledByNextAfter = (expression, scopes) => {
|
|
65006
|
+
const callExpression = getDirectArgumentCall(expression);
|
|
65007
|
+
return Boolean(callExpression && isNextAfterCallee(callExpression.callee, callExpression, scopes));
|
|
65008
|
+
};
|
|
65009
|
+
const getFunctionBindingSymbol = (functionNode, scopes) => {
|
|
65010
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.scopeFor(functionNode).symbols.find((symbol) => symbol.declarationNode === functionNode) ?? null;
|
|
65011
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
65012
|
+
const parent = functionRoot.parent;
|
|
65013
|
+
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== functionRoot || !isNodeOfType(parent.id, "Identifier")) return null;
|
|
65014
|
+
return scopes.symbolFor(parent.id);
|
|
65015
|
+
};
|
|
65016
|
+
const isDirectlyExported = (symbol) => {
|
|
65017
|
+
let declaration = symbol.declarationNode;
|
|
65018
|
+
if (isNodeOfType(declaration, "VariableDeclarator")) declaration = declaration.parent;
|
|
65019
|
+
return Boolean(declaration?.parent && (isNodeOfType(declaration.parent, "ExportNamedDeclaration") || isNodeOfType(declaration.parent, "ExportDefaultDeclaration")));
|
|
65020
|
+
};
|
|
65021
|
+
const isLexicallyInsideFunction = (node, functionNode) => {
|
|
65022
|
+
let enclosingFunction = findEnclosingFunction$1(node);
|
|
65023
|
+
while (enclosingFunction) {
|
|
65024
|
+
if (enclosingFunction === functionNode) return true;
|
|
65025
|
+
enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
65026
|
+
}
|
|
65027
|
+
return false;
|
|
65028
|
+
};
|
|
65029
|
+
const isExclusivelyScheduledByNextAfter = (functionNode, scopes, visitedFunctionSymbolIds) => {
|
|
65030
|
+
if (isScheduledByNextAfter(functionNode, scopes)) return true;
|
|
65031
|
+
const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
|
|
65032
|
+
if (!functionSymbol || isDirectlyExported(functionSymbol) || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
|
|
65033
|
+
const nextVisitedFunctionSymbolIds = new Set(visitedFunctionSymbolIds).add(functionSymbol.id);
|
|
65034
|
+
let hasAfterUse = false;
|
|
65035
|
+
for (const reference of functionSymbol.references) {
|
|
65036
|
+
if (reference.flag !== "read") return false;
|
|
65037
|
+
if (isLexicallyInsideFunction(reference.identifier, functionNode)) continue;
|
|
65038
|
+
if (isScheduledByNextAfter(reference.identifier, scopes)) {
|
|
65039
|
+
hasAfterUse = true;
|
|
65040
|
+
continue;
|
|
65041
|
+
}
|
|
65042
|
+
if (!isInsideNextAfterCallback(reference.identifier, scopes, nextVisitedFunctionSymbolIds)) return false;
|
|
65043
|
+
hasAfterUse = true;
|
|
65044
|
+
}
|
|
65045
|
+
return hasAfterUse;
|
|
65046
|
+
};
|
|
65047
|
+
const isInsideNextAfterCallback = (node, scopes, visitedFunctionSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
65048
|
+
let enclosingFunction = findEnclosingFunction$1(node);
|
|
65049
|
+
while (enclosingFunction) {
|
|
65050
|
+
if (isExclusivelyScheduledByNextAfter(enclosingFunction, scopes, visitedFunctionSymbolIds)) return true;
|
|
65051
|
+
enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
65052
|
+
}
|
|
65053
|
+
return false;
|
|
65054
|
+
};
|
|
64948
65055
|
const serverAfterNonblocking = defineRule({
|
|
64949
65056
|
id: "server-after-nonblocking",
|
|
64950
65057
|
title: "Blocking side effect before response",
|
|
@@ -64979,6 +65086,7 @@ const serverAfterNonblocking = defineRule({
|
|
|
64979
65086
|
if (!objectName) return;
|
|
64980
65087
|
const methodName = node.callee.property.name;
|
|
64981
65088
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
65089
|
+
if (isInsideNextAfterCallback(node, context.scopes)) return;
|
|
64982
65090
|
context.report({
|
|
64983
65091
|
node,
|
|
64984
65092
|
message: `${objectName}.${methodName}() runs before the response, so your users wait longer for it.`
|
|
@@ -66253,17 +66361,34 @@ const stylePropObject = defineRule({
|
|
|
66253
66361
|
};
|
|
66254
66362
|
}
|
|
66255
66363
|
});
|
|
66364
|
+
//#endregion
|
|
66365
|
+
//#region src/plugin/rules/security-scan/utils/has-use-server-directive-in-content.ts
|
|
66366
|
+
const hasUseServerDirectiveInContent = (content, relativePath = "source.tsx") => {
|
|
66367
|
+
const programNode = parseSourceText({
|
|
66368
|
+
filename: relativePath,
|
|
66369
|
+
sourceText: content,
|
|
66370
|
+
shouldAttachParentReferences: false
|
|
66371
|
+
});
|
|
66372
|
+
return programNode === null ? false : hasDirective(programNode, "use server");
|
|
66373
|
+
};
|
|
66374
|
+
//#endregion
|
|
66375
|
+
//#region src/plugin/rules/security-scan/supabase-client-owned-authz-field.ts
|
|
66376
|
+
const scanSupabaseClientOwnedAuthzField = scanByPattern({
|
|
66377
|
+
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
66378
|
+
pattern: /\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,
|
|
66379
|
+
requireAll: [/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],
|
|
66380
|
+
message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
|
|
66381
|
+
});
|
|
66256
66382
|
const supabaseClientOwnedAuthzField = defineRule({
|
|
66257
66383
|
id: "supabase-client-owned-authz-field",
|
|
66258
66384
|
title: "Client writes Supabase authorization field",
|
|
66259
66385
|
severity: "error",
|
|
66260
66386
|
recommendation: "Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",
|
|
66261
|
-
scan:
|
|
66262
|
-
|
|
66263
|
-
|
|
66264
|
-
|
|
66265
|
-
|
|
66266
|
-
})
|
|
66387
|
+
scan: (file) => {
|
|
66388
|
+
const findings = scanSupabaseClientOwnedAuthzField(file);
|
|
66389
|
+
if (findings.length === 0) return findings;
|
|
66390
|
+
return hasUseServerDirectiveInContent(file.content, file.relativePath) ? [] : findings;
|
|
66391
|
+
}
|
|
66267
66392
|
});
|
|
66268
66393
|
//#endregion
|
|
66269
66394
|
//#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
|