From 5dc2b259bd67534d1933f90f32a6bf9998387c2b Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Mon, 14 Sep 2026 04:42:27 +0100 Subject: [PATCH] feat: ban Stryker suppression comments and enforce an 800-line file limit no-warning-comments flags any Stryker disable/disable-next-line comment, since ESLint's own noInlineConfig has no awareness of Stryker's separate comment syntax and can't otherwise catch mutation-testing coverage being silently suppressed. max-lines caps a file at 800 real lines of code, skipping blank lines and comments so WHY-explanations don't count against it. --- README.md | 2 +- src/recommended-type-checked.test.ts | 43 ++++++++++++++++++++++++++++ src/recommended-type-checked.ts | 4 +++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cfea1d7..4acffd1 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Consumers need `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as required pee pnpm add -D @exadev/eslint-config typescript-eslint eslint ``` -The default export is the full, type-checked ruleset: typescript-eslint's `strictTypeChecked` + `stylisticTypeChecked` presets (`strictTypeChecked` subsumes `recommendedTypeChecked`, so it already includes `no-deprecated`, `no-misused-spread`, `no-mixed-enums`, `no-unnecessary-condition`, `use-unknown-in-catch-callback-variable`, `return-await`, `related-getter-setter-pairs`, `no-unnecessary-type-parameters`, and more -- those are no longer re-listed below), `exadev/barrel-policy` at `mode: 'banned'` (see [Barrel policy](#barrel-policy)), `exadev/no-object-assign`, `exadev/no-mutable-union-array-param`, `exadev/no-array-isarray-mutation`, `exadev/no-enum-number-widening`, `exadev/no-enum-reverse-lookup-widening`, `exadev/no-map-instanceof-mutation`, `exadev/no-set-instanceof-mutation`, `exadev/prefer-readonly-array-param`, `exadev/prefer-readonly-object-param`, `exadev/prefer-numeric-sort-compare`, `exadev/no-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, `@typescript-eslint/consistent-type-imports`, `@typescript-eslint/consistent-type-exports`, `@typescript-eslint/consistent-return` (a function that implicitly returns `undefined` on one path and a real value on another -- a common real bug, not a deliberate design), `@typescript-eslint/no-non-null-assertion` banning the `!` operator (the same manual-override escape hatch as a type assertion, under a different spelling), `@typescript-eslint/no-redeclare`, `@typescript-eslint/no-shadow`, `@typescript-eslint/no-use-before-define` set to `{ functions: false }` (a genuine temporal-dead-zone crash risk for `let`/`const`/`class`/enum bindings, but exempting function declarations, which are fully hoisted and therefore runtime-safe to call before their point of textual declaration -- this codebase's own rule files consistently define their helper functions after the logic that calls them), `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright, `@typescript-eslint/method-signature-style` set to `'property'` (method-shorthand signatures are checked bivariantly under `strictFunctionTypes`, which is unsound), `@typescript-eslint/prefer-readonly`, `@typescript-eslint/promise-function-async`, `@typescript-eslint/require-array-sort-compare`, `@typescript-eslint/strict-void-return` (not yet in any typescript-eslint preset -- disallows passing a value-returning function where a void-returning one is expected, e.g. `arr.forEach(x => otherArray.push(x))`, which typechecks today under TS's own void-return contravariance leniency), `@typescript-eslint/switch-exhaustiveness-check`, `@typescript-eslint/strict-boolean-expressions` at the rule's own bare defaults (an unambiguous non-nullable truthy check stays allowed; an ambiguous nullable check does not), and `@typescript-eslint/no-magic-numbers` tuned to exempt array indexes, enum members, readonly class properties, default parameter values, and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`) -- the type-assertion and ts-comment rules are relaxed in test files, and `no-magic-numbers` is not (see below). Spread it directly into `tseslint.config(...)`: +The default export is the full, type-checked ruleset: typescript-eslint's `strictTypeChecked` + `stylisticTypeChecked` presets (`strictTypeChecked` subsumes `recommendedTypeChecked`, so it already includes `no-deprecated`, `no-misused-spread`, `no-mixed-enums`, `no-unnecessary-condition`, `use-unknown-in-catch-callback-variable`, `return-await`, `related-getter-setter-pairs`, `no-unnecessary-type-parameters`, and more -- those are no longer re-listed below), `exadev/barrel-policy` at `mode: 'banned'` (see [Barrel policy](#barrel-policy)), `exadev/no-object-assign`, `exadev/no-mutable-union-array-param`, `exadev/no-array-isarray-mutation`, `exadev/no-enum-number-widening`, `exadev/no-enum-reverse-lookup-widening`, `exadev/no-map-instanceof-mutation`, `exadev/no-set-instanceof-mutation`, `exadev/prefer-readonly-array-param`, `exadev/prefer-readonly-object-param`, `exadev/prefer-numeric-sort-compare`, `exadev/no-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, `@typescript-eslint/consistent-type-imports`, `@typescript-eslint/consistent-type-exports`, `@typescript-eslint/consistent-return` (a function that implicitly returns `undefined` on one path and a real value on another -- a common real bug, not a deliberate design), `@typescript-eslint/no-non-null-assertion` banning the `!` operator (the same manual-override escape hatch as a type assertion, under a different spelling), `@typescript-eslint/no-redeclare`, `@typescript-eslint/no-shadow`, `@typescript-eslint/no-use-before-define` set to `{ functions: false }` (a genuine temporal-dead-zone crash risk for `let`/`const`/`class`/enum bindings, but exempting function declarations, which are fully hoisted and therefore runtime-safe to call before their point of textual declaration -- this codebase's own rule files consistently define their helper functions after the logic that calls them), `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright, `@typescript-eslint/method-signature-style` set to `'property'` (method-shorthand signatures are checked bivariantly under `strictFunctionTypes`, which is unsound), `@typescript-eslint/prefer-readonly`, `@typescript-eslint/promise-function-async`, `@typescript-eslint/require-array-sort-compare`, `@typescript-eslint/strict-void-return` (not yet in any typescript-eslint preset -- disallows passing a value-returning function where a void-returning one is expected, e.g. `arr.forEach(x => otherArray.push(x))`, which typechecks today under TS's own void-return contravariance leniency), `@typescript-eslint/switch-exhaustiveness-check`, `@typescript-eslint/strict-boolean-expressions` at the rule's own bare defaults (an unambiguous non-nullable truthy check stays allowed; an ambiguous nullable check does not), `@typescript-eslint/no-magic-numbers` tuned to exempt array indexes, enum members, readonly class properties, default parameter values, and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`), `max-lines` set to `{ max: 800, skipBlankLines: true, skipComments: true }` (counting only real code, so a file isn't pushed over the limit by whitespace or its own WHY-explanation comments), and `no-warning-comments` banning any comment containing `Stryker disable` (Stryker's own mutation-testing suppression directive, invisible to ESLint's `noInlineConfig` above since it isn't an eslint-disable comment at all) -- the type-assertion and ts-comment rules are relaxed in test files, and `no-magic-numbers`/`max-lines`/`no-warning-comments` are not (see below). Spread it directly into `tseslint.config(...)`: ```ts // eslint.config.ts diff --git a/src/recommended-type-checked.test.ts b/src/recommended-type-checked.test.ts index 8e89dbe..92c2f55 100644 --- a/src/recommended-type-checked.test.ts +++ b/src/recommended-type-checked.test.ts @@ -101,3 +101,46 @@ describe('recommended-type-checked test-file relaxation', () => { expect(lint('const x = 1;\n', 'src/foo.test.ts')).toContain('@typescript-eslint/consistent-type-assertions'); }); }); + +describe('no-warning-comments (Stryker suppression comments)', () => { + it('bans a Stryker disable-next-line comment', () => { + expect(lint('// Stryker disable next-line all\nconst x = 1;\n', 'src/foo.ts')).toContain('no-warning-comments'); + }); + + it('bans a Stryker disable comment scoped to a mutator list', () => { + expect(lint('// Stryker disable all: reason\nconst x = 1;\n', 'src/foo.ts')).toContain('no-warning-comments'); + }); + + it('matches case-insensitively', () => { + expect(lint('// stryker DISABLE all\nconst x = 1;\n', 'src/foo.ts')).toContain('no-warning-comments'); + }); + + it('does not flag an unrelated comment', () => { + expect(lint('// a perfectly ordinary comment\nconst x = 1;\n', 'src/foo.ts')).not.toContain('no-warning-comments'); + }); +}); + +// Matches the `max: 800` configured on the rule under test above. +const MAX_LINES = 800; +// Comfortably more blank/comment lines than MAX_LINES, to prove they are never counted no matter how many pile up. +const NON_CODE_LINE_COUNT = MAX_LINES * 2; + +// Each generated line declares a uniquely-named const -- a repeated `const x = 1;` would itself be a parse error (redeclaration in the same scope), which would mask what these tests actually check. +function generateLinesOfCode(count: number): string { + return Array.from({ length: count }, (_, index) => `const generatedLine${String(index)} = ${String(index)};`).join('\n'); +} + +describe('max-lines', () => { + it('bans a file over 800 real lines of code', () => { + expect(lint(generateLinesOfCode(MAX_LINES + 1), 'src/foo.ts')).toContain('max-lines'); + }); + + it('allows a file at or under 800 real lines of code', () => { + expect(lint(generateLinesOfCode(MAX_LINES), 'src/foo.ts')).not.toContain('max-lines'); + }); + + it('does not count blank lines or comment-only lines toward the limit', () => { + const code = `${'\n// a comment\n'.repeat(NON_CODE_LINE_COUNT)}${generateLinesOfCode(1)}\n`; + expect(lint(code, 'src/foo.ts')).not.toContain('max-lines'); + }); +}); diff --git a/src/recommended-type-checked.ts b/src/recommended-type-checked.ts index 0b4510d..22b27fe 100644 --- a/src/recommended-type-checked.ts +++ b/src/recommended-type-checked.ts @@ -79,6 +79,10 @@ const recommendedTypeChecked: ConfigArrayValue = [ '@typescript-eslint/strict-void-return': 'error', // A switch over a union or enum missing a member, with no default to fall back on, silently does nothing for the missing case instead of erroring. '@typescript-eslint/switch-exhaustiveness-check': 'error', + // A file over 800 lines is a signal to split it, not a target to hit exactly -- `skipBlankLines`/`skipComments` count only real code, so a file isn't pushed over the limit by whitespace or the WHY-explanations this codebase's own conventions require. + 'max-lines': ['error', { max: 800, skipBlankLines: true, skipComments: true }], + // A Stryker mutation-testing suppression comment (its own `disable`/`disable next-line` directive, always led by the tool's own name) silences coverage for the line(s) it annotates -- unlike an eslint-disable comment (already banned outright by noInlineConfig above), ESLint has no native awareness of Stryker's own comment syntax, so it is otherwise invisible to lint. `location: 'anywhere'` matches the term wherever it falls in the comment (Stryker's own syntax always trails it with a mutator list, `all`, or a reason), not only at the very start. + 'no-warning-comments': ['error', { terms: ['stryker disable'], location: 'anywhere' }], }, }, {