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
1 change: 1 addition & 0 deletions apps/oxlint/src-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { BeforeHook, Visitor, VisitorWithHooks } from './plugins/types.ts';
export type { Context, Diagnostic } from './plugins/context.ts';
export type { Fix, Fixer, FixFn, NodeOrToken, Range } from './plugins/fix.ts';
export type { CreateOnceRule, CreateRule, Plugin, Rule } from './plugins/load.ts';
export type { SourceCode } from './plugins/source_code.ts';
export type { AfterHook, BeforeHook, Node, RuleMeta, Visitor, VisitorWithHooks } from './plugins/types.ts';

const { defineProperty, getPrototypeOf, hasOwn, setPrototypeOf, create: ObjectCreate } = Object;
Expand Down
16 changes: 15 additions & 1 deletion apps/oxlint/src-js/plugins/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getFixes } from './fix.js';
import { SourceCode } from './source_code.js';

import type { Fix, FixFn } from './fix.ts';
import type { Node } from './types.ts';
Expand Down Expand Up @@ -32,11 +33,13 @@ export const diagnostics: DiagnosticReport[] = [];
* @param context - `Context` object
* @param ruleIndex - Index of this rule within `ruleIds` passed from Rust
* @param filePath - Absolute path of file being linted
* @param sourceText - Source text of file being linted
*/
export let setupContextForFile: (
context: Context,
ruleIndex: number,
filePath: string,
sourceText: string,
) => void;

/**
Expand Down Expand Up @@ -65,6 +68,10 @@ export interface InternalContext {
ruleIndex: number;
// Absolute path of file being linted
filePath: string;
// `SourceCode` class instance for this rule.
// Rule has single `SourceCode` instance that is updated for each file
// (NOT new `SourceCode` instance for each file).
sourceCode: SourceCode;
// Options
options: unknown[];
// `true` if rule can provide fixes (`meta.fixable` in `RuleMeta` is 'code' or 'whitespace')
Expand All @@ -89,6 +96,7 @@ export class Context {
this.#internal = {
id: fullRuleName,
filePath: '',
sourceCode: new SourceCode(),
ruleIndex: -1,
options: [],
isFixable,
Expand Down Expand Up @@ -116,6 +124,11 @@ export class Context {
return getInternal(this, 'access `context.options`').options;
}

// Getter for `SourceCode` for file being linted.
get sourceCode() {
return getInternal(this, 'access `context.sourceCode`').sourceCode;
}

/**
* Report error.
* @param diagnostic - Diagnostic object
Expand All @@ -135,11 +148,12 @@ export class Context {
}

static {
setupContextForFile = (context, ruleIndex, filePath) => {
setupContextForFile = (context, ruleIndex, filePath, sourceText) => {
// TODO: Support `options`
const internal = context.#internal;
internal.ruleIndex = ruleIndex;
internal.filePath = filePath;
internal.sourceCode.text = sourceText;
};

getInternal = (context, actionDescription) => {
Expand Down
15 changes: 8 additions & 7 deletions apps/oxlint/src-js/plugins/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,21 @@ function lintFileImpl(filePath: string, bufferId: number, buffer: Uint8Array | n
throw new Error('Expected `ruleIds` to be a non-zero len array');
}

// Decode source text from buffer
const { uint32 } = buffer,
programPos = uint32[DATA_POINTER_POS_32],
sourceByteLen = uint32[(programPos + SOURCE_LEN_OFFSET) >> 2];

const sourceText = textDecoder.decode(buffer.subarray(0, sourceByteLen));

// Get visitors for this file from all rules
initCompiledVisitor();

for (let i = 0; i < ruleIds.length; i++) {
const ruleId = ruleIds[i],
ruleAndContext = registeredRules[ruleId];
const { rule, context } = ruleAndContext;
setupContextForFile(context, i, filePath);
setupContextForFile(context, i, filePath, sourceText);

let { visitor } = ruleAndContext;
if (visitor === null) {
Expand Down Expand Up @@ -139,12 +146,6 @@ function lintFileImpl(filePath: string, bufferId: number, buffer: Uint8Array | n
// Some rules seen in the wild return an empty visitor object from `create` if some initial check fails
// e.g. file extension is not one the rule acts on.
if (needsVisit) {
const { uint32 } = buffer,
programPos = uint32[DATA_POINTER_POS_32],
sourceByteLen = uint32[(programPos + SOURCE_LEN_OFFSET) >> 2];

const sourceText = textDecoder.decode(buffer.subarray(0, sourceByteLen));

// `preserveParens` argument is `false`, to match ESLint.
// ESLint does not include `ParenthesizedExpression` nodes in its AST.
const program = deserializeProgramOnly(buffer, sourceText, sourceByteLen, false);
Expand Down
34 changes: 34 additions & 0 deletions apps/oxlint/src-js/plugins/source_code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Node } from './types.ts';

const { max } = Math;

/**
* `SourceCode` class.
*
* Each rule has its own `SourceCode` object. It is stored in `Context` for that rule.
*
* A new `SourceCode` instance is NOT generated for each file.
* The `SourceCode` instance for the rule is updated for each file.
*/
export class SourceCode {
// Source text.
// Initially `null`, but set to source text string before linting each file.
text: string = null as unknown as string;

getText(
node?: Node | null | undefined,
beforeCount?: number | null | undefined,
afterCount?: number | null | undefined,
): string {
// ESLint treats all falsy values for `node` as undefined
if (!node) return this.text;

// ESLint ignores falsy values for `beforeCount` and `afterCount`
let { start, end } = node;
if (beforeCount) start = max(start - beforeCount, 0);
if (afterCount) end += afterCount;
return this.text.slice(start, end);
}

// TODO: Add more methods
}
4 changes: 4 additions & 0 deletions apps/oxlint/test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ describe('oxlint CLI', () => {
await testFixture('context_properties');
});

