@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 +47 -18
- package/dist/index.js +47 -18
- package/dist/rule-examples.cjs +33 -0
- package/dist/rule-examples.js +33 -0
- package/package.json +1 -1
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.
|
|
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: "
|
|
3909
|
+
title: "Load data while rendering on the server",
|
|
3910
3910
|
outcome: "no-match",
|
|
3911
|
-
files: [
|
|
3912
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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',
|
|
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
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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: "
|
|
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.
|
|
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: "
|
|
3870
|
+
title: "Load data while rendering on the server",
|
|
3871
3871
|
outcome: "no-match",
|
|
3872
|
-
files: [
|
|
3873
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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',
|
|
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
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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: "
|
|
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
|
};
|
package/dist/rule-examples.cjs
CHANGED
|
@@ -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
|
package/dist/rule-examples.js
CHANGED
|
@@ -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
|
};
|