@sarj/eslint-plugin 16.15.0 → 16.15.1

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 CHANGED
@@ -52,7 +52,7 @@ module.exports = __toCommonJS(src_exports);
52
52
  // package.json
53
53
  var package_default = {
54
54
  name: "@sarj/eslint-plugin",
55
- version: "16.15.0",
55
+ version: "16.15.1",
56
56
  packageManager: "npm@12.2.0",
57
57
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
58
58
  type: "module",
@@ -3906,10 +3906,13 @@ var NO_CLIENT_SIDE_DATA_FETCHING_DOCUMENTATION = {
3906
3906
  examples: [
3907
3907
  {
3908
3908
  id: "effect-without-fetch",
3909
- title: "An effect performs no data request",
3909
+ title: "Load data while rendering on the server",
3910
3910
  outcome: "no-match",
3911
- files: [{ path: "src/users.tsx", source: "import { useEffect } from 'react'; useEffect(() => { console.log('mounted'); }, []);" }],
3912
- focusPath: "src/users.tsx",
3911
+ files: [
3912
+ { path: "app/users/page.tsx", source: "import { loadUsers } from './users-client';\nexport default async function Users() { const users = await loadUsers(); return <pre>{JSON.stringify(users)}</pre>; }" },
3913
+ { path: "app/users/users-client.ts", source: "export async function loadUsers(): Promise<unknown> { const response = await fetch('https://api.example.com/users'); return response.json(); }" }
3914
+ ],
3915
+ focusPath: "app/users/page.tsx",
3913
3916
  expectedCount: 0,
3914
3917
  public: true
3915
3918
  },
@@ -3917,7 +3920,7 @@ var NO_CLIENT_SIDE_DATA_FETCHING_DOCUMENTATION = {
3917
3920
  id: "fetch-inside-effect",
3918
3921
  title: "An effect starts a data request",
3919
3922
  outcome: "match",
3920
- files: [{ path: "src/users.tsx", source: "useEffect(() => { fetch('/api/users'); }, []);" }],
3923
+ files: [{ path: "src/users.tsx", source: "'use client'; import { useEffect } from 'react';\nexport function Users() { useEffect(() => { fetch('/api/users'); }, []); return null; }" }],
3921
3924
  focusPath: "src/users.tsx",
3922
3925
  expectedCount: 1,
3923
3926
  public: true
@@ -6058,11 +6061,11 @@ var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
6058
6061
  examples: [
6059
6062
  {
6060
6063
  id: "focused-try-block",
6061
- title: "Three selected operations stay within the default threshold",
6064
+ title: "Keep only operations with the same recovery policy in the try block",
6062
6065
  outcome: "no-match",
6063
6066
  files: [{
6064
6067
  path: "src/load.ts",
6065
- source: "function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } finish(); }"
6068
+ source: "function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } const d = four(); finish(); }"
6066
6069
  }],
6067
6070
  focusPath: "src/load.ts",
6068
6071
  expectedCount: 0,
@@ -7850,7 +7853,7 @@ var NO_JSON_STRINGIFY_OBJECT_EQUALITY_DOCUMENTATION = {
7850
7853
  id: "domain-comparator",
7851
7854
  title: "Use an explicit structural comparator",
7852
7855
  outcome: "no-match",
7853
- files: [{ path: "src/compare.ts", source: "const same = sameRecord(actual, expected);" }],
7856
+ files: [{ path: "src/compare.ts", source: "type RecordState = { id: string; state: string };\nfunction sameRecord(actual: RecordState, expected: RecordState): boolean { return actual.id === expected.id && actual.state === expected.state; }\nconst same = sameRecord({ id: '1', state: 'ready' }, { state: 'ready', id: '1' });" }],
7854
7857
  focusPath: "src/compare.ts",
7855
7858
  expectedCount: 0,
7856
7859
  public: true
@@ -10578,8 +10581,9 @@ var NO_RESTRICTED_LIBRARY_LOAD_DOCUMENTATION = {
10578
10581
  category: "architecture",
10579
10582
  limitations: ["Only literal dynamic imports, unshadowed CommonJS loads/resolution calls, and runtime TypeScript import-equals declarations are checked; erased type imports are excluded. A configured restriction list is required."],
10580
10583
  examples: [
10581
- { id: "static-import", title: "Static imports remain the static-import rule's responsibility", outcome: "no-match", files: [{ path: "src/client.ts", source: "import axios from 'axios';" }], focusPath: "src/client.ts", expectedCount: 0, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] },
10582
- { id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] }
10584
+ { id: "configured-replacement", title: "Use the configured library replacement", outcome: "no-match", files: [{ path: "src/client.ts", source: "import ky from 'ky'; const client = ky;" }], focusPath: "src/client.ts", expectedCount: 0, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] },
10585
+ { id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] },
10586
+ { id: "static-import-owned-upstream", title: "Leave static restrictions to the owning import rule", outcome: "no-match", files: [{ path: "src/client.ts", source: "import axios from 'axios';" }], focusPath: "src/client.ts", expectedCount: 0, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] }
10583
10587
  ]
10584
10588
  };
10585
10589
  function staticModule(node) {
@@ -14084,7 +14088,7 @@ var NO_DECLARATION_COMMENT_WALL_DOCUMENTATION = {
14084
14088
  id: "uncommented-members",
14085
14089
  title: "Let clear member names stand alone",
14086
14090
  outcome: "no-match",
14087
- files: [{ path: "src/status.ts", source: "enum Status { Pending = 'pending', Done = 'done', Failed = 'failed' }" }],
14091
+ files: [{ path: "src/status.ts", source: "enum Status { Pending = 'pending', Finished = 'finished', Failed = 'failed' }" }],
14088
14092
  focusPath: "src/status.ts",
14089
14093
  expectedCount: 0,
14090
14094
  public: true
@@ -14375,7 +14379,7 @@ var NO_TYPE_MEMBER_COMMENT_WALL_DOCUMENTATION = {
14375
14379
  id: "uncommented-members",
14376
14380
  title: "Let clear member names and types stand alone",
14377
14381
  outcome: "no-match",
14378
- files: [{ path: "src/credentials.ts", source: "interface Credentials { host: string; port: number; username: string; }" }],
14382
+ files: [{ path: "src/credentials.ts", source: "interface Credentials { host?: string; port?: number; username?: string; password?: string; }" }],
14379
14383
  focusPath: "src/credentials.ts",
14380
14384
  expectedCount: 0,
14381
14385
  public: true
@@ -14964,12 +14968,37 @@ var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
14964
14968
  },
14965
14969
  {
14966
14970
  id: "zod-native-enum",
14967
- title: "Do not wrap a TypeScript enum",
14971
+ title: "Declare literal values instead of an enum-like object",
14968
14972
  outcome: "match",
14969
14973
  files: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.nativeEnum({ Active: "active", Inactive: "inactive" });' }],
14970
14974
  focusPath: "src/status.ts",
14971
14975
  expectedCount: 1,
14972
14976
  public: true
14977
+ },
14978
+ {
14979
+ id: "imported-typescript-enum",
14980
+ scenarioId: "imported-enum",
14981
+ title: "Do not hide an imported TypeScript enum behind z.enum",
14982
+ outcome: "match",
14983
+ files: [
14984
+ { path: "src/status-values.ts", source: "export enum Status { Active = 'active', Inactive = 'inactive' }" },
14985
+ { path: "src/status.ts", source: "import { z } from 'zod'; import { Status } from './status-values.js';\nexport const StatusSchema = z.enum(Status);" }
14986
+ ],
14987
+ installedDependencies: [{ module: "zod", version: "4.6.7" }],
14988
+ focusPath: "src/status.ts",
14989
+ expectedCount: 1,
14990
+ public: true
14991
+ },
14992
+ {
14993
+ id: "schema-owned-string-values",
14994
+ scenarioId: "imported-enum",
14995
+ title: "Let the schema own its literal value domain",
14996
+ outcome: "no-match",
14997
+ files: [{ path: "src/status.ts", source: "import { z } from 'zod';\nexport const StatusSchema = z.enum(['active', 'inactive']);" }],
14998
+ installedDependencies: [{ module: "zod", version: "4.6.7" }],
14999
+ focusPath: "src/status.ts",
15000
+ expectedCount: 0,
15001
+ public: true
14973
15002
  }
14974
15003
  ]
14975
15004
  };
@@ -17882,7 +17911,7 @@ var PREFER_MODULE_LEVEL_REFINED_SCHEMA_DOCUMENTATION = {
17882
17911
  outcome: "no-match",
17883
17912
  files: [{
17884
17913
  path: "src/options.ts",
17885
- source: "import { z } from 'zod'; const BatchSizeSchema = z.number().int().min(1).max(1000); export function parse(value: unknown) { return BatchSizeSchema.parse(value); }"
17914
+ source: "import { z } from 'zod'; const NameSchema = z.string().trim().min(1).max(128); export function parse(value: unknown) { return NameSchema.parse(value); }"
17886
17915
  }],
17887
17916
  focusPath: "src/options.ts",
17888
17917
  expectedCount: 0,
@@ -19517,7 +19546,7 @@ var PREFER_SCHEMA_FOR_API_PAYLOAD_DOCUMENTATION = {
19517
19546
  "Test fixtures, generated clients and recognized local-file reads are excluded. This is bounded local analysis, not a general control-flow or mutation proof."
19518
19547
  ],
19519
19548
  examples: [
19520
- { id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
19549
+ { id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "import { z } from 'zod';\nconst UserSchema = z.object({ id: z.string() });\nasync function load(response: Response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], installedDependencies: [{ module: "zod", version: "4.6.7" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
19521
19550
  { id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
19522
19551
  ]
19523
19552
  };
@@ -20193,8 +20222,8 @@ var PREFER_SWITCH_FOR_REPEATED_EQUALITY_DOCUMENTATION = {
20193
20222
  "The rule deliberately ignores compound predicates, loose equality, ranges, and chains that compare different discriminants."
20194
20223
  ],
20195
20224
  examples: [
20196
- { id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "switch (kind) { case 'a': return a(); case 'b': return b(); case 'c': return c(); default: return fallback(); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
20197
- { id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "function render(kind: string) { if (kind === 'a') return a(); else if (kind === 'b') return b(); else if (kind === 'c') return c(); }" }], focusPath: "src/render.ts", expectedCount: 1, public: true },
20225
+ { id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "function render(kind: string) { switch (kind) { case 'a': return 'alpha'; case 'b': return 'beta'; case 'c': return 'gamma'; default: return 'unknown'; } }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
20226
+ { id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "function render(kind: string) { if (kind === 'a') return 'alpha'; else if (kind === 'b') return 'beta'; else if (kind === 'c') return 'gamma'; return 'unknown'; }" }], focusPath: "src/render.ts", expectedCount: 1, public: true },
20198
20227
  { id: "escaped-middle", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){if(kind==='a')return 1;else if(\\u006bind==='b')return 2;else if(kind==='c')return 3;return 0;}
20199
20228
  console.log(JSON.stringify(["a","b","c","d"].map(probe)));` }], focusPath: "src/dispatch.ts", expectedCount: 1, public: true },
20200
20229
  { id: "different-discriminants", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "no-match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){const other="other";if(kind==='a')return 1;else if(other==='b')return 2;else if(kind==='c')return 3;return 0;}
@@ -22732,7 +22761,7 @@ var REQUIRE_ASSERT_NEVER_DOCUMENTATION = {
22732
22761
  category: "correctness",
22733
22762
  limitations: ["Requires type information and singleton case values covering the current union. The helper must accept never to provide a compile-time check; its spelling alone is not a proof of that contract. Review the desired runtime behavior for unexpected external values."],
22734
22763
  examples: [
22735
- { id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: assertNever(kind); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
22764
+ { id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "function assertNever(value: never): never { throw new Error(`Unexpected kind: ${String(value)}`); }\ndeclare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: assertNever(kind); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
22736
22765
  { id: "empty-default", title: "Do not leave an exhaustive default empty", outcome: "match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: }" }], focusPath: "src/render.ts", expectedCount: 1, public: true }
22737
22766
  ]
22738
22767
  };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "@sarj/eslint-plugin",
4
- version: "16.15.0",
4
+ version: "16.15.1",
5
5
  packageManager: "npm@12.2.0",
6
6
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
7
7
  type: "module",
@@ -3867,10 +3867,13 @@ var NO_CLIENT_SIDE_DATA_FETCHING_DOCUMENTATION = {
3867
3867
  examples: [
3868
3868
  {
3869
3869
  id: "effect-without-fetch",
3870
- title: "An effect performs no data request",
3870
+ title: "Load data while rendering on the server",
3871
3871
  outcome: "no-match",
3872
- files: [{ path: "src/users.tsx", source: "import { useEffect } from 'react'; useEffect(() => { console.log('mounted'); }, []);" }],
3873
- focusPath: "src/users.tsx",
3872
+ files: [
3873
+ { path: "app/users/page.tsx", source: "import { loadUsers } from './users-client';\nexport default async function Users() { const users = await loadUsers(); return <pre>{JSON.stringify(users)}</pre>; }" },
3874
+ { path: "app/users/users-client.ts", source: "export async function loadUsers(): Promise<unknown> { const response = await fetch('https://api.example.com/users'); return response.json(); }" }
3875
+ ],
3876
+ focusPath: "app/users/page.tsx",
3874
3877
  expectedCount: 0,
3875
3878
  public: true
3876
3879
  },
@@ -3878,7 +3881,7 @@ var NO_CLIENT_SIDE_DATA_FETCHING_DOCUMENTATION = {
3878
3881
  id: "fetch-inside-effect",
3879
3882
  title: "An effect starts a data request",
3880
3883
  outcome: "match",
3881
- files: [{ path: "src/users.tsx", source: "useEffect(() => { fetch('/api/users'); }, []);" }],
3884
+ files: [{ path: "src/users.tsx", source: "'use client'; import { useEffect } from 'react';\nexport function Users() { useEffect(() => { fetch('/api/users'); }, []); return null; }" }],
3882
3885
  focusPath: "src/users.tsx",
3883
3886
  expectedCount: 1,
3884
3887
  public: true
@@ -6019,11 +6022,11 @@ var NO_FAT_TRY_BLOCKS_DOCUMENTATION = {
6019
6022
  examples: [
6020
6023
  {
6021
6024
  id: "focused-try-block",
6022
- title: "Three selected operations stay within the default threshold",
6025
+ title: "Keep only operations with the same recovery policy in the try block",
6023
6026
  outcome: "no-match",
6024
6027
  files: [{
6025
6028
  path: "src/load.ts",
6026
- source: "function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } finish(); }"
6029
+ source: "function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } const d = four(); finish(); }"
6027
6030
  }],
6028
6031
  focusPath: "src/load.ts",
6029
6032
  expectedCount: 0,
@@ -7818,7 +7821,7 @@ var NO_JSON_STRINGIFY_OBJECT_EQUALITY_DOCUMENTATION = {
7818
7821
  id: "domain-comparator",
7819
7822
  title: "Use an explicit structural comparator",
7820
7823
  outcome: "no-match",
7821
- files: [{ path: "src/compare.ts", source: "const same = sameRecord(actual, expected);" }],
7824
+ files: [{ path: "src/compare.ts", source: "type RecordState = { id: string; state: string };\nfunction sameRecord(actual: RecordState, expected: RecordState): boolean { return actual.id === expected.id && actual.state === expected.state; }\nconst same = sameRecord({ id: '1', state: 'ready' }, { state: 'ready', id: '1' });" }],
7822
7825
  focusPath: "src/compare.ts",
7823
7826
  expectedCount: 0,
7824
7827
  public: true
@@ -10559,8 +10562,9 @@ var NO_RESTRICTED_LIBRARY_LOAD_DOCUMENTATION = {
10559
10562
  category: "architecture",
10560
10563
  limitations: ["Only literal dynamic imports, unshadowed CommonJS loads/resolution calls, and runtime TypeScript import-equals declarations are checked; erased type imports are excluded. A configured restriction list is required."],
10561
10564
  examples: [
10562
- { id: "static-import", title: "Static imports remain the static-import rule's responsibility", outcome: "no-match", files: [{ path: "src/client.ts", source: "import axios from 'axios';" }], focusPath: "src/client.ts", expectedCount: 0, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] },
10563
- { id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] }
10565
+ { id: "configured-replacement", title: "Use the configured library replacement", outcome: "no-match", files: [{ path: "src/client.ts", source: "import ky from 'ky'; const client = ky;" }], focusPath: "src/client.ts", expectedCount: 0, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] },
10566
+ { id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] },
10567
+ { id: "static-import-owned-upstream", title: "Leave static restrictions to the owning import rule", outcome: "no-match", files: [{ path: "src/client.ts", source: "import axios from 'axios';" }], focusPath: "src/client.ts", expectedCount: 0, ruleOptions: [{ libraries: [{ id: "LIB101", module: "axios", replacement: "Ky" }] }] }
10564
10568
  ]
10565
10569
  };
10566
10570
  function staticModule(node) {
@@ -14065,7 +14069,7 @@ var NO_DECLARATION_COMMENT_WALL_DOCUMENTATION = {
14065
14069
  id: "uncommented-members",
14066
14070
  title: "Let clear member names stand alone",
14067
14071
  outcome: "no-match",
14068
- files: [{ path: "src/status.ts", source: "enum Status { Pending = 'pending', Done = 'done', Failed = 'failed' }" }],
14072
+ files: [{ path: "src/status.ts", source: "enum Status { Pending = 'pending', Finished = 'finished', Failed = 'failed' }" }],
14069
14073
  focusPath: "src/status.ts",
14070
14074
  expectedCount: 0,
14071
14075
  public: true
@@ -14356,7 +14360,7 @@ var NO_TYPE_MEMBER_COMMENT_WALL_DOCUMENTATION = {
14356
14360
  id: "uncommented-members",
14357
14361
  title: "Let clear member names and types stand alone",
14358
14362
  outcome: "no-match",
14359
- files: [{ path: "src/credentials.ts", source: "interface Credentials { host: string; port: number; username: string; }" }],
14363
+ files: [{ path: "src/credentials.ts", source: "interface Credentials { host?: string; port?: number; username?: string; password?: string; }" }],
14360
14364
  focusPath: "src/credentials.ts",
14361
14365
  expectedCount: 0,
14362
14366
  public: true
@@ -14952,12 +14956,37 @@ var NO_ZOD_NATIVE_ENUM_DOCUMENTATION = {
14952
14956
  },
14953
14957
  {
14954
14958
  id: "zod-native-enum",
14955
- title: "Do not wrap a TypeScript enum",
14959
+ title: "Declare literal values instead of an enum-like object",
14956
14960
  outcome: "match",
14957
14961
  files: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.nativeEnum({ Active: "active", Inactive: "inactive" });' }],
14958
14962
  focusPath: "src/status.ts",
14959
14963
  expectedCount: 1,
14960
14964
  public: true
14965
+ },
14966
+ {
14967
+ id: "imported-typescript-enum",
14968
+ scenarioId: "imported-enum",
14969
+ title: "Do not hide an imported TypeScript enum behind z.enum",
14970
+ outcome: "match",
14971
+ files: [
14972
+ { path: "src/status-values.ts", source: "export enum Status { Active = 'active', Inactive = 'inactive' }" },
14973
+ { path: "src/status.ts", source: "import { z } from 'zod'; import { Status } from './status-values.js';\nexport const StatusSchema = z.enum(Status);" }
14974
+ ],
14975
+ installedDependencies: [{ module: "zod", version: "4.6.7" }],
14976
+ focusPath: "src/status.ts",
14977
+ expectedCount: 1,
14978
+ public: true
14979
+ },
14980
+ {
14981
+ id: "schema-owned-string-values",
14982
+ scenarioId: "imported-enum",
14983
+ title: "Let the schema own its literal value domain",
14984
+ outcome: "no-match",
14985
+ files: [{ path: "src/status.ts", source: "import { z } from 'zod';\nexport const StatusSchema = z.enum(['active', 'inactive']);" }],
14986
+ installedDependencies: [{ module: "zod", version: "4.6.7" }],
14987
+ focusPath: "src/status.ts",
14988
+ expectedCount: 0,
14989
+ public: true
14961
14990
  }
14962
14991
  ]
14963
14992
  };
@@ -17879,7 +17908,7 @@ var PREFER_MODULE_LEVEL_REFINED_SCHEMA_DOCUMENTATION = {
17879
17908
  outcome: "no-match",
17880
17909
  files: [{
17881
17910
  path: "src/options.ts",
17882
- source: "import { z } from 'zod'; const BatchSizeSchema = z.number().int().min(1).max(1000); export function parse(value: unknown) { return BatchSizeSchema.parse(value); }"
17911
+ source: "import { z } from 'zod'; const NameSchema = z.string().trim().min(1).max(128); export function parse(value: unknown) { return NameSchema.parse(value); }"
17883
17912
  }],
17884
17913
  focusPath: "src/options.ts",
17885
17914
  expectedCount: 0,
@@ -19525,7 +19554,7 @@ var PREFER_SCHEMA_FOR_API_PAYLOAD_DOCUMENTATION = {
19525
19554
  "Test fixtures, generated clients and recognized local-file reads are excluded. This is bounded local analysis, not a general control-flow or mutation proof."
19526
19555
  ],
19527
19556
  examples: [
19528
- { id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
19557
+ { id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "import { z } from 'zod';\nconst UserSchema = z.object({ id: z.string() });\nasync function load(response: Response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], installedDependencies: [{ module: "zod", version: "4.6.7" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
19529
19558
  { id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response: Response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
19530
19559
  ]
19531
19560
  };
@@ -20201,8 +20230,8 @@ var PREFER_SWITCH_FOR_REPEATED_EQUALITY_DOCUMENTATION = {
20201
20230
  "The rule deliberately ignores compound predicates, loose equality, ranges, and chains that compare different discriminants."
20202
20231
  ],
20203
20232
  examples: [
20204
- { id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "switch (kind) { case 'a': return a(); case 'b': return b(); case 'c': return c(); default: return fallback(); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
20205
- { id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "function render(kind: string) { if (kind === 'a') return a(); else if (kind === 'b') return b(); else if (kind === 'c') return c(); }" }], focusPath: "src/render.ts", expectedCount: 1, public: true },
20233
+ { id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "function render(kind: string) { switch (kind) { case 'a': return 'alpha'; case 'b': return 'beta'; case 'c': return 'gamma'; default: return 'unknown'; } }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
20234
+ { id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "function render(kind: string) { if (kind === 'a') return 'alpha'; else if (kind === 'b') return 'beta'; else if (kind === 'c') return 'gamma'; return 'unknown'; }" }], focusPath: "src/render.ts", expectedCount: 1, public: true },
20206
20235
  { id: "escaped-middle", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){if(kind==='a')return 1;else if(\\u006bind==='b')return 2;else if(kind==='c')return 3;return 0;}
20207
20236
  console.log(JSON.stringify(["a","b","c","d"].map(probe)));` }], focusPath: "src/dispatch.ts", expectedCount: 1, public: true },
20208
20237
  { id: "different-discriminants", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "no-match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){const other="other";if(kind==='a')return 1;else if(other==='b')return 2;else if(kind==='c')return 3;return 0;}
@@ -22747,7 +22776,7 @@ var REQUIRE_ASSERT_NEVER_DOCUMENTATION = {
22747
22776
  category: "correctness",
22748
22777
  limitations: ["Requires type information and singleton case values covering the current union. The helper must accept never to provide a compile-time check; its spelling alone is not a proof of that contract. Review the desired runtime behavior for unexpected external values."],
22749
22778
  examples: [
22750
- { id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: assertNever(kind); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
22779
+ { id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "function assertNever(value: never): never { throw new Error(`Unexpected kind: ${String(value)}`); }\ndeclare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: assertNever(kind); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
22751
22780
  { id: "empty-default", title: "Do not leave an exhaustive default empty", outcome: "match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: }" }], focusPath: "src/render.ts", expectedCount: 1, public: true }
22752
22781
  ]
22753
22782
  };
@@ -48,6 +48,7 @@ async function verifyRuleExamples(rule, setup = {}) {
48
48
  try {
49
49
  for (const file of example.files) {
50
50
  const path = fixturePath(root, file.path);
51
+ validateFixtureSyntax(example.id, file, path);
51
52
  await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
52
53
  await (0, import_promises.writeFile)(path, file.source);
53
54
  }
@@ -92,6 +93,14 @@ function fixturePath(root, path) {
92
93
  if (!resolved.startsWith(root + import_node_path.sep)) throw new Error(`unsafe example path: ${path}`);
93
94
  return resolved;
94
95
  }
96
+ function validateFixtureSyntax(exampleId, file, path) {
97
+ if (!/\.[cm]?[jt]sx?$/u.test(file.path)) return;
98
+ try {
99
+ parser.parse(file.source, { filePath: path });
100
+ } catch (error) {
101
+ throw new Error(`${exampleId}: invalid fixture ${file.path}`, { cause: error });
102
+ }
103
+ }
95
104
  async function verifyExample(linter, config, example, root, ruleId) {
96
105
  const focus = example.files.find((file) => file.path === example.focusPath);
97
106
  if (focus === void 0) throw new Error(`${example.id}: focus file is missing`);
@@ -100,6 +109,19 @@ async function verifyExample(linter, config, example, root, ruleId) {
100
109
  if (messages.some((message) => message.fatal || message.ruleId !== ruleId)) {
101
110
  throw new Error(`${example.id}: invalid fixture: ${messages.map((message) => message.message).join("; ")}`);
102
111
  }
112
+ const lines = linter.getSourceCode().lines;
113
+ if (messages.some((message) => !physicalDiagnosticLocation(message, lines))) {
114
+ throw new Error(`${example.id}: invalid diagnostic location`);
115
+ }
116
+ const locations = messages.map((message) => JSON.stringify([
117
+ message.ruleId,
118
+ message.messageId ?? message.message,
119
+ message.line,
120
+ message.column,
121
+ message.endLine,
122
+ message.endColumn
123
+ ]));
124
+ if (new Set(locations).size !== locations.length) throw new Error(`${example.id}: duplicate diagnostic`);
103
125
  if (messages.length !== example.expectedCount) {
104
126
  throw new Error(`${example.id}: expected ${example.expectedCount} findings, received ${messages.length}`);
105
127
  }
@@ -116,6 +138,17 @@ async function verifyExample(linter, config, example, root, ruleId) {
116
138
  }
117
139
  }
118
140
  }
141
+ function physicalDiagnosticLocation(message, lines) {
142
+ if (!physicalPosition(lines, message.line, message.column)) return false;
143
+ if (message.endLine === void 0 && message.endColumn === void 0) return true;
144
+ if (!physicalPosition(lines, message.endLine, message.endColumn)) return false;
145
+ return message.endLine !== void 0 && message.endColumn !== void 0 && (message.endLine > message.line || message.endLine === message.line && message.endColumn >= message.column);
146
+ }
147
+ function physicalPosition(lines, line, column) {
148
+ if (line === void 0 || column === void 0 || !Number.isInteger(line) || !Number.isInteger(column)) return false;
149
+ const text = lines[line - 1];
150
+ return text !== void 0 && column >= 1 && column <= text.length + 1;
151
+ }
119
152
  // Annotate the CommonJS export names for ESM import in node:
120
153
  0 && (module.exports = {
121
154
  verifyRuleExamples
@@ -14,6 +14,7 @@ async function verifyRuleExamples(rule, setup = {}) {
14
14
  try {
15
15
  for (const file of example.files) {
16
16
  const path = fixturePath(root, file.path);
17
+ validateFixtureSyntax(example.id, file, path);
17
18
  await mkdir(dirname(path), { recursive: true });
18
19
  await writeFile(path, file.source);
19
20
  }
@@ -58,6 +59,14 @@ function fixturePath(root, path) {
58
59
  if (!resolved.startsWith(root + sep)) throw new Error(`unsafe example path: ${path}`);
59
60
  return resolved;
60
61
  }
62
+ function validateFixtureSyntax(exampleId, file, path) {
63
+ if (!/\.[cm]?[jt]sx?$/u.test(file.path)) return;
64
+ try {
65
+ parser.parse(file.source, { filePath: path });
66
+ } catch (error) {
67
+ throw new Error(`${exampleId}: invalid fixture ${file.path}`, { cause: error });
68
+ }
69
+ }
61
70
  async function verifyExample(linter, config, example, root, ruleId) {
62
71
  const focus = example.files.find((file) => file.path === example.focusPath);
63
72
  if (focus === void 0) throw new Error(`${example.id}: focus file is missing`);
@@ -66,6 +75,19 @@ async function verifyExample(linter, config, example, root, ruleId) {
66
75
  if (messages.some((message) => message.fatal || message.ruleId !== ruleId)) {
67
76
  throw new Error(`${example.id}: invalid fixture: ${messages.map((message) => message.message).join("; ")}`);
68
77
  }
78
+ const lines = linter.getSourceCode().lines;
79
+ if (messages.some((message) => !physicalDiagnosticLocation(message, lines))) {
80
+ throw new Error(`${example.id}: invalid diagnostic location`);
81
+ }
82
+ const locations = messages.map((message) => JSON.stringify([
83
+ message.ruleId,
84
+ message.messageId ?? message.message,
85
+ message.line,
86
+ message.column,
87
+ message.endLine,
88
+ message.endColumn
89
+ ]));
90
+ if (new Set(locations).size !== locations.length) throw new Error(`${example.id}: duplicate diagnostic`);
69
91
  if (messages.length !== example.expectedCount) {
70
92
  throw new Error(`${example.id}: expected ${example.expectedCount} findings, received ${messages.length}`);
71
93
  }
@@ -82,6 +104,17 @@ async function verifyExample(linter, config, example, root, ruleId) {
82
104
  }
83
105
  }
84
106
  }
107
+ function physicalDiagnosticLocation(message, lines) {
108
+ if (!physicalPosition(lines, message.line, message.column)) return false;
109
+ if (message.endLine === void 0 && message.endColumn === void 0) return true;
110
+ if (!physicalPosition(lines, message.endLine, message.endColumn)) return false;
111
+ return message.endLine !== void 0 && message.endColumn !== void 0 && (message.endLine > message.line || message.endLine === message.line && message.endColumn >= message.column);
112
+ }
113
+ function physicalPosition(lines, line, column) {
114
+ if (line === void 0 || column === void 0 || !Number.isInteger(line) || !Number.isInteger(column)) return false;
115
+ const text = lines[line - 1];
116
+ return text !== void 0 && column >= 1 && column <= text.length + 1;
117
+ }
85
118
  export {
86
119
  verifyRuleExamples
87
120
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.15.0",
3
+ "version": "16.15.1",
4
4
  "packageManager": "npm@12.2.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",