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