@sarj/eslint-plugin 19.17.0 → 19.18.0
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/catalog.js +7 -3
- package/dist/{chunk-4USNCXB3.js → chunk-E2O4LN7G.js} +112 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/catalog.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
publicDocumentation,
|
|
3
3
|
ruleSpec,
|
|
4
4
|
src_default
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-E2O4LN7G.js";
|
|
6
6
|
|
|
7
7
|
// src/docs/directive-disable-enable-pair.ts
|
|
8
8
|
var DIRECTIVE_DISABLE_ENABLE_PAIR_DOCUMENTATION = {
|
|
@@ -5118,11 +5118,13 @@ var PREFER_DISCRIMINATED_UNION_DOCUMENTATION = {
|
|
|
5118
5118
|
summary: "Preserve producer-proven tag/payload and Boolean state relationships in return types.",
|
|
5119
5119
|
defaultLevel: "warning",
|
|
5120
5120
|
rationale: "A flat object with optional branch data loses the relationship between a tag and its payload. Callers cannot narrow to the required fields, and the type permits incomplete or contradictory combinations.",
|
|
5121
|
-
remediation: "If the variants are mutually exclusive, represent each with a literal tag and its required payload.
|
|
5121
|
+
remediation: "If the variants are mutually exclusive, represent each with a literal tag and its required payload. For predicates over one current state, consider returning that authoritative state instead of storing multiple derived flags. Preserve every valid state, independent dimension, shared field, payload value type and nullability. For a producer returning a subset of a broader wire contract, give the producer a precise domain return type after validation.",
|
|
5122
5122
|
category: "correctness",
|
|
5123
5123
|
autofix: "safe",
|
|
5124
5124
|
limitations: [
|
|
5125
|
-
"Names, Boolean counts and optional-field counts never establish a finding. A producer's literal writes establish either tag-specific payload presence or a pair of required Boolean fields with two or three distinct combinations, with both fields varying. Boolean members may use boolean, true | false or one unmerged nongeneric local alias of either. All four combinations, constant dimensions
|
|
5125
|
+
"Names, Boolean counts and optional-field counts never establish a finding. A producer's literal writes establish either tag-specific payload presence or a pair of required Boolean fields with two or three distinct combinations, with both fields varying. Boolean members may use boolean, true | false or one unmerged nongeneric local alias of either. All four combinations, constant dimensions and optional/unknown dimensions are excluded. Expression-valued flags require the separate typed evidence described below. At most eight Boolean fields are compared. Only one relationship is reported per contract; existing literal/payload evidence takes precedence. Independently nullable payloads do not erase Boolean evidence.",
|
|
5126
|
+
"With type services enabled, a synchronous nongeneric producer with exactly one own return, a final direct object return and a supported flat local contract also exposes strict equality predicates over the same earlier local const binding. Native types at both reads must be finite domains of at most sixteen string/number literals containing both distinct singleton comparison values. Literal constants and direct named-import/local enum members are supported; enum declarations and members are resolved by symbol identity and raw runtime literal values, including renamed imports and reexports. At most 128 candidate pairs per file are analyzed; exceeding the bound abstains from all typed findings in that file. No automatic fix is offered for these projections.",
|
|
5127
|
+
"The const snapshot may be destructured or copied from another primitive binding, passed to callbacks, or read by closures without losing its value. Mutable bindings, parameters and module captures, property reads/getters, open/nullable/object/symbol domains, async producers, multiple own returns, generic/inherited/imported contracts and dynamic eval abstain. Nested callback returns do not belong to the producer. Enum namespace writes, aliases, escapes, computed accesses, calls and merged declarations in the analyzed file abstain. Cross-file enum declarations can supply type/constant identity; external mutation, dishonest declarations/assertions and runtime monkeypatching are not modeled. The analysis proves a relationship under those type and mutation assumptions, not domain intent or a whole-program ownership guarantee.",
|
|
5126
5128
|
"Producer analysis requires a finite literal tag, every declared tag syntactically witnessed, exhaustive simple return paths without literal constant conditions, plain object returns, and consistent optional-field presence for each tag. It resolves only unmerged local aliases/interfaces without inheritance or generics, one local alias of a literal tag domain, and the built-in Promise around async returns. Callable or opaque own then properties abstain for async producers because Promise assimilation can replace the fulfilled value. Inherited then methods and external prototype mutation are not modeled. This is syntax-only evidence of field presence; explicit undefined, void and null payloads do not establish payload requirements, while expression value types are not inferred. Type-only assertion/satisfies/non-null wrappers, literal computed string keys and a sole-use const immediately before its return are recognized. Const alias chains, observation, mutation and dynamic eval abstain. Dynamic returns, spreads, dynamic computed keys, accessors, loops and try statements are deliberately skipped.",
|
|
5127
5129
|
"A switch producer is supported only for a non-overloaded function declaration when its sole body statement switches on an unchanged direct string or number parameter, has unique same-kind literal labels and a default, and every nonempty case body is a sole object return or throw, optionally inside sole blocks. Empty labels may share the next terminating body; nested conditions, conditional returns, statement prefixes, breaks, other fallthrough, captured/closed inputs, dynamic eval and switches under preceding control flow abstain. Broad parameter domains establish input witnesses for each label and the default; this does not infer branch-specific runtime effects or expression value types. Path visits, depth and returned objects are bounded conservatively.",
|
|
5128
5130
|
"These are refactoring advisories, not proven defects or proof that other writers cannot produce more states. Historical observations, partial results, unknown states and independently completing operations may need the broader contract. Review all readers and writers before migrating a shared contract; preserve every valid combination. A union of existing literal flag pairs is the smallest type-only replacement; a named disposition is a reviewed domain migration.",
|
|
@@ -5138,6 +5140,8 @@ var PREFER_DISCRIMINATED_UNION_DOCUMENTATION = {
|
|
|
5138
5140
|
{ id: "explicit-produced-variants", scenarioId: "produced-variants", title: "Give each produced variant its payload", outcome: "no-match", files: [{ path: "src/job.ts", source: 'function job(done: boolean): { phase: "queued"; ticket: string } | { phase: "complete" } { return done ? { phase: "complete" } : { phase: "queued", ticket: "t" }; }' }], focusPath: "src/job.ts", expectedCount: 0, public: true },
|
|
5139
5141
|
{ id: "flat-switch-producer", scenarioId: "switch-producer", title: "A complete dispatcher preserves each returned variant", outcome: "match", files: [{ path: "src/job.ts", source: 'function job(command: string): { phase: "queued" | "complete"; ticket?: string } { switch (command) { case "start": return { phase: "queued", ticket: "t" }; default: return { phase: "complete" }; } }' }], focusPath: "src/job.ts", expectedCount: 1, public: true },
|
|
5140
5142
|
{ id: "explicit-switch-variants", scenarioId: "switch-producer", title: "An explicit dispatcher result supports payload narrowing", outcome: "no-match", files: [{ path: "src/job.ts", source: 'function job(command: string): { phase: "queued"; ticket: string } | { phase: "complete" } { switch (command) { case "start": return { phase: "queued", ticket: "t" }; default: return { phase: "complete" }; } }' }], focusPath: "src/job.ts", expectedCount: 0, public: true },
|
|
5143
|
+
{ id: "immutable-state-projections", scenarioId: "state-projection", title: "Two predicates over one immutable phase cannot both be true", outcome: "match", files: [{ path: "src/projection.ts", source: 'type Phase = "ready" | "waiting" | "unknown"; declare function snapshot(): Phase; function state(): { ready: boolean; waiting: boolean } { const phase = snapshot(); return { ready: phase === "ready", waiting: phase === "waiting" }; }' }], focusPath: "src/projection.ts", expectedCount: 1, public: true },
|
|
5144
|
+
{ id: "authoritative-state", scenarioId: "state-projection", title: "Return the phase while preserving the independent timeout dimension", outcome: "no-match", files: [{ path: "src/projection.ts", source: 'type Phase = "ready" | "waiting" | "reconnecting" | "unknown"; declare function snapshot(): Phase; function state(timeout: boolean): { phase: Phase; timeout: boolean } { const phase = snapshot(); return { phase, timeout }; }' }], focusPath: "src/projection.ts", expectedCount: 0, public: true },
|
|
5141
5145
|
{
|
|
5142
5146
|
id: "safe-autofix",
|
|
5143
5147
|
title: "Preserve an unused private producer's exact existing variants",
|
|
@@ -23411,6 +23411,94 @@ function literalTag(node) {
|
|
|
23411
23411
|
return typeof node.value === "string" || typeof node.value === "number" || typeof node.value === "boolean" ? node.value : void 0;
|
|
23412
23412
|
}
|
|
23413
23413
|
|
|
23414
|
+
// src/rules/_discriminated-union-projections.ts
|
|
23415
|
+
function projectedBooleanPairs(owner, returned2, source) {
|
|
23416
|
+
if (owner.async || owner.generator || isPresent(owner.typeParameters) || owner.body?.type !== "BlockStatement" || parentOf(returned2) !== owner.body || owner.body.body.at(-1) !== returned2 || returned2.argument?.type !== "ObjectExpression") return [];
|
|
23417
|
+
const contract = returnContract(owner, source);
|
|
23418
|
+
const shape = contract === void 0 ? void 0 : objectContract(contract.members);
|
|
23419
|
+
if (shape === void 0 || contract === void 0) return [];
|
|
23420
|
+
const properties2 = returnedProperties([returned2.argument], shape)?.[0];
|
|
23421
|
+
if (properties2 === void 0) return [];
|
|
23422
|
+
const fields2 = [...shape.members].filter(([, member3]) => !member3.optional && booleanType(member3.typeAnnotation?.typeAnnotation, source));
|
|
23423
|
+
if (fields2.length > 8) return [];
|
|
23424
|
+
const comparisons = [];
|
|
23425
|
+
for (const [field] of fields2) {
|
|
23426
|
+
const value = properties2.get(field);
|
|
23427
|
+
if (value?.type !== "BinaryExpression" || value.operator !== "===" || value.left.type !== "Identifier" || !constantSyntax(value.right, source)) continue;
|
|
23428
|
+
const binding3 = localSnapshot(value.left, owner, source);
|
|
23429
|
+
if (binding3 !== null) comparisons.push({ field, binding: binding3, left: value.left, right: value.right });
|
|
23430
|
+
}
|
|
23431
|
+
return comparisons.flatMap((left, index) => comparisons.slice(index + 1).filter((right) => right.binding === left.binding).map((right) => ({ reportNode: contract.reportNode, left, right })));
|
|
23432
|
+
}
|
|
23433
|
+
function localSnapshot(identifier, owner, source) {
|
|
23434
|
+
const variable = findVariable(source.getScope(identifier), identifier.name);
|
|
23435
|
+
const declaration = variable?.defs[0]?.node;
|
|
23436
|
+
if (variable === null || variable.defs.length !== 1 || declaration?.type !== "VariableDeclarator" || !isConstDeclaration(parentOf(declaration)) || !isPresent(declaration.init) || enclosingFunction(declaration) !== owner || declaration.range[1] >= identifier.range[0] || !stableValueBinding(variable)) return null;
|
|
23437
|
+
return variable;
|
|
23438
|
+
}
|
|
23439
|
+
function constantSyntax(node, source) {
|
|
23440
|
+
if (node.type === "Literal") return typeof node.value === "string" || typeof node.value === "number" && Number.isFinite(node.value);
|
|
23441
|
+
if (node.type !== "MemberExpression" || node.computed || node.optional || node.object.type !== "Identifier" || node.property.type !== "Identifier") return false;
|
|
23442
|
+
const variable = findVariable(source.getScope(node.object), node.object.name);
|
|
23443
|
+
const definition = variable?.defs[0]?.node;
|
|
23444
|
+
if (variable === null || variable.defs.length !== 1 || definition?.type !== "TSEnumDeclaration" && definition?.type !== "ImportSpecifier" || !stableValueBinding(variable)) return false;
|
|
23445
|
+
return variable.references.every((reference) => {
|
|
23446
|
+
if (typeOnlyReference(reference)) return true;
|
|
23447
|
+
const member3 = parentOf(reference.identifier);
|
|
23448
|
+
if (member3?.type !== "MemberExpression" || member3.object !== reference.identifier || member3.computed || member3.optional || writtenMember(member3)) return false;
|
|
23449
|
+
const use = parentOf(member3);
|
|
23450
|
+
return !((use?.type === "CallExpression" || use?.type === "NewExpression") && use.callee === member3);
|
|
23451
|
+
});
|
|
23452
|
+
}
|
|
23453
|
+
function enumConstant(node, native) {
|
|
23454
|
+
if (node.type !== "MemberExpression") return false;
|
|
23455
|
+
const root = native.node(node.object);
|
|
23456
|
+
const member3 = native.node(node.property);
|
|
23457
|
+
let symbol = root === void 0 ? void 0 : native.checker.getSymbolAtLocation(root);
|
|
23458
|
+
if (symbol === void 0 || member3 === void 0) return false;
|
|
23459
|
+
if ((symbol.flags & native.sync.SymbolFlags.Alias) !== 0) symbol = native.checker.getAliasedSymbol(symbol);
|
|
23460
|
+
const declarations = symbol.declarations;
|
|
23461
|
+
if (declarations.length !== 1 || declarations[0]?.kind !== native.ast.SyntaxKind.EnumDeclaration) return false;
|
|
23462
|
+
const selected = native.checker.getSymbolAtLocation(member3)?.declarations;
|
|
23463
|
+
return selected?.length === 1 && selected[0]?.kind === native.ast.SyntaxKind.EnumMember;
|
|
23464
|
+
}
|
|
23465
|
+
function projectedBooleanEvidence(file, candidates2) {
|
|
23466
|
+
const nodes = candidates2.flatMap(({ left, right }) => [left.left, right.left, left.right, right.right]);
|
|
23467
|
+
const types = file.typesOf(nodes);
|
|
23468
|
+
const unions = types.flatMap((type) => type !== void 0 && type.isUnionType() ? [type] : []);
|
|
23469
|
+
const lists = file.batch(unions.map((type) => type.getTypes.gen()));
|
|
23470
|
+
let index = 0;
|
|
23471
|
+
const domains = types.map((type) => type === void 0 ? void 0 : primitiveValues(type.isUnionType() ? lists[index++] ?? [] : [type], file));
|
|
23472
|
+
const native = nativeTypes(file);
|
|
23473
|
+
const constants = /* @__PURE__ */ new Map();
|
|
23474
|
+
function constant(node, value) {
|
|
23475
|
+
if (node.type === "Literal") return node.value === value;
|
|
23476
|
+
let valid = constants.get(node);
|
|
23477
|
+
if (valid === void 0) {
|
|
23478
|
+
valid = enumConstant(node, native);
|
|
23479
|
+
constants.set(node, valid);
|
|
23480
|
+
}
|
|
23481
|
+
return valid;
|
|
23482
|
+
}
|
|
23483
|
+
return candidates2.map(({ left, right }, ordinal) => {
|
|
23484
|
+
const [a, b, x, y] = domains.slice(ordinal * 4, ordinal * 4 + 4);
|
|
23485
|
+
const first = x?.[0];
|
|
23486
|
+
const second = y?.[0];
|
|
23487
|
+
return a !== void 0 && b !== void 0 && x?.length === 1 && y?.length === 1 && first !== void 0 && second !== void 0 && first !== second && constant(left.right, first) && constant(right.right, second) && a.includes(first) && a.includes(second) && b.includes(first) && b.includes(second);
|
|
23488
|
+
});
|
|
23489
|
+
}
|
|
23490
|
+
function primitiveValues(types, file) {
|
|
23491
|
+
if (types.length < 1 || types.length > 16) return void 0;
|
|
23492
|
+
const values = [];
|
|
23493
|
+
for (const type of types) {
|
|
23494
|
+
if ((type.flags & (file.flags.StringLiteral | file.flags.NumberLiteral)) === 0) return void 0;
|
|
23495
|
+
const value = type.value;
|
|
23496
|
+
if (typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) return void 0;
|
|
23497
|
+
values.push(value);
|
|
23498
|
+
}
|
|
23499
|
+
return values;
|
|
23500
|
+
}
|
|
23501
|
+
|
|
23414
23502
|
// src/rules/prefer-discriminated-union.ts
|
|
23415
23503
|
var NAME99 = "prefer-discriminated-union";
|
|
23416
23504
|
var prefer_discriminated_union_default = defineRule({
|
|
@@ -23424,12 +23512,14 @@ var prefer_discriminated_union_default = defineRule({
|
|
|
23424
23512
|
schema: [],
|
|
23425
23513
|
messages: {
|
|
23426
23514
|
preferProducedVariants: "This producer returns tag-specific payloads for `{{tag}}`, but its flat return contract makes them optional. If these variants define the domain, use a discriminated union with each variant's required payload, or give this producer a precise return type separate from the broader contract.",
|
|
23427
|
-
preferProducedState: "This producer returns a restricted set of literal combinations for `{{fields}}`, while its Boolean fields permit all four. Review a union preserving every returned combination, or a typed disposition if these are domain phases. Other writers and readers of a shared contract require review."
|
|
23515
|
+
preferProducedState: "This producer returns a restricted set of literal combinations for `{{fields}}`, while its Boolean fields permit all four. Review a union preserving every returned combination, or a typed disposition if these are domain phases. Other writers and readers of a shared contract require review.",
|
|
23516
|
+
preferProjectedState: "This producer compares the same immutable primitive snapshot with distinct state values for `{{fields}}`, but its Boolean return contract also permits true/true. Review returning the authoritative state or preserving every produced combination in a union. Keep independent dimensions and unknown states; other writers and readers of a shared contract require review."
|
|
23428
23517
|
}
|
|
23429
23518
|
},
|
|
23430
23519
|
createOnce(context) {
|
|
23431
23520
|
const reportedContracts = /* @__PURE__ */ new Set();
|
|
23432
23521
|
const producers = [];
|
|
23522
|
+
const returns = /* @__PURE__ */ new Map();
|
|
23433
23523
|
function rememberProducer(node) {
|
|
23434
23524
|
producers.push(node);
|
|
23435
23525
|
}
|
|
@@ -23449,6 +23539,7 @@ var prefer_discriminated_union_default = defineRule({
|
|
|
23449
23539
|
before() {
|
|
23450
23540
|
reportedContracts.clear();
|
|
23451
23541
|
producers.length = 0;
|
|
23542
|
+
returns.clear();
|
|
23452
23543
|
const file = { filename: context.filename, cwd: context.cwd };
|
|
23453
23544
|
if (isTestFile(file) || isGeneratedFile(file, context.sourceCode.text)) return false;
|
|
23454
23545
|
return void 0;
|
|
@@ -23456,8 +23547,28 @@ var prefer_discriminated_union_default = defineRule({
|
|
|
23456
23547
|
ArrowFunctionExpression: rememberProducer,
|
|
23457
23548
|
FunctionDeclaration: rememberProducer,
|
|
23458
23549
|
FunctionExpression: rememberProducer,
|
|
23550
|
+
ReturnStatement(node) {
|
|
23551
|
+
const owner = enclosingFunction(node);
|
|
23552
|
+
if (owner !== null) returns.set(owner, returns.has(owner) ? null : node);
|
|
23553
|
+
},
|
|
23459
23554
|
"Program:exit"() {
|
|
23460
23555
|
for (const producer of producers) checkProducer(producer);
|
|
23556
|
+
if (hasIdentifierNamed(context.sourceCode, "eval")) return;
|
|
23557
|
+
const candidates2 = [];
|
|
23558
|
+
for (const producer of producers) {
|
|
23559
|
+
const returned2 = returns.get(producer);
|
|
23560
|
+
if (!isPresent(returned2)) continue;
|
|
23561
|
+
candidates2.push(...projectedBooleanPairs(producer, returned2, context.sourceCode).filter(({ reportNode }) => !reportedContracts.has(reportNode)));
|
|
23562
|
+
if (candidates2.length > 128) return;
|
|
23563
|
+
}
|
|
23564
|
+
if (candidates2.length === 0) return;
|
|
23565
|
+
const evidence = tsrsServices(context)?.run((file) => projectedBooleanEvidence(file, candidates2));
|
|
23566
|
+
if (!isPresent(evidence)) return;
|
|
23567
|
+
candidates2.forEach(({ reportNode, left, right }, index) => {
|
|
23568
|
+
if (evidence[index] !== true || reportedContracts.has(reportNode)) return;
|
|
23569
|
+
reportedContracts.add(reportNode);
|
|
23570
|
+
context.report({ node: reportNode, messageId: "preferProjectedState", data: { fields: [left.field, right.field].join("`, `") } });
|
|
23571
|
+
});
|
|
23461
23572
|
}
|
|
23462
23573
|
};
|
|
23463
23574
|
}
|
package/dist/index.js
CHANGED