@sarj/eslint-plugin 1.0.0 → 2.0.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/README.md +7 -128
- package/dist/index.cjs +13512 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +115 -0
- package/dist/index.d.ts +115 -12
- package/dist/index.js +13503 -93
- package/dist/index.js.map +1 -0
- package/package.json +35 -21
- package/dist/index.d.ts.map +0 -1
- package/dist/rules/enforce-file-structure.d.ts +0 -4
- package/dist/rules/enforce-file-structure.d.ts.map +0 -1
- package/dist/rules/enforce-file-structure.js +0 -90
- package/dist/rules/no-enum.d.ts +0 -8
- package/dist/rules/no-enum.d.ts.map +0 -1
- package/dist/rules/no-enum.js +0 -29
- package/dist/rules/no-enum.test.d.ts +0 -2
- package/dist/rules/no-enum.test.d.ts.map +0 -1
- package/dist/rules/no-enum.test.js +0 -44
- package/dist/rules/no-raw-env.d.ts +0 -8
- package/dist/rules/no-raw-env.d.ts.map +0 -1
- package/dist/rules/no-raw-env.js +0 -33
- package/dist/rules/no-raw-env.test.d.ts +0 -2
- package/dist/rules/no-raw-env.test.d.ts.map +0 -1
- package/dist/rules/no-raw-env.test.js +0 -36
- package/dist/rules/prefer-shadcn.d.ts +0 -4
- package/dist/rules/prefer-shadcn.d.ts.map +0 -1
- package/dist/rules/prefer-shadcn.js +0 -50
- package/dist/rules/prefer-shadcn.test.d.ts +0 -2
- package/dist/rules/prefer-shadcn.test.d.ts.map +0 -1
- package/dist/rules/prefer-shadcn.test.js +0 -58
- package/dist/rules/require-assert-never.d.ts +0 -8
- package/dist/rules/require-assert-never.d.ts.map +0 -1
- package/dist/rules/require-assert-never.js +0 -53
- package/dist/rules/require-assert-never.test.d.ts +0 -2
- package/dist/rules/require-assert-never.test.d.ts.map +0 -1
- package/dist/rules/require-assert-never.test.js +0 -76
- package/dist/rules/require-zod-form-validation.d.ts +0 -8
- package/dist/rules/require-zod-form-validation.d.ts.map +0 -1
- package/dist/rules/require-zod-form-validation.js +0 -59
- package/dist/rules/zod-naming-convention.d.ts +0 -8
- package/dist/rules/zod-naming-convention.d.ts.map +0 -1
- package/dist/rules/zod-naming-convention.js +0 -53
- package/dist/rules/zod-naming-convention.test.d.ts +0 -2
- package/dist/rules/zod-naming-convention.test.d.ts.map +0 -1
- package/dist/rules/zod-naming-convention.test.js +0 -38
package/package.json
CHANGED
|
@@ -1,52 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarj/eslint-plugin",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Custom ESLint rules
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.
|
|
7
|
-
"
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
10
12
|
"import": "./dist/index.js",
|
|
11
|
-
"
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
12
14
|
}
|
|
13
15
|
},
|
|
14
16
|
"files": [
|
|
15
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
16
19
|
],
|
|
17
20
|
"publishConfig": {
|
|
18
|
-
"access": "public"
|
|
21
|
+
"access": "public",
|
|
22
|
+
"provenance": true
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/sarj-ai/linting.git",
|
|
28
|
+
"directory": "packages/typescript"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/sarj-ai/linting/tree/main/packages/typescript",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/sarj-ai/linting/issues"
|
|
19
33
|
},
|
|
20
34
|
"scripts": {
|
|
21
|
-
"build": "
|
|
35
|
+
"build": "tsup",
|
|
22
36
|
"test": "vitest run",
|
|
23
|
-
"
|
|
24
|
-
"lint": "eslint src",
|
|
25
|
-
"prepublishOnly": "yarn build"
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
26
38
|
},
|
|
27
39
|
"keywords": [
|
|
28
40
|
"eslint",
|
|
29
41
|
"eslintplugin",
|
|
30
42
|
"eslint-plugin",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
43
|
+
"sarj",
|
|
44
|
+
"react",
|
|
45
|
+
"nextjs",
|
|
46
|
+
"zod"
|
|
34
47
|
],
|
|
35
|
-
"author": "
|
|
48
|
+
"author": "sarj-ai",
|
|
36
49
|
"license": "MIT",
|
|
37
50
|
"devDependencies": {
|
|
38
|
-
"@types/
|
|
39
|
-
"@types/node": "^22.0.0",
|
|
51
|
+
"@types/node": "^20.0.0",
|
|
40
52
|
"@typescript-eslint/parser": "^8.0.0",
|
|
53
|
+
"@typescript-eslint/rule-tester": "^8.0.0",
|
|
54
|
+
"@typescript-eslint/utils": "^8.0.0",
|
|
41
55
|
"eslint": "^9.0.0",
|
|
42
|
-
"
|
|
56
|
+
"tsup": "^8.3.0",
|
|
57
|
+
"typescript": "^5.6.0",
|
|
43
58
|
"vitest": "^2.0.0"
|
|
44
59
|
},
|
|
45
60
|
"peerDependencies": {
|
|
46
61
|
"eslint": ">=9.0.0"
|
|
47
62
|
},
|
|
48
63
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
50
|
-
}
|
|
51
|
-
"packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8"
|
|
64
|
+
"node": ">=20.0.0"
|
|
65
|
+
}
|
|
52
66
|
}
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,90 +0,0 @@
|
|
|
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;
|
package/dist/rules/no-enum.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/rules/no-enum.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
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;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"no-enum.test.d.ts","sourceRoot":"","sources":["../../src/rules/no-enum.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { RuleTester } from "eslint";
|
|
2
|
-
import { describe, it } from "vitest";
|
|
3
|
-
import { noEnum } from "./no-enum.js";
|
|
4
|
-
// Note: This test requires @typescript-eslint/parser to work properly
|
|
5
|
-
// For now, we'll skip the actual test execution and just verify the rule exports correctly
|
|
6
|
-
describe("no-enum", () => {
|
|
7
|
-
it("should export a valid rule module", () => {
|
|
8
|
-
if (!noEnum.meta || !noEnum.create) {
|
|
9
|
-
throw new Error("Rule is not properly exported");
|
|
10
|
-
}
|
|
11
|
-
if (noEnum.meta.type !== "suggestion") {
|
|
12
|
-
throw new Error("Expected rule type to be 'suggestion'");
|
|
13
|
-
}
|
|
14
|
-
if (!noEnum.meta.messages?.noEnum) {
|
|
15
|
-
throw new Error("Expected noEnum message to be defined");
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
// Full test requires TypeScript parser
|
|
19
|
-
it("should work with TypeScript parser", async () => {
|
|
20
|
-
const parser = await import("@typescript-eslint/parser");
|
|
21
|
-
const ruleTester = new RuleTester({
|
|
22
|
-
languageOptions: {
|
|
23
|
-
ecmaVersion: 2022,
|
|
24
|
-
sourceType: "module",
|
|
25
|
-
parser,
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
ruleTester.run("no-enum", noEnum, {
|
|
29
|
-
valid: [
|
|
30
|
-
// Union types are OK
|
|
31
|
-
{ code: 'type Status = "active" | "inactive";' },
|
|
32
|
-
// Const objects are OK
|
|
33
|
-
{ code: 'const Status = { ACTIVE: "active", INACTIVE: "inactive" } as const;' },
|
|
34
|
-
],
|
|
35
|
-
invalid: [
|
|
36
|
-
// Regular enum
|
|
37
|
-
{
|
|
38
|
-
code: "enum Status { Active, Inactive }",
|
|
39
|
-
errors: [{ messageId: "noEnum" }],
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,8 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/rules/no-raw-env.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"no-raw-env.test.d.ts","sourceRoot":"","sources":["../../src/rules/no-raw-env.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { RuleTester } from "eslint";
|
|
2
|
-
import { describe, it } from "vitest";
|
|
3
|
-
import { noRawEnv } from "./no-raw-env.js";
|
|
4
|
-
const ruleTester = new RuleTester({
|
|
5
|
-
languageOptions: {
|
|
6
|
-
ecmaVersion: 2022,
|
|
7
|
-
sourceType: "module",
|
|
8
|
-
},
|
|
9
|
-
});
|
|
10
|
-
describe("no-raw-env", () => {
|
|
11
|
-
it("should pass valid and fail invalid cases", () => {
|
|
12
|
-
ruleTester.run("no-raw-env", noRawEnv, {
|
|
13
|
-
valid: [
|
|
14
|
-
// Using validated env schema
|
|
15
|
-
{ code: 'const apiKey = env.API_KEY;' },
|
|
16
|
-
{ code: 'const dbUrl = config.databaseUrl;' },
|
|
17
|
-
{ code: 'const { NODE_ENV } = validatedEnv;' },
|
|
18
|
-
],
|
|
19
|
-
invalid: [
|
|
20
|
-
// Direct process.env access
|
|
21
|
-
{
|
|
22
|
-
code: 'const apiKey = process.env.API_KEY;',
|
|
23
|
-
errors: [{ messageId: "noRawEnv" }],
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
code: 'if (process.env.NODE_ENV === "production") {}',
|
|
27
|
-
errors: [{ messageId: "noRawEnv" }],
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
code: 'console.log(process.env.DEBUG);',
|
|
31
|
-
errors: [{ messageId: "noRawEnv" }],
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,50 +0,0 @@
|
|
|
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;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-shadcn.test.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-shadcn.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { RuleTester } from "eslint";
|
|
2
|
-
import { describe, it } from "vitest";
|
|
3
|
-
import { preferShadcn } from "./prefer-shadcn.js";
|
|
4
|
-
const ruleTester = new RuleTester({
|
|
5
|
-
languageOptions: {
|
|
6
|
-
ecmaVersion: 2022,
|
|
7
|
-
sourceType: "module",
|
|
8
|
-
parserOptions: {
|
|
9
|
-
ecmaFeatures: {
|
|
10
|
-
jsx: true,
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
describe("prefer-shadcn", () => {
|
|
16
|
-
it("should pass valid and fail invalid cases", () => {
|
|
17
|
-
ruleTester.run("prefer-shadcn", preferShadcn, {
|
|
18
|
-
valid: [
|
|
19
|
-
// shadcn components are OK
|
|
20
|
-
{ code: '<Button onClick={handleClick}>Click me</Button>' },
|
|
21
|
-
{ code: '<Input type="text" placeholder="Enter name" />' },
|
|
22
|
-
{ code: '<Select value={value} onValueChange={setValue} />' },
|
|
23
|
-
{ code: '<Table><TableBody /></Table>' },
|
|
24
|
-
// Other elements are OK
|
|
25
|
-
{ code: '<div className="container">Content</div>' },
|
|
26
|
-
{ code: '<span>Text</span>' },
|
|
27
|
-
{ code: '<form onSubmit={handleSubmit}></form>' },
|
|
28
|
-
],
|
|
29
|
-
invalid: [
|
|
30
|
-
// Native button
|
|
31
|
-
{
|
|
32
|
-
code: '<button onClick={handleClick}>Click me</button>',
|
|
33
|
-
errors: [{ messageId: "preferShadcn" }],
|
|
34
|
-
},
|
|
35
|
-
// Native input
|
|
36
|
-
{
|
|
37
|
-
code: '<input type="text" placeholder="Enter name" />',
|
|
38
|
-
errors: [{ messageId: "preferShadcn" }],
|
|
39
|
-
},
|
|
40
|
-
// Native select
|
|
41
|
-
{
|
|
42
|
-
code: '<select value={value} onChange={handleChange}><option>A</option></select>',
|
|
43
|
-
errors: [{ messageId: "preferShadcn" }],
|
|
44
|
-
},
|
|
45
|
-
// Native textarea
|
|
46
|
-
{
|
|
47
|
-
code: '<textarea rows={5} />',
|
|
48
|
-
errors: [{ messageId: "preferShadcn" }],
|
|
49
|
-
},
|
|
50
|
-
// Native table
|
|
51
|
-
{
|
|
52
|
-
code: '<table><tbody /></table>',
|
|
53
|
-
errors: [{ messageId: "preferShadcn" }],
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
});
|
|
@@ -1,8 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,53 +0,0 @@
|
|
|
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;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"require-assert-never.test.d.ts","sourceRoot":"","sources":["../../src/rules/require-assert-never.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { RuleTester } from "eslint";
|
|
2
|
-
import { describe, it } from "vitest";
|
|
3
|
-
import { requireAssertNever } from "./require-assert-never.js";
|
|
4
|
-
const ruleTester = new RuleTester({
|
|
5
|
-
languageOptions: {
|
|
6
|
-
ecmaVersion: 2022,
|
|
7
|
-
sourceType: "module",
|
|
8
|
-
},
|
|
9
|
-
});
|
|
10
|
-
describe("require-assert-never", () => {
|
|
11
|
-
it("should pass valid and fail invalid cases", () => {
|
|
12
|
-
ruleTester.run("require-assert-never", requireAssertNever, {
|
|
13
|
-
valid: [
|
|
14
|
-
// Switch with assertNever in default case
|
|
15
|
-
{
|
|
16
|
-
code: `
|
|
17
|
-
switch (status) {
|
|
18
|
-
case "active":
|
|
19
|
-
return 1;
|
|
20
|
-
default:
|
|
21
|
-
assertNever(status);
|
|
22
|
-
}
|
|
23
|
-
`,
|
|
24
|
-
},
|
|
25
|
-
// Switch with throw assertNever
|
|
26
|
-
{
|
|
27
|
-
code: `
|
|
28
|
-
switch (type) {
|
|
29
|
-
case "a":
|
|
30
|
-
break;
|
|
31
|
-
default:
|
|
32
|
-
throw assertNever(type);
|
|
33
|
-
}
|
|
34
|
-
`,
|
|
35
|
-
},
|
|
36
|
-
// Switch without default case is OK
|
|
37
|
-
{
|
|
38
|
-
code: `
|
|
39
|
-
switch (value) {
|
|
40
|
-
case 1:
|
|
41
|
-
return "one";
|
|
42
|
-
case 2:
|
|
43
|
-
return "two";
|
|
44
|
-
}
|
|
45
|
-
`,
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
invalid: [
|
|
49
|
-
// Default case without assertNever
|
|
50
|
-
{
|
|
51
|
-
code: `
|
|
52
|
-
switch (status) {
|
|
53
|
-
case "active":
|
|
54
|
-
return 1;
|
|
55
|
-
default:
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
`,
|
|
59
|
-
errors: [{ messageId: "missingAssertNever" }],
|
|
60
|
-
},
|
|
61
|
-
// Empty default case
|
|
62
|
-
{
|
|
63
|
-
code: `
|
|
64
|
-
switch (type) {
|
|
65
|
-
case "a":
|
|
66
|
-
break;
|
|
67
|
-
default:
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
`,
|
|
71
|
-
errors: [{ messageId: "missingAssertNever" }],
|
|
72
|
-
},
|
|
73
|
-
],
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
});
|