oxlint-plugin-arch 0.12.3 → 0.12.5

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 CHANGED
@@ -225,6 +225,39 @@ function astSelectorRule(selector, message) {
225
225
  }
226
226
  });
227
227
  }
228
+ function astIsOneLine(node) {
229
+ return node.loc.start.line === node.loc.end.line;
230
+ }
231
+ function astIsInlineFunction(node) {
232
+ return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
233
+ }
234
+ function astIsCall(node) {
235
+ return node.type === "CallExpression" || node.type === "NewExpression";
236
+ }
237
+ function astIsPipe(node) {
238
+ return node.callee.type === "MemberExpression" && astMemberName(node.callee) === "pipe";
239
+ }
240
+ function astChildren(node) {
241
+ return Object.entries(node).filter(([key]) => !astSkippedKeys.has(key)).map(([, child]) => child);
242
+ }
243
+ function astSome(value, test, enter = () => true) {
244
+ if (Array.isArray(value)) {
245
+ return value.some((child) => astSome(child, test, enter));
246
+ }
247
+ return astIsNode(value) && (test(value) || enter(value) && astChildren(value).some((child) => astSome(child, test, enter)));
248
+ }
249
+ function astCallDepth(value) {
250
+ if (Array.isArray(value)) {
251
+ return Math.max(0, ...value.map(astCallDepth));
252
+ }
253
+ if (!astIsNode(value) || astIsInlineFunction(value)) {
254
+ return 0;
255
+ }
256
+ if (astIsCall(value)) {
257
+ return Math.max(value.arguments.length === 0 ? 0 : 1 + astCallDepth(value.arguments), astCallDepth(value.callee));
258
+ }
259
+ return astCallDepth(astChildren(value));
260
+ }
228
261
 
229
262
  // src/rules/backend/backend-orpc-output.ts
230
263
  function backendFindChainCall(expression, method) {
@@ -477,14 +510,15 @@ var backendRegistry = defineRule3({
477
510
  registry: { type: "string", minLength: 1 },
478
511
  services: { type: "string", minLength: 1 },
479
512
  skip: { type: "array", items: { type: "string" } },
480
- object: { type: "string", minLength: 1 }
513
+ object: { type: "string", minLength: 1 },
514
+ file: { type: "string", minLength: 1, default: "{folder}.service.ts" }
481
515
  },
482
516
  required: ["registry", "services"]
483
517
  }
484
518
  ],
485
519
  messages: {
486
520
  unregistered: "Register the {{folder}} service here under '{{key}}'.",
487
- missing: "Domain folder '{{folder}}' needs {{folder}}.service.ts; add it, or list the folder in skip."
521
+ missing: "Domain folder '{{folder}}' needs {{file}}; add it, or list the folder in skip."
488
522
  }
489
523
  },
