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.
Files changed (2) hide show
  1. package/dist/index.js +68 -185
  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;
@@ -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 = unwrapDiscardedExpression(statement);
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 = unwrapDiscardedExpression(statement);
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 = unwrapDiscardedExpression(onlyStatement);
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 = unwrapDiscardedExpression(statement);
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 = unwrapDiscardedExpression(statement);
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 = unwrapDiscardedExpression(onlyStatement);
46034
- } else if (body) singleAssignment = unwrapDiscardedExpression(body);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.6-dev.fbd85e3",
3
+ "version": "0.7.6",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",