cspell
Version:
A Spelling Checker for Code!
209 lines • 7.58 kB
JavaScript
import assert from 'node:assert';
import { IssueType, unknownWordsChoices, } from '@cspell/cspell-types';
import { dynamicImport } from '@cspell/dynamic-import';
import { pkgDir } from '../pkgInfo.js';
import { ApplicationError, toError } from './errors.js';
import { clean } from './util.js';
function filterFeatureIssues(features, issue, reportOptions) {
if (issue.issueType === IssueType.directive) {
return (features?.issueType && reportOptions?.validateDirectives) || false;
}
if (features?.unknownWords) {
return true;
}
if (!reportOptions) {
// If no options are provided, report the issue.
return true;
}
// The reporter doesn't support the `unknownWords` feature. Handle the logic here.
let reportIssue = issue.isFlagged;
reportIssue ||= !reportOptions.unknownWords || reportOptions.unknownWords === unknownWordsChoices.ReportAll;
reportIssue ||= reportOptions.unknownWords === unknownWordsChoices.ReportSimple && issue.hasSimpleSuggestions;
reportIssue ||=
reportOptions.unknownWords === unknownWordsChoices.ReportCommonTypos && issue.hasPreferredSuggestions;
return reportIssue || false;
}
function handleIssue(reporter, issue, reportOptions) {
if (!reporter.issue)
return;
if (!filterFeatureIssues(reporter.features, issue, reportOptions)) {
// The reporter does not want to handle this issue.
return;
}
if (!reporter.features?.contextGeneration && !issue.context) {
issue = { ...issue };
issue.context = issue.line; // Ensure context is always set if the reporter does not support context generation.
}
return reporter.issue(issue, reportOptions);
}
/**
* Loads reporter modules configured in cspell config file
*/
export async function loadReporters(reporters, defaultReporter, config) {
async function loadReporter(reporterSettings) {
if (reporterSettings === 'default')
return defaultReporter;
if (!Array.isArray(reporterSettings)) {
reporterSettings = [reporterSettings];
}
const [moduleName, settings] = reporterSettings;
try {
const { getReporter } = await dynamicImport(moduleName, [process.cwd(), pkgDir]);
return getReporter(settings, config);
}
catch (e) {
throw new ApplicationError(`Failed to load reporter ${moduleName}: ${toError(e).message}`);
}
}
reporters = !reporters || !reporters.length ? ['default'] : [...reporters];
const loadedReporters = await Promise.all(reporters.map(loadReporter));
return loadedReporters.filter((v) => v !== undefined);
}
export function finalizeReporter(reporter) {
if (!reporter)
return undefined;
if (reporterIsFinalized(reporter)) {
return reporter;
}
const final = {
issue: (...params) => reporter.issue?.(...params),
info: (...params) => reporter.info?.(...params),
debug: (...params) => reporter.debug?.(...params),
progress: (...params) => reporter.progress?.(...params),
error: (...params) => reporter.error?.(...params),
result: (...params) => reporter.result?.(...params),
features: reporter.features,
};
return final;
}
function reporterIsFinalized(reporter) {
return ((!!reporter &&
reporter.features &&
typeof reporter.issue === 'function' &&
typeof reporter.info === 'function' &&
typeof reporter.debug === 'function' &&
typeof reporter.error === 'function' &&
typeof reporter.progress === 'function' &&
typeof reporter.result === 'function') ||
false);
}
const reportIssueOptionsKeyMap = {
unknownWords: 'unknownWords',
validateDirectives: 'validateDirectives',
showContext: 'showContext',
};
function setValue(options, key, value) {
if (value !== undefined) {
options[key] = value;
}
}
export function extractReporterIssueOptions(settings) {
const src = settings;
const options = {};
for (const key in reportIssueOptionsKeyMap) {
const k = key;
setValue(options, k, src[k]);
}
return options;
}
export function mergeReportIssueOptions(a, b) {
const options = extractReporterIssueOptions(a);
if (!b)
return options;
for (const key in reportIssueOptionsKeyMap) {
const k = key;
setValue(options, k, b[k]);
}
return options;
}
export class LintReporter {
defaultReporter;
#reporters = [];
#config;
#finalized = false;
constructor(defaultReporter, config) {
this.defaultReporter = defaultReporter;
this.#config = config;
if (defaultReporter) {
this.#reporters.push(finalizeReporter(defaultReporter));
}
}
get config() {
return this.#config;
}
set config(config) {
assert(!this.#finalized, 'Cannot change the configuration of a finalized reporter');
this.#config = config;
}
issue(issue, reportOptions) {
for (const reporter of this.#reporters) {
handleIssue(reporter, issue, reportOptions);
}
}
info(...params) {
for (const reporter of this.#reporters) {
reporter.info(...params);
}
}
debug(...params) {
for (const reporter of this.#reporters) {
reporter.debug(...params);
}
}
error(...params) {
for (const reporter of this.#reporters) {
reporter.error(...params);
}
}
progress(...params) {
for (const reporter of this.#reporters) {
reporter.progress(...params);
}
}
async result(result) {
await Promise.all(this.#reporters.map((reporter) => reporter.result?.(result)));
}
get features() {
return {
unknownWords: true,
issueType: true,
};
}
async loadReportersAndFinalize(reporters) {
assert(!this.#finalized, 'Cannot change the configuration of a finalized reporter');
const loaded = await loadReporters(reporters, this.defaultReporter, this.config);
this.#reporters = [...new Set(loaded)].map((reporter) => finalizeReporter(reporter));
}
emitProgressBegin(filename, fileNum, fileCount) {
this.progress({
type: 'ProgressFileBegin',
fileNum,
fileCount,
filename,
});
}
emitProgressComplete(filename, fileNum, fileCount, result) {
const filteredIssues = result.issues.filter((issue) => filterFeatureIssues({}, issue, result.reportIssueOptions));
const numIssues = filteredIssues.length;
for (const reporter of this.#reporters) {
const progress = clean({
type: 'ProgressFileComplete',
fileNum,
fileCount,
filename,
elapsedTimeMs: result.elapsedTimeMs,
processed: result.processed,
numErrors: numIssues || result.errors,
cached: result.cached,
perf: result.perf,
issues: reporter.features && result.issues,
reportIssueOptions: reporter.features && result.reportIssueOptions,
});
reporter.progress(progress);
}
// Show the spelling errors after emitting the progress.
result.issues.forEach((issue) => this.issue(issue, result.reportIssueOptions));
return numIssues;
}
}
//# sourceMappingURL=reporters.js.map