@sarj/eslint-plugin 0.2.1 → 1.0.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/README.md +132 -3
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +98 -0
- package/dist/rules/enforce-file-structure.d.ts +4 -0
- package/dist/rules/enforce-file-structure.d.ts.map +1 -0
- package/dist/rules/enforce-file-structure.js +90 -0
- package/dist/rules/no-enum.d.ts +8 -0
- package/dist/rules/no-enum.d.ts.map +1 -0
- package/dist/rules/no-enum.js +29 -0
- package/dist/rules/no-raw-env.d.ts +8 -0
- package/dist/rules/no-raw-env.d.ts.map +1 -0
- package/dist/rules/no-raw-env.js +33 -0
- package/dist/rules/prefer-shadcn.d.ts +4 -0
- package/dist/rules/prefer-shadcn.d.ts.map +1 -0
- package/dist/rules/prefer-shadcn.js +50 -0
- package/dist/rules/require-assert-never.d.ts +8 -0
- package/dist/rules/require-assert-never.d.ts.map +1 -0
- package/dist/rules/require-assert-never.js +53 -0
- package/dist/rules/require-zod-form-validation.d.ts +8 -0
- package/dist/rules/require-zod-form-validation.d.ts.map +1 -0
- package/dist/rules/require-zod-form-validation.js +59 -0
- package/dist/rules/zod-naming-convention.d.ts +8 -0
- package/dist/rules/zod-naming-convention.d.ts.map +1 -0
- package/dist/rules/zod-naming-convention.js +53 -0
- package/package.json +33 -11
- package/.claude/settings.local.json +0 -9
- package/lib/index.js +0 -19
- package/lib/rules/enforce-file-structure.js +0 -85
- package/lib/rules/require-assert-never.js +0 -46
- package/lib/rules/require-zod-form-validation.js +0 -47
- package/lib/rules/zod-naming-convention.js +0 -44
package/README.md
CHANGED
|
@@ -1,9 +1,138 @@
|
|
|
1
|
-
# sarj-
|
|
1
|
+
# @sarj/eslint-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Custom ESLint rules collection for Sarj projects. Supports ESLint 9+ flat config format.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
yarn add -D @sarj/eslint-plugin eslint
|
|
9
9
|
```
|
|
10
|
+
|
|
11
|
+
## Usage (ESLint 9+ Flat Config)
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// eslint.config.js
|
|
15
|
+
import sarj, { configs } from "@sarj/eslint-plugin";
|
|
16
|
+
|
|
17
|
+
export default [
|
|
18
|
+
// Use a preset config
|
|
19
|
+
...configs.recommended,
|
|
20
|
+
|
|
21
|
+
// Or configure manually
|
|
22
|
+
{
|
|
23
|
+
plugins: {
|
|
24
|
+
"@sarj": sarj,
|
|
25
|
+
},
|
|
26
|
+
rules: {
|
|
27
|
+
"@sarj/zod-naming-convention": "warn",
|
|
28
|
+
"@sarj/require-assert-never": "error",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Available Configs
|
|
35
|
+
|
|
36
|
+
| Config | Description |
|
|
37
|
+
|--------|-------------|
|
|
38
|
+
| `configs.recommended` | Basic sarj rules for everyday use |
|
|
39
|
+
| `configs.strict` | All sarj rules at error level |
|
|
40
|
+
| `configs.style-guide` | Comprehensive rules (sarj + core ESLint) |
|
|
41
|
+
|
|
42
|
+
## Rules
|
|
43
|
+
|
|
44
|
+
| Rule | Description | Recommended |
|
|
45
|
+
|------|-------------|-------------|
|
|
46
|
+
| `@sarj/zod-naming-convention` | Enforce Zod schemas to be named with a Z prefix | warn |
|
|
47
|
+
| `@sarj/require-assert-never` | Require assertNever in switch default cases | error |
|
|
48
|
+
| `@sarj/require-zod-form-validation` | Require Zod validation when parsing FormData | error |
|
|
49
|
+
| `@sarj/enforce-file-structure` | Enforce consistent file structure | warn |
|
|
50
|
+
| `@sarj/no-raw-env` | Disallow direct process.env access | - |
|
|
51
|
+
| `@sarj/prefer-shadcn` | Prefer shadcn/ui components over native HTML | - |
|
|
52
|
+
| `@sarj/no-enum` | Disallow TypeScript enums | - |
|
|
53
|
+
|
|
54
|
+
## Rule Details
|
|
55
|
+
|
|
56
|
+
### `zod-naming-convention`
|
|
57
|
+
|
|
58
|
+
Ensures Zod schemas are named with a Z prefix for consistency.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// Bad
|
|
62
|
+
const userSchema = z.object({ name: z.string() });
|
|
63
|
+
|
|
64
|
+
// Good
|
|
65
|
+
const ZUserSchema = z.object({ name: z.string() });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `require-assert-never`
|
|
69
|
+
|
|
70
|
+
Ensures switch statements with default cases use assertNever for exhaustive type checking.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// Bad
|
|
74
|
+
switch (status) {
|
|
75
|
+
case "active":
|
|
76
|
+
return 1;
|
|
77
|
+
default:
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Good
|
|
82
|
+
switch (status) {
|
|
83
|
+
case "active":
|
|
84
|
+
return 1;
|
|
85
|
+
default:
|
|
86
|
+
assertNever(status);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `no-raw-env`
|
|
91
|
+
|
|
92
|
+
Disallows direct access to process.env. Use a Zod-validated env schema instead.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// Bad
|
|
96
|
+
const apiKey = process.env.API_KEY;
|
|
97
|
+
|
|
98
|
+
// Good
|
|
99
|
+
const apiKey = env.API_KEY; // from validated schema
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `prefer-shadcn`
|
|
103
|
+
|
|
104
|
+
Encourages use of shadcn/ui components instead of native HTML elements.
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
// Bad
|
|
108
|
+
<button onClick={handleClick}>Submit</button>
|
|
109
|
+
|
|
110
|
+
// Good
|
|
111
|
+
<Button onClick={handleClick}>Submit</Button>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `no-enum`
|
|
115
|
+
|
|
116
|
+
Discourages TypeScript enums in favor of union types or const objects.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// Bad
|
|
120
|
+
enum Status {
|
|
121
|
+
Active,
|
|
122
|
+
Inactive,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Good
|
|
126
|
+
type Status = "active" | "inactive";
|
|
127
|
+
// or
|
|
128
|
+
const Status = { ACTIVE: "active", INACTIVE: "inactive" } as const;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Requirements
|
|
132
|
+
|
|
133
|
+
- ESLint >= 9.0.0
|
|
134
|
+
- Node.js >= 18.0.0
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ESLint, Linter, Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* @sarj/eslint-plugin
|
|
4
|
+
*
|
|
5
|
+
* Custom ESLint rules collection for Sarj projects.
|
|
6
|
+
* Supports ESLint 9+ flat config format.
|
|
7
|
+
*/
|
|
8
|
+
declare const rules: Record<string, Rule.RuleModule>;
|
|
9
|
+
declare const plugin: ESLint.Plugin;
|
|
10
|
+
declare const configs: Record<string, Linter.Config[]>;
|
|
11
|
+
export { plugin as default, configs, rules };
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAUnD;;;;;GAKG;AAEH,QAAA,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAQ1C,CAAC;AAEF,QAAA,MAAM,MAAM,EAAE,MAAM,CAAC,MAMpB,CAAC;AAGF,QAAA,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAsE5C,CAAC;AAEF,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { zodNamingConvention } from "./rules/zod-naming-convention.js";
|
|
2
|
+
import { requireAssertNever } from "./rules/require-assert-never.js";
|
|
3
|
+
import { requireZodFormValidation } from "./rules/require-zod-form-validation.js";
|
|
4
|
+
import { enforceFileStructure } from "./rules/enforce-file-structure.js";
|
|
5
|
+
import { noRawEnv } from "./rules/no-raw-env.js";
|
|
6
|
+
import { preferShadcn } from "./rules/prefer-shadcn.js";
|
|
7
|
+
import { noEnum } from "./rules/no-enum.js";
|
|
8
|
+
/**
|
|
9
|
+
* @sarj/eslint-plugin
|
|
10
|
+
*
|
|
11
|
+
* Custom ESLint rules collection for Sarj projects.
|
|
12
|
+
* Supports ESLint 9+ flat config format.
|
|
13
|
+
*/
|
|
14
|
+
const rules = {
|
|
15
|
+
"zod-naming-convention": zodNamingConvention,
|
|
16
|
+
"require-assert-never": requireAssertNever,
|
|
17
|
+
"require-zod-form-validation": requireZodFormValidation,
|
|
18
|
+
"enforce-file-structure": enforceFileStructure,
|
|
19
|
+
"no-raw-env": noRawEnv,
|
|
20
|
+
"prefer-shadcn": preferShadcn,
|
|
21
|
+
"no-enum": noEnum,
|
|
22
|
+
};
|
|
23
|
+
const plugin = {
|
|
24
|
+
meta: {
|
|
25
|
+
name: "@sarj/eslint-plugin",
|
|
26
|
+
version: "1.0.0",
|
|
27
|
+
},
|
|
28
|
+
rules,
|
|
29
|
+
};
|
|
30
|
+
// ESLint 9 flat configs
|
|
31
|
+
const configs = {
|
|
32
|
+
// Basic sarj rules only
|
|
33
|
+
recommended: [
|
|
34
|
+
{
|
|
35
|
+
plugins: {
|
|
36
|
+
"@sarj": plugin,
|
|
37
|
+
},
|
|
38
|
+
rules: {
|
|
39
|
+
"@sarj/zod-naming-convention": "warn",
|
|
40
|
+
"@sarj/require-assert-never": "error",
|
|
41
|
+
"@sarj/require-zod-form-validation": "error",
|
|
42
|
+
"@sarj/enforce-file-structure": "warn",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
// All sarj rules at error level
|
|
47
|
+
strict: [
|
|
48
|
+
{
|
|
49
|
+
plugins: {
|
|
50
|
+
"@sarj": plugin,
|
|
51
|
+
},
|
|
52
|
+
rules: {
|
|
53
|
+
"@sarj/zod-naming-convention": "error",
|
|
54
|
+
"@sarj/require-assert-never": "error",
|
|
55
|
+
"@sarj/require-zod-form-validation": "error",
|
|
56
|
+
"@sarj/enforce-file-structure": "error",
|
|
57
|
+
"@sarj/no-raw-env": "error",
|
|
58
|
+
"@sarj/prefer-shadcn": "error",
|
|
59
|
+
"@sarj/no-enum": "error",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
// Comprehensive style guide rules (sarj + external)
|
|
64
|
+
"style-guide": [
|
|
65
|
+
{
|
|
66
|
+
plugins: {
|
|
67
|
+
"@sarj": plugin,
|
|
68
|
+
},
|
|
69
|
+
rules: {
|
|
70
|
+
// === Sarj custom rules ===
|
|
71
|
+
"@sarj/zod-naming-convention": "error",
|
|
72
|
+
"@sarj/require-assert-never": "error",
|
|
73
|
+
"@sarj/require-zod-form-validation": "error",
|
|
74
|
+
"@sarj/enforce-file-structure": "error",
|
|
75
|
+
"@sarj/no-raw-env": "error",
|
|
76
|
+
"@sarj/prefer-shadcn": "error",
|
|
77
|
+
"@sarj/no-enum": "error",
|
|
78
|
+
// === Core ESLint rules ===
|
|
79
|
+
"no-var": "error",
|
|
80
|
+
"prefer-const": "error",
|
|
81
|
+
"object-shorthand": ["error", "always"],
|
|
82
|
+
"no-nested-ternary": "error",
|
|
83
|
+
"no-console": ["warn", { allow: ["warn", "error"] }],
|
|
84
|
+
eqeqeq: ["error", "always"],
|
|
85
|
+
"no-param-reassign": "error",
|
|
86
|
+
"no-return-await": "error",
|
|
87
|
+
"prefer-template": "error",
|
|
88
|
+
"array-callback-return": "error",
|
|
89
|
+
// === Complexity limits ===
|
|
90
|
+
complexity: ["warn", { max: 15 }],
|
|
91
|
+
"max-depth": ["warn", { max: 4 }],
|
|
92
|
+
"max-nested-callbacks": ["warn", { max: 3 }],
|
|
93
|
+
"max-params": ["error", { max: 5 }],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
export { plugin as default, configs, rules };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enforce-file-structure.d.ts","sourceRoot":"","sources":["../../src/rules/enforce-file-structure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAgDnC,eAAO,MAAM,oBAAoB,EAAE,IAAI,CAAC,UA8DvC,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce consistent file structure
|
|
3
|
+
* @description Enforces: imports -> types -> constants -> functions -> exports
|
|
4
|
+
*/
|
|
5
|
+
const SECTIONS = ["imports", "types", "constants", "functions", "exports"];
|
|
6
|
+
function getStatementSection(statement) {
|
|
7
|
+
const type = statement.type;
|
|
8
|
+
switch (type) {
|
|
9
|
+
case "ImportDeclaration":
|
|
10
|
+
return 0; // imports
|
|
11
|
+
case "TSTypeAliasDeclaration":
|
|
12
|
+
case "TSInterfaceDeclaration":
|
|
13
|
+
case "TSEnumDeclaration":
|
|
14
|
+
return 1; // types
|
|
15
|
+
case "VariableDeclaration": {
|
|
16
|
+
// Check if it's a constant (const with UPPER_CASE or simple values)
|
|
17
|
+
const varDecl = statement;
|
|
18
|
+
if (varDecl.kind === "const") {
|
|
19
|
+
const declarator = varDecl.declarations[0];
|
|
20
|
+
if (declarator?.id.type === "Identifier" &&
|
|
21
|
+
declarator.id.name === declarator.id.name.toUpperCase()) {
|
|
22
|
+
return 2; // constants
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return 3; // functions (const fn = ...)
|
|
26
|
+
}
|
|
27
|
+
case "FunctionDeclaration":
|
|
28
|
+
return 3; // functions
|
|
29
|
+
case "ExportNamedDeclaration":
|
|
30
|
+
case "ExportDefaultDeclaration":
|
|
31
|
+
return 4; // exports
|
|
32
|
+
default:
|
|
33
|
+
return 3; // default to functions
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export const enforceFileStructure = {
|
|
37
|
+
meta: {
|
|
38
|
+
type: "suggestion",
|
|
39
|
+
docs: {
|
|
40
|
+
description: "Enforce consistent file structure: imports -> types -> constants -> functions -> exports",
|
|
41
|
+
recommended: true,
|
|
42
|
+
},
|
|
43
|
+
schema: [],
|
|
44
|
+
messages: {
|
|
45
|
+
incorrectOrder: "File structure violation: {{current}} should come after {{expected}}",
|
|
46
|
+
useServerDirective: "Server action files must start with 'use server' directive",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
create(context) {
|
|
50
|
+
const filename = context.filename;
|
|
51
|
+
const isServerAction = filename.includes("/actions/") || filename.includes("action");
|
|
52
|
+
return {
|
|
53
|
+
Program(node) {
|
|
54
|
+
const program = node;
|
|
55
|
+
const body = program.body;
|
|
56
|
+
let currentSection = 0;
|
|
57
|
+
// Check for "use server" directive in server action files
|
|
58
|
+
if (isServerAction) {
|
|
59
|
+
const firstNode = body[0];
|
|
60
|
+
if (!firstNode ||
|
|
61
|
+
firstNode.type !== "ExpressionStatement" ||
|
|
62
|
+
firstNode.expression.type !== "Literal" ||
|
|
63
|
+
firstNode.expression.value !== "use server") {
|
|
64
|
+
context.report({
|
|
65
|
+
node,
|
|
66
|
+
messageId: "useServerDirective",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const statement of body) {
|
|
71
|
+
const statementSection = getStatementSection(statement);
|
|
72
|
+
if (statementSection < currentSection) {
|
|
73
|
+
context.report({
|
|
74
|
+
node: statement,
|
|
75
|
+
messageId: "incorrectOrder",
|
|
76
|
+
data: {
|
|
77
|
+
current: SECTIONS[statementSection],
|
|
78
|
+
expected: SECTIONS[currentSection],
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
currentSection = Math.max(currentSection, statementSection);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
export default enforceFileStructure;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Disallow TypeScript enums, prefer union types
|
|
4
|
+
* @description Enums add runtime code and have unintuitive behavior. Use union types instead.
|
|
5
|
+
*/
|
|
6
|
+
export declare const noEnum: Rule.RuleModule;
|
|
7
|
+
export default noEnum;
|
|
8
|
+
//# sourceMappingURL=no-enum.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-enum.d.ts","sourceRoot":"","sources":["../../src/rules/no-enum.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEnC;;;GAGG;AAEH,eAAO,MAAM,MAAM,EAAE,IAAI,CAAC,UAyBzB,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Disallow TypeScript enums, prefer union types
|
|
3
|
+
* @description Enums add runtime code and have unintuitive behavior. Use union types instead.
|
|
4
|
+
*/
|
|
5
|
+
export const noEnum = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Disallow TypeScript enums, prefer union types or const objects",
|
|
10
|
+
recommended: true,
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
noEnum: 'Enums are discouraged. Use union types (\'type Status = "active" | "inactive"\') or const objects instead.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
TSEnumDeclaration(node) {
|
|
21
|
+
context.report({
|
|
22
|
+
node,
|
|
23
|
+
messageId: "noEnum",
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
export default noEnum;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Disallow direct process.env access
|
|
4
|
+
* @description Use Zod-validated env schema instead of process.env
|
|
5
|
+
*/
|
|
6
|
+
export declare const noRawEnv: Rule.RuleModule;
|
|
7
|
+
export default noRawEnv;
|
|
8
|
+
//# sourceMappingURL=no-raw-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-raw-env.d.ts","sourceRoot":"","sources":["../../src/rules/no-raw-env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGnC;;;GAGG;AAEH,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,UA8B3B,CAAC;AAEF,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Disallow direct process.env access
|
|
3
|
+
* @description Use Zod-validated env schema instead of process.env
|
|
4
|
+
*/
|
|
5
|
+
export const noRawEnv = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Disallow direct process.env access",
|
|
10
|
+
recommended: true,
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
noRawEnv: "Use Zod-validated env schema instead of process.env directly",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
MemberExpression(node) {
|
|
20
|
+
if (node.object.type === "Identifier" &&
|
|
21
|
+
node.object.name === "process" &&
|
|
22
|
+
node.property.type === "Identifier" &&
|
|
23
|
+
node.property.name === "env") {
|
|
24
|
+
context.report({
|
|
25
|
+
node,
|
|
26
|
+
messageId: "noRawEnv",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
export default noRawEnv;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-shadcn.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-shadcn.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAgBnC,eAAO,MAAM,YAAY,EAAE,IAAI,CAAC,UAwC/B,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Prefer shadcn/ui components over native HTML elements
|
|
3
|
+
* @description Use shadcn components for consistent design system
|
|
4
|
+
*/
|
|
5
|
+
const REPLACEMENTS = {
|
|
6
|
+
button: "Button",
|
|
7
|
+
input: "Input",
|
|
8
|
+
select: "Select",
|
|
9
|
+
textarea: "Textarea",
|
|
10
|
+
table: "Table",
|
|
11
|
+
dialog: "Dialog",
|
|
12
|
+
};
|
|
13
|
+
export const preferShadcn = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: "suggestion",
|
|
16
|
+
docs: {
|
|
17
|
+
description: "Prefer shadcn/ui components over native HTML elements",
|
|
18
|
+
recommended: true,
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
messages: {
|
|
22
|
+
preferShadcn: "Use shadcn <{{replacement}}> component from @/components/ui/{{lowercase}} instead of native <{{element}}>",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
JSXOpeningElement(node) {
|
|
29
|
+
// Only check lowercase element names (native HTML)
|
|
30
|
+
if (!node.name || node.name.type !== "JSXIdentifier") {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const elementName = node.name.name;
|
|
34
|
+
const replacement = REPLACEMENTS[elementName];
|
|
35
|
+
if (replacement) {
|
|
36
|
+
context.report({
|
|
37
|
+
node,
|
|
38
|
+
messageId: "preferShadcn",
|
|
39
|
+
data: {
|
|
40
|
+
element: elementName,
|
|
41
|
+
replacement,
|
|
42
|
+
lowercase: elementName,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
export default preferShadcn;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Require assertNever in switch statement default cases
|
|
4
|
+
* @description Ensures exhaustive type checking in switch statements
|
|
5
|
+
*/
|
|
6
|
+
export declare const requireAssertNever: Rule.RuleModule;
|
|
7
|
+
export default requireAssertNever;
|
|
8
|
+
//# sourceMappingURL=require-assert-never.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-assert-never.d.ts","sourceRoot":"","sources":["../../src/rules/require-assert-never.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGnC;;;GAGG;AAEH,eAAO,MAAM,kBAAkB,EAAE,IAAI,CAAC,UAwDrC,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Require assertNever in switch statement default cases
|
|
3
|
+
* @description Ensures exhaustive type checking in switch statements
|
|
4
|
+
*/
|
|
5
|
+
export const requireAssertNever = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Require assertNever in switch statement default cases for exhaustive type checking",
|
|
10
|
+
recommended: true,
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
missingAssertNever: "Switch statement default case must call assertNever() for exhaustive type checking",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
SwitchStatement(node) {
|
|
20
|
+
const defaultCase = node.cases.find((caseNode) => caseNode.test === null);
|
|
21
|
+
if (!defaultCase) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const hasAssertNever = defaultCase.consequent.some((statement) => {
|
|
25
|
+
// Check for ExpressionStatement containing CallExpression
|
|
26
|
+
if (statement.type === "ExpressionStatement") {
|
|
27
|
+
const exprStmt = statement;
|
|
28
|
+
if (exprStmt.expression.type === "CallExpression") {
|
|
29
|
+
const callee = exprStmt.expression.callee;
|
|
30
|
+
return callee.type === "Identifier" && callee.name === "assertNever";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Check for ThrowStatement with assertNever call
|
|
34
|
+
if (statement.type === "ThrowStatement") {
|
|
35
|
+
const throwStmt = statement;
|
|
36
|
+
if (throwStmt.argument?.type === "CallExpression") {
|
|
37
|
+
const callee = throwStmt.argument.callee;
|
|
38
|
+
return callee.type === "Identifier" && callee.name === "assertNever";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
});
|
|
43
|
+
if (!hasAssertNever) {
|
|
44
|
+
context.report({
|
|
45
|
+
node: defaultCase,
|
|
46
|
+
messageId: "missingAssertNever",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
export default requireAssertNever;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Require Zod validation when parsing FormData
|
|
4
|
+
* @description Ensures form data is validated with Zod schemas
|
|
5
|
+
*/
|
|
6
|
+
export declare const requireZodFormValidation: Rule.RuleModule;
|
|
7
|
+
export default requireZodFormValidation;
|
|
8
|
+
//# sourceMappingURL=require-zod-form-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-zod-form-validation.d.ts","sourceRoot":"","sources":["../../src/rules/require-zod-form-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGnC;;;GAGG;AAEH,eAAO,MAAM,wBAAwB,EAAE,IAAI,CAAC,UAkE3C,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Require Zod validation when parsing FormData
|
|
3
|
+
* @description Ensures form data is validated with Zod schemas
|
|
4
|
+
*/
|
|
5
|
+
export const requireZodFormValidation = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Require Zod validation when parsing FormData",
|
|
10
|
+
recommended: true,
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
missingZodValidation: "FormData parsing must use Zod schema validation (e.g., Schema.parse())",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
CallExpression(node) {
|
|
20
|
+
const callExpr = node;
|
|
21
|
+
// Check for formData.get() calls
|
|
22
|
+
if (callExpr.callee.type !== "MemberExpression") {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const callee = callExpr.callee;
|
|
26
|
+
if (callee.property.type !== "Identifier" ||
|
|
27
|
+
callee.property.name !== "get" ||
|
|
28
|
+
callee.object.type !== "Identifier" ||
|
|
29
|
+
callee.object.name !== "formData") {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Check if this is inside a Zod .parse() call
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
let parent = node.parent;
|
|
35
|
+
let hasZodValidation = false;
|
|
36
|
+
// Traverse up to find if we're inside a .parse() call
|
|
37
|
+
while (parent) {
|
|
38
|
+
if (parent.type === "CallExpression" &&
|
|
39
|
+
parent.callee.type === "MemberExpression") {
|
|
40
|
+
const parentCallee = parent.callee;
|
|
41
|
+
if (parentCallee.property.type === "Identifier" &&
|
|
42
|
+
parentCallee.property.name === "parse") {
|
|
43
|
+
hasZodValidation = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
parent = parent.parent;
|
|
48
|
+
}
|
|
49
|
+
if (!hasZodValidation) {
|
|
50
|
+
context.report({
|
|
51
|
+
node: node,
|
|
52
|
+
messageId: "missingZodValidation",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
export default requireZodFormValidation;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Enforce Zod schemas to be named with a Z prefix
|
|
4
|
+
* @description Ensures consistent naming for Zod schemas (e.g., ZUserSchema instead of userSchema)
|
|
5
|
+
*/
|
|
6
|
+
export declare const zodNamingConvention: Rule.RuleModule;
|
|
7
|
+
export default zodNamingConvention;
|
|
8
|
+
//# sourceMappingURL=zod-naming-convention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod-naming-convention.d.ts","sourceRoot":"","sources":["../../src/rules/zod-naming-convention.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGnC;;;GAGG;AAEH,eAAO,MAAM,mBAAmB,EAAE,IAAI,CAAC,UAqDtC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce Zod schemas to be named with a Z prefix
|
|
3
|
+
* @description Ensures consistent naming for Zod schemas (e.g., ZUserSchema instead of userSchema)
|
|
4
|
+
*/
|
|
5
|
+
export const zodNamingConvention = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Enforce Zod schemas to be named with a Z prefix",
|
|
10
|
+
recommended: false,
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
zodPrefix: "Zod schema names should start with 'Z' (e.g., Z{{suggestedName}})",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
VariableDeclarator(node) {
|
|
20
|
+
const declarator = node;
|
|
21
|
+
if (!declarator.init || declarator.init.type !== "CallExpression") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const callExpr = declarator.init;
|
|
25
|
+
if (!callExpr.callee || callExpr.callee.type !== "MemberExpression") {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const callee = callExpr.callee;
|
|
29
|
+
const isDirectZodCall = callee.object.type === "Identifier" &&
|
|
30
|
+
callee.object.name === "z";
|
|
31
|
+
const isChainedZodCall = callee.object.type === "CallExpression" &&
|
|
32
|
+
callee.object.callee?.type === "MemberExpression" &&
|
|
33
|
+
callee.object.callee.object?.type === "Identifier" &&
|
|
34
|
+
callee.object.callee.object.name === "z";
|
|
35
|
+
if (isDirectZodCall || isChainedZodCall) {
|
|
36
|
+
if (declarator.id.type !== "Identifier") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const variableName = declarator.id.name;
|
|
40
|
+
if (!variableName.startsWith("Z")) {
|
|
41
|
+
const suggestedName = variableName.charAt(0).toUpperCase() + variableName.slice(1);
|
|
42
|
+
context.report({
|
|
43
|
+
node: declarator.id,
|
|
44
|
+
messageId: "zodPrefix",
|
|
45
|
+
data: { suggestedName },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
export default zodNamingConvention;
|
package/package.json
CHANGED
|
@@ -1,30 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarj/eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Custom ESLint rules collection",
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Custom ESLint rules collection for Sarj projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
6
17
|
"publishConfig": {
|
|
7
18
|
"access": "public"
|
|
8
19
|
},
|
|
9
20
|
"scripts": {
|
|
10
|
-
"
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"lint": "eslint src",
|
|
25
|
+
"prepublishOnly": "yarn build"
|
|
11
26
|
},
|
|
12
27
|
"keywords": [
|
|
13
28
|
"eslint",
|
|
14
29
|
"eslintplugin",
|
|
15
30
|
"eslint-plugin",
|
|
16
|
-
"zod"
|
|
31
|
+
"zod",
|
|
32
|
+
"typescript",
|
|
33
|
+
"flat-config"
|
|
17
34
|
],
|
|
18
|
-
"author": "
|
|
35
|
+
"author": "Sarj",
|
|
19
36
|
"license": "MIT",
|
|
20
37
|
"devDependencies": {
|
|
21
|
-
"eslint": "^
|
|
22
|
-
"
|
|
38
|
+
"@types/eslint": "^9.6.0",
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
41
|
+
"eslint": "^9.0.0",
|
|
42
|
+
"typescript": "^5.7.0",
|
|
43
|
+
"vitest": "^2.0.0"
|
|
23
44
|
},
|
|
24
45
|
"peerDependencies": {
|
|
25
|
-
"eslint": ">=
|
|
46
|
+
"eslint": ">=9.0.0"
|
|
26
47
|
},
|
|
27
48
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
29
|
-
}
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
},
|
|
51
|
+
"packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8"
|
|
30
52
|
}
|
package/lib/index.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
rules: {
|
|
3
|
-
"zod-naming-convention": require("./rules/zod-naming-convention"),
|
|
4
|
-
"require-assert-never": require("./rules/require-assert-never"),
|
|
5
|
-
"require-zod-form-validation": require("./rules/require-zod-form-validation"),
|
|
6
|
-
"enforce-file-structure": require("./rules/enforce-file-structure"),
|
|
7
|
-
},
|
|
8
|
-
configs: {
|
|
9
|
-
recommended: {
|
|
10
|
-
plugins: ["sarj-eslint"],
|
|
11
|
-
rules: {
|
|
12
|
-
"sarj-eslint/zod-naming-convention": "warn",
|
|
13
|
-
"sarj-eslint/require-assert-never": "error",
|
|
14
|
-
"sarj-eslint/require-zod-form-validation": "error",
|
|
15
|
-
"sarj-eslint/enforce-file-structure": "warn",
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
};
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
meta: {
|
|
3
|
-
type: "suggestion",
|
|
4
|
-
docs: {
|
|
5
|
-
description: "Enforce consistent file structure: imports → types → constants → functions → exports",
|
|
6
|
-
category: "Stylistic Issues",
|
|
7
|
-
recommended: true,
|
|
8
|
-
},
|
|
9
|
-
schema: [],
|
|
10
|
-
messages: {
|
|
11
|
-
incorrectOrder: "File structure violation: {{current}} should come after {{expected}}",
|
|
12
|
-
useServerDirective: "Server action files must start with 'use server' directive"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
create(context) {
|
|
16
|
-
const filename = context.getFilename();
|
|
17
|
-
const isServerAction = filename.includes('/actions/') || filename.includes('action');
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
Program(node) {
|
|
21
|
-
const body = node.body;
|
|
22
|
-
let currentSection = 0; // 0: imports, 1: types, 2: constants, 3: functions, 4: exports
|
|
23
|
-
const sections = ['imports', 'types', 'constants', 'functions', 'exports'];
|
|
24
|
-
|
|
25
|
-
// Check for "use server" directive in server action files
|
|
26
|
-
if (isServerAction) {
|
|
27
|
-
const firstNode = body[0];
|
|
28
|
-
if (!firstNode ||
|
|
29
|
-
firstNode.type !== 'ExpressionStatement' ||
|
|
30
|
-
firstNode.expression.type !== 'Literal' ||
|
|
31
|
-
firstNode.expression.value !== 'use server') {
|
|
32
|
-
context.report({
|
|
33
|
-
node: node,
|
|
34
|
-
messageId: "useServerDirective"
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
body.forEach(statement => {
|
|
40
|
-
let statementSection = getStatementSection(statement);
|
|
41
|
-
|
|
42
|
-
if (statementSection < currentSection) {
|
|
43
|
-
context.report({
|
|
44
|
-
node: statement,
|
|
45
|
-
messageId: "incorrectOrder",
|
|
46
|
-
data: {
|
|
47
|
-
current: sections[statementSection],
|
|
48
|
-
expected: sections[currentSection]
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
} else {
|
|
52
|
-
currentSection = Math.max(currentSection, statementSection);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
function getStatementSection(statement) {
|
|
59
|
-
switch (statement.type) {
|
|
60
|
-
case 'ImportDeclaration':
|
|
61
|
-
return 0; // imports
|
|
62
|
-
case 'TSTypeAliasDeclaration':
|
|
63
|
-
case 'TSInterfaceDeclaration':
|
|
64
|
-
case 'TSEnumDeclaration':
|
|
65
|
-
return 1; // types
|
|
66
|
-
case 'VariableDeclaration':
|
|
67
|
-
// Check if it's a constant (const with UPPER_CASE or simple values)
|
|
68
|
-
if (statement.kind === 'const') {
|
|
69
|
-
const declarator = statement.declarations[0];
|
|
70
|
-
if (declarator && declarator.id.name === declarator.id.name.toUpperCase()) {
|
|
71
|
-
return 2; // constants
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return 3; // functions (const fn = ...)
|
|
75
|
-
case 'FunctionDeclaration':
|
|
76
|
-
return 3; // functions
|
|
77
|
-
case 'ExportNamedDeclaration':
|
|
78
|
-
case 'ExportDefaultDeclaration':
|
|
79
|
-
return 4; // exports
|
|
80
|
-
default:
|
|
81
|
-
return 3; // default to functions
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
meta: {
|
|
3
|
-
type: "problem",
|
|
4
|
-
docs: {
|
|
5
|
-
description: "Require assertNever in switch statement default cases for exhaustive checking",
|
|
6
|
-
category: "Best Practices",
|
|
7
|
-
recommended: true,
|
|
8
|
-
},
|
|
9
|
-
schema: [],
|
|
10
|
-
messages: {
|
|
11
|
-
missingAssertNever: "Switch statement default case must call assertNever() for exhaustive type checking"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
create(context) {
|
|
15
|
-
return {
|
|
16
|
-
SwitchStatement(node) {
|
|
17
|
-
const defaultCase = node.cases.find(caseNode => caseNode.test === null);
|
|
18
|
-
|
|
19
|
-
if (defaultCase) {
|
|
20
|
-
const hasAssertNever = defaultCase.consequent.some(statement => {
|
|
21
|
-
// Check for ExpressionStatement containing CallExpression
|
|
22
|
-
if (statement.type === 'ExpressionStatement' &&
|
|
23
|
-
statement.expression.type === 'CallExpression') {
|
|
24
|
-
const callee = statement.expression.callee;
|
|
25
|
-
return callee.name === 'assertNever';
|
|
26
|
-
}
|
|
27
|
-
// Check for ThrowStatement with assertNever call
|
|
28
|
-
if (statement.type === 'ThrowStatement' &&
|
|
29
|
-
statement.argument.type === 'CallExpression') {
|
|
30
|
-
const callee = statement.argument.callee;
|
|
31
|
-
return callee.name === 'assertNever';
|
|
32
|
-
}
|
|
33
|
-
return false;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
if (!hasAssertNever) {
|
|
37
|
-
context.report({
|
|
38
|
-
node: defaultCase,
|
|
39
|
-
messageId: "missingAssertNever"
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
meta: {
|
|
3
|
-
type: "problem",
|
|
4
|
-
docs: {
|
|
5
|
-
description: "Require Zod validation when parsing FormData",
|
|
6
|
-
category: "Best Practices",
|
|
7
|
-
recommended: true,
|
|
8
|
-
},
|
|
9
|
-
schema: [],
|
|
10
|
-
messages: {
|
|
11
|
-
missingZodValidation: "FormData parsing must use Zod schema validation (e.g., Schema.parse())"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
create(context) {
|
|
15
|
-
return {
|
|
16
|
-
CallExpression(node) {
|
|
17
|
-
// Check for formData.get() calls
|
|
18
|
-
if (node.callee.type === 'MemberExpression' &&
|
|
19
|
-
node.callee.property.name === 'get' &&
|
|
20
|
-
node.callee.object.name === 'formData') {
|
|
21
|
-
|
|
22
|
-
// Check if this is inside a Zod .parse() call
|
|
23
|
-
let parent = node.parent;
|
|
24
|
-
let hasZodValidation = false;
|
|
25
|
-
|
|
26
|
-
// Traverse up to find if we're inside a .parse() call
|
|
27
|
-
while (parent) {
|
|
28
|
-
if (parent.type === 'CallExpression' &&
|
|
29
|
-
parent.callee.type === 'MemberExpression' &&
|
|
30
|
-
parent.callee.property.name === 'parse') {
|
|
31
|
-
hasZodValidation = true;
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
parent = parent.parent;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!hasZodValidation) {
|
|
38
|
-
context.report({
|
|
39
|
-
node,
|
|
40
|
-
messageId: "missingZodValidation"
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
meta: {
|
|
3
|
-
type: "suggestion",
|
|
4
|
-
docs: {
|
|
5
|
-
description: "Enforce Zod schemas to be named with a Z prefix",
|
|
6
|
-
category: "Stylistic Issues",
|
|
7
|
-
recommended: false
|
|
8
|
-
},
|
|
9
|
-
fixable: null,
|
|
10
|
-
schema: []
|
|
11
|
-
},
|
|
12
|
-
create(context) {
|
|
13
|
-
return {
|
|
14
|
-
VariableDeclarator(node) {
|
|
15
|
-
// Check if the right side involves z.object() or other Zod schema creation
|
|
16
|
-
if (
|
|
17
|
-
node.init &&
|
|
18
|
-
node.init.type === "CallExpression" &&
|
|
19
|
-
node.init.callee &&
|
|
20
|
-
(
|
|
21
|
-
// Check direct calls like z.object()
|
|
22
|
-
(node.init.callee.type === "MemberExpression" &&
|
|
23
|
-
node.init.callee.object &&
|
|
24
|
-
node.init.callee.object.name === "z") ||
|
|
25
|
-
// Check chained calls like z.object().extend()
|
|
26
|
-
(node.init.callee.type === "MemberExpression" &&
|
|
27
|
-
node.init.callee.object &&
|
|
28
|
-
node.init.callee.object.callee &&
|
|
29
|
-
node.init.callee.object.callee.object &&
|
|
30
|
-
node.init.callee.object.callee.object.name === "z")
|
|
31
|
-
)
|
|
32
|
-
) {
|
|
33
|
-
const variableName = node.id.name;
|
|
34
|
-
if (!variableName.startsWith("Z")) {
|
|
35
|
-
context.report({
|
|
36
|
-
node: node.id,
|
|
37
|
-
message: "Zod schema names should start with Z",
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
},
|
|
44
|
-
};
|