• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

streetsidesoftware / cspell / 8745810937

18 Apr 2024 11:05PM UTC coverage: 93.481% (+0.02%) from 93.46%
8745810937

Pull #5502

github

web-flow
Merge 53f2b8079 into c515cc91c
Pull Request #5502: chore: Add lint rules

6439 of 7332 branches covered (87.82%)

126 of 144 new or added lines in 68 files covered. (87.5%)

3 existing lines in 3 files now uncovered.

13192 of 14112 relevant lines covered (93.48%)

23103.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.09
/packages/cspell-lib/src/lib/Settings/InDocSettings.ts
1
import { opAppend, opFilter, opMap, pipeSync } from '@cspell/cspell-pipe/sync';
2
import type { CSpellUserSettings, DictionaryDefinitionInline } from '@cspell/cspell-types';
3
import type { Sequence } from 'gensequence';
4
import { genSequence } from 'gensequence';
5

6
import type { ExtendedSuggestion } from '../Models/Suggestion.js';
7
import { createSpellingDictionary } from '../SpellingDictionary/index.js';
8
import * as Text from '../util/text.js';
9
import { clean, isDefined } from '../util/util.js';
10
import { mergeInDocSettings } from './CSpellSettingsServer.js';
11

12
// cspell:ignore gimuy
13
const regExMatchRegEx = /\/.*\/[gimuy]*/;
15✔
14
const regExCSpellInDocDirective = /\b(?:spell-?checker|c?spell)::?(.*)/gi;
15✔
15
const regExCSpellDirectiveKey = /(?<=\b(?:spell-?checker|c?spell)::?)(?!:)(.*)/i;
15✔
16
const regExInFileSettings = [regExCSpellInDocDirective, /\b(LocalWords:?.*)/g];
15✔
17

18
export type CSpellUserSettingsKeys = keyof CSpellUserSettings;
19

20
const officialDirectives = [
15✔
21
    'enable',
22
    'disable',
23
    'disable-line',
24
    'disable-next',
25
    'disable-next-line',
26
    'word',
27
    'words',
28
    'ignore',
29
    'ignoreWord',
30
    'ignoreWords',
31
    'ignore-word',
32
    'ignore-words',
33
    'includeRegExp',
34
    'ignoreRegExp',
35
    'local', // Do not suggest.
36
    'locale',
37
    'language',
38
    'dictionaries',
39
    'dictionary',
40
    'forbid',
41
    'forbidWord',
42
    'forbid-word',
43
    'flag',
44
    'flagWord',
45
    'flag-word',
46
    'enableCompoundWords',
47
    'enableAllowCompoundWords',
48
    'disableCompoundWords',
49
    'disableAllowCompoundWords',
50
    'enableCaseSensitive',
51
    'disableCaseSensitive',
52
];
53

54
const noSuggestDirectives = new Set(['local']);
15✔
55

56
const preferredDirectives = [
15✔
57
    'enable',
58
    'disable',
59
    'disable-line',
60
    'disable-next-line',
61
    'words',
62
    'ignore',
63
    'forbid',
64
    'locale',
65
    'dictionary',
66
    'dictionaries',
67
    'enableCaseSensitive',
68
    'disableCaseSensitive',
69
];
70

71
const allDirectives = new Set([...preferredDirectives, ...officialDirectives]);
15✔
72
const allDirectiveSuggestions: ExtendedSuggestion[] = [
15✔
73
    ...pipeSync(
74
        allDirectives,
75
        opMap((word) => ({ word })),
465✔
76
    ),
77
];
78

79
const dictInDocSettings = createSpellingDictionary(allDirectives, 'Directives', 'Directive List', {
15✔
80
    supportNonStrictSearches: false,
81
});
82

83
const EmptyWords: string[] = [];
15✔
84
Object.freeze(EmptyWords);
15✔
85

86
const staticInDocumentDictionaryName = `[in-document-dict]`;
15✔
87

