@sarj/eslint-plugin 15.5.0 → 15.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4231,7 +4231,7 @@ var import_utils21 = require("@typescript-eslint/utils");
4231
4231
  var noGenericSingleExportModuleDocumentation = {
4232
4232
  summary: "Disallow generic module stems when one runtime export already names the responsibility.",
4233
4233
  rationale: "A generic filename hides the sole exported responsibility and makes navigation less descriptive.",
4234
- remediation: "Rename the module after its single runtime export.",
4234
+ remediation: "Choose a responsibility-bearing module name or colocate the export with its domain.",
4235
4235
  category: "maintainability",
4236
4236
  limitations: ["Only configured generic stems with exactly one public runtime export are reported."],
4237
4237
  examples: [
@@ -4240,36 +4240,21 @@ var noGenericSingleExportModuleDocumentation = {
4240
4240
  ]
4241
4241
  };
4242
4242
  var GENERIC_STEMS = /* @__PURE__ */ new Set([
4243
- "base",
4244
4243
  "common",
4245
- "constant",
4246
- "constants",
4247
- "core",
4248
- "enum",
4249
- "enums",
4250
4244
  "helper",
4251
4245
  "helpers",
4252
4246
  "misc",
4253
- "model",
4254
- "models",
4255
4247
  "shared",
4256
4248
  "stuff",
4257
- "type",
4258
- "types",
4259
4249
  "util",
4260
4250
  "utils"
4261
4251
  ]);
4262
- var CONVENTIONAL_STEMS = /* @__PURE__ */ new Set(["base", "models"]);
4263
4252
  var CJS_OBJECT_EXPORT_METHODS = /* @__PURE__ */ new Set(["assign", "defineProperties", "defineProperty"]);
4264
4253
  function fileParts(filename) {
4265
4254
  const base = filename.replaceAll("\\", "/").split("/").at(-1) ?? "";
4266
4255
  const extension = base.match(/(\.[cm]?[jt]sx?)$/u)?.[1] ?? ".ts";
4267
- const segments = base.slice(0, -extension.length).split(".");
4268
- return {
4269
- extension,
4270
- stem: segments[0]?.toLowerCase() ?? "",
4271
- suffixes: segments.slice(1)
4272
- };
4256
+ const [stem3 = "", ...suffixes] = base.slice(0, -extension.length).split(".");
4257
+ return { stem: stem3, suffixes: suffixes.map((suffix) => suffix.toLowerCase()) };
4273
4258
  }
4274
4259
  function declaredNames(declaration) {
4275
4260
  if (declaration.declare === true) return [];
@@ -4285,50 +4270,6 @@ function declaredNames(declaration) {
4285
4270
  (item) => item.id.type === import_utils21.AST_NODE_TYPES.Identifier ? [item.id.name] : []
4286
4271
  );
4287
4272
  }
4288
- function typeOnlyBindings(program) {
4289
- const names = /* @__PURE__ */ new Set();
4290
- const runtimeNames = /* @__PURE__ */ new Set();
4291
- for (const statement of program.body) {
4292
- const declaration = statement.type === import_utils21.AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;
4293
- if (declaration?.type === import_utils21.AST_NODE_TYPES.TSInterfaceDeclaration || declaration?.type === import_utils21.AST_NODE_TYPES.TSTypeAliasDeclaration) {
4294
- names.add(declaration.id.name);
4295
- continue;
4296
- }
4297
- if (declaration?.type === import_utils21.AST_NODE_TYPES.TSEnumDeclaration && declaration.const) {
4298
- names.add(declaration.id.name);
4299
- continue;
4300
- }
4301
- if (statement.type === import_utils21.AST_NODE_TYPES.ImportDeclaration) {
4302
- for (const specifier of statement.specifiers) {
4303
- if (statement.importKind === "type" || specifier.type === import_utils21.AST_NODE_TYPES.ImportSpecifier && specifier.importKind === "type") {
4304
- names.add(specifier.local.name);
4305
- } else {
4306
- runtimeNames.add(specifier.local.name);
4307
- }
4308
- }
4309
- continue;
4310
- }
4311
- if (declaration?.type === import_utils21.AST_NODE_TYPES.TSDeclareFunction && declaration.id !== null) {
4312
- names.add(declaration.id.name);
4313
- continue;
4314
- }
4315
- if (declaration !== null && declaration.declare === true) {
4316
- if ((declaration.type === import_utils21.AST_NODE_TYPES.ClassDeclaration || declaration.type === import_utils21.AST_NODE_TYPES.FunctionDeclaration || declaration.type === import_utils21.AST_NODE_TYPES.TSEnumDeclaration || declaration.type === import_utils21.AST_NODE_TYPES.TSModuleDeclaration) && declaration.id !== null && declaration.id.type === import_utils21.AST_NODE_TYPES.Identifier) names.add(declaration.id.name);
4317
- if (declaration.type === import_utils21.AST_NODE_TYPES.VariableDeclaration) {
4318
- for (const item of declaration.declarations) {
4319
- if (item.id.type === import_utils21.AST_NODE_TYPES.Identifier) names.add(item.id.name);
4320
- }
4321
- }
4322
- continue;
4323
- }
4324
- if (declaration !== null && "type" in declaration) {
4325
- for (const name of declaredNames(declaration)) {
4326
- runtimeNames.add(name);
4327
- }
4328
- }
4329
- }
4330
- return new Set([...names].filter((name) => !runtimeNames.has(name)));
4331
- }
4332
4273
  function runtimeExports(program) {
4333
4274
  const exports2 = [];
4334
4275
  const typeBindings = typeOnlyBindings(program);
@@ -4367,25 +4308,49 @@ function runtimeExports(program) {
4367
4308
  const unique = new Map(exports2.map((entry) => [entry.key, entry]));
4368
4309
  return { exports: [...unique.values()], ambiguous };
4369
4310
  }
4370
- function publicTypeExportCount(program) {
4311
+ function typeOnlyBindings(program) {
4371
4312
  const names = /* @__PURE__ */ new Set();
4372
- const typeBindings = typeOnlyBindings(program);
4313
+ const runtimeNames = /* @__PURE__ */ new Set();
4373
4314
  for (const statement of program.body) {
4374
- if (statement.type !== import_utils21.AST_NODE_TYPES.ExportNamedDeclaration) continue;
4375
- const declaration = statement.declaration;
4376
- if (declaration?.type === import_utils21.AST_NODE_TYPES.TSInterfaceDeclaration || declaration?.type === import_utils21.AST_NODE_TYPES.TSTypeAliasDeclaration) names.add(declaration.id.name);
4377
- for (const specifier of statement.specifiers) {
4378
- const local = specifier.local.type === import_utils21.AST_NODE_TYPES.Identifier ? specifier.local.name : specifier.local.value;
4379
- if (statement.exportKind !== "type" && specifier.exportKind !== "type" && !typeBindings.has(local)) {
4380
- continue;
4315
+ const declaration = statement.type === import_utils21.AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;
4316
+ if (declaration?.type === import_utils21.AST_NODE_TYPES.TSInterfaceDeclaration || declaration?.type === import_utils21.AST_NODE_TYPES.TSTypeAliasDeclaration) {
4317
+ names.add(declaration.id.name);
4318
+ continue;
4319
+ }
4320
+ if (declaration?.type === import_utils21.AST_NODE_TYPES.TSEnumDeclaration && declaration.const) {
4321
+ names.add(declaration.id.name);
4322
+ continue;
4323
+ }
4324
+ if (statement.type === import_utils21.AST_NODE_TYPES.ImportDeclaration) {
4325
+ for (const specifier of statement.specifiers) {
4326
+ if (statement.importKind === "type" || specifier.type === import_utils21.AST_NODE_TYPES.ImportSpecifier && specifier.importKind === "type") {
4327
+ names.add(specifier.local.name);
4328
+ } else {
4329
+ runtimeNames.add(specifier.local.name);
4330
+ }
4331
+ }
4332
+ continue;
4333
+ }
4334
+ if (declaration?.type === import_utils21.AST_NODE_TYPES.TSDeclareFunction && declaration.id !== null) {
4335
+ names.add(declaration.id.name);
4336
+ continue;
4337
+ }
4338
+ if (declaration !== null && declaration.declare === true) {
4339
+ if ((declaration.type === import_utils21.AST_NODE_TYPES.ClassDeclaration || declaration.type === import_utils21.AST_NODE_TYPES.FunctionDeclaration || declaration.type === import_utils21.AST_NODE_TYPES.TSEnumDeclaration || declaration.type === import_utils21.AST_NODE_TYPES.TSModuleDeclaration) && declaration.id !== null && declaration.id.type === import_utils21.AST_NODE_TYPES.Identifier) names.add(declaration.id.name);
4340
+ if (declaration.type === import_utils21.AST_NODE_TYPES.VariableDeclaration) {
4341
+ for (const item of declaration.declarations) {
4342
+ if (item.id.type === import_utils21.AST_NODE_TYPES.Identifier) names.add(item.id.name);
4343
+ }
4344
+ }
4345
+ continue;
4346
+ }
4347
+ if (declaration !== null && "type" in declaration) {
4348
+ for (const name of declaredNames(declaration)) {
4349
+ runtimeNames.add(name);
4381
4350
  }
4382
- names.add(specifier.exported.type === import_utils21.AST_NODE_TYPES.Identifier ? specifier.exported.name : specifier.exported.value);
4383
4351
  }
4384
4352
  }
4385
- return names.size;
4386
- }
4387
- function kebabCase(name) {
4388
- return name.replaceAll(/oauth/giu, "Oauth").replaceAll(/graphql/giu, "Graphql").replaceAll(/grpc/giu, "Grpc").replaceAll(/([a-z\d])([A-Z])/gu, "$1-$2").replaceAll(/([A-Z]+)([A-Z][a-z])/gu, "$1-$2").replaceAll(/[_\s]+/gu, "-").replaceAll(/-+/gu, "-").replaceAll(/^-|-$/gu, "").toLowerCase();
4353
+ return new Set([...names].filter((name) => !runtimeNames.has(name)));
4389
4354
  }
4390
4355
  function isGlobalIdentifier(context, node) {
4391
4356
  const variable = import_utils21.ASTUtils.findVariable(context.sourceCode.getScope(node), node.name);
@@ -4407,14 +4372,14 @@ var no_generic_single_export_module_default = createRule({
4407
4372
  docs: { description: "Disallow generic module stems when one runtime export already names the responsibility." },
4408
4373
  schema: [],
4409
4374
  messages: {
4410
- genericSingleExport: "Module stem `{{stem}}` is generic and its only runtime export is `{{exported}}`; rename the file to `{{expected}}`."
4375
+ genericSingleExport: "Module stem `{{stem}}` is generic and its only runtime export is `{{exported}}`; choose a responsibility-bearing module name or colocate the export with its domain."
4411
4376
  }
4412
4377
  },
4413
4378
  defaultOptions: [],
4414
4379
  create(context) {
4415
4380
  const file = fileParts(context.filename);
4416
4381
  const { stem: stem3 } = file;
4417
- if (!GENERIC_STEMS.has(stem3) || CONVENTIONAL_STEMS.has(stem3) || isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
4382
+ if (!GENERIC_STEMS.has(stem3) || isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
4418
4383
  let hasCommonJsExport = false;
4419
4384
  return {
4420
4385
  CallExpression(node) {
@@ -4427,25 +4392,19 @@ var no_generic_single_export_module_default = createRule({
4427
4392
  },
4428
4393
  "Program:exit"(program) {
4429
4394
  if (hasCommonJsExport) return;
4430
- if ((stem3 === "type" || stem3 === "types") && publicTypeExportCount(program) >= 2) return;
4431
4395
  const exports2 = runtimeExports(program);
4432
4396
  if (exports2.ambiguous || exports2.exports.length !== 1) return;
4433
4397
  const onlyExport = exports2.exports[0];
4434
4398
  if (onlyExport === void 0) return;
4435
4399
  const { name: exported, node } = onlyExport;
4436
4400
  if (isConventionalFrameworkUtility(context.filename, exported)) return;
4437
- const expectedStem = kebabCase(exported);
4438
- const suffixStem = file.suffixes.map(kebabCase).join("-");
4439
- const currentResponsibility = [stem3, suffixStem].filter(Boolean).join("-");
4440
- if (expectedStem === currentResponsibility || GENERIC_STEMS.has(expectedStem) || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(expectedStem)) return;
4441
- const suffixTail = suffixStem === "" ? "" : `-${suffixStem}`;
4442
- const renameStem = suffixTail !== "" && expectedStem.endsWith(suffixTail) ? expectedStem.slice(0, -suffixTail.length) : expectedStem;
4443
- if (renameStem === "" || GENERIC_STEMS.has(renameStem)) return;
4444
- const suffix = file.suffixes.map((part) => `.${kebabCase(part)}`).join("");
4401
+ const exportedRole = exported.replaceAll(/[^a-z0-9]/giu, "").toLowerCase();
4402
+ const pathRole = [stem3, ...file.suffixes].join("").replaceAll(/[^a-z0-9]/giu, "");
4403
+ if (exportedRole === pathRole && file.suffixes.length > 0) return;
4445
4404
  context.report({
4446
4405
  node,
4447
4406
  messageId: "genericSingleExport",
4448
- data: { stem: stem3, exported, expected: `${renameStem}${suffix}${file.extension}` }
4407
+ data: { stem: stem3, exported }
4449
4408
  });
4450
4409
  }
4451
4410
  };
@@ -13908,8 +13867,13 @@ var import_utils68 = require("@typescript-eslint/utils");
13908
13867
  var stepdownDocumentation = {
13909
13868
  summary: "Place a private helper below its sole direct same-scope caller.",
13910
13869
  rationale: "Caller-first ordering lets a reader follow the main flow before descending into implementation details.",
13911
- remediation: "Move the private helper immediately below its sole caller.",
13870
+ remediation: "Move the private helper below its sole caller.",
13912
13871
  category: "maintainability",
13872
+ limitations: [
13873
+ "Generated and test files, cycles, dynamic references, overload targets, and helpers with multiple callers are excluded.",
13874
+ "Class methods are reported only when both helper and caller are private so accessibility ordering remains authoritative.",
13875
+ "Runtime class-field, static-block, computed-member, and decorator barriers are never crossed."
13876
+ ],
13913
13877
  examples: [
13914
13878
  { id: "caller-before-helper", title: "Place the caller first", outcome: "no-match", files: [{ path: "src/run.ts", source: "function run() { return load(); }\nfunction load() { return 1; }" }], focusPath: "src/run.ts", expectedCount: 0, public: true },
13915
13879
  { id: "helper-before-caller", title: "Do not lead with a sole-caller helper", outcome: "match", files: [{ path: "src/run.ts", source: "function load() { return 1; }\nfunction run() { return load(); }" }], focusPath: "src/run.ts", expectedCount: 1, public: true }
@@ -13918,7 +13882,7 @@ var stepdownDocumentation = {
13918
13882
  function isFunction(node) {
13919
13883
  return node.type === import_utils68.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils68.AST_NODE_TYPES.FunctionDeclaration || node.type === import_utils68.AST_NODE_TYPES.FunctionExpression;
13920
13884
  }
13921
- function reportMisordered(context, candidates, scopeDefinitions, calls, pinned) {
13885
+ function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true) {
13922
13886
  const byName = new Map(scopeDefinitions.map((definition) => [definition.name, definition]));
13923
13887
  const cycles = cycleComponents(calls);
13924
13888
  const callers = /* @__PURE__ */ new Map();
@@ -13936,7 +13900,7 @@ function reportMisordered(context, candidates, scopeDefinitions, calls, pinned)
13936
13900
  const callerName2 = helperCallers[0];
13937
13901
  if (callerName2 === void 0 || cycles.has(helper.name) && cycles.get(helper.name) === cycles.get(callerName2)) continue;
13938
13902
  const caller = byName.get(callerName2);
13939
- if (caller === void 0 || helper.node.range[0] >= caller.node.range[0]) continue;
13903
+ if (caller === void 0 || helper.node.range[0] >= caller.node.range[0] || !canMove(helper, caller)) continue;
13940
13904
  context.report({
13941
13905
  node: helper.node,
13942
13906
  messageId: "helperAboveOnlyCaller",
@@ -14016,8 +13980,10 @@ function moduleScope(context, program) {
14016
13980
  })
14017
13981
  );
14018
13982
  const exported = exportedNames(program);
14019
- const scopeDefinitions = declarations.filter((node) => counts.get(node.name) === 1 && !overloadNames.has(node.name));
14020
- const definitions = scopeDefinitions.filter((definition) => !exported.has(definition.name));
13983
+ const scopeDefinitions = declarations.filter((node) => counts.get(node.name) === 1);
13984
+ const definitions = scopeDefinitions.filter(
13985
+ (definition) => !exported.has(definition.name) && !overloadNames.has(definition.name)
13986
+ );
14021
13987
  const byFunction = new Map(scopeDefinitions.map((definition) => [definition.functionNode, definition]));
14022
13988
  const calls = /* @__PURE__ */ new Map();
14023
13989
  const pinned = /* @__PURE__ */ new Set();
@@ -14261,11 +14227,36 @@ function classScope(context, node, computedReferenceNames) {
14261
14227
  return [definition.name, accessibility2];
14262
14228
  })
14263
14229
  );
14230
+ const methodByName = new Map(scopeDefinitions.map((definition) => [definition.name, definition.node]));
14264
14231
  for (const [caller, callees] of calls) {
14265
- if (accessibility.get(caller) === "private") continue;
14232
+ const callerMethod = methodByName.get(caller);
14233
+ if (accessibility.get(caller) === "private" && callerMethod?.type === import_utils68.AST_NODE_TYPES.MethodDefinition && callerMethod.decorators.length === 0) continue;
14266
14234
  for (const callee of callees) pinned.add(callee);
14267
14235
  }
14268
- reportMisordered(context, definitions, scopeDefinitions, calls, pinned);
14236
+ const memberIndexes = new Map(node.body.body.map((member, index) => [member, index]));
14237
+ const runtimeBarrierPrefix = [0];
14238
+ for (const member of node.body.body) {
14239
+ runtimeBarrierPrefix.push((runtimeBarrierPrefix.at(-1) ?? 0) + (isClassRuntimeBarrier(member) ? 1 : 0));
14240
+ }
14241
+ reportMisordered(context, definitions, scopeDefinitions, calls, pinned, (helper, caller) => {
14242
+ const helperIndex = memberIndexes.get(helper.node);
14243
+ const callerIndex = memberIndexes.get(caller.node);
14244
+ if (helperIndex === void 0 || callerIndex === void 0) return false;
14245
+ return runtimeBarrierPrefix[callerIndex + 1] === runtimeBarrierPrefix[helperIndex + 1];
14246
+ });
14247
+ }
14248
+ function isClassRuntimeBarrier(member) {
14249
+ switch (member.type) {
14250
+ case import_utils68.AST_NODE_TYPES.StaticBlock:
14251
+ return true;
14252
+ case import_utils68.AST_NODE_TYPES.PropertyDefinition:
14253
+ case import_utils68.AST_NODE_TYPES.AccessorProperty:
14254
+ return member.static || member.computed || member.decorators.length > 0 || member.value !== null;
14255
+ case import_utils68.AST_NODE_TYPES.MethodDefinition:
14256
+ return member.computed || member.decorators.length > 0;
14257
+ default:
14258
+ return false;
14259
+ }
14269
14260
  }
14270
14261
  var stepdown_default = createRule({
14271
14262
  name: "stepdown",
@@ -14590,7 +14581,7 @@ var rules = {
14590
14581
  };
14591
14582
  var meta = {
14592
14583
  name: "@sarj/eslint-plugin",
14593
- version: "15.5.0"
14584
+ version: "15.6.0"
14594
14585
  };
14595
14586
  var applicationOnlyRules = [
14596
14587
  "no-restricted-library-load",
package/dist/index.d.cts CHANGED
@@ -415,7 +415,7 @@ type FlatPreset = {
415
415
  declare const plugin: {
416
416
  readonly meta: {
417
417
  readonly name: "@sarj/eslint-plugin";
418
- readonly version: "15.5.0";
418
+ readonly version: "15.6.0";
419
419
  };
420
420
  readonly rules: {
421
421
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
package/dist/index.d.ts CHANGED
@@ -415,7 +415,7 @@ type FlatPreset = {
415
415
  declare const plugin: {
416
416
  readonly meta: {
417
417
  readonly name: "@sarj/eslint-plugin";
418
- readonly version: "15.5.0";
418
+ readonly version: "15.6.0";
419
419
  };
420
420
  readonly rules: {
421
421
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
package/dist/index.js CHANGED
@@ -4193,7 +4193,7 @@ import { AST_NODE_TYPES as AST_NODE_TYPES16, ASTUtils as ASTUtils4 } from "@type
4193
4193
  var noGenericSingleExportModuleDocumentation = {
4194
4194
  summary: "Disallow generic module stems when one runtime export already names the responsibility.",
4195
4195
  rationale: "A generic filename hides the sole exported responsibility and makes navigation less descriptive.",
4196
- remediation: "Rename the module after its single runtime export.",
4196
+ remediation: "Choose a responsibility-bearing module name or colocate the export with its domain.",
4197
4197
  category: "maintainability",
4198
4198
  limitations: ["Only configured generic stems with exactly one public runtime export are reported."],
4199
4199
  examples: [
@@ -4202,36 +4202,21 @@ var noGenericSingleExportModuleDocumentation = {
4202
4202
  ]
4203
4203
  };
4204
4204
  var GENERIC_STEMS = /* @__PURE__ */ new Set([
4205
- "base",
4206
4205
  "common",
4207
- "constant",
4208
- "constants",
4209
- "core",
4210
- "enum",
4211
- "enums",
4212
4206
  "helper",
4213
4207
  "helpers",
4214
4208
  "misc",
4215
- "model",
4216
- "models",
4217
4209
  "shared",
4218
4210
  "stuff",
4219
- "type",
4220
- "types",
4221
4211
  "util",
4222
4212
  "utils"
4223
4213
  ]);
4224
- var CONVENTIONAL_STEMS = /* @__PURE__ */ new Set(["base", "models"]);
4225
4214
  var CJS_OBJECT_EXPORT_METHODS = /* @__PURE__ */ new Set(["assign", "defineProperties", "defineProperty"]);
4226
4215
  function fileParts(filename) {
4227
4216
  const base = filename.replaceAll("\\", "/").split("/").at(-1) ?? "";
4228
4217
  const extension = base.match(/(\.[cm]?[jt]sx?)$/u)?.[1] ?? ".ts";
4229
- const segments = base.slice(0, -extension.length).split(".");
4230
- return {
4231
- extension,
4232
- stem: segments[0]?.toLowerCase() ?? "",
4233
- suffixes: segments.slice(1)
4234
- };
4218
+ const [stem3 = "", ...suffixes] = base.slice(0, -extension.length).split(".");
4219
+ return { stem: stem3, suffixes: suffixes.map((suffix) => suffix.toLowerCase()) };
4235
4220
  }
4236
4221
  function declaredNames(declaration) {
4237
4222
  if (declaration.declare === true) return [];
@@ -4247,50 +4232,6 @@ function declaredNames(declaration) {
4247
4232
  (item) => item.id.type === AST_NODE_TYPES16.Identifier ? [item.id.name] : []
4248
4233
  );
4249
4234
  }
4250
- function typeOnlyBindings(program) {
4251
- const names = /* @__PURE__ */ new Set();
4252
- const runtimeNames = /* @__PURE__ */ new Set();
4253
- for (const statement of program.body) {
4254
- const declaration = statement.type === AST_NODE_TYPES16.ExportNamedDeclaration ? statement.declaration : statement;
4255
- if (declaration?.type === AST_NODE_TYPES16.TSInterfaceDeclaration || declaration?.type === AST_NODE_TYPES16.TSTypeAliasDeclaration) {
4256
- names.add(declaration.id.name);
4257
- continue;
4258
- }
4259
- if (declaration?.type === AST_NODE_TYPES16.TSEnumDeclaration && declaration.const) {
4260
- names.add(declaration.id.name);
4261
- continue;
4262
- }
4263
- if (statement.type === AST_NODE_TYPES16.ImportDeclaration) {
4264
- for (const specifier of statement.specifiers) {
4265
- if (statement.importKind === "type" || specifier.type === AST_NODE_TYPES16.ImportSpecifier && specifier.importKind === "type") {
4266
- names.add(specifier.local.name);
4267
- } else {
4268
- runtimeNames.add(specifier.local.name);
4269
- }
4270
- }
4271
- continue;
4272
- }
4273
- if (declaration?.type === AST_NODE_TYPES16.TSDeclareFunction && declaration.id !== null) {
4274
- names.add(declaration.id.name);
4275
- continue;
4276
- }
4277
- if (declaration !== null && declaration.declare === true) {
4278
- if ((declaration.type === AST_NODE_TYPES16.ClassDeclaration || declaration.type === AST_NODE_TYPES16.FunctionDeclaration || declaration.type === AST_NODE_TYPES16.TSEnumDeclaration || declaration.type === AST_NODE_TYPES16.TSModuleDeclaration) && declaration.id !== null && declaration.id.type === AST_NODE_TYPES16.Identifier) names.add(declaration.id.name);
4279
- if (declaration.type === AST_NODE_TYPES16.VariableDeclaration) {
4280
- for (const item of declaration.declarations) {
4281
- if (item.id.type === AST_NODE_TYPES16.Identifier) names.add(item.id.name);
4282
- }
4283
- }
4284
- continue;
4285
- }
4286
- if (declaration !== null && "type" in declaration) {
4287
- for (const name of declaredNames(declaration)) {
4288
- runtimeNames.add(name);
4289
- }
4290
- }
4291
- }
4292
- return new Set([...names].filter((name) => !runtimeNames.has(name)));
4293
- }
4294
4235
  function runtimeExports(program) {
4295
4236
  const exports = [];
4296
4237
  const typeBindings = typeOnlyBindings(program);
@@ -4329,25 +4270,49 @@ function runtimeExports(program) {
4329
4270
  const unique = new Map(exports.map((entry) => [entry.key, entry]));
4330
4271
  return { exports: [...unique.values()], ambiguous };
4331
4272
  }
4332
- function publicTypeExportCount(program) {
4273
+ function typeOnlyBindings(program) {
4333
4274
  const names = /* @__PURE__ */ new Set();
4334
- const typeBindings = typeOnlyBindings(program);
4275
+ const runtimeNames = /* @__PURE__ */ new Set();
4335
4276
  for (const statement of program.body) {
4336
- if (statement.type !== AST_NODE_TYPES16.ExportNamedDeclaration) continue;
4337
- const declaration = statement.declaration;
4338
- if (declaration?.type === AST_NODE_TYPES16.TSInterfaceDeclaration || declaration?.type === AST_NODE_TYPES16.TSTypeAliasDeclaration) names.add(declaration.id.name);
4339
- for (const specifier of statement.specifiers) {
4340
- const local = specifier.local.type === AST_NODE_TYPES16.Identifier ? specifier.local.name : specifier.local.value;
4341
- if (statement.exportKind !== "type" && specifier.exportKind !== "type" && !typeBindings.has(local)) {
4342
- continue;
4277
+ const declaration = statement.type === AST_NODE_TYPES16.ExportNamedDeclaration ? statement.declaration : statement;
4278
+ if (declaration?.type === AST_NODE_TYPES16.TSInterfaceDeclaration || declaration?.type === AST_NODE_TYPES16.TSTypeAliasDeclaration) {
4279
+ names.add(declaration.id.name);
4280
+ continue;
4281
+ }
4282
+ if (declaration?.type === AST_NODE_TYPES16.TSEnumDeclaration && declaration.const) {
4283
+ names.add(declaration.id.name);
4284
+ continue;
4285
+ }
4286
+ if (statement.type === AST_NODE_TYPES16.ImportDeclaration) {
4287
+ for (const specifier of statement.specifiers) {
4288
+ if (statement.importKind === "type" || specifier.type === AST_NODE_TYPES16.ImportSpecifier && specifier.importKind === "type") {
4289
+ names.add(specifier.local.name);
4290
+ } else {
4291
+ runtimeNames.add(specifier.local.name);
4292
+ }
4293
+ }
4294
+ continue;
4295
+ }
4296
+ if (declaration?.type === AST_NODE_TYPES16.TSDeclareFunction && declaration.id !== null) {
4297
+ names.add(declaration.id.name);
4298
+ continue;
4299
+ }
4300
+ if (declaration !== null && declaration.declare === true) {
4301
+ if ((declaration.type === AST_NODE_TYPES16.ClassDeclaration || declaration.type === AST_NODE_TYPES16.FunctionDeclaration || declaration.type === AST_NODE_TYPES16.TSEnumDeclaration || declaration.type === AST_NODE_TYPES16.TSModuleDeclaration) && declaration.id !== null && declaration.id.type === AST_NODE_TYPES16.Identifier) names.add(declaration.id.name);
4302
+ if (declaration.type === AST_NODE_TYPES16.VariableDeclaration) {
4303
+ for (const item of declaration.declarations) {
4304
+ if (item.id.type === AST_NODE_TYPES16.Identifier) names.add(item.id.name);
4305
+ }
4306
+ }
4307
+ continue;
4308
+ }
4309
+ if (declaration !== null && "type" in declaration) {
4310
+ for (const name of declaredNames(declaration)) {
4311
+ runtimeNames.add(name);
4343
4312
  }
4344
- names.add(specifier.exported.type === AST_NODE_TYPES16.Identifier ? specifier.exported.name : specifier.exported.value);
4345
4313
  }
4346
4314
  }
4347
- return names.size;
4348
- }
4349
- function kebabCase(name) {
4350
- return name.replaceAll(/oauth/giu, "Oauth").replaceAll(/graphql/giu, "Graphql").replaceAll(/grpc/giu, "Grpc").replaceAll(/([a-z\d])([A-Z])/gu, "$1-$2").replaceAll(/([A-Z]+)([A-Z][a-z])/gu, "$1-$2").replaceAll(/[_\s]+/gu, "-").replaceAll(/-+/gu, "-").replaceAll(/^-|-$/gu, "").toLowerCase();
4315
+ return new Set([...names].filter((name) => !runtimeNames.has(name)));
4351
4316
  }
4352
4317
  function isGlobalIdentifier(context, node) {
4353
4318
  const variable = ASTUtils4.findVariable(context.sourceCode.getScope(node), node.name);
@@ -4369,14 +4334,14 @@ var no_generic_single_export_module_default = createRule({
4369
4334
  docs: { description: "Disallow generic module stems when one runtime export already names the responsibility." },
4370
4335
  schema: [],
4371
4336
  messages: {
4372
- genericSingleExport: "Module stem `{{stem}}` is generic and its only runtime export is `{{exported}}`; rename the file to `{{expected}}`."
4337
+ genericSingleExport: "Module stem `{{stem}}` is generic and its only runtime export is `{{exported}}`; choose a responsibility-bearing module name or colocate the export with its domain."
4373
4338
  }
4374
4339
  },
4375
4340
  defaultOptions: [],
4376
4341
  create(context) {
4377
4342
  const file = fileParts(context.filename);
4378
4343
  const { stem: stem3 } = file;
4379
- if (!GENERIC_STEMS.has(stem3) || CONVENTIONAL_STEMS.has(stem3) || isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
4344
+ if (!GENERIC_STEMS.has(stem3) || isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
4380
4345
  let hasCommonJsExport = false;
4381
4346
  return {
4382
4347
  CallExpression(node) {
@@ -4389,25 +4354,19 @@ var no_generic_single_export_module_default = createRule({
4389
4354
  },
4390
4355
  "Program:exit"(program) {
4391
4356
  if (hasCommonJsExport) return;
4392
- if ((stem3 === "type" || stem3 === "types") && publicTypeExportCount(program) >= 2) return;
4393
4357
  const exports = runtimeExports(program);
4394
4358
  if (exports.ambiguous || exports.exports.length !== 1) return;
4395
4359
  const onlyExport = exports.exports[0];
4396
4360
  if (onlyExport === void 0) return;
4397
4361
  const { name: exported, node } = onlyExport;
4398
4362
  if (isConventionalFrameworkUtility(context.filename, exported)) return;
4399
- const expectedStem = kebabCase(exported);
4400
- const suffixStem = file.suffixes.map(kebabCase).join("-");
4401
- const currentResponsibility = [stem3, suffixStem].filter(Boolean).join("-");
4402
- if (expectedStem === currentResponsibility || GENERIC_STEMS.has(expectedStem) || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(expectedStem)) return;
4403
- const suffixTail = suffixStem === "" ? "" : `-${suffixStem}`;
4404
- const renameStem = suffixTail !== "" && expectedStem.endsWith(suffixTail) ? expectedStem.slice(0, -suffixTail.length) : expectedStem;
4405
- if (renameStem === "" || GENERIC_STEMS.has(renameStem)) return;
4406
- const suffix = file.suffixes.map((part) => `.${kebabCase(part)}`).join("");
4363
+ const exportedRole = exported.replaceAll(/[^a-z0-9]/giu, "").toLowerCase();
4364
+ const pathRole = [stem3, ...file.suffixes].join("").replaceAll(/[^a-z0-9]/giu, "");
4365
+ if (exportedRole === pathRole && file.suffixes.length > 0) return;
4407
4366
  context.report({
4408
4367
  node,
4409
4368
  messageId: "genericSingleExport",
4410
- data: { stem: stem3, exported, expected: `${renameStem}${suffix}${file.extension}` }
4369
+ data: { stem: stem3, exported }
4411
4370
  });
4412
4371
  }
4413
4372
  };
@@ -13883,8 +13842,13 @@ import { AST_NODE_TYPES as AST_NODE_TYPES54, ASTUtils as ASTUtils15 } from "@typ
13883
13842
  var stepdownDocumentation = {
13884
13843
  summary: "Place a private helper below its sole direct same-scope caller.",
13885
13844
  rationale: "Caller-first ordering lets a reader follow the main flow before descending into implementation details.",
13886
- remediation: "Move the private helper immediately below its sole caller.",
13845
+ remediation: "Move the private helper below its sole caller.",
13887
13846
  category: "maintainability",
13847
+ limitations: [
13848
+ "Generated and test files, cycles, dynamic references, overload targets, and helpers with multiple callers are excluded.",
13849
+ "Class methods are reported only when both helper and caller are private so accessibility ordering remains authoritative.",
13850
+ "Runtime class-field, static-block, computed-member, and decorator barriers are never crossed."
13851
+ ],
13888
13852
  examples: [
13889
13853
  { id: "caller-before-helper", title: "Place the caller first", outcome: "no-match", files: [{ path: "src/run.ts", source: "function run() { return load(); }\nfunction load() { return 1; }" }], focusPath: "src/run.ts", expectedCount: 0, public: true },
13890
13854
  { id: "helper-before-caller", title: "Do not lead with a sole-caller helper", outcome: "match", files: [{ path: "src/run.ts", source: "function load() { return 1; }\nfunction run() { return load(); }" }], focusPath: "src/run.ts", expectedCount: 1, public: true }
@@ -13893,7 +13857,7 @@ var stepdownDocumentation = {
13893
13857
  function isFunction(node) {
13894
13858
  return node.type === AST_NODE_TYPES54.ArrowFunctionExpression || node.type === AST_NODE_TYPES54.FunctionDeclaration || node.type === AST_NODE_TYPES54.FunctionExpression;
13895
13859
  }
13896
- function reportMisordered(context, candidates, scopeDefinitions, calls, pinned) {
13860
+ function reportMisordered(context, candidates, scopeDefinitions, calls, pinned, canMove = () => true) {
13897
13861
  const byName = new Map(scopeDefinitions.map((definition) => [definition.name, definition]));
13898
13862
  const cycles = cycleComponents(calls);
13899
13863
  const callers = /* @__PURE__ */ new Map();
@@ -13911,7 +13875,7 @@ function reportMisordered(context, candidates, scopeDefinitions, calls, pinned)
13911
13875
  const callerName2 = helperCallers[0];
13912
13876
  if (callerName2 === void 0 || cycles.has(helper.name) && cycles.get(helper.name) === cycles.get(callerName2)) continue;
13913
13877
  const caller = byName.get(callerName2);
13914
- if (caller === void 0 || helper.node.range[0] >= caller.node.range[0]) continue;
13878
+ if (caller === void 0 || helper.node.range[0] >= caller.node.range[0] || !canMove(helper, caller)) continue;
13915
13879
  context.report({
13916
13880
  node: helper.node,
13917
13881
  messageId: "helperAboveOnlyCaller",
@@ -13991,8 +13955,10 @@ function moduleScope(context, program) {
13991
13955
  })
13992
13956
  );
13993
13957
  const exported = exportedNames(program);
13994
- const scopeDefinitions = declarations.filter((node) => counts.get(node.name) === 1 && !overloadNames.has(node.name));
13995
- const definitions = scopeDefinitions.filter((definition) => !exported.has(definition.name));
13958
+ const scopeDefinitions = declarations.filter((node) => counts.get(node.name) === 1);
13959
+ const definitions = scopeDefinitions.filter(
13960
+ (definition) => !exported.has(definition.name) && !overloadNames.has(definition.name)
13961
+ );
13996
13962
  const byFunction = new Map(scopeDefinitions.map((definition) => [definition.functionNode, definition]));
13997
13963
  const calls = /* @__PURE__ */ new Map();
13998
13964
  const pinned = /* @__PURE__ */ new Set();
@@ -14236,11 +14202,36 @@ function classScope(context, node, computedReferenceNames) {
14236
14202
  return [definition.name, accessibility2];
14237
14203
  })
14238
14204
  );
14205
+ const methodByName = new Map(scopeDefinitions.map((definition) => [definition.name, definition.node]));
14239
14206
  for (const [caller, callees] of calls) {
14240
- if (accessibility.get(caller) === "private") continue;
14207
+ const callerMethod = methodByName.get(caller);
14208
+ if (accessibility.get(caller) === "private" && callerMethod?.type === AST_NODE_TYPES54.MethodDefinition && callerMethod.decorators.length === 0) continue;
14241
14209
  for (const callee of callees) pinned.add(callee);
14242
14210
  }
14243
- reportMisordered(context, definitions, scopeDefinitions, calls, pinned);
14211
+ const memberIndexes = new Map(node.body.body.map((member, index) => [member, index]));
14212
+ const runtimeBarrierPrefix = [0];
14213
+ for (const member of node.body.body) {
14214
+ runtimeBarrierPrefix.push((runtimeBarrierPrefix.at(-1) ?? 0) + (isClassRuntimeBarrier(member) ? 1 : 0));
14215
+ }
14216
+ reportMisordered(context, definitions, scopeDefinitions, calls, pinned, (helper, caller) => {
14217
+ const helperIndex = memberIndexes.get(helper.node);
14218
+ const callerIndex = memberIndexes.get(caller.node);
14219
+ if (helperIndex === void 0 || callerIndex === void 0) return false;
14220
+ return runtimeBarrierPrefix[callerIndex + 1] === runtimeBarrierPrefix[helperIndex + 1];
14221
+ });
14222
+ }
14223
+ function isClassRuntimeBarrier(member) {
14224
+ switch (member.type) {
14225
+ case AST_NODE_TYPES54.StaticBlock:
14226
+ return true;
14227
+ case AST_NODE_TYPES54.PropertyDefinition:
14228
+ case AST_NODE_TYPES54.AccessorProperty:
14229
+ return member.static || member.computed || member.decorators.length > 0 || member.value !== null;
14230
+ case AST_NODE_TYPES54.MethodDefinition:
14231
+ return member.computed || member.decorators.length > 0;
14232
+ default:
14233
+ return false;
14234
+ }
14244
14235
  }
14245
14236
  var stepdown_default = createRule({
14246
14237
  name: "stepdown",
@@ -14568,7 +14559,7 @@ var rules = {
14568
14559
  };
14569
14560
  var meta = {
14570
14561
  name: "@sarj/eslint-plugin",
14571
- version: "15.5.0"
14562
+ version: "15.6.0"
14572
14563
  };
14573
14564
  var applicationOnlyRules = [
14574
14565
  "no-restricted-library-load",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "15.5.0",
3
+ "version": "15.6.0",
4
4
  "packageManager": "npm@11.19.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",