1import { defineRule, type ESTree } from "@oxlint/plugins";
2
3// Comments that switch a diagnostic off or on. None of them explains another.
4const 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.
7const TS_DIRECTIVE_PATTERN = /^\s*\*?\s*@ts-(?:expect-error|ignore|nocheck)(?![\w-])(.*)$/su;
8const 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 */
14export 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});