88
export interface DirectiveIssue {
89
    /**
90
     * the start and end offsets within the document of the issue.
91
     */
92
    range: [start: number, end: number];
93
    /**
94
     * The text causing the issue.
95
     */
96
    text: string;
97
    message: string;
98
    suggestions: string[];
99
    suggestionsEx: ExtendedSuggestion[];
100
}
101

102
export function getInDocumentSettings(text: string): CSpellUserSettings {
103
    const collectedSettings = getPossibleInDocSettings(text)
134✔
104
        .concatMap((a) => parseSettingMatch(a))
217✔
105
        .reduce(
106
            (s, setting) => {
107
                return mergeInDocSettings(s, setting);
199✔
108
            },
109
            { id: 'in-doc-settings' } as CSpellUserSettings,
110
        );
111
    const {
112
        words,
113
        flagWords,
114
        ignoreWords,
115
        suggestWords,
116
        dictionaries = [],
125✔
117
        dictionaryDefinitions = [],
134✔
118
        ...rest
119
    } = collectedSettings;
134✔
120
    const dict: DictionaryDefinitionInline | undefined =
134✔
121
        (words || flagWords || ignoreWords || suggestWords) &&
122
        clean({
123
            name: staticInDocumentDictionaryName,
124
            words,
125
            flagWords,
126
            ignoreWords,
127
            suggestWords,
128
        });
129

130
    const dictSettings = dict
134✔
131
        ? {
132
              dictionaries: [...dictionaries, staticInDocumentDictionaryName],
133
              dictionaryDefinitions: [...dictionaryDefinitions, dict],
134
          }
135
        : clean({
136
              dictionaries: dictionaries.length ? dictionaries : undefined,
102!
137
              dictionaryDefinitions: dictionaryDefinitions.length ? dictionaryDefinitions : undefined,
102!
138
          });
139

140
    const settings = {
134✔
141
        ...rest,
142
        ...dictSettings,
143
    };
144
    // console.log('InDocSettings: %o', settings);
145
    return settings;
134✔
146
}
147

148
export function validateInDocumentSettings(docText: string, _settings: CSpellUserSettings): Iterable<DirectiveIssue> {
149
    return pipeSync(getPossibleInDocSettings(docText), opMap(parseSettingMatchValidation), opFilter(isDefined));
40✔
150
}
151

152
const settingParsers: readonly (readonly [RegExp, (m: string) => CSpellUserSettings])[] = [
15✔
153
    [/^(?:enable|disable)(?:allow)?CompoundWords\b(?!-)/i, parseCompoundWords],
154
    [/^(?:enable|disable)CaseSensitive\b(?!-)/i, parseCaseSensitive],
155
    [/^enable\b(?!-)/i, parseEnable],
156
    [/^disable(-line|-next(-line)?)?\b(?!-)/i, parseDisable],
157
    [/^words?\b(?!-)/i, parseWords],
158
    [/^ignore(?:-?words?)?\b(?!-)/i, parseIgnoreWords],
159
    [/^(?:flag|forbid)(?:-?words?)?\b(?!-)/i, parseFlagWords],
160
    [/^ignore_?Reg_?Exp\s+.+$/i, parseIgnoreRegExp],
161
    [/^include_?Reg_?Exp\s+.+$/i, parseIncludeRegExp],
162
    [/^locale?\b(?!-)/i, parseLocale],
163
    [/^language\s\b(?!-)/i, parseLocale],
164
    [/^dictionar(?:y|ies)\b(?!-)/i, parseDictionaries], // cspell:disable-line
165
    [/^LocalWords:/, (w) => parseWords(w.replaceAll(/^LocalWords:?/gi, ' '))],
13✔
166
] as const;
167

168
export const regExSpellingGuardBlock =
169
    /(\bc?spell(?:-?checker)?::?)\s*disable(?!-line|-next)\b[\s\S]*?((?:\1\s*enable\b)|$)/gi;
