Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions apps/oxlint/src-js/plugins/directives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* 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_disable_directive`, `match_enable_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;
}

const LABEL_PATTERN =
/^\s*(?<label>(?:eslint|oxlint)-(?:disable(?:(?:-next)?-line)?|enable))(?:\s|$)/u;
const LINE_DIRECTIVE_PATTERN = /^(?:eslint|oxlint)-disable-(?:-next)?-line$/u;
const JUSTIFICATION_SEP_PATTERN = /\s-{2,}\s/u;

export function getDisableDirectives() {
Comment thread
camc314 marked this conversation as resolved.
const problems: Problem[] = [];
const directives: Directive[] = [];

getAllComments().forEach((comment) => {
if (comment.type === "Shebang") return;

let match = LABEL_PATTERN.exec(comment.value);
if (!match?.groups?.label) return;

// Only some comment types are supported as line comments
if (comment.type === "Line" && LINE_DIRECTIVE_PATTERN.test(match.groups.label)) return;
Comment thread
camc314 marked this conversation as resolved.

const { label } = match.groups;

// Validate directive does not span multiple lines
if (
(label === "eslint-disable-line" || label === "oxlint-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,
});
return;
}

const rest = comment.value.slice(match[0].length).trim();
match = JUSTIFICATION_SEP_PATTERN.exec(rest);

const [value, justification] = match
? [rest.slice(0, match.index).trim(), rest.slice(match.index + match[0].length).trim()]
: [rest, ""];
Comment thread
camc314 marked this conversation as resolved.

directives.push({
// oxlint- and eslint- are each 7 characters
type: label.slice(7) as DirectiveType,
node: comment,
value,
justification,
});
});

return { problems, directives };
}
4 changes: 4 additions & 0 deletions apps/oxlint/src-js/plugins/source_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import visitorKeys from "../generated/keys.ts";
import { resetComments } from "./comments.ts";
import * as commentMethods from "./comments_methods.ts";
import { ecmaVersion } from "./context.ts";
import * as directiveMethods from "./directives.ts";
import * as locationMethods from "./location.ts";
import { initLines, lines, lineStartIndices, resetLinesAndLocs } from "./location.ts";
import { resetScopeManager, SCOPE_MANAGER } from "./scope.ts";
Expand Down Expand Up @@ -348,6 +349,9 @@ export const SOURCE_CODE = Object.freeze({
getTokenByRangeStart: tokenMethods.getTokenByRangeStart,
isSpaceBetween: tokenMethods.isSpaceBetween,
isSpaceBetweenTokens: tokenMethods.isSpaceBetweenTokens,

// Directive methods
getDisableDirectives: directiveMethods.getDisableDirectives,
});

export type SourceCode = typeof SOURCE_CODE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"jsPlugins": ["./plugin.ts"],
"rules": {
"get-disable-directives-plugin/get-disable-directives": "error"
}
}
26 changes: 26 additions & 0 deletions apps/oxlint/test/fixtures/getDisableDirectives/files/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// oxlint-disable no-unused-vars
let a;

// eslint-disable no-unused-vars
let b;

// oxlint-disable-next-line no-console
console.log("test");

// eslint-disable-next-line no-console
console.log("test2");

let c; // oxlint-disable-line no-unused-vars
let d; // eslint-disable-line no-unused-vars

// eslint-disable no-foo -- justification for disabling no-foo
// oxlint-disable no-bar -- justification for disabling no-bar
let e;
let f;

/* oxlint-disable no-unused-vars */
/* eslint-disable no-unused-vars */
let g;
let h;
/* oxlint-enable no-unused-vars */
/* eslint-enable no-unused-vars */
47 changes: 47 additions & 0 deletions apps/oxlint/test/fixtures/getDisableDirectives/output.snap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Exit code
1

# stdout
```
x get-disable-directives-plugin(get-disable-directives): getDisableDirectives:
| total: 12
| block: 6
| line: 2
| next-line: 2
| enable: 2
,-[files/test.js:2:1]
1 | // oxlint-disable no-unused-vars
2 | ,-> let a;
3 | |
4 | | // eslint-disable no-unused-vars
5 | | let b;
6 | |
7 | | // oxlint-disable-next-line no-console
8 | | console.log("test");
9 | |
10 | | // eslint-disable-next-line no-console
11 | | console.log("test2");
12 | |
13 | | let c; // oxlint-disable-line no-unused-vars
14 | | let d; // eslint-disable-line no-unused-vars
15 | |
16 | | // eslint-disable no-foo -- justification for disabling no-foo
17 | | // oxlint-disable no-bar -- justification for disabling no-bar
18 | | let e;
19 | | let f;
20 | |
21 | | /* oxlint-disable no-unused-vars */
22 | | /* eslint-disable no-unused-vars */
23 | | let g;
24 | | let h;
25 | | /* oxlint-enable no-unused-vars */
26 | `-> /* eslint-enable no-unused-vars */
`----

Found 0 warnings and 1 error.
Finished in Xms on 1 file with 96 rules using X threads.
```

# stderr
```
```
69 changes: 69 additions & 0 deletions apps/oxlint/test/fixtures/getDisableDirectives/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import assert from "node:assert";

import type { Plugin, Rule } from "#oxlint/plugins";

const rule: Rule = {
create(context) {
const { sourceCode } = context;
const { problems, directives } = sourceCode.getDisableDirectives();

// Test problems (e.g., multi-line disable-line)
assert.strictEqual(problems.length, 0, `Expected no problems, got ${problems.length}`);

// Test that we have directives
assert(directives.length > 0, "Expected some directives");

// Test directive types
const [
blockDirectivesCount,
lineDirectivesCount,
nextLineDirectivesCount,
enableDirectivesCount,
] = directives.reduce(
([blockDirectives, lineDirectives, nextLineDirectives, enableDirectives], { type }) => [
blockDirectives + (type === "disable" ? 1 : 0),
lineDirectives + (type === "disable-line" ? 1 : 0),
nextLineDirectives + (type === "disable-next-line" ? 1 : 0),
enableDirectives + (type === "enable" ? 1 : 0),
],
[0, 0, 0, 0],
);

assert(blockDirectivesCount > 0, "Expected block directives");
assert(lineDirectivesCount > 0, "Expected line directives");
assert(nextLineDirectivesCount > 0, "Expected next-line directives");
assert(enableDirectivesCount > 0, "Expected enable directives");

// Test that all directives have required fields
for (const directive of directives) {
assert(
["disable", "disable-line", "disable-next-line", "enable"].includes(directive.type),
`Invalid directive type: ${directive.type}`,
);
assert(directive.node, "Directive must have a node");
assert(directive.value !== undefined, "Directive must have a value");
}

context.report({
message:
`getDisableDirectives:\n` +
` total: ${directives.length}\n` +
` block: ${blockDirectivesCount}\n` +
` line: ${lineDirectivesCount}\n` +
` next-line: ${nextLineDirectivesCount}\n` +
` enable: ${enableDirectivesCount}`,
node: sourceCode.ast,
});

return {};
},
};

const plugin: Plugin = {
meta: { name: "get-disable-directives-plugin" },
rules: {
"get-disable-directives": rule,
},
};

export default plugin;
Loading