it('should give access to source code via `context.sourceCode`', async () => {
await testFixture('sourceCode');
});

it('should support `createOnce`', async () => {
await testFixture('createOnce');
});
Expand Down
14 changes: 13 additions & 1 deletion apps/oxlint/test/fixtures/createOnce/output.snap.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@
: ^
`----

x create-once-plugin(always-run): createOnce: sourceCode: Cannot access `context.sourceCode` in `createOnce`
,-[files/1.js:1:1]
1 | let x;
: ^
`----

x create-once-plugin(always-run): createOnce: this === rule: true
,-[files/1.js:1:1]
1 | let x;
Expand Down Expand Up @@ -201,6 +207,12 @@
: ^
`----

x create-once-plugin(always-run): createOnce: sourceCode: Cannot access `context.sourceCode` in `createOnce`
,-[files/2.js:1:1]
1 | let y;
: ^
`----

x create-once-plugin(always-run): createOnce: this === rule: true
,-[files/2.js:1:1]
1 | let y;
Expand Down Expand Up @@ -255,7 +267,7 @@
: ^
`----

Found 0 warnings and 42 errors.
Found 0 warnings and 44 errors.
Finished in Xms on 2 files using X threads.
```

Expand Down
2 changes: 2 additions & 0 deletions apps/oxlint/test/fixtures/createOnce/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const alwaysRunRule: Rule = {
const filenameError = tryCatch(() => context.filename);
const physicalFilenameError = tryCatch(() => context.physicalFilename);
const optionsError = tryCatch(() => context.options);
const sourceCodeError = tryCatch(() => context.sourceCode);
const reportError = tryCatch(() => context.report({ message: 'oh no', node: SPAN }));

return {
Expand All @@ -35,6 +36,7 @@ const alwaysRunRule: Rule = {
context.report({ message: `createOnce: filename: ${filenameError?.message}`, node: SPAN });
context.report({ message: `createOnce: physicalFilename: ${physicalFilenameError?.message}`, node: SPAN });
context.report({ message: `createOnce: options: ${optionsError?.message}`, node: SPAN });
context.report({ message: `createOnce: sourceCode: ${sourceCodeError?.message}`, node: SPAN });
context.report({ message: `createOnce: report: ${reportError?.message}`, node: SPAN });

context.report({ message: `before hook: id: ${context.id}`, node: SPAN });
Expand Down
8 changes: 8 additions & 0 deletions apps/oxlint/test/fixtures/sourceCode/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"jsPlugins": ["./plugin.ts"],
"categories": { "correctness": "off" },
"rules": {
"source-code-plugin/create": "error",
"source-code-plugin/create-once": "error"
}
}
1 change: 1 addition & 0 deletions apps/oxlint/test/fixtures/sourceCode/files/1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let foo, bar;
1 change: 1 addition & 0 deletions apps/oxlint/test/fixtures/sourceCode/files/2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let qux;
148 changes: 148 additions & 0 deletions apps/oxlint/test/fixtures/sourceCode/output.snap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Exit code
1

# stdout
```
x source-code-plugin(create): create:
| sourceCode.text: "let foo, bar;\n"
| sourceCode.getText(): "let foo, bar;\n"
,-[files/1.js:1:1]
1 | let foo, bar;
: ^
`----

x source-code-plugin(create-once): after:
| source: "let foo, bar;\n"
,-[files/1.js:1:1]
1 | let foo, bar;
: ^
`----

x source-code-plugin(create-once): before:
| sourceCode.text: "let foo, bar;\n"
| sourceCode.getText(): "let foo, bar;\n"
,-[files/1.js:1:1]
1 | let foo, bar;
: ^
`----

x source-code-plugin(create): var decl:
| source: "let foo, bar;"
,-[files/1.js:1:1]
1 | let foo, bar;
: ^^^^^^^^^^^^^
`----

x source-code-plugin(create-once): var decl:
| source: "let foo, bar;"
,-[files/1.js:1:1]
1 | let foo, bar;
: ^^^^^^^^^^^^^
`----

x source-code-plugin(create): ident "foo":
| source: "foo"
| source with before: "t foo"
| source with after: "foo,"
| source with both: "t foo,"
,-[files/1.js:1:5]
1 | let foo, bar;
: ^^^
`----

x source-code-plugin(create-once): ident "foo":
| source: "foo"
| source with before: "t foo"
| source with after: "foo,"
| source with both: "t foo,"
,-[files/1.js:1:5]
1 | let foo, bar;
: ^^^
`----

x source-code-plugin(create): ident "bar":
| source: "bar"
| source with before: ", bar"
| source with after: "bar;"
| source with both: ", bar;"
,-[files/1.js:1:10]
1 | let foo, bar;
: ^^^
`----

x source-code-plugin(create-once): ident "bar":
| source: "bar"
| source with before: ", bar"
| source with after: "bar;"
| source with both: ", bar;"
,-[files/1.js:1:10]
1 | let foo, bar;
: ^^^
`----

x source-code-plugin(create): create:
| sourceCode.text: "let qux;\n"
| sourceCode.getText(): "let qux;\n"
,-[files/2.js:1:1]
1 | let qux;
: ^
`----

x source-code-plugin(create-once): after:
| source: "let qux;\n"
,-[files/2.js:1:1]
1 | let qux;
: ^
`----

x source-code-plugin(create-once): before:
| sourceCode.text: "let qux;\n"
| sourceCode.getText(): "let qux;\n"
,-[files/2.js:1:1]
1 | let qux;
: ^
`----

x source-code-plugin(create): var decl:
| source: "let qux;"
,-[files/2.js:1:1]
1 | let qux;
: ^^^^^^^^
`----

x source-code-plugin(create-once): var decl:
| source: "let qux;"
,-[files/2.js:1:1]
1 | let qux;
: ^^^^^^^^
`----

x source-code-plugin(create): ident "qux":
| source: "qux"
| source with before: "t qux"
| source with after: "qux;"
| source with both: "t qux;"
,-[files/2.js:1:5]
1 | let qux;
: ^^^
`----

x source-code-plugin(create-once): ident "qux":
| source: "qux"
| source with before: "t qux"
| source with after: "qux;"
| source with both: "t qux;"
,-[files/2.js:1:5]
1 | let qux;
: ^^^
`----

Found 0 warnings and 16 errors.
Finished in Xms on 2 files using X threads.
```

# stderr
```
WARNING: JS plugins are experimental and not subject to semver.
Breaking changes are possible while JS plugins support is under development.
```
Loading
Loading