@sarj/eslint-plugin 16.36.5 → 16.36.6
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 +31 -13
- package/dist/index.js +31 -13
- 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,7 @@ 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 ?? '';");
|
|
2711
2730
|
var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
|
|
2712
2731
|
defaultLevel: "warning",
|
|
2713
2732
|
summary: "Keep the covering object contract in a private scalar consumer union.",
|
|
@@ -2718,21 +2737,23 @@ var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
|
|
|
2718
2737
|
limitations: [
|
|
2719
2738
|
"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
2739
|
"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.",
|
|
2740
|
+
"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.",
|
|
2722
2741
|
"Reports only proper native assignability: covered is assignable to covering and the reverse is false. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any also cannot witness coverage. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; mutually assignable but nonidentical structures are excluded.",
|
|
2723
2742
|
"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
2743
|
],
|
|
2725
2744
|
examples: [
|
|
2726
2745
|
{ 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 }
|
|
2746
|
+
{ 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: "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") }] },
|
|
2748
|
+
{ 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
2749
|
]
|
|
2729
2750
|
};
|
|
2730
2751
|
function scalarFields(type, checker) {
|
|
2731
|
-
if (!closedObjectContract(type, checker)) return null;
|
|
2752
|
+
if (!closedObjectContract(type, checker, true)) return null;
|
|
2732
2753
|
const fields2 = /* @__PURE__ */ new Map();
|
|
2733
2754
|
for (const property of type.getProperties()) {
|
|
2734
2755
|
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;
|
|
2756
|
+
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
2757
|
fields2.set(property.name, (property.flags & import_typescript7.default.SymbolFlags.Optional) === 0);
|
|
2737
2758
|
}
|
|
2738
2759
|
return fields2.size ? fields2 : null;
|
|
@@ -2819,7 +2840,7 @@ var import_typescript8 = __toESM(require("typescript"), 1);
|
|
|
2819
2840
|
var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
|
|
2820
2841
|
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
2842
|
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
|
|
2843
|
+
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
2844
|
var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
2824
2845
|
defaultLevel: "warning",
|
|
2825
2846
|
summary: "Prefer the proven read-only subset for a private consumer parameter.",
|
|
@@ -2838,8 +2859,8 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
2838
2859
|
{ 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
2860
|
{ 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
2861
|
{ 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:
|
|
2862
|
+
{ 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;", "") }] },
|
|
2863
|
+
{ 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
2864
|
]
|
|
2844
2865
|
};
|
|
2845
2866
|
function inlineFields(node, reads) {
|
|
@@ -2853,9 +2874,6 @@ function inlineFields(node, reads) {
|
|
|
2853
2874
|
}
|
|
2854
2875
|
return fields2;
|
|
2855
2876
|
}
|
|
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
2877
|
var prefer_minimal_parameter_contract_default = createRule({
|
|
2860
2878
|
name: "prefer-minimal-parameter-contract",
|
|
2861
2879
|
documentation: PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION,
|
|
@@ -4007,7 +4025,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
4007
4025
|
// package.json
|
|
4008
4026
|
var package_default = {
|
|
4009
4027
|
name: "@sarj/eslint-plugin",
|
|
4010
|
-
version: "16.36.
|
|
4028
|
+
version: "16.36.6",
|
|
4011
4029
|
packageManager: "npm@12.2.1",
|
|
4012
4030
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
4013
4031
|
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,7 @@ 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 ?? '';");
|
|
2660
2679
|
var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
|
|
2661
2680
|
defaultLevel: "warning",
|
|
2662
2681
|
summary: "Keep the covering object contract in a private scalar consumer union.",
|
|
@@ -2667,21 +2686,23 @@ var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
|
|
|
2667
2686
|
limitations: [
|
|
2668
2687
|
"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
2688
|
"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.",
|
|
2689
|
+
"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.",
|
|
2671
2690
|
"Reports only proper native assignability: covered is assignable to covering and the reverse is false. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any also cannot witness coverage. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; mutually assignable but nonidentical structures are excluded.",
|
|
2672
2691
|
"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
2692
|
],
|
|
2674
2693
|
examples: [
|
|
2675
2694
|
{ 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 }
|
|
2695
|
+
{ 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: "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") }] },
|
|
2697
|
+
{ 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
2698
|
]
|
|
2678
2699
|
};
|
|
2679
2700
|
function scalarFields(type, checker) {
|
|
2680
|
-
if (!closedObjectContract(type, checker)) return null;
|
|
2701
|
+
if (!closedObjectContract(type, checker, true)) return null;
|
|
2681
2702
|
const fields2 = /* @__PURE__ */ new Map();
|
|
2682
2703
|
for (const property of type.getProperties()) {
|
|
2683
2704
|
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;
|
|
2705
|
+
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
2706
|
fields2.set(property.name, (property.flags & ts7.SymbolFlags.Optional) === 0);
|
|
2686
2707
|
}
|
|
2687
2708
|
return fields2.size ? fields2 : null;
|
|
@@ -2768,7 +2789,7 @@ import ts8 from "typescript";
|
|
|
2768
2789
|
var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
|
|
2769
2790
|
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
2791
|
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
|
|
2792
|
+
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
2793
|
var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
2773
2794
|
defaultLevel: "warning",
|
|
2774
2795
|
summary: "Prefer the proven read-only subset for a private consumer parameter.",
|
|
@@ -2787,8 +2808,8 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
2787
2808
|
{ 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
2809
|
{ 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
2810
|
{ 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:
|
|
2811
|
+
{ 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;", "") }] },
|
|
2812
|
+
{ 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
2813
|
]
|
|
2793
2814
|
};
|
|
2794
2815
|
function inlineFields(node, reads) {
|
|
@@ -2802,9 +2823,6 @@ function inlineFields(node, reads) {
|
|
|
2802
2823
|
}
|
|
2803
2824
|
return fields2;
|
|
2804
2825
|
}
|
|
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
2826
|
var prefer_minimal_parameter_contract_default = createRule({
|
|
2809
2827
|
name: "prefer-minimal-parameter-contract",
|
|
2810
2828
|
documentation: PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION,
|
|
@@ -3960,7 +3978,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
3960
3978
|
// package.json
|
|
3961
3979
|
var package_default = {
|
|
3962
3980
|
name: "@sarj/eslint-plugin",
|
|
3963
|
-
version: "16.36.
|
|
3981
|
+
version: "16.36.6",
|
|
3964
3982
|
packageManager: "npm@12.2.1",
|
|
3965
3983
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
3966
3984
|
type: "module",
|