Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add --file [files...] command line option to lint command. #5328

Merged
merged 1 commit into from Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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