| 1 | import { defineRule, type ESTree } from "@oxlint/plugins"; |
| 2 | |
| 3 | // Comments that switch a diagnostic off or on. None of them explains another. |
| 4 | const DIRECTIVE_PATTERN = |
| 5 | /^\s*\*?\s*(?:(?:oxlint|eslint)-(?:disable|enable)|@ts-(?:expect-error|ignore|nocheck)|@effect-diagnostics)/u; |
| 6 | // TypeScript's own directives; whatever follows one is its reason. |
| 7 | const TS_DIRECTIVE_PATTERN = /^\s*\*?\s*@ts-(?:expect-error|ignore|nocheck)(?![\w-])(.*)$/su; |
| 8 | const EMPTY_COMMENT_PATTERN = /^[\s*]*$/u; |
| 9 | |
| 10 | /** |
| 11 | * Reports a directive that disables a lint rule or the type checker without saying why. The reason |
| 12 | * is a `-- reason` suffix (text after the directive, for `@ts-*`) or a comment on the line above. |
| 13 | */ |
| 14 | export default defineRule({ |
| 15 | meta: { |
| 16 | type: "suggestion", |
| 17 | docs: { |
| 18 | description: "Require every lint or type-checker suppression to say why.", |
| 19 | }, |
| 20 | }, |
| 21 | create(context) { |
| 22 | const { sourceCode } = context; |
| 23 | return { |
| 24 | Program() { |
| 25 | const comments = sourceCode.getAllComments(); |
| 26 | const ownLineCommentsByEndLine = new Map<number, ESTree.Comment>(); |
| 27 | for (const comment of comments) { |
| 28 | const { line, column } = comment.loc.start; |
| 29 | if ((sourceCode.lines[line - 1] ?? "").slice(0, column).trim() === "") { |
| 30 | ownLineCommentsByEndLine.set(comment.loc.end.line, comment); |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | // A comment directly above explains a directive, even across stacked directives and empty |
| 35 | // comment lines. |
| 36 | const explainedAbove = (directive: ESTree.Comment) => { |
| 37 | let above = ownLineCommentsByEndLine.get(directive.loc.start.line - 1); |
| 38 | while ( |
| 39 | above && |
| 40 | (DIRECTIVE_PATTERN.test(above.value) || EMPTY_COMMENT_PATTERN.test(above.value)) |
| 41 | ) { |
| 42 | above = ownLineCommentsByEndLine.get(above.loc.start.line - 1); |
| 43 | } |
| 44 | return above !== undefined; |
| 45 | }; |
| 46 | |
| 47 | for (const directive of sourceCode.getDisableDirectives().directives) { |
| 48 | if (directive.type === "enable" || directive.justification?.trim()) continue; |
| 49 | if (explainedAbove(directive.node)) continue; |
| 50 | context.report({ |
| 51 | loc: directive.node.loc, |
| 52 | message: |
| 53 | "Say why: end the directive with `-- reason` or put a comment on the line above.", |
| 54 | }); |
| 55 | } |
| 56 | |
| 57 | for (const comment of comments) { |
| 58 | const after = TS_DIRECTIVE_PATTERN.exec(comment.value)?.[1]; |
| 59 | if (after === undefined || after.replace(/^[\s:-]+/u, "").trim()) continue; |
| 60 | if (explainedAbove(comment)) continue; |
| 61 | context.report({ |
| 62 | loc: comment.loc, |
| 63 | message: |
| 64 | "Say why: write the reason after the directive or in a comment on the line above.", |
| 65 | }); |
| 66 | } |
| 67 | }, |
| 68 | }; |
| 69 | }, |
| 70 | }); |