go-bananas/web/eslint.config.js

130 lines
5.4 KiB
JavaScript
Raw Normal View History

2026-10-07 15:11:35 +02:00
import js from '@eslint/js'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import globals from 'globals'
import tseslint from 'typescript-eslint'
/**
* ESLint flat configuration.
*
* Two decisions are worth stating:
*
* 1. Type-aware linting is on (`projectService`), because the rules that catch
* real bugs in a codebase using `unknown` at its boundaries need type
* information. This is also why TypeScript is pinned to 6.x: see ADR 0004.
*
* 2. `--max-warnings 0` in the npm script. A warning that is tolerated is a
* warning nobody reads, so the build is the place it stops.
*/
export default tseslint.config(
{
ignores: ['dist', 'coverage', 'node_modules', '*.config.js', '*.config.ts'],
},
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
globals: { ...globals.browser, ...globals.es2022 },
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
// --- correctness -----------------------------------------------------
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
// A promise-returning handler is the normal React pattern; the void
// operator is the fix for the cases that are actually a mistake.
{ checksVoidReturn: { arguments: false, attributes: false } },
],
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/restrict-template-expressions': [
'error',
{ allowNumber: true, allowBoolean: true },
],
'@typescript-eslint/switch-exhaustiveness-check': 'error',
// `||` is used deliberately where an empty string should fall through,
// which is exactly what `??` does not do.
'@typescript-eslint/prefer-nullish-coalescing': 'off',
// A type alias reads better than an interface for everything in this
// codebase, and the two are interchangeable in practice.
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
// --- readability -----------------------------------------------------
// Both return-type annotation rules are off by design.
//
// `tsc` with `strict` already enforces a type at every boundary: a
// component with a wrongly-typed prop is a compile error whether or not
// its return type is written out. What the ESLint rules add is annotation
// churn — one per callback, and `(): JSX.Element =>` on components that
// legitimately start returning null and then need the annotation widened.
// That is a maintenance cost with no defect class behind it.
//
// What *is* enforced instead: every component declares a typed props
// interface, every exported non-component function carries a JSDoc
// `@returns`, and `strict` plus `noUncheckedIndexedAccess` run on every
// build. See docs/standards.md.
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
// An empty function is a legitimate no-op: a ResizeObserver stub, an
// intentionally ignored error, a deliberate empty effect body.
'@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions', 'methods'] }],
// A leading or trailing underscore is the convention for an intentionally
// unused binding, and no-unused-vars above is configured to honour it. The
// two rules have to agree or every intentionally-ignored value is a
// naming error.
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'variable',
format: ['camelCase', 'UPPER_CASE', 'PascalCase'],
leadingUnderscore: 'allow',
trailingUnderscore: 'allow',
},
{ selector: 'parameter', format: ['camelCase'], leadingUnderscore: 'allow' },
{ selector: 'typeLike', format: ['PascalCase'] },
],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrors: 'none' },
],
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'no-console': ['warn', { allow: ['warn', 'error'] }],
eqeqeq: ['error', 'always', { null: 'ignore' }],
'prefer-const': 'error',
'object-shorthand': 'error',
},
},
// Tests are allowed to reach into internals and to use `any` in a double.
{
files: ['**/*.test.ts', '**/*.test.tsx', 'src/test/**'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/unbound-method': 'off',
'no-console': 'off',
},
},
)