oxlint-plugin-arch 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +7 -7
  3. package/dist/index.js +2154 -504
  4. package/dist/preset/groups/top.d.ts +2 -0
  5. package/dist/preset/preset-sets.d.ts +2 -0
  6. package/dist/preset/preset.constants.d.ts +4 -1
  7. package/dist/preset/preset.js +299 -57
  8. package/dist/preset/preset.utils.d.ts +2 -0
  9. package/dist/rules/code/code-inside.d.ts +1 -0
  10. package/dist/rules/effect/effect-layer-keys.d.ts +1 -0
  11. package/dist/rules/effect/effect-service-params.d.ts +1 -0
  12. package/dist/rules/effect/effect-yield-name.d.ts +1 -0
  13. package/dist/rules/effect/effect-yields-first.d.ts +1 -0
  14. package/dist/rules/files/files-siblings.d.ts +1 -0
  15. package/dist/rules/files/files-top-level.d.ts +1 -0
  16. package/dist/rules/layout/layout-type-arguments.d.ts +1 -0
  17. package/dist/rules/naming/naming-imports.d.ts +1 -0
  18. package/dist/rules/naming/naming-keys.d.ts +1 -0
  19. package/dist/rules/naming/naming-labels.d.ts +1 -0
  20. package/dist/rules/naming/naming-pairs.d.ts +1 -0
  21. package/dist/types/boundaries.types.d.ts +33 -1
  22. package/dist/types/code.types.d.ts +12 -0
  23. package/dist/types/comments.types.d.ts +1 -0
  24. package/dist/types/dillon-anti-slop.types.d.ts +6 -0
  25. package/dist/types/effect.types.d.ts +21 -0
  26. package/dist/types/files.types.d.ts +33 -3
  27. package/dist/types/layout.types.d.ts +16 -0
  28. package/dist/types/naming.types.d.ts +74 -2
  29. package/dist/types/preset.types.d.ts +26 -0
  30. package/dist/types/react.types.d.ts +13 -0
  31. package/dist/types/types.types.d.ts +4 -0
  32. package/dist/utils/ast.utils.d.ts +1 -2
  33. package/dist/utils/boundaries.utils.d.ts +72 -4
  34. package/dist/utils/naming.utils.d.ts +4 -1
  35. package/dist/utils/options.utils.d.ts +2 -0
  36. package/dist/vendor/dillon-anti-slop/index.js +40 -15
  37. package/package.json +1 -1
  38. package/src/vendor/dillon-anti-slop/UPSTREAM.md +3 -0
package/dist/index.js CHANGED
@@ -50,6 +50,12 @@ function optionsAssertMockingApis(patterns) {
50
50
  throw new OptionsError("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
51
51
  }
52
52
  }
53
+ function optionsEscape(value) {
54
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
+ }
56
+ function optionsWildcards(names) {
57
+ return [names ?? []].flat().map((name) => new RegExp(`^${name.split("*").map(optionsEscape).join(".*")}$`));
58
+ }
53
59
 
54
60
  // src/utils/ast.utils.ts
55
61
  var astSkippedKeys = new Set(["parent", "type", "start", "end", "range", "loc", "tokens", "comments"]);
@@ -237,9 +243,6 @@ function astIsInlineFunction(node) {
237
243
  function astIsCall(node) {
238
244
  return node.type === "CallExpression" || node.type === "NewExpression";
239
245
  }
240
- function astIsPipe(node) {
241
- return node.callee.type === "MemberExpression" && astMemberName(node.callee) === "pipe";
242
- }
243
246
  function astChildren(node) {
244
247
  return Object.entries(node).filter(([key]) => !astSkippedKeys.has(key)).map(([, child]) => child);
245
248
  }
@@ -249,17 +252,20 @@ function astSome(value, test, enter = () => true) {
249
252
  }
250
253
  return astIsNode(value) && (test(value) || enter(value) && astChildren(value).some((child) => astSome(child, test, enter)));
251
254
  }
