@sarj/eslint-plugin 16.38.53 → 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 +82 -33
- package/dist/index.js +82 -33
- package/package.json +1 -1
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
|
|
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 !==
|
|
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)
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
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 !==
|
|
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
|
});
|
|
@@ -3122,6 +3168,7 @@ var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function l
|
|
|
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);';
|
|
3124
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);");
|
|
3125
3172
|
var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
3126
3173
|
defaultLevel: "warning",
|
|
3127
3174
|
summary: "Prefer the proven read-only subset for a private consumer parameter.",
|
|
@@ -3130,8 +3177,8 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
3130
3177
|
category: "architecture",
|
|
3131
3178
|
autofix: "safe",
|
|
3132
3179
|
limitations: [
|
|
3133
|
-
"Automatic fixes additionally require every existing identifier
|
|
3134
|
-
"Requires parser-owned TypeScript information and a strict external module. Only top-level private function declarations and raw const arrows with
|
|
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.",
|
|
3135
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.",
|
|
3136
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."
|
|
3137
3184
|
],
|
|
@@ -3143,7 +3190,9 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
3143
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;", "") }] },
|
|
3144
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 },
|
|
3145
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;", "") }] },
|
|
3146
|
-
{ 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 }
|
|
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 }
|
|
3147
3196
|
]
|
|
3148
3197
|
};
|
|
3149
3198
|
function inlineFields(node, reads) {
|
|
@@ -3169,7 +3218,7 @@ var prefer_minimal_parameter_contract_default = createRule({
|
|
|
3169
3218
|
const candidates = [];
|
|
3170
3219
|
let dynamic = false;
|
|
3171
3220
|
const remember = (node) => {
|
|
3172
|
-
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);
|
|
3173
3222
|
};
|
|
3174
3223
|
return {
|
|
3175
3224
|
FunctionDeclaration: remember,
|
|
@@ -3185,7 +3234,7 @@ var prefer_minimal_parameter_contract_default = createRule({
|
|
|
3185
3234
|
const checker = services.program.getTypeChecker();
|
|
3186
3235
|
const exports2 = exportedSymbols(source, checker);
|
|
3187
3236
|
for (const candidate2 of candidates) {
|
|
3188
|
-
const consumer = privateScalarConsumer(candidate2, context.sourceCode, services, exports2, true);
|
|
3237
|
+
const consumer = privateScalarConsumer(candidate2, context.sourceCode, services, exports2, { scalarCalls: true, auxiliaryScalars: true });
|
|
3189
3238
|
const annotation = consumer?.parameter.type;
|
|
3190
3239
|
if (!consumer || !annotation || !import_typescript8.default.isTypeLiteralNode(annotation)) continue;
|
|
3191
3240
|
const fields2 = inlineFields(annotation, consumer.reads);
|
|
@@ -4264,7 +4313,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
4264
4313
|
// package.json
|
|
4265
4314
|
var package_default = {
|
|
4266
4315
|
name: "@sarj/eslint-plugin",
|
|
4267
|
-
version: "16.38.
|
|
4316
|
+
version: "16.38.54",
|
|
4268
4317
|
packageManager: "npm@12.2.1",
|
|
4269
4318
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
4270
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
|
|
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 !==
|
|
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)
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
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 !==
|
|
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
|
});
|
|
@@ -3071,6 +3117,7 @@ var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function l
|
|
|
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);';
|
|
3073
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);");
|
|
3074
3121
|
var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
3075
3122
|
defaultLevel: "warning",
|
|
3076
3123
|
summary: "Prefer the proven read-only subset for a private consumer parameter.",
|
|
@@ -3079,8 +3126,8 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
3079
3126
|
category: "architecture",
|
|
3080
3127
|
autofix: "safe",
|
|
3081
3128
|
limitations: [
|
|
3082
|
-
"Automatic fixes additionally require every existing identifier
|
|
3083
|
-
"Requires parser-owned TypeScript information and a strict external module. Only top-level private function declarations and raw const arrows with
|
|
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.",
|
|
3084
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.",
|
|
3085
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."
|
|
3086
3133
|
],
|
|
@@ -3092,7 +3139,9 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
3092
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;", "") }] },
|
|
3093
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 },
|
|
3094
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;", "") }] },
|
|
3095
|
-
{ 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 }
|
|
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 }
|
|
3096
3145
|
]
|
|
3097
3146
|
};
|
|
3098
3147
|
function inlineFields(node, reads) {
|
|
@@ -3118,7 +3167,7 @@ var prefer_minimal_parameter_contract_default = createRule({
|
|
|
3118
3167
|
const candidates = [];
|
|
3119
3168
|
let dynamic = false;
|
|
3120
3169
|
const remember = (node) => {
|
|
3121
|
-
if (consumerAnnotation(node)?.type === AST_NODE_TYPES25.TSTypeLiteral) candidates.push(node);
|
|
3170
|
+
if (consumerAnnotation(node, true)?.type === AST_NODE_TYPES25.TSTypeLiteral) candidates.push(node);
|
|
3122
3171
|
};
|
|
3123
3172
|
return {
|
|
3124
3173
|
FunctionDeclaration: remember,
|
|
@@ -3134,7 +3183,7 @@ var prefer_minimal_parameter_contract_default = createRule({
|
|
|
3134
3183
|
const checker = services.program.getTypeChecker();
|
|
3135
3184
|
const exports = exportedSymbols(source, checker);
|
|
3136
3185
|
for (const candidate2 of candidates) {
|
|
3137
|
-
const consumer = privateScalarConsumer(candidate2, context.sourceCode, services, exports, true);
|
|
3186
|
+
const consumer = privateScalarConsumer(candidate2, context.sourceCode, services, exports, { scalarCalls: true, auxiliaryScalars: true });
|
|
3138
3187
|
const annotation = consumer?.parameter.type;
|
|
3139
3188
|
if (!consumer || !annotation || !ts8.isTypeLiteralNode(annotation)) continue;
|
|
3140
3189
|
const fields2 = inlineFields(annotation, consumer.reads);
|
|
@@ -4217,7 +4266,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
4217
4266
|
// package.json
|
|
4218
4267
|
var package_default = {
|
|
4219
4268
|
name: "@sarj/eslint-plugin",
|
|
4220
|
-
version: "16.38.
|
|
4269
|
+
version: "16.38.54",
|
|
4221
4270
|
packageManager: "npm@12.2.1",
|
|
4222
4271
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
4223
4272
|
type: "module",
|