cspell
Version:
A Spelling Checker for Code!
252 lines • 10.4 kB
JavaScript
import * as path from 'node:path';
import { format } from 'node:util';
import chalk from 'chalk';
import chalkTemplate from 'chalk-template';
import { isSpellingDictionaryLoadError } from 'cspell-lib';
import { URI } from 'vscode-uri';
import { uniqueFilterFnGenerator } from './util/util.js';
const templateIssue = `{green $filename}:{yellow $row:$col} - $message ({red $text}) $quickFix`;
const templateIssueNoFix = `{green $filename}:{yellow $row:$col} - $message ({red $text})`;
const templateIssueWithSuggestions = `{green $filename}:{yellow $row:$col} - $message ({red $text}) Suggestions: {yellow [$suggestions]}`;
const templateIssueWithContext = `{green $filename}:{yellow $row:$col} $padRowCol- $message ({red $text})$padContext -- {gray $contextLeft}{red {underline $text}}{gray $contextRight}`;
const templateIssueWithContextWithSuggestions = `{green $filename}:{yellow $row:$col} $padRowCol- $message ({red $text})$padContext -- {gray $contextLeft}{red {underline $text}}{gray $contextRight}\n\t Suggestions: {yellow [$suggestions]}`;
const templateIssueLegacy = `${chalk.green('$filename')}[$row, $col]: $message: ${chalk.red('$text')}`;
const templateIssueWordsOnly = '$text';
/**
*
* @param template - The template to use for the issue.
* @param uniqueIssues - If true, only unique issues will be reported.
* @param reportedIssuesCollection - optional collection to store reported issues.
* @returns issueEmitter function
*/
function genIssueEmitter(template, uniqueIssues, reportedIssuesCollection) {
const uniqueFilter = uniqueIssues ? uniqueFilterFnGenerator((issue) => issue.text) : () => true;
const defaultWidth = 10;
let maxWidth = defaultWidth;
let uri;
return function issueEmitter(issue) {
if (!uniqueFilter(issue))
return;
if (uri !== issue.uri) {
maxWidth = defaultWidth;
uri = issue.uri;
}
maxWidth = Math.max(maxWidth * 0.999, issue.text.length, 10);
const issueText = formatIssue(template, issue, Math.ceil(maxWidth));
reportedIssuesCollection?.push(issueText);
console.log(issueText);
};
}
function nullEmitter() {
/* empty */
}
function relativeFilename(filename, cwd) {
const rel = path.relative(cwd, filename);
if (rel.startsWith('..'))
return filename;
return '.' + path.sep + rel;
}
function relativeUriFilename(uri, fsPathRoot) {
const fsPath = URI.parse(uri).fsPath;
const rel = path.relative(fsPathRoot, fsPath);
if (rel.startsWith('..'))
return fsPath;
return '.' + path.sep + rel;
}
function reportProgress(p, cwd) {
if (p.type === 'ProgressFileComplete') {
return reportProgressFileComplete(p);
}
if (p.type === 'ProgressFileBegin') {
return reportProgressFileBegin(p, cwd);
}
}
function reportProgressFileBegin(p, cwd) {
const fc = '' + p.fileCount;
const fn = (' '.repeat(fc.length) + p.fileNum).slice(-fc.length);
const idx = fn + '/' + fc;
const filename = chalk.gray(relativeFilename(p.filename, cwd));
process.stderr.write(`\r${idx} ${filename}`);
}
function reportProgressFileComplete(p) {
const time = reportTime(p.elapsedTimeMs, !!p.cached);
const skipped = p.processed === false ? ' skipped' : '';
const hasErrors = p.numErrors ? chalk.red ` X` : '';
console.error(` ${time}${skipped}${hasErrors}`);
}
function reportTime(elapsedTimeMs, cached) {
if (cached)
return chalk.green('cached');
if (elapsedTimeMs === undefined)
return '-';
const color = elapsedTimeMs < 1000 ? chalk.white : elapsedTimeMs < 2000 ? chalk.yellow : chalk.redBright;
return color(elapsedTimeMs.toFixed(2) + 'ms');
}
export function getReporter(options, config) {
const uniqueIssues = config?.unique || false;
const issueTemplate = options.wordsOnly
? templateIssueWordsOnly
: options.legacy
? templateIssueLegacy
: options.showContext
? options.showSuggestions
? templateIssueWithContextWithSuggestions
: templateIssueWithContext
: options.showSuggestions
? templateIssueWithSuggestions
: options.showSuggestions === false
? templateIssueNoFix
: templateIssue;
const { fileGlobs, silent, summary, issues, progress, verbose, debug } = options;
const emitters = {
Debug: !silent && debug ? (s) => console.info(chalk.cyan(s)) : nullEmitter,
Info: !silent && verbose ? (s) => console.info(chalk.yellow(s)) : nullEmitter,
Warning: (s) => console.info(chalk.yellow(s)),
};
function infoEmitter(message, msgType) {
emitters[msgType]?.(message);
}
const root = URI.file(path.resolve(options.root || process.cwd()));
const fsPathRoot = root.fsPath;
function relativeIssue(fn) {
const fnFilename = options.relative
? (uri) => relativeUriFilename(uri, fsPathRoot)
: (uri) => URI.parse(uri).fsPath;
return (i) => {
const filename = i.uri ? fnFilename(i.uri) : '';
const r = { ...i, filename };
fn(r);
};
}
const issuesCollection = progress ? [] : undefined;
const errorCollection = [];
function errorEmitter(message, error) {
if (isSpellingDictionaryLoadError(error)) {
error = error.cause;
}
const errorText = format(chalk.red(message), error.toString());
errorCollection?.push(errorText);
console.error(errorText);
}
const resultEmitter = (result) => {
if (!fileGlobs.length && !result.files) {
return;
}
const { files, issues, cachedFiles, filesWithIssues, errors } = result;
const numFilesWithIssues = filesWithIssues.size;
if (issuesCollection?.length || errorCollection?.length) {
console.error('-------------------------------------------');
}
if (issuesCollection?.length) {
console.error('Issues found:');
issuesCollection.forEach((issue) => console.error(issue));
}
const cachedFilesText = cachedFiles ? ` (${cachedFiles} from cache)` : '';
const withErrorsText = errors ? ` with ${errors} error${errors === 1 ? '' : 's'}` : '';
const numFilesWidthIssuesText = numFilesWithIssues === 1 ? '1 file' : `${numFilesWithIssues} files`;
const summaryMessage = `CSpell\x3a Files checked: ${files}${cachedFilesText}, Issues found: ${issues} in ${numFilesWidthIssuesText}${withErrorsText}.`;
console.error(summaryMessage);
if (errorCollection?.length && issues > 5) {
console.error('-------------------------------------------');
console.error('Errors:');
errorCollection.forEach((error) => console.error(error));
}
};
return {
issue: relativeIssue(silent || !issues ? nullEmitter : genIssueEmitter(issueTemplate, uniqueIssues, issuesCollection)),
error: silent ? nullEmitter : errorEmitter,
info: infoEmitter,
debug: emitters.Debug,
progress: !silent && progress ? (p) => reportProgress(p, fsPathRoot) : nullEmitter,
result: !silent && summary ? resultEmitter : nullEmitter,
};
}
function formatIssue(templateStr, issue, maxIssueTextWidth) {
function clean(t) {
return t.replace(/\s+/, ' ');
}
const { uri = '', filename, row, col, text, context, offset } = issue;
const contextLeft = clean(context.text.slice(0, offset - context.offset));
const contextRight = clean(context.text.slice(offset + text.length - context.offset));
const contextFull = clean(context.text);
const padContext = ' '.repeat(Math.max(maxIssueTextWidth - text.length, 0));
const rowText = row.toString();
const colText = col.toString();
const padRowCol = ' '.repeat(Math.max(1, 8 - (rowText.length + colText.length)));
const suggestions = formatSuggestions(issue);
const msg = issue.message || (issue.isFlagged ? 'Forbidden word' : 'Unknown word');
const message = issue.isFlagged ? `{yellow ${msg}}` : msg;
const substitutions = {
$col: colText,
$contextFull: contextFull,
$contextLeft: contextLeft,
$contextRight: contextRight,
$filename: filename,
$padContext: padContext,
$padRowCol: padRowCol,
$row: rowText,
$suggestions: suggestions,
$text: text,
$uri: uri,
$quickFix: formatQuickFix(issue),
};
const t = template(templateStr.replace(/\$message/g, message));
return substitute(chalkTemplate(t), substitutions).trimEnd();
}
function formatSuggestions(issue) {
if (issue.suggestionsEx) {
return issue.suggestionsEx
.map((sug) => sug.isPreferred
? chalk.italic(chalk.bold(sug.wordAdjustedToMatchCase || sug.word)) + '*'
: sug.wordAdjustedToMatchCase || sug.word)
.join(', ');
}
if (issue.suggestions) {
return issue.suggestions.join(', ');
}
return '';
}
function formatQuickFix(issue) {
if (!issue.suggestionsEx?.length)
return '';
const preferred = issue.suggestionsEx
.filter((sug) => sug.isPreferred)
.map((sug) => sug.wordAdjustedToMatchCase || sug.word);
if (!preferred.length)
return '';
const fixes = preferred.map((w) => chalk.italic(chalk.yellow(w)));
return `fix: (${fixes.join(', ')})`;
}
class TS extends Array {
raw;
constructor(s) {
super(s);
this.raw = [s];
}
}
function template(s) {
return new TS(s);
}
function substitute(text, substitutions) {
const subs = [];
for (const [match, replaceWith] of Object.entries(substitutions)) {
const len = match.length;
for (let i = text.indexOf(match); i >= 0; i = text.indexOf(match, i + 1)) {
subs.push([i, i + len, replaceWith]);
}
}
subs.sort((a, b) => a[0] - b[0]);
let i = 0;
function sub(r) {
const [a, b, t] = r;
const prefix = text.slice(i, a);
i = b;
return prefix + t;
}
const parts = subs.map(sub);
return parts.join('') + text.slice(i);
}
export const __testing__ = {
formatIssue,
};
//# sourceMappingURL=cli-reporter.js.map