textlint
Version:
The pluggable linting tool for text and markdown.
313 lines (294 loc) • 10.8 kB
text/typescript
// LICENSE : MIT
"use strict";
import { TextlintKernelDescriptor } from "@textlint/kernel";
import { TextLintFormatterOption } from "../../textlint-interface";
import { TextLintCore } from "../textlint-core";
import { RuleMap } from "../../engine/rule-map";
import { PluginMap } from "../../engine/processor-map";
import { Config } from "../config";
import { findFiles, pathsToGlobPatterns } from "../../util/old-find-util";
import { TextLintModuleLoader } from "./textlint-module-loader";
import { ExecuteFileBackerManager } from "../../engine/execute-file-backer-manager";
import { CacheBacker } from "../../engine/execute-file-backers/cache-backer";
import { SeverityLevel } from "../../shared/type/SeverityLevel";
import { TextlintMessage, TextlintResult } from "@textlint/kernel";
import debug0 from "debug";
import path from "path";
import { separateByAvailability } from "../../util/separate-by-availability";
const debug = debug0("textlint:engine-core");
/**
* Core of TextLintEngine.
* It is internal user.
*
* Hackable adaptor
*
* - executeOnFiles
* - executeOnText
* - formatResults
*
* There are hackable by `executor` option.
*/
export abstract class AbstractTextLintEngine<LintResult extends TextlintResult> {
private moduleLoader: TextLintModuleLoader;
private pluginMap: PluginMap;
private ruleMap: RuleMap;
private filterRuleMap: RuleMap;
private executeFileBackerManger: ExecuteFileBackerManager;
private textlint: TextLintCore;
private config: Config;
// abstract interface
// Each engines should be implemented these
/**
* @param {TextLintCore} textlintCore
* @returns {function()}
*/
abstract onFile: (textlintCore: TextLintCore) => (filePath: string) => Promise<LintResult>;
/**
* @param {TextLintCore} textlintCore
* @returns {function()}
*/
abstract onText: (textlintCore: TextLintCore) => (text: string, ext?: string) => Promise<LintResult>;
/**
* @param {TextLintFormatterOption} formatterConfig
*/
abstract onFormat: (formatterConfig: TextLintFormatterOption) => (results: LintResult[]) => string;
/**
* Process files are wanted to lint.
* TextLintEngine is a wrapper of textlint.js.
* Aim to be called from cli with cli options.
* @param {Config|Object} [options] the options is command line options or Config object.
* @constructor
*/
constructor(options?: Config | object) {
/**
* @type {Config}
*/
if (options instanceof Config) {
// Almost internal use-case
this.config = options;
} else {
this.config = Config.initWithAutoLoading(options);
}
/**
* @type {TextLintCore}
* @private
*/
this.textlint = new TextLintCore(this.config);
/**
* @type {ExecuteFileBackerManager}
* @private
*/
this.executeFileBackerManger = new ExecuteFileBackerManager();
const cacheBaker = new CacheBacker(this.config);
if (this.config.cache) {
this.executeFileBackerManger.add(cacheBaker);
} else {
cacheBaker.destroyCache();
}
/**
* @type {RuleMap} ruleMap is used for linting/fixer
* @private
*/
this.ruleMap = new RuleMap();
/**
* @type {RuleMap} filerRuleMap is used for filtering
* @private
*/
this.filterRuleMap = new RuleMap();
/**
* @type {PluginMap}
* @private
*/
this.pluginMap = new PluginMap();
/**
* @type {TextLintModuleLoader}
* @private
*/
this.moduleLoader = new TextLintModuleLoader(this.config);
this.moduleLoader.on(TextLintModuleLoader.Event.rule, ([ruleName, ruleCreator]) => {
this.ruleMap.defineRule(ruleName, ruleCreator);
});
this.moduleLoader.on(TextLintModuleLoader.Event.filterRule, ([ruleName, ruleCreator]) => {
this.filterRuleMap.defineRule(ruleName, ruleCreator);
});
this.moduleLoader.on(TextLintModuleLoader.Event.plugin, ([pluginName, plugin]) => {
this.pluginMap.set(pluginName, plugin);
});
// load rule/plugin/processor
this.moduleLoader.loadFromConfig(this.config);
// set settings to textlint core
this._setupRules();
}
/**
* @deprecated remove this method
*/
setRulesBaseDirectory() {
throw new Error(`Should not use setRulesBaseDirectory(), insteadof use
new TextLintEngine({
rulesBaseDirectory: directory
})
`);
}
/**
* load plugin manually
* Note: it high cost, please use config
* @param {string} pluginName
* @deprecated use Constructor(config) insteadof it
*/
loadPlugin(pluginName: string) {
this.moduleLoader.loadPlugin(pluginName);
this._setupRules();
}
/**
* load plugin manually
* Note: it high cost, please use config
* @param {string} presetName
* @deprecated use Constructor(config) insteadof it
*/
loadPreset(presetName: string) {
this.moduleLoader.loadPreset(presetName);
this._setupRules();
}
/**
* load rule manually
* Note: it high cost, please use config
* @param {string} ruleName
* @deprecated use Constructor(config) insteadof it
*/
loadRule(ruleName: string) {
this.moduleLoader.loadRule(ruleName);
this._setupRules();
}
/**
* load filter rule manually
* Note: it high cost, please use config
* @param {string} ruleName
* @deprecated use Constructor(config) insteadof it
*/
loadFilerRule(ruleName: string) {
this.moduleLoader.loadFilterRule(ruleName);
this._setupRules();
}
/**
* Update rules from current config
* @private
*/
private _setupRules() {
// set Rules
const textlintConfig = this.config ? this.config.toJSON() : {};
this.textlint.setupRules(this.ruleMap.getAllRules(), textlintConfig.rulesConfig);
this.textlint.setupFilterRules(this.filterRuleMap.getAllRules(), textlintConfig.filterRulesConfig);
// set Processor
this.textlint.setupPlugins(this.pluginMap.toJSON(), textlintConfig.pluginsConfig);
}
/**
* Remove all registered rule and clear messages.
* @private
*/
resetRules() {
this.textlint.resetRules();
this.ruleMap.resetRules();
this.filterRuleMap.resetRules();
}
/**
* Return available extensions of plugins that include built-in plugins
* @example
* ```
* engine.availableExtensions; // => [".txt", ".md"]
* ```
*/
get availableExtensions(): string[] {
return this.textlint.textlintKernelDescriptor.availableExtensions;
}
/**
* Return meta descriptor object for this engine
*
* WARNING: This is experimental getter method.
* It will be renamed.
*/
get textlintrcDescriptor(): TextlintKernelDescriptor {
return this.textlint.textlintKernelDescriptor;
}
/**
* Executes the current configuration on an array of file and directory names.
* @param {String[]} files An array of file and directory names.
* @returns {Promise<TextlintResult[]>} The results for all files that were linted.
*/
executeOnFiles(files: string[]): Promise<LintResult[]> {
const execFile = this.onFile(this.textlint);
const patterns = pathsToGlobPatterns(files, {
extensions: this.textlintrcDescriptor.availableExtensions
});
const targetFiles = findFiles(patterns, { ignoreFilePath: this.config.ignoreFile });
// Maybe, unAvailableFilePath should be warning.
// But, The user can use glob pattern like `src/**/*` as arguments.
// pathsToGlobPatterns not modified that pattern.
// So, unAvailableFilePath should be ignored silently.
const { availableFiles, unAvailableFiles } = separateByAvailability(targetFiles, {
extensions: this.textlintrcDescriptor.availableExtensions
});
debug("Process files", availableFiles);
debug("No Process files that are un-support extensions:", unAvailableFiles);
// FIXME: remove cast
return this.executeFileBackerManger.process(availableFiles, execFile) as Promise<LintResult[]>;
}
/**
* If want to lint a text, use it.
* But, if you have a target file, use {@link executeOnFiles} instead of it.
* @param {string} text linting text content
* @param {string} ext ext is a type for linting. default: ".txt"
* @returns {Promise<TextlintResult[]>}
*/
executeOnText(text: string, ext: string = ".txt"): Promise<LintResult[]> {
const textlint = this.textlint;
const execText = this.onText(textlint);
// filePath or ext
const actualExt = ext[0] === "." ? ext : path.extname(ext);
if (actualExt.length === 0) {
throw new Error("should specify the extension.\nex) .md");
}
return execText(text, actualExt).then((result: LintResult) => {
return [result];
});
}
/**
* format {@link results} and return output text.
* @param {TextlintResult[]} results the collection of result
* @returns {string} formatted output text
* @example
* console.log(formatResults(results));
*/
formatResults(results: LintResult[]): string {
// default formatter: "stylish"
const formatter = this.onFormat({
formatterName: this.config.formatterName || "stylish",
color: this.config.color
});
return formatter(results);
}
/**
* Checks if the given message is an error message.
* @param {TextlintMessage} message The message to check.
* @returns {boolean} Whether or not the message is an error message.
*/
isErrorMessage(message: TextlintMessage): boolean {
return message.severity === SeverityLevel.error;
}
/**
* Checks if the given results contain error message.
* If there is even one error then return true.
* @param {TextlintResult[]} results Linting result collection
* @returns {Boolean} Whether or not the results contain error message.
*/
isErrorResults(results: TextlintResult[]): boolean {
return results.some((result) => {
return result.messages.some(this.isErrorMessage);
});
}
/**
* @returns {boolean}
*/
hasRuleAtLeastOne() {
return this.ruleMap.hasRuleAtLeastOne();
}
}