From 098897cdddb1a998082d949b408b4e8557e4ad71 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Thu, 10 Sep 2026 21:15:15 +0500 Subject: [PATCH] chore: update lint rules, add tests --- AGENTS.md | 24 +++ web/eslint-plugins/__fixtures__/src/app.css | 11 + .../src/lib/components/CheckerCanvas.svelte | 1 + .../src/lib/components/ui/Button.svelte | 1 + .../__fixtures__/src/lib/core/errors.ts | 1 + .../__fixtures__/src/lib/i18n/t.ts | 1 + .../__fixtures__/src/lib/theme.svelte.ts | 1 + .../__fixtures__/src/lib/v1/old.ts | 2 + .../__fixtures__/src/routes/+page.svelte | 1 + .../__fixtures__/src/routes/v1/+layout.svelte | 1 + .../__tests__/design-tokens.test.ts | 199 ++++++++++++++++++ web/eslint-plugins/__tests__/helpers.ts | 34 +++ .../__tests__/no-mixed-imports.test.ts | 102 +++++++++ web/eslint-plugins/isolation/index.js | 4 +- .../isolation/no-mixed-imports.js | 20 +- web/eslint.config.js | 61 +++++- web/package.json | 1 + web/vitest.config.ts | 2 +- 18 files changed, 446 insertions(+), 21 deletions(-) create mode 100644 web/eslint-plugins/__fixtures__/src/app.css create mode 100644 web/eslint-plugins/__fixtures__/src/lib/components/CheckerCanvas.svelte create mode 100644 web/eslint-plugins/__fixtures__/src/lib/components/ui/Button.svelte create mode 100644 web/eslint-plugins/__fixtures__/src/lib/core/errors.ts create mode 100644 web/eslint-plugins/__fixtures__/src/lib/i18n/t.ts create mode 100644 web/eslint-plugins/__fixtures__/src/lib/theme.svelte.ts create mode 100644 web/eslint-plugins/__fixtures__/src/lib/v1/old.ts create mode 100644 web/eslint-plugins/__fixtures__/src/routes/+page.svelte create mode 100644 web/eslint-plugins/__fixtures__/src/routes/v1/+layout.svelte create mode 100644 web/eslint-plugins/__tests__/design-tokens.test.ts create mode 100644 web/eslint-plugins/__tests__/helpers.ts create mode 100644 web/eslint-plugins/__tests__/no-mixed-imports.test.ts diff --git a/AGENTS.md b/AGENTS.md index bc58cac..6a3eb78 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,6 +43,30 @@ размеры в kit, не-Prefix токены в preview.css и т.п.) — техдолг: чинить только по заведённым tasks, не игнорировать правилом. +### Тесты кастомных линт-правил + +Линт-правила в `web/eslint-plugins/` покрыты юнит-тестами (Vitest): + +- `web/eslint-plugins/__tests__/design-tokens.test.ts` — три чистых правила + (`no-hardcoded-in-svelte`, `no-category-mismatch`, + `no-token-definition-in-svelte`) через `RuleTester` со строковыми кейсами; + `no-undefined-in-svelte` — через `Linter` API, т.к. читает словарь токенов из + `__fixtures__/src/app.css` (не из реального `src/app.css`). +- `web/eslint-plugins/__tests__/no-mixed-imports.test.ts` — isolation-правило + через `Linter` API с `cwd` на `__fixtures__`: правило резолвит импорты по + реальным файлам, поэтому цели импортов обязаны существовать на диске. +- Хелпер `__tests__/helpers.ts` собирает `Linter` с `cwd = __fixtures__` — + `process.cwd()` не трогается, реальные `src/` не читаются. + +Запуск: `pnpm --dir web test:rules` (`vitest run eslint-plugins/__tests__`). +Полный `pnpm --dir web test` тоже их гоняет. + +Фикстуры живут в `web/eslint-plugins/__fixtures__/` и сами прогоняются линтом +(`eslint .`), поэтому добавление/правка стабов — тоже работа с валидным кодом. +Помнить: `allowDefaultProject` в `eslint.config.js` перечисляет test-файлы и +фикстуры точечно (glob'ы с `**` там запрещены tseslint) — при добавлении +файлов в `__tests__/`/`__fixtures__/` дописать их туда же. + ## Правила кода ### Svelte 5: типизация props через `interface Props` diff --git a/web/eslint-plugins/__fixtures__/src/app.css b/web/eslint-plugins/__fixtures__/src/app.css new file mode 100644 index 0000000..68440c7 --- /dev/null +++ b/web/eslint-plugins/__fixtures__/src/app.css @@ -0,0 +1,11 @@ +:root { + --color-fg: #101010; + + --space-1: calc(var(--size-1) / 2); + + --z-nav: 100; + + --duration-fast: 120ms; + + --text-md: 0.875rem; +} diff --git a/web/eslint-plugins/__fixtures__/src/lib/components/CheckerCanvas.svelte b/web/eslint-plugins/__fixtures__/src/lib/components/CheckerCanvas.svelte new file mode 100644 index 0000000..605d2b3 --- /dev/null +++ b/web/eslint-plugins/__fixtures__/src/lib/components/CheckerCanvas.svelte @@ -0,0 +1 @@ + diff --git a/web/eslint-plugins/__fixtures__/src/lib/components/ui/Button.svelte b/web/eslint-plugins/__fixtures__/src/lib/components/ui/Button.svelte new file mode 100644 index 0000000..605d2b3 --- /dev/null +++ b/web/eslint-plugins/__fixtures__/src/lib/components/ui/Button.svelte @@ -0,0 +1 @@ + diff --git a/web/eslint-plugins/__fixtures__/src/lib/core/errors.ts b/web/eslint-plugins/__fixtures__/src/lib/core/errors.ts new file mode 100644 index 0000000..f2e4b52 --- /dev/null +++ b/web/eslint-plugins/__fixtures__/src/lib/core/errors.ts @@ -0,0 +1 @@ +// Fixture stub for lint-rule tests: content is ignored, must exist for import resolution. diff --git a/web/eslint-plugins/__fixtures__/src/lib/i18n/t.ts b/web/eslint-plugins/__fixtures__/src/lib/i18n/t.ts new file mode 100644 index 0000000..f2e4b52 --- /dev/null +++ b/web/eslint-plugins/__fixtures__/src/lib/i18n/t.ts @@ -0,0 +1 @@ +// Fixture stub for lint-rule tests: content is ignored, must exist for import resolution. diff --git a/web/eslint-plugins/__fixtures__/src/lib/theme.svelte.ts b/web/eslint-plugins/__fixtures__/src/lib/theme.svelte.ts new file mode 100644 index 0000000..f2e4b52 --- /dev/null +++ b/web/eslint-plugins/__fixtures__/src/lib/theme.svelte.ts @@ -0,0 +1 @@ +// Fixture stub for lint-rule tests: content is ignored, must exist for import resolution. diff --git a/web/eslint-plugins/__fixtures__/src/lib/v1/old.ts b/web/eslint-plugins/__fixtures__/src/lib/v1/old.ts new file mode 100644 index 0000000..7d1982c --- /dev/null +++ b/web/eslint-plugins/__fixtures__/src/lib/v1/old.ts @@ -0,0 +1,2 @@ +// Fixture stub for lint-rule tests: content is ignored (the test supplies the +// source code), this file only has to EXIST for import resolution. diff --git a/web/eslint-plugins/__fixtures__/src/routes/+page.svelte b/web/eslint-plugins/__fixtures__/src/routes/+page.svelte new file mode 100644 index 0000000..605d2b3 --- /dev/null +++ b/web/eslint-plugins/__fixtures__/src/routes/+page.svelte @@ -0,0 +1 @@ + diff --git a/web/eslint-plugins/__fixtures__/src/routes/v1/+layout.svelte b/web/eslint-plugins/__fixtures__/src/routes/v1/+layout.svelte new file mode 100644 index 0000000..605d2b3 --- /dev/null +++ b/web/eslint-plugins/__fixtures__/src/routes/v1/+layout.svelte @@ -0,0 +1 @@ + diff --git a/web/eslint-plugins/__tests__/design-tokens.test.ts b/web/eslint-plugins/__tests__/design-tokens.test.ts new file mode 100644 index 0000000..9beec67 --- /dev/null +++ b/web/eslint-plugins/__tests__/design-tokens.test.ts @@ -0,0 +1,199 @@ +// Tests for the design-tokens plugin rules (web/eslint-plugins/design-tokens). +// +// Strategy: +// - no-hardcoded-in-svelte, no-category-mismatch, no-token-definition-in-svelte +// are pure AST checks -> standard RuleTester with string cases. +// - no-undefined-in-svelte reads the token dictionary from /src/app.css, +// so it runs through the Linter API with cwd pinned to the fixtures dir +// (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 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 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"; + +const parserOptions = { + parser: tseslint.parser, +}; + +// Vitest doesn't expose describe/it as globals (no `globals: true` in the +// config), so RuleTester would fall back to its synchronous default handler. +// Register the real ones so each case becomes a proper vitest test. +RuleTester.describe = describe; +RuleTester.it = it; + +const ruleTester = new RuleTester({ + languageOptions: { + parser: svelteParser, + parserOptions, + }, +}); + +/** Wrap CSS from a \n`; + +const frame = (style: string) => ({ + code: component(style), + filename: "Component.svelte", +}); + +describe("design-tokens/no-hardcoded-in-svelte", () => { + ruleTester.run("no-hardcoded-in-svelte", noHardcoded, { + valid: [ + frame(".box { color: var(--color-fg); }"), + frame(".box { padding: var(--space-3); }"), + frame(".box { background-color: transparent; }"), + frame(".box { width: 100%; }"), + frame(".box { line-height: 1.5; }"), + frame(".box { transition: var(--duration-fast); }"), + frame(".box { z-index: var(--z-nav); }"), + frame(".box { font-size: var(--text-md); }"), + frame("
no style block at all
"), + ], + invalid: [ + { + ...frame(".box { color: #ff0000; }"), + errors: [{ messageId: "hardcodedColor" }], + }, + { + ...frame(".box { padding: 16px; }"), + errors: [{ messageId: "hardcodedSize" }], + }, + { + ...frame(".box { transition: 200ms; }"), + errors: [{ messageId: "hardcodedDuration" }], + }, + { + ...frame(".box { z-index: 100; }"), + errors: [{ messageId: "hardcodedZIndex" }], + }, + { + ...frame( + "@media (max-width: 640px) { .box { width: var(--text-md); } }", + ), + errors: [{ messageId: "hardcodedBreakpoint" }], + }, + { + ...frame( + "@media (min-width: var(--bp-m)) { .box { width: var(--text-md); } }", + ), + errors: [{ messageId: "varInMedia" }], + }, + { + ...frame( + ".box { color: color-mix(in srgb, var(--color-a), var(--color-b)); }", + ), + errors: [{ messageId: "colorMix" }], + }, + ], + }); +}); + +describe("design-tokens/no-category-mismatch", () => { + ruleTester.run("no-category-mismatch", noCategoryMismatch, { + valid: [ + frame(".box { padding: var(--space-1); }"), + frame(".box { color: var(--color-fg); }"), + frame(".box { border: 1px solid var(--color-border); }"), + ], + invalid: [ + { + ...frame(".box { padding: var(--color-fg); }"), + errors: [{ messageId: "categoryMismatch" }], + }, + { + ...frame(".box { color: var(--space-1); }"), + errors: [{ messageId: "categoryMismatch" }], + }, + ], + }); +}); + +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" }], + }, + ], + }); +}); + +describe("design-tokens/no-undefined-in-svelte", () => { + const undefinedConfig: FlatConfig = [ + { + files: ["**/*.svelte"], + plugins: { + "design-tokens": { rules: { "no-undefined-in-svelte": noUndefined } }, + }, + rules: { "design-tokens/no-undefined-in-svelte": "error" }, + languageOptions: { + parser: svelteParser, + parserOptions, + }, + }, + ]; + + it("accepts tokens defined in the fixture app.css", () => { + const messages = verifyInFixtures( + undefinedConfig, + component(".box { color: var(--color-fg); }"), + "lib/components/Foo.svelte", + ); + expect(messages).toEqual([]); + }); + + it("accepts component-local overrides", () => { + const messages = verifyInFixtures( + undefinedConfig, + component(".box { --local-x: var(--color-fg); color: var(--local-x); }"), + "lib/components/Foo.svelte", + ); + expect(messages).toEqual([]); + }); + + it("flags an unknown token even if it exists in the real web/src/app.css", () => { + const messages = verifyInFixtures( + undefinedConfig, + component(".box { color: var(--color-love); }"), + "lib/components/Foo.svelte", + ); + expect(messages).toHaveLength(1); + expect(messages[0].messageId).toBe("undefinedToken"); + expect(messages[0].message).toContain("--color-love"); + }); + + it("flags unknown tokens referenced inside @media params", () => { + const messages = verifyInFixtures( + undefinedConfig, + component( + "@media (width >= var(--bp-xs)) { .box { width: var(--text-md); } }", + ), + "lib/components/Foo.svelte", + ); + expect(messages).toHaveLength(1); + expect(messages[0].messageId).toBe("undefinedToken"); + expect(messages[0].message).toContain("--bp-xs"); + }); +}); diff --git a/web/eslint-plugins/__tests__/helpers.ts b/web/eslint-plugins/__tests__/helpers.ts new file mode 100644 index 0000000..7ad5675 --- /dev/null +++ b/web/eslint-plugins/__tests__/helpers.ts @@ -0,0 +1,34 @@ +// Shared harness for testing the custom ESLint rules in eslint-plugins/. +// +// Two strategies, picked per rule in the test files: +// - RuleTester (eslint) — pure string cases, no filesystem. Best fit for the +// design-tokens rules that only inspect the postcss AST of Svelte `