@sarj/eslint-plugin 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
rules: {
|
|
3
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"),
|
|
4
7
|
},
|
|
5
8
|
configs: {
|
|
6
9
|
recommended: {
|
|
7
10
|
plugins: ["sarj-eslint"],
|
|
8
11
|
rules: {
|
|
9
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",
|
|
10
16
|
},
|
|
11
17
|
},
|
|
12
18
|
},
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
};
|
|
@@ -4,10 +4,10 @@ module.exports = {
|
|
|
4
4
|
docs: {
|
|
5
5
|
description: "Enforce Zod schemas to be named with a Z prefix",
|
|
6
6
|
category: "Stylistic Issues",
|
|
7
|
-
recommended: false
|
|
7
|
+
recommended: false
|
|
8
8
|
},
|
|
9
|
-
|
|
10
|
-
schema: []
|
|
9
|
+
fixable: "code",
|
|
10
|
+
schema: []
|
|
11
11
|
},
|
|
12
12
|
create(context) {
|
|
13
13
|
return {
|
|
@@ -17,31 +17,31 @@ module.exports = {
|
|
|
17
17
|
node.init &&
|
|
18
18
|
node.init.type === "CallExpression" &&
|
|
19
19
|
node.init.callee &&
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
node.init.callee.
|
|
23
|
-
|
|
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") ||
|
|
24
25
|
// Check chained calls like z.object().extend()
|
|
25
|
-
(node.init.callee.type === "MemberExpression" &&
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
)
|
|
30
32
|
) {
|
|
31
33
|
const variableName = node.id.name;
|
|
32
34
|
if (!variableName.startsWith("Z")) {
|
|
33
35
|
context.report({
|
|
34
36
|
node: node.id,
|
|
35
37
|
message: "Zod schema names should start with Z",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// },
|
|
38
|
+
fix(fixer) {
|
|
39
|
+
return fixer.replaceText(node.id, `Z${variableName}`);
|
|
40
|
+
},
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
},
|
|
44
45
|
};
|
|
45
46
|
},
|
|
46
|
-
};
|
|
47
|
-
|
|
47
|
+
};
|