eslint-plugin-command
Version:
Comment-as-command for one-off codemod with ESLint
284 lines (276 loc) • 8.03 kB
JavaScript
import { t as builtinCommands } from "./commands-Ceu3K7Gv.mjs";
//#region package.json
var version = "3.4.0";
//#endregion
//#region src/traverse.ts
const SKIP = Symbol("skip");
const STOP = Symbol("stop");
function traverse(context, node, visitor) {
const allVisitorKeys = context.sourceCode.visitorKeys;
const queue = [];
queue.push({
node,
parent: null,
parentKey: null,
parentPath: null
});
while (queue.length) {
const currentPath = queue.shift();
const result = visitor(currentPath, {
SKIP,
STOP
});
if (result === STOP) return false;
if (result === SKIP) continue;
const visitorKeys = allVisitorKeys[currentPath.node.type];
if (!visitorKeys) continue;
for (const visitorKey of visitorKeys) {
const child = currentPath.node[visitorKey];
if (!child) continue;
else if (Array.isArray(child)) for (const item of child) queue.push({
node: item,
parent: currentPath.node,
parentKey: visitorKey,
parentPath: currentPath
});
else queue.push({
node: child,
parent: currentPath.node,
parentKey: visitorKey,
parentPath: currentPath
});
}
}
return true;
}
//#endregion
//#region src/context.ts
var CommandContext = class {
constructor(context, comment, command, matches) {
this.context = context;
this.comment = comment;
this.command = command;
this.source = context.sourceCode;
this.matches = matches;
}
/**
* A shorthand of `this.context.sourceCode.getText(node)`
*
* When `node` is `null` or `undefined`, it returns an empty string
*/
getTextOf(node) {
if (!node) return "";
if (Array.isArray(node)) return this.context.sourceCode.text.slice(node[0], node[1]);
return this.context.sourceCode.getText(node);
}
/**
* Report an ESLint error on the triggering comment, without fix
*/
reportError(message, ...causes) {
this.context.report({
loc: this.comment.loc,
messageId: "command-error",
data: {
command: this.command.name,
message
}
});
for (const cause of causes) {
const { message: message$1, ...pos } = cause;
this.context.report({
...pos,
messageId: "command-error-cause",
data: {
command: this.command.name,
message: message$1
}
});
}
}
/**
* Report an ESLint error.
* Different from normal `context.report` as that it requires `message` instead of `messageId`.
*/
report(descriptor) {
const { message, ...report } = descriptor;
const { comment, source } = this;
if (report.nodes) report.loc ||= {
start: report.nodes[0].loc.start,
end: report.nodes[report.nodes.length - 1].loc.end
};
this.context.report({
...report,
messageId: "command-fix",
data: {
command: this.command.name,
message,
...report.data
},
*fix(fixer) {
if (report.fix) {
const result = report.fix(fixer);
if (result && "next" in result) for (const fix of result) yield fix;
else if (result) yield result;
}
if (report.removeComment !== false) {
const lastToken = source.getTokenBefore(comment, { includeComments: true })?.range[1];
let rangeStart = source.getIndexFromLoc({
line: comment.loc.start.line,
column: 0
}) - 1;
if (lastToken != null) rangeStart = Math.max(0, lastToken, rangeStart);
let rangeEnd = comment.range[1];
if (comment.loc.start.line === 1) {
if (source.text[rangeEnd] === "\n") rangeEnd += 1;
}
yield fixer.removeRange([rangeStart, rangeEnd]);
}
}
});
}
/**
* Utility to traverse the AST starting from a node
*/
traverse(node, cb) {
return traverse(this.context, node, cb);
}
findNodeBelow(...args) {
let options;
if (typeof args[0] === "string") options = { types: args };
else if (typeof args[0] === "function") options = { filter: args[0] };
else options = args[0];
const { shallow = false, findAll = false } = options;
const tokenBelow = this.context.sourceCode.getTokenAfter(this.comment);
if (!tokenBelow) return;
const nodeBelow = this.context.sourceCode.getNodeByRangeIndex(tokenBelow.range[1]);
if (!nodeBelow) return;
const result = [];
let target = nodeBelow;
while (target.parent && target.parent.loc.start.line === nodeBelow.loc.start.line) target = target.parent;
const filter = options.filter ? options.filter : (node) => options.types.includes(node.type);
this.traverse(target, (path) => {
if (path.node.loc.start.line !== nodeBelow.loc.start.line) return STOP;
if (filter(path.node)) {
result.push(path.node);
if (!findAll) return STOP;
if (shallow) return SKIP;
}
});
return findAll ? result : result[0];
}
/**
* Get the parent block of the triggering comment
*/
getParentBlock() {
const node = this.source.getNodeByRangeIndex(this.comment.range[0]);
if (node?.type === "BlockStatement") {
if (this.source.getCommentsInside(node).includes(this.comment)) return node;
}
if (node) console.warn(`Expected BlockStatement, got ${node.type}. This is probably an internal bug.`);
return this.source.ast;
}
/**
* Get indent string of a specific line
*/
getIndentOfLine(line) {
const match = (this.source.getLines()[line - 1] || "").match(/^\s*/);
return match ? match[0] : "";
}
};
//#endregion
//#region src/utils.ts
/**
* Creates reusable function to create rules with default options and docs URLs.
*
* @param urlCreator Creates a documentation URL for a given rule name.
* @returns Function to create a rule with the docs URL format.
*/
function RuleCreator(urlCreator) {
return function createNamedRule({ name, meta, ...rule }) {
return createRule({
meta: {
...meta,
docs: {
...meta.docs,
url: urlCreator(name)
}
},
...rule
});
};
}
/**
* Creates a well-typed TSESLint custom ESLint rule without a docs URL.
*
* @returns Well-typed TSESLint custom ESLint rule.
* @remarks It is generally better to provide a docs URL function to RuleCreator.
*/
function createRule({ create, defaultOptions, meta }) {
return {
create: ((context) => {
return create(context, context.options.map((options, index) => {
return {
...defaultOptions[index] || {},
...options || {}
};
}));
}),
defaultOptions,
meta
};
}
const createEslintRule = RuleCreator(() => "https://github.com/antfu/eslint-plugin-command");
//#endregion
//#region src/rule.ts
function createRuleWithCommands(commands) {
return createEslintRule({
name: "command",
meta: {
type: "problem",
docs: { description: "Comment-as-command for one-off codemod with ESLint" },
fixable: "code",
schema: [],
messages: {
"command-error": "[{{command}}] error: {{message}}",
"command-error-cause": "[{{command}}] error cause: {{message}}",
"command-fix": "[{{command}}] fix: {{message}}"
}
},
defaultOptions: [],
create: (context) => {
const comments = context.sourceCode.getAllComments();
for (const comment of comments) {
const commandRaw = comment.value;
for (const command of commands) {
const type = command.commentType ?? "line";
if (type === "line" && comment.type !== "Line") continue;
if (type === "block" && comment.type !== "Block") continue;
let matches = typeof command.match === "function" ? command.match(comment) : commandRaw.match(command.match);
if (!matches) continue;
if (matches === true) matches = "__dummy__".match("__dummy__");
if (command.action(new CommandContext(context, comment, command, matches)) !== false) break;
}
}
return {};
}
});
}
var rule_default = /* @__PURE__ */ createRuleWithCommands(builtinCommands);
//#endregion
//#region src/plugin.ts
function createPluginWithCommands(options = {}) {
const { name = "command" } = options;
const plugin = options.commands ? createRuleWithCommands(options.commands) : rule_default;
return {
meta: {
name,
version
},
rules: { command: plugin }
};
}
//#endregion
//#region src/index.ts
var src_default = createPluginWithCommands();
//#endregion
export { createPluginWithCommands as n, src_default as t };