"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports._testing_ = exports.createInit = exports.checkText = exports.trace = exports.lint = exports.CSpellApplicationConfiguration = exports.MessageTypes = exports.IncludeExcludeFlag = void 0; const glob = require("glob"); const cspell = require("cspell-lib"); const fsp = require("fs-extra"); const path = require("path"); const commentJson = require("comment-json"); const util = require("./util/util"); const cspell_lib_1 = require("cspell-lib"); const Validator = require("cspell-lib"); const getStdin = require("get-stdin"); var cspell_lib_2 = require("cspell-lib"); Object.defineProperty(exports, "IncludeExcludeFlag", { enumerable: true, get: function () { return cspell_lib_2.IncludeExcludeFlag; } }); const cspell_glob_1 = require("cspell-glob"); // cspell:word nocase const UTF8 = 'utf8'; const STDIN = 'stdin'; exports.MessageTypes = { Debug: 'Debug', Info: 'Info', Progress: 'Progress', }; const defaultMinimatchOptions = { nocase: true }; const defaultConfigGlob = '{cspell.json,.cspell.json}'; const defaultConfigGlobOptions = defaultMinimatchOptions; const nullEmitter = () => { /* empty */ }; class CSpellApplicationConfiguration { constructor(files, options, emitters) { this.files = files; this.options = options; this.emitters = emitters; this.configGlob = defaultConfigGlob; this.configGlobOptions = defaultConfigGlobOptions; this.root = path.resolve(options.root || process.cwd()); this.info = emitters.info || nullEmitter; this.debug = emitters.debug || ((msg) => this.info(msg, exports.MessageTypes.Debug)); this.configFile = options.config; this.excludes = calcExcludeGlobInfo(this.root, options.exclude); this.logIssue = emitters.issue || nullEmitter; this.local = options.local || ''; this.uniqueFilter = options.unique ? util.uniqueFilterFnGenerator((issue) => issue.text) : () => true; } } exports.CSpellApplicationConfiguration = CSpellApplicationConfiguration; function lint(files, options, emitters) { const cfg = new CSpellApplicationConfiguration(files, options, emitters); return runLint(cfg); } exports.lint = lint; function runLint(cfg) { const configErrors = new Set(); return run(); async function processFile(fileInfo, configInfo) { const settingsFromCommandLine = util.clean({ languageId: cfg.options.languageId || undefined, language: cfg.local || undefined, }); const result = { fileInfo, issues: [], processed: false, errors: 0, configErrors: 0, elapsedTimeMs: 0, }; const { filename, text } = fileInfo; const info = calcFinalConfigInfo(configInfo, settingsFromCommandLine, filename, text); const config = info.configInfo.config; const source = info.configInfo.source; cfg.debug(`Filename: ${filename}, Extension: ${path.extname(filename)}, LanguageIds: ${info.languageIds.toString()}`); if (!info.configInfo.config.enabled) return result; result.configErrors += reportConfigurationErrors(info.configInfo.config); const debugCfg = { config: { ...config, source: null }, source }; cfg.debug(commentJson.stringify(debugCfg, undefined, 2)); const startTime = Date.now(); try { const wordOffsets = await cspell.validateText(text, info.configInfo.config); result.processed = true; result.issues = cspell.Text.calculateTextDocumentOffsets(filename, text, wordOffsets); } catch (e) { cfg.emitters.error(`Failed to process "${filename}"`, e); result.errors += 1; } result.elapsedTimeMs = Date.now() - startTime; const elapsed = result.elapsedTimeMs / 1000.0; const dictionaries = config.dictionaries || []; cfg.info(`Checking: ${filename}, File type: ${config.languageId}, Language: ${config.language} ... Issues: ${result.issues.length} ${elapsed}S`, exports.MessageTypes.Info); cfg.info(`Dictionaries Used: ${dictionaries.join(', ')}`, exports.MessageTypes.Info); result.issues.filter(cfg.uniqueFilter).forEach((issue) => cfg.logIssue(issue)); return result; } /** * The file loader is written this way to cause files to be loaded in parallel while the previous one is being processed. * @param fileNames names of files to load one at a time. */ function* fileLoader(fileNames) { for (const filename of fileNames) { // console.log(`${Date.now()} Start reading ${filename}`); const file = readFileInfo(filename); // .then(f => (console.log(`${Date.now()} Loaded ${filename} (${f.text.length / 1024}K)`), f)) // console.log(`${Date.now()} Waiting for request ${filename}`); yield file; // console.log(`${Date.now()} Yield ${filename}`); } } async function processFiles(files, configInfo) { const status = runResult(); for (const fileP of files) { const file = await fileP; if (!file || !file.text) { continue; } const r = await processFile(file, configInfo); status.files += 1; if (r.issues.length || r.errors) { status.filesWithIssues.add(file.filename); status.issues += r.issues.length; status.errors += r.errors; } status.errors += r.configErrors; } return status; } function reportConfigurationErrors(config) { const errors = cspell.extractImportErrors(config); let count = 0; errors.forEach((ref) => { const key = ref.error.toString(); if (configErrors.has(key)) return; configErrors.add(key); count += 1; cfg.emitters.error('Configuration', ref.error); }); return count; } async function readConfig() { if (cfg.configFile) { const config = cspell.readSettings(cfg.configFile); return { source: cfg.configFile, config }; } const configFiles = (await globP(cfg.configGlob, cfg.configGlobOptions)).filter(util.uniqueFn()); cfg.info(`Config Files Found:\n ${configFiles.join('\n ')}\n`, exports.MessageTypes.Info); const config = cspell.readSettingsFiles(configFiles); return { source: configFiles.join(' || '), config }; } function countConfigErrors(configInfo) { return reportConfigurationErrors(configInfo.config); } async function run() { header(); const configInfo = await readConfig(); const configErrors = countConfigErrors(configInfo); if (configErrors) return runResult({ errors: configErrors }); // Get Exclusions from the config files. const { root } = cfg; const globOptions = { root, cwd: root }; const exclusionGlobs = extractGlobExcludesFromConfig(root, configInfo.source, configInfo.config).concat(cfg.excludes); const files = filterFiles(await findFiles(cfg.files, globOptions), exclusionGlobs); return processFiles(fileLoader(files), configInfo); } function header() { cfg.info(` cspell; Date: ${new Date().toUTCString()} Options: verbose: ${yesNo(!!cfg.options.verbose)} config: ${cfg.configGlob} exclude: ${extractPatterns(cfg.excludes) .map((a) => a.glob) .join('\n ')} files: ${cfg.files} wordsOnly: ${yesNo(!!cfg.options.wordsOnly)} unique: ${yesNo(!!cfg.options.unique)} `, exports.MessageTypes.Info); } function isExcluded(filename, globs) { const { root } = cfg; const absFilename = path.resolve(root, filename); for (const glob of globs) { const m = glob.matcher.matchEx(absFilename); if (m.matched) { cfg.info(`Excluded File: ${path.relative(root, absFilename)}; Excluded by ${m.glob} from ${glob.source}`, exports.MessageTypes.Info); return true; } } return false; } function filterFiles(files, excludeGlobs) { const excludeInfo = extractPatterns(excludeGlobs).map((g) => `Glob: ${g.glob} from ${g.source}`); cfg.info(`Exclusion Globs: \n ${excludeInfo.join('\n ')}\n`, exports.MessageTypes.Info); const result = files.filter((filename) => !isExcluded(filename, excludeGlobs)); return result; } } function runResult(init = {}) { const { files = 0, filesWithIssues = new Set(), issues = 0, errors = 0 } = init; return { files, filesWithIssues, issues, errors }; } function extractPatterns(globs) { const r = globs.reduce((info, g) => { const source = g.source; const patterns = typeof g.matcher.patterns === 'string' ? [g.matcher.patterns] : g.matcher.patterns; return info.concat(patterns.map((glob) => ({ glob, source }))); }, []); return r; } async function trace(words, options) { const configGlob = options.config || defaultConfigGlob; const configGlobOptions = options.config ? {} : defaultConfigGlobOptions; const configFiles = (await globP(configGlob, configGlobOptions)).filter(util.uniqueFn()); const config = cspell.mergeSettings(cspell.getDefaultSettings(), cspell.getGlobalSettings(), cspell.readSettingsFiles(configFiles)); const results = await cspell_lib_1.traceWords(words, config); return results; } exports.trace = trace; async function checkText(filename, options) { const configGlob = options.config || defaultConfigGlob; const configGlobOptions = options.config ? {} : defaultConfigGlobOptions; const pSettings = globP(configGlob, configGlobOptions).then((filenames) => ({ source: filenames[0], config: cspell.readSettingsFiles(filenames), })); const [foundSettings, text] = await Promise.all([pSettings, readFile(filename)]); const settingsFromCommandLine = util.clean({ languageId: options.languageId || undefined, local: options.local || undefined, }); const info = calcFinalConfigInfo(foundSettings, settingsFromCommandLine, filename, text); return Validator.checkText(text, info.configInfo.config); } exports.checkText = checkText; function createInit(_) { return Promise.reject(); } exports.createInit = createInit; const defaultExcludeGlobs = ['node_modules/**']; function readFileInfo(filename, encoding = UTF8) { const pText = filename === STDIN ? getStdin() : fsp.readFile(filename, encoding); return pText.then((text) => ({ text, filename }), (error) => { return error.code === 'EISDIR' ? Promise.resolve({ text: '', filename }) : Promise.reject({ ...error, message: `Error reading file: "${filename}"` }); }); } 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 globPats = globPatterns.filter((filename) => filename !== STDIN); const stdin = globPats.length < globPatterns.length ? [STDIN] : []; const globResults = globPats.length ? await globP(globPats, options) : []; const cwd = options.cwd || process.cwd(); return stdin.concat(globResults.map((filename) => path.resolve(cwd, filename))); } function calcExcludeGlobInfo(root, commandLineExclude) { const commandLineExcludes = { globs: commandLineExclude ? commandLineExclude.split(/\s+/g) : [], source: 'arguments', }; const defaultExcludes = { globs: defaultExcludeGlobs, source: 'default', }; const choice = commandLineExcludes.globs.length ? commandLineExcludes : defaultExcludes; const matcher = new cspell_glob_1.GlobMatcher(choice.globs, root); return [ { matcher, source: choice.source, }, ]; } function extractGlobExcludesFromConfig(root, source, config) { if (!config.ignorePaths || !config.ignorePaths.length) { return []; } const matcher = new cspell_glob_1.GlobMatcher(config.ignorePaths, root); return [{ source, matcher }]; } function calcFinalConfigInfo(configInfo, settingsFromCommandLine, filename, text) { const ext = path.extname(filename); const fileSettings = cspell.calcOverrideSettings(configInfo.config, path.resolve(filename)); const settings = cspell.mergeSettings(cspell.getDefaultSettings(), cspell.getGlobalSettings(), fileSettings, settingsFromCommandLine); const languageIds = settings.languageId ? [settings.languageId] : cspell.getLanguagesForExt(ext); const config = cspell.constructSettingsForText(settings, text, languageIds); return { configInfo: { ...configInfo, config }, filename, text, languageIds }; } function yesNo(value) { return value ? 'Yes' : 'No'; } function findBaseDir(pat) { const globChars = /[*@()?|[\]{},]/; while (globChars.test(pat)) { pat = path.dirname(pat); } return pat; } function exists(filename) { try { fsp.accessSync(filename); } catch (e) { return false; } return true; } /** * Attempt to normalize a pattern based upon the root. * If the pattern is absolute, check to see if it exists and adjust the root, otherwise it is assumed to be based upon the current root. * If the pattern starts with a relative path, adjust the root to match. * The challenge is with the patterns that begin with `/`. Is is an absolute path or relative pattern? * @param pat glob pattern * @param root absolute path | empty * @returns the adjusted root and pattern. */ function normalizePattern(pat, root) { // Absolute pattern if (path.isAbsolute(pat)) { const dir = findBaseDir(pat); if (dir.length > 1 && exists(dir)) { // Assume it is an absolute path return { pattern: pat, root: path.sep, }; } } // normal pattern if (!/^\.\./.test(pat)) { return { pattern: pat, root, }; } // relative pattern pat = path.sep === '\\' ? pat.replace(/\\/g, '/') : pat; const patParts = pat.split('/'); const rootParts = root.split(path.sep); let i = 0; for (; i < patParts.length && patParts[i] === '..'; ++i) { rootParts.pop(); } return { pattern: patParts.slice(i).join('/'), root: rootParts.join(path.sep), }; } async function globP(pattern, options) { const root = (options === null || options === void 0 ? void 0 : options.root) || process.cwd(); const opts = options || {}; const rawPatterns = typeof pattern === 'string' ? [pattern] : pattern; const normPatterns = rawPatterns.map((pat) => normalizePattern(pat, root)); const globResults = normPatterns.map(async (pat) => { const opt = { ...opts, root: pat.root, cwd: pat.root }; const absolutePaths = (await _globP(pat.pattern, opt)).map((filename) => path.resolve(pat.root, filename)); const relativeToRoot = absolutePaths.map((absFilename) => path.relative(root, absFilename)); return relativeToRoot; }); const results = (await Promise.all(globResults)).reduce((prev, next) => prev.concat(next), []); return results; } function _globP(pattern, options) { if (!pattern) { return Promise.resolve([]); } return new Promise((resolve, reject) => { const cb = (err, matches) => { if (err) { reject(err); } resolve(matches); }; glob(pattern, options, cb); }); } exports._testing_ = { _globP, findFiles, normalizePattern, }; //# sourceMappingURL=application.js.map