@sarj/eslint-plugin 19.4.0 → 19.7.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 CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  publicDocumentation,
3
3
  ruleSpec,
4
4
  src_default
5
- } from "./chunk-6RX5RU75.js";
5
+ } from "./chunk-OX5DMK42.js";
6
6
 
7
7
  // src/docs/directive-disable-enable-pair.ts
8
8
  var DIRECTIVE_DISABLE_ENABLE_PAIR_DOCUMENTATION = {
@@ -4120,22 +4120,175 @@ var PREFER_AWAIT_IN_ASYNC_RETURN_DOCUMENTATION = {
4120
4120
  var PREFER_CODED_ERROR_RESULT_DOCUMENTATION = {
4121
4121
  summary: "Return a stable error code and typed context instead of only a rendered failure message.",
4122
4122
  defaultLevel: "warning",
4123
- rationale: "A message-only failure hides the identity needed for repair, conflict handling, localization and exhaustive dispatch. Preserve a finite error code and the minimum handling context; let the consumer format its message.",
4123
+ rationale: "A message-only or opaque failure hides the identity needed for repair, conflict handling, localization and exhaustive dispatch. Preserve a finite error code and the minimum handling context; let the consumer format its message. Reuse the native no-explicit-any rule, compiler narrowing and type-aware exhaustiveness checks.",
4124
4124
  remediation: "Use the existing Result or outcome union with a finite error code and variant-specific context. Preserve public behavior through a presentation adapter, and handle or propagate each relevant variant without parsing message text.",
4125
4125
  category: "correctness",
4126
4126
  autofix: "none",
4127
4127
  limitations: [
4128
- "Checks source-resolved local unions with required literal ok/success true and false branches, and named neverthrow Result/ResultAsync imports. Broad string error/message/reason payloads and open string code fields are review candidates. String literal error-code unions remain valid.",
4129
- "Local aliases, unmerged interfaces without inheritance and generic substitutions are bounded to 16 resolution levels, 64 union members and 256 traversal steps. Imported local declarations, namespace imports, conditional/intersection/mapped types and unresolved contracts abstain. Optional discriminants, independent success/error payloads and partial-data failures are excluded.",
4128
+ "Accepting an inferred producer does not prove a finite inferred contract: object payloads or errAsync can widen a code to string. Annotate the existing Result/ResultAsync return when necessary. Native assignability, narrowing and exhaustive-handler checks remain authoritative.",
4129
+ "Checks source-resolved local unions with required literal ok/success booleans, or status/state/kind/type/tag values ok/success versus err/error/failure/failed (case insensitive). Conflicting outcome tags abstain. A shared outcome tag does not also count as a domain error code. Named and namespace neverthrow Result/ResultAsync imports establish their generic error position.",
4130
+ "Presence-selected unions require an error property on every failure branch and the same single required non-control value property on every success branch. Optional error/value fields, multiple value keys, loading states and error branches carrying the success value abstain. Required errorMessage and Error-suffixed failure channels are also reviewed; multiple custom channels abstain and a primary error payload retains precedence.",
4131
+ "Broad string and unknown error channels, unshadowed native Error-family types and open string/unknown code fields receive review. Explicit Any stays with the existing no-explicit-any rule. Finite literal codes need no invented payload. Imported domain errors, validation-library errors, generic error parameters, ordinary formatters, legitimate absence and redacted failure states remain valid.",
4132
+ "Local aliases, unmerged interfaces without inheritance and generic substitutions are bounded to 16 resolution levels, 64 union members and 256 traversal steps. Imported local declarations, conditional/intersection/mapped types and unresolved contracts abstain. Optional discriminants, independent success/error payloads and partial-data failures are excluded.",
4130
4133
  "Required code/kind/type/tag fields of unresolved or imported type defer to the type checker; a finite code does not prove that its cases or payloads are useful. Generic error parameters are retained rather than guessed. Ordinary string formatters, absence, validation-library errors and redacted failure states without a string payload are not flagged.",
4131
- "A directly returned plain object with literal ok/success false and a literal string code/kind/type/tag also receives review when its message/reason is an interpolated template or proven string concatenation. Nested plain error objects and expression-bodied arrows are supported. Spreads, accessors, computed keys, helper formatting calls and nonliteral codes abstain. This proves formatting ownership, not which context the domain must expose; presentation adapters remain a reviewed exception.",
4132
- "This warning requests domain review: a final presentation boundary may intentionally accept every failure and render one message. No fix invents error codes, fields, handlers or changes serialization, exception unwinding, transactions, retries or logging."
4134
+ "A directly returned failure object, neverthrow err/errAsync call or new Err with an interpolated template or proven string concatenation receives review. Without a code, the diagnostic asks for a domain contract; with a literal code, it asks for consumer formatting. Literal-only compositions, including earlier stable local constants, remain valid codes. Nested plain error objects, expression-bodied arrows and one direct await are supported. Import identity and value-space shadowing are checked; arbitrary aliases, spreads, accessors, computed keys, helper formatting calls and nonliteral codes abstain. This proves formatting ownership, not which context the domain must expose; presentation adapters remain a reviewed exception.",
4135
+ "This warning requests domain review: an infrastructure adapter may intentionally retain unknown/Error, and a final presentation boundary may accept every failure and render one message. Neither proves a recoverable domain rejection. No fix invents error codes, fields, handlers or changes serialization, exception unwinding, transactions, retries or logging."
4133
4136
  ],
4134
4137
  references: [
4135
4138
  "https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions",
4136
4139
  "https://github.com/supermacro/neverthrow"
4137
4140
  ],
4138
4141
  examples: [
4142
+ {
4143
+ id: "presence-selected-message",
4144
+ scenarioId: "presence-selected-result",
4145
+ title: "An error-presence check still needs a useful error contract",
4146
+ outcome: "match",
4147
+ files: [
4148
+ {
4149
+ path: "app/result.ts",
4150
+ source: "export type Outcome<T> = { value: T } | { error: string; field?: 'name' };"
4151
+ }
4152
+ ],
4153
+ focusPath: "app/result.ts",
4154
+ expectedCount: 1,
4155
+ public: true
4156
+ },
4157
+ {
4158
+ id: "presence-selected-domain-error",
4159
+ scenarioId: "presence-selected-result",
4160
+ title: "Retain the existing presence check with a finite error union",
4161
+ outcome: "no-match",
4162
+ files: [
4163
+ {
4164
+ path: "app/result.ts",
4165
+ source: "type SaveError = { code: 'duplicate'; name: string } | { code: 'denied' }; export type Outcome<T> = { value: T } | { error: SaveError };"
4166
+ }
4167
+ ],
4168
+ focusPath: "app/result.ts",
4169
+ expectedCount: 0,
4170
+ public: true
4171
+ },
4172
+ {
4173
+ id: "string-outcome-tag",
4174
+ scenarioId: "string-outcome",
4175
+ title: "An outcome tag distinguishes success from failure, not the failure's cause",
4176
+ outcome: "match",
4177
+ files: [
4178
+ {
4179
+ path: "app/result.ts",
4180
+ source: "export type Outcome<T> = { kind: 'ok'; value: T } | { kind: 'error'; message: string };"
4181
+ }
4182
+ ],
4183
+ focusPath: "app/result.ts",
4184
+ expectedCount: 1,
4185
+ public: true
4186
+ },
4187
+ {
4188
+ id: "string-outcome-domain-code",
4189
+ scenarioId: "string-outcome",
4190
+ title: "Preserve the established string tag and add the actionable error",
4191
+ outcome: "no-match",
4192
+ files: [
4193
+ {
4194
+ path: "app/result.ts",
4195
+ source: "export type Outcome<T> = { kind: 'ok'; value: T } | { kind: 'error'; error: { code: 'duplicate'; name: string } };"
4196
+ }
4197
+ ],
4198
+ focusPath: "app/result.ts",
4199
+ expectedCount: 0,
4200
+ public: true
4201
+ },
4202
+ {
4203
+ id: "opaque-error-channel",
4204
+ scenarioId: "opaque-error",
4205
+ title: "Unknown preserves the need to narrow but hides the domain's cases",
4206
+ outcome: "match",
4207
+ files: [
4208
+ {
4209
+ path: "app/result.ts",
4210
+ source: "type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; export type Saved = Result<number, unknown>;"
4211
+ }
4212
+ ],
4213
+ focusPath: "app/result.ts",
4214
+ expectedCount: 1,
4215
+ public: true
4216
+ },
4217
+ {
4218
+ id: "specific-error-channel",
4219
+ scenarioId: "opaque-error",
4220
+ title: "Expose the domain's rejection case without inventing context",
4221
+ outcome: "no-match",
4222
+ files: [
4223
+ {
4224
+ path: "app/result.ts",
4225
+ source: "type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; export type Saved = Result<number, { code: 'unavailable' }>;"
4226
+ }
4227
+ ],
4228
+ focusPath: "app/result.ts",
4229
+ expectedCount: 0,
4230
+ public: true
4231
+ },
4232
+ {
4233
+ id: "named-error-carrier",
4234
+ scenarioId: "named-error-channel",
4235
+ title: "A failure-only Error-suffixed field can still erase its cases",
4236
+ outcome: "match",
4237
+ files: [
4238
+ {
4239
+ path: "app/result.ts",
4240
+ source: "export type Outcome<T> = { ok: true; value: T } | { ok: false; fallbackError: string };"
4241
+ }
4242
+ ],
4243
+ focusPath: "app/result.ts",
4244
+ expectedCount: 1,
4245
+ public: true
4246
+ },
4247
+ {
4248
+ id: "typed-named-error-carrier",
4249
+ scenarioId: "named-error-channel",
4250
+ title: "Preserve the existing field name with the required context",
4251
+ outcome: "no-match",
4252
+ files: [
4253
+ {
4254
+ path: "app/result.ts",
4255
+ source: "export type Outcome<T> = { ok: true; value: T } | { ok: false; fallbackError: { code: 'unsupported'; requestedKind: string } };"
4256
+ }
4257
+ ],
4258
+ focusPath: "app/result.ts",
4259
+ expectedCount: 0,
4260
+ public: true
4261
+ },
4262
+ {
4263
+ id: "library-error-constructor",
4264
+ scenarioId: "library-error-presentation",
4265
+ title: "A proven Result constructor does not make formatted text actionable",
4266
+ outcome: "match",
4267
+ files: [
4268
+ {
4269
+ path: "app/result.ts",
4270
+ source: "import { err } from 'neverthrow'; export function reject(name: string) { return err(`Missing ${name}`); }"
4271
+ }
4272
+ ],
4273
+ focusPath: "app/result.ts",
4274
+ expectedCount: 1,
4275
+ public: true
4276
+ },
4277
+ {
4278
+ id: "library-domain-constructor",
4279
+ scenarioId: "library-error-presentation",
4280
+ title: "Reuse the established Result library with a typed domain error",
4281
+ outcome: "no-match",
4282
+ files: [
4283
+ {
4284
+ path: "app/result.ts",
4285
+ source: "import { err, type Result } from 'neverthrow'; type Missing = { code: 'missing'; name: string }; export function reject(name: string): Result<never, Missing> { return err({ code: 'missing', name }); }"
4286
+ }
4287
+ ],
4288
+ focusPath: "app/result.ts",
4289
+ expectedCount: 0,
4290
+ public: true
4291
+ },
4139
4292
  {
4140
4293
  id: "message-only-result",
4141
4294
  title: "A failure erases identity into a message",
@@ -4194,6 +4347,66 @@ var PREFER_CODED_ERROR_RESULT_DOCUMENTATION = {
4194
4347
  expectedCount: 0,
4195
4348
  public: true
4196
4349
  },
4350
+ {
4351
+ id: "exclusive-message-outcome",
4352
+ scenarioId: "partial-data",
4353
+ title: "An exclusive failure still needs an actionable identity",
4354
+ outcome: "match",
4355
+ files: [
4356
+ {
4357
+ path: "app/result.ts",
4358
+ source: "export type Outcome<T> = { ok: true; data: T } | { ok: false; error: string };"
4359
+ }
4360
+ ],
4361
+ focusPath: "app/result.ts",
4362
+ expectedCount: 1,
4363
+ public: true
4364
+ },
4365
+ {
4366
+ id: "independent-partial-outcomes",
4367
+ scenarioId: "partial-data",
4368
+ title: "Partial data and its independent error remain separate outcomes",
4369
+ outcome: "no-match",
4370
+ files: [
4371
+ {
4372
+ path: "app/result.ts",
4373
+ source: "export type Partial<T> = { ok: true; data: T; error?: string } | { ok: false; data: T; error: string };"
4374
+ }
4375
+ ],
4376
+ focusPath: "app/result.ts",
4377
+ expectedCount: 0,
4378
+ public: true
4379
+ },
4380
+ {
4381
+ id: "exclusive-error-state",
4382
+ scenarioId: "absence-state",
4383
+ title: "Conventional success and failure tags establish one Result",
4384
+ outcome: "match",
4385
+ files: [
4386
+ {
4387
+ path: "app/result.ts",
4388
+ source: "export type Outcome<T> = { status: 'success'; value: T } | { status: 'failure'; reason: string };"
4389
+ }
4390
+ ],
4391
+ focusPath: "app/result.ts",
4392
+ expectedCount: 1,
4393
+ public: true
4394
+ },
4395
+ {
4396
+ id: "legitimate-unconfigured-state",
4397
+ scenarioId: "absence-state",
4398
+ title: "A loading report can preserve an unconfigured absence state",
4399
+ outcome: "no-match",
4400
+ files: [
4401
+ {
4402
+ path: "app/result.ts",
4403
+ source: "export type Source<T> = { status: 'loaded'; value: T } | { status: 'failed' | 'unconfigured'; reason: string };"
4404
+ }
4405
+ ],
4406
+ focusPath: "app/result.ts",
4407
+ expectedCount: 0,
4408
+ public: true
4409
+ },
4197
4410
  {
4198
4411
  id: "producer-renders-context",
4199
4412
  scenarioId: "consumer-formatting",
@@ -21504,8 +21504,30 @@ var NAME91 = "prefer-coded-error-result";
21504
21504
  var MAX_DEPTH = 16;
21505
21505
  var MAX_VARIANTS = 64;
21506
21506
  var MAX_TYPE_STEPS = 256;
21507
- var STATUS_NAMES = ["ok", "success"];
21507
+ var BOOLEAN_STATUS_NAMES = ["ok", "success"];
21508
+ var STRING_STATUS_NAMES = ["status", "state", "kind", "type", "tag"];
21509
+ var STATUS_NAMES = [
21510
+ ...BOOLEAN_STATUS_NAMES,
21511
+ ...STRING_STATUS_NAMES
21512
+ ];
21508
21513
  var CODE_NAMES = ["code", "kind", "type", "tag"];
21514
+ var SUCCESS_TAGS = /* @__PURE__ */ new Set(["ok", "success"]);
21515
+ var FAILURE_TAGS = /* @__PURE__ */ new Set([
21516
+ "err",
21517
+ "error",
21518
+ "failure",
21519
+ "failed"
21520
+ ]);
21521
+ var NATIVE_ERRORS = /* @__PURE__ */ new Set([
21522
+ "Error",
21523
+ "EvalError",
21524
+ "RangeError",
21525
+ "ReferenceError",
21526
+ "SyntaxError",
21527
+ "TypeError",
21528
+ "URIError",
21529
+ "AggregateError"
21530
+ ]);
21509
21531
  function view(node) {
21510
21532
  return { node, origin: node, bindings: /* @__PURE__ */ new Map() };
21511
21533
  }
@@ -21515,7 +21537,7 @@ function resolve2(value, source, depth = 0) {
21515
21537
  if (node.type !== "TSTypeReference") return value;
21516
21538
  if (node.typeName.type !== "Identifier") return null;
21517
21539
  const variable = typeBinding(source, node.typeName);
21518
- if (variable === null) return null;
21540
+ if (variable === null || variable.defs.length === 0) return value;
21519
21541
  const bound = value.bindings.get(variable);
21520
21542
  if (bound !== void 0) return resolve2(bound, source, depth + 1);
21521
21543
  const declaration = variable.defs.length === 1 ? variable.defs[0]?.node : void 0;
@@ -21577,30 +21599,40 @@ function propertyView(property, owner) {
21577
21599
  const node = property.typeAnnotation?.typeAnnotation;
21578
21600
  return node === void 0 ? null : { node, origin: node, bindings: owner.bindings };
21579
21601
  }
21580
- function booleanTag(property, owner, source) {
21602
+ function outcomeTag(property, owner, source, name) {
21581
21603
  if (property === void 0 || property.optional) return null;
21582
21604
  const type = propertyView(property, owner);
21583
21605
  const resolved = type === null ? null : resolve2(type, source);
21584
21606
  const node = resolved?.node;
21585
- return node?.type === "TSLiteralType" && node.literal.type === "Literal" && typeof node.literal.value === "boolean" ? node.literal.value : null;
21607
+ return node?.type === "TSLiteralType" && node.literal.type === "Literal" ? outcomeValue(name, node.literal.value) : null;
21586
21608
  }
21587
- function hasCode(fields2, owner, source) {
21609
+ function outcomeValue(name, value) {
21610
+ if (BOOLEAN_STATUS_NAMES.some((candidate2) => candidate2 === name))
21611
+ return typeof value === "boolean" ? value : null;
21612
+ if (!STRING_STATUS_NAMES.some((candidate2) => candidate2 === name) || typeof value !== "string")
21613
+ return null;
21614
+ const tag = value.toLowerCase();
21615
+ return SUCCESS_TAGS.has(tag) ? true : FAILURE_TAGS.has(tag) ? false : null;
21616
+ }
21617
+ function hasCode(fields2, owner, source, ignoredCodes = []) {
21588
21618
  return CODE_NAMES.some((name) => {
21619
+ if (ignoredCodes.includes(name)) return false;
21589
21620
  const property = fields2.get(name);
21590
21621
  if (property === void 0 || property.optional) return false;
21591
21622
  const type = propertyView(property, owner);
21592
- return type !== null && openString(type, source) === null;
21623
+ return type !== null && openScalar(type, source) === null;
21593
21624
  });
21594
21625
  }
21595
- function openString(value, source, depth = 0, budget = { remaining: MAX_TYPE_STEPS }) {
21626
+ function openScalar(value, source, depth = 0, budget = { remaining: MAX_TYPE_STEPS }) {
21596
21627
  if (depth >= MAX_DEPTH || --budget.remaining < 0) return null;
21597
21628
  const resolved = resolve2(value, source);
21598
21629
  if (resolved === null) return null;
21599
- if (resolved.node.type === "TSStringKeyword") return resolved.origin;
21630
+ if (resolved.node.type === "TSStringKeyword" || resolved.node.type === "TSUnknownKeyword")
21631
+ return resolved.origin;
21600
21632
  if (resolved.node.type !== "TSUnionType" || resolved.node.types.length > MAX_VARIANTS)
21601
21633
  return null;
21602
21634
  for (const member3 of resolved.node.types) {
21603
- const broad = openString(
21635
+ const broad = openScalar(
21604
21636
  { ...resolved, node: member3 },
21605
21637
  source,
21606
21638
  depth + 1,
@@ -21614,7 +21646,9 @@ function weakError(value, source, depth = 0, budget = { remaining: MAX_TYPE_STEP
21614
21646
  if (depth >= MAX_DEPTH || --budget.remaining < 0) return null;
21615
21647
  const resolved = resolve2(value, source);
21616
21648
  if (resolved === null) return null;
21617
- if (resolved.node.type === "TSStringKeyword") return resolved.origin;
21649
+ if (resolved.node.type === "TSStringKeyword" || resolved.node.type === "TSUnknownKeyword")
21650
+ return resolved.origin;
21651
+ if (nativeErrorType(resolved.node, source)) return resolved.origin;
21618
21652
  if (resolved.node.type === "TSUnionType") {
21619
21653
  if (resolved.node.types.length > MAX_VARIANTS) return null;
21620
21654
  for (const member3 of resolved.node.types) {
@@ -21632,22 +21666,44 @@ function weakError(value, source, depth = 0, budget = { remaining: MAX_TYPE_STEP
21632
21666
  if (fields2 === null || hasCode(fields2, resolved, source)) return null;
21633
21667
  return weakObject(fields2, resolved, source, depth, budget);
21634
21668
  }
21635
- function weakObject(fields2, owner, source, depth, budget) {
21636
- const code = openStringField(fields2, owner, source, CODE_NAMES, budget);
21669
+ function nativeErrorType(node, source) {
21670
+ if (node.type !== "TSTypeReference" || node.typeName.type !== "Identifier" || !NATIVE_ERRORS.has(node.typeName.name) || (node.typeArguments?.params.length ?? 0) !== 0)
21671
+ return false;
21672
+ const binding3 = typeBinding(source, node.typeName);
21673
+ return binding3 === null || binding3.defs.length === 0;
21674
+ }
21675
+ function weakObject(fields2, owner, source, depth, budget, ignoredCodes = []) {
21676
+ const code = openStringField(
21677
+ fields2,
21678
+ owner,
21679
+ source,
21680
+ CODE_NAMES.filter((name) => !ignoredCodes.includes(name)),
21681
+ budget
21682
+ );
21637
21683
  if (code !== null) return code;
21638
21684
  const error = fields2.get("error");
21639
21685
  if (error !== void 0) {
21640
21686
  const inner = propertyView(error, owner);
21641
21687
  return inner === null || error.optional ? null : weakError(inner, source, depth + 1, budget);
21642
21688
  }
21689
+ const carriers = [...fields2].filter(([name]) => errorCarrier(name));
21690
+ if (carriers.length > 1) return null;
21691
+ const carrier = carriers[0]?.[1];
21692
+ if (carrier !== void 0) {
21693
+ const inner = propertyView(carrier, owner);
21694
+ return inner === null || carrier.optional ? null : weakError(inner, source, depth + 1, budget);
21695
+ }
21643
21696
  return openStringField(fields2, owner, source, ["message", "reason"], budget);
21644
21697
  }
21698
+ function errorCarrier(name) {
21699
+ return name === "error" || name === "errorMessage" || name.endsWith("Error");
21700
+ }
21645
21701
  function openStringField(fields2, owner, source, names, budget) {
21646
21702
  for (const name of names) {
21647
21703
  const property = fields2.get(name);
21648
21704
  if (property === void 0 || property.optional) continue;
21649
21705
  const inner = propertyView(property, owner);
21650
- const broad = inner === null ? null : openString(inner, source, 0, budget);
21706
+ const broad = inner === null ? null : openScalar(inner, source, 0, budget);
21651
21707
  if (broad !== null) return broad;
21652
21708
  }
21653
21709
  return null;
@@ -21660,26 +21716,70 @@ function failureMessages(value, source) {
21660
21716
  fields: properties(member3)
21661
21717
  }));
21662
21718
  if (objects.some((object) => object.fields === null)) return [];
21719
+ let selected = null;
21720
+ const discriminators = [];
21663
21721
  for (const status of STATUS_NAMES) {
21664
- const tags = objects.map(
21665
- (object) => booleanTag(object.fields?.get(status), object.member, source)
21722
+ const tags2 = objects.map(
21723
+ (object) => outcomeTag(object.fields?.get(status), object.member, source, status)
21666
21724
  );
21667
- if (tags.some((tag) => tag === null) || !tags.includes(true) || !tags.includes(false))
21668
- continue;
21669
- if (objects.some(
21670
- (object, index) => tags[index] === true && ["error", "errors"].some((name) => object.fields?.has(name))
21671
- ))
21725
+ if (tags2.some((tag) => tag === null) || !tags2.includes(true) || !tags2.includes(false))
21672
21726
  continue;
21673
- return objects.flatMap((object, index) => {
21674
- if (tags[index] !== false || object.fields === null || hasCode(object.fields, object.member, source))
21675
- return [];
21676
- if (["data", "value", "result"].some((name) => object.fields?.has(name)))
21677
- return [];
21678
- const error = weakError(object.member, source);
21679
- return error === null ? [] : [error];
21680
- });
21727
+ if (selected !== null && tags2.some((tag, index) => tag !== selected?.[index]))
21728
+ return [];
21729
+ discriminators.push(status);
21730
+ if (selected === null) {
21731
+ selected = tags2;
21732
+ }
21681
21733
  }
21682
- return [];
21734
+ if (selected === null) return presenceFailures(objects, source);
21735
+ const tags = selected;
21736
+ if (objects.some(
21737
+ (object, index) => tags[index] === true && [...object.fields?.keys() ?? []].some(
21738
+ (name) => name === "errors" || errorCarrier(name)
21739
+ )
21740
+ ))
21741
+ return [];
21742
+ return objects.flatMap((object, index) => {
21743
+ if (tags[index] !== false || object.fields === null || hasCode(object.fields, object.member, source, discriminators) || ["data", "value", "result"].some((name) => object.fields?.has(name)))
21744
+ return [];
21745
+ const error = weakObject(
21746
+ object.fields,
21747
+ object.member,
21748
+ source,
21749
+ 0,
21750
+ { remaining: MAX_TYPE_STEPS },
21751
+ discriminators
21752
+ );
21753
+ return error === null ? [] : [error];
21754
+ });
21755
+ }
21756
+ function presenceFailures(objects, source) {
21757
+ const failed2 = objects.filter((object) => object.fields?.has("error"));
21758
+ const successful = objects.filter((object) => !object.fields?.has("error"));
21759
+ if (failed2.length === 0 || successful.length === 0) return [];
21760
+ let successKey = null;
21761
+ for (const object of successful) {
21762
+ if (object.fields === null || object.fields.size !== 1) return [];
21763
+ const property = [...object.fields][0];
21764
+ if (property === void 0 || property[1].optional || errorCarrier(property[0]) || property[0] === "errors" || STATUS_NAMES.includes(property[0]) || CODE_NAMES.some((name) => name === property[0]))
21765
+ return [];
21766
+ if (successKey !== null && successKey !== property[0]) return [];
21767
+ successKey = property[0];
21768
+ }
21769
+ if (successKey === null) return [];
21770
+ const key = successKey;
21771
+ if (failed2.some(
21772
+ (object) => object.fields?.get("error")?.optional || object.fields?.has(key)
21773
+ ))
21774
+ return [];
21775
+ return failed2.flatMap((object) => {
21776
+ if (object.fields === null || hasCode(object.fields, object.member, source))
21777
+ return [];
21778
+ const error = object.fields.get("error");
21779
+ const inner = error === void 0 ? null : propertyView(error, object.member);
21780
+ const weak = inner === null ? null : weakError(inner, source);
21781
+ return weak === null ? [] : [weak];
21782
+ });
21683
21783
  }
21684
21784
  function variants(value, source, depth = 0, budget = { remaining: MAX_TYPE_STEPS }) {
21685
21785
  if (--budget.remaining < 0) return null;
@@ -21701,18 +21801,37 @@ function variants(value, source, depth = 0, budget = { remaining: MAX_TYPE_STEPS
21701
21801
  return result;
21702
21802
  }
21703
21803
  function libraryError(reference, source) {
21704
- if (reference.typeName.type !== "Identifier" || reference.typeArguments?.params.length !== 2)
21804
+ if (reference.typeArguments?.params.length !== 2) return null;
21805
+ if (!["Result", "ResultAsync"].includes(
21806
+ libraryName(reference.typeName, source, true) ?? ""
21807
+ ))
21705
21808
  return null;
21706
- const variable = typeBinding(source, reference.typeName);
21809
+ const error = reference.typeArguments.params[1];
21810
+ return error === void 0 ? null : weakError(view(error), source);
21811
+ }
21812
+ function libraryName(node, source, typeSpace) {
21813
+ const reference = libraryReference(node);
21814
+ if (reference === null) return null;
21815
+ const { root, member: member3 } = reference;
21816
+ const variable = typeSpace ? typeBinding(source, root) : findVariable(source.getScope(root), root.name);
21707
21817
  const definition = variable?.defs.length === 1 ? variable.defs[0]?.node : void 0;
21708
- if (definition?.type !== "ImportSpecifier") return null;
21818
+ if (definition?.type !== "ImportSpecifier" && definition?.type !== "ImportNamespaceSpecifier")
21819
+ return null;
21820
+ if (variable === null || !stableValueBinding(variable)) return null;
21709
21821
  const declaration = parentOf(definition);
21710
21822
  if (declaration?.type !== "ImportDeclaration" || declaration.source.value !== "neverthrow")
21711
21823
  return null;
21712
- if (!["Result", "ResultAsync"].includes(importSpecifierName(definition) ?? ""))
21824
+ if (!typeSpace && (declaration.importKind === "type" || definition.type === "ImportSpecifier" && definition.importKind === "type"))
21713
21825
  return null;
21714
- const error = reference.typeArguments.params[1];
21715
- return error === void 0 ? null : weakError(view(error), source);
21826
+ return member3 === null && definition.type === "ImportSpecifier" ? importSpecifierName(definition) : member3 !== null && definition.type === "ImportNamespaceSpecifier" ? member3 : null;
21827
+ }
21828
+ function libraryReference(node) {
21829
+ if (node.type === "Identifier") return { root: node, member: null };
21830
+ if (node.type === "TSQualifiedName" && node.left.type === "Identifier" && node.right.type === "Identifier")
21831
+ return { root: node.left, member: node.right.name };
21832
+ if (node.type === "MemberExpression" && !node.computed && !node.optional && node.object.type === "Identifier" && node.property.type === "Identifier")
21833
+ return { root: node.object, member: node.property.name };
21834
+ return null;
21716
21835
  }
21717
21836
  function weakAnnotation(node, source, depth = 0, budget = { remaining: MAX_TYPE_STEPS }) {
21718
21837
  if (depth >= MAX_DEPTH || --budget.remaining < 0) return false;
@@ -21724,23 +21843,65 @@ function weakAnnotation(node, source, depth = 0, budget = { remaining: MAX_TYPE_
21724
21843
  const argument = node.typeArguments.params[0];
21725
21844
  return argument !== void 0 && weakAnnotation(argument, source, depth + 1, budget);
21726
21845
  }
21727
- function renderedFailure(node) {
21728
- const outer = objectFields(unwrapExpression(node));
21729
- if (outer === null || !STATUS_NAMES.some((name) => {
21846
+ function renderedFailure(node, source) {
21847
+ let value = unwrapExpression(node);
21848
+ if (value.type === "AwaitExpression")
21849
+ value = unwrapExpression(value.argument);
21850
+ if (value.type === "CallExpression" || value.type === "NewExpression") {
21851
+ if (value.type === "CallExpression" && value.optional || value.arguments.length !== 1)
21852
+ return null;
21853
+ const name = libraryName(value.callee, source, false);
21854
+ if (value.type === "NewExpression" ? name !== "Err" : name !== "err" && name !== "errAsync")
21855
+ return null;
21856
+ const argument = value.arguments[0];
21857
+ if (argument === void 0 || argument.type === "SpreadElement")
21858
+ return null;
21859
+ return renderedPayload(argument, source);
21860
+ }
21861
+ const outer = objectFields(value);
21862
+ if (outer === null) return null;
21863
+ const discriminators = STATUS_NAMES.filter((name) => {
21730
21864
  const status = outer.get(name);
21731
- return status?.type === "Literal" && status.value === false;
21732
- }))
21865
+ return status?.type === "Literal" && outcomeValue(name, status.value) === false;
21866
+ });
21867
+ if (discriminators.length === 0) return null;
21868
+ if (STATUS_NAMES.some((name) => {
21869
+ const status = outer.get(name);
21870
+ return status?.type === "Literal" && outcomeValue(name, status.value) === true;
21871
+ }) || ["data", "value", "result"].some((name) => outer.has(name)))
21733
21872
  return null;
21734
21873
  const error = outer.get("error");
21735
- const fields2 = error === void 0 ? outer : objectFields(unwrapExpression(error));
21736
- if (fields2 === null || !CODE_NAMES.some((name) => {
21874
+ return renderedPayload(
21875
+ error ?? value,
21876
+ source,
21877
+ error === void 0 ? discriminators : []
21878
+ );
21879
+ }
21880
+ function renderedPayload(node, source, ignoredCodes = []) {
21881
+ if (renderedString(node, source))
21882
+ return { node, messageId: "preferCodedErrorResult" };
21883
+ const fields2 = objectFields(unwrapExpression(node));
21884
+ if (fields2 === null) return null;
21885
+ const codes = CODE_NAMES.filter(
21886
+ (name) => !ignoredCodes.includes(name) && fields2.has(name)
21887
+ );
21888
+ const coded = codes.some((name) => {
21737
21889
  const code = fields2.get(name);
21738
21890
  return code?.type === "Literal" && typeof code.value === "string";
21739
- }))
21740
- return null;
21741
- for (const name of ["message", "reason"]) {
21742
- const value = fields2.get(name);
21743
- if (value !== void 0 && renderedString(value)) return value;
21891
+ });
21892
+ if (codes.length !== 0 && !coded) return null;
21893
+ const primary = fields2.get("error");
21894
+ const carriers = [...fields2].filter(([name]) => errorCarrier(name));
21895
+ if (primary === void 0 && carriers.length > 1) return null;
21896
+ const carrier = primary ?? carriers[0]?.[1];
21897
+ if (!coded && carrier !== void 0)
21898
+ return renderedString(carrier, source) ? { node: carrier, messageId: "preferCodedErrorResult" } : null;
21899
+ for (const [name, value] of fields2) {
21900
+ if ((name === "message" || name === "reason" || errorCarrier(name)) && renderedString(value, source))
21901
+ return {
21902
+ node: value,
21903
+ messageId: coded ? "formatAtConsumer" : "preferCodedErrorResult"
21904
+ };
21744
21905
  }
21745
21906
  return null;
21746
21907
  }
@@ -21756,14 +21917,36 @@ function objectFields(node) {
21756
21917
  }
21757
21918
  return fields2;
21758
21919
  }
21759
- function renderedString(node, depth = 0) {
21760
- const value = unwrapExpression(node);
21761
- if (depth >= MAX_DEPTH) return false;
21762
- if (value.type === "TemplateLiteral") return value.expressions.length > 0;
21763
- if (value.type !== "BinaryExpression" || value.operator !== "+") return false;
21764
- return [value.left, value.right].some(
21765
- (part) => part.type === "Literal" && typeof part.value === "string" || renderedString(part, depth + 1)
21766
- );
21920
+ function renderedString(node, source) {
21921
+ let remaining = MAX_TYPE_STEPS;
21922
+ function literalComposition(input, depth) {
21923
+ if (depth >= MAX_DEPTH || --remaining < 0) return null;
21924
+ const value = unwrapExpression(input);
21925
+ if (value.type === "Identifier") {
21926
+ const initializer = stableConstInitializer(source, value);
21927
+ return initializer === null ? false : literalComposition(initializer.value, depth + 1);
21928
+ }
21929
+ if (value.type === "Literal")
21930
+ return value.value === null || ["string", "number", "boolean", "bigint"].includes(typeof value.value);
21931
+ const children = value.type === "TemplateLiteral" ? value.expressions : value.type === "BinaryExpression" && value.operator === "+" ? [value.left, value.right] : null;
21932
+ if (children === null) return false;
21933
+ for (const child of children) {
21934
+ const constant = literalComposition(child, depth + 1);
21935
+ if (constant !== true) return constant;
21936
+ }
21937
+ return true;
21938
+ }
21939
+ function rendered(input, depth) {
21940
+ if (depth >= MAX_DEPTH || --remaining < 0) return false;
21941
+ const value = unwrapExpression(input);
21942
+ if (value.type === "TemplateLiteral") return value.expressions.length > 0;
21943
+ if (value.type !== "BinaryExpression" || value.operator !== "+")
21944
+ return false;
21945
+ return [value.left, value.right].some(
21946
+ (part) => part.type === "Literal" && typeof part.value === "string" || rendered(part, depth + 1)
21947
+ );
21948
+ }
21949
+ return literalComposition(node, 0) === false && rendered(node, 0);
21767
21950
  }
21768
21951
  var prefer_coded_error_result_default = defineRule({
21769
21952
  meta: {
@@ -21774,7 +21957,7 @@ var prefer_coded_error_result_default = defineRule({
21774
21957
  },
21775
21958
  schema: [],
21776
21959
  messages: {
21777
- preferCodedErrorResult: "This Result failure carries only a rendered string or an open string code. Return a finite error code with the typed context needed by the caller, and format the message at the consumer. Keep an intentional presentation boundary explicit.",
21960
+ preferCodedErrorResult: "This Result failure hides its identity in a broad string, unknown, or generic Error channel. Expose expected failures as finite domain error codes with the typed context needed by the caller, and format the message at the consumer. Review intentional infrastructure and presentation boundaries explicitly.",
21778
21961
  formatAtConsumer: "This coded failure formats context into a message at the producer. Return the code and typed context, and let the consumer format the message. Preserve an intentional presentation boundary explicitly."
21779
21962
  }
21780
21963
  },
@@ -21794,13 +21977,13 @@ var prefer_coded_error_result_default = defineRule({
21794
21977
  }
21795
21978
  function checkProducer(owner, value) {
21796
21979
  if (owner === null) return;
21797
- const rendered = renderedFailure(value);
21798
- if (rendered === null || reported.has(rendered)) return;
21980
+ const rendered = renderedFailure(value, context.sourceCode);
21981
+ if (rendered === null || reported.has(rendered.node)) return;
21799
21982
  const contract = owner.returnType?.typeAnnotation;
21800
21983
  if (contract !== void 0 && weakAnnotation(contract, context.sourceCode))
21801
21984
  return;
21802
- reported.add(rendered);
21803
- context.report({ node: rendered, messageId: "formatAtConsumer" });
21985
+ reported.add(rendered.node);
21986
+ context.report(rendered);
21804
21987
  }
21805
21988
  return {
21806
21989
  before() {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  src_default
3
- } from "./chunk-6RX5RU75.js";
3
+ } from "./chunk-OX5DMK42.js";
4
4
  export {
5
5
  src_default as default
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "19.4.0",
3
+ "version": "19.7.0",
4
4
  "packageManager": "npm@12.2.1",
5
5
  "description": "Custom Oxlint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",