@sarj/eslint-plugin 9.15.0 → 9.16.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/README.md CHANGED
@@ -23,7 +23,7 @@ pnpm add -D @sarj/eslint-plugin
23
23
  ```js
24
24
  // eslint.config.mjs
25
25
  import sarj from "@sarj/eslint-plugin";
26
- export default [...sarj.configs.recommended];
26
+ export default [sarj.configs.recommended];
27
27
  ```
28
28
 
29
29
  67 rules. Each source under `src/rules/` states one concise claim and links to
@@ -308,7 +308,7 @@ declared stateless.
308
308
  import sarj from "@sarj/eslint-plugin";
309
309
 
310
310
  export default [
311
- ...sarj.configs.strict,
311
+ sarj.configs.strict,
312
312
  {
313
313
  rules: {
314
314
  // This repo keeps its HTTP layer in `lib/api/`, not `clients/`.
package/dist/index.cjs CHANGED
@@ -3560,6 +3560,23 @@ function runtimeExports(program) {
3560
3560
  const unique = new Map(exports2.map((entry) => [entry.key, entry]));
3561
3561
  return { exports: [...unique.values()], ambiguous };
3562
3562
  }
3563
+ function publicTypeExportCount(program) {
3564
+ const names = /* @__PURE__ */ new Set();
3565
+ const typeBindings = typeOnlyBindings(program);
3566
+ for (const statement of program.body) {
3567
+ if (statement.type !== import_utils21.AST_NODE_TYPES.ExportNamedDeclaration) continue;
3568
+ const declaration = statement.declaration;
3569
+ if (declaration?.type === import_utils21.AST_NODE_TYPES.TSInterfaceDeclaration || declaration?.type === import_utils21.AST_NODE_TYPES.TSTypeAliasDeclaration) names.add(declaration.id.name);
3570
+ for (const specifier of statement.specifiers) {
3571
+ const local = specifier.local.type === import_utils21.AST_NODE_TYPES.Identifier ? specifier.local.name : specifier.local.value;
3572
+ if (statement.exportKind !== "type" && specifier.exportKind !== "type" && !typeBindings.has(local)) {
3573
+ continue;
3574
+ }
3575
+ names.add(specifier.exported.type === import_utils21.AST_NODE_TYPES.Identifier ? specifier.exported.name : specifier.exported.value);
3576
+ }
3577
+ }
3578
+ return names.size;
3579
+ }
3563
3580
  function kebabCase(name) {
3564
3581
  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();
3565
3582
  }
@@ -3602,6 +3619,7 @@ var no_generic_single_export_module_default = createRule({
3602
3619
  },
3603
3620
  "Program:exit"(program) {
3604
3621
  if (hasCommonJsExport) return;
3622
+ if ((stem3 === "type" || stem3 === "types") && publicTypeExportCount(program) >= 2) return;
3605
3623
  const exports2 = runtimeExports(program);
3606
3624
  if (exports2.ambiguous || exports2.exports.length !== 1) return;
3607
3625
  const onlyExport = exports2.exports[0];
@@ -3735,7 +3753,7 @@ function moduleScopeBindingName(node) {
3735
3753
  }
3736
3754
  if (topLevel.type === import_utils23.AST_NODE_TYPES.ClassDeclaration) {
3737
3755
  let owner = node.parent;
3738
- while (owner !== void 0 && owner.parent !== topLevel.body) owner = owner.parent;
3756
+ while (owner != null && owner.parent !== topLevel.body) owner = owner.parent;
3739
3757
  return (owner?.type === import_utils23.AST_NODE_TYPES.MethodDefinition || owner?.type === import_utils23.AST_NODE_TYPES.TSAbstractMethodDefinition || owner?.type === import_utils23.AST_NODE_TYPES.PropertyDefinition) && owner.value === node ? topLevel.id?.name ?? (current.type === import_utils23.AST_NODE_TYPES.ExportDefaultDeclaration ? "default" : null) : null;
3740
3758
  }
3741
3759
  if (topLevel.type === import_utils23.AST_NODE_TYPES.VariableDeclaration) {
@@ -3746,7 +3764,7 @@ function moduleScopeBindingName(node) {
3746
3764
  if (declarator.id.type === import_utils23.AST_NODE_TYPES.Identifier && (initializer?.type === import_utils23.AST_NODE_TYPES.ClassExpression || initializer?.type === import_utils23.AST_NODE_TYPES.ObjectExpression)) {
3747
3765
  let owner = node.parent;
3748
3766
  const container = initializer.type === import_utils23.AST_NODE_TYPES.ClassExpression ? initializer.body : initializer;
3749
- while (owner !== void 0 && owner.parent !== container) owner = owner.parent;
3767
+ while (owner != null && owner.parent !== container) owner = owner.parent;
3750
3768
  if ((owner?.type === import_utils23.AST_NODE_TYPES.MethodDefinition || owner?.type === import_utils23.AST_NODE_TYPES.PropertyDefinition || owner?.type === import_utils23.AST_NODE_TYPES.Property) && owner.value === node) return declarator.id.name;
3751
3769
  }
3752
3770
  }
@@ -3898,7 +3916,10 @@ var no_positional_tuple_return_default = createRule({
3898
3916
  const tuple = tupleReturnType(annotation, aliases);
3899
3917
  if (tuple === null || tuple.elementTypes.length < MIN_ELEMENTS) return;
3900
3918
  context.report({
3901
- node: tuple,
3919
+ // Report the public boundary's annotation, not a shared alias declaration.
3920
+ // Otherwise every exported function returning the same alias produces a
3921
+ // stack of diagnostics at the alias's single source location.
3922
+ node: annotation,
3902
3923
  messageId: "noPositionalTupleReturn",
3903
3924
  data: { name, count: String(tuple.elementTypes.length) }
3904
3925
  });
@@ -7539,6 +7560,7 @@ var prefer_input_group_search_default = createRule({
7539
7560
  // src/rules/prefer-immutable-module-constant.ts
7540
7561
  var import_utils52 = require("@typescript-eslint/utils");
7541
7562
  var CONSTANT_NAME = /^_?[A-Z][A-Z0-9_]*$/;
7563
+ var JAVASCRIPT_FILE_RE = /\.[cm]?jsx?$/i;
7542
7564
  var MUTATING_METHODS = /* @__PURE__ */ new Set([
7543
7565
  "add",
7544
7566
  "clear",
@@ -7662,7 +7684,7 @@ var prefer_immutable_module_constant_default = createRule({
7662
7684
  const variable = import_utils52.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
7663
7685
  return variable === null || variable.defs.length === 0;
7664
7686
  };
7665
- if (isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
7687
+ if (JAVASCRIPT_FILE_RE.test(context.filename) || isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
7666
7688
  return {};
7667
7689
  }
7668
7690
  const exportedNames2 = /* @__PURE__ */ new Set();
@@ -10772,6 +10794,9 @@ var HTTP_TRANSPORT_TYPE_RE = /^(?:KyInstance|AxiosInstance|Session)$/;
10772
10794
  var BUILTIN_CONTAINER_TYPE_RE = /^(?:Record|Map|WeakMap|Set|WeakSet|Array|ReadonlyArray|ReadonlyMap|ReadonlySet|Promise|Partial|Required|Readonly|Pick|Omit|Exclude|Extract|NonNullable|Awaited|Parameters|ReturnType|InstanceType)$/;
10773
10795
  var VALUE_TYPE_RE = /^(?:ArrayBuffer|Blob|Buffer|Date|RegExp|URL|URLSearchParams)$/;
10774
10796
  var TRANSPORT_WRAPPER_NAME_RE = /Client$/;
10797
+ var ERROR_VALUE_NAME_RE = /Error$/;
10798
+ var FLUENT_BUILDER_NAME_RE = /Builder$/;
10799
+ var FLUENT_RESULT_TYPE_RE = /(?:Builder|Base|Query|Without)(?:\W|$)/;
10775
10800
  var ROUTER_FACTORY_NAME = "Router";
10776
10801
  var FRAMEWORK_HTTP_TYPES = /* @__PURE__ */ new Set(["Request", "Response", "NextFunction"]);
10777
10802
  var STORAGE_ASSIGNMENT_OPERATORS = /* @__PURE__ */ new Set(["=", "&&=", "??=", "||="]);
@@ -11013,6 +11038,19 @@ var publicMethodNames = (body2, functionAliases) => {
11013
11038
  }
11014
11039
  return names;
11015
11040
  };
11041
+ var isFluentConstructionObject = (node, getText) => {
11042
+ if (node.id === null) return false;
11043
+ const methods = node.body.body.filter(
11044
+ (member) => member.type === import_utils67.AST_NODE_TYPES.MethodDefinition && member.kind === "method" && !member.static && member.accessibility !== "private" && member.accessibility !== "protected" && member.value.body !== null
11045
+ );
11046
+ if (methods.length === 0) return false;
11047
+ return methods.every((member) => {
11048
+ const result = member.value.returnType?.typeAnnotation;
11049
+ if (result === void 0) return false;
11050
+ const returnsOwnType = result.type === import_utils67.AST_NODE_TYPES.TSTypeReference && result.typeName.type === import_utils67.AST_NODE_TYPES.Identifier && result.typeName.name === node.id?.name;
11051
+ return returnsOwnType || FLUENT_BUILDER_NAME_RE.test(node.id?.name ?? "") && FLUENT_RESULT_TYPE_RE.test(getText(result));
11052
+ });
11053
+ };
11016
11054
  function localClassAbstractness(program) {
11017
11055
  const classes = /* @__PURE__ */ new Map();
11018
11056
  const parents = /* @__PURE__ */ new Map();
@@ -11158,6 +11196,7 @@ var require_interface_for_injected_service_default = createRule({
11158
11196
  ClassDeclaration(node) {
11159
11197
  if (node.id === null) return;
11160
11198
  if (!isExportedClass2(node, detachedExports)) return;
11199
+ if (ERROR_VALUE_NAME_RE.test(node.id.name)) return;
11161
11200
  if (node.abstract === true) return;
11162
11201
  if (node.decorators.length > 0) return;
11163
11202
  const ctor = node.body.body.find(
@@ -11173,6 +11212,7 @@ var require_interface_for_injected_service_default = createRule({
11173
11212
  if (constructedFields > collaborators.length) return;
11174
11213
  if (isFrameworkWiring(node.body)) return;
11175
11214
  if (isTransportWrapper(node.id.name, collaborators, context.sourceCode.ast)) return;
11215
+ if (isFluentConstructionObject(node, (result) => context.sourceCode.getText(result))) return;
11176
11216
  const methods = publicMethodNames(node.body, objectTypes().functionAliases);
11177
11217
  if (methods.length === 0) return;
11178
11218
  if (hasServicePort(node, methods, localClasses, localInterfaces)) return;
@@ -12032,7 +12072,7 @@ var rules = {
12032
12072
  };
12033
12073
  var meta = {
12034
12074
  name: "@sarj/eslint-plugin",
12035
- version: "9.15.0"
12075
+ version: "9.16.0"
12036
12076
  };
12037
12077
  var applicationOnlyRules = [
12038
12078
  "no-restricted-library-load",
package/dist/index.d.cts CHANGED
@@ -466,7 +466,7 @@ type FlatPreset = {
466
466
  declare const plugin: {
467
467
  meta: {
468
468
  readonly name: "@sarj/eslint-plugin";
469
- readonly version: "9.15.0";
469
+ readonly version: "9.16.0";
470
470
  };
471
471
  rules: {
472
472
  "duplicate-test-body": _typescript_eslint_utils_ts_eslint.RuleModule<"duplicateTestBody", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
package/dist/index.d.ts CHANGED
@@ -466,7 +466,7 @@ type FlatPreset = {
466
466
  declare const plugin: {
467
467
  meta: {
468
468
  readonly name: "@sarj/eslint-plugin";
469
- readonly version: "9.15.0";
469
+ readonly version: "9.16.0";
470
470
  };
471
471
  rules: {
472
472
  "duplicate-test-body": _typescript_eslint_utils_ts_eslint.RuleModule<"duplicateTestBody", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
package/dist/index.js CHANGED
@@ -3518,6 +3518,23 @@ function runtimeExports(program) {
3518
3518
  const unique = new Map(exports.map((entry) => [entry.key, entry]));
3519
3519
  return { exports: [...unique.values()], ambiguous };
3520
3520
  }
3521
+ function publicTypeExportCount(program) {
3522
+ const names = /* @__PURE__ */ new Set();
3523
+ const typeBindings = typeOnlyBindings(program);
3524
+ for (const statement of program.body) {
3525
+ if (statement.type !== AST_NODE_TYPES15.ExportNamedDeclaration) continue;
3526
+ const declaration = statement.declaration;
3527
+ if (declaration?.type === AST_NODE_TYPES15.TSInterfaceDeclaration || declaration?.type === AST_NODE_TYPES15.TSTypeAliasDeclaration) names.add(declaration.id.name);
3528
+ for (const specifier of statement.specifiers) {
3529
+ const local = specifier.local.type === AST_NODE_TYPES15.Identifier ? specifier.local.name : specifier.local.value;
3530
+ if (statement.exportKind !== "type" && specifier.exportKind !== "type" && !typeBindings.has(local)) {
3531
+ continue;
3532
+ }
3533
+ names.add(specifier.exported.type === AST_NODE_TYPES15.Identifier ? specifier.exported.name : specifier.exported.value);
3534
+ }
3535
+ }
3536
+ return names.size;
3537
+ }
3521
3538
  function kebabCase(name) {
3522
3539
  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();
3523
3540
  }
@@ -3560,6 +3577,7 @@ var no_generic_single_export_module_default = createRule({
3560
3577
  },
3561
3578
  "Program:exit"(program) {
3562
3579
  if (hasCommonJsExport) return;
3580
+ if ((stem3 === "type" || stem3 === "types") && publicTypeExportCount(program) >= 2) return;
3563
3581
  const exports = runtimeExports(program);
3564
3582
  if (exports.ambiguous || exports.exports.length !== 1) return;
3565
3583
  const onlyExport = exports.exports[0];
@@ -3693,7 +3711,7 @@ function moduleScopeBindingName(node) {
3693
3711
  }
3694
3712
  if (topLevel.type === AST_NODE_TYPES16.ClassDeclaration) {
3695
3713
  let owner = node.parent;
3696
- while (owner !== void 0 && owner.parent !== topLevel.body) owner = owner.parent;
3714
+ while (owner != null && owner.parent !== topLevel.body) owner = owner.parent;
3697
3715
  return (owner?.type === AST_NODE_TYPES16.MethodDefinition || owner?.type === AST_NODE_TYPES16.TSAbstractMethodDefinition || owner?.type === AST_NODE_TYPES16.PropertyDefinition) && owner.value === node ? topLevel.id?.name ?? (current.type === AST_NODE_TYPES16.ExportDefaultDeclaration ? "default" : null) : null;
3698
3716
  }
3699
3717
  if (topLevel.type === AST_NODE_TYPES16.VariableDeclaration) {
@@ -3704,7 +3722,7 @@ function moduleScopeBindingName(node) {
3704
3722
  if (declarator.id.type === AST_NODE_TYPES16.Identifier && (initializer?.type === AST_NODE_TYPES16.ClassExpression || initializer?.type === AST_NODE_TYPES16.ObjectExpression)) {
3705
3723
  let owner = node.parent;
3706
3724
  const container = initializer.type === AST_NODE_TYPES16.ClassExpression ? initializer.body : initializer;
3707
- while (owner !== void 0 && owner.parent !== container) owner = owner.parent;
3725
+ while (owner != null && owner.parent !== container) owner = owner.parent;
3708
3726
  if ((owner?.type === AST_NODE_TYPES16.MethodDefinition || owner?.type === AST_NODE_TYPES16.PropertyDefinition || owner?.type === AST_NODE_TYPES16.Property) && owner.value === node) return declarator.id.name;
3709
3727
  }
3710
3728
  }
@@ -3856,7 +3874,10 @@ var no_positional_tuple_return_default = createRule({
3856
3874
  const tuple = tupleReturnType(annotation, aliases);
3857
3875
  if (tuple === null || tuple.elementTypes.length < MIN_ELEMENTS) return;
3858
3876
  context.report({
3859
- node: tuple,
3877
+ // Report the public boundary's annotation, not a shared alias declaration.
3878
+ // Otherwise every exported function returning the same alias produces a
3879
+ // stack of diagnostics at the alias's single source location.
3880
+ node: annotation,
3860
3881
  messageId: "noPositionalTupleReturn",
3861
3882
  data: { name, count: String(tuple.elementTypes.length) }
3862
3883
  });
@@ -7500,6 +7521,7 @@ var prefer_input_group_search_default = createRule({
7500
7521
  // src/rules/prefer-immutable-module-constant.ts
7501
7522
  import { AST_NODE_TYPES as AST_NODE_TYPES38, ASTUtils as ASTUtils6 } from "@typescript-eslint/utils";
7502
7523
  var CONSTANT_NAME = /^_?[A-Z][A-Z0-9_]*$/;
7524
+ var JAVASCRIPT_FILE_RE = /\.[cm]?jsx?$/i;
7503
7525
  var MUTATING_METHODS = /* @__PURE__ */ new Set([
7504
7526
  "add",
7505
7527
  "clear",
@@ -7623,7 +7645,7 @@ var prefer_immutable_module_constant_default = createRule({
7623
7645
  const variable = ASTUtils6.findVariable(sourceCode.getScope(identifier), identifier.name);
7624
7646
  return variable === null || variable.defs.length === 0;
7625
7647
  };
7626
- if (isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
7648
+ if (JAVASCRIPT_FILE_RE.test(context.filename) || isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
7627
7649
  return {};
7628
7650
  }
7629
7651
  const exportedNames2 = /* @__PURE__ */ new Set();
@@ -10736,6 +10758,9 @@ var HTTP_TRANSPORT_TYPE_RE = /^(?:KyInstance|AxiosInstance|Session)$/;
10736
10758
  var BUILTIN_CONTAINER_TYPE_RE = /^(?:Record|Map|WeakMap|Set|WeakSet|Array|ReadonlyArray|ReadonlyMap|ReadonlySet|Promise|Partial|Required|Readonly|Pick|Omit|Exclude|Extract|NonNullable|Awaited|Parameters|ReturnType|InstanceType)$/;
10737
10759
  var VALUE_TYPE_RE = /^(?:ArrayBuffer|Blob|Buffer|Date|RegExp|URL|URLSearchParams)$/;
10738
10760
  var TRANSPORT_WRAPPER_NAME_RE = /Client$/;
10761
+ var ERROR_VALUE_NAME_RE = /Error$/;
10762
+ var FLUENT_BUILDER_NAME_RE = /Builder$/;
10763
+ var FLUENT_RESULT_TYPE_RE = /(?:Builder|Base|Query|Without)(?:\W|$)/;
10739
10764
  var ROUTER_FACTORY_NAME = "Router";
10740
10765
  var FRAMEWORK_HTTP_TYPES = /* @__PURE__ */ new Set(["Request", "Response", "NextFunction"]);
10741
10766
  var STORAGE_ASSIGNMENT_OPERATORS = /* @__PURE__ */ new Set(["=", "&&=", "??=", "||="]);
@@ -10977,6 +11002,19 @@ var publicMethodNames = (body2, functionAliases) => {
10977
11002
  }
10978
11003
  return names;
10979
11004
  };
11005
+ var isFluentConstructionObject = (node, getText) => {
11006
+ if (node.id === null) return false;
11007
+ const methods = node.body.body.filter(
11008
+ (member) => member.type === AST_NODE_TYPES52.MethodDefinition && member.kind === "method" && !member.static && member.accessibility !== "private" && member.accessibility !== "protected" && member.value.body !== null
11009
+ );
11010
+ if (methods.length === 0) return false;
11011
+ return methods.every((member) => {
11012
+ const result = member.value.returnType?.typeAnnotation;
11013
+ if (result === void 0) return false;
11014
+ const returnsOwnType = result.type === AST_NODE_TYPES52.TSTypeReference && result.typeName.type === AST_NODE_TYPES52.Identifier && result.typeName.name === node.id?.name;
11015
+ return returnsOwnType || FLUENT_BUILDER_NAME_RE.test(node.id?.name ?? "") && FLUENT_RESULT_TYPE_RE.test(getText(result));
11016
+ });
11017
+ };
10980
11018
  function localClassAbstractness(program) {
10981
11019
  const classes = /* @__PURE__ */ new Map();
10982
11020
  const parents = /* @__PURE__ */ new Map();
@@ -11122,6 +11160,7 @@ var require_interface_for_injected_service_default = createRule({
11122
11160
  ClassDeclaration(node) {
11123
11161
  if (node.id === null) return;
11124
11162
  if (!isExportedClass2(node, detachedExports)) return;
11163
+ if (ERROR_VALUE_NAME_RE.test(node.id.name)) return;
11125
11164
  if (node.abstract === true) return;
11126
11165
  if (node.decorators.length > 0) return;
11127
11166
  const ctor = node.body.body.find(
@@ -11137,6 +11176,7 @@ var require_interface_for_injected_service_default = createRule({
11137
11176
  if (constructedFields > collaborators.length) return;
11138
11177
  if (isFrameworkWiring(node.body)) return;
11139
11178
  if (isTransportWrapper(node.id.name, collaborators, context.sourceCode.ast)) return;
11179
+ if (isFluentConstructionObject(node, (result) => context.sourceCode.getText(result))) return;
11140
11180
  const methods = publicMethodNames(node.body, objectTypes().functionAliases);
11141
11181
  if (methods.length === 0) return;
11142
11182
  if (hasServicePort(node, methods, localClasses, localInterfaces)) return;
@@ -11996,7 +12036,7 @@ var rules = {
11996
12036
  };
11997
12037
  var meta = {
11998
12038
  name: "@sarj/eslint-plugin",
11999
- version: "9.15.0"
12039
+ version: "9.16.0"
12000
12040
  };
12001
12041
  var applicationOnlyRules = [
12002
12042
  "no-restricted-library-load",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "9.15.0",
3
+ "version": "9.16.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",