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
2 changes: 1 addition & 1 deletion apps/oxlint/scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const parserFilePaths = [
'generated/lazy/types.js',
'generated/lazy/walk.js',
*/
'generated/deserialize/ts.js',
'generated/deserialize/ts_range.js',
'generated/visit/keys.js',
'generated/visit/types.js',
'generated/visit/visitor.d.ts',
Expand Down
3 changes: 2 additions & 1 deletion apps/oxlint/src-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { CreateOnceRule, Plugin, Rule } from './plugins/load.ts';
import type { BeforeHook, Visitor, VisitorWithHooks } from './plugins/types.ts';

export type { Context, Diagnostic, DiagnosticBase, DiagnosticWithLoc, DiagnosticWithNode } from './plugins/context.ts';
export type { Fix, Fixer, FixFn, Range } from './plugins/fix.ts';
export type { Fix, Fixer, FixFn } from './plugins/fix.ts';
export type { CreateOnceRule, CreateRule, Plugin, Rule } from './plugins/load.ts';
export type {
Definition,
Expand All @@ -23,6 +23,7 @@ export type {
Location,
Node,
NodeOrToken,
Range,
RuleMeta,
Token,
Visitor,
Expand Down
13 changes: 11 additions & 2 deletions apps/oxlint/src-js/plugins/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,23 @@ export class Context {
const { node } = diagnostic as DiagnosticWithNode;
if (node == null) throw new TypeError('Either `node` or `loc` is required');
if (typeof node !== 'object') throw new TypeError('`node` must be an object');
({ start, end } = node);

// ESLint uses `loc` here instead of `range`.
// We can't do that because AST nodes don't have `loc` property yet. In any case, `range` is preferable,
// as otherwise we have to convert `loc` to `range` which is expensive at present.
// TODO: Revisit this once we have `loc` support in AST, and a fast translation table to convert `loc` to `range`.
const { range } = node;
if (range === null || typeof range !== 'object') throw new TypeError('`node.range` must be present');
start = range[0];
end = range[1];

// Do type validation checks here, to ensure no error in serialization / deserialization.
// Range validation happens on Rust side.
if (
typeof start !== 'number' || typeof end !== 'number' ||
start < 0 || end < 0 || (start | 0) !== start || (end | 0) !== end
) {
throw new TypeError('`node.start` and `node.end` must be non-negative integers');
throw new TypeError('`node.range[0]` and `node.range[1]` must be non-negative integers');
}
}

Expand Down
12 changes: 5 additions & 7 deletions apps/oxlint/src-js/plugins/fix.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { assertIs } from './utils.js';

import type { Diagnostic, InternalContext } from './context.ts';
import type { NodeOrToken } from './types.ts';
import type { NodeOrToken, Range } from './types.ts';

const { prototype: ArrayPrototype, from: ArrayFrom } = Array,
{ getPrototypeOf, hasOwn, prototype: ObjectPrototype } = Object,
Expand All @@ -18,37 +18,35 @@ export type FixFn = (
// Type of a fix, as returned by `fix` function.
export type Fix = { range: Range; text: string };

export type Range = [number, number];

// Fixer, passed as argument to `fix` function passed to `Context#report()`.
//
// Fixer is stateless, so reuse a single object for all fixes.
// Freeze the object to prevent user mutating it.
const FIXER = Object.freeze({
insertTextBefore(nodeOrToken: NodeOrToken, text: string): Fix {
const { start } = nodeOrToken;
const start = nodeOrToken.range[0];
return { range: [start, start], text };
},
insertTextBeforeRange(range: Range, text: string): Fix {
const start = range[0];
return { range: [start, start], text };
},
insertTextAfter(nodeOrToken: NodeOrToken, text: string): Fix {
const { end } = nodeOrToken;
const end = nodeOrToken.range[1];
return { range: [end, end], text };
},
insertTextAfterRange(range: Range, text: string): Fix {
const end = range[1];
return { range: [end, end], text };
},
remove(nodeOrToken: NodeOrToken): Fix {
return { range: [nodeOrToken.start, nodeOrToken.end], text: '' };
return { range: nodeOrToken.range, text: '' };
},
removeRange(range: Range): Fix {
return { range, text: '' };
},
replaceText(nodeOrToken: NodeOrToken, text: string): Fix {
return { range: [nodeOrToken.start, nodeOrToken.end], text };
return { range: nodeOrToken.range, text };
},
replaceTextRange(range: Range, text: string): Fix {
return { range, text };
Expand Down
2 changes: 1 addition & 1 deletion apps/oxlint/src-js/plugins/source_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
// @ts-expect-error
} from '../generated/constants.js';
// @ts-expect-error we need to generate `.d.ts` file for this module
import { deserializeProgramOnly } from '../../dist/generated/deserialize/ts.js';
import { deserializeProgramOnly } from '../../dist/generated/deserialize/ts_range.js';

import type { Program } from '@oxc-project/types';
import type { Scope, ScopeManager, Variable } from './scope.ts';
Expand Down
38 changes: 23 additions & 15 deletions apps/oxlint/src-js/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,32 @@ export interface VisitorWithHooks extends Visitor {
// Visit function for a specific AST node type.
export type VisitFn = (node: Node) => void;

// Internal interface for any type which has `start` and `end` properties.
// We'll add `range` and `loc` properties to this later.
// Internal interface for any type which has location properties.
interface Spanned {
start: number;
end: number;
// This property should not be optional - all AST nodes do have a `range` field.
// But ESTree types have `range` field as optional, so to allow AST nodes to be passed
// to methods which take `Node`, we have to make it optional here too.
// TODO: Fix this
range?: Range;
loc?: Location;
}

// Range of source offsets.
export type Range = [number, number];

// Source code location.
export interface Location {
start: LineColumn;
end: LineColumn;
}

// Line number + column number pair.
// `line` is 1-indexed, `column` is 0-indexed.
export interface LineColumn {
line: number;
column: number;
}

// AST node type.
Expand All @@ -50,19 +71,6 @@ export interface Comment extends Spanned {
value: string;
}

// Source code location.
export interface Location {
start: LineColumn;
end: LineColumn;
}

// Line number + column number pair.
// `line` is 1-indexed, `column` is 0-indexed.
export interface LineColumn {
line: number;
column: number;
}

// Element of compiled visitor array.
// * `VisitFn | null` for leaf nodes.
// * `EnterExit | null` for non-leaf nodes.
Expand Down
4 changes: 2 additions & 2 deletions apps/oxlint/test/fixtures/context_properties/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { sep } from 'node:path';

import type { Plugin, Rule } from '../../../dist/index.js';
import type { Node, Plugin, Rule } from '../../../dist/index.js';

const SPAN = { start: 0, end: 0 };
const SPAN: Node = { start: 0, end: 0, range: [0, 0] };

const DIR_PATH_LEN = import.meta.dirname.length + 1;

Expand Down
4 changes: 2 additions & 2 deletions apps/oxlint/test/fixtures/createOnce/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { sep } from 'node:path';

import type { Plugin, Rule } from '../../../dist/index.js';
import type { Node, Plugin, Rule } from '../../../dist/index.js';

const SPAN = { start: 0, end: 0 };
const SPAN: Node = { start: 0, end: 0, range: [0, 0] };

const DIR_PATH_LEN = import.meta.dirname.length + 1;

Expand Down
6 changes: 4 additions & 2 deletions apps/oxlint/test/fixtures/definePlugin/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { sep } from 'node:path';
import { definePlugin } from '../../../dist/index.js';
import type { Rule } from '../../../dist/index.js';

import type { Node, Rule } from '../../../dist/index.js';

// `loc` is required for ESLint
const SPAN = {
const SPAN: Node = {
start: 0,
end: 0,
range: [0, 0],
loc: {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { sep } from 'node:path';
import { definePlugin, defineRule } from '../../../dist/index.js';

import type { Node } from '../../../dist/index.js';

// `loc` is required for ESLint
const SPAN = {
const SPAN: Node = {
start: 0,
end: 0,
range: [0, 0],
loc: {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 },
Expand Down
5 changes: 4 additions & 1 deletion apps/oxlint/test/fixtures/defineRule/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { sep } from 'node:path';
import { defineRule } from '../../../dist/index.js';

import type { Node } from '../../../dist/index.js';

// `loc` is required for ESLint
const SPAN = {
const SPAN: Node = {
start: 0,
end: 0,
range: [0, 0],
loc: {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 },
Expand Down
88 changes: 87 additions & 1 deletion apps/oxlint/test/fixtures/estree/output.snap.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,93 @@
14 | `-> type U = (((((string)) | ((number)))));
`----

Found 0 warnings and 1 error.
x estree-check(check): program:
| start/end: [59,265]
| range: [59,265]
,-[files/index.ts:5:1]
4 | // All `Identifier`s
5 | ,-> let a = { x: y };
6 | |
7 | | // No `ParenthesizedExpression`s in AST
8 | | const b = (x * ((('str' + ((123))))));
9 | |
10 | | // TS syntax
11 | | type T = string;
12 | |
13 | | // No `TSParenthesizedType`s in AST
14 | `-> type U = (((((string)) | ((number)))));
`----

x estree-check(check): ident "a":
| start/end: [63,64]
| range: [63,64]
,-[files/index.ts:5:5]
4 | // All `Identifier`s
5 | let a = { x: y };
: ^
6 |
`----

x estree-check(check): ident "x":
| start/end: [69,70]
| range: [69,70]
,-[files/index.ts:5:11]
4 | // All `Identifier`s
5 | let a = { x: y };
: ^
6 |
`----

x estree-check(check): ident "y":
| start/end: [72,73]
| range: [72,73]
,-[files/index.ts:5:14]
4 | // All `Identifier`s
5 | let a = { x: y };
: ^
6 |
`----

x estree-check(check): ident "b":
| start/end: [124,125]
| range: [124,125]
,-[files/index.ts:8:7]
7 | // No `ParenthesizedExpression`s in AST
8 | const b = (x * ((('str' + ((123))))));
: ^
9 |
`----

x estree-check(check): ident "x":
| start/end: [129,130]
| range: [129,130]
,-[files/index.ts:8:12]
7 | // No `ParenthesizedExpression`s in AST
8 | const b = (x * ((('str' + ((123))))));
: ^
9 |
`----

x estree-check(check): ident "T":
| start/end: [176,177]
| range: [176,177]
,-[files/index.ts:11:6]
10 | // TS syntax
11 | type T = string;
: ^
12 |
`----

x estree-check(check): ident "U":
| start/end: [230,231]
| range: [230,231]
,-[files/index.ts:14:6]
13 | // No `TSParenthesizedType`s in AST
14 | type U = (((((string)) | ((number)))));
: ^
`----

Found 0 warnings and 9 errors.
Finished in Xms on 1 file using X threads.
```

Expand Down
12 changes: 12 additions & 0 deletions apps/oxlint/test/fixtures/estree/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const plugin: Plugin = {
const visits: string[] = [];
return {
Program(program) {
context.report({
message: 'program:\n' +
`start/end: [${program.start},${program.end}]\n` +
`range: [${program.range}]`,
node: program,
});
visits.push(program.type);
},
VariableDeclaration(decl) {
Expand All @@ -26,6 +32,12 @@ const plugin: Plugin = {
visits.push(`${decl.type}: (init: ${decl.init.type})`);
},
Identifier(ident) {
context.report({
message: `ident "${ident.name}":\n` +
`start/end: [${ident.start},${ident.end}]\n` +
`range: [${ident.range}]`,
node: ident,
});
visits.push(`${ident.type}: ${ident.name}`);
},
ObjectExpression(expr) {
Expand Down
12 changes: 7 additions & 5 deletions apps/oxlint/test/fixtures/fixes/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Diagnostic, Plugin, Range } from '../../../dist/index.js';
import type { Diagnostic, Node, Plugin } from '../../../dist/index.js';

const plugin: Plugin = {
meta: {
Expand Down Expand Up @@ -87,7 +87,8 @@ const plugin: Plugin = {
// Fixes can be in any order
return [
fixer.insertTextAfter(node, 'ty'),
fixer.replaceText(node, 'mp'),
// Test that any object with `range` property works
fixer.replaceText({ range: [node.start, node.end] } as Node, 'mp'),
fixer.insertTextBefore(node, 'nu'),
];
},
Expand All @@ -98,7 +99,7 @@ const plugin: Plugin = {
node,
fix(fixer) {
// Fixes can be in any order
const range: Range = [node.start, node.end];
const { range } = node;
return [
fixer.replaceTextRange(range, 'er'),
fixer.insertTextAfterRange(range, 'mouse'),
Expand All @@ -114,7 +115,8 @@ const plugin: Plugin = {
*fix(fixer) {
yield fixer.insertTextBefore(node, 'gra');
yield fixer.replaceText(node, 'nu');
yield fixer.insertTextAfter(node, 'lar');
// Test that any object with `range` property works
yield fixer.insertTextAfter({ range: [node.start, node.end] } as Node, 'lar');
},
});
case 'j':
Expand All @@ -124,7 +126,7 @@ const plugin: Plugin = {
// `fix` can be a generator function
*fix(fixer) {
// Fixes can be in any order
const range: Range = [node.start, node.end];
const { range } = node;
yield fixer.insertTextAfterRange(range, 'bunga');
yield fixer.replaceTextRange(range, 'a');
yield fixer.insertTextBeforeRange(range, 'cow');
Expand Down
Loading
Loading