15✔
170
export const regExSpellingGuardNext = /\bc?spell(?:-?checker)?::?\s*disable-next\b.*\s\s?.*/gi;
15✔
171
export const regExSpellingGuardLine = /^.*\bc?spell(?:-?checker)?::?\s*disable-line\b.*/gim;
15✔
172

173
const emptySettings: CSpellUserSettings = Object.freeze({});
15✔
174

175
const issueMessages = {
15✔
176
    unknownDirective: 'Unknown CSpell directive',
177
} as const;
178

179
function parseSettingMatchValidation(matchArray: RegExpMatchArray): DirectiveIssue | undefined {
180
    const [fullMatch = ''] = matchArray;
66!
181

182
    const directiveMatch = fullMatch.match(regExCSpellDirectiveKey);
66✔
183
    if (!directiveMatch) return undefined;
66✔
184

185
    const match = directiveMatch[1];
61✔
186
    const possibleSetting = match.trim();
61✔
187
    if (!possibleSetting) return undefined;
61!
188

189
    const start = (matchArray.index || 0) + (directiveMatch.index || 0) + (match.length - match.trimStart().length);
61!
190
    const text = possibleSetting.replace(/^([-\w]+)?.*/, '$1');
61✔
191
    const end = start + text.length;
61✔
192

193
    if (!text) return undefined;
61✔
194

195
    const matchingParsers = settingParsers.filter(([regex]) => regex.test(possibleSetting));
780✔
196
    if (matchingParsers.length > 0) return undefined;
60✔
197

198
    // No matches were found, let make some suggestions.
199
    const dictSugs = dictInDocSettings
18✔
200
        .suggest(text, { ignoreCase: false })
201
        .map(({ word, isPreferred }) => (isPreferred ? { word, isPreferred } : { word }))
36!
202
        .filter((a) => !noSuggestDirectives.has(a.word));
36✔
203
    const sugs = pipeSync(dictSugs, opAppend(allDirectiveSuggestions), filterUniqueSuggestions);
18✔
204
    const suggestionsEx = [...sugs].slice(0, 8);
18✔
205
    const suggestions = suggestionsEx.map((s) => s.word);
144✔
206

207
    const issue: DirectiveIssue = {
18✔
208
        range: [start, end],
209
        text,
210
        message: issueMessages.unknownDirective,
211
        suggestions,
212
        suggestionsEx,
213
    };
214

215
    return issue;
18✔
216
}
217

218
function* filterUniqueSuggestions(sugs: Iterable<ExtendedSuggestion>): Iterable<ExtendedSuggestion> {
219
    const map = new Map<string, ExtendedSuggestion>();
18✔
220

221
    for (const sug of sugs) {
18✔
222
        const existing = map.get(sug.word);
592✔
223
        if (existing && sug.isPreferred) {
592!
NEW
224
            existing.isPreferred = true;
×
225
        }
226
        yield sug;
592✔
227
    }
228
}
229

230
function parseSettingMatch(matchArray: RegExpMatchArray): CSpellUserSettings[] {
231
    const [, match = ''] = matchArray;
217!
232
    const possibleSetting = match.trim();
217✔
233

234
    return settingParsers
217✔
235
        .filter(([regex]) => regex.test(possibleSetting))
2,821✔
236
        .map(([, fn]) => fn)
199✔
237
        .map((fn) => fn(possibleSetting));
199✔
238
}
239

240
function parseCompoundWords(match: string): CSpellUserSettings {
241
    const allowCompoundWords = /enable/i.test(match);
28✔
242
    return { allowCompoundWords };
28✔
243
}
244

245
function parseCaseSensitive(match: string): CSpellUserSettings {
246
    const caseSensitive = /enable/i.test(match);
2✔
247
    return { caseSensitive };
2✔
248
}
249

250
function parseWords(match: string): CSpellUserSettings {
251
    const words = match
69✔
252
        // .replace(/[@#$%^&={}/"]/g, ' ')
253
        .split(/[,\s;]+/g)
254
        .slice(1)
255
        .filter((a) => !!a);
178✔
256
    return { words };
69✔
257
}
258

