oxlint-plugin-react-doctor 0.7.6-dev.fbd85e3 → 0.7.6
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 +68 -185
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6929,30 +6929,6 @@ const corsCookieTrustRisk = defineRule({
|
|
|
6929
6929
|
})
|
|
6930
6930
|
});
|
|
6931
6931
|
//#endregion
|
|
6932
|
-
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
6933
|
-
const findMatchingBracket = (content, openIndex) => {
|
|
6934
|
-
const open = content[openIndex];
|
|
6935
|
-
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
6936
|
-
if (close === "") return -1;
|
|
6937
|
-
let depth = 0;
|
|
6938
|
-
let stringDelimiter = null;
|
|
6939
|
-
for (let index = openIndex; index < content.length; index += 1) {
|
|
6940
|
-
const character = content[index];
|
|
6941
|
-
if (stringDelimiter !== null) {
|
|
6942
|
-
if (character === "\\") index += 1;
|
|
6943
|
-
else if (character === stringDelimiter) stringDelimiter = null;
|
|
6944
|
-
continue;
|
|
6945
|
-
}
|
|
6946
|
-
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
6947
|
-
else if (character === open) depth += 1;
|
|
6948
|
-
else if (character === close) {
|
|
6949
|
-
depth -= 1;
|
|
6950
|
-
if (depth === 0) return index;
|
|
6951
|
-
}
|
|
6952
|
-
}
|
|
6953
|
-
return -1;
|
|
6954
|
-
};
|
|
6955
|
-
//#endregion
|
|
6956
6932
|
//#region src/plugin/rules/security-scan/dangerous-html-sink.ts
|
|
6957
6933
|
const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
|
|
6958
6934
|
const HTML_VALUE_START_PATTERN = /(?:__html\s*:|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(\s*[^,]*,|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\()\s*([\s\S]*)/;
|
|
@@ -6968,7 +6944,6 @@ const I18N_VALUE_PATTERN = /\b(?:t|i18n|translate|formatMessage|intl)\s*[.(]/;
|
|
|
6968
6944
|
const ESCAPING_SERIALIZER_CALL_PATTERN = /^(?:[\w$.]+\.)?(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*)\s*\(/;
|
|
6969
6945
|
const HIGHLIGHTER_LIBRARY_PATTERN = /\b(?:shiki|prism|hljs|highlightjs|getHighlighter|codeToHtml|codeToHast|refractor|lowlight|starry-night)\b|highlight\.js/i;
|
|
6970
6946
|
const SERIALIZER_ASSIGNMENT_PATTERN = /[:=]\s*[^\n;]*(?:\b(?:katex|shiki|hljs|prism|mermaid)\b|hast-util-to-html|renderHtmlFromRichText|(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast)\s*\()/i;
|
|
6971
|
-
const SERIALIZER_CALL_PROVENANCE_PATTERN = /\b(?:(?:katex|shiki|hljs|prism|mermaid|highlighter)[\w$]*\.(?:render\w*|highlight\w*|codeTo(?:Html|Hast))|(?:toHtml|render(?:Html|HTML)[A-Za-z]*|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*))\s*\(/i;
|
|
6972
6947
|
const BARE_IDENTIFIER_VALUE_PATTERN = /^[\w$]+\s*(?:[;,})\n]|$)/;
|
|
6973
6948
|
const IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN = /^[\w$]+\s*(?:\|\||\?\?)\s*(?:"[^"\n]*"|'[^'\n]*'|`[^`$]*`)\s*(?:[;,})\n]|$)/;
|
|
6974
6949
|
const MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN = /^[\w$]+(?:\.[\w$]+|\[[^\]]*\])+\s*(?:[;,})\n]|$)/;
|
|
@@ -7128,66 +7103,22 @@ const findContainingBlockEndIndex = (fileContent, targetIndex) => {
|
|
|
7128
7103
|
return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
|
|
7129
7104
|
};
|
|
7130
7105
|
const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
|
|
7131
|
-
const initializerPattern = new RegExp(`(const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
|
|
7106
|
+
const initializerPattern = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
|
|
7132
7107
|
let nearestDeclaration = null;
|
|
7133
7108
|
let nearestDeclarationIndex = -1;
|
|
7134
7109
|
for (const match of fileContent.matchAll(initializerPattern)) {
|
|
7135
7110
|
const declarationIndex = match.index;
|
|
7136
7111
|
if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
|
|
7137
7112
|
nearestDeclarationIndex = declarationIndex;
|
|
7138
|
-
const initializer = match[
|
|
7113
|
+
const initializer = match[1];
|
|
7139
7114
|
if (initializer === void 0) continue;
|
|
7140
7115
|
nearestDeclaration = {
|
|
7141
|
-
declarationIndex,
|
|
7142
7116
|
initializer,
|
|
7143
|
-
initializerStartIndex: declarationIndex + match[0].length - initializer.length
|
|
7144
|
-
isImmutable: match[1] === "const"
|
|
7117
|
+
initializerStartIndex: declarationIndex + match[0].length - initializer.length
|
|
7145
7118
|
};
|
|
7146
7119
|
}
|
|
7147
7120
|
return nearestDeclaration;
|
|
7148
7121
|
};
|
|
7149
|
-
const isDeclarationStable = (identifier, declaration, usageIndex, fileContent) => {
|
|
7150
|
-
if (declaration.isImmutable) return true;
|
|
7151
|
-
const textAfterInitializer = fileContent.slice(declaration.initializerStartIndex + declaration.initializer.length, usageIndex);
|
|
7152
|
-
return !new RegExp(`(?:^|[^\\w$.])${escapeRegExp(identifier)}\\s*=(?!=)`).test(textAfterInitializer);
|
|
7153
|
-
};
|
|
7154
|
-
const isTrustedHighlighterValue = (valueExpression, fileContent, sinkIndex) => {
|
|
7155
|
-
if (!/highlight/i.test(valueExpression)) return false;
|
|
7156
|
-
const identifier = valueExpression.match(/^([\w$]+)/)?.[1];
|
|
7157
|
-
if (identifier === void 0) return false;
|
|
7158
|
-
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7159
|
-
if (declaration !== null) return isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(declaration.initializer);
|
|
7160
|
-
return /highlighted/i.test(valueExpression) || HIGHLIGHTER_LIBRARY_PATTERN.test(fileContent);
|
|
7161
|
-
};
|
|
7162
|
-
const isExplicitlyTrustedHtmlValue = (valueExpression, fileContent, sinkIndex, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7163
|
-
const trimmedExpression = valueExpression.trim();
|
|
7164
|
-
const concatenatedParts = splitTopLevelByPlus(trimmedExpression);
|
|
7165
|
-
if (concatenatedParts.length > 1) return concatenatedParts.every((part) => isExplicitlyTrustedHtmlValue(part, fileContent, sinkIndex, visitedIdentifiers));
|
|
7166
|
-
const interpolationParts = [...trimmedExpression.matchAll(/\$\{([^}]*)\}/g)].flatMap((match) => match[1] === void 0 ? [] : [match[1]]);
|
|
7167
|
-
if (interpolationParts.length > 1) return interpolationParts.every((part) => isExplicitlyTrustedHtmlValue(part, fileContent, sinkIndex, visitedIdentifiers));
|
|
7168
|
-
const identifier = trimmedExpression.match(/^([\w$]+)(?:(?:\??\.[\w$]+)|(?:\[\s*(?:"[^"\n]*"|'[^'\n]*'|\d+)\s*\]))*$/)?.[1];
|
|
7169
|
-
if (identifier && !visitedIdentifiers.has(identifier)) {
|
|
7170
|
-
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7171
|
-
if (declaration && isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) {
|
|
7172
|
-
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7173
|
-
nextVisitedIdentifiers.add(identifier);
|
|
7174
|
-
return isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex, nextVisitedIdentifiers);
|
|
7175
|
-
}
|
|
7176
|
-
}
|
|
7177
|
-
if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`) || MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`) || SANITIZER_PATTERN.test(trimmedExpression) || ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression) || I18N_VALUE_PATTERN.test(trimmedExpression) || ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression) || isTrustedHighlighterValue(trimmedExpression, fileContent, sinkIndex)) return true;
|
|
7178
|
-
return false;
|
|
7179
|
-
};
|
|
7180
|
-
const doesExpressionAliasIdentifier = (expression, targetIdentifier, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7181
|
-
const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
|
|
7182
|
-
if (!identifier) return false;
|
|
7183
|
-
if (identifier === targetIdentifier) return true;
|
|
7184
|
-
if (visitedIdentifiers.has(identifier)) return false;
|
|
7185
|
-
const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
|
|
7186
|
-
if (!declaration?.isImmutable) return false;
|
|
7187
|
-
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7188
|
-
nextVisitedIdentifiers.add(identifier);
|
|
7189
|
-
return doesExpressionAliasIdentifier(declaration.initializer, targetIdentifier, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
|
|
7190
|
-
};
|
|
7191
7122
|
const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
|
|
7192
7123
|
const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
|
|
7193
7124
|
let closestSource = null;
|
|
@@ -7195,16 +7126,12 @@ const findContainingFunctionParameterSource = (identifier, sinkIndex, fileConten
|
|
|
7195
7126
|
for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
|
|
7196
7127
|
const matchIndex = match.index;
|
|
7197
7128
|
if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
|
|
7198
|
-
|
|
7199
|
-
if (bodyEndIndex < sinkIndex) continue;
|
|
7129
|
+
if (findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{")) < sinkIndex) continue;
|
|
7200
7130
|
const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
|
|
7201
7131
|
if (parameterIndex < 0) continue;
|
|
7202
7132
|
closestStartIndex = matchIndex;
|
|
7203
|
-
const functionName = match[1] ?? "";
|
|
7204
7133
|
closestSource = {
|
|
7205
|
-
|
|
7206
|
-
declarationNameIndex: matchIndex + match[0].indexOf(functionName),
|
|
7207
|
-
functionName,
|
|
7134
|
+
functionName: match[1] ?? "",
|
|
7208
7135
|
parameterIndex
|
|
7209
7136
|
};
|
|
7210
7137
|
}
|
|
@@ -7235,56 +7162,28 @@ const splitTopLevelArguments = (argumentText) => {
|
|
|
7235
7162
|
argumentsList.push(argumentText.slice(startIndex).trim());
|
|
7236
7163
|
return argumentsList;
|
|
7237
7164
|
};
|
|
7238
|
-
const
|
|
7239
|
-
const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
|
|
7240
|
-
if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
|
|
7241
|
-
if (findContainingFunctionParameterSource(identifier, expressionIndex, fileContent) !== null) return true;
|
|
7242
|
-
const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
|
|
7243
|
-
if (!declaration || !isDeclarationStable(identifier, declaration, expressionIndex, fileContent)) return false;
|
|
7244
|
-
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7245
|
-
nextVisitedIdentifiers.add(identifier);
|
|
7246
|
-
return isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
|
|
7247
|
-
};
|
|
7248
|
-
const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers, visitedCallSites) => {
|
|
7165
|
+
const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers) => {
|
|
7249
7166
|
const trimmedExpression = expression.trim();
|
|
7250
|
-
if (
|
|
7251
|
-
|
|
7252
|
-
if (
|
|
7253
|
-
if (
|
|
7167
|
+
if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
|
|
7168
|
+
if (MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
|
|
7169
|
+
if (SANITIZER_PATTERN.test(trimmedExpression)) return false;
|
|
7170
|
+
if (ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression)) return false;
|
|
7171
|
+
if (I18N_VALUE_PATTERN.test(trimmedExpression)) return false;
|
|
7172
|
+
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression)) return false;
|
|
7173
|
+
if (HTML_TAINT_PATTERN.test(trimmedExpression)) return true;
|
|
7174
|
+
const identifier = trimmedExpression.match(/^([\w$]+)\s*(?:[;,})\n]|$)/)?.[1];
|
|
7175
|
+
if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
|
|
7254
7176
|
visitedIdentifiers.add(identifier);
|
|
7255
7177
|
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7178
|
+
if (declaration !== null && isHtmlTainted(declaration.initializer, fileContent, sinkIndex, visitedIdentifiers)) return true;
|
|
7256
7179
|
const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
|
|
7257
|
-
if (
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
if (
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
if (parameterSource !== null && parameterSource.functionName.length > 0) {
|
|
7265
|
-
const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(`, "g");
|
|
7266
|
-
let didInspectCallArgument = false;
|
|
7267
|
-
let didInspectOnlyExplicitlyTrustedArguments = true;
|
|
7268
|
-
for (const callMatch of fileContent.matchAll(callPattern)) {
|
|
7269
|
-
if (callMatch.index === parameterSource.declarationNameIndex || visitedCallSites.has(callMatch.index)) continue;
|
|
7270
|
-
const openingParenthesisIndex = callMatch.index + callMatch[0].lastIndexOf("(");
|
|
7271
|
-
const closingParenthesisIndex = findMatchingBracket(fileContent, openingParenthesisIndex);
|
|
7272
|
-
if (closingParenthesisIndex < 0) continue;
|
|
7273
|
-
const argument = splitTopLevelArguments(fileContent.slice(openingParenthesisIndex + 1, closingParenthesisIndex))[parameterSource.parameterIndex];
|
|
7274
|
-
if (argument === void 0) continue;
|
|
7275
|
-
if (callMatch.index >= parameterSource.declarationNameIndex && callMatch.index <= parameterSource.bodyEndIndex && doesExpressionAliasIdentifier(argument, identifier, callMatch.index, fileContent)) continue;
|
|
7276
|
-
didInspectCallArgument = true;
|
|
7277
|
-
didInspectOnlyExplicitlyTrustedArguments &&= isExplicitlyTrustedHtmlValue(argument, fileContent, callMatch.index);
|
|
7278
|
-
const callVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7279
|
-
callVisitedIdentifiers.delete(identifier);
|
|
7280
|
-
const nextVisitedCallSites = new Set(visitedCallSites);
|
|
7281
|
-
nextVisitedCallSites.add(callMatch.index);
|
|
7282
|
-
if (isHtmlTainted(argument, fileContent, callMatch.index, callVisitedIdentifiers, nextVisitedCallSites)) return true;
|
|
7283
|
-
}
|
|
7284
|
-
if (didInspectCallArgument) return !didInspectOnlyExplicitlyTrustedArguments && HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7285
|
-
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7286
|
-
}
|
|
7287
|
-
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7180
|
+
if (parameterSource === null || parameterSource.functionName.length === 0) return false;
|
|
7181
|
+
const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(([^)]*)\\)`, "g");
|
|
7182
|
+
for (const callMatch of fileContent.matchAll(callPattern)) {
|
|
7183
|
+
const argument = splitTopLevelArguments(callMatch[1] ?? "")[parameterSource.parameterIndex];
|
|
7184
|
+
if (argument !== void 0 && isHtmlTainted(argument, fileContent, sinkIndex, new Set(visitedIdentifiers))) return true;
|
|
7185
|
+
}
|
|
7186
|
+
return false;
|
|
7288
7187
|
};
|
|
7289
7188
|
const dangerousHtmlSink = defineRule({
|
|
7290
7189
|
id: "dangerous-html-sink",
|
|
@@ -7323,7 +7222,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
7323
7222
|
const longValueTail = HTML_VALUE_START_PATTERN.exec(lines.slice(lineIndex, lineIndex + 1 + STATIC_TEMPLATE_LOOKAHEAD_LINES).join("\n"))?.[1]?.trimStart();
|
|
7324
7223
|
let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
|
|
7325
7224
|
const valueIdentifier = BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) || MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN.test(valueExpression) || IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN.test(valueExpression) ? valueExpression.match(/^[\w$]+/)?.[0] : void 0;
|
|
7326
|
-
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)
|
|
7225
|
+
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
|
|
7327
7226
|
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
|
|
7328
7227
|
if (declarationInitializer !== null) {
|
|
7329
7228
|
if (isPureStringLiteralConcat(declarationInitializer)) continue;
|
|
@@ -7332,23 +7231,17 @@ const dangerousHtmlSink = defineRule({
|
|
|
7332
7231
|
}
|
|
7333
7232
|
if (templateInterpolations === "") continue;
|
|
7334
7233
|
const judgedExpression = templateInterpolations ?? valueExpression;
|
|
7335
|
-
|
|
7336
|
-
if (
|
|
7337
|
-
if (
|
|
7338
|
-
if (!
|
|
7339
|
-
if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set())) continue;
|
|
7234
|
+
if (SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
7235
|
+
if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7236
|
+
if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7237
|
+
if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set())) continue;
|
|
7340
7238
|
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
7341
|
-
if (
|
|
7239
|
+
if (/highlighted/i.test(valueExpression)) continue;
|
|
7240
|
+
if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
7342
7241
|
if (valueIdentifier !== void 0) {
|
|
7343
7242
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
if (!(visibleInitializer !== void 0 && (splitTopLevelByPlus(visibleInitializer).length > 1 || (visibleInitializer.match(/\$\{/g)?.length ?? 0) > 1))) {
|
|
7347
|
-
const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7348
|
-
if (visibleInitializer === void 0 ? fromSerializer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(visibleInitializer)) continue;
|
|
7349
|
-
const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7350
|
-
if (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`)) continue;
|
|
7351
|
-
}
|
|
7243
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
7244
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
7352
7245
|
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
7353
7246
|
if (new RegExp(`highlight[\\w$]*\\s*\\.map\\(\\s*(?:async\\s+)?\\(?\\s*${escapedIdentifier}\\b`, "i").test(file.content) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
7354
7247
|
}
|
|
@@ -12897,6 +12790,30 @@ const insecureCryptoRisk = defineRule({
|
|
|
12897
12790
|
}
|
|
12898
12791
|
});
|
|
12899
12792
|
//#endregion
|
|
12793
|
+
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
12794
|
+
const findMatchingBracket = (content, openIndex) => {
|
|
12795
|
+
const open = content[openIndex];
|
|
12796
|
+
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
12797
|
+
if (close === "") return -1;
|
|
12798
|
+
let depth = 0;
|
|
12799
|
+
let stringDelimiter = null;
|
|
12800
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
12801
|
+
const character = content[index];
|
|
12802
|
+
if (stringDelimiter !== null) {
|
|
12803
|
+
if (character === "\\") index += 1;
|
|
12804
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
12805
|
+
continue;
|
|
12806
|
+
}
|
|
12807
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
12808
|
+
else if (character === open) depth += 1;
|
|
12809
|
+
else if (character === close) {
|
|
12810
|
+
depth -= 1;
|
|
12811
|
+
if (depth === 0) return index;
|
|
12812
|
+
}
|
|
12813
|
+
}
|
|
12814
|
+
return -1;
|
|
12815
|
+
};
|
|
12816
|
+
//#endregion
|
|
12900
12817
|
//#region src/plugin/rules/security-scan/insecure-session-cookie.ts
|
|
12901
12818
|
const AUTH_COOKIE_NAME_TOKEN = `(?<![A-Za-z0-9])(?:session|sess|sid|connect\\.sid|auth|jwt|access[_-]?token|refresh[_-]?token|id[_-]?token)(?![A-Za-z0-9])`;
|
|
12902
12819
|
const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
|
|
@@ -13653,28 +13570,6 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
13653
13570
|
} })
|
|
13654
13571
|
});
|
|
13655
13572
|
//#endregion
|
|
13656
|
-
//#region src/plugin/utils/unwrap-discarded-expression.ts
|
|
13657
|
-
const unwrapDiscardedExpression = (node) => {
|
|
13658
|
-
let expression = isNodeOfType(node, "ExpressionStatement") ? node.expression : node;
|
|
13659
|
-
for (;;) {
|
|
13660
|
-
expression = stripParenExpression(expression);
|
|
13661
|
-
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") {
|
|
13662
|
-
expression = expression.argument;
|
|
13663
|
-
continue;
|
|
13664
|
-
}
|
|
13665
|
-
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
13666
|
-
const expressions = expression.expressions ?? [];
|
|
13667
|
-
const finalExpression = expressions.at(-1);
|
|
13668
|
-
const prefixExpressions = expressions.slice(0, -1);
|
|
13669
|
-
if (finalExpression && prefixExpressions.length > 0 && prefixExpressions.every((prefixExpression) => isNodeOfType(stripParenExpression(prefixExpression), "Literal"))) {
|
|
13670
|
-
expression = finalExpression;
|
|
13671
|
-
continue;
|
|
13672
|
-
}
|
|
13673
|
-
}
|
|
13674
|
-
return expression;
|
|
13675
|
-
}
|
|
13676
|
-
};
|
|
13677
|
-
//#endregion
|
|
13678
13573
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
13679
13574
|
const ITERATOR_METHOD_NAMES$2 = new Set([
|
|
13680
13575
|
"forEach",
|
|
@@ -13859,19 +13754,9 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
13859
13754
|
});
|
|
13860
13755
|
return foundAttachment;
|
|
13861
13756
|
};
|
|
13862
|
-
const getStyleAssignment = (node) => {
|
|
13863
|
-
if (!isNodeOfType(node, "ExpressionStatement")) return null;
|
|
13864
|
-
const expression = unwrapDiscardedExpression(node);
|
|
13865
|
-
if (!isNodeOfType(expression, "AssignmentExpression")) return null;
|
|
13866
|
-
if (!isNodeOfType(expression.left, "MemberExpression")) return null;
|
|
13867
|
-
if (!isNodeOfType(expression.left.object, "MemberExpression")) return null;
|
|
13868
|
-
if (!isNodeOfType(expression.left.object.property, "Identifier")) return null;
|
|
13869
|
-
return expression.left.object.property.name === "style" ? expression : null;
|
|
13870
|
-
};
|
|
13871
13757
|
const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
13872
|
-
|
|
13873
|
-
|
|
13874
|
-
const elementExpression = assignment.left.object.object;
|
|
13758
|
+
if (!isNodeOfType(styleWriteStatement, "ExpressionStatement") || !isNodeOfType(styleWriteStatement.expression, "AssignmentExpression") || !isNodeOfType(styleWriteStatement.expression.left, "MemberExpression") || !isNodeOfType(styleWriteStatement.expression.left.object, "MemberExpression")) return false;
|
|
13759
|
+
const elementExpression = styleWriteStatement.expression.left.object.object;
|
|
13875
13760
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
13876
13761
|
if (!creationRoot) return false;
|
|
13877
13762
|
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
|
|
@@ -13883,17 +13768,15 @@ const jsBatchDomCss = defineRule({
|
|
|
13883
13768
|
severity: "warn",
|
|
13884
13769
|
recommendation: "Do all your reads first, then all your writes. Mixing them inside a loop makes the browser recalculate the layout again and again, which is slow",
|
|
13885
13770
|
create: (context) => {
|
|
13886
|
-
const
|
|
13887
|
-
|
|
13888
|
-
return assignment !== null && isNodeOfType(assignment.left, "MemberExpression") && isNodeOfType(assignment.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(assignment.left.property.name);
|
|
13889
|
-
};
|
|
13771
|
+
const isStyleAssignment = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.object, "MemberExpression") && isNodeOfType(node.expression.left.object.property, "Identifier") && node.expression.left.object.property.name === "style";
|
|
13772
|
+
const writesLayoutAffectingProperty = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(node.expression.left.property.name);
|
|
13890
13773
|
return { BlockStatement(node) {
|
|
13891
13774
|
const perIterationBody = findEnclosingPerIterationBody(node);
|
|
13892
13775
|
if (!perIterationBody) return;
|
|
13893
13776
|
const statements = node.body ?? [];
|
|
13894
13777
|
let layoutReads = null;
|
|
13895
13778
|
for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
|
|
13896
|
-
if (
|
|
13779
|
+
if (!isStyleAssignment(statements[statementIndex]) || !isStyleAssignment(statements[statementIndex - 1])) continue;
|
|
13897
13780
|
if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
|
|
13898
13781
|
layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
|
|
13899
13782
|
if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
|
|
@@ -28909,7 +28792,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
28909
28792
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
28910
28793
|
for (const statement of componentBody.body ?? []) {
|
|
28911
28794
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
28912
|
-
const expression =
|
|
28795
|
+
const expression = statement.expression;
|
|
28913
28796
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
28914
28797
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
28915
28798
|
effectCalls.push(expression);
|
|
@@ -33141,7 +33024,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33141
33024
|
if (mirrorBindings.length === 0) return;
|
|
33142
33025
|
for (const statement of componentBody.body ?? []) {
|
|
33143
33026
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
33144
|
-
const effectCall =
|
|
33027
|
+
const effectCall = statement.expression;
|
|
33145
33028
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
33146
33029
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
33147
33030
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -33155,7 +33038,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33155
33038
|
const bodyStatements = getCallbackStatements(callback);
|
|
33156
33039
|
if (bodyStatements.length !== 1) continue;
|
|
33157
33040
|
const onlyStatement = bodyStatements[0];
|
|
33158
|
-
const expression =
|
|
33041
|
+
const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
|
|
33159
33042
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
33160
33043
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
33161
33044
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -37256,7 +37139,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
37256
37139
|
return expressionReadsStateValue(argument, stateName);
|
|
37257
37140
|
};
|
|
37258
37141
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
37259
|
-
const expression =
|
|
37142
|
+
const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
|
|
37260
37143
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
37261
37144
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
37262
37145
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -37605,7 +37488,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
37605
37488
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
37606
37489
|
for (const statement of functionBody.body ?? []) {
|
|
37607
37490
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37608
|
-
const effectCall =
|
|
37491
|
+
const effectCall = statement.expression;
|
|
37609
37492
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
37610
37493
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
37611
37494
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -46030,8 +45913,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
46030
45913
|
if (statements.length !== 1) return;
|
|
46031
45914
|
const onlyStatement = statements[0];
|
|
46032
45915
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
46033
|
-
singleAssignment =
|
|
46034
|
-
} else if (body) singleAssignment =
|
|
45916
|
+
singleAssignment = onlyStatement.expression;
|
|
45917
|
+
} else if (body) singleAssignment = body;
|
|
46035
45918
|
if (!singleAssignment) return;
|
|
46036
45919
|
const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
|
|
46037
45920
|
const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
|