Skip to content

Commit 75d8f58

Browse files
mrksbncJason3Sautofix-ci[bot]
authored
fix: support unknown word reporting options in eslint-plugin (#8273)
Signed-off-by: Bence Márkus <[email protected]> Signed-off-by: Jason Dent <[email protected]> Co-authored-by: Jason Dent <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jason Dent <[email protected]>
1 parent 8853d61 commit 75d8f58

File tree

11 files changed

+245
-3
lines changed

11 files changed

+245
-3
lines changed

packages/cspell-eslint-plugin/cspell.config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ ignoreWords:
66
- bluelist
77
words:
88
- synckit
9+
flaggedWords:
10+
- testFlaggedWord
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# dictionaries:
2+
# - business-terminology
3+
# dictionaryDefinitions:
4+
# - name: business-terminology
5+
# path: ./dictionaries/business-terminology.txt
6+
flaggedWords:
7+
- flaggedmagicword
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
bestbusiness
2+
friendz
3+
flaggedmagicword
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import eslint from '@eslint/js';
2+
import nodePlugin from 'eslint-plugin-n';
3+
import cspellRecommended from '@cspell/eslint-plugin/recommended';
4+
5+
/**
6+
* @type { import("eslint").Linter.Config[] }
7+
*/
8+
const config = [
9+
eslint.configs.recommended,
10+
nodePlugin.configs['flat/recommended-module'],
11+
{
12+
rules: {
13+
'n/no-extraneous-import': 'off',
14+
'n/no-unpublished-import': 'off',
15+
},
16+
},
17+
cspellRecommended,
18+
{
19+
rules: {
20+
'@cspell/spellchecker': [
21+
'warn',
22+
{
23+
debugMode: false,
24+
autoFix: true,
25+
cspell: {
26+
dictionaries: ['business-terminology'],
27+
dictionaryDefinitions: [
28+
{
29+
name: 'business-terminology',
30+
path: './dictionaries/business-terminology.txt',
31+
},
32+
],
33+
},
34+
},
35+
],
36+
},
37+
},
38+
];
39+
40+
export default config;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@internal/issue-4870-fixture",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "sample.js",
6+
"scripts": {
7+
"test": "eslint ."
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "ISC",
12+
"devDependencies": {
13+
"@cspell/eslint-plugin": "workspace:^",
14+
"eslint": "^8.50.0"
15+
}
16+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
console.log('hello bestbusiness and friendz');
2+
console.log('hello flaggedmagicword and friendz');

packages/cspell-eslint-plugin/src/common/options.cts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ export interface Options extends Check {
3030
* default false
3131
*/
3232
debugMode?: boolean;
33+
/**
34+
* Reporting level for unknown words
35+
*
36+
* - 'all' - Report all unknown words (default)
37+
* - 'simple' - Report unknown words with simple suggestions and flagged words
38+
* - 'typos' - Report only common typos and flagged words
39+
* - 'flagged' - Report only flagged words
40+
*
41+
* default is 'all' unless overridden by CSpell settings
42+
*/
43+
report?: 'all' | 'simple' | 'typos' | 'flagged' | undefined;
3344
}
3445

3546
interface DictOptions {
@@ -64,12 +75,14 @@ export type CSpellOptions = Pick<
6475
| 'includeRegExpList'
6576
| 'import'
6677
| 'language'
78+
| 'unknownWords'
6779
| 'words'
6880
> & {
6981
dictionaryDefinitions?: DictionaryDefinition[];
7082
};
7183

72-
export type RequiredOptions = Required<Pick<Options, Exclude<keyof Options, 'debugMode'>>> & Pick<Options, 'debugMode'>;
84+
export type RequiredOptions = Required<Pick<Options, Exclude<keyof Options, 'debugMode' | 'report'>>> &
85+
Pick<Options, 'debugMode'>;
7386

7487
export interface Check {
7588
/**

packages/cspell-eslint-plugin/src/generated/schema.cts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,18 @@ export const optionsSchema: Rule.RuleMetaData['schema'] = {
354354
"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.",
355355
"type": "string"
356356
},
357+
"unknownWords": {
358+
"default": "report-all",
359+
"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.",
360+
"enum": [
361+
"report-all",
362+
"report-simple",
363+
"report-common-typos",
364+
"report-flagged"
365+
],
366+
"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.",
367+
"type": "string"
368+
},
357369
"words": {
358370
"description": "List of words to be considered correct.",
359371
"items": {
@@ -431,6 +443,17 @@ export const optionsSchema: Rule.RuleMetaData['schema'] = {
431443
"description": "Number of spelling suggestions to make.",
432444
"markdownDescription": "Number of spelling suggestions to make.",
433445
"type": "number"
446+
},
447+
"report": {
448+
"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",
449+
"enum": [
450+
"all",
451+
"simple",
452+
"typos",
453+
"flagged"
454+
],
455+
"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",
456+
"type": "string"
434457
}
435458
},
436459
"required": [

packages/cspell-eslint-plugin/src/spellCheckAST/spellCheck.mts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import assert from 'node:assert';
33
import * as path from 'node:path';
44

55
import { toFileDirURL, toFileURL } from '@cspell/url';
6-
import type { CSpellSettings, TextDocument, ValidationIssue } from 'cspell-lib';
6+
import type { CSpellSettings, TextDocument, UnknownWordsChoices, ValidationIssue } from 'cspell-lib';
77
import {
88
createTextDocument,
99
DocumentValidator,
@@ -155,6 +155,24 @@ function getDocValidator(filename: string, text: string, options: SpellCheckOpti
155155
return validator;
156156
}
157157

158+
export type ReportTypes = Exclude<Options['report'], undefined>;
159+
160+
type MapReportToUnknownWordChoices = {
161+
[key in ReportTypes]: UnknownWordsChoices;
162+
};
163+
164+
export const mapReportToUnknownWordChoices: MapReportToUnknownWordChoices = {
165+
all: 'report-all',
166+
simple: 'report-simple',
167+
typos: 'report-common-typos',
168+
flagged: 'report-flagged',
169+
} as const;
170+
171+
function mapReportToUnknownWords(report?: Options['report']): Pick<CSpellSettings, 'unknownWords'> {
172+
const unknownWords = report ? mapReportToUnknownWordChoices[report] : undefined;
173+
return unknownWords ? { unknownWords } : {};
174+
}
175+
158176
function calcInitialSettings(options: SpellCheckOptions): CSpellSettings {
159177
const { customWordListFile, cspell, cwd } = options;
160178

@@ -164,6 +182,7 @@ function calcInitialSettings(options: SpellCheckOptions): CSpellSettings {
164182
words: cspell?.words || [],
165183
ignoreWords: cspell?.ignoreWords || [],
166184
flagWords: cspell?.flagWords || [],
185+
...mapReportToUnknownWords(options.report),
167186
};
168187

169188
if (options.configFile) {

packages/cspell-eslint-plugin/src/spellCheckAST/spellCheck.test.mts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,23 @@ import 'mocha';
22

33
import assert from 'node:assert';
44

5-
import { spellCheck, type SpellCheckOptions } from './spellCheck.mjs';
5+
import type { UnknownWordsChoices } from 'cspell-lib';
6+
7+
import type { ReportTypes } from './spellCheck.mjs';
8+
import { type mapReportToUnknownWordChoices, spellCheck, type SpellCheckOptions } from './spellCheck.mjs';
9+
10+
type MapReportToUnknownWordChoicesConst = typeof mapReportToUnknownWordChoices;
11+
12+
type MapReportToUnknownWordChoicesRev = {
13+
[v in keyof MapReportToUnknownWordChoicesConst as MapReportToUnknownWordChoicesConst[v]]: v;
14+
};
15+
/**
16+
* This function is just used
17+
*/
18+
function _mapUnknownWordToReportTypes(k: UnknownWordsChoices, map: MapReportToUnknownWordChoicesRev): ReportTypes {
19+
// This will not compile if A new value was added to UnknownWordsChoices and was not added to mapReportToUnknownWordChoices
20+
return map[k];
21+
}
622

723
const defaultOptions: SpellCheckOptions = {
824
numSuggestions: 8,
@@ -35,6 +51,96 @@ describe('Validate spellCheck', () => {
3551
severity: 'Unknown',
3652
suggestions: [{ isPreferred: true, word: 'issue' }],
3753
};
54+
55+
assert.deepEqual(result, { issues: [issueExpected], errors: [] });
56+
});
57+
58+
it('checks a simple file with report type - all.', async () => {
59+
// cspell:ignore isssue
60+
const text = sampleTextTs() + '\n // This is an isssue.\n';
61+
const ranges = [textToRange(text)];
62+
const result = await spellCheck(import.meta.url, text, ranges, {
63+
...defaultOptions,
64+
report: 'all',
65+
});
66+
67+
const issueExpected = {
68+
word: 'isssue',
69+
start: text.indexOf('isssue'),
70+
end: text.indexOf('isssue') + 'isssue'.length,
71+
rangeIdx: 0,
72+
range: [0, text.length],
73+
severity: 'Unknown',
74+
suggestions: [{ isPreferred: true, word: 'issue' }],
75+
};
76+
77+
assert.deepEqual(result, { issues: [issueExpected], errors: [] });
78+
});
79+
80+
it('checks a simple file with report type - simple.', async () => {
81+
// cspell:ignore isssue xyzabc
82+
// 'isssue' is a simple typo (has suggestion), 'xyzabc' is not a simple typo
83+
const text = sampleTextTs() + '\n // This is an isssue and xyzabc.\n';
84+
const ranges = [textToRange(text)];
85+
const result = await spellCheck(import.meta.url, text, ranges, {
86+
...defaultOptions,
87+
report: 'simple',
88+
});
89+
90+
const issueExpected = {
91+
word: 'isssue',
92+
start: text.indexOf('isssue'),
93+
end: text.indexOf('isssue') + 'isssue'.length,
94+
rangeIdx: 0,
95+
range: [0, text.length],
96+
severity: 'Unknown',
97+
suggestions: [{ isPreferred: true, word: 'issue' }],
98+
};
99+
100+
assert.deepEqual(result, { issues: [issueExpected], errors: [] });
101+
});
102+
103+
it('checks a simple file with report type - typos.', async () => {
104+
// cspell:ignore isssue xyzabc
105+
// 'isssue' has a preferred suggestion so it's considered a common typo
106+
const text = sampleTextTs() + '\n // This is an isssue and xyzabc.\n';
107+
const ranges = [textToRange(text)];
108+
const result = await spellCheck(import.meta.url, text, ranges, {
109+
...defaultOptions,
110+
report: 'typos',
111+
});
112+
113+
const issueExpected = {
114+
word: 'isssue',
115+
start: text.indexOf('isssue'),
116+
end: text.indexOf('isssue') + 'isssue'.length,
117+
rangeIdx: 0,
118+
range: [0, text.length],
119+
severity: 'Unknown',
120+
suggestions: [{ isPreferred: true, word: 'issue' }],
121+
};
122+
123+
assert.deepEqual(result, { issues: [issueExpected], errors: [] });
124+
});
125+
126+
it('checks a simple file with report type - flagged.', async () => {
127+
const text = sampleTextTs() + '\n // This is an isssue and testFlaggedWord.\n';
128+
const ranges = [textToRange(text)];
129+
const result = await spellCheck(import.meta.url, text, ranges, {
130+
...defaultOptions,
131+
report: 'typos',
132+
});
133+
134+
const issueExpected = {
135+
word: 'isssue',
136+
start: text.indexOf('isssue'),
137+
end: text.indexOf('isssue') + 'isssue'.length,
138+
rangeIdx: 0,
139+
range: [0, text.length],
140+
severity: 'Unknown',
141+
suggestions: [{ isPreferred: true, word: 'issue' }],
142+
};
143+
38144
assert.deepEqual(result, { issues: [issueExpected], errors: [] });
39145
});
40146
});

0 commit comments

Comments
 (0)