490
524
  createOnce(context) {
@@ -526,11 +560,12 @@ var backendRegistry = defineRule3({
526
560
  const domains = entries.filter((entry) => entry.isDirectory() && !(options.skip ?? []).includes(entry.name)).map((entry) => entry.name);
527
561
  for (const folder of domains) {
528
562
  const key = namingCamelCase(folder);
529
- if (!existsSync(join(options.services, folder, `${folder}.service.ts`))) {
563
+ const file = (options.file ?? "{folder}.service.ts").replaceAll("{folder}", folder);
564
+ if (!existsSync(join(options.services, folder, file))) {
530
565
  context.report({
531
566
  node: program,
532
567
  messageId: "missing",
533
- data: { folder }
568
+ data: { folder, file }
534
569
  });
535
570
  } else if (![key, `${key}s`, key.replace(/y$/, "ies")].some((name) => keys.has(name))) {
536
571
  context.report({
@@ -1103,11 +1138,14 @@ var boundariesDeny = defineRule6({
1103
1138
  // src/rules/boundaries/boundaries-imports.ts
1104
1139
  import { defineRule as defineRule7 } from "@oxlint/plugins";
1105
1140
  import { basename, resolve as resolve2 } from "node:path";
1106
- var boundariesPublicSurface = /\.(?:service|client|server|rsc|utils)$/;
1141
+ var boundariesSurfaces = ["service", "client", "server", "rsc", "utils"];
1107
1142
  var boundariesActionOrQuery = /\/(?:[^/]+)-(?:action|query)\./;
1108
1143
  function boundariesContains(directory, candidate) {
1109
1144
  return candidate === directory || candidate.startsWith(`${directory}/`);
1110
1145
  }
1146
+ function boundariesIsSurface(options, stem) {
1147
+ return (options.surfaces ?? boundariesSurfaces).some((surface) => stem.endsWith(`.${surface}`));
1148
+ }
1111
1149
  function boundariesStripExtension(path) {
1112
1150
  return path.replace(/\.[cm]?[jt]sx?$/, "");
1113
1151
  }
@@ -1136,12 +1174,12 @@ function boundariesPrivateDomain({ options, file, target, reExport, roots, owner
1136
1174
  const targetDomain = targetOwner ? boundariesDomain(target, targetOwner, boundariesServiceFolder(options, targetOwner)) : undefined;
1137
1175
  const sameDomain = owner && owner === targetOwner && fileDomain && fileDomain === targetDomain;
1138
1176
  const targetStem = boundariesStripExtension(target);
1139
- const publicSurface = boundariesPublicSurface.test(targetStem) && !boundariesActionOrQuery.test(targetStem);
1177
+ const publicSurface = boundariesIsSurface(options, targetStem) && !boundariesActionOrQuery.test(targetStem);
1140
1178
  const privateTarget = roles.some((role) => targetStem.endsWith(`.${role}`)) || targetDomain && !publicSurface && !/\.(?:types|constants)$/.test(targetStem);
1141
1179
  if (!privateTarget) {
1142
1180
  return;
1143
1181
  }
1144
- if (reExport && /\.(?:service|client|server|rsc|utils)\.ts$/.test(file) && !boundariesActionOrQuery.test(file)) {
1182
+ if (reExport && file.endsWith(".ts") && boundariesIsSurface(options, file.slice(0, -".ts".length)) && !boundariesActionOrQuery.test(file)) {
1145
1183
  return "public service and utility surfaces must not re-export private domain internals";
1146
1184
  }
1147
1185
  return sameDomain ? undefined : "private domain files stay in the same domain and application; use its service API or shared utilities";
@@ -1179,6 +1217,7 @@ var boundariesImports = defineRule7({
1179
1217
  backend: { type: "array", items: { type: "string" } },
1180
1218
  packages: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }] },
1181
1219
  roles: { type: "array", items: { type: "string" } },
1220
+ surfaces: { type: "array", items: { type: "string", minLength: 1 }, default: boundariesSurfaces },
1182
1221
  services: { type: "array", items: { type: "string" } },
1183
1222
  portable: { type: "boolean" },
1184
1223
  backendPackages: { type: "array", items: { type: "string" } },
@@ -1274,11 +1313,9 @@ var boundariesNextDefaults = {
1274
1313
  serverPackages: [...boundariesServerPackages, "server-only", "next/headers", "next/cache", "next/server"],
1275
1314
  clientPackages: ["client-only", "react-dom/client"],
1276
1315
  hookPackages: ["react"],
1277
- publicEnv: "NEXT_PUBLIC_"
1316
+ publicEnv: "NEXT_PUBLIC_",
1317
+ serverHooks: ["useId", "use"]
1278
1318
  };
1279
- function boundariesIsClientHook(name) {
1280
- return /^use[A-Z]/.test(name) && name !== "useId" && name !== "use";
1281
- }
1282
1319
  var boundariesNext = defineRule8({
1283
1320
  meta: {
1284
1321
  type: "problem",
@@ -1286,7 +1323,11 @@ var boundariesNext = defineRule8({
1286
1323
  {
1287
1324
  type: "object",
1288
1325
  additionalProperties: false,
1289
- properties: { ...boundariesRuntimeProperties, publicEnv: { type: "string" } }
1326
+ properties: {
1327
+ ...boundariesRuntimeProperties,
1328
+ publicEnv: { type: "string" },
1329
+ serverHooks: { type: "array", items: { type: "string", minLength: 1 }, default: boundariesNextDefaults.serverHooks }
1330
+ }
1290
1331
  }
1291
1332
  ],
1292
1333
  messages: {
@@ -1302,6 +1343,7 @@ var boundariesNext = defineRule8({
1302
1343
  let client = false;
1303
1344
  let shared = false;
1304
1345
  const reactNamespaces = new Set;
1346
+ const clientHook = (name) => /^use[A-Z]/.test(name) && !options.serverHooks.includes(name);
1305
1347
  function check(node, source, typeOnly) {
1306
1348
  if (typeOnly) {
1307
1349
  return;
@@ -1365,14 +1407,14 @@ var boundariesNext = defineRule8({
1365
1407
  reactNamespaces.add(specifier);
1366
1408
  continue;
1367
1409
  }
1368
- if (specifier.importKind !== "type" && specifier.imported.type === "Identifier" && boundariesIsClientHook(specifier.imported.name)) {
1410
+ if (specifier.importKind !== "type" && specifier.imported.type === "Identifier" && clientHook(specifier.imported.name)) {
1369
1411
  boundariesReport(context, specifier, specifier.imported.name, boundariesClientHookReason);
1370
1412
  }
1371
1413
  }
1372
1414
  },
1373
1415
  MemberExpression(node) {
1374
1416
  const name = boundariesMemberName(node);
1375
- if (!name || !boundariesIsClientHook(name) || node.object.type !== "Identifier") {
1417
+ if (!name || !clientHook(name) || node.object.type !== "Identifier") {
1376
1418
  return;
1377
1419
  }
1378
1420
  const binding = boundariesDeclaredVariable(context.sourceCode.getScope(node.object), node.object.name)?.defs[0]?.node;
@@ -1416,12 +1458,54 @@ var boundariesNext = defineRule8({
1416
1458
  });
1417
1459
 
1418
1460
  // src/rules/boundaries/boundaries-rpc-database.ts
1419
- var boundariesQueryMethods = "/^(selectFrom|insertInto|updateTable|deleteFrom|mergeInto|executeQuery)$/";
1420
- var boundariesRpcDatabase = astSelectorRule(`CallExpression[callee.computed=false][callee.property.name=${boundariesQueryMethods}], CallExpression[callee.computed=true][callee.property.value=${boundariesQueryMethods}]`, "RPC handlers delegate database work to services.");
1461
+ import { defineRule as defineRule9 } from "@oxlint/plugins";
1462
+ var boundariesRpcDatabaseMethods = ["selectFrom", "insertInto", "updateTable", "deleteFrom", "mergeInto", "executeQuery"];
1463
+ var boundariesRpcDatabase = defineRule9({
1464
+ meta: {
1465
+ type: "problem",
1466
+ schema: [
1467
+ {
1468
+ type: "object",
1469
+ additionalProperties: false,
1470
+ properties: {
1471
+ message: { type: "string" },
1472
+ methods: { type: "array", items: { type: "string", minLength: 1 }, default: boundariesRpcDatabaseMethods }
1473
+ }
1474
+ }
1475
+ ],
1476
+ messages: { forbidden: "{{message}}" }
1477
+ },
1478
+ createOnce(context) {
1479
+ let message = "";
1480
+ let methods = new Set;
1481
+ return {
1482
+ before() {
1483
+ const options = optionsFirst(context, {});
1484
+ message = options.message ?? "RPC handlers delegate database work to services.";
1485
+ methods = new Set(options.methods ?? boundariesRpcDatabaseMethods);
1486
+ },
1487
+ CallExpression(node) {
1488
+ const callee = node.callee;
1489
+ if (callee.type !== "MemberExpression") {
1490
+ return;
1491
+ }
1492
+ const property = callee.property;
1493
+ const literal = property.type === "Literal" && typeof property.value === "string" ? property.value : undefined;
1494
+ const name = callee.computed ? literal : astMemberName(callee);
1495
+ if (name !== undefined && methods.has(name)) {
1496
+ context.report({
1497
+ node,
1498
+ messageId: "forbidden",
1499
+ data: { message }
1500
+ });
1501
+ }
1502
+ }
1503
+ };
1504
+ }
1505
+ });
1421
1506
 
1422
1507
  // src/rules/boundaries/boundaries-tanstack.ts
1423
- import { defineRule as defineRule9 } from "@oxlint/plugins";
1424
- var boundariesRenderers = ["renderServerComponent", "renderToReadableStream"];
1508
+ import { defineRule as defineRule10 } from "@oxlint/plugins";
1425
1509
  var boundariesChainMethods = ["handler", "server", "client", "middleware", "validator", "inputValidator"];
1426
1510
  var boundariesSplitFactories = ["createIsomorphicFn", "createMiddleware"];
1427
1511
  var boundariesRuntimeTypeWrappers = [
@@ -1457,12 +1541,13 @@ var boundariesTanstackDefaults = {
1457
1541
  clientPackages: ["react-dom/client", "@tanstack/react-start/client-only", "@tanstack/solid-start/client-only", "@tanstack/vue-start/client-only"],
1458
1542
  hookPackages: ["react", "@tanstack/react-query", "@tanstack/react-router"],
1459
1543
  startPackages: ["@tanstack/react-start"],
1460
- rscPackages: ["@tanstack/react-start/rsc", "@tanstack/react-start-rsc"]
1544
+ rscPackages: ["@tanstack/react-start/rsc", "@tanstack/react-start-rsc"],
1545
+ renderers: ["renderServerComponent", "renderToReadableStream"]
1461
1546
  };
1462
- function boundariesIsClientHook2(name) {
1547
+ function boundariesIsClientHook(name) {
1463
1548
  return /^use[A-Z]/.test(name) && name !== "useId";
1464
1549
  }
1465
- var boundariesTanstack = defineRule9({
1550
+ var boundariesTanstack = defineRule10({
1466
1551
  meta: {
1467
1552
  type: "problem",
1468
1553
  schema: [
@@ -1473,7 +1558,8 @@ var boundariesTanstack = defineRule9({
1473
1558
  ...boundariesRuntimeProperties,
1474
1559
  rpc: { type: "array", items: { type: "string" } },
1475
1560
  startPackages: { type: "array", items: { type: "string" } },
1476
- rscPackages: { type: "array", items: { type: "string" } }
1561
+ rscPackages: { type: "array", items: { type: "string" } },
1562
+ renderers: { type: "array", items: { type: "string", minLength: 1 }, default: boundariesTanstackDefaults.renderers }
1477
1563
  }
1478
1564
  }
1479
1565
  ],
@@ -1629,7 +1715,7 @@ var boundariesTanstack = defineRule9({
1629
1715
  return;
1630
1716
  }
1631
1717
  for (const specifier of node.specifiers) {
1632
- if (options.rscPackages.includes(node.source.value) && specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && boundariesRenderers.includes(specifier.imported.name)) {
1718
+ if (options.rscPackages.includes(node.source.value) && specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && options.renderers.includes(specifier.imported.name)) {
1633
1719
  check(node, node.source.value, false, false, specifier);
1634
1720
  }
1635
1721
  }
@@ -1637,7 +1723,7 @@ var boundariesTanstack = defineRule9({
1637
1723
  return;
1638
1724
  }
1639
1725
  for (const specifier of node.specifiers) {
1640
- if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type" || specifier.imported.type !== "Identifier" || !boundariesIsClientHook2(specifier.imported.name)) {
1726
+ if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type" || specifier.imported.type !== "Identifier" || !boundariesIsClientHook(specifier.imported.name)) {
1641
1727
  continue;
1642
1728
  }
1643
1729
  const references = runtimeReferences(node, specifier.local.name);
@@ -1656,10 +1742,10 @@ var boundariesTanstack = defineRule9({
1656
1742
  return;
1657
1743
  }
1658
1744
  const kind = imported(node.object);
1659
- if (kind === "rsc-renderers" && boundariesRenderers.includes(name) && (shared || !serverPosition(node.start, true))) {
1745
+ if (kind === "rsc-renderers" && options.renderers.includes(name) && (shared || !serverPosition(node.start, true))) {
1660
1746
  boundariesReport(context, node, name, "server renderers require a server execution boundary");
1661
1747
  }
1662
- if ((serverFile || rscFile) && kind === "client-hooks" && boundariesIsClientHook2(name) && !clientSide(node.start)) {
1748
+ if ((serverFile || rscFile) && kind === "client-hooks" && boundariesIsClientHook(name) && !clientSide(node.start)) {
1663
1749
  boundariesReport(context, node, name, boundariesClientHookReason2);
1664
1750
  }
1665
1751
  },
@@ -1691,8 +1777,8 @@ var boundariesTanstack = defineRule9({
1691
1777
  });
1692
1778
 
1693
1779
  // src/rules/boundaries/boundaries-token-owner.ts
1694
- import { defineRule as defineRule10 } from "@oxlint/plugins";
1695
- var boundariesTokenOwner = defineRule10({
1780
+ import { defineRule as defineRule11 } from "@oxlint/plugins";
1781
+ var boundariesTokenOwner = defineRule11({
1696
1782
  meta: {
1697
1783
  type: "problem",
1698
1784
  schema: [
@@ -1786,9 +1872,68 @@ var boundariesTokenOwner = defineRule10({
1786
1872
  }
1787
1873
  });
1788
1874
 
1875
+ // src/rules/code/code-branch-once.ts
1876
+ import { defineRule as defineRule12 } from "@oxlint/plugins";
1877
+ function codeBranchOnceSubjects(node, source) {
1878
+ if (node.type === "LogicalExpression") {
1879
+ return [...codeBranchOnceSubjects(node.left, source), ...codeBranchOnceSubjects(node.right, source)];
1880
+ }
1881
+ if (node.type === "UnaryExpression" && node.operator === "!") {
1882
+ return codeBranchOnceSubjects(node.argument, source);
1883
+ }
1884
+ if (node.type !== "BinaryExpression" || node.operator !== "===" && node.operator !== "!==") {
1885
+ return [];
1886
+ }
1887
+ const sides = [node.left, node.right];
1888
+ const member = sides.find((side) => side.type === "MemberExpression");
1889
+ const literal = sides.some((side) => side.type === "Literal" && typeof side.value === "string");
1890
+ return member && literal ? [source.getText(member)] : [];
1891
+ }
1892
+ var codeBranchOnce = defineRule12({
1893
+ meta: {
1894
+ type: "suggestion",
1895
+ schema: [
1896
+ {
1897
+ type: "object",
1898
+ additionalProperties: false,
1899
+ properties: { min: { type: "integer", minimum: 2, default: 2 } }
1900
+ }
1901
+ ],
1902
+ messages: {
1903
+ once: "{{subject}} is compared to a string in {{count}} ternaries in this file. Branch on it once and return a record."
1904
+ }
1905
+ },
1906
+ createOnce(context) {
1907
+ let groups = new Map;
1908
+ let min = 2;
1909
+ return {
1910
+ before() {
1911
+ groups = new Map;
1912
+ min = optionsFirst(context, {}).min ?? 2;
1913
+ },
1914
+ ConditionalExpression(node) {
1915
+ for (const subject of new Set(codeBranchOnceSubjects(node.test, context.sourceCode))) {
1916
+ groups.set(subject, [...groups.get(subject) ?? [], node]);
1917
+ }
1918
+ },
1919
+ "Program:exit"() {
1920
+ for (const [subject, ternaries] of groups) {
1921
+ for (const node of ternaries.length >= min ? ternaries : []) {
1922
+ context.report({
1923
+ node,
1924
+ messageId: "once",
1925
+ data: { subject, count: String(ternaries.length) }
1926
+ });
1927
+ }
1928
+ }
1929
+ }
1930
+ };
1931
+ }
1932
+ });
1933
+
1789
1934
  // src/rules/code/code-constructor.ts
1790
- import { defineRule as defineRule11 } from "@oxlint/plugins";
1791
- var codeConstructor = defineRule11({
1935
+ import { defineRule as defineRule13 } from "@oxlint/plugins";
1936
+ var codeConstructor = defineRule13({
1792
1937
  meta: {
1793
1938
  type: "problem",
1794
1939
  schema: [
@@ -1871,9 +2016,304 @@ var codeConstructor = defineRule11({
1871
2016
  // src/rules/code/code-double-negation.ts
1872
2017
  var codeDoubleNegation = astSelectorRule('UnaryExpression[operator="!"] > UnaryExpression[operator="!"]', "Use Boolean(value) or an explicit comparison.");
1873
2018
 
2019
+ // src/rules/code/code-function-nesting.ts
2020
+ import { defineRule as defineRule14 } from "@oxlint/plugins";
2021
+ var codeFunctionNesting = defineRule14({
2022
+ meta: {
2023
+ type: "suggestion",
2024
+ schema: [
2025
+ {
2026
+ type: "object",
2027
+ additionalProperties: false,
2028
+ properties: { max: { type: "integer", minimum: 1, default: 4 } }
2029
+ }
2030
+ ],
2031
+ messages: {
2032
+ deep: "Functions nest {{depth}} deep (max {{max}}). Name the inner step."
2033
+ }
2034
+ },
2035
+ createOnce(context) {
2036
+ let max = 4;
2037
+ const check = (node) => {
2038
+ let depth = 0;
2039
+ for (let current = node;current; current = current.parent) {
2040
+ depth += current.type === "FunctionDeclaration" || astIsInlineFunction(current) ? 1 : 0;
2041
+ }
2042
+ if (depth === max + 1) {
2043
+ context.report({
2044
+ node,
2045
+ messageId: "deep",
2046
+ data: { depth: String(depth), max: String(max) }
2047
+ });
2048
+ }
2049
+ };
2050
+ return {
2051
+ before() {
2052
+ max = optionsFirst(context, {}).max ?? 4;
2053
+ },
2054
+ ArrowFunctionExpression: check,
2055
+ FunctionExpression: check,
2056
+ FunctionDeclaration: check
2057
+ };
2058
+ }
2059
+ });
2060
+
2061
+ // src/rules/code/code-inline.ts
2062
+ import { defineRule as defineRule15 } from "@oxlint/plugins";
2063
+
2064
+ // src/utils/source.utils.ts
2065
+ var sourceDirective = /^\s*(?:\/\s*<(?:reference\b|amd-[\w-]+\b)|@ts-(?:check|ignore|expect-error|nocheck)\b|(?:#|@)__(?:PURE|NO_SIDE_EFFECTS)__\b|@jsx(?:ImportSource|Runtime|Frag)?\b|@vite-ignore\b|(?:[#@]\s*)?sourceMappingURL=|(?:es|ox)lint-(?:disable|enable)|prettier-ignore|biome-ignore|[cv]8 ignore|istanbul ignore|webpack[A-Z])/;
2066
+ var sourceLegalBlock = "\x00legal-block\x00";
2067
+ var sourceIndentSchema = {
2068
+ anyOf: [
2069
+ { type: "integer", minimum: 0 },
2070
+ { type: "string", enum: ["tab"] }
2071
+ ]
2072
+ };
2073
+ function sourceCommentText(comment) {
2074
+ const value = (comment.type === "Block" ? comment.value.replace(/^\s*\* ?/gm, "") : comment.value).trimStart();
2075
+ return comment.type === "Block" && comment.value.startsWith("!") ? sourceLegalBlock + value : value;
2076
+ }
2077
+ function sourceIsDirective(text) {
2078
+ return text.startsWith(sourceLegalBlock) || sourceDirective.test(text);
2079
+ }
2080
+ function sourceLineIndent(text, offset) {
2081
+ const lineStart = text.lastIndexOf(`
2082
+ `, offset - 1) + 1;
2083
+ return /^[\t ]*/.exec(text.slice(lineStart, offset))?.[0] ?? "";
2084
+ }
2085
+ function sourceIndentUnit(indent = 2) {
2086
+ return indent === "tab" ? "\t" : " ".repeat(indent);
2087
+ }
2088
+ function sourceNewline(text) {
2089
+ return text.includes(`\r
2090
+ `) ? `\r
2091
+ ` : `
2092
+ `;
2093
+ }
2094
+ function sourceLineStart(text, offset) {
2095
+ return text.lastIndexOf(`
2096
+ `, offset - 1) + 1;
2097
+ }
2098
+ function sourceLineEnd(text, offset) {
2099
+ const end = text.indexOf(`
2100
+ `, offset);
2101
+ return end === -1 ? text.length : end;
2102
+ }
2103
+ function sourceJoinedWidth(text, start, joined, end) {
2104
+ return start - sourceLineStart(text, start) + joined.length + text.slice(end, sourceLineEnd(text, end)).trimEnd().length;
2105
+ }
2106
+ function sourceItemGaps(open, items, close) {
2107
+ const edges = [open, ...items.flatMap((item) => item.range), close];
2108
+ return Array.from({ length: items.length + 1 }, (_, index) => [edges[index * 2] ?? open, edges[index * 2 + 1] ?? close]);
2109
+ }
2110
+ function sourceRespace(fixer, text, gaps, spacing) {
2111
+ const matches = gaps.map((gap) => /^\s*(,?)\s*$/.exec(text.slice(...gap)));
2112
+ if (matches.some((match) => match === null)) {
2113
+ return null;
2114
+ }
2115
+ return gaps.map((gap, index) => fixer.replaceTextRange(gap, `${matches[index]?.[1] ?? ""}${spacing(index, index === gaps.length - 1)}`));
2116
+ }
2117
+
2118
+ // src/rules/code/code-inline.ts
2119
+ function codeInlineIsData(node) {
2120
+ return node.type === "ObjectExpression" || node.type === "ArrayExpression" || (node.type === "TSSatisfiesExpression" || node.type === "TSAsExpression") && codeInlineIsData(node.expression);
2121
+ }
2122
+ function codeInlineOuterCall(node) {
2123
+ const parent = node.parent;
2124
+ if (!parent || astIsInlineFunction(parent) || parent.type.endsWith("Statement")) {
2125
+ return;
2126
+ }
2127
+ return astIsCall(parent) && parent.arguments.some((argument) => argument === node) ? parent : codeInlineOuterCall(parent);
2128
+ }
2129
+ function codeInlineEnclosingCalls(node) {
2130
+ const outer = codeInlineOuterCall(node);
2131
+ return outer && astIsOneLine(outer) ? 1 + codeInlineEnclosingCalls(outer) : 0;
2132
+ }
2133
+ var codeInlineWrapped = new Set(["ConditionalExpression", "LogicalExpression", "BinaryExpression", "ArrowFunctionExpression"]);
2134
+ var codeInlineEffects = new Set([
2135
+ "CallExpression",
2136
+ "NewExpression",
2137
+ "AssignmentExpression",
2138
+ "UpdateExpression",
2139
+ "AwaitExpression",
2140
+ "YieldExpression",
2141
+ "TaggedTemplateExpression",
2142
+ "ImportExpression"
2143
+ ]);
2144
+ function codeInlineEffectBetween(program, start, end) {
2145
+ return astSome(program, (node) => node.range[0] >= start && node.range[1] <= end && (codeInlineEffects.has(node.type) || node.type === "UnaryExpression" && node.operator === "delete"), (node) => node.range[1] > start && node.range[0] < end && !(node.range[0] >= start && (node.type === "FunctionDeclaration" || astIsInlineFunction(node))));
2146
+ }
2147
+ var codeInline = defineRule15({
2148
+ meta: {
2149
+ type: "suggestion",
2150
+ schema: [
2151
+ {
2152
+ type: "object",
2153
+ additionalProperties: false,
2154
+ properties: {
2155
+ max: { type: "integer", minimum: 1, default: 160 },
2156
+ nesting: { type: "integer", minimum: 1, default: 2 },
2157
+ allow: optionsPatternSchema,
2158
+ keep: optionsPatternSchema,
2159
+ services: { type: "array", items: { type: "string" } }
2160
+ }
2161
+ }
2162
+ ],
2163
+ messages: {
2164
+ once: "{{name}} is used once: inline it.",
2165
+ trivial: "{{name}} only wraps one call or expression: inline it at each use."
2166
+ }
2167
+ },
2168
+ createOnce(context) {
2169
+ let options = {};
2170
+ let allowed = [];
2171
+ let kept = [];
2172
+ const reads = (node) => {
2173
+ const variable = context.sourceCode.getDeclaredVariables(node).at(0);
2174
+ return variable ? variable.references.filter((reference) => !reference.init) : [];
2175
+ };
2176
+ const top = (node, id, init) => {
2177
+ if (id.type !== "Identifier" || !astIsOneLine(node) || init && codeInlineIsData(init) || optionsPatternsTest(allowed, id.name)) {
2178
+ return;
2179
+ }
2180
+ const uses = reads(node);
2181
+ if (uses.some((reference) => reference.identifier.parent.type === "ExportSpecifier")) {
2182
+ return;
2183
+ }
2184
+ if (uses.length === 1) {
2185
+ if (init && codeInlineEffectBetween(context.sourceCode.ast, node.range[1], uses[0].identifier.range[0])) {
2186
+ return;
2187
+ }
2188
+ context.report({
2189
+ node: id,
2190
+ messageId: "once",
2191
+ data: { name: id.name }
2192
+ });
2193
+ return;
2194
+ }
2195
+ const arrow = init?.type === "ArrowFunctionExpression" && init.body.type !== "BlockStatement" && !astSome(init.body, astIsInlineFunction);
2196
+ const call = init?.type === "CallExpression" && !astSome(init.arguments, astIsInlineFunction) && astCallDepth(init) <= 2;
2197
+ if (!optionsPatternsTest(kept, id.name) && (arrow || call)) {
2198
+ context.report({
2199
+ node: id,
2200
+ messageId: "trivial",
2201
+ data: { name: id.name }
2202
+ });
2203
+ }
2204
+ };
2205
+ const local = (declarator) => {
2206
+ const { id, init } = declarator;
2207
+ const yields = (node) => node.type === "YieldExpression";
2208
+ if (id.type !== "Identifier" || !init || !astIsOneLine(declarator) || codeInlineIsData(init) || astSome(init, yields, (node) => !astIsInlineFunction(node))) {
2209
+ return;
2210
+ }
2211
+ const uses = reads(declarator);
2212
+ const use = uses.at(0)?.identifier;
2213
+ if (!use || uses.length > 1 || optionsPatternsTest(allowed, id.name)) {
2214
+ return;
2215
+ }
2216
+ const text = context.sourceCode.text;
2217
+ const line = text.slice(sourceLineStart(text, use.range[0]), sourceLineEnd(text, use.range[0]));
2218
+ const owner = use.parent.type === "Property" ? use.parent.parent.parent : null;
2219
+ const service = owner && astIsCall(owner) && owner.callee.type === "MemberExpression" && (options.services ?? []).includes(astMemberName(owner.callee) ?? "");
2220
+ let deferred = false;
2221
+ for (let current = use.parent;current && current !== declarator.parent.parent; current = current.parent) {
2222
+ deferred ||= current.type === "FunctionDeclaration" || astIsInlineFunction(current);
2223
+ }
2224
+ const wrapped = codeInlineWrapped.has(init.type) && use.parent.type === "MemberExpression" && use.parent.object === use;
2225
+ if (service || wrapped || deferred || codeInlineEnclosingCalls(use) + astCallDepth(init) > (options.nesting ?? 2) || line.length - id.name.length + context.sourceCode.getText(init).length > (options.max ?? 160) || codeInlineEffectBetween(context.sourceCode.ast, declarator.range[1], use.range[0])) {
2226
+ return;
2227
+ }
2228
+ context.report({
2229
+ node: id,
2230
+ messageId: "once",
2231
+ data: { name: id.name }
2232
+ });
2233
+ };
2234
+ return {
2235
+ before() {
2236
+ options = optionsFirst(context, {});
2237
+ allowed = optionsPatterns(options.allow);
2238
+ kept = optionsPatterns(options.keep);
2239
+ },
2240
+ VariableDeclaration(node) {
2241
+ if (node.kind === "const" && node.parent.type === "BlockStatement") {
2242
+ node.declarations.forEach(local);
2243
+ }
2244
+ },
2245
+ Program(program) {
2246
+ for (const statement of program.body) {
2247
+ if (statement.type === "VariableDeclaration" && statement.kind === "const") {
2248
+ statement.declarations.forEach((declarator) => top(declarator, declarator.id, declarator.init));
2249
+ }
2250
+ if (statement.type === "FunctionDeclaration" && statement.id) {
2251
+ top(statement, statement.id, null);
2252
+ }
2253
+ }
2254
+ }
2255
+ };
2256
+ }
2257
+ });
2258
+
2259
+ // src/rules/code/code-justified-call.ts
2260
+ import { defineRule as defineRule16 } from "@oxlint/plugins";
2261
+ var codeJustifiedCall = defineRule16({
2262
+ meta: {
2263
+ type: "problem",
2264
+ schema: [
2265
+ {
2266
+ type: "object",
2267
+ additionalProperties: false,
2268
+ required: ["calls"],
2269
+ properties: {
2270
+ calls: { type: "array", items: { type: "string" }, minItems: 1 },
2271
+ comment: { type: "string", default: "SAFETY:" },
2272
+ message: { type: "string" }
2273
+ }
2274
+ }
2275
+ ],
2276
+ messages: {
2277
+ missing: "{{call}} needs a `// {{comment}} <why>` comment on the line above.{{message}}"
2278
+ }
2279
+ },
2280
+ createOnce(context) {
2281
+ let calls = new Set;
2282
+ let comment = "SAFETY:";
2283
+ let message = "";
2284
+ let comments = [];
2285
+ const justified = (line) => {
2286
+ const above = comments.find((candidate) => candidate.loc.end.line === line - 1);
2287
+ if (!above) {
2288
+ return false;
2289
+ }
2290
+ return above.value.trimStart().startsWith(`${comment} `) || above.type === "Line" && justified(above.loc.start.line);
2291
+ };
2292
+ return {
2293
+ before() {
2294
+ const options = optionsFirst(context, { calls: [] });
2295
+ calls = new Set(options.calls);
2296
+ comment = options.comment ?? "SAFETY:";
2297
+ message = options.message ? ` ${options.message}` : "";
2298
+ comments = context.sourceCode.getAllComments();
2299
+ },
2300
+ CallExpression(node) {
2301
+ const call = astDottedName(node.callee);
2302
+ if (call && calls.has(call) && !justified(node.loc.start.line)) {
2303
+ context.report({
2304
+ node,
2305
+ messageId: "missing",
2306
+ data: { call, comment, message }
2307
+ });
2308
+ }
2309
+ }
2310
+ };
2311
+ }
2312
+ });
2313
+
1874
2314
  // src/rules/code/code-literal-in.ts
1875
- import { defineRule as defineRule12 } from "@oxlint/plugins";
1876
- var codeLiteralIn = defineRule12({
2315
+ import { defineRule as defineRule17 } from "@oxlint/plugins";
2316
+ var codeLiteralIn = defineRule17({
1877
2317
  meta: {
1878
2318
  type: "suggestion",
1879
2319
  schema: [
@@ -1914,7 +2354,7 @@ var codeLiteralIn = defineRule12({
1914
2354
  });
1915
2355
 
1916
2356
  // src/rules/code/code-mutable-state.ts
1917
- import { defineRule as defineRule13 } from "@oxlint/plugins";
2357
+ import { defineRule as defineRule18 } from "@oxlint/plugins";
1918
2358
  var functionScopes = new Set([
1919
2359
  "FunctionDeclaration",
1920
2360
  "FunctionExpression",
@@ -1931,7 +2371,7 @@ function hasAncestor(node, test) {
1931
2371
  }
1932
2372
  return false;
1933
2373
  }
1934
- var codeMutableState = defineRule13({
2374
+ var codeMutableState = defineRule18({
1935
2375
  meta: {
1936
2376
  type: "problem",
1937
2377
  schema: [
@@ -1989,7 +2429,7 @@ var codeMutableState = defineRule13({
1989
2429
  });
1990
2430
 
1991
2431
  // src/rules/code/code-object-params.ts
1992
- import { defineRule as defineRule14 } from "@oxlint/plugins";
2432
+ import { defineRule as defineRule19 } from "@oxlint/plugins";
1993
2433
  function isObjectParam(node) {
1994
2434
  if (node.type === "ObjectPattern") {
1995
2435
  return true;
@@ -2003,7 +2443,7 @@ function isObjectParam(node) {
2003
2443
  const annotation = node.typeAnnotation?.typeAnnotation;
2004
2444
  return !annotation || annotation.type === "TSTypeReference" || annotation.type === "TSTypeLiteral" || annotation.type === "TSIntersectionType";
2005
2445
  }
2006
- var codeObjectParams = defineRule14({
2446
+ var codeObjectParams = defineRule19({
2007
2447
  meta: {
2008
2448
  type: "problem",
2009
2449
  schema: [
@@ -2052,8 +2492,8 @@ var codeObjectParams = defineRule14({
2052
2492
  });
2053
2493
 
2054
2494
  // src/rules/code/code-paired-call.ts
2055
- import { defineRule as defineRule15 } from "@oxlint/plugins";
2056
- var codePairedCall = defineRule15({
2495
+ import { defineRule as defineRule20 } from "@oxlint/plugins";
2496
+ var codePairedCall = defineRule20({
2057
2497
  meta: {
2058
2498
  type: "problem",
2059
2499
  schema: [
@@ -2084,7 +2524,6 @@ var codePairedCall = defineRule15({
2084
2524
  }
2085
2525
  },
2086
2526
  createOnce(context) {
2087
- const aliases = new Map;
2088
2527
  let pairs = [];
2089
2528
  return {
2090
2529
  before() {
@@ -2092,26 +2531,10 @@ var codePairedCall = defineRule15({
2092
2531
  const all = when && require2 ? [{ when, require: require2 }, ...configured] : configured;
2093
2532
  pairs = all.map((pair) => ({ ...pair, firstWhen: null, sawRequired: false }));
2094
2533
  },
2095
- Program(program) {
2096
- aliases.clear();
2097
- for (const statement of program.body) {
2098
- if (statement.type !== "ImportDeclaration") {
2099
- continue;
2100
- }
2101
- for (const specifier of statement.specifiers) {
2102
- if (specifier.type !== "ImportSpecifier") {
2103
- continue;
2104
- }
2105
- const imported = astModuleName(specifier.imported);
2106
- if (imported !== specifier.local.name) {
2107
- aliases.set(specifier.local.name, imported);
2108
- }
2109
- }
2110
- }
2111
- },
2112
2534
  CallExpression(node) {
2113
- const called = astCallName(node);
2114
- const name = called ? aliases.get(called) ?? called : undefined;
2535
+ const callee = node.callee;
2536
+ const binding = callee.type === "Identifier" ? boundariesDeclaredVariable(context.sourceCode.getScope(node), callee.name)?.defs.at(0)?.node : undefined;
2537
+ const name = binding?.type === "ImportSpecifier" ? astModuleName(binding.imported) : astCallName(node);
2115
2538
  for (const pair of pairs) {
2116
2539
  if (name === pair.when) {
2117
2540
  pair.firstWhen ??= node;
@@ -2137,7 +2560,7 @@ var codePairedCall = defineRule15({
2137
2560
  });
2138
2561
 
2139
2562
  // src/rules/code/code-promise-mutation.ts
2140
- import { defineRule as defineRule16 } from "@oxlint/plugins";
2563
+ import { defineRule as defineRule21 } from "@oxlint/plugins";
2141
2564
  function mutationBinding(node) {
2142
2565
  if (node.type === "Identifier") {
2143
2566
  return node;
@@ -2166,7 +2589,7 @@ function mutationBindings(node) {
2166
2589
  const binding = mutationBinding(node);
2167
2590
  return binding ? [binding] : [];
2168
2591
  }
2169
- var codePromiseMutation = defineRule16({
2592
+ var codePromiseMutation = defineRule21({
2170
2593
  meta: {
2171
2594
  type: "problem",
2172
2595
  schema: [
@@ -2258,8 +2681,96 @@ var codePromiseMutation = defineRule16({
2258
2681
  // src/rules/code/code-switch.ts
2259
2682
  var codeSwitch = astSelectorRule("SwitchStatement", "Avoid switch statements.");
2260
2683
 
2684
+ // src/rules/code/code-traced.ts
2685
+ import { defineRule as defineRule22 } from "@oxlint/plugins";
2686
+ var codeTraced = defineRule22({
2687
+ meta: {
2688
+ type: "problem",
2689
+ schema: [
2690
+ {
2691
+ type: "object",
2692
+ additionalProperties: false,
2693
+ properties: {
2694
+ untraced: { type: "array", items: { type: "string" }, default: ["Effect.fnUntraced"] },
2695
+ exports: optionsPatternSchema,
2696
+ services: { type: "array", items: { type: "string" }, default: ["of"] },
2697
+ skip: optionsPatternSchema,
2698
+ message: { type: "string" }
2699
+ }
2700
+ }
2701
+ ],
2702
+ messages: {
2703
+ surface: "{{message}}"
2704
+ }
2705
+ },
2706
+ createOnce(context) {
2707
+ let untraced = new Set;
2708
+ let services = new Set;
2709
+ let exported = true;
2710
+ let skipped = [];
2711
+ let message = "";
2712
+ const isUntraced = (node) => node?.type === "CallExpression" && untraced.has(astDottedName(node.callee) ?? "");
2713
+ const resolves = (value) => {
2714
+ if (value.type !== "Identifier") {
2715
+ return isUntraced(value);
2716
+ }
2717
+ let scope = context.sourceCode.getScope(value);
2718
+ while (scope && !scope.set.has(value.name)) {
2719
+ scope = scope.upper;
2720
+ }
2721
+ const declaration = scope?.set.get(value.name)?.defs.at(0)?.node;
2722
+ return declaration?.type === "VariableDeclarator" && isUntraced(declaration.init);
2723
+ };
2724
+ return {
2725
+ before() {
2726
+ const options = optionsFirst(context, {});
2727
+ const files = optionsPatterns(options.exports);
2728
+ untraced = new Set(options.untraced ?? ["Effect.fnUntraced"]);
2729
+ services = new Set(options.services ?? ["of"]);
2730
+ exported = files.length === 0 || optionsPatternsTest(files, context.filename.replaceAll("\\", "/"));
2731
+ skipped = optionsPatterns(options.skip);
2732
+ message = options.message ?? `An exported function or service method gets a span: replace ${[...untraced].join(", ")} with its traced form.`;
2733
+ },
2734
+ ExportNamedDeclaration(node) {
2735
+ if (!exported) {
2736
+ return;
2737
+ }
2738
+ const declarators = node.declaration?.type === "VariableDeclaration" ? node.declaration.declarations.filter((declarator) => isUntraced(declarator.init)) : [];
2739
+ const specifiers = node.source ? [] : node.specifiers.filter((specifier) => resolves(specifier.local));
2740
+ for (const surface of [...declarators, ...specifiers]) {
2741
+ context.report({
2742
+ node: surface,
2743
+ messageId: "surface",
2744
+ data: { message }
2745
+ });
2746
+ }
2747
+ },
2748
+ CallExpression(node) {
2749
+ const service = node.arguments.at(0);
2750
+ const callee = node.callee;
2751
+ if (callee.type !== "MemberExpression" || !services.has(astMemberName(callee) ?? "") || service?.type !== "ObjectExpression") {
2752
+ return;
2753
+ }
2754
+ const tag = callee.object.type === "MemberExpression" ? astMemberName(callee.object) : astDottedName(callee.object);
2755
+ if (optionsPatternsTest(skipped, tag ?? "")) {
2756
+ return;
2757
+ }
2758
+ for (const property of service.properties) {
2759
+ if (property.type === "Property" && resolves(property.value)) {
2760
+ context.report({
2761
+ node: property,
2762
+ messageId: "surface",
2763
+ data: { message }
2764
+ });
2765
+ }
2766
+ }
2767
+ }
2768
+ };
2769
+ }
2770
+ });
2771
+
2261
2772
  // src/rules/code/code-trivial-functions.ts
2262
- import { defineRule as defineRule17 } from "@oxlint/plugins";
2773
+ import { defineRule as defineRule23 } from "@oxlint/plugins";
2263
2774
  var codeTrivialFunctionsBanned = [
2264
2775
  "isRecord",
2265
2776
  "isPlainObject",
@@ -2389,7 +2900,7 @@ function guardStatement(node, state) {
2389
2900
  return guardFallback(declaration.init);
2390
2901
  });
2391
2902
  }
2392
- var codeTrivialFunctions = defineRule17({
2903
+ var codeTrivialFunctions = defineRule23({
2393
2904
  meta: {
2394
2905
  type: "problem",
2395
2906
  schema: [
@@ -2519,41 +3030,31 @@ var codeTrivialFunctions = defineRule17({
2519
3030
  }
2520
3031
  });
2521
3032
 
2522
- // src/rules/comments/comments-code.ts
2523
- import { defineRule as defineRule18 } from "@oxlint/plugins";
2524
-
2525
- // src/utils/source.utils.ts
2526
- var sourceDirective = /^\s*(?:\/\s*<(?:reference\b|amd-[\w-]+\b)|@ts-(?:check|ignore|expect-error|nocheck)\b|(?:#|@)__(?:PURE|NO_SIDE_EFFECTS)__\b|@jsx(?:ImportSource|Runtime|Frag)?\b|@vite-ignore\b|(?:[#@]\s*)?sourceMappingURL=|(?:es|ox)lint-(?:disable|enable)|prettier-ignore|biome-ignore|[cv]8 ignore|istanbul ignore|webpack[A-Z])/;
2527
- var sourceLegalBlock = "\x00legal-block\x00";
2528
- var sourceIndentSchema = {
2529
- anyOf: [
2530
- { type: "integer", minimum: 0 },
2531
- { type: "string", enum: ["tab"] }
2532
- ]
2533
- };
2534
- function sourceCommentText(comment) {
2535
- const value = (comment.type === "Block" ? comment.value.replace(/^\s*\* ?/gm, "") : comment.value).trimStart();
2536
- return comment.type === "Block" && comment.value.startsWith("!") ? sourceLegalBlock + value : value;
2537
- }
2538
- function sourceIsDirective(text) {
2539
- return text.startsWith(sourceLegalBlock) || sourceDirective.test(text);
2540
- }
2541
- function sourceLineIndent(text, offset) {
2542
- const lineStart = text.lastIndexOf(`
2543
- `, offset - 1) + 1;
2544
- return /^[\t ]*/.exec(text.slice(lineStart, offset))?.[0] ?? "";
2545
- }
2546
- function sourceIndentUnit(indent = 2) {
2547
- return indent === "tab" ? "\t" : " ".repeat(indent);
2548
- }
2549
- function sourceNewline(text) {
2550
- return text.includes(`\r
2551
- `) ? `\r
2552
- ` : `
2553
- `;
2554
- }
3033
+ // src/rules/code/code-yield-argument.ts
3034
+ import { defineRule as defineRule24 } from "@oxlint/plugins";
3035
+ var codeYieldArgument = defineRule24({
3036
+ meta: {
3037
+ type: "suggestion",
3038
+ messages: {
3039
+ named: "Yield into a named const first, then pass the name."
3040
+ }
3041
+ },
3042
+ createOnce(context) {
3043
+ return {
3044
+ YieldExpression(node) {
3045
+ const parent = node.parent;
3046
+ const argument = astIsCall(parent) && parent.arguments.some((item) => item === node);
3047
+ const receiver = parent.type === "MemberExpression" && parent.object === node;
3048
+ if (node.delegate && (argument || receiver)) {
3049
+ context.report({ node, messageId: "named" });
3050
+ }
3051
+ }
3052
+ };
3053
+ }
3054
+ });
2555
3055
 
2556
3056
  // src/rules/comments/comments-code.ts
3057
+ import { defineRule as defineRule25 } from "@oxlint/plugins";
2557
3058
  var commentsLineGap = /^[\t \r\n\u2028\u2029]*$/;
2558
3059
  var commentsLineTerminator = /\r\n|[\n\r\u2028\u2029]/;
2559
3060
  var commentsUnicodeTerminator = /[\u2028\u2029]/;
@@ -2604,7 +3105,7 @@ function commentsFollowsOpeningBrace(text, offset) {
2604
3105
  }
2605
3106
  return false;
2606
3107
  }
2607
- var commentsCode = defineRule18({
3108
+ var commentsCode = defineRule25({
2608
3109
  meta: {
2609
3110
  type: "suggestion",
2610
3111
  fixable: "code",
@@ -2613,7 +3114,7 @@ var commentsCode = defineRule18({
2613
3114
  type: "object",
2614
3115
  additionalProperties: false,
2615
3116
  properties: {
2616
- why: { type: "boolean", default: true },
3117
+ why: { anyOf: [{ type: "boolean" }, { type: "string", minLength: 1 }], default: true },
2617
3118
  allow: { type: "array", items: { type: "string" } },
2618
3119
  jsdoc: { type: "boolean", default: false },
2619
3120
  members: { type: "string", enum: ["off", "also", "only"] }
@@ -2627,6 +3128,7 @@ var commentsCode = defineRule18({
2627
3128
  },
2628
3129
  createOnce(context) {
2629
3130
  let allowWhy = true;
3131
+ let whyPrefix = "Why: ";
2630
3132
  let allowJsdoc = false;
2631
3133
  let allowed = [];
2632
3134
  let members = "off";
@@ -2651,7 +3153,8 @@ var commentsCode = defineRule18({
2651
3153
  return {
2652
3154
  before() {
2653
3155
  const options = optionsFirst(context, {});
2654
- allowWhy = options.why ?? true;
3156
+ allowWhy = options.why !== false;
3157
+ whyPrefix = `${typeof options.why === "string" ? options.why.trim() : "Why:"} `;
2655
3158
  allowJsdoc = options.jsdoc ?? false;
2656
3159
  allowed = (options.allow ?? []).map((pattern) => new RegExp(pattern));
2657
3160
  members = options.members ?? "off";
@@ -2680,7 +3183,7 @@ var commentsCode = defineRule18({
2680
3183
  mightContainJsxComment = true;
2681
3184
  }
2682
3185
  const followsPrevious = previous !== undefined && comment.loc.start.line === previous.loc.end.line + 1 && commentsLineGap.test(text.slice(previous.range[1], comment.range[0]));
2683
- const isWhy = value.startsWith("Why: ") || comment.type === "Line" && whyContinuation && followsPrevious;
3186
+ const isWhy = value.startsWith(whyPrefix) || comment.type === "Line" && whyContinuation && followsPrevious;
2684
3187
  if (/^(?:eslint|oxlint)-disable-next-line\b/.test(value)) {
2685
3188
  protectedLines.push(comment.loc.end.line + 1);
2686
3189
  }
@@ -2768,9 +3271,9 @@ var commentsCode = defineRule18({
2768
3271
  });
2769
3272
 
2770
3273
  // src/rules/comments/comments-directives.ts
2771
- import { defineRule as defineRule19 } from "@oxlint/plugins";
3274
+ import { defineRule as defineRule26 } from "@oxlint/plugins";
2772
3275
  var directive = /^\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\b(.*)$/s;
2773
- var commentsDirectives = defineRule19({
3276
+ var commentsDirectives = defineRule26({
2774
3277
  meta: {
2775
3278
  type: "problem",
2776
3279
  schema: [],
@@ -2798,9 +3301,9 @@ var commentsDirectives = defineRule19({
2798
3301
  });
2799
3302
 
2800
3303
  // src/rules/copy/copy-dashes.ts
2801
- import { defineRule as defineRule20 } from "@oxlint/plugins";
3304
+ import { defineRule as defineRule27 } from "@oxlint/plugins";
2802
3305
  var copyDashesMessage = "Use a comma, colon, period or parentheses instead.";
2803
- var copyDashes = defineRule20({
3306
+ var copyDashes = defineRule27({
2804
3307
  meta: {
2805
3308
  type: "suggestion",
2806
3309
  schema: [
@@ -2856,7 +3359,7 @@ var copyDashes = defineRule20({
2856
3359
  });
2857
3360
 
2858
3361
  // src/rules/files/files-components-only.ts
2859
- import { defineRule as defineRule21 } from "@oxlint/plugins";
3362
+ import { defineRule as defineRule28 } from "@oxlint/plugins";
2860
3363
 
2861
3364
  // src/utils/react.utils.ts
2862
3365
  function reactReturnsOutput(program, node) {
@@ -2928,7 +3431,7 @@ function reactIsComponent(program, candidate) {
2928
3431
  function componentName(basename2) {
2929
3432
  return basename2.replace(/\.(tsx?|jsx?)$/, "").replaceAll(/(?:^[-.]*|[-.]+)(.?)/g, (_, char) => char.toUpperCase());
2930
3433
  }
2931
- var filesComponentsOnly = defineRule21({
3434
+ var filesComponentsOnly = defineRule28({
2932
3435
  meta: {
2933
3436
  type: "problem",
2934
3437
  schema: [
@@ -2936,7 +3439,7 @@ var filesComponentsOnly = defineRule21({
2936
3439
  type: "object",
2937
3440
  additionalProperties: false,
2938
3441
  properties: {
2939
- filename: { type: "boolean" },
3442
+ filename: { oneOf: [{ type: "boolean" }, { type: "string", enum: ["each"] }] },
2940
3443
  roles: { type: "array", items: { type: "string" } },
2941
3444
  types: { type: "boolean" },
2942
3445
  deny: optionsPatternSchema
@@ -2997,10 +3500,13 @@ var filesComponentsOnly = defineRule21({
2997
3500
  return;
2998
3501
  }
2999
3502
  const names = components.map((candidate) => candidate.name.toLowerCase());
3000
- if (expectedKeys.some((key) => names.includes(key) || names.every((name) => name.startsWith(key)))) {
3503
+ if (matchFileName !== "each" && expectedKeys.some((key) => names.includes(key) || names.every((name) => name.startsWith(key)))) {
3001
3504
  return;
3002
3505
  }
3003
3506
  for (const { name, node } of components) {
3507
+ if (matchFileName === "each" && expectedKeys.some((key) => name.toLowerCase().startsWith(key))) {
3508
+ continue;
3509
+ }
3004
3510
  context.report({
3005
3511
  node,
3006
3512
  messageId: "nameMismatch",
@@ -3013,7 +3519,7 @@ var filesComponentsOnly = defineRule21({
3013
3519
  });
3014
3520
 
3015
3521
  // src/rules/files/files-constants-only.ts
3016
- import { defineRule as defineRule22 } from "@oxlint/plugins";
3522
+ import { defineRule as defineRule29 } from "@oxlint/plugins";
3017
3523
  function unwrap(expression) {
3018
3524
  if (expression.type === "ParenthesizedExpression" || expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSTypeAssertion" || expression.type === "TSNonNullExpression" || expression.type === "TSInstantiationExpression") {
3019
3525
  return unwrap(expression.expression);
@@ -3068,7 +3574,7 @@ function collectBindings(program) {
3068
3574
  }
3069
3575
  return bindings;
3070
3576
  }
3071
- var filesConstantsOnly = defineRule22({
3577
+ var filesConstantsOnly = defineRule29({
3072
3578
  meta: {
3073
3579
  type: "problem",
3074
3580
  schema: [
@@ -3167,7 +3673,7 @@ var filesConstantsOnly = defineRule22({
3167
3673
  });
3168
3674
 
3169
3675
  // src/rules/files/files-domain-constants.ts
3170
- import { defineRule as defineRule23 } from "@oxlint/plugins";
3676
+ import { defineRule as defineRule30 } from "@oxlint/plugins";
3171
3677
  import { basename as basename2, dirname as dirname2 } from "node:path";
3172
3678
  function isData(expression) {
3173
3679
  const value = astUnwrap(expression);
@@ -3185,14 +3691,21 @@ function isData(expression) {
3185
3691
  }
3186
3692
  return value.type === "NewExpression" && value.callee.type === "Identifier" && ["Map", "Set"].includes(value.callee.name) && value.arguments[0]?.type === "ArrayExpression" && value.arguments[0].elements.length > 0;
3187
3693
  }
3188
- var filesDomainConstants = defineRule23({
3694
+ var filesDomainConstants = defineRule30({
3189
3695
  meta: {
3190
3696
  type: "problem",
3191
3697
  schema: [
3192
3698
  {
3193
3699
  type: "object",
3194
3700
  additionalProperties: false,
3195
- properties: { data: { type: "boolean" }, services: { type: "boolean" } }
3701
+ properties: {
3702
+ data: { type: "boolean" },
3703
+ services: { type: "boolean" },
3704
+ pattern: { ...optionsPatternSchema, default: "^[A-Z][A-Z0-9_]+$" },
3705
+ folders: { type: "array", items: { type: "string", minLength: 1 }, default: ["services"] },
3706
+ roles: { type: "array", items: { type: "string", minLength: 1 }, default: ["action", "query"] },
3707
+ file: { type: "string", minLength: 1, default: "{domain}.constants.ts" }
3708
+ }
3196
3709
  }
3197
3710
  ],
3198
3711
  messages: {
@@ -3201,11 +3714,19 @@ var filesDomainConstants = defineRule23({
3201
3714
  },
3202
3715
  createOnce(context) {
3203
3716
  let includeData = false;
3717
+ let names = [];
3718
+ let folders = [];
3719
+ let roles = [];
3720
+ let target = "";
3204
3721
  let serviceObjects = new Set;
3205
3722
  return {
3206
3723
  Program(program) {
3207
3724
  const options = optionsFirst(context, {});
3208
3725
  includeData = Boolean(options.data);
3726
+ names = optionsPatterns(options.pattern ?? "^[A-Z][A-Z0-9_]+$");
3727
+ folders = options.folders ?? ["services"];
3728
+ roles = options.roles ?? ["action", "query"];
3729
+ target = options.file ?? "{domain}.constants.ts";
3209
3730
  serviceObjects = new Set(options.services ? declarationsPublicServiceMethods(program).map((method) => method.binding) : []);
3210
3731
  },
3211
3732
  VariableDeclarator(node) {
@@ -3215,15 +3736,19 @@ var filesDomainConstants = defineRule23({
3215
3736
  return;
3216
3737
  }
3217
3738
  const name = node.id.type === "Identifier" ? node.id.name : undefined;
3218
- if ((!name || !/^[A-Z][A-Z0-9_]+$/.test(name)) && (!includeData || !node.init || !isData(node.init))) {
3739
+ if ((!name || !optionsPatternsTest(names, name)) && (!includeData || !node.init || !isData(node.init))) {
3219
3740
  return;
3220
3741
  }
3221
3742
  const file = context.filename.replaceAll("\\", "/");
3222
- const owner = file.includes("/services/") && dirname2(file).split("/").at(-1) !== "services" ? basename2(dirname2(file)) : basename2(file).split(".")[0].replace(/-(?:action|query)$/, "");
3743
+ const folder = basename2(dirname2(file));
3744
+ const stem = basename2(file).split(".")[0];
3745
+ const role = roles.find((candidate) => stem.endsWith(`-${candidate}`));
3746
+ const domain = role ? stem.slice(0, -role.length - 1) : stem;
3747
+ const owner = folders.some((candidate) => file.includes(`/${candidate}/`) && folder !== candidate) ? folder : domain;
3223
3748
  context.report({
3224
3749
  node,
3225
3750
  messageId: "forbidden",
3226
- data: { name: name ?? "<destructured binding>", file: `${owner}.constants.ts` }
3751
+ data: { name: name ?? "<destructured binding>", file: target.replaceAll("{domain}", owner) }
3227
3752
  });
3228
3753
  }
3229
3754
  };
@@ -3231,11 +3756,11 @@ var filesDomainConstants = defineRule23({
3231
3756
  });
3232
3757
 
3233
3758
  // src/rules/files/files-exports.ts
3234
- import { defineRule as defineRule24 } from "@oxlint/plugins";
3759
+ import { defineRule as defineRule31 } from "@oxlint/plugins";
3235
3760
  function escapeRegExp(value) {
3236
3761
  return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
3237
3762
  }
3238
- var filesExports = defineRule24({
3763
+ var filesExports = defineRule31({
3239
3764
  meta: {
3240
3765
  type: "problem",
3241
3766
  schema: [
@@ -3316,11 +3841,12 @@ var filesExports = defineRule24({
3316
3841
  });
3317
3842
 
3318
3843
  // src/rules/files/files-externals.ts
3319
- import { defineRule as defineRule25 } from "@oxlint/plugins";
3844
+ import { defineRule as defineRule32 } from "@oxlint/plugins";
3320
3845
  import { existsSync as existsSync2, readFileSync } from "node:fs";
3321
3846
  import { builtinModules as builtinModules2 } from "node:module";
3322
3847
  import { dirname as dirname3, join as join2 } from "node:path";
3323
- var filesExternals = defineRule25({
3848
+ var filesExternalsConfigs = "^(?:vite|next)\\.config\\.[cm]?[jt]s$";
3849
+ var filesExternals = defineRule32({
3324
3850
  meta: {
3325
3851
  type: "problem",
3326
3852
  schema: [
@@ -3328,7 +3854,8 @@ var filesExternals = defineRule25({
3328
3854
  type: "object",
3329
3855
  additionalProperties: false,
3330
3856
  properties: {
3331
- packages: { type: "array", items: { type: "string", minLength: 1 } }
3857
+ packages: { type: "array", items: { type: "string", minLength: 1 } },
3858
+ files: { ...optionsPatternSchema, default: filesExternalsConfigs }
3332
3859
  }
3333
3860
  }
3334
3861
  ],
@@ -3384,7 +3911,8 @@ var filesExternals = defineRule25({
3384
3911
  }
3385
3912
  return {
3386
3913
  before() {
3387
- if (!/^(?:vite|next)\.config\.[cm]?[jt]s$/.test(namingBasename(context.filename))) {
3914
+ const files = optionsPatterns(optionsFirst(context, {}).files ?? filesExternalsConfigs);
3915
+ if (!optionsPatternsTest(files, namingBasename(context.filename))) {
3388
3916
  return false;
3389
3917
  }
3390
3918
  },
@@ -3411,7 +3939,7 @@ var filesExternals = defineRule25({
3411
3939
  });
3412
3940
 
3413
3941
  // src/rules/files/files-factory-keys.ts
3414
- import { defineRule as defineRule26 } from "@oxlint/plugins";
3942
+ import { defineRule as defineRule33 } from "@oxlint/plugins";
3415
3943
  function returnedObject(expression) {
3416
3944
  if (expression?.type === "TSAsExpression" || expression?.type === "TSSatisfiesExpression" || expression?.type === "TSTypeAssertion" || expression?.type === "TSNonNullExpression") {
3417
3945
  return returnedObject(expression.expression);
@@ -3422,7 +3950,7 @@ function compileKeys(pattern, keys, requireKeys) {
3422
3950
  const allowedList = keys.concat(requireKeys.filter((key) => !keys.includes(key)));
3423
3951
  return { patterns: optionsPatternsWithEmpty(pattern), allowed: new Set(allowedList), allowedList, required: requireKeys };
3424
3952
  }
3425
- var filesFactoryKeys = defineRule26({
3953
+ var filesFactoryKeys = defineRule33({
3426
3954
  meta: {
3427
3955
  type: "problem",
3428
3956
  schema: [
@@ -3540,8 +4068,8 @@ var filesFactoryKeys = defineRule26({
3540
4068
  });
3541
4069
 
3542
4070
  // src/rules/files/files-helpers.ts
3543
- import { defineRule as defineRule27 } from "@oxlint/plugins";
3544
- var filesHelpers = defineRule27({
4071
+ import { defineRule as defineRule34 } from "@oxlint/plugins";
4072
+ var filesHelpers = defineRule34({
3545
4073
  meta: {
3546
4074
  type: "problem",
3547
4075
  schema: [
@@ -3589,8 +4117,8 @@ var filesHelpers = defineRule27({
3589
4117
  });
3590
4118
 
3591
4119
  // src/rules/files/files-no-functions.ts
3592
- import { defineRule as defineRule28 } from "@oxlint/plugins";
3593
- var filesNoFunctions = defineRule28({
4120
+ import { defineRule as defineRule35 } from "@oxlint/plugins";
4121
+ var filesNoFunctions = defineRule35({
3594
4122
  meta: {
3595
4123
  type: "problem",
3596
4124
  schema: [
@@ -3644,8 +4172,8 @@ var filesNoFunctions = defineRule28({
3644
4172
  });
3645
4173
 
3646
4174
  // src/rules/files/files-restricted.ts
3647
- import { defineRule as defineRule29 } from "@oxlint/plugins";
3648
- var filesRestricted = defineRule29({
4175
+ import { defineRule as defineRule36 } from "@oxlint/plugins";
4176
+ var filesRestricted = defineRule36({
3649
4177
  meta: {
3650
4178
  type: "problem",
3651
4179
  schema: [
@@ -3677,7 +4205,7 @@ var filesRestricted = defineRule29({
3677
4205
  });
3678
4206
 
3679
4207
  // src/rules/files/files-route.ts
3680
- import { defineRule as defineRule30 } from "@oxlint/plugins";
4208
+ import { defineRule as defineRule37 } from "@oxlint/plugins";
3681
4209
  function inspectAdapter(context, item, bannedHooks, banIntrinsicJsx) {
3682
4210
  astVisit(item.node.body, [item.node], (node) => {
3683
4211
  if (banIntrinsicJsx && node.type === "JSXOpeningElement" && node.name.type === "JSXIdentifier" && /^[a-z]/.test(node.name.name)) {
@@ -3697,7 +4225,7 @@ function inspectAdapter(context, item, bannedHooks, banIntrinsicJsx) {
3697
4225
  }
3698
4226
  });
3699
4227
  }
3700
- var filesRoute = defineRule30({
4228
+ var filesRoute = defineRule37({
3701
4229
  meta: {
3702
4230
  type: "problem",
3703
4231
  schema: [
@@ -3767,7 +4295,7 @@ var filesRoute = defineRule30({
3767
4295
  });
3768
4296
 
3769
4297
  // src/rules/files/files-types-only.ts
3770
- import { defineRule as defineRule31 } from "@oxlint/plugins";
4298
+ import { defineRule as defineRule38 } from "@oxlint/plugins";
3771
4299
  function runtimeImportLocalNames(statement) {
3772
4300
  return statement.specifiers.flatMap((specifier) => specifier.type !== "ImportSpecifier" || specifier.importKind !== "type" ? [specifier.local.name] : []);
3773
4301
  }
@@ -3936,7 +4464,7 @@ function runtimeKind(node) {
3936
4464
  }
3937
4465
  return node.type === "FunctionDeclaration" ? "function" : "variable";
3938
4466
  }
3939
- var filesTypesOnly = defineRule31({
4467
+ var filesTypesOnly = defineRule38({
3940
4468
  meta: {
3941
4469
  type: "problem",
3942
4470
  schema: [
@@ -4043,9 +4571,66 @@ var filesTypesOnly = defineRule31({
4043
4571
  }
4044
4572
  });
4045
4573
 
4574
+ // src/rules/layout/layout-arrow-body.ts
4575
+ import { defineRule as defineRule39 } from "@oxlint/plugins";
4576
+ var layoutArrowBody = defineRule39({
4577
+ meta: {
4578
+ type: "layout",
4579
+ fixable: "whitespace",
4580
+ schema: [
4581
+ {
4582
+ type: "object",
4583
+ additionalProperties: false,
4584
+ properties: { max: { type: "integer", minimum: 1, default: 160 } }
4585
+ }
4586
+ ],
4587
+ messages: {
4588
+ sameLine: "Start the arrow body on the same line as =>. When the line is too long, break its arguments."
4589
+ }
4590
+ },
4591
+ createOnce(context) {
4592
+ let max = 160;
4593
+ return {
4594
+ before() {
4595
+ max = optionsFirst(context, {}).max ?? 160;
4596
+ },
4597
+ ArrowFunctionExpression(node) {
4598
+ const source = context.sourceCode;
4599
+ const body = node.body;
4600
+ const arrow = source.getTokenBefore(body, { filter: (token) => token.value === "=>" });
4601
+ const next = arrow ? source.getTokenAfter(arrow) : null;
4602
+ if (body.type === "BlockStatement" || !arrow || !next || next.loc.start.line === arrow.loc.end.line || source.commentsExistBetween(arrow, next)) {
4603
+ return;
4604
+ }
4605
+ let root = body;
4606
+ let calls = 0;
4607
+ while (root.type === "CallExpression" && root.callee.type === "MemberExpression") {
4608
+ calls += 1;
4609
+ root = root.callee.object;
4610
+ }
4611
+ const open = astIsCall(body) && astIsOneLine(body) ? source.getTokenAfter(body.typeArguments ?? body.callee) : null;
4612
+ const callEnd = open ? open.range[1] : sourceLineEnd(source.text, next.range[0]);
4613
+ const headEnd = calls >= 2 && astIsOneLine(body) ? root.range[1] : callEnd;
4614
+ if (arrow.loc.end.column + 1 + source.text.slice(next.range[0], headEnd).length > max) {
4615
+ return;
4616
+ }
4617
+ context.report({
4618
+ node: body,
4619
+ messageId: "sameLine",
4620
+ fix: (fixer) => fixer.replaceTextRange([arrow.range[1], next.range[0]], " ")
4621
+ });
4622
+ }
4623
+ };
4624
+ }
4625
+ });
4626
+
4046
4627
  // src/rules/layout/layout-call-arguments.ts
4047
- import { defineRule as defineRule32 } from "@oxlint/plugins";
4048
- var layoutCallArguments = defineRule32({
4628
+ import { defineRule as defineRule40 } from "@oxlint/plugins";
4629
+ function layoutCallArgumentsHuggable(node) {
4630
+ return astIsInlineFunction(node) || node.type === "ObjectExpression" || node.type === "ArrayExpression" || astIsCall(node) && node.arguments.length === 1 && (node.callee.type !== "MemberExpression" || astIsOneLine(node.callee)) && node.arguments.every(layoutCallArgumentsHuggable);
4631
+ }
4632
+ var layoutCallArgumentsMax = { type: "integer", minimum: 1 };
4633
+ var layoutCallArguments = defineRule40({
4049
4634
  meta: {
4050
4635
  type: "layout",
4051
4636
  fixable: "whitespace",
@@ -4059,22 +4644,43 @@ var layoutCallArguments = defineRule32({
4059
4644
  minimum: 2,
4060
4645
  default: 3
4061
4646
  },
4062
- indent: {
4063
- type: "integer",
4064
- minimum: 1,
4065
- default: 2
4647
+ indent: { ...sourceIndentSchema, default: 2 },
4648
+ split: {
4649
+ type: "boolean",
4650
+ default: true
4651
+ },
4652
+ hug: {
4653
+ type: "object",
4654
+ additionalProperties: false,
4655
+ required: ["max"],
4656
+ properties: { max: layoutCallArgumentsMax }
4657
+ },
4658
+ join: {
4659
+ type: "object",
4660
+ additionalProperties: false,
4661
+ required: ["max"],
4662
+ properties: {
4663
+ max: layoutCallArgumentsMax,
4664
+ nesting: layoutCallArgumentsMax,
4665
+ items: layoutCallArgumentsMax,
4666
+ keep: { type: "array", items: { type: "string" } }
4667
+ }
4066
4668
  }
4067
4669
  }
4068
4670
  }
4069
4671
  ],
4070
4672
  messages: {
4071
- multiline: "A call with {{min}}+ arguments puts each argument on its own line."
4673
+ multiline: "A call with {{min}}+ arguments puts each argument on its own line.",
4674
+ hug: "Put every argument on its own line. Only a last callback, object or array may hug the call, after simple arguments.",
4675
+ prefer: "Hug the last argument: it is a callback, object or array after simple arguments.",
4676
+ join: "This fits on one line, so keep it on one line."
4072
4677
  }
4073
4678
  },
4074
4679
  createOnce(context) {
4680
+ let options = {};
4075
4681
  let minArguments = 3;
4076
- let indent = 2;
4077
- const check = (node) => {
4682
+ let indentUnit = sourceIndentUnit();
4683
+ const split = (node) => {
4078
4684
  const args = node.arguments;
4079
4685
  const last = args.at(-1);
4080
4686
  const source = context.sourceCode;
@@ -4097,39 +4703,121 @@ var layoutCallArguments = defineRule32({
4097
4703
  const unsafe = source.getCommentsInside(node).length > 0 || text[open] !== "(" || text[close] !== ")" || !/^\s*$/.test(between[0]) || between.slice(1, -1).some((gap) => !/^\s*,\s*$/.test(gap)) || !/^\s*,?\s*$/.test(between.at(-1) ?? "");
4098
4704
  const baseIndent = sourceLineIndent(text, open);
4099
4705
  const newline = sourceNewline(text);
4100
- const argumentIndent = `${baseIndent}${" ".repeat(indent)}`;
4706
+ const argumentIndent = `${baseIndent}${indentUnit}`;
4707
+ context.report({
4708
+ node,
4709
+ messageId: "multiline",
4710
+ data: { min: String(minArguments) },
4711
+ fix: unsafe ? undefined : (fixer) => gaps.map((gap, index) => {
4712
+ if (index === 0) {
4713
+ return fixer.replaceTextRange(gap, `${newline}${argumentIndent}`);
4714
+ }
4715
+ const comma = between[index].includes(",") ? "," : "";
4716
+ return fixer.replaceTextRange(gap, `${comma}${newline}${index === gaps.length - 1 ? baseIndent : argumentIndent}`);
4717
+ })
4718
+ });
4719
+ };
4720
+ const hug = (node, max) => {
4721
+ const source = context.sourceCode;
4722
+ const text = source.text;
4723
+ const args = node.arguments;
4724
+ const first = args.at(0);
4725
+ const last = args.at(-1);
4726
+ const open = source.getTokenAfter(node.typeArguments ?? node.callee);
4727
+ const close = source.getLastToken(node);
4728
+ if (!first || !last || !open || !close || args.every(astIsOneLine) || source.getCommentsInside(node).length > 0) {
4729
+ return;
4730
+ }
4731
+ const leading = args.slice(0, -1);
4732
+ const canHug = !astIsPipe(node) && leading.every((arg) => astIsOneLine(arg) && astCallDepth(arg) <= 1) && layoutCallArgumentsHuggable(last);
4733
+ const gaps = sourceItemGaps(open.range[1], args, close.range[0]);
4734
+ if (first.loc.start.line > open.loc.end.line) {
4735
+ const head = [...leading.map((arg) => source.getText(arg)), text.slice(last.range[0], sourceLineEnd(text, last.range[0]))];
4736
+ const hugged = `${text.slice(sourceLineStart(text, open.range[0]), open.range[1])}${head.join(", ")}`;
4737
+ if (canHug && args.length <= 2 && hugged.length <= max) {
4738
+ context.report({
4739
+ node,
4740
+ messageId: "prefer",
4741
+ fix: (fixer) => sourceRespace(fixer, text, gaps, (index, end) => index === 0 || end ? "" : " ")
4742
+ });
4743
+ }
4744
+ return;
4745
+ }
4746
+ if (astIsOneLine(node) || canHug) {
4747
+ return;
4748
+ }
4749
+ const newline = sourceNewline(text);
4750
+ const base = sourceLineIndent(text, open.range[0]);
4751
+ const inner = `${base}${indentUnit}`;
4752
+ context.report({
4753
+ node,
4754
+ messageId: "hug",
4755
+ fix: (fixer) => sourceRespace(fixer, text, gaps, (_, end) => `${newline}${end ? base : inner}`)
4756
+ });
4757
+ };
4758
+ const join3 = (node, items, open, close) => {
4759
+ const source = context.sourceCode;
4760
+ const text = source.text;
4761
+ const first = items.at(0);
4762
+ if (!first || !open || !close || items.length > Math.min(options.join?.items ?? 2, options.split === false ? Number.POSITIVE_INFINITY : minArguments - 1) || first.loc.start.line === open.loc.end.line || !items.every(astIsOneLine) || source.commentsExistBetween(open, close)) {
4763
+ return;
4764
+ }
4765
+ const gaps = sourceItemGaps(open.range[1], items, close.range[0]);
4766
+ const comma = text.slice(...gaps.at(-1) ?? [0, 0]).includes(",") ? "," : "";
4767
+ if (sourceJoinedWidth(text, open.range[1], `${items.map((item) => source.getText(item)).join(", ")}${comma}`, close.range[0]) > (options.join?.max ?? 0)) {
4768
+ return;
4769
+ }
4101
4770
  context.report({
4102
4771
  node,
4103
- messageId: "multiline",
4104
- data: { min: String(minArguments) },
4105
- fix: unsafe ? undefined : (fixer) => gaps.map((gap, index) => {
4106
- if (index === 0) {
4107
- return fixer.replaceTextRange(gap, `${newline}${argumentIndent}`);
4108
- }
4109
- const comma = between[index].includes(",") ? "," : "";
4110
- return fixer.replaceTextRange(gap, `${comma}${newline}${index === gaps.length - 1 ? baseIndent : argumentIndent}`);
4111
- })
4772
+ messageId: "join",
4773
+ fix: (fixer) => sourceRespace(fixer, text, gaps, (index, end) => index === 0 || end ? "" : " ")
4112
4774
  });
4113
4775
  };
4776
+ const call = (node) => {
4777
+ const source = context.sourceCode;
4778
+ if (options.split !== false) {
4779
+ split(node);
4780
+ }
4781
+ if (options.hug) {
4782
+ hug(node, options.hug.max);
4783
+ }
4784
+ if (options.join && !(options.join.keep ?? []).includes(source.getText(node.callee)) && !(astIsPipe(node) && node.arguments.length > 1) && astCallDepth(node) <= (options.join.nesting ?? Number.POSITIVE_INFINITY)) {
4785
+ join3(node, node.arguments, source.getTokenAfter(node.typeArguments ?? node.callee), source.getLastToken(node));
4786
+ }
4787
+ };
4788
+ const parameters = (node) => {
4789
+ const source = context.sourceCode;
4790
+ const first = node.params.at(0);
4791
+ const last = node.params.at(-1);
4792
+ if (!options.join || !first || !last) {
4793
+ return;
4794
+ }
4795
+ join3(node, node.params, source.getTokenBefore(first, { filter: (token) => token.value === "(" }), source.getTokenAfter(last, { filter: (token) => token.value === ")" }));
4796
+ };
4114
4797
  return {
4115
4798
  before() {
4116
- const options = optionsFirst(context, {});
4799
+ options = optionsFirst(context, {});
4117
4800
  minArguments = options.min ?? 3;
4118
- indent = options.indent ?? 2;
4801
+ indentUnit = sourceIndentUnit(options.indent);
4119
4802
  },
4120
- CallExpression: check,
4121
- NewExpression: check
4803
+ CallExpression: call,
4804
+ NewExpression: call,
4805
+ FunctionExpression: parameters,
4806
+ FunctionDeclaration: parameters,
4807
+ ArrowFunctionExpression: parameters,
4808
+ TSFunctionType: parameters,
4809
+ TSMethodSignature: parameters
4122
4810
  };
4123
4811
  }
4124
4812
  });
4125
4813
 
4126
4814
  // src/rules/layout/layout-call-array.ts
4127
- import { defineRule as defineRule33 } from "@oxlint/plugins";
4815
+ import { defineRule as defineRule41 } from "@oxlint/plugins";
4128
4816
  var layoutCallArrayDefaultCallees = ["Promise.all", "Promise.allSettled"];
4129
4817
  function layoutCallArrayRoot(node) {
4130
4818
  return node.type === "MemberExpression" ? layoutCallArrayRoot(node.object) : node;
4131
4819
  }
4132
- var layoutCallArray = defineRule33({
4820
+ var layoutCallArray = defineRule41({
4133
4821
  meta: {
4134
4822
  type: "layout",
4135
4823
  fixable: "whitespace",
@@ -4244,9 +4932,56 @@ var layoutCallArray = defineRule33({
4244
4932
  }
4245
4933
  });
4246
4934
 
4935
+ // src/rules/layout/layout-call-depth.ts
4936
+ import { defineRule as defineRule42 } from "@oxlint/plugins";
4937
+ function layoutCallDepthOuter(node) {
4938
+ const parent = node.parent;
4939
+ if (!parent || parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression" || parent.type.endsWith("Statement")) {
4940
+ return;
4941
+ }
4942
+ return astIsCall(parent) && parent.arguments.some((argument) => argument === node) ? parent : layoutCallDepthOuter(parent);
4943
+ }
4944
+ var layoutCallDepth = defineRule42({
4945
+ meta: {
4946
+ type: "layout",
4947
+ schema: [
4948
+ {
4949
+ type: "object",
4950
+ additionalProperties: false,
4951
+ properties: { max: { type: "integer", minimum: 1, default: 2 } }
4952
+ }
4953
+ ],
4954
+ messages: {
4955
+ deep: "Calls nest {{depth}} deep on one line (max {{max}}). Break the outer call or name an inner value."
4956
+ }
4957
+ },
4958
+ createOnce(context) {
4959
+ let max = 2;
4960
+ const check = (node) => {
4961
+ const depth = astCallDepth(node);
4962
+ const outer = layoutCallDepthOuter(node);
4963
+ if (!astIsOneLine(node) || depth <= max || outer !== undefined && astIsOneLine(outer) && astCallDepth(outer) > max) {
4964
+ return;
4965
+ }
4966
+ context.report({
4967
+ node,
4968
+ messageId: "deep",
4969
+ data: { depth: String(depth), max: String(max) }
4970
+ });
4971
+ };
4972
+ return {
4973
+ before() {
4974
+ max = optionsFirst(context, {}).max ?? 2;
4975
+ },
4976
+ CallExpression: check,
4977
+ NewExpression: check
4978
+ };
4979
+ }
4980
+ });
4981
+
4247
4982
  // src/rules/layout/layout-chain.ts
4248
- import { defineRule as defineRule34 } from "@oxlint/plugins";
4249
- var layoutChain = defineRule34({
4983
+ import { defineRule as defineRule43 } from "@oxlint/plugins";
4984
+ var layoutChain = defineRule43({
4250
4985
  meta: {
4251
4986
  type: "layout",
4252
4987
  fixable: "whitespace",
@@ -4364,9 +5099,65 @@ var layoutChain = defineRule34({
4364
5099
  }
4365
5100
  });
4366
5101
 
5102
+ // src/rules/layout/layout-items.ts
5103
+ import { defineRule as defineRule44 } from "@oxlint/plugins";
5104
+ var layoutItems = defineRule44({
5105
+ meta: {
5106
+ type: "layout",
5107
+ fixable: "whitespace",
5108
+ schema: [
5109
+ {
5110
+ type: "object",
5111
+ additionalProperties: false,
5112
+ properties: { indent: { ...sourceIndentSchema, default: 2 } }
5113
+ }
5114
+ ],
5115
+ messages: {
5116
+ object: "A multi-line object puts each property on its own line.",
5117
+ array: "A multi-line array puts each element on its own line."
5118
+ }
5119
+ },
5120
+ createOnce(context) {
5121
+ let indentUnit = sourceIndentUnit();
5122
+ const check = (node) => {
5123
+ const source = context.sourceCode;
5124
+ const items = node.type === "ObjectExpression" ? node.properties : node.elements;
5125
+ const present = items.filter((item) => item !== null);
5126
+ const open = source.getFirstToken(node);
5127
+ const close = source.getLastToken(node);
5128
+ if (!open || !close || astIsOneLine(node) || present.length < 2) {
5129
+ return;
5130
+ }
5131
+ const packed = [open, ...present].some((item, index) => {
5132
+ const next = present.at(index);
5133
+ return next !== undefined && item.loc.end.line === next.loc.start.line;
5134
+ });
5135
+ if (!packed) {
5136
+ return;
5137
+ }
5138
+ const text = source.text;
5139
+ const newline = sourceNewline(text);
5140
+ const base = sourceLineIndent(text, node.range[0]);
5141
+ const gaps = sourceItemGaps(open.range[1], present, close.range[0]);
5142
+ context.report({
5143
+ node,
5144
+ messageId: node.type === "ObjectExpression" ? "object" : "array",
5145
+ fix: (fixer) => present.length === items.length ? sourceRespace(fixer, text, gaps, (_, end) => `${newline}${base}${end ? "" : indentUnit}`) : null
5146
+ });
5147
+ };
5148
+ return {
5149
+ before() {
5150
+ indentUnit = sourceIndentUnit(optionsFirst(context, {}).indent);
5151
+ },
5152
+ ObjectExpression: check,
5153
+ ArrayExpression: check
5154
+ };
5155
+ }
5156
+ });
5157
+
4367
5158
  // src/rules/layout/layout-jsx-attributes.ts
4368
- import { defineRule as defineRule35 } from "@oxlint/plugins";
4369
- var layoutJsxAttributes = defineRule35({
5159
+ import { defineRule as defineRule45 } from "@oxlint/plugins";
5160
+ var layoutJsxAttributes = defineRule45({
4370
5161
  meta: {
4371
5162
  type: "layout",
4372
5163
  fixable: "whitespace",
@@ -4376,12 +5167,19 @@ var layoutJsxAttributes = defineRule35({
4376
5167
  additionalProperties: false,
4377
5168
  properties: {
4378
5169
  min: { type: "integer", minimum: 1 },
4379
- indent: sourceIndentSchema
5170
+ indent: sourceIndentSchema,
5171
+ join: {
5172
+ type: "object",
5173
+ additionalProperties: false,
5174
+ required: ["max"],
5175
+ properties: { max: { type: "integer", minimum: 1 } }
5176
+ }
4380
5177
  }
4381
5178
  }
4382
5179
  ],
4383
5180
  messages: {
4384
- multiline: "Put every JSX attribute and the closing token on its own line."
5181
+ multiline: "Put every JSX attribute and the closing token on its own line.",
5182
+ join: "This tag fits on one line, so keep it on one line."
4385
5183
  }
4386
5184
  },
4387
5185
  createOnce(context) {
@@ -4389,35 +5187,52 @@ var layoutJsxAttributes = defineRule35({
4389
5187
  let indentUnit = sourceIndentUnit();
4390
5188
  let newline = `
4391
5189
  `;
5190
+ let joinMax;
4392
5191
  return {
4393
5192
  before() {
4394
5193
  const options = optionsFirst(context, {});
4395
5194
  minAttributes = options.min ?? 3;
4396
5195
  indentUnit = sourceIndentUnit(options.indent);
4397
5196
  newline = sourceNewline(context.sourceCode.text);
5197
+ joinMax = options.join?.max;
4398
5198
  },
4399
5199
  JSXOpeningElement(node) {
4400
- if (node.attributes.length < minAttributes || node.loc.start.line !== node.loc.end.line) {
4401
- return;
4402
- }
4403
5200
  const source = context.sourceCode;
4404
5201
  const text = source.text;
5202
+ const attributes = node.attributes;
5203
+ const split = attributes.length >= minAttributes;
5204
+ if (split ? !astIsOneLine(node) : joinMax === undefined || astIsOneLine(node)) {
5205
+ return;
5206
+ }
5207
+ if (!split) {
5208
+ const start = (node.typeArguments ?? node.name).range[1];
5209
+ if (attributes.length === 0 || !attributes.every(astIsOneLine) || text.slice(node.range[0], start).includes(`
5210
+ `) || source.getCommentsInside(node).length > 0) {
5211
+ return;
5212
+ }
5213
+ const end = node.range[1] - (node.selfClosing ? 2 : 1);
5214
+ const ranges = sourceItemGaps(start, attributes, end);
5215
+ const closing = node.selfClosing ? " " : "";
5216
+ if (sourceJoinedWidth(text, start, ` ${attributes.map((attribute) => source.getText(attribute)).join(" ")}${closing}`, end) > (joinMax ?? 0)) {
5217
+ return;
5218
+ }
5219
+ context.report({
5220
+ node,
5221
+ messageId: "join",
5222
+ fix: (fixer) => ranges.some((range) => !/^\s*$/.test(text.slice(...range))) ? null : ranges.map((range, index) => fixer.replaceTextRange(range, index === ranges.length - 1 ? closing : " "))
5223
+ });
5224
+ return;
5225
+ }
4405
5226
  const baseIndent = sourceLineIndent(text, node.range[0]);
4406
5227
  const attributeIndent = baseIndent + indentUnit;
4407
- const attributes = node.attributes;
4408
5228
  context.report({
4409
5229
  node,
4410
5230
  messageId: "multiline",
4411
5231
  fix: (fixer) => {
4412
- const last = attributes.at(-1);
4413
- if (source.getCommentsInside(node).length > 0 || !last) {
5232
+ if (source.getCommentsInside(node).length > 0) {
4414
5233
  return null;
4415
5234
  }
4416
- const ranges = [
4417
- [(node.typeArguments ?? node.name).range[1], attributes[0].range[0]],
4418
- ...attributes.slice(1).map((attribute, index) => [attributes[index].range[1], attribute.range[0]]),
4419
- [last.range[1], node.range[1] - (node.selfClosing ? 2 : 1)]
4420
- ];
5235
+ const ranges = sourceItemGaps((node.typeArguments ?? node.name).range[1], attributes, node.range[1] - (node.selfClosing ? 2 : 1));
4421
5236
  if (ranges.some((range) => !/^\s*$/.test(text.slice(...range)))) {
4422
5237
  return null;
4423
5238
  }
@@ -4430,8 +5245,8 @@ var layoutJsxAttributes = defineRule35({
4430
5245
  });
4431
5246
 
4432
5247
  // src/rules/layout/layout-key-value.ts
4433
- import { defineRule as defineRule36 } from "@oxlint/plugins";
4434
- var layoutKeyValue = defineRule36({
5248
+ import { defineRule as defineRule46 } from "@oxlint/plugins";
5249
+ var layoutKeyValue = defineRule46({
4435
5250
  meta: {
4436
5251
  type: "layout",
4437
5252
  fixable: "whitespace",
@@ -4467,11 +5282,15 @@ var layoutKeyValue = defineRule36({
4467
5282
  });
4468
5283
 
4469
5284
  // src/rules/layout/layout-object.ts
4470
- import { defineRule as defineRule37 } from "@oxlint/plugins";
5285
+ import { defineRule as defineRule47 } from "@oxlint/plugins";
4471
5286
  function layoutObjectIsWhitespace(text) {
4472
5287
  return /^[\t\n\r\f ]*$/.test(text);
4473
5288
  }
4474
- var layoutObject = defineRule37({
5289
+ function layoutObjectIsFlat(property) {
5290
+ const value = property.type === "SpreadElement" ? property.argument : property.value;
5291
+ return astIsOneLine(property) && (property.type === "SpreadElement" || !property.method && property.kind === "init") && value.type !== "ObjectExpression" && value.type !== "ArrayExpression" && !astIsInlineFunction(value);
5292
+ }
5293
+ var layoutObject = defineRule47({
4475
5294
  meta: {
4476
5295
  type: "layout",
4477
5296
  fixable: "whitespace",
@@ -4482,13 +5301,23 @@ var layoutObject = defineRule37({
4482
5301
  properties: {
4483
5302
  min: { type: "integer", minimum: 1, default: 3 },
4484
5303
  scope: { type: "string", enum: ["call-args", "all"], default: "call-args" },
4485
- indent: { ...sourceIndentSchema, default: 2 }
5304
+ indent: { ...sourceIndentSchema, default: 2 },
5305
+ join: {
5306
+ type: "object",
5307
+ additionalProperties: false,
5308
+ required: ["max"],
5309
+ properties: {
5310
+ max: { type: "integer", minimum: 1 },
5311
+ keep: { type: "array", items: { type: "string" } }
5312
+ }
5313
+ }
4486
5314
  }
4487
5315
  }
4488
5316
  ],
4489
5317
  messages: {
4490
5318
  callArgs: "Object with {{min}}+ properties in a call should have each property on its own line.",
4491
- always: "Object with {{min}}+ properties should have each property on its own line."
5319
+ always: "Object with {{min}}+ properties should have each property on its own line.",
5320
+ join: "This object fits on one line, so keep it on one line."
4492
5321
  }
4493
5322
  },
4494
5323
  createOnce(context) {
@@ -4497,6 +5326,7 @@ var layoutObject = defineRule37({
4497
5326
  let indentUnit = sourceIndentUnit();
4498
5327
  let newline = `
4499
5328
  `;
5329
+ let join3;
4500
5330
  return {
4501
5331
  before() {
4502
5332
  const options = optionsFirst(context, {});
@@ -4504,21 +5334,43 @@ var layoutObject = defineRule37({
4504
5334
  scope = options.scope ?? "call-args";
4505
5335
  indentUnit = sourceIndentUnit(options.indent);
4506
5336
  newline = sourceNewline(context.sourceCode.text);
5337
+ join3 = options.join;
4507
5338
  },
4508
5339
  ObjectExpression(node) {
4509
- if (node.properties.length < minProperties || node.loc.start.line !== node.loc.end.line) {
5340
+ const source = context.sourceCode;
5341
+ const text = source.text;
5342
+ const properties = node.properties;
5343
+ const split = properties.length >= minProperties;
5344
+ if (split ? !astIsOneLine(node) : !join3 || astIsOneLine(node)) {
4510
5345
  return;
4511
5346
  }
4512
5347
  const parent = node.parent;
4513
- if (scope === "call-args" && !((parent.type === "CallExpression" || parent.type === "NewExpression") && parent.arguments.includes(node))) {
5348
+ const call = (parent.type === "CallExpression" || parent.type === "NewExpression") && parent.arguments.includes(node) ? parent : undefined;
5349
+ if (scope === "call-args" && !call) {
5350
+ return;
5351
+ }
5352
+ if (!split) {
5353
+ const callee = call ? astDottedName(call.callee) : undefined;
5354
+ const kept = callee !== undefined && (join3?.keep ?? []).some((name) => callee === name || callee.endsWith(`.${name}`));
5355
+ if (kept || properties.length === 0 || !properties.every(layoutObjectIsFlat) || source.getCommentsInside(node).length > 0) {
5356
+ return;
5357
+ }
5358
+ const gaps = sourceItemGaps(node.range[0] + 1, properties, node.range[1] - 1);
5359
+ const comma = text.slice(...gaps.at(-1) ?? [0, 0]).includes(",") ? "," : "";
5360
+ const joined = ` ${properties.map((property) => source.getText(property)).join(", ")}${comma} `;
5361
+ if (sourceJoinedWidth(text, node.range[0] + 1, joined, node.range[1] - 1) > (join3?.max ?? 0)) {
5362
+ return;
5363
+ }
5364
+ context.report({
5365
+ node,
5366
+ messageId: "join",
5367
+ fix: (fixer) => sourceRespace(fixer, text, gaps, () => " ")
5368
+ });
4514
5369
  return;
4515
5370
  }
4516
- const source = context.sourceCode;
4517
- const text = source.text;
4518
5371
  const baseIndent = sourceLineIndent(text, node.range[0]);
4519
5372
  const propertyIndent = `${baseIndent}${indentUnit}`;
4520
5373
  const close = node.range[1] - 1;
4521
- const properties = node.properties;
4522
5374
  context.report({
4523
5375
  node,
4524
5376
  messageId: scope === "all" ? "always" : "callArgs",
@@ -4555,7 +5407,7 @@ var layoutObject = defineRule37({
4555
5407
  });
4556
5408
 
4557
5409
  // src/rules/layout/layout-padding.ts
4558
- import { defineRule as defineRule38 } from "@oxlint/plugins";
5410
+ import { defineRule as defineRule48 } from "@oxlint/plugins";
4559
5411
  var layoutPaddingControl = new Set([
4560
5412
  "IfStatement",
4561
5413
  "ForStatement",
@@ -4584,7 +5436,7 @@ function layoutPaddingKind(node) {
4584
5436
  function layoutPaddingIsMultilineVariable(node, kind) {
4585
5437
  return kind === "variable" && node.loc.start.line !== node.loc.end.line;
4586
5438
  }
4587
- var layoutPadding = defineRule38({
5439
+ var layoutPadding = defineRule48({
4588
5440
  meta: {
4589
5441
  type: "layout",
4590
5442
  fixable: "whitespace",
@@ -4672,13 +5524,48 @@ var layoutPadding = defineRule38({
4672
5524
  }
4673
5525
  });
4674
5526
 
5527
+ // src/rules/layout/layout-pipe.ts
5528
+ import { defineRule as defineRule49 } from "@oxlint/plugins";
5529
+ var layoutPipe = defineRule49({
5530
+ meta: {
5531
+ type: "layout",
5532
+ messages: {
5533
+ after: "No ).pipe( after a call that spans lines. Name the value and pipe the name, or pass the call to the step data-first.",
5534
+ step: "A pipe step may span lines only through a callback, object or array. Name this call first, then pass the name."
5535
+ }
5536
+ },
5537
+ createOnce(context) {
5538
+ return {
5539
+ CallExpression(node) {
5540
+ const callee = node.callee;
5541
+ if (callee.type === "MemberExpression" && astMemberName(callee) === "pipe" && astIsCall(callee.object) && !astIsOneLine(callee.object) && callee.property.loc.start.line === callee.object.loc.end.line) {
5542
+ context.report({ node: callee.property, messageId: "after" });
5543
+ }
5544
+ if (!astIsPipe(node)) {
5545
+ return;
5546
+ }
5547
+ for (const step of node.arguments) {
5548
+ for (const argument of astIsCall(step) ? step.arguments : []) {
5549
+ if (astIsCall(argument) && !astIsOneLine(argument)) {
5550
+ context.report({ node: argument, messageId: "step" });
5551
+ }
5552
+ }
5553
+ }
5554
+ }
5555
+ };
5556
+ }
5557
+ });
5558
+
4675
5559
  // src/rules/layout/layout-ternary.ts
4676
- import { defineRule as defineRule39 } from "@oxlint/plugins";
5560
+ import { defineRule as defineRule50 } from "@oxlint/plugins";
5561
+ function layoutTernaryLines(node) {
5562
+ return node.loc.end.line - node.loc.start.line + 1;
5563
+ }
4677
5564
  function layoutTernaryBreak(between, operator, newline) {
4678
5565
  const at = between.indexOf(operator);
4679
5566
  return `${between.slice(0, at).trimEnd()}${newline}${operator} ${between.slice(at + 1).trimStart()}`;
4680
5567
  }
4681
- var layoutTernary = defineRule39({
5568
+ var layoutTernary = defineRule50({
4682
5569
  meta: {
4683
5570
  type: "layout",
4684
5571
  fixable: "whitespace",
@@ -4692,30 +5579,62 @@ var layoutTernary = defineRule39({
4692
5579
  minimum: 1,
4693
5580
  default: 160
4694
5581
  },
4695
- indent: {
4696
- type: "integer",
4697
- minimum: 1,
4698
- default: 2
5582
+ indent: { ...sourceIndentSchema, default: 2 },
5583
+ fit: {
5584
+ type: "boolean",
5585
+ default: true
5586
+ },
5587
+ branch: {
5588
+ type: "object",
5589
+ additionalProperties: false,
5590
+ required: ["max"],
5591
+ properties: { max: { type: "integer", minimum: 1 } }
4699
5592
  }
4700
5593
  }
4701
5594
  }
4702
5595
  ],
4703
5596
  messages: {
5597
+ long: "This ternary branch spans {{lines}} lines (max {{max}}). Name it.",
5598
+ yield: "A yield* ternary takes one-line operands. Name the multi-line branch.",
4704
5599
  join: "Keep this conditional on one line ({{length}} columns; limit {{max}}, except in template literals).",
4705
5600
  split: "This conditional does not fit on one line ({{length}} of {{max}} columns); break before `?` and `:`."
4706
5601
  }
4707
5602
  },
4708
5603
  createOnce(context) {
4709
5604
  let maxLength = 160;
4710
- let indent = 2;
5605
+ let indentUnit = sourceIndentUnit();
5606
+ let fit = true;
5607
+ let branchMax;
5608
+ const branch = (node, max) => {
5609
+ const long = [node.consequent, node.alternate].filter((operand) => layoutTernaryLines(operand) > max);
5610
+ for (const operand of long) {
5611
+ context.report({
5612
+ node: operand,
5613
+ messageId: "long",
5614
+ data: { lines: String(layoutTernaryLines(operand)), max: String(max) }
5615
+ });
5616
+ }
5617
+ const yielded = node.parent.type === "YieldExpression" && node.parent.delegate;
5618
+ if (yielded && long.length === 0 && [node.test, node.consequent, node.alternate].some((operand) => layoutTernaryLines(operand) > 1)) {
5619
+ context.report({ node, messageId: "yield" });
5620
+ }
5621
+ };
4711
5622
  return {
4712
5623
  before() {
4713
5624
  const options = optionsFirst(context, {});
4714
5625
  maxLength = options.max ?? 160;
4715
- indent = options.indent ?? 2;
5626
+ indentUnit = sourceIndentUnit(options.indent);
5627
+ fit = options.fit ?? true;
5628
+ branchMax = options.branch?.max;
4716
5629
  },
4717
5630
  ConditionalExpression(node) {
4718
5631
  const source = context.sourceCode;
5632
+ if (branchMax !== undefined) {
5633
+ branch(node, branchMax);
5634
+ }
5635
+ if (!fit) {
5636
+ return;
5637
+ }
4719
5638
  const operands = [node.test, node.consequent, node.alternate];
4720
5639
  if (node.parent.type === "ConditionalExpression" || operands.some((operand) => operand.type === "ConditionalExpression" || operand.loc.start.line !== operand.loc.end.line) || source.getCommentsInside(node).length > 0) {
4721
5640
  return;
@@ -4744,7 +5663,7 @@ var layoutTernary = defineRule39({
4744
5663
  if (!onOneLine || interpolated || length <= maxLength) {
4745
5664
  return;
4746
5665
  }
4747
- const newline = `${sourceNewline(text)}${sourceLineIndent(text, node.range[0])}${" ".repeat(indent)}`;
5666
+ const newline = `${sourceNewline(text)}${sourceLineIndent(text, node.range[0])}${indentUnit}`;
4748
5667
  const question = [node.test.range[1], node.consequent.range[0]];
4749
5668
  const colon = [node.consequent.range[1], node.alternate.range[0]];
4750
5669
  context.report({
@@ -4762,9 +5681,9 @@ var layoutTernary = defineRule39({
4762
5681
  });
4763
5682
 
4764
5683
  // src/rules/naming/naming-declarations.ts
4765
- import { defineRule as defineRule40 } from "@oxlint/plugins";
5684
+ import { defineRule as defineRule51 } from "@oxlint/plugins";
4766
5685
  var namingDeclarationsKinds = ["function", "const", "let", "var", "class", "type", "interface", "enum"];
4767
- var namingDeclarations = defineRule40({
5686
+ var namingDeclarations = defineRule51({
4768
5687
  meta: {
4769
5688
  type: "problem",
4770
5689
  schema: [
@@ -4796,11 +5715,9 @@ var namingDeclarations = defineRule40({
4796
5715
  const declarator = node.type === "VariableDeclarator";
4797
5716
  const kind = declarationsKindOf(node, declarator ? [node.parent] : []);
4798
5717
  const name = declarationsIdName(node);
4799
- let insideLoop = false;
4800
- for (let current = declarator ? node.parent : null;current && !insideLoop; current = current.parent) {
4801
- insideLoop = current.type === "ForStatement" || current.type === "ForInStatement" || current.type === "ForOfStatement";
4802
- }
4803
- if (!kind || !name || insideLoop || kinds && !kinds.has(kind) || optionsPatternsTest(allowed, name)) {
5718
+ const loop = declarator ? node.parent.parent : null;
5719
+ const loopHeader = loop?.type === "ForStatement" ? loop.init === node.parent : (loop?.type === "ForInStatement" || loop?.type === "ForOfStatement") && loop.left === node.parent;
5720
+ if (!kind || !name || loopHeader || kinds && !kinds.has(kind) || optionsPatternsTest(allowed, name)) {
4804
5721
  return;
4805
5722
  }
4806
5723
  if (expected.length > 0) {
@@ -4843,7 +5760,7 @@ var namingDeclarations = defineRule40({
4843
5760
  });
4844
5761
 
4845
5762
  // src/rules/naming/naming-exports.ts
4846
- import { defineRule as defineRule41 } from "@oxlint/plugins";
5763
+ import { defineRule as defineRule52 } from "@oxlint/plugins";
4847
5764
  var namingExportsScopeProperties = {
4848
5765
  scope: { type: "string", enum: ["exports", "functions", "declarations"] },
4849
5766
  types: { type: "boolean" },
@@ -4869,7 +5786,7 @@ function matchTemplate(basename3, template, placeholder = "[a-z0-9-]+") {
4869
5786
  }
4870
5787
  return new RegExp(`^${pattern}$`).exec(basename3)?.groups ?? null;
4871
5788
  }
4872
- var namingExports = defineRule41({
5789
+ var namingExports = defineRule52({
4873
5790
  meta: {
4874
5791
  type: "problem",
4875
5792
  schema: {
@@ -4964,9 +5881,12 @@ var namingExports = defineRule41({
4964
5881
  }
4965
5882
  continue;
4966
5883
  }
4967
- const names = scope === "declarations" ? [...declarations ??= declarationsNamed(program)] : (bindings ??= exportsCollect(program)).flatMap((binding) => options.types && binding.typeOnly ? [] : [{ name: binding.localName ?? binding.exportedName, node: binding.node }]);
5884
+ const names = scope === "declarations" ? [...declarations ??= declarationsNamed(program)] : (bindings ??= exportsCollect(program)).flatMap((binding) => {
5885
+ const name = binding.default ? binding.localName ?? binding.exportedName : binding.exportedName;
5886
+ return options.types && binding.typeOnly ? [] : [{ name, node: binding.node }];
5887
+ });
4968
5888
  if (scope === "functions") {
4969
- const exported = new Set(names.map((item) => item.name));
5889
+ const exported = new Set([...names.map((item) => item.name), ...(bindings ?? []).flatMap((binding) => binding.localName ?? [])]);
4970
5890
  names.push(...(declarations ??= declarationsNamed(program)).filter(({ name, node }) => !exported.has(name) && (node.type === "FunctionDeclaration" || node.type === "VariableDeclarator" && declarationsIsFunctionValue(node.init))));
4971
5891
  }
4972
5892
  if (options.services) {
@@ -5003,10 +5923,10 @@ var namingExports = defineRule41({
5003
5923
  });
5004
5924
 
5005
5925
  // src/rules/naming/naming-file.ts
5006
- import { defineRule as defineRule42 } from "@oxlint/plugins";
5926
+ import { defineRule as defineRule53 } from "@oxlint/plugins";
5007
5927
  import { readdirSync as readdirSync2 } from "node:fs";
5008
5928
  import { dirname as dirname4 } from "node:path";
5009
- var namingFile = defineRule42({
5929
+ var namingFile = defineRule53({
5010
5930
  meta: {
5011
5931
  type: "problem",
5012
5932
  schema: {
@@ -5076,9 +5996,9 @@ var namingFile = defineRule42({
5076
5996
  });
5077
5997
 
5078
5998
  // src/rules/naming/naming-folder.ts
5079
- import { defineRule as defineRule43 } from "@oxlint/plugins";
5999
+ import { defineRule as defineRule54 } from "@oxlint/plugins";
5080
6000
  import { posix as posix2 } from "node:path";
5081
- var namingFolder = defineRule43({
6001
+ var namingFolder = defineRule54({
5082
6002
  meta: {
5083
6003
  type: "problem",
5084
6004
  schema: [
@@ -5152,9 +6072,9 @@ var reactDynamicClasses = astSelectorRule(':matches(JSXAttribute[name.name="clas
5152
6072
  var reactEmptyEffect = astSelectorRule(':matches(CallExpression[callee.name=/^use(Layout)?Effect$/], CallExpression[callee.object.name="React"][callee.property.name=/^use(Layout)?Effect$/]) > :matches(ArrowFunctionExpression, FunctionExpression) > BlockStatement[body.length=0]', "Remove the empty effect.");
5153
6073
 
5154
6074
  // src/rules/react/react-memo.ts
5155
- import { defineRule as defineRule44 } from "@oxlint/plugins";
6075
+ import { defineRule as defineRule55 } from "@oxlint/plugins";
5156
6076
  var reactMemoNames = ["memo", "useMemo", "useCallback"];
5157
- var reactMemo = defineRule44({
6077
+ var reactMemo = defineRule55({
5158
6078
  meta: {
5159
6079
  type: "problem",
5160
6080
  schema: [],
@@ -5189,7 +6109,7 @@ var reactMemo = defineRule44({
5189
6109
  });
5190
6110
 
5191
6111
  // src/rules/react/react-props.ts
5192
- import { defineRule as defineRule45 } from "@oxlint/plugins";
6112
+ import { defineRule as defineRule56 } from "@oxlint/plugins";
5193
6113
  function reactPropsTarget(node) {
5194
6114
  if (node?.type === "FunctionDeclaration" || node?.type === "FunctionExpression" || node?.type === "ArrowFunctionExpression") {
5195
6115
  return { fn: node };
@@ -5216,7 +6136,7 @@ function reactIsPassThrough(type) {
5216
6136
  const argument = type.typeArguments?.params.length === 1 ? type.typeArguments.params[0] : undefined;
5217
6137
  return name.type === "Identifier" && name.name === "ComponentProps" && (argument?.type === "TSTypeQuery" || argument?.type === "TSLiteralType" && argument.literal.type === "Literal" && typeof argument.literal.value === "string");
5218
6138
  }
5219
- var reactProps = defineRule45({
6139
+ var reactProps = defineRule56({
5220
6140
  meta: {
5221
6141
  type: "problem",
5222
6142
  schema: [
@@ -5224,7 +6144,8 @@ var reactProps = defineRule45({
5224
6144
  type: "object",
5225
6145
  additionalProperties: false,
5226
6146
  properties: {
5227
- prefix: { type: "boolean" }
6147
+ prefix: { type: "boolean" },
6148
+ suffix: { type: "string", minLength: 1, default: "Props" }
5228
6149
  }
5229
6150
  }
5230
6151
  ],
@@ -5238,7 +6159,7 @@ var reactProps = defineRule45({
5238
6159
  createOnce(context) {
5239
6160
  return {
5240
6161
  Program(program) {
5241
- const { prefix: filePrefix = false } = optionsFirst(context, {});
6162
+ const { prefix: filePrefix = false, suffix = "Props" } = optionsFirst(context, {});
5242
6163
  const stem = namingFileStem(namingBasename(context.filename).replace(/\.(tsx?|jsx?)$/, ""));
5243
6164
  const prefix = namingNormalized(stem, "remove-separators");
5244
6165
  const exported = new Set(exportsCollect(program).flatMap((binding) => binding.typeOnly ? [] : [binding.localName]));
@@ -5263,7 +6184,7 @@ var reactProps = defineRule45({
5263
6184
  continue;
5264
6185
  }
5265
6186
  const type = target.typeArgument ?? reactParameterType(parameter);
5266
- const expected = `${candidate.name}Props`;
6187
+ const expected = `${candidate.name}${suffix}`;
5267
6188
  const data = { name: candidate.name, expected };
5268
6189
  if (filePrefix && !exported.has(candidate.name) && !candidate.name.toLowerCase().startsWith(prefix)) {
5269
6190
  context.report({
@@ -5314,7 +6235,7 @@ var reactProps = defineRule45({
5314
6235
  });
5315
6236
 
5316
6237
  // src/rules/schemas/schemas-inline.ts
5317
- import { defineRule as defineRule46 } from "@oxlint/plugins";
6238
+ import { defineRule as defineRule57 } from "@oxlint/plugins";
5318
6239
 
5319
6240
  // src/utils/schemas.utils.ts
5320
6241
  var schemasElementOptionsSchema = {
@@ -5350,7 +6271,7 @@ function schemasIsAllowedElement(node, allowScalars, namespaces, structuralMetho
5350
6271
  }
5351
6272
 
5352
6273
  // src/rules/schemas/schemas-inline.ts
5353
- var schemasInline = defineRule46({
6274
+ var schemasInline = defineRule57({
5354
6275
  meta: {
5355
6276
  type: "problem",
5356
6277
  schema: [schemasElementOptionsSchema],
@@ -5399,11 +6320,11 @@ var schemasInline = defineRule46({
5399
6320
  });
5400
6321
 
5401
6322
  // src/rules/schemas/schemas-local.ts
5402
- import { defineRule as defineRule47 } from "@oxlint/plugins";
6323
+ import { defineRule as defineRule58 } from "@oxlint/plugins";
5403
6324
  var schemasLocalImportMessage = "Do not import runtime schema constructors from {{source}}; import a named schema instead.";
5404
6325
  var schemasLocalConstructionMessage = "Do not construct schemas locally; import a named schema instead.";
5405
6326
  var schemasLocalCalls = ["config", "prettifyError", "treeifyError", "flattenError", "formatError"];
5406
- var schemasLocal = defineRule47({
6327
+ var schemasLocal = defineRule58({
5407
6328
  meta: {
5408
6329
  type: "problem",
5409
6330
  schema: [
@@ -5433,8 +6354,6 @@ var schemasLocal = defineRule47({
5433
6354
  let packages = new Set;
5434
6355
  let calls = new Set;
5435
6356
  let pending = [];
5436
- let schemaUses = new Set;
5437
- let callUses = new Set;
5438
6357
  let importMessage = schemasLocalImportMessage;
5439
6358
  let constructionMessage = schemasLocalConstructionMessage;
5440
6359
  function isAllowedCall(node, member) {
@@ -5466,8 +6385,6 @@ var schemasLocal = defineRule47({
5466
6385
  packages = new Set(options.packages);
5467
6386
  calls = new Set(options.calls ?? schemasLocalCalls);
5468
6387
  pending = [];
5469
- schemaUses = new Set;
5470
- callUses = new Set;
5471
6388
  importMessage = options.importMessage ?? options.message ?? schemasLocalImportMessage;
5472
6389
  constructionMessage = options.constructionMessage ?? options.message ?? schemasLocalConstructionMessage;
5473
6390
  if ((options.allow ?? []).some((suffix) => filename.endsWith(suffix)) || (options.patterns ?? []).some((pattern) => new RegExp(pattern).test(filename))) {
@@ -5491,18 +6408,8 @@ var schemasLocal = defineRule47({
5491
6408
  }
5492
6409
  pending.push({ node, locals });
5493
6410
  },
5494
- Identifier(node) {
5495
- if (!constructors.has(node.name) || node.parent.type.startsWith("Import")) {
5496
- return;
5497
- }
5498
- if (isAllowedCall(node, node.parent)) {
5499
- callUses.add(node.name);
5500
- return;
5501
- }
5502
- schemaUses.add(node.name);
5503
- },
5504
6411
  "Program:exit"() {
5505
- const runtime = pending.filter(({ locals }) => locals.some((name) => schemaUses.has(name) || !callUses.has(name)));
6412
+ const runtime = pending.filter(({ node, locals }) => context.sourceCode.getDeclaredVariables(node).filter((variable) => locals.includes(variable.name)).some((variable) => variable.references.length === 0 || variable.references.some((reference) => !isAllowedCall(reference.identifier, reference.identifier.parent))));
5506
6413
  for (const { node } of runtime) {
5507
6414
  reportRuntimeImport(node, node.source.value);
5508
6415
  }
@@ -5528,8 +6435,8 @@ var schemasLocal = defineRule47({
5528
6435
  });
5529
6436
 
5530
6437
  // src/rules/schemas/schemas-rederive.ts
5531
- import { defineRule as defineRule48 } from "@oxlint/plugins";
5532
- var schemasRederive = defineRule48({
6438
+ import { defineRule as defineRule59 } from "@oxlint/plugins";
6439
+ var schemasRederive = defineRule59({
5533
6440
  meta: {
5534
6441
  type: "problem",
5535
6442
  schema: [
@@ -5575,7 +6482,7 @@ var schemasRederive = defineRule48({
5575
6482
  });
5576
6483
 
5577
6484
  // src/rules/schemas/schemas-single-use.ts
5578
- import { defineRule as defineRule49 } from "@oxlint/plugins";
6485
+ import { defineRule as defineRule60 } from "@oxlint/plugins";
5579
6486
  function isScalarSchema(node, namespaces, structuralMethods) {
5580
6487
  if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") {
5581
6488
  return false;
@@ -5589,7 +6496,7 @@ function isScalarSchema(node, namespaces, structuralMethods) {
5589
6496
  }
5590
6497
  return node.callee.object.type === "CallExpression" && isScalarSchema(node.callee.object, namespaces, structuralMethods);
5591
6498
  }
5592
- var schemasSingleUse = defineRule49({
6499
+ var schemasSingleUse = defineRule60({
5593
6500
  meta: {
5594
6501
  type: "suggestion",
5595
6502
  schema: [schemasElementOptionsSchema],
@@ -5665,7 +6572,7 @@ var schemasSingleUse = defineRule49({
5665
6572
  });
5666
6573
 
5667
6574
  // src/rules/tests/tests-assertions.ts
5668
- import { defineRule as defineRule50 } from "@oxlint/plugins";
6575
+ import { defineRule as defineRule61 } from "@oxlint/plugins";
5669
6576
  var testsAssertionsModifiers = new Set(["for", "each", "concurrent", "sequential", "only", "skip"]);
5670
6577
  var testsAssertionsFunctions = [
5671
6578
  "expect",
@@ -5855,7 +6762,7 @@ function isAssertedTest(registration, sourceCode, state) {
5855
6762
  }
5856
6763
  return rows.length > 0 && rows.every((row) => hasAssertion(callback, row.bindings, new Set, state));
5857
6764
  }
5858
- var testsAssertions = defineRule50({
6765
+ var testsAssertions = defineRule61({
5859
6766
  meta: {
5860
6767
  type: "problem",
5861
6768
  schema: [
@@ -5981,17 +6888,36 @@ var testsAssertions = defineRule50({
5981
6888
  });
5982
6889
 
5983
6890
  // src/rules/tests/tests-concurrent-db.ts
5984
- import { defineRule as defineRule51 } from "@oxlint/plugins";
5985
- var testsConcurrentDb = defineRule51({
6891
+ import { defineRule as defineRule62 } from "@oxlint/plugins";
6892
+ var testsConcurrentDb = defineRule62({
5986
6893
  meta: {
5987
6894
  type: "problem",
5988
- schema: [{ type: "object", properties: { message: { type: "string" } }, additionalProperties: false }],
6895
+ schema: [
6896
+ {
6897
+ type: "object",
6898
+ additionalProperties: false,
6899
+ properties: {
6900
+ message: { type: "string" },
6901
+ fixtures: { type: "array", items: { type: "string", minLength: 1 }, default: ["db"] },
6902
+ callees: { type: "array", items: { type: "string", minLength: 1 }, default: ["test.concurrent"] }
6903
+ }
6904
+ }
6905
+ ],
5989
6906
  messages: { forbidden: "{{message}}" }
5990
6907
  },
5991
6908
  createOnce(context) {
6909
+ let message = "";
6910
+ let fixtures = new Set;
6911
+ let callees = new Set;
5992
6912
  return {
6913
+ before() {
6914
+ const options = optionsFirst(context, {});
6915
+ message = options.message ?? "Obtain an isolated database fixture for concurrent tests.";
6916
+ fixtures = new Set(options.fixtures ?? ["db"]);
6917
+ callees = new Set(options.callees ?? ["test.concurrent"]);
6918
+ },
5993
6919
  CallExpression(node) {
5994
- const callback = astDottedName(node.callee) === "test.concurrent" ? node.arguments.findLast(declarationsIsFunctionValue) : undefined;
6920
+ const callback = callees.has(astDottedName(node.callee) ?? "") ? node.arguments.findLast(declarationsIsFunctionValue) : undefined;
5995
6921
  const parameter = callback?.params.at(0);
5996
6922
  const patterns = parameter?.type === "ObjectPattern" ? [parameter] : [];
5997
6923
  if (callback && parameter?.type === "Identifier") {
@@ -6004,8 +6930,8 @@ var testsConcurrentDb = defineRule51({
6004
6930
  }
6005
6931
  }
6006
6932
  for (const property of patterns.flatMap((pattern) => pattern.properties)) {
6007
- if (property.type === "Property" && property.key.type === "Identifier" && property.key.name === "db") {
6008
- const { message = "Obtain an isolated database fixture for concurrent tests." } = optionsFirst(context, {});
6933
+ const key = property.type === "Property" && !property.computed ? property.key : undefined;
6934
+ if (fixtures.has((key?.type === "Identifier" ? key.name : key && astStaticString(key)) ?? "")) {
6009
6935
  context.report({
6010
6936
  node: property,
6011
6937
  messageId: "forbidden",
@@ -6019,10 +6945,10 @@ var testsConcurrentDb = defineRule51({
6019
6945
  });
6020
6946
 
6021
6947
  // src/rules/tests/tests-modifiers.ts
6022
- import { defineRule as defineRule52 } from "@oxlint/plugins";
6948
+ import { defineRule as defineRule63 } from "@oxlint/plugins";
6023
6949
  var testsModifiersNames = ["test", "it", "describe", "suite"];
6024
6950
  var testsModifiersPackages = new Set(["vitest", "bun:test", "@playwright/test", "@jest/globals"]);
6025
- var testsModifiersBanned = new Set(["only", "skip", "todo"]);
6951
+ var testsModifiersBanned = ["only", "skip", "todo"];
6026
6952
  var testsModifiersBuilders = new Set(["if", "runIf", "skipIf"]);
6027
6953
  function modifierName(node) {
6028
6954
  if (node.type === "Identifier") {
@@ -6038,14 +6964,17 @@ function modifierName(node) {
6038
6964
  const property = astMemberName(node);
6039
6965
  return object && property ? `${object}.${property}` : undefined;
6040
6966
  }
6041
- var testsModifiers = defineRule52({
6967
+ var testsModifiers = defineRule63({
6042
6968
  meta: {
6043
6969
  type: "problem",
6044
6970
  schema: [
6045
6971
  {
6046
6972
  type: "object",
6047
6973
  additionalProperties: false,
6048
- properties: { functions: { type: "array", items: { type: "string" } } }
6974
+ properties: {
6975
+ functions: { type: "array", items: { type: "string" } },
6976
+ modifiers: { type: "array", items: { type: "string", minLength: 1 }, default: testsModifiersBanned }
6977
+ }
6049
6978
  }
6050
6979
  ],
6051
6980
  messages: {
@@ -6054,12 +6983,12 @@ var testsModifiers = defineRule52({
6054
6983
  },
6055
6984
  createOnce(context) {
6056
6985
  let names = new Set;
6986
+ let banned = new Set;
6057
6987
  return {
6058
6988
  before() {
6059
- names = new Set([
6060
- ...testsModifiersNames,
6061
- ...optionsFirst(context, {}).functions ?? []
6062
- ]);
6989
+ const options = optionsFirst(context, {});
6990
+ names = new Set([...testsModifiersNames, ...options.functions ?? []]);
6991
+ banned = new Set(options.modifiers ?? testsModifiersBanned);
6063
6992
  },
6064
6993
  ImportDeclaration(node) {
6065
6994
  if (!testsModifiersPackages.has(node.source.value)) {
@@ -6077,7 +7006,7 @@ var testsModifiers = defineRule52({
6077
7006
  },
6078
7007
  MemberExpression(node) {
6079
7008
  const name = modifierName(node);
6080
- if (!name || !testsModifiersBanned.has(name.split(".").at(-1) ?? "")) {
7009
+ if (!name || !banned.has(name.split(".").at(-1) ?? "")) {
6081
7010
  return;
6082
7011
  }
6083
7012
  if ([...names].some((test) => name.startsWith(`${test}.`))) {
@@ -6089,9 +7018,9 @@ var testsModifiers = defineRule52({
6089
7018
  });
6090
7019
 
6091
7020
  // src/rules/tests/tests-titles.ts
6092
- import { defineRule as defineRule53 } from "@oxlint/plugins";
6093
- var testsTitlesModifiers = new Set(["only", "skip", "concurrent", "sequential", "todo", "each", "for"]);
6094
- var testsTitles = defineRule53({
7021
+ import { defineRule as defineRule64 } from "@oxlint/plugins";
7022
+ var testsTitlesModifiers = ["only", "skip", "concurrent", "sequential", "todo", "each", "for"];
7023
+ var testsTitles = defineRule64({
6095
7024
  meta: {
6096
7025
  type: "suggestion",
6097
7026
  schema: [
@@ -6100,6 +7029,7 @@ var testsTitles = defineRule53({
6100
7029
  additionalProperties: false,
6101
7030
  properties: {
6102
7031
  callees: { type: "array", items: { type: "string" } },
7032
+ modifiers: { type: "array", items: { type: "string", minLength: 1 }, default: testsTitlesModifiers },
6103
7033
  deny: optionsPatternSchema,
6104
7034
  require: optionsPatternSchema,
6105
7035
  flags: { type: "string", pattern: "^[dgimsuvy]*$" }
@@ -6119,6 +7049,7 @@ var testsTitles = defineRule53({
6119
7049
  },
6120
7050
  createOnce(context) {
6121
7051
  let callees = [];
7052
+ let modifiers = new Set;
6122
7053
  let forbid = [];
6123
7054
  let required = [];
6124
7055
  let requiredLabel = "";
@@ -6127,6 +7058,7 @@ var testsTitles = defineRule53({
6127
7058
  const options = optionsFirst(context);
6128
7059
  const flags = options.flags ?? "";
6129
7060
  callees = options.callees;
7061
+ modifiers = new Set(options.modifiers ?? testsTitlesModifiers);
6130
7062
  forbid = options.deny === undefined ? [] : [options.deny].flat().map((source) => ({ regex: new RegExp(source, flags), pattern: source }));
6131
7063
  required = optionsPatternsWithEmpty(options.require, flags);
6132
7064
  requiredLabel = optionsPatternLabel(options.require);
@@ -6137,7 +7069,7 @@ var testsTitles = defineRule53({
6137
7069
  CallExpression(node) {
6138
7070
  const title = node.arguments.at(0);
6139
7071
  const callee = node.callee;
6140
- const configured = astMatchesCallee(astDottedName(callee), callees, testsTitlesModifiers) || callee.type === "CallExpression" && astMatchesCallee(astDottedName(callee.callee), callees, testsTitlesModifiers) || callee.type === "TaggedTemplateExpression" && astMatchesCallee(astDottedName(callee.tag), callees, testsTitlesModifiers);
7072
+ const configured = astMatchesCallee(astDottedName(callee), callees, modifiers) || callee.type === "CallExpression" && astMatchesCallee(astDottedName(callee.callee), callees, modifiers) || callee.type === "TaggedTemplateExpression" && astMatchesCallee(astDottedName(callee.tag), callees, modifiers);
6141
7073
  if (!title || !configured) {
6142
7074
  return;
6143
7075
  }
@@ -6166,11 +7098,11 @@ var testsTitles = defineRule53({
6166
7098
  });
6167
7099
 
6168
7100
  // src/rules/types/types-declarations.ts
6169
- import { defineRule as defineRule54 } from "@oxlint/plugins";
7101
+ import { defineRule as defineRule65 } from "@oxlint/plugins";
6170
7102
  function isValuePattern(source) {
6171
7103
  return /^\^?=/.test(source);
6172
7104
  }
6173
- var typesDeclarations = defineRule54({
7105
+ var typesDeclarations = defineRule65({
6174
7106
  meta: {
6175
7107
  type: "problem",
6176
7108
  schema: [
@@ -6213,7 +7145,7 @@ var typesDeclarations = defineRule54({
6213
7145
  });
6214
7146
 
6215
7147
  // src/rules/types/types-imported-alias.ts
6216
- import { defineRule as defineRule55 } from "@oxlint/plugins";
7148
+ import { defineRule as defineRule66 } from "@oxlint/plugins";
6217
7149
  function importedAliasTarget(declaration, imports) {
6218
7150
  if (declaration?.type !== "TSTypeAliasDeclaration") {
6219
7151
  return;
@@ -6225,7 +7157,7 @@ function importedAliasTarget(declaration, imports) {
6225
7157
  const { name } = type.typeName;
6226
7158
  return imports.has(name) && !declaration.typeParameters?.params.some((parameter) => parameter.name.name === name) ? name : undefined;
6227
7159
  }
6228
- var typesImportedAlias = defineRule55({
7160
+ var typesImportedAlias = defineRule66({
6229
7161
  meta: {
6230
7162
  type: "problem",
6231
7163
  schema: [],
@@ -6287,7 +7219,7 @@ var typesImportedAlias = defineRule55({
6287
7219
  });
6288
7220
 
6289
7221
  // src/rules/types/types-inline.ts
6290
- import { defineRule as defineRule56 } from "@oxlint/plugins";
7222
+ import { defineRule as defineRule67 } from "@oxlint/plugins";
6291
7223
  function parameterAnnotation(parameter) {
6292
7224
  if (parameter.type === "TSParameterProperty") {
6293
7225
  return parameterAnnotation(parameter.parameter);
@@ -6297,7 +7229,7 @@ function parameterAnnotation(parameter) {
6297
7229
  }
6298
7230
  return parameter.typeAnnotation;
6299
7231
  }
6300
- var typesInline = defineRule56({
7232
+ var typesInline = defineRule67({
6301
7233
  meta: {
6302
7234
  type: "problem",
6303
7235
  schema: [
@@ -6373,8 +7305,8 @@ var typesInline = defineRule56({
6373
7305
  var typesLocalAlias = astSelectorRule("TSTypeAliasDeclaration", "Local component/hook contracts use interfaces named for their owner. Derived or reusable aliases belong in domain types; do not substitute an empty interface.");
6374
7306
 
6375
7307
  // src/rules/types/types-namespace-import.ts
6376
- import { defineRule as defineRule57 } from "@oxlint/plugins";
6377
- var typesNamespaceImport = defineRule57({
7308
+ import { defineRule as defineRule68 } from "@oxlint/plugins";
7309
+ var typesNamespaceImport = defineRule68({
6378
7310
  meta: {
6379
7311
  type: "suggestion",
6380
7312
  schema: [
@@ -6416,26 +7348,50 @@ var typesNamespaceImport = defineRule57({
6416
7348
  });
6417
7349
 
6418
7350
  // src/rules/types/types-service.ts
6419
- import { defineRule as defineRule58 } from "@oxlint/plugins";
6420
- var typesService = defineRule58({
7351
+ import { defineRule as defineRule69 } from "@oxlint/plugins";
7352
+ var typesService = defineRule69({
6421
7353
  meta: {
6422
7354
  type: "problem",
6423
- schema: [],
7355
+ schema: [
7356
+ {
7357
+ type: "object",
7358
+ additionalProperties: false,
7359
+ properties: {
7360
+ suffix: { type: "string", minLength: 1, default: "Service" },
7361
+ factory: { type: "string", minLength: 1, default: "make" }
7362
+ }
7363
+ }
7364
+ ],
6424
7365
  messages: {
6425
- domain: "Move this type to its domain types file; only the matching factory-derived Service alias belongs here."
7366
+ domain: "Move this type to its domain types file; only the matching factory-derived {{suffix}} alias belongs here."
6426
7367
  }
6427
7368
  },
6428
7369
  createOnce(context) {
7370
+ let suffix = "Service";
7371
+ let factory = "make";
6429
7372
  return {
7373
+ before() {
7374
+ const options = optionsFirst(context, {});
7375
+ suffix = options.suffix ?? "Service";
7376
+ factory = options.factory ?? "make";
7377
+ },
6430
7378
  TSInterfaceDeclaration(node) {
6431
- context.report({ node, messageId: "domain" });
7379
+ context.report({
7380
+ node,
7381
+ messageId: "domain",
7382
+ data: { suffix }
7383
+ });
6432
7384
  },
6433
7385
  TSTypeAliasDeclaration(node) {
6434
7386
  const type = node.typeAnnotation;
6435
7387
  const argument = type.type === "TSTypeReference" && type.typeArguments?.params.length === 1 ? type.typeArguments.params[0] : undefined;
6436
- const isFactoryService = node.id.name.endsWith("Service") && type.type === "TSTypeReference" && type.typeName.type === "Identifier" && type.typeName.name === "ReturnType" && argument?.type === "TSTypeQuery" && argument.exprName.type === "Identifier" && argument.exprName.name === `make${node.id.name}`;
7388
+ const isFactoryService = node.id.name.endsWith(suffix) && type.type === "TSTypeReference" && type.typeName.type === "Identifier" && type.typeName.name === "ReturnType" && argument?.type === "TSTypeQuery" && argument.exprName.type === "Identifier" && argument.exprName.name === `${factory}${node.id.name}`;
6437
7389
  if (!isFactoryService) {
6438
- context.report({ node, messageId: "domain" });
7390
+ context.report({
7391
+ node,
7392
+ messageId: "domain",
7393
+ data: { suffix }
7394
+ });
6439
7395
  }
6440
7396
  }
6441
7397
  };
@@ -6443,12 +7399,12 @@ var typesService = defineRule58({
6443
7399
  });
6444
7400
 
6445
7401
  // src/rules/types/types-unknown.ts
6446
- import { defineRule as defineRule59 } from "@oxlint/plugins";
7402
+ import { defineRule as defineRule70 } from "@oxlint/plugins";
6447
7403
  function isCatchName(node) {
6448
7404
  const key = "key" in node && !(("computed" in node) && node.computed) ? node.key : undefined;
6449
7405
  return key?.type === "Identifier" && key.name === "catch";
6450
7406
  }
6451
- var typesUnknown = defineRule59({
7407
+ var typesUnknown = defineRule70({
6452
7408
  meta: { type: "problem", schema: [], messages: { forbidden: "Use an owned domain type rather than unknown." } },
6453
7409
  createOnce(context) {
6454
7410
  return {
@@ -6485,15 +7441,21 @@ var src_default = eslintCompatPlugin({
6485
7441
  "boundaries-rpc-database": boundariesRpcDatabase,
6486
7442
  "boundaries-tanstack": boundariesTanstack,
6487
7443
  "boundaries-token-owner": boundariesTokenOwner,
7444
+ "code-branch-once": codeBranchOnce,
6488
7445
  "code-constructor": codeConstructor,
6489
7446
  "code-double-negation": codeDoubleNegation,
7447
+ "code-function-nesting": codeFunctionNesting,
7448
+ "code-inline": codeInline,
7449
+ "code-justified-call": codeJustifiedCall,
6490
7450
  "code-literal-in": codeLiteralIn,
6491
7451
  "code-mutable-state": codeMutableState,
6492
7452
  "code-object-params": codeObjectParams,
6493
7453
  "code-paired-call": codePairedCall,
6494
7454
  "code-promise-mutation": codePromiseMutation,
6495
7455
  "code-switch": codeSwitch,
7456
+ "code-traced": codeTraced,
6496
7457
  "code-trivial-functions": codeTrivialFunctions,
7458
+ "code-yield-argument": codeYieldArgument,
6497
7459
  "comments-code": commentsCode,
6498
7460
  "comments-directives": commentsDirectives,
6499
7461
  "copy-dashes": copyDashes,
@@ -6508,13 +7470,17 @@ var src_default = eslintCompatPlugin({
6508
7470
  "files-restricted": filesRestricted,
6509
7471
  "files-route": filesRoute,
6510
7472
  "files-types-only": filesTypesOnly,
7473
+ "layout-arrow-body": layoutArrowBody,
6511
7474
  "layout-call-arguments": layoutCallArguments,
6512
7475
  "layout-call-array": layoutCallArray,
7476
+ "layout-call-depth": layoutCallDepth,
6513
7477
  "layout-chain": layoutChain,
7478
+ "layout-items": layoutItems,
6514
7479
  "layout-jsx-attributes": layoutJsxAttributes,
6515
7480
  "layout-key-value": layoutKeyValue,
6516
7481
  "layout-object": layoutObject,
6517
7482
  "layout-padding": layoutPadding,
7483
+ "layout-pipe": layoutPipe,
6518
7484
  "layout-ternary": layoutTernary,
6519
7485
  "naming-declarations": namingDeclarations,
6520
7486
  "naming-exports": namingExports,