You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
392 lines
16 KiB
392 lines
16 KiB
"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
|