Skip to content

Commit af80c05

Browse files
committed
feat(linter/plugins): add defineRule API
1 parent 2dc8adb commit af80c05

File tree

9 files changed

+147
-3
lines changed

9 files changed

+147
-3
lines changed

apps/oxlint/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "oxlint",
33
"version": "1.16.0",
4+
"main": "dist/index.js",
45
"bin": "dist/cli.js",
56
"type": "module",
67
"scripts": {

apps/oxlint/src-js/index.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { Context } from './plugins/context.ts';
2+
import type { Rule } from './plugins/load.ts';
3+
4+
const { defineProperty, getPrototypeOf, setPrototypeOf } = Object;
5+
6+
const dummyOptions: unknown[] = [],
7+
dummyReport = () => {};
8+
9+
// Define a rule.
10+
// If rule has `createOnce` method, add an ESLint-compatible `create` method which delegates to `createOnce`.
11+
export function defineRule(rule: Rule): Rule {
12+
if (!('createOnce' in rule)) return rule;
13+
if ('create' in rule) throw new Error('Rules must define only `create` or `createOnce` methods, not both');
14+
15+
// Run `createOnce` with empty context object.
16+
// Really, `context` should be an instance of `Context`, which would throw error on accessing e.g. `id`
17+
// in body of `createOnce`. But any such bugs should have been caught when testing the rule in Oxlint,
18+
// so should be OK to take this shortcut.
19+
const context = Object.create(null, {
20+
id: { value: '', enumerable: true, configurable: true },
21+
options: { value: dummyOptions, enumerable: true, configurable: true },
22+
report: { value: dummyReport, enumerable: true, configurable: true },
23+
});
24+
25+
const { before: beforeHook, after: afterHook, ...visitor } = rule.createOnce(context as Context);
26+
27+
// Add `after` hook to `Program:exit` visit fn
28+
if (afterHook !== null) {
29+
const programExit = visitor['Program:exit'];
30+
visitor['Program:exit'] = programExit
31+
? (node) => {
32+
programExit(node);
33+
afterHook();
34+
}
35+
: (_node) => afterHook();
36+
}
37+
38+
// Create `create` function
39+
rule.create = (eslintContext) => {
40+
// Copy properties from ESLint's context object to `context`.
41+
// ESLint's context object is an object of form `{ id, options, report }`, with all other properties
42+
// and methods on another object which is its prototype.
43+
defineProperty(context, 'id', { value: eslintContext.id });
44+
defineProperty(context, 'options', { value: eslintContext.options });
45+
defineProperty(context, 'report', { value: eslintContext.report });
46+
setPrototypeOf(context, getPrototypeOf(eslintContext));
47+
48+
// If `before` hook returns `false`, skip rest of traversal by returning an empty object as visitor
49+
if (beforeHook !== null) {
50+
const shouldRun = beforeHook();
51+
if (shouldRun === false) return {};
52+
}
53+
54+
// Return same visitor each time
55+
return visitor;
56+
};
57+
58+
return rule;
59+
}

apps/oxlint/src-js/plugins/load.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ interface Plugin {
1616
// Linter rule.
1717
// `Rule` can have either `create` method, or `createOnce` method.
1818
// If `createOnce` method is present, `create` is ignored.
19-
type Rule = CreateRule | CreateOnceRule;
19+
export type Rule = CreateRule | CreateOnceRule;
2020

2121
interface CreateRule {
2222
create: (context: Context) => Visitor;
2323
}
2424

25-
interface CreateOnceRule {
25+
export interface CreateOnceRule {
2626
create?: (context: Context) => Visitor;
2727
createOnce: (context: Context) => VisitorWithHooks;
2828
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"plugins": ["./test_plugin"],
3+
"categories": {"correctness": "off"},
4+
"rules": {
5+
"define-rule-plugin/create": "error",
6+
"define-rule-plugin/create-once": "error"
7+
},
8+
"ignorePatterns": ["test_plugin/**"]
9+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let a, b;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let c, d;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { dirname, sep } from 'node:path';
2+
import { defineRule } from 'oxlint';
3+
4+
const SPAN = { start: 0, end: 0 };
5+
6+
const PARENT_DIR_PATH_LEN = dirname(import.meta.dirname).length + 1;
7+
8+
const relativePath = sep === '/'
9+
? path => path.slice(PARENT_DIR_PATH_LEN)
10+
: path => path.slice(PARENT_DIR_PATH_LEN).replace(/\\/g, '/');
11+
12+
const createRule = defineRule({
13+
create(context) {
14+
return {
15+
Identifier(node) {
16+
context.report({
17+
message: `ident visit fn "${node.name}": filename: ${relativePath(context.filename)}`,
18+
node,
19+
});
20+
},
21+
};
22+
},
23+
});
24+
25+
const createOnceRule = defineRule({
26+
createOnce(context) {
27+
// `fileNum` should be different for each file.
28+
// `identNum` should start at 1 for each file.
29+
let fileNum = 0, identNum;
30+
return {
31+
before() {
32+
fileNum++;
33+
identNum = 0;
34+
context.report({
35+
message: 'before hook:\n'
36+
+ `fileNum: ${fileNum}\n`
37+
+ `filename: ${relativePath(context.filename)}`,
38+
node: SPAN,
39+
});
40+
},
41+
Identifier(node) {
42+
identNum++;
43+
context.report({
44+
message: `ident visit fn "${node.name}":\n`
45+
+ `fileNum: ${fileNum}\n`
46+
+ `identNum: ${identNum}\n`
47+
+ `filename: ${relativePath(context.filename)}`,
48+
node,
49+
});
50+
},
51+
after() {
52+
context.report({
53+
message: 'after hook:\n'
54+
+ `fileNum: ${fileNum}\n`
55+
+ `identNum: ${identNum}\n`
56+
+ `filename: ${relativePath(context.filename)}`,
57+
node: SPAN,
58+
});
59+
},
60+
};
61+
},
62+
});
63+
64+
export default {
65+
meta: {
66+
name: "define-rule-plugin",
67+
},
68+
rules: {
69+
create: createRule,
70+
"create-once": createOnceRule,
71+
},
72+
};

apps/oxlint/tsdown.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defineConfig } from 'tsdown';
22

33
export default defineConfig({
4-
entry: ['src-js/cli.ts', 'src-js/plugins/index.ts'],
4+
entry: ['src-js/index.ts', 'src-js/cli.ts', 'src-js/plugins/index.ts'],
55
format: ['esm'],
66
platform: 'node',
77
target: 'node20',

npm/oxlint/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"url": "git+https://github.com/oxc-project/oxc",
1414
"directory": "npm/oxlint"
1515
},
16+
"main": "dist/index.js",
1617
"bin": {
1718
"oxlint": "bin/oxlint",
1819
"oxc_language_server": "bin/oxc_language_server"

0 commit comments

Comments
 (0)