oxlint-plugin-react-doctor 0.6.1 → 0.6.2-dev.397816a

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 +94 -91
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -212,8 +212,16 @@ const sliceBelowSourceRoot = (filename) => {
212
212
  if (cutAt < 0) return filename;
213
213
  return filename.slice(cutAt);
214
214
  };
215
+ let lastFilename;
216
+ let lastResult = false;
215
217
  const isTestlikeFilename = (rawFilename) => {
216
218
  if (!rawFilename) return false;
219
+ if (rawFilename === lastFilename) return lastResult;
220
+ lastFilename = rawFilename;
221
+ lastResult = computeIsTestlikeFilename(rawFilename);
222
+ return lastResult;
223
+ };
224
+ const computeIsTestlikeFilename = (rawFilename) => {
217
225
  const filename = rawFilename.replaceAll("\\", "/");
218
226
  const lastSlash = filename.lastIndexOf("/");
219
227
  const basename = lastSlash === -1 ? filename : filename.slice(lastSlash + 1);
@@ -983,16 +991,8 @@ const buildBindingIndex = (root) => {
983
991
  }
984
992
  }
985
993
  }
986
- const nodeRecord = node;
987
- for (const key of Object.keys(nodeRecord)) {
988
- if (key === "parent") continue;
989
- const child = nodeRecord[key];
990
- if (Array.isArray(child)) {
991
- for (const item of child) if (isAstNode(item)) visit(item);
992
- } else if (isAstNode(child)) visit(child);
993
- }
994
994
  };
995
- visit(root);
995
+ walkAst(root, visit);
996
996
  return out;
997
997
  };
998
998
  const programRootCache = /* @__PURE__ */ new WeakMap();
@@ -1423,49 +1423,40 @@ const getElementType = (openingElement, settings) => {
1423
1423
  //#region src/plugin/utils/find-import-source-for-name.ts
1424
1424
  const collectFromProgram = (programRoot) => {
1425
1425
  const lookup = /* @__PURE__ */ new Map();
1426
- const visit = (node) => {
1427
- if (node.type === "ImportDeclaration" && "source" in node && node.source) {
1428
- const source = node.source.value;
1429
- if (typeof source !== "string") return;
1430
- if ("specifiers" in node && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
1431
- if (!("local" in specifier) || !specifier.local) continue;
1432
- const local = specifier.local;
1433
- if (typeof local.name !== "string") continue;
1434
- if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
1435
- source,
1436
- imported: null,
1437
- isDefault: true,
1438
- isNamespace: false
1439
- });
1440
- else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
1426
+ const bodyStatements = programRoot.body ?? [];
1427
+ for (const node of bodyStatements) {
1428
+ if (node.type !== "ImportDeclaration" || !("source" in node) || !node.source) continue;
1429
+ const source = node.source.value;
1430
+ if (typeof source !== "string") continue;
1431
+ if (!("specifiers" in node) || !Array.isArray(node.specifiers)) continue;
1432
+ for (const specifier of node.specifiers) {
1433
+ if (!("local" in specifier) || !specifier.local) continue;
1434
+ const local = specifier.local;
1435
+ if (typeof local.name !== "string") continue;
1436
+ if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
1437
+ source,
1438
+ imported: null,
1439
+ isDefault: true,
1440
+ isNamespace: false
1441
+ });
1442
+ else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
1443
+ source,
1444
+ imported: null,
1445
+ isDefault: false,
1446
+ isNamespace: true
1447
+ });
1448
+ else if (specifier.type === "ImportSpecifier") {
1449
+ const importedNode = specifier.imported;
1450
+ const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
1451
+ lookup.set(local.name, {
1441
1452
  source,
1442
- imported: null,
1453
+ imported: importedName,
1443
1454
  isDefault: false,
1444
- isNamespace: true
1455
+ isNamespace: false
1445
1456
  });
1446
- else if (specifier.type === "ImportSpecifier") {
1447
- const importedNode = specifier.imported;
1448
- const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
1449
- lookup.set(local.name, {
1450
- source,
1451
- imported: importedName,
1452
- isDefault: false,
1453
- isNamespace: false
1454
- });
1455
- }
1456
1457
  }
1457
- return;
1458
1458
  }
1459
- const nodeRecord = node;
1460
- for (const key of Object.keys(nodeRecord)) {
1461
- if (key === "parent") continue;
1462
- const child = nodeRecord[key];
1463
- if (Array.isArray(child)) {
1464
- for (const item of child) if (isAstNode(item)) visit(item);
1465
- } else if (isAstNode(child)) visit(child);
1466
- }
1467
- };
1468
- visit(programRoot);
1459
+ }
1469
1460
  return lookup;
1470
1461
  };
1471
1462
  const importLookupCache = /* @__PURE__ */ new WeakMap();
@@ -1479,6 +1470,12 @@ const getImportLookup = (node) => {
1479
1470
  }
1480
1471
  return cached;
1481
1472
  };
