@sarj/eslint-plugin 16.36.1 → 16.36.3
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 +13 -6
- package/dist/index.js +13 -6
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2773,6 +2773,7 @@ var import_typescript8 = __toESM(require("typescript"), 1);
|
|
|
2773
2773
|
var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
|
|
2774
2774
|
var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
|
|
2775
2775
|
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);';
|
|
2776
|
+
var NULLABLE_EXAMPLE = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
|
|
2776
2777
|
var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
2777
2778
|
defaultLevel: "warning",
|
|
2778
2779
|
summary: "Prefer the proven read-only subset for a private consumer parameter.",
|
|
@@ -2783,26 +2784,32 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
2783
2784
|
limitations: [
|
|
2784
2785
|
"Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Only unread required property signatures are removed; every read or optional member, modifier and terminator token is preserved. Edits that would merge distinct CR and LF terminators remain manual.",
|
|
2785
2786
|
"Requires parser-owned TypeScript information and a strict external module. Only top-level private function declarations and raw const arrows with exactly one explicit parameter and an independent broad primitive return annotation are supported. Every callable reference must be a direct call with one identifier argument; exports including type-only exports, typeof queries, callbacks, storage, constructors, contextual signatures, overloads, generics, decorators, async functions and direct eval prevent findings.",
|
|
2786
|
-
"Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. Fields may be plain or readonly; the consumer body must still prove read-only usage. Retained field modifiers and optional members are preserved. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.",
|
|
2787
|
+
"Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. An unread required field may additionally have exactly one such keyword and null or undefined in a direct two-part union; read or optional nullable fields and more complex unions remain excluded. Fields may be plain or readonly; the consumer body must still prove read-only usage. Retained field modifiers and optional members are preserved. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.",
|
|
2787
2788
|
"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."
|
|
2788
2789
|
],
|
|
2789
2790
|
examples: [
|
|
2790
2791
|
{ id: "unused-required-field", title: "A private name consumer unnecessarily requires email", outcome: "match", files: [{ path: "src/case.ts", source: EXAMPLE2 }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: EXAMPLE2.replace("readonly email: string;", "") }] },
|
|
2791
2792
|
{ 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 },
|
|
2792
2793
|
{ 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;", "") }] },
|
|
2793
|
-
{ 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 }
|
|
2794
|
+
{ 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 },
|
|
2795
|
+
{ id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("email: string | null;", "") }] },
|
|
2796
|
+
{ id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
|
|
2794
2797
|
]
|
|
2795
2798
|
};
|
|
2796
|
-
function inlineFields(node) {
|
|
2799
|
+
function inlineFields(node, reads) {
|
|
2797
2800
|
const fields2 = /* @__PURE__ */ new Map();
|
|
2798
2801
|
for (const member3 of node.members) {
|
|
2799
|
-
if (!import_typescript8.default.isPropertySignature(member3) || !member3.name || !import_typescript8.default.isIdentifier(member3.name) && !import_typescript8.default.isStringLiteralLike(member3.name) || (member3.modifiers?.length ?? 0) > 1 || member3.modifiers?.some((modifier) => modifier.kind !== import_typescript8.default.SyntaxKind.ReadonlyKeyword)
|
|
2802
|
+
if (!import_typescript8.default.isPropertySignature(member3) || !member3.name || !import_typescript8.default.isIdentifier(member3.name) && !import_typescript8.default.isStringLiteralLike(member3.name) || (member3.modifiers?.length ?? 0) > 1 || member3.modifiers?.some((modifier) => modifier.kind !== import_typescript8.default.SyntaxKind.ReadonlyKeyword)) return null;
|
|
2800
2803
|
const key = member3.name.text;
|
|
2804
|
+
if (!primitiveKeyword(member3.type) && (member3.questionToken || reads.has(key) || !nullishPrimitiveUnion(member3.type))) return null;
|
|
2801
2805
|
if (fields2.has(key)) return null;
|
|
2802
2806
|
fields2.set(key, !member3.questionToken);
|
|
2803
2807
|
}
|
|
2804
2808
|
return fields2;
|
|
2805
2809
|
}
|
|
2810
|
+
function nullishPrimitiveUnion(node) {
|
|
2811
|
+
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);
|
|
2812
|
+
}
|
|
2806
2813
|
var prefer_minimal_parameter_contract_default = createRule({
|
|
2807
2814
|
name: "prefer-minimal-parameter-contract",
|
|
2808
2815
|
documentation: PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION,
|
|
@@ -2834,7 +2841,7 @@ var prefer_minimal_parameter_contract_default = createRule({
|
|
|
2834
2841
|
const consumer = privateScalarConsumer(candidate2, context.sourceCode, services, exports2, true);
|
|
2835
2842
|
const annotation = consumer?.parameter.type;
|
|
2836
2843
|
if (!consumer || !annotation || !import_typescript8.default.isTypeLiteralNode(annotation)) continue;
|
|
2837
|
-
const fields2 = inlineFields(annotation);
|
|
2844
|
+
const fields2 = inlineFields(annotation, consumer.reads);
|
|
2838
2845
|
if (!fields2 || [...consumer.reads].some((key) => !fields2.has(key)) || ![...consumer.reads].some((key) => fields2.get(key))) continue;
|
|
2839
2846
|
const unused = [...fields2].filter(([key, required]) => required && !consumer.reads.has(key)).map(([key]) => key).sort();
|
|
2840
2847
|
if (!unused.length) continue;
|
|
@@ -3954,7 +3961,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
3954
3961
|
// package.json
|
|
3955
3962
|
var package_default = {
|
|
3956
3963
|
name: "@sarj/eslint-plugin",
|
|
3957
|
-
version: "16.36.
|
|
3964
|
+
version: "16.36.3",
|
|
3958
3965
|
packageManager: "npm@12.2.1",
|
|
3959
3966
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
3960
3967
|
type: "module",
|
package/dist/index.js
CHANGED
|
@@ -2722,6 +2722,7 @@ import ts8 from "typescript";
|
|
|
2722
2722
|
var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
|
|
2723
2723
|
var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
|
|
2724
2724
|
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);';
|
|
2725
|
+
var NULLABLE_EXAMPLE = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
|
|
2725
2726
|
var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
2726
2727
|
defaultLevel: "warning",
|
|
2727
2728
|
summary: "Prefer the proven read-only subset for a private consumer parameter.",
|
|
@@ -2732,26 +2733,32 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
2732
2733
|
limitations: [
|
|
2733
2734
|
"Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Only unread required property signatures are removed; every read or optional member, modifier and terminator token is preserved. Edits that would merge distinct CR and LF terminators remain manual.",
|
|
2734
2735
|
"Requires parser-owned TypeScript information and a strict external module. Only top-level private function declarations and raw const arrows with exactly one explicit parameter and an independent broad primitive return annotation are supported. Every callable reference must be a direct call with one identifier argument; exports including type-only exports, typeof queries, callbacks, storage, constructors, contextual signatures, overloads, generics, decorators, async functions and direct eval prevent findings.",
|
|
2735
|
-
"Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. Fields may be plain or readonly; the consumer body must still prove read-only usage. Retained field modifiers and optional members are preserved. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.",
|
|
2736
|
+
"Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. An unread required field may additionally have exactly one such keyword and null or undefined in a direct two-part union; read or optional nullable fields and more complex unions remain excluded. Fields may be plain or readonly; the consumer body must still prove read-only usage. Retained field modifiers and optional members are preserved. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.",
|
|
2736
2737
|
"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."
|
|
2737
2738
|
],
|
|
2738
2739
|
examples: [
|
|
2739
2740
|
{ id: "unused-required-field", title: "A private name consumer unnecessarily requires email", outcome: "match", files: [{ path: "src/case.ts", source: EXAMPLE2 }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: EXAMPLE2.replace("readonly email: string;", "") }] },
|
|
2740
2741
|
{ 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 },
|
|
2741
2742
|
{ 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;", "") }] },
|
|
2742
|
-
{ 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 }
|
|
2743
|
+
{ 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 },
|
|
2744
|
+
{ id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("email: string | null;", "") }] },
|
|
2745
|
+
{ id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
|
|
2743
2746
|
]
|
|
2744
2747
|
};
|
|
2745
|
-
function inlineFields(node) {
|
|
2748
|
+
function inlineFields(node, reads) {
|
|
2746
2749
|
const fields2 = /* @__PURE__ */ new Map();
|
|
2747
2750
|
for (const member3 of node.members) {
|
|
2748
|
-
if (!ts8.isPropertySignature(member3) || !member3.name || !ts8.isIdentifier(member3.name) && !ts8.isStringLiteralLike(member3.name) || (member3.modifiers?.length ?? 0) > 1 || member3.modifiers?.some((modifier) => modifier.kind !== ts8.SyntaxKind.ReadonlyKeyword)
|
|
2751
|
+
if (!ts8.isPropertySignature(member3) || !member3.name || !ts8.isIdentifier(member3.name) && !ts8.isStringLiteralLike(member3.name) || (member3.modifiers?.length ?? 0) > 1 || member3.modifiers?.some((modifier) => modifier.kind !== ts8.SyntaxKind.ReadonlyKeyword)) return null;
|
|
2749
2752
|
const key = member3.name.text;
|
|
2753
|
+
if (!primitiveKeyword(member3.type) && (member3.questionToken || reads.has(key) || !nullishPrimitiveUnion(member3.type))) return null;
|
|
2750
2754
|
if (fields2.has(key)) return null;
|
|
2751
2755
|
fields2.set(key, !member3.questionToken);
|
|
2752
2756
|
}
|
|
2753
2757
|
return fields2;
|
|
2754
2758
|
}
|
|
2759
|
+
function nullishPrimitiveUnion(node) {
|
|
2760
|
+
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);
|
|
2761
|
+
}
|
|
2755
2762
|
var prefer_minimal_parameter_contract_default = createRule({
|
|
2756
2763
|
name: "prefer-minimal-parameter-contract",
|
|
2757
2764
|
documentation: PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION,
|
|
@@ -2783,7 +2790,7 @@ var prefer_minimal_parameter_contract_default = createRule({
|
|
|
2783
2790
|
const consumer = privateScalarConsumer(candidate2, context.sourceCode, services, exports, true);
|
|
2784
2791
|
const annotation = consumer?.parameter.type;
|
|
2785
2792
|
if (!consumer || !annotation || !ts8.isTypeLiteralNode(annotation)) continue;
|
|
2786
|
-
const fields2 = inlineFields(annotation);
|
|
2793
|
+
const fields2 = inlineFields(annotation, consumer.reads);
|
|
2787
2794
|
if (!fields2 || [...consumer.reads].some((key) => !fields2.has(key)) || ![...consumer.reads].some((key) => fields2.get(key))) continue;
|
|
2788
2795
|
const unused = [...fields2].filter(([key, required]) => required && !consumer.reads.has(key)).map(([key]) => key).sort();
|
|
2789
2796
|
if (!unused.length) continue;
|
|
@@ -3907,7 +3914,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
3907
3914
|
// package.json
|
|
3908
3915
|
var package_default = {
|
|
3909
3916
|
name: "@sarj/eslint-plugin",
|
|
3910
|
-
version: "16.36.
|
|
3917
|
+
version: "16.36.3",
|
|
3911
3918
|
packageManager: "npm@12.2.1",
|
|
3912
3919
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
3913
3920
|
type: "module",
|