diff --git a/.claude/rules/coding-style.md b/.claude/rules/coding-style.md index 82d7e65b6..5dee900e2 100644 --- a/.claude/rules/coding-style.md +++ b/.claude/rules/coding-style.md @@ -11,7 +11,7 @@ Before writing new logic, decide which layer it belongs to. Run this check at pl | DB schema | `prisma/` | Migrations are immutable after apply | | DB access | `src/lib/server/` | Server-only; never import in client code | | Validation | `src/**/zod/` | `z.number().int()` for Int fields; comment dual-enforcement with SQL CHECK | -| Domain types | `src/**/types/` (`_types/` inside `src/routes/`) | Plural aliases; TSDoc on every export; no `any` | +| Domain types | `src/**/types/` (`_types/` inside `src/routes/`) | Plural aliases; TSDoc on every export; avoid `any`; see alternatives | | Test data | `src/**/fixtures/` (`_fixtures/` inside `src/routes/`) | Write before implementation (TDD); use realistic values | | Business logic | `src/**/services/` | Return pure values or `null`; no `Response`/`json()` | | Pure utilities | `src/**/utils/` (`_utils/` inside `src/routes/`) | No side effects; adjacent unit test required | @@ -26,7 +26,16 @@ Before writing new logic, decide which layer it belongs to. Run this check at pl - **Abbreviations**: avoid non-standard abbreviations (`res` → `response`, `btn` → `button`). When in doubt, spell it out. - **Lambda parameters**: no single-character names (e.g., use `placement`, `workbook`). Iterator index `i` is the only exception. - **`upsert`**: only use when the implementation performs both insert and update. For insert-only, use `initialize`, `seed`, or another accurate verb. -- **`any`**: before using `any`, check the value's origin — adding a missing `@types/*` or `devDependency` often provides the correct type. +- **`any`**: before using `any`, check the value's origin — adding a missing `@types/*` or `devDependency` often provides the correct type. When `any` seems unavoidable, use the narrowest alternative: + + | Situation | Alternative | + | ---------------------------------------------------------- | ------------------------------------------------------------------------ | + | Assign to a property not on the type | `obj as T & { prop: U }` (intersection cast) | + | Return type too complex to write manually | `ReturnType` | + | Partial mock: only specific properties matter | `Partial`, `Pick`, or `satisfies` — prefer these first | + | Partial mock: none of the above narrow the type far enough | `as unknown as T` — last resort; bypasses type checking entirely | + | Inline `: any` annotation where inference reaches | Delete the annotation | + - **UI labels**: if a label does not match actual behavior, update it or add an inline comment explaining the intentional mismatch. - **Constant names**: reflect what the value IS (content), not what it is used for (purpose). e.g., a set holding all enum tab values is `EXISTING_TABS`, not `VALID_TABS`. - **New files**: before naming a new file or directory, grep the relevant `src/` directory to confirm existing conventions. Confirm at plan time, not during implementation: @@ -39,23 +48,44 @@ Before writing new logic, decide which layer it belongs to. Run this check at pl - **Braces**: always use braces for single-statement `if` blocks. Never `if () return;` — write `if () { return; }`. - **Plural type aliases**: define `type Placements = Placement[]` instead of using `Placement[]` directly in signatures and variables. +- **Empty `catch` blocks**: never use `catch { }` or `catch (_e)` to silence errors. Every `catch` must re-throw, log, or contain an explanatory comment justifying the suppression. Silent swallowing hides bugs and makes failures untraceable. + +```typescript +// Bad: silently discards the error +try { ... } catch { } +try { ... } catch (_e) { } + +// Good: log and re-throw (adds context before propagating) +try { ... } catch (error) { + console.error('Operation failed:', error); + throw error; +} + +// Good: intentional suppression with explanation +try { + localStorage.setItem(key, value); +} catch { + // localStorage may be unavailable (private browsing) — fall back to in-memory store +} +``` ### No Hard-Coded Values Extract magic numbers and strings to named constants. Never embed literal values whose meaning is not self-evident from the type or immediate context. ```typescript -// Bad +// Bad: magic literals embedded inline if (grade >= 11) { ... } -const url = '/api/workbooks/submit'; +const response = await fetch('/api/workbooks/submit', options); // Good const MIN_GRADE = 11; +const SUBMIT_URL = '/api/workbooks/submit'; if (grade >= MIN_GRADE) { ... } -const SUBMIT_URL = '/api/workbooks/submit'; +const response = await fetch(SUBMIT_URL, options); ``` Place constants at the top of the file, or in a dedicated `constants/` module when shared across files. @@ -67,7 +97,7 @@ Within a file, order declarations as follows: 1. Exported functions and classes (public API first) 2. Internal helper functions (supporting the exports above) -Shared helper functions (used by two or more exports) should be grouped at the end of the file. +Place a private helper immediately after the single export that uses it. Place helpers shared by two or more exports at the end of the file. ## Documentation diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index 7c86d2f73..a457c660f 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -24,6 +24,25 @@ If a task description does not mention tests, add them anyway for any non-trivia - Never delete, comment out, or weaken assertions (e.g. `toEqual` → `toBeDefined`) to make tests pass - Fix the implementation, not the test; if the test itself is wrong, explain why in a comment or commit message +## Unused Imports in Test Files + +An unused import in a test file is a signal that a test was planned but not yet written — not dead code to remove. + +Before deleting such an import, check whether the corresponding test case is missing and add it: + +```typescript +// Bad: remove the import because it's unused +import { ABCLikeProvider } from './contest_table_provider'; + +// Good: the import is unused because the test is missing — add the test +test('expects to create ABCLike preset correctly', () => { + const group = prepareContestProviderPresets().ABCLike(); + expect(group.getProvider(ContestType.ABC_LIKE)).toBeInstanceOf(ABCLikeProvider); +}); +``` + +Removing the import silences the linter but leaves a coverage gap. Adding the test both satisfies the linter and improves coverage. + ## Test Types | Type | Tool | Location | Run Command | diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..dd4578e07 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,29 @@ +{ + "ignorePatterns": ["**/*.svelte"], + "env": { + "browser": true, + "node": true + }, + "globals": { + "$state": "readonly", + "$derived": "readonly", + "$effect": "readonly", + "$props": "readonly", + "$bindable": "readonly", + "$inspect": "readonly" + }, + "rules": { + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-expect-error": "allow-with-description", + "ts-ignore": false + } + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } + ], + "@typescript-eslint/no-explicit-any": "warn" + } +} diff --git a/AGENTS.md b/AGENTS.md index 87cf970db..7b394d932 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ Always prefer simplicity over pathological correctness. YAGNI, KISS, DRY. No bac ## Tech Stack -SvelteKit 2 + Svelte 5 (Runes) + TypeScript | PostgreSQL + Prisma | Flowbite Svelte + Tailwind 4 | Vitest + Playwright +SvelteKit 2 + Svelte 5 (Runes) + TypeScript | PostgreSQL + Prisma | Flowbite Svelte + Tailwind 4 | Vitest + Playwright | oxlint (JS/TS) + ESLint (Svelte) ## Commands @@ -32,7 +32,7 @@ pnpm test # Run all tests pnpm test:unit # Vitest unit tests pnpm test:e2e # Playwright E2E tests pnpm coverage # Report test coverage -pnpm lint # ESLint check +pnpm lint # Prettier + oxlint (JS/TS) + ESLint (.svelte) check pnpm format # Prettier format pnpm check # Svelte type check pnpm exec prisma generate # Generate Prisma client @@ -78,7 +78,7 @@ prisma/schema.prisma # Database schema - **Forms**: Superforms + Zod validation - **Tests**: Write tests before implementation (TDD). Use `@quramy/prisma-fabbrica` for factories only in `prisma/seed.ts`. For service-layer unit tests, mock the DB with `vi.mock('$lib/server/database', ...)` — do not use fabbrica there. Use Nock for HTTP mocking - **Naming**: `camelCase` variables, `PascalCase` types/components, `snake_case` files/routes, `kebab-case` directories -- **Pre-commit**: Lefthook runs Prettier + ESLint (bypass: `LEFTHOOK=0 git commit`) +- **Pre-commit**: Lefthook runs Prettier + oxlint (JS/TS) + ESLint (.svelte only) (bypass: `LEFTHOOK=0 git commit`) ## References diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32abbd8a2..63c04f796 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,8 @@ - パッケージマネージャ - [pnpm](https://pnpm.io/ja/) - 文法およびフォーマットチェッカー - - [ESLint](https://eslint.org/) + - [oxlint](https://oxc.rs/docs/guide/usage/linter.html): JS/TS ファイルの高速リンター(50–100x 高速) + - [ESLint](https://eslint.org/): Svelte ファイル専用リンター(eslint-plugin-svelte)のみ使用 - [Prettier](https://prettier.io/) - [lefthook](https://github.com/evilmartians/lefthook): Git hooks 管理ツール(コミット前の自動フォーマット・リント) - Search Engine Optimization (SEO) 対策 @@ -283,7 +284,8 @@ - **Pre-commit Hook**: ステージ済みファイルのみに対して以下を実行 - `prettier --write`: コード書式の自動修正(JavaScript、TypeScript、Markdown、Svelte) - - `eslint`: リント(JavaScript、TypeScript、Svelte) + - `oxlint`: JS/TS ファイルのリント(JavaScript、TypeScript) + - `eslint`: Svelte ファイルのリント(.svelte のみ) Hook は自動的にセットアップされるため、特別な操作は不要です。 diff --git a/e2e/custom_colors.spec.ts b/e2e/custom_colors.spec.ts index 5dda1bafc..12089982f 100644 --- a/e2e/custom_colors.spec.ts +++ b/e2e/custom_colors.spec.ts @@ -25,7 +25,7 @@ test.describe('Custom colors for TailwindCSS v4 configuration', () => { if (cssFiles.length === 0) { cssFiles = allCssFiles; } - } catch (e) { + } catch { // True error: directory not found or inaccessible throw new Error(`Not found CSS directory: ${cssDir}`); } diff --git a/e2e/signin.spec.ts b/e2e/signin.spec.ts index cc9a2496a..b5c928489 100644 --- a/e2e/signin.spec.ts +++ b/e2e/signin.spec.ts @@ -25,7 +25,7 @@ async function login(page: Page, username: string, password: string): Promise { +async function logout(page: Page, _username: string): Promise { // Step 1: Click user button to display dropdown // Use ID selector because role="presentation" due to Flowbite-Svelte NavLi + Dropdown const userButton = page.locator('button[id="nav-user-page"]'); diff --git a/eslint.config.mjs b/eslint.config.mjs index 10d3cbaa1..4adef090a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,7 +6,6 @@ import globals from 'globals'; import tsParser from '@typescript-eslint/parser'; import svelteParser from 'svelte-eslint-parser'; import sveltePlugin from 'eslint-plugin-svelte'; -import js from '@eslint/js'; export default [ { @@ -28,10 +27,10 @@ export default [ '**/vite.config.js.timestamp-*', '**/vite.config.ts.timestamp-*', 'prisma/.fabbrica/index.ts', + // oxlint handles JS/TS files + '**/*.{js,ts,tsx,mjs,cjs}', ], }, - // Base JS rules first - js.configs.recommended, // Svelte rules override JS rules where appropriate (intentional) // This allows Svelte-specific handling of rules like no-undef, no-unused-vars ...sveltePlugin.configs['flat/recommended'], diff --git a/lefthook.yml b/lefthook.yml index 183d3f78b..0eb723864 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -7,6 +7,10 @@ pre-commit: run: pnpm exec prettier --write {staged_files} glob: '**/*.{js,jsx,ts,tsx,md,svelte}' + - name: oxlint + run: pnpm exec oxlint {staged_files} + glob: '**/*.{js,jsx,ts,tsx}' + - name: eslint run: pnpm exec eslint {staged_files} - glob: '**/*.{js,jsx,ts,tsx,svelte}' + glob: '**/*.svelte' diff --git a/package.json b/package.json index d267ec030..82c9b9970 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test": "npm run test:e2e && npm run test:unit", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "prettier --check . && eslint .", + "lint": "prettier --check . && oxlint . && eslint .", "format": "prettier --write .", "test:e2e": "playwright test", "test:unit": "vitest", @@ -26,7 +26,6 @@ "@dnd-kit/abstract": "0.3.2", "@dnd-kit/dom": "0.3.2", "@eslint/eslintrc": "3.3.5", - "@eslint/js": "10.0.1", "@playwright/test": "1.58.2", "@quramy/prisma-fabbrica": "2.3.3", "@sveltejs/adapter-vercel": "6.3.3", @@ -47,9 +46,10 @@ "flowbite": "3.1.2", "flowbite-svelte": "1.31.0", "globals": "17.4.0", - "lefthook": "2.1.4", "jsdom": "29.0.1", + "lefthook": "2.1.4", "nock": "14.0.11", + "oxlint": "^1.56.0", "pnpm": "10.32.1", "prettier": "3.8.1", "prettier-plugin-svelte": "3.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4aac66598..40ca7a575 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,6 @@ importers: '@eslint/eslintrc': specifier: 3.3.5 version: 3.3.5 - '@eslint/js': - specifier: 10.0.1 - version: 10.0.1(eslint@10.1.0(jiti@1.21.7)) '@playwright/test': specifier: 1.58.2 version: 1.58.2 @@ -147,6 +144,9 @@ importers: nock: specifier: 14.0.11 version: 14.0.11 + oxlint: + specifier: ^1.56.0 + version: 1.56.0 pnpm: specifier: 10.32.1 version: 10.32.1 @@ -879,15 +879,6 @@ packages: resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@10.0.1': - resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^10.0.0 - peerDependenciesMeta: - eslint: - optional: true - '@eslint/object-schema@3.0.3': resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -1231,6 +1222,128 @@ packages: cpu: [x64] os: [win32] + '@oxlint/binding-android-arm-eabi@1.56.0': + resolution: {integrity: sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.56.0': + resolution: {integrity: sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.56.0': + resolution: {integrity: sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.56.0': + resolution: {integrity: sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.56.0': + resolution: {integrity: sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.56.0': + resolution: {integrity: sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.56.0': + resolution: {integrity: sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.56.0': + resolution: {integrity: sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.56.0': + resolution: {integrity: sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.56.0': + resolution: {integrity: sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.56.0': + resolution: {integrity: sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.56.0': + resolution: {integrity: sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.56.0': + resolution: {integrity: sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.56.0': + resolution: {integrity: sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.56.0': + resolution: {integrity: sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.56.0': + resolution: {integrity: sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.56.0': + resolution: {integrity: sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.56.0': + resolution: {integrity: sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.56.0': + resolution: {integrity: sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@playwright/test@1.58.2': resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} engines: {node: '>=18'} @@ -4242,6 +4355,16 @@ packages: resolution: {integrity: sha512-oa5KKSDNLHZGaiqIGAbCWXeN9IJUAz9MElWcQX90epDxdKc9Hrt/BsLj3K4gDqfAYa5dwdH+ZCFJG9hR74fiGg==} engines: {node: ^20.19.0 || >=22.12.0} + oxlint@1.56.0: + resolution: {integrity: sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.15.0' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + p-finally@2.0.1: resolution: {integrity: sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==} engines: {node: '>=8'} @@ -6002,10 +6125,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@10.0.1(eslint@10.1.0(jiti@1.21.7))': - optionalDependencies: - eslint: 10.1.0(jiti@1.21.7) - '@eslint/object-schema@3.0.3': {} '@eslint/plugin-kit@0.6.1': @@ -6321,6 +6440,63 @@ snapshots: '@oxc-transform/binding-win32-x64-msvc@0.111.0': optional: true + '@oxlint/binding-android-arm-eabi@1.56.0': + optional: true + + '@oxlint/binding-android-arm64@1.56.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.56.0': + optional: true + + '@oxlint/binding-darwin-x64@1.56.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.56.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.56.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.56.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.56.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.56.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.56.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.56.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.56.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.56.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.56.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.56.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.56.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.56.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.56.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.56.0': + optional: true + '@playwright/test@1.58.2': dependencies: playwright: 1.58.2 @@ -9573,6 +9749,28 @@ snapshots: '@oxc-transform/binding-win32-ia32-msvc': 0.111.0 '@oxc-transform/binding-win32-x64-msvc': 0.111.0 + oxlint@1.56.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.56.0 + '@oxlint/binding-android-arm64': 1.56.0 + '@oxlint/binding-darwin-arm64': 1.56.0 + '@oxlint/binding-darwin-x64': 1.56.0 + '@oxlint/binding-freebsd-x64': 1.56.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.56.0 + '@oxlint/binding-linux-arm-musleabihf': 1.56.0 + '@oxlint/binding-linux-arm64-gnu': 1.56.0 + '@oxlint/binding-linux-arm64-musl': 1.56.0 + '@oxlint/binding-linux-ppc64-gnu': 1.56.0 + '@oxlint/binding-linux-riscv64-gnu': 1.56.0 + '@oxlint/binding-linux-riscv64-musl': 1.56.0 + '@oxlint/binding-linux-s390x-gnu': 1.56.0 + '@oxlint/binding-linux-x64-gnu': 1.56.0 + '@oxlint/binding-linux-x64-musl': 1.56.0 + '@oxlint/binding-openharmony-arm64': 1.56.0 + '@oxlint/binding-win32-arm64-msvc': 1.56.0 + '@oxlint/binding-win32-ia32-msvc': 1.56.0 + '@oxlint/binding-win32-x64-msvc': 1.56.0 + p-finally@2.0.1: {} p-limit@3.1.0: diff --git a/src/features/tasks/utils/contest-table/awc_provider.test.ts b/src/features/tasks/utils/contest-table/awc_provider.test.ts index d293ea9ee..e27a74239 100644 --- a/src/features/tasks/utils/contest-table/awc_provider.test.ts +++ b/src/features/tasks/utils/contest-table/awc_provider.test.ts @@ -68,7 +68,7 @@ describe('AWC0001OnwardsProvider', () => { const filtered = provider.filter(taskResultsForAWC0001OnwardsProvider); const table = provider.generateTable(filtered); - Object.entries(table).forEach(([contestId, problems]) => { + Object.entries(table).forEach(([_contestId, problems]) => { const problemCount = Object.keys(problems).length; expect(problemCount).toBe(5); expect(Object.keys(problems)).toEqual(['A', 'B', 'C', 'D', 'E']); diff --git a/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts b/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts index 8559ac2e5..979271046 100644 --- a/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts +++ b/src/features/tasks/utils/contest-table/contest_table_provider_groups.test.ts @@ -71,6 +71,128 @@ describe('prepareContestProviderPresets', () => { expect(group.getProvider(ContestType.ABC)).toBeInstanceOf(ABC212ToABC318Provider); }); + test('expects to create fromABC126ToABC211 preset correctly', () => { + const group = prepareContestProviderPresets().ABC126ToABC211(); + + expect(group.getGroupName()).toBe('From ABC 126 to ABC 211'); + expect(group.getMetadata()).toEqual({ + buttonLabel: 'ABC 126 〜 211', + ariaLabel: 'Filter contests from ABC 126 to ABC 211', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.ABC)).toBeInstanceOf(ABC126ToABC211Provider); + }); + + test('expects to create fromABC042ToABC125 preset correctly', () => { + const group = prepareContestProviderPresets().ABC042ToABC125(); + + expect(group.getGroupName()).toBe('From ABC 042 to ABC 125'); + expect(group.getMetadata()).toEqual({ + buttonLabel: 'ABC 042 〜 125', + ariaLabel: 'Filter contests from ABC 042 to ABC 125', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.ABC)).toBeInstanceOf(ABC042ToABC125Provider); + }); + + test('expects to create fromABC001ToABC041 preset correctly', () => { + const group = prepareContestProviderPresets().ABC001ToABC041(); + + expect(group.getGroupName()).toBe('From ABC 001 to ABC 041'); + expect(group.getMetadata()).toEqual({ + buttonLabel: '旧 ABC', + ariaLabel: 'Filter contests from ABC 001 to ABC 041', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.ABC)).toBeInstanceOf(ABC001ToABC041Provider); + }); + + test('expects to create ARC104Onwards preset correctly', () => { + const group = prepareContestProviderPresets().ARC104Onwards(); + + expect(group.getGroupName()).toBe('ARC 104 Onwards'); + expect(group.getMetadata()).toEqual({ + buttonLabel: 'ARC 104 〜 ', + ariaLabel: 'Filter contests from ARC 104 onwards', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.ARC)).toBeInstanceOf(ARC104OnwardsProvider); + }); + + test('expects to create ARC058ToARC103 preset correctly', () => { + const group = prepareContestProviderPresets().ARC058ToARC103(); + + expect(group.getGroupName()).toBe('ARC 058 To ARC 103'); + expect(group.getMetadata()).toEqual({ + buttonLabel: 'ARC 058 〜 103', + ariaLabel: 'Filter contests from ARC 058 to ARC 103', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.ARC)).toBeInstanceOf(ARC058ToARC103Provider); + }); + + test('expects to create ARC001ToARC057 preset correctly', () => { + const group = prepareContestProviderPresets().ARC001ToARC057(); + + expect(group.getGroupName()).toBe('ARC 001 To ARC 057'); + expect(group.getMetadata()).toEqual({ + buttonLabel: '旧 ARC', + ariaLabel: 'Filter contests from ARC 001 to ARC 057', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.ARC)).toBeInstanceOf(ARC001ToARC057Provider); + }); + + test('expects to create AGC001Onwards preset correctly', () => { + const group = prepareContestProviderPresets().AGC001Onwards(); + + expect(group.getGroupName()).toBe('AGC 001 Onwards'); + expect(group.getMetadata()).toEqual({ + buttonLabel: 'AGC 001 〜 ', + ariaLabel: 'Filter contests from AGC 001 onwards', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.AGC)).toBeInstanceOf(AGC001OnwardsProvider); + }); + + test('expects to create ABCLike preset correctly', () => { + const group = prepareContestProviderPresets().ABCLike(); + + expect(group.getGroupName()).toBe('ABC-Like'); + expect(group.getMetadata()).toEqual({ + buttonLabel: 'ABC-Like', + ariaLabel: 'Filter contests from ABC-Like', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.ABC_LIKE)).toBeInstanceOf(ABCLikeProvider); + }); + + test('expects to create AWC0001Onwards preset correctly', () => { + const group = prepareContestProviderPresets().AWC0001Onwards(); + + expect(group.getGroupName()).toBe('AWC 0001 Onwards'); + expect(group.getMetadata()).toEqual({ + buttonLabel: 'AWC 0001 〜 ', + ariaLabel: 'Filter contests from AWC 0001 onwards', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.AWC)).toBeInstanceOf(AWC0001OnwardsProvider); + }); + + test('expects to create MathAndAlgorithm preset correctly', () => { + const group = prepareContestProviderPresets().MathAndAlgorithm(); + + expect(group.getGroupName()).toBe('アルゴリズムと数学'); + expect(group.getMetadata()).toEqual({ + buttonLabel: 'アルゴリズムと数学', + ariaLabel: 'Filter Math and Algorithm', + }); + expect(group.getSize()).toBe(1); + expect(group.getProvider(ContestType.MATH_AND_ALGORITHM)).toBeInstanceOf( + MathAndAlgorithmProvider, + ); + }); + test('expects to create Typical90 preset correctly', () => { const group = prepareContestProviderPresets().Typical90(); diff --git a/src/features/tasks/utils/contest-table/joi_providers.test.ts b/src/features/tasks/utils/contest-table/joi_providers.test.ts index 4b7013faa..16e18ae21 100644 --- a/src/features/tasks/utils/contest-table/joi_providers.test.ts +++ b/src/features/tasks/utils/contest-table/joi_providers.test.ts @@ -1,6 +1,7 @@ import { describe, test, expect } from 'vitest'; import { ContestType } from '$lib/types/contest'; +import type { TaskResults } from '$lib/types/task'; import { JOIFirstQualRoundProvider, @@ -21,7 +22,7 @@ describe('JOIFirstQualRoundProvider', () => { { contest_id: 'abc123', task_id: 'abc123_a' }, ]; - const filtered = provider.filter(mockJOITasks as any); + const filtered = provider.filter(mockJOITasks as unknown as TaskResults); expect(filtered?.every((task) => task.contest_id.startsWith('joi'))).toBe(true); expect(filtered?.length).toBe(4); @@ -39,7 +40,7 @@ describe('JOIFirstQualRoundProvider', () => { { contest_id: 'joi2022yo1a', task_id: 'joi2022yo1a_c' }, ]; - const filtered = provider.filter(mockJOITasks as any); + const filtered = provider.filter(mockJOITasks as unknown as TaskResults); expect(filtered?.length).toBe(6); expect(filtered?.filter((task) => task.contest_id.includes('2024')).length).toBe(3); @@ -93,7 +94,7 @@ describe('JOIFirstQualRoundProvider', () => { { contest_id: 'joi2024yo1c', task_id: 'joi2024yo1c_c', task_table_index: 'C' }, ]; - const table = provider.generateTable(mockJOITasks as any); + const table = provider.generateTable(mockJOITasks as unknown as TaskResults); expect(table).toHaveProperty('joi2024yo1a'); expect(table).toHaveProperty('joi2024yo1b'); @@ -115,7 +116,7 @@ describe('JOIFirstQualRoundProvider', () => { { contest_id: 'joi2023yo1c', task_id: 'joi2023yo1c_a' }, ]; - const roundIds = provider.getContestRoundIds(mockJOITasks as any); + const roundIds = provider.getContestRoundIds(mockJOITasks as unknown as TaskResults); expect(roundIds).toContain('joi2024yo1a'); expect(roundIds).toContain('joi2024yo1b'); @@ -139,7 +140,7 @@ describe('JOISecondQualRound2020OnwardsProvider', () => { { contest_id: 'abc123', task_id: 'abc123_a' }, ]; - const filtered = provider.filter(mockJOITasks as any); + const filtered = provider.filter(mockJOITasks as unknown as TaskResults); expect(filtered?.every((task) => task.contest_id.startsWith('joi'))).toBe(true); expect(filtered?.length).toBe(3); @@ -155,7 +156,7 @@ describe('JOISecondQualRound2020OnwardsProvider', () => { { contest_id: 'joi2022yo2', task_id: 'joi2022yo2_a' }, ]; - const filtered = provider.filter(mockJOITasks as any); + const filtered = provider.filter(mockJOITasks as unknown as TaskResults); expect(filtered?.length).toBe(4); expect(filtered?.filter((task) => task.contest_id.includes('2024')).length).toBe(2); @@ -193,7 +194,7 @@ describe('JOISecondQualRound2020OnwardsProvider', () => { test('expects to handle empty task results', () => { const provider = new JOISecondQualRound2020OnwardsProvider(ContestType.JOI); - const filtered = provider.filter([] as any); + const filtered = provider.filter([] as TaskResults); expect(filtered).toEqual([]); }); @@ -212,7 +213,7 @@ describe('JOIQualRoundFrom2006To2019Provider', () => { { contest_id: 'abc123', task_id: 'abc123_a' }, ]; - const filtered = provider.filter(mockJOITasks as any); + const filtered = provider.filter(mockJOITasks as unknown as TaskResults); expect(filtered?.every((task) => task.contest_id.startsWith('joi'))).toBe(true); expect(filtered?.length).toBe(3); @@ -227,7 +228,7 @@ describe('JOIQualRoundFrom2006To2019Provider', () => { { contest_id: 'joi2024yo2', task_id: 'joi2024yo2_a' }, ]; - const filtered = provider.filter(mockJOITasks as any); + const filtered = provider.filter(mockJOITasks as unknown as TaskResults); expect(filtered?.length).toBe(1); expect(filtered?.[0].contest_id).toBe('joi2019yo'); @@ -263,7 +264,7 @@ describe('JOIQualRoundFrom2006To2019Provider', () => { test('expects to handle empty task results', () => { const provider = new JOIQualRoundFrom2006To2019Provider(ContestType.JOI); - const filtered = provider.filter([] as any); + const filtered = provider.filter([] as TaskResults); expect(filtered).toEqual([]); }); @@ -282,7 +283,7 @@ describe('JOISemiFinalRoundProvider', () => { { contest_id: 'abc123', task_id: 'abc123_a' }, ]; - const filtered = provider.filter(mockJOITasks as any); + const filtered = provider.filter(mockJOITasks as unknown as TaskResults); expect(filtered?.every((task) => task.contest_id.startsWith('joi'))).toBe(true); expect(filtered?.length).toBe(3); @@ -298,7 +299,7 @@ describe('JOISemiFinalRoundProvider', () => { { contest_id: 'joi2022ho', task_id: 'joi2022ho_a' }, ]; - const filtered = provider.filter(mockJOITasks as any); + const filtered = provider.filter(mockJOITasks as unknown as TaskResults); expect(filtered?.length).toBe(4); expect(filtered?.filter((task) => task.contest_id.includes('2024')).length).toBe(2); @@ -334,7 +335,7 @@ describe('JOISemiFinalRoundProvider', () => { test('expects to handle empty task results', () => { const provider = new JOISemiFinalRoundProvider(ContestType.JOI); - const filtered = provider.filter([] as any); + const filtered = provider.filter([] as TaskResults); expect(filtered).toEqual([]); }); @@ -350,7 +351,7 @@ describe('JOISemiFinalRoundProvider', () => { { contest_id: 'abc123', task_id: 'abc123_a' }, ]; - const filtered = provider.filter(mockJOITasks as any); + const filtered = provider.filter(mockJOITasks as unknown as TaskResults); expect(filtered?.length).toBe(3); expect(filtered?.some((task) => task.contest_id === 'joi2026sf')).toBe(true); @@ -372,7 +373,7 @@ describe('JOISemiFinalRoundProvider', () => { { contest_id: 'joi2024ho', task_id: 'joi2024ho_a', task_table_index: 'A' }, ]; - const table = provider.generateTable(mockJOITasks as any); + const table = provider.generateTable(mockJOITasks as unknown as TaskResults); expect(table).toHaveProperty('joi2026sf'); expect(table).toHaveProperty('joi2024ho'); diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index 856f2b6c3..000000000 --- a/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/types/apidata.ts b/src/lib/types/apidata.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/test/lib/services/fixtures/task_results.ts b/src/test/lib/services/fixtures/task_results.ts index 749b14b4b..c4f63d289 100644 --- a/src/test/lib/services/fixtures/task_results.ts +++ b/src/test/lib/services/fixtures/task_results.ts @@ -1,6 +1,14 @@ import { ContestType } from '$lib/types/contest'; import { TaskGrade } from '$lib/types/task'; +type MockedSubmissionStatus = { + id: string; + status_name: string; + image_path: string; + label_name: string; + is_ac: boolean; +}; + export const MOCK_TASKS_DATA = [ { id: '1', @@ -84,7 +92,7 @@ export const MOCK_SUBMISSION_STATUSES_DATA = [ ] as const; export const MOCK_SUBMISSION_STATUSES = new Map( - MOCK_SUBMISSION_STATUSES_DATA as unknown as Array<[string, any]>, + MOCK_SUBMISSION_STATUSES_DATA as unknown as Array<[string, MockedSubmissionStatus]>, ); export const MOCK_ANSWERS_WITH_ANSWERS = new Map([ diff --git a/src/test/lib/services/task_results.test.ts b/src/test/lib/services/task_results.test.ts index 6f8077068..42d02a79a 100644 --- a/src/test/lib/services/task_results.test.ts +++ b/src/test/lib/services/task_results.test.ts @@ -439,7 +439,7 @@ function createMergedTaskResults( setupAnswers(); }); - testCases.forEach(({ contest_id, task_id }: any) => { + testCases.forEach(({ contest_id, task_id }) => { test(`expects to preserve contest_id and task_id${testNameSuffix} for ${contest_id}:${task_id}`, async () => { const taskResults = await getTaskResults('user_123'); const taskResult = taskResults.find( diff --git a/src/test/lib/utils/auth_forms.test.ts b/src/test/lib/utils/auth_forms.test.ts index 0820cd882..2309d9fbd 100644 --- a/src/test/lib/utils/auth_forms.test.ts +++ b/src/test/lib/utils/auth_forms.test.ts @@ -4,11 +4,10 @@ import type { SuperValidated } from 'sveltekit-superforms'; // Mock external dependencies BEFORE importing the module under test vi.mock('@sveltejs/kit', () => { const redirectImpl = (status: number, location: string) => { - const error = new Error('Redirect'); - - (error as any).name = 'Redirect'; - (error as any).status = status; - (error as any).location = location; + const error = new Error('Redirect') as Error & { status: number; location: string }; + error.name = 'Redirect'; + error.status = status; + error.location = location; throw error; }; @@ -137,7 +136,7 @@ describe('auth_forms', () => { } as unknown as SuperValidated, string>; }); - vi.mocked(zod4).mockImplementation((schema: unknown) => schema as any); + vi.mocked(zod4).mockImplementation((schema: unknown) => schema as ReturnType); }); afterEach(() => { diff --git a/src/test/lib/utils/authorship.test.ts b/src/test/lib/utils/authorship.test.ts index 50be82597..b04fa8f41 100644 --- a/src/test/lib/utils/authorship.test.ts +++ b/src/test/lib/utils/authorship.test.ts @@ -3,11 +3,10 @@ import { expect, test, describe, vi, afterEach } from 'vitest'; // Mock modules vi.mock('@sveltejs/kit', () => { const redirectImpl = (status: number, location: string) => { - const error = new Error('Redirect'); - - (error as any).name = 'Redirect'; - (error as any).status = status; - (error as any).location = location; + const error = new Error('Redirect') as Error & { status: number; location: string }; + error.name = 'Redirect'; + error.status = status; + error.location = location; throw error; };