259
function parseLocale(match: string): CSpellUserSettings {
260
    const parts = match.trim().split(/[\s,]+/);
29✔
261
    const language = parts.slice(1).join(',');
29✔
262
    return language ? { language } : emptySettings;
29✔
263
}
264

265
function parseIgnoreWords(match: string): CSpellUserSettings {
266
    const wordsSetting = parseWords(match);
37✔
267
    const ignoreWords = wordsSetting.words;
37✔
268
    return ignoreWords && ignoreWords.length ? { ignoreWords } : emptySettings;
37!
269
}
270

271
function parseFlagWords(match: string): CSpellUserSettings {
272
    const wordsSetting = parseWords(match);
2✔
273
    const flagWords = wordsSetting.words;
2✔
274
    return flagWords && flagWords.length ? { flagWords } : emptySettings;
2!
275
}
276

277
function parseRegEx(match: string): string[] {
278
    const patterns = [match.replace(/^[^\s]+\s+/, '')].map((a) => {
32✔
279
        const m = a.match(regExMatchRegEx);
32✔
280
        if (m && m[0]) {
32✔
281
            return m[0];
12✔
282
        }
283
        return a.replace(/((?:[^\s]|\\ )+).*/, '$1');
20✔
284
    });
285
    return patterns;
32✔
286
}
287

288
function parseIgnoreRegExp(match: string): CSpellUserSettings {
289
    const ignoreRegExpList = parseRegEx(match);
32✔
290
    return { ignoreRegExpList };
32✔
291
}
292

293
function parseIncludeRegExp(match: string): CSpellUserSettings {
294
    const includeRegExpList = parseRegEx(match);
×
295
    return { includeRegExpList };
×
296
}
297

298
function parseDictionaries(match: string): CSpellUserSettings {
299
    const dictionaries = match.split(/[,\s]+/g).slice(1);
9✔
300
    return { dictionaries };
9✔
301
}
302

303
function getPossibleInDocSettings(text: string): Sequence<RegExpExecArray> {
304
    return genSequence(regExInFileSettings).concatMap((regexp) => Text.match(regexp, text));
350✔
305
}
306

307
function getWordsFromDocument(text: string): string[] {
308
    const dict = extractInDocDictionary(getInDocumentSettings(text));
1✔
309
    return dict?.words || EmptyWords;
1!
310
}
311

312
function parseEnable(_match: string): CSpellUserSettings {
313
    // Do nothing. Enable / Disable is handled in a different way.
314
    return {};
10✔
315
}
316

317
function parseDisable(_match: string): CSpellUserSettings {
318
    // Do nothing. Enable / Disable is handled in a different way.
319
    return {};
20✔
320
}
321

322
export function extractInDocDictionary(settings: CSpellUserSettings): DictionaryDefinitionInline | undefined {
323
    const inDocDicts = settings.dictionaryDefinitions?.filter((def) => def.name === staticInDocumentDictionaryName);
4✔
324
    const dict = inDocDicts?.[0] as DictionaryDefinitionInline;
4✔
325
    return dict;
4✔
326
}
327

328
export function getIgnoreWordsFromDocument(text: string): string[] {
329
    const dict = extractInDocDictionary(getInDocumentSettings(text));
3✔
330
    return dict?.ignoreWords || EmptyWords;
3✔
331
}
332

333
export function getIgnoreRegExpFromDocument(text: string): (string | RegExp)[] {
334
    const { ignoreRegExpList = [] } = getInDocumentSettings(text);
1!
335
    return ignoreRegExpList;
1✔
336
}
337

338
/**
339
 * These internal functions are used exposed for unit testing.
340
 */
341
export const internal = {
15✔
342
    getPossibleInDocSettings,
343
    getWordsFromDocument,
344
    parseWords,
345
    parseCompoundWords,
346
    parseIgnoreRegExp,
347
    parseIgnoreWords,
348
    staticInDocumentDictionaryName,
349
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc