oxlint-plugin-react-doctor 0.5.8-dev.441e6af → 0.5.8-dev.5639b1e
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/LICENSE +14 -1
- package/dist/index.js +272 -76
- package/package.json +5 -4
package/LICENSE
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
MIT License
|
|
1
|
+
Modified MIT License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 Million Software, Inc.
|
|
4
4
|
|
|
@@ -19,3 +19,16 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Our only modification is that the following uses require prior written
|
|
24
|
+
permission from the copyright holder. To request permission, contact
|
|
25
|
+
founders@million.dev.
|
|
26
|
+
|
|
27
|
+
1. Using the Software, its source code, or any derivative works thereof, in
|
|
28
|
+
whole or in part, as training, fine-tuning, or evaluation data, or as input
|
|
29
|
+
to any automated pipeline for training or improving any machine learning
|
|
30
|
+
model or AI system.
|
|
31
|
+
|
|
32
|
+
2. Selling the Software, or offering it to third parties as a paid, hosted, or
|
|
33
|
+
managed product or service (including any commercial API or SaaS offering)
|
|
34
|
+
whose value derives entirely or substantially from the Software.
|
package/dist/index.js
CHANGED
|
@@ -1176,6 +1176,20 @@ const getImportSourceForName = (contextNode, localIdentifierName) => {
|
|
|
1176
1176
|
if (!lookup) return null;
|
|
1177
1177
|
return lookup.get(localIdentifierName)?.source ?? null;
|
|
1178
1178
|
};
|
|
1179
|
+
const getImportBindingForName = (contextNode, localIdentifierName) => {
|
|
1180
|
+
const info = getImportLookup(contextNode)?.get(localIdentifierName);
|
|
1181
|
+
if (!info) return null;
|
|
1182
|
+
if (info.isNamespace) return {
|
|
1183
|
+
source: info.source,
|
|
1184
|
+
exportedName: null,
|
|
1185
|
+
isNamespace: true
|
|
1186
|
+
};
|
|
1187
|
+
return {
|
|
1188
|
+
source: info.source,
|
|
1189
|
+
exportedName: info.isDefault ? "default" : info.imported,
|
|
1190
|
+
isNamespace: false
|
|
1191
|
+
};
|
|
1192
|
+
};
|
|
1179
1193
|
//#endregion
|
|
1180
1194
|
//#region src/plugin/utils/find-variable-initializer.ts
|
|
1181
1195
|
const FUNCTION_LIKE_TYPES$1 = new Set([
|
|
@@ -1414,6 +1428,14 @@ const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
|
|
|
1414
1428
|
"forbidden",
|
|
1415
1429
|
"unauthorized"
|
|
1416
1430
|
]);
|
|
1431
|
+
const CACHE_REVALIDATION_FUNCTION_NAMES = new Set([
|
|
1432
|
+
"revalidateTag",
|
|
1433
|
+
"revalidatePath",
|
|
1434
|
+
"expireTag",
|
|
1435
|
+
"expirePath",
|
|
1436
|
+
"unstable_expireTag",
|
|
1437
|
+
"unstable_expirePath"
|
|
1438
|
+
]);
|
|
1417
1439
|
const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
|
|
1418
1440
|
const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
|
|
1419
1441
|
const APP_DIRECTORY_PATTERN = /\/app\//;
|
|
@@ -6721,7 +6743,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
|
6721
6743
|
"ParenthesizedExpression",
|
|
6722
6744
|
"ChainExpression"
|
|
6723
6745
|
]);
|
|
6724
|
-
const unwrapExpression$
|
|
6746
|
+
const unwrapExpression$2 = (node) => {
|
|
6725
6747
|
let current = node;
|
|
6726
6748
|
while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
|
|
6727
6749
|
const inner = current.expression;
|
|
@@ -6828,7 +6850,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
6828
6850
|
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
6829
6851
|
const initializerRaw = declarator.init;
|
|
6830
6852
|
if (!initializerRaw) return false;
|
|
6831
|
-
const initializer = unwrapExpression$
|
|
6853
|
+
const initializer = unwrapExpression$2(initializerRaw);
|
|
6832
6854
|
if (symbol.kind === "const") {
|
|
6833
6855
|
if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
|
|
6834
6856
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
@@ -6848,13 +6870,13 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
6848
6870
|
return false;
|
|
6849
6871
|
};
|
|
6850
6872
|
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
6851
|
-
const initializer = symbol.initializer ? unwrapExpression$
|
|
6873
|
+
const initializer = symbol.initializer ? unwrapExpression$2(symbol.initializer) : null;
|
|
6852
6874
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
6853
6875
|
return getHookName(initializer.callee) === "useEffectEvent";
|
|
6854
6876
|
};
|
|
6855
6877
|
const getFunctionValueNode = (symbol) => {
|
|
6856
6878
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
6857
|
-
const initializer = symbol.initializer ? unwrapExpression$
|
|
6879
|
+
const initializer = symbol.initializer ? unwrapExpression$2(symbol.initializer) : null;
|
|
6858
6880
|
if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
|
|
6859
6881
|
return null;
|
|
6860
6882
|
};
|
|
@@ -6990,23 +7012,23 @@ const computeDepKey = (reference) => {
|
|
|
6990
7012
|
return fullName;
|
|
6991
7013
|
};
|
|
6992
7014
|
const computeDeclaredDepKey = (entry) => {
|
|
6993
|
-
const stripped = unwrapExpression$
|
|
7015
|
+
const stripped = unwrapExpression$2(entry);
|
|
6994
7016
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
6995
7017
|
if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
|
|
6996
7018
|
return null;
|
|
6997
7019
|
};
|
|
6998
7020
|
const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
|
|
6999
7021
|
if (!depsArgument) return false;
|
|
7000
|
-
const strippedDepsArgument = unwrapExpression$
|
|
7022
|
+
const strippedDepsArgument = unwrapExpression$2(depsArgument);
|
|
7001
7023
|
if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
|
|
7002
7024
|
return strippedDepsArgument.elements.some((element) => {
|
|
7003
7025
|
if (!element) return false;
|
|
7004
|
-
const strippedElement = unwrapExpression$
|
|
7026
|
+
const strippedElement = unwrapExpression$2(element);
|
|
7005
7027
|
return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
|
|
7006
7028
|
});
|
|
7007
7029
|
};
|
|
7008
7030
|
const stringifyMemberChain = (node) => {
|
|
7009
|
-
const stripped = unwrapExpression$
|
|
7031
|
+
const stripped = unwrapExpression$2(node);
|
|
7010
7032
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
7011
7033
|
if (isNodeOfType(stripped, "ThisExpression")) return "this";
|
|
7012
7034
|
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
@@ -7048,13 +7070,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
|
|
|
7048
7070
|
return false;
|
|
7049
7071
|
};
|
|
7050
7072
|
const getMemberRootIdentifier = (node) => {
|
|
7051
|
-
const stripped = unwrapExpression$
|
|
7073
|
+
const stripped = unwrapExpression$2(node);
|
|
7052
7074
|
if (isNodeOfType(stripped, "Identifier")) return stripped;
|
|
7053
7075
|
if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
|
|
7054
7076
|
return null;
|
|
7055
7077
|
};
|
|
7056
7078
|
const hasComputedMemberExpression = (node) => {
|
|
7057
|
-
const stripped = unwrapExpression$
|
|
7079
|
+
const stripped = unwrapExpression$2(node);
|
|
7058
7080
|
if (!isNodeOfType(stripped, "MemberExpression")) return false;
|
|
7059
7081
|
if (stripped.computed) return true;
|
|
7060
7082
|
return hasComputedMemberExpression(stripped.object);
|
|
@@ -7075,7 +7097,7 @@ const isRegExpLiteral = (node) => {
|
|
|
7075
7097
|
};
|
|
7076
7098
|
const isUnstableInitializer = (node) => {
|
|
7077
7099
|
if (!node) return false;
|
|
7078
|
-
const stripped = unwrapExpression$
|
|
7100
|
+
const stripped = unwrapExpression$2(node);
|
|
7079
7101
|
if (isRegExpLiteral(stripped)) return true;
|
|
7080
7102
|
if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
|
|
7081
7103
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
@@ -7231,7 +7253,7 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
7231
7253
|
const visit = (current) => {
|
|
7232
7254
|
if (didFindMemberCall) return;
|
|
7233
7255
|
if (isNodeOfType(current, "CallExpression")) {
|
|
7234
|
-
if (getMemberRootIdentifier(unwrapExpression$
|
|
7256
|
+
if (getMemberRootIdentifier(unwrapExpression$2(current.callee))?.name === rootName) {
|
|
7235
7257
|
didFindMemberCall = true;
|
|
7236
7258
|
return;
|
|
7237
7259
|
}
|
|
@@ -7294,7 +7316,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7294
7316
|
return;
|
|
7295
7317
|
}
|
|
7296
7318
|
const depsArgumentRaw = node.arguments[depsArgumentIndex];
|
|
7297
|
-
const callbackExpression = unwrapExpression$
|
|
7319
|
+
const callbackExpression = unwrapExpression$2(callbackArgument);
|
|
7298
7320
|
let callbackToAnalyze = null;
|
|
7299
7321
|
const forcedCaptureKeys = /* @__PURE__ */ new Set();
|
|
7300
7322
|
if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
|
|
@@ -7302,7 +7324,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7302
7324
|
const callbackSymbol = context.scopes.symbolFor(callbackExpression);
|
|
7303
7325
|
const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
|
|
7304
7326
|
if (functionValueNode) callbackToAnalyze = functionValueNode;
|
|
7305
|
-
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$
|
|
7327
|
+
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$2(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
|
|
7306
7328
|
else if (depsArgumentRaw) {
|
|
7307
7329
|
if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
|
|
7308
7330
|
context.report({
|
|
@@ -7359,7 +7381,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7359
7381
|
});
|
|
7360
7382
|
return;
|
|
7361
7383
|
}
|
|
7362
|
-
const depsArgument = unwrapExpression$
|
|
7384
|
+
const depsArgument = unwrapExpression$2(depsArgumentRaw);
|
|
7363
7385
|
if (isNodeOfType(depsArgument, "Literal") && depsArgument.value === null || isNodeOfType(depsArgument, "Identifier") && depsArgument.name === "undefined") {
|
|
7364
7386
|
if (isAutoDependenciesHook(hookName)) return;
|
|
7365
7387
|
if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
|
|
@@ -7400,11 +7422,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7400
7422
|
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
7401
7423
|
const hasLiteralDepElement = depsArgument.elements.some((element) => {
|
|
7402
7424
|
if (!element) return false;
|
|
7403
|
-
return isLiteralOrEmptyTemplate(unwrapExpression$
|
|
7425
|
+
return isLiteralOrEmptyTemplate(unwrapExpression$2(element));
|
|
7404
7426
|
});
|
|
7405
7427
|
const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
|
|
7406
7428
|
if (!element) return false;
|
|
7407
|
-
return isNonStringLiteral(unwrapExpression$
|
|
7429
|
+
return isNonStringLiteral(unwrapExpression$2(element));
|
|
7408
7430
|
});
|
|
7409
7431
|
if (hasNonStringLiteralDep) context.report({
|
|
7410
7432
|
node: depsArgument,
|
|
@@ -7425,7 +7447,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7425
7447
|
});
|
|
7426
7448
|
continue;
|
|
7427
7449
|
}
|
|
7428
|
-
const stripped = unwrapExpression$
|
|
7450
|
+
const stripped = unwrapExpression$2(elementNode);
|
|
7429
7451
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
7430
7452
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
7431
7453
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
@@ -10789,10 +10811,22 @@ const isWithinChildrenToArray = (jsxNode) => {
|
|
|
10789
10811
|
}
|
|
10790
10812
|
return false;
|
|
10791
10813
|
};
|
|
10814
|
+
const spreadCanOverwriteKey = (spreadAttribute) => {
|
|
10815
|
+
const argument = spreadAttribute.argument;
|
|
10816
|
+
if (!isNodeOfType(argument, "ObjectExpression")) return true;
|
|
10817
|
+
for (const property of argument.properties) {
|
|
10818
|
+
if (!isNodeOfType(property, "Property")) return true;
|
|
10819
|
+
if (property.computed) return true;
|
|
10820
|
+
const propertyKey = property.key;
|
|
10821
|
+
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "key") return true;
|
|
10822
|
+
if (isNodeOfType(propertyKey, "Literal") && String(propertyKey.value) === "key") return true;
|
|
10823
|
+
}
|
|
10824
|
+
return false;
|
|
10825
|
+
};
|
|
10792
10826
|
const checkKeyBeforeSpread = (context, openingElement) => {
|
|
10793
10827
|
let keyIndex = null;
|
|
10794
10828
|
let keyAttribute = null;
|
|
10795
|
-
let
|
|
10829
|
+
let lastOverwritingSpreadIndex = null;
|
|
10796
10830
|
for (let attributeIndex = 0; attributeIndex < openingElement.attributes.length; attributeIndex++) {
|
|
10797
10831
|
const attribute = openingElement.attributes[attributeIndex];
|
|
10798
10832
|
if (isNodeOfType(attribute, "JSXAttribute")) {
|
|
@@ -10800,11 +10834,9 @@ const checkKeyBeforeSpread = (context, openingElement) => {
|
|
|
10800
10834
|
keyIndex = attributeIndex;
|
|
10801
10835
|
keyAttribute = attribute;
|
|
10802
10836
|
}
|
|
10803
|
-
} else if (isNodeOfType(attribute, "JSXSpreadAttribute"))
|
|
10804
|
-
if (firstSpreadIndex === null) firstSpreadIndex = attributeIndex;
|
|
10805
|
-
}
|
|
10837
|
+
} else if (isNodeOfType(attribute, "JSXSpreadAttribute") && spreadCanOverwriteKey(attribute)) lastOverwritingSpreadIndex = attributeIndex;
|
|
10806
10838
|
}
|
|
10807
|
-
if (keyIndex !== null &&
|
|
10839
|
+
if (keyIndex !== null && lastOverwritingSpreadIndex !== null && lastOverwritingSpreadIndex > keyIndex && keyAttribute) context.report({
|
|
10808
10840
|
node: keyAttribute,
|
|
10809
10841
|
message: KEY_BEFORE_SPREAD
|
|
10810
10842
|
});
|
|
@@ -27010,7 +27042,7 @@ const preferHtmlDialog = defineRule({
|
|
|
27010
27042
|
});
|
|
27011
27043
|
//#endregion
|
|
27012
27044
|
//#region src/plugin/utils/function-returns-object-literal.ts
|
|
27013
|
-
const unwrapExpression = (node) => {
|
|
27045
|
+
const unwrapExpression$1 = (node) => {
|
|
27014
27046
|
let current = node;
|
|
27015
27047
|
for (;;) {
|
|
27016
27048
|
if ((current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression") && "expression" in current && isAstNode(current.expression)) {
|
|
@@ -27023,7 +27055,7 @@ const unwrapExpression = (node) => {
|
|
|
27023
27055
|
const doesFunctionReturnsObjectLiteral = (functionNode) => {
|
|
27024
27056
|
if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
|
|
27025
27057
|
const body = functionNode.body;
|
|
27026
|
-
if (body && body.type !== "BlockStatement") return unwrapExpression(body).type === "ObjectExpression";
|
|
27058
|
+
if (body && body.type !== "BlockStatement") return unwrapExpression$1(body).type === "ObjectExpression";
|
|
27027
27059
|
}
|
|
27028
27060
|
const body = functionNode.body;
|
|
27029
27061
|
if (!body || body.type !== "BlockStatement") return false;
|
|
@@ -27031,7 +27063,7 @@ const doesFunctionReturnsObjectLiteral = (functionNode) => {
|
|
|
27031
27063
|
const visit = (node) => {
|
|
27032
27064
|
if (returnsObject) return;
|
|
27033
27065
|
if (node.type === "ReturnStatement" && "argument" in node && node.argument != null) {
|
|
27034
|
-
if (unwrapExpression(node.argument).type === "ObjectExpression") returnsObject = true;
|
|
27066
|
+
if (unwrapExpression$1(node.argument).type === "ObjectExpression") returnsObject = true;
|
|
27035
27067
|
return;
|
|
27036
27068
|
}
|
|
27037
27069
|
const nodeRecord = node;
|
|
@@ -29463,6 +29495,19 @@ const REACT_NATIVE_TEXT_COMPONENTS = new Set([
|
|
|
29463
29495
|
"H5",
|
|
29464
29496
|
"H6"
|
|
29465
29497
|
]);
|
|
29498
|
+
const REACT_NATIVE_RAW_TEXT_HOST_COMPONENTS = new Set([
|
|
29499
|
+
"View",
|
|
29500
|
+
"ScrollView",
|
|
29501
|
+
"SafeAreaView",
|
|
29502
|
+
"KeyboardAvoidingView",
|
|
29503
|
+
"ImageBackground",
|
|
29504
|
+
"Modal",
|
|
29505
|
+
"Pressable",
|
|
29506
|
+
"TouchableOpacity",
|
|
29507
|
+
"TouchableHighlight",
|
|
29508
|
+
"TouchableWithoutFeedback",
|
|
29509
|
+
"TouchableNativeFeedback"
|
|
29510
|
+
]);
|
|
29466
29511
|
const REACT_NATIVE_TEXT_COMPONENT_KEYWORDS = new Set([
|
|
29467
29512
|
"Text",
|
|
29468
29513
|
"Title",
|
|
@@ -29475,7 +29520,11 @@ const REACT_NATIVE_TEXT_COMPONENT_KEYWORDS = new Set([
|
|
|
29475
29520
|
"Description",
|
|
29476
29521
|
"Body"
|
|
29477
29522
|
]);
|
|
29478
|
-
const REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS = new Set([
|
|
29523
|
+
const REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS = new Set([
|
|
29524
|
+
"Fragment",
|
|
29525
|
+
"fbt",
|
|
29526
|
+
"fbs"
|
|
29527
|
+
]);
|
|
29479
29528
|
const DEPRECATED_RN_MODULE_REPLACEMENTS = new Map([
|
|
29480
29529
|
["AsyncStorage", "@react-native-async-storage/async-storage"],
|
|
29481
29530
|
["Picker", "@react-native-picker/picker"],
|
|
@@ -30433,23 +30482,29 @@ const jsxRootForwardsChildrenIntoText = (jsxRoot, bindings, isTextHandlingElemen
|
|
|
30433
30482
|
return didForwardIntoText;
|
|
30434
30483
|
};
|
|
30435
30484
|
const isMeaningfulJsxChild = (child) => !isNodeOfType(child, "JSXText") || Boolean(child.value?.trim());
|
|
30436
|
-
const
|
|
30437
|
-
let
|
|
30485
|
+
const jsxRootForwardsChildren = (jsxRoot, bindings, isTextHandlingElement, countsAsForwardTarget) => {
|
|
30486
|
+
let didForward = false;
|
|
30438
30487
|
walkAst(jsxRoot, (node) => {
|
|
30439
|
-
if (
|
|
30488
|
+
if (didForward || isFunctionNode(node)) return false;
|
|
30440
30489
|
if (!isNodeOfType(node, "JSXElement") && !isNodeOfType(node, "JSXFragment")) return;
|
|
30441
30490
|
if (isNodeOfType(node, "JSXElement")) {
|
|
30442
30491
|
const elementName = resolveJsxElementName(node.openingElement);
|
|
30443
30492
|
if (elementName && isTextHandlingElement(elementName)) return false;
|
|
30444
|
-
if (!(node.children ?? []).some(isMeaningfulJsxChild) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
|
|
30445
|
-
|
|
30493
|
+
if (!(node.children ?? []).some(isMeaningfulJsxChild) && countsAsForwardTarget(node) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
|
|
30494
|
+
didForward = true;
|
|
30446
30495
|
return;
|
|
30447
30496
|
}
|
|
30448
30497
|
}
|
|
30449
|
-
|
|
30498
|
+
if (countsAsForwardTarget(node) && (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings))) didForward = true;
|
|
30450
30499
|
});
|
|
30451
|
-
return
|
|
30500
|
+
return didForward;
|
|
30452
30501
|
};
|
|
30502
|
+
const jsxRootRendersChildrenOutsideText = (jsxRoot, bindings, isTextHandlingElement) => jsxRootForwardsChildren(jsxRoot, bindings, isTextHandlingElement, () => true);
|
|
30503
|
+
const jsxRootRendersChildrenIntoNonTextHost = (jsxRoot, bindings, isTextHandlingElement, isNonTextHostElement) => jsxRootForwardsChildren(jsxRoot, bindings, isTextHandlingElement, (node) => {
|
|
30504
|
+
if (!isNodeOfType(node, "JSXElement")) return false;
|
|
30505
|
+
const elementName = resolveJsxElementName(node.openingElement);
|
|
30506
|
+
return elementName !== null && isNonTextHostElement(elementName);
|
|
30507
|
+
});
|
|
30453
30508
|
const resolveStyledFactoryBaseName = (definitionNode) => {
|
|
30454
30509
|
let current = stripParenExpression(definitionNode);
|
|
30455
30510
|
while (current) {
|
|
@@ -30486,49 +30541,71 @@ const resolveClassRenderFunction = (classNode) => {
|
|
|
30486
30541
|
}
|
|
30487
30542
|
return null;
|
|
30488
30543
|
};
|
|
30489
|
-
const
|
|
30490
|
-
if (!componentName || !isReactComponentName(componentName)) return;
|
|
30491
|
-
if (wrappers.has(componentName)) return;
|
|
30492
|
-
if (!definitionNode) return;
|
|
30544
|
+
const classifyChildrenForwarding = (definitionNode, isTextHandlingElement, isNonTextHostElement) => {
|
|
30493
30545
|
const unwrapped = unwrapComponentDefinition(definitionNode);
|
|
30494
30546
|
const styledBaseName = resolveStyledFactoryBaseName(unwrapped);
|
|
30495
|
-
if (styledBaseName
|
|
30496
|
-
|
|
30497
|
-
return;
|
|
30547
|
+
if (styledBaseName) {
|
|
30548
|
+
if (isTextHandlingElement(styledBaseName)) return "text";
|
|
30549
|
+
if (isNonTextHostElement(styledBaseName)) return "nonText";
|
|
30550
|
+
return "unknown";
|
|
30498
30551
|
}
|
|
30499
30552
|
const functionNode = resolveClassRenderFunction(unwrapped) ?? (isFunctionNode(unwrapped) ? unwrapped : null);
|
|
30500
|
-
if (!functionNode) return;
|
|
30553
|
+
if (!functionNode) return "unknown";
|
|
30501
30554
|
const bindings = resolveParamChildrenBindings(functionNode);
|
|
30502
30555
|
collectChildrenAliases(functionNode, bindings);
|
|
30503
30556
|
const jsxRoots = collectReturnedJsxRoots(functionNode);
|
|
30504
|
-
if (jsxRoots.some((jsxRoot) =>
|
|
30557
|
+
if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenIntoNonTextHost(jsxRoot, bindings, isTextHandlingElement, isNonTextHostElement))) return "nonText";
|
|
30558
|
+
if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenOutsideText(jsxRoot, bindings, isTextHandlingElement))) return "unknown";
|
|
30505
30559
|
for (const jsxRoot of jsxRoots) {
|
|
30506
30560
|
if (isNodeOfType(jsxRoot, "JSXElement")) {
|
|
30507
30561
|
const rootName = resolveJsxElementName(jsxRoot.openingElement);
|
|
30508
|
-
if (rootName && isTextHandlingElement(rootName))
|
|
30509
|
-
wrappers.add(componentName);
|
|
30510
|
-
return;
|
|
30511
|
-
}
|
|
30512
|
-
}
|
|
30513
|
-
if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) {
|
|
30514
|
-
wrappers.add(componentName);
|
|
30515
|
-
return;
|
|
30562
|
+
if (rootName && isTextHandlingElement(rootName)) return "text";
|
|
30516
30563
|
}
|
|
30564
|
+
if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) return "text";
|
|
30517
30565
|
}
|
|
30566
|
+
return "unknown";
|
|
30567
|
+
};
|
|
30568
|
+
const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, isNonTextHostElement, wrappers, nonTextWrappers) => {
|
|
30569
|
+
if (!componentName || !isReactComponentName(componentName)) return;
|
|
30570
|
+
if (wrappers.has(componentName)) return;
|
|
30571
|
+
if (!definitionNode) return;
|
|
30572
|
+
const kind = classifyChildrenForwarding(definitionNode, isTextHandlingElement, isNonTextHostElement);
|
|
30573
|
+
if (kind === "text") wrappers.add(componentName);
|
|
30574
|
+
else if (kind === "nonText") nonTextWrappers.add(componentName);
|
|
30518
30575
|
};
|
|
30519
30576
|
const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
|
|
30520
|
-
const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
|
|
30577
|
+
const collectTextWrapperComponents = (programNode, isTextHandlingRoot, isNonTextHostRoot) => {
|
|
30521
30578
|
const wrappers = /* @__PURE__ */ new Set();
|
|
30579
|
+
const nonTextWrappers = /* @__PURE__ */ new Set();
|
|
30522
30580
|
const isTextHandlingElement = (elementName) => isTextHandlingRoot(elementName) || wrappers.has(elementName);
|
|
30581
|
+
const isNonTextHostElement = (elementName) => isNonTextHostRoot(elementName) || nonTextWrappers.has(elementName);
|
|
30582
|
+
const recordDeclaration = (componentName, definitionNode) => recordWrapperFromDeclaration(componentName, definitionNode, isTextHandlingElement, isNonTextHostElement, wrappers, nonTextWrappers);
|
|
30523
30583
|
for (let pass = 0; pass < MAX_TRANSITIVE_WRAPPER_PASSES; pass += 1) {
|
|
30524
|
-
const
|
|
30584
|
+
const wrappersSizeBeforePass = wrappers.size;
|
|
30585
|
+
const nonTextSizeBeforePass = nonTextWrappers.size;
|
|
30525
30586
|
walkAst(programNode, (node) => {
|
|
30526
|
-
if (isNodeOfType(node, "VariableDeclarator"))
|
|
30527
|
-
else if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "ClassDeclaration"))
|
|
30587
|
+
if (isNodeOfType(node, "VariableDeclarator")) recordDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init ?? null);
|
|
30588
|
+
else if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "ClassDeclaration")) recordDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node);
|
|
30528
30589
|
});
|
|
30529
|
-
if (wrappers.size ===
|
|
30590
|
+
if (wrappers.size === wrappersSizeBeforePass && nonTextWrappers.size === nonTextSizeBeforePass) break;
|
|
30530
30591
|
}
|
|
30531
|
-
|
|
30592
|
+
for (const wrapperName of wrappers) nonTextWrappers.delete(wrapperName);
|
|
30593
|
+
return {
|
|
30594
|
+
textWrappers: wrappers,
|
|
30595
|
+
nonTextWrappers
|
|
30596
|
+
};
|
|
30597
|
+
};
|
|
30598
|
+
//#endregion
|
|
30599
|
+
//#region src/plugin/rules/react-native/utils/resolve-imported-component-forwarding.ts
|
|
30600
|
+
const resolveImportedComponentForwarding = (contextNode, fromFilename, localName, isTextHandlingRoot, isNonTextHostRoot) => {
|
|
30601
|
+
const binding = getImportBindingForName(contextNode, localName);
|
|
30602
|
+
if (!binding || binding.isNamespace || binding.exportedName === null) return null;
|
|
30603
|
+
const resolvedNode = resolveCrossFileFunctionExport(fromFilename, binding.source, binding.exportedName);
|
|
30604
|
+
if (!resolvedNode) return null;
|
|
30605
|
+
const moduleProgram = findProgramRoot(resolvedNode);
|
|
30606
|
+
if (moduleProgram === null) return classifyChildrenForwarding(resolvedNode, isTextHandlingRoot, isNonTextHostRoot);
|
|
30607
|
+
const { textWrappers, nonTextWrappers } = collectTextWrapperComponents(moduleProgram, isTextHandlingRoot, isNonTextHostRoot);
|
|
30608
|
+
return classifyChildrenForwarding(resolvedNode, (elementName) => isTextHandlingRoot(elementName) || textWrappers.has(elementName), (elementName) => isNonTextHostRoot(elementName) || nonTextWrappers.has(elementName));
|
|
30532
30609
|
};
|
|
30533
30610
|
//#endregion
|
|
30534
30611
|
//#region src/plugin/rules/react-native/utils/is-expo-ui-component-element.ts
|
|
@@ -30599,19 +30676,37 @@ const rnNoRawText = defineRule({
|
|
|
30599
30676
|
recommendation: "Text outside a `<Text>` component crashes on React Native. Wrap it like `<Text>{value}</Text>`.",
|
|
30600
30677
|
create: (context) => {
|
|
30601
30678
|
let isDomComponentFile = false;
|
|
30602
|
-
let
|
|
30679
|
+
let autoDetectedTextWrappers = /* @__PURE__ */ new Set();
|
|
30680
|
+
let autoDetectedNonTextWrappers = /* @__PURE__ */ new Set();
|
|
30681
|
+
const isNonTextHostName = (elementName) => !isReactComponentName(elementName) || REACT_NATIVE_RAW_TEXT_HOST_COMPONENTS.has(elementName);
|
|
30682
|
+
const isRawTextReportTarget = (elementName) => elementName !== null && (isNonTextHostName(elementName) || autoDetectedNonTextWrappers.has(elementName));
|
|
30683
|
+
const importedNonTextWrapperCache = /* @__PURE__ */ new Map();
|
|
30684
|
+
const isImportedNonTextWrapper = (elementName, contextNode) => {
|
|
30685
|
+
if (elementName === null || !isReactComponentName(elementName)) return false;
|
|
30686
|
+
const { filename } = context;
|
|
30687
|
+
if (filename === void 0) return false;
|
|
30688
|
+
const cached = importedNonTextWrapperCache.get(elementName);
|
|
30689
|
+
if (cached !== void 0) return cached;
|
|
30690
|
+
const isNonTextWrapper = resolveImportedComponentForwarding(contextNode, filename, elementName, isTextHandlingComponent, isNonTextHostName) === "nonText";
|
|
30691
|
+
importedNonTextWrapperCache.set(elementName, isNonTextWrapper);
|
|
30692
|
+
return isNonTextWrapper;
|
|
30693
|
+
};
|
|
30603
30694
|
return {
|
|
30604
30695
|
Program(programNode) {
|
|
30605
30696
|
isDomComponentFile = hasDirective(programNode, "use dom");
|
|
30606
|
-
|
|
30697
|
+
const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
|
|
30698
|
+
autoDetectedTextWrappers = childrenForwarding.textWrappers;
|
|
30699
|
+
autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
|
|
30607
30700
|
},
|
|
30608
30701
|
JSXElement(node) {
|
|
30609
30702
|
if (isDomComponentFile) return;
|
|
30610
30703
|
const elementName = resolveTextBoundaryName(node.openingElement);
|
|
30611
|
-
if (elementName && (isTextHandlingComponent(elementName) ||
|
|
30704
|
+
if (elementName && (isTextHandlingComponent(elementName) || autoDetectedTextWrappers.has(elementName))) return;
|
|
30612
30705
|
if (isExpoUiComponentElement(node.openingElement, node, "ListItem")) return;
|
|
30613
30706
|
if (isInsidePlatformOsWebBranch(node)) return;
|
|
30614
30707
|
if (isTransparentTextWrapper(elementName) && isInsideTextHandlingComponent(node)) return;
|
|
30708
|
+
if (!(node.children ?? []).some(isRawTextContent)) return;
|
|
30709
|
+
if (!isRawTextReportTarget(elementName) && !isImportedNonTextWrapper(elementName, node)) return;
|
|
30615
30710
|
for (const child of node.children ?? []) {
|
|
30616
30711
|
if (!isRawTextContent(child)) continue;
|
|
30617
30712
|
context.report({
|
|
@@ -35155,6 +35250,67 @@ const isAuthGuardName = (calleeName) => {
|
|
|
35155
35250
|
return false;
|
|
35156
35251
|
};
|
|
35157
35252
|
//#endregion
|
|
35253
|
+
//#region src/plugin/utils/is-non-privileged-server-action.ts
|
|
35254
|
+
const NON_DATA_EFFECT_FUNCTION_NAMES = new Set([...CACHE_REVALIDATION_FUNCTION_NAMES, ...NEXTJS_NAVIGATION_FUNCTIONS]);
|
|
35255
|
+
const isCacheOrNavigationCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && NON_DATA_EFFECT_FUNCTION_NAMES.has(node.callee.name);
|
|
35256
|
+
const unwrapExpression = (node) => {
|
|
35257
|
+
let current = node;
|
|
35258
|
+
while (current) {
|
|
35259
|
+
if (isNodeOfType(current, "TSAsExpression") || isNodeOfType(current, "TSNonNullExpression") || isNodeOfType(current, "TSSatisfiesExpression") || isNodeOfType(current, "ChainExpression")) {
|
|
35260
|
+
current = current.expression;
|
|
35261
|
+
continue;
|
|
35262
|
+
}
|
|
35263
|
+
if (isNodeOfType(current, "SequenceExpression")) {
|
|
35264
|
+
current = current.expressions?.[current.expressions.length - 1];
|
|
35265
|
+
continue;
|
|
35266
|
+
}
|
|
35267
|
+
return current;
|
|
35268
|
+
}
|
|
35269
|
+
return null;
|
|
35270
|
+
};
|
|
35271
|
+
const isDataExposingValue = (node) => {
|
|
35272
|
+
const value = unwrapExpression(node);
|
|
35273
|
+
if (!value) return false;
|
|
35274
|
+
if (isCacheOrNavigationCall(value)) return false;
|
|
35275
|
+
return !isLiteralOnlyExpression(value);
|
|
35276
|
+
};
|
|
35277
|
+
const isLiteralOnlyExpression = (node) => {
|
|
35278
|
+
if (!node) return false;
|
|
35279
|
+
if (isNodeOfType(node, "Literal")) return true;
|
|
35280
|
+
if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every(isLiteralOnlyExpression);
|
|
35281
|
+
if (isNodeOfType(node, "UnaryExpression")) return isLiteralOnlyExpression(node.argument);
|
|
35282
|
+
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => element === null || !isNodeOfType(element, "SpreadElement") && isLiteralOnlyExpression(element));
|
|
35283
|
+
if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => isNodeOfType(property, "Property") && (!property.computed || isLiteralOnlyExpression(property.key)) && isLiteralOnlyExpression(property.value));
|
|
35284
|
+
return false;
|
|
35285
|
+
};
|
|
35286
|
+
const getReturnedOrThrownArgument = (node) => {
|
|
35287
|
+
if (isNodeOfType(node, "ReturnStatement")) return node.argument ?? null;
|
|
35288
|
+
if (isNodeOfType(node, "ThrowStatement")) return node.argument ?? null;
|
|
35289
|
+
return null;
|
|
35290
|
+
};
|
|
35291
|
+
const isDataExposingReturnOrThrow = (node) => isDataExposingValue(getReturnedOrThrownArgument(node));
|
|
35292
|
+
const isPrivilegedEffect = (node) => isNodeOfType(node, "CallExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "UnaryExpression") && node.operator === "delete" || isDataExposingReturnOrThrow(node);
|
|
35293
|
+
const isNonPrivilegedServerAction = (functionNode) => {
|
|
35294
|
+
const functionBody = functionNode.body;
|
|
35295
|
+
if (!functionBody) return false;
|
|
35296
|
+
if (!isNodeOfType(functionBody, "BlockStatement") && isDataExposingValue(functionBody)) return false;
|
|
35297
|
+
let hasNonDataEffectCall = false;
|
|
35298
|
+
let hasPrivilegedEffect = false;
|
|
35299
|
+
walkAst(functionBody, (child) => {
|
|
35300
|
+
if (hasPrivilegedEffect) return false;
|
|
35301
|
+
if (child !== functionBody && isFunctionLike$2(child)) return false;
|
|
35302
|
+
if (isCacheOrNavigationCall(child)) {
|
|
35303
|
+
hasNonDataEffectCall = true;
|
|
35304
|
+
return;
|
|
35305
|
+
}
|
|
35306
|
+
if (isPrivilegedEffect(child)) {
|
|
35307
|
+
hasPrivilegedEffect = true;
|
|
35308
|
+
return false;
|
|
35309
|
+
}
|
|
35310
|
+
});
|
|
35311
|
+
return hasNonDataEffectCall && !hasPrivilegedEffect;
|
|
35312
|
+
};
|
|
35313
|
+
//#endregion
|
|
35158
35314
|
//#region src/plugin/rules/server/server-auth-actions.ts
|
|
35159
35315
|
const isAsyncFunctionLikeNode = (node) => {
|
|
35160
35316
|
if (!node) return false;
|
|
@@ -35229,6 +35385,7 @@ const getAuthScanRoots = (functionNode) => {
|
|
|
35229
35385
|
const inspectServerAction = (candidate, fileHasUseServerDirective, allowedFunctionNames, context) => {
|
|
35230
35386
|
if (!(fileHasUseServerDirective || hasUseServerDirective(candidate.functionNode))) return;
|
|
35231
35387
|
if (containsAuthCheck(getAuthScanRoots(candidate.functionNode), allowedFunctionNames, GENERIC_AUTH_METHOD_NAMES)) return;
|
|
35388
|
+
if (isNonPrivilegedServerAction(candidate.functionNode)) return;
|
|
35232
35389
|
context.report({
|
|
35233
35390
|
node: candidate.reportNode,
|
|
35234
35391
|
message: `Anyone can call server action "${candidate.displayName}" without logging in, since it has no auth check.`
|
|
@@ -35854,22 +36011,6 @@ const isSupabaseMigrationPath = (relativePath) => /(?:^|\/)supabase\/(?:migratio
|
|
|
35854
36011
|
//#endregion
|
|
35855
36012
|
//#region src/plugin/rules/security-scan/utils/is-sql-path.ts
|
|
35856
36013
|
const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || isSupabaseMigrationPath(relativePath);
|
|
35857
|
-
const supabaseRlsPolicyRisk = defineRule({
|
|
35858
|
-
id: "supabase-rls-policy-risk",
|
|
35859
|
-
title: "Permissive Supabase RLS policy",
|
|
35860
|
-
severity: "error",
|
|
35861
|
-
recommendation: "Keep public-read policies explicit, but gate inserts, updates, deletes, and service-role bypasses behind `auth.uid()` plus trusted tenant membership.",
|
|
35862
|
-
scan: scanByPattern({
|
|
35863
|
-
shouldScan: (file) => isSqlPath(file.relativePath),
|
|
35864
|
-
pattern: [
|
|
35865
|
-
/disable\s+row\s+level\s+security/i,
|
|
35866
|
-
/create\s+policy[\s\S]{0,700}auth\.role\(\)\s*=\s*["']service_role["']/i,
|
|
35867
|
-
/create\s+policy[\s\S]{0,700}\bfor\s+(?:all|insert|update|delete)\b[\s\S]{0,500}\b(?:using|with\s+check)\s*\(\s*true\s*\)/i,
|
|
35868
|
-
/create\s+policy(?:(?!\bfor\s+select\b)[\s\S]){0,700}\b(?:using|with\s+check)\s*\(\s*true\s*\)/i
|
|
35869
|
-
],
|
|
35870
|
-
message: "Supabase policy SQL disables RLS, permits writes broadly, or references a service-role bypass."
|
|
35871
|
-
})
|
|
35872
|
-
});
|
|
35873
36014
|
//#endregion
|
|
35874
36015
|
//#region src/plugin/rules/security-scan/utils/sanitize-sql-for-scan.ts
|
|
35875
36016
|
const DOLLAR_QUOTE_TAG_PATTERN = /^\$[A-Za-z_]?\w*\$/;
|
|
@@ -36046,6 +36187,60 @@ const sanitizeSqlForScan = (content) => {
|
|
|
36046
36187
|
return characters.join("");
|
|
36047
36188
|
};
|
|
36048
36189
|
//#endregion
|
|
36190
|
+
//#region src/plugin/rules/security-scan/supabase-rls-policy-risk.ts
|
|
36191
|
+
const DISABLED_RLS_PATTERN = /disable\s+row\s+level\s+security/i;
|
|
36192
|
+
const SERVICE_ROLE_BODY_BYPASS_PATTERN = /auth\.role\(\)\s*=\s*["']service_role["']/i;
|
|
36193
|
+
const CREATE_POLICY_PATTERN = /create\s+policy/gi;
|
|
36194
|
+
const STATEMENT_END_PATTERN = /;|create\s+policy/i;
|
|
36195
|
+
const PERMISSIVE_TRUE_PATTERN = /\b(?:using|with\s+check)\s*\(\s*true\s*\)/i;
|
|
36196
|
+
const FOR_SELECT_PATTERN = /\bfor\s+select\b/i;
|
|
36197
|
+
const TO_CLAUSE_PATTERN = /\bto\s+([\s\S]+?)(?=\s+(?:using|with\s+check|as|for)\b|;|$)/i;
|
|
36198
|
+
const SERVER_ONLY_ROLES = new Set([
|
|
36199
|
+
"service_role",
|
|
36200
|
+
"postgres",
|
|
36201
|
+
"supabase_admin"
|
|
36202
|
+
]);
|
|
36203
|
+
const isServerOnlyScoped = (statement) => {
|
|
36204
|
+
const toClause = TO_CLAUSE_PATTERN.exec(statement);
|
|
36205
|
+
if (toClause === null) return false;
|
|
36206
|
+
const roles = toClause[1].split(",").map((role) => role.trim().replace(/["'`]/g, "").toLowerCase()).filter(Boolean);
|
|
36207
|
+
return roles.length > 0 && roles.every((role) => SERVER_ONLY_ROLES.has(role));
|
|
36208
|
+
};
|
|
36209
|
+
const isRiskyPolicyStatement = (statement, rawStatement) => {
|
|
36210
|
+
if (SERVICE_ROLE_BODY_BYPASS_PATTERN.test(rawStatement)) return true;
|
|
36211
|
+
if (!PERMISSIVE_TRUE_PATTERN.test(statement)) return false;
|
|
36212
|
+
if (FOR_SELECT_PATTERN.test(statement)) return false;
|
|
36213
|
+
return !isServerOnlyScoped(statement);
|
|
36214
|
+
};
|
|
36215
|
+
const POLICY_RISK_MESSAGE = "Supabase policy SQL disables RLS, permits writes broadly, or references a service-role bypass.";
|
|
36216
|
+
const supabaseRlsPolicyRisk = defineRule({
|
|
36217
|
+
id: "supabase-rls-policy-risk",
|
|
36218
|
+
title: "Permissive Supabase RLS policy",
|
|
36219
|
+
severity: "error",
|
|
36220
|
+
recommendation: "Keep public-read policies explicit, but gate inserts, updates, deletes, and service-role bypasses behind `auth.uid()` plus trusted tenant membership.",
|
|
36221
|
+
scan: (file) => {
|
|
36222
|
+
if (!isSqlPath(file.relativePath)) return [];
|
|
36223
|
+
const content = sanitizeSqlForScan(file.content);
|
|
36224
|
+
let earliestRiskIndex = content.search(DISABLED_RLS_PATTERN);
|
|
36225
|
+
CREATE_POLICY_PATTERN.lastIndex = 0;
|
|
36226
|
+
for (let policyMatch = CREATE_POLICY_PATTERN.exec(content); policyMatch !== null; policyMatch = CREATE_POLICY_PATTERN.exec(content)) {
|
|
36227
|
+
const afterKeyword = policyMatch.index + policyMatch[0].length;
|
|
36228
|
+
const terminatorOffset = content.slice(afterKeyword).search(STATEMENT_END_PATTERN);
|
|
36229
|
+
const statementEnd = terminatorOffset < 0 ? content.length : afterKeyword + terminatorOffset;
|
|
36230
|
+
if (!isRiskyPolicyStatement(content.slice(policyMatch.index, statementEnd), file.content.slice(policyMatch.index, statementEnd))) continue;
|
|
36231
|
+
if (earliestRiskIndex < 0 || policyMatch.index < earliestRiskIndex) earliestRiskIndex = policyMatch.index;
|
|
36232
|
+
break;
|
|
36233
|
+
}
|
|
36234
|
+
if (earliestRiskIndex < 0) return [];
|
|
36235
|
+
const { line, column } = getLocationAtIndex(content, earliestRiskIndex);
|
|
36236
|
+
return [{
|
|
36237
|
+
message: POLICY_RISK_MESSAGE,
|
|
36238
|
+
line,
|
|
36239
|
+
column
|
|
36240
|
+
}];
|
|
36241
|
+
}
|
|
36242
|
+
});
|
|
36243
|
+
//#endregion
|
|
36049
36244
|
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
36050
36245
|
const CREATE_PUBLIC_TABLE_PATTERN = /create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)\s*\.)(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi;
|
|
36051
36246
|
const enableRlsForTablePattern = (tableName) => new RegExp(`alter\\s+table\\s+(?:if\\s+exists\\s+)?(?:only\\s+)?(?:public\\s*\\.\\s*)?["\`]?${escapeRegExp(tableName)}["\`]?\\s+(?:force\\s+)?enable\\s+row\\s+level\\s+security`, "i");
|
|
@@ -42536,6 +42731,7 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
42536
42731
|
"nextjs-missing-metadata",
|
|
42537
42732
|
"nextjs-no-use-search-params-without-suspense",
|
|
42538
42733
|
"no-mutating-reducer-state",
|
|
42734
|
+
"rn-no-raw-text",
|
|
42539
42735
|
"rn-prefer-expo-image"
|
|
42540
42736
|
]);
|
|
42541
42737
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-react-doctor",
|
|
3
|
-
"version": "0.5.8-dev.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.8-dev.5639b1e",
|
|
4
|
+
"description": "React Doctor rules for oxlint.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
7
7
|
"linter",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"bugs": {
|
|
22
22
|
"url": "https://github.com/millionco/react-doctor/issues"
|
|
23
23
|
},
|
|
24
|
-
"license": "
|
|
24
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
25
25
|
"author": "Aiden Bai",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"dist/**/*.js",
|
|
33
|
-
"dist/**/*.d.ts"
|
|
33
|
+
"dist/**/*.d.ts",
|
|
34
|
+
"LICENSE"
|
|
34
35
|
],
|
|
35
36
|
"type": "module",
|
|
36
37
|
"sideEffects": false,
|