Skip to content

Commit 17eb19c

Browse files
authored
Merge pull request #3309 from AtCoder-NoviSteps/#2226
Use Oxlint instead of ESLint for JavaScript/TypeScript linting and add tests (#2226)
2 parents 2c84975 + 77c5221 commit 17eb19c

20 files changed

Lines changed: 475 additions & 66 deletions

.claude/rules/coding-style.md

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Before writing new logic, decide which layer it belongs to. Run this check at pl
1111
| DB schema | `prisma/` | Migrations are immutable after apply |
1212
| DB access | `src/lib/server/` | Server-only; never import in client code |
1313
| Validation | `src/**/zod/` | `z.number().int()` for Int fields; comment dual-enforcement with SQL CHECK |
14-
| Domain types | `src/**/types/` (`_types/` inside `src/routes/`) | Plural aliases; TSDoc on every export; no `any` |
14+
| Domain types | `src/**/types/` (`_types/` inside `src/routes/`) | Plural aliases; TSDoc on every export; avoid `any`; see alternatives |
1515
| Test data | `src/**/fixtures/` (`_fixtures/` inside `src/routes/`) | Write before implementation (TDD); use realistic values |
1616
| Business logic | `src/**/services/` | Return pure values or `null`; no `Response`/`json()` |
1717
| 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
2626
- **Abbreviations**: avoid non-standard abbreviations (`res``response`, `btn``button`). When in doubt, spell it out.
2727
- **Lambda parameters**: no single-character names (e.g., use `placement`, `workbook`). Iterator index `i` is the only exception.
2828
- **`upsert`**: only use when the implementation performs both insert and update. For insert-only, use `initialize`, `seed`, or another accurate verb.
29-
- **`any`**: before using `any`, check the value's origin — adding a missing `@types/*` or `devDependency` often provides the correct type.
29+
- **`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:
30+
31+
| Situation | Alternative |
32+
| ---------------------------------------------------------- | ------------------------------------------------------------------------ |
33+
| Assign to a property not on the type | `obj as T & { prop: U }` (intersection cast) |
34+
| Return type too complex to write manually | `ReturnType<typeof fn>` |
35+
| Partial mock: only specific properties matter | `Partial<T>`, `Pick<T, 'a' \| 'b'>`, or `satisfies` — prefer these first |
36+
| Partial mock: none of the above narrow the type far enough | `as unknown as T` — last resort; bypasses type checking entirely |
37+
| Inline `: any` annotation where inference reaches | Delete the annotation |
38+
3039
- **UI labels**: if a label does not match actual behavior, update it or add an inline comment explaining the intentional mismatch.
3140
- **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`.
3241
- **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
3948

4049
- **Braces**: always use braces for single-statement `if` blocks. Never `if () return;` — write `if () { return; }`.
4150
- **Plural type aliases**: define `type Placements = Placement[]` instead of using `Placement[]` directly in signatures and variables.
51+
- **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.
52+
53+
```typescript
54+
// Bad: silently discards the error
55+
try { ... } catch { }
56+
try { ... } catch (_e) { }
57+
58+
// Good: log and re-throw (adds context before propagating)
59+
try { ... } catch (error) {
60+
console.error('Operation failed:', error);
61+
throw error;
62+
}
63+
64+
// Good: intentional suppression with explanation
65+
try {
66+
localStorage.setItem(key, value);
67+
} catch {
68+
// localStorage may be unavailable (private browsing) — fall back to in-memory store
69+
}
70+
```
4271

4372
### No Hard-Coded Values
4473

4574
Extract magic numbers and strings to named constants. Never embed literal values whose meaning is not self-evident from the type or immediate context.
4675

4776
```typescript
48-
// Bad
77+
// Bad: magic literals embedded inline
4978
if (grade >= 11) { ... }
5079

51-
const url = '/api/workbooks/submit';
80+
const response = await fetch('/api/workbooks/submit', options);
5281

5382
// Good
5483
const MIN_GRADE = 11;
84+
const SUBMIT_URL = '/api/workbooks/submit';
5585

5686
if (grade >= MIN_GRADE) { ... }
5787

58-
const SUBMIT_URL = '/api/workbooks/submit';
88+
const response = await fetch(SUBMIT_URL, options);
5989
```
6090

6191
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:
6797
1. Exported functions and classes (public API first)
6898
2. Internal helper functions (supporting the exports above)
6999

70-
Shared helper functions (used by two or more exports) should be grouped at the end of the file.
100+
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.
71101

72102
## Documentation
73103

.claude/rules/testing.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,25 @@ If a task description does not mention tests, add them anyway for any non-trivia
2424
- Never delete, comment out, or weaken assertions (e.g. `toEqual``toBeDefined`) to make tests pass
2525
- Fix the implementation, not the test; if the test itself is wrong, explain why in a comment or commit message
2626

27+
## Unused Imports in Test Files
28+
29+
An unused import in a test file is a signal that a test was planned but not yet written — not dead code to remove.
30+
31+
Before deleting such an import, check whether the corresponding test case is missing and add it:
32+
33+
```typescript
34+
// Bad: remove the import because it's unused
35+
import { ABCLikeProvider } from './contest_table_provider';
36+
37+
// Good: the import is unused because the test is missing — add the test
38+
test('expects to create ABCLike preset correctly', () => {
39+
const group = prepareContestProviderPresets().ABCLike();
40+
expect(group.getProvider(ContestType.ABC_LIKE)).toBeInstanceOf(ABCLikeProvider);
41+
});
42+
```
43+
44+
Removing the import silences the linter but leaves a coverage gap. Adding the test both satisfies the linter and improves coverage.
45+
2746
## Test Types
2847

2948
| Type | Tool | Location | Run Command |

.oxlintrc.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"ignorePatterns": ["**/*.svelte"],
3+
"env": {
4+
"browser": true,
5+
"node": true
6+
},
7+
"globals": {
8+
"$state": "readonly",
9+
"$derived": "readonly",
10+
"$effect": "readonly",
11+
"$props": "readonly",
12+
"$bindable": "readonly",
13+
"$inspect": "readonly"
14+
},
15+
"rules": {
16+
"@typescript-eslint/ban-ts-comment": [
17+
"error",
18+
{
19+
"ts-expect-error": "allow-with-description",
20+
"ts-ignore": false
21+
}
22+
],
23+
"@typescript-eslint/no-unused-vars": [
24+
"error",
25+
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
26+
],
27+
"@typescript-eslint/no-explicit-any": "warn"
28+
}
29+
}

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Always prefer simplicity over pathological correctness. YAGNI, KISS, DRY. No bac
2121

2222
## Tech Stack
2323

24-
SvelteKit 2 + Svelte 5 (Runes) + TypeScript | PostgreSQL + Prisma | Flowbite Svelte + Tailwind 4 | Vitest + Playwright
24+
SvelteKit 2 + Svelte 5 (Runes) + TypeScript | PostgreSQL + Prisma | Flowbite Svelte + Tailwind 4 | Vitest + Playwright | oxlint (JS/TS) + ESLint (Svelte)
2525

2626
## Commands
2727

@@ -32,7 +32,7 @@ pnpm test # Run all tests
3232
pnpm test:unit # Vitest unit tests
3333
pnpm test:e2e # Playwright E2E tests
3434
pnpm coverage # Report test coverage
35-
pnpm lint # ESLint check
35+
pnpm lint # Prettier + oxlint (JS/TS) + ESLint (.svelte) check
3636
pnpm format # Prettier format
3737
pnpm check # Svelte type check
3838
pnpm exec prisma generate # Generate Prisma client
@@ -78,7 +78,7 @@ prisma/schema.prisma # Database schema
7878
- **Forms**: Superforms + Zod validation
7979
- **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
8080
- **Naming**: `camelCase` variables, `PascalCase` types/components, `snake_case` files/routes, `kebab-case` directories
81-
- **Pre-commit**: Lefthook runs Prettier + ESLint (bypass: `LEFTHOOK=0 git commit`)
81+
- **Pre-commit**: Lefthook runs Prettier + oxlint (JS/TS) + ESLint (.svelte only) (bypass: `LEFTHOOK=0 git commit`)
8282

8383
## References
8484

CONTRIBUTING.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
- パッケージマネージャ
4848
- [pnpm](https://pnpm.io/ja/)
4949
- 文法およびフォーマットチェッカー
50-
- [ESLint](https://eslint.org/)
50+
- [oxlint](https://oxc.rs/docs/guide/usage/linter.html): JS/TS ファイルの高速リンター(50–100x 高速)
51+
- [ESLint](https://eslint.org/): Svelte ファイル専用リンター(eslint-plugin-svelte)のみ使用
5152
- [Prettier](https://prettier.io/)
5253
- [lefthook](https://github.com/evilmartians/lefthook): Git hooks 管理ツール(コミット前の自動フォーマット・リント)
5354
- Search Engine Optimization (SEO) 対策
@@ -283,7 +284,8 @@
283284

284285
- **Pre-commit Hook**: ステージ済みファイルのみに対して以下を実行
285286
- `prettier --write`: コード書式の自動修正(JavaScript、TypeScript、Markdown、Svelte)
286-
- `eslint`: リント(JavaScript、TypeScript、Svelte)
287+
- `oxlint`: JS/TS ファイルのリント(JavaScript、TypeScript)
288+
- `eslint`: Svelte ファイルのリント(.svelte のみ)
287289

288290
Hook は自動的にセットアップされるため、特別な操作は不要です。
289291

e2e/custom_colors.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ test.describe('Custom colors for TailwindCSS v4 configuration', () => {
2525
if (cssFiles.length === 0) {
2626
cssFiles = allCssFiles;
2727
}
28-
} catch (e) {
28+
} catch {
2929
// True error: directory not found or inaccessible
3030
throw new Error(`Not found CSS directory: ${cssDir}`);
3131
}

e2e/signin.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ async function login(page: Page, username: string, password: string): Promise<vo
2525
await expect(page).toHaveURL('/', { timeout: UP_TO_ONE_MINUTE });
2626
}
2727

28-
async function logout(page: Page, username: string): Promise<void> {
28+
async function logout(page: Page, _username: string): Promise<void> {
2929
// Step 1: Click user button to display dropdown
3030
// Use ID selector because role="presentation" due to Flowbite-Svelte NavLi + Dropdown
3131
const userButton = page.locator('button[id="nav-user-page"]');

eslint.config.mjs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import globals from 'globals';
66
import tsParser from '@typescript-eslint/parser';
77
import svelteParser from 'svelte-eslint-parser';
88
import sveltePlugin from 'eslint-plugin-svelte';
9-
import js from '@eslint/js';
109

1110
export default [
1211
{
@@ -28,10 +27,10 @@ export default [
2827
'**/vite.config.js.timestamp-*',
2928
'**/vite.config.ts.timestamp-*',
3029
'prisma/.fabbrica/index.ts',
30+
// oxlint handles JS/TS files
31+
'**/*.{js,ts,tsx,mjs,cjs}',
3132
],
3233
},
33-
// Base JS rules first
34-
js.configs.recommended,
3534
// Svelte rules override JS rules where appropriate (intentional)
3635
// This allows Svelte-specific handling of rules like no-undef, no-unused-vars
3736
...sveltePlugin.configs['flat/recommended'],

lefthook.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ pre-commit:
77
run: pnpm exec prettier --write {staged_files}
88
glob: '**/*.{js,jsx,ts,tsx,md,svelte}'
99

10+
- name: oxlint
11+
run: pnpm exec oxlint {staged_files}
12+
glob: '**/*.{js,jsx,ts,tsx}'
13+
1014
- name: eslint
1115
run: pnpm exec eslint {staged_files}
12-
glob: '**/*.{js,jsx,ts,tsx,svelte}'
16+
glob: '**/*.svelte'

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"test": "npm run test:e2e && npm run test:unit",
1414
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
1515
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
16-
"lint": "prettier --check . && eslint .",
16+
"lint": "prettier --check . && oxlint . && eslint .",
1717
"format": "prettier --write .",
1818
"test:e2e": "playwright test",
1919
"test:unit": "vitest",
@@ -26,7 +26,6 @@
2626
"@dnd-kit/abstract": "0.3.2",
2727
"@dnd-kit/dom": "0.3.2",
2828
"@eslint/eslintrc": "3.3.5",
29-
"@eslint/js": "10.0.1",
3029
"@playwright/test": "1.58.2",
3130
"@quramy/prisma-fabbrica": "2.3.3",
3231
"@sveltejs/adapter-vercel": "6.3.3",
@@ -47,9 +46,10 @@
4746
"flowbite": "3.1.2",
4847
"flowbite-svelte": "1.31.0",
4948
"globals": "17.4.0",
50-
"lefthook": "2.1.4",
5149
"jsdom": "29.0.1",
50+
"lefthook": "2.1.4",
5251
"nock": "14.0.11",
52+
"oxlint": "^1.56.0",
5353
"pnpm": "10.32.1",
5454
"prettier": "3.8.1",
5555
"prettier-plugin-svelte": "3.5.1",

0 commit comments

Comments
 (0)