@sarj/eslint-plugin 16.38.52 → 16.38.54

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
@@ -2662,14 +2662,32 @@ function validConsumerFixCalls(node, parameter, sourceCode, services) {
2662
2662
  if (owner.type !== import_utils24.AST_NODE_TYPES.FunctionDeclaration && owner.type !== import_utils24.AST_NODE_TYPES.VariableDeclarator) return false;
2663
2663
  const binding3 = sourceCode.getDeclaredVariables(owner).find((variable) => variable.defs.some((definition) => definition.node === owner && (definition.type === "FunctionName" || definition.type === "Variable")));
2664
2664
  if (!binding3) return false;
2665
+ const declaration = services.esTreeNodeToTSNodeMap.get(node);
2666
+ if (!import_typescript3.default.isFunctionDeclaration(declaration) && !import_typescript3.default.isArrowFunction(declaration) || parameter.parent !== declaration || declaration.parameters.some((entry) => import_typescript3.default.isIdentifier(entry.name) && entry.name.text === "this")) return false;
2667
+ const selected = declaration.parameters.indexOf(parameter);
2668
+ if (selected < 0) return false;
2665
2669
  const checker = services.program.getTypeChecker();
2666
- const original = checker.getTypeAtLocation(parameter);
2670
+ const originals = declaration.parameters.map((entry) => checker.getTypeAtLocation(entry));
2671
+ const original = originals[selected];
2672
+ if (!original) return false;
2667
2673
  const fields2 = /* @__PURE__ */ new Set();
2668
2674
  const collect = (type) => {
2669
2675
  if (type.isUnionOrIntersection()) type.types.forEach(collect);
2670
2676
  else type.getProperties().forEach((property) => fields2.add(property.name));
2671
2677
  };
2672
2678
  collect(original);
2679
+ const validArguments = (call) => {
2680
+ if (declaration.parameters.length > 1) {
2681
+ const nativeCall = services.esTreeNodeToTSNodeMap.get(call);
2682
+ if (!import_typescript3.default.isCallExpression(nativeCall) || checker.getResolvedSignature(nativeCall)?.declaration !== declaration) return false;
2683
+ }
2684
+ return call.arguments.every((argument, index) => {
2685
+ const target = originals[index];
2686
+ const nativeArgument = services.esTreeNodeToTSNodeMap.get(argument);
2687
+ const type = checker.getTypeAtLocation(nativeArgument);
2688
+ return target !== void 0 && (index === selected ? knownArgument(type, nativeArgument, fields2, checker) : known(type)) && checker.isTypeAssignableTo(type, target);
2689
+ });
2690
+ };
2673
2691
  let calls = 0;
2674
2692
  for (const reference of binding3.references) {
2675
2693
  if (reference.isWrite()) {
@@ -2677,10 +2695,7 @@ function validConsumerFixCalls(node, parameter, sourceCode, services) {
2677
2695
  continue;
2678
2696
  }
2679
2697
  const call = reference.identifier.parent;
2680
- if (reference.isTypeReference || !reference.isReadOnly() || call.type !== import_utils24.AST_NODE_TYPES.CallExpression || call.callee !== reference.identifier || call.optional || call.typeArguments || call.arguments.length !== 1 || call.arguments[0]?.type !== import_utils24.AST_NODE_TYPES.Identifier) return false;
2681
- const argument = services.esTreeNodeToTSNodeMap.get(call.arguments[0]);
2682
- const type = checker.getTypeAtLocation(argument);
2683
- if (!knownArgument(type, argument, fields2, checker) || !checker.isTypeAssignableTo(type, original)) return false;
2698
+ if (reference.isTypeReference || !reference.isReadOnly() || call.type !== import_utils24.AST_NODE_TYPES.CallExpression || call.callee !== reference.identifier || call.optional || call.typeArguments || call.arguments.length !== declaration.parameters.length || !call.arguments.every((argument) => argument.type === import_utils24.AST_NODE_TYPES.Identifier) || !validArguments(call)) return false;
2684
2699
  calls += 1;
2685
2700
  }
2686
2701
  return calls > 0;
@@ -2777,10 +2792,10 @@ var RETURNED_OPERANDS = /* @__PURE__ */ new Set([import_typescript5.default.Synt
2777
2792
  function scalarParameterView(parameter, body3, checker) {
2778
2793
  return parameterReads(parameter, body3, checker, true);
2779
2794
  }
2780
- function scalarParameterReads(parameter, body3, checker, callProgram) {
2781
- return parameterReads(parameter, body3, checker, false, callProgram);
2795
+ function scalarParameterReads(parameter, body3, checker, callProgram, auxiliaries) {
2796
+ return parameterReads(parameter, body3, checker, false, callProgram, auxiliaries);
2782
2797
  }
2783
- function parameterReads(parameter, body3, checker, jsx, callProgram) {
2798
+ function parameterReads(parameter, body3, checker, jsx, callProgram, auxiliaries) {
2784
2799
  if (parameter.dotDotDotToken || parameter.initializer || import_typescript5.default.isArrayBindingPattern(parameter.name)) return null;
2785
2800
  const root = import_typescript5.default.isIdentifier(parameter.name) ? checker.getSymbolAtLocation(parameter.name) : void 0;
2786
2801
  if (import_typescript5.default.isIdentifier(parameter.name) && !root) return null;
@@ -2788,7 +2803,7 @@ function parameterReads(parameter, body3, checker, jsx, callProgram) {
2788
2803
  const aliases = parameterAliases(parameter.name, checker, reads);
2789
2804
  if (!aliases) return null;
2790
2805
  const expression = returnedExpression(body3);
2791
- const view = { checker, props: checker.getTypeAtLocation(parameter), root, aliases, reads, callProgram };
2806
+ const view = { checker, props: checker.getTypeAtLocation(parameter), root, aliases, reads, callProgram, auxiliaries };
2792
2807
  return expression && scalarView(expression, view, jsx) ? reads : null;
2793
2808
  }
2794
2809
  function scalarType(type, allowUndefined = true) {
@@ -2820,6 +2835,7 @@ function scalarView(node, view, jsx = false) {
2820
2835
  if (import_typescript5.default.isJsxExpression(node)) return !node.dotDotDotToken && (!node.expression || scalarView(node.expression, view, jsx));
2821
2836
  if (import_typescript5.default.isIdentifier(node)) {
2822
2837
  const binding3 = view.checker.getSymbolAtLocation(node);
2838
+ if (binding3 && view.auxiliaries?.has(binding3)) return scalarType(view.checker.getTypeAtLocation(node));
2823
2839
  const key = binding3 && view.aliases.get(binding3);
2824
2840
  return key !== void 0 && readField(node, key, view);
2825
2841
  }
@@ -2890,8 +2906,19 @@ function primitiveKeyword(node) {
2890
2906
  function nullishPrimitiveUnion(node) {
2891
2907
  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);
2892
2908
  }
2893
- function consumerAnnotation(node) {
2894
- if (node.params.length !== 1) return void 0;
2909
+ function consumerAnnotation(node, auxiliaryScalars = false) {
2910
+ if (node.params.length !== 1) {
2911
+ if (!auxiliaryScalars) return void 0;
2912
+ let annotation;
2913
+ for (const parameter2 of node.params) {
2914
+ if (parameter2.type !== import_utils25.AST_NODE_TYPES.Identifier && parameter2.type !== import_utils25.AST_NODE_TYPES.ObjectPattern) continue;
2915
+ const current = parameter2.typeAnnotation?.typeAnnotation;
2916
+ if (current?.type !== import_utils25.AST_NODE_TYPES.TSTypeLiteral) continue;
2917
+ if (annotation) return void 0;
2918
+ annotation = current;
2919
+ }
2920
+ return annotation;
2921
+ }
2895
2922
  const parameter = node.params[0];
2896
2923
  return parameter?.type === import_utils25.AST_NODE_TYPES.Identifier || parameter?.type === import_utils25.AST_NODE_TYPES.ObjectPattern ? parameter.typeAnnotation?.typeAnnotation : void 0;
2897
2924
  }
@@ -2899,29 +2926,48 @@ function exportedSymbols(source, checker) {
2899
2926
  const symbol = checker.getSymbolAtLocation(source);
2900
2927
  return new Set(symbol ? checker.getExportsOfModule(symbol).map((entry) => (entry.flags & import_typescript6.default.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(entry) : entry) : []);
2901
2928
  }
2902
- function privateScalarConsumer(node, sourceCode, services, exports2, scalarCalls = false) {
2929
+ function privateScalarConsumer(node, sourceCode, services, exports2, options = {}) {
2903
2930
  const native = services.esTreeNodeToTSNodeMap.get(node);
2904
- if (!import_typescript6.default.isFunctionDeclaration(native) && !import_typescript6.default.isArrowFunction(native) || !native.body || !independentSignature(native)) return null;
2931
+ if (!import_typescript6.default.isFunctionDeclaration(native) && !import_typescript6.default.isArrowFunction(native) || !native.body) return null;
2932
+ let parameter = native.parameters[0];
2933
+ if (native.parameters.length !== 1) {
2934
+ const annotation = consumerAnnotation(node, options.auxiliaryScalars);
2935
+ const selected = annotation && services.esTreeNodeToTSNodeMap.get(annotation);
2936
+ parameter = native.parameters.find((entry) => entry.type === selected);
2937
+ }
2938
+ if (!parameter || !independentSignature(native, parameter, options.auxiliaryScalars === true)) return null;
2905
2939
  const checker = services.program.getTypeChecker();
2906
2940
  if (!import_typescript6.default.isExternalModule(native.getSourceFile()) || !services.program.getCompilerOptions().strict) return null;
2907
- const parameter = native.parameters[0];
2908
- if (!parameter) return null;
2909
- const binding3 = callableBinding(node, sourceCode);
2910
- if (!binding3 || binding3.defs.length !== 1) return null;
2911
- const name = import_typescript6.default.isFunctionDeclaration(native) ? native.name : import_typescript6.default.isVariableDeclaration(native.parent) ? native.parent.name : void 0;
2912
- const symbol = name && checker.getSymbolAtLocation(name);
2913
- if (!symbol || symbol.declarations?.length !== 1 || exports2.has(symbol) || !directCalls(binding3)) return null;
2941
+ if (!privateDirectCallable(node, native, sourceCode, checker, exports2)) return null;
2914
2942
  const signature = checker.getSignatureFromDeclaration(native);
2915
2943
  if (!signature || signature.typeParameters?.length || checker.getTypePredicateOfSignature(signature)) return null;
2916
2944
  const expression = returnedExpression(native.body);
2917
2945
  if (!expression || !checker.isTypeAssignableTo(checker.getTypeAtLocation(expression), checker.getReturnTypeOfSignature(signature))) return null;
2918
- const reads = scalarParameterReads(parameter, native.body, checker, scalarCalls ? services.program : void 0);
2946
+ const auxiliaries = native.parameters.length > 1 ? auxiliaryBindings(native, parameter, checker) : void 0;
2947
+ if (auxiliaries === null) return null;
2948
+ const reads = scalarParameterReads(parameter, native.body, checker, options.scalarCalls ? services.program : void 0, auxiliaries);
2919
2949
  return reads?.size ? { parameter, reads } : null;
2920
2950
  }
2921
- function independentSignature(native) {
2922
- if (native.parameters.length !== 1 || native.typeParameters?.length || native.asteriskToken || native.modifiers?.length || !primitiveKeyword(native.type)) return false;
2923
- const parameter = native.parameters[0];
2924
- return parameter !== void 0 && parameter.type !== void 0 && !parameter.modifiers?.length && !parameter.questionToken && !parameter.initializer && !parameter.dotDotDotToken && (!import_typescript6.default.isIdentifier(parameter.name) || parameter.name.text !== "this");
2951
+ function privateDirectCallable(node, native, sourceCode, checker, exports2) {
2952
+ const binding3 = callableBinding(node, sourceCode);
2953
+ if (!binding3 || binding3.defs.length !== 1) return false;
2954
+ const name = import_typescript6.default.isFunctionDeclaration(native) ? native.name : import_typescript6.default.isVariableDeclaration(native.parent) ? native.parent.name : void 0;
2955
+ const symbol = name && checker.getSymbolAtLocation(name);
2956
+ return symbol !== void 0 && symbol.declarations?.length === 1 && !exports2.has(symbol) && directCalls(binding3, native.parameters.length);
2957
+ }
2958
+ function independentSignature(native, selected, auxiliaryScalars) {
2959
+ if (!auxiliaryScalars && native.parameters.length !== 1 || native.typeParameters?.length || native.asteriskToken || native.modifiers?.length || !primitiveKeyword(native.type)) return false;
2960
+ return native.parameters.every((parameter) => parameter.type !== void 0 && !parameter.modifiers?.length && !parameter.questionToken && !parameter.initializer && !parameter.dotDotDotToken && (!import_typescript6.default.isIdentifier(parameter.name) || parameter.name.text !== "this") && (parameter === selected || import_typescript6.default.isIdentifier(parameter.name) && primitiveKeyword(parameter.type)));
2961
+ }
2962
+ function auxiliaryBindings(native, selected, checker) {
2963
+ const bindings = /* @__PURE__ */ new Set();
2964
+ for (const parameter of native.parameters) {
2965
+ if (parameter === selected) continue;
2966
+ const symbol = import_typescript6.default.isIdentifier(parameter.name) ? checker.getSymbolAtLocation(parameter.name) : void 0;
2967
+ if (!symbol || symbol.declarations?.length !== 1 || symbol.declarations[0] !== parameter || bindings.has(symbol)) return null;
2968
+ bindings.add(symbol);
2969
+ }
2970
+ return bindings;
2925
2971
  }
2926
2972
  function callableBinding(node, sourceCode) {
2927
2973
  if (node.type === import_utils25.AST_NODE_TYPES.FunctionDeclaration) {
@@ -2932,12 +2978,12 @@ function callableBinding(node, sourceCode) {
2932
2978
  if (declaration.type !== import_utils25.AST_NODE_TYPES.VariableDeclarator || declaration.init !== node || declaration.id.type !== import_utils25.AST_NODE_TYPES.Identifier || declaration.id.typeAnnotation || declaration.parent.kind !== "const" || declaration.parent.parent.type !== import_utils25.AST_NODE_TYPES.Program) return void 0;
2933
2979
  return sourceCode.getDeclaredVariables(declaration).find((variable) => variable.defs.some((definition) => definition.type === "Variable" && definition.node === declaration));
2934
2980
  }
2935
- function directCalls(binding3) {
2981
+ function directCalls(binding3, arity) {
2936
2982
  let calls = 0;
2937
2983
  const stable2 = stableValueBinding(binding3, (reference) => {
2938
2984
  const identifier = reference.identifier;
2939
2985
  const call = identifier.parent;
2940
- if (reference.isTypeReference || !reference.isReadOnly() || call.type !== import_utils25.AST_NODE_TYPES.CallExpression || call.callee !== identifier || call.optional || call.typeArguments || call.arguments.length !== 1 || call.arguments[0]?.type !== import_utils25.AST_NODE_TYPES.Identifier) return false;
2986
+ if (reference.isTypeReference || !reference.isReadOnly() || call.type !== import_utils25.AST_NODE_TYPES.CallExpression || call.callee !== identifier || call.optional || call.typeArguments || call.arguments.length !== arity || !call.arguments.every((argument) => argument.type === import_utils25.AST_NODE_TYPES.Identifier)) return false;
2941
2987
  calls += 1;
2942
2988
  return true;
2943
2989
  });
@@ -3121,6 +3167,8 @@ var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":tru
3121
3167
  var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
3122
3168
  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);';
3123
3169
  var NULLABLE_EXAMPLE2 = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
3170
+ var READ_NULLABLE_EXAMPLE = EXAMPLE2.replace('name: "Ada"', "name: null").replace("readonly name: string", "readonly name: string | null").replace("return user.name;", 'return user.name ?? "";');
3171
+ var AUXILIARY_EXAMPLE = EXAMPLE2.replaceAll("readonly ", "").replace("function label(user:", 'const prefix = "Hello "; function label(prefix: string, user:').replace("return user.name;", "return prefix + user.name;").replace("label(account);", "label(prefix, account);");
3124
3172
  var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
3125
3173
  defaultLevel: "warning",
3126
3174
  summary: "Prefer the proven read-only subset for a private consumer parameter.",
@@ -3129,9 +3177,9 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
3129
3177
  category: "architecture",
3130
3178
  autofix: "safe",
3131
3179
  limitations: [
3132
- "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. Only unread required property signatures are removed; every read or optional member, modifier and terminator token is preserved. Edits that would merge distinct CR and LF terminators remain manual.",
3133
- "Requires parser-owned TypeScript information and a strict external module. Only top-level private function declarations and raw const arrows with exactly one explicit parameter and an independent broad primitive return annotation are supported. Every callable reference must be a direct call with one identifier argument; exports including type-only exports, typeof queries, callbacks, storage, constructors, contextual signatures, overloads, generics, decorators, async functions and direct eval prevent findings.",
3134
- "Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. An unread required field may additionally have exactly one such keyword and null or undefined in a direct two-part union; read or optional nullable fields and more complex unions remain excluded. Fields may be plain or readonly; the consumer body must still prove read-only usage. Retained field modifiers and optional members are preserved. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.",
3180
+ "Automatic fixes additionally require every existing identifier argument to be natively assignable to its corresponding original parameter contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Only unread required object property signatures are removed; every read or optional member, modifier and terminator token is preserved. Auxiliary annotations and argument order stay unchanged. Edits that would merge distinct CR and LF terminators remain manual.",
3181
+ "Requires parser-owned TypeScript information and a strict external module. Only top-level private function declarations and raw const arrows with one explicit inline object parameter and an independent broad primitive return annotation are supported. Other parameters must be required identifiers annotated directly as string, number, boolean or bigint; optional, default, rest, destructured, domain-specific and explicit this auxiliaries are excluded. Every callable reference must be a direct call with an identifier argument for every parameter; exports including type-only exports, typeof queries, callbacks, storage, constructors, contextual signatures, overloads, generics, decorators, async functions and direct eval prevent findings.",
3182
+ "Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. A read field or an unread required field may additionally have exactly one such keyword and null or undefined in a direct two-part union; unread optional nullable fields and more complex unions remain excluded. Fields may be plain or readonly; the consumer body must still prove read-only usage. Every read field keeps its exact type, modifiers and optionality. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.",
3135
3183
  "The single return expression may use only fixed scalar field reads, flat destructuring, literals, operators, templates and scalar calls to same-file implemented top-level functions or raw const arrows with an explicit return annotation. These calls require one native resolved declaration, no generic or predicate signature, scalar arguments/result and complete active native checking of the local implementation file. Their helper bodies need not be pure because only scalar values cross the call and only an erased parameter annotation changes. Imported, aliased, ambient, overloaded, optional, spread and member calls remain unsupported. Bound destructured fields count as reads even when their local aliases are unused. Whole-object escape, identity, mutation, closures, reflection, serialization, dynamic keys, implicit Object capabilities and JSX are unsupported. Unknown semantic information causes abstention. Findings outside the fix proof remain manual; this is not a requirement to minimize every object or discard useful producer fields."
3136
3184
  ],
3137
3185
  examples: [
@@ -3140,7 +3188,11 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
3140
3188
  { 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;", "") }] },
3141
3189
  { 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 },
3142
3190
  { 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;", "") }] },
3143
- { 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 }
3191
+ { 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 },
3192
+ { id: "retained-nullable-read", scenarioId: "retained-nullable-field", title: "A nullable name remains while an unread email requirement is removed", outcome: "match", files: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE.replace("readonly email: string;", "") }] },
3193
+ { id: "retained-nullable-all-used", scenarioId: "retained-nullable-field", title: "Both requirements remain when the nullable view reads email too", outcome: "no-match", files: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE.replace('return user.name ?? "";', "return user.name ?? user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
3194
+ { id: "auxiliary-scalar", scenarioId: "auxiliary-scalar-parameter", title: "A scalar prefix does not require the object's unread email", outcome: "match", files: [{ path: "src/case.ts", source: AUXILIARY_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: AUXILIARY_EXAMPLE.replace("email: string;", "") }] },
3195
+ { id: "auxiliary-all-used", scenarioId: "auxiliary-scalar-parameter", title: "Reading both object fields retains their requirements", outcome: "no-match", files: [{ path: "src/case.ts", source: AUXILIARY_EXAMPLE.replace("return prefix + user.name;", "return prefix + user.name + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
3144
3196
  ]
3145
3197
  };
3146
3198
  function inlineFields(node, reads) {
@@ -3148,7 +3200,7 @@ function inlineFields(node, reads) {
3148
3200
  for (const member3 of node.members) {
3149
3201
  if (!import_typescript8.default.isPropertySignature(member3) || !member3.name || !import_typescript8.default.isIdentifier(member3.name) && !import_typescript8.default.isStringLiteralLike(member3.name) || (member3.modifiers?.length ?? 0) > 1 || member3.modifiers?.some((modifier) => modifier.kind !== import_typescript8.default.SyntaxKind.ReadonlyKeyword)) return null;
3150
3202
  const key = member3.name.text;
3151
- if (!primitiveKeyword(member3.type) && (member3.questionToken || reads.has(key) || !nullishPrimitiveUnion(member3.type))) return null;
3203
+ if (!primitiveKeyword(member3.type) && (!nullishPrimitiveUnion(member3.type) || member3.questionToken && !reads.has(key))) return null;
3152
3204
  if (fields2.has(key)) return null;
3153
3205
  fields2.set(key, !member3.questionToken);
3154
3206
  }
@@ -3166,7 +3218,7 @@ var prefer_minimal_parameter_contract_default = createRule({
3166
3218
  const candidates = [];
3167
3219
  let dynamic = false;
3168
3220
  const remember = (node) => {
3169
- if (consumerAnnotation(node)?.type === import_utils27.AST_NODE_TYPES.TSTypeLiteral) candidates.push(node);
3221
+ if (consumerAnnotation(node, true)?.type === import_utils27.AST_NODE_TYPES.TSTypeLiteral) candidates.push(node);
3170
3222
  };
3171
3223
  return {
3172
3224
  FunctionDeclaration: remember,
@@ -3182,7 +3234,7 @@ var prefer_minimal_parameter_contract_default = createRule({
3182
3234
  const checker = services.program.getTypeChecker();
3183
3235
  const exports2 = exportedSymbols(source, checker);
3184
3236
  for (const candidate2 of candidates) {
3185
- const consumer = privateScalarConsumer(candidate2, context.sourceCode, services, exports2, true);
3237
+ const consumer = privateScalarConsumer(candidate2, context.sourceCode, services, exports2, { scalarCalls: true, auxiliaryScalars: true });
3186
3238
  const annotation = consumer?.parameter.type;
3187
3239
  if (!consumer || !annotation || !import_typescript8.default.isTypeLiteralNode(annotation)) continue;
3188
3240
  const fields2 = inlineFields(annotation, consumer.reads);
@@ -4261,7 +4313,7 @@ function isSearchParamReceiver(call, services) {
4261
4313
  // package.json
4262
4314
  var package_default = {
4263
4315
  name: "@sarj/eslint-plugin",
4264
- version: "16.38.52",
4316
+ version: "16.38.54",
4265
4317
  packageManager: "npm@12.2.1",
4266
4318
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
4267
4319
  type: "module",
package/dist/index.js CHANGED
@@ -2611,14 +2611,32 @@ function validConsumerFixCalls(node, parameter, sourceCode, services) {
2611
2611
  if (owner.type !== AST_NODE_TYPES22.FunctionDeclaration && owner.type !== AST_NODE_TYPES22.VariableDeclarator) return false;
2612
2612
  const binding3 = sourceCode.getDeclaredVariables(owner).find((variable) => variable.defs.some((definition) => definition.node === owner && (definition.type === "FunctionName" || definition.type === "Variable")));
2613
2613
  if (!binding3) return false;
2614
+ const declaration = services.esTreeNodeToTSNodeMap.get(node);
2615
+ if (!ts3.isFunctionDeclaration(declaration) && !ts3.isArrowFunction(declaration) || parameter.parent !== declaration || declaration.parameters.some((entry) => ts3.isIdentifier(entry.name) && entry.name.text === "this")) return false;
2616
+ const selected = declaration.parameters.indexOf(parameter);
2617
+ if (selected < 0) return false;
2614
2618
  const checker = services.program.getTypeChecker();
2615
- const original = checker.getTypeAtLocation(parameter);
2619
+ const originals = declaration.parameters.map((entry) => checker.getTypeAtLocation(entry));
2620
+ const original = originals[selected];
2621
+ if (!original) return false;
2616
2622
  const fields2 = /* @__PURE__ */ new Set();
2617
2623
  const collect = (type) => {
2618
2624
  if (type.isUnionOrIntersection()) type.types.forEach(collect);
2619
2625
  else type.getProperties().forEach((property) => fields2.add(property.name));
2620
2626
  };
2621
2627
  collect(original);
2628
+ const validArguments = (call) => {
2629
+ if (declaration.parameters.length > 1) {
2630
+ const nativeCall = services.esTreeNodeToTSNodeMap.get(call);
2631
+ if (!ts3.isCallExpression(nativeCall) || checker.getResolvedSignature(nativeCall)?.declaration !== declaration) return false;
2632
+ }
2633
+ return call.arguments.every((argument, index) => {
2634
+ const target = originals[index];
2635
+ const nativeArgument = services.esTreeNodeToTSNodeMap.get(argument);
2636
+ const type = checker.getTypeAtLocation(nativeArgument);
2637
+ return target !== void 0 && (index === selected ? knownArgument(type, nativeArgument, fields2, checker) : known(type)) && checker.isTypeAssignableTo(type, target);
2638
+ });
2639
+ };
2622
2640
  let calls = 0;
2623
2641
  for (const reference of binding3.references) {
2624
2642
  if (reference.isWrite()) {
@@ -2626,10 +2644,7 @@ function validConsumerFixCalls(node, parameter, sourceCode, services) {
2626
2644
  continue;
2627
2645
  }
2628
2646
  const call = reference.identifier.parent;
2629
- if (reference.isTypeReference || !reference.isReadOnly() || call.type !== AST_NODE_TYPES22.CallExpression || call.callee !== reference.identifier || call.optional || call.typeArguments || call.arguments.length !== 1 || call.arguments[0]?.type !== AST_NODE_TYPES22.Identifier) return false;
2630
- const argument = services.esTreeNodeToTSNodeMap.get(call.arguments[0]);
2631
- const type = checker.getTypeAtLocation(argument);
2632
- if (!knownArgument(type, argument, fields2, checker) || !checker.isTypeAssignableTo(type, original)) return false;
2647
+ if (reference.isTypeReference || !reference.isReadOnly() || call.type !== AST_NODE_TYPES22.CallExpression || call.callee !== reference.identifier || call.optional || call.typeArguments || call.arguments.length !== declaration.parameters.length || !call.arguments.every((argument) => argument.type === AST_NODE_TYPES22.Identifier) || !validArguments(call)) return false;
2633
2648
  calls += 1;
2634
2649
  }
2635
2650
  return calls > 0;
@@ -2726,10 +2741,10 @@ var RETURNED_OPERANDS = /* @__PURE__ */ new Set([ts5.SyntaxKind.AmpersandAmpersa
2726
2741
  function scalarParameterView(parameter, body3, checker) {
2727
2742
  return parameterReads(parameter, body3, checker, true);
2728
2743
  }
2729
- function scalarParameterReads(parameter, body3, checker, callProgram) {
2730
- return parameterReads(parameter, body3, checker, false, callProgram);
2744
+ function scalarParameterReads(parameter, body3, checker, callProgram, auxiliaries) {
2745
+ return parameterReads(parameter, body3, checker, false, callProgram, auxiliaries);
2731
2746
  }
2732
- function parameterReads(parameter, body3, checker, jsx, callProgram) {
2747
+ function parameterReads(parameter, body3, checker, jsx, callProgram, auxiliaries) {
2733
2748
  if (parameter.dotDotDotToken || parameter.initializer || ts5.isArrayBindingPattern(parameter.name)) return null;
2734
2749
  const root = ts5.isIdentifier(parameter.name) ? checker.getSymbolAtLocation(parameter.name) : void 0;
2735
2750
  if (ts5.isIdentifier(parameter.name) && !root) return null;
@@ -2737,7 +2752,7 @@ function parameterReads(parameter, body3, checker, jsx, callProgram) {
2737
2752
  const aliases = parameterAliases(parameter.name, checker, reads);
2738
2753
  if (!aliases) return null;
2739
2754
  const expression = returnedExpression(body3);
2740
- const view = { checker, props: checker.getTypeAtLocation(parameter), root, aliases, reads, callProgram };
2755
+ const view = { checker, props: checker.getTypeAtLocation(parameter), root, aliases, reads, callProgram, auxiliaries };
2741
2756
  return expression && scalarView(expression, view, jsx) ? reads : null;
2742
2757
  }
2743
2758
  function scalarType(type, allowUndefined = true) {
@@ -2769,6 +2784,7 @@ function scalarView(node, view, jsx = false) {
2769
2784
  if (ts5.isJsxExpression(node)) return !node.dotDotDotToken && (!node.expression || scalarView(node.expression, view, jsx));
2770
2785
  if (ts5.isIdentifier(node)) {
2771
2786
  const binding3 = view.checker.getSymbolAtLocation(node);
2787
+ if (binding3 && view.auxiliaries?.has(binding3)) return scalarType(view.checker.getTypeAtLocation(node));
2772
2788
  const key = binding3 && view.aliases.get(binding3);
2773
2789
  return key !== void 0 && readField(node, key, view);
2774
2790
  }
@@ -2839,8 +2855,19 @@ function primitiveKeyword(node) {
2839
2855
  function nullishPrimitiveUnion(node) {
2840
2856
  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);
2841
2857
  }
2842
- function consumerAnnotation(node) {
2843
- if (node.params.length !== 1) return void 0;
2858
+ function consumerAnnotation(node, auxiliaryScalars = false) {
2859
+ if (node.params.length !== 1) {
2860
+ if (!auxiliaryScalars) return void 0;
2861
+ let annotation;
2862
+ for (const parameter2 of node.params) {
2863
+ if (parameter2.type !== AST_NODE_TYPES23.Identifier && parameter2.type !== AST_NODE_TYPES23.ObjectPattern) continue;
2864
+ const current = parameter2.typeAnnotation?.typeAnnotation;
2865
+ if (current?.type !== AST_NODE_TYPES23.TSTypeLiteral) continue;
2866
+ if (annotation) return void 0;
2867
+ annotation = current;
2868
+ }
2869
+ return annotation;
2870
+ }
2844
2871
  const parameter = node.params[0];
2845
2872
  return parameter?.type === AST_NODE_TYPES23.Identifier || parameter?.type === AST_NODE_TYPES23.ObjectPattern ? parameter.typeAnnotation?.typeAnnotation : void 0;
2846
2873
  }
@@ -2848,29 +2875,48 @@ function exportedSymbols(source, checker) {
2848
2875
  const symbol = checker.getSymbolAtLocation(source);
2849
2876
  return new Set(symbol ? checker.getExportsOfModule(symbol).map((entry) => (entry.flags & ts6.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(entry) : entry) : []);
2850
2877
  }
2851
- function privateScalarConsumer(node, sourceCode, services, exports, scalarCalls = false) {
2878
+ function privateScalarConsumer(node, sourceCode, services, exports, options = {}) {
2852
2879
  const native = services.esTreeNodeToTSNodeMap.get(node);
2853
- if (!ts6.isFunctionDeclaration(native) && !ts6.isArrowFunction(native) || !native.body || !independentSignature(native)) return null;
2880
+ if (!ts6.isFunctionDeclaration(native) && !ts6.isArrowFunction(native) || !native.body) return null;
2881
+ let parameter = native.parameters[0];
2882
+ if (native.parameters.length !== 1) {
2883
+ const annotation = consumerAnnotation(node, options.auxiliaryScalars);
2884
+ const selected = annotation && services.esTreeNodeToTSNodeMap.get(annotation);
2885
+ parameter = native.parameters.find((entry) => entry.type === selected);
2886
+ }
2887
+ if (!parameter || !independentSignature(native, parameter, options.auxiliaryScalars === true)) return null;
2854
2888
  const checker = services.program.getTypeChecker();
2855
2889
  if (!ts6.isExternalModule(native.getSourceFile()) || !services.program.getCompilerOptions().strict) return null;
2856
- const parameter = native.parameters[0];
2857
- if (!parameter) return null;
2858
- const binding3 = callableBinding(node, sourceCode);
2859
- if (!binding3 || binding3.defs.length !== 1) return null;
2860
- const name = ts6.isFunctionDeclaration(native) ? native.name : ts6.isVariableDeclaration(native.parent) ? native.parent.name : void 0;
2861
- const symbol = name && checker.getSymbolAtLocation(name);
2862
- if (!symbol || symbol.declarations?.length !== 1 || exports.has(symbol) || !directCalls(binding3)) return null;
2890
+ if (!privateDirectCallable(node, native, sourceCode, checker, exports)) return null;
2863
2891
  const signature = checker.getSignatureFromDeclaration(native);
2864
2892
  if (!signature || signature.typeParameters?.length || checker.getTypePredicateOfSignature(signature)) return null;
2865
2893
  const expression = returnedExpression(native.body);
2866
2894
  if (!expression || !checker.isTypeAssignableTo(checker.getTypeAtLocation(expression), checker.getReturnTypeOfSignature(signature))) return null;
2867
- const reads = scalarParameterReads(parameter, native.body, checker, scalarCalls ? services.program : void 0);
2895
+ const auxiliaries = native.parameters.length > 1 ? auxiliaryBindings(native, parameter, checker) : void 0;
2896
+ if (auxiliaries === null) return null;
2897
+ const reads = scalarParameterReads(parameter, native.body, checker, options.scalarCalls ? services.program : void 0, auxiliaries);
2868
2898
  return reads?.size ? { parameter, reads } : null;
2869
2899
  }
2870
- function independentSignature(native) {
2871
- if (native.parameters.length !== 1 || native.typeParameters?.length || native.asteriskToken || native.modifiers?.length || !primitiveKeyword(native.type)) return false;
2872
- const parameter = native.parameters[0];
2873
- return parameter !== void 0 && parameter.type !== void 0 && !parameter.modifiers?.length && !parameter.questionToken && !parameter.initializer && !parameter.dotDotDotToken && (!ts6.isIdentifier(parameter.name) || parameter.name.text !== "this");
2900
+ function privateDirectCallable(node, native, sourceCode, checker, exports) {
2901
+ const binding3 = callableBinding(node, sourceCode);
2902
+ if (!binding3 || binding3.defs.length !== 1) return false;
2903
+ const name = ts6.isFunctionDeclaration(native) ? native.name : ts6.isVariableDeclaration(native.parent) ? native.parent.name : void 0;
2904
+ const symbol = name && checker.getSymbolAtLocation(name);
2905
+ return symbol !== void 0 && symbol.declarations?.length === 1 && !exports.has(symbol) && directCalls(binding3, native.parameters.length);
2906
+ }
2907
+ function independentSignature(native, selected, auxiliaryScalars) {
2908
+ if (!auxiliaryScalars && native.parameters.length !== 1 || native.typeParameters?.length || native.asteriskToken || native.modifiers?.length || !primitiveKeyword(native.type)) return false;
2909
+ return native.parameters.every((parameter) => parameter.type !== void 0 && !parameter.modifiers?.length && !parameter.questionToken && !parameter.initializer && !parameter.dotDotDotToken && (!ts6.isIdentifier(parameter.name) || parameter.name.text !== "this") && (parameter === selected || ts6.isIdentifier(parameter.name) && primitiveKeyword(parameter.type)));
2910
+ }
2911
+ function auxiliaryBindings(native, selected, checker) {
2912
+ const bindings = /* @__PURE__ */ new Set();
2913
+ for (const parameter of native.parameters) {
2914
+ if (parameter === selected) continue;
2915
+ const symbol = ts6.isIdentifier(parameter.name) ? checker.getSymbolAtLocation(parameter.name) : void 0;
2916
+ if (!symbol || symbol.declarations?.length !== 1 || symbol.declarations[0] !== parameter || bindings.has(symbol)) return null;
2917
+ bindings.add(symbol);
2918
+ }
2919
+ return bindings;
2874
2920
  }
2875
2921
  function callableBinding(node, sourceCode) {
2876
2922
  if (node.type === AST_NODE_TYPES23.FunctionDeclaration) {
@@ -2881,12 +2927,12 @@ function callableBinding(node, sourceCode) {
2881
2927
  if (declaration.type !== AST_NODE_TYPES23.VariableDeclarator || declaration.init !== node || declaration.id.type !== AST_NODE_TYPES23.Identifier || declaration.id.typeAnnotation || declaration.parent.kind !== "const" || declaration.parent.parent.type !== AST_NODE_TYPES23.Program) return void 0;
2882
2928
  return sourceCode.getDeclaredVariables(declaration).find((variable) => variable.defs.some((definition) => definition.type === "Variable" && definition.node === declaration));
2883
2929
  }
2884
- function directCalls(binding3) {
2930
+ function directCalls(binding3, arity) {
2885
2931
  let calls = 0;
2886
2932
  const stable2 = stableValueBinding(binding3, (reference) => {
2887
2933
  const identifier = reference.identifier;
2888
2934
  const call = identifier.parent;
2889
- if (reference.isTypeReference || !reference.isReadOnly() || call.type !== AST_NODE_TYPES23.CallExpression || call.callee !== identifier || call.optional || call.typeArguments || call.arguments.length !== 1 || call.arguments[0]?.type !== AST_NODE_TYPES23.Identifier) return false;
2935
+ if (reference.isTypeReference || !reference.isReadOnly() || call.type !== AST_NODE_TYPES23.CallExpression || call.callee !== identifier || call.optional || call.typeArguments || call.arguments.length !== arity || !call.arguments.every((argument) => argument.type === AST_NODE_TYPES23.Identifier)) return false;
2890
2936
  calls += 1;
2891
2937
  return true;
2892
2938
  });
@@ -3070,6 +3116,8 @@ var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":tru
3070
3116
  var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
3071
3117
  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);';
3072
3118
  var NULLABLE_EXAMPLE2 = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
3119
+ var READ_NULLABLE_EXAMPLE = EXAMPLE2.replace('name: "Ada"', "name: null").replace("readonly name: string", "readonly name: string | null").replace("return user.name;", 'return user.name ?? "";');
3120
+ var AUXILIARY_EXAMPLE = EXAMPLE2.replaceAll("readonly ", "").replace("function label(user:", 'const prefix = "Hello "; function label(prefix: string, user:').replace("return user.name;", "return prefix + user.name;").replace("label(account);", "label(prefix, account);");
3073
3121
  var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
3074
3122
  defaultLevel: "warning",
3075
3123
  summary: "Prefer the proven read-only subset for a private consumer parameter.",
@@ -3078,9 +3126,9 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
3078
3126
  category: "architecture",
3079
3127
  autofix: "safe",
3080
3128
  limitations: [
3081
- "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. Only unread required property signatures are removed; every read or optional member, modifier and terminator token is preserved. Edits that would merge distinct CR and LF terminators remain manual.",
3082
- "Requires parser-owned TypeScript information and a strict external module. Only top-level private function declarations and raw const arrows with exactly one explicit parameter and an independent broad primitive return annotation are supported. Every callable reference must be a direct call with one identifier argument; exports including type-only exports, typeof queries, callbacks, storage, constructors, contextual signatures, overloads, generics, decorators, async functions and direct eval prevent findings.",
3083
- "Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. An unread required field may additionally have exactly one such keyword and null or undefined in a direct two-part union; read or optional nullable fields and more complex unions remain excluded. Fields may be plain or readonly; the consumer body must still prove read-only usage. Retained field modifiers and optional members are preserved. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.",
3129
+ "Automatic fixes additionally require every existing identifier argument to be natively assignable to its corresponding original parameter contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Only unread required object property signatures are removed; every read or optional member, modifier and terminator token is preserved. Auxiliary annotations and argument order stay unchanged. Edits that would merge distinct CR and LF terminators remain manual.",
3130
+ "Requires parser-owned TypeScript information and a strict external module. Only top-level private function declarations and raw const arrows with one explicit inline object parameter and an independent broad primitive return annotation are supported. Other parameters must be required identifiers annotated directly as string, number, boolean or bigint; optional, default, rest, destructured, domain-specific and explicit this auxiliaries are excluded. Every callable reference must be a direct call with an identifier argument for every parameter; exports including type-only exports, typeof queries, callbacks, storage, constructors, contextual signatures, overloads, generics, decorators, async functions and direct eval prevent findings.",
3131
+ "Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. A read field or an unread required field may additionally have exactly one such keyword and null or undefined in a direct two-part union; unread optional nullable fields and more complex unions remain excluded. Fields may be plain or readonly; the consumer body must still prove read-only usage. Every read field keeps its exact type, modifiers and optionality. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.",
3084
3132
  "The single return expression may use only fixed scalar field reads, flat destructuring, literals, operators, templates and scalar calls to same-file implemented top-level functions or raw const arrows with an explicit return annotation. These calls require one native resolved declaration, no generic or predicate signature, scalar arguments/result and complete active native checking of the local implementation file. Their helper bodies need not be pure because only scalar values cross the call and only an erased parameter annotation changes. Imported, aliased, ambient, overloaded, optional, spread and member calls remain unsupported. Bound destructured fields count as reads even when their local aliases are unused. Whole-object escape, identity, mutation, closures, reflection, serialization, dynamic keys, implicit Object capabilities and JSX are unsupported. Unknown semantic information causes abstention. Findings outside the fix proof remain manual; this is not a requirement to minimize every object or discard useful producer fields."
3085
3133
  ],
3086
3134
  examples: [
@@ -3089,7 +3137,11 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
3089
3137
  { 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;", "") }] },
3090
3138
  { 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 },
3091
3139
  { 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;", "") }] },
3092
- { 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 }
3140
+ { 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 },
3141
+ { id: "retained-nullable-read", scenarioId: "retained-nullable-field", title: "A nullable name remains while an unread email requirement is removed", outcome: "match", files: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE.replace("readonly email: string;", "") }] },
3142
+ { id: "retained-nullable-all-used", scenarioId: "retained-nullable-field", title: "Both requirements remain when the nullable view reads email too", outcome: "no-match", files: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE.replace('return user.name ?? "";', "return user.name ?? user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
3143
+ { id: "auxiliary-scalar", scenarioId: "auxiliary-scalar-parameter", title: "A scalar prefix does not require the object's unread email", outcome: "match", files: [{ path: "src/case.ts", source: AUXILIARY_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: AUXILIARY_EXAMPLE.replace("email: string;", "") }] },
3144
+ { id: "auxiliary-all-used", scenarioId: "auxiliary-scalar-parameter", title: "Reading both object fields retains their requirements", outcome: "no-match", files: [{ path: "src/case.ts", source: AUXILIARY_EXAMPLE.replace("return prefix + user.name;", "return prefix + user.name + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
3093
3145
  ]
3094
3146
  };
3095
3147
  function inlineFields(node, reads) {
@@ -3097,7 +3149,7 @@ function inlineFields(node, reads) {
3097
3149
  for (const member3 of node.members) {
3098
3150
  if (!ts8.isPropertySignature(member3) || !member3.name || !ts8.isIdentifier(member3.name) && !ts8.isStringLiteralLike(member3.name) || (member3.modifiers?.length ?? 0) > 1 || member3.modifiers?.some((modifier) => modifier.kind !== ts8.SyntaxKind.ReadonlyKeyword)) return null;
3099
3151
  const key = member3.name.text;
3100
- if (!primitiveKeyword(member3.type) && (member3.questionToken || reads.has(key) || !nullishPrimitiveUnion(member3.type))) return null;
3152
+ if (!primitiveKeyword(member3.type) && (!nullishPrimitiveUnion(member3.type) || member3.questionToken && !reads.has(key))) return null;
3101
3153
  if (fields2.has(key)) return null;
3102
3154
  fields2.set(key, !member3.questionToken);
3103
3155
  }
@@ -3115,7 +3167,7 @@ var prefer_minimal_parameter_contract_default = createRule({
3115
3167
  const candidates = [];
3116
3168
  let dynamic = false;
3117
3169
  const remember = (node) => {
3118
- if (consumerAnnotation(node)?.type === AST_NODE_TYPES25.TSTypeLiteral) candidates.push(node);
3170
+ if (consumerAnnotation(node, true)?.type === AST_NODE_TYPES25.TSTypeLiteral) candidates.push(node);
3119
3171
  };
3120
3172
  return {
3121
3173
  FunctionDeclaration: remember,
@@ -3131,7 +3183,7 @@ var prefer_minimal_parameter_contract_default = createRule({
3131
3183
  const checker = services.program.getTypeChecker();
3132
3184
  const exports = exportedSymbols(source, checker);
3133
3185
  for (const candidate2 of candidates) {
3134
- const consumer = privateScalarConsumer(candidate2, context.sourceCode, services, exports, true);
3186
+ const consumer = privateScalarConsumer(candidate2, context.sourceCode, services, exports, { scalarCalls: true, auxiliaryScalars: true });
3135
3187
  const annotation = consumer?.parameter.type;
3136
3188
  if (!consumer || !annotation || !ts8.isTypeLiteralNode(annotation)) continue;
3137
3189
  const fields2 = inlineFields(annotation, consumer.reads);
@@ -4214,7 +4266,7 @@ function isSearchParamReceiver(call, services) {
4214
4266
  // package.json
4215
4267
  var package_default = {
4216
4268
  name: "@sarj/eslint-plugin",
4217
- version: "16.38.52",
4269
+ version: "16.38.54",
4218
4270
  packageManager: "npm@12.2.1",
4219
4271
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
4220
4272
  type: "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.38.52",
3
+ "version": "16.38.54",
4
4
  "packageManager": "npm@12.2.1",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",