2024-12-04 02:59:39 +01:00
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
|
|
|
|
import { core } from "ext:core/mod.js";
|
|
|
|
const {
|
2024-12-03 13:45:31 +01:00
|
|
|
op_lint_get_rule,
|
|
|
|
op_lint_get_source,
|
|
|
|
op_lint_report,
|
2024-12-04 02:59:39 +01:00
|
|
|
} = core.ops;
|
2024-12-01 04:53:47 +01:00
|
|
|
|
2024-12-04 03:29:05 +01:00
|
|
|
const state = {
|
|
|
|
plugins: {},
|
|
|
|
};
|
|
|
|
|
2024-12-01 04:53:47 +01:00
|
|
|
export class Context {
|
|
|
|
id;
|
|
|
|
|
|
|
|
fileName;
|
|
|
|
|
|
|
|
constructor(id, fileName) {
|
|
|
|
this.id = id;
|
|
|
|
this.fileName = fileName;
|
|
|
|
}
|
|
|
|
|
2024-12-03 13:45:31 +01:00
|
|
|
source() {
|
2024-12-04 03:29:05 +01:00
|
|
|
// TODO(bartlomieju): cache it on the state - it won't change between files, but callers can mutate it.
|
2024-12-03 13:45:31 +01:00
|
|
|
return op_lint_get_source();
|
|
|
|
}
|
|
|
|
|
2024-12-02 02:27:30 +01:00
|
|
|
report(data) {
|
2024-12-03 03:11:26 +01:00
|
|
|
let start, end;
|
|
|
|
|
|
|
|
if (data.node) {
|
|
|
|
start = data.node.span.start - 1;
|
|
|
|
end = data.node.span.end - 1;
|
|
|
|
} else if (data.span) {
|
|
|
|
start = data.span.start - 1;
|
|
|
|
end = data.span.end - 1;
|
|
|
|
} else {
|
|
|
|
throw new Error(
|
|
|
|
"Either `node` or `span` must be provided when reporting an error",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-12-02 18:29:30 +01:00
|
|
|
op_lint_report(
|
|
|
|
this.id,
|
|
|
|
this.fileName,
|
|
|
|
data.message,
|
2024-12-03 03:11:26 +01:00
|
|
|
start,
|
|
|
|
end,
|
2024-12-02 18:29:30 +01:00
|
|
|
);
|
2024-12-01 04:53:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-04 04:30:32 +01:00
|
|
|
export function installPlugin(plugin) {
|
|
|
|
console.log("plugin", plugin);
|
|
|
|
if (typeof plugin !== "object") {
|
|
|
|
throw new Error("Linter plugin must be an object");
|
|
|
|
}
|
|
|
|
if (typeof plugin.name !== "string") {
|
|
|
|
throw new Error("Linter plugin name must be a string");
|
|
|
|
}
|
|
|
|
if (typeof plugin.rules !== "object") {
|
|
|
|
throw new Error("Linter plugin rules must be an object");
|
|
|
|
}
|
|
|
|
if (typeof state.plugins[plugin.name] !== "undefined") {
|
|
|
|
throw new Error(`Linter plugin ${plugin.name} has already been registered`);
|
|
|
|
}
|
|
|
|
state.plugins[plugin.name] = plugin.rules;
|
|
|
|
console.log("Installed plugin", plugin.name, plugin.rules);
|
2024-12-04 03:29:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export function runPluginsForFile(fileName, serializedAst) {
|
|
|
|
const ast = JSON.parse(serializedAst, (key, value) => {
|
|
|
|
if (key === "ctxt") {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
});
|
|
|
|
|
2024-12-04 04:30:32 +01:00
|
|
|
for (const pluginName of Object.keys(state.plugins)) {
|
|
|
|
runRulesFromPlugin(pluginName, state.plugins[pluginName], fileName, ast);
|
2024-12-04 03:29:05 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-04 04:30:32 +01:00
|
|
|
function runRulesFromPlugin(pluginName, plugin, fileName, ast) {
|
2024-12-04 03:29:05 +01:00
|
|
|
for (const ruleName of Object.keys(plugin)) {
|
|
|
|
const rule = plugin[ruleName];
|
|
|
|
|
|
|
|
if (typeof rule.create !== "function") {
|
|
|
|
throw new Error("Rule's `create` property must be a function");
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(bartlomieju): can context be created less often, maybe once per plugin or even once per `runRulesForFile` invocation?
|
2024-12-04 04:30:32 +01:00
|
|
|
const id = `${pluginName}/${ruleName}`;
|
2024-12-04 03:29:05 +01:00
|
|
|
const ctx = new Context(id, fileName);
|
|
|
|
const visitor = rule.create(ctx);
|
|
|
|
traverse(ast, visitor);
|
|
|
|
|
|
|
|
if (typeof rule.destroy === "function") {
|
|
|
|
rule.destroy(ctx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-03 02:51:29 +01:00
|
|
|
function traverse(ast, visitor, parent = null) {
|
|
|
|
if (!ast || typeof ast !== "object") {
|
|
|
|
return;
|
2024-12-01 05:14:46 +01:00
|
|
|
}
|
2024-12-03 02:51:29 +01:00
|
|
|
|
|
|
|
// Get node type, accounting for SWC's type property naming
|
|
|
|
const nodeType = ast.type || (ast.nodeType ? ast.nodeType : null);
|
|
|
|
|
|
|
|
// Skip if not a valid AST node
|
|
|
|
if (!nodeType) {
|
|
|
|
return;
|
2024-12-01 05:14:46 +01:00
|
|
|
}
|
2024-12-03 02:51:29 +01:00
|
|
|
|
|
|
|
ast.parent = parent;
|
|
|
|
// Call visitor if it exists for this node type
|
|
|
|
if (visitor[nodeType] && typeof visitor[nodeType] === "function") {
|
2024-12-03 03:11:26 +01:00
|
|
|
visitor[nodeType](ast);
|
2024-12-01 05:14:46 +01:00
|
|
|
}
|
2024-12-03 02:51:29 +01:00
|
|
|
|
|
|
|
// Traverse child nodes
|
|
|
|
for (const key in ast) {
|
2024-12-03 03:11:26 +01:00
|
|
|
if (key === "parent" || key === "type") {
|
2024-12-03 02:51:29 +01:00
|
|
|
continue;
|
2024-12-01 05:14:46 +01:00
|
|
|
}
|
2024-12-03 02:51:29 +01:00
|
|
|
|
|
|
|
const child = ast[key];
|
|
|
|
|
|
|
|
if (Array.isArray(child)) {
|
|
|
|
child.forEach((item) => traverse(item, visitor, ast));
|
|
|
|
} else if (child && typeof child === "object") {
|
|
|
|
traverse(child, visitor, ast);
|
2024-12-01 05:14:46 +01:00
|
|
|
}
|
|
|
|
}
|
2024-12-01 04:53:47 +01:00
|
|
|
}
|