From 971c56ac9ea64e61c7ea15b8c4f6db013c67bab0 Mon Sep 17 00:00:00 2001 From: Jason Dent Date: Fri, 3 May 2024 16:30:43 +0200 Subject: [PATCH] fix: Add option to show a performance report. (#5561) --- packages/cspell-lib/api/api.d.ts | 2 +- .../src/lib/__snapshots__/index.test.ts.snap | 1 + packages/cspell-lib/src/lib/index.ts | 1 + .../src/app/__snapshots__/app.test.ts.snap | 281 +++++++++++++++++- packages/cspell/src/app/app.test.ts | 4 + packages/cspell/src/app/cli-reporter.ts | 65 +++- packages/cspell/src/app/commandLint.ts | 46 ++- packages/cspell/src/app/lint/lint.ts | 1 + packages/cspell/src/app/options.ts | 5 + packages/cspell/src/app/util/fileHelper.ts | 2 +- 10 files changed, 393 insertions(+), 15 deletions(-) diff --git a/packages/cspell-lib/api/api.d.ts b/packages/cspell-lib/api/api.d.ts index 8661ee26934d..4a918882d243 100644 --- a/packages/cspell-lib/api/api.d.ts +++ b/packages/cspell-lib/api/api.d.ts @@ -1037,4 +1037,4 @@ declare namespace text_d { export { text_d___testing__ as __testing__, text_d_calculateTextDocumentOffsets as calculateTextDocumentOffsets, text_d_camelToSnake as camelToSnake, text_d_cleanText as cleanText, text_d_cleanTextOffset as cleanTextOffset, text_d_extractLinesOfText as extractLinesOfText, text_d_extractPossibleWordsFromTextOffset as extractPossibleWordsFromTextOffset, text_d_extractText as extractText, text_d_extractWordsFromCode as extractWordsFromCode, text_d_extractWordsFromCodeTextOffset as extractWordsFromCodeTextOffset, text_d_extractWordsFromText as extractWordsFromText, text_d_extractWordsFromTextOffset as extractWordsFromTextOffset, text_d_isFirstCharacterLower as isFirstCharacterLower, text_d_isFirstCharacterUpper as isFirstCharacterUpper, text_d_isLowerCase as isLowerCase, text_d_isUpperCase as isUpperCase, text_d_lcFirst as lcFirst, text_d_match as match, text_d_matchCase as matchCase, text_d_matchStringToTextOffset as matchStringToTextOffset, text_d_matchToTextOffset as matchToTextOffset, text_d_removeAccents as removeAccents, text_d_snakeToCamel as snakeToCamel, text_d_splitCamelCaseWord as splitCamelCaseWord, text_d_splitCamelCaseWordWithOffset as splitCamelCaseWordWithOffset, text_d_stringToRegExp as stringToRegExp, text_d_textOffset as textOffset, text_d_ucFirst as ucFirst }; } -export { type CheckTextInfo, type ConfigurationDependencies, type CreateTextDocumentParams, type DetermineFinalDocumentSettingsResult, type Document, DocumentValidator, type DocumentValidatorOptions, ENV_CSPELL_GLOB_ROOT, type ExcludeFilesGlobMap, type ExclusionFunction, exclusionHelper_d as ExclusionHelper, type FeatureFlag, FeatureFlags, ImportError, type ImportFileRefWithError$1 as ImportFileRefWithError, IncludeExcludeFlag, type IncludeExcludeOptions, index_link_d as Link, type Logger, type PerfTimer, type SpellCheckFileOptions, type SpellCheckFileResult, SpellingDictionaryLoadError, type SuggestedWord, SuggestionError, type SuggestionOptions, type SuggestionsForWordResult, text_d as Text, type TextDocument, type TextDocumentLine, type TextDocumentRef, type TextInfoItem, type TraceOptions, type TraceResult, type TraceWordResult, UnknownFeatureFlagError, type ValidationIssue, calcOverrideSettings, checkFilenameMatchesExcludeGlob as checkFilenameMatchesGlob, checkText, checkTextDocument, clearCachedFiles, clearCaches, combineTextAndLanguageSettings, combineTextAndLanguageSettings as constructSettingsForText, createConfigLoader, createPerfTimer, createTextDocument, currentSettingsFileVersion, defaultConfigFilenames, defaultFileName, defaultFileName as defaultSettingsFilename, determineFinalDocumentSettings, extractDependencies, extractImportErrors, fileToDocument, fileToTextDocument, finalizeSettings, getCachedFileSize, getDefaultBundledSettingsAsync, getDefaultConfigLoader, getDefaultSettings, getDictionary, getGlobalSettings, getGlobalSettingsAsync, getLanguagesForBasename as getLanguageIdsForBaseFilename, getLanguagesForExt, getLogger, getSources, getSystemFeatureFlags, getVirtualFS, isBinaryFile, isSpellingDictionaryLoadError, loadConfig, loadPnP, mergeInDocSettings, mergeSettings, readRawSettings, readSettings, readSettingsFiles, refreshDictionaryCache, resolveFile, searchForConfig, sectionCSpell, setLogger, shouldCheckDocument, spellCheckDocument, spellCheckFile, suggestionsForWord, suggestionsForWords, traceWords, traceWordsAsync, updateTextDocument, validateText }; +export { type CheckTextInfo, type ConfigurationDependencies, type CreateTextDocumentParams, type DetermineFinalDocumentSettingsResult, type Document, DocumentValidator, type DocumentValidatorOptions, ENV_CSPELL_GLOB_ROOT, type ExcludeFilesGlobMap, type ExclusionFunction, exclusionHelper_d as ExclusionHelper, type FeatureFlag, FeatureFlags, ImportError, type ImportFileRefWithError$1 as ImportFileRefWithError, IncludeExcludeFlag, type IncludeExcludeOptions, index_link_d as Link, type Logger, type PerfTimer, type SpellCheckFileOptions, type SpellCheckFilePerf, type SpellCheckFileResult, SpellingDictionaryLoadError, type SuggestedWord, SuggestionError, type SuggestionOptions, type SuggestionsForWordResult, text_d as Text, type TextDocument, type TextDocumentLine, type TextDocumentRef, type TextInfoItem, type TraceOptions, type TraceResult, type TraceWordResult, UnknownFeatureFlagError, type ValidationIssue, calcOverrideSettings, checkFilenameMatchesExcludeGlob as checkFilenameMatchesGlob, checkText, checkTextDocument, clearCachedFiles, clearCaches, combineTextAndLanguageSettings, combineTextAndLanguageSettings as constructSettingsForText, createConfigLoader, createPerfTimer, createTextDocument, currentSettingsFileVersion, defaultConfigFilenames, defaultFileName, defaultFileName as defaultSettingsFilename, determineFinalDocumentSettings, extractDependencies, extractImportErrors, fileToDocument, fileToTextDocument, finalizeSettings, getCachedFileSize, getDefaultBundledSettingsAsync, getDefaultConfigLoader, getDefaultSettings, getDictionary, getGlobalSettings, getGlobalSettingsAsync, getLanguagesForBasename as getLanguageIdsForBaseFilename, getLanguagesForExt, getLogger, getSources, getSystemFeatureFlags, getVirtualFS, isBinaryFile, isSpellingDictionaryLoadError, loadConfig, loadPnP, mergeInDocSettings, mergeSettings, readRawSettings, readSettings, readSettingsFiles, refreshDictionaryCache, resolveFile, searchForConfig, sectionCSpell, setLogger, shouldCheckDocument, spellCheckDocument, spellCheckFile, suggestionsForWord, suggestionsForWords, traceWords, traceWordsAsync, updateTextDocument, validateText }; diff --git a/packages/cspell-lib/src/lib/__snapshots__/index.test.ts.snap b/packages/cspell-lib/src/lib/__snapshots__/index.test.ts.snap index 48ae5bd60795..2c3ef53917e1 100644 --- a/packages/cspell-lib/src/lib/__snapshots__/index.test.ts.snap +++ b/packages/cspell-lib/src/lib/__snapshots__/index.test.ts.snap @@ -116,6 +116,7 @@ exports[`Validate the cspell API > Verify API exports 1`] = ` "Warning": "Warning", }, "SpellCheckFileOptions": undefined, + "SpellCheckFilePerf": undefined, "SpellCheckFileResult": undefined, "SpellingDictionary": undefined, "SpellingDictionaryCollection": undefined, diff --git a/packages/cspell-lib/src/lib/index.ts b/packages/cspell-lib/src/lib/index.ts index 15d715874f21..a764a0bd5470 100644 --- a/packages/cspell-lib/src/lib/index.ts +++ b/packages/cspell-lib/src/lib/index.ts @@ -60,6 +60,7 @@ export { spellCheckDocument, spellCheckFile, SpellCheckFileOptions, + SpellCheckFilePerf, SpellCheckFileResult, } from './spellCheckFile.js'; export { diff --git a/packages/cspell/src/app/__snapshots__/app.test.ts.snap b/packages/cspell/src/app/__snapshots__/app.test.ts.snap index c6851ada094a..2cec1bf7a639 100644 --- a/packages/cspell/src/app/__snapshots__/app.test.ts.snap +++ b/packages/cspell/src/app/__snapshots__/app.test.ts.snap @@ -351,6 +351,36 @@ error CSpell: Files checked: 0, Issues found: 0 in 0 files with 1 error." exports[`Validate cli > app 'cspell-import-missing.json' Expect Error: [Function CheckFailed] 3`] = `""`; +exports[`Validate cli > app 'current_file --show-perf-summary' Expect Error: undefined 1`] = `[]`; + +exports[`Validate cli > app 'current_file --show-perf-summary' Expect Error: undefined 2`] = ` +"error 0.00ms +error CSpell: Files checked: 1, Issues found: 0 in 0 files. +error ------------------------------------------- +error Performance Summary: +error Files Processed: 1 +error Files Skipped : 0 +error Files Cached : 0 +error Processing Time: 0.00ms +error Stats: +error __determineTextDocumentSettings: 0.00ms +error __finalizeSettings : 0.00ms +error __getDictionaryInternal : 0.00ms +error __GlobMatcher : 0.00ms +error __shouldCheck : 0.00ms +error _checkDocument : 0.00ms +error _prepTime : 0.00ms +error checkTimeMs : 0.00ms +error loadTimeMs : 0.00ms +error prepareTimeMs : 0.00ms +error totalTimeMs : 0.00ms" +`; + +exports[`Validate cli > app 'current_file --show-perf-summary' Expect Error: undefined 3`] = ` +" +1/1 ./src/app/app.test.ts" +`; + exports[`Validate cli > app 'current_file --verbose' Expect Error: undefined 1`] = `[]`; exports[`Validate cli > app 'current_file --verbose' Expect Error: undefined 2`] = ` @@ -500,6 +530,254 @@ exports[`Validate cli > app 'link remove' 1`] = `""`; exports[`Validate cli > app 'link' 1`] = `""`; +exports[`Validate cli > app 'lint --help --verbose' Expect Error: 'outputHelp' 1`] = ` +[ + "Usage: cspell lint [options] [globs...] [file:// ...] [stdin[://]]", + "", + "Patterns:", + " - [globs...] Glob Patterns", + " - [stdin] Read from "stdin" assume text file.", + " - [stdin://] Read from "stdin", use for file type and config.", + " - [file://] Check the file at ", + "", + "Examples:", + " cspell . Recursively check all files.", + " cspell lint . The same as "cspell ."", + " cspell "*.js" Check all .js files in the current directory", + " cspell "**/*.js" Check all .js files recursively", + " cspell "src/**/*.js" Only check .js under src", + " cspell "**/*.txt" "**/*.js" Check both .js and .txt files.", + " cspell "**/*.{txt,js,md}" Check .txt, .js, and .md files.", + " cat LICENSE | cspell stdin Check stdin", + " cspell stdin://docs/doc.md Check stdin as if it was "./docs/doc.md"", + "", + "Check spelling", + "", + "Options:", + " -c, --config Configuration file to use. By default cspell", + " looks for cspell.json in the current directory.", + " -v, --verbose Display more information about the files being", + " checked and the configuration.", + " --locale Set language locales. i.e. "en,fr" for English", + " and French, or "en-GB" for British English.", + " --language-id Force programming language for unknown", + " extensions. i.e. "php" or "scala"", + " --words-only Only output the words not found in the", + " dictionaries.", + " -u, --unique Only output the first instance of a word not", + " found in the dictionaries.", + " -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.", + " --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.", + " -s, --silent Silent mode, suppress error messages.", + " --no-exit-code Do not return an exit code if issues are found.", + " --quiet Only show spelling issues or errors.", + " --fail-fast Exit after first file with an issue or error.", + " -r, --root Root directory, defaults to current directory.", + " --no-relative Issues are displayed with absolute path instead", + " of relative to the root.", + " --show-context Show the surrounding text around an issue.", + " --show-suggestions Show spelling suggestions.", + " --no-show-suggestions Do not show spelling suggestions or fixes.", + " --no-must-find-files Do not error if no files are found.", + " --cache Use cache to only check changed files.", + " --no-cache Do not use cache.", + " --cache-reset Reset the cache file.", + " --cache-strategy Strategy to use for detecting changed files.", + " (choices: "metadata", "content")", + " --cache-location Path to the cache file or directory. (default:", + " ".cspellcache")", + " --dot Include files and directories starting with \`.\`", + " (period) when matching globs.", + " --gitignore Ignore files matching glob patterns found in", + " .gitignore files.", + " --no-gitignore Do NOT use .gitignore files.", + " --gitignore-root Prevent searching for .gitignore files past", + " root.", + " --validate-directives Validate in-document CSpell directives.", + " --no-color Turn off color.", + " --color Force color.", + " --no-default-configuration Do not load the default configuration and", + " dictionaries.", + " --debug Output information useful for debugging", + " cspell.json files.", + " --reporter Specify one or more reporters to use.", + " -h, --help display help for command", + "", + "Hidden Options:", + " --languageId Alias of "--language-id". Force programming", + " language for unknown extensions. i.e. "php" or", + " "scala"", + " --wordsOnly Only output the words not found in the", + " dictionaries.", + " --files [file...] Alias of "--file". Files to spell check.", + " --no-fail-fast Process all files even if there is an error.", + " --relative Issues are displayed relative to the root.", + " --must-find-files Error if no files are found.", + " --legacy Legacy output", + " --local Deprecated -- Use: --locale", + " --no-validate-directives Do not validate in-document CSpell directives.", + " --default-configuration Load the default configuration and dictionaries.", + " --skip-validation Collect and process documents, but do not spell", + " check.", + " --issues-summary-report Output a summary of issues found.", + " --show-perf-summary Output a performance summary report.", + " -f,--flag Declare an execution flag value", + "", + "More Examples:", + "", + " cspell "**/*.js" --reporter @cspell/cspell-json-reporter", + " This will spell check all ".js" files recursively and use", + " "@cspell/cspell-json-reporter".", + "", + " cspell . --reporter default", + " This will force the default reporter to be used overriding", + " any reporters defined in the configuration.", + "", + " cspell . --reporter .//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", + "", + "", +] +`; + +exports[`Validate cli > app 'lint --help --verbose' Expect Error: 'outputHelp' 2`] = `""`; + +exports[`Validate cli > app 'lint --help --verbose' Expect Error: 'outputHelp' 3`] = `""`; + +exports[`Validate cli > app 'lint --help' Expect Error: 'outputHelp' 1`] = ` +[ + "Usage: cspell lint [options] [globs...] [file:// ...] [stdin[://]]", + "", + "Patterns:", + " - [globs...] Glob Patterns", + " - [stdin] Read from "stdin" assume text file.", + " - [stdin://] Read from "stdin", use for file type and config.", + " - [file://] Check the file at ", + "", + "Examples:", + " cspell . Recursively check all files.", + " cspell lint . The same as "cspell ."", + " cspell "*.js" Check all .js files in the current directory", + " cspell "**/*.js" Check all .js files recursively", + " cspell "src/**/*.js" Only check .js under src", + " cspell "**/*.txt" "**/*.js" Check both .js and .txt files.", + " cspell "**/*.{txt,js,md}" Check .txt, .js, and .md files.", + " cat LICENSE | cspell stdin Check stdin", + " cspell stdin://docs/doc.md Check stdin as if it was "./docs/doc.md"", + "", + "Check spelling", + "", + "Options:", + " -c, --config Configuration file to use. By default cspell", + " looks for cspell.json in the current directory.", + " -v, --verbose Display more information about the files being", + " checked and the configuration.", + " --locale Set language locales. i.e. "en,fr" for English", + " and French, or "en-GB" for British English.", + " --language-id Force programming language for unknown", + " extensions. i.e. "php" or "scala"", + " --words-only Only output the words not found in the", + " dictionaries.", + " -u, --unique Only output the first instance of a word not", + " found in the dictionaries.", + " -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.", + " --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.", + " -s, --silent Silent mode, suppress error messages.", + " --no-exit-code Do not return an exit code if issues are found.", + " --quiet Only show spelling issues or errors.", + " --fail-fast Exit after first file with an issue or error.", + " -r, --root Root directory, defaults to current directory.", + " --no-relative Issues are displayed with absolute path instead", + " of relative to the root.", + " --show-context Show the surrounding text around an issue.", + " --show-suggestions Show spelling suggestions.", + " --no-show-suggestions Do not show spelling suggestions or fixes.", + " --no-must-find-files Do not error if no files are found.", + " --cache Use cache to only check changed files.", + " --no-cache Do not use cache.", + " --cache-reset Reset the cache file.", + " --cache-strategy Strategy to use for detecting changed files.", + " (choices: "metadata", "content")", + " --cache-location Path to the cache file or directory. (default:", + " ".cspellcache")", + " --dot Include files and directories starting with \`.\`", + " (period) when matching globs.", + " --gitignore Ignore files matching glob patterns found in", + " .gitignore files.", + " --no-gitignore Do NOT use .gitignore files.", + " --gitignore-root Prevent searching for .gitignore files past", + " root.", + " --validate-directives Validate in-document CSpell directives.", + " --no-color Turn off color.", + " --color Force color.", + " --no-default-configuration Do not load the default configuration and", + " dictionaries.", + " --debug Output information useful for debugging", + " cspell.json files.", + " --reporter Specify one or more reporters to use.", + " -h, --help display help for command", + "", + "More Examples:", + "", + " cspell "**/*.js" --reporter @cspell/cspell-json-reporter", + " This will spell check all ".js" files recursively and use", + " "@cspell/cspell-json-reporter".", + "", + " cspell . --reporter default", + " This will force the default reporter to be used overriding", + " any reporters defined in the configuration.", + "", + " cspell . --reporter .//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", + "", + "", +] +`; + +exports[`Validate cli > app 'lint --help' Expect Error: 'outputHelp' 2`] = `""`; + +exports[`Validate cli > app 'lint --help' Expect Error: 'outputHelp' 3`] = `""`; + exports[`Validate cli > app 'must find force no error' Expect Error: undefined 1`] = `[]`; exports[`Validate cli > app 'must find force no error' Expect Error: undefined 2`] = `"error CSpell: Files checked: 0, Issues found: 0 in 0 files."`; @@ -542,7 +820,7 @@ exports[`Validate cli > app 'no-args' Expect Error: 'outputHelp' 1`] = ` " checked and the configuration.", " --locale Set language locales. i.e. "en,fr" for English", " and French, or "en-GB" for British English.", - " --language-id Force programming language for unknown", + " --language-id Force programming language for unknown", " extensions. i.e. "php" or "scala"", " --words-only Only output the words not found in the", " dictionaries.", @@ -585,7 +863,6 @@ exports[`Validate cli > app 'no-args' Expect Error: 'outputHelp' 1`] = ` " --gitignore-root Prevent searching for .gitignore files past", " root.", " --validate-directives Validate in-document CSpell directives.", - " --no-validate-directives Do not validate in-document CSpell directives.", " --no-color Turn off color.", " --color Force color.", " --no-default-configuration Do not load the default configuration and", diff --git a/packages/cspell/src/app/app.test.ts b/packages/cspell/src/app/app.test.ts index 216b2d566130..682085ea6e58 100644 --- a/packages/cspell/src/app/app.test.ts +++ b/packages/cspell/src/app/app.test.ts @@ -159,7 +159,10 @@ describe('Validate cli', () => { ${'with errors and excludes'} | ${['-r', 'samples', '*', '-e', 'Dutch.txt', '-c', 'samples/.cspell.json']} | ${app.CheckFailed} | ${true} | ${true} | ${false} ${'no-args'} | ${[]} | ${'outputHelp'} | ${false} | ${false} | ${false} ${'--help'} | ${['--help']} | ${'outputHelp'} | ${false} | ${false} | ${false} + ${'lint --help'} | ${['lint', '--help']} | ${'outputHelp'} | ${false} | ${false} | ${false} + ${'lint --help --verbose'} | ${['lint', '--help', '--verbose']} | ${'outputHelp'} | ${false} | ${false} | ${false} ${'current_file'} | ${[__filename]} | ${undefined} | ${true} | ${false} | ${false} + ${'current_file --show-perf-summary'} | ${[__filename, '--show-perf-summary']} | ${undefined} | ${true} | ${false} | ${false} ${'with spelling errors Dutch.txt'} | ${[pathSamples('Dutch.txt')]} | ${app.CheckFailed} | ${true} | ${true} | ${false} ${'with spelling errors Dutch.txt words only'} | ${[pathSamples('Dutch.txt'), '--wordsOnly']} | ${app.CheckFailed} | ${true} | ${true} | ${false} ${'with spelling errors Dutch.txt --legacy'} | ${[pathSamples('Dutch.txt'), '--legacy']} | ${app.CheckFailed} | ${true} | ${true} | ${false} @@ -387,6 +390,7 @@ function makeLogger() { t = t.replace(RegExp(escapeRegExp(projectRoot), 'gi'), '.'); t = t.replaceAll('\\', '/'); t = t.replaceAll(/(?<=^info\s+Date:).*$/gm, ' Sat, 03 Apr 2021 11:25:33 GMT'); + t = t.replaceAll(/ +[\d.]+ms\b/g, ' 0.00ms'); t = t.replaceAll(/\b[\d.]+ms\b/g, '0.00ms'); t = t.replaceAll(/\b[\d.]+S\b/g, '0.00S'); return t; diff --git a/packages/cspell/src/app/cli-reporter.ts b/packages/cspell/src/app/cli-reporter.ts index f0b7dcccf132..27fbd058821a 100644 --- a/packages/cspell/src/app/cli-reporter.ts +++ b/packages/cspell/src/app/cli-reporter.ts @@ -12,7 +12,7 @@ import type { } from '@cspell/cspell-types'; import chalk from 'chalk'; import chalkTemplate from 'chalk-template'; -import type { ImportError, SpellingDictionaryLoadError } from 'cspell-lib'; +import type { ImportError, SpellCheckFilePerf, SpellingDictionaryLoadError } from 'cspell-lib'; import { isSpellingDictionaryLoadError } from 'cspell-lib'; import { URI } from 'vscode-uri'; @@ -119,6 +119,7 @@ export interface ReporterOptions | 'relative' | 'root' | 'showContext' + | 'showPerfSummary' | 'showSuggestions' | 'silent' | 'summary' @@ -128,7 +129,18 @@ export interface ReporterOptions fileGlobs: string[]; } +interface ProgressFileCompleteWithPerf extends ProgressFileComplete { + perf?: SpellCheckFilePerf; +} + export function getReporter(options: ReporterOptions, config?: ReporterConfiguration): FinalizedReporter { + const perfStats = { + filesProcessed: 0, + filesSkipped: 0, + filesCached: 0, + elapsedTimeMs: 0, + perf: Object.create(null) as SpellCheckFilePerf, + }; const uniqueIssues = config?.unique || false; const issueTemplate = options.wordsOnly ? templateIssueWordsOnly @@ -143,7 +155,7 @@ export function getReporter(options: ReporterOptions, config?: ReporterConfigura : options.showSuggestions === false ? templateIssueNoFix : templateIssue; - const { fileGlobs, silent, summary, issues, progress, verbose, debug } = options; + const { fileGlobs, silent, summary, issues, progress: showProgress, verbose, debug } = options; const emitters: InfoEmitter = { Debug: !silent && debug ? (s) => console.info(chalk.cyan(s)) : nullEmitter, @@ -168,7 +180,7 @@ export function getReporter(options: ReporterOptions, config?: ReporterConfigura }; } - const issuesCollection: string[] | undefined = progress ? [] : undefined; + const issuesCollection: string[] | undefined = showProgress ? [] : undefined; const errorCollection: string[] | undefined = []; function errorEmitter(message: string, error: Error | SpellingDictionaryLoadError | ImportError) { @@ -209,8 +221,53 @@ export function getReporter(options: ReporterOptions, config?: ReporterConfigura console.error('Errors:'); errorCollection.forEach((error) => console.error(error)); } + + if (options.showPerfSummary) { + console.error('-------------------------------------------'); + console.error('Performance Summary:'); + console.error(` Files Processed: ${perfStats.filesProcessed.toString().padStart(6)}`); + console.error(` Files Skipped : ${perfStats.filesSkipped.toString().padStart(6)}`); + console.error(` Files Cached : ${perfStats.filesCached.toString().padStart(6)}`); + console.error(` Processing Time: ${perfStats.elapsedTimeMs.toFixed(2).padStart(9)}ms`); + console.error('Stats:'); + const stats = Object.entries(perfStats.perf) + .filter((p): p is [string, number] => !!p[1]) + .map(([key, value]) => [key, value.toFixed(2)] as const); + const padName = Math.max(...stats.map((s) => s[0].length)); + const padValue = Math.max(...stats.map((s) => s[1].length)); + stats.sort((a, b) => a[0].localeCompare(b[0])); + for (const [key, value] of stats) { + value && console.error(` ${key.padEnd(padName)}: ${value.padStart(padValue)}ms`); + } + } }; + function collectPerfStats(p: ProgressFileCompleteWithPerf) { + if (p.cached) { + perfStats.filesCached++; + return; + } + perfStats.filesProcessed += p.processed ? 1 : 0; + perfStats.filesSkipped += !p.processed ? 1 : 0; + perfStats.elapsedTimeMs += p.elapsedTimeMs || 0; + + if (!p.perf) return; + for (const [key, value] of Object.entries(p.perf)) { + if (typeof value === 'number') { + perfStats.perf[key] = (perfStats.perf[key] || 0) + value; + } + } + } + + function progress(p: ProgressItem) { + if (!silent && showProgress) { + reportProgress(p, fsPathRoot); + } + if (p.type === 'ProgressFileComplete') { + collectPerfStats(p); + } + } + return { issue: relativeIssue( silent || !issues ? nullEmitter : genIssueEmitter(issueTemplate, uniqueIssues, issuesCollection), @@ -218,7 +275,7 @@ export function getReporter(options: ReporterOptions, config?: ReporterConfigura error: silent ? nullEmitter : errorEmitter, info: infoEmitter, debug: emitters.Debug, - progress: !silent && progress ? (p) => reportProgress(p, fsPathRoot) : nullEmitter, + progress, result: !silent && summary ? resultEmitter : nullEmitter, }; } diff --git a/packages/cspell/src/app/commandLint.ts b/packages/cspell/src/app/commandLint.ts index ac5f9cdc4a7e..13c3f1f3cd34 100644 --- a/packages/cspell/src/app/commandLint.ts +++ b/packages/cspell/src/app/commandLint.ts @@ -1,4 +1,4 @@ -import type { Command } from 'commander'; +import type { AddHelpTextContext, Command } from 'commander'; import { Option as CommanderOption } from 'commander'; import * as App from './application.js'; @@ -73,11 +73,11 @@ export function commandLint(prog: Command): Command { '--locale ', 'Set language locales. i.e. "en,fr" for English and French, or "en-GB" for British English.', ) - .option('--language-id ', 'Force programming language for unknown extensions. i.e. "php" or "scala"') + .option('--language-id ', 'Force programming language for unknown extensions. i.e. "php" or "scala"') .addOption( crOpt( - '--languageId ', - 'Force programming language for unknown extensions. i.e. "php" or "scala"', + '--languageId ', + 'Alias of "--language-id". Force programming language for unknown extensions. i.e. "php" or "scala"', ).hideHelp(), ) .option('--words-only', 'Only output the words not found in the dictionaries.') @@ -97,7 +97,7 @@ export function commandLint(prog: Command): Command { 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()) + .addOption(crOpt('--files [file...]', 'Alias of "--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.') @@ -148,7 +148,7 @@ export function commandLint(prog: Command): Command { .option('--no-gitignore', 'Do NOT use .gitignore files.') .option('--gitignore-root ', 'Prevent searching for .gitignore files past root.', collect) .option('--validate-directives', 'Validate in-document CSpell directives.') - .option('--no-validate-directives', 'Do not validate in-document CSpell directives.') + .addOption(crOpt('--no-validate-directives', 'Do not validate in-document CSpell directives.').hideHelp()) .option('--no-color', 'Turn off color.') .option('--color', 'Force color.') .addOption(crOpt('--default-configuration', 'Load the default configuration and dictionaries.').hideHelp()) @@ -161,12 +161,13 @@ export function commandLint(prog: Command): Command { .hideHelp(), ) .addOption(crOpt('--issues-summary-report', 'Output a summary of issues found.').hideHelp()) + .addOption(crOpt('--show-perf-summary', 'Output a performance summary report.').hideHelp()) // Planned options // .option('--dictionary ', 'Enable a dictionary by name.', collect) // .option('--no-dictionary ', 'Disable a dictionary by name.', collect) // .option('--import', 'Import a configuration file.', collect) .usage(usage) - .addHelpText('after', advanced) + .addHelpText('after', augmentCommandHelp) .arguments('[globs...]') .action(async (fileGlobs: string[], options: LinterCliOptions) => { // console.error('lint: %o', { fileGlobs, options }); @@ -194,6 +195,37 @@ export function commandLint(prog: Command): Command { return spellCheckCommand; } +/** + * Add additional help text to the command. + * When the verbose flag is set, show the hidden options. + * @param context + * @returns + */ +function augmentCommandHelp(context: AddHelpTextContext) { + const output: string[] = []; + const command = context.command; + const showHidden = !!command.opts().verbose; + const hiddenHelp: string[] = []; + const help = command.createHelp(); + const hiddenOptions = command.options.filter((opt) => opt.hidden && showHidden); + const flagColWidth = Math.max(...command.options.map((opt) => opt.flags.length), 0); + const indent = flagColWidth + 4; + for (const options of hiddenOptions) { + if (!hiddenHelp.length) { + hiddenHelp.push('\nHidden Options:'); + } + hiddenHelp.push( + help.wrap( + ` ${options.flags.padEnd(flagColWidth)} ${options.description}`, + process.stdout.columns || 80, + indent, + ), + ); + } + output.push(...hiddenHelp, advanced); + return output.join('\n'); +} + /** * Create Option - a helper function to create a commander option. * @param name - the name of the option diff --git a/packages/cspell/src/app/lint/lint.ts b/packages/cspell/src/app/lint/lint.ts index 37fd48b1ef4c..4d3c37c1cc73 100644 --- a/packages/cspell/src/app/lint/lint.ts +++ b/packages/cspell/src/app/lint/lint.ts @@ -273,6 +273,7 @@ export async function runLint(cfg: LintRequest): Promise { processed: result?.processed, numErrors: result?.issues.length || result?.errors, cached: result?.cached, + perf: result?.perf, }), ); diff --git a/packages/cspell/src/app/options.ts b/packages/cspell/src/app/options.ts index 7cc1d72e6379..c2e7b147b61f 100644 --- a/packages/cspell/src/app/options.ts +++ b/packages/cspell/src/app/options.ts @@ -228,6 +228,11 @@ export interface LinterCliOptions extends LinterOptions { * Generate a summary report of issues. */ issuesSummaryReport?: boolean; + + /** + * Generate a summary report of performance. + */ + showPerfSummary?: boolean; } export function fixLegacy(opts: T & LegacyOptions): Omit { diff --git a/packages/cspell/src/app/util/fileHelper.ts b/packages/cspell/src/app/util/fileHelper.ts index 3fa7c28c6ff5..7213f7be39f4 100644 --- a/packages/cspell/src/app/util/fileHelper.ts +++ b/packages/cspell/src/app/util/fileHelper.ts @@ -47,7 +47,7 @@ export interface FileInfo { errorCode?: string; } -type Perf = Record; +export type Perf = cspell.SpellCheckFilePerf; export interface FileResult { fileInfo: FileInfo;