UNPKG

cspell

Version:

A Spelling Checker for Code!

1,477 lines (1,452 loc) 88.2 kB
import { createRequire } from "node:module"; import chalk, { Chalk } from "chalk"; 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, SuggestionError, Text, checkTextDocument, combineTextAndLanguageSettings, createPerfTimer, extractDependencies, extractImportErrors, fileToDocument, getDefaultSettings, getDictionary, getGlobalSettingsAsync, getSystemFeatureFlags, isBinaryFile, isSpellingDictionaryLoadError, mergeSettings, setLogger, shouldCheckDocument, spellCheckDocument, suggestionsForWords, traceWordsAsync } from "cspell-lib"; import assert from "node:assert"; import { format, formatWithOptions } from "node:util"; import { toFileDirURL, toFilePathOrHref, toFileURL, urlRelative } from "@cspell/url"; import { makeTemplate } from "chalk-template"; import fs, { stat } from "node:fs/promises"; import { MutableCSpellConfigFile, createReaderWriter, cspellConfigFileSchema, isCfgArrayNode } from "cspell-config-lib"; import * as fs$1 from "node:fs"; import { mkdirSync, promises } from "node:fs"; import { fileURLToPath } from "node:url"; import * as path$1 from "node:path"; import path, { dirname, isAbsolute, posix, relative, resolve } from "node:path"; import { opMap as opMap$1, pipe } from "@cspell/cspell-pipe/sync"; import { IssueType, MessageTypes, unknownWordsChoices } from "@cspell/cspell-types"; import { _debug } from "cspell-dictionary"; import { GitIgnore, findRepoRoot } from "cspell-gitignore"; import { GlobMatcher, fileOrGlobToGlob, workaroundPicomatchBug } from "cspell-glob"; import * as crypto from "node:crypto"; import streamConsumers from "node:stream/consumers"; import { readFileText, toURL } from "cspell-io"; import { glob } from "tinyglobby"; import * as readline from "node:readline"; import { isMainThread } from "node:worker_threads"; import fileEntryCache from "file-entry-cache"; import { dynamicImport } from "@cspell/dynamic-import"; //#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) { const depth = stream.getColorDepth?.() || 0; switch (depth) { 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(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; const ex = e; return typeof ex.message === "string"; } function toApplicationError(e, message) { if (e instanceof ApplicationError && !message) return e; const err = toError(e); return new ApplicationError(message ?? err.message, void 0, err); } //#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; const fn = (" ".repeat(fc.length) + p.fileNum).slice(-fc.length); const idx = fn + "/" + fc; const filename = io.chalk.gray(relativeUriFilename(p.filename, cwd)); return { idx, filename }; } 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 skipped = p.processed === false ? " skipped" : ""; const hasErrors = p.numErrors ? io.chalk.red` X` : ""; const newLine = skipped && (verbose || debug) || hasErrors || isSlow(p.elapsedTimeMs) || io.getColorLevel() < 1 ? "\n" : ""; const msg = `${idx} ${filename} ${time}${skipped}${hasErrors}${newLine || "\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); const color = !slow ? io.chalk.white : slow === 1 ? io.chalk.yellow : io.chalk.redBright; return color(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, elapsedTimeMs: 0, 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) : ""; const r = { ...i, filename, fullFilename }; fn(r); }; } const repeatIssues = false; 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: issues$1, cachedFiles, filesWithIssues, errors } = result; const numFilesWithIssues = filesWithIssues.size; 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 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\u003A Files checked: ${files}${cachedFilesText}, Issues found: ${issues$1} in ${numFilesWidthIssuesText}${withErrorsText}.`; consoleError(summaryMessage); if (errorCollection?.length && issues$1 > 5) { consoleError("-------------------------------------------"); consoleError("Errors:"); errorCollection.forEach((error) => consoleError(error)); } if (options.showPerfSummary) { consoleError("-------------------------------------------"); consoleError("Performance Summary:"); consoleError(` Files Processed: ${perfStats.filesProcessed.toString().padStart(6)}`); consoleError(` Files Skipped : ${perfStats.filesSkipped.toString().padStart(6)}`); consoleError(` Files Cached : ${perfStats.filesCached.toString().padStart(6)}`); consoleError(` Processing Time: ${perfStats.elapsedTimeMs.toFixed(2).padStart(9)}ms`); consoleError("Stats:"); const stats = Object.entries(perfStats.perf).filter((p) => !!p[1]).map(([key, value]) => [key, value.toFixed(2)]); const padName = Math.max(...stats.map((s) => s[0].length)); const padValue = Math.max(...stats.map((s) => s[1].length)); stats.sort((a, b) => a[0].localeCompare(b[0])); for (const [key, value] of stats) value && consoleError(` ${key.padEnd(padName)}: ${value.padStart(padValue)}ms`); } }; function collectPerfStats(p) { if (p.cached) { perfStats.filesCached++; return; } perfStats.filesProcessed += p.processed ? 1 : 0; perfStats.filesSkipped += !p.processed ? 1 : 0; perfStats.elapsedTimeMs += 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 formatIssue(io, templateStr, issue, maxIssueTextWidth) { function clean$1(t$1) { return t$1.replace(/\s+/, " "); } const { uri = "", filename, row, col, text, context = issue.line, offset } = issue; const contextLeft = clean$1(context.text.slice(0, offset - context.offset)); const contextRight = clean$1(context.text.slice(offset + text.length - context.offset)); const contextFull = clean$1(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$1 = 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$1, $text: text, $uri: uri, $quickFix: formatQuickFix(io, issue), $message: msg, $messageColored: messageColored }; const t = templateStr.replaceAll("$messageColored", messageColored); const chalkTemplate = makeTemplate(io.chalk); return substitute(chalkTemplate(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 ""; const fixes = preferred.map((w) => io.chalk.italic(io.chalk.yellow(w))); return `fix: (${fixes.join(", ")})`; } function substitute(text, substitutions) { const subs = []; for (const [match, replaceWith] of Object.entries(substitutions)) { const len = match.length; for (let i$1 = text.indexOf(match); i$1 >= 0; i$1 = text.indexOf(match, i$1)) { const end = i$1 + len; const reg = /\b/y; reg.lastIndex = end; if (reg.test(text)) subs.push([ i$1, end, replaceWith ]); i$1 = 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; } const parts = subs.map(sub); return parts.join("") + text.slice(i); } function assertCheckTemplate(template) { const r = checkTemplate(template); if (r instanceof Error) throw r; } function checkTemplate(template) { const chalk$1 = new Chalk(); const chalkTemplate = makeTemplate(chalk$1); 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 t = chalkTemplate(template); const result = substitute(t, substitutions); const problems = [...result.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) { const msg = e instanceof Error ? e.message : `${e}`; return new ApplicationError(msg); } } //#endregion //#region src/config/adjustConfig.ts async function fileExists(url) { if (url.protocol !== "file:") return false; try { const stats = await promises.stat(url); return stats.isFile(); } catch (e) { const err = toError(e); if (err.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$1) { try { require.resolve(name$1, { 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$1 = configFile.getNode("dictionaries", []); assert(isCfgArrayNode(dicts$1)); const knownDicts$1 = new Set(dicts$1.value); for (const dict of dictionaries) if (!knownDicts$1.has(dict)) { dicts$1.push(dict); knownDicts$1.add(dict); } if (!found && comment) configFile.setComment("dictionaries", comment); return; } const settings = configFile.settings; const dicts = 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; const useComment = addComments && oldValue === void 0 && comment || void 0; setConfigFieldValue(config, key, value, useComment); } 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 url = determineFileNameURL(options); const configFile = await createConfigFile(rw, url, 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$2 = outputUrl.pathname; if (path$2.endsWith(".json") || path$2.endsWith(".jsonc") || path$2.endsWith(".yaml") || path$2.endsWith(".yml")) return outputUrl; if (/\.{m,c}?{j,t}s$/.test(path$2)) throw new Error(`Unsupported file extension: ${path$2}`); 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$1, value] = flag; try { featureFlags.setFlag(name$1, value); } catch { console.warn(`Unknown flag: "${name$1}"`); } } 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; //#endregion //#region src/pkgInfo.ts const name = "cspell"; const version$1 = "9.1.5"; const engines = { node: ">=20" }; const npmPackage = { name, version: version$1, engines }; //#endregion //#region src/util/async.ts const asyncMap = operators.opMapAsync; const asyncFilter = operators.opFilterAsync; const asyncAwait = operators.opAwaitAsync; const asyncFlatten = operators.opFlattenAsync; //#endregion //#region src/util/constants.ts const UTF8 = "utf8"; const STDIN = "stdin"; const STDINProtocol = "stdin:"; const STDINUrlPrefix = "stdin://"; const FileUrlPrefix = "file://"; //#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 ignoreRaw = typeof options?.ignore === "string" ? [options.ignore] : options?.ignore; const ignore = ignoreRaw?.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$1 = new Intl.Collator("en").compare; const absolutePaths = (await glob$1(patterns, useOptions)).sort(compare$1); const relativePaths = absolutePaths.map((absFilename) => path$1.relative(cwd, absFilename)); return relativePaths; } function calcGlobs(commandLineExclude) { const globs = new Set((commandLineExclude || []).flatMap((glob$2) => glob$2.split(/(?<!\\)\s+/g)).map((g) => g.replaceAll("\\ ", " "))); const commandLineExcludes = { globs: [...globs], source: "arguments" }; const defaultExcludes = { globs: defaultExcludeGlobs, source: "default" }; return commandLineExcludes.globs.length ? commandLineExcludes : defaultExcludes; } function extractPatterns(globs) { const r = globs.reduce((info, g) => { const source = g.source; const patterns = g.matcher.patternsNormalizedToRoot; return [...info, ...patterns.map((glob$2) => ({ glob: glob$2, source }))]; }, []); return r; } function calcExcludeGlobInfo(root, commandLineExclude) { commandLineExclude = typeof commandLineExclude === "string" ? [commandLineExclude] : commandLineExclude; const choice = calcGlobs(commandLineExclude); const matcher = new GlobMatcher(choice.globs, { root, dot: true }); return [{ matcher, source: choice.source }]; } /** * Build GlobMatcher from command line or config file globs. * @param globs Glob patterns or file paths * @param root - directory to use as the root */ function buildGlobMatcher(globs, root, isExclude) { const withRoots = globs.map((g) => { const source = typeof g === "string" ? "command line" : void 0; return { source, ...fileOrGlobToGlob(g, root) }; }); return new GlobMatcher(withRoots, { root, mode: isExclude ? "exclude" : "include" }); } function extractGlobsFromMatcher(globMatcher) { return globMatcher.patternsNormalizedToRoot.map((g) => g.glob); } function normalizeGlobsToRoot(globs, root, isExclude) { const urls = globs.filter((g) => typeof g === "string" && isPossibleUrlRegExp.test(g)); const onlyGlobs = globs.filter((g) => typeof g !== "string" || !isPossibleUrlRegExp.test(g)); return [urls, extractGlobsFromMatcher(buildGlobMatcher(onlyGlobs, root, isExclude))].flat(); } const isPossibleGlobRegExp = /[()*?[{}]/; const isPossibleUrlRegExp = /^[\d_a-z-]{3,}:\/\//; /** * If a 'glob' is a path to a directory, then append `**` so that * directory searches work. * @param glob - a glob, file, or directory * @param root - root to use. * @returns `**` is appended directories. */ async function adjustPossibleDirectory(glob$2, root) { const g = typeof glob$2 === "string" ? { glob: glob$2, root } : { glob: glob$2.glob, root: glob$2.root ?? root }; if (isPossibleGlobRegExp.test(g.glob)) return glob$2; if (isPossibleUrlRegExp.test(g.glob)) return glob$2; const dirPath = path$1.resolve(g.root, g.glob); try { const stat$1 = await promises.stat(dirPath); if (stat$1.isDirectory()) { const useGlob = posix.join(posixPath(g.glob), "**"); return typeof glob$2 === "string" ? useGlob : { ...glob$2, glob: useGlob }; } } catch { return glob$2; } return glob$2; } function posixPath(p) { return path$1.sep === "\\" ? p.replaceAll("\\", "/") : p; } async function normalizeFileOrGlobsToRoot(globs, root) { const adjustedGlobs = await Promise.all(globs.map((g) => adjustPossibleDirectory(g, root))); return normalizeGlobsToRoot(adjustedGlobs, root, false); } function glob$1(patterns, options) { patterns = typeof patterns === "string" ? workaroundPicomatchBug(patterns) : patterns.map((g) => workaroundPicomatchBug(g)); return glob(patterns, options); } //#endregion //#region src/util/stdin.ts function readStdin() { return readline.createInterface(process.stdin); } //#endregion //#region src/util/stdinUrl.ts function isStdinUrl(url) { if (url instanceof URL) return url.protocol === STDINProtocol; return url.startsWith(STDINProtocol); } /** * Normalize and resolve a stdin url. * @param url - stdin url to resolve. * @param cwd - file path to resolve relative paths against. * @returns */ function resolveStdinUrl(url, cwd) { assert(url.startsWith(STDINProtocol), `Expected url to start with ${STDINProtocol}`); const path$2 = decodeURIComponent(url).slice(STDINProtocol.length).replace(/^\/\//, "").replace(/^\/([a-z]:)/i, "$1"); const fileUrl = toFileURL(path$2, cwd); return fileUrl.toString().replace(/^file:/, STDINProtocol) + (path$2 ? "" : "/"); } //#endregion //#region src/util/fileHelper.ts function fileInfoToDocument(fileInfo, languageId, locale) { const { filename, text } = fileInfo; languageId = languageId || void 0; locale = locale || void 0; const uri = filenameToUrl(filename); if (uri.href.startsWith(STDINProtocol)) return clean({ uri: uri.href, text, languageId, locale }); return fileToDocument(uri.href, text, languageId, locale); } function filenameToUrl(filename, cwd = ".") { if (filename instanceof URL) return filename; const cwdURL = toFileDirURL(cwd); if (filename === STDIN) return new URL("stdin:///"); if (isStdinUrl(filename)) return new URL(resolveStdinUrl(filename, cwd)); return toFileURL(filename, cwdURL); } function filenameToUri(filename, cwd) { return toURL(filenameToUrl(filename, cwd)); } function isBinaryFile$1(filename, cwd) { const uri = filenameToUri(filename, cwd); if (uri.protocol.startsWith("stdin")) return false; return isBinaryFile(uri); } function resolveFilename(filename, cwd) { cwd = cwd || process.cwd(); if (filename === STDIN) return STDINUrlPrefix; if (filename.startsWith(FileUrlPrefix)) { const url = new URL(filename.slice(FileUrlPrefix.length), toFileDirURL(cwd)); return fileURLToPath(url); } if (isStdinUrl(filename)) return resolveStdinUrl(filename, cwd); return path$1.resolve(cwd, filename); } function readFileInfo(filename, encoding = UTF8, handleNotFound = false) { filename = resolveFilename(filename); const pText = filename.startsWith(STDINProtocol) ? streamConsumers.text(process.stdin) : readFileText(filename, encoding); return pText.then((text) => ({ text, filename }), (e) => { const error = toError(e); return handleNotFound && error.code === "EISDIR" ? Promise.resolve({ text: "", filename, errorCode: error.code }) : handleNotFound && error.code === "ENOENT" ? Promise.resolve({ text: "", filename, errorCode: error.code }) : Promise.reject(new IOError(`Error reading file: "${filename}"`, error)); }); } function readFile(filename, encoding = UTF8) { return readFileInfo(filename, encoding).then((info) => info.text); } /** * Looks for matching glob patterns or stdin * @param globPatterns patterns or stdin */ async function findFiles(globPatterns, options) { const stdin = []; const globPats = globPatterns.filter((filename) => !isStdin(filename) && !filename.startsWith(FileUrlPrefix) ? true : (stdin.push(filename), false)); const globResults = globPats.length ? await globP(globPats, options) : []; const cwd = options.cwd || process.cwd(); return [...stdin, ...globResults].map((filename) => resolveFilename(filename, cwd)); } const resolveFilenames = asyncMap(resolveFilename); /** * Read * @param listFiles - array of file paths to read that will contain a list of files. Paths contained in each * file will be resolved relative to the containing file. * @returns - a list of files to be processed. */ function readFileListFiles(listFiles) { let useStdin = false; const files = listFiles.filter((file) => { const isStdin$1 = file === "stdin"; useStdin = useStdin || isStdin$1; return !isStdin$1; }); const found = asyncPipe(files, asyncMap((file) => readFileListFile(file)), asyncAwait(), asyncFlatten()); const stdin = useStdin ? readStdin() : []; return asyncPipe(mergeAsyncIterables(found, stdin), resolveFilenames); } /** * Read a `listFile` and return the containing file paths resolved relative to the `listFile`. * @param listFiles - array of file paths to read that will contain a list of files. Paths contained in each * file will be resolved relative to the containing file. * @returns - a list of files to be processed. */ async function readFileListFile(listFile) { try { const relTo = path$1.resolve(path$1.dirname(listFile)); const content = await readFile(listFile); const lines = content.split("\n").map((a) => a.trim()).filter((a) => !!a).map((file) => path$1.resolve(relTo, file)); return lines; } catch (err) { throw toApplicationError(err, `Error reading file list from: "${listFile}"`); } } function isStdin(filename) { return filename === STDIN || isStdinUrl(filename); } async function isFile(filename) { if (isStdin(filename)) return true; try { const stat$1 = await promises.stat(filename); return stat$1.isFile(); } catch { return false; } } async function isDir(filename) { try { const stat$1 = await promises.stat(filename); return stat$1.isDirectory(); } catch { return false; } } function isNotDir(filename) { return isDir(filename).then((a) => !a); } function relativeToCwd(filename, cwd = process.cwd()) { const urlCwd = toFileDirURL(cwd); const url = toFileURL(filename, urlCwd); const rel = urlRelative(urlCwd, url); if (rel.startsWith("..")) return toFilePathOrHref(url); return rel; } //#endregion //#region src/util/cache/file-entry-cache.mts function createFromFile$1(pathToCache, useChecksum) { return fileEntryCache.createFromFile(pathToCache, useChecksum); } //#endregion //#region src/util/cache/fileEntryCache.ts function createFromFile(pathToCache, useCheckSum, useRelative) { const absPathToCache = path$1.resolve(pathToCache); const relDir = path$1.dirname(absPathToCache); mkdirSync(relDir, { recursive: true }); const create = wrap(() => createFromFile$1(absPathToCache, useCheckSum)); const feCache = create(); const cacheWrapper = { get cache() { return feCache.cache; }, getHash(buffer) { return feCache.getHash(buffer); }, hasFileChanged: wrap((cwd, file) => { return feCache.hasFileChanged(resolveFile(cwd, file)); }), analyzeFiles: wrap((cwd, files) => { return feCache.analyzeFiles(resolveFiles(cwd, files)); }), getFileDescriptor: wrap((cwd, file) => { return feCache.getFileDescriptor(resolveFile(cwd, file)); }), getUpdatedFiles: wrap((cwd, files) => { return feCache.getUpdatedFiles(resolveFiles(cwd, files)); }), normalizeEntries: wrap((cwd, files) => { return feCache.normalizeEntries(resolveFiles(cwd, files)); }), removeEntry: wrap((cwd, file) => { return feCache.removeEntry(resolveFile(cwd, file)); }), deleteCacheFile() { feCache.deleteCacheFile(); }, destroy() { feCache.destroy(); }, reconcile: wrap((_cwd, noPrune) => { feCache.reconcile(noPrune); }) }; return cacheWrapper; function resolveFile(cwd, file) { if (!useRelative) return normalizePath(file); const r = path$1.relative(relDir, path$1.resolve(cwd, file)); return normalizePath(r); } function resolveFiles(cwd, files) { return files?.map((file) => resolveFile(cwd, file)); } function wrap(fn) { return (...params) => { const cwd = process.cwd(); try { isMainThread && process.chdir(relDir); return fn(cwd, ...params); } finally { isMainThread && process.chdir(cwd); } }; } } function normalizePath(filePath) { if (path$1.sep === "/") return filePath; return filePath.split(path$1.sep).join("/"); } //#endregion //#region src/util/cache/ObjectCollection.ts const compare = Intl.Collator().compare; var ShallowObjectCollection = class { tree = {}; get(v) { if (typeof v !== "object" || v === null) return v; const keys = Object.entries(v).filter((entry) => entry[1] !== void 0).sort((a, b) => compare(a[0], b[0])); let t = this.tree; for (const [key, obj] of keys) { if (!t.c) t.c = /* @__PURE__ */ new Map(); const c0 = t.c.get(key); const cc = c0 || /* @__PURE__ */ new Map(); if (!c0) t.c.set(key, cc); const c1 = cc.get(obj); const ccc = c1 || {}; if (!c1) cc.set(obj, ccc); t = ccc; } if (t.v) return t.v; t.v = v; return v; } }; //#endregion //#region src/util/cache/DiskCache.ts const cacheDataKeys = { v: "v", r: "r", d: "d" }; /** * Meta Data Version is used to detect if the structure of the meta data has changed. * This is used in combination with the Suffix and the version of CSpell. */ const META_DATA_BASE_VERSION = "1"; const META_DATA_VERSION_SUFFIX = "-" + META_DATA_BASE_VERSION + "-" + Object.keys(cacheDataKeys).join("|"); /** * Caches cspell results on disk */ var DiskCache = class { cacheFileLocation; cacheDir; fileEntryCache; dependencyCache = /* @__PURE__ */ new Map(); dependencyCacheTree = {}; objectCollection = new ShallowObjectCollection(); ocCacheFileResult = new ShallowObjectCollection(); version; constructor(cacheFileLocation, useCheckSum, cspellVersion, useUniversalCache) { this.useCheckSum = useCheckSum; this.cspellVersion = cspellVersion; this.useUniversalCache = useUniversalCache; this.cacheFileLocation = resolve(cacheFileLocation); this.cacheDir = dirname(this.cacheFileLocation); this.fileEntryCache = createFromFile(this.cacheFileLocation, useCheckSum, useUniversalCache); this.version = calcVersion(cspellVersion); } async getCachedLintResults(filename) { filename = normalizePath(filename); const fileDescriptor = this.fileEntryCache.getFileDescriptor(filename); const meta = fileDescriptor.meta; const data = meta?.data; const result = data?.r; const versionMatches = this.version === data?.v; if (fileDescriptor.notFound || fileDescriptor.changed || !meta || !result || !versionMatches || !this.checkDependencies(data.d)) return void 0; const dd = { ...data }; if (dd.d) dd.d = setTreeEntry(this.dependencyCacheTree, dd.d); dd.r = dd.r && this.normalizeResult(dd.r); meta.data = this.objectCollection.get(dd); const hasErrors = !!result && (result.errors > 0 || result.configErrors > 0 || result.issues.length > 0); const cached = true; const shouldReadFile = hasErrors; return { ...result, elapsedTimeMs: void 0, fileInfo: shouldReadFile ? await readFileInfo(filename) : { filename }, cached }; } setCachedLintResults({ fileInfo, elapsedTimeMs: _, cached: __,...result }, dependsUponFiles) { const fileDescriptor = this.fileEntryCache.getFileDescriptor(fileInfo.filename); const meta = fileDescriptor.meta; if (fileDescriptor.notFound || !meta) return; const data = this.objectCollection.get({ v: this.version, r: this.normalizeResult(result), d: this.calcDependencyHashes(dependsUponFiles) }); meta.data = data; } reconcile() { this.fileEntryCache.reconcile(); } reset() { this.fileEntryCache.destroy(); this.dependencyCache.clear(); this.dependencyCacheTree = {}; this.objectCollection = new ShallowObjectCollection(); this.ocCacheFileResult = new ShallowObjectCollection(); } normalizeResult(result) { const { issues, processed, errors, configErrors, reportIssueOptions,...rest } = result; if (!Object.keys(rest).length) return this.ocCacheFileResult.get(result); return this.ocCacheFileResult.get({ issues, processed, errors, configErrors, reportIssueOptions }); } calcDependencyHashes(dependsUponFiles) { dependsUponFiles.sort(); const c = getTreeEntry(this.dependencyCacheTree, dependsUponFiles); if (c?.d) return c.d; const dependencies = dependsUponFiles.map((f) => this.getDependency(f)); return setTreeEntry(this.dependencyCacheTree, dependencies); } checkDependency(dep) { const depFile = this.resolveFile(dep.f); const cDep = this.dependencyCache.get(depFile); if (cDep && compDep(dep, cDep)) return true; if (cDep) return false; const d = this.getFileDep(depFile); if (compDep(dep, d)) { this.dependencyCache.set(depFile, dep); return true; } this.dependencyCache.set(depFile, d); return false; } getDependency(file) { const dep = this.dependencyCache.get(file); if (dep) return dep; const d = this.getFileDep(file); this.dependencyCache.set(file, d); return d; } getFileDep(file) { assert(isAbsolute(file), `Dependency must be absolute "${file}"`); const f = this.toRelFile(file); let h; try { const buffer = fs$1.readFileSync(file); h = this.getHash(buffer); } catch { return { f }; } return { f, h }; } checkDependencies(dependencies) { if (!dependencies) return false; for (const dep of dependencies) if (!this.checkDependency(dep)) return false; return true; } getHash(buffer) { return crypto.createHash("md5").update(buffer).digest("hex"); } resolveFile(file) { return normalizePath(resolve(this.cacheDir, file)); } toRelFile(file) { return normalizePath(this.useUniversalCache ? relative(this.cacheDir, file) : file); } }; function getTreeEntry(tree, keys) { let r = tree; for (const k of keys) { r = r.c?.get(k); if (!r) return r; } return r; } function setTreeEntry(tree, deps, update = false) { let r = tree; for (const d$1 of deps) { const k = d$1.f; if (!r.c) r.c = /* @__PURE__ */ new Map(); const cn = r.c.get(k); const n = cn ?? {}; if (!cn) r.c.set(k, n); r = n; } let d = r.d; if (!d || r.d && update) { r.d = deps; d = deps; } return d; } function compDep(a, b) { return a.f === b.f && a.h === b.h; } function calcVersion(version$2) { return version$2 + META_DATA_VERSION_SUFFIX; } //#endregion //#region src/util/cache/DummyCache.ts /** * Dummy cache implementation that should be usd if caching option is disabled. */ var DummyCache = class { getCachedLintResults() { return Promise.resolve(void 0); } setCachedLintResults() { return; } reconcile() { return; } reset() { return; } }; //#endregion //#region src/util/cache/createCache.ts const DEFAULT_CACHE_LOCATION = ".cspellcache"; const versionSuffix = ""; /** * Creates CSpellLintResultCache (disk cache if caching is enabled in config or dummy otherwise) */ function createCache(options) { const { useCache, cacheLocation, cacheStrategy, reset } = options; const location = path.resolve(cacheLocation); const useChecksum = cacheStrategy === "content"; const version$2 = normalizeVersion(options.version); const useUniversal = options.cacheFormat === "universal"; const cache = useCache ? new DiskCache(location, useChecksum, version$2, useUniversal) : new DummyCache(); reset && cache.reset(); return cache; } async function calcCacheSettings(config, cacheOptions, root) { const cs = config.cache ?? {}; const useCache = cacheOptions.cache ?? cs.useCache ?? false; const cacheLocation = await resolveCacheLocation(path.resolve(root, cacheOptions.cacheLocation ?? cs.cacheLocation ?? DEFAULT_CACHE_LOCATION)); const cacheStrategy = cacheOptions.cacheStrategy ?? cs.cacheStrategy ?? "content"; const cacheFormat = cacheOptions.cacheFormat ?? cs.cacheFormat ?? "universal"; const optionals = {}; if (cacheOptions.cacheReset) optionals.reset = true; return { ...optionals, useCache, cacheLocation, cacheStrategy, version: cacheOptions.version, cacheFormat }; } async function resolveCacheLocation(cacheLocation) { try { const s = await stat(cacheLocation); if (s.isFile()) return cacheLocation; return path.join(cacheLocation, DEFAULT_CACHE_LOCATION); } catch (err) { if (isErrorLike(err) && err.code === "ENOENT") return cacheLocation; throw err; } } /** * Normalizes the version and return only `major.minor + versionSuffix` * @param version The cspell semantic version. */ function normalizeVersion(version$2) { const parts = version$2.split(".").slice(0, 2); assert(parts.length === 2); return parts.join(".") + versionSuffix; } //#endregion //#region src/util/configFileHelper.ts async function readConfig(configFile, root, stopConfigSearchAt) { configFile ??= getEnvironmentVariable(environmentKeys.CSPELL_CONFIG_PATH); if (configFile) { const cfgFile = typeof configFile === "string" ? await readConfigHandleError(configFile) : configFile; return configFileToConfigInfo(cfgFile); } const config = await cspell.searchForConfig(root, { stopSearchAt: stopConfigSearchAt }); const defaultConfigFile = getEnvironmentVariable(environmentKeys.CSPELL_DEFAULT_CONFIG_PATH); if (!config && defaultConfig