diff --git a/packages/cspell-eslint-plugin/cspell.config.yaml b/packages/cspell-eslint-plugin/cspell.config.yaml index 4260b5126583..9c797cbcb748 100644 --- a/packages/cspell-eslint-plugin/cspell.config.yaml +++ b/packages/cspell-eslint-plugin/cspell.config.yaml @@ -6,3 +6,5 @@ ignoreWords: - bluelist words: - synckit +flaggedWords: + - testFlaggedWord diff --git a/packages/cspell-eslint-plugin/fixtures/issue-8261/cspell.config.yaml b/packages/cspell-eslint-plugin/fixtures/issue-8261/cspell.config.yaml new file mode 100644 index 000000000000..b76449f62426 --- /dev/null +++ b/packages/cspell-eslint-plugin/fixtures/issue-8261/cspell.config.yaml @@ -0,0 +1,7 @@ +# dictionaries: +# - business-terminology +# dictionaryDefinitions: +# - name: business-terminology +# path: ./dictionaries/business-terminology.txt +flaggedWords: + - flaggedmagicword diff --git a/packages/cspell-eslint-plugin/fixtures/issue-8261/dictionaries/business-terminology.txt b/packages/cspell-eslint-plugin/fixtures/issue-8261/dictionaries/business-terminology.txt new file mode 100644 index 000000000000..e3b175eab0ca --- /dev/null +++ b/packages/cspell-eslint-plugin/fixtures/issue-8261/dictionaries/business-terminology.txt @@ -0,0 +1,3 @@ +bestbusiness +friendz +flaggedmagicword diff --git a/packages/cspell-eslint-plugin/fixtures/issue-8261/eslint.config.mjs b/packages/cspell-eslint-plugin/fixtures/issue-8261/eslint.config.mjs new file mode 100644 index 000000000000..879e408e4190 --- /dev/null +++ b/packages/cspell-eslint-plugin/fixtures/issue-8261/eslint.config.mjs @@ -0,0 +1,40 @@ +import eslint from '@eslint/js'; +import nodePlugin from 'eslint-plugin-n'; +import cspellRecommended from '@cspell/eslint-plugin/recommended'; + +/** + * @type { import("eslint").Linter.Config[] } + */ +const config = [ + eslint.configs.recommended, + nodePlugin.configs['flat/recommended-module'], + { + rules: { + 'n/no-extraneous-import': 'off', + 'n/no-unpublished-import': 'off', + }, + }, + cspellRecommended, + { + rules: { + '@cspell/spellchecker': [ + 'warn', + { + debugMode: false, + autoFix: true, + cspell: { + dictionaries: ['business-terminology'], + dictionaryDefinitions: [ + { + name: 'business-terminology', + path: './dictionaries/business-terminology.txt', + }, + ], + }, + }, + ], + }, + }, +]; + +export default config; diff --git a/packages/cspell-eslint-plugin/fixtures/issue-8261/package.json b/packages/cspell-eslint-plugin/fixtures/issue-8261/package.json new file mode 100644 index 000000000000..774efd7aaf77 --- /dev/null +++ b/packages/cspell-eslint-plugin/fixtures/issue-8261/package.json @@ -0,0 +1,16 @@ +{ + "name": "@internal/issue-4870-fixture", + "version": "1.0.0", + "description": "", + "main": "sample.js", + "scripts": { + "test": "eslint ." + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@cspell/eslint-plugin": "workspace:^", + "eslint": "^8.50.0" + } +} diff --git a/packages/cspell-eslint-plugin/fixtures/issue-8261/sample.js b/packages/cspell-eslint-plugin/fixtures/issue-8261/sample.js new file mode 100644 index 000000000000..930a2ed567f1 --- /dev/null +++ b/packages/cspell-eslint-plugin/fixtures/issue-8261/sample.js @@ -0,0 +1,2 @@ +console.log('hello bestbusiness and friendz'); +console.log('hello flaggedmagicword and friendz'); diff --git a/packages/cspell-eslint-plugin/src/common/options.cts b/packages/cspell-eslint-plugin/src/common/options.cts index d1d99712c6e3..aa939cc58644 100644 --- a/packages/cspell-eslint-plugin/src/common/options.cts +++ b/packages/cspell-eslint-plugin/src/common/options.cts @@ -30,6 +30,17 @@ export interface Options extends Check { * default false */ debugMode?: boolean; + /** + * Reporting level for unknown words + * + * - 'all' - Report all unknown words (default) + * - 'simple' - Report unknown words with simple suggestions and flagged words + * - 'typos' - Report only common typos and flagged words + * - 'flagged' - Report only flagged words + * + * default is 'all' unless overridden by CSpell settings + */ + report?: 'all' | 'simple' | 'typos' | 'flagged' | undefined; } interface DictOptions { @@ -64,12 +75,14 @@ export type CSpellOptions = Pick< | 'includeRegExpList' | 'import' | 'language' + | 'unknownWords' | 'words' > & { dictionaryDefinitions?: DictionaryDefinition[]; }; -export type RequiredOptions = Required>> & Pick; +export type RequiredOptions = Required>> & + Pick; export interface Check { /** diff --git a/packages/cspell-eslint-plugin/src/generated/schema.cts b/packages/cspell-eslint-plugin/src/generated/schema.cts index 344784bc4c3a..c3aa728baa01 100644 --- a/packages/cspell-eslint-plugin/src/generated/schema.cts +++ b/packages/cspell-eslint-plugin/src/generated/schema.cts @@ -354,6 +354,18 @@ export const optionsSchema: Rule.RuleMetaData['schema'] = { "markdownDescription": "Current active spelling language. This specifies the language locale to use in choosing the\ngeneral dictionary.\n\nFor example:\n\n- \"en-GB\" for British English.\n- \"en,nl\" to enable both English and Dutch.", "type": "string" }, + "unknownWords": { + "default": "report-all", + "description": "Controls how unknown words are handled.\n\n- `report-all` - Report all unknown words (default behavior)\n- `report-simple` - Report unknown words that have simple spelling errors, typos, and flagged words.\n- `report-common-typos` - Report unknown words that are common typos and flagged words.\n- `report-flagged` - Report unknown words that are flagged.", + "enum": [ + "report-all", + "report-simple", + "report-common-typos", + "report-flagged" + ], + "markdownDescription": "Controls how unknown words are handled.\n\n- `report-all` - Report all unknown words (default behavior)\n- `report-simple` - Report unknown words that have simple spelling errors, typos, and flagged words.\n- `report-common-typos` - Report unknown words that are common typos and flagged words.\n- `report-flagged` - Report unknown words that are flagged.", + "type": "string" + }, "words": { "description": "List of words to be considered correct.", "items": { @@ -431,6 +443,17 @@ export const optionsSchema: Rule.RuleMetaData['schema'] = { "description": "Number of spelling suggestions to make.", "markdownDescription": "Number of spelling suggestions to make.", "type": "number" + }, + "report": { + "description": "Reporting level for unknown words\n\n- 'all' - Report all unknown words (default)\n- 'simple' - Report unknown words with simple suggestions and flagged words\n- 'typos' - Report only common typos and flagged words\n- 'flagged' - Report only flagged words\n\n default is 'all' unless overridden by CSpell settings", + "enum": [ + "all", + "simple", + "typos", + "flagged" + ], + "markdownDescription": "Reporting level for unknown words\n\n- 'all' - Report all unknown words (default)\n- 'simple' - Report unknown words with simple suggestions and flagged words\n- 'typos' - Report only common typos and flagged words\n- 'flagged' - Report only flagged words\n\n default is 'all' unless overridden by CSpell settings", + "type": "string" } }, "required": [ diff --git a/packages/cspell-eslint-plugin/src/spellCheckAST/spellCheck.mts b/packages/cspell-eslint-plugin/src/spellCheckAST/spellCheck.mts index e69705c93c8d..fb3b78a0a956 100644 --- a/packages/cspell-eslint-plugin/src/spellCheckAST/spellCheck.mts +++ b/packages/cspell-eslint-plugin/src/spellCheckAST/spellCheck.mts @@ -3,7 +3,7 @@ import assert from 'node:assert'; import * as path from 'node:path'; import { toFileDirURL, toFileURL } from '@cspell/url'; -import type { CSpellSettings, TextDocument, ValidationIssue } from 'cspell-lib'; +import type { CSpellSettings, TextDocument, UnknownWordsChoices, ValidationIssue } from 'cspell-lib'; import { createTextDocument, DocumentValidator, @@ -155,6 +155,24 @@ function getDocValidator(filename: string, text: string, options: SpellCheckOpti return validator; } +export type ReportTypes = Exclude; + +type MapReportToUnknownWordChoices = { + [key in ReportTypes]: UnknownWordsChoices; +}; + +export const mapReportToUnknownWordChoices: MapReportToUnknownWordChoices = { + all: 'report-all', + simple: 'report-simple', + typos: 'report-common-typos', + flagged: 'report-flagged', +} as const; + +function mapReportToUnknownWords(report?: Options['report']): Pick { + const unknownWords = report ? mapReportToUnknownWordChoices[report] : undefined; + return unknownWords ? { unknownWords } : {}; +} + function calcInitialSettings(options: SpellCheckOptions): CSpellSettings { const { customWordListFile, cspell, cwd } = options; @@ -164,6 +182,7 @@ function calcInitialSettings(options: SpellCheckOptions): CSpellSettings { words: cspell?.words || [], ignoreWords: cspell?.ignoreWords || [], flagWords: cspell?.flagWords || [], + ...mapReportToUnknownWords(options.report), }; if (options.configFile) { diff --git a/packages/cspell-eslint-plugin/src/spellCheckAST/spellCheck.test.mts b/packages/cspell-eslint-plugin/src/spellCheckAST/spellCheck.test.mts index 2cda69fcbef1..5f824685508f 100644 --- a/packages/cspell-eslint-plugin/src/spellCheckAST/spellCheck.test.mts +++ b/packages/cspell-eslint-plugin/src/spellCheckAST/spellCheck.test.mts @@ -2,7 +2,23 @@ import 'mocha'; import assert from 'node:assert'; -import { spellCheck, type SpellCheckOptions } from './spellCheck.mjs'; +import type { UnknownWordsChoices } from 'cspell-lib'; + +import type { ReportTypes } from './spellCheck.mjs'; +import { type mapReportToUnknownWordChoices, spellCheck, type SpellCheckOptions } from './spellCheck.mjs'; + +type MapReportToUnknownWordChoicesConst = typeof mapReportToUnknownWordChoices; + +type MapReportToUnknownWordChoicesRev = { + [v in keyof MapReportToUnknownWordChoicesConst as MapReportToUnknownWordChoicesConst[v]]: v; +}; +/** + * This function is just used + */ +function _mapUnknownWordToReportTypes(k: UnknownWordsChoices, map: MapReportToUnknownWordChoicesRev): ReportTypes { + // This will not compile if A new value was added to UnknownWordsChoices and was not added to mapReportToUnknownWordChoices + return map[k]; +} const defaultOptions: SpellCheckOptions = { numSuggestions: 8, @@ -35,6 +51,96 @@ describe('Validate spellCheck', () => { severity: 'Unknown', suggestions: [{ isPreferred: true, word: 'issue' }], }; + + assert.deepEqual(result, { issues: [issueExpected], errors: [] }); + }); + + it('checks a simple file with report type - all.', async () => { + // cspell:ignore isssue + const text = sampleTextTs() + '\n // This is an isssue.\n'; + const ranges = [textToRange(text)]; + const result = await spellCheck(import.meta.url, text, ranges, { + ...defaultOptions, + report: 'all', + }); + + const issueExpected = { + word: 'isssue', + start: text.indexOf('isssue'), + end: text.indexOf('isssue') + 'isssue'.length, + rangeIdx: 0, + range: [0, text.length], + severity: 'Unknown', + suggestions: [{ isPreferred: true, word: 'issue' }], + }; + + assert.deepEqual(result, { issues: [issueExpected], errors: [] }); + }); + + it('checks a simple file with report type - simple.', async () => { + // cspell:ignore isssue xyzabc + // 'isssue' is a simple typo (has suggestion), 'xyzabc' is not a simple typo + const text = sampleTextTs() + '\n // This is an isssue and xyzabc.\n'; + const ranges = [textToRange(text)]; + const result = await spellCheck(import.meta.url, text, ranges, { + ...defaultOptions, + report: 'simple', + }); + + const issueExpected = { + word: 'isssue', + start: text.indexOf('isssue'), + end: text.indexOf('isssue') + 'isssue'.length, + rangeIdx: 0, + range: [0, text.length], + severity: 'Unknown', + suggestions: [{ isPreferred: true, word: 'issue' }], + }; + + assert.deepEqual(result, { issues: [issueExpected], errors: [] }); + }); + + it('checks a simple file with report type - typos.', async () => { + // cspell:ignore isssue xyzabc + // 'isssue' has a preferred suggestion so it's considered a common typo + const text = sampleTextTs() + '\n // This is an isssue and xyzabc.\n'; + const ranges = [textToRange(text)]; + const result = await spellCheck(import.meta.url, text, ranges, { + ...defaultOptions, + report: 'typos', + }); + + const issueExpected = { + word: 'isssue', + start: text.indexOf('isssue'), + end: text.indexOf('isssue') + 'isssue'.length, + rangeIdx: 0, + range: [0, text.length], + severity: 'Unknown', + suggestions: [{ isPreferred: true, word: 'issue' }], + }; + + assert.deepEqual(result, { issues: [issueExpected], errors: [] }); + }); + + it('checks a simple file with report type - flagged.', async () => { + const text = sampleTextTs() + '\n // This is an isssue and testFlaggedWord.\n'; + const ranges = [textToRange(text)]; + const result = await spellCheck(import.meta.url, text, ranges, { + ...defaultOptions, + report: 'typos', + }); + + const issueExpected = { + word: 'isssue', + start: text.indexOf('isssue'), + end: text.indexOf('isssue') + 'isssue'.length, + rangeIdx: 0, + range: [0, text.length], + severity: 'Unknown', + suggestions: [{ isPreferred: true, word: 'issue' }], + }; + assert.deepEqual(result, { issues: [issueExpected], errors: [] }); }); }); diff --git a/packages/cspell-eslint-plugin/src/test/index.test.mts b/packages/cspell-eslint-plugin/src/test/index.test.mts index 399b408722a1..34de7a8bfb9b 100644 --- a/packages/cspell-eslint-plugin/src/test/index.test.mts +++ b/packages/cspell-eslint-plugin/src/test/index.test.mts @@ -96,6 +96,17 @@ ruleTester.run('cspell', Rule.rules.spellchecker, { ], }, }), + readFix('issue-8261/sample.js', { + cspell: { + dictionaries: ['business-terms'], + dictionaryDefinitions: [ + { + name: 'business-terms', + path: fixtureRelativeToCwd('issue-8261/dictionaries/business-terminology.txt'), + }, + ], + }, + }), ], invalid: [ // cspell:ignore Guuide Gallaxy BADD functionn coool