@heyramzi/lint 0.0.0-stage → 1.32.2
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/LICENSE +21 -0
- package/README.md +277 -2
- package/bin/heyramzi-comment-length.js +99 -0
- package/bin/heyramzi-commit-msg.js +31 -0
- package/bin/heyramzi-file-tokens.js +66 -0
- package/bin/heyramzi-inline-path.js +79 -0
- package/bin/heyramzi-jev-code.js +107 -0
- package/bin/heyramzi-push-diff.js +93 -0
- package/bin/heyramzi-repo-rules.js +70 -0
- package/bin/heyramzi-slop.js +314 -0
- package/bin/heyramzi-svelte-markup.js +50 -0
- package/bin/heyramzi-ui-rules.js +56 -0
- package/comment-length.js +368 -0
- package/commit-msg.js +57 -0
- package/config/oxlintrc.json +87 -0
- package/data/invisibles.js +218 -0
- package/data/jev-bench.js +115 -0
- package/data/jev-code-bench.js +313 -0
- package/data/slop-words.d.ts +64 -0
- package/data/slop-words.js +2681 -0
- package/file-tokens.js +122 -0
- package/gate-files.js +14 -0
- package/index.js +116 -0
- package/inline-path.js +90 -0
- package/jev-code.js +194 -0
- package/jev.js +341 -0
- package/package.json +80 -4
- package/processors/markdown.js +22 -0
- package/push-diff.js +55 -0
- package/register.d.ts +43 -0
- package/register.js +255 -0
- package/repo-rules/packages.js +164 -0
- package/repo-rules/paths.js +101 -0
- package/repo-rules/toolchain.js +172 -0
- package/repo-rules/tree.js +51 -0
- package/repo-rules/workflows.js +116 -0
- package/repo-rules.js +54 -0
- package/rules/convex-ast.js +66 -0
- package/rules/convex-index-name.js +40 -0
- package/rules/convex-internal-only.js +59 -0
- package/rules/convex-no-date-now-in-query.js +48 -0
- package/rules/convex-no-deprecated-crons.js +40 -0
- package/rules/convex-no-filter.js +41 -0
- package/rules/convex-no-get-metadata.js +26 -0
- package/rules/convex-no-v-any.js +24 -0
- package/rules/convex-react-client-module-scope.js +37 -0
- package/rules/convex-require-args.js +39 -0
- package/rules/convex-require-returns.js +31 -0
- package/rules/convex-runtime-imports.js +48 -0
- package/rules/max-function-lines.js +105 -0
- package/rules/max-object-methods.js +86 -0
- package/rules/no-ai-words.js +78 -0
- package/rules/no-app-stores.js +25 -0
- package/rules/no-chained-type-assertions.js +91 -0
- package/rules/no-class.js +47 -0
- package/rules/no-conditional-empty-object-spread.js +64 -0
- package/rules/no-deep-phosphor-imports.js +29 -0
- package/rules/no-default-export.js +70 -0
- package/rules/no-em-dash.js +194 -0
- package/rules/no-emoji.js +56 -0
- package/rules/no-hardcoded-models.js +63 -0
- package/rules/no-inline-path.js +46 -0
- package/rules/no-inline-svg.js +67 -0
- package/rules/no-known-value-widening.js +289 -0
- package/rules/no-legacy-compat.js +35 -0
- package/rules/no-overkill-fallback.js +77 -0
- package/rules/no-raw-try-catch.js +26 -0
- package/rules/no-redundant-prop-fallback.js +153 -0
- package/rules/no-relative-imports.js +213 -0
- package/rules/no-relative-type-imports.js +40 -0
- package/rules/no-rune-outside-svelte-file.js +56 -0
- package/rules/no-rune-shadowing-binding.js +85 -0
- package/rules/no-skipped-or-focused-tests.js +74 -0
- package/rules/no-snake-case-props.js +45 -0
- package/rules/no-svelte4-syntax.js +33 -0
- package/rules/no-vitest-import-in-vite-plus.js +64 -0
- package/rules/props-interface.js +44 -0
- package/rules/redirect-external-needs-option.js +45 -0
- package/rules/require-safety-comment-for-type-assertion.js +110 -0
- package/rules/svelte-section-order.js +115 -0
- package/rules/test-file.js +8 -0
- package/scripts/count-hits.mjs +213 -0
- package/scripts/harvest-voice.mjs +161 -0
- package/scripts/jev-bench.mjs +41 -0
- package/scripts/jev-code-bench.mjs +61 -0
- package/scripts/measure-register.mjs +61 -0
- package/slop.d.ts +53 -0
- package/slop.js +644 -0
- package/svelte-markup.js +107 -0
- package/svelte-template.js +258 -0
- package/ui-rules.js +324 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
// SvelteKit generates `./$types` beside each route; it has no alias.
|
|
6
|
+
const EXEMPT = new Set(["./$types", "./$types.js"]);
|
|
7
|
+
const TEST_CALLS = new Set(["mock", "doMock", "unmock", "importActual", "requireActual"]);
|
|
8
|
+
// Vite and friends load these before any alias exists, so only package.json `imports` is safe there.
|
|
9
|
+
const CONFIG_FILE = /\.config\.[cm]?[jt]s$/;
|
|
10
|
+
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
const memo = (key, fn) => {
|
|
13
|
+
if (!cache.has(key)) cache.set(key, fn());
|
|
14
|
+
return cache.get(key);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** JSONC to JSON: drops comments and trailing commas, leaves strings alone. */
|
|
18
|
+
function parseJsonc(text) {
|
|
19
|
+
let out = "";
|
|
20
|
+
for (let i = 0; i < text.length; i++) {
|
|
21
|
+
const c = text[i];
|
|
22
|
+
if (c === '"') {
|
|
23
|
+
let j = i + 1;
|
|
24
|
+
while (j < text.length && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
|
|
25
|
+
out += text.slice(i, j + 1);
|
|
26
|
+
i = j;
|
|
27
|
+
} else if (c === "/" && text[i + 1] === "/") {
|
|
28
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
29
|
+
out += "\n";
|
|
30
|
+
} else if (c === "/" && text[i + 1] === "*") {
|
|
31
|
+
i = text.indexOf("*/", i + 2) + 1;
|
|
32
|
+
} else out += c;
|
|
33
|
+
}
|
|
34
|
+
return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readJson(file) {
|
|
38
|
+
return memo(`json:${file}`, () => {
|
|
39
|
+
if (!existsSync(file)) return null;
|
|
40
|
+
try {
|
|
41
|
+
return parseJsonc(readFileSync(file, "utf8"));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findUp(dir, name) {
|
|
49
|
+
for (let d = dir; ; d = dirname(d)) {
|
|
50
|
+
if (existsSync(join(d, name))) return join(d, name);
|
|
51
|
+
if (dirname(d) === d) return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveExtends(from, spec) {
|
|
56
|
+
if (spec.startsWith("."))
|
|
57
|
+
return resolve(dirname(from), spec.endsWith(".json") ? spec : `${spec}.json`);
|
|
58
|
+
const req = createRequire(from);
|
|
59
|
+
for (const s of [spec, `${spec}.json`, `${spec}/tsconfig.json`]) {
|
|
60
|
+
try {
|
|
61
|
+
return req.resolve(s);
|
|
62
|
+
} catch {
|
|
63
|
+
// try the next form
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Merged tsconfig: paths with the directory they resolve from, plus the include globs. */
|
|
70
|
+
function loadTsconfig(file) {
|
|
71
|
+
return memo(`ts:${file}`, () => {
|
|
72
|
+
const json = readJson(file);
|
|
73
|
+
if (!json) return null;
|
|
74
|
+
const dir = dirname(file);
|
|
75
|
+
let merged = { paths: null, base: null, baseUrl: null, include: null, root: dir };
|
|
76
|
+
const parents = [json.extends ?? []].flat();
|
|
77
|
+
for (const p of parents) {
|
|
78
|
+
const parent = resolveExtends(file, p);
|
|
79
|
+
const loaded = parent && loadTsconfig(parent);
|
|
80
|
+
if (loaded) Object.assign(merged, loaded, { root: dir });
|
|
81
|
+
}
|
|
82
|
+
const opts = json.compilerOptions ?? {};
|
|
83
|
+
if (opts.baseUrl) merged.baseUrl = resolve(dir, opts.baseUrl);
|
|
84
|
+
if (opts.paths) Object.assign(merged, { paths: opts.paths, base: dir });
|
|
85
|
+
if (json.include || json.files) {
|
|
86
|
+
merged.include = [...(json.include ?? []), ...(json.files ?? [])].map((g) => resolve(dir, g));
|
|
87
|
+
}
|
|
88
|
+
return merged;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const globToRegExp = (glob) =>
|
|
93
|
+
new RegExp(
|
|
94
|
+
`^${glob
|
|
95
|
+
.split(sep)
|
|
96
|
+
.join("/")
|
|
97
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
98
|
+
.replace(/\*\*\//g, "\uE000")
|
|
99
|
+
.replace(/\*\*/g, "\uE001")
|
|
100
|
+
.replace(/\*/g, "[^/]*")
|
|
101
|
+
.replace(/\uE000/g, "(?:.*/)?")
|
|
102
|
+
.replace(/\uE001/g, ".*")}(?:/.*)?$`,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const included = (ts, file) =>
|
|
106
|
+
!ts.include
|
|
107
|
+
? file.startsWith(ts.root + sep)
|
|
108
|
+
: ts.include.some((g) => globToRegExp(g).test(file.split(sep).join("/")));
|
|
109
|
+
|
|
110
|
+
/** `X/*` → `Y/*` pairs only: an alias that maps a directory, as [prefix, absolute dir]. */
|
|
111
|
+
function wildcardPairs(map, base) {
|
|
112
|
+
const pairs = [];
|
|
113
|
+
for (const [key, value] of Object.entries(map ?? {})) {
|
|
114
|
+
let target = [value].flat()[0];
|
|
115
|
+
if (target && typeof target === "object") target = target.import ?? target.default;
|
|
116
|
+
if (typeof target !== "string" || !key.endsWith("/*") || !target.endsWith("/*")) continue;
|
|
117
|
+
pairs.push([key.slice(0, -1), resolve(base, target.slice(0, -2))]);
|
|
118
|
+
}
|
|
119
|
+
return pairs;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Aliases usable from `file`: package.json `imports` first, since Node, tsx, Vite and esbuild all honour them. */
|
|
123
|
+
function aliasesFor(file) {
|
|
124
|
+
const dir = dirname(file);
|
|
125
|
+
const pkgFile = findUp(dir, "package.json");
|
|
126
|
+
const pkg = pkgFile && readJson(pkgFile);
|
|
127
|
+
const aliases = wildcardPairs(pkg?.imports, pkgFile ? dirname(pkgFile) : dir);
|
|
128
|
+
const tsFile = findUp(dir, "tsconfig.json");
|
|
129
|
+
const ts = tsFile && loadTsconfig(tsFile);
|
|
130
|
+
const runsUnbundled = /\.[cm]js$/.test(file) || CONFIG_FILE.test(basename(file));
|
|
131
|
+
if (ts?.paths && !runsUnbundled && included(ts, file)) {
|
|
132
|
+
aliases.push(...wildcardPairs(ts.paths, ts.baseUrl ?? ts.base));
|
|
133
|
+
}
|
|
134
|
+
return aliases;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const firstArg = (node) => node.arguments?.[0];
|
|
138
|
+
|
|
139
|
+
/** The aliased specifier, or null. The deepest alias directory wins; on a tie tsconfig `paths` beat package.json `imports`. */
|
|
140
|
+
export function toAlias(file, source) {
|
|
141
|
+
const target = resolve(dirname(file), source);
|
|
142
|
+
let best = null;
|
|
143
|
+
for (const [prefix, dir] of aliasesFor(file)) {
|
|
144
|
+
if (!target.startsWith(dir + sep)) continue;
|
|
145
|
+
if (!best || dir.length >= best[1].length) best = [prefix, dir];
|
|
146
|
+
}
|
|
147
|
+
if (!best) return null;
|
|
148
|
+
const rest = relative(best[1], target).split(sep).join("/");
|
|
149
|
+
return best[0] + rest + (source.endsWith("/") ? "/" : "");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default {
|
|
153
|
+
meta: {
|
|
154
|
+
type: "suggestion",
|
|
155
|
+
fixable: "code",
|
|
156
|
+
docs: {
|
|
157
|
+
description:
|
|
158
|
+
"Disallow relative module specifiers. Use a path alias from tsconfig `paths` or package.json `imports`.",
|
|
159
|
+
},
|
|
160
|
+
messages: {
|
|
161
|
+
noRelativeImport: "Use a path alias instead of relative import '{{ source }}'.",
|
|
162
|
+
noAlias:
|
|
163
|
+
"Relative import '{{ source }}' has no alias covering its target. Add one to tsconfig `paths` or package.json `imports`.",
|
|
164
|
+
},
|
|
165
|
+
schema: [],
|
|
166
|
+
},
|
|
167
|
+
create(context) {
|
|
168
|
+
const file = context.physicalFilename ?? context.filename;
|
|
169
|
+
const check = (literal) => {
|
|
170
|
+
if (!literal || typeof literal.value !== "string") return;
|
|
171
|
+
const source = literal.value;
|
|
172
|
+
if (
|
|
173
|
+
!source.startsWith("./") &&
|
|
174
|
+
!source.startsWith("../") &&
|
|
175
|
+
source !== "." &&
|
|
176
|
+
source !== ".."
|
|
177
|
+
)
|
|
178
|
+
return;
|
|
179
|
+
if (EXEMPT.has(source)) return;
|
|
180
|
+
const alias = toAlias(file, source);
|
|
181
|
+
if (!alias) {
|
|
182
|
+
context.report({ node: literal, messageId: "noAlias", data: { source } });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const quote = context.sourceCode.getText(literal)[0];
|
|
186
|
+
context.report({
|
|
187
|
+
node: literal,
|
|
188
|
+
messageId: "noRelativeImport",
|
|
189
|
+
data: { source },
|
|
190
|
+
fix: (fixer) => fixer.replaceText(literal, quote + alias + quote),
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
ImportDeclaration: (node) => check(node.source),
|
|
195
|
+
ExportNamedDeclaration: (node) => check(node.source),
|
|
196
|
+
ExportAllDeclaration: (node) => check(node.source),
|
|
197
|
+
ImportExpression: (node) => check(node.source),
|
|
198
|
+
TSImportType: (node) => check(node.source),
|
|
199
|
+
TSExternalModuleReference: (node) => check(node.expression),
|
|
200
|
+
CallExpression(node) {
|
|
201
|
+
const { callee } = node;
|
|
202
|
+
if (callee.type === "Identifier" && callee.name === "require") check(firstArg(node));
|
|
203
|
+
if (
|
|
204
|
+
callee.type === "MemberExpression" &&
|
|
205
|
+
callee.object.type === "Identifier" &&
|
|
206
|
+
["vi", "jest"].includes(callee.object.name) &&
|
|
207
|
+
TEST_CALLS.has(callee.property.name)
|
|
208
|
+
)
|
|
209
|
+
check(firstArg(node));
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "suggestion",
|
|
4
|
+
deprecated: true,
|
|
5
|
+
replacedBy: ["no-relative-imports"],
|
|
6
|
+
docs: {
|
|
7
|
+
description: "Disallow relative imports for type-only imports. Use @/ path alias instead.",
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
noRelativeTypeImport:
|
|
11
|
+
"Use @/ path alias for type imports instead of relative import '{{ source }}'.",
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
ImportDeclaration(node) {
|
|
18
|
+
if (node.importKind === "type" || hasTypeImport(node)) {
|
|
19
|
+
const source = node.source.value;
|
|
20
|
+
if (source === "./$types") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (source.startsWith(".")) {
|
|
24
|
+
context.report({
|
|
25
|
+
node: node.source,
|
|
26
|
+
messageId: "noRelativeTypeImport",
|
|
27
|
+
data: { source },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function hasTypeImport(node) {
|
|
37
|
+
return node.specifiers.some(
|
|
38
|
+
(specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban a rune (`$state`, `$derived`, `$effect`, `$props`...) in a file Svelte doesn't compile.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Runes work in `.svelte`, `.svelte.ts` and `.svelte.js` only. In a plain `.ts` file `$derived` throws `rune_outside_svelte` at runtime. Rename the file to `.svelte.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const RUNES = new Set([
|
|
10
|
+
"$state",
|
|
11
|
+
"$derived",
|
|
12
|
+
"$effect",
|
|
13
|
+
"$props",
|
|
14
|
+
"$bindable",
|
|
15
|
+
"$inspect",
|
|
16
|
+
"$host",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const COMPILED = /\.svelte(\.[^./\\]+)?\.[jt]s$|\.svelte$/;
|
|
20
|
+
|
|
21
|
+
function runeOf(callee) {
|
|
22
|
+
if (callee.type === "Identifier") return callee.name;
|
|
23
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier") {
|
|
24
|
+
return callee.object.name;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
meta: {
|
|
31
|
+
type: "problem",
|
|
32
|
+
docs: { description: "Disallow runes outside .svelte, .svelte.ts and .svelte.js files." },
|
|
33
|
+
messages: {
|
|
34
|
+
runeOutside:
|
|
35
|
+
"`{{ rune }}` only works in a `.svelte`, `.svelte.ts` or `.svelte.js` file; here it throws `rune_outside_svelte`. Rename this file to `{{ suggested }}`.",
|
|
36
|
+
},
|
|
37
|
+
schema: [],
|
|
38
|
+
},
|
|
39
|
+
create(context) {
|
|
40
|
+
const filename = context.filename;
|
|
41
|
+
if (COMPILED.test(filename) || filename.endsWith(".d.ts")) return {};
|
|
42
|
+
if (!/\.[cm]?[jt]sx?$/.test(filename)) return {};
|
|
43
|
+
return {
|
|
44
|
+
CallExpression(node) {
|
|
45
|
+
const rune = runeOf(node.callee);
|
|
46
|
+
if (!rune || !RUNES.has(rune)) return;
|
|
47
|
+
const base = filename.split(/[\\/]/).pop();
|
|
48
|
+
context.report({
|
|
49
|
+
node,
|
|
50
|
+
messageId: "runeOutside",
|
|
51
|
+
data: { rune, suggested: base.replace(/\.[cm]?([jt]s)x?$/, ".svelte.$1") },
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban a top-level binding named like a rune (`state`, `derived`, `effect`...) in a file that calls that rune.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* `let { state } = $props()` makes `$state(...)` read as a store subscription and the call throws `store_invalid_shape`. Svelte only warns, `store_rune_conflict`, so a push never stopped it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const RUNES = new Set(["state", "derived", "effect", "inspect", "host", "bindable"]);
|
|
10
|
+
|
|
11
|
+
function boundNames(pattern, out) {
|
|
12
|
+
if (!pattern) return;
|
|
13
|
+
switch (pattern.type) {
|
|
14
|
+
case "Identifier":
|
|
15
|
+
out.push(pattern);
|
|
16
|
+
break;
|
|
17
|
+
case "ObjectPattern":
|
|
18
|
+
for (const prop of pattern.properties) {
|
|
19
|
+
boundNames(prop.type === "RestElement" ? prop.argument : prop.value, out);
|
|
20
|
+
}
|
|
21
|
+
break;
|
|
22
|
+
case "ArrayPattern":
|
|
23
|
+
for (const element of pattern.elements) boundNames(element, out);
|
|
24
|
+
break;
|
|
25
|
+
case "AssignmentPattern":
|
|
26
|
+
boundNames(pattern.left, out);
|
|
27
|
+
break;
|
|
28
|
+
case "RestElement":
|
|
29
|
+
boundNames(pattern.argument, out);
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function declaredNames(statement, out) {
|
|
36
|
+
const declaration =
|
|
37
|
+
statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
38
|
+
if (!declaration) return;
|
|
39
|
+
if (declaration.type === "VariableDeclaration") {
|
|
40
|
+
for (const declarator of declaration.declarations) boundNames(declarator.id, out);
|
|
41
|
+
} else if (
|
|
42
|
+
declaration.type === "FunctionDeclaration" ||
|
|
43
|
+
declaration.type === "ClassDeclaration"
|
|
44
|
+
) {
|
|
45
|
+
if (declaration.id) out.push(declaration.id);
|
|
46
|
+
} else if (statement.type === "ImportDeclaration") {
|
|
47
|
+
for (const specifier of statement.specifiers) out.push(specifier.local);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
meta: {
|
|
53
|
+
type: "problem",
|
|
54
|
+
docs: { description: "Disallow top-level bindings that shadow a rune's name." },
|
|
55
|
+
messages: {
|
|
56
|
+
shadowsRune:
|
|
57
|
+
"`{{ name }}` collides with the `${{ name }}` rune: the compiler reads `${{ name }}(...)` as a store subscription and it throws `store_invalid_shape` at runtime. Rename it (`{{ name }}Value`, `current{{ cap }}`).",
|
|
58
|
+
},
|
|
59
|
+
schema: [],
|
|
60
|
+
},
|
|
61
|
+
create(context) {
|
|
62
|
+
if (
|
|
63
|
+
!context.filename.endsWith(".svelte") &&
|
|
64
|
+
!/\.svelte(\.[^./\\]+)?\.[jt]s$/.test(context.filename)
|
|
65
|
+
) {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
Program(program) {
|
|
70
|
+
const source = context.sourceCode.text;
|
|
71
|
+
const names = [];
|
|
72
|
+
for (const statement of program.body) declaredNames(statement, names);
|
|
73
|
+
for (const id of names) {
|
|
74
|
+
if (!RUNES.has(id.name)) continue;
|
|
75
|
+
if (!new RegExp(`\\$${id.name}\\s*[(<]`).test(source)) continue;
|
|
76
|
+
context.report({
|
|
77
|
+
node: id,
|
|
78
|
+
messageId: "shadowsRune",
|
|
79
|
+
data: { name: id.name, cap: id.name[0].toUpperCase() + id.name.slice(1) },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban a test left skipped or focused: `.only`, `xit`, `it.skip("name", fn)`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* A skipped test hides a failure and a focused one drops the rest of the suite. Playwright's conditional `test.skip(isCI, "reason")` passes: it has a condition, not a title.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const SUITE = new Set(["describe", "it", "test", "suite", "bench"]);
|
|
10
|
+
const X_NAMES = new Set(["xdescribe", "xit", "xtest"]);
|
|
11
|
+
const F_NAMES = new Set(["fdescribe", "fit", "ftest"]);
|
|
12
|
+
const FUNCTIONS = new Set(["ArrowFunctionExpression", "FunctionExpression"]);
|
|
13
|
+
|
|
14
|
+
const isTitle = (node) =>
|
|
15
|
+
node?.type === "TemplateLiteral" || (node?.type === "Literal" && typeof node.value === "string");
|
|
16
|
+
|
|
17
|
+
/** `it("name", fn)`: a title, then a body. Keeps a helper that happens to be called `fit` clear. */
|
|
18
|
+
function looksLikeTest(args) {
|
|
19
|
+
return isTitle(args[0]) && args.slice(1).some((arg) => FUNCTIONS.has(arg.type));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Walks `test.describe.skip` down to the identifier `test`. */
|
|
23
|
+
function baseName(node) {
|
|
24
|
+
let current = node;
|
|
25
|
+
while (current?.type === "MemberExpression") current = current.object;
|
|
26
|
+
return current?.type === "Identifier" ? current.name : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function modifierOf(callee) {
|
|
30
|
+
if (callee.type !== "MemberExpression" || callee.computed) return null;
|
|
31
|
+
const name = callee.property.name;
|
|
32
|
+
if (name !== "skip" && name !== "only") return null;
|
|
33
|
+
return SUITE.has(baseName(callee.object)) ? name : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default {
|
|
37
|
+
meta: {
|
|
38
|
+
type: "problem",
|
|
39
|
+
docs: { description: "Disallow skipped and focused tests." },
|
|
40
|
+
messages: {
|
|
41
|
+
only: "`.only` runs this test alone and silently drops the rest of the suite. Remove it.",
|
|
42
|
+
skip: "A skipped test hides a failure. Fix it or delete it; never `.skip` or `x`-prefix it to get a green build. A conditional `test.skip(condition, 'reason')` is fine.",
|
|
43
|
+
focused: "`{{ name }}` focuses this test and drops the rest of the suite. Use `{{ plain }}`.",
|
|
44
|
+
disabled: "`{{ name }}` disables the test. Fix it or delete it, then use `{{ plain }}`.",
|
|
45
|
+
},
|
|
46
|
+
schema: [],
|
|
47
|
+
},
|
|
48
|
+
create(context) {
|
|
49
|
+
return {
|
|
50
|
+
CallExpression(node) {
|
|
51
|
+
const callee = node.callee;
|
|
52
|
+
if (callee.type === "Identifier") {
|
|
53
|
+
if (!looksLikeTest(node.arguments)) return;
|
|
54
|
+
const plain = callee.name.slice(1);
|
|
55
|
+
if (F_NAMES.has(callee.name)) {
|
|
56
|
+
context.report({ node, messageId: "focused", data: { name: callee.name, plain } });
|
|
57
|
+
} else if (X_NAMES.has(callee.name)) {
|
|
58
|
+
context.report({ node, messageId: "disabled", data: { name: callee.name, plain } });
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const modifier = modifierOf(callee);
|
|
63
|
+
if (modifier === "only") {
|
|
64
|
+
context.report({ node, messageId: "only" });
|
|
65
|
+
} else if (modifier === "skip") {
|
|
66
|
+
const [first, second] = node.arguments;
|
|
67
|
+
if (!first || isTitle(first) || FUNCTIONS.has(second?.type)) {
|
|
68
|
+
context.report({ node, messageId: "skip" });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const SNAKE_CASE_RE = /^[a-z]+(_[a-z0-9]+)+$/;
|
|
2
|
+
|
|
3
|
+
function toCamelCase(name) {
|
|
4
|
+
return name.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Use camelCase properties; quote fixed wire-format and database keys.",
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
snakeCaseProp:
|
|
15
|
+
"Property '{{ name }}' uses snake_case. Rename to '{{ suggested }}' internally, or quote a fixed external key.",
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
function checkPropertyName(node, nameNode) {
|
|
21
|
+
// Quoted keys name fixed protocol fields; destructuring reads an existing field.
|
|
22
|
+
if (node.computed || node.parent?.type === "ObjectPattern" || nameNode.type !== "Identifier")
|
|
23
|
+
return;
|
|
24
|
+
const name = nameNode.name;
|
|
25
|
+
if (SNAKE_CASE_RE.test(name)) {
|
|
26
|
+
context.report({
|
|
27
|
+
node: nameNode,
|
|
28
|
+
messageId: "snakeCaseProp",
|
|
29
|
+
data: { name, suggested: toCamelCase(name) },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
// Object literal properties: { company_name: "foo" }
|
|
36
|
+
Property(node) {
|
|
37
|
+
if (node.key) checkPropertyName(node, node.key);
|
|
38
|
+
},
|
|
39
|
+
// TypeScript interface/type properties: company_name: string
|
|
40
|
+
TSPropertySignature(node) {
|
|
41
|
+
if (node.key) checkPropertyName(node, node.key);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban Svelte 4 script syntax: `export let` and `$:`. Write Svelte 5.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Markup (`on:click`, `<slot>`, `<svelte:component>`) can't reach oxlint, so `heyramzi-svelte-markup` reads it. Props are `$props()`, derived values `$derived`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: { description: "Disallow `export let` and `$:` in .svelte files." },
|
|
13
|
+
messages: {
|
|
14
|
+
exportLet: "`export let` is Svelte 4. Declare props with `let { name } = $props()`.",
|
|
15
|
+
reactiveLabel:
|
|
16
|
+
"`$:` is Svelte 4. Use `$derived` for a value that follows other state, `$effect` for a side effect.",
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
if (!context.filename.endsWith(".svelte")) return {};
|
|
22
|
+
return {
|
|
23
|
+
ExportNamedDeclaration(node) {
|
|
24
|
+
if (node.declaration?.type === "VariableDeclaration" && node.declaration.kind === "let") {
|
|
25
|
+
context.report({ node, messageId: "exportLet" });
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
LabeledStatement(node) {
|
|
29
|
+
if (node.label.name === "$") context.report({ node, messageId: "reactiveLabel" });
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In a Vite+ package that has no `vitest` of its own, import test helpers from `vite-plus/test`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Under `vp test` the `vitest` re-export fails with "Cannot read properties of undefined (reading 'config')". A package that depends on vitest directly runs standalone and isn't touched.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
|
|
13
|
+
function declares(pkg, name) {
|
|
14
|
+
return ["dependencies", "devDependencies", "peerDependencies"].some(
|
|
15
|
+
(field) => pkg[field] && name in pkg[field],
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Walks up to the repo root: the nearest package.json that names vitest or vite-plus decides. */
|
|
20
|
+
function usesVitePlusRunner(dir) {
|
|
21
|
+
if (cache.has(dir)) return cache.get(dir);
|
|
22
|
+
let verdict = null;
|
|
23
|
+
const manifest = path.join(dir, "package.json");
|
|
24
|
+
if (existsSync(manifest)) {
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(readFileSync(manifest, "utf8"));
|
|
27
|
+
if (declares(pkg, "vitest")) verdict = false;
|
|
28
|
+
else if (declares(pkg, "vite-plus")) verdict = true;
|
|
29
|
+
} catch {
|
|
30
|
+
verdict = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (verdict === null) {
|
|
34
|
+
const parent = path.dirname(dir);
|
|
35
|
+
verdict =
|
|
36
|
+
existsSync(path.join(dir, ".git")) || parent === dir ? false : usesVitePlusRunner(parent);
|
|
37
|
+
}
|
|
38
|
+
cache.set(dir, verdict);
|
|
39
|
+
return verdict;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
meta: {
|
|
44
|
+
type: "problem",
|
|
45
|
+
docs: { description: "Disallow importing from vitest in a Vite+ package." },
|
|
46
|
+
messages: {
|
|
47
|
+
vitestImport:
|
|
48
|
+
'Import from "vite-plus/test", not "vitest". Under `vp test` the vitest re-export fails with "Cannot read properties of undefined (reading \'config\')".',
|
|
49
|
+
},
|
|
50
|
+
schema: [],
|
|
51
|
+
},
|
|
52
|
+
create(context) {
|
|
53
|
+
if (!usesVitePlusRunner(path.dirname(context.filename))) return {};
|
|
54
|
+
return {
|
|
55
|
+
ImportDeclaration(node) {
|
|
56
|
+
if (node.importKind === "type" || node.source.value !== "vitest") return;
|
|
57
|
+
context.report({ node, messageId: "vitestImport" });
|
|
58
|
+
},
|
|
59
|
+
ImportExpression(node) {
|
|
60
|
+
if (node.source?.value === "vitest") context.report({ node, messageId: "vitestImport" });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `type Props = {...}` in a .svelte file. Write `interface Props {...}`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* A union or a conditional type can't be an interface, so those stay allowed. The fix rewrites an object-literal alias, the common case.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const NOT_AN_INTERFACE = new Set(["TSUnionType", "TSConditionalType", "TSMappedType"]);
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "suggestion",
|
|
14
|
+
fixable: "code",
|
|
15
|
+
docs: { description: "Require `interface Props` over `type Props` in Svelte components." },
|
|
16
|
+
messages: {
|
|
17
|
+
typeProps:
|
|
18
|
+
"Declare component props as `interface Props { ... }`, not `type Props`. Extend another type with `interface Props extends Other { ... }`.",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
if (!context.filename.endsWith(".svelte")) return {};
|
|
24
|
+
return {
|
|
25
|
+
TSTypeAliasDeclaration(node) {
|
|
26
|
+
if (node.id.name !== "Props") return;
|
|
27
|
+
if (NOT_AN_INTERFACE.has(node.typeAnnotation.type)) return;
|
|
28
|
+
const fixable =
|
|
29
|
+
node.typeAnnotation.type === "TSTypeLiteral" && !node.typeParameters && !node.declare;
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: "typeProps",
|
|
33
|
+
fix: fixable
|
|
34
|
+
? (fixer) =>
|
|
35
|
+
fixer.replaceTextRange(
|
|
36
|
+
[node.range[0], node.typeAnnotation.range[0]],
|
|
37
|
+
"interface Props ",
|
|
38
|
+
)
|
|
39
|
+
: undefined,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|