Skip to content

Commit

Permalink
feat: Add command-line options (#5328)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S committed Mar 1, 2024
1 parent 63e3927 commit d1888ea
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 58 deletions.
9 changes: 9 additions & 0 deletions packages/cspell/src/app/__snapshots__/app.test.ts.snap
Expand Up @@ -537,6 +537,8 @@ exports[`Validate cli > app 'no-args' Expect Error: 'outputHelp' 1`] = `
" --file-list <path or stdin> 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.",
" --file [file...] Specify files to spell check. They are filtered",
" by the [globs...].",
" --no-issues Do not show the spelling errors.",
" --no-progress Turn off progress messages",
" --no-summary Turn off summary message in console.",
Expand Down Expand Up @@ -589,6 +591,13 @@ exports[`Validate cli > app 'no-args' Expect Error: 'outputHelp' 1`] = `
" cspell . --reporter ./<path>/reporter.cjs",
" Use a custom reporter. See API for details.",
"",
" cspell "*.md" --exclude CHANGELOG.md --files README.md CHANGELOG.md",
" Spell check only check "README.md" but NOT "CHANGELOG.md".",
"",
" cspell "/*.md" --no-must-find-files --files $FILES",
" Only spell check the "/*.md" files in $FILES,",
" where $FILES is a shell variable that contains the list of files.",
"",
"References:",
" https://cspell.org",
" https://github.com/streetsidesoftware/cspell",
Expand Down
103 changes: 64 additions & 39 deletions packages/cspell/src/app/commandLint.ts
Expand Up @@ -43,16 +43,21 @@ More Examples:
cspell . --reporter ./<path>/reporter.cjs
Use a custom reporter. See API for details.
cspell "*.md" --exclude CHANGELOG.md --files README.md CHANGELOG.md
Spell check only check "README.md" but NOT "CHANGELOG.md".
cspell "/*.md" --no-must-find-files --files $FILES
Only spell check the "/*.md" files in $FILES,
where $FILES is a shell variable that contains the list of files.
References:
https://cspell.org
https://github.com/streetsidesoftware/cspell
`;

function collect(value: string, previous: string[] | undefined): string[] {
if (!previous) {
return [value];
}
return previous.concat([value]);
function collect(value: string | string[], previous: string[] | undefined): string[] {
const values = Array.isArray(value) ? value : [value];
return previous ? [...previous, ...values] : values;
}

export function commandLint(prog: Command): Command {
Expand All @@ -70,65 +75,66 @@ export function commandLint(prog: Command): Command {
)
.option('--language-id <language>', 'Force programming language for unknown extensions. i.e. "php" or "scala"')
.addOption(
new CommanderOption(
crOpt(
'--languageId <language>',
'Force programming language for unknown extensions. i.e. "php" or "scala"',
).hideHelp(),
)
.option('--words-only', 'Only output the words not found in the dictionaries.')
.addOption(
new CommanderOption('--wordsOnly', 'Only output the words not found in the dictionaries.').hideHelp(),
)
.addOption(crOpt('--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(
'-e, --exclude <glob>',
'Exclude files matching the glob pattern. This option can be used multiple times to add multiple globs. ',
collect,
)
// .option('--include <glob>', 'Include files matching the glob pattern. This option can be used multiple times.', collect)
.option(
'--file-list <path or stdin>',
'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('--file [file...]', 'Specify files to spell check. They are filtered by the [globs...].', collect)
.addOption(crOpt('--files [file...]', 'Files to spell check.', collect).hideHelp())
.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.')
.option('-s, --silent', 'Silent mode, suppress error messages.')
.option('--no-exit-code', 'Do not return an exit code if issues are found.')
.addOption(
new CommanderOption('--quiet', 'Only show spelling issues or errors.').implies({
crOpt('--quiet', 'Only show spelling issues or errors.').implies({
summary: false,
progress: false,
}),
)
.option('--fail-fast', 'Exit after first file with an issue or error.')
.addOption(new CommanderOption('--no-fail-fast', 'Process all files even if there is an error.').hideHelp())
.addOption(crOpt('--no-fail-fast', 'Process all files even if there is an error.').hideHelp())
.option('-r, --root <root folder>', 'Root directory, defaults to current directory.')
.addOption(
new CommanderOption('--relative', 'Issues are displayed relative to the root.').default(true).hideHelp(),
)
.addOption(crOpt('--relative', 'Issues are displayed relative to the root.').default(true).hideHelp())
.option('--no-relative', 'Issues are displayed with absolute path instead of relative to the root.')
.option('--show-context', 'Show the surrounding text around an issue.')
.option('--show-suggestions', 'Show spelling suggestions.')
.addOption(
new CommanderOption('--no-show-suggestions', 'Do not show spelling suggestions or fixes.').default(
undefined,
),
)
.addOption(new CommanderOption('--must-find-files', 'Error if no files are found.').default(true).hideHelp())
.addOption(crOpt('--no-show-suggestions', 'Do not show spelling suggestions or fixes.').default(undefined))
.addOption(crOpt('--must-find-files', 'Error if no files are found.').default(true).hideHelp())
.option('--no-must-find-files', 'Do not error if no files are found.')
// The --filter-files option is still under design review.
// .option('--filter-files', 'Use the `files` configuration to filter files found.')
// .option(
// '--no-filter-files',
// 'Do NOT use the `files` configuration to filter files (Only applies to --files options).',
// )
// 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')
.addOption(new CommanderOption('--legacy', 'Legacy output').hideHelp())
.addOption(new CommanderOption('--local <local>', 'Deprecated -- Use: --locale').hideHelp())
.addOption(crOpt('--legacy', 'Legacy output').hideHelp())
.addOption(crOpt('--local <local>', 'Deprecated -- Use: --locale').hideHelp())
.option('--cache', 'Use cache to only check changed files.')
.option('--no-cache', 'Do not use cache.')
.option('--cache-reset', 'Reset the cache file.')
.addOption(
new CommanderOption('--cache-strategy <strategy>', 'Strategy to use for detecting changed files.').choices([
crOpt('--cache-strategy <strategy>', 'Strategy to use for detecting changed files.').choices([
'metadata',
'content',
]),
Expand All @@ -145,38 +151,33 @@ export function commandLint(prog: Command): Command {
.option('--no-validate-directives', 'Do not validate in-document CSpell directives.')
.option('--no-color', 'Turn off color.')
.option('--color', 'Force color.')
.addOption(
new CommanderOption(
'--default-configuration',
'Load the default configuration and dictionaries.',
).hideHelp(),
)
.addOption(
new CommanderOption(
'--no-default-configuration',
'Do not load the default configuration and dictionaries.',
),
)
.addOption(crOpt('--default-configuration', 'Load the default configuration and dictionaries.').hideHelp())
.addOption(crOpt('--no-default-configuration', 'Do not load the default configuration and dictionaries.'))
.option('--debug', 'Output information useful for debugging cspell.json files.')
.option('--reporter <module|path>', 'Specify one or more reporters to use.', collect)
.addOption(
new CommanderOption('--skip-validation', 'Collect and process documents, but do not spell check.')
crOpt('--skip-validation', 'Collect and process documents, but do not spell check.')
.implies({ cache: false })
.hideHelp(),
)
.addOption(new CommanderOption('--issues-summary-report', 'Output a summary of issues found.').hideHelp())
.addOption(crOpt('--issues-summary-report', 'Output a summary of issues found.').hideHelp())
// Planned options
// .option('--dictionary <dictionary name>', 'Enable a dictionary by name.', collect)
// .option('--no-dictionary <dictionary name>', 'Disable a dictionary by name.', collect)
// .option('--import', 'Import a configuration file.', collect)
.usage(usage)
.addHelpText('after', advanced)
.arguments('[globs...]')
.action(async (fileGlobs: string[], options: LinterCliOptions) => {
// console.error('lint: %o', { fileGlobs, options });
const useExitCode = options.exitCode ?? true;
if (options.skipValidation) {
options.cache = false;
}
App.parseApplicationFeatureFlags(options.flag);
const { mustFindFiles, fileList } = options;
const { mustFindFiles, fileList, files, file } = options;
const result = await App.lint(fileGlobs, options);
if (!fileGlobs.length && !result.files && !result.errors && !fileList) {
if (!fileGlobs.length && !result.files && !result.errors && !fileList && !files?.length && !file?.length) {
spellCheckCommand.outputHelp();
throw new CheckFailed('outputHelp', 1);
}
Expand All @@ -192,3 +193,27 @@ export function commandLint(prog: Command): Command {

return spellCheckCommand;
}

/**
* Create Option - a helper function to create a commander option.
* @param name - the name of the option
* @param description - the description of the option
* @param parseArg - optional function to parse the argument
* @param defaultValue - optional default value
* @returns CommanderOption
*/
function crOpt<T>(
name: string,
description: string,
parseArg?: (value: string, previous: T) => T,
defaultValue?: T,
): CommanderOption {
const option = new CommanderOption(name, description);
if (parseArg) {
option.argParser(parseArg);
}
if (defaultValue !== undefined) {
option.default(defaultValue);
}
return option;
}
23 changes: 23 additions & 0 deletions packages/cspell/src/app/lint/LintRequest.test.ts
@@ -0,0 +1,23 @@
import { describe, expect, test } from 'vitest';

import { getReporter } from '../cli-reporter.js';
import { LintRequest } from './LintRequest.js';

const oc = expect.objectContaining;

describe('LintRequest', () => {
test.each`
options | expected
${{}} | ${oc({ fileGlobs: [], files: undefined })}
${{ files: ['one\ntwo'] }} | ${oc({ fileGlobs: [], files: ['one', 'two'] })}
${{ file: ['one', 'two'], files: ['two'] }} | ${oc({ fileGlobs: [], files: ['one', 'two'] })}
${{ file: ['one', 'two'] }} | ${oc({ fileGlobs: [], files: ['one', 'two'] })}
${{ showContext: undefined }} | ${oc({ showContext: 0 })}
${{ showContext: true }} | ${oc({ showContext: 20 })}
${{ showContext: 3 }} | ${oc({ showContext: 3 })}
`('create LintRequest $options', ({ options, expected }) => {
const fileGlobs: string[] = [];
const request = new LintRequest(fileGlobs, options, getReporter({ fileGlobs }));
expect(request).toEqual(expected);
});
});
17 changes: 16 additions & 1 deletion packages/cspell/src/app/lint/LintRequest.ts
Expand Up @@ -10,6 +10,7 @@ const defaultContextRange = 20;

interface Deprecated {
fileLists?: LinterOptions['fileList'];
local?: LinterOptions['locale'];
}

export class LintRequest {
Expand All @@ -22,6 +23,7 @@ export class LintRequest {
readonly showContext: number;
readonly enableGlobDot: boolean | undefined;
readonly fileLists: string[];
readonly files: string[] | undefined;

constructor(
readonly fileGlobs: string[],
Expand All @@ -31,12 +33,25 @@ export class LintRequest {
this.root = path.resolve(options.root || process.cwd());
this.configFile = options.config;
this.excludes = calcExcludeGlobInfo(this.root, options.exclude);
this.locale = options.locale || '';
this.locale = options.locale ?? options.local ?? '';
this.enableGlobDot = options.dot;
// this.uniqueFilter = options.unique ? util.uniqueFilterFnGenerator((issue: Issue) => issue.text) : () => true;
this.uniqueFilter = () => true;
this.showContext =
options.showContext === true ? defaultContextRange : options.showContext ? options.showContext : 0;
this.fileLists = (options.fileList ?? options.fileLists) || [];
this.files = mergeFiles(options.file, options.files);
}
}

function mergeFiles(a: string[] | undefined, b: string[] | undefined): string[] | undefined {
const files = merge(a, b);
if (!files) return undefined;
return [...new Set(files.flatMap((a) => a.split('\n').map((a) => a.trim())).filter((a) => !!a))];
}

function merge<T>(a: T[] | undefined, b: T[] | undefined): T[] | undefined {
if (!a) return b;
if (!b) return a;
return [...a, ...b];
}
8 changes: 8 additions & 0 deletions packages/cspell/src/app/lint/lint.test.ts
Expand Up @@ -35,6 +35,8 @@ describe('Linter Validation Tests', () => {
expect(rWithFiles.files).toBe(1);
});

const optionsRootCSpellJson = { root, config: j(root, 'cspell.json') };

// cspell:ignore Tufte checkedd
test.each`
files | options | expectedRunResult | expectedReport
Expand Down Expand Up @@ -62,6 +64,12 @@ describe('Linter Validation Tests', () => {
${[]} | ${{ root, config: j(root, 'cspell.json'), fileLists: [filesToCheckWithMissing], mustFindFiles: true }} | ${oc({ errors: 1, files: 3 })} | ${oc({ errorCount: 1, errors: [expect.anything()], issues: [] })}
${["'**'"]} | ${{ root, config: j(root, 'cspell.json'), mustFindFiles: true }} | ${oc({ errors: 0, files: 0 })} | ${oc({ errorCount: 1, errors: [expect.any(CheckFailed)], issues: [] })}
${['**']} | ${{ root: j(configSamples, 'yaml-regexp') }} | ${oc({ errors: 0, files: 2 })} | ${oc({ errorCount: 0, errors: [], issues: [oc({ text: 'checkedd' })] })}
${[]} | ${{ ...optionsRootCSpellJson, files: ['README.md', 'LICENSE'], dot: true }} | ${oc({ errors: 0, files: 2 })} | ${oc({ errorCount: 0, errors: [], issues: [] })}
${['*.md']} | ${{ ...optionsRootCSpellJson, files: ['README.md', 'LICENSE'], dot: true }} | ${oc({ errors: 0, files: 1 })} | ${oc({ errorCount: 0, errors: [], issues: [] })}
${[]} | ${{ ...optionsRootCSpellJson, files: ['README.md', 'missing.txt'], dot: true, mustFindFiles: false }} | ${oc({ errors: 0, files: 1 })} | ${oc({ errorCount: 0, errors: [], issues: [] })}
${[]} | ${{ ...optionsRootCSpellJson, files: ['README.md', 'missing.txt'], dot: true, mustFindFiles: true }} | ${oc({ errors: 1, files: 2 })} | ${oc({ errorCount: 1, errors: [expect.anything()], issues: [] })}
${[]} | ${{ ...optionsRootCSpellJson, files: ['../../README.md'], dot: true }} | ${oc({ errors: 0, files: 1 })} | ${oc({ errorCount: 0, errors: [], issues: [] })}
${[]} | ${{ ...optionsRootCSpellJson, files: ['../../resources/patreon.png' /* skip binary */], dot: true }} | ${oc({ errors: 0, files: 0 })} | ${oc({ errorCount: 0, errors: [], issues: [] })}
`('runLint $files $options', async ({ files, options, expectedRunResult, expectedReport }) => {
const reporter = new InMemoryReporter();
const runResult = await runLint(new LintRequest(files, options, reporter));
Expand Down

0 comments on commit d1888ea

Please sign in to comment.