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
20 changes: 15 additions & 5 deletions apps/oxlint/src-js/plugins/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,34 +73,44 @@ export class Context {

// Getter for full rule name, in form `<plugin>/<rule>`
get id() {
return this.#internal.id;
const internal = this.#internal;
if (internal.filePath === '') throw new Error('Cannot access `context.id` in `createOnce`');
return internal.id;
}

// Getter for absolute path of file being linted.
get filename() {
return this.#internal.filePath;
const { filePath } = this.#internal;
if (filePath === '') throw new Error('Cannot access `context.filename` in `createOnce`');
return filePath;
}

// Getter for absolute path of file being linted.
// TODO: Unclear how this differs from `filename`.
get physicalFilename() {
return this.#internal.filePath;
const { filePath } = this.#internal;
if (filePath === '') throw new Error('Cannot access `context.physicalFilename` in `createOnce`');
return filePath;
}

// Getter for options for file being linted.
get options() {
return this.#internal.options;
const internal = this.#internal;
if (internal.filePath === '') throw new Error('Cannot access `context.options` in `createOnce`');
return internal.options;
}

/**
* Report error.
* @param diagnostic - Diagnostic object
*/
report(diagnostic: Diagnostic): void {
const internal = this.#internal;
if (internal.filePath === '') throw new Error('Cannot report errors in `createOnce`');
diagnostics.push({
message: diagnostic.message,
loc: { start: diagnostic.node.start, end: diagnostic.node.end },
ruleIndex: this.#internal.ruleIndex,
ruleIndex: internal.ruleIndex,
});
}

Expand Down
39 changes: 36 additions & 3 deletions apps/oxlint/src-js/plugins/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { deserializeProgramOnly } from '../../dist/generated/deserialize/ts.mjs'
// @ts-expect-error we need to generate `.d.ts` file for this module
import { walkProgram } from '../../dist/generated/visit/walk.mjs';

import type { AfterHook } from './types.ts';

// Buffer with typed array views of itself stored as properties
interface BufferWithArrays extends Uint8Array {
uint32: Uint32Array;
Expand All @@ -38,6 +40,9 @@ const buffers: (BufferWithArrays | null)[] = [];
// Text decoder, for decoding source text from buffer
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true });

// Array of `after` hooks to run after traversal. This array reused for every file.
const afterHooks: AfterHook[] = [];

// Run rules on a file.
export function lintFile(filePath: string, bufferId: number, buffer: Uint8Array | null, ruleIds: number[]): string {
// If new buffer, add it to `buffers` array. Otherwise, get existing buffer from array.
Expand Down Expand Up @@ -69,13 +74,32 @@ export function lintFile(filePath: string, bufferId: number, buffer: Uint8Array

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

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

let { visitor } = ruleAndContext;
if (visitor === null) {
// Rule defined with `create` method
visitor = rule.create(context);
} else {
// Rule defined with `createOnce` method
const { beforeHook, afterHook } = ruleAndContext;
if (beforeHook !== null) {
// If `before` hook returns `false`, skip this rule
const shouldRun = beforeHook();
if (shouldRun === false) continue;
}
// Note: If `before` hook returned `false`, `after` hook is not called
if (afterHook !== null) afterHooks.push(afterHook);
}

addVisitorToCompiled(visitor);
}

const needsVisit = finalizeCompiledVisitor();

// Visit AST.
Expand Down Expand Up @@ -110,6 +134,15 @@ export function lintFile(filePath: string, bufferId: number, buffer: Uint8Array
*/
}

// Run `after` hooks
if (afterHooks.length !== 0) {
for (const afterHook of afterHooks) {
afterHook();
}
// Reset array, ready for next file
afterHooks.length = 0;
}

// Send diagnostics back to Rust
const ret = JSON.stringify(diagnostics);
diagnostics.length = 0;
Expand Down
60 changes: 48 additions & 12 deletions apps/oxlint/src-js/plugins/load.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Context } from './context.js';
import { getErrorMessage } from './utils.js';

import type { Visitor } from './types.ts';
import type { AfterHook, BeforeHook, Visitor, VisitorWithHooks } from './types.ts';

// Linter plugin, comprising multiple rules
interface Plugin {
Expand All @@ -13,20 +13,46 @@ interface Plugin {
};
}

// Linter rule
interface Rule {
// Linter rule.
// `Rule` can have either `create` method, or `createOnce` method.
// If `createOnce` method is present, `create` is ignored.
type Rule = CreateRule | CreateOnceRule;

interface CreateRule {
create: (context: Context) => Visitor;
}

interface CreateOnceRule {
create?: (context: Context) => Visitor;
createOnce: (context: Context) => VisitorWithHooks;
}

// Linter rule and context object.
// If `rule` has a `createOnce` method, the visitor it returns is stored in `visitor`.
type RuleAndContext = CreateRuleAndContext | CreateOnceRuleAndContext;

interface CreateRuleAndContext {
rule: CreateRule;
context: Context;
visitor: null;
beforeHook: null;
afterHook: null;
}

interface CreateOnceRuleAndContext {
rule: CreateOnceRule;
context: Context;
visitor: Visitor;
beforeHook: BeforeHook | null;
afterHook: AfterHook | null;
}

// Absolute paths of plugins which have been loaded
const registeredPluginPaths = new Set<string>();

// Rule objects for loaded rules.
// Indexed by `ruleId`, which is passed to `lintFile`.
export const registeredRules: {
rule: Rule;
context: Context;
}[] = [];
export const registeredRules: RuleAndContext[] = [];

/**
* Load a plugin.
Expand Down Expand Up @@ -64,11 +90,21 @@ async function loadPluginImpl(path: string): Promise<string> {
const ruleNamesLen = ruleNames.length;

for (let i = 0; i < ruleNamesLen; i++) {
const ruleName = ruleNames[i];
registeredRules.push({
rule: rules[ruleName],
context: new Context(`${pluginName}/${ruleName}`),
});
const ruleName = ruleNames[i],
rule = rules[ruleName];

const context = new Context(`${pluginName}/${ruleName}`);

let ruleAndContext;
if ('createOnce' in rule) {
// TODO: Compile visitor object to array here, instead of repeating compilation on each file
const { before: beforeHook, after: afterHook, ...visitor } = rule.createOnce(context);
ruleAndContext = { rule, context, visitor, beforeHook: beforeHook || null, afterHook: afterHook || null };
} else {
ruleAndContext = { rule, context, visitor: null, beforeHook: null, afterHook: null };
}

registeredRules.push(ruleAndContext);
}

return JSON.stringify({ Success: { name: pluginName, offset, ruleNames } });
Expand Down
16 changes: 15 additions & 1 deletion apps/oxlint/src-js/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,21 @@ export interface Visitor {
}
*/

export type { VisitorObject as Visitor } from '../../dist/generated/visit/visitor.d.ts';
import type { VisitorObject as Visitor } from '../../dist/generated/visit/visitor.d.ts';
export type { Visitor };

// Hook function that runs before traversal.
// If returns `false`, traversal is skipped for the rule.
export type BeforeHook = () => boolean | undefined;

// Hook function that runs after traversal.
export type AfterHook = () => void;

// Visitor object returned by a `Rule`'s `createOnce` function.
export interface VisitorWithHooks extends Visitor {
before?: BeforeHook;
after?: AfterHook;
}

// Visit function for a specific AST node type.
export type VisitFn = (node: Node) => void;
Expand Down
Loading
Loading