@textlint/kernel
Version:
textlint kernel is core logic by pure JavaScript.
269 lines (253 loc) • 9.66 kB
text/typescript
// LICENSE : MIT
;
import { TextlintRuleErrorImpl } from "../context/TextlintRuleErrorImpl";
import { EventEmitter, PromiseEventEmitter } from "./promise-event-emitter";
import { resolveLocation, resolveFixCommandLocation } from "../core/source-location";
import timing from "../util/timing";
import { invariant } from "../util/invariant";
import MessageType from "../shared/type/MessageType";
import { AnyTxtNode, TxtParentNode } from "@textlint/ast-node-types";
import type {
TextlintFilterRuleContext,
TextlintFilterRuleOptions,
TextlintFilterRuleReporter,
TextlintFilterRuleShouldIgnoreFunction,
TextlintFilterRuleShouldIgnoreFunctionArgs,
TextlintMessageFixCommand,
TextlintRuleContext,
TextlintRuleContextReportFunction,
TextlintRuleContextReportFunctionArgs,
TextlintRuleOptions,
TextlintRuleReporter,
TextlintSourceCode
} from "@textlint/types";
import { normalizeTextlintKeyPath } from "@textlint/utils";
import { TextlintRuleContextImpl } from "../context/TextlintRuleContextImpl";
import _debug from "debug";
import { Controller as TraverseController } from "@textlint/ast-traverse";
const traverseController = new TraverseController();
const debug = _debug("textlint:core-task");
class RuleTypeEmitter extends PromiseEventEmitter {}
export interface IgnoreReportedMessage {
ruleId: string;
type: typeof MessageType.ignore;
// location info
// TODO: compatible with TextLintMessage
// line: number; // start with 1
// column: number;// start with 1
// // indexed-location
// index: number;// start with 0
range: readonly [startIndex: number, endIndex: number];
ignoringRuleId: string;
}
export interface LintReportedMessage {
type: typeof MessageType.lint;
ruleId: string;
message: string;
index: number;
line: number; // start with 1(1-based line number)
column: number; // start with 1(1-based column number)
// range is 0-based values
range: readonly [startIndex: number, endIndex: number];
// loc is 1-based values
// line start with 1
// column start with 1
loc: {
start: {
line: number;
column: number;
};
end: {
line: number;
column: number;
};
};
severity: number; // it's for compatible ESLint formatter
fix?: TextlintMessageFixCommand;
}
/**
* CoreTask receive AST and prepare, traverse AST, emit nodeType event!
* You can observe task and receive "message" event that is TextLintMessage.
*/
export default abstract class TextLintCoreTask extends EventEmitter {
private ruleTypeEmitter: RuleTypeEmitter;
static get events() {
return {
// receive start event
start: "start",
// receive message from each rules
message: "message",
// receive complete event
complete: "complete",
// receive error event
error: "error"
};
}
constructor() {
super();
this.ruleTypeEmitter = new RuleTypeEmitter();
}
abstract start(): void;
createShouldIgnore(): TextlintFilterRuleShouldIgnoreFunction {
const shouldIgnore = (args: TextlintFilterRuleShouldIgnoreFunctionArgs) => {
const { ruleId, range, optional } = args;
invariant(
typeof range[0] !== "undefined" && typeof range[1] !== "undefined" && range[0] >= 0 && range[1] >= 0,
"ignoreRange should have actual range: " + range
);
// FIXME: should have index, loc
// should be compatible with LintReportedMessage?
const message: IgnoreReportedMessage = {
type: MessageType.ignore,
ruleId: ruleId,
range: range,
// ignoring target ruleId - default: filter all messages
// This ruleId should be normalized, because the user can report any value
ignoringRuleId: optional.ruleId ? normalizeTextlintKeyPath(optional.ruleId) : "*"
};
this.emit(TextLintCoreTask.events.message, message);
};
return shouldIgnore;
}
createReporter(sourceCode: TextlintSourceCode): TextlintRuleContextReportFunction {
/**
* push new RuleError to results
* @param {ReportMessage} reportArgs
*/
const reportFunction = (reportArgs: TextlintRuleContextReportFunctionArgs) => {
const { ruleId, node, severity, ruleError } = reportArgs;
const { loc, range } = resolveLocation({
source: sourceCode,
ruleId,
node,
ruleError
});
const { fix } = resolveFixCommandLocation({
node,
ruleError
});
debug("%s report %s", ruleId, ruleError);
// add TextLintMessage
const message: LintReportedMessage = {
type: MessageType.lint,
ruleId: ruleId,
message: ruleError.message,
index: range[0],
line: loc.start.line,
column: loc.start.column,
range,
loc,
severity: severity, // it's for compatible ESLint formatter
fix: fix !== undefined ? fix : undefined
};
if (!(ruleError instanceof TextlintRuleErrorImpl)) {
// FIXME: RuleReportedObject should be removed
// `error` is a any data.
const data = ruleError;
(message as any).data = data;
}
this.emit(TextLintCoreTask.events.message, message);
};
return reportFunction;
}
/**
* start process and emitting events.
* You can listen message by `task.on("message", message => {})`
* @param {SourceCode} sourceCode
*/
startTraverser(sourceCode: TextlintSourceCode) {
this.emit(TextLintCoreTask.events.start);
const promiseQueue: Array<Promise<Array<void>>> = [];
const ruleTypeEmitter = this.ruleTypeEmitter;
traverseController.traverse(sourceCode.ast as TxtParentNode, {
enter(node: AnyTxtNode, parent?: AnyTxtNode) {
const type = node.type;
Object.defineProperty(node, "parent", { value: parent });
if (ruleTypeEmitter.listenerCount(type) > 0) {
const promise = ruleTypeEmitter.emit(type, node);
promiseQueue.push(promise);
}
},
leave(node: AnyTxtNode) {
const type = `${node.type}:exit`;
if (ruleTypeEmitter.listenerCount(type) > 0) {
const promise = ruleTypeEmitter.emit(type, node);
promiseQueue.push(promise);
}
}
});
Promise.all(promiseQueue)
.then(() => {
this.emit(TextLintCoreTask.events.complete);
})
.catch((error) => {
this.emit(TextLintCoreTask.events.error, error);
});
}
/**
* try to get rule object
*/
tryToGetRuleObject(
ruleCreator: TextlintRuleReporter,
ruleContext: Readonly<TextlintRuleContext>,
ruleOptions?: TextlintRuleOptions
) {
try {
return ruleCreator(ruleContext, ruleOptions);
} catch (error) {
if (error instanceof Error) {
error.message = `Error while loading rule '${ruleContext.id}': ${error.message}`;
}
throw error;
}
}
/**
* try to get filter rule object
*/
tryToGetFilterRuleObject(
ruleCreator: TextlintFilterRuleReporter,
ruleContext: Readonly<TextlintFilterRuleContext>,
ruleOptions?: TextlintFilterRuleOptions
) {
try {
return ruleCreator(ruleContext, ruleOptions);
} catch (error) {
if (error instanceof Error) {
error.message = `Error while loading filter rule '${ruleContext.id}': ${error.message}`;
}
throw error;
}
}
/**
* add all the node types as listeners of the rule
* @param {Function} ruleCreator
* @param {Readonly<RuleContext>|Readonly<FilterRuleContext>} ruleContext
* @param {Object|boolean|undefined} ruleOptions
* @returns {Object}
*/
tryToAddListenRule(
ruleCreator: TextlintRuleReporter | TextlintFilterRuleReporter,
ruleContext: Readonly<TextlintRuleContext> | Readonly<TextlintFilterRuleContext>,
ruleOptions?: TextlintRuleOptions | TextlintFilterRuleOptions
): void {
const ruleObject =
ruleContext instanceof TextlintRuleContextImpl
? this.tryToGetRuleObject(
ruleCreator as TextlintRuleReporter,
ruleContext as Readonly<TextlintRuleContext>,
ruleOptions
)
: this.tryToGetFilterRuleObject(
ruleCreator as TextlintFilterRuleReporter,
ruleContext as Readonly<TextlintFilterRuleContext>,
ruleOptions
);
const types = Object.keys(ruleObject);
types.forEach((nodeType) => {
this.ruleTypeEmitter.on(
nodeType,
timing.enabled ? timing.time(ruleContext.id, ruleObject[nodeType]) : ruleObject[nodeType]!
);
});
}
}