Files
oxc-project__oxc/apps/oxlint/src-js/plugins/directives.ts
camc314 1d22912cb4 fix(linter/plugins): parse rule-less directive justifications (#25559)
## Summary

- extract directive justifications before parsing directive labels, matching ESLint
- return an empty rule value for rule-less directives while preserving their justification
- cover block, enable, line, and next-line directives with both eslint and oxlint prefixes

Closes #25556
2026-08-12 16:44:01 +00:00

80 lines
2.6 KiB
TypeScript

/**
* Parses disable/enable directive comments (e.g., `eslint-disable`, `oxlint-enable`).
*
* Supported patterns match either:
* - ESLint: `eslint-disable`, `eslint-disable-line`, `eslint-disable-next-line`, `eslint-enable`
* - Oxlint: `oxlint-disable`, `oxlint-disable-line`, `oxlint-disable-next-line`, `oxlint-enable`
*
* The pattern matching mirrors the Rust implementation in
* `crates/oxc_linter/src/disable_directives.rs` (`match_directive`).
* This ensures consistent behavior between the Rust linter and JS plugins.
*
* @see <https://oxc.rs/docs/guide/usage/linter/ignore-comments.html#inline-ignore-comments>
*/
import { Comment } from "./comments";
import { getAllComments } from "./comments_methods";
import { Location } from "./location";
interface Problem {
ruleId: string | null;
message: string;
loc: Location;
}
type DirectiveType = "disable" | "enable" | "disable-line" | "disable-next-line";
interface Directive {
type: DirectiveType;
node: Comment;
value: string;
justification: string;
}
// Captures the whole directive label (e.g. `oxlint-disable-line`) and its type (`disable-line`)
const LABEL_PATTERN = /^\s*((?:eslint|oxlint)-(disable(?:(?:-next)?-line)?|enable))(?:\s|$)/u;
const JUSTIFICATION_SEP_PATTERN = /\s-{2,}\s/u;
export function getDisableDirectives(): { problems: Problem[]; directives: Directive[] } {
const problems: Problem[] = [];
const directives: Directive[] = [];
const comments = getAllComments();
// Skip `Shebang` comment
let i = comments.length > 0 && comments[0].type === "Shebang" ? 1 : 0;
for (; i < comments.length; i++) {
const comment = comments[i];
const sepMatch = JUSTIFICATION_SEP_PATTERN.exec(comment.value);
const directivePart = (
sepMatch === null ? comment.value : comment.value.slice(0, sepMatch.index)
).trim();
const match = LABEL_PATTERN.exec(directivePart);
if (match === null) continue;
const label = match[1];
const type = match[2] as DirectiveType;
// Validate directive does not span multiple lines
if (type === "disable-line" && comment.loc.start.line !== comment.loc.end.line) {
problems.push({
ruleId: null,
message: `${label} comment should not span multiple lines.`,
loc: comment.loc,
});
continue;
}
const value = directivePart.slice(match[0].length).trim();
const justification =
sepMatch === null ? "" : comment.value.slice(sepMatch.index + sepMatch[0].length).trim();
directives.push({ type, node: comment, value, justification });
}
return { problems, directives };
}