UNPKG

cspell

Version:

A Spelling Checker for Code!

1,436 lines 117 kB
import { createRequire } from "node:module"; import { isAsyncIterable, opFilter, opMap, opTap, operators, pipeAsync, pipeAsync as asyncPipe, toAsyncIterable, toAsyncIterable as mergeAsyncIterables } from "@cspell/cspell-pipe"; import * as cspell from "cspell-lib"; import { ENV_CSPELL_GLOB_ROOT, IncludeExcludeFlag, MessageTypes, SuggestionError, Text, checkTextDocument, combineTextAndLanguageSettings, createDictionaryReferenceCollection, createPerfTimer, extractDependencies, extractImportErrors, fileToDocument, getDefaultSettings, getGlobalSettingsAsync, getSystemFeatureFlags, isBinaryFile, isSpellingDictionaryLoadError, mergeSettings, setLogger, shouldCheckDocument, spellCheckDocument, suggestionsForWords, traceWordsAsync } from "cspell-lib"; import assert from "node:assert"; import { format, formatWithOptions, stripVTControlCharacters } from "node:util"; import { isUrlLike, toFileDirURL, toFilePathOrHref, toFileURL, urlRelative } from "@cspell/url"; import chalk, { Chalk } from "chalk"; import { makeTemplate } from "chalk-template"; import ansiRegex from "ansi-regex"; import fs, { stat } from "node:fs/promises"; import { MutableCSpellConfigFile, createReaderWriter, cspellConfigFileSchema, isCfgArrayNode } from "cspell-config-lib"; import { promises } from "node:fs"; import { fileURLToPath } from "node:url"; import * as path$1 from "node:path"; import path, { isAbsolute, posix, relative, resolve, sep } from "node:path"; import { enablePerformanceMeasurements, measurePerf } from "@cspell/cspell-performance-monitor"; import { opMap as opMap$1, pipe } from "@cspell/cspell-pipe/sync"; import { IssueType, MessageTypes as MessageTypes$1, unknownWordsChoices } from "@cspell/cspell-types"; import { dictionaryCacheEnableLogging, dictionaryCacheGetLog } from "cspell-dictionary"; import { GitIgnore, findRepoRoot } from "cspell-gitignore"; import { GlobMatcher, fileOrGlobToGlob, workaroundPicomatchBug } from "cspell-glob"; import { dynamicImport } from "@cspell/dynamic-import"; import crypto from "node:crypto"; import streamConsumers from "node:stream/consumers"; import { getStat, readFileText, toURL } from "cspell-io"; import { glob } from "tinyglobby"; import * as readline from "node:readline"; import { parse, stringify } from "flatted"; //#region src/console.ts var ImplChannel = class { constructor(stream) { this.stream = stream; } write = (msg) => this.stream.write(msg); writeLine = (msg) => this.write(msg + "\n"); clearLine = (dir, callback) => this.stream.clearLine?.(dir, callback) ?? false; printLine = (...params) => this.writeLine(params.length && formatWithOptions({ colors: this.stream.hasColors?.() }, ...params) || ""); getColorLevel = () => getColorLevel(this.stream); }; var Console = class { stderrChannel; stdoutChannel; constructor(stdout = process.stdout, stderr = process.stderr) { this.stdout = stdout; this.stderr = stderr; this.stderrChannel = new ImplChannel(this.stderr); this.stdoutChannel = new ImplChannel(this.stdout); } log = (...p) => this.stdoutChannel.printLine(...p); error = (...p) => this.stderrChannel.printLine(...p); info = this.log; warn = this.error; }; const console = new Console(); function getColorLevel(stream) { switch (stream.getColorDepth?.() || 0) { case 1: return 1; case 4: return 2; case 24: return 3; default: return 0; } } //#endregion //#region src/util/errors.ts var CheckFailed = class extends Error { constructor(message, exitCode = 1) { super(message); this.exitCode = exitCode; } }; var ApplicationError = class extends Error { constructor(message, exitCode = 1, cause) { super(message); this.exitCode = exitCode; this.cause = cause; } }; var IOError = class extends ApplicationError { constructor(message, cause) { super(message, void 0, cause); this.cause = cause; } get code() { return this.cause.code; } isNotFound() { return this.cause.code === "ENOENT"; } }; function toError$1(e) { if (isError(e)) return e; if (isErrorLike(e)) { const ex = new Error(e.message, { cause: e }); if (e.code !== void 0) ex.code = e.code; return ex; } const message = format(e); return new Error(message); } function isError(e) { return e instanceof Error; } function isErrorLike(e) { if (e instanceof Error) return true; if (!e || typeof e !== "object") return false; return typeof e.message === "string"; } function toApplicationError(e, message) { if (e instanceof ApplicationError && !message) return e; const err = toError$1(e); return new ApplicationError(message ?? err.message, void 0, err); } //#endregion //#region src/util/perfMeasurements.ts function getPerfMeasurements() { const measurements = performance.getEntriesByType("measure"); const root = { depth: -1, totalTimeMs: 0, nestedTimeMs: 0, children: /* @__PURE__ */ new Map() }; if (!measurements.length) return []; const stack = []; let depth = 0; for (let i = 0; i < measurements.length; i++) { const m = measurements[i]; rollUpStack(m.startTime); const s = { m, p: addToParent(depth === 0 ? root : stack[depth - 1].p, m) }; stack[depth++] = s; } sortChildren(root); return [...root.children.values()].flatMap((r) => [...flattenChildren(r)]); function contains(m, t) { const stop = m.startTime + m.duration; return t >= m.startTime && t < stop; } function rollUpStack(t) { for (; depth > 0 && !contains(stack[depth - 1].m, t); --depth); } function addToParent(p, m) { p.children ??= /* @__PURE__ */ new Map(); p.nestedTimeMs += m.duration; return updateChild(p.children, m, p.depth + 1); } function updateChild(children, m, depth) { const p = children.get(m.name); if (p) { p.totalTimeMs += m.duration; p.count += 1; p.minTimeMs = Math.min(p.minTimeMs, m.duration); p.maxTimeMs = Math.max(p.maxTimeMs, m.duration); return p; } const n = { name: m.name, depth, totalTimeMs: m.duration, nestedTimeMs: 0, count: 1, minTimeMs: m.duration, maxTimeMs: m.duration }; children.set(m.name, n); return n; } function* flattenChildren(m) { yield m; if (!m.children) return; for (const child of m.children.values()) yield* flattenChildren(child); } function sortChildren(m) { if (!m.children) return; m.children = new Map([...m.children.entries()].sort((a, b) => b[1].totalTimeMs - a[1].totalTimeMs)); m.children.forEach(sortChildren); } } //#endregion //#region src/util/ansi.ts function isAnsiString(s) { return s.includes("\x1B") || s.includes("›"); } /** * * @param s - the string to measure - should NOT contains ANSI codes * @param tabWidth - * @returns */ function width(s, tabWidth = 1) { return s.replaceAll("…", ".").replaceAll(" ", " ".repeat(tabWidth)).replaceAll(/\p{M}/gu, "").replaceAll(/\p{L}/gu, ".").replaceAll(/[\u0000-\u001F\u0300-\u036F]/g, "").replaceAll(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, ".").length; } /** * Measure the width of a string containing ANSI control characters. * @param s - string to measure with width in characters. * @returns the approximate number of screen characters. */ function ansiWidth(s) { return width(stripVTControlCharacters(s)); } function fragmentString(str, splitOnRegex, sType) { const fragments = []; let lastIndex = 0; for (const match of str.matchAll(new RegExp(splitOnRegex))) { if (match.index > lastIndex) fragments.push({ type: "text", text: str.slice(lastIndex, match.index) }); fragments.push({ type: sType, text: match[0] }); lastIndex = match.index + match[0].length; } if (lastIndex < str.length) fragments.push({ type: "text", text: str.slice(lastIndex) }); return fragments; } const ansi = ansiRegex(); function parseAnsiStr(str) { return fragmentString(str, ansi, "ansi"); } /** * Prune the end of a string to fit within a specified width, adding an ellipsis if necessary. * @param str - the text to prune - ANSI is supported * @param maxWidth - the maximum width of the text * @param pad - the string to use for padding, default is '…' * @returns the pruned text */ function pruneAnsiTextEnd(str, maxWidth, pad = "…") { if (!maxWidth || maxWidth <= 0) return str; if (str.length <= maxWidth) return str; if (ansiWidth(str) <= maxWidth) return str; const padWidth = ansiWidth(pad); const fragments = parseAnsiStr(str); let remaining = maxWidth - padWidth; for (const frag of fragments) { if (frag.type !== "text") continue; if (remaining <= 0) { frag.text = ""; continue; } const pruned = pruneTextEnd(frag.text, remaining, pad); if (pruned !== frag.text) { frag.text = pruned; remaining = 0; continue; } remaining -= width(frag.text); } return fragments.map((frag) => frag.text).join(""); } /** * Prune the start of a string to fit within a specified width, adding an ellipsis if necessary. * @param str - the text to prune - ANSI is supported * @param maxWidth - the maximum width of the text * @param pad - the string to use for padding, default is '…' * @returns the pruned text */ function pruneAnsiTextStart(str, maxWidth, pad = "…") { if (!maxWidth || maxWidth <= 0) return str; if (str.length <= maxWidth) return str; if (ansiWidth(str) <= maxWidth) return str; const padWidth = ansiWidth(pad); const fragments = parseAnsiStr(str); let remaining = maxWidth - padWidth; for (const frag of fragments.reverse()) { if (frag.type !== "text") continue; if (remaining <= 0) { frag.text = ""; continue; } const pruned = pruneTextStart(frag.text, remaining, pad); if (pruned !== frag.text) { frag.text = pruned; remaining = 0; continue; } remaining -= width(frag.text); } return fragments.reverse().map((frag) => frag.text).join(""); } /** * Prune the end of a string to fit within a specified width, adding an ellipsis if necessary. * @param str - the text to prune - ANSI is not supported * @param maxWidth - the maximum width of the text * @param pad - the string to use for padding, default is '…' * @returns the pruned text */ function pruneTextEnd(str, maxWidth, pad = "…") { if (!maxWidth || maxWidth <= 0) return str; if (str.length <= maxWidth) return str; if (isAnsiString(str)) return pruneAnsiTextEnd(str, maxWidth, pad); const maxWidthWithPad = maxWidth - width(pad); const letters = [...str]; let len = 0; for (let i = 0; i < letters.length; i++) { const c = letters[i]; len += width(c); if (len > maxWidthWithPad) { let j = i + 1; while (j < letters.length && width(letters[j]) === 0) ++j; return j === letters.length ? str : letters.slice(0, i).join("") + pad; } } return str; } /** * Prune the start of a string to fit within a specified width, adding an ellipsis if necessary. * @param str - the text to prune - ANSI is not supported * @param maxWidth - the maximum width of the text * @param pad - the string to use for padding, default is '…' * @returns the pruned text */ function pruneTextStart(str, maxWidth, pad = "…") { if (!maxWidth || maxWidth <= 0) return str; if (str.length <= maxWidth) return str; const maxWidthWithPad = maxWidth - width(pad); const letters = [...str]; let len = 0; for (let i = letters.length - 1; i >= 1; i--) { const c = letters[i]; len += width(c); if (len > maxWidthWithPad) { i += 1; while (i < letters.length && width(letters[i]) === 0) ++i; return pad + letters.slice(i).join(""); } } return str; } //#endregion //#region src/util/pad.ts function pad(s, w) { const p = padWidth(s, w); if (!p) return s; return s.padEnd(p + s.length); } function padWidth(s, target) { const sWidth = ansiWidth(s); return Math.max(target - sWidth, 0); } function padLeft(s, w) { const p = padWidth(s, w); if (!p) return s; return s.padStart(p + s.length); } //#endregion //#region src/util/table.ts function tableToLines(table, deliminator) { const del = deliminator || table.deliminator || " | "; const columnWidths = []; const columnAlignments = table.columnAlignments || []; const maxColumnWidthsMap = table.maxColumnWidths || {}; const tableIndent = table.indent ? typeof table.indent === "number" ? " ".repeat(table.indent) : table.indent : ""; const { header, rows } = table; const simpleHeader = header.map((col) => Array.isArray(col) ? col[1] : col); const columnFieldNames = header.map((col) => Array.isArray(col) ? col[0] : col); const maxColumnWidths = columnFieldNames.map((field, idx) => maxColumnWidthsMap[field] ?? maxColumnWidthsMap[idx]); function getCell(row, col) { return getCellFromRow(rows[row], col); } function getCellFromRow(row, col) { if (!row) return void 0; if (Array.isArray(row)) return row[col]; return row[columnFieldNames[col]]; } function rowToCells(row) { if (Array.isArray(row)) return row; return columnFieldNames.map((fieldName) => row[fieldName]); } function getText(col, maxWidth) { return !col ? "" : typeof col === "string" ? pruneTextEnd(col, maxWidth) : col(maxWidth); } function getRCText(row, col, maxWidth) { return getText(getCell(row, col), maxWidth); } function recordHeaderWidths(header) { header.forEach((col, idx) => { columnWidths[idx] = Math.max(ansiWidth(col), columnWidths[idx] || 0); }); } function recordColWidths() { for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) for (let colIndex = 0; colIndex < columnFieldNames.length; colIndex++) columnWidths[colIndex] = Math.max(ansiWidth(getRCText(rowIndex, colIndex, void 0)), columnWidths[colIndex] || 0); } function justifyRow(c, i) { return columnAlignments[i] === "R" ? padLeft(c, columnWidths[i]) : pad(c, columnWidths[i]); } function toHeaderLine(header) { return tableIndent + decorateRowWith(header.map((c, i) => getText(c, columnWidths[i])), justifyRow, headerDecorator).join(del); } function toLine(row) { return tableIndent + decorateRowWith(rowToCells(row).map((c, i) => getText(c, columnWidths[i])), justifyRow).join(del); } function* process() { if (table.title) yield table.title; yield toHeaderLine(simpleHeader); yield* rows.map(toLine); } function sumColumnWidths() { return columnWidths.reduce((sum, width) => sum + width, 0); } function adjustColWidths() { for (let i = 0; i < columnWidths.length; i++) { const mw = maxColumnWidths[i]; if (!mw) continue; columnWidths[i] = Math.min(columnWidths[i], mw); } if (!table.terminalWidth) return; const dWidth = (columnWidths.length - 1) * ansiWidth(del); const lineWidth = table.terminalWidth - dWidth; if (lineWidth <= columnWidths.length * 2) { const fixedWidth = Math.max(Math.min(...columnWidths), 5); for (let i = 0; i < columnWidths.length; i++) columnWidths[i] = fixedWidth; return; } if (columnWidths.length === 1) { columnWidths[0] = lineWidth; return; } function trimWidestColumn(neededToTrim) { let first = 0; let second = 0; for (let i = 0; i < columnWidths.length; i++) if (columnWidths[i] > columnWidths[first]) { second = first; first = i; } else if (columnWidths[i] > columnWidths[second]) second = i; const diff = Math.max(columnWidths[first] - columnWidths[second], 1); columnWidths[first] -= Math.min(diff, neededToTrim); } for (let sum = sumColumnWidths(); sum > lineWidth; sum = sumColumnWidths()) trimWidestColumn(sum - lineWidth); } recordHeaderWidths(simpleHeader); recordColWidths(); adjustColWidths(); return [...process()]; } function headerDecorator(t) { return chalk.bold(chalk.underline(t)); } function decorateRowWith(row, ...decorators) { return decorators.reduce((row, decorator) => row.map(decorator), row); } //#endregion //#region src/util/util.ts const uniqueFn = uniqueFilterFnGenerator; function uniqueFilterFnGenerator(extractFn) { const values = /* @__PURE__ */ new Set(); const extractor = extractFn || ((a) => a); return (v) => { const vv = extractor(v); const ret = !values.has(vv); values.add(vv); return ret; }; } /** * Removed all properties with a value of `undefined` from the object. * @param src - the object to clean. * @returns the same object with all properties with a value of `undefined` removed. */ function clean(src) { const r = src; for (const key of Object.keys(r)) if (r[key] === void 0) delete r[key]; return r; } //#endregion //#region src/cli-reporter.ts 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 = `{green $filename}[$row, $col]: $message: {red $text}`; const templateIssueWordsOnly = "$text"; assert(true); /** * * @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(stdIO, errIO, 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 * .999, issue.text.length, 10); const issueText = formatIssue(stdIO, template, issue, Math.ceil(maxWidth)); reportedIssuesCollection?.push(formatIssue(errIO, template, issue, Math.ceil(maxWidth))); stdIO.writeLine(issueText); }; } function nullEmitter() {} function relativeUriFilename(uri, rootURL) { const url = toFileURL(uri); const rel = urlRelative(rootURL, url); if (rel.startsWith("..")) return toFilePathOrHref(url); return rel; } function reportProgress(io, p, cwdURL, options) { if (p.type === "ProgressFileComplete") return reportProgressFileComplete(io, p, cwdURL, options); if (p.type === "ProgressFileBegin") return reportProgressFileBegin(io, p, cwdURL); } function determineFilename(io, p, cwd) { const fc = "" + p.fileCount; return { idx: (" ".repeat(fc.length) + p.fileNum).slice(-fc.length) + "/" + fc, filename: io.chalk.gray(relativeUriFilename(p.filename, cwd)) }; } function reportProgressFileBegin(io, p, cwdURL) { const { idx, filename } = determineFilename(io, p, cwdURL); if (io.getColorLevel() > 0) { io.clearLine?.(0); io.write(`${idx} ${filename}\r`); } } function reportProgressFileComplete(io, p, cwd, options) { const { idx, filename } = determineFilename(io, p, cwd); const { verbose, debug } = options; const time = reportTime(io, p.elapsedTimeMs, !!p.cached); const skippedReason = p.skippedReason ? ` (${p.skippedReason})` : ""; const skipped = p.processed === false ? ` skipped${skippedReason}` : ""; const hasErrors = p.numErrors ? io.chalk.red` X` : ""; const msg = `${idx} ${filename} ${time}${skipped}${hasErrors}${(verbose || debug || hasErrors || isSlow(p.elapsedTimeMs) || io.getColorLevel() < 1 ? "\n" : "") || "\r"}`; io.write(msg); } function reportTime(io, elapsedTimeMs, cached) { if (cached) return io.chalk.green("cached"); if (elapsedTimeMs === void 0) return "-"; const slow = isSlow(elapsedTimeMs); return (!slow ? io.chalk.white : slow === 1 ? io.chalk.yellow : io.chalk.redBright)(elapsedTimeMs.toFixed(2) + "ms"); } function isSlow(elapsedTmeMs) { if (!elapsedTmeMs || elapsedTmeMs < 1e3) return 0; if (elapsedTmeMs < 2e3) return 1; return 2; } function getReporter(options, config) { const perfStats = { filesProcessed: 0, filesSkipped: 0, filesCached: 0, accumulatedTimeMs: 0, startTime: performance.now(), perf: Object.create(null) }; const noColor = options.color === false; const forceColor = options.color === true; const uniqueIssues = config?.unique || false; const defaultIssueTemplate = 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: showProgress, verbose, debug } = options; const issueTemplate = config?.issueTemplate || defaultIssueTemplate; assertCheckTemplate(issueTemplate); const console$1 = config?.console || console; const colorLevel = noColor ? 0 : forceColor ? 2 : console$1.stdoutChannel.getColorLevel(); const stdio = { ...console$1.stdoutChannel, chalk: new Chalk({ level: colorLevel }) }; const stderr = { ...console$1.stderrChannel, chalk: new Chalk({ level: colorLevel }) }; const consoleError = (msg) => stderr.writeLine(msg); function createInfoLog(wrap) { return (msg) => console$1.info(wrap(msg)); } const emitters = { Debug: !silent && debug ? createInfoLog(stdio.chalk.cyan) : nullEmitter, Info: !silent && verbose ? createInfoLog(stdio.chalk.yellow) : nullEmitter, Warning: createInfoLog(stdio.chalk.yellow) }; function infoEmitter(message, msgType) { emitters[msgType]?.(message); } const rootURL = toFileDirURL(options.root || process.cwd()); function relativeIssue(fn) { const fnFilename = options.relative ? (uri) => relativeUriFilename(uri, rootURL) : (uri) => toFilePathOrHref(toFileURL(uri, rootURL)); return (i) => { const fullFilename = i.uri ? toFilePathOrHref(toFileURL(i.uri, rootURL)) : ""; const filename = i.uri ? fnFilename(i.uri) : ""; fn({ ...i, filename, fullFilename }); }; } const issuesCollection = void 0; const errorCollection = []; function errorEmitter(message, error) { if (isSpellingDictionaryLoadError(error)) error = error.cause; const errorText = formatWithOptions({ colors: stderr.stream.hasColors?.() }, stderr.chalk.red(message), debug ? error : error.toString()); errorCollection?.push(errorText); consoleError(errorText); } const resultEmitter = (result) => { if (!fileGlobs.length && !result.files) return; const { files, issues, cachedFiles, filesWithIssues, errors, skippedFiles } = result; const numFilesWithIssues = filesWithIssues.size; const chalk = stderr.chalk; if (stderr.getColorLevel() > 0) { stderr.write("\r"); stderr.clearLine(0); } if (issuesCollection?.length || errorCollection?.length) consoleError("-------------------------------------------"); if (issuesCollection?.length) { consoleError("Issues found:"); issuesCollection.forEach((issue) => consoleError(issue)); } const filesChecked = files - (skippedFiles || 0); const cachedFilesText = cachedFiles ? ` (${cachedFiles} from cache)` : ""; const skippedFilesText = skippedFiles ? `, skipped: ${skippedFiles}` : ""; const withErrorsText = errors ? ` with ${errors} error${errors === 1 ? "" : "s"}` : ""; consoleError(`CSpell\u003A Files checked: ${filesChecked}${cachedFilesText}${skippedFilesText}, Issues found: ${issues} in ${numFilesWithIssues === 1 ? "1 file" : `${numFilesWithIssues} files`}${withErrorsText}.`); if (errorCollection?.length && issues > 5) { consoleError("-------------------------------------------"); consoleError("Errors:"); errorCollection.forEach((error) => consoleError(error)); } if (options.showPerfSummary) { const elapsedTotal = performance.now() - perfStats.startTime; consoleError("-------------------------------------------"); consoleError("Performance Summary:"); consoleError(` Files Processed : ${perfStats.filesProcessed.toString().padStart(11)}`); consoleError(` Files Skipped : ${perfStats.filesSkipped.toString().padStart(11)}`); consoleError(` Files Cached : ${perfStats.filesCached.toString().padStart(11)}`); consoleError(` Processing Time : ${perfStats.accumulatedTimeMs.toFixed(2).padStart(9)}ms`); consoleError(` Total Time : ${elapsedTotal.toFixed(2).padStart(9)}ms`); const tableStats = { title: chalk.bold("Perf Stats:"), header: ["Name", "Time (ms)"], columnAlignments: ["L", "R"], indent: 2, rows: Object.entries(perfStats.perf).filter((p) => !!p[1]).map(([key, value]) => [key, value.toFixed(2)]) }; consoleError(""); for (const line of tableToLines(tableStats)) consoleError(line); if (options.verboseLevel) verbosePerfReport(); } }; function verbosePerfReport() { const perfMeasurements = getPerfMeasurements(); if (!perfMeasurements.length) return; const notable = extractNotableBySelfTimeInGroup(perfMeasurements); const chalk = stderr.chalk; const maxDepth = Math.max(...perfMeasurements.map((m) => m.depth)); const depthIndicator = (d) => "⋅".repeat(d) + " ".repeat(maxDepth - d); const rows = perfMeasurements.map((m) => { const cbd = (text) => colorByDepth(chalk, m.depth, text); const cNotable = (text) => notable.has(m) ? chalk.yellow(text) : text; return [ chalk.dim("⋅".repeat(m.depth)) + colorByDepthGrayscale(stderr.chalk, m.depth, m.name), cbd(m.totalTimeMs.toFixed(2) + chalk.dim(depthIndicator(m.depth))), cbd(cNotable((m.totalTimeMs - m.nestedTimeMs).toFixed(2))), cbd(m.count.toString()), cbd(m.minTimeMs.toFixed(2)), cbd(m.maxTimeMs.toFixed(2)), cbd((m.totalTimeMs / m.count).toFixed(2)) ]; }); const table = tableToLines({ title: chalk.bold("Detailed Measurements:"), header: [ "Name", "Total Time (ms)", "Self (ms)", "Count", "Min (ms)", "Max (ms)", "Avg (ms)" ], rows, columnAlignments: [ "L", "R", "R", "R", "R", "R", "R" ], indent: 2 }); consoleError("\n-------------------------------------------\n"); for (const line of table) consoleError(line); } function colorByDepth(chalk, depth, text) { const colors = [ chalk.green, chalk.cyan, chalk.blue, chalk.magenta, chalk.red ]; const color = colors[depth % colors.length]; if (depth / colors.length >= 1) return chalk.dim(color(text)); return color(text); } function colorByDepthGrayscale(chalk, depth, text) { const grayLevel = Math.max(32, 255 - depth * 20); return chalk.rgb(grayLevel, grayLevel, grayLevel)(text); } function collectPerfStats(p) { if (p.cached) { perfStats.filesCached++; return; } perfStats.filesProcessed += p.processed ? 1 : 0; perfStats.filesSkipped += !p.processed ? 1 : 0; perfStats.accumulatedTimeMs += p.elapsedTimeMs || 0; if (!p.perf) return; for (const [key, value] of Object.entries(p.perf)) if (typeof value === "number") perfStats.perf[key] = (perfStats.perf[key] || 0) + value; } function progress(p) { if (!silent && showProgress) reportProgress(stderr, p, rootURL, options); if (p.type === "ProgressFileComplete") collectPerfStats(p); } return { issue: relativeIssue(silent || !issues ? nullEmitter : genIssueEmitter(stdio, stderr, issueTemplate, uniqueIssues, issuesCollection)), error: silent ? nullEmitter : errorEmitter, info: infoEmitter, debug: emitters.Debug, progress, result: !silent && summary ? resultEmitter : nullEmitter, features: void 0 }; } function extractNotableBySelfTimeInGroup(measurements) { const notable = /* @__PURE__ */ new Set(); if (!measurements.length) return notable; let highest; let highestSelfTime = 0; for (const m of measurements) { if (m.depth === 0 || !highest) { if (highest) notable.add(highest); highest = m; highestSelfTime = m.totalTimeMs - m.nestedTimeMs; continue; } const selfTime = m.totalTimeMs - m.nestedTimeMs; if (selfTime > highestSelfTime) { highest = m; highestSelfTime = selfTime; } } if (highest) notable.add(highest); return notable; } function formatIssue(io, templateStr, issue, maxIssueTextWidth) { function clean(t) { return t.replace(/\s+/, " "); } const { uri = "", filename, row, col, text, context = issue.line, 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(io, issue); const msg = issue.message || (issue.isFlagged ? "Forbidden word" : "Unknown word"); const messageColored = 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(io, issue), $message: msg, $messageColored: messageColored }; const t = templateStr.replaceAll("$messageColored", messageColored); return substitute(makeTemplate(io.chalk)(t), substitutions).trimEnd(); } function formatSuggestions(io, issue) { if (issue.suggestionsEx) return issue.suggestionsEx.map((sug) => sug.isPreferred ? io.chalk.italic(io.chalk.bold(sug.wordAdjustedToMatchCase || sug.word)) + "*" : sug.wordAdjustedToMatchCase || sug.word).join(", "); if (issue.suggestions) return issue.suggestions.join(", "); return ""; } function formatQuickFix(io, issue) { if (!issue.suggestionsEx?.length) return ""; const preferred = issue.suggestionsEx.filter((sug) => sug.isPreferred).map((sug) => sug.wordAdjustedToMatchCase || sug.word); if (!preferred.length) return ""; return `fix: (${preferred.map((w) => io.chalk.italic(io.chalk.yellow(w))).join(", ")})`; } 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)) { const end = i + len; const reg = /\b/y; reg.lastIndex = end; if (reg.test(text)) subs.push([ i, end, replaceWith ]); i = end; } } 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; } return subs.map(sub).join("") + text.slice(i); } function assertCheckTemplate(template) { const r = checkTemplate(template); if (r instanceof Error) throw r; } function checkTemplate(template) { const chalkTemplate = makeTemplate(new Chalk()); const substitutions = { $col: "<col>", $contextFull: "<contextFull>", $contextLeft: "<contextLeft>", $contextRight: "<contextRight>", $filename: "<filename>", $padContext: "<padContext>", $padRowCol: "<padRowCol>", $row: "<row>", $suggestions: "<suggestions>", $text: "<text>", $uri: "<uri>", $quickFix: "<quickFix>", $message: "<message>", $messageColored: "<messageColored>" }; try { const problems = [...substitute(chalkTemplate(template), substitutions).matchAll(/\$[a-z]+/gi)].map((m) => m[0]); if (problems.length) throw new Error(`Unresolved template variable${problems.length > 1 ? "s" : ""}: ${problems.map((v) => `'${v}'`).join(", ")}`); return true; } catch (e) { return new ApplicationError(e instanceof Error ? e.message : `${e}`); } } //#endregion //#region src/config/adjustConfig.ts async function fileExists(url) { if (url.protocol !== "file:") return false; try { return (await promises.stat(url)).isFile(); } catch (e) { if (toError$1(e).code === "ENOENT") return false; throw e; } } async function resolveImports(configFile, imports) { const fromConfigDir = new URL("./", configFile.url); const fromCurrentDir = toFileDirURL("./"); const require = createRequire(fromConfigDir); function isPackageName(name) { try { require.resolve(name, { paths: [fileURLToPath(fromConfigDir)] }); return true; } catch { return false; } } const _imports = []; for (const imp of imports) { const url = new URL(imp, fromCurrentDir); if (url.protocol !== "file:") { _imports.push(imp); continue; } if (await fileExists(url)) { let rel = urlRelative(fromConfigDir, url); if (!(rel.startsWith("./") || rel.startsWith("../"))) rel = "./" + rel; _imports.push(rel); continue; } if (url.protocol !== "file:") { _imports.push(url.href); continue; } if (isPackageName(imp)) { _imports.push(imp); continue; } throw new Error(`Cannot resolve import: ${imp}`); } return _imports; } function addImportsToMutableConfigFile(configFile, resolvedImports, comment) { let importNode = configFile.getNode("import", []); if (importNode.type === "scalar") { configFile.setValue("import", [importNode.value]); importNode = configFile.getNode("import", []); } assert(isCfgArrayNode(importNode)); const knownImports = new Set(importNode.value); for (const imp of resolvedImports) { if (knownImports.has(imp)) continue; importNode.push(imp); } if (comment) configFile.setComment("import", comment); } async function addImportsToConfigFile(configFile, imports, comment) { const resolvedImports = await resolveImports(configFile, imports); if (configFile instanceof MutableCSpellConfigFile) return addImportsToMutableConfigFile(configFile, resolvedImports, comment); const settings = configFile.settings; let importNode = settings.import; if (!Array.isArray(importNode)) { importNode = typeof importNode === "string" ? [importNode] : []; settings.import = importNode; if (comment) configFile.setComment("import", comment); } assert(Array.isArray(importNode)); const knownImports = new Set(importNode); for (const imp of resolvedImports) { if (knownImports.has(imp)) continue; importNode.push(imp); } } function setConfigFieldValue(configFile, key, value, comment) { configFile.setValue(key, value); if (comment !== void 0) configFile.setComment(key, comment); } function addDictionariesToConfigFile(configFile, dictionaries, comment) { if (configFile instanceof MutableCSpellConfigFile) { const found = configFile.getValue("dictionaries"); const dicts = configFile.getNode("dictionaries", []); assert(isCfgArrayNode(dicts)); const knownDicts = new Set(dicts.value); for (const dict of dictionaries) if (!knownDicts.has(dict)) { dicts.push(dict); knownDicts.add(dict); } if (!found && comment) configFile.setComment("dictionaries", comment); return; } const dicts = configFile.settings.dictionaries || []; const knownDicts = new Set(dicts); for (const dict of dictionaries) if (!knownDicts.has(dict)) { dicts.push(dict); knownDicts.add(dict); } setConfigFieldValue(configFile, "dictionaries", dicts, comment); } //#endregion //#region src/config/config.ts function applyValuesToConfigFile(config, settings, defaultValues, addComments) { const currentSettings = config.settings || {}; for (const [k, entry] of Object.entries(defaultValues)) { const { value: defaultValue, comment } = entry; const key = k; const newValue = settings[key]; const oldValue = currentSettings[key]; const value = newValue ?? oldValue ?? defaultValue; if (newValue === void 0 && oldValue !== void 0 || value === void 0) continue; setConfigFieldValue(config, key, value, addComments && oldValue === void 0 && comment || void 0); } return config; } //#endregion //#region src/config/constants.ts const defaultConfig = { $schema: { value: void 0, comment: " The schema for the configuration file." }, version: { value: "0.2", comment: " The version of the configuration file format." }, name: { value: void 0, comment: " The name of the configuration. Use for display purposes only." }, description: { value: void 0, comment: " A description of the configuration." }, language: { value: "en", comment: " The locale to use when spell checking. (e.g., en, en-GB, de-DE" }, import: { value: void 0, comment: " Configuration or packages to import." }, dictionaryDefinitions: { value: void 0, comment: " Define user dictionaries." }, dictionaries: { value: void 0, comment: " Enable the dictionaries." }, ignorePaths: { value: void 0, comment: " Glob patterns of files to be skipped." }, files: { value: void 0, comment: " Glob patterns of files to be included." }, words: { value: void 0, comment: " Words to be considered correct." }, ignoreWords: { value: void 0, comment: " Words to be ignored." }, flagWords: { value: void 0, comment: " Words to be flagged as incorrect." }, overrides: { value: void 0, comment: " Set configuration based upon file globs." }, languageSettings: { value: void 0, comment: " Define language specific settings." }, enabledFileTypes: { value: void 0, comment: " Enable for specific file types." }, caseSensitive: { value: void 0, comment: " Enable case sensitive spell checking." }, patterns: { value: void 0, comment: " Regular expression patterns." }, ignoreRegExpList: { value: void 0, comment: " Regular expressions / patterns of text to be ignored." }, includeRegExpList: { value: void 0, comment: " Regular expressions / patterns of text to be included." } }; //#endregion //#region src/config/configInit.ts const schemaRef = cspellConfigFileSchema; const defaultConfigJson = `\ { } `; const defaultConfigYaml = ` `; async function configInit(options) { const rw = createReaderWriter(); const configFile = await createConfigFile(rw, determineFileNameURL(options), options); await applyOptionsToConfigFile(configFile, options); await fs.mkdir(new URL("./", configFile.url), { recursive: true }); if (options.stdout) console.stdoutChannel.write(rw.serialize(configFile)); else await rw.writeConfig(configFile); } async function applyOptionsToConfigFile(configFile, options) { const settings = {}; const addComments = options.comments || options.comments === void 0 && !options.removeComments && !configFile.url.pathname.endsWith(".json"); if (options.comments === false) configFile.removeAllComments(); if (options.schema ?? true) configFile.setSchema(schemaRef); if (options.locale) settings.language = options.locale; applyValuesToConfigFile(configFile, settings, defaultConfig, addComments); if (options.import) await addImportsToConfigFile(configFile, options.import, addComments && defaultConfig.import?.comment || void 0); if (options.dictionary) addDictionariesToConfigFile(configFile, options.dictionary, addComments && defaultConfig.dictionaries?.comment || void 0); return configFile; } function determineFileNameURL(options) { if (options.config) return toFileURL(options.config); const defaultFileName = determineDefaultFileName(options); const outputUrl = toFileURL(options.output || defaultFileName); const path = outputUrl.pathname; if (path.endsWith(".json") || path.endsWith(".jsonc") || path.endsWith(".yaml") || path.endsWith(".yml")) return outputUrl; if (/\.{m,c}?{j,t}s$/.test(path)) throw new Error(`Unsupported file extension: ${path}`); return new URL(defaultFileName, toFileDirURL(outputUrl)); } function determineDefaultFileName(options) { switch (options.format || "yaml") { case "json": return "cspell.json"; case "jsonc": return "cspell.jsonc"; case "yaml": return "cspell.config.yaml"; case "yml": return "cspell.config.yml"; } throw new Error(`Unsupported format: ${options.format}`); } function getDefaultContent(options) { switch (options.format) { case void 0: case "yaml": return defaultConfigYaml; case "json": case "jsonc": return defaultConfigJson; default: throw new Error(`Unsupported format: ${options.format}`); } } async function createConfigFile(rw, url, options) { if (url.pathname.endsWith("package.json")) return rw.readConfig(url); const content = await fs.readFile(url, "utf8").catch(() => getDefaultContent(options)); return rw.parse({ url, content }); } //#endregion //#region src/featureFlags/featureFlags.ts function getFeatureFlags() { return getSystemFeatureFlags(); } function parseFeatureFlags(flags, featureFlags = getFeatureFlags()) { if (!flags) return featureFlags; const flagsKvP = flags.map((f) => f.split(":", 2)); for (const flag of flagsKvP) { const [name, value] = flag; try { featureFlags.setFlag(name, value); } catch { console.warn(`Unknown flag: "${name}"`); } } return featureFlags; } //#endregion //#region src/environment.ts const environmentKeys = { CSPELL_ENABLE_DICTIONARY_LOGGING: "CSPELL_ENABLE_DICTIONARY_LOGGING", CSPELL_ENABLE_DICTIONARY_LOG_FILE: "CSPELL_ENABLE_DICTIONARY_LOG_FILE", CSPELL_ENABLE_DICTIONARY_LOG_FIELDS: "CSPELL_ENABLE_DICTIONARY_LOG_FIELDS", CSPELL_GLOB_ROOT: "CSPELL_GLOB_ROOT", CSPELL_CONFIG_PATH: "CSPELL_CONFIG_PATH", CSPELL_DEFAULT_CONFIG_PATH: "CSPELL_DEFAULT_CONFIG_PATH" }; function setEnvironmentVariable(key, value) { process.env[key] = value; } function getEnvironmentVariable(key) { return process.env[key]; } function truthy(value) { switch (value?.toLowerCase().trim()) { case "t": case "true": case "on": case "yes": case "1": return true; } return false; } //#endregion //#region src/dirname.ts let _dirname; try { if (typeof import.meta.url !== "string") throw new Error("assert"); _dirname = fileURLToPath(new URL(".", import.meta.url)); } catch { _dirname = __dirname; } const pkgDir = _dirname; const npmPackage = { name: "cspell", version: "10.0.0", engines: { node: ">=22.18.0" } }; //#endregion //#region src/reporters/reporters.ts function filterFeatureIssues(features, issue, reportOptions) { if (issue.issueType === IssueType.directive) return features?.issueType && reportOptions?.validateDirectives || false; if (features?.unknownWords) return true; if (!reportOptions) return true; if (issue.isFlagged || !reportOptions.unknownWords || reportOptions.unknownWords === unknownWordsChoices.ReportAll) return true; if (issue.hasPreferredSuggestions && reportOptions.unknownWords !== unknownWordsChoices.ReportFlagged) return true; if (issue.hasSimpleSuggestions && reportOptions.unknownWords === unknownWordsChoices.ReportSimple) return true; return false; } function handleIssue(reporter, issue, reportOptions) { if (!reporter.issue) return; if (!filterFeatureIssues(reporter.features, issue, reportOptions)) return; if (!reporter.features?.contextGeneration && !issue.context) { issue = { ...issue }; issue.context = issue.line; } return reporter.issue(issue, reportOptions); } /** * Loads reporter modules configured in cspell config file */ 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$1(e).message}`); } } reporters = !reporters || !reporters.length ? ["default"] : [...reporters]; return (await Promise.all(reporters.map(loadReporter))).filter((v) => v !== void 0); } function finalizeReporter(reporter) { if (!reporter) return void 0; if (reporterIsFinalized(reporter)) return reporter; return { 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 }; } 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 !== void 0) options[key] = value; } function extractReporterIssueOptions(settings) { const src = settings; const options = {}; for (const key in reportIssueOptionsKeyMap) { const k = key; setValue(options, k, src[k]); } return options; } 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; } var LintReporter = class { #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 numIssues = result.issues.filter((issue) => filterFeatureIssues({}, issue, result.reportIssueOptions)).length; for (const reporter of this.#reporters) { const progress = clean({ type: "ProgressFileComplete", fileNum, fileCount, filename, elapsedTimeMs: result.elapsedTimeMs, processed: result.processed, skippedReason: result.skippedReason, numErrors: numIssues || result.errors, cached: result.cached, perf: result.perf, issues: reporter.features && result.issues, reportIssueOptions: reporter.features && result.reportIssueOptions }); reporter.progress(progress); } result.issues.forEach((issue) => this.issue(issue, result.reportIssueOptions)); return numIssues; } }; var ReportItemCollector = class { #collection; constructor(collection) { this.#collection = collection; } get #items() { if (!this.#collection.reportItems) this.#collection.reportItems = []; return this.#collection.reportItems; } get items() { return this.#collection.reportItems; } info(...params) { this.#items.push({ type: "info", payload: params }); } debug(...params) { this.#items.push({ type: "debug", payload: params }); } error(...params) { this.#items.push({ type: "error", payload: params }); } }; function replayReportItems(reportItemsCollection, reporter) { const items = reportItemsCollection.reportItems; if (!items) return; for (const item of items) switch (item.type) { case "info": reporter.info(...item.payload); break; case "debug": reporter.debug(...item.payload); break; case "error": reporter.error(...item.payload); break; } } //#endregion //#region src/util/async.ts const asyncMap = operators.opMapAsync; operators.opFilterAsync; const asyncAwait = operators.opAwaitAsync; const asyncFlatten = operators.opFlattenAsync; //#endregion //#region src/util/constants.ts const UTF8 = "utf8"; const STDINProtocol = "stdin:"; const STDINUrlPrefix = "stdin://"; //#endregion //#region src/util/glob.ts const defaultExcludeGlobs = ["node_modules/**"]; /** * * @param pattern - glob patterns and NOT file paths. It can be a file path turned into a glob. * @param options - search options. */ async function globP(pattern, options) { const cwd = options?.root || options?.cwd || process.cwd(); const ignore = (typeof options?.ignore === "string" ? [options.ignore] : options?.ignore)?.filter((g) => !g.startsWith("../")); const onlyFiles = options?.nodir; const dot = options?.dot; const patterns = typeof pattern === "string" ? [pattern] : pattern; const useOptions = clean({ cwd, onlyFiles, dot, ignore, absolute: true, followSymbolicLinks: false, expandDirectories: false }); const compare = new Intl.Collator("en").compare; return (await glob$1(patterns, useOptions)).sort(compare).map((absFilename) => path$1.relative(cwd, absFilename)); } function calcGlobs(commandLineExclude) { const commandLineExcludes = { globs: [...new Set((commandLineExclude || []).flatMap((glob) => glob.split(/(?<!\\)\s+/g)).map((g) => g.replaceAll("\\ ", " ")))], source: "arguments" }; const defaultExcludes = { globs: defaultExcludeGlobs, source: "def