252
- function astCallDepth(value) {
255
+ function astCallDepth(value, callbacks = false) {
253
256
  if (Array.isArray(value)) {
254
- return Math.max(0, ...value.map(astCallDepth));
257
+ return Math.max(0, ...value.map((item) => astCallDepth(item, callbacks)));
255
258
  }
256
- if (!astIsNode(value) || astIsInlineFunction(value)) {
259
+ if (!astIsNode(value)) {
257
260
  return 0;
258
261
  }
262
+ if (astIsInlineFunction(value)) {
263
+ return callbacks ? astCallDepth(value.body, callbacks) : 0;
264
+ }
259
265
  if (astIsCall(value)) {
260
- return Math.max(value.arguments.length === 0 ? 0 : 1 + astCallDepth(value.arguments), astCallDepth(value.callee));
266
+ return Math.max(value.arguments.length === 0 ? 0 : 1 + astCallDepth(value.arguments, callbacks), astCallDepth(value.callee, callbacks));
261
267
  }
262
- return astCallDepth(astChildren(value));
268
+ return astCallDepth(astChildren(value), callbacks);
263
269
  }
264
270
 
265
271
  // src/rules/backend/backend-orpc-output.ts
@@ -455,7 +461,8 @@ var namingStemSchemaProperties = {
455
461
  roles: { type: "array", items: { type: "string" } },
456
462
  separators: { type: "array", items: { type: "string" } },
457
463
  normalize: { type: "string", enum: ["remove-separators", "none"] },
458
- singularize: { type: "string", enum: ["none", "trailing-s"] }
464
+ singularize: { type: "string", enum: ["none", "trailing-s"] },
465
+ prefix: { type: "array", items: { type: "string", enum: ["file", "folder", "parent"] }, minItems: 1 }
459
466
  };
460
467
  function namingBasename(filePath) {
461
468
  return posix.basename(filePath.replaceAll("\\", "/"));
@@ -482,17 +489,73 @@ function namingFileStem(basename, mode = "before-first-dot", trailingRoles = [],
482
489
  }
483
490
  return stem;
484
491
  }
492
+ function namingSingular(value) {
493
+ const parts = value.split(/[-_]+/).filter(Boolean);
494
+ const last = parts.at(-1) ?? "";
495
+ return last.endsWith("s") && last.length > 1 ? [...parts.slice(0, -1), last.slice(0, -1)].join("-") : value;
496
+ }
497
+ function namingFolders(filename) {
498
+ return filename.replaceAll("\\", "/").split("/").slice(0, -1).toReversed();
499
+ }
485
500
  function namingFilePrefixesFor(filename, options) {
486
501
  const basename = namingBasename(filename).replace(/\.(tsx?|jsx?)$/, "");
487
- const stem = namingFileStem(basename, options.stem, options.roles, options.separators);
502
+ const [folder = "", parent = ""] = namingFolders(filename);
488
503
  const normalize = options.normalize ?? "remove-separators";
489
- const prefixes = [namingNormalized(stem, normalize)];
490
- const parts = stem.split(/[-_]+/).filter(Boolean);
491
- const last = parts.at(-1) ?? "";
492
- if (options.singularize === "trailing-s" && last.endsWith("s") && last.length > 1) {
493
- prefixes.push(namingNormalized([...parts.slice(0, -1), last.slice(0, -1)].join("-"), normalize));
504
+ const sources = { file: namingFileStem(basename, options.stem, options.roles, options.separators), folder, parent };
505
+ const prefixes = (options.prefix ?? ["file"]).flatMap((source) => {
506
+ const stem = sources[source];
507
+ return options.singularize === "trailing-s" ? [stem, namingSingular(stem)] : [stem];
508
+ });
509
+ return [...new Set(prefixes.map((stem) => namingNormalized(stem, normalize)).filter(Boolean))];
510
+ }
511
+ function namingTemplatePattern(template, placeholder = "[a-z0-9-]+") {
512
+ const seen = new Set;
513
+ const source = template.split(/(\{[^{}]+\})/).map((part) => {
514
+ const name = /^\{[^{}]+\}$/.test(part) ? part.slice(1, -1) : undefined;
515
+ if (name === undefined) {
516
+ return part.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
517
+ }
518
+ const again = seen.has(name);
519
+ seen.add(name);
520
+ return again ? `\\k<${name}>` : `(?<${name}>${placeholder})`;
521
+ }).join("");
522
+ try {
523
+ return new RegExp(`^${source}$`);
524
+ } catch {
525
+ return null;
494
526
  }
495
- return [...new Set(prefixes.filter(Boolean))];
527
+ }
528
+ function namingCaptures(filename, options = {}) {
529
+ const path = filename.replaceAll("\\", "/");
530
+ const file = namingBasename(path).replace(/\.(tsx?|jsx?)$/, "");
531
+ const [folder = "", parent = ""] = namingFolders(path);
532
+ const stem = namingFileStem(file, options.stem === "folder" ? undefined : options.stem, options.roles, options.separators);
533
+ const read = options.stem === "folder" ? folder : stem;
534
+ const last = read.split(/[-_]+/).at(-1) ?? "";
535
+ const domain = options.singularize === "trailing-s" && last.length > 1 && last.endsWith("s") ? read.slice(0, -1) : read;
536
+ const builtIn = { file, stem, folder, parent, domain };
537
+ if (options.file === undefined) {
538
+ return builtIn;
539
+ }
540
+ const depth = options.file.split("/").length;
541
+ const match = namingTemplatePattern(options.file, options.placeholder)?.exec(path.split("/").slice(-depth).join("/"));
542
+ return match ? { ...builtIn, ...match.groups } : null;
543
+ }
544
+ function namingExpand(template, captures, camelCase, escape = (value) => value) {
545
+ return template.replaceAll(/\{([A-Za-z][A-Za-z0-9]*)\}/g, (placeholder, key) => {
546
+ const name = key.toLowerCase();
547
+ if (!Object.hasOwn(captures, name)) {
548
+ return placeholder;
549
+ }
550
+ const value = captures[name];
551
+ if (key.length > 1 && key === key.toUpperCase()) {
552
+ return escape(value.split(/[-_\s.]+/).filter(Boolean).join("_").toUpperCase());
553
+ }
554
+ if (/^[A-Z]/.test(key)) {
555
+ return escape(namingPascalCase(value));
556
+ }
557
+ return escape(camelCase ? namingCamelCase(value) : value);
558
+ });
496
559
  }
497
560
  function namingHasFilePrefix(name, prefixes, normalize) {
498
561
  const comparable = normalize === "none" ? name : name.replaceAll("_", "").toLowerCase();
@@ -842,6 +905,27 @@ import { defineRule as defineRule5 } from "@oxlint/plugins";
842
905
  // src/utils/boundaries.utils.ts
843
906
  import { builtinModules } from "node:module";
844
907
  import { dirname, resolve } from "node:path";
908
+ var boundariesRuntimeConventions = {
909
+ server: ["server"],
910
+ rsc: ["rsc"],
911
+ actions: ["action"],
912
+ client: ["client"],
913
+ clientOnly: ["client-only"],
914
+ hooks: ["hooks"],
915
+ shared: ["utils", "constants"],
916
+ lib: "lib"
917
+ };
918
+ var boundariesSuffixList = { type: "array", items: { type: "string", minLength: 1 } };
919
+ function boundariesConventionsFor(options) {
920
+ return { ...boundariesRuntimeConventions, ...options.conventions };
921
+ }
922
+ function boundariesHasSuffix(suffixes, path, optionalExtension = false) {
923
+ const extension = optionalExtension ? "(?:\\.[cm]?[jt]sx?)?" : "\\.[cm]?[jt]sx?";
924
+ return suffixes.length > 0 && new RegExp(`\\.(?:${suffixes.map(optionsEscape).join("|")})${extension}$`).test(path);
925
+ }
926
+ function boundariesIsClientOnly(conventions, target) {
927
+ return conventions.hooks.some((folder) => target.includes(`/${folder}/`)) || conventions.clientOnly.some((suffix) => new RegExp(`\\.${optionsEscape(suffix)}(?:\\.|$)`).test(target));
928
+ }
845
929
  var boundariesServerGlobals = ["process", "Bun", "Deno", "Buffer", "__dirname", "__filename", "require"];
846
930
  var boundariesBrowserGlobals = [
847
931
  "window",
@@ -877,7 +961,21 @@ var boundariesRuntimeProperties = {
877
961
  dynamic: { type: "array", items: { type: "string" } },
878
962
  serverPackages: { type: "array", items: { type: "string" } },
879
963
  clientPackages: { type: "array", items: { type: "string" } },
880
- hookPackages: { type: "array", items: { type: "string" } }
964
+ hookPackages: { type: "array", items: { type: "string" } },
965
+ conventions: {
966
+ type: "object",
967
+ additionalProperties: false,
968
+ properties: {
969
+ server: boundariesSuffixList,
970
+ rsc: boundariesSuffixList,
971
+ actions: boundariesSuffixList,
972
+ client: boundariesSuffixList,
973
+ clientOnly: boundariesSuffixList,
974
+ hooks: boundariesSuffixList,
975
+ shared: boundariesSuffixList,
976
+ lib: { type: "string", minLength: 1 }
977
+ }
978
+ }
881
979
  };
882
980
  var boundariesServerPackages = ["@orpc/server", "react-dom/server"];
883
981
  var boundariesRuntimeMessages = {
@@ -908,16 +1006,21 @@ function boundariesReport(context, node, source, reason) {
908
1006
  data: { source, reason }
909
1007
  });
910
1008
  }
911
- function boundariesImportExpression(context, node, allowComputed, check) {
912
- const source = boundariesLiteralSource(node);
913
- if (source !== undefined) {
914
- check(source);
915
- } else if (!allowComputed) {
916
- context.report({ node, messageId: "computed" });
917
- }
1009
+ function boundariesRuntimeImportVisitors(context, check, allowComputed) {
1010
+ return {
1011
+ ...boundariesExportVisitors(check),
1012
+ ImportExpression(node) {
1013
+ const source = boundariesLiteralSource(node);
1014
+ if (source !== undefined) {
1015
+ check(node, source, false, false);
1016
+ } else if (!allowComputed()) {
1017
+ context.report({ node, messageId: "computed" });
1018
+ }
1019
+ }
1020
+ };
918
1021
  }
919
- function boundariesIsSharedFile(options, file) {
920
- return /\.(?:utils|constants)\.[cm]?[jt]sx?$/.test(file) || options.web.some((path) => file.startsWith(`${path}/lib/`));
1022
+ function boundariesIsSharedFile(options, conventions, file) {
1023
+ return boundariesHasSuffix(conventions.shared, file) || options.web.some((path) => file.startsWith(`${path}/${conventions.lib}/`));
921
1024
  }
922
1025
  function boundariesDeclaredVariable(start, name) {
923
1026
  for (let scope = start;scope; scope = scope.upper) {
@@ -946,7 +1049,7 @@ function boundariesMemberName(node) {
946
1049
  }
947
1050
  return node.computed && node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : null;
948
1051
  }
949
- function boundariesResolveTarget(options, file, source) {
1052
+ function boundariesResolveTarget(options, file, source, services = "services") {
950
1053
  const owner = [...options.web, ...options.backend].sort((a, b) => b.length - a.length).find((root) => file.startsWith(`${root}/`));
951
1054
  const web = owner && options.web.includes(owner) ? owner : undefined;
952
1055
  const alias = Object.entries(options.aliases).sort(([a], [b]) => b.length - a.length).find(([prefix]) => source === prefix || source.startsWith(`${prefix}/`));
@@ -957,13 +1060,13 @@ function boundariesResolveTarget(options, file, source) {
957
1060
  return resolve(dirname(file), source).replaceAll("\\", "/");
958
1061
  }
959
1062
  if (source.startsWith("@/") && web) {
960
- return `${web}/${source.slice(2)}`;
1063
+ return resolve(web, source.slice(2)).replaceAll("\\", "/");
961
1064
  }
962
1065
  if (source.startsWith("#/") && owner) {
963
- return `${owner}/${source.slice(2)}`;
1066
+ return resolve(owner, source.slice(2)).replaceAll("\\", "/");
964
1067
  }
965
1068
  if (source.startsWith("#services/") && owner) {
966
- return `${owner}/services/${source.slice(10)}`;
1069
+ return resolve(owner, services, source.slice("#services/".length)).replaceAll("\\", "/");
967
1070
  }
968
1071
  return source;
969
1072
  }
@@ -1080,7 +1183,17 @@ var boundariesDeny = defineRule6({
1080
1183
  additionalProperties: false,
1081
1184
  properties: {
1082
1185
  imports: { type: "object", additionalProperties: { type: "string" } },
1083
- calls: { type: "object", additionalProperties: { type: "string" } }
1186
+ calls: { type: "object", additionalProperties: { type: "string" } },
1187
+ only: {
1188
+ type: "array",
1189
+ items: {
1190
+ type: "object",
1191
+ additionalProperties: false,
1192
+ properties: { imports: optionsPatternSchema, calls: optionsPatternSchema, in: optionsPatternSchema, message: { type: "string" } },
1193
+ required: ["in"],
1194
+ anyOf: [{ required: ["imports"] }, { required: ["calls"] }]
1195
+ }
1196
+ }
1084
1197
  }
1085
1198
  }
1086
1199
  ],
@@ -1105,8 +1218,17 @@ var boundariesDeny = defineRule6({
1105
1218
  return {
1106
1219
  before() {
1107
1220
  const options = optionsFirst(context, {});
1108
- imports = Object.entries(options.imports ?? {}).map(([pattern, message]) => [optionsGlob(pattern), message && ` ${message}`]);
1109
- calls = new Map(Object.entries(options.calls ?? {}).map(([name, message]) => [name, message && ` ${message}`]));
1221
+ const filename = context.filename.replaceAll("\\", "/");
1222
+ const elsewhere = (options.only ?? []).filter((entry) => ![entry.in].flat().some((place) => optionsGlob(`**/${place}{,/**}`).test(filename)));
1223
+ const owned = (entry) => ` ${entry.message ?? `Only ${[entry.in].flat().join(", ")} may use it.`}`;
1224
+ imports = [
1225
+ ...Object.entries(options.imports ?? {}).map(([pattern, message]) => [optionsGlob(pattern), message && ` ${message}`]),
1226
+ ...elsewhere.flatMap((entry) => [entry.imports ?? []].flat().map((pattern) => [optionsGlob(pattern), owned(entry)]))
1227
+ ];
1228
+ calls = new Map([
1229
+ ...elsewhere.flatMap((entry) => [entry.calls ?? []].flat().map((name) => [name, owned(entry)])),
1230
+ ...Object.entries(options.calls ?? {}).map(([name, message]) => [name, message && ` ${message}`])
1231
+ ]);
1110
1232
  if (imports.length === 0 && calls.size === 0) {
1111
1233
  return false;
1112
1234
  }
@@ -1145,7 +1267,25 @@ var boundariesDeny = defineRule6({
1145
1267
  import { defineRule as defineRule7 } from "@oxlint/plugins";
1146
1268
  import { basename, resolve as resolve2 } from "node:path";
1147
1269
  var boundariesSurfaces = ["service", "client", "server", "rsc", "utils"];
1148
- var boundariesActionOrQuery = /\/(?:[^/]+)-(?:action|query)\./;
1270
+ var boundariesConventions = {
1271
+ services: "services",
1272
+ registry: "service",
1273
+ types: "types",
1274
+ lib: "lib",
1275
+ contracts: ["types", "constants"],
1276
+ implementations: ["service", "utils"],
1277
+ implementationFolders: ["core"],
1278
+ operations: ["action", "query"],
1279
+ operationFolders: ["actions", "queries"],
1280
+ domainFiles: ["action", "query", "driver"],
1281
+ clients: ["client"]
1282
+ };
1283
+ function boundariesAny(values) {
1284
+ return values.length === 0 ? "(?!)" : `(?:${values.map(optionsEscape).join("|")})`;
1285
+ }
1286
+ function boundariesIsOperation(conventions, path) {
1287
+ return new RegExp(`/[^/]+-${boundariesAny(conventions.operations)}\\.`).test(path);
1288
+ }
1149
1289
  function boundariesContains(directory, candidate) {
1150
1290
  return candidate === directory || candidate.startsWith(`${directory}/`);
1151
1291
  }
@@ -1155,44 +1295,56 @@ function boundariesIsSurface(options, stem) {
1155
1295
  function boundariesStripExtension(path) {
1156
1296
  return path.replace(/\.[cm]?[jt]sx?$/, "");
1157
1297
  }
1298
+ function boundariesConventionsOf(options) {
1299
+ return { ...boundariesConventions, ...options.conventions };
1300
+ }
1158
1301
  function boundariesServiceFolder(options, root) {
1159
1302
  const nested = [...options.web, ...options.backend].filter((other) => other.length > root.length && boundariesContains(root, other));
1160
- return options.services?.find((folder) => folder.startsWith(`${root}/`) && !nested.some((other) => boundariesContains(other, folder))) ?? `${root}/services`;
1303
+ return options.services?.find((folder) => folder.startsWith(`${root}/`) && !nested.some((other) => boundariesContains(other, folder))) ?? `${root}/${boundariesConventionsOf(options).services}`;
1161
1304
  }
1162
- function boundariesDomain(path, appRoot, services) {
1305
+ function boundariesDomain(conventions, path, appRoot, services) {
1163
1306
  const relative = path.slice(appRoot.length + 1);
1164
- if (relative.startsWith("types/") && relative.endsWith(".types.ts")) {
1165
- return relative.slice("types/".length).split(".")[0];
1307
+ if (relative.startsWith(`${conventions.types}/`) && relative.endsWith(`.${conventions.types}.ts`)) {
1308
+ return relative.slice(conventions.types.length + 1).split(".")[0];
1166
1309
  }
1167
1310
  if (!boundariesContains(services, path) || path === services) {
1168
1311
  return;
1169
1312
  }
1170
1313
  const parts = path.slice(services.length + 1).split("/");
1171
- return parts.length > 1 ? parts[0] : parts[0].split(".")[0].replace(/-(?:action|query|driver)$/, "");
1314
+ return parts.length > 1 ? parts[0] : parts[0].split(".")[0].replace(new RegExp(`-${boundariesAny(conventions.domainFiles)}$`), "");
1172
1315
  }
1173
1316
  function boundariesPrivateDomain({ options, file, target, reExport, roots, owner }) {
1174
1317
  const roles = options.roles;
1318
+ const conventions = boundariesConventionsOf(options);
1175
1319
  if (!roles?.length) {
1176
1320
  return;
1177
1321
  }
1178
1322
  const targetOwner = roots.find((path) => boundariesContains(path, target));
1179
- const fileDomain = owner ? boundariesDomain(file, owner, boundariesServiceFolder(options, owner)) : undefined;
1180
- const targetDomain = targetOwner ? boundariesDomain(target, targetOwner, boundariesServiceFolder(options, targetOwner)) : undefined;
1323
+ const fileDomain = owner ? boundariesDomain(conventions, file, owner, boundariesServiceFolder(options, owner)) : undefined;
1324
+ const targetDomain = targetOwner ? boundariesDomain(conventions, target, targetOwner, boundariesServiceFolder(options, targetOwner)) : undefined;
1181
1325
  const sameDomain = owner && owner === targetOwner && fileDomain && fileDomain === targetDomain;
1182
1326
  const targetStem = boundariesStripExtension(target);
1183
- const publicSurface = boundariesIsSurface(options, targetStem) && !boundariesActionOrQuery.test(targetStem);
1184
- const privateTarget = roles.some((role) => targetStem.endsWith(`.${role}`)) || targetDomain && !publicSurface && !/\.(?:types|constants)$/.test(targetStem);
1327
+ const publicSurface = boundariesIsSurface(options, targetStem) && !boundariesIsOperation(conventions, targetStem);
1328
+ const privateTarget = roles.some((role) => targetStem.endsWith(`.${role}`)) || targetDomain && !publicSurface && !new RegExp(`\\.${boundariesAny(conventions.contracts)}$`).test(targetStem);
1185
1329
  if (!privateTarget) {
1186
1330
  return;
1187
1331
  }
1188
- if (reExport && file.endsWith(".ts") && boundariesIsSurface(options, file.slice(0, -".ts".length)) && !boundariesActionOrQuery.test(file)) {
1332
+ if (reExport && file.endsWith(".ts") && boundariesIsSurface(options, file.slice(0, -".ts".length)) && !boundariesIsOperation(conventions, file)) {
1189
1333
  return "public service and utility surfaces must not re-export private domain internals";
1190
1334
  }
1191
1335
  return sameDomain ? undefined : "private domain files stay in the same domain and application; use its service API or shared utilities";
1192
1336
  }
1337
+ function boundariesPortableFolder(options) {
1338
+ if (!options.portable) {
1339
+ return;
1340
+ }
1341
+ return typeof options.portable === "string" ? options.portable : boundariesConventionsOf(options).lib;
1342
+ }
1193
1343
  function boundariesPortableLib(check) {
1194
1344
  const { options, file, source, target, typeOnly, reExport, roots, web, backendImport } = check;
1195
- if (!options.portable || !web || !boundariesContains(`${web}/lib`, file) || boundariesServiceFolder(options, web) === `${web}/lib`) {
1345
+ const { types } = boundariesConventionsOf(options);
1346
+ const lib = boundariesPortableFolder(options);
1347
+ if (!lib || !web || !boundariesContains(`${web}/${lib}`, file) || boundariesServiceFolder(options, web) === `${web}/${lib}`) {
1196
1348
  return;
1197
1349
  }
1198
1350
  const portableTarget = target.startsWith("/") ? resolve2(target).replaceAll("\\", "/") : target;
@@ -1202,11 +1354,11 @@ function boundariesPortableLib(check) {
1202
1354
  if (backendImport || backendTarget || appService || appPackage) {
1203
1355
  return "portable frontend lib does not import application services or application APIs";
1204
1356
  }
1205
- const appType = roots.some((path) => boundariesContains(`${path}/types`, portableTarget));
1357
+ const appType = roots.some((path) => boundariesContains(`${path}/${types}`, portableTarget));
1206
1358
  const stem = boundariesStripExtension(basename(file)).replace(/\.utils$/, "");
1207
- const ownType = typeOnly && !reExport && boundariesStripExtension(portableTarget) === `${web}/types/${stem}.types`;
1359
+ const ownType = typeOnly && !reExport && boundariesStripExtension(portableTarget) === `${web}/${types}/${stem}.${types}`;
1208
1360
  if (appType && !ownType) {
1209
- return `portable frontend lib may only type-import its own contract '${web}/types/${stem}.types.ts'; app type re-exports are forbidden`;
1361
+ return `portable frontend lib may only type-import its own contract '${web}/${types}/${stem}.${types}.ts'; app type re-exports are forbidden`;
1210
1362
  }
1211
1363
  return;
1212
1364
  }
@@ -1225,7 +1377,24 @@ var boundariesImports = defineRule7({
1225
1377
  roles: { type: "array", items: { type: "string" } },
1226
1378
  surfaces: { type: "array", items: { type: "string", minLength: 1 }, default: boundariesSurfaces },
1227
1379
  services: { type: "array", items: { type: "string" } },
1228
- portable: { type: "boolean" },
1380
+ portable: { anyOf: [{ type: "boolean" }, { type: "string", minLength: 1 }] },
1381
+ conventions: {
1382
+ type: "object",
1383
+ additionalProperties: false,
1384
+ properties: {
1385
+ services: { type: "string", minLength: 1 },
1386
+ registry: { type: "string", minLength: 1 },
1387
+ types: { type: "string", minLength: 1 },
1388
+ lib: { type: "string", minLength: 1 },
1389
+ contracts: { type: "array", items: { type: "string", minLength: 1 } },
1390
+ implementations: { type: "array", items: { type: "string", minLength: 1 } },
1391
+ implementationFolders: { type: "array", items: { type: "string", minLength: 1 } },
1392
+ operations: { type: "array", items: { type: "string", minLength: 1 } },
1393
+ operationFolders: { type: "array", items: { type: "string", minLength: 1 } },
1394
+ domainFiles: { type: "array", items: { type: "string", minLength: 1 } },
1395
+ clients: { type: "array", items: { type: "string", minLength: 1 } }
1396
+ }
1397
+ },
1229
1398
  backendPackages: { type: "array", items: { type: "string" } },
1230
1399
  entrypoints: { type: "array", items: { type: "string" } },
1231
1400
  appPackages: { type: "array", items: { type: "string" } },
@@ -1244,7 +1413,8 @@ var boundariesImports = defineRule7({
1244
1413
  const file = context.filename.replaceAll("\\", "/");
1245
1414
  const roots = [...options.web, ...options.backend].sort((a, b) => b.length - a.length);
1246
1415
  const owner = roots.find((path) => boundariesContains(path, file));
1247
- const target = boundariesResolveTarget(options, file, source);
1416
+ const conventions = boundariesConventionsOf(options);
1417
+ const target = boundariesResolveTarget(options, file, source, conventions.services);
1248
1418
  const web = owner && options.web.includes(owner) ? owner : undefined;
1249
1419
  const backendImport = options.backend.some((path) => boundariesContains(path, target)) || boundariesMatchesImport(options.backendPackages, source);
1250
1420
  const imported = { options, file, source, target, typeOnly, reExport, roots, owner, web, backendImport };
@@ -1253,13 +1423,16 @@ var boundariesImports = defineRule7({
1253
1423
  const serviceFolders = roots.map((path) => boundariesServiceFolder(options, path));
1254
1424
  const serviceTarget = serviceFolders.find((folder) => boundariesContains(folder, target));
1255
1425
  const serviceFile = serviceFolders.find((folder) => boundariesContains(folder, file));
1256
- const domainContract = /\.(?:types|constants)\.ts$/.test(file) && !typeOnly && (/\/(?:core\/|services\/.*(?:\.(?:service|utils)|-(?:action|query)\.|\/(?:actions|queries)\/))/.test(target) || serviceTarget !== undefined && /\/.*(?:\.(?:service|utils)|-(?:action|query)\.|\/(?:actions|queries)\/)/.test(target.slice(serviceTarget.length)));
1426
+ const implementation = `(?:\\.${boundariesAny(conventions.implementations)}|-${boundariesAny(conventions.operations)}\\.|/${boundariesAny(conventions.operationFolders)}/)`;
1427
+ const services = optionsEscape(conventions.services);
1428
+ const domainContract = new RegExp(`\\.${boundariesAny(conventions.contracts)}\\.ts$`).test(file) && !typeOnly && (new RegExp(`/(?:${boundariesAny(conventions.implementationFolders)}/|${services}/.*${implementation})`).test(target) || serviceTarget !== undefined && new RegExp(`/.*${implementation}`).test(target.slice(serviceTarget.length)));
1429
+ const registry = `/${optionsEscape(conventions.registry)}(?:\\.ts)?$`;
1257
1430
  const reason = [
1258
1431
  web && backendImport && !options.entrypoints.includes(source) ? "frontend code uses only public backend entry points" : undefined,
1259
1432
  inPackage && appTarget ? "shared packages do not import app internals" : undefined,
1260
- (file.includes("/services/") && /\/services\/service(?:\.ts)?$/.test(target) || serviceFile !== undefined && /^\/service(?:\.ts)?$/.test(target.slice(serviceFile.length))) && !typeOnly ? "service dependencies are injected rather than read from the registry" : undefined,
1433
+ (file.includes(`/${conventions.services}/`) && new RegExp(`/${services}${registry}`).test(target) || serviceFile !== undefined && new RegExp(`^${registry}`).test(target.slice(serviceFile.length))) && !typeOnly ? "service dependencies are injected rather than read from the registry" : undefined,
1261
1434
  domainContract ? "domain types/constants use type-only imports from implementations" : undefined,
1262
- file.endsWith(".client.ts") && !typeOnly && boundariesMatchesImport(options.uiPackages ?? ["react", "react-dom"], source) ? "client services are React-free; use hooks for React behavior" : undefined,
1435
+ conventions.clients.some((client) => file.endsWith(`.${client}.ts`)) && !typeOnly && boundariesMatchesImport(options.uiPackages ?? ["react", "react-dom"], source) ? "client services are React-free; use hooks for React behavior" : undefined,
1263
1436
  boundariesPrivateDomain(imported),
1264
1437
  boundariesPortableLib(imported)
1265
1438
  ].findLast((candidate) => candidate !== undefined);
@@ -1300,7 +1473,8 @@ var boundariesImports = defineRule7({
1300
1473
  TSImportType(node) {
1301
1474
  const options = { ...boundariesImportsDefaults, ...optionsFirst(context) };
1302
1475
  const file = context.filename.replaceAll("\\", "/");
1303
- if (options.portable && options.web.some((path) => file.startsWith(`${path}/lib/`) && boundariesServiceFolder(options, path) !== `${path}/lib`)) {
1476
+ const lib = boundariesPortableFolder(options);
1477
+ if (lib && options.web.some((path) => file.startsWith(`${path}/${lib}/`) && boundariesServiceFolder(options, path) !== `${path}/${lib}`)) {
1304
1478
  check(node, node.source.value, true);
1305
1479
  }
1306
1480
  }
@@ -1344,6 +1518,7 @@ var boundariesNext = defineRule8({
1344
1518
  },
1345
1519
  createOnce(context) {
1346
1520
  let options;
1521
+ let conventions = boundariesConventionsFor({});
1347
1522
  let file = "";
1348
1523
  let server = false;
1349
1524
  let client = false;
@@ -1355,12 +1530,12 @@ var boundariesNext = defineRule8({
1355
1530
  return;
1356
1531
  }
1357
1532
  const target = boundariesResolveTarget(options, file, source);
1358
- const clientOnly = /(?:\/hooks\/|\.client-only(?:\.|$))/.test(target) || boundariesMatchesImport(options.clientPackages, source) || boundariesMatchesImport(options.client, source);
1533
+ const clientOnly = boundariesIsClientOnly(conventions, target) || boundariesMatchesImport(options.clientPackages, source) || boundariesMatchesImport(options.client, source);
1359
1534
  if (clientOnly && (server || shared)) {
1360
1535
  boundariesReport(context, node, source, "browser-only dependencies do not belong in server, RSC or shared modules");
1361
1536
  return;
1362
1537
  }
1363
- const serverOnly = boundariesIsServerPlatformImport(source, options.serverPackages) || boundariesIsBackendImport(options, target, source) || /\.(?:server|rsc)(?:\.[cm]?[jt]sx?)?$/.test(target) || boundariesMatchesImport(options.server, source);
1538
+ const serverOnly = boundariesIsServerPlatformImport(source, options.serverPackages) || boundariesIsBackendImport(options, target, source) || boundariesHasSuffix([...conventions.server, ...conventions.rsc], target, true) || boundariesMatchesImport(options.server, source);
1364
1539
  if (!serverOnly || server) {
1365
1540
  return;
1366
1541
  }
@@ -1369,6 +1544,7 @@ var boundariesNext = defineRule8({
1369
1544
  return {
1370
1545
  Program(program) {
1371
1546
  options = { ...boundariesNextDefaults, ...optionsFirst(context) };
1547
+ conventions = boundariesConventionsFor(options);
1372
1548
  reactNamespaces.clear();
1373
1549
  file = context.filename.replaceAll("\\", "/");
1374
1550
  const directives = new Map;
@@ -1379,9 +1555,9 @@ var boundariesNext = defineRule8({
1379
1555
  }
1380
1556
  const useServer = directives.get("use server");
1381
1557
  const useClient = directives.get("use client");
1382
- const serverName = /\.(?:server|rsc|action)\.[cm]?[jt]sx?$/.test(file);
1383
- const clientName = /\.client\.[cm]?[jt]sx?$/.test(file);
1384
- shared = boundariesIsSharedFile(options, file);
1558
+ const serverName = boundariesHasSuffix([...conventions.server, ...conventions.rsc, ...conventions.actions], file);
1559
+ const clientName = boundariesHasSuffix(conventions.client, file);
1560
+ shared = boundariesIsSharedFile(options, conventions, file);
1385
1561
  server = !useClient && (serverName || Boolean(useServer));
1386
1562
  client = clientName || Boolean(useClient);
1387
1563
  if (useClient && serverName) {
@@ -1398,7 +1574,7 @@ var boundariesNext = defineRule8({
1398
1574
  data: { directive: "use server", side: "client" }
1399
1575
  });
1400
1576
  }
1401
- if (/\.action\.[cm]?[jt]sx?$/.test(file) && !useServer) {
1577
+ if (boundariesHasSuffix(conventions.actions, file) && !useServer) {
1402
1578
  context.report({ node: program, messageId: "action" });
1403
1579
  }
1404
1580
  },
@@ -1428,10 +1604,7 @@ var boundariesNext = defineRule8({
1428
1604
  boundariesReport(context, node, `${node.object.name}.${name}`, boundariesClientHookReason);
1429
1605
  }
1430
1606
  },
1431
- ...boundariesExportVisitors(check),
1432
- ImportExpression(node) {
1433
- boundariesImportExpression(context, node, Boolean(options.dynamic?.includes(file)), (source) => check(node, source, false));
1434
- },
1607
+ ...boundariesRuntimeImportVisitors(context, check, () => Boolean(options.dynamic?.includes(file))),
1435
1608
  "Program:exit"() {
1436
1609
  const scope = context.sourceCode.scopeManager.scopes.at(0);
1437
1610
  if (!scope) {
@@ -1548,11 +1721,9 @@ var boundariesTanstackDefaults = {
1548
1721
  hookPackages: ["react", "@tanstack/react-query", "@tanstack/react-router"],
1549
1722
  startPackages: ["@tanstack/react-start"],
1550
1723
  rscPackages: ["@tanstack/react-start/rsc", "@tanstack/react-start-rsc"],
1551
- renderers: ["renderServerComponent", "renderToReadableStream"]
1724
+ renderers: ["renderServerComponent", "renderToReadableStream"],
1725
+ serverHooks: ["useId"]
1552
1726
  };
1553
- function boundariesIsClientHook(name) {
1554
- return /^use[A-Z]/.test(name) && name !== "useId";
1555
- }
1556
1727
  var boundariesTanstack = defineRule10({
1557
1728
  meta: {
1558
1729
  type: "problem",
@@ -1565,7 +1736,8 @@ var boundariesTanstack = defineRule10({
1565
1736
  rpc: { type: "array", items: { type: "string" } },
1566
1737
  startPackages: { type: "array", items: { type: "string" } },
1567
1738
  rscPackages: { type: "array", items: { type: "string" } },
1568
- renderers: { type: "array", items: { type: "string", minLength: 1 }, default: boundariesTanstackDefaults.renderers }
1739
+ renderers: { type: "array", items: { type: "string", minLength: 1 }, default: boundariesTanstackDefaults.renderers },
1740
+ serverHooks: { type: "array", items: { type: "string", minLength: 1 }, default: boundariesTanstackDefaults.serverHooks }
1569
1741
  }
1570
1742
  }
1571
1743
  ],
@@ -1573,6 +1745,7 @@ var boundariesTanstack = defineRule10({
1573
1745
  },
1574
1746
  createOnce(context) {
1575
1747
  let options;
1748
+ let conventions = boundariesConventionsFor({});
1576
1749
  let file = "";
1577
1750
  let serverFile = false;
1578
1751
  let rscFile = false;
@@ -1581,6 +1754,7 @@ var boundariesTanstack = defineRule10({
1581
1754
  let referenced;
1582
1755
  let imports = new Map;
1583
1756
  const boundaries = [];
1757
+ const clientHook = (name) => /^use[A-Z]/.test(name) && !options.serverHooks.includes(name);
1584
1758
  function imported(node) {
1585
1759
  const entry = imports.get(node.name);
1586
1760
  const variable = entry ? boundariesDeclaredVariable(context.sourceCode.getScope(node), node.name) : null;
@@ -1623,7 +1797,7 @@ var boundariesTanstack = defineRule10({
1623
1797
  return;
1624
1798
  }
1625
1799
  const target = boundariesResolveTarget(options, file, source);
1626
- const clientImport = /(?:\/hooks\/|\.client-only(?:\.|$))/.test(target) || boundariesMatchesImport(options.clientPackages, source) || boundariesMatchesImport(options.client, source);
1800
+ const clientImport = boundariesIsClientOnly(conventions, target) || boundariesMatchesImport(options.clientPackages, source) || boundariesMatchesImport(options.client, source);
1627
1801
  if (clientImport && (serverFile || rscFile || shared) && !clientSide(node.start)) {
1628
1802
  const references = node.type === "ImportDeclaration" ? runtimeReferences(node) : [];
1629
1803
  if (shared || references.length === 0 || !references.every(clientSide)) {
@@ -1631,7 +1805,7 @@ var boundariesTanstack = defineRule10({
1631
1805
  }
1632
1806
  return;
1633
1807
  }
1634
- const serverImport = boundariesIsServerPlatformImport(source, options.serverPackages) || boundariesIsBackendImport(options, target, source) || /\.server(?:\.[cm]?[jt]sx?)?$/.test(target) || boundariesMatchesImport(options.server, source);
1808
+ const serverImport = boundariesIsServerPlatformImport(source, options.serverPackages) || boundariesIsBackendImport(options, target, source) || boundariesHasSuffix(conventions.server, target, true) || boundariesMatchesImport(options.server, source);
1635
1809
  if (!renderer && !serverImport) {
1636
1810
  return;
1637
1811
  }
@@ -1659,10 +1833,11 @@ var boundariesTanstack = defineRule10({
1659
1833
  return {
1660
1834
  Program(program) {
1661
1835
  options = { ...boundariesTanstackDefaults, ...optionsFirst(context) };
1836
+ conventions = boundariesConventionsFor(options);
1662
1837
  file = context.filename.replaceAll("\\", "/");
1663
- serverFile = /\.server\.[cm]?[jt]sx?$/.test(file);
1664
- rscFile = /\.rsc\.[cm]?[jt]sx?$/.test(file);
1665
- shared = boundariesIsSharedFile(options, file);
1838
+ serverFile = boundariesHasSuffix(conventions.server, file);
1839
+ rscFile = boundariesHasSuffix(conventions.rsc, file);
1840
+ shared = boundariesIsSharedFile(options, conventions, file);
1666
1841
  runtimeIdentifiers.clear();
1667
1842
  referenced = undefined;
1668
1843
  boundaries.length = 0;
@@ -1729,7 +1904,7 @@ var boundariesTanstack = defineRule10({
1729
1904
  return;
1730
1905
  }
1731
1906
  for (const specifier of node.specifiers) {
1732
- if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type" || specifier.imported.type !== "Identifier" || !boundariesIsClientHook(specifier.imported.name)) {
1907
+ if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type" || specifier.imported.type !== "Identifier" || !clientHook(specifier.imported.name)) {
1733
1908
  continue;
1734
1909
  }
1735
1910
  const references = runtimeReferences(node, specifier.local.name);
@@ -1738,10 +1913,7 @@ var boundariesTanstack = defineRule10({
1738
1913
  }
1739
1914
  }
1740
1915
  },
1741
- ...boundariesExportVisitors(check),
1742
- ImportExpression(node) {
1743
- boundariesImportExpression(context, node, Boolean(options.dynamic?.includes(file)), (source) => check(node, source, false));
1744
- },
1916
+ ...boundariesRuntimeImportVisitors(context, check, () => Boolean(options.dynamic?.includes(file))),
1745
1917
  MemberExpression(node) {
1746
1918
  const name = boundariesMemberName(node);
1747
1919
  if (!name || node.object.type !== "Identifier") {
@@ -1751,7 +1923,7 @@ var boundariesTanstack = defineRule10({
1751
1923
  if (kind === "rsc-renderers" && options.renderers.includes(name) && (shared || !serverPosition(node.start, true))) {
1752
1924
  boundariesReport(context, node, name, "server renderers require a server execution boundary");
1753
1925
  }
1754
- if ((serverFile || rscFile) && kind === "client-hooks" && boundariesIsClientHook(name) && !clientSide(node.start)) {
1926
+ if ((serverFile || rscFile) && kind === "client-hooks" && clientHook(name) && !clientSide(node.start)) {
1755
1927
  boundariesReport(context, node, name, boundariesClientHookReason2);
1756
1928
  }
1757
1929
  },
@@ -2230,6 +2402,7 @@ var codeInline = defineRule16({
2230
2402
  properties: {
2231
2403
  max: { type: "integer", minimum: 1, default: 160 },
2232
2404
  nesting: { type: "integer", minimum: 1, default: 2 },
2405
+ callbacks: { type: "boolean", default: false },
2233
2406
  allow: optionsPatternSchema,
2234
2407
  keep: optionsPatternSchema,
2235
2408
  services: { type: "array", items: { type: "string" } }
@@ -2258,7 +2431,8 @@ var codeInline = defineRule16({
2258
2431
  return;
2259
2432
  }
2260
2433
  if (uses.length === 1) {
2261
- if (init && codeInlineEffectBetween(context.sourceCode.ast, node.range[1], uses[0].identifier.range[0])) {
2434
+ const use = uses[0].identifier;
2435
+ if (init && (codeInlineEffectBetween(context.sourceCode.ast, node.range[1], use.range[0]) || codeInlineEnclosingCalls(use) + astCallDepth(init, options.callbacks ?? false) > (options.nesting ?? 2))) {
2262
2436
  return;
2263
2437
  }
2264
2438
  context.report({
@@ -2270,7 +2444,12 @@ var codeInline = defineRule16({
2270
2444
  }
2271
2445
  const arrow = init?.type === "ArrowFunctionExpression" && init.body.type !== "BlockStatement" && !astSome(init.body, astIsInlineFunction);
2272
2446
  const call = init?.type === "CallExpression" && !astSome(init.arguments, astIsInlineFunction) && astCallDepth(init) <= 2;
2273
- if (!optionsPatternsTest(kept, id.name) && (arrow || call)) {
2447
+ const text = context.sourceCode.text;
2448
+ const fits = init !== null && uses.every(({ identifier }) => {
2449
+ const line = text.slice(sourceLineStart(text, identifier.range[0]), sourceLineEnd(text, identifier.range[0]));
2450
+ return line.length - id.name.length + context.sourceCode.getText(init).length <= (options.max ?? 160) && codeInlineEnclosingCalls(identifier) + astCallDepth(init, options.callbacks ?? false) <= (options.nesting ?? 2);
2451
+ });
2452
+ if (!optionsPatternsTest(kept, id.name) && (arrow || call) && fits) {
2274
2453
  context.report({
2275
2454
  node: id,
2276
2455
  messageId: "trivial",
@@ -2298,7 +2477,7 @@ var codeInline = defineRule16({
2298
2477
  deferred ||= current.type === "FunctionDeclaration" || astIsInlineFunction(current);
2299
2478
  }
2300
2479
  const wrapped = codeInlineWrapped.has(init.type) && use.parent.type === "MemberExpression" && use.parent.object === use;
2301
- 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])) {
2480
+ if (service || wrapped || deferred || codeInlineEnclosingCalls(use) + astCallDepth(init, options.callbacks ?? false) > (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])) {
2302
2481
  return;
2303
2482
  }
2304
2483
  context.report({
@@ -2332,9 +2511,85 @@ var codeInline = defineRule16({
2332
2511
  }
2333
2512
  });
2334
2513
 
2335
- // src/rules/code/code-justified-call.ts
2514
+ // src/rules/code/code-inside.ts
2336
2515
  import { defineRule as defineRule17 } from "@oxlint/plugins";
2337
- var codeJustifiedCall = defineRule17({
2516
+ function codeInsideOwners(node) {
2517
+ const parent = node.parent;
2518
+ if (!parent) {
2519
+ return [];
2520
+ }
2521
+ const callback = node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
2522
+ const owner = callback && astIsCall(parent) && parent.arguments.some((item) => item === node) ? astDottedName(parent.callee) : undefined;
2523
+ return [...owner === undefined ? [] : [owner], ...codeInsideOwners(parent)];
2524
+ }
2525
+ var codeInside = defineRule17({
2526
+ meta: {
2527
+ type: "problem",
2528
+ schema: [
2529
+ {
2530
+ type: "object",
2531
+ additionalProperties: false,
2532
+ properties: {
2533
+ inside: {
2534
+ type: "array",
2535
+ items: {
2536
+ type: "object",
2537
+ additionalProperties: false,
2538
+ properties: {
2539
+ callee: optionsPatternSchema,
2540
+ calls: { type: "object", additionalProperties: { type: "string" } }
2541
+ },
2542
+ required: ["callee", "calls"]
2543
+ }
2544
+ }
2545
+ },
2546
+ required: ["inside"]
2547
+ }
2548
+ ],
2549
+ messages: {
2550
+ inside: "Do not call '{{name}}' inside {{owner}}.{{message}}"
2551
+ }
2552
+ },
2553
+ createOnce(context) {
2554
+ let entries = [];
2555
+ return {
2556
+ before() {
2557
+ entries = optionsFirst(context, { inside: [] }).inside.map((entry) => ({
2558
+ callees: optionsWildcards(entry.callee),
2559
+ calls: Object.entries(entry.calls).map(([name, message]) => [optionsWildcards(name)[0], message && ` ${message}`])
2560
+ }));
2561
+ if (entries.length === 0) {
2562
+ return false;
2563
+ }
2564
+ },
2565
+ CallExpression(node) {
2566
+ const name = astDottedName(node.callee);
2567
+ if (name === undefined) {
2568
+ return;
2569
+ }
2570
+ const banned = entries.flatMap(({ callees, calls }) => calls.flatMap(([pattern, message]) => pattern.test(name) ? [{ callees, message }] : []));
2571
+ if (banned.length === 0) {
2572
+ return;
2573
+ }
2574
+ const owners = codeInsideOwners(node);
2575
+ for (const { callees, message } of banned) {
2576
+ const owner = owners.find((item) => optionsPatternsTest(callees, item));
2577
+ if (owner !== undefined) {
2578
+ context.report({
2579
+ node: node.callee,
2580
+ messageId: "inside",
2581
+ data: { name, owner, message }
2582
+ });
2583
+ }
2584
+ }
2585
+ }
2586
+ };
2587
+ }
2588
+ });
2589
+
2590
+ // src/rules/code/code-justified-call.ts
2591
+ import { defineRule as defineRule18 } from "@oxlint/plugins";
2592
+ var codeJustifiedCall = defineRule18({
2338
2593
  meta: {
2339
2594
  type: "problem",
2340
2595
  schema: [
@@ -2388,8 +2643,8 @@ var codeJustifiedCall = defineRule17({
2388
2643
  });
2389
2644
 
2390
2645
  // src/rules/code/code-literal-in.ts
2391
- import { defineRule as defineRule18 } from "@oxlint/plugins";
2392
- var codeLiteralIn = defineRule18({
2646
+ import { defineRule as defineRule19 } from "@oxlint/plugins";
2647
+ var codeLiteralIn = defineRule19({
2393
2648
  meta: {
2394
2649
  type: "suggestion",
2395
2650
  schema: [
@@ -2430,7 +2685,7 @@ var codeLiteralIn = defineRule18({
2430
2685
  });
2431
2686
 
2432
2687
  // src/rules/code/code-mutable-state.ts
2433
- import { defineRule as defineRule19 } from "@oxlint/plugins";
2688
+ import { defineRule as defineRule20 } from "@oxlint/plugins";
2434
2689
  var functionScopes = new Set([
2435
2690
  "FunctionDeclaration",
2436
2691
  "FunctionExpression",
@@ -2447,7 +2702,7 @@ function hasAncestor(node, test) {
2447
2702
  }
2448
2703
  return false;
2449
2704
  }
2450
- var codeMutableState = defineRule19({
2705
+ var codeMutableState = defineRule20({
2451
2706
  meta: {
2452
2707
  type: "problem",
2453
2708
  schema: [
@@ -2505,7 +2760,7 @@ var codeMutableState = defineRule19({
2505
2760
  });
2506
2761
 
2507
2762
  // src/rules/code/code-object-params.ts
2508
- import { defineRule as defineRule20 } from "@oxlint/plugins";
2763
+ import { defineRule as defineRule21 } from "@oxlint/plugins";
2509
2764
  function isObjectParam(node) {
2510
2765
  if (node.type === "ObjectPattern") {
2511
2766
  return true;
@@ -2519,7 +2774,7 @@ function isObjectParam(node) {
2519
2774
  const annotation = node.typeAnnotation?.typeAnnotation;
2520
2775
  return !annotation || annotation.type === "TSTypeReference" || annotation.type === "TSTypeLiteral" || annotation.type === "TSIntersectionType";
2521
2776
  }
2522
- var codeObjectParams = defineRule20({
2777
+ var codeObjectParams = defineRule21({
2523
2778
  meta: {
2524
2779
  type: "problem",
2525
2780
  schema: [
@@ -2568,8 +2823,8 @@ var codeObjectParams = defineRule20({
2568
2823
  });
2569
2824
 
2570
2825
  // src/rules/code/code-paired-call.ts
2571
- import { defineRule as defineRule21 } from "@oxlint/plugins";
2572
- var codePairedCall = defineRule21({
2826
+ import { defineRule as defineRule22 } from "@oxlint/plugins";
2827
+ var codePairedCall = defineRule22({
2573
2828
  meta: {
2574
2829
  type: "problem",
2575
2830
  schema: [
@@ -2636,7 +2891,7 @@ var codePairedCall = defineRule21({
2636
2891
  });
2637
2892
 
2638
2893
  // src/rules/code/code-promise-mutation.ts
2639
- import { defineRule as defineRule22 } from "@oxlint/plugins";
2894
+ import { defineRule as defineRule23 } from "@oxlint/plugins";
2640
2895
  function mutationBinding(node) {
2641
2896
  if (node.type === "Identifier") {
2642
2897
  return node;
@@ -2665,7 +2920,7 @@ function mutationBindings(node) {
2665
2920
  const binding = mutationBinding(node);
2666
2921
  return binding ? [binding] : [];
2667
2922
  }
2668
- var codePromiseMutation = defineRule22({
2923
+ var codePromiseMutation = defineRule23({
2669
2924
  meta: {
2670
2925
  type: "problem",
2671
2926
  schema: [
@@ -2758,8 +3013,8 @@ var codePromiseMutation = defineRule22({
2758
3013
  var codeSwitch = astSelectorRule("SwitchStatement", "Avoid switch statements.");
2759
3014
 
2760
3015
  // src/rules/code/code-traced.ts
2761
- import { defineRule as defineRule23 } from "@oxlint/plugins";
2762
- var codeTraced = defineRule23({
3016
+ import { defineRule as defineRule24 } from "@oxlint/plugins";
3017
+ var codeTraced = defineRule24({
2763
3018
  meta: {
2764
3019
  type: "problem",
2765
3020
  schema: [
@@ -2846,7 +3101,7 @@ var codeTraced = defineRule23({
2846
3101
  });
2847
3102
 
2848
3103
  // src/rules/code/code-trivial-functions.ts
2849
- import { defineRule as defineRule24 } from "@oxlint/plugins";
3104
+ import { defineRule as defineRule25 } from "@oxlint/plugins";
2850
3105
  var codeTrivialFunctionsBanned = [
2851
3106
  "isRecord",
2852
3107
  "isPlainObject",
@@ -3004,7 +3259,7 @@ function codeTrivialFunctionsDefaults(program) {
3004
3259
  }
3005
3260
  return starts;
3006
3261
  }
3007
- var codeTrivialFunctions = defineRule24({
3262
+ var codeTrivialFunctions = defineRule25({
3008
3263
  meta: {
3009
3264
  type: "problem",
3010
3265
  schema: [
@@ -3146,8 +3401,8 @@ var codeTrivialFunctions = defineRule24({
3146
3401
  });
3147
3402
 
3148
3403
  // src/rules/code/code-yield-argument.ts
3149
- import { defineRule as defineRule25 } from "@oxlint/plugins";
3150
- var codeYieldArgument = defineRule25({
3404
+ import { defineRule as defineRule26 } from "@oxlint/plugins";
3405
+ var codeYieldArgument = defineRule26({
3151
3406
  meta: {
3152
3407
  type: "suggestion",
3153
3408
  messages: {
@@ -3169,7 +3424,7 @@ var codeYieldArgument = defineRule25({
3169
3424
  });
3170
3425
 
3171
3426
  // src/rules/comments/comments-code.ts
3172
- import { defineRule as defineRule26 } from "@oxlint/plugins";
3427
+ import { defineRule as defineRule27 } from "@oxlint/plugins";
3173
3428
  var commentsLineGap = /^[\t \r\n\u2028\u2029]*$/;
3174
3429
  var commentsLineTerminator = /\r\n|[\n\r\u2028\u2029]/;
3175
3430
  var commentsUnicodeTerminator = /[\u2028\u2029]/;
@@ -3220,7 +3475,7 @@ function commentsFollowsOpeningBrace(text, offset) {
3220
3475
  }
3221
3476
  return false;
3222
3477
  }
3223
- var commentsCode = defineRule26({
3478
+ var commentsCode = defineRule27({
3224
3479
  meta: {
3225
3480
  type: "suggestion",
3226
3481
  fixable: "code",
@@ -3232,7 +3487,8 @@ var commentsCode = defineRule26({
3232
3487
  why: { anyOf: [{ type: "boolean" }, { type: "string", minLength: 1 }], default: true },
3233
3488
  allow: { type: "array", items: { type: "string" } },
3234
3489
  jsdoc: { type: "boolean", default: false },
3235
- members: { type: "string", enum: ["off", "also", "only"] }
3490
+ members: { type: "string", enum: ["off", "also", "only"] },
3491
+ steps: { type: "boolean", default: false }
3236
3492
  }
3237
3493
  }
3238
3494
  ],
@@ -3247,9 +3503,31 @@ var commentsCode = defineRule26({
3247
3503
  let allowJsdoc = false;
3248
3504
  let allowed = [];
3249
3505
  let members = "off";
3506
+ let steps = false;
3507
+ let functionDepth = 0;
3250
3508
  const memberComments = new Set;
3509
+ const stepStarts = new Set;
3510
+ function enterFunction() {
3511
+ functionDepth++;
3512
+ }
3513
+ function exitFunction() {
3514
+ functionDepth--;
3515
+ }
3516
+ function addSteps(nodes, inFunction) {
3517
+ if (steps && inFunction) {
3518
+ for (const node of nodes) {
3519
+ stepStarts.add(node.range[0]);
3520
+ }
3521
+ }
3522
+ }
3523
+ function isStep(comment) {
3524
+ const source = context.sourceCode;
3525
+ const before = source.getTokenBefore(comment, { includeComments: true });
3526
+ const after = source.getTokenAfter(comment, { includeComments: true });
3527
+ return comment.type === "Line" && before?.loc.end.line !== comment.loc.start.line && !(before && (before.type === "Line" || before.type === "Block") && before.loc.end.line === comment.loc.start.line - 1) && after !== null && after.type !== "Line" && after.type !== "Block" && after.loc.start.line === comment.loc.end.line + 1 && stepStarts.has(after.range[0]);
3528
+ }
3251
3529
  function keepComment(comment, value, isWhy) {
3252
- return allowWhy && isWhy || sourceIsDirective(value) || allowJsdoc && comment.type === "Block" && context.sourceCode.text.startsWith("/**", comment.range[0]) || allowed.some((pattern) => pattern.test(value.trim()));
3530
+ return allowWhy && isWhy || sourceIsDirective(value) || allowJsdoc && comment.type === "Block" && context.sourceCode.text.startsWith("/**", comment.range[0]) || allowed.some((pattern) => pattern.test(value.trim())) || steps && isStep(comment);
3253
3531
  }
3254
3532
  function checkMember(node) {
3255
3533
  const parent = node.parent;
@@ -3273,7 +3551,10 @@ var commentsCode = defineRule26({
3273
3551
  allowJsdoc = options.jsdoc ?? false;
3274
3552
  allowed = (options.allow ?? []).map((pattern) => new RegExp(pattern));
3275
3553
  members = options.members ?? "off";
3554
+ steps = options.steps ?? false;
3555
+ functionDepth = 0;
3276
3556
  memberComments.clear();
3557
+ stepStarts.clear();
3277
3558
  },
3278
3559
  TSPropertySignature: checkMember,
3279
3560
  TSMethodSignature: checkMember,
@@ -3282,6 +3563,17 @@ var commentsCode = defineRule26({
3282
3563
  TSConstructSignatureDeclaration: checkMember,
3283
3564
  Property: checkMember,
3284
3565
  SpreadElement: checkMember,
3566
+ FunctionDeclaration: enterFunction,
3567
+ "FunctionDeclaration:exit": exitFunction,
3568
+ FunctionExpression: enterFunction,
3569
+ "FunctionExpression:exit": exitFunction,
3570
+ ArrowFunctionExpression: enterFunction,
3571
+ "ArrowFunctionExpression:exit": exitFunction,
3572
+ BlockStatement: (node) => addSteps(node.body, functionDepth > 0),
3573
+ SwitchCase: (node) => addSteps(node.consequent, functionDepth > 0),
3574
+ IfStatement: (node) => addSteps([node.consequent, ...node.alternate ? [node.alternate] : []], functionDepth > 0),
3575
+ "ForStatement, ForInStatement, ForOfStatement, WhileStatement, DoWhileStatement, LabeledStatement": (node) => addSteps([node.body], functionDepth > 0),
3576
+ ObjectExpression: (node) => addSteps(node.properties, true),
3285
3577
  "Program:exit"(program) {
3286
3578
  const text = context.sourceCode.text;
3287
3579
  const found = [];
@@ -3386,9 +3678,9 @@ var commentsCode = defineRule26({
3386
3678
  });
3387
3679
 
3388
3680
  // src/rules/comments/comments-directives.ts
3389
- import { defineRule as defineRule27 } from "@oxlint/plugins";
3681
+ import { defineRule as defineRule28 } from "@oxlint/plugins";
3390
3682
  var directive = /^\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\b(.*)$/s;
3391
- var commentsDirectives = defineRule27({
3683
+ var commentsDirectives = defineRule28({
3392
3684
  meta: {
3393
3685
  type: "problem",
3394
3686
  schema: [],
@@ -3416,9 +3708,9 @@ var commentsDirectives = defineRule27({
3416
3708
  });
3417
3709
 
3418
3710
  // src/rules/copy/copy-dashes.ts
3419
- import { defineRule as defineRule28 } from "@oxlint/plugins";
3711
+ import { defineRule as defineRule29 } from "@oxlint/plugins";
3420
3712
  var copyDashesMessage = "Use a comma, colon, period or parentheses instead.";
3421
- var copyDashes = defineRule28({
3713
+ var copyDashes = defineRule29({
3422
3714
  meta: {
3423
3715
  type: "suggestion",
3424
3716
  schema: [
@@ -3474,7 +3766,7 @@ var copyDashes = defineRule28({
3474
3766
  });
3475
3767
 
3476
3768
  // src/rules/effect/effect-layer-file.ts
3477
- import { defineRule as defineRule29 } from "@oxlint/plugins";
3769
+ import { defineRule as defineRule30 } from "@oxlint/plugins";
3478
3770
 
3479
3771
  // src/utils/effect.utils.ts
3480
3772
  var effectBuilders = ["Effect.gen", "Effect.fn", "Effect.fnUntraced"];
@@ -3516,7 +3808,7 @@ var effectLayerFileDefaultConstructors = [
3516
3808
  "Layer.sync",
3517
3809
  "Layer.unwrap"
3518
3810
  ];
3519
- var effectLayerFile = defineRule29({
3811
+ var effectLayerFile = defineRule30({
3520
3812
  meta: {
3521
3813
  type: "problem",
3522
3814
  schema: [
@@ -3527,6 +3819,7 @@ var effectLayerFile = defineRule29({
3527
3819
  layer: { type: "string" },
3528
3820
  make: { type: "string" },
3529
3821
  prefix: { type: "string" },
3822
+ suffix: { type: "string" },
3530
3823
  builders: { type: "array", items: { type: "string" } },
3531
3824
  layers: optionsPatternSchema,
3532
3825
  services: optionsPatternSchema,
@@ -3539,8 +3832,8 @@ var effectLayerFile = defineRule29({
3539
3832
  notLayer: "`{{layer}}` is built with Layer: a Layer call, a Layer pipe, or a function returning one.",
3540
3833
  builderName: "The Effect that builds the service is named `{{make}}`, one per layer file.",
3541
3834
  moduleMethod: "Only `{{make}}` is an Effect at module level; move this into `{{make}}`, or into a utils file if two files use it.",
3542
- moduleHelper: "A layer file holds only `{{make}}`, {{prefix}}<Role> layers and `{{layer}}`; move this into `{{make}}`, or into a utils file if two files use it.",
3543
- localLayer: "A Layer other than `{{layer}}` is named {{prefix}}<Role> and is not exported.",
3835
+ moduleHelper: "A layer file holds only `{{make}}`, {{prefix}}<Role>{{suffix}} layers and `{{layer}}`; move this into `{{make}}`, or into a utils file if two files use it.",
3836
+ localLayer: "A Layer other than `{{layer}}` is named {{prefix}}<Role>{{suffix}} and is not exported.",
3544
3837
  makeReturn: "`{{make}}` returns the service through its tag (Tag.of({ … })), a Context, or a Layer.",
3545
3838
  inlineBuilder: "Build this layer from `{{make}}`, not an inline Effect: Layer.effect(Tag, {{make}}). One built layer per file."
3546
3839
  }
@@ -3549,6 +3842,7 @@ var effectLayerFile = defineRule29({
3549
3842
  let layer = "layer";
3550
3843
  let make = "make";
3551
3844
  let prefix = "layer";
3845
+ let suffix = "";
3552
3846
  let builders = new Set(effectBuilders);
3553
3847
  let layers = [];
3554
3848
  let services = [];
@@ -3565,14 +3859,15 @@ var effectLayerFile = defineRule29({
3565
3859
  const options = optionsFirst(context, {});
3566
3860
  layer = options.layer ?? "layer";
3567
3861
  make = options.make ?? "make";
3568
- prefix = options.prefix ?? "layer";
3862
+ suffix = options.suffix ?? "";
3863
+ prefix = options.prefix ?? (suffix ? "" : "layer");
3569
3864
  builders = new Set(options.builders ?? effectBuilders);
3570
3865
  constructors = new Set(options.constructors ?? effectLayerFileDefaultConstructors);
3571
- layers = optionsPatterns(options.layers ?? effectLayerFileDefaultLayers);
3866
+ layers = optionsPatterns(options.layers ?? [...effectLayerFileDefaultLayers, ...suffix ? [`(?:^|\\.)\\w+${optionsEscape(suffix)}$`] : []]);
3572
3867
  services = [...optionsPatterns(options.services ?? effectLayerFileDefaultServices), ...layers];
3573
3868
  },
3574
3869
  Program(program) {
3575
- const data = { layer, make, prefix };
3870
+ const data = { layer, make, prefix, suffix };
3576
3871
  const declarators = program.body.flatMap((statement) => {
3577
3872
  const declaration = astDeclaration(statement);
3578
3873
  const exported = statement.type === "ExportNamedDeclaration";
@@ -3632,7 +3927,8 @@ var effectLayerFile = defineRule29({
3632
3927
  data
3633
3928
  });
3634
3929
  }
3635
- if (isLayer && name !== layer && (exported || !name.startsWith(prefix) || !/^[A-Z]\w*$/.test(name.slice(prefix.length)))) {
3930
+ const role = name.endsWith(suffix) ? name.slice(prefix.length, name.length - suffix.length) : "";
3931
+ if (isLayer && name !== layer && (exported || !name.startsWith(prefix) || !/^[A-Z]\w*$/.test(role))) {
3636
3932
  context.report({
3637
3933
  node,
3638
3934
  messageId: "localLayer",
@@ -3669,17 +3965,147 @@ var effectLayerFile = defineRule29({
3669
3965
  context.report({
3670
3966
  node: argument,
3671
3967
  messageId: "inlineBuilder",
3672
- data: { layer, make, prefix }
3968
+ data: { layer, make, prefix, suffix }
3969
+ });
3970
+ }
3971
+ }
3972
+ }
3973
+ };
3974
+ }
3975
+ });
3976
+
3977
+ // src/rules/effect/effect-layer-keys.ts
3978
+ import { defineRule as defineRule31 } from "@oxlint/plugins";
3979
+ import { readFileSync } from "node:fs";
3980
+ var effectLayerKeysPairs = new Map([["{", "}"], ["(", ")"], ["[", "]"], ["<", ">"]]);
3981
+ function effectLayerKeysDeclared(text, declaration) {
3982
+ const code = text.replaceAll(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"/g, " ");
3983
+ const start = code.indexOf(`${declaration}<`);
3984
+ const open = start === -1 ? -1 : code.indexOf("{", start);
3985
+ if (open === -1) {
3986
+ return null;
3987
+ }
3988
+ const paths = [];
3989
+ const frames = [{ open: "{", members: 0 }];
3990
+ const key = /\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\??\s*(?=[:(<])/y;
3991
+ let fresh = true;
3992
+ for (let index = open + 1;index < code.length && frames.length > 0; index += 1) {
3993
+ const char = code[index];
3994
+ const members = frames.every((frame) => frame.open === "{");
3995
+ key.lastIndex = index;
3996
+ const match = fresh && members ? key.exec(code) : null;
3997
+ if (match) {
3998
+ const rest = code.slice(key.lastIndex).replace(/^:\s*/, "");
3999
+ const path = [...frames.flatMap((frame) => frame.key ?? []), match[1]].join(".");
4000
+ const parent = frames.at(-1);
4001
+ if (parent) {
4002
+ parent.members += 1;
4003
+ }
4004
+ if (rest.startsWith("{")) {
4005
+ frames.push({
4006
+ open: "{",
4007
+ key: match[1],
4008
+ members: 0
4009
+ });
4010
+ index = code.length - rest.length;
4011
+ } else {
4012
+ paths.push(path);
4013
+ fresh = false;
4014
+ index = key.lastIndex - 1;
4015
+ }
4016
+ continue;
4017
+ }
4018
+ if (effectLayerKeysPairs.has(char)) {
4019
+ frames.push({ open: char, members: 0 });
4020
+ } else if (char === effectLayerKeysPairs.get(frames.at(-1)?.open ?? "") && !(char === ">" && code[index - 1] === "=")) {
4021
+ const closed = frames.pop();
4022
+ if (closed?.key !== undefined && closed.members === 0) {
4023
+ paths.push([...frames.flatMap((frame) => frame.key ?? []), closed.key].join("."));
4024
+ }
4025
+ }
4026
+ fresh = members && (char === "{" || char === ";" || char === "," || char === "}" || fresh && /\s/.test(char));
4027
+ }
4028
+ return paths;
4029
+ }
4030
+ function effectLayerKeysBound(object, path) {
4031
+ return object.properties.flatMap((property) => {
4032
+ const key = declarationsPropertyName(property);
4033
+ if (property.type !== "Property" || key === undefined) {
4034
+ return [];
4035
+ }
4036
+ return property.value.type === "ObjectExpression" ? effectLayerKeysBound(property.value, [...path, key]) : [[[...path, key].join("."), property]];
4037
+ });
4038
+ }
4039
+ var effectLayerKeys = defineRule31({
4040
+ meta: {
4041
+ type: "problem",
4042
+ schema: [
4043
+ {
4044
+ type: "object",
4045
+ additionalProperties: false,
4046
+ properties: {
4047
+ service: { type: "string", minLength: 1 },
4048
+ callees: { type: "array", items: { type: "string" }, minItems: 1 },
4049
+ declaration: { type: "string", minLength: 1 }
4050
+ },
4051
+ required: ["service"]
4052
+ }
4053
+ ],
4054
+ messages: {
4055
+ extra: "'{{path}}' is not a method of the service declared in {{file}}.",
4056
+ missing: "The service in {{file}} declares {{paths}}, which this layer does not bind."
4057
+ }
4058
+ },
4059
+ createOnce(context) {
4060
+ let declared = null;
4061
+ let callees = [];
4062
+ let file = "";
4063
+ return {
4064
+ before() {
4065
+ const options = optionsFirst(context, { service: "" });
4066
+ file = namingExpand(options.service, namingCaptures(context.filename) ?? {}, false);
4067
+ callees = optionsWildcards(options.callees ?? ["*.of", "*.bind"]);
4068
+ try {
4069
+ declared = effectLayerKeysDeclared(readFileSync(file, "utf8"), options.declaration ?? "Context.Service");
4070
+ } catch {
4071
+ declared = null;
4072
+ }
4073
+ if (!declared) {
4074
+ return false;
4075
+ }
4076
+ },
4077
+ CallExpression(node) {
4078
+ const first = node.arguments.at(0);
4079
+ if (!declared || first?.type !== "ObjectExpression" || !optionsPatternsTest(callees, astDottedName(node.callee) ?? "")) {
4080
+ return;
4081
+ }
4082
+ const bound = effectLayerKeysBound(first, []);
4083
+ const names = new Set(bound.map(([path]) => path));
4084
+ const short = file.split("/").slice(-2).join("/");
4085
+ const missing = declared.filter((path) => !names.has(path) && !bound.some(([name]) => name.startsWith(`${path}.`)));
4086
+ for (const [path, property] of bound) {
4087
+ if (!declared.some((name) => name === path || path.startsWith(`${name}.`))) {
4088
+ context.report({
4089
+ node: property,
4090
+ messageId: "extra",
4091
+ data: { path, file: short }
3673
4092
  });
3674
4093
  }
3675
4094
  }
4095
+ if (missing.length > 0) {
4096
+ context.report({
4097
+ node: first,
4098
+ messageId: "missing",
4099
+ data: { paths: missing.map((path) => `'${path}'`).join(", "), file: short }
4100
+ });
4101
+ }
3676
4102
  }
3677
4103
  };
3678
4104
  }
3679
4105
  });
3680
4106
 
3681
4107
  // src/rules/effect/effect-layers.ts
3682
- import { defineRule as defineRule30 } from "@oxlint/plugins";
4108
+ import { defineRule as defineRule32 } from "@oxlint/plugins";
3683
4109
  var effectLayersDefaultMerge = ["Layer.mergeAll"];
3684
4110
  var effectLayersDefaultProvide = ["Effect.provide", "Layer.provide", "Layer.provideMerge"];
3685
4111
  var effectLayersDefaultMin = 2;
@@ -3691,7 +4117,7 @@ var effectLayersGroupSchema = {
3691
4117
  min: { type: "integer", minimum: 1 }
3692
4118
  }
3693
4119
  };
3694
- var effectLayers = defineRule30({
4120
+ var effectLayers = defineRule32({
3695
4121
  meta: {
3696
4122
  type: "layout",
3697
4123
  fixable: "whitespace",
@@ -3773,11 +4199,11 @@ var effectLayers = defineRule30({
3773
4199
  });
3774
4200
 
3775
4201
  // src/rules/effect/effect-merge-pipe.ts
3776
- import { defineRule as defineRule31 } from "@oxlint/plugins";
4202
+ import { defineRule as defineRule33 } from "@oxlint/plugins";
3777
4203
  var effectMergePipeDefaultMerge = ["Layer.mergeAll"];
3778
4204
  var effectMergePipeDefaultPipe = ["pipe", "Function.pipe"];
3779
4205
  var effectMergePipeDefaultMethods = ["pipe"];
3780
- var effectMergePipe = defineRule31({
4206
+ var effectMergePipe = defineRule33({
3781
4207
  meta: {
3782
4208
  type: "layout",
3783
4209
  schema: [
@@ -3822,7 +4248,7 @@ var effectMergePipe = defineRule31({
3822
4248
  });
3823
4249
 
3824
4250
  // src/rules/effect/effect-schema-first.ts
3825
- import { defineRule as defineRule32 } from "@oxlint/plugins";
4251
+ import { defineRule as defineRule34 } from "@oxlint/plugins";
3826
4252
  var effectSchemaFirstDefaultRuntime = [
3827
4253
  "Effect",
3828
4254
  "Layer",
@@ -3884,7 +4310,7 @@ function effectSchemaFirstReference(node) {
3884
4310
  }
3885
4311
  return node.type === "TSInterfaceHeritage" ? astDottedName(node.expression) : undefined;
3886
4312
  }
3887
- var effectSchemaFirst = defineRule32({
4313
+ var effectSchemaFirst = defineRule34({
3888
4314
  meta: {
3889
4315
  type: "suggestion",
3890
4316
  schema: [
@@ -3894,6 +4320,7 @@ var effectSchemaFirst = defineRule32({
3894
4320
  properties: {
3895
4321
  member: { type: "string" },
3896
4322
  keep: optionsPatternSchema,
4323
+ allow: optionsPatternSchema,
3897
4324
  runtime: { type: "array", items: { type: "string" } },
3898
4325
  rows: {
3899
4326
  type: "object",
@@ -3923,6 +4350,7 @@ var effectSchemaFirst = defineRule32({
3923
4350
  createOnce(context) {
3924
4351
  let member = "Type";
3925
4352
  let keep = [];
4353
+ let allow = [];
3926
4354
  let runtime = new Set(effectSchemaFirstDefaultRuntime);
3927
4355
  let rows;
3928
4356
  let suffix = "Record";
@@ -3987,6 +4415,7 @@ var effectSchemaFirst = defineRule32({
3987
4415
  const options = optionsFirst(context, {});
3988
4416
  member = options.member ?? "Type";
3989
4417
  keep = optionsPatterns(options.keep);
4418
+ allow = optionsPatterns(options.allow);
3990
4419
  runtime = new Set(options.runtime ?? effectSchemaFirstDefaultRuntime);
3991
4420
  rows = options.rows ? new Set(options.rows.types ?? ["Kysely.Selectable"]) : undefined;
3992
4421
  suffix = options.rows?.suffix ?? "Record";
@@ -4006,7 +4435,7 @@ var effectSchemaFirst = defineRule32({
4006
4435
  const typed = new Set(exported.flatMap((node) => node.type === "TSTypeAliasDeclaration" && derives(node) ? [node.id.name] : []));
4007
4436
  for (const node of exported.flatMap((declaration) => declaration.type === "VariableDeclaration" ? declaration.declarations : [])) {
4008
4437
  const isSchema = node.init?.type === "CallExpression" || node.init?.type === "MemberExpression";
4009
- if (isSchema && node.id.type === "Identifier" && !typed.has(node.id.name)) {
4438
+ if (isSchema && node.id.type === "Identifier" && !typed.has(node.id.name) && !optionsPatternsTest(allow, node.id.name)) {
4010
4439
  context.report({
4011
4440
  node,
4012
4441
  messageId: "schemaType",
@@ -4056,8 +4485,104 @@ var effectSchemaFirst = defineRule32({
4056
4485
  }
4057
4486
  });
4058
4487
 
4488
+ // src/rules/effect/effect-service-params.ts
4489
+ import { defineRule as defineRule35 } from "@oxlint/plugins";
4490
+ function effectServiceParamsKey(member) {
4491
+ if (member.type !== "TSPropertySignature" && member.type !== "TSMethodSignature" || member.computed) {
4492
+ return;
4493
+ }
4494
+ return member.key.type === "Identifier" ? member.key.name : undefined;
4495
+ }
4496
+ function effectServiceParamsTypeName(name) {
4497
+ if (name.type === "TSQualifiedName") {
4498
+ return `${effectServiceParamsTypeName(name.left)}.${name.right.name}`;
4499
+ }
4500
+ return name.type === "Identifier" ? name.name : "this";
4501
+ }
4502
+ var effectServiceParams = defineRule35({
4503
+ meta: {
4504
+ type: "problem",
4505
+ schema: [
4506
+ {
4507
+ type: "object",
4508
+ additionalProperties: false,
4509
+ properties: {
4510
+ name: optionsPatternSchema,
4511
+ allow: optionsPatternSchema,
4512
+ declaration: { type: "string", minLength: 1 }
4513
+ }
4514
+ }
4515
+ ],
4516
+ messages: {
4517
+ name: "'{{path}}' takes '{{type}}'; a service method takes a params type that matches {{pattern}}.",
4518
+ inline: "'{{path}}' takes an unnamed type; a service method takes a params type that matches {{pattern}}."
4519
+ }
4520
+ },
4521
+ createOnce(context) {
4522
+ let options = {};
4523
+ let captures = {};
4524
+ let allowed = [];
4525
+ function checkParams(params, keys) {
4526
+ const first = params.at(0);
4527
+ const annotation = first && "typeAnnotation" in first ? first.typeAnnotation?.typeAnnotation : undefined;
4528
+ if (!first || !annotation) {
4529
+ return;
4530
+ }
4531
+ const values = { ...captures, path: keys.join("-"), key: keys.at(-1) ?? "" };
4532
+ const sources = [options.name ?? "^{Folder}\\w*Params$"].flat().map((source) => namingExpand(source, values, true, optionsEscape));
4533
+ const data = { path: keys.join("."), pattern: optionsPatternLabel(sources) };
4534
+ if (annotation.type !== "TSTypeReference") {
4535
+ context.report({
4536
+ node: first,
4537
+ messageId: "inline",
4538
+ data
4539
+ });
4540
+ return;
4541
+ }
4542
+ const type = effectServiceParamsTypeName(annotation.typeName);
4543
+ const last = type.split(".").at(-1) ?? type;
4544
+ if (!optionsPatternsTest(allowed, type) && !optionsPatternsTest(optionsPatterns(sources), last)) {
4545
+ context.report({
4546
+ node: first,
4547
+ messageId: "name",
4548
+ data: { ...data, type }
4549
+ });
4550
+ }
4551
+ }
4552
+ function check(literal, path) {
4553
+ for (const member of literal.members) {
4554
+ const key = effectServiceParamsKey(member);
4555
+ const type = member.type === "TSPropertySignature" ? member.typeAnnotation?.typeAnnotation : undefined;
4556
+ if (key === undefined) {
4557
+ continue;
4558
+ }
4559
+ if (member.type === "TSMethodSignature") {
4560
+ checkParams(member.params, [...path, key]);
4561
+ } else if (type?.type === "TSFunctionType") {
4562
+ checkParams(type.params, [...path, key]);
4563
+ } else if (type?.type === "TSTypeLiteral") {
4564
+ check(type, [...path, key]);
4565
+ }
4566
+ }
4567
+ }
4568
+ return {
4569
+ before() {
4570
+ options = optionsFirst(context, {});
4571
+ captures = namingCaptures(context.filename) ?? {};
4572
+ allowed = optionsPatterns(options.allow);
4573
+ },
4574
+ TSTypeLiteral(node) {
4575
+ const call = node.parent.type === "TSTypeParameterInstantiation" ? node.parent.parent : undefined;
4576
+ if (call?.type === "CallExpression" && astDottedName(call.callee) === (options.declaration ?? "Context.Service")) {
4577
+ check(node, []);
4578
+ }
4579
+ }
4580
+ };
4581
+ }
4582
+ });
4583
+
4059
4584
  // src/rules/effect/effect-values-first.ts
4060
- import { defineRule as defineRule33 } from "@oxlint/plugins";
4585
+ import { defineRule as defineRule36 } from "@oxlint/plugins";
4061
4586
  var effectValuesFirstDefaultPinned = [
4062
4587
  "^Layer\\.",
4063
4588
  "^Context\\.",
@@ -4066,7 +4591,7 @@ var effectValuesFirstDefaultPinned = [
4066
4591
  "(?:^|\\.)layer[^.]*$",
4067
4592
  "^Schema\\.(?:asserts|decode\\w*|encode\\w*|is|isSchema|isSchemaError|resolveAnnotations)$"
4068
4593
  ];
4069
- var effectValuesFirst = defineRule33({
4594
+ var effectValuesFirst = defineRule36({
4070
4595
  meta: {
4071
4596
  type: "suggestion",
4072
4597
  schema: [
@@ -4148,12 +4673,96 @@ var effectValuesFirst = defineRule33({
4148
4673
  }
4149
4674
  });
4150
4675
 
4151
- // src/rules/files/files-components-only.ts
4152
- import { defineRule as defineRule34 } from "@oxlint/plugins";
4153
-
4154
- // src/utils/react.utils.ts
4155
- function reactReturnsOutput(program, node) {
4156
- const expressions = astReturnExpressions(node);
4676
+ // src/rules/effect/effect-yield-name.ts
4677
+ import { defineRule as defineRule37 } from "@oxlint/plugins";
4678
+ var effectYieldName = defineRule37({
4679
+ meta: {
4680
+ type: "suggestion",
4681
+ schema: [
4682
+ {
4683
+ type: "object",
4684
+ additionalProperties: false,
4685
+ properties: {
4686
+ names: { type: "object", additionalProperties: optionsPatternSchema },
4687
+ allow: optionsPatternSchema
4688
+ }
4689
+ }
4690
+ ],
4691
+ messages: {
4692
+ name: "Name this '{{expected}}', not '{{actual}}': it holds {{source}}."
4693
+ }
4694
+ },
4695
+ createOnce(context) {
4696
+ return {
4697
+ VariableDeclarator(node) {
4698
+ const { id, init } = node;
4699
+ if (id.type !== "Identifier" || init?.type !== "YieldExpression" || !init.delegate || init.argument?.type !== "MemberExpression") {
4700
+ return;
4701
+ }
4702
+ const service = astMemberName(init.argument);
4703
+ const source = astDottedName(init.argument);
4704
+ if (!service || !source || !/^[A-Z]/.test(service)) {
4705
+ return;
4706
+ }
4707
+ const options = optionsFirst(context, {});
4708
+ const named = options.names?.[source] ?? options.names?.[`*.${service}`];
4709
+ const expected = named === undefined ? [`${service.charAt(0).toLowerCase()}${service.slice(1)}`] : [named].flat();
4710
+ if (!expected.includes(id.name) && !optionsPatternsTest(optionsPatterns(options.allow), id.name)) {
4711
+ context.report({
4712
+ node: id,
4713
+ messageId: "name",
4714
+ data: { expected: expected.join(" or "), actual: id.name, source }
4715
+ });
4716
+ }
4717
+ }
4718
+ };
4719
+ }
4720
+ });
4721
+
4722
+ // src/rules/effect/effect-yields-first.ts
4723
+ import { defineRule as defineRule38 } from "@oxlint/plugins";
4724
+ function effectYieldsFirstService(statement) {
4725
+ const init = statement.type === "VariableDeclaration" && statement.declarations.length === 1 ? statement.declarations[0].init : null;
4726
+ const argument = init?.type === "YieldExpression" && init.delegate ? init.argument : null;
4727
+ return argument?.type === "MemberExpression" && /^[A-Z]/.test(astMemberName(argument) ?? "") ? astDottedName(argument) : undefined;
4728
+ }
4729
+ var effectYieldsFirst = defineRule38({
4730
+ meta: {
4731
+ type: "suggestion",
4732
+ messages: {
4733
+ first: "Yield {{source}} with the other dependencies, at the top of the function."
4734
+ }
4735
+ },
4736
+ createOnce(context) {
4737
+ return {
4738
+ FunctionExpression(node) {
4739
+ if (!node.generator || !node.body) {
4740
+ return;
4741
+ }
4742
+ let started = false;
4743
+ for (const statement of node.body.body) {
4744
+ const source = effectYieldsFirstService(statement);
4745
+ if (source === undefined) {
4746
+ started = true;
4747
+ } else if (started) {
4748
+ context.report({
4749
+ node: statement,
4750
+ messageId: "first",
4751
+ data: { source }
4752
+ });
4753
+ }
4754
+ }
4755
+ }
4756
+ };
4757
+ }
4758
+ });
4759
+
4760
+ // src/rules/files/files-components-only.ts
4761
+ import { defineRule as defineRule39 } from "@oxlint/plugins";
4762
+
4763
+ // src/utils/react.utils.ts
4764
+ function reactReturnsOutput(program, node) {
4765
+ const expressions = astReturnExpressions(node);
4157
4766
  if (expressions.some((expression) => astContainsJsx(expression) || expression.type === "Literal" && expression.value === null)) {
4158
4767
  return true;
4159
4768
  }
@@ -4221,7 +4830,7 @@ function reactIsComponent(program, candidate) {
4221
4830
  function componentName(basename2) {
4222
4831
  return basename2.replace(/\.(tsx?|jsx?)$/, "").replaceAll(/(?:^[-.]*|[-.]+)(.?)/g, (_, char) => char.toUpperCase());
4223
4832
  }
4224
- var filesComponentsOnly = defineRule34({
4833
+ var filesComponentsOnly = defineRule39({
4225
4834
  meta: {
4226
4835
  type: "problem",
4227
4836
  schema: [
@@ -4309,7 +4918,7 @@ var filesComponentsOnly = defineRule34({
4309
4918
  });
4310
4919
 
4311
4920
  // src/rules/files/files-constants-only.ts
4312
- import { defineRule as defineRule35 } from "@oxlint/plugins";
4921
+ import { defineRule as defineRule40 } from "@oxlint/plugins";
4313
4922
  function unwrap(expression) {
4314
4923
  if (expression.type === "ParenthesizedExpression" || expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSTypeAssertion" || expression.type === "TSNonNullExpression" || expression.type === "TSInstantiationExpression") {
4315
4924
  return unwrap(expression.expression);
@@ -4364,7 +4973,7 @@ function collectBindings(program) {
4364
4973
  }
4365
4974
  return bindings;
4366
4975
  }
4367
- var filesConstantsOnly = defineRule35({
4976
+ var filesConstantsOnly = defineRule40({
4368
4977
  meta: {
4369
4978
  type: "problem",
4370
4979
  schema: [
@@ -4463,7 +5072,7 @@ var filesConstantsOnly = defineRule35({
4463
5072
  });
4464
5073
 
4465
5074
  // src/rules/files/files-domain-constants.ts
4466
- import { defineRule as defineRule36 } from "@oxlint/plugins";
5075
+ import { defineRule as defineRule41 } from "@oxlint/plugins";
4467
5076
  import { basename as basename2, dirname as dirname2 } from "node:path";
4468
5077
  function isData(expression) {
4469
5078
  const value = astUnwrap(expression);
@@ -4481,7 +5090,7 @@ function isData(expression) {
4481
5090
  }
4482
5091
  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;
4483
5092
  }
4484
- var filesDomainConstants = defineRule36({
5093
+ var filesDomainConstants = defineRule41({
4485
5094
  meta: {
4486
5095
  type: "problem",
4487
5096
  schema: [
@@ -4546,12 +5155,8 @@ var filesDomainConstants = defineRule36({
4546
5155
  });
4547
5156
 
4548
5157
  // src/rules/files/files-exports.ts
4549
- import { defineRule as defineRule37 } from "@oxlint/plugins";
4550
- import { posix as posix2 } from "node:path";
4551
- function escapeRegExp(value) {
4552
- return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
4553
- }
4554
- var filesExports = defineRule37({
5158
+ import { defineRule as defineRule42 } from "@oxlint/plugins";
5159
+ var filesExports = defineRule42({
4555
5160
  meta: {
4556
5161
  type: "problem",
4557
5162
  schema: [
@@ -4567,6 +5172,8 @@ var filesExports = defineRule37({
4567
5172
  separators: { type: "array", items: { type: "string" } },
4568
5173
  types: { type: "boolean" },
4569
5174
  factory: { type: "string" },
5175
+ file: { type: "string" },
5176
+ placeholder: { type: "string" },
4570
5177
  scope: { type: "string", enum: ["exports", "declarations"] }
4571
5178
  },
4572
5179
  anyOf: [{ required: ["names"] }, { required: ["patterns"] }, { required: ["factory"] }]
@@ -4605,16 +5212,12 @@ var filesExports = defineRule37({
4605
5212
  if (options.names === undefined && options.patterns === undefined) {
4606
5213
  return;
4607
5214
  }
4608
- const read = options.stem === "folder" ? namingBasename(posix2.dirname(context.filename.replaceAll("\\", "/"))) : namingFileStem(basename3, options.stem, options.roles, options.separators);
4609
- const last = read.split(/[-_]+/).at(-1) ?? "";
4610
- const domain = options.singularize === "trailing-s" && last.length > 1 && last.endsWith("s") ? read.slice(0, -1) : read;
4611
- const pascalDomain = namingPascalCase(domain);
4612
- const camelDomain = namingCamelCase(domain);
4613
- const allowed = (options.names ?? []).map((template) => template.replaceAll("{Domain}", pascalDomain).replaceAll("{domain}", camelDomain));
4614
- const patterns = (options.patterns ?? []).map((template) => {
4615
- const source = template.replaceAll("{Domain}", escapeRegExp(pascalDomain)).replaceAll("{domain}", escapeRegExp(camelDomain));
4616
- return new RegExp(`^(?:${source})$`);
4617
- });
5215
+ const captures = namingCaptures(context.filename, options);
5216
+ if (!captures) {
5217
+ return;
5218
+ }
5219
+ const allowed = (options.names ?? []).map((template) => namingExpand(template, captures, true));
5220
+ const patterns = (options.patterns ?? []).map((template) => new RegExp(`^(?:${namingExpand(template, captures, true, optionsEscape)})$`));
4618
5221
  const allowedLabel = [...allowed, ...patterns.map((pattern) => pattern.toString())].join(", ");
4619
5222
  for (const binding of exportsCollect(program)) {
4620
5223
  if (options.types && binding.typeOnly) {
@@ -4635,12 +5238,12 @@ var filesExports = defineRule37({
4635
5238
  });
4636
5239
 
4637
5240
  // src/rules/files/files-externals.ts
4638
- import { defineRule as defineRule38 } from "@oxlint/plugins";
4639
- import { existsSync as existsSync2, readFileSync } from "node:fs";
5241
+ import { defineRule as defineRule43 } from "@oxlint/plugins";
5242
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
4640
5243
  import { builtinModules as builtinModules2 } from "node:module";
4641
5244
  import { dirname as dirname3, join as join2 } from "node:path";
4642
5245
  var filesExternalsConfigs = "^(?:vite|next)\\.config\\.[cm]?[jt]s$";
4643
- var filesExternals = defineRule38({
5246
+ var filesExternals = defineRule43({
4644
5247
  meta: {
4645
5248
  type: "problem",
4646
5249
  schema: [
@@ -4666,7 +5269,7 @@ var filesExternals = defineRule38({
4666
5269
  }
4667
5270
  let dependencies;
4668
5271
  try {
4669
- const manifest = JSON.parse(readFileSync(join2(folder, "package.json"), "utf8"));
5272
+ const manifest = JSON.parse(readFileSync2(join2(folder, "package.json"), "utf8"));
4670
5273
  dependencies = [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.optionalDependencies ?? {})];
4671
5274
  } catch {
4672
5275
  return;
@@ -4733,7 +5336,7 @@ var filesExternals = defineRule38({
4733
5336
  });
4734
5337
 
4735
5338
  // src/rules/files/files-factory-keys.ts
4736
- import { defineRule as defineRule39 } from "@oxlint/plugins";
5339
+ import { defineRule as defineRule44 } from "@oxlint/plugins";
4737
5340
  function returnedObject(expression) {
4738
5341
  if (expression?.type === "TSAsExpression" || expression?.type === "TSSatisfiesExpression" || expression?.type === "TSTypeAssertion" || expression?.type === "TSNonNullExpression") {
4739
5342
  return returnedObject(expression.expression);
@@ -4744,7 +5347,7 @@ function compileKeys(pattern, keys, requireKeys) {
4744
5347
  const allowedList = keys.concat(requireKeys.filter((key) => !keys.includes(key)));
4745
5348
  return { patterns: optionsPatternsWithEmpty(pattern), allowed: new Set(allowedList), allowedList, required: requireKeys };
4746
5349
  }
4747
- var filesFactoryKeys = defineRule39({
5350
+ var filesFactoryKeys = defineRule44({
4748
5351
  meta: {
4749
5352
  type: "problem",
4750
5353
  schema: [
@@ -4862,8 +5465,8 @@ var filesFactoryKeys = defineRule39({
4862
5465
  });
4863
5466
 
4864
5467
  // src/rules/files/files-helpers.ts
4865
- import { defineRule as defineRule40 } from "@oxlint/plugins";
4866
- var filesHelpers = defineRule40({
5468
+ import { defineRule as defineRule45 } from "@oxlint/plugins";
5469
+ var filesHelpers = defineRule45({
4867
5470
  meta: {
4868
5471
  type: "problem",
4869
5472
  schema: [
@@ -4911,8 +5514,8 @@ var filesHelpers = defineRule40({
4911
5514
  });
4912
5515
 
4913
5516
  // src/rules/files/files-no-functions.ts
4914
- import { defineRule as defineRule41 } from "@oxlint/plugins";
4915
- var filesNoFunctions = defineRule41({
5517
+ import { defineRule as defineRule46 } from "@oxlint/plugins";
5518
+ var filesNoFunctions = defineRule46({
4916
5519
  meta: {
4917
5520
  type: "problem",
4918
5521
  schema: [
@@ -4966,8 +5569,8 @@ var filesNoFunctions = defineRule41({
4966
5569
  });
4967
5570
 
4968
5571
  // src/rules/files/files-restricted.ts
4969
- import { defineRule as defineRule42 } from "@oxlint/plugins";
4970
- var filesRestricted = defineRule42({
5572
+ import { defineRule as defineRule47 } from "@oxlint/plugins";
5573
+ var filesRestricted = defineRule47({
4971
5574
  meta: {
4972
5575
  type: "problem",
4973
5576
  schema: [
@@ -4999,7 +5602,7 @@ var filesRestricted = defineRule42({
4999
5602
  });
5000
5603
 
5001
5604
  // src/rules/files/files-route.ts
5002
- import { defineRule as defineRule43 } from "@oxlint/plugins";
5605
+ import { defineRule as defineRule48 } from "@oxlint/plugins";
5003
5606
  function inspectAdapter(context, item, bannedHooks, banIntrinsicJsx) {
5004
5607
  astVisit(item.node.body, [item.node], (node) => {
5005
5608
  if (banIntrinsicJsx && node.type === "JSXOpeningElement" && node.name.type === "JSXIdentifier" && /^[a-z]/.test(node.name.name)) {
@@ -5019,7 +5622,7 @@ function inspectAdapter(context, item, bannedHooks, banIntrinsicJsx) {
5019
5622
  }
5020
5623
  });
5021
5624
  }
5022
- var filesRoute = defineRule43({
5625
+ var filesRoute = defineRule48({
5023
5626
  meta: {
5024
5627
  type: "problem",
5025
5628
  schema: [
@@ -5088,8 +5691,247 @@ var filesRoute = defineRule43({
5088
5691
  }
5089
5692
  });
5090
5693
 
5694
+ // src/rules/files/files-siblings.ts
5695
+ import { defineRule as defineRule49 } from "@oxlint/plugins";
5696
+ import { existsSync as existsSync3 } from "node:fs";
5697
+ import { dirname as dirname4, join as join3 } from "node:path";
5698
+ var filesSiblings = defineRule49({
5699
+ meta: {
5700
+ type: "problem",
5701
+ schema: [
5702
+ {
5703
+ type: "object",
5704
+ additionalProperties: false,
5705
+ properties: {
5706
+ require: optionsPatternSchema,
5707
+ message: { type: "string" }
5708
+ },
5709
+ required: ["require"]
5710
+ }
5711
+ ],
5712
+ messages: {
5713
+ missing: "This file needs '{{file}}' beside it.{{message}}"
5714
+ }
5715
+ },
5716
+ createOnce(context) {
5717
+ return {
5718
+ Program(program) {
5719
+ const options = optionsFirst(context, { require: [] });
5720
+ const captures = namingCaptures(context.filename) ?? {};
5721
+ for (const template of [options.require].flat()) {
5722
+ const file = namingExpand(template, captures, false);
5723
+ if (!existsSync3(join3(dirname4(context.filename), file))) {
5724
+ context.report({
5725
+ node: program,
5726
+ messageId: "missing",
5727
+ data: { file, message: options.message ? ` ${options.message}` : "" }
5728
+ });
5729
+ }
5730
+ }
5731
+ }
5732
+ };
5733
+ }
5734
+ });
5735
+
5736
+ // src/rules/files/files-top-level.ts
5737
+ import { defineRule as defineRule50 } from "@oxlint/plugins";
5738
+ var filesTopLevelKinds = [
5739
+ "import",
5740
+ "export",
5741
+ "reexport",
5742
+ "default",
5743
+ "type",
5744
+ "interface",
5745
+ "enum",
5746
+ "class",
5747
+ "namespace",
5748
+ "const",
5749
+ "let",
5750
+ "var",
5751
+ "function",
5752
+ "function-const",
5753
+ "call",
5754
+ "statement"
5755
+ ];
5756
+ var filesTopLevelDeclared = new Map([
5757
+ ["FunctionDeclaration", "function"],
5758
+ ["TSDeclareFunction", "function"],
5759
+ ["ClassDeclaration", "class"],
5760
+ ["TSTypeAliasDeclaration", "type"],
5761
+ ["TSInterfaceDeclaration", "interface"],
5762
+ ["TSEnumDeclaration", "enum"],
5763
+ ["TSModuleDeclaration", "namespace"]
5764
+ ]);
5765
+ var filesTopLevelEntries = {
5766
+ type: "array",
5767
+ items: {
5768
+ anyOf: [
5769
+ { type: "string" },
5770
+ {
5771
+ type: "object",
5772
+ additionalProperties: false,
5773
+ properties: {
5774
+ kind: { anyOf: [{ type: "string", enum: [...filesTopLevelKinds] }, { type: "array", items: { type: "string", enum: [...filesTopLevelKinds] }, minItems: 1 }] },
5775
+ names: optionsPatternSchema,
5776
+ from: optionsPatternSchema,
5777
+ exported: { type: "boolean" },
5778
+ message: { type: "string" }
5779
+ },
5780
+ required: ["kind"]
5781
+ }
5782
+ ]
5783
+ }
5784
+ };
5785
+ var filesTopLevelValues = new Map([
5786
+ ["Literal", "<literal>"],
5787
+ ["TemplateLiteral", "<literal>"],
5788
+ ["ObjectExpression", "<object>"],
5789
+ ["ArrayExpression", "<array>"],
5790
+ ["FunctionExpression", "<function>"],
5791
+ ["ArrowFunctionExpression", "<function>"]
5792
+ ]);
5793
+ function filesTopLevelHead(node) {
5794
+ if (!node) {
5795
+ return "<none>";
5796
+ }
5797
+ if (node.type === "AwaitExpression") {
5798
+ return filesTopLevelHead(node.argument);
5799
+ }
5800
+ if (node.type === "ParenthesizedExpression" || node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "ChainExpression") {
5801
+ return filesTopLevelHead(node.expression);
5802
+ }
5803
+ if (node.type === "CallExpression" || node.type === "NewExpression") {
5804
+ return filesTopLevelHead(node.callee);
5805
+ }
5806
+ if (node.type === "TaggedTemplateExpression") {
5807
+ return filesTopLevelHead(node.tag);
5808
+ }
5809
+ if (node.type === "MemberExpression") {
5810
+ return astDottedName(node) ?? filesTopLevelHead(node.object);
5811
+ }
5812
+ return node.type === "Identifier" ? node.name : filesTopLevelValues.get(node.type) ?? "<other>";
5813
+ }
5814
+ function filesTopLevelItems(statement) {
5815
+ const item = (kind, name, node2, from = "", exported2 = false) => ({ kind, name, from, exported: exported2, node: node2 });
5816
+ if (statement.type === "ExpressionStatement" && statement.directive) {
5817
+ return [];
5818
+ }
5819
+ if (statement.type === "ImportDeclaration") {
5820
+ return [item("import", statement.source.value, statement)];
5821
+ }
5822
+ if (statement.type === "ExportAllDeclaration") {
5823
+ return [item("reexport", statement.source.value, statement, "", true)];
5824
+ }
5825
+ if (statement.type === "ExportNamedDeclaration" && !statement.declaration) {
5826
+ const kind = statement.source ? "reexport" : "export";
5827
+ const names = statement.specifiers.map((specifier) => item(kind, astModuleName(specifier.exported), specifier, statement.source?.value ?? "", true));
5828
+ return names.length > 0 ? names : [item(kind, "", statement, statement.source?.value ?? "", true)];
5829
+ }
5830
+ const exported = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration";
5831
+ const node = exported ? statement.declaration : statement;
5832
+ if (!node) {
5833
+ return [];
5834
+ }
5835
+ if (node.type === "VariableDeclaration") {
5836
+ return node.declarations.flatMap((declarator) => {
5837
+ const variable = node.kind === "using" || node.kind === "await using" ? "statement" : node.kind;
5838
+ const kind = declarationsIsFunctionValue(declarator.init) ? "function-const" : variable;
5839
+ return astBindings(declarator.id).map((binding) => item(kind, binding.name, declarator, filesTopLevelHead(declarator.init), exported));
5840
+ });
5841
+ }
5842
+ const declared = filesTopLevelDeclared.get(node.type);
5843
+ if (declared) {
5844
+ const id = "id" in node ? node.id : null;
5845
+ return [item(declared, id && "name" in id ? id.name : "", node, "", exported)];
5846
+ }
5847
+ if (statement.type === "ExportDefaultDeclaration") {
5848
+ return [item("default", filesTopLevelHead(node), statement, filesTopLevelHead(node), true)];
5849
+ }
5850
+ if (statement.type === "ExpressionStatement") {
5851
+ const expression = astUnwrap(statement.expression);
5852
+ if (expression.type === "CallExpression" || expression.type === "TaggedTemplateExpression") {
5853
+ return [item("call", filesTopLevelHead(expression), statement)];
5854
+ }
5855
+ }
5856
+ return [item("statement", statement.type, statement)];
5857
+ }
5858
+ var filesTopLevel = defineRule50({
5859
+ meta: {
5860
+ type: "problem",
5861
+ schema: [
5862
+ {
5863
+ type: "object",
5864
+ additionalProperties: false,
5865
+ properties: {
5866
+ allow: filesTopLevelEntries,
5867
+ deny: filesTopLevelEntries,
5868
+ message: { type: "string" },
5869
+ stem: { type: "string", enum: [...namingStemModes, "folder"] },
5870
+ singularize: { type: "string", enum: ["none", "trailing-s"] },
5871
+ roles: { type: "array", items: { type: "string" } },
5872
+ separators: { type: "array", items: { type: "string" } },
5873
+ file: { type: "string" },
5874
+ placeholder: { type: "string" }
5875
+ },
5876
+ anyOf: [{ required: ["allow"] }, { required: ["deny"] }]
5877
+ }
5878
+ ],
5879
+ messages: {
5880
+ denied: "Top-level {{what}} is not allowed in this file.{{message}}",
5881
+ unlisted: "Top-level {{what}} is not allowed in this file; allowed: {{allowed}}.{{message}}"
5882
+ }
5883
+ },
5884
+ createOnce(context) {
5885
+ return {
5886
+ Program(program) {
5887
+ const options = optionsFirst(context, {});
5888
+ const captures = namingCaptures(context.filename, options);
5889
+ if (!captures) {
5890
+ return;
5891
+ }
5892
+ const patterns = (pattern) => pattern === undefined ? null : optionsPatterns([pattern].flat().map((source) => namingExpand(source, captures, true, optionsEscape)));
5893
+ const matcher = (entry) => {
5894
+ if (typeof entry !== "string") {
5895
+ return { kinds: [entry.kind].flat(), names: patterns(entry.names), from: patterns(entry.from), exported: entry.exported, message: entry.message ?? "" };
5896
+ }
5897
+ const kind = filesTopLevelKinds.find((name) => name === entry);
5898
+ return kind ? { kinds: [kind], names: null, from: null, message: "" } : { kinds: ["call"], names: optionsWildcards(entry), from: null, message: "" };
5899
+ };
5900
+ const matches = (item) => (entry) => entry.kinds.includes(item.kind) && (entry.names === null || optionsPatternsTest(entry.names, item.name)) && (entry.from === null || optionsPatternsTest(entry.from, item.from)) && (entry.exported === undefined || entry.exported === item.exported);
5901
+ const allow = options.allow?.map(matcher);
5902
+ const deny = (options.deny ?? []).map(matcher);
5903
+ const label = (entry) => [
5904
+ entry.exported === undefined ? [] : [entry.exported ? "exported" : "unexported"],
5905
+ [entry.kind].flat().join("/"),
5906
+ entry.names === undefined ? [] : ["named", [entry.names].flat().join(" | ")],
5907
+ entry.from === undefined ? [] : ["from", [entry.from].flat().join(" | ")]
5908
+ ].flat().join(" ");
5909
+ const allowed = (options.allow ?? []).map((entry) => typeof entry === "string" ? entry : label(entry)).join(", ");
5910
+ const hint = options.message ? ` ${options.message}` : "";
5911
+ for (const item of program.body.flatMap(filesTopLevelItems)) {
5912
+ const what = item.name && item.kind !== "statement" ? `${item.kind} '${item.name}'` : item.kind;
5913
+ const denied = deny.find(matches(item));
5914
+ if (denied) {
5915
+ context.report({
5916
+ node: item.node,
5917
+ messageId: "denied",
5918
+ data: { what, message: denied.message ? ` ${denied.message}` : hint }
5919
+ });
5920
+ } else if (allow && !allow.some(matches(item))) {
5921
+ context.report({
5922
+ node: item.node,
5923
+ messageId: "unlisted",
5924
+ data: { what, allowed, message: hint }
5925
+ });
5926
+ }
5927
+ }
5928
+ }
5929
+ };
5930
+ }
5931
+ });
5932
+
5091
5933
  // src/rules/files/files-types-only.ts
5092
- import { defineRule as defineRule44 } from "@oxlint/plugins";
5934
+ import { defineRule as defineRule51 } from "@oxlint/plugins";
5093
5935
  function runtimeImportLocalNames(statement) {
5094
5936
  return statement.specifiers.flatMap((specifier) => specifier.type !== "ImportSpecifier" || specifier.importKind !== "type" ? [specifier.local.name] : []);
5095
5937
  }
@@ -5258,7 +6100,7 @@ function runtimeKind(node) {
5258
6100
  }
5259
6101
  return node.type === "FunctionDeclaration" ? "function" : "variable";
5260
6102
  }
5261
- var filesTypesOnly = defineRule44({
6103
+ var filesTypesOnly = defineRule51({
5262
6104
  meta: {
5263
6105
  type: "problem",
5264
6106
  schema: [
@@ -5366,8 +6208,8 @@ var filesTypesOnly = defineRule44({
5366
6208
  });
5367
6209
 
5368
6210
  // src/rules/layout/layout-arrow-body.ts
5369
- import { defineRule as defineRule45 } from "@oxlint/plugins";
5370
- var layoutArrowBody = defineRule45({
6211
+ import { defineRule as defineRule52 } from "@oxlint/plugins";
6212
+ var layoutArrowBody = defineRule52({
5371
6213
  meta: {
5372
6214
  type: "layout",
5373
6215
  fixable: "whitespace",
@@ -5419,12 +6261,12 @@ var layoutArrowBody = defineRule45({
5419
6261
  });
5420
6262
 
5421
6263
  // src/rules/layout/layout-call-arguments.ts
5422
- import { defineRule as defineRule46 } from "@oxlint/plugins";
6264
+ import { defineRule as defineRule53 } from "@oxlint/plugins";
5423
6265
  function layoutCallArgumentsHuggable(node) {
5424
6266
  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);
5425
6267
  }
5426
6268
  var layoutCallArgumentsMax = { type: "integer", minimum: 1 };
5427
- var layoutCallArguments = defineRule46({
6269
+ var layoutCallArguments = defineRule53({
5428
6270
  meta: {
5429
6271
  type: "layout",
5430
6272
  fixable: "whitespace",
@@ -5443,6 +6285,12 @@ var layoutCallArguments = defineRule46({
5443
6285
  type: "boolean",
5444
6286
  default: true
5445
6287
  },
6288
+ pipe: {
6289
+ type: "object",
6290
+ additionalProperties: false,
6291
+ required: ["min"],
6292
+ properties: { min: { type: "integer", minimum: 2 }, methods: { type: "array", items: { type: "string" } } }
6293
+ },
5446
6294
  hug: {
5447
6295
  type: "object",
5448
6296
  additionalProperties: false,
@@ -5465,6 +6313,7 @@ var layoutCallArguments = defineRule46({
5465
6313
  ],
5466
6314
  messages: {
5467
6315
  multiline: "A call with {{min}}+ arguments puts each argument on its own line.",
6316
+ pipe: "A pipe with {{min}}+ steps puts each step on its own line.",
5468
6317
  hug: "Put every argument on its own line. Only a last callback, object or array may hug the call, after simple arguments.",
5469
6318
  prefer: "Hug the last argument: it is a callback, object or array after simple arguments.",
5470
6319
  join: "This fits on one line, so keep it on one line."
@@ -5474,11 +6323,13 @@ var layoutCallArguments = defineRule46({
5474
6323
  let options = {};
5475
6324
  let minArguments = 3;
5476
6325
  let indentUnit = sourceIndentUnit();
5477
- const split = (node) => {
6326
+ let pipeMethods = new Set;
6327
+ const isPipe = (node) => node.callee.type === "MemberExpression" && pipeMethods.has(astMemberName(node.callee) ?? "");
6328
+ const split = (node, min, messageId) => {
5478
6329
  const args = node.arguments;
5479
6330
  const last = args.at(-1);
5480
6331
  const source = context.sourceCode;
5481
- if (args.length < minArguments || !last || args.some((arg) => (arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression") && arg.loc.start.line !== arg.loc.end.line)) {
6332
+ if (args.length < min || !last || args.some((arg) => (arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression") && arg.loc.start.line !== arg.loc.end.line)) {
5482
6333
  return;
5483
6334
  }
5484
6335
  const text = source.text;
@@ -5500,8 +6351,8 @@ var layoutCallArguments = defineRule46({
5500
6351
  const argumentIndent = `${baseIndent}${indentUnit}`;
5501
6352
  context.report({
5502
6353
  node,
5503
- messageId: "multiline",
5504
- data: { min: String(minArguments) },
6354
+ messageId,
6355
+ data: { min: String(min) },
5505
6356
  fix: unsafe ? undefined : (fixer) => gaps.map((gap, index) => {
5506
6357
  if (index === 0) {
5507
6358
  return fixer.replaceTextRange(gap, `${newline}${argumentIndent}`);
@@ -5519,11 +6370,23 @@ var layoutCallArguments = defineRule46({
5519
6370
  const last = args.at(-1);
5520
6371
  const open = source.getTokenAfter(node.typeArguments ?? node.callee);
5521
6372
  const close = source.getLastToken(node);
5522
- if (!first || !last || !open || !close || args.every(astIsOneLine) || source.getCommentsInside(node).length > 0) {
6373
+ if (!first || !last || !open || !close || source.getCommentsInside(node).length > 0) {
6374
+ return;
6375
+ }
6376
+ if (args.every(astIsOneLine)) {
6377
+ if (first.loc.start.line > open.loc.end.line && close.loc.start.line === last.loc.end.line) {
6378
+ const newline2 = sourceNewline(text);
6379
+ const base2 = sourceLineIndent(text, open.range[0]);
6380
+ context.report({
6381
+ node,
6382
+ messageId: "hug",
6383
+ fix: (fixer) => sourceRespace(fixer, text, sourceItemGaps(open.range[1], args, close.range[0]), (_, end) => `${newline2}${end ? base2 : `${base2}${indentUnit}`}`)
6384
+ });
6385
+ }
5523
6386
  return;
5524
6387
  }
5525
6388
  const leading = args.slice(0, -1);
5526
- const canHug = !astIsPipe(node) && leading.every((arg) => astIsOneLine(arg) && astCallDepth(arg) <= 1) && layoutCallArgumentsHuggable(last);
6389
+ const canHug = !isPipe(node) && leading.every((arg) => astIsOneLine(arg) && astCallDepth(arg) <= 1) && layoutCallArgumentsHuggable(last);
5527
6390
  const gaps = sourceItemGaps(open.range[1], args, close.range[0]);
5528
6391
  if (first.loc.start.line > open.loc.end.line) {
5529
6392
  const head = [...leading.map((arg) => source.getText(arg)), text.slice(last.range[0], sourceLineEnd(text, last.range[0]))];
@@ -5549,7 +6412,7 @@ var layoutCallArguments = defineRule46({
5549
6412
  fix: (fixer) => sourceRespace(fixer, text, gaps, (_, end) => `${newline}${end ? base : inner}`)
5550
6413
  });
5551
6414
  };
5552
- const join3 = (node, items, open, close) => {
6415
+ const join4 = (node, items, open, close) => {
5553
6416
  const source = context.sourceCode;
5554
6417
  const text = source.text;
5555
6418
  const first = items.at(0);
@@ -5569,14 +6432,16 @@ var layoutCallArguments = defineRule46({
5569
6432
  };
5570
6433
  const call = (node) => {
5571
6434
  const source = context.sourceCode;
5572
- if (options.split !== false) {
5573
- split(node);
6435
+ if (options.pipe && isPipe(node)) {
6436
+ split(node, options.pipe.min, "pipe");
6437
+ } else if (options.split !== false) {
6438
+ split(node, minArguments, "multiline");
5574
6439
  }
5575
6440
  if (options.hug) {
5576
6441
  hug(node, options.hug.max);
5577
6442
  }
5578
- 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)) {
5579
- join3(node, node.arguments, source.getTokenAfter(node.typeArguments ?? node.callee), source.getLastToken(node));
6443
+ if (options.join && !(options.join.keep ?? []).includes(source.getText(node.callee)) && !(isPipe(node) && (node.arguments.length > 1 || options.pipe)) && astCallDepth(node) <= (options.join.nesting ?? Number.POSITIVE_INFINITY)) {
6444
+ join4(node, node.arguments, source.getTokenAfter(node.typeArguments ?? node.callee), source.getLastToken(node));
5580
6445
  }
5581
6446
  };
5582
6447
  const parameters = (node) => {
@@ -5586,13 +6451,14 @@ var layoutCallArguments = defineRule46({
5586
6451
  if (!options.join || !first || !last) {
5587
6452
  return;
5588
6453
  }
5589
- join3(node, node.params, source.getTokenBefore(first, { filter: (token) => token.value === "(" }), source.getTokenAfter(last, { filter: (token) => token.value === ")" }));
6454
+ join4(node, node.params, source.getTokenBefore(first, { filter: (token) => token.value === "(" }), source.getTokenAfter(last, { filter: (token) => token.value === ")" }));
5590
6455
  };
5591
6456
  return {
5592
6457
  before() {
5593
6458
  options = optionsFirst(context, {});
5594
6459
  minArguments = options.min ?? 3;
5595
6460
  indentUnit = sourceIndentUnit(options.indent);
6461
+ pipeMethods = new Set(options.pipe?.methods ?? ["pipe"]);
5596
6462
  },
5597
6463
  CallExpression: call,
5598
6464
  NewExpression: call,
@@ -5606,12 +6472,12 @@ var layoutCallArguments = defineRule46({
5606
6472
  });
5607
6473
 
5608
6474
  // src/rules/layout/layout-call-array.ts
5609
- import { defineRule as defineRule47 } from "@oxlint/plugins";
6475
+ import { defineRule as defineRule54 } from "@oxlint/plugins";
5610
6476
  var layoutCallArrayDefaultCallees = ["Promise.all", "Promise.allSettled"];
5611
6477
  function layoutCallArrayRoot(node) {
5612
6478
  return node.type === "MemberExpression" ? layoutCallArrayRoot(node.object) : node;
5613
6479
  }
5614
- var layoutCallArray = defineRule47({
6480
+ var layoutCallArray = defineRule54({
5615
6481
  meta: {
5616
6482
  type: "layout",
5617
6483
  fixable: "whitespace",
@@ -5727,22 +6593,29 @@ var layoutCallArray = defineRule47({
5727
6593
  });
5728
6594
 
5729
6595
  // src/rules/layout/layout-call-depth.ts
5730
- import { defineRule as defineRule48 } from "@oxlint/plugins";
5731
- function layoutCallDepthOuter(node) {
6596
+ import { defineRule as defineRule55 } from "@oxlint/plugins";
6597
+ function layoutCallDepthOuter(node, callbacks) {
5732
6598
  const parent = node.parent;
5733
- if (!parent || parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression" || parent.type.endsWith("Statement")) {
6599
+ if (!parent || !callbacks && (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression")) {
5734
6600
  return;
5735
6601
  }
5736
- return astIsCall(parent) && parent.arguments.some((argument) => argument === node) ? parent : layoutCallDepthOuter(parent);
6602
+ if (parent.type.endsWith("Statement")) {
6603
+ let owner = parent;
6604
+ while (owner?.type.endsWith("Statement")) {
6605
+ owner = owner.parent;
6606
+ }
6607
+ return callbacks && owner && astIsInlineFunction(owner) ? layoutCallDepthOuter(owner, callbacks) : undefined;
6608
+ }
6609
+ return astIsCall(parent) && parent.arguments.some((argument) => argument === node) ? parent : layoutCallDepthOuter(parent, callbacks);
5737
6610
  }
5738
- var layoutCallDepth = defineRule48({
6611
+ var layoutCallDepth = defineRule55({
5739
6612
  meta: {
5740
6613
  type: "layout",
5741
6614
  schema: [
5742
6615
  {
5743
6616
  type: "object",
5744
6617
  additionalProperties: false,
5745
- properties: { max: { type: "integer", minimum: 1, default: 2 } }
6618
+ properties: { max: { type: "integer", minimum: 1, default: 2 }, callbacks: { type: "boolean", default: false } }
5746
6619
  }
5747
6620
  ],
5748
6621
  messages: {
@@ -5751,10 +6624,11 @@ var layoutCallDepth = defineRule48({
5751
6624
  },
5752
6625
  createOnce(context) {
5753
6626
  let max = 2;
6627
+ let callbacks = false;
5754
6628
  const check = (node) => {
5755
- const depth = astCallDepth(node);
5756
- const outer = layoutCallDepthOuter(node);
5757
- if (!astIsOneLine(node) || depth <= max || outer !== undefined && astIsOneLine(outer) && astCallDepth(outer) > max) {
6629
+ const depth = astCallDepth(node, callbacks);
6630
+ const outer = layoutCallDepthOuter(node, callbacks);
6631
+ if (!astIsOneLine(node) || depth <= max || outer !== undefined && astIsOneLine(outer) && astCallDepth(outer, callbacks) > max) {
5758
6632
  return;
5759
6633
  }
5760
6634
  context.report({
@@ -5765,7 +6639,9 @@ var layoutCallDepth = defineRule48({
5765
6639
  };
5766
6640
  return {
5767
6641
  before() {
5768
- max = optionsFirst(context, {}).max ?? 2;
6642
+ const options = optionsFirst(context, {});
6643
+ max = options.max ?? 2;
6644
+ callbacks = options.callbacks ?? false;
5769
6645
  },
5770
6646
  CallExpression: check,
5771
6647
  NewExpression: check
@@ -5774,8 +6650,8 @@ var layoutCallDepth = defineRule48({
5774
6650
  });
5775
6651
 
5776
6652
  // src/rules/layout/layout-chain.ts
5777
- import { defineRule as defineRule49 } from "@oxlint/plugins";
5778
- var layoutChain = defineRule49({
6653
+ import { defineRule as defineRule56 } from "@oxlint/plugins";
6654
+ var layoutChain = defineRule56({
5779
6655
  meta: {
5780
6656
  type: "layout",
5781
6657
  fixable: "whitespace",
@@ -5894,8 +6770,8 @@ var layoutChain = defineRule49({
5894
6770
  });
5895
6771
 
5896
6772
  // src/rules/layout/layout-items.ts
5897
- import { defineRule as defineRule50 } from "@oxlint/plugins";
5898
- var layoutItems = defineRule50({
6773
+ import { defineRule as defineRule57 } from "@oxlint/plugins";
6774
+ var layoutItems = defineRule57({
5899
6775
  meta: {
5900
6776
  type: "layout",
5901
6777
  fixable: "whitespace",
@@ -5950,8 +6826,8 @@ var layoutItems = defineRule50({
5950
6826
  });
5951
6827
 
5952
6828
  // src/rules/layout/layout-jsx-attributes.ts
5953
- import { defineRule as defineRule51 } from "@oxlint/plugins";
5954
- var layoutJsxAttributes = defineRule51({
6829
+ import { defineRule as defineRule58 } from "@oxlint/plugins";
6830
+ var layoutJsxAttributes = defineRule58({
5955
6831
  meta: {
5956
6832
  type: "layout",
5957
6833
  fixable: "whitespace",
@@ -6039,8 +6915,8 @@ var layoutJsxAttributes = defineRule51({
6039
6915
  });
6040
6916
 
6041
6917
  // src/rules/layout/layout-key-value.ts
6042
- import { defineRule as defineRule52 } from "@oxlint/plugins";
6043
- var layoutKeyValue = defineRule52({
6918
+ import { defineRule as defineRule59 } from "@oxlint/plugins";
6919
+ var layoutKeyValue = defineRule59({
6044
6920
  meta: {
6045
6921
  type: "layout",
6046
6922
  fixable: "whitespace",
@@ -6076,7 +6952,7 @@ var layoutKeyValue = defineRule52({
6076
6952
  });
6077
6953
 
6078
6954
  // src/rules/layout/layout-object.ts
6079
- import { defineRule as defineRule53 } from "@oxlint/plugins";
6955
+ import { defineRule as defineRule60 } from "@oxlint/plugins";
6080
6956
  function layoutObjectIsWhitespace(text) {
6081
6957
  return /^[\t\n\r\f ]*$/.test(text);
6082
6958
  }
@@ -6084,7 +6960,7 @@ function layoutObjectIsFlat(property) {
6084
6960
  const value = property.type === "SpreadElement" ? property.argument : property.value;
6085
6961
  return astIsOneLine(property) && (property.type === "SpreadElement" || !property.method && property.kind === "init") && value.type !== "ObjectExpression" && value.type !== "ArrayExpression" && !astIsInlineFunction(value);
6086
6962
  }
6087
- var layoutObject = defineRule53({
6963
+ var layoutObject = defineRule60({
6088
6964
  meta: {
6089
6965
  type: "layout",
6090
6966
  fixable: "whitespace",
@@ -6096,6 +6972,10 @@ var layoutObject = defineRule53({
6096
6972
  min: { type: "integer", minimum: 1, default: 3 },
6097
6973
  scope: { type: "string", enum: ["call-args", "all"], default: "call-args" },
6098
6974
  indent: { ...sourceIndentSchema, default: 2 },
6975
+ params: { type: "boolean", default: false },
6976
+ nested: { type: "boolean", default: false },
6977
+ siblings: { type: "boolean", default: false },
6978
+ inner: { type: "integer", minimum: 1 },
6099
6979
  join: {
6100
6980
  type: "object",
6101
6981
  additionalProperties: false,
@@ -6111,6 +6991,11 @@ var layoutObject = defineRule53({
6111
6991
  messages: {
6112
6992
  callArgs: "Object with {{min}}+ properties in a call should have each property on its own line.",
6113
6993
  always: "Object with {{min}}+ properties should have each property on its own line.",
6994
+ nested: "An object holding an object or function should have each property on its own line.",
6995
+ nestedName: "An object holding an object or function should have each property on its own line. Name this call first: split here, it would break a chained call or a ternary.",
6996
+ siblings: "A sibling call in this list splits its object; split this one too.",
6997
+ inner: "An object with {{inner}}+ entries inside a split object should have each entry on its own line.",
6998
+ params: "Destructured parameter with {{min}}+ properties should have each property on its own line.",
6114
6999
  join: "This object fits on one line, so keep it on one line."
6115
7000
  }
6116
7001
  },
@@ -6120,39 +7005,167 @@ var layoutObject = defineRule53({
6120
7005
  let indentUnit = sourceIndentUnit();
6121
7006
  let newline = `
6122
7007
  `;
6123
- let join3;
6124
- return {
6125
- before() {
6126
- const options = optionsFirst(context, {});
6127
- minProperties = options.min ?? 3;
6128
- scope = options.scope ?? "call-args";
6129
- indentUnit = sourceIndentUnit(options.indent);
6130
- newline = sourceNewline(context.sourceCode.text);
6131
- join3 = options.join;
6132
- },
6133
- ObjectExpression(node) {
6134
- const source = context.sourceCode;
6135
- const text = source.text;
6136
- const properties = node.properties;
6137
- const split = properties.length >= minProperties;
6138
- if (split ? !astIsOneLine(node) : !join3 || astIsOneLine(node)) {
6139
- return;
7008
+ let join4;
7009
+ let params = false;
7010
+ let nested = false;
7011
+ let siblings = false;
7012
+ let inner;
7013
+ const splitCounts = new WeakMap;
7014
+ function holdsNested(node) {
7015
+ return nested && astIsOneLine(node) && node.properties.some((property) => property.type === "Property" && (property.value.type === "ObjectExpression" && property.value.properties.length > 0 || astIsInlineFunction(property.value))) && !insideOneLineObject(node.parent);
7016
+ }
7017
+ function insideOneLineObject(start) {
7018
+ for (let ancestor = start;ancestor && !ancestor.type.endsWith("Statement"); ancestor = ancestor.parent) {
7019
+ if (ancestor.type === "ObjectExpression" && astIsOneLine(ancestor)) {
7020
+ return true;
6140
7021
  }
6141
- const parent = node.parent;
6142
- const call = (parent.type === "CallExpression" || parent.type === "NewExpression") && parent.arguments.includes(node) ? parent : undefined;
6143
- if (scope === "call-args" && !call) {
6144
- return;
7022
+ }
7023
+ return false;
7024
+ }
7025
+ function breaksAround(node) {
7026
+ for (let child = node, parent = node.parent;parent; child = parent, parent = parent.parent) {
7027
+ if (parent.type === "ConditionalExpression" || parent.type === "MemberExpression" && parent.object === child && astIsCall(child)) {
7028
+ return true;
6145
7029
  }
6146
- if (!split) {
6147
- const callee = call ? astDottedName(call.callee) : undefined;
6148
- const kept = callee !== undefined && (join3?.keep ?? []).some((name) => callee === name || callee.endsWith(`.${name}`));
6149
- if (kept || properties.length === 0 || !properties.every(layoutObjectIsFlat) || source.getCommentsInside(node).length > 0) {
6150
- return;
7030
+ if (astIsInlineFunction(parent) || parent.type.endsWith("Statement") || parent.type.endsWith("Declaration")) {
7031
+ return false;
7032
+ }
7033
+ }
7034
+ return false;
7035
+ }
7036
+ function keptByInner(node) {
7037
+ const parent = node.parent;
7038
+ return inner !== undefined && node.properties.length >= inner && parent.type === "Property" && parent.value === node && !astIsOneLine(parent.parent);
7039
+ }
7040
+ function splitSiblings(node) {
7041
+ const call = node.parent;
7042
+ const list = call.parent;
7043
+ if (!siblings || node.properties.length === 0 || !astIsCall(call) || list?.type !== "ArrayExpression") {
7044
+ return 0;
7045
+ }
7046
+ let counts = splitCounts.get(list);
7047
+ if (!counts) {
7048
+ counts = new Map;
7049
+ for (const element of list.elements) {
7050
+ if (!element || !astIsCall(element)) {
7051
+ continue;
6151
7052
  }
6152
- const gaps = sourceItemGaps(node.range[0] + 1, properties, node.range[1] - 1);
7053
+ for (const [index, argument] of element.arguments.entries()) {
7054
+ if (argument.type === "ObjectExpression" && !astIsOneLine(argument)) {
7055
+ const key = `${context.sourceCode.getText(element.callee)}#${index}`;
7056
+ counts.set(key, (counts.get(key) ?? 0) + 1);
7057
+ }
7058
+ }
7059
+ }
7060
+ splitCounts.set(list, counts);
7061
+ }
7062
+ return counts.get(`${context.sourceCode.getText(call.callee)}#${call.arguments.indexOf(node)}`) ?? 0;
7063
+ }
7064
+ function joinBlocked(node, call) {
7065
+ const callee = call && astIsCall(call) ? astDottedName(call.callee) : undefined;
7066
+ const kept = callee !== undefined && (join4?.keep ?? []).some((name) => callee === name || callee.endsWith(`.${name}`));
7067
+ return kept || keptByInner(node) || splitSiblings(node) > 1 || node.properties.length === 0 || !node.properties.every(layoutObjectIsFlat) || context.sourceCode.getCommentsInside(node).length > 0;
7068
+ }
7069
+ function reportSplit(node, properties, open, close, messageId) {
7070
+ const source = context.sourceCode;
7071
+ const text = source.text;
7072
+ const baseIndent = sourceLineIndent(text, open);
7073
+ const propertyIndent = `${baseIndent}${indentUnit}`;
7074
+ context.report({
7075
+ node,
7076
+ messageId,
7077
+ data: { min: String(minProperties), inner: String(inner ?? "") },
7078
+ fix: (fixer) => {
7079
+ if (messageId === "nestedName" || properties.length === 0 || source.getCommentsInside(node).length > 0 || !layoutObjectIsWhitespace(text.slice(open + 1, properties[0].range[0]))) {
7080
+ return null;
7081
+ }
7082
+ const gaps = [[open + 1, properties[0].range[0]]];
7083
+ for (const [index, property] of properties.entries()) {
7084
+ const next = properties.at(index + 1);
7085
+ const after = property.range[1];
7086
+ const limit = next ? next.range[0] : close;
7087
+ const between = text.slice(after, limit);
7088
+ const comma = between.indexOf(",");
7089
+ if (comma === -1) {
7090
+ if (next || !layoutObjectIsWhitespace(between)) {
7091
+ return null;
7092
+ }
7093
+ gaps.push([after, close]);
7094
+ continue;
7095
+ }
7096
+ if (!layoutObjectIsWhitespace(between.slice(0, comma)) || !layoutObjectIsWhitespace(between.slice(comma + 1))) {
7097
+ return null;
7098
+ }
7099
+ gaps.push([after + comma + 1, limit]);
7100
+ }
7101
+ return gaps.map((gap, index) => fixer.replaceTextRange(gap, index === gaps.length - 1 ? `${newline}${baseIndent}` : `${newline}${propertyIndent}`));
7102
+ }
7103
+ });
7104
+ }
7105
+ function checkParams(node) {
7106
+ if (!params) {
7107
+ return;
7108
+ }
7109
+ for (const param of node.params) {
7110
+ const pattern = param.type === "AssignmentPattern" ? param.left : param;
7111
+ if (pattern.type !== "ObjectPattern" || pattern.properties.length < minProperties) {
7112
+ continue;
7113
+ }
7114
+ const text = context.sourceCode.text;
7115
+ const close = text.lastIndexOf("}", (pattern.typeAnnotation?.range[0] ?? pattern.range[1]) - 1);
7116
+ if (!text.slice(pattern.range[0], close).includes(`
7117
+ `)) {
7118
+ reportSplit(pattern, pattern.properties, pattern.range[0], close, "params");
7119
+ }
7120
+ }
7121
+ }
7122
+ return {
7123
+ before() {
7124
+ const options = optionsFirst(context, {});
7125
+ minProperties = options.min ?? 3;
7126
+ scope = options.scope ?? "call-args";
7127
+ indentUnit = sourceIndentUnit(options.indent);
7128
+ newline = sourceNewline(context.sourceCode.text);
7129
+ join4 = options.join;
7130
+ params = options.params ?? false;
7131
+ nested = options.nested ?? false;
7132
+ siblings = options.siblings ?? false;
7133
+ inner = options.inner;
7134
+ },
7135
+ FunctionDeclaration: checkParams,
7136
+ FunctionExpression: checkParams,
7137
+ ArrowFunctionExpression: checkParams,
7138
+ ObjectExpression(node) {
7139
+ const source = context.sourceCode;
7140
+ const text = source.text;
7141
+ const properties = node.properties;
7142
+ const parent = node.parent;
7143
+ const full = properties.length >= minProperties;
7144
+ const holds = !full && holdsNested(node);
7145
+ if (!full && !holds && astIsOneLine(node) && splitSiblings(node) > 0) {
7146
+ reportSplit(node, properties, node.range[0], node.range[1] - 1, "siblings");
7147
+ return;
7148
+ }
7149
+ if (astIsOneLine(node) && keptByInner(node)) {
7150
+ reportSplit(node, properties, node.range[0], node.range[1] - 1, "inner");
7151
+ return;
7152
+ }
7153
+ const split = full || holds;
7154
+ if (split ? !astIsOneLine(node) : !join4 || astIsOneLine(node)) {
7155
+ return;
7156
+ }
7157
+ const call = (parent.type === "CallExpression" || parent.type === "NewExpression") && parent.arguments.includes(node) ? parent : undefined;
7158
+ if (scope === "call-args" && !call) {
7159
+ return;
7160
+ }
7161
+ if (!split) {
7162
+ if (joinBlocked(node, call)) {
7163
+ return;
7164
+ }
7165
+ const gaps = sourceItemGaps(node.range[0] + 1, properties, node.range[1] - 1);
6153
7166
  const comma = text.slice(...gaps.at(-1) ?? [0, 0]).includes(",") ? "," : "";
6154
7167
  const joined = ` ${properties.map((property) => source.getText(property)).join(", ")}${comma} `;
6155
- if (sourceJoinedWidth(text, node.range[0] + 1, joined, node.range[1] - 1) > (join3?.max ?? 0)) {
7168
+ if (sourceJoinedWidth(text, node.range[0] + 1, joined, node.range[1] - 1) > (join4?.max ?? 0)) {
6156
7169
  return;
6157
7170
  }
6158
7171
  context.report({
@@ -6162,46 +7175,16 @@ var layoutObject = defineRule53({
6162
7175
  });
6163
7176
  return;
6164
7177
  }
6165
- const baseIndent = sourceLineIndent(text, node.range[0]);
6166
- const propertyIndent = `${baseIndent}${indentUnit}`;
6167
- const close = node.range[1] - 1;
6168
- context.report({
6169
- node,
6170
- messageId: scope === "all" ? "always" : "callArgs",
6171
- data: { min: String(minProperties) },
6172
- fix: (fixer) => {
6173
- if (source.getCommentsInside(node).length > 0 || !layoutObjectIsWhitespace(text.slice(node.range[0] + 1, properties[0].range[0]))) {
6174
- return null;
6175
- }
6176
- const gaps = [[node.range[0] + 1, properties[0].range[0]]];
6177
- for (const [index, property] of properties.entries()) {
6178
- const next = properties.at(index + 1);
6179
- const after = property.range[1];
6180
- const limit = next ? next.range[0] : close;
6181
- const between = text.slice(after, limit);
6182
- const comma = between.indexOf(",");
6183
- if (comma === -1) {
6184
- if (next || !layoutObjectIsWhitespace(between)) {
6185
- return null;
6186
- }
6187
- gaps.push([after, close]);
6188
- continue;
6189
- }
6190
- if (!layoutObjectIsWhitespace(between.slice(0, comma)) || !layoutObjectIsWhitespace(between.slice(comma + 1))) {
6191
- return null;
6192
- }
6193
- gaps.push([after + comma + 1, limit]);
6194
- }
6195
- return gaps.map((gap, index) => fixer.replaceTextRange(gap, index === gaps.length - 1 ? `${newline}${baseIndent}` : `${newline}${propertyIndent}`));
6196
- }
6197
- });
7178
+ const messageId = scope === "all" ? "always" : "callArgs";
7179
+ const nestedId = breaksAround(node) ? "nestedName" : "nested";
7180
+ reportSplit(node, properties, node.range[0], node.range[1] - 1, holds ? nestedId : messageId);
6198
7181
  }
6199
7182
  };
6200
7183
  }
6201
7184
  });
6202
7185
 
6203
7186
  // src/rules/layout/layout-padding.ts
6204
- import { defineRule as defineRule54 } from "@oxlint/plugins";
7187
+ import { defineRule as defineRule61 } from "@oxlint/plugins";
6205
7188
  var layoutPaddingControl = new Set([
6206
7189
  "IfStatement",
6207
7190
  "ForStatement",
@@ -6230,7 +7213,7 @@ function layoutPaddingKind(node) {
6230
7213
  function layoutPaddingIsMultilineVariable(node, kind) {
6231
7214
  return kind === "variable" && node.loc.start.line !== node.loc.end.line;
6232
7215
  }
6233
- var layoutPadding = defineRule54({
7216
+ var layoutPadding = defineRule61({
6234
7217
  meta: {
6235
7218
  type: "layout",
6236
7219
  fixable: "whitespace",
@@ -6319,23 +7302,33 @@ var layoutPadding = defineRule54({
6319
7302
  });
6320
7303
 
6321
7304
  // src/rules/layout/layout-pipe.ts
6322
- import { defineRule as defineRule55 } from "@oxlint/plugins";
6323
- var layoutPipe = defineRule55({
7305
+ import { defineRule as defineRule62 } from "@oxlint/plugins";
7306
+ var layoutPipe = defineRule62({
6324
7307
  meta: {
6325
7308
  type: "layout",
7309
+ schema: [{ type: "object", additionalProperties: false, properties: { methods: { type: "array", items: { type: "string" } } } }],
6326
7310
  messages: {
6327
- 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.",
7311
+ after: "No ).{{method}}( after a call that spans lines. Name the value and pipe the name, or pass the call to the step data-first.",
6328
7312
  step: "A pipe step may span lines only through a callback, object or array. Name this call first, then pass the name."
6329
7313
  }
6330
7314
  },
6331
7315
  createOnce(context) {
7316
+ let methods = new Set;
6332
7317
  return {
7318
+ before() {
7319
+ methods = new Set(optionsFirst(context, {}).methods ?? ["pipe"]);
7320
+ },
6333
7321
  CallExpression(node) {
6334
7322
  const callee = node.callee;
6335
- if (callee.type === "MemberExpression" && astMemberName(callee) === "pipe" && astIsCall(callee.object) && !astIsOneLine(callee.object) && callee.property.loc.start.line === callee.object.loc.end.line) {
6336
- context.report({ node: callee.property, messageId: "after" });
7323
+ const piped = callee.type === "MemberExpression" && methods.has(astMemberName(callee) ?? "");
7324
+ if (piped && astIsCall(callee.object) && !astIsOneLine(callee.object) && callee.property.loc.start.line === callee.object.loc.end.line) {
7325
+ context.report({
7326
+ node: callee.property,
7327
+ messageId: "after",
7328
+ data: { method: astMemberName(callee) ?? "pipe" }
7329
+ });
6337
7330
  }
6338
- if (!astIsPipe(node)) {
7331
+ if (!piped) {
6339
7332
  return;
6340
7333
  }
6341
7334
  for (const step of node.arguments) {
@@ -6351,7 +7344,7 @@ var layoutPipe = defineRule55({
6351
7344
  });
6352
7345
 
6353
7346
  // src/rules/layout/layout-ternary.ts
6354
- import { defineRule as defineRule56 } from "@oxlint/plugins";
7347
+ import { defineRule as defineRule63 } from "@oxlint/plugins";
6355
7348
  function layoutTernaryLines(node) {
6356
7349
  return node.loc.end.line - node.loc.start.line + 1;
6357
7350
  }
@@ -6359,7 +7352,7 @@ function layoutTernaryBreak(between, operator, newline) {
6359
7352
  const at = between.indexOf(operator);
6360
7353
  return `${between.slice(0, at).trimEnd()}${newline}${operator} ${between.slice(at + 1).trimStart()}`;
6361
7354
  }
6362
- var layoutTernary = defineRule56({
7355
+ var layoutTernary = defineRule63({
6363
7356
  meta: {
6364
7357
  type: "layout",
6365
7358
  fixable: "whitespace",
@@ -6474,14 +7467,75 @@ var layoutTernary = defineRule56({
6474
7467
  }
6475
7468
  });
6476
7469
 
7470
+ // src/rules/layout/layout-type-arguments.ts
7471
+ import { defineRule as defineRule64 } from "@oxlint/plugins";
7472
+ var layoutTypeArguments = defineRule64({
7473
+ meta: {
7474
+ type: "layout",
7475
+ fixable: "code",
7476
+ schema: [
7477
+ {
7478
+ type: "object",
7479
+ additionalProperties: false,
7480
+ properties: {
7481
+ min: { type: "integer", minimum: 2, default: 3 },
7482
+ indent: { ...sourceIndentSchema, default: 2 }
7483
+ }
7484
+ }
7485
+ ],
7486
+ messages: {
7487
+ union: "A union of {{min}}+ types in type arguments puts each type on its own line."
7488
+ }
7489
+ },
7490
+ createOnce(context) {
7491
+ let minMembers = 3;
7492
+ let indentUnit = sourceIndentUnit();
7493
+ return {
7494
+ before() {
7495
+ const options = optionsFirst(context, {});
7496
+ minMembers = options.min ?? 3;
7497
+ indentUnit = sourceIndentUnit(options.indent);
7498
+ },
7499
+ TSTypeParameterInstantiation(node) {
7500
+ const unions = node.params.flatMap((param) => param.type === "TSUnionType" && param.types.length >= minMembers ? [param] : []);
7501
+ if (unions.length === 0 || !astIsOneLine(node)) {
7502
+ return;
7503
+ }
7504
+ const source = context.sourceCode;
7505
+ const text = source.text;
7506
+ const items = node.params.flatMap((param) => param.type === "TSUnionType" && unions.includes(param) ? param.types : [param]);
7507
+ const gaps = sourceItemGaps(node.range[0] + 1, items, node.range[1] - 1);
7508
+ const firsts = new Set(unions.map((union) => union.types[0]));
7509
+ const matches = gaps.map((gap) => /^\s*(,?)\s*(\|?)\s*$/.exec(text.slice(...gap)));
7510
+ const newline = sourceNewline(text);
7511
+ const base = sourceLineIndent(text, node.range[0]);
7512
+ const inner = `${base}${indentUnit}`;
7513
+ const unsafe = text[node.range[0]] !== "<" || text[node.range[1] - 1] !== ">" || source.getCommentsInside(node).length > 0 || matches.some((match) => !match);
7514
+ context.report({
7515
+ node,
7516
+ messageId: "union",
7517
+ data: { min: String(minMembers) },
7518
+ fix: unsafe ? undefined : (fixer) => gaps.map((gap, index) => {
7519
+ const comma = matches[index]?.[1] ?? "";
7520
+ const next = items.at(index);
7521
+ const bar = matches[index]?.[2] || next && firsts.has(next) ? "| " : "";
7522
+ return fixer.replaceTextRange(gap, next ? `${comma}${newline}${inner}${bar}` : `${comma}${newline}${base}`);
7523
+ })
7524
+ });
7525
+ }
7526
+ };
7527
+ }
7528
+ });
7529
+
6477
7530
  // src/rules/naming/naming-declarations.ts
6478
- import { defineRule as defineRule57 } from "@oxlint/plugins";
7531
+ import { defineRule as defineRule65 } from "@oxlint/plugins";
6479
7532
  var namingDeclarationsKinds = ["function", "const", "let", "var", "class", "type", "interface", "enum"];
6480
- var namingDeclarations = defineRule57({
7533
+ var namingDeclarations = defineRule65({
6481
7534
  meta: {
6482
7535
  type: "problem",
6483
- schema: [
6484
- {
7536
+ schema: {
7537
+ type: "array",
7538
+ items: {
6485
7539
  type: "object",
6486
7540
  additionalProperties: false,
6487
7541
  properties: {
@@ -6492,56 +7546,61 @@ var namingDeclarations = defineRule57({
6492
7546
  allow: optionsPatternSchema
6493
7547
  }
6494
7548
  }
6495
- ],
7549
+ },
6496
7550
  messages: {
6497
7551
  prefix: "'{{name}}' must start with file prefix '{{prefix}}'.",
6498
7552
  pattern: "'{{name}}' must match {{pattern}}."
6499
7553
  }
6500
7554
  },
6501
7555
  createOnce(context) {
6502
- let options = {};
6503
- let kinds = null;
6504
- let allowed = [];
6505
- let expected = [];
6506
- let prefixes = [];
6507
- let label = "";
7556
+ let checks = [];
6508
7557
  function check(node) {
6509
7558
  const declarator = node.type === "VariableDeclarator";
6510
7559
  const kind = declarationsKindOf(node, declarator ? [node.parent] : []);
6511
7560
  const name = declarationsIdName(node);
6512
7561
  const loop = declarator ? node.parent.parent : null;
6513
7562
  const loopHeader = loop?.type === "ForStatement" ? loop.init === node.parent : (loop?.type === "ForInStatement" || loop?.type === "ForOfStatement") && loop.left === node.parent;
6514
- if (!kind || !name || loopHeader || kinds && !kinds.has(kind) || optionsPatternsTest(allowed, name)) {
7563
+ if (!kind || !name || loopHeader) {
6515
7564
  return;
6516
7565
  }
6517
- if (expected.length > 0) {
6518
- if (optionsPatternsTest(expected, name)) {
6519
- return;
7566
+ for (const { kinds, allowed, expected, prefixes, label, normalize } of checks) {
7567
+ if (kinds && !kinds.has(kind) || optionsPatternsTest(allowed, name)) {
7568
+ continue;
7569
+ }
7570
+ if (expected.length > 0) {
7571
+ if (!optionsPatternsTest(expected, name)) {
7572
+ context.report({
7573
+ node,
7574
+ messageId: "pattern",
7575
+ data: { name, pattern: label }
7576
+ });
7577
+ }
7578
+ continue;
7579
+ }
7580
+ if (!namingHasFilePrefix(name, prefixes, normalize)) {
7581
+ context.report({
7582
+ node,
7583
+ messageId: "prefix",
7584
+ data: { name, prefix: prefixes.join(" or ") }
7585
+ });
6520
7586
  }
6521
- context.report({
6522
- node,
6523
- messageId: "pattern",
6524
- data: { name, pattern: label }
6525
- });
6526
- return;
6527
- }
6528
- if (namingHasFilePrefix(name, prefixes, options.normalize)) {
6529
- return;
6530
7587
  }
6531
- context.report({
6532
- node,
6533
- messageId: "prefix",
6534
- data: { name, prefix: prefixes.join(" or ") }
6535
- });
6536
7588
  }
6537
7589
  return {
6538
7590
  before() {
6539
- options = optionsFirst(context, { stem: "before-first-dot", normalize: "remove-separators" });
6540
- kinds = options.kinds?.length ? new Set(options.kinds) : null;
6541
- allowed = optionsPatterns(options.allow);
6542
- expected = optionsPatterns(options.pattern, options.flags);
6543
- label = optionsPatternLabel(options.pattern);
6544
- prefixes = namingFilePrefixesFor(context.filename, options);
7591
+ const given = optionsAll(context);
7592
+ const captures = namingCaptures(context.filename) ?? {};
7593
+ checks = (given.length > 0 ? given : [{}]).map((options) => {
7594
+ const sources = options.pattern === undefined ? [] : [options.pattern].flat().map((source) => namingExpand(source, captures, true, optionsEscape));
7595
+ return {
7596
+ kinds: options.kinds?.length ? new Set(options.kinds) : null,
7597
+ allowed: optionsPatterns(options.allow),
7598
+ expected: optionsPatterns(sources, options.flags),
7599
+ prefixes: namingFilePrefixesFor(context.filename, options),
7600
+ label: optionsPatternLabel(sources),
7601
+ normalize: options.normalize
7602
+ };
7603
+ });
6545
7604
  },
6546
7605
  FunctionDeclaration: check,
6547
7606
  ClassDeclaration: check,
@@ -6554,33 +7613,14 @@ var namingDeclarations = defineRule57({
6554
7613
  });
6555
7614
 
6556
7615
  // src/rules/naming/naming-exports.ts
6557
- import { defineRule as defineRule58 } from "@oxlint/plugins";
7616
+ import { defineRule as defineRule66 } from "@oxlint/plugins";
6558
7617
  var namingExportsScopeProperties = {
6559
7618
  scope: { type: "string", enum: ["exports", "functions", "declarations"] },
6560
7619
  types: { type: "boolean" },
6561
7620
  services: { type: "boolean" },
6562
7621
  allow: optionsPatternSchema
6563
7622
  };
6564
- function matchTemplate(basename3, template, placeholder = "[a-z0-9-]+") {
6565
- let pattern = "";
6566
- let index = 0;
6567
- while (index < template.length) {
6568
- const char = template[index];
6569
- if (char === "{") {
6570
- const end = template.indexOf("}", index);
6571
- if (end === -1) {
6572
- return null;
6573
- }
6574
- pattern += `(?<${template.slice(index + 1, end)}>${placeholder})`;
6575
- index = end + 1;
6576
- continue;
6577
- }
6578
- pattern += /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
6579
- index += 1;
6580
- }
6581
- return new RegExp(`^${pattern}$`).exec(basename3)?.groups ?? null;
6582
- }
6583
- var namingExports = defineRule58({
7623
+ var namingExports = defineRule66({
6584
7624
  meta: {
6585
7625
  type: "problem",
6586
7626
  schema: {
@@ -6610,7 +7650,7 @@ var namingExports = defineRule58({
6610
7650
  mode: { type: "string", enum: ["template"] },
6611
7651
  ...namingExportsScopeProperties,
6612
7652
  file: { type: "string" },
6613
- export: { type: "string" },
7653
+ export: optionsPatternSchema,
6614
7654
  match: { type: "string", enum: ["all", "some"] },
6615
7655
  placeholder: { type: "string" },
6616
7656
  camelCase: { type: "boolean" }
@@ -6637,23 +7677,15 @@ var namingExports = defineRule58({
6637
7677
  const allowed = optionsPatterns(options.allow);
6638
7678
  if (options.mode === "template") {
6639
7679
  const file = namingBasename(context.filename);
6640
- const groups = matchTemplate(file, options.file, options.placeholder);
6641
- if (!groups) {
7680
+ const captures2 = namingCaptures(context.filename, { file: options.file, placeholder: options.placeholder });
7681
+ if (!captures2) {
6642
7682
  continue;
6643
7683
  }
6644
- const expected2 = options.export.replaceAll(/\{([^}]+)\}/g, (placeholder, key) => {
6645
- const group = groups[key.toLowerCase()];
6646
- if (!group) {
6647
- return placeholder;
6648
- }
6649
- if (/^[A-Z]/.test(key)) {
6650
- return namingPascalCase(group);
6651
- }
6652
- return options.camelCase ? namingCamelCase(group) : group;
6653
- });
7684
+ const names2 = [options.export].flat().map((template) => namingExpand(template, captures2, options.camelCase ?? false));
7685
+ const expected2 = names2.join(" or ");
6654
7686
  const functions = (scope === "exports" ? exportsFunctions(program, true) : declarationsFunctions(program)).filter(({ name }) => !optionsPatternsTest(allowed, name));
6655
7687
  if (options.match === "some") {
6656
- if (functions.some((item) => item.name === expected2)) {
7688
+ if (functions.some((item) => names2.includes(item.name))) {
6657
7689
  continue;
6658
7690
  }
6659
7691
  const first = functions.at(0);
@@ -6665,7 +7697,7 @@ var namingExports = defineRule58({
6665
7697
  continue;
6666
7698
  }
6667
7699
  for (const item of functions) {
6668
- if (item.name !== expected2) {
7700
+ if (!names2.includes(item.name)) {
6669
7701
  context.report({
6670
7702
  node: item.node,
6671
7703
  messageId: "mismatch",
@@ -6687,7 +7719,9 @@ var namingExports = defineRule58({
6687
7719
  names.push(...declarationsPublicServiceMethods(program));
6688
7720
  }
6689
7721
  const prefixes = options.mode === "pattern" ? [] : namingFilePrefixesFor(context.filename, options);
6690
- const expected = options.mode === "pattern" ? optionsPatternsWithEmpty(options.pattern, options.flags) : [];
7722
+ const captures = namingCaptures(context.filename) ?? {};
7723
+ const sources = options.mode === "pattern" ? [options.pattern].flat().map((source) => namingExpand(source, captures, true, optionsEscape)) : [];
7724
+ const expected = optionsPatternsWithEmpty(sources, options.mode === "pattern" ? options.flags : undefined);
6691
7725
  for (const item of declarationsUnique(names)) {
6692
7726
  if (optionsPatternsTest(allowed, item.name)) {
6693
7727
  continue;
@@ -6697,7 +7731,7 @@ var namingExports = defineRule58({
6697
7731
  context.report({
6698
7732
  node: item.node,
6699
7733
  messageId: "pattern",
6700
- data: { name: item.name, pattern: optionsPatternLabel(options.pattern) }
7734
+ data: { name: item.name, pattern: optionsPatternLabel(sources) }
6701
7735
  });
6702
7736
  }
6703
7737
  continue;
@@ -6716,71 +7750,586 @@ var namingExports = defineRule58({
6716
7750
  }
6717
7751
  });
6718
7752
 
6719
- // src/rules/naming/naming-file.ts
6720
- import { defineRule as defineRule59 } from "@oxlint/plugins";
6721
- import { readdirSync as readdirSync2 } from "node:fs";
6722
- import { dirname as dirname4 } from "node:path";
6723
- var namingFile = defineRule59({
7753
+ // src/rules/naming/naming-file.ts
7754
+ import { defineRule as defineRule67 } from "@oxlint/plugins";
7755
+ import { readdirSync as readdirSync2 } from "node:fs";
7756
+ import { dirname as dirname5 } from "node:path";
7757
+ var namingFile = defineRule67({
7758
+ meta: {
7759
+ type: "problem",
7760
+ schema: {
7761
+ type: "array",
7762
+ items: {
7763
+ type: "object",
7764
+ additionalProperties: false,
7765
+ properties: {
7766
+ pattern: optionsPatternSchema,
7767
+ message: { type: "string" },
7768
+ flags: { type: "string" },
7769
+ unique: { type: "string", minLength: 1 },
7770
+ template: optionsPatternSchema,
7771
+ placeholder: { type: "string" }
7772
+ },
7773
+ required: ["message"],
7774
+ anyOf: [{ required: ["pattern"] }, { required: ["template"] }]
7775
+ }
7776
+ },
7777
+ messages: {
7778
+ mismatch: "{{message}}",
7779
+ duplicate: "'{{key}}' is also used by {{others}}; this part of the file name must be unique in its folder."
7780
+ }
7781
+ },
7782
+ createOnce(context) {
7783
+ return {
7784
+ Program(program) {
7785
+ const basename3 = namingBasename(context.filename);
7786
+ let siblings;
7787
+ for (const { pattern, message, flags, unique, template, placeholder } of optionsAll(context)) {
7788
+ const patterns = optionsPatternsWithEmpty(pattern, flags);
7789
+ const path = context.filename.replaceAll("\\", "/").split("/");
7790
+ const templates = [template ?? []].flat();
7791
+ const fits = templates.some((item) => namingTemplatePattern(item, placeholder)?.test(path.slice(-item.split("/").length).join("/")));
7792
+ if (pattern !== undefined && !optionsPatternsTest(patterns, basename3) || templates.length > 0 && !fits) {
7793
+ context.report({
7794
+ node: program,
7795
+ messageId: "mismatch",
7796
+ data: { message }
7797
+ });
7798
+ }
7799
+ if (unique === undefined) {
7800
+ continue;
7801
+ }
7802
+ const matcher = new RegExp(unique);
7803
+ const keyOf = (name) => {
7804
+ const match = matcher.exec(name);
7805
+ return match ? match.at(1) ?? match[0] : undefined;
7806
+ };
7807
+ const key = keyOf(basename3);
7808
+ if (key === undefined) {
7809
+ continue;
7810
+ }
7811
+ if (!siblings) {
7812
+ try {
7813
+ siblings = readdirSync2(dirname5(context.filename), { withFileTypes: true }).flatMap((entry) => entry.isFile() ? [entry.name] : []);
7814
+ } catch {
7815
+ siblings = [];
7816
+ }
7817
+ }
7818
+ const others = siblings.filter((name) => name !== basename3 && optionsPatternsTest(patterns, name) && keyOf(name) === key);
7819
+ if (others.length > 0) {
7820
+ context.report({
7821
+ node: program,
7822
+ messageId: "duplicate",
7823
+ data: { key, others: others.join(", ") }
7824
+ });
7825
+ }
7826
+ }
7827
+ }
7828
+ };
7829
+ }
7830
+ });
7831
+
7832
+ // src/rules/naming/naming-folder.ts
7833
+ import { defineRule as defineRule68 } from "@oxlint/plugins";
7834
+ import { posix as posix2 } from "node:path";
7835
+ var namingFolder = defineRule68({
7836
+ meta: {
7837
+ type: "problem",
7838
+ schema: [
7839
+ {
7840
+ type: "object",
7841
+ additionalProperties: false,
7842
+ properties: {
7843
+ singularize: { type: "string", enum: ["none", "trailing-s"] },
7844
+ separators: { type: "array", items: { type: "string" } },
7845
+ after: { type: "string" },
7846
+ prefixes: { type: "array", items: { type: "string", minLength: 1 } }
7847
+ }
7848
+ }
7849
+ ],
7850
+ messages: {
7851
+ prefix: "File '{{file}}' must start with '{{expected}}' to match its domain folder '{{folder}}'."
7852
+ }
7853
+ },
7854
+ createOnce(context) {
7855
+ return {
7856
+ Program(program) {
7857
+ const { singularize = "trailing-s", separators = ["-"], after, prefixes: stripPrefixes = [] } = optionsFirst(context, {});
7858
+ const all = posix2.dirname(context.filename.replaceAll("\\", "/")).split("/").filter((part) => part && part !== ".");
7859
+ const parent = [all.at(-1) ?? ""];
7860
+ let folders = after ? undefined : parent;
7861
+ const marker = (after ?? "").split("/").filter(Boolean);
7862
+ for (let index = all.length - marker.length;marker.length > 0 && index >= 0 && !folders; index -= 1) {
7863
+ if (marker.every((part, offset) => all[index + offset] === part)) {
7864
+ folders = all.slice(index + marker.length);
7865
+ }
7866
+ }
7867
+ if (!folders) {
7868
+ return;
7869
+ }
7870
+ const resolved = folders.length > 0 ? folders : parent;
7871
+ const last = resolved.at(-1) ?? "";
7872
+ const joiner = separators[0] ?? "-";
7873
+ const singular = singularize === "trailing-s" && last.endsWith("s") && last.length > 1;
7874
+ const prefixes = [
7875
+ resolved.join(joiner),
7876
+ ...singular ? [[...resolved.slice(0, -1), last.slice(0, -1)].join(joiner)] : []
7877
+ ].filter(Boolean);
7878
+ const file = namingBasename(context.filename);
7879
+ const rawStem = file.replace(/\.(tsx?|jsx?)$/, "");
7880
+ const strip = stripPrefixes.find((prefix) => rawStem.startsWith(prefix));
7881
+ const stem = strip ? rawStem.slice(strip.length) : rawStem;
7882
+ if (prefixes.some((prefix) => stem === prefix || separators.some((separator) => stem.startsWith(`${prefix}${separator}`)))) {
7883
+ return;
7884
+ }
7885
+ context.report({
7886
+ node: program,
7887
+ messageId: "prefix",
7888
+ data: {
7889
+ file,
7890
+ folder: resolved.join("/"),
7891
+ expected: prefixes.map((prefix) => `${stripPrefixes[0] ?? ""}${prefix}`).join(" or ")
7892
+ }
7893
+ });
7894
+ }
7895
+ };
7896
+ }
7897
+ });
7898
+
7899
+ // src/rules/naming/naming-imports.ts
7900
+ import { defineRule as defineRule69 } from "@oxlint/plugins";
7901
+ var namingImports = defineRule69({
7902
+ meta: {
7903
+ type: "problem",
7904
+ schema: [
7905
+ {
7906
+ type: "object",
7907
+ additionalProperties: false,
7908
+ properties: {
7909
+ imports: {
7910
+ type: "array",
7911
+ items: {
7912
+ type: "object",
7913
+ additionalProperties: false,
7914
+ properties: {
7915
+ source: { type: "string" },
7916
+ name: optionsPatternSchema,
7917
+ namespace: { type: "boolean" },
7918
+ placeholder: { type: "string" }
7919
+ },
7920
+ required: ["source", "name"]
7921
+ }
7922
+ }
7923
+ },
7924
+ required: ["imports"]
7925
+ }
7926
+ ],
7927
+ messages: {
7928
+ name: "Import '{{source}}' as '{{expected}}', not '{{actual}}'.",
7929
+ namespace: "Import '{{source}}' as a namespace: import * as {{expected}}."
7930
+ }
7931
+ },
7932
+ createOnce(context) {
7933
+ let entries = [];
7934
+ return {
7935
+ before() {
7936
+ entries = optionsFirst(context, { imports: [] }).imports.flatMap((entry) => {
7937
+ const pattern = namingTemplatePattern(entry.source, entry.placeholder ?? "[^/]+");
7938
+ return pattern ? [{ pattern, names: [entry.name].flat(), namespace: entry.namespace !== false }] : [];
7939
+ });
7940
+ if (entries.length === 0) {
7941
+ return false;
7942
+ }
7943
+ },
7944
+ ImportDeclaration(node) {
7945
+ const source = node.source.value;
7946
+ for (const entry of entries) {
7947
+ const match = entry.pattern.exec(source);
7948
+ if (!match) {
7949
+ continue;
7950
+ }
7951
+ const names = entry.names.map((template) => namingExpand(template, { ...match.groups }, true));
7952
+ const expected = names.join(" or ");
7953
+ for (const specifier of node.specifiers) {
7954
+ if (specifier.type !== "ImportNamespaceSpecifier" && entry.namespace) {
7955
+ context.report({
7956
+ node: specifier,
7957
+ messageId: "namespace",
7958
+ data: { source, expected }
7959
+ });
7960
+ } else if (specifier.type !== "ImportSpecifier" && !names.includes(specifier.local.name)) {
7961
+ context.report({
7962
+ node: specifier,
7963
+ messageId: "name",
7964
+ data: { source, expected, actual: specifier.local.name }
7965
+ });
7966
+ }
7967
+ }
7968
+ return;
7969
+ }
7970
+ }
7971
+ };
7972
+ }
7973
+ });
7974
+
7975
+ // src/rules/naming/naming-keys.ts
7976
+ import { defineRule as defineRule70 } from "@oxlint/plugins";
7977
+ function namingKeysNames(value) {
7978
+ const dotted = astDottedName(value);
7979
+ return dotted === undefined ? [] : [...new Set([dotted.split(".").at(-1) ?? dotted, dotted.split(".")[0]])];
7980
+ }
7981
+ function namingKeysSingular(key, options) {
7982
+ return options.singularize === "trailing-s" && key.length > 1 && key.endsWith("s") ? [key, key.slice(0, -1)] : [key];
7983
+ }
7984
+ function namingKeysSources(options, captures, keys) {
7985
+ const last = keys.at(-1) ?? "";
7986
+ return [options.name ?? []].flat().flatMap((source) => namingKeysSingular(last, options).map((key) => {
7987
+ const values = { ...captures, path: keys.join("-"), key };
7988
+ return namingExpand(source, values, true, optionsEscape);
7989
+ }));
7990
+ }
7991
+ function namingKeysFunction(value) {
7992
+ if (astIsInlineFunction(value)) {
7993
+ return value;
7994
+ }
7995
+ return value.type === "CallExpression" ? value.arguments.find(astIsInlineFunction) ?? namingKeysFunction(value.callee) : undefined;
7996
+ }
7997
+ function namingKeysYielded(node) {
7998
+ let found;
7999
+ astSome(node, (candidate) => {
8000
+ const call = candidate.type === "YieldExpression" && candidate.delegate && candidate.argument ? astUnwrap(candidate.argument) : undefined;
8001
+ found ??= call?.type === "CallExpression" ? call : undefined;
8002
+ return found !== undefined;
8003
+ });
8004
+ return found;
8005
+ }
8006
+ function namingKeysAssigned(fn, returned) {
8007
+ let found;
8008
+ astSome(fn.body, (candidate) => {
8009
+ const local = candidate.type === "VariableDeclarator" && candidate.id.type === "Identifier" && candidate.init ? candidate.id.name : undefined;
8010
+ const used = local !== undefined && astSome(returned, (node) => node.type === "Identifier" && node.name === local);
8011
+ found ??= used && candidate.type === "VariableDeclarator" && candidate.init ? namingKeysYielded(candidate.init) : undefined;
8012
+ return found !== undefined;
8013
+ });
8014
+ return found;
8015
+ }
8016
+ function namingKeysCalled(fn) {
8017
+ const returned = astReturnExpressions(fn).at(-1);
8018
+ if (!returned) {
8019
+ return;
8020
+ }
8021
+ const plain = astUnwrap(returned);
8022
+ const direct = !fn.generator && plain.type === "CallExpression" ? plain : undefined;
8023
+ const call = namingKeysYielded(returned) ?? namingKeysAssigned(fn, returned) ?? direct;
8024
+ return call ? astDottedName(call.callee) : undefined;
8025
+ }
8026
+ var namingKeys = defineRule70({
8027
+ meta: {
8028
+ type: "problem",
8029
+ schema: [
8030
+ {
8031
+ type: "object",
8032
+ additionalProperties: false,
8033
+ properties: {
8034
+ callees: { type: "array", items: { type: "string" }, minItems: 1 },
8035
+ exports: optionsPatternSchema,
8036
+ name: optionsPatternSchema,
8037
+ keys: optionsPatternSchema,
8038
+ inline: { type: "boolean" },
8039
+ delegate: { type: "boolean" },
8040
+ allow: optionsPatternSchema,
8041
+ singularize: { type: "string", enum: ["none", "trailing-s"] }
8042
+ },
8043
+ anyOf: [{ required: ["callees"] }, { required: ["exports"] }]
8044
+ }
8045
+ ],
8046
+ messages: {
8047
+ name: "'{{name}}' under key '{{path}}' must match {{pattern}}.",
8048
+ key: "Key '{{key}}' in '{{path}}' must match {{pattern}}.",
8049
+ inline: "Bind a named function under '{{path}}', not an inline one.",
8050
+ delegate: "'{{path}}' must return a call to '{{path}}' on its service, not '{{called}}'."
8051
+ }
8052
+ },
8053
+ createOnce(context) {
8054
+ let options = {};
8055
+ let captures = {};
8056
+ let callees = [];
8057
+ let exported = [];
8058
+ let allowed = [];
8059
+ let keyPatterns = [];
8060
+ function checkValue(value, keys) {
8061
+ const path = keys.join(".");
8062
+ const fn = namingKeysFunction(value);
8063
+ const names = namingKeysNames(value);
8064
+ const sources = namingKeysSources(options, captures, keys);
8065
+ const called = options.delegate && fn ? namingKeysCalled(fn) : undefined;
8066
+ if (fn && astIsInlineFunction(value) && options.inline === false) {
8067
+ context.report({
8068
+ node: value,
8069
+ messageId: "inline",
8070
+ data: { path }
8071
+ });
8072
+ }
8073
+ if (called !== undefined && called.split(".").slice(1).join(".") !== path) {
8074
+ context.report({
8075
+ node: value,
8076
+ messageId: "delegate",
8077
+ data: { path, called }
8078
+ });
8079
+ }
8080
+ if (sources.length > 0 && names.length > 0 && !names.some((name) => optionsPatternsTest(optionsPatterns(sources), name))) {
8081
+ context.report({
8082
+ node: value,
8083
+ messageId: "name",
8084
+ data: { name: names[0], path, pattern: optionsPatternLabel(sources) }
8085
+ });
8086
+ }
8087
+ }
8088
+ function check(object, path) {
8089
+ for (const property of object.properties) {
8090
+ const key = declarationsPropertyName(property);
8091
+ const keys = key === undefined ? path : [...path, key];
8092
+ if (property.type !== "Property" || key === undefined || optionsPatternsTest(allowed, keys.join("."))) {
8093
+ continue;
8094
+ }
8095
+ if (keyPatterns.length > 0 && !optionsPatternsTest(keyPatterns, key)) {
8096
+ context.report({
8097
+ node: property.key,
8098
+ messageId: "key",
8099
+ data: { key, path: keys.join("."), pattern: optionsPatternLabel(options.keys) }
8100
+ });
8101
+ }
8102
+ if (property.value.type === "ObjectExpression") {
8103
+ check(property.value, keys);
8104
+ } else {
8105
+ checkValue(property.value, keys);
8106
+ }
8107
+ }
8108
+ }
8109
+ return {
8110
+ before() {
8111
+ options = optionsFirst(context, {});
8112
+ captures = namingCaptures(context.filename) ?? {};
8113
+ callees = optionsWildcards(options.callees);
8114
+ exported = optionsPatterns(options.exports);
8115
+ allowed = optionsPatterns(options.allow);
8116
+ keyPatterns = optionsPatterns(options.keys);
8117
+ if (callees.length === 0 && exported.length === 0) {
8118
+ return false;
8119
+ }
8120
+ },
8121
+ CallExpression(node) {
8122
+ const first = node.arguments.at(0);
8123
+ if (first?.type === "ObjectExpression" && optionsPatternsTest(callees, astDottedName(node.callee) ?? "")) {
8124
+ check(first, []);
8125
+ }
8126
+ },
8127
+ VariableDeclarator(node) {
8128
+ const statement = node.parent.parent;
8129
+ const top = statement?.type === "ExportNamedDeclaration" && statement.parent.type === "Program";
8130
+ const value = node.init ? astUnwrap(node.init) : null;
8131
+ if (top && node.id.type === "Identifier" && value?.type === "ObjectExpression" && optionsPatternsTest(exported, node.id.name)) {
8132
+ check(value, []);
8133
+ }
8134
+ }
8135
+ };
8136
+ }
8137
+ });
8138
+
8139
+ // src/rules/naming/naming-labels.ts
8140
+ import { defineRule as defineRule71 } from "@oxlint/plugins";
8141
+ function namingLabelsRoot(node) {
8142
+ return node.type === "CallExpression" ? namingLabelsRoot(node.callee) : astDottedName(node);
8143
+ }
8144
+ function namingLabelsBinding(node, within) {
8145
+ const parent = node.parent;
8146
+ if (!parent) {
8147
+ return;
8148
+ }
8149
+ if (parent.type === "VariableDeclarator" && parent.init === node) {
8150
+ return parent.id.type === "Identifier" ? parent.id.name : undefined;
8151
+ }
8152
+ if ((parent.type === "ClassDeclaration" || parent.type === "ClassExpression") && parent.superClass === node) {
8153
+ return parent.id?.name;
8154
+ }
8155
+ if (parent.type === "CallExpression" && parent.callee === node || parent.type === "MemberExpression" && parent.object === node) {
8156
+ return namingLabelsBinding(parent, within);
8157
+ }
8158
+ const owner = parent.type === "CallExpression" && parent.callee.type === "MemberExpression" ? astDottedName(parent.callee) : undefined;
8159
+ return owner !== undefined && optionsPatternsTest(within, owner) ? owner.split(".").at(-2) : undefined;
8160
+ }
8161
+ function namingLabelsTarget(first, property) {
8162
+ if (property === undefined) {
8163
+ return first;
8164
+ }
8165
+ const held = first.type === "ObjectExpression" ? first.properties.find((item) => declarationsPropertyName(item) === property) : undefined;
8166
+ return held?.type === "Property" ? held.value : undefined;
8167
+ }
8168
+ function namingLabelsPlain(text, loose) {
8169
+ return loose ? text.replaceAll(/[._\-/\s]/g, "").toLowerCase() : text;
8170
+ }
8171
+ function namingLabelsExpected({ entry, drop }, captures, binding) {
8172
+ const name = drop.reduce((text, pattern) => text.replace(pattern, ""), binding);
8173
+ return [entry.name ?? []].flat().map((template) => namingExpand(template, { ...captures, name }, false));
8174
+ }
8175
+ var namingLabels = defineRule71({
8176
+ meta: {
8177
+ type: "problem",
8178
+ schema: [
8179
+ {
8180
+ type: "object",
8181
+ additionalProperties: false,
8182
+ properties: {
8183
+ labels: {
8184
+ type: "array",
8185
+ items: {
8186
+ type: "object",
8187
+ additionalProperties: false,
8188
+ properties: {
8189
+ callee: optionsPatternSchema,
8190
+ property: { type: "string" },
8191
+ name: optionsPatternSchema,
8192
+ pattern: optionsPatternSchema,
8193
+ drop: optionsPatternSchema,
8194
+ loose: { type: "boolean" },
8195
+ within: optionsPatternSchema
8196
+ },
8197
+ required: ["callee"],
8198
+ anyOf: [{ required: ["name"] }, { required: ["pattern"] }]
8199
+ }
8200
+ }
8201
+ },
8202
+ required: ["labels"]
8203
+ }
8204
+ ],
8205
+ messages: {
8206
+ name: "'{{label}}' must be '{{expected}}', after the name '{{name}}'.",
8207
+ pattern: "'{{label}}' must match {{pattern}}."
8208
+ }
8209
+ },
8210
+ createOnce(context) {
8211
+ let entries = [];
8212
+ let captures = {};
8213
+ return {
8214
+ before() {
8215
+ const paths = namingCaptures(context.filename) ?? {};
8216
+ captures = paths;
8217
+ entries = optionsFirst(context, { labels: [] }).labels.map((entry) => ({
8218
+ callees: optionsWildcards(entry.callee),
8219
+ entry,
8220
+ drop: optionsPatterns(entry.drop),
8221
+ within: optionsWildcards(entry.within),
8222
+ patterns: [entry.pattern ?? []].flat().map((source) => namingExpand(source, paths, true, optionsEscape))
8223
+ }));
8224
+ if (entries.length === 0) {
8225
+ return false;
8226
+ }
8227
+ },
8228
+ CallExpression(node) {
8229
+ const root = namingLabelsRoot(node.callee);
8230
+ const first = node.arguments.at(0);
8231
+ if (root === undefined || !first) {
8232
+ return;
8233
+ }
8234
+ for (const compiled of entries) {
8235
+ const target = optionsPatternsTest(compiled.callees, root) ? namingLabelsTarget(first, compiled.entry.property) : undefined;
8236
+ const label = target ? astStaticString(target) : undefined;
8237
+ if (!target || label === undefined) {
8238
+ continue;
8239
+ }
8240
+ if (compiled.patterns.length > 0 && !optionsPatternsTest(optionsPatterns(compiled.patterns), label)) {
8241
+ context.report({
8242
+ node: target,
8243
+ messageId: "pattern",
8244
+ data: { label, pattern: optionsPatternLabel(compiled.patterns) }
8245
+ });
8246
+ }
8247
+ const binding = namingLabelsBinding(node, compiled.within);
8248
+ const expected = binding === undefined ? [] : namingLabelsExpected(compiled, captures, binding);
8249
+ const plain = expected.map((item) => namingLabelsPlain(item, compiled.entry.loose));
8250
+ if (binding !== undefined && expected.length > 0 && !plain.includes(namingLabelsPlain(label, compiled.entry.loose))) {
8251
+ context.report({
8252
+ node: target,
8253
+ messageId: "name",
8254
+ data: { label, expected: expected.join(" or "), name: binding }
8255
+ });
8256
+ }
8257
+ }
8258
+ }
8259
+ };
8260
+ }
8261
+ });
8262
+
8263
+ // src/rules/naming/naming-pairs.ts
8264
+ import { defineRule as defineRule72 } from "@oxlint/plugins";
8265
+ function namingPairsArguments(node, found) {
8266
+ if (node.type === "MemberExpression") {
8267
+ return namingPairsArguments(node.object, found);
8268
+ }
8269
+ if (node.type !== "CallExpression") {
8270
+ return found;
8271
+ }
8272
+ const method = node.callee.type === "MemberExpression" ? astMemberName(node.callee) : undefined;
8273
+ const first = node.arguments.at(0);
8274
+ if (method !== undefined && first && !found.has(method)) {
8275
+ found.set(method, first);
8276
+ }
8277
+ return namingPairsArguments(node.callee, found);
8278
+ }
8279
+ var namingPairs = defineRule72({
6724
8280
  meta: {
6725
8281
  type: "problem",
6726
- schema: {
6727
- type: "array",
6728
- items: {
8282
+ schema: [
8283
+ {
6729
8284
  type: "object",
6730
8285
  additionalProperties: false,
6731
8286
  properties: {
6732
- pattern: optionsPatternSchema,
6733
- message: { type: "string" },
6734
- flags: { type: "string" },
6735
- unique: { type: "string", minLength: 1 }
8287
+ pairs: {
8288
+ type: "array",
8289
+ items: {
8290
+ type: "object",
8291
+ additionalProperties: false,
8292
+ properties: {
8293
+ methods: { type: "array", items: { type: "string" }, minItems: 2, maxItems: 2 },
8294
+ strip: optionsPatternSchema,
8295
+ allow: optionsPatternSchema
8296
+ },
8297
+ required: ["methods"]
8298
+ }
8299
+ }
6736
8300
  },
6737
- required: ["pattern", "message"]
8301
+ required: ["pairs"]
6738
8302
  }
6739
- },
8303
+ ],
6740
8304
  messages: {
6741
- mismatch: "{{message}}",
6742
- duplicate: "'{{key}}' is also used by {{others}}; this part of the file name must be unique in its folder."
8305
+ pair: "'{{second}}' in .{{secondMethod}}() must share its name with '{{first}}' in .{{firstMethod}}()."
6743
8306
  }
6744
8307
  },
6745
8308
  createOnce(context) {
6746
8309
  return {
6747
- Program(program) {
6748
- const basename3 = namingBasename(context.filename);
6749
- let siblings;
6750
- for (const { pattern, message, flags, unique } of optionsAll(context)) {
6751
- const patterns = optionsPatternsWithEmpty(pattern, flags);
6752
- if (!optionsPatternsTest(patterns, basename3)) {
6753
- context.report({
6754
- node: program,
6755
- messageId: "mismatch",
6756
- data: { message }
6757
- });
6758
- }
6759
- if (unique === undefined) {
6760
- continue;
6761
- }
6762
- const matcher = new RegExp(unique);
6763
- const keyOf = (name) => {
6764
- const match = matcher.exec(name);
6765
- return match ? match.at(1) ?? match[0] : undefined;
6766
- };
6767
- const key = keyOf(basename3);
6768
- if (key === undefined) {
8310
+ CallExpression(node) {
8311
+ const parent = node.parent;
8312
+ const chained = parent.type === "MemberExpression" && parent.object === node || parent.type === "CallExpression" && parent.callee === node;
8313
+ const { pairs } = optionsFirst(context, { pairs: [] });
8314
+ if (chained || pairs.length === 0) {
8315
+ return;
8316
+ }
8317
+ const found = namingPairsArguments(node, new Map);
8318
+ for (const { methods: [firstMethod, secondMethod], strip, allow } of pairs) {
8319
+ const firstNode = found.get(firstMethod);
8320
+ const secondNode = found.get(secondMethod);
8321
+ const first = firstNode ? astDottedName(firstNode)?.split(".").at(-1) : undefined;
8322
+ const second = secondNode ? astDottedName(secondNode)?.split(".").at(-1) : undefined;
8323
+ const skipped = optionsPatterns(allow);
8324
+ const stem = (name) => optionsPatterns(strip).reduce((text, pattern) => text.replace(pattern, ""), name);
8325
+ if (!secondNode || first === undefined || second === undefined || optionsPatternsTest(skipped, first) || optionsPatternsTest(skipped, second)) {
6769
8326
  continue;
6770
8327
  }
6771
- if (!siblings) {
6772
- try {
6773
- siblings = readdirSync2(dirname4(context.filename), { withFileTypes: true }).flatMap((entry) => entry.isFile() ? [entry.name] : []);
6774
- } catch {
6775
- siblings = [];
6776
- }
6777
- }
6778
- const others = siblings.filter((name) => name !== basename3 && optionsPatternsTest(patterns, name) && keyOf(name) === key);
6779
- if (others.length > 0) {
8328
+ if (stem(first) !== stem(second)) {
6780
8329
  context.report({
6781
- node: program,
6782
- messageId: "duplicate",
6783
- data: { key, others: others.join(", ") }
8330
+ node: secondNode,
8331
+ messageId: "pair",
8332
+ data: { first, second, firstMethod, secondMethod }
6784
8333
  });
6785
8334
  }
6786
8335
  }
@@ -6789,113 +8338,175 @@ var namingFile = defineRule59({
6789
8338
  }
6790
8339
  });
6791
8340
 
6792
- // src/rules/naming/naming-folder.ts
6793
- import { defineRule as defineRule60 } from "@oxlint/plugins";
6794
- import { posix as posix3 } from "node:path";
6795
- var namingFolder = defineRule60({
8341
+ // src/rules/naming/naming-types.ts
8342
+ import { defineRule as defineRule73 } from "@oxlint/plugins";
8343
+ var namingTypes = defineRule73({
8344
+ meta: {
8345
+ type: "problem",
8346
+ schema: [{ type: "object", additionalProperties: false, properties: { pattern: optionsPatternSchema, message: { type: "string" } } }],
8347
+ messages: { forbidden: "{{message}}" }
8348
+ },
8349
+ createOnce(context) {
8350
+ let patterns = [];
8351
+ let message = "";
8352
+ return {
8353
+ before() {
8354
+ const options = optionsFirst(context, {});
8355
+ patterns = optionsPatterns(options.pattern ?? "^[A-Z][a-zA-Z0-9]*$");
8356
+ message = options.message ?? "Use PascalCase for interfaces.";
8357
+ },
8358
+ TSInterfaceDeclaration(node) {
8359
+ if (!optionsPatternsTest(patterns, node.id.name)) {
8360
+ context.report({
8361
+ node,
8362
+ messageId: "forbidden",
8363
+ data: { message }
8364
+ });
8365
+ }
8366
+ }
8367
+ };
8368
+ }
8369
+ });
8370
+
8371
+ // src/rules/react/react-dynamic-classes.ts
8372
+ import { defineRule as defineRule74 } from "@oxlint/plugins";
8373
+ var reactDynamicClasses = defineRule74({
6796
8374
  meta: {
6797
8375
  type: "problem",
6798
8376
  schema: [
6799
8377
  {
6800
8378
  type: "object",
6801
8379
  additionalProperties: false,
6802
- properties: {
6803
- singularize: { type: "string", enum: ["none", "trailing-s"] },
6804
- separators: { type: "array", items: { type: "string" } },
6805
- after: { type: "string" },
6806
- prefixes: { type: "array", items: { type: "string", minLength: 1 } }
6807
- }
8380
+ properties: { attributes: { type: "array", items: { type: "string" } }, message: { type: "string" } }
6808
8381
  }
6809
8382
  ],
6810
- messages: {
6811
- prefix: "File '{{file}}' must start with '{{expected}}' to match its domain folder '{{folder}}'."
6812
- }
8383
+ messages: { forbidden: "{{message}}" }
6813
8384
  },
6814
8385
  createOnce(context) {
6815
- return {
6816
- Program(program) {
6817
- const { singularize = "trailing-s", separators = ["-"], after, prefixes: stripPrefixes = [] } = optionsFirst(context, {});
6818
- const all = posix3.dirname(context.filename.replaceAll("\\", "/")).split("/").filter((part) => part && part !== ".");
6819
- const parent = [all.at(-1) ?? ""];
6820
- let folders = after ? undefined : parent;
6821
- const marker = (after ?? "").split("/").filter(Boolean);
6822
- for (let index = all.length - marker.length;marker.length > 0 && index >= 0 && !folders; index -= 1) {
6823
- if (marker.every((part, offset) => all[index + offset] === part)) {
6824
- folders = all.slice(index + marker.length);
6825
- }
6826
- }
6827
- if (!folders) {
8386
+ let attributes = new Set;
8387
+ let message = "";
8388
+ const check = (node) => {
8389
+ for (let parent = node.parent;parent; parent = parent.parent) {
8390
+ if (parent.type === "JSXAttribute" && parent.name.type === "JSXIdentifier" && attributes.has(parent.name.name)) {
8391
+ context.report({
8392
+ node,
8393
+ messageId: "forbidden",
8394
+ data: { message }
8395
+ });
6828
8396
  return;
6829
8397
  }
6830
- const resolved = folders.length > 0 ? folders : parent;
6831
- const last = resolved.at(-1) ?? "";
6832
- const joiner = separators[0] ?? "-";
6833
- const singular = singularize === "trailing-s" && last.endsWith("s") && last.length > 1;
6834
- const prefixes = [
6835
- resolved.join(joiner),
6836
- ...singular ? [[...resolved.slice(0, -1), last.slice(0, -1)].join(joiner)] : []
6837
- ].filter(Boolean);
6838
- const file = namingBasename(context.filename);
6839
- const rawStem = file.replace(/\.(tsx?|jsx?)$/, "");
6840
- const strip = stripPrefixes.find((prefix) => rawStem.startsWith(prefix));
6841
- const stem = strip ? rawStem.slice(strip.length) : rawStem;
6842
- if (prefixes.some((prefix) => stem === prefix || separators.some((separator) => stem.startsWith(`${prefix}${separator}`)))) {
8398
+ }
8399
+ };
8400
+ return {
8401
+ before() {
8402
+ const options = optionsFirst(context, {});
8403
+ attributes = new Set(options.attributes ?? ["className"]);
8404
+ message = options.message ?? "Write complete class names instead of constructing them dynamically.";
8405
+ },
8406
+ "JSXAttribute TemplateLiteral[expressions.length>0]": check,
8407
+ 'JSXAttribute BinaryExpression[operator="+"]': check
8408
+ };
8409
+ }
8410
+ });
8411
+
8412
+ // src/rules/react/react-empty-effect.ts
8413
+ import { defineRule as defineRule75 } from "@oxlint/plugins";
8414
+ var reactEmptyEffect = defineRule75({
8415
+ meta: {
8416
+ type: "problem",
8417
+ schema: [
8418
+ {
8419
+ type: "object",
8420
+ additionalProperties: false,
8421
+ properties: { hooks: { type: "array", items: { type: "string" } }, message: { type: "string" } }
8422
+ }
8423
+ ],
8424
+ messages: { forbidden: "{{message}}" }
8425
+ },
8426
+ createOnce(context) {
8427
+ let hooks = new Set;
8428
+ let message = "";
8429
+ return {
8430
+ before() {
8431
+ const options = optionsFirst(context, {});
8432
+ hooks = new Set(options.hooks ?? ["useEffect", "useLayoutEffect", "React.useEffect", "React.useLayoutEffect"]);
8433
+ message = options.message ?? "Remove the empty effect.";
8434
+ },
8435
+ CallExpression(node) {
8436
+ if (!hooks.has(astDottedName(node.callee) ?? "")) {
6843
8437
  return;
6844
8438
  }
6845
- context.report({
6846
- node: program,
6847
- messageId: "prefix",
6848
- data: {
6849
- file,
6850
- folder: resolved.join("/"),
6851
- expected: prefixes.map((prefix) => `${stripPrefixes[0] ?? ""}${prefix}`).join(" or ")
8439
+ for (const argument of node.arguments) {
8440
+ if (astIsInlineFunction(argument) && argument.body?.type === "BlockStatement" && argument.body.body.length === 0) {
8441
+ context.report({
8442
+ node: argument.body,
8443
+ messageId: "forbidden",
8444
+ data: { message }
8445
+ });
6852
8446
  }
6853
- });
8447
+ }
6854
8448
  }
6855
8449
  };
6856
8450
  }
6857
8451
  });
6858
8452
 
6859
- // src/rules/naming/naming-types.ts
6860
- var namingTypes = astSelectorRule("TSInterfaceDeclaration:not([id.name=/^[A-Z][a-zA-Z0-9]*$/])", "Use PascalCase for interfaces.");
6861
-
6862
- // src/rules/react/react-dynamic-classes.ts
6863
- var reactDynamicClasses = astSelectorRule(':matches(JSXAttribute[name.name="className"] TemplateLiteral[expressions.length>0], JSXAttribute[name.name="className"] BinaryExpression[operator="+"])', "Write complete class names instead of constructing them dynamically.");
6864
-
6865
- // src/rules/react/react-empty-effect.ts
6866
- 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.");
6867
-
6868
8453
  // src/rules/react/react-memo.ts
6869
- import { defineRule as defineRule61 } from "@oxlint/plugins";
6870
- var reactMemoNames = ["memo", "useMemo", "useCallback"];
6871
- var reactMemo = defineRule61({
8454
+ import { defineRule as defineRule76 } from "@oxlint/plugins";
8455
+ var reactMemo = defineRule76({
6872
8456
  meta: {
6873
8457
  type: "problem",
6874
- schema: [],
8458
+ schema: [
8459
+ {
8460
+ type: "object",
8461
+ additionalProperties: false,
8462
+ properties: {
8463
+ names: { type: "array", items: { type: "string" } },
8464
+ sources: { type: "array", items: { type: "string" } },
8465
+ message: { type: "string" }
8466
+ }
8467
+ }
8468
+ ],
6875
8469
  messages: {
6876
- memo: "Use the React Compiler rather than manual memoization."
8470
+ memo: "{{message}}"
6877
8471
  }
6878
8472
  },
6879
8473
  createOnce(context) {
8474
+ let names = [];
8475
+ let sources = [];
8476
+ let message = "";
6880
8477
  return {
8478
+ before() {
8479
+ const options = optionsFirst(context, {});
8480
+ names = options.names ?? ["memo", "useMemo", "useCallback"];
8481
+ sources = options.sources ?? ["react"];
8482
+ message = options.message ?? "Use the React Compiler rather than manual memoization.";
8483
+ },
6881
8484
  ImportDeclaration(node) {
6882
- if (node.source.value !== "react") {
8485
+ if (!sources.includes(node.source.value)) {
6883
8486
  return;
6884
8487
  }
6885
8488
  for (const specifier of node.specifiers) {
6886
- if (specifier.type === "ImportSpecifier" && reactMemoNames.includes(astModuleName(specifier.imported))) {
6887
- context.report({ node: specifier, messageId: "memo" });
8489
+ if (specifier.type === "ImportSpecifier" && names.includes(astModuleName(specifier.imported))) {
8490
+ context.report({
8491
+ node: specifier,
8492
+ messageId: "memo",
8493
+ data: { message }
8494
+ });
6888
8495
  }
6889
8496
  }
6890
8497
  },
6891
8498
  MemberExpression(node) {
6892
- if (node.object.type !== "Identifier" || !reactMemoNames.includes(astMemberName(node) ?? "")) {
8499
+ if (node.object.type !== "Identifier" || !names.includes(boundariesMemberName(node) ?? "")) {
6893
8500
  return;
6894
8501
  }
6895
8502
  const variable = boundariesDeclaredVariable(context.sourceCode.getScope(node), node.object.name);
6896
- const isReact = variable ? variable.defs.some((def) => def.type === "ImportBinding" && def.node.type !== "ImportSpecifier" && def.parent?.type === "ImportDeclaration" && def.parent.source.value === "react") : node.object.name === "React";
8503
+ const isReact = variable ? variable.defs.some((def) => def.type === "ImportBinding" && (def.node.type !== "ImportSpecifier" || astModuleName(def.node.imported) === "default") && def.parent?.type === "ImportDeclaration" && sources.includes(def.parent.source.value)) : node.object.name === "React" && sources.includes("react");
6897
8504
  if (isReact) {
6898
- context.report({ node, messageId: "memo" });
8505
+ context.report({
8506
+ node,
8507
+ messageId: "memo",
8508
+ data: { message }
8509
+ });
6899
8510
  }
6900
8511
  }
6901
8512
  };
@@ -6903,7 +8514,7 @@ var reactMemo = defineRule61({
6903
8514
  });
6904
8515
 
6905
8516
  // src/rules/react/react-props.ts
6906
- import { defineRule as defineRule62 } from "@oxlint/plugins";
8517
+ import { defineRule as defineRule77 } from "@oxlint/plugins";
6907
8518
  function reactPropsTarget(node) {
6908
8519
  if (node?.type === "FunctionDeclaration" || node?.type === "FunctionExpression" || node?.type === "ArrowFunctionExpression") {
6909
8520
  return { fn: node };
@@ -6930,7 +8541,7 @@ function reactIsPassThrough(type) {
6930
8541
  const argument = type.typeArguments?.params.length === 1 ? type.typeArguments.params[0] : undefined;
6931
8542
  return name.type === "Identifier" && name.name === "ComponentProps" && (argument?.type === "TSTypeQuery" || argument?.type === "TSLiteralType" && argument.literal.type === "Literal" && typeof argument.literal.value === "string");
6932
8543
  }
6933
- var reactProps = defineRule62({
8544
+ var reactProps = defineRule77({
6934
8545
  meta: {
6935
8546
  type: "problem",
6936
8547
  schema: [
@@ -7029,8 +8640,8 @@ var reactProps = defineRule62({
7029
8640
  });
7030
8641
 
7031
8642
  // src/rules/react/react-query-props.ts
7032
- import { defineRule as defineRule63 } from "@oxlint/plugins";
7033
- var reactQueryProps = defineRule63({
8643
+ import { defineRule as defineRule78 } from "@oxlint/plugins";
8644
+ var reactQueryProps = defineRule78({
7034
8645
  meta: {
7035
8646
  type: "problem",
7036
8647
  schema: [
@@ -7111,7 +8722,7 @@ var reactQueryProps = defineRule63({
7111
8722
  });
7112
8723
 
7113
8724
  // src/rules/schemas/schemas-inline.ts
7114
- import { defineRule as defineRule64 } from "@oxlint/plugins";
8725
+ import { defineRule as defineRule79 } from "@oxlint/plugins";
7115
8726
 
7116
8727
  // src/utils/schemas.utils.ts
7117
8728
  var schemasElementOptionsSchema = {
@@ -7147,7 +8758,7 @@ function schemasIsAllowedElement(node, allowScalars, namespaces, structuralMetho
7147
8758
  }
7148
8759
 
7149
8760
  // src/rules/schemas/schemas-inline.ts
7150
- var schemasInline = defineRule64({
8761
+ var schemasInline = defineRule79({
7151
8762
  meta: {
7152
8763
  type: "problem",
7153
8764
  schema: [schemasElementOptionsSchema],
@@ -7196,11 +8807,11 @@ var schemasInline = defineRule64({
7196
8807
  });
7197
8808
 
7198
8809
  // src/rules/schemas/schemas-local.ts
7199
- import { defineRule as defineRule65 } from "@oxlint/plugins";
8810
+ import { defineRule as defineRule80 } from "@oxlint/plugins";
7200
8811
  var schemasLocalImportMessage = "Do not import runtime schema constructors from {{source}}; import a named schema instead.";
7201
8812
  var schemasLocalConstructionMessage = "Do not construct schemas locally; import a named schema instead.";
7202
8813
  var schemasLocalCalls = ["config", "prettifyError", "treeifyError", "flattenError", "formatError"];
7203
- var schemasLocal = defineRule65({
8814
+ var schemasLocal = defineRule80({
7204
8815
  meta: {
7205
8816
  type: "problem",
7206
8817
  schema: [
@@ -7349,8 +8960,8 @@ var schemasLocal = defineRule65({
7349
8960
  });
7350
8961
 
7351
8962
  // src/rules/schemas/schemas-rederive.ts
7352
- import { defineRule as defineRule66 } from "@oxlint/plugins";
7353
- var schemasRederive = defineRule66({
8963
+ import { defineRule as defineRule81 } from "@oxlint/plugins";
8964
+ var schemasRederive = defineRule81({
7354
8965
  meta: {
7355
8966
  type: "problem",
7356
8967
  schema: [
@@ -7396,7 +9007,7 @@ var schemasRederive = defineRule66({
7396
9007
  });
7397
9008
 
7398
9009
  // src/rules/schemas/schemas-single-use.ts
7399
- import { defineRule as defineRule67 } from "@oxlint/plugins";
9010
+ import { defineRule as defineRule82 } from "@oxlint/plugins";
7400
9011
  function isScalarSchema(node, namespaces, structuralMethods) {
7401
9012
  if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") {
7402
9013
  return false;
@@ -7410,7 +9021,7 @@ function isScalarSchema(node, namespaces, structuralMethods) {
7410
9021
  }
7411
9022
  return node.callee.object.type === "CallExpression" && isScalarSchema(node.callee.object, namespaces, structuralMethods);
7412
9023
  }
7413
- var schemasSingleUse = defineRule67({
9024
+ var schemasSingleUse = defineRule82({
7414
9025
  meta: {
7415
9026
  type: "suggestion",
7416
9027
  schema: [schemasElementOptionsSchema],
@@ -7486,7 +9097,7 @@ var schemasSingleUse = defineRule67({
7486
9097
  });
7487
9098
 
7488
9099
  // src/rules/tests/tests-assertions.ts
7489
- import { defineRule as defineRule68 } from "@oxlint/plugins";
9100
+ import { defineRule as defineRule83 } from "@oxlint/plugins";
7490
9101
  var testsAssertionsModifiers = new Set(["for", "each", "concurrent", "sequential", "only", "skip"]);
7491
9102
  var testsAssertionsFunctions = [
7492
9103
  "expect",
@@ -7676,7 +9287,7 @@ function isAssertedTest(registration, sourceCode, state) {
7676
9287
  }
7677
9288
  return rows.length > 0 && rows.every((row) => hasAssertion(callback, row.bindings, new Set, state));
7678
9289
  }
7679
- var testsAssertions = defineRule68({
9290
+ var testsAssertions = defineRule83({
7680
9291
  meta: {
7681
9292
  type: "problem",
7682
9293
  schema: [
@@ -7802,8 +9413,8 @@ var testsAssertions = defineRule68({
7802
9413
  });
7803
9414
 
7804
9415
  // src/rules/tests/tests-concurrent-db.ts
7805
- import { defineRule as defineRule69 } from "@oxlint/plugins";
7806
- var testsConcurrentDb = defineRule69({
9416
+ import { defineRule as defineRule84 } from "@oxlint/plugins";
9417
+ var testsConcurrentDb = defineRule84({
7807
9418
  meta: {
7808
9419
  type: "problem",
7809
9420
  schema: [
@@ -7859,7 +9470,7 @@ var testsConcurrentDb = defineRule69({
7859
9470
  });
7860
9471
 
7861
9472
  // src/rules/tests/tests-modifiers.ts
7862
- import { defineRule as defineRule70 } from "@oxlint/plugins";
9473
+ import { defineRule as defineRule85 } from "@oxlint/plugins";
7863
9474
  var testsModifiersNames = ["test", "it", "describe", "suite"];
7864
9475
  var testsModifiersPackages = new Set(["vitest", "bun:test", "@playwright/test", "@jest/globals"]);
7865
9476
  var testsModifiersBanned = ["only", "skip", "todo"];
@@ -7878,7 +9489,7 @@ function modifierName(node) {
7878
9489
  const property = astMemberName(node);
7879
9490
  return object && property ? `${object}.${property}` : undefined;
7880
9491
  }
7881
- var testsModifiers = defineRule70({
9492
+ var testsModifiers = defineRule85({
7882
9493
  meta: {
7883
9494
  type: "problem",
7884
9495
  schema: [
@@ -7932,9 +9543,9 @@ var testsModifiers = defineRule70({
7932
9543
  });
7933
9544
 
7934
9545
  // src/rules/tests/tests-titles.ts
7935
- import { defineRule as defineRule71 } from "@oxlint/plugins";
9546
+ import { defineRule as defineRule86 } from "@oxlint/plugins";
7936
9547
  var testsTitlesModifiers = ["only", "skip", "concurrent", "sequential", "todo", "each", "for"];
7937
- var testsTitles = defineRule71({
9548
+ var testsTitles = defineRule86({
7938
9549
  meta: {
7939
9550
  type: "suggestion",
7940
9551
  schema: [
@@ -8012,11 +9623,11 @@ var testsTitles = defineRule71({
8012
9623
  });
8013
9624
 
8014
9625
  // src/rules/types/types-declarations.ts
8015
- import { defineRule as defineRule72 } from "@oxlint/plugins";
9626
+ import { defineRule as defineRule87 } from "@oxlint/plugins";
8016
9627
  function isValuePattern(source) {
8017
9628
  return /^\^?=/.test(source);
8018
9629
  }
8019
- var typesDeclarations = defineRule72({
9630
+ var typesDeclarations = defineRule87({
8020
9631
  meta: {
8021
9632
  type: "problem",
8022
9633
  schema: [
@@ -8067,8 +9678,8 @@ var typesDeclarations = defineRule72({
8067
9678
  });
8068
9679
 
8069
9680
  // src/rules/types/types-duplicate.ts
8070
- import { defineRule as defineRule73 } from "@oxlint/plugins";
8071
- var typesDuplicate = defineRule73({
9681
+ import { defineRule as defineRule88 } from "@oxlint/plugins";
9682
+ var typesDuplicate = defineRule88({
8072
9683
  meta: {
8073
9684
  type: "suggestion",
8074
9685
  schema: [
@@ -8132,7 +9743,7 @@ var typesDuplicate = defineRule73({
8132
9743
  });
8133
9744
 
8134
9745
  // src/rules/types/types-imported-alias.ts
8135
- import { defineRule as defineRule74 } from "@oxlint/plugins";
9746
+ import { defineRule as defineRule89 } from "@oxlint/plugins";
8136
9747
  function importedAliasTarget(declaration, imports) {
8137
9748
  if (declaration?.type !== "TSTypeAliasDeclaration") {
8138
9749
  return;
@@ -8144,7 +9755,7 @@ function importedAliasTarget(declaration, imports) {
8144
9755
  const { name } = type.typeName;
8145
9756
  return imports.has(name) && !declaration.typeParameters?.params.some((parameter) => parameter.name.name === name) ? name : undefined;
8146
9757
  }
8147
- var typesImportedAlias = defineRule74({
9758
+ var typesImportedAlias = defineRule89({
8148
9759
  meta: {
8149
9760
  type: "problem",
8150
9761
  schema: [],
@@ -8206,7 +9817,7 @@ var typesImportedAlias = defineRule74({
8206
9817
  });
8207
9818
 
8208
9819
  // src/rules/types/types-inline.ts
8209
- import { defineRule as defineRule75 } from "@oxlint/plugins";
9820
+ import { defineRule as defineRule90 } from "@oxlint/plugins";
8210
9821
  function parameterAnnotation(parameter) {
8211
9822
  if (parameter.type === "TSParameterProperty") {
8212
9823
  return parameterAnnotation(parameter.parameter);
@@ -8216,7 +9827,7 @@ function parameterAnnotation(parameter) {
8216
9827
  }
8217
9828
  return parameter.typeAnnotation;
8218
9829
  }
8219
- var typesInline = defineRule75({
9830
+ var typesInline = defineRule90({
8220
9831
  meta: {
8221
9832
  type: "problem",
8222
9833
  schema: [
@@ -8328,11 +9939,38 @@ var typesInline = defineRule75({
8328
9939
  });
8329
9940
 
8330
9941
  // src/rules/types/types-local-alias.ts
8331
- 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.");
9942
+ import { defineRule as defineRule91 } from "@oxlint/plugins";
9943
+ var typesLocalAlias = defineRule91({
9944
+ meta: {
9945
+ type: "problem",
9946
+ schema: [{ type: "object", additionalProperties: false, properties: { allow: optionsPatternSchema, message: { type: "string" } } }],
9947
+ messages: { forbidden: "{{message}}" }
9948
+ },
9949
+ createOnce(context) {
9950
+ let allow = [];
9951
+ let message = "";
9952
+ return {
9953
+ before() {
9954
+ const options = optionsFirst(context, {});
9955
+ allow = optionsPatterns(options.allow);
9956
+ message = options.message ?? "Local component/hook contracts use interfaces named for their owner. Derived or reusable aliases belong in domain types; do not substitute an empty interface.";
9957
+ },
9958
+ TSTypeAliasDeclaration(node) {
9959
+ if (!optionsPatternsTest(allow, node.id.name)) {
9960
+ context.report({
9961
+ node,
9962
+ messageId: "forbidden",
9963
+ data: { message }
9964
+ });
9965
+ }
9966
+ }
9967
+ };
9968
+ }
9969
+ });
8332
9970
 
8333
9971
  // src/rules/types/types-namespace-import.ts
8334
- import { defineRule as defineRule76 } from "@oxlint/plugins";
8335
- var typesNamespaceImport = defineRule76({
9972
+ import { defineRule as defineRule92 } from "@oxlint/plugins";
9973
+ var typesNamespaceImport = defineRule92({
8336
9974
  meta: {
8337
9975
  type: "suggestion",
8338
9976
  schema: [
@@ -8374,8 +10012,8 @@ var typesNamespaceImport = defineRule76({
8374
10012
  });
8375
10013
 
8376
10014
  // src/rules/types/types-service.ts
8377
- import { defineRule as defineRule77 } from "@oxlint/plugins";
8378
- var typesService = defineRule77({
10015
+ import { defineRule as defineRule93 } from "@oxlint/plugins";
10016
+ var typesService = defineRule93({
8379
10017
  meta: {
8380
10018
  type: "problem",
8381
10019
  schema: [
@@ -8425,12 +10063,12 @@ var typesService = defineRule77({
8425
10063
  });
8426
10064
 
8427
10065
  // src/rules/types/types-unknown.ts
8428
- import { defineRule as defineRule78 } from "@oxlint/plugins";
10066
+ import { defineRule as defineRule94 } from "@oxlint/plugins";
8429
10067
  function isCatchName(node) {
8430
10068
  const key = "key" in node && !(("computed" in node) && node.computed) ? node.key : undefined;
8431
10069
  return key?.type === "Identifier" && key.name === "catch";
8432
10070
  }
8433
- var typesUnknown = defineRule78({
10071
+ var typesUnknown = defineRule94({
8434
10072
  meta: { type: "problem", schema: [], messages: { forbidden: "Use an owned domain type rather than unknown." } },
8435
10073
  createOnce(context) {
8436
10074
  return {
@@ -8473,6 +10111,7 @@ var src_default = eslintCompatPlugin({
8473
10111
  "code-double-negation": codeDoubleNegation,
8474
10112
  "code-function-nesting": codeFunctionNesting,
8475
10113
  "code-inline": codeInline,
10114
+ "code-inside": codeInside,
8476
10115
  "code-justified-call": codeJustifiedCall,
8477
10116
  "code-literal-in": codeLiteralIn,
8478
10117
  "code-mutable-state": codeMutableState,
@@ -8487,10 +10126,14 @@ var src_default = eslintCompatPlugin({
8487
10126
  "comments-directives": commentsDirectives,
8488
10127
  "copy-dashes": copyDashes,
8489
10128
  "effect-layer-file": effectLayerFile,
10129
+ "effect-layer-keys": effectLayerKeys,
8490
10130
  "effect-layers": effectLayers,
8491
10131
  "effect-merge-pipe": effectMergePipe,
8492
10132
  "effect-schema-first": effectSchemaFirst,
10133
+ "effect-service-params": effectServiceParams,
8493
10134
  "effect-values-first": effectValuesFirst,
10135
+ "effect-yield-name": effectYieldName,
10136
+ "effect-yields-first": effectYieldsFirst,
8494
10137
  "files-components-only": filesComponentsOnly,
8495
10138
  "files-constants-only": filesConstantsOnly,
8496
10139
  "files-domain-constants": filesDomainConstants,
@@ -8501,6 +10144,8 @@ var src_default = eslintCompatPlugin({
8501
10144
  "files-no-functions": filesNoFunctions,
8502
10145
  "files-restricted": filesRestricted,
8503
10146
  "files-route": filesRoute,
10147
+ "files-siblings": filesSiblings,
10148
+ "files-top-level": filesTopLevel,
8504
10149
  "files-types-only": filesTypesOnly,
8505
10150
  "layout-arrow-body": layoutArrowBody,
8506
10151
  "layout-call-arguments": layoutCallArguments,
@@ -8514,10 +10159,15 @@ var src_default = eslintCompatPlugin({
8514
10159
  "layout-padding": layoutPadding,
8515
10160
  "layout-pipe": layoutPipe,
8516
10161
  "layout-ternary": layoutTernary,
10162
+ "layout-type-arguments": layoutTypeArguments,
8517
10163
  "naming-declarations": namingDeclarations,
8518
10164
  "naming-exports": namingExports,
8519
10165
  "naming-file": namingFile,
8520
10166
  "naming-folder": namingFolder,
10167
+ "naming-imports": namingImports,
10168
+ "naming-keys": namingKeys,
10169
+ "naming-labels": namingLabels,
10170
+ "naming-pairs": namingPairs,
8521
10171
  "naming-types": namingTypes,
8522
10172
  "react-dynamic-classes": reactDynamicClasses,
8523
10173
  "react-empty-effect": reactEmptyEffect,