1473
+ const hasImportFromModules = (contextNode, moduleSources) => {
1474
+ const lookup = getImportLookup(contextNode);
1475
+ if (!lookup) return false;
1476
+ for (const info of lookup.values()) if (moduleSources.includes(info.source)) return true;
1477
+ return false;
1478
+ };
1482
1479
  const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) => {
1483
1480
  const lookup = getImportLookup(contextNode);
1484
1481
  if (!lookup) return false;
@@ -1631,6 +1628,7 @@ const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
1631
1628
  //#region src/plugin/utils/is-generated-image-render-context.ts
1632
1629
  const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
1633
1630
  const SATORI_MODULE = "satori";
1631
+ const GENERATED_IMAGE_MODULES = [...IMAGE_RESPONSE_MODULES, SATORI_MODULE];
1634
1632
  const generatedImageJsxCache = /* @__PURE__ */ new WeakMap();
1635
1633
  const isGeneratedImageRenderFilename = (rawFilename) => {
1636
1634
  if (!rawFilename) return false;
@@ -1774,7 +1772,7 @@ const collectGeneratedImageJsxNodes = (programRoot) => {
1774
1772
  const cached = generatedImageJsxCache.get(programRoot);
1775
1773
  if (cached) return cached;
1776
1774
  const generatedImageJsxNodes = /* @__PURE__ */ new WeakSet();
1777
- walkAst(programRoot, (descendantNode) => {
1775
+ if (hasImportFromModules(programRoot, GENERATED_IMAGE_MODULES)) walkAst(programRoot, (descendantNode) => {
1778
1776
  if (!isGeneratedImageRendererCall(descendantNode)) return;
1779
1777
  for (const argument of descendantNode.arguments) markGeneratedImageExpression(argument, programRoot, generatedImageJsxNodes, /* @__PURE__ */ new Set());
1780
1778
  });
@@ -3967,9 +3965,11 @@ const collectScopedReferenceIdentifierNames = (node, into, shadowed) => {
3967
3965
  return;
3968
3966
  }
3969
3967
  if (typeof node.type === "string" && node.type.startsWith("TS")) return;
3970
- for (const [key, child] of Object.entries(node)) {
3968
+ const nodeRecord = node;
3969
+ for (const key of Object.keys(nodeRecord)) {
3971
3970
  if (key === "parent") continue;
3972
3971
  if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
3972
+ const child = nodeRecord[key];
3973
3973
  if (Array.isArray(child)) {
3974
3974
  for (const item of child) if (isAstNode(item)) collectScopedReferenceIdentifierNames(item, into, shadowed);
3975
3975
  } else if (isAstNode(child)) collectScopedReferenceIdentifierNames(child, into, shadowed);
@@ -10439,10 +10439,15 @@ const jsCombineIterations = defineRule({
10439
10439
  severity: "warn",
10440
10440
  recommendation: "Combine `.map().filter()` style chains into one pass with `.reduce()` or a `for...of` loop, so you only loop over the list once",
10441
10441
  create: (context) => {
10442
- let generatorNamesInFile = /* @__PURE__ */ new Set();
10442
+ let programNode = null;
10443
+ let generatorNamesInFile = null;
10444
+ const getGeneratorNamesInFile = () => {
10445
+ generatorNamesInFile ??= programNode ? collectGeneratorNames(programNode) : /* @__PURE__ */ new Set();
10446
+ return generatorNamesInFile;
10447
+ };
10443
10448
  return {
10444
- Program(programNode) {
10445
- generatorNamesInFile = collectGeneratorNames(programNode);
10449
+ Program(node) {
10450
+ programNode = node;
10446
10451
  },
10447
10452
  CallExpression(node) {
10448
10453
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
@@ -10464,7 +10469,7 @@ const jsCombineIterations = defineRule({
10464
10469
  if (isNullFilteringPredicate(filterArgument)) return;
10465
10470
  if (isTypePredicateArrow(filterArgument)) return;
10466
10471
  }
10467
- if (isReceiverChainIteratorRooted(innerCall.callee.object, generatorNamesInFile)) return;
10472
+ if (isReceiverChainIteratorRooted(innerCall.callee.object, getGeneratorNamesInFile())) return;
10468
10473
  if (isSmallLiteralArrayRootedChain(innerCall.callee.object)) return;
10469
10474
  if (isStringSplitRootedChain(innerCall.callee.object)) return;
10470
10475
  context.report({
@@ -29521,21 +29526,18 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
29521
29526
  reportNode
29522
29527
  };
29523
29528
  };
29524
- const collectAllNodes = (programRoot) => {
29525
- const out = [];
29526
- const visit = (node) => {
29527
- out.push(node);
29528
- const record = node;
29529
- for (const key of Object.keys(record)) {
29530
- if (key === "parent") continue;
29531
- const child = record[key];
29532
- if (Array.isArray(child)) {
29533
- for (const item of child) if (isAstNode(item)) visit(item);
29534
- } else if (isAstNode(child)) visit(child);
29535
- }
29529
+ const collectRelevantNodes = (programRoot) => {
29530
+ const exportNodes = [];
29531
+ const componentCandidates = [];
29532
+ walkAst(programRoot, (child) => {
29533
+ const childType = child.type;
29534
+ if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
29535
+ else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
29536
+ });
29537
+ return {
29538
+ exportNodes,
29539
+ componentCandidates
29536
29540
  };
29537
- visit(programRoot);
29538
- return out;
29539
29541
  };
29540
29542
  const isEntryPointFile = (filename) => {
29541
29543
  const lastSlash = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
@@ -29582,13 +29584,13 @@ const onlyExportComponents = defineRule({
29582
29584
  };
29583
29585
  return { Program(node) {
29584
29586
  if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
29585
- const allNodes = collectAllNodes(node);
29587
+ const { exportNodes, componentCandidates } = collectRelevantNodes(node);
29586
29588
  const exports = [];
29587
29589
  let hasReactExport = false;
29588
29590
  let hasAnyExports = false;
29589
29591
  const localComponents = [];
29590
29592
  const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
29591
- for (const child of allNodes) {
29593
+ for (const child of exportNodes) {
29592
29594
  if (isNodeOfType(child, "ExportAllDeclaration")) {
29593
29595
  if (child.exportKind === "type") continue;
29594
29596
  hasAnyExports = true;
@@ -29724,7 +29726,7 @@ const onlyExportComponents = defineRule({
29724
29726
  }
29725
29727
  return false;
29726
29728
  };
29727
- for (const child of allNodes) {
29729
+ for (const child of componentCandidates) {
29728
29730
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
29729
29731
  if (isReactComponentName(child.id.name) && !isInsideExport(child)) localComponents.push(child.id);
29730
29732
  }
@@ -46841,23 +46843,29 @@ const FALLBACK_CFG = {
46841
46843
  enclosingFunction: () => null,
46842
46844
  isUnconditionalFromEntry: () => false
46843
46845
  };
46846
+ const scopesByProgram = /* @__PURE__ */ new WeakMap();
46847
+ const cfgByProgram = /* @__PURE__ */ new WeakMap();
46844
46848
  const wrapWithSemanticContext = (rule) => ({
46845
46849
  ...rule,
46846
46850
  create: (baseContext) => {
46847
46851
  let programRoot = null;
46848
- let cachedScopes = null;
46849
- let cachedCfg = null;
46850
46852
  const getScopes = () => {
46851
- if (cachedScopes) return cachedScopes;
46852
46853
  if (!programRoot) return buildFallbackScopes();
46853
- cachedScopes = analyzeScopes(programRoot);
46854
- return cachedScopes;
46854
+ let scopes = scopesByProgram.get(programRoot);
46855
+ if (!scopes) {
46856
+ scopes = analyzeScopes(programRoot);
46857
+ scopesByProgram.set(programRoot, scopes);
46858
+ }
46859
+ return scopes;
46855
46860
  };
46856
46861
  const getCfg = () => {
46857
- if (cachedCfg) return cachedCfg;
46858
46862
  if (!programRoot) return FALLBACK_CFG;
46859
- cachedCfg = analyzeControlFlow(programRoot);
46860
- return cachedCfg;
46863
+ let cfg = cfgByProgram.get(programRoot);
46864
+ if (!cfg) {
46865
+ cfg = analyzeControlFlow(programRoot);
46866
+ cfgByProgram.set(programRoot, cfg);
46867
+ }
46868
+ return cfg;
46861
46869
  };
46862
46870
  const enrichedContext = {
46863
46871
  report: baseContext.report,
@@ -46872,23 +46880,18 @@ const wrapWithSemanticContext = (rule) => ({
46872
46880
  return getCfg();
46873
46881
  }
46874
46882
  };
46875
- const captureRootIfNeeded = (node) => {
46876
- if (programRoot) return;
46877
- programRoot = findProgramRoot(node);
46878
- };
46879
46883
  const visitors = rule.create(enrichedContext);
46880
- const wrappedVisitors = {};
46884
+ const passthroughVisitors = {};
46881
46885
  for (const [nodeType, handler] of Object.entries(visitors)) {
46882
46886
  if (typeof handler !== "function") continue;
46883
- wrappedVisitors[nodeType] = ((node) => {
46884
- captureRootIfNeeded(node);
46885
- handler(node);
46886
- });
46887
+ passthroughVisitors[nodeType] = handler;
46887
46888
  }
46888
- if (!visitors.Program) wrappedVisitors.Program = (node) => {
46889
- captureRootIfNeeded(node);
46890
- };
46891
- return wrappedVisitors;
46889
+ const innerProgramHandler = passthroughVisitors.Program;
46890
+ passthroughVisitors.Program = ((node) => {
46891
+ programRoot = node;
46892
+ if (innerProgramHandler) innerProgramHandler(node);
46893
+ });
46894
+ return passthroughVisitors;
46892
46895
  }
46893
46896
  });
46894
46897
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.6.1",
3
+ "version": "0.6.2-dev.397816a",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",