chore: update lint rules

This commit is contained in:
2026-09-11 07:39:51 +05:00
parent 87bd4482a3
commit fa5f7660c7
12 changed files with 532 additions and 79 deletions
@@ -8,15 +8,15 @@
// (see __tests__/helpers.ts). The fixture dictionary has exactly five
// tokens; anything else must be reported even if it exists in the REAL
// web/src/app.css.
import { RuleTester, Linter } from "eslint";
import { RuleTester } from "eslint";
import svelteParser from "svelte-eslint-parser";
import tseslint from "typescript-eslint";
import { describe, expect, it } from "vitest";
import noHardcoded from "../design-tokens/no-hardcoded-in-svelte.js";
import noCategoryMismatch from "../design-tokens/no-category-mismatch.js";
import noHardcoded from "../design-tokens/no-hardcoded-in-svelte.js";
import noTokenDefinition from "../design-tokens/no-token-definition-in-svelte.js";
import noUndefined from "../design-tokens/no-undefined-in-svelte.js";
import { verifyInFixtures, type FlatConfig } from "./helpers.js";
import { asRuleModule, verifyInFixtures, type FlatConfig } from "./helpers.js";
const parserOptions = {
parser: tseslint.parser,
@@ -45,7 +45,7 @@ const frame = (style: string) => ({
});
describe("design-tokens/no-hardcoded-in-svelte", () => {
ruleTester.run("no-hardcoded-in-svelte", noHardcoded, {
ruleTester.run("no-hardcoded-in-svelte", asRuleModule(noHardcoded), {
valid: [
frame(".box { color: var(--color-fg); }"),
frame(".box { padding: var(--space-3); }"),
@@ -97,7 +97,7 @@ describe("design-tokens/no-hardcoded-in-svelte", () => {
});
describe("design-tokens/no-category-mismatch", () => {
ruleTester.run("no-category-mismatch", noCategoryMismatch, {
ruleTester.run("no-category-mismatch", asRuleModule(noCategoryMismatch), {
valid: [
frame(".box { padding: var(--space-1); }"),
frame(".box { color: var(--color-fg); }"),
@@ -117,27 +117,31 @@ describe("design-tokens/no-category-mismatch", () => {
});
describe("design-tokens/no-token-definition-in-svelte", () => {
ruleTester.run("no-token-definition-in-svelte", noTokenDefinition, {
valid: [
frame(".box { --local: var(--color-fg); }"),
frame(".box { --local: calc(var(--space-1) * 2); }"),
frame(".box { --ratio: 1.5; }"),
],
invalid: [
{
...frame(".box { --local: #ff0000; }"),
errors: [{ messageId: "tokenPrimitive" }],
},
{
...frame(".box { --local: 12px; }"),
errors: [{ messageId: "tokenPrimitive" }],
},
{
...frame(".box { --local: oklch(0.5 0.1 240); }"),
errors: [{ messageId: "tokenPrimitive" }],
},
],
});
ruleTester.run(
"no-token-definition-in-svelte",
asRuleModule(noTokenDefinition),
{
valid: [
frame(".box { --local: var(--color-fg); }"),
frame(".box { --local: calc(var(--space-1) * 2); }"),
frame(".box { --ratio: 1.5; }"),
],
invalid: [
{
...frame(".box { --local: #ff0000; }"),
errors: [{ messageId: "tokenPrimitive" }],
},
{
...frame(".box { --local: 12px; }"),
errors: [{ messageId: "tokenPrimitive" }],
},
{
...frame(".box { --local: oklch(0.5 0.1 240); }"),
errors: [{ messageId: "tokenPrimitive" }],
},
],
},
);
});
describe("design-tokens/no-undefined-in-svelte", () => {
@@ -145,7 +149,9 @@ describe("design-tokens/no-undefined-in-svelte", () => {
{
files: ["**/*.svelte"],
plugins: {
"design-tokens": { rules: { "no-undefined-in-svelte": noUndefined } },
"design-tokens": {
rules: { "no-undefined-in-svelte": asRuleModule(noUndefined) },
},
},
rules: { "design-tokens/no-undefined-in-svelte": "error" },
languageOptions: {
+16
View File
@@ -9,6 +9,7 @@
// `verifyInFixtures` runs a rule with cwd fixed to `__fixtures__/`, so both
// work without touching process.cwd() or the real src/ tree.
import { Linter } from "eslint";
import type { Rule } from "eslint";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -32,3 +33,18 @@ export function verifyInFixtures(
const linter = new Linter({ configType: "flat", cwd: FIXTURES_DIR });
return linter.verify(code, config, path.join(FIXTURES_SRC, relFile));
}
/** Wrap a script body into a minimal Svelte component (runs in runes mode). */
export function svelteComponent(script: string): string {
return `<main>Hello</main>\n\n<script lang="ts">\n${script}\n</script>\n`;
}
/**
* Narrow an inferred JS rule to the typed `Rule.RuleModule`. The plugin rule
* files are plain JS, so TypeScript infers a widened shape (`meta.type: string`)
* that `RuleTester.run` rejects; the shape is structurally correct at runtime,
* which is why existing rules keep their JS inference and only the tests cast.
*/
export function asRuleModule(rule: unknown): Rule.RuleModule {
return rule as Rule.RuleModule;
}
@@ -0,0 +1,99 @@
// Tests for the conventions/interface-props rule
// (web/eslint-plugins/conventions/interface-props.js).
//
// The rule is a pure AST check (no filesystem), so it runs through the standard
// RuleTester with svelte-eslint-parser + the TS sub-parser. No fixtures needed.
import { RuleTester } from "eslint";
import svelteParser from "svelte-eslint-parser";
import tseslint from "typescript-eslint";
import { describe, it } from "vitest";
import interfaceProps from "../conventions/interface-props.js";
import { asRuleModule, svelteComponent } from "./helpers.js";
RuleTester.describe = describe;
RuleTester.it = it;
const ruleTester = new RuleTester({
languageOptions: {
parser: svelteParser,
parserOptions: { parser: tseslint.parser },
},
});
const frame = (script: string) => ({
code: svelteComponent(script),
filename: "Component.svelte",
});
ruleTester.run("interface-props", asRuleModule(interfaceProps), {
valid: [
{ code: "<main>Hello</main>", filename: "Component.svelte" },
frame("let x = 1;"),
// $state / $derived calls are unrelated, not props.
frame("const s = $state(0);"),
frame("const doubled = $derived(s * 2);"),
frame(
"interface Props { label: string }\nlet { label }: Props = $props();",
),
frame(
"interface Props { label: string }\nlet { label = 'x' }: Props = $props();",
),
frame(
"interface Props { value: string }\nlet { value = $bindable() }: Props = $props();",
),
// Interface declared after the destructuring is still fine.
frame(
"let { label }: Props = $props();\ninterface Props { label: string }",
),
frame(
"interface Props { id: string; rest?: unknown }\nlet { id, ...rest }: Props = $props();",
),
frame(
"import type { Snippet } from 'svelte';\n" +
"interface Props { children?: Snippet }\n" +
"let { children }: Props = $props();",
),
],
invalid: [
{
// Untyped destructuring with a local interface -> autofixable.
...frame("interface Props { label: string }\nlet { label } = $props();"),
output: svelteComponent(
"interface Props { label: string }\nlet { label }: Props = $props();",
),
errors: [{ messageId: "untypedDestructure" }],
},
{
// Untyped destructuring without an interface -> report, no fix.
...frame("let { label } = $props();"),
errors: [{ messageId: "untypedDestructure" }],
},
{
// Binding the whole props object instead of destructuring.
...frame("let props = $props();"),
errors: [{ messageId: "noDestructure" }],
},
{
// Inline generic is banned outright.
...frame("const props = $props<{ a: string }>();"),
errors: [{ messageId: "inlineGeneric" }],
},
{
// A different type name than the local interface Props.
...frame(
"interface Props { a: string }\nlet { a }: StageProps = $props();",
),
errors: [{ messageId: "notNamedProps" }],
},
{
// Inline object props type instead of interface Props.
...frame("let { a }: { a: string } = $props();"),
errors: [{ messageId: "inlineObjectType" }],
},
{
// Reference to Props without any local declaration.
...frame("let { a }: Props = $props();"),
errors: [{ messageId: "missingInterface" }],
},
],
});
@@ -11,12 +11,16 @@
// shared-> everything else: core/, i18n/, theme, ...
import { describe, expect, it } from "vitest";
import noMixedImports from "../isolation/no-mixed-imports.js";
import { verifyInFixtures, type FlatConfig } from "./helpers.js";
import { asRuleModule, verifyInFixtures, type FlatConfig } from "./helpers.js";
const isolationConfig: FlatConfig = [
{
files: ["**/*.{ts,svelte}"],
plugins: { isolation: { rules: { "no-mixed-imports": noMixedImports } } },
plugins: {
isolation: {
rules: { "no-mixed-imports": asRuleModule(noMixedImports) },
},
},
rules: { "isolation/no-mixed-imports": "error" },
},
];
@@ -0,0 +1,78 @@
// Tests for the conventions/no-string-union-alias rule
// (web/eslint-plugins/conventions/no-string-union-alias.js).
//
// The rule is a pure AST check (no filesystem), so it runs through the standard
// RuleTester: plain .ts via the tseslint parser, and (because the rule is also
// enabled on new .svelte scripts) .svelte via the svelte tester. No fixtures.
import { RuleTester } from "eslint";
import svelteParser from "svelte-eslint-parser";
import tseslint from "typescript-eslint";
import { describe, it } from "vitest";
import noStringUnionAlias from "../conventions/no-string-union-alias.js";
import { asRuleModule, svelteComponent } from "./helpers.js";
RuleTester.describe = describe;
RuleTester.it = it;
const tsTester = new RuleTester({
languageOptions: {
parser: tseslint.parser,
},
});
const svelteTester = new RuleTester({
languageOptions: {
parser: svelteParser,
parserOptions: { parser: tseslint.parser },
},
});
const frame = (script: string) => ({
code: svelteComponent(script),
filename: "Component.svelte",
});
tsTester.run("no-string-union-alias", asRuleModule(noStringUnionAlias), {
valid: [
{ code: "type X = 1 | 2;", filename: "types.ts" },
{ code: "type X = 'a' | number;", filename: "types.ts" },
{ code: "type X = string;", filename: "types.ts" },
{ code: "const KIND = { a: 1, b: 2 } as const;", filename: "types.ts" },
{ code: "function f(x: 'a' | 'b') {}", filename: "types.ts" },
{ code: "const x: 'a' | 'b' = 'a';", filename: "types.ts" },
{ code: "type X = 'a' | TemplateStrings;", filename: "types.ts" },
],
invalid: [
{
code: "type Kind = 'a' | 'b' | 'c';",
filename: "types.ts",
errors: [{ messageId: "stringUnion" }],
},
{
// Nested/parenthesized unions still resolve to string literals.
code: "type Kind = ('a' | 'b') | 'c';",
filename: "types.ts",
errors: [{ messageId: "stringUnion" }],
},
{
code: "type Lang =\n\t'en'\n\t| 'ru';",
filename: "types.ts",
errors: [{ messageId: "stringUnion" }],
},
],
});
// The rule also runs on new .svelte files (their scripts are TS).
svelteTester.run(
"no-string-union-alias (svelte script)",
asRuleModule(noStringUnionAlias),
{
valid: [frame("type X = 1 | 2;"), frame("const M = { a: 1 } as const;")],
invalid: [
{
...frame("type Kind = 'a' | 'b' | 'c';"),
errors: [{ messageId: "stringUnion" }],
},
],
},
);