From 99abcc2a6a50dc403e60bb4ee1035b3cdb9485af Mon Sep 17 00:00:00 2001 From: Jason Dent Date: Sun, 26 Dec 2021 13:29:55 +0100 Subject: [PATCH] feat: Support `--file-list` cli option Related to #1850 --- cspell.code-workspace | 7 ++++ .../cspell/src/__snapshots__/app.test.ts.snap | 12 +++--- packages/cspell/src/app.ts | 2 +- packages/cspell/src/application.ts | 4 +- packages/cspell/src/cli-reporter.ts | 27 +++++++++++-- packages/cspell/src/commandLint.ts | 40 ++++++++----------- packages/cspell/src/lint/LintRequest.ts | 4 +- packages/cspell/src/lint/lint.ts | 5 ++- packages/cspell/src/options.ts | 23 +++++++++++ packages/cspell/src/util/glob.ts | 2 +- 10 files changed, 87 insertions(+), 39 deletions(-) diff --git a/cspell.code-workspace b/cspell.code-workspace index 9e713528d091..526ad426bb58 100644 --- a/cspell.code-workspace +++ b/cspell.code-workspace @@ -93,5 +93,12 @@ "cSpell.customDictionaries": { "workspace": true } + }, + "extensions": { + "recommendations": [ + "streetsidesoftware.code-spell-checker", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] } } diff --git a/packages/cspell/src/__snapshots__/app.test.ts.snap b/packages/cspell/src/__snapshots__/app.test.ts.snap index a53492523f35..06328b727090 100644 --- a/packages/cspell/src/__snapshots__/app.test.ts.snap +++ b/packages/cspell/src/__snapshots__/app.test.ts.snap @@ -11,7 +11,7 @@ Array [ " -h, --help display help for command", "", "Commands:", - " lint [options] [files...] Check spelling", + " lint [options] [globs...] Check spelling", " trace [options] Trace words", " Search for words in the configuration and dictionaries.", " check [options] Spell check file(s) and display the result. The", @@ -289,7 +289,7 @@ exports[`Validate cli app must find with error Expect Error: [Function CheckFail exports[`Validate cli app no-args Expect Error: outputHelp 1`] = ` Array [ - "Usage: cspell lint [options] [files...]", + "Usage: cspell lint [options] [globs...]", "", "Check spelling", "", @@ -306,11 +306,12 @@ Array [ " dictionaries.", " -u, --unique Only output the first instance of a word not", " found in the dictionaries.", - " --debug Output information useful for debugging", - " cspell.json files.", " -e, --exclude Exclude files matching the glob pattern. This", " option can be used multiple times to add", " multiple globs.", + " --file-list Specify a list of files to be spell checked. The", + " list is filtered against the glob file patterns.", + " Note: the format is 1 file path per line.", " --no-issues Do not show the spelling errors.", " --no-progress Turn off progress messages", " --no-summary Turn off summary message in console", @@ -320,7 +321,6 @@ Array [ " --show-context Show the surrounding text around an issue.", " --show-suggestions Show spelling suggestions.", " --no-must-find-files Do not error if no files are found", - " --legacy Legacy output", " --cache Only check changed files (default: false)", " --cache-strategy Strategy to use for detecting changed files", " (choices: \\"metadata\\", \\"content\\")", @@ -335,6 +335,8 @@ Array [ " root.", " --no-color Turn off color.", " --color Force color", + " --debug Output information useful for debugging", + " cspell.json files.", " -h, --help display help for command", "", "", diff --git a/packages/cspell/src/app.ts b/packages/cspell/src/app.ts index 3619b78e2cde..53874c2b218e 100644 --- a/packages/cspell/src/app.ts +++ b/packages/cspell/src/app.ts @@ -9,7 +9,7 @@ import { ApplicationError } from './util/errors'; // eslint-disable-next-line @typescript-eslint/no-var-requires const npmPackage = require(path.join(__dirname, '..', 'package.json')); -export { LinterCliOptions as Options } from './commandLint'; +export { LinterCliOptions as Options } from './options'; export { CheckFailed } from './util/errors'; export async function run(program?: commander.Command, argv?: string[]): Promise { diff --git a/packages/cspell/src/application.ts b/packages/cspell/src/application.ts index b23a9169053c..d013b0edaec3 100644 --- a/packages/cspell/src/application.ts +++ b/packages/cspell/src/application.ts @@ -11,8 +11,8 @@ export type { TraceResult } from 'cspell-lib'; export type AppError = NodeJS.ErrnoException; -export function lint(files: string[], options: LinterOptions, emitters: CSpellReporter): Promise { - const cfg = new LintRequest(files, options, emitters); +export function lint(fileGlobs: string[], options: LinterOptions, emitters: CSpellReporter): Promise { + const cfg = new LintRequest(fileGlobs, options, emitters); return runLint(cfg); } diff --git a/packages/cspell/src/cli-reporter.ts b/packages/cspell/src/cli-reporter.ts index 843163889d1f..92acd790bac6 100644 --- a/packages/cspell/src/cli-reporter.ts +++ b/packages/cspell/src/cli-reporter.ts @@ -2,8 +2,8 @@ import chalk = require('chalk'); import type { CSpellReporter, Issue, MessageType, ProgressItem, RunResult } from '@cspell/cspell-types'; import { ImportError, isSpellingDictionaryLoadError, SpellingDictionaryLoadError } from 'cspell-lib'; import * as path from 'path'; -import { Options } from './app'; import { URI } from 'vscode-uri'; +import { LinterCliOptions } from './options'; const templateIssue = `{green $filename}:{yellow $row:$col} - $message ({red $text})`; const templateIssueWithSuggestions = `{green $filename}:{yellow $row:$col} - $message ({red $text}) Suggestions: {yellow [$suggestions]}`; @@ -79,7 +79,26 @@ function reportTime(elapsedTimeMs: number | undefined, cached: boolean): string return color(elapsedTimeMs.toFixed(2) + 'ms'); } -export function getReporter(options: Options): CSpellReporter { +export interface ReporterOptions + extends Pick< + LinterCliOptions, + | 'debug' + | 'issues' + | 'legacy' + | 'progress' + | 'relative' + | 'root' + | 'showContext' + | 'showSuggestions' + | 'silent' + | 'summary' + | 'verbose' + | 'wordsOnly' + > { + fileGlobs: string[]; +} + +export function getReporter(options: ReporterOptions): CSpellReporter { const issueTemplate = options.wordsOnly ? templateIssueWordsOnly : options.legacy @@ -91,7 +110,7 @@ export function getReporter(options: Options): CSpellReporter { : options.showSuggestions ? templateIssueWithSuggestions : templateIssue; - const { files, silent, summary, issues, progress, verbose, debug } = options; + const { fileGlobs, silent, summary, issues, progress, verbose, debug } = options; const emitters: InfoEmitter = { Debug: !silent && debug ? (s) => console.info(chalk.cyan(s)) : nullEmitter, @@ -117,7 +136,7 @@ export function getReporter(options: Options): CSpellReporter { } const resultEmitter = (result: RunResult) => { - if (!files.length && !result.files) { + if (!fileGlobs.length && !result.files) { return; } console.error( diff --git a/packages/cspell/src/commandLint.ts b/packages/cspell/src/commandLint.ts index e765049f64f5..3bbec88f963f 100644 --- a/packages/cspell/src/commandLint.ts +++ b/packages/cspell/src/commandLint.ts @@ -1,23 +1,10 @@ import { Command, Option as CommanderOption } from 'commander'; import * as App from './application'; import { getReporter } from './cli-reporter'; -import { LinterOptions } from './options'; +import { LinterCliOptions, LinterOptions } from './options'; import { DEFAULT_CACHE_LOCATION } from './util/cache'; import { CheckFailed } from './util/errors'; -export interface LinterCliOptions extends LinterOptions { - files: string[]; - legacy?: boolean; - summary: boolean; - issues: boolean; - silent: boolean; - mustFindFiles: boolean; - progress?: boolean; - /** - * issues are shown with a relative path to the root or `cwd` - */ - relative?: boolean; -} // interface InitOptions extends Options {} const usage = ` @@ -63,12 +50,18 @@ export function commandLint(prog: Command): Command { new CommanderOption('--wordsOnly', 'Only output the words not found in the dictionaries.').hideHelp() ) .option('-u, --unique', 'Only output the first instance of a word not found in the dictionaries.') - .option('--debug', 'Output information useful for debugging cspell.json files.') .option( '-e, --exclude ', 'Exclude files matching the glob pattern. This option can be used multiple times to add multiple globs. ', collect ) + .option( + '--file-list ', + 'Specify a list of files to be spell checked.' + + ' The list is filtered against the glob file patterns.' + + ' Note: the format is 1 file path per line.', + collect + ) .option('--no-issues', 'Do not show the spelling errors.') .option('--no-progress', 'Turn off progress messages') .option('--no-summary', 'Turn off summary message in console') @@ -82,7 +75,7 @@ export function commandLint(prog: Command): Command { // The following options are planned features // .option('-w, --watch', 'Watch for any changes to the matching files and report any errors') // .option('--force', 'Force the exit value to always be 0') - .option('--legacy', 'Legacy output') + .addOption(new CommanderOption('--legacy', 'Legacy output').hideHelp()) .addOption(new CommanderOption('--local ', 'Deprecated -- Use: --locale').hideHelp()) .option('--cache', 'Only check changed files', false) .addOption( @@ -98,14 +91,15 @@ export function commandLint(prog: Command): Command { .option('--gitignore-root ', 'Prevent searching for .gitignore files past root.', collect) .option('--no-color', 'Turn off color.') .option('--color', 'Force color') + .option('--debug', 'Output information useful for debugging cspell.json files.') .addHelpText('after', usage) - .arguments('[files...]') - .action((files: string[], options: LinterCliOptions) => { - options.files = files; - const { mustFindFiles } = options; - const cliReporter = getReporter(options); - return App.lint(files, options, cliReporter).then((result) => { - if (!files.length && !result.files) { + .arguments('[globs...]') + .action((fileGlobs: string[], options: LinterCliOptions) => { + const { mustFindFiles, fileList } = options; + const cliReporter = getReporter({ ...options, fileGlobs }); + const lintOptions: LinterOptions = { ...options, fileLists: fileList }; + return App.lint(fileGlobs, lintOptions, cliReporter).then((result) => { + if (!fileGlobs.length && !result.files) { spellCheckCommand.outputHelp(); throw new CheckFailed('outputHelp', 1); } diff --git a/packages/cspell/src/lint/LintRequest.ts b/packages/cspell/src/lint/LintRequest.ts index 86da4e4054e7..014aa2effae2 100644 --- a/packages/cspell/src/lint/LintRequest.ts +++ b/packages/cspell/src/lint/LintRequest.ts @@ -15,8 +15,9 @@ export class LintRequest { readonly root: string; readonly showContext: number; readonly enableGlobDot: boolean | undefined; + readonly fileLists: string[]; - constructor(readonly files: string[], readonly options: LinterOptions, readonly reporter: CSpellReporter) { + constructor(readonly fileGlobs: string[], readonly options: LinterOptions, readonly reporter: CSpellReporter) { this.root = path.resolve(options.root || process.cwd()); this.configFile = options.config; this.excludes = calcExcludeGlobInfo(this.root, options.exclude); @@ -25,5 +26,6 @@ export class LintRequest { this.uniqueFilter = options.unique ? util.uniqueFilterFnGenerator((issue: Issue) => issue.text) : () => true; this.showContext = options.showContext === true ? defaultContextRange : options.showContext ? options.showContext : 0; + this.fileLists = options.fileLists || []; } } diff --git a/packages/cspell/src/lint/lint.ts b/packages/cspell/src/lint/lint.ts index 454d37b5d540..a917624b7372 100644 --- a/packages/cspell/src/lint/lint.ts +++ b/packages/cspell/src/lint/lint.ts @@ -22,6 +22,7 @@ import { LintRequest } from './LintRequest'; export async function runLint(cfg: LintRequest): Promise { let { reporter } = cfg; + const { fileLists } = cfg; cspell.setLogger(getLoggerFromReporter(reporter)); const configErrors = new Set(); @@ -189,7 +190,7 @@ export async function runLint(cfg: LintRequest): Promise { const gitignoreRoots = cfg.options.gitignoreRoot ?? configInfo.config.gitignoreRoot; const gitIgnore = useGitignore ? await generateGitIgnore(gitignoreRoots) : undefined; - const cliGlobs: Glob[] = cfg.files; + const cliGlobs: Glob[] = cfg.fileGlobs; const allGlobs: Glob[] = cliGlobs.length ? cliGlobs : configInfo.config.files || []; const combinedGlobs = normalizeGlobsToRoot(allGlobs, cfg.root, false); const cliExcludeGlobs = extractPatterns(cfg.excludes).map((p) => p.glob); @@ -197,7 +198,7 @@ export async function runLint(cfg: LintRequest): Promise { const includeGlobs = combinedGlobs.filter((g) => !g.startsWith('!')); const excludeGlobs = combinedGlobs.filter((g) => g.startsWith('!')).concat(normalizedExcludes); const fileGlobs: string[] = includeGlobs; - if (!fileGlobs.length) { + if (!fileGlobs.length && !fileLists.length) { // Nothing to do. return runResult(); } diff --git a/packages/cspell/src/options.ts b/packages/cspell/src/options.ts index d5160f817014..bd1bfb8db19f 100644 --- a/packages/cspell/src/options.ts +++ b/packages/cspell/src/options.ts @@ -51,6 +51,12 @@ export interface LinterOptions extends BaseOptions, CacheOptions { * Stop searching for a `.gitignore`s when a root is reached. */ gitignoreRoot?: string | string[]; + /** + * List of files that contains the paths to files to be spell checked. + * The files in the lists will be filtered against the glob patterns. + * - an entry of `stdin` means to read the file list from **`stdin`** + */ + fileLists?: string[] | undefined; } export interface TraceOptions extends BaseOptions { @@ -64,3 +70,20 @@ export interface BaseOptions { locale?: string; local?: string; // deprecated } + +export interface LinterCliOptions extends Omit { + legacy?: boolean; + summary: boolean; + issues: boolean; + silent: boolean; + mustFindFiles: boolean; + progress?: boolean; + /** + * issues are shown with a relative path to the root or `cwd` + */ + relative?: boolean; + /** + * List of file paths to files that contains a list of files to be spell checked. + */ + fileList?: string[]; +} diff --git a/packages/cspell/src/util/glob.ts b/packages/cspell/src/util/glob.ts index 3310007554fa..59bdbf749723 100644 --- a/packages/cspell/src/util/glob.ts +++ b/packages/cspell/src/util/glob.ts @@ -122,7 +122,7 @@ export function extractGlobExcludesFromConfig(root: string, source: string, conf /** * Build GlobMatcher from command line or config file globs. - * @param globs Glob patterns. + * @param globs Glob patterns or file paths * @param root - directory to use as the root */ export function buildGlobMatcher(globs: Glob[], root: string, isExclude: boolean): GlobMatcher {