@sarj/eslint-plugin 16.38.50 → 16.38.53
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 +64 -9
- package/dist/index.js +64 -9
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2573,16 +2573,68 @@ function typeBinding(source, identifier) {
|
|
|
2573
2573
|
}
|
|
2574
2574
|
return null;
|
|
2575
2575
|
}
|
|
2576
|
+
var REFERENCE_INDEXES = /* @__PURE__ */ new WeakMap();
|
|
2576
2577
|
function retainsNamedTypeUses(source, removed) {
|
|
2577
2578
|
const manager = source.scopeManager;
|
|
2578
2579
|
if (manager === null) return false;
|
|
2579
|
-
const
|
|
2580
|
-
|
|
2580
|
+
const index = referenceIndex(source, manager);
|
|
2581
|
+
for (const reference of referencesWithin(index.references, removed)) {
|
|
2582
|
+
if (!reference.isRead()) continue;
|
|
2581
2583
|
const binding3 = reference.resolved;
|
|
2582
2584
|
if (!binding3 || !reference.isTypeReference && !reference.isValueReference) return false;
|
|
2583
|
-
if (binding3.defs.length === 0)
|
|
2584
|
-
|
|
2585
|
-
|
|
2585
|
+
if (binding3.defs.length === 0) continue;
|
|
2586
|
+
const reads = publicReadExtents(index, binding3);
|
|
2587
|
+
const extent = reference.isTypeReference ? reference.isValueReference ? reads.both : reads.type : reads.value;
|
|
2588
|
+
if (extent === null || extent.minStart >= removed.range[0] && extent.maxEnd <= removed.range[1]) return false;
|
|
2589
|
+
}
|
|
2590
|
+
return true;
|
|
2591
|
+
}
|
|
2592
|
+
function referenceIndex(source, manager) {
|
|
2593
|
+
const cached = REFERENCE_INDEXES.get(source);
|
|
2594
|
+
if (cached?.manager === manager) return cached;
|
|
2595
|
+
const references = manager.scopes.flatMap((scope) => scope.references);
|
|
2596
|
+
references.sort((left, right) => left.identifier.range[0] - right.identifier.range[0]);
|
|
2597
|
+
const index = { manager, references, publicReads: /* @__PURE__ */ new Map() };
|
|
2598
|
+
REFERENCE_INDEXES.set(source, index);
|
|
2599
|
+
return index;
|
|
2600
|
+
}
|
|
2601
|
+
function* referencesWithin(references, removed) {
|
|
2602
|
+
let low = 0;
|
|
2603
|
+
let high = references.length;
|
|
2604
|
+
while (low < high) {
|
|
2605
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
2606
|
+
const reference = references[middle];
|
|
2607
|
+
if (reference === void 0 || reference.identifier.range[0] >= removed.range[0]) high = middle;
|
|
2608
|
+
else low = middle + 1;
|
|
2609
|
+
}
|
|
2610
|
+
for (let index = low; index < references.length; index++) {
|
|
2611
|
+
const reference = references[index];
|
|
2612
|
+
if (reference === void 0) continue;
|
|
2613
|
+
if (reference.identifier.range[0] > removed.range[1]) break;
|
|
2614
|
+
if (insideTypeRange(reference.identifier, removed)) yield reference;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
function publicReadExtents(index, binding3) {
|
|
2618
|
+
const cached = index.publicReads.get(binding3);
|
|
2619
|
+
if (cached !== void 0) return cached;
|
|
2620
|
+
let type = null;
|
|
2621
|
+
let value = null;
|
|
2622
|
+
let both = null;
|
|
2623
|
+
for (const retained of binding3.references) {
|
|
2624
|
+
if (!retained.isRead() || !retained.isTypeReference && !retained.isValueReference || binding3.defs.some((definition) => insideTypeRange(retained.identifier, definition.node)) || !publicTypeUse(retained.identifier)) continue;
|
|
2625
|
+
if (retained.isTypeReference) type = extendExtent(type, retained.identifier);
|
|
2626
|
+
if (retained.isValueReference) value = extendExtent(value, retained.identifier);
|
|
2627
|
+
if (retained.isTypeReference && retained.isValueReference) both = extendExtent(both, retained.identifier);
|
|
2628
|
+
}
|
|
2629
|
+
const result = { type, value, both };
|
|
2630
|
+
index.publicReads.set(binding3, result);
|
|
2631
|
+
return result;
|
|
2632
|
+
}
|
|
2633
|
+
function extendExtent(extent, identifier) {
|
|
2634
|
+
return {
|
|
2635
|
+
minStart: Math.min(extent?.minStart ?? identifier.range[0], identifier.range[0]),
|
|
2636
|
+
maxEnd: Math.max(extent?.maxEnd ?? identifier.range[1], identifier.range[1])
|
|
2637
|
+
};
|
|
2586
2638
|
}
|
|
2587
2639
|
function publicTypeUse(identifier) {
|
|
2588
2640
|
let node = identifier;
|
|
@@ -3069,6 +3121,7 @@ var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":tru
|
|
|
3069
3121
|
var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
|
|
3070
3122
|
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);';
|
|
3071
3123
|
var NULLABLE_EXAMPLE2 = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
|
|
3124
|
+
var READ_NULLABLE_EXAMPLE = EXAMPLE2.replace('name: "Ada"', "name: null").replace("readonly name: string", "readonly name: string | null").replace("return user.name;", 'return user.name ?? "";');
|
|
3072
3125
|
var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
3073
3126
|
defaultLevel: "warning",
|
|
3074
3127
|
summary: "Prefer the proven read-only subset for a private consumer parameter.",
|
|
@@ -3079,7 +3132,7 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
3079
3132
|
limitations: [
|
|
3080
3133
|
"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.",
|
|
3081
3134
|
"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.",
|
|
3082
|
-
"Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported.
|
|
3135
|
+
"Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. A read field or an unread required field may additionally have exactly one such keyword and null or undefined in a direct two-part union; unread optional nullable fields and more complex unions remain excluded. Fields may be plain or readonly; the consumer body must still prove read-only usage. Every read field keeps its exact type, modifiers and optionality. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.",
|
|
3083
3136
|
"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."
|
|
3084
3137
|
],
|
|
3085
3138
|
examples: [
|
|
@@ -3088,7 +3141,9 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
3088
3141
|
{ 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;", "") }] },
|
|
3089
3142
|
{ 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 },
|
|
3090
3143
|
{ 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;", "") }] },
|
|
3091
|
-
{ 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 }
|
|
3144
|
+
{ id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
|
|
3145
|
+
{ id: "retained-nullable-read", scenarioId: "retained-nullable-field", title: "A nullable name remains while an unread email requirement is removed", outcome: "match", files: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE.replace("readonly email: string;", "") }] },
|
|
3146
|
+
{ id: "retained-nullable-all-used", scenarioId: "retained-nullable-field", title: "Both requirements remain when the nullable view reads email too", outcome: "no-match", files: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE.replace('return user.name ?? "";', "return user.name ?? user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
|
|
3092
3147
|
]
|
|
3093
3148
|
};
|
|
3094
3149
|
function inlineFields(node, reads) {
|
|
@@ -3096,7 +3151,7 @@ function inlineFields(node, reads) {
|
|
|
3096
3151
|
for (const member3 of node.members) {
|
|
3097
3152
|
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;
|
|
3098
3153
|
const key = member3.name.text;
|
|
3099
|
-
if (!primitiveKeyword(member3.type) && (member3.
|
|
3154
|
+
if (!primitiveKeyword(member3.type) && (!nullishPrimitiveUnion(member3.type) || member3.questionToken && !reads.has(key))) return null;
|
|
3100
3155
|
if (fields2.has(key)) return null;
|
|
3101
3156
|
fields2.set(key, !member3.questionToken);
|
|
3102
3157
|
}
|
|
@@ -4209,7 +4264,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
4209
4264
|
// package.json
|
|
4210
4265
|
var package_default = {
|
|
4211
4266
|
name: "@sarj/eslint-plugin",
|
|
4212
|
-
version: "16.38.
|
|
4267
|
+
version: "16.38.53",
|
|
4213
4268
|
packageManager: "npm@12.2.1",
|
|
4214
4269
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
4215
4270
|
type: "module",
|
package/dist/index.js
CHANGED
|
@@ -2522,16 +2522,68 @@ function typeBinding(source, identifier) {
|
|
|
2522
2522
|
}
|
|
2523
2523
|
return null;
|
|
2524
2524
|
}
|
|
2525
|
+
var REFERENCE_INDEXES = /* @__PURE__ */ new WeakMap();
|
|
2525
2526
|
function retainsNamedTypeUses(source, removed) {
|
|
2526
2527
|
const manager = source.scopeManager;
|
|
2527
2528
|
if (manager === null) return false;
|
|
2528
|
-
const
|
|
2529
|
-
|
|
2529
|
+
const index = referenceIndex(source, manager);
|
|
2530
|
+
for (const reference of referencesWithin(index.references, removed)) {
|
|
2531
|
+
if (!reference.isRead()) continue;
|
|
2530
2532
|
const binding3 = reference.resolved;
|
|
2531
2533
|
if (!binding3 || !reference.isTypeReference && !reference.isValueReference) return false;
|
|
2532
|
-
if (binding3.defs.length === 0)
|
|
2533
|
-
|
|
2534
|
-
|
|
2534
|
+
if (binding3.defs.length === 0) continue;
|
|
2535
|
+
const reads = publicReadExtents(index, binding3);
|
|
2536
|
+
const extent = reference.isTypeReference ? reference.isValueReference ? reads.both : reads.type : reads.value;
|
|
2537
|
+
if (extent === null || extent.minStart >= removed.range[0] && extent.maxEnd <= removed.range[1]) return false;
|
|
2538
|
+
}
|
|
2539
|
+
return true;
|
|
2540
|
+
}
|
|
2541
|
+
function referenceIndex(source, manager) {
|
|
2542
|
+
const cached = REFERENCE_INDEXES.get(source);
|
|
2543
|
+
if (cached?.manager === manager) return cached;
|
|
2544
|
+
const references = manager.scopes.flatMap((scope) => scope.references);
|
|
2545
|
+
references.sort((left, right) => left.identifier.range[0] - right.identifier.range[0]);
|
|
2546
|
+
const index = { manager, references, publicReads: /* @__PURE__ */ new Map() };
|
|
2547
|
+
REFERENCE_INDEXES.set(source, index);
|
|
2548
|
+
return index;
|
|
2549
|
+
}
|
|
2550
|
+
function* referencesWithin(references, removed) {
|
|
2551
|
+
let low = 0;
|
|
2552
|
+
let high = references.length;
|
|
2553
|
+
while (low < high) {
|
|
2554
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
2555
|
+
const reference = references[middle];
|
|
2556
|
+
if (reference === void 0 || reference.identifier.range[0] >= removed.range[0]) high = middle;
|
|
2557
|
+
else low = middle + 1;
|
|
2558
|
+
}
|
|
2559
|
+
for (let index = low; index < references.length; index++) {
|
|
2560
|
+
const reference = references[index];
|
|
2561
|
+
if (reference === void 0) continue;
|
|
2562
|
+
if (reference.identifier.range[0] > removed.range[1]) break;
|
|
2563
|
+
if (insideTypeRange(reference.identifier, removed)) yield reference;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function publicReadExtents(index, binding3) {
|
|
2567
|
+
const cached = index.publicReads.get(binding3);
|
|
2568
|
+
if (cached !== void 0) return cached;
|
|
2569
|
+
let type = null;
|
|
2570
|
+
let value = null;
|
|
2571
|
+
let both = null;
|
|
2572
|
+
for (const retained of binding3.references) {
|
|
2573
|
+
if (!retained.isRead() || !retained.isTypeReference && !retained.isValueReference || binding3.defs.some((definition) => insideTypeRange(retained.identifier, definition.node)) || !publicTypeUse(retained.identifier)) continue;
|
|
2574
|
+
if (retained.isTypeReference) type = extendExtent(type, retained.identifier);
|
|
2575
|
+
if (retained.isValueReference) value = extendExtent(value, retained.identifier);
|
|
2576
|
+
if (retained.isTypeReference && retained.isValueReference) both = extendExtent(both, retained.identifier);
|
|
2577
|
+
}
|
|
2578
|
+
const result = { type, value, both };
|
|
2579
|
+
index.publicReads.set(binding3, result);
|
|
2580
|
+
return result;
|
|
2581
|
+
}
|
|
2582
|
+
function extendExtent(extent, identifier) {
|
|
2583
|
+
return {
|
|
2584
|
+
minStart: Math.min(extent?.minStart ?? identifier.range[0], identifier.range[0]),
|
|
2585
|
+
maxEnd: Math.max(extent?.maxEnd ?? identifier.range[1], identifier.range[1])
|
|
2586
|
+
};
|
|
2535
2587
|
}
|
|
2536
2588
|
function publicTypeUse(identifier) {
|
|
2537
2589
|
let node = identifier;
|
|
@@ -3018,6 +3070,7 @@ var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":tru
|
|
|
3018
3070
|
var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
|
|
3019
3071
|
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);';
|
|
3020
3072
|
var NULLABLE_EXAMPLE2 = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
|
|
3073
|
+
var READ_NULLABLE_EXAMPLE = EXAMPLE2.replace('name: "Ada"', "name: null").replace("readonly name: string", "readonly name: string | null").replace("return user.name;", 'return user.name ?? "";');
|
|
3021
3074
|
var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
3022
3075
|
defaultLevel: "warning",
|
|
3023
3076
|
summary: "Prefer the proven read-only subset for a private consumer parameter.",
|
|
@@ -3028,7 +3081,7 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
3028
3081
|
limitations: [
|
|
3029
3082
|
"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.",
|
|
3030
3083
|
"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.",
|
|
3031
|
-
"Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported.
|
|
3084
|
+
"Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. A read field or an unread required field may additionally have exactly one such keyword and null or undefined in a direct two-part union; unread optional nullable fields and more complex unions remain excluded. Fields may be plain or readonly; the consumer body must still prove read-only usage. Every read field keeps its exact type, modifiers and optionality. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.",
|
|
3032
3085
|
"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."
|
|
3033
3086
|
],
|
|
3034
3087
|
examples: [
|
|
@@ -3037,7 +3090,9 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
3037
3090
|
{ 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;", "") }] },
|
|
3038
3091
|
{ 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 },
|
|
3039
3092
|
{ 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;", "") }] },
|
|
3040
|
-
{ 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 }
|
|
3093
|
+
{ id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
|
|
3094
|
+
{ id: "retained-nullable-read", scenarioId: "retained-nullable-field", title: "A nullable name remains while an unread email requirement is removed", outcome: "match", files: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE.replace("readonly email: string;", "") }] },
|
|
3095
|
+
{ id: "retained-nullable-all-used", scenarioId: "retained-nullable-field", title: "Both requirements remain when the nullable view reads email too", outcome: "no-match", files: [{ path: "src/case.ts", source: READ_NULLABLE_EXAMPLE.replace('return user.name ?? "";', "return user.name ?? user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
|
|
3041
3096
|
]
|
|
3042
3097
|
};
|
|
3043
3098
|
function inlineFields(node, reads) {
|
|
@@ -3045,7 +3100,7 @@ function inlineFields(node, reads) {
|
|
|
3045
3100
|
for (const member3 of node.members) {
|
|
3046
3101
|
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;
|
|
3047
3102
|
const key = member3.name.text;
|
|
3048
|
-
if (!primitiveKeyword(member3.type) && (member3.
|
|
3103
|
+
if (!primitiveKeyword(member3.type) && (!nullishPrimitiveUnion(member3.type) || member3.questionToken && !reads.has(key))) return null;
|
|
3049
3104
|
if (fields2.has(key)) return null;
|
|
3050
3105
|
fields2.set(key, !member3.questionToken);
|
|
3051
3106
|
}
|
|
@@ -4162,7 +4217,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
4162
4217
|
// package.json
|
|
4163
4218
|
var package_default = {
|
|
4164
4219
|
name: "@sarj/eslint-plugin",
|
|
4165
|
-
version: "16.38.
|
|
4220
|
+
version: "16.38.53",
|
|
4166
4221
|
packageManager: "npm@12.2.1",
|
|
4167
4222
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
4168
4223
|
type: "module",
|