@heyramzi/lint 0.0.0-stage → 1.32.4

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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +277 -2
  3. package/bin/heyramzi-comment-length.js +99 -0
  4. package/bin/heyramzi-commit-msg.js +31 -0
  5. package/bin/heyramzi-file-tokens.js +66 -0
  6. package/bin/heyramzi-inline-path.js +79 -0
  7. package/bin/heyramzi-jev-code.js +107 -0
  8. package/bin/heyramzi-push-diff.js +93 -0
  9. package/bin/heyramzi-repo-rules.js +70 -0
  10. package/bin/heyramzi-slop.js +314 -0
  11. package/bin/heyramzi-svelte-markup.js +50 -0
  12. package/bin/heyramzi-ui-rules.js +56 -0
  13. package/comment-length.js +368 -0
  14. package/commit-msg.js +57 -0
  15. package/config/oxlintrc.json +87 -0
  16. package/data/invisibles.js +218 -0
  17. package/data/jev-bench.js +115 -0
  18. package/data/jev-code-bench.js +313 -0
  19. package/data/slop-words.d.ts +64 -0
  20. package/data/slop-words.js +2681 -0
  21. package/file-tokens.js +122 -0
  22. package/gate-files.js +14 -0
  23. package/index.js +116 -0
  24. package/inline-path.js +90 -0
  25. package/jev-code.js +194 -0
  26. package/jev.js +341 -0
  27. package/package.json +81 -4
  28. package/processors/markdown.js +22 -0
  29. package/push-diff.js +55 -0
  30. package/register.d.ts +43 -0
  31. package/register.js +255 -0
  32. package/repo-rules/packages.js +164 -0
  33. package/repo-rules/paths.js +101 -0
  34. package/repo-rules/toolchain.js +172 -0
  35. package/repo-rules/tree.js +51 -0
  36. package/repo-rules/workflows.js +116 -0
  37. package/repo-rules.js +54 -0
  38. package/rules/convex-ast.js +66 -0
  39. package/rules/convex-index-name.js +40 -0
  40. package/rules/convex-internal-only.js +59 -0
  41. package/rules/convex-no-date-now-in-query.js +48 -0
  42. package/rules/convex-no-deprecated-crons.js +40 -0
  43. package/rules/convex-no-filter.js +41 -0
  44. package/rules/convex-no-get-metadata.js +26 -0
  45. package/rules/convex-no-v-any.js +24 -0
  46. package/rules/convex-react-client-module-scope.js +37 -0
  47. package/rules/convex-require-args.js +39 -0
  48. package/rules/convex-require-returns.js +31 -0
  49. package/rules/convex-runtime-imports.js +48 -0
  50. package/rules/max-function-lines.js +105 -0
  51. package/rules/max-object-methods.js +86 -0
  52. package/rules/no-ai-words.js +78 -0
  53. package/rules/no-app-stores.js +25 -0
  54. package/rules/no-chained-type-assertions.js +91 -0
  55. package/rules/no-class.js +47 -0
  56. package/rules/no-conditional-empty-object-spread.js +64 -0
  57. package/rules/no-deep-phosphor-imports.js +29 -0
  58. package/rules/no-default-export.js +70 -0
  59. package/rules/no-em-dash.js +198 -0
  60. package/rules/no-emoji.js +56 -0
  61. package/rules/no-hardcoded-models.js +63 -0
  62. package/rules/no-inline-path.js +46 -0
  63. package/rules/no-inline-svg.js +67 -0
  64. package/rules/no-known-value-widening.js +289 -0
  65. package/rules/no-legacy-compat.js +35 -0
  66. package/rules/no-overkill-fallback.js +77 -0
  67. package/rules/no-raw-try-catch.js +26 -0
  68. package/rules/no-redundant-prop-fallback.js +153 -0
  69. package/rules/no-relative-imports.js +213 -0
  70. package/rules/no-relative-type-imports.js +40 -0
  71. package/rules/no-rune-outside-svelte-file.js +56 -0
  72. package/rules/no-rune-shadowing-binding.js +85 -0
  73. package/rules/no-skipped-or-focused-tests.js +74 -0
  74. package/rules/no-snake-case-props.js +45 -0
  75. package/rules/no-svelte4-syntax.js +33 -0
  76. package/rules/no-vitest-import-in-vite-plus.js +64 -0
  77. package/rules/props-interface.js +44 -0
  78. package/rules/redirect-external-needs-option.js +45 -0
  79. package/rules/require-safety-comment-for-type-assertion.js +110 -0
  80. package/rules/svelte-section-order.js +115 -0
  81. package/rules/test-file.js +8 -0
  82. package/scripts/count-hits.mjs +213 -0
  83. package/scripts/harvest-voice.mjs +161 -0
  84. package/scripts/jev-bench.mjs +41 -0
  85. package/scripts/jev-code-bench.mjs +61 -0
  86. package/scripts/measure-register.mjs +61 -0
  87. package/slop-views.js +122 -0
  88. package/slop.d.ts +53 -0
  89. package/slop.js +534 -0
  90. package/svelte-markup.js +107 -0
  91. package/svelte-template.js +258 -0
  92. package/ui-rules.js +324 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `.filter()` on a Convex query builder reads the whole range and drops rows in JS.
3
+ */
4
+ import { propertyName, stepDown } from "#lint/rules/convex-ast.js";
5
+
6
+ const TERMINALS = new Set(["collect", "take", "first", "unique", "paginate"]);
7
+
8
+ function isDbQuery(call) {
9
+ return propertyName(call.callee) === "query" && propertyName(call.callee.object) === "db";
10
+ }
11
+
12
+ /** True when the chain under `.filter` is a query builder that nothing has run yet. */
13
+ function buildsQuery(start) {
14
+ for (let node = start; node; node = stepDown(node)) {
15
+ if (node.type !== "CallExpression") continue;
16
+ const method = propertyName(node.callee);
17
+ if (method && TERMINALS.has(method)) return false;
18
+ if (isDbQuery(node)) return true;
19
+ }
20
+ return false;
21
+ }
22
+
23
+ export default {
24
+ meta: {
25
+ type: "problem",
26
+ docs: { description: "Disallow `.filter()` chained on `ctx.db.query()`. Use `withIndex`." },
27
+ messages: {
28
+ noFilter:
29
+ "`.filter()` reads the whole table and throws rows away in JS, and the read is billed. Add the index to `schema.ts` and use `.withIndex(name, (q) => q.eq(field, value))`.",
30
+ },
31
+ schema: [],
32
+ },
33
+ create(context) {
34
+ return {
35
+ CallExpression(node) {
36
+ if (propertyName(node.callee) !== "filter") return;
37
+ if (buildsQuery(node.callee.object)) context.report({ node, messageId: "noFilter" });
38
+ },
39
+ };
40
+ },
41
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `ctx.storage.getMetadata` is deprecated.
3
+ */
4
+ import { propertyName } from "#lint/rules/convex-ast.js";
5
+
6
+ export default {
7
+ meta: {
8
+ type: "problem",
9
+ docs: { description: "Disallow the deprecated `storage.getMetadata`." },
10
+ messages: {
11
+ noGetMetadata:
12
+ '`storage.getMetadata` is deprecated. Use `ctx.db.system.get("_storage", storageId)`.',
13
+ },
14
+ schema: [],
15
+ },
16
+ create(context) {
17
+ return {
18
+ CallExpression(node) {
19
+ if (propertyName(node.callee) !== "getMetadata") return;
20
+ if (propertyName(node.callee.object) === "storage") {
21
+ context.report({ node, messageId: "noGetMetadata" });
22
+ }
23
+ },
24
+ };
25
+ },
26
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `v.any()` turns a validator off.
3
+ */
4
+ export default {
5
+ meta: {
6
+ type: "problem",
7
+ docs: { description: "Disallow `v.any()`. Name the shape." },
8
+ messages: {
9
+ noAny:
10
+ "`v.any()` switches validation off. Use `v.object({...})`, `v.record(v.string(), ...)` or `v.union(...)`.",
11
+ },
12
+ schema: [],
13
+ },
14
+ create(context) {
15
+ return {
16
+ CallExpression(node) {
17
+ const callee = node.callee;
18
+ if (callee.type !== "MemberExpression" || callee.computed) return;
19
+ if (callee.object.type !== "Identifier" || callee.object.name !== "v") return;
20
+ if (callee.property.name === "any") context.report({ node, messageId: "noAny" });
21
+ },
22
+ };
23
+ },
24
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `new ConvexReactClient` belongs at module scope.
3
+ */
4
+ export default {
5
+ meta: {
6
+ type: "problem",
7
+ docs: { description: "Disallow creating `ConvexReactClient` inside a function or component." },
8
+ messages: {
9
+ moduleScope:
10
+ "`new ConvexReactClient` inside a function builds a new client on every call. Create it once at module scope and import it.",
11
+ },
12
+ schema: [],
13
+ },
14
+ create(context) {
15
+ let depth = 0;
16
+ const enter = () => {
17
+ depth++;
18
+ };
19
+ const exit = () => {
20
+ depth--;
21
+ };
22
+ return {
23
+ FunctionDeclaration: enter,
24
+ "FunctionDeclaration:exit": exit,
25
+ FunctionExpression: enter,
26
+ "FunctionExpression:exit": exit,
27
+ ArrowFunctionExpression: enter,
28
+ "ArrowFunctionExpression:exit": exit,
29
+ NewExpression(node) {
30
+ if (depth === 0) return;
31
+ if (node.callee.type === "Identifier" && node.callee.name === "ConvexReactClient") {
32
+ context.report({ node, messageId: "moduleScope" });
33
+ }
34
+ },
35
+ };
36
+ },
37
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A registered Convex function takes an object and declares `args`.
3
+ */
4
+ import { findProperty, registerName } from "#lint/rules/convex-ast.js";
5
+
6
+ export default {
7
+ meta: {
8
+ type: "problem",
9
+ docs: {
10
+ description: "Require the object syntax and an `args` validator on every Convex function.",
11
+ },
12
+ messages: {
13
+ oldSyntax:
14
+ "`{{ name }}(async (ctx) => ...)` is the old function syntax. Pass an object: `{{ name }}({ args: {...}, returns: v.null(), handler: async (ctx, args) => ... })`.",
15
+ missingArgs:
16
+ "`{{ name }}` has no `args`. Without a validator it's an unvalidated public endpoint. Add `args: {}` for none.",
17
+ },
18
+ schema: [],
19
+ },
20
+ create(context) {
21
+ return {
22
+ CallExpression(node) {
23
+ const name = registerName(node);
24
+ if (!name || node.arguments.length !== 1) return;
25
+ const [config] = node.arguments;
26
+ if (config.type === "ArrowFunctionExpression" || config.type === "FunctionExpression") {
27
+ context.report({ node, messageId: "oldSyntax", data: { name } });
28
+ return;
29
+ }
30
+ if (config.type !== "ObjectExpression") return;
31
+ if (config.properties.some((property) => property.type === "SpreadElement")) return;
32
+ if (!findProperty(config, "handler")) return;
33
+ if (!findProperty(config, "args")) {
34
+ context.report({ node, messageId: "missingArgs", data: { name } });
35
+ }
36
+ },
37
+ };
38
+ },
39
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A registered Convex function declares a `returns` validator.
3
+ */
4
+ import { findProperty, registerName } from "#lint/rules/convex-ast.js";
5
+
6
+ export default {
7
+ meta: {
8
+ type: "problem",
9
+ docs: { description: "Require a `returns` validator on every Convex function." },
10
+ messages: {
11
+ missingReturns:
12
+ "`{{ name }}` has no `returns`. Add a validator for what the handler returns, `v.null()` for nothing.",
13
+ },
14
+ schema: [],
15
+ },
16
+ create(context) {
17
+ return {
18
+ CallExpression(node) {
19
+ const name = registerName(node);
20
+ if (!name || node.arguments.length !== 1) return;
21
+ const [config] = node.arguments;
22
+ if (config.type !== "ObjectExpression") return;
23
+ if (config.properties.some((property) => property.type === "SpreadElement")) return;
24
+ if (!findProperty(config, "handler")) return;
25
+ if (!findProperty(config, "returns")) {
26
+ context.report({ node, messageId: "missingReturns", data: { name } });
27
+ }
28
+ },
29
+ };
30
+ },
31
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Node built-ins need `"use node"`, and a `"use node"` file holds actions only.
3
+ */
4
+ import { builtinModules } from "node:module";
5
+ import { hasUseNode, registerName } from "#lint/rules/convex-ast.js";
6
+
7
+ const NODE_ONLY = new Set(["query", "mutation", "internalQuery", "internalMutation"]);
8
+
9
+ function isBuiltin(source) {
10
+ if (source.startsWith("node:")) return true;
11
+ return builtinModules.includes(source.split("/")[0]);
12
+ }
13
+
14
+ export default {
15
+ meta: {
16
+ type: "problem",
17
+ docs: {
18
+ description: 'Keep Node built-ins in `"use node"` files and keep queries out of them.',
19
+ },
20
+ messages: {
21
+ needsUseNode:
22
+ 'Importing "{{ source }}" needs `"use node"` as the first line, and that file can then hold actions only. Move the Node work into an `internalAction` in its own file.',
23
+ actionsOnly:
24
+ 'A `"use node"` file can only define actions, and `{{ name }}` isn\'t one. Move it to a file without the directive.',
25
+ },
26
+ schema: [],
27
+ },
28
+ create(context) {
29
+ let useNode = false;
30
+ return {
31
+ Program(node) {
32
+ useNode = hasUseNode(node);
33
+ },
34
+ ImportDeclaration(node) {
35
+ if (useNode || node.importKind === "type") return;
36
+ const source = node.source.value;
37
+ if (typeof source === "string" && isBuiltin(source)) {
38
+ context.report({ node, messageId: "needsUseNode", data: { source } });
39
+ }
40
+ },
41
+ CallExpression(node) {
42
+ if (!useNode) return;
43
+ const name = registerName(node);
44
+ if (name && NODE_ONLY.has(name)) context.report({ node, messageId: "actionsOnly", data: { name } });
45
+ },
46
+ };
47
+ },
48
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Cap function body length (excluding blank lines and comments).
3
+ *
4
+ * Encourages small, focused functions. Default max: 30 lines.
5
+ * Counts only meaningful lines inside the function body.
6
+ */
7
+ function getFunctionName(node) {
8
+ // Named function declaration
9
+ if (node.id) return node.id.name;
10
+
11
+ // Variable declarator: const foo = () => {}
12
+ if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id) {
13
+ return node.parent.id.name;
14
+ }
15
+
16
+ // Object method: { foo() {} }
17
+ if (node.parent && node.parent.type === "Property" && node.parent.key) {
18
+ return node.parent.key.name || node.parent.key.value || "anonymous";
19
+ }
20
+
21
+ // Class method
22
+ if (node.parent && node.parent.type === "MethodDefinition" && node.parent.key) {
23
+ return node.parent.key.name || node.parent.key.value || "anonymous";
24
+ }
25
+
26
+ return "anonymous";
27
+ }
28
+
29
+ export default {
30
+ meta: {
31
+ type: "suggestion",
32
+ docs: {
33
+ description: "Enforce a maximum number of meaningful lines per function body.",
34
+ },
35
+ messages: {
36
+ tooLong:
37
+ "Function '{{ name }}' has {{ count }} lines (max {{ max }}). Break it into smaller functions.",
38
+ },
39
+ schema: [
40
+ {
41
+ type: "object",
42
+ properties: {
43
+ max: { type: "integer", minimum: 1 },
44
+ },
45
+ additionalProperties: false,
46
+ },
47
+ ],
48
+ },
49
+ create(context) {
50
+ const max = (context.options[0] && context.options[0].max) || 30;
51
+ const sourceCode = context.sourceCode || context.getSourceCode();
52
+
53
+ function countMeaningfulLines(node) {
54
+ const body = node.body;
55
+ if (!body || body.type !== "BlockStatement") return 0;
56
+
57
+ const startLine = body.loc.start.line + 1; // Skip opening brace
58
+ const endLine = body.loc.end.line; // Skip closing brace
59
+
60
+ if (startLine >= endLine) return 0;
61
+
62
+ let count = 0;
63
+ for (let line = startLine; line < endLine; line++) {
64
+ const lineText = sourceCode.lines[line - 1];
65
+ if (!lineText) continue;
66
+
67
+ const trimmed = lineText.trim();
68
+
69
+ // Skip blank lines
70
+ if (trimmed === "") continue;
71
+
72
+ // Skip single-line comments
73
+ if (trimmed.startsWith("//")) continue;
74
+
75
+ // Skip comment-only lines (block comments)
76
+ if (trimmed.startsWith("/*") || trimmed.startsWith("*")) continue;
77
+
78
+ count++;
79
+ }
80
+
81
+ return count;
82
+ }
83
+
84
+ function check(node) {
85
+ const count = countMeaningfulLines(node);
86
+ if (count > max) {
87
+ context.report({
88
+ node,
89
+ messageId: "tooLong",
90
+ data: {
91
+ name: getFunctionName(node),
92
+ count: String(count),
93
+ max: String(max),
94
+ },
95
+ });
96
+ }
97
+ }
98
+
99
+ return {
100
+ FunctionDeclaration: check,
101
+ FunctionExpression: check,
102
+ ArrowFunctionExpression: check,
103
+ };
104
+ },
105
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Cap the number of methods on an exported const object.
3
+ */
4
+
5
+ /*
6
+ * When a const wrapper grows too large, split it into smaller objects by sub-domain. Default max: 10 methods.
7
+ */
8
+
9
+ /*
10
+ * Only counts callable properties (methods, arrow functions, function expressions). Plain values and spreads are ignored. Only applies to exported const declarations.
11
+ */
12
+ function isCallable(property) {
13
+ if (property.type !== "Property") return false;
14
+
15
+ // Method shorthand: { foo() {} }
16
+ if (property.method) return true;
17
+
18
+ const value = property.value;
19
+ if (!value) return false;
20
+
21
+ // Arrow function: { foo: () => {} }
22
+ if (value.type === "ArrowFunctionExpression") return true;
23
+
24
+ // Function expression: { foo: function() {} }
25
+ if (value.type === "FunctionExpression") return true;
26
+
27
+ return false;
28
+ }
29
+
30
+ export default {
31
+ meta: {
32
+ type: "suggestion",
33
+ docs: {
34
+ description: "Enforce a maximum number of methods on exported const objects.",
35
+ },
36
+ messages: {
37
+ tooMany:
38
+ "Object '{{ name }}' has {{ count }} methods (max {{ max }}). Split into smaller objects by sub-domain.",
39
+ },
40
+ schema: [
41
+ {
42
+ type: "object",
43
+ properties: {
44
+ max: { type: "integer", minimum: 1 },
45
+ },
46
+ additionalProperties: false,
47
+ },
48
+ ],
49
+ },
50
+ create(context) {
51
+ const max = (context.options[0] && context.options[0].max) || 10;
52
+
53
+ return {
54
+ ExportNamedDeclaration(node) {
55
+ const decl = node.declaration;
56
+ if (!decl || decl.type !== "VariableDeclaration") return;
57
+ if (decl.kind !== "const") return;
58
+
59
+ for (const declarator of decl.declarations) {
60
+ if (!declarator.init) continue;
61
+
62
+ const init = declarator.init;
63
+
64
+ // Handle `{ ... } as const` via TSAsExpression
65
+ const obj = init.type === "TSAsExpression" ? init.expression : init;
66
+
67
+ if (obj.type !== "ObjectExpression") continue;
68
+
69
+ const methodCount = obj.properties.filter(isCallable).length;
70
+
71
+ if (methodCount > max) {
72
+ context.report({
73
+ node: declarator,
74
+ messageId: "tooMany",
75
+ data: {
76
+ name: declarator.id.name || "anonymous",
77
+ count: String(methodCount),
78
+ max: String(max),
79
+ },
80
+ });
81
+ }
82
+ }
83
+ },
84
+ };
85
+ },
86
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Ban AI-generated slop words and phrases in string literals.
3
+ * Word list sourced from hardikpandya/stop-slop + local additions.
4
+ */
5
+
6
+ import { words as BANNED_WORDS, phrases as BANNED_PHRASES } from "#lint/data/slop-words.js";
7
+
8
+ const wordRegex = new RegExp(`\\b(${BANNED_WORDS.join("|")})\\b`, "gi");
9
+ const phraseRegexes = BANNED_PHRASES.map((p) => ({
10
+ phrase: p,
11
+ regex: new RegExp(p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"),
12
+ }));
13
+
14
+ function isUserFacingString(node) {
15
+ if (node.parent?.type === "ImportDeclaration") return false;
16
+ if (node.parent?.type === "ImportExpression") return false;
17
+ if (node.parent?.type === "CallExpression" && node.parent.callee?.name === "require")
18
+ return false;
19
+ if (node.parent?.type === "Property" && node.parent.key === node) return false;
20
+ if (node.parent?.type === "MemberExpression" && node.parent.property === node) return false;
21
+ return true;
22
+ }
23
+
24
+ export default {
25
+ meta: {
26
+ type: "suggestion",
27
+ docs: {
28
+ description: "Disallow AI-generated slop words and phrases in string literals.",
29
+ url: "https://github.com/hardikpandya/stop-slop",
30
+ },
31
+ messages: {
32
+ bannedWord: 'AI slop: "{{ word }}". Reword to sound human.',
33
+ bannedPhrase: 'AI slop: "{{ phrase }}". Reword to sound human.',
34
+ },
35
+ schema: [],
36
+ },
37
+ create(context) {
38
+ function checkValue(value, node) {
39
+ if (typeof value !== "string") return;
40
+ if (value.length < 10) return;
41
+
42
+ for (const match of value.matchAll(wordRegex)) {
43
+ context.report({
44
+ node,
45
+ messageId: "bannedWord",
46
+ data: { word: match[1] },
47
+ });
48
+ }
49
+
50
+ for (const { phrase, regex } of phraseRegexes) {
51
+ if (regex.test(value)) {
52
+ context.report({
53
+ node,
54
+ messageId: "bannedPhrase",
55
+ data: { phrase },
56
+ });
57
+ }
58
+ }
59
+ }
60
+
61
+ return {
62
+ Literal(node) {
63
+ if (!isUserFacingString(node)) return;
64
+ checkValue(node.value, node);
65
+ },
66
+ TemplateLiteral(node) {
67
+ if (!isUserFacingString(node)) return;
68
+ for (const quasi of node.quasis) {
69
+ checkValue(quasi.value.cooked, node);
70
+ }
71
+ },
72
+ // JSON support
73
+ JSONLiteral(node) {
74
+ checkValue(node.value, node);
75
+ },
76
+ };
77
+ },
78
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Ban `$app/stores`. Read `page`, `navigating` and `updated` from `$app/state`.
3
+ */
4
+ const message =
5
+ "`$app/stores` is the Svelte 4 API. Use `import { page } from '$app/state'` and read `page.url`, not `$page.url`.";
6
+
7
+ export default {
8
+ meta: {
9
+ type: "problem",
10
+ docs: { description: "Disallow importing from $app/stores." },
11
+ messages: { appStores: message },
12
+ schema: [],
13
+ },
14
+ create(context) {
15
+ const check = (node) => {
16
+ if (node.source?.value === "$app/stores") context.report({ node, messageId: "appStores" });
17
+ };
18
+ return {
19
+ ImportDeclaration: check,
20
+ ExportNamedDeclaration: check,
21
+ ExportAllDeclaration: check,
22
+ ImportExpression: check,
23
+ };
24
+ },
25
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Disallow chained type assertions (`x as unknown as Foo`, `<Foo><unknown>x`).
3
+ */
4
+
5
+ /*
6
+ * A chain launders a value through a top type so the second assertion always compiles. The result is a claim with no evidence behind it.
7
+ */
8
+
9
+ /*
10
+ * Keep the precise type, or parse the value at its boundary before narrowing it.
11
+ */
12
+
13
+ /*
14
+ * Chains built only of `as const` are allowed.
15
+ */
16
+
17
+ /*
18
+ * Ported from dmmulroy/anti-slop (MIT).
19
+ */
20
+
21
+ const ASSERTION_TYPES = new Set(["TSAsExpression", "TSTypeAssertion"]);
22
+
23
+ function isAssertion(node) {
24
+ return node != null && ASSERTION_TYPES.has(node.type);
25
+ }
26
+
27
+ function unwrapParens(node) {
28
+ let current = node;
29
+ while (current?.type === "ParenthesizedExpression") current = current.expression;
30
+ return current;
31
+ }
32
+
33
+ function isConstAssertion(node) {
34
+ const annotation = node.typeAnnotation;
35
+ return (
36
+ annotation?.type === "TSTypeReference" &&
37
+ annotation.typeName?.type === "Identifier" &&
38
+ annotation.typeName.name === "const"
39
+ );
40
+ }
41
+
42
+ /** Only report the outermost assertion of a chain, so one chain yields one diagnostic. */
43
+ function isOutermost(node) {
44
+ let current = node;
45
+ let parent = node.parent;
46
+ while (parent?.type === "ParenthesizedExpression" && parent.expression === current) {
47
+ current = parent;
48
+ parent = parent.parent;
49
+ }
50
+ return !isAssertion(parent) || parent.expression !== current;
51
+ }
52
+
53
+ function isForbiddenChain(node) {
54
+ let count = 0;
55
+ let hasNonConst = false;
56
+ let current = node;
57
+
58
+ while (isAssertion(current)) {
59
+ count += 1;
60
+ if (!isConstAssertion(current)) hasNonConst = true;
61
+ current = unwrapParens(current.expression);
62
+ }
63
+
64
+ return count > 1 && hasNonConst;
65
+ }
66
+
67
+ export default {
68
+ meta: {
69
+ type: "problem",
70
+ docs: {
71
+ description:
72
+ "Disallow chained type assertions. They fabricate evidence by routing a value through a top type.",
73
+ },
74
+ messages: {
75
+ chained:
76
+ "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.",
77
+ },
78
+ schema: [],
79
+ },
80
+ create(context) {
81
+ const check = (node) => {
82
+ if (!isOutermost(node) || !isForbiddenChain(node)) return;
83
+ context.report({ node, messageId: "chained" });
84
+ };
85
+
86
+ return {
87
+ TSAsExpression: check,
88
+ TSTypeAssertion: check,
89
+ };
90
+ },
91
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Ban class declarations. Only Error subclasses are allowed.
3
+ *
4
+ * Use const wrapper objects or factory functions instead.
5
+ * Error subclasses (extends Error, extends *Error) are allowed
6
+ * because instanceof discrimination requires class syntax.
7
+ */
8
+ export default {
9
+ meta: {
10
+ type: "problem",
11
+ docs: {
12
+ description:
13
+ "Disallow class declarations. Use const wrappers or factory functions. Only Error subclasses are allowed.",
14
+ },
15
+ messages: {
16
+ noClass:
17
+ "Use a const wrapper object or factory function instead of a class. Classes are only allowed for Error subclasses.",
18
+ },
19
+ schema: [],
20
+ },
21
+ create(context) {
22
+ /** Names that require class syntax (Error hierarchy) */
23
+ const ALLOWED_SUPERCLASSES = /Error$/;
24
+
25
+ function check(node) {
26
+ const superClass = node.superClass;
27
+
28
+ if (superClass) {
29
+ const superName =
30
+ superClass.type === "Identifier"
31
+ ? superClass.name
32
+ : superClass.type === "MemberExpression" && superClass.property.type === "Identifier"
33
+ ? superClass.property.name
34
+ : null;
35
+
36
+ if (superName && ALLOWED_SUPERCLASSES.test(superName)) return;
37
+ }
38
+
39
+ context.report({ node, messageId: "noClass" });
40
+ }
41
+
42
+ return {
43
+ ClassDeclaration: check,
44
+ ClassExpression: check,
45
+ };
46
+ },
47
+ };