@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,289 @@
1
+ /**
2
+ * Disallow widening a syntactically known value to a broad type.
3
+ */
4
+
5
+ /*
6
+ * When the initializer is a literal, an object, an array or a function, the compiler already knows the type.
7
+ */
8
+
9
+ /*
10
+ * Annotating the binding `unknown`, `any`, `object` or `Record<string, unknown>` throws that away, and the next reader has to assert it back. Keep inference, or use `satisfies`, or name a contract.
11
+ */
12
+
13
+ /*
14
+ * Empty objects and empty arrays are exempt: they are accumulators, not evidence.
15
+ */
16
+
17
+ /*
18
+ * So are bindings that are mutated later in the file (`x.y = ...`, `Object.assign(x, ...)`): there the wide annotation is what makes the later writes legal, not laziness.
19
+ */
20
+
21
+ /*
22
+ * Simplified from dmmulroy/anti-slop (MIT). The original resolves aliases across a whole type environment; this checks the annotation and the initializer only, which is where the pattern actually shows up.
23
+ */
24
+
25
+ const BROAD_KEYWORDS = new Set(["TSUnknownKeyword", "TSAnyKeyword", "TSObjectKeyword"]);
26
+
27
+ const BROAD_RECORD_KEYS = new Set(["TSStringKeyword", "TSNumberKeyword", "TSSymbolKeyword"]);
28
+
29
+ /** Initializers whose type the compiler can read straight off the syntax. */
30
+ const KNOWN_VALUE_TYPES = new Set([
31
+ "ArrayExpression",
32
+ "ArrowFunctionExpression",
33
+ "ClassExpression",
34
+ "FunctionExpression",
35
+ "Literal",
36
+ "NewExpression",
37
+ "ObjectExpression",
38
+ "TemplateLiteral",
39
+ ]);
40
+
41
+ function unwrapType(type) {
42
+ let current = type;
43
+ while (current?.type === "TSParenthesizedType") current = current.typeAnnotation;
44
+ return current;
45
+ }
46
+
47
+ function unwrapExpression(node) {
48
+ let current = node;
49
+ while (current?.type === "ParenthesizedExpression") current = current.expression;
50
+ return current;
51
+ }
52
+
53
+ function typeReferenceName(type) {
54
+ return type.typeName?.type === "Identifier" ? type.typeName.name : null;
55
+ }
56
+
57
+ function isTopType(type) {
58
+ const unwrapped = unwrapType(type);
59
+ return unwrapped?.type === "TSUnknownKeyword" || unwrapped?.type === "TSAnyKeyword";
60
+ }
61
+
62
+ function isBroadRecordKey(type) {
63
+ const unwrapped = unwrapType(type);
64
+ if (!unwrapped) return false;
65
+ if (BROAD_RECORD_KEYS.has(unwrapped.type)) return true;
66
+ if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKey);
67
+ return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
68
+ }
69
+
70
+ /** `Record<string, unknown>` and the index-signature spelling of the same thing. */
71
+ function isBroadRecord(type) {
72
+ const unwrapped = unwrapType(type);
73
+ if (!unwrapped) return false;
74
+
75
+ if (unwrapped.type === "TSTypeReference") {
76
+ if (typeReferenceName(unwrapped) !== "Record") return false;
77
+ const params = unwrapped.typeArguments?.params ?? [];
78
+ return params.length === 2 && isBroadRecordKey(params[0]) && isTopType(params[1]);
79
+ }
80
+
81
+ if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
82
+ const [member] = unwrapped.members;
83
+ if (member?.type !== "TSIndexSignature" || member.parameters.length !== 1) return false;
84
+ const [parameter] = member.parameters;
85
+ return (
86
+ isBroadRecordKey(parameter?.typeAnnotation?.typeAnnotation) &&
87
+ isTopType(member.typeAnnotation?.typeAnnotation)
88
+ );
89
+ }
90
+
91
+ function broadTypeName(type) {
92
+ const unwrapped = unwrapType(type);
93
+ if (!unwrapped) return null;
94
+ if (BROAD_KEYWORDS.has(unwrapped.type)) {
95
+ return unwrapped.type === "TSObjectKeyword"
96
+ ? "object"
97
+ : unwrapped.type === "TSAnyKeyword"
98
+ ? "any"
99
+ : "unknown";
100
+ }
101
+ return isBroadRecord(unwrapped) ? "open dictionary" : null;
102
+ }
103
+
104
+ /** Accumulators start empty and get filled; that is not discarded evidence. */
105
+ function isEmptyContainer(node) {
106
+ const unwrapped = unwrapExpression(node);
107
+ if (unwrapped?.type === "ObjectExpression") return unwrapped.properties.length === 0;
108
+ if (unwrapped?.type === "ArrayExpression") return unwrapped.elements.length === 0;
109
+ return false;
110
+ }
111
+
112
+ /** `null` and `undefined` are placeholders awaiting a real value, not evidence. */
113
+ function isPlaceholder(node) {
114
+ const unwrapped = unwrapExpression(node);
115
+ if (unwrapped?.type === "Identifier") return unwrapped.name === "undefined";
116
+ return unwrapped?.type === "Literal" && unwrapped.value === null;
117
+ }
118
+
119
+ function isKnownValue(node) {
120
+ const unwrapped = unwrapExpression(node);
121
+ if (!unwrapped) return false;
122
+ if (isPlaceholder(unwrapped)) return false;
123
+ // `x as const` keeps its evidence; anything else asserted is already covered
124
+ // by no-chained-type-assertions and require-safety-comment-for-type-assertion.
125
+ if (unwrapped.type === "TSAsExpression") {
126
+ const annotation = unwrapped.typeAnnotation;
127
+ return (
128
+ annotation?.type === "TSTypeReference" &&
129
+ annotation.typeName?.type === "Identifier" &&
130
+ annotation.typeName.name === "const"
131
+ );
132
+ }
133
+ return KNOWN_VALUE_TYPES.has(unwrapped.type) && !isEmptyContainer(unwrapped);
134
+ }
135
+
136
+ /** `x.y = ...` and `Object.assign(x, ...)`: both need `x` to stay wide. */
137
+ function isMutationOf(node, names, reassigned) {
138
+ if (node.type === "AssignmentExpression") {
139
+ const { left } = node;
140
+ if (left?.type === "MemberExpression" && left.object?.type === "Identifier") {
141
+ names.add(left.object.name);
142
+ } else if (left?.type === "Identifier") {
143
+ reassigned.add(left.name);
144
+ }
145
+ return;
146
+ }
147
+ if (node.type !== "CallExpression") return;
148
+ const callee = node.callee;
149
+ if (
150
+ callee?.type === "MemberExpression" &&
151
+ callee.object?.type === "Identifier" &&
152
+ callee.object.name === "Object" &&
153
+ callee.property?.name === "assign"
154
+ ) {
155
+ const [target] = node.arguments;
156
+ if (target?.type === "Identifier") names.add(target.name);
157
+ }
158
+ }
159
+
160
+ function collectMutatedNames(root, names, reassigned) {
161
+ const stack = [root];
162
+ while (stack.length > 0) {
163
+ const node = stack.pop();
164
+ isMutationOf(node, names, reassigned);
165
+ for (const key of Object.keys(node)) {
166
+ if (key === "parent") continue;
167
+ const value = node[key];
168
+ if (Array.isArray(value)) {
169
+ for (const child of value) {
170
+ if (child != null && typeof child.type === "string") stack.push(child);
171
+ }
172
+ } else if (value != null && typeof value.type === "string") {
173
+ stack.push(value);
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ function referencesName(root, name) {
180
+ const stack = [root];
181
+ while (stack.length > 0) {
182
+ const current = stack.pop();
183
+ if (current.type === "Identifier" && current.name === name) return true;
184
+ for (const key of Object.keys(current)) {
185
+ if (key === "parent") continue;
186
+ const value = current[key];
187
+ if (Array.isArray(value)) {
188
+ for (const child of value) {
189
+ if (child != null && typeof child.type === "string") stack.push(child);
190
+ }
191
+ } else if (value != null && typeof value.type === "string") {
192
+ stack.push(value);
193
+ }
194
+ }
195
+ }
196
+ return false;
197
+ }
198
+
199
+ function functionOwnName(node) {
200
+ if (node.id?.name) return node.id.name;
201
+ const parent = node.parent;
202
+ if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") {
203
+ return parent.id.name;
204
+ }
205
+ return null;
206
+ }
207
+
208
+ /**
209
+ * A function that calls itself has no inferable return type, so the annotation
210
+ * is load-bearing rather than lazy. Removing it would only produce TS7023.
211
+ */
212
+ function isRecursive(node) {
213
+ const name = functionOwnName(node);
214
+ if (name === null || node.body == null) return false;
215
+ return referencesName(node.body, name);
216
+ }
217
+
218
+ export default {
219
+ meta: {
220
+ type: "problem",
221
+ docs: {
222
+ description:
223
+ "Disallow annotating a known value with a broad type that discards its evidence.",
224
+ },
225
+ messages: {
226
+ widening:
227
+ "The explicit `{{target}}` type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or name a contract.",
228
+ },
229
+ schema: [],
230
+ },
231
+ create(context) {
232
+ // WHY: name-based rather than scope-resolved. A shadowed name in the same
233
+ // file only ever costs a missed report, never a wrong one.
234
+ const mutatedNames = new Set();
235
+ const reassignedNames = new Set();
236
+
237
+ const report = (valueNode, annotation, subject) => {
238
+ const target = annotation ? broadTypeName(annotation.typeAnnotation) : null;
239
+ if (target === null || !isKnownValue(valueNode)) return;
240
+ context.report({ node: valueNode, messageId: "widening", data: { subject, target } });
241
+ };
242
+
243
+ return {
244
+ // WHY: a mutation sits below its declaration, so visitor order would reach
245
+ // the declarator first. Collect every mutated name up front instead.
246
+ Program(node) {
247
+ mutatedNames.clear();
248
+ reassignedNames.clear();
249
+ collectMutatedNames(node, mutatedNames, reassignedNames);
250
+ },
251
+ VariableDeclarator(node) {
252
+ if (node.init == null || node.id.type !== "Identifier") return;
253
+ if (mutatedNames.has(node.id.name)) return;
254
+ // A `let` that is reassigned later declares the type of its whole lifetime,
255
+ // not of its first value. `const` gets no such exemption.
256
+ if (node.parent?.kind !== "const" && reassignedNames.has(node.id.name)) return;
257
+ // A binding read inside its own initializer (a Proxy handler, a stub whose
258
+ // methods return it) has no inferable type; dropping the annotation gives TS7022.
259
+ if (referencesName(node.init, node.id.name)) return;
260
+ report(node.init, node.id.typeAnnotation, `binding \`${node.id.name}\``);
261
+ },
262
+ PropertyDefinition(node) {
263
+ if (node.value == null) return;
264
+ report(node.value, node.typeAnnotation, "this property");
265
+ },
266
+ ReturnStatement(node) {
267
+ if (node.argument == null) return;
268
+ let owner = node.parent;
269
+ while (owner && owner.type !== "Program") {
270
+ if (
271
+ owner.type === "ArrowFunctionExpression" ||
272
+ owner.type === "FunctionDeclaration" ||
273
+ owner.type === "FunctionExpression"
274
+ ) {
275
+ break;
276
+ }
277
+ owner = owner.parent;
278
+ }
279
+ if (!owner || owner.type === "Program") return;
280
+ if (isRecursive(owner)) return;
281
+ report(node.argument, owner.returnType, "this return value");
282
+ },
283
+ ArrowFunctionExpression(node) {
284
+ if (node.body.type === "BlockStatement" || isRecursive(node)) return;
285
+ report(node.body, node.returnType, "this return value");
286
+ },
287
+ };
288
+ },
289
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A re-export renamed to a leading `_` keeps a dead name alive. Compat words in comments are
3
+ * the comment gate's job: `comment-length.js`.
4
+ */
5
+ export default {
6
+ meta: {
7
+ type: "suggestion",
8
+ docs: {
9
+ description: "Disallow re-exports renamed to a leading `_`. Delete the unused name instead.",
10
+ },
11
+ messages: {
12
+ unusedRename:
13
+ 'Suspicious rename to "_{{ name }}". If the original is unused, delete it instead of renaming with a leading "_" prefix.',
14
+ },
15
+ schema: [],
16
+ },
17
+ create(context) {
18
+ return {
19
+ ExportNamedDeclaration(node) {
20
+ if (!node.source) return;
21
+ for (const specifier of node.specifiers) {
22
+ const exported = specifier.exported.name ?? specifier.exported.value;
23
+ const local = specifier.local.name ?? specifier.local.value;
24
+ if (exported !== local && exported.startsWith("_")) {
25
+ context.report({
26
+ node: specifier,
27
+ messageId: "unusedRename",
28
+ data: { name: exported },
29
+ });
30
+ }
31
+ }
32
+ },
33
+ };
34
+ },
35
+ };
@@ -0,0 +1,77 @@
1
+ // Warns about overkill nullish coalescing patterns: 1. Triple ?? chains: `a ?? b ?? c` (defensive over-engineering) 2. Trailing `?? null`: `x?.foo ?? null` (no-op, undefined is already nullish)
2
+
3
+ // 3. Trailing `?? ""`: `x ?? ""` (empty string fallbacks rarely needed) 4. `(x?.length ?? 0) > 0` (verbose, `x?.length` is truthy when > 0)
4
+
5
+ export default {
6
+ meta: {
7
+ type: "suggestion",
8
+ docs: {
9
+ description: "Disallow overkill nullish coalescing patterns (triple chains, ?? null, ?? '').",
10
+ },
11
+ messages: {
12
+ tripleChain: "Triple ?? chain is overkill. Simplify to at most two levels.",
13
+ trailingNull:
14
+ "'?? null' is redundant. The expression already evaluates to undefined/null without it.",
15
+ trailingEmptyString:
16
+ "'?? \"\"' is likely redundant. Template strings and most contexts handle undefined gracefully.",
17
+ verboseLengthCheck:
18
+ "'(x?.length ?? 0) > 0' is verbose. Use 'x?.length' as a truthy check or 'x.length > 0' if non-nullable.",
19
+ },
20
+ schema: [],
21
+ },
22
+ create(context) {
23
+ return {
24
+ LogicalExpression(node) {
25
+ if (node.operator !== "??") return;
26
+
27
+ // Rule 1: Triple ?? chain, a ?? b ?? c
28
+ // The outermost ?? has left side that is also a ??
29
+ if (node.left.type === "LogicalExpression" && node.left.operator === "??") {
30
+ context.report({ node, messageId: "tripleChain" });
31
+ return;
32
+ }
33
+
34
+ // Rule 2: Trailing ?? null
35
+ if (node.right.type === "Literal" && node.right.value === null) {
36
+ context.report({ node, messageId: "trailingNull" });
37
+ return;
38
+ }
39
+
40
+ // Rule 3: Trailing ?? ""
41
+ if (
42
+ node.right.type === "Literal" &&
43
+ node.right.value === "" &&
44
+ typeof node.right.value === "string"
45
+ ) {
46
+ context.report({ node, messageId: "trailingEmptyString" });
47
+ return;
48
+ }
49
+ },
50
+
51
+ // Rule 4: (x?.length ?? 0) > 0
52
+ BinaryExpression(node) {
53
+ if (node.operator !== ">" && node.operator !== ">=") return;
54
+ if (node.right.type !== "Literal" || node.right.value !== 0) return;
55
+
56
+ // Check if left is (x?.length ?? 0) wrapped in parens or not
57
+ const left = node.left;
58
+ if (
59
+ left.type === "LogicalExpression" &&
60
+ left.operator === "??" &&
61
+ left.right.type === "Literal" &&
62
+ left.right.value === 0
63
+ ) {
64
+ // Check that left.left ends with .length
65
+ const inner = left.left;
66
+ if (
67
+ inner.type === "MemberExpression" &&
68
+ inner.property.type === "Identifier" &&
69
+ inner.property.name === "length"
70
+ ) {
71
+ context.report({ node, messageId: "verboseLengthCheck" });
72
+ }
73
+ }
74
+ },
75
+ };
76
+ },
77
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Disallow raw try/catch blocks in favor of tryCatch() / tryCatchAsync().
3
+ * These helpers from @heyramzi/utils return a Result<T, E> for typed error handling.
4
+ */
5
+
6
+ export default {
7
+ meta: {
8
+ type: "suggestion",
9
+ docs: {
10
+ description:
11
+ "Disallow raw try/catch blocks. Use tryCatch() or tryCatchAsync() from @heyramzi/utils instead.",
12
+ },
13
+ messages: {
14
+ noRawTryCatch:
15
+ "Avoid raw try/catch. Use tryCatch() for sync or tryCatchAsync() for async contexts from @heyramzi/utils.",
16
+ },
17
+ schema: [],
18
+ },
19
+ create(context) {
20
+ return {
21
+ TryStatement(node) {
22
+ context.report({ node, messageId: "noRawTryCatch" });
23
+ },
24
+ };
25
+ },
26
+ };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Ban a `||` or `??` fallback that repeats the default the prop already has in its destructuring.
3
+ */
4
+
5
+ /*
6
+ * Bad: `let { font = 'sans' } = $props()` then `font || 'sans'`. A different fallback passes, since `null` and `""` slip past a default. Covers parameters, `$props()` and markup.
7
+ */
8
+ import { readTemplate } from "#lint/svelte-template.js";
9
+
10
+ /** A literal's identity as a string: `s:sans`, `n:3`, `b:true`, `null`. */
11
+ function literalKey(node) {
12
+ if (node?.type !== "Literal" || node.regex || typeof node.value === "bigint") return null;
13
+ if (node.value === null) return "null";
14
+ const kind = { string: "s", number: "n", boolean: "b" }[typeof node.value];
15
+ return kind ? `${kind}:${node.value}` : null;
16
+ }
17
+
18
+ /** `= "x"` and `= $bindable("x")` both default to "x". `$bindable()` defaults to nothing. */
19
+ function defaultKey(assignment) {
20
+ const right = assignment.right;
21
+ if (right.type === "CallExpression" && right.callee?.name === "$bindable") {
22
+ return literalKey(right.arguments[0]);
23
+ }
24
+ return literalKey(right);
25
+ }
26
+
27
+ function collectDefaults(pattern, into) {
28
+ if (pattern?.type !== "ObjectPattern") return;
29
+ for (const prop of pattern.properties) {
30
+ if (prop.type !== "Property" || prop.value?.type !== "AssignmentPattern") continue;
31
+ const name = prop.value.left?.name || prop.key?.name;
32
+ const key = defaultKey(prop.value);
33
+ if (name && key !== null) into.set(name, key);
34
+ }
35
+ }
36
+
37
+ function paramNames(pattern, into) {
38
+ if (!pattern) return;
39
+ if (pattern.type === "Identifier") into.add(pattern.name);
40
+ else if (pattern.type === "AssignmentPattern") paramNames(pattern.left, into);
41
+ else if (pattern.type === "RestElement") paramNames(pattern.argument, into);
42
+ else if (pattern.type === "ArrayPattern") pattern.elements.forEach((e) => paramNames(e, into));
43
+ else if (pattern.type === "ObjectPattern") {
44
+ for (const prop of pattern.properties) {
45
+ paramNames(prop.type === "RestElement" ? prop.argument : prop.value, into);
46
+ }
47
+ }
48
+ }
49
+
50
+ function escapeRegExp(text) {
51
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52
+ }
53
+
54
+ /** The key of a literal as written in markup: `'sans'`, `3`, `true`. */
55
+ function markupKey(token) {
56
+ if (/^["']/.test(token)) return `s:${token.slice(1, -1)}`;
57
+ if (/^-?\d/.test(token)) return `n:${Number(token)}`;
58
+ return token === "null" ? "null" : `b:${token}`;
59
+ }
60
+
61
+ export default {
62
+ meta: {
63
+ type: "suggestion",
64
+ docs: {
65
+ description:
66
+ "Disallow a || / ?? fallback that repeats the default a prop already has in its destructuring.",
67
+ },
68
+ messages: {
69
+ redundantFallback:
70
+ "'{{ name }}' already defaults to the same value in destructuring. Remove the '{{ op }}' fallback.",
71
+ redundantMarkupFallback:
72
+ "'{{ name }}' already defaults to the same value in `$props()`, but line {{ line }} falls back again with '{{ op }}'. Use '{{ name }}' alone.",
73
+ },
74
+ schema: [],
75
+ },
76
+ create(context) {
77
+ const scopeStack = [];
78
+ const propDefaults = new Map();
79
+
80
+ function enterFunction(node) {
81
+ const defaults = new Map();
82
+ const params = new Set();
83
+ for (const param of node.params || []) {
84
+ collectDefaults(param, defaults);
85
+ paramNames(param, params);
86
+ }
87
+ scopeStack.push({ defaults, params });
88
+ }
89
+
90
+ function exitFunction() {
91
+ scopeStack.pop();
92
+ }
93
+
94
+ function checkLogical(node) {
95
+ if (node.operator !== "||" && node.operator !== "??") return;
96
+ const { left, right } = node;
97
+ if (left.type !== "Identifier") return;
98
+ const rightKey = literalKey(right);
99
+ if (rightKey === null) return;
100
+
101
+ const current = scopeStack[scopeStack.length - 1];
102
+ const shadowed = scopeStack.some((scope) => scope.params.has(left.name));
103
+ const known =
104
+ current?.defaults.get(left.name) ?? (shadowed ? undefined : propDefaults.get(left.name));
105
+ if (known === rightKey) {
106
+ context.report({
107
+ node,
108
+ messageId: "redundantFallback",
109
+ data: { name: left.name, op: node.operator },
110
+ });
111
+ }
112
+ }
113
+
114
+ function checkMarkup() {
115
+ if (!context.filename.endsWith(".svelte") || propDefaults.size === 0) return;
116
+ const template = readTemplate(context.filename);
117
+ if (!template) return;
118
+ const blockText = template.blocks.map((block) => block.code).join("\n");
119
+ for (const [name, key] of propDefaults) {
120
+ // A loop variable or snippet parameter of the same name isn't the prop.
121
+ if (new RegExp(`(?<![\\w$.])${escapeRegExp(name)}(?![\\w$])`).test(blockText)) continue;
122
+ const fallback = new RegExp(
123
+ `(?<![\\w$.])${escapeRegExp(name)}\\s*(\\?\\?|\\|\\|)\\s*("[^"\\\\]*"|'[^'\\\\]*'|-?\\d[\\w.]*|true|false|null)(?![\\w$.(\\[])`,
124
+ );
125
+ for (const expression of template.expressions) {
126
+ const match = fallback.exec(expression.code);
127
+ if (!match || markupKey(match[2]) !== key) continue;
128
+ context.report({
129
+ loc: { line: 1, column: 0 },
130
+ messageId: "redundantMarkupFallback",
131
+ data: { name, op: match[1], line: String(expression.line) },
132
+ });
133
+ }
134
+ }
135
+ }
136
+
137
+ return {
138
+ FunctionDeclaration: enterFunction,
139
+ FunctionExpression: enterFunction,
140
+ ArrowFunctionExpression: enterFunction,
141
+ "FunctionDeclaration:exit": exitFunction,
142
+ "FunctionExpression:exit": exitFunction,
143
+ "ArrowFunctionExpression:exit": exitFunction,
144
+ VariableDeclarator(node) {
145
+ if (node.init?.type === "CallExpression" && node.init.callee?.name === "$props") {
146
+ collectDefaults(node.id, propDefaults);
147
+ }
148
+ },
149
+ LogicalExpression: checkLogical,
150
+ "Program:exit": checkMarkup,
151
+ };
152
+ },
153
+ };