@sarj/eslint-plugin 16.36.5 → 16.36.7

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 (3) hide show
  1. package/dist/index.cjs +101 -24
  2. package/dist/index.js +101 -24
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -2314,8 +2314,13 @@ function commentSuppressesChecking(text, kind) {
2314
2314
 
2315
2315
  // src/rules/_closed-object-contract.ts
2316
2316
  var import_typescript2 = __toESM(require("typescript"), 1);
2317
- function closedObjectContract(type, checker) {
2318
- return visitContract(type, { checker, active: /* @__PURE__ */ new Set(), completed: /* @__PURE__ */ new WeakMap() });
2317
+ function closedObjectContract(type, checker, singlePropertyOrigins = false) {
2318
+ return visitContract(type, {
2319
+ checker,
2320
+ active: /* @__PURE__ */ new Set(),
2321
+ completed: /* @__PURE__ */ new WeakMap(),
2322
+ origins: singlePropertyOrigins ? /* @__PURE__ */ new Map() : void 0
2323
+ });
2319
2324
  }
2320
2325
  function visitContract(type, proof) {
2321
2326
  const known2 = proof.completed.get(type);
@@ -2329,10 +2334,20 @@ function visitContract(type, proof) {
2329
2334
  }
2330
2335
  function completeObject(type, proof) {
2331
2336
  if (!ordinaryObject(type, proof.checker)) return false;
2337
+ if (proof.origins && !singleOrigins(type, proof.origins)) return false;
2332
2338
  const declarations = type.getSymbol()?.getDeclarations()?.filter((declaration) => import_typescript2.default.isInterfaceDeclaration(declaration) || import_typescript2.default.isTypeLiteralNode(declaration));
2333
2339
  if (!declarations?.length) return false;
2334
2340
  return declarations.every((declaration) => completeDeclaration(declaration, proof));
2335
2341
  }
2342
+ function singleOrigins(type, origins) {
2343
+ for (const property of type.getProperties()) {
2344
+ const declarations = property.getDeclarations();
2345
+ const declaration = declarations?.[0];
2346
+ if (!declaration || declarations?.length !== 1 || origins.has(property.name) && origins.get(property.name) !== declaration) return false;
2347
+ origins.set(property.name, declaration);
2348
+ }
2349
+ return true;
2350
+ }
2336
2351
  function ordinaryObject(type, checker) {
2337
2352
  if ((type.flags & import_typescript2.default.TypeFlags.Object) === 0 || type.isUnion() || checker.getIndexInfosOfType(type).length > 0) return false;
2338
2353
  const object = type;
@@ -2651,6 +2666,9 @@ function directEval(node) {
2651
2666
  function primitiveKeyword(node) {
2652
2667
  return node !== void 0 && (node.kind === import_typescript6.default.SyntaxKind.StringKeyword || node.kind === import_typescript6.default.SyntaxKind.NumberKeyword || node.kind === import_typescript6.default.SyntaxKind.BooleanKeyword || node.kind === import_typescript6.default.SyntaxKind.BigIntKeyword);
2653
2668
  }
2669
+ function nullishPrimitiveUnion(node) {
2670
+ return node !== void 0 && import_typescript6.default.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === import_typescript6.default.SyntaxKind.UndefinedKeyword || import_typescript6.default.isLiteralTypeNode(part) && part.literal.kind === import_typescript6.default.SyntaxKind.NullKeyword);
2671
+ }
2654
2672
  function consumerAnnotation(node) {
2655
2673
  if (node.params.length !== 1) return void 0;
2656
2674
  const parameter = node.params[0];
@@ -2708,6 +2726,8 @@ function directCalls(binding3) {
2708
2726
  // src/rules/no-redundant-consumer-union.ts
2709
2727
  var CONFIG = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
2710
2728
  var EXAMPLE = 'export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: "Ada", breed: "Collie"}; function label(value: Dog | Animal): string {return value.name;} label(dog);';
2729
+ var NULLABLE_EXAMPLE = EXAMPLE.replace("readonly name: string", "readonly name: string | null").replace("return value.name;", "return value.name ?? '';");
2730
+ var OPTIONAL_EXAMPLE = EXAMPLE.replace("readonly breed: string", "readonly breed?: string");
2711
2731
  var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2712
2732
  defaultLevel: "warning",
2713
2733
  summary: "Keep the covering object contract in a private scalar consumer union.",
@@ -2718,25 +2738,91 @@ var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2718
2738
  limitations: [
2719
2739
  "Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Removed named types/imports must retain an independent top-level exported type-alias or direct type-export read. Other removable annotations/assertions, the covering annotation and own-definition/self references cannot keep one another live; otherwise simultaneous fixes can introduce unused-type errors. The exact authored covering type and terminator token sequence are retained; edits that would merge distinct CR and LF terminators remain manual; no producer, call or function body is rewritten.",
2720
2740
  "Only an inline binary parameter union in a strict module's top-level private scalar consumer is supported. Exactly one parameter, an explicit broad primitive return annotation, a single pure returned scalar expression and direct identifier-argument callers are required. Type queries or type-only exports of the callable, public APIs, callbacks, contextual signatures, overloads, generics, decorators, async functions, direct eval and fresh-object callers prevent findings.",
2721
- "Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, callbacks and recursive or nested object fields are unsupported.",
2722
- "Reports only proper native assignability: covered is assignable to covering and the reverse is false. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any also cannot witness coverage. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; mutually assignable but nonidentical structures are excluded.",
2741
+ "Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties, optionally paired directly with null or undefined in an exact two-part union. Required and optional nullable fields may be read under the same native coverage proof. Every field must have one actual declaration origin throughout each alternative's inheritance/intersection graph. Shared ancestors retain that origin; repeated merged fields, explicit overrides and independently declared overlapping fields abstain, including valid compatible declarations. This excludes recovered conflicting properties without claiming whole-source health. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, parenthesized or more complex property unions, callbacks and recursive or nested object fields are unsupported.",
2742
+ "Reports proper native assignability, where covered is assignable to covering and the reverse is false, or mutual native assignability with a strict smaller declared covering surface and only optional covered-only members. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any cannot replace explicit declared coverage or the native Optional requirement. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; equal-key and incomparable mutually assignable structures are excluded. The mutual case additionally requires inline contracts or locally bound interfaces/type aliases whose alias and heritage references can be traced through the parser-owned type scope. Merged interface fragments must all resolve to the reference's same native symbol; mixed exported/local recovery abstains. Imported, global and qualified-namespace references in that path abstain; proper one-way coverage retains its existing alias/import support.",
2723
2743
  "General union simplification can change Extract, conditional types, in-narrowing, fresh-literal checking and public presentation. Those contexts are excluded even when ordinary assignment succeeds. Flat destructuring counts every bound field as read; whole-object escape, mutation, helpers, closures, reflection, serialization, dynamic reads, implicit Object capabilities and JSX prevent findings. Imported Python protocol assignability is not approximated by this rule."
2724
2744
  ],
2725
2745
  examples: [
2726
2746
  { id: "covered-alternative", title: "The private consumer needs the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: EXAMPLE.replace("Dog | Animal", "Animal") }] },
2727
- { id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
2747
+ { id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
2748
+ { id: "optional-covered-alternative", scenarioId: "optional-readonly-contract", title: "An unread optional breed retains the smaller Animal surface", outcome: "match", files: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE.replace("Dog | Animal", "Animal") }] },
2749
+ { id: "optional-public-contract", scenarioId: "optional-readonly-contract", title: "A public optional union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
2750
+ { id: "nullable-covered-alternative", scenarioId: "nullable-readonly-contract", title: "A nullable name retains the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("Dog | Animal", "Animal") }] },
2751
+ { id: "nullable-domain-contract", scenarioId: "nullable-readonly-contract", title: "A literal breed keeps the authored domain contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("readonly breed: string", "readonly breed: 'Collie'") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
2728
2752
  ]
2729
2753
  };
2730
2754
  function scalarFields(type, checker) {
2731
- if (!closedObjectContract(type, checker)) return null;
2755
+ if (!closedObjectContract(type, checker, true)) return null;
2732
2756
  const fields2 = /* @__PURE__ */ new Map();
2733
2757
  for (const property of type.getProperties()) {
2734
2758
  const declarations = property.getDeclarations();
2735
- if (!declarations?.length || !declarations.every((member3) => import_typescript7.default.isPropertySignature(member3) && member3.name && (import_typescript7.default.isIdentifier(member3.name) || import_typescript7.default.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === import_typescript7.default.SyntaxKind.ReadonlyKeyword && primitiveKeyword(member3.type))) return null;
2759
+ if (!declarations?.length || !declarations.every((member3) => import_typescript7.default.isPropertySignature(member3) && member3.name && (import_typescript7.default.isIdentifier(member3.name) || import_typescript7.default.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === import_typescript7.default.SyntaxKind.ReadonlyKeyword && (primitiveKeyword(member3.type) || nullishPrimitiveUnion(member3.type)))) return null;
2736
2760
  fields2.set(property.name, (property.flags & import_typescript7.default.SymbolFlags.Optional) === 0);
2737
2761
  }
2738
2762
  return fields2.size ? fields2 : null;
2739
2763
  }
2764
+ function localContractBindings(roots, source, services) {
2765
+ const pending = [...roots];
2766
+ const visited = /* @__PURE__ */ new Set();
2767
+ for (let index = 0; index < pending.length; index++) {
2768
+ const node = pending[index];
2769
+ if (!node || visited.has(node)) continue;
2770
+ visited.add(node);
2771
+ const references = localContractReferences(node, source, services);
2772
+ if (references === null) return false;
2773
+ pending.push(...references);
2774
+ }
2775
+ return true;
2776
+ }
2777
+ function localContractReferences(node, source, services) {
2778
+ switch (node.type) {
2779
+ case import_utils24.AST_NODE_TYPES.TSTypeLiteral:
2780
+ return [];
2781
+ case import_utils24.AST_NODE_TYPES.TSIntersectionType:
2782
+ return node.types;
2783
+ case import_utils24.AST_NODE_TYPES.TSTypeReference:
2784
+ return !node.typeArguments && node.typeName.type === import_utils24.AST_NODE_TYPES.Identifier ? [node.typeName] : null;
2785
+ case import_utils24.AST_NODE_TYPES.Identifier:
2786
+ return localBindingReferences(node, source, services);
2787
+ default:
2788
+ return null;
2789
+ }
2790
+ }
2791
+ function localBindingReferences(identifier, source, services) {
2792
+ const definitions2 = typeBinding(source, identifier)?.defs.filter((definition) => definition.isTypeDefinition).map((definition) => definition.node);
2793
+ if (!definitions2?.length || !definitions2.every(moduleTypeDeclaration)) return null;
2794
+ const first = definitions2[0];
2795
+ if (definitions2.length === 1 && first?.type === import_utils24.AST_NODE_TYPES.TSTypeAliasDeclaration && !first.typeParameters) return [first.typeAnnotation];
2796
+ const interfaces = definitions2.filter((definition) => definition.type === import_utils24.AST_NODE_TYPES.TSInterfaceDeclaration);
2797
+ if (interfaces.length !== definitions2.length || interfaces.some((declaration) => declaration.typeParameters)) return null;
2798
+ if (interfaces.length > 1) {
2799
+ const checker = services.program.getTypeChecker();
2800
+ const symbol = checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(identifier));
2801
+ if (!symbol || interfaces.some((declaration) => checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(declaration.id)) !== symbol)) return null;
2802
+ }
2803
+ const bases = interfaces.flatMap((declaration) => declaration.extends.map((base) => base.expression));
2804
+ return bases.every((base) => base.type === import_utils24.AST_NODE_TYPES.Identifier) ? bases : null;
2805
+ }
2806
+ function moduleTypeDeclaration(node) {
2807
+ const parent = node.parent;
2808
+ if (parent?.type === import_utils24.AST_NODE_TYPES.Program) return true;
2809
+ if (parent?.type !== import_utils24.AST_NODE_TYPES.ExportNamedDeclaration && parent?.type !== import_utils24.AST_NODE_TYPES.ExportDefaultDeclaration) return false;
2810
+ return parent.parent.type === import_utils24.AST_NODE_TYPES.Program;
2811
+ }
2812
+ function coveringAlternative(leftNode, rightNode, checker, fieldsOf, reads) {
2813
+ const left = checker.getTypeFromTypeNode(leftNode);
2814
+ const right = checker.getTypeFromTypeNode(rightNode);
2815
+ const leftFields = fieldsOf(left);
2816
+ const rightFields = fieldsOf(right);
2817
+ if (!leftFields || !rightFields) return null;
2818
+ const forward = checker.isTypeAssignableTo(left, right);
2819
+ const reverse = checker.isTypeAssignableTo(right, left);
2820
+ const mutual = forward && reverse;
2821
+ if (!forward && !reverse || mutual && leftFields.size === rightFields.size) return null;
2822
+ const [covered2, covering, coveredFields, coveringFields] = forward && (!reverse || leftFields.size > rightFields.size) ? [leftNode, rightNode, leftFields, rightFields] : [rightNode, leftNode, rightFields, leftFields];
2823
+ if ([...coveringFields.keys()].some((key) => !coveredFields.has(key)) || mutual && [...coveredFields].some(([key, required]) => required && !coveringFields.has(key)) || [...reads].some((key) => !coveringFields.has(key)) || ![...reads].some((key) => coveringFields.get(key))) return null;
2824
+ return { covered: covered2, covering, mutual };
2825
+ }
2740
2826
  function privateUnionFix(sourceCode, services, proof) {
2741
2827
  const covering = services.tsNodeToESTreeNodeMap.get(proof.covering);
2742
2828
  if (!validConsumerFixCalls(proof.candidate, proof.parameter, sourceCode, services)) return null;
@@ -2787,17 +2873,11 @@ var no_redundant_consumer_union_default = createRule({
2787
2873
  const leftNode = annotation.types[0];
2788
2874
  const rightNode = annotation.types[1];
2789
2875
  if (!leftNode || !rightNode) continue;
2790
- const left = checker.getTypeFromTypeNode(leftNode);
2791
- const right = checker.getTypeFromTypeNode(rightNode);
2792
- const leftFields = fieldsOf(left);
2793
- const rightFields = fieldsOf(right);
2794
- if (!leftFields || !rightFields) continue;
2795
- const forward = checker.isTypeAssignableTo(left, right);
2796
- const reverse = checker.isTypeAssignableTo(right, left);
2797
- if (forward === reverse) continue;
2798
- const [coveredNode, coveringNode, coveredFields, coveringFields] = forward ? [leftNode, rightNode, leftFields, rightFields] : [rightNode, leftNode, rightFields, leftFields];
2799
- if ([...coveringFields.keys()].some((key) => !coveredFields.has(key)) || [...consumer.reads].some((key) => !coveringFields.has(key)) || ![...consumer.reads].some((key) => coveringFields.get(key))) continue;
2876
+ const coverage = coveringAlternative(leftNode, rightNode, checker, fieldsOf, consumer.reads);
2877
+ if (!coverage) continue;
2800
2878
  const reportNode = services.tsNodeToESTreeNodeMap.get(annotation);
2879
+ if (coverage.mutual && (reportNode.type !== import_utils24.AST_NODE_TYPES.TSUnionType || !localContractBindings(reportNode.types, context.sourceCode, services))) continue;
2880
+ const { covered: coveredNode, covering: coveringNode } = coverage;
2801
2881
  const fix = privateUnionFix(context.sourceCode, services, {
2802
2882
  candidate: candidate2,
2803
2883
  parameter: consumer.parameter,
@@ -2819,7 +2899,7 @@ var import_typescript8 = __toESM(require("typescript"), 1);
2819
2899
  var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
2820
2900
  var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
2821
2901
  var HELPER_EXAMPLE = 'export {}; const account = {name: "Ada", email: "a"}; function formatName(name: string): string {return name.toUpperCase();} function label(user: {email: string; name: string}): string {return formatName(user.name);} label(account);';
2822
- var NULLABLE_EXAMPLE = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
2902
+ var NULLABLE_EXAMPLE2 = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
2823
2903
  var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
2824
2904
  defaultLevel: "warning",
2825
2905
  summary: "Prefer the proven read-only subset for a private consumer parameter.",
@@ -2838,8 +2918,8 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
2838
2918
  { id: "used-fields", title: "Every required dependency is read", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE2.replace("return user.name;", "return user.name + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
2839
2919
  { id: "scalar-helper", scenarioId: "local-scalar-helper", title: "Only the name crosses a local scalar helper call", outcome: "match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("email: string;", "") }] },
2840
2920
  { id: "helper-and-other-field", scenarioId: "local-scalar-helper", title: "The consumer still requires a separately read field", outcome: "no-match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("return formatName(user.name);", "return formatName(user.name) + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
2841
- { id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("email: string | null;", "") }] },
2842
- { id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
2921
+ { id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2 }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("email: string | null;", "") }] },
2922
+ { id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
2843
2923
  ]
2844
2924
  };
2845
2925
  function inlineFields(node, reads) {
@@ -2853,9 +2933,6 @@ function inlineFields(node, reads) {
2853
2933
  }
2854
2934
  return fields2;
2855
2935
  }
2856
- function nullishPrimitiveUnion(node) {
2857
- return node !== void 0 && import_typescript8.default.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === import_typescript8.default.SyntaxKind.UndefinedKeyword || import_typescript8.default.isLiteralTypeNode(part) && part.literal.kind === import_typescript8.default.SyntaxKind.NullKeyword);
2858
- }
2859
2936
  var prefer_minimal_parameter_contract_default = createRule({
2860
2937
  name: "prefer-minimal-parameter-contract",
2861
2938
  documentation: PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION,
@@ -4007,7 +4084,7 @@ function isSearchParamReceiver(call, services) {
4007
4084
  // package.json
4008
4085
  var package_default = {
4009
4086
  name: "@sarj/eslint-plugin",
4010
- version: "16.36.5",
4087
+ version: "16.36.7",
4011
4088
  packageManager: "npm@12.2.1",
4012
4089
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
4013
4090
  type: "module",
package/dist/index.js CHANGED
@@ -2263,8 +2263,13 @@ function commentSuppressesChecking(text, kind) {
2263
2263
 
2264
2264
  // src/rules/_closed-object-contract.ts
2265
2265
  import ts2 from "typescript";
2266
- function closedObjectContract(type, checker) {
2267
- return visitContract(type, { checker, active: /* @__PURE__ */ new Set(), completed: /* @__PURE__ */ new WeakMap() });
2266
+ function closedObjectContract(type, checker, singlePropertyOrigins = false) {
2267
+ return visitContract(type, {
2268
+ checker,
2269
+ active: /* @__PURE__ */ new Set(),
2270
+ completed: /* @__PURE__ */ new WeakMap(),
2271
+ origins: singlePropertyOrigins ? /* @__PURE__ */ new Map() : void 0
2272
+ });
2268
2273
  }
2269
2274
  function visitContract(type, proof) {
2270
2275
  const known2 = proof.completed.get(type);
@@ -2278,10 +2283,20 @@ function visitContract(type, proof) {
2278
2283
  }
2279
2284
  function completeObject(type, proof) {
2280
2285
  if (!ordinaryObject(type, proof.checker)) return false;
2286
+ if (proof.origins && !singleOrigins(type, proof.origins)) return false;
2281
2287
  const declarations = type.getSymbol()?.getDeclarations()?.filter((declaration) => ts2.isInterfaceDeclaration(declaration) || ts2.isTypeLiteralNode(declaration));
2282
2288
  if (!declarations?.length) return false;
2283
2289
  return declarations.every((declaration) => completeDeclaration(declaration, proof));
2284
2290
  }
2291
+ function singleOrigins(type, origins) {
2292
+ for (const property of type.getProperties()) {
2293
+ const declarations = property.getDeclarations();
2294
+ const declaration = declarations?.[0];
2295
+ if (!declaration || declarations?.length !== 1 || origins.has(property.name) && origins.get(property.name) !== declaration) return false;
2296
+ origins.set(property.name, declaration);
2297
+ }
2298
+ return true;
2299
+ }
2285
2300
  function ordinaryObject(type, checker) {
2286
2301
  if ((type.flags & ts2.TypeFlags.Object) === 0 || type.isUnion() || checker.getIndexInfosOfType(type).length > 0) return false;
2287
2302
  const object = type;
@@ -2600,6 +2615,9 @@ function directEval(node) {
2600
2615
  function primitiveKeyword(node) {
2601
2616
  return node !== void 0 && (node.kind === ts6.SyntaxKind.StringKeyword || node.kind === ts6.SyntaxKind.NumberKeyword || node.kind === ts6.SyntaxKind.BooleanKeyword || node.kind === ts6.SyntaxKind.BigIntKeyword);
2602
2617
  }
2618
+ function nullishPrimitiveUnion(node) {
2619
+ return node !== void 0 && ts6.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === ts6.SyntaxKind.UndefinedKeyword || ts6.isLiteralTypeNode(part) && part.literal.kind === ts6.SyntaxKind.NullKeyword);
2620
+ }
2603
2621
  function consumerAnnotation(node) {
2604
2622
  if (node.params.length !== 1) return void 0;
2605
2623
  const parameter = node.params[0];
@@ -2657,6 +2675,8 @@ function directCalls(binding3) {
2657
2675
  // src/rules/no-redundant-consumer-union.ts
2658
2676
  var CONFIG = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
2659
2677
  var EXAMPLE = 'export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: "Ada", breed: "Collie"}; function label(value: Dog | Animal): string {return value.name;} label(dog);';
2678
+ var NULLABLE_EXAMPLE = EXAMPLE.replace("readonly name: string", "readonly name: string | null").replace("return value.name;", "return value.name ?? '';");
2679
+ var OPTIONAL_EXAMPLE = EXAMPLE.replace("readonly breed: string", "readonly breed?: string");
2660
2680
  var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2661
2681
  defaultLevel: "warning",
2662
2682
  summary: "Keep the covering object contract in a private scalar consumer union.",
@@ -2667,25 +2687,91 @@ var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2667
2687
  limitations: [
2668
2688
  "Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Removed named types/imports must retain an independent top-level exported type-alias or direct type-export read. Other removable annotations/assertions, the covering annotation and own-definition/self references cannot keep one another live; otherwise simultaneous fixes can introduce unused-type errors. The exact authored covering type and terminator token sequence are retained; edits that would merge distinct CR and LF terminators remain manual; no producer, call or function body is rewritten.",
2669
2689
  "Only an inline binary parameter union in a strict module's top-level private scalar consumer is supported. Exactly one parameter, an explicit broad primitive return annotation, a single pure returned scalar expression and direct identifier-argument callers are required. Type queries or type-only exports of the callable, public APIs, callbacks, contextual signatures, overloads, generics, decorators, async functions, direct eval and fresh-object callers prevent findings.",
2670
- "Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, callbacks and recursive or nested object fields are unsupported.",
2671
- "Reports only proper native assignability: covered is assignable to covering and the reverse is false. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any also cannot witness coverage. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; mutually assignable but nonidentical structures are excluded.",
2690
+ "Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties, optionally paired directly with null or undefined in an exact two-part union. Required and optional nullable fields may be read under the same native coverage proof. Every field must have one actual declaration origin throughout each alternative's inheritance/intersection graph. Shared ancestors retain that origin; repeated merged fields, explicit overrides and independently declared overlapping fields abstain, including valid compatible declarations. This excludes recovered conflicting properties without claiming whole-source health. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, parenthesized or more complex property unions, callbacks and recursive or nested object fields are unsupported.",
2691
+ "Reports proper native assignability, where covered is assignable to covering and the reverse is false, or mutual native assignability with a strict smaller declared covering surface and only optional covered-only members. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any cannot replace explicit declared coverage or the native Optional requirement. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; equal-key and incomparable mutually assignable structures are excluded. The mutual case additionally requires inline contracts or locally bound interfaces/type aliases whose alias and heritage references can be traced through the parser-owned type scope. Merged interface fragments must all resolve to the reference's same native symbol; mixed exported/local recovery abstains. Imported, global and qualified-namespace references in that path abstain; proper one-way coverage retains its existing alias/import support.",
2672
2692
  "General union simplification can change Extract, conditional types, in-narrowing, fresh-literal checking and public presentation. Those contexts are excluded even when ordinary assignment succeeds. Flat destructuring counts every bound field as read; whole-object escape, mutation, helpers, closures, reflection, serialization, dynamic reads, implicit Object capabilities and JSX prevent findings. Imported Python protocol assignability is not approximated by this rule."
2673
2693
  ],
2674
2694
  examples: [
2675
2695
  { id: "covered-alternative", title: "The private consumer needs the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: EXAMPLE.replace("Dog | Animal", "Animal") }] },
2676
- { id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
2696
+ { id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
2697
+ { id: "optional-covered-alternative", scenarioId: "optional-readonly-contract", title: "An unread optional breed retains the smaller Animal surface", outcome: "match", files: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE.replace("Dog | Animal", "Animal") }] },
2698
+ { id: "optional-public-contract", scenarioId: "optional-readonly-contract", title: "A public optional union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
2699
+ { id: "nullable-covered-alternative", scenarioId: "nullable-readonly-contract", title: "A nullable name retains the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("Dog | Animal", "Animal") }] },
2700
+ { id: "nullable-domain-contract", scenarioId: "nullable-readonly-contract", title: "A literal breed keeps the authored domain contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("readonly breed: string", "readonly breed: 'Collie'") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
2677
2701
  ]
2678
2702
  };
2679
2703
  function scalarFields(type, checker) {
2680
- if (!closedObjectContract(type, checker)) return null;
2704
+ if (!closedObjectContract(type, checker, true)) return null;
2681
2705
  const fields2 = /* @__PURE__ */ new Map();
2682
2706
  for (const property of type.getProperties()) {
2683
2707
  const declarations = property.getDeclarations();
2684
- if (!declarations?.length || !declarations.every((member3) => ts7.isPropertySignature(member3) && member3.name && (ts7.isIdentifier(member3.name) || ts7.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === ts7.SyntaxKind.ReadonlyKeyword && primitiveKeyword(member3.type))) return null;
2708
+ if (!declarations?.length || !declarations.every((member3) => ts7.isPropertySignature(member3) && member3.name && (ts7.isIdentifier(member3.name) || ts7.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === ts7.SyntaxKind.ReadonlyKeyword && (primitiveKeyword(member3.type) || nullishPrimitiveUnion(member3.type)))) return null;
2685
2709
  fields2.set(property.name, (property.flags & ts7.SymbolFlags.Optional) === 0);
2686
2710
  }
2687
2711
  return fields2.size ? fields2 : null;
2688
2712
  }
2713
+ function localContractBindings(roots, source, services) {
2714
+ const pending = [...roots];
2715
+ const visited = /* @__PURE__ */ new Set();
2716
+ for (let index = 0; index < pending.length; index++) {
2717
+ const node = pending[index];
2718
+ if (!node || visited.has(node)) continue;
2719
+ visited.add(node);
2720
+ const references = localContractReferences(node, source, services);
2721
+ if (references === null) return false;
2722
+ pending.push(...references);
2723
+ }
2724
+ return true;
2725
+ }
2726
+ function localContractReferences(node, source, services) {
2727
+ switch (node.type) {
2728
+ case AST_NODE_TYPES22.TSTypeLiteral:
2729
+ return [];
2730
+ case AST_NODE_TYPES22.TSIntersectionType:
2731
+ return node.types;
2732
+ case AST_NODE_TYPES22.TSTypeReference:
2733
+ return !node.typeArguments && node.typeName.type === AST_NODE_TYPES22.Identifier ? [node.typeName] : null;
2734
+ case AST_NODE_TYPES22.Identifier:
2735
+ return localBindingReferences(node, source, services);
2736
+ default:
2737
+ return null;
2738
+ }
2739
+ }
2740
+ function localBindingReferences(identifier, source, services) {
2741
+ const definitions2 = typeBinding(source, identifier)?.defs.filter((definition) => definition.isTypeDefinition).map((definition) => definition.node);
2742
+ if (!definitions2?.length || !definitions2.every(moduleTypeDeclaration)) return null;
2743
+ const first = definitions2[0];
2744
+ if (definitions2.length === 1 && first?.type === AST_NODE_TYPES22.TSTypeAliasDeclaration && !first.typeParameters) return [first.typeAnnotation];
2745
+ const interfaces = definitions2.filter((definition) => definition.type === AST_NODE_TYPES22.TSInterfaceDeclaration);
2746
+ if (interfaces.length !== definitions2.length || interfaces.some((declaration) => declaration.typeParameters)) return null;
2747
+ if (interfaces.length > 1) {
2748
+ const checker = services.program.getTypeChecker();
2749
+ const symbol = checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(identifier));
2750
+ if (!symbol || interfaces.some((declaration) => checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(declaration.id)) !== symbol)) return null;
2751
+ }
2752
+ const bases = interfaces.flatMap((declaration) => declaration.extends.map((base) => base.expression));
2753
+ return bases.every((base) => base.type === AST_NODE_TYPES22.Identifier) ? bases : null;
2754
+ }
2755
+ function moduleTypeDeclaration(node) {
2756
+ const parent = node.parent;
2757
+ if (parent?.type === AST_NODE_TYPES22.Program) return true;
2758
+ if (parent?.type !== AST_NODE_TYPES22.ExportNamedDeclaration && parent?.type !== AST_NODE_TYPES22.ExportDefaultDeclaration) return false;
2759
+ return parent.parent.type === AST_NODE_TYPES22.Program;
2760
+ }
2761
+ function coveringAlternative(leftNode, rightNode, checker, fieldsOf, reads) {
2762
+ const left = checker.getTypeFromTypeNode(leftNode);
2763
+ const right = checker.getTypeFromTypeNode(rightNode);
2764
+ const leftFields = fieldsOf(left);
2765
+ const rightFields = fieldsOf(right);
2766
+ if (!leftFields || !rightFields) return null;
2767
+ const forward = checker.isTypeAssignableTo(left, right);
2768
+ const reverse = checker.isTypeAssignableTo(right, left);
2769
+ const mutual = forward && reverse;
2770
+ if (!forward && !reverse || mutual && leftFields.size === rightFields.size) return null;
2771
+ const [covered2, covering, coveredFields, coveringFields] = forward && (!reverse || leftFields.size > rightFields.size) ? [leftNode, rightNode, leftFields, rightFields] : [rightNode, leftNode, rightFields, leftFields];
2772
+ if ([...coveringFields.keys()].some((key) => !coveredFields.has(key)) || mutual && [...coveredFields].some(([key, required]) => required && !coveringFields.has(key)) || [...reads].some((key) => !coveringFields.has(key)) || ![...reads].some((key) => coveringFields.get(key))) return null;
2773
+ return { covered: covered2, covering, mutual };
2774
+ }
2689
2775
  function privateUnionFix(sourceCode, services, proof) {
2690
2776
  const covering = services.tsNodeToESTreeNodeMap.get(proof.covering);
2691
2777
  if (!validConsumerFixCalls(proof.candidate, proof.parameter, sourceCode, services)) return null;
@@ -2736,17 +2822,11 @@ var no_redundant_consumer_union_default = createRule({
2736
2822
  const leftNode = annotation.types[0];
2737
2823
  const rightNode = annotation.types[1];
2738
2824
  if (!leftNode || !rightNode) continue;
2739
- const left = checker.getTypeFromTypeNode(leftNode);
2740
- const right = checker.getTypeFromTypeNode(rightNode);
2741
- const leftFields = fieldsOf(left);
2742
- const rightFields = fieldsOf(right);
2743
- if (!leftFields || !rightFields) continue;
2744
- const forward = checker.isTypeAssignableTo(left, right);
2745
- const reverse = checker.isTypeAssignableTo(right, left);
2746
- if (forward === reverse) continue;
2747
- const [coveredNode, coveringNode, coveredFields, coveringFields] = forward ? [leftNode, rightNode, leftFields, rightFields] : [rightNode, leftNode, rightFields, leftFields];
2748
- if ([...coveringFields.keys()].some((key) => !coveredFields.has(key)) || [...consumer.reads].some((key) => !coveringFields.has(key)) || ![...consumer.reads].some((key) => coveringFields.get(key))) continue;
2825
+ const coverage = coveringAlternative(leftNode, rightNode, checker, fieldsOf, consumer.reads);
2826
+ if (!coverage) continue;
2749
2827
  const reportNode = services.tsNodeToESTreeNodeMap.get(annotation);
2828
+ if (coverage.mutual && (reportNode.type !== AST_NODE_TYPES22.TSUnionType || !localContractBindings(reportNode.types, context.sourceCode, services))) continue;
2829
+ const { covered: coveredNode, covering: coveringNode } = coverage;
2750
2830
  const fix = privateUnionFix(context.sourceCode, services, {
2751
2831
  candidate: candidate2,
2752
2832
  parameter: consumer.parameter,
@@ -2768,7 +2848,7 @@ import ts8 from "typescript";
2768
2848
  var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
2769
2849
  var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
2770
2850
  var HELPER_EXAMPLE = 'export {}; const account = {name: "Ada", email: "a"}; function formatName(name: string): string {return name.toUpperCase();} function label(user: {email: string; name: string}): string {return formatName(user.name);} label(account);';
2771
- var NULLABLE_EXAMPLE = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
2851
+ var NULLABLE_EXAMPLE2 = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
2772
2852
  var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
2773
2853
  defaultLevel: "warning",
2774
2854
  summary: "Prefer the proven read-only subset for a private consumer parameter.",
@@ -2787,8 +2867,8 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
2787
2867
  { id: "used-fields", title: "Every required dependency is read", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE2.replace("return user.name;", "return user.name + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
2788
2868
  { id: "scalar-helper", scenarioId: "local-scalar-helper", title: "Only the name crosses a local scalar helper call", outcome: "match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("email: string;", "") }] },
2789
2869
  { id: "helper-and-other-field", scenarioId: "local-scalar-helper", title: "The consumer still requires a separately read field", outcome: "no-match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("return formatName(user.name);", "return formatName(user.name) + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
2790
- { id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("email: string | null;", "") }] },
2791
- { id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
2870
+ { id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2 }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("email: string | null;", "") }] },
2871
+ { id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
2792
2872
  ]
2793
2873
  };
2794
2874
  function inlineFields(node, reads) {
@@ -2802,9 +2882,6 @@ function inlineFields(node, reads) {
2802
2882
  }
2803
2883
  return fields2;
2804
2884
  }
2805
- function nullishPrimitiveUnion(node) {
2806
- return node !== void 0 && ts8.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === ts8.SyntaxKind.UndefinedKeyword || ts8.isLiteralTypeNode(part) && part.literal.kind === ts8.SyntaxKind.NullKeyword);
2807
- }
2808
2885
  var prefer_minimal_parameter_contract_default = createRule({
2809
2886
  name: "prefer-minimal-parameter-contract",
2810
2887
  documentation: PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION,
@@ -3960,7 +4037,7 @@ function isSearchParamReceiver(call, services) {
3960
4037
  // package.json
3961
4038
  var package_default = {
3962
4039
  name: "@sarj/eslint-plugin",
3963
- version: "16.36.5",
4040
+ version: "16.36.7",
3964
4041
  packageManager: "npm@12.2.1",
3965
4042
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
3966
4043
  type: "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.36.5",
3
+ "version": "16.36.7",
4
4
  "packageManager": "npm@12.2.1",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",