diff --git a/README.md b/README.md index 3f144a467..042c769bb 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,10 @@ jobs: # Default: warning inline: warning + # Reports flagged / forbidden words as errors. + # If true, errors will still be reported even if `inline` is "none" + treat_flagged_words_as_errors: false + # Generate Spelling suggestions. suggestions: false diff --git a/action-src/src/ActionParams.ts b/action-src/src/ActionParams.ts index f62ca88eb..de2742119 100644 --- a/action-src/src/ActionParams.ts +++ b/action-src/src/ActionParams.ts @@ -23,6 +23,11 @@ export interface ActionParams { * @default 'warning' */ inline: InlineWorkflowCommand; + /** + * Determines if flagged words should be treated as errors. + * @default 'false' + */ + treat_flagged_words_as_errors: TrueFalse; /** * Determines if the action should be failed if any spelling issues are found. * @@ -65,6 +70,7 @@ const defaultActionParams: ActionParams = { config: '', root: '', inline: 'warning', + treat_flagged_words_as_errors: 'false', strict: 'true', verbose: 'false', check_dot_files: 'explicit', @@ -121,6 +127,7 @@ export function validateActionParams( validateConfig, validateRoot, validateOptions('inline', ['error', 'warning', 'none']), + validateTrueFalse('treat_flagged_words_as_errors'), validateTrueFalse('strict'), validateTrueFalse('incremental_files_only'), validateTrueFalse('verbose'), diff --git a/action-src/src/__snapshots__/action.test.ts.snap b/action-src/src/__snapshots__/action.test.ts.snap index 8d538c1e2..82383f4dd 100644 --- a/action-src/src/__snapshots__/action.test.ts.snap +++ b/action-src/src/__snapshots__/action.test.ts.snap @@ -882,3 +882,300 @@ exports[`Validate Action > check files "'sampleCode/**'" incremental: true sugs: "::error::4 spelling issues found in 1 of the 4 files checked.", ] `; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'error'" 1`] = ` +[ + "fixtures/sampleCode/samples_with_errors/withErrors.ts:5:19 Unknown word (Functon) Suggestions: (functor, function, Function, futon, fulton)", + "fixtures/sampleCode/samples_with_errors/withErrors.ts:5:27 Unknown word (countt) Suggestions: (count, Count, counts, county, cont)", + "fixtures/sampleCode/samples_with_errors/withErrors.ts:9:15 Forbidden word (blacklist) Suggestions: (denylist*, backlist, blocklist, blockList, BlockList)", + "fixtures/sampleCode/samples_with_errors/withErrors.ts:9:53 Misspelled word (colours) Suggestions: (colors*, coolers, color, coors, clouds)", +] +`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'error'" 2`] = `[]`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'error'" 3`] = ` +[ + [ + "Pull Request +", + ], + [ + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=5,col=19::Unknown word (Functon) Suggestions: (functor, function, Function, futon, fulton) +", + ], + [ + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=5,col=27::Unknown word (countt) Suggestions: (count, Count, counts, county, cont) +", + ], + [ + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=9,col=15::Forbidden word (blacklist) Suggestions: (denylist*, backlist, blocklist, blockList, BlockList) +", + ], + [ + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=9,col=53::Misspelled word (colours) Suggestions: (colors*, coolers, color, coors, clouds) +", + ], + [ + "Files checked: 4, Issues found: 4 in 1 files. +", + ], + [ + " +", + ], + [ + "::set-output name=success::false +", + ], + [ + " +", + ], + [ + "::set-output name=number_of_files_checked::4 +", + ], + [ + " +", + ], + [ + "::set-output name=number_of_issues::4 +", + ], + [ + " +", + ], + [ + "::set-output name=number_of_files_with_issues::1 +", + ], + [ + " +", + ], + [ + "::set-output name=files_with_issues::["fixtures/sampleCode/samples_with_errors/withErrors.ts"] +", + ], + [ + " +", + ], + [ + "::set-output name=result::{"success":false,"number_of_issues":4,"number_of_files_checked":4,"files_with_issues":["fixtures/sampleCode/samples_with_errors/withErrors.ts"]} +", + ], +] +`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'error'" 4`] = ` +[ + "Pull Request", + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=5,col=19::Unknown word (Functon) Suggestions: (functor, function, Function, futon, fulton)", + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=5,col=27::Unknown word (countt) Suggestions: (count, Count, counts, county, cont)", + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=9,col=15::Forbidden word (blacklist) Suggestions: (denylist*, backlist, blocklist, blockList, BlockList)", + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=9,col=53::Misspelled word (colours) Suggestions: (colors*, coolers, color, coors, clouds)", + "Files checked: 4, Issues found: 4 in 1 files.", + "::set-output name=success::false", + "::set-output name=number_of_files_checked::4", + "::set-output name=number_of_issues::4", + "::set-output name=number_of_files_with_issues::1", + "::set-output name=files_with_issues::["fixtures/sampleCode/samples_with_errors/withErrors.ts"]", + "::set-output name=result::{"success":false,"number_of_issues":4,"number_of_files_checked":4,"files_with_issues":["fixtures/sampleCode/samples_with_errors/withErrors.ts"]}", +] +`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'none'" 1`] = ` +[ + "fixtures/sampleCode/samples_with_errors/withErrors.ts:9:15 Forbidden word (blacklist) Suggestions: (denylist*, backlist, blocklist, blockList, BlockList)", +] +`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'none'" 2`] = `[]`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'none'" 3`] = ` +[ + [ + "Pull Request +", + ], + [ + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=9,col=15::Forbidden word (blacklist) Suggestions: (denylist*, backlist, blocklist, blockList, BlockList) +", + ], + [ + "Files checked: 4, Issues found: 4 in 1 files. +", + ], + [ + " +", + ], + [ + "::set-output name=success::false +", + ], + [ + " +", + ], + [ + "::set-output name=number_of_files_checked::4 +", + ], + [ + " +", + ], + [ + "::set-output name=number_of_issues::4 +", + ], + [ + " +", + ], + [ + "::set-output name=number_of_files_with_issues::1 +", + ], + [ + " +", + ], + [ + "::set-output name=files_with_issues::["fixtures/sampleCode/samples_with_errors/withErrors.ts"] +", + ], + [ + " +", + ], + [ + "::set-output name=result::{"success":false,"number_of_issues":4,"number_of_files_checked":4,"files_with_issues":["fixtures/sampleCode/samples_with_errors/withErrors.ts"]} +", + ], +] +`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'none'" 4`] = ` +[ + "Pull Request", + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=9,col=15::Forbidden word (blacklist) Suggestions: (denylist*, backlist, blocklist, blockList, BlockList)", + "Files checked: 4, Issues found: 4 in 1 files.", + "::set-output name=success::false", + "::set-output name=number_of_files_checked::4", + "::set-output name=number_of_issues::4", + "::set-output name=number_of_files_with_issues::1", + "::set-output name=files_with_issues::["fixtures/sampleCode/samples_with_errors/withErrors.ts"]", + "::set-output name=result::{"success":false,"number_of_issues":4,"number_of_files_checked":4,"files_with_issues":["fixtures/sampleCode/samples_with_errors/withErrors.ts"]}", +] +`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'warning'" 1`] = ` +[ + "fixtures/sampleCode/samples_with_errors/withErrors.ts:5:19 Unknown word (Functon) Suggestions: (functor, function, Function, futon, fulton)", + "fixtures/sampleCode/samples_with_errors/withErrors.ts:5:27 Unknown word (countt) Suggestions: (count, Count, counts, county, cont)", + "fixtures/sampleCode/samples_with_errors/withErrors.ts:9:15 Forbidden word (blacklist) Suggestions: (denylist*, backlist, blocklist, blockList, BlockList)", + "fixtures/sampleCode/samples_with_errors/withErrors.ts:9:53 Misspelled word (colours) Suggestions: (colors*, coolers, color, coors, clouds)", +] +`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'warning'" 2`] = `[]`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'warning'" 3`] = ` +[ + [ + "Pull Request +", + ], + [ + "::warning file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=5,col=19::Unknown word (Functon) Suggestions: (functor, function, Function, futon, fulton) +", + ], + [ + "::warning file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=5,col=27::Unknown word (countt) Suggestions: (count, Count, counts, county, cont) +", + ], + [ + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=9,col=15::Forbidden word (blacklist) Suggestions: (denylist*, backlist, blocklist, blockList, BlockList) +", + ], + [ + "::warning file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=9,col=53::Misspelled word (colours) Suggestions: (colors*, coolers, color, coors, clouds) +", + ], + [ + "Files checked: 4, Issues found: 4 in 1 files. +", + ], + [ + " +", + ], + [ + "::set-output name=success::false +", + ], + [ + " +", + ], + [ + "::set-output name=number_of_files_checked::4 +", + ], + [ + " +", + ], + [ + "::set-output name=number_of_issues::4 +", + ], + [ + " +", + ], + [ + "::set-output name=number_of_files_with_issues::1 +", + ], + [ + " +", + ], + [ + "::set-output name=files_with_issues::["fixtures/sampleCode/samples_with_errors/withErrors.ts"] +", + ], + [ + " +", + ], + [ + "::set-output name=result::{"success":false,"number_of_issues":4,"number_of_files_checked":4,"files_with_issues":["fixtures/sampleCode/samples_with_errors/withErrors.ts"]} +", + ], +] +`; + +exports[`Validate Action > check files flag errors "'sampleCode/**'" sugs: true 'pull_request_with_files.json', "'warning'" 4`] = ` +[ + "Pull Request", + "::warning file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=5,col=19::Unknown word (Functon) Suggestions: (functor, function, Function, futon, fulton)", + "::warning file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=5,col=27::Unknown word (countt) Suggestions: (count, Count, counts, county, cont)", + "::error file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=9,col=15::Forbidden word (blacklist) Suggestions: (denylist*, backlist, blocklist, blockList, BlockList)", + "::warning file=fixtures/sampleCode/samples_with_errors/withErrors.ts,line=9,col=53::Misspelled word (colours) Suggestions: (colors*, coolers, color, coors, clouds)", + "Files checked: 4, Issues found: 4 in 1 files.", + "::set-output name=success::false", + "::set-output name=number_of_files_checked::4", + "::set-output name=number_of_issues::4", + "::set-output name=number_of_files_with_issues::1", + "::set-output name=files_with_issues::["fixtures/sampleCode/samples_with_errors/withErrors.ts"]", + "::set-output name=result::{"success":false,"number_of_issues":4,"number_of_files_checked":4,"files_with_issues":["fixtures/sampleCode/samples_with_errors/withErrors.ts"]}", +] +`; diff --git a/action-src/src/action.test.ts b/action-src/src/action.test.ts index a10156570..2681ec626 100644 --- a/action-src/src/action.test.ts +++ b/action-src/src/action.test.ts @@ -111,6 +111,34 @@ describe('Validate Action', () => { expect(spyStdout.mock.calls.map((call) => call.join('').trim()).filter((a) => !!a)).toMatchSnapshot(); }, ); + + test.each` + files | suggestions | inline | contextFile | expected + ${'sampleCode/**'} | ${true} | ${'warning'} | ${'pull_request_with_files.json'} | ${false} + ${'sampleCode/**'} | ${true} | ${'error'} | ${'pull_request_with_files.json'} | ${false} + ${'sampleCode/**'} | ${true} | ${'none'} | ${'pull_request_with_files.json'} | ${false} + `( + 'check files flag errors "$files" sugs: $suggestions $contextFile, "$inline"', + async ({ files, suggestions, contextFile, inline, expected }) => { + const warnings: string[] = []; + spyWarn.mockImplementation((msg: string) => warnings.push(msg)); + const params = { + INPUT_FILES: files, + INPUT_INCREMENTAL_FILES_ONLY: 'false', + INPUT_INLINE: inline, + INPUT_ROOT: path.resolve(sourceDir, 'fixtures'), + INPUT_CONFIG: path.resolve(sourceDir, 'fixtures/cspell.json'), + INPUT_SUGGESTIONS: suggestions ? 'true' : 'false', + INPUT_TREAT_FLAGGED_WORDS_AS_ERRORS: 'true', + }; + const context = createContextFromFile(contextFile, params); + await expect(action(context)).resolves.toBe(expected); + expect(warnings).toMatchSnapshot(); + expect(spyLog.mock.calls).toMatchSnapshot(); + expect(spyStdout.mock.calls).toMatchSnapshot(); + expect(spyStdout.mock.calls.map((call) => call.join('').trim()).filter((a) => !!a)).toMatchSnapshot(); + }, + ); }); function cleanEnv() { diff --git a/action-src/src/checkSpelling.ts b/action-src/src/checkSpelling.ts index 887bb150c..99aa0d6c4 100644 --- a/action-src/src/checkSpelling.ts +++ b/action-src/src/checkSpelling.ts @@ -96,6 +96,7 @@ async function checkSpelling( const reporterOptions = { verbose: params.verbose === 'true', + treatFlaggedWordsAsErrors: params.treat_flagged_words_as_errors === 'true', }; const collector = new CSpellReporterForGithubAction(params.inline, reporterOptions, core); diff --git a/action-src/src/getActionParams.ts b/action-src/src/getActionParams.ts index a8cd0b356..b94cf3a9b 100644 --- a/action-src/src/getActionParams.ts +++ b/action-src/src/getActionParams.ts @@ -3,19 +3,21 @@ import { getInput } from '@actions/core'; import { ActionParamsInput, applyDefaults, TrueFalse } from './ActionParams.js'; export function getActionParams(): ActionParamsInput { - return applyDefaults({ + const params: ActionParamsInput = { // github_token: getInput('github_token', { required: true }), files: getInput('files'), incremental_files_only: tf(getInput('incremental_files_only')), config: getInput('config'), root: getInput('root'), inline: getInput('inline').toLowerCase(), + treat_flagged_words_as_errors: tf(getInput('treat_flagged_words_as_errors')), strict: tf(getInput('strict')), verbose: tf(getInput('verbose')), check_dot_files: tf(getInput('check_dot_files')), use_cspell_files: tf(getInput('use_cspell_files')), suggestions: tf(getInput('suggestions')), - }); + }; + return applyDefaults(params); } function tf(v: string | boolean | number): TrueFalse | string { diff --git a/action-src/src/reporter.ts b/action-src/src/reporter.ts index 4cc88fb1d..8ab38bb04 100644 --- a/action-src/src/reporter.ts +++ b/action-src/src/reporter.ts @@ -39,6 +39,7 @@ export type ReportIssueCommand = 'error' | 'warning' | 'none'; export interface ReporterOptions { verbose: boolean; + treatFlaggedWordsAsErrors: boolean; } export class CSpellReporterForGithubAction { @@ -106,23 +107,26 @@ ${error.stack} Object.assign(this.result, result); this.finished = true; const command = this.reportIssueCommand; - - if (!['error', 'warning'].includes(command)) { - return; - } + const errorCommand = this.options.treatFlaggedWordsAsErrors ? 'error' : command; const cwd = process.cwd(); this.issues.forEach((item) => { + const isError = item.isFlagged || false; const hasPreferred = item.suggestionsEx?.some((s) => s.isPreferred) || false; - const msgPrefix = item.isFlagged ? 'Forbidden word' : hasPreferred ? 'Misspelled word' : 'Unknown word'; + const msgPrefix = isError ? 'Forbidden word' : hasPreferred ? 'Misspelled word' : 'Unknown word'; const suggestions = item.suggestionsEx?.map((s) => s.word + (s.isPreferred ? '*' : '')).join(', ') || ''; const sugMsg = suggestions ? ` Suggestions: (${suggestions})` : ''; const message = `${msgPrefix} (${item.text})${sugMsg}`; + const cmd = isError ? errorCommand : command; + + if (!['error', 'warning'].includes(cmd)) { + return; + } // format: ::warning file={name},line={line},col={col}::{message} issueCommand( - command, + cmd, { file: relative(cwd, item.uri || ''), line: item.row, diff --git a/action-src/src/spell.test.ts b/action-src/src/spell.test.ts index 776433174..bf9dda593 100644 --- a/action-src/src/spell.test.ts +++ b/action-src/src/spell.test.ts @@ -8,6 +8,8 @@ import { resolveFile, resolveFiles, root, sourceDir } from './test/helper.js'; const sc = expect.stringContaining; +const rOptions = { verbose: false, treatFlaggedWordsAsErrors: false }; + describe('Validate Spell Checking', () => { test('Linting some files', async () => { const options = { @@ -21,7 +23,7 @@ describe('Validate Spell Checking', () => { info: vi.fn(f), warning: vi.fn(f), }; - const reporter = new CSpellReporterForGithubAction('none', { verbose: false }, logger); + const reporter = new CSpellReporterForGithubAction('none', { ...rOptions, verbose: false }, logger); await spell.lint(['action-src/src/spell.ts', 'fixtures/sampleCode/ts/**/*.ts'], options, reporter.reporter); const r = reporter; expect(r.result.files).toBe(2); @@ -44,7 +46,11 @@ describe('Validate Spell Checking', () => { info: vi.fn(f), warning: vi.fn(f), }; - const reporter = new CSpellReporterForGithubAction('none', { verbose: true }, logger); + const reporter = new CSpellReporterForGithubAction( + 'none', + { verbose: true, treatFlaggedWordsAsErrors: false }, + logger, + ); await spell.lint(['action-src/src/spell.ts', 'fixtures/sampleCode/ts/**/*.ts'], options, reporter.reporter); const r = reporter; expect(r.result.files).toBe(2); @@ -78,7 +84,7 @@ describe('Validate Spell Checking', () => { info: vi.fn((msg) => info.push(msg)), warning: vi.fn(f), }; - const reporter = new CSpellReporterForGithubAction('none', { verbose: true }, logger); + const reporter = new CSpellReporterForGithubAction('none', { ...rOptions, verbose: true }, logger); await spell.lint([glob], options, reporter.reporter); expect(info.sort()).toEqual(expected); }); @@ -124,7 +130,7 @@ describe('Validate Spell Checking', () => { info: vi.fn((msg) => info.push(msg)), warning: vi.fn(f), }; - const reporter = new CSpellReporterForGithubAction('none', { verbose: false }, logger); + const reporter = new CSpellReporterForGithubAction('none', { ...rOptions, verbose: false }, logger); await spell.lint(globs, opts, reporter.reporter); expect(reporter.result).toEqual({ ...defaultResult, ...expected }); }); diff --git a/action.yaml b/action.yaml index 09f15dbea..2b90c816a 100644 --- a/action.yaml +++ b/action.yaml @@ -28,6 +28,11 @@ inputs: Allowed values are: warning, error, none default: warning required: false + treat_flagged_words_as_errors: + description: > + Treat flagged / forbidden words as errors. + Allowed values are: true, false + default: "false" strict: description: | Determines if the action should be failed if any spelling issues are found. diff --git a/action/lib/main_root.cjs b/action/lib/main_root.cjs index e0c1b5415..d8d1fe4f5 100644 --- a/action/lib/main_root.cjs +++ b/action/lib/main_root.cjs @@ -41199,6 +41199,7 @@ var defaultActionParams = { config: "", root: "", inline: "warning", + treat_flagged_words_as_errors: "false", strict: "true", verbose: "false", check_dot_files: "explicit", @@ -41238,6 +41239,7 @@ function validateActionParams(params, logError2) { validateConfig, validateRoot, validateOptions("inline", ["error", "warning", "none"]), + validateTrueFalse("treat_flagged_words_as_errors"), validateTrueFalse("strict"), validateTrueFalse("incremental_files_only"), validateTrueFalse("verbose"), @@ -41734,18 +41736,21 @@ ${error4.stack} Object.assign(this.result, result); this.finished = true; const command = this.reportIssueCommand; - if (!["error", "warning"].includes(command)) { - return; - } + const errorCommand = this.options.treatFlaggedWordsAsErrors ? "error" : command; const cwd = process.cwd(); this.issues.forEach((item) => { + const isError6 = item.isFlagged || false; const hasPreferred = item.suggestionsEx?.some((s) => s.isPreferred) || false; - const msgPrefix = item.isFlagged ? "Forbidden word" : hasPreferred ? "Misspelled word" : "Unknown word"; + const msgPrefix = isError6 ? "Forbidden word" : hasPreferred ? "Misspelled word" : "Unknown word"; const suggestions2 = item.suggestionsEx?.map((s) => s.word + (s.isPreferred ? "*" : "")).join(", ") || ""; const sugMsg = suggestions2 ? ` Suggestions: (${suggestions2})` : ""; const message = `${msgPrefix} (${item.text})${sugMsg}`; + const cmd = isError6 ? errorCommand : command; + if (!["error", "warning"].includes(cmd)) { + return; + } (0, import_command.issueCommand)( - command, + cmd, { file: relative2(cwd, item.uri || ""), line: item.row, @@ -64382,7 +64387,8 @@ async function checkSpelling(params, globs, files) { showSuggestions: params.suggestions === "true" }; const reporterOptions = { - verbose: params.verbose === "true" + verbose: params.verbose === "true", + treatFlaggedWordsAsErrors: params.treat_flagged_words_as_errors === "true" }; const collector = new CSpellReporterForGithubAction(params.inline, reporterOptions, core2); await lint2(globs || [], options, collector.reporter); @@ -64392,19 +64398,21 @@ async function checkSpelling(params, globs, files) { // src/getActionParams.ts var import_core3 = __toESM(require_core(), 1); function getActionParams() { - return applyDefaults({ + const params = { // github_token: getInput('github_token', { required: true }), files: (0, import_core3.getInput)("files"), incremental_files_only: tf((0, import_core3.getInput)("incremental_files_only")), config: (0, import_core3.getInput)("config"), root: (0, import_core3.getInput)("root"), inline: (0, import_core3.getInput)("inline").toLowerCase(), + treat_flagged_words_as_errors: tf((0, import_core3.getInput)("treat_flagged_words_as_errors")), strict: tf((0, import_core3.getInput)("strict")), verbose: tf((0, import_core3.getInput)("verbose")), check_dot_files: tf((0, import_core3.getInput)("check_dot_files")), use_cspell_files: tf((0, import_core3.getInput)("use_cspell_files")), suggestions: tf((0, import_core3.getInput)("suggestions")) - }); + }; + return applyDefaults(params); } function tf(v) { const mapValues = {