import js from '@eslint/js'; import tseslint from 'typescript-eslint'; import sonarjs from 'eslint-plugin-sonarjs'; import globals from 'globals'; // Named `.mjs` because this package is CommonJS — `eslint.config.js` would be // parsed as CJS and the imports above would fail. // // Policy: every preset is downgraded to advisory, and the rules that actually // fail the build are listed once at the bottom. That way the CI gate is // readable in one place rather than inferred from four presets' defaults. // The reasoning behind the split, and the measurements it rests on, are in // docs/superpowers/specs/2026-08-19-eslint-design.md. /** * Rewrites a preset's enabled rules to `warn`, preserving each rule's options. * Rules the preset explicitly turned off stay off — a preset that disables a * rule means it, and flipping those to `warn` turns the whole of SonarJS's * opt-in catalogue (file headers, naming conventions) into daily noise. */ const advisory = (config) => ({ ...config, rules: Object.fromEntries( Object.entries(config.rules ?? {}).map(([rule, level]) => { const severity = Array.isArray(level) ? level[0] : level; if (severity === 'off' || severity === 0) return [rule, level]; return [rule, Array.isArray(level) ? ['warn', ...level.slice(1)] : 'warn']; }) ), }); export default tseslint.config( // src/db-drizzle/schema.ts and relations.ts are `drizzle-kit pull` output, not // written by anyone here. #261 hand-fixed an unused-parameter warning in the // schema and #217's re-pull put it straight back, which is the whole argument: // linting generated code buys a fix that the next regeneration undoes. The // hand-written files in that directory are still linted. { ignores: [ 'dist/**', 'coverage/**', 'eslint.config.mjs', 'src/db-drizzle/schema.ts', 'src/db-drizzle/relations.ts' ] }, ...[js.configs.recommended, ...tseslint.configs.recommended, sonarjs.configs.recommended].map( advisory ), { files: ['src/**/*.ts'], languageOptions: { globals: globals.node, parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname, }, }, rules: { // The two rules this repo has actually been bitten by. #59 is the whole // argument: an async handler whose rejection nothing forwards produces no // response at all, and the request hangs rather than failing visibly. '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': [ 'error', { checksVoidReturn: { attributes: false } }, ], }, }, { // The test suites, in scope since #298. They had never been linted at all: // this config said tests were out of scope because tsconfig.json includes // only `src`, and that stayed true for long enough that two defects lived // here undetected — a unit test that opened a real TLS connection to Gmail // on every run, and integration tests that mocked the shared pg pool and // made a suite unrunnable. Neither is something lint would necessarily have // caught, but neither was ever looked at. // // `project` rather than `projectService`, for the reason the frontend's // equivalent block records: the service resolves each file to the nearest // tsconfig.json, which for tests/ is the one that excludes them, and every // file then errors as not part of a project. files: ['tests/**/*.ts'], languageOptions: { globals: { ...globals.node, ...globals.jest }, parserOptions: { project: ['./tsconfig.test.json'], tsconfigRootDir: import.meta.dirname, }, }, rules: { // The same rule src is held to, and it matters at least as much here. // An unawaited promise in a test does not fail the test — it passes, // having asserted nothing, and the failure surfaces later as a suite that // will not exit. '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': [ 'error', { checksVoidReturn: { attributes: false } }, ], // Everything below is switched off for tests rather than left as a // warning, on #60's argument: bringing these files in scope produced 77 // warnings, of which 60 were rules that cannot be true in a test. A rule // that cannot be true here is noise, and noise hides the rules that can. // What is left is signal — unused variables, useless escapes, a regex // worth a second look. // 41 of the 77. Test credentials are the entire point of a test, and this // project's own rule is that they must live only in test paths — which is // here. Flagging them where they belong trains a reader to skip the rule // where they do not. 'sonarjs/no-hardcoded-passwords': 'off', // Stub servers and fixtures: `http://127.0.0.1:`. There is no // transport to secure between a test and a socket it opened itself. 'sonarjs/no-clear-text-protocols': 'off', // 203.0.113.5 is TEST-NET-3, reserved by RFC 5737 for exactly this. A // documentation address is the correct thing to hardcode. 'sonarjs/no-hardcoded-ip': 'off', // `os.tmpdir()`, via mkdtemp, which is how these suites get a scratch // uploads directory they can delete afterwards. 'sonarjs/publicly-writable-directories': 'off', // Math.random for a run id. Nothing here is a secret; it only has to not // collide with a parallel worker. 'sonarjs/pseudo-random': 'off', // Sorting two string arrays to compare them is how several guards assert // set equality. The locale-aware comparator the rule wants would change // nothing except the reading. 'sonarjs/no-alphabetical-sort': 'off', }, } );