oxlint-plugin-react-doctor 0.7.6-dev.e026a23 → 0.7.6-dev.e877ca7

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.
Files changed (2) hide show
  1. package/dist/index.js +75 -204
  2. 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[2];
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
- const bodyEndIndex = findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{"));
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
- bodyEndIndex,
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 isBackedByFunctionParameter = (expression, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
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 (isExplicitlyTrustedHtmlValue(trimmedExpression, fileContent, sinkIndex)) return false;
7251
- const identifier = trimmedExpression.match(/^([\w$]+)(?:\.|\s*(?:[;,})\n]|$))/)?.[1];
7252
- if (identifier === void 0) return HTML_TAINT_PATTERN.test(trimmedExpression);
7253
- if (visitedIdentifiers.has(identifier)) return false;
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 (declaration !== null && (parameterSource === null || declaration.declarationIndex > parameterSource.declarationNameIndex)) {
7258
- if (isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex)) return false;
7259
- if (!isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) return true;
7260
- if (isHtmlTainted(declaration.initializer, fileContent, declaration.initializerStartIndex, visitedIdentifiers, visitedCallSites)) return true;
7261
- if (isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent)) return false;
7262
- return HTML_TAINT_PATTERN.test(trimmedExpression);
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) && findContainingFunctionParameterSource(valueIdentifier, sinkIndex, file.content) === null) {
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
- const doesJudgedExpressionCombineValues = splitTopLevelByPlus(judgedExpression).length > 1 || (templateInterpolations?.match(/\$\{/g)?.length ?? 0) > 1;
7336
- if (!doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
7337
- if (!doesJudgedExpressionCombineValues && ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
7338
- if (!doesJudgedExpressionCombineValues && I18N_VALUE_PATTERN.test(judgedExpression)) continue;
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 (isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
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
- const visibleDeclaration = findVisibleIdentifierDeclaration(valueIdentifier, sinkIndex, file.content);
7345
- const visibleInitializer = visibleDeclaration?.initializer;
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
- const assignment = getStyleAssignment(styleWriteStatement);
13873
- if (!assignment || !isNodeOfType(assignment.left, "MemberExpression") || !isNodeOfType(assignment.left.object, "MemberExpression")) return false;
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 writesLayoutAffectingProperty = (node) => {
13887
- const assignment = getStyleAssignment(node);
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 (getStyleAssignment(statements[statementIndex]) === null || getStyleAssignment(statements[statementIndex - 1]) === null) continue;
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;
@@ -15291,21 +15174,9 @@ const isSmallFixedListMember = (receiver) => {
15291
15174
  };
15292
15175
  const getResolvedInitializer = (receiver) => {
15293
15176
  if (!isNodeOfType(receiver, "Identifier")) return null;
15294
- const binding = findVariableInitializer(receiver, receiver.name);
15295
- const initializer = binding?.initializer ?? null;
15296
- if (!binding || !initializer) return null;
15297
- const isDefault = isNodeOfType(binding.bindingIdentifier.parent, "AssignmentPattern");
15298
- if (isNodeOfType(initializer, "Identifier")) {
15299
- const aliased = findVariableInitializer(initializer, initializer.name);
15300
- if (aliased?.initializer) return {
15301
- initializer: aliased.initializer,
15302
- isDefault: isDefault || isNodeOfType(aliased.bindingIdentifier.parent, "AssignmentPattern")
15303
- };
15304
- }
15305
- return {
15306
- initializer,
15307
- isDefault
15308
- };
15177
+ const initializer = findVariableInitializer(receiver, receiver.name)?.initializer ?? null;
15178
+ if (initializer && isNodeOfType(initializer, "Identifier")) return findVariableInitializer(initializer, initializer.name)?.initializer ?? initializer;
15179
+ return initializer;
15309
15180
  };
15310
15181
  const isSplitCall = (expression) => {
15311
15182
  if (!expression) return false;
@@ -15471,8 +15342,8 @@ const isBoundedConstantCollection = (collection) => {
15471
15342
  if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
15472
15343
  if (isSmallInlineLiteralArray(stripped)) return true;
15473
15344
  if (isNodeOfType(stripped, "Identifier")) {
15474
- const resolved = getResolvedInitializer(stripped);
15475
- if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
15345
+ const initializer = getResolvedInitializer(stripped);
15346
+ if (initializer && isSmallInlineLiteralArray(initializer)) return true;
15476
15347
  }
15477
15348
  return false;
15478
15349
  };
@@ -15524,8 +15395,8 @@ const jsSetMapLookups = defineRule({
15524
15395
  if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
15525
15396
  const resolvedInitializer = getResolvedInitializer(receiver);
15526
15397
  if (resolvedInitializer) {
15527
- if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
15528
- if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
15398
+ if (isLikelyStringReceiver(resolvedInitializer)) return;
15399
+ if (isSmallInlineLiteralArray(resolvedInitializer)) return;
15529
15400
  }
15530
15401
  if (isStringElementOfSplitIteration(receiver)) return;
15531
15402
  if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
@@ -28921,7 +28792,7 @@ const findTopLevelEffectCalls = (componentBody) => {
28921
28792
  if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
28922
28793
  for (const statement of componentBody.body ?? []) {
28923
28794
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
28924
- const expression = unwrapDiscardedExpression(statement);
28795
+ const expression = statement.expression;
28925
28796
  if (!isNodeOfType(expression, "CallExpression")) continue;
28926
28797
  if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
28927
28798
  effectCalls.push(expression);
@@ -33153,7 +33024,7 @@ const noMirrorPropEffect = defineRule({
33153
33024
  if (mirrorBindings.length === 0) return;
33154
33025
  for (const statement of componentBody.body ?? []) {
33155
33026
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
33156
- const effectCall = unwrapDiscardedExpression(statement);
33027
+ const effectCall = statement.expression;
33157
33028
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
33158
33029
  if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
33159
33030
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
@@ -33167,7 +33038,7 @@ const noMirrorPropEffect = defineRule({
33167
33038
  const bodyStatements = getCallbackStatements(callback);
33168
33039
  if (bodyStatements.length !== 1) continue;
33169
33040
  const onlyStatement = bodyStatements[0];
33170
- const expression = unwrapDiscardedExpression(onlyStatement);
33041
+ const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
33171
33042
  if (!isNodeOfType(expression, "CallExpression")) continue;
33172
33043
  if (!isNodeOfType(expression.callee, "Identifier")) continue;
33173
33044
  if (!isSetterIdentifier(expression.callee.name)) continue;
@@ -37268,7 +37139,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
37268
37139
  return expressionReadsStateValue(argument, stateName);
37269
37140
  };
37270
37141
  const getUnconditionalSetterCall = (statement, setterNames) => {
37271
- const expression = unwrapDiscardedExpression(statement);
37142
+ const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
37272
37143
  if (!isNodeOfType(expression, "CallExpression")) return null;
37273
37144
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
37274
37145
  if (!setterNames.has(expression.callee.name)) return null;
@@ -37617,7 +37488,7 @@ const noSelfUpdatingEffect = defineRule({
37617
37488
  const setterNames = new Set(setterNameToStateName.keys());
37618
37489
  for (const statement of functionBody.body ?? []) {
37619
37490
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
37620
- const effectCall = unwrapDiscardedExpression(statement);
37491
+ const effectCall = statement.expression;
37621
37492
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
37622
37493
  if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
37623
37494
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
@@ -46042,8 +45913,8 @@ const rnAnimationReactionAsDerived = defineRule({
46042
45913
  if (statements.length !== 1) return;
46043
45914
  const onlyStatement = statements[0];
46044
45915
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
46045
- singleAssignment = unwrapDiscardedExpression(onlyStatement);
46046
- } else if (body) singleAssignment = unwrapDiscardedExpression(body);
45916
+ singleAssignment = onlyStatement.expression;
45917
+ } else if (body) singleAssignment = body;
46047
45918
  if (!singleAssignment) return;
46048
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";
46049
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.6-dev.e026a23",
3
+ "version": "0.7.6-dev.e877ca7",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",