Skip to content

Commit

Permalink
feat: Support spell checking more document types
Browse files Browse the repository at this point in the history
fixes #5740
fixes: #3464
  • Loading branch information
Jason3S committed Jun 13, 2024
1 parent acf59a4 commit cea5ce9
Show file tree
Hide file tree
Showing 19 changed files with 855 additions and 137 deletions.
19 changes: 19 additions & 0 deletions packages/cspell-eslint-plugin/assets/options.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@
"description": "Spell check JSX Text",
"type": "boolean"
},
"checkScope": {
"description": "Scope selectors to spell check.",
"items": {
"description": "A scope selector entry is a tuple that defines a scope selector and whether to spell check it.",
"items": [
{
"description": "The scope selector is a string that defines the context in which a rule applies. Examples:\n- `YAMLPair[value] YAMLScalar` - check the value of a YAML pair.\n- `YAMLPair[key] YAMLScalar` - check the key of a YAML pair.",
"type": "string"
},
{
"type": "boolean"
}
],
"maxItems": 2,
"minItems": 2,
"type": "array"
},
"type": "array"
},
"checkStringTemplates": {
"default": true,
"description": "Spell check template strings",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import eslint from '@eslint/js';
import cspellRecommended from '@cspell/eslint-plugin/recommended';
import parserYml from 'yaml-eslint-parser';
import pluginYml from 'eslint-plugin-yml';

/**
* @type { import("eslint").Linter.FlatConfig[] }
*/
const config = [
eslint.configs.recommended,
cspellRecommended,
...pluginYml.configs['flat/standard'],
{
files: ['**/*.yaml', '**/*.yml'],
languageOptions: {
parser: parserYml,
},
// plugins: {
// yml: pluginYml,
// },
rules: {
'@cspell/spellchecker': 'warn',
},
},
];

export default config;
24 changes: 24 additions & 0 deletions packages/cspell-eslint-plugin/fixtures/yaml-support/sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
# Starting comment
list:
- one
- two
- three
- 4
- 5
- true
- false
obj:
key: value
key2: value2 # comment after value
key3: value3
command: |
echo "Hello, World!"
echo "Goodbye, World!"
command2: >-
line 1
line 2
'command three': >
line 3
line 4
# comment
11 changes: 8 additions & 3 deletions packages/cspell-eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@
"assets",
"dist",
"!**/__mocks__",
"!**/*.tsbuildInfo",
"!**/*.test.*",
"!**/*.spec.*",
"!**/*.test.*",
"!**/test*/**",
"!**/*.tsbuildInfo",
"!**/*.map"
],
"scripts": {
Expand All @@ -60,6 +61,7 @@
"clean-build": "pnpm run clean && pnpm run build",
"coverage": "echo coverage",
"test-watch": "pnpm run test -- --watch",
"test-yaml": "npx mocha --timeout 10000 \"dist/**/yaml.test.mjs\"",
"test": "npx mocha --timeout 10000 \"dist/**/*.test.mjs\""
},
"repository": {
Expand All @@ -81,14 +83,17 @@
"@typescript-eslint/parser": "^7.13.0",
"@typescript-eslint/types": "^7.13.0",
"eslint": "^9.4.0",
"eslint-plugin-markdown": "^5.0.0",
"eslint-plugin-n": "^17.8.1",
"eslint-plugin-react": "^7.34.2",
"eslint-plugin-simple-import-sort": "^12.1.0",
"eslint-plugin-yml": "^1.14.0",
"globals": "^15.4.0",
"mocha": "^10.4.0",
"ts-json-schema-generator": "^2.3.0",
"typescript": "^5.4.5",
"typescript-eslint": "^7.13.0"
"typescript-eslint": "^7.13.0",
"yaml-eslint-parser": "^1.2.3"
},
"dependencies": {
"@cspell/cspell-types": "workspace:*",
Expand Down
24 changes: 24 additions & 0 deletions packages/cspell-eslint-plugin/src/common/options.cts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ export interface Check {
* ```
*/
customWordListFile?: CustomWordListFilePath | CustomWordListFile | undefined;

/**
* Scope selectors to spell check.
* @since 8.9.0
*/
checkScope?: ScopeSelectorList;
}

/**
Expand All @@ -134,3 +140,21 @@ export const defaultOptions: Options = {
generateSuggestions: true,
autoFix: false,
};

/**
* The scope selector is a string that defines the context in which a rule applies.
* Examples:
* - `YAMLPair[value] YAMLScalar` - check the value of a YAML pair.
* - `YAMLPair[key] YAMLScalar` - check the key of a YAML pair.
*/
export type ScopeSelector = string;

/**
* A scope selector entry is a tuple that defines a scope selector and whether to spell check it.
*/
export type ScopeSelectorEntry = [ScopeSelector, boolean];

/**
* A list of scope selectors.
*/
export type ScopeSelectorList = ScopeSelectorEntry[];
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const defaultCheckOptions: Required<Check> = {
customWordListFile: undefined,
ignoreImportProperties: true,
ignoreImports: true,
checkScope: [],
};

export const defaultOptions: RequiredOptions = {
Expand Down
24 changes: 24 additions & 0 deletions packages/cspell-eslint-plugin/src/test-util/testEach.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'mocha';

export function testEach<T extends object>(cases: T[]): (title: string, fn: (arg: T) => void) => void {
function fixTitle(title: string, testData: T) {
const parts = title.split(/\b/g);
for (let i = parts.length - 1; i >= 0; i--) {
const prev = parts[i - 1];
if (prev && prev.endsWith('$')) {
parts[i - 1] = prev.slice(0, -1);
parts[i] = '$' + parts[i];
}
}

const map = new Map<string, string>(Object.entries(testData).map(([key, value]) => ['$' + key, `"${value}"`]));

return parts.map((part) => map.get(part) ?? part).join('');
}

return (title, fn) => {
for (const c of cases) {
it(fixTitle(title, c), () => fn(c));
}
};
}
9 changes: 9 additions & 0 deletions packages/cspell-eslint-plugin/src/test-util/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "../../dist/test-util/compile.tsbuildInfo",
"rootDir": ".",
"outDir": "../../dist/test-util"
},
"include": ["."]
}
37 changes: 0 additions & 37 deletions packages/cspell-eslint-plugin/src/test/index.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { fileURLToPath } from 'node:url';

import typeScriptParser from '@typescript-eslint/parser';
import { RuleTester } from 'eslint';
import react from 'eslint-plugin-react';
import globals from 'globals';

import type { Options as RuleOptions } from '../plugin/index.cjs';
import Rule from '../plugin/index.cjs';
Expand All @@ -25,13 +23,11 @@ type Options = Partial<RuleOptions>;
const ruleTester = new RuleTester({});

const KnownErrors: TestCaseError[] = [
ce('Unknown word: "Summmer"', 8),
ce('Unknown word: "friendz"', 8),
ce('Forbidden word: "Bluelist"', 8),
ce('Forbidden word: "bluelist"', 8),
ce('Forbidden word: "café"', 8),
ce('Unknown word: "uuug"', 8),
ce('Unknown word: "Welcomeeeee"', 0),
ce('Unknown word: "bestbusiness"', 0),
ce('Unknown word: "muawhahaha"', 0),
ce('Unknown word: "uuuug"', 0),
Expand Down Expand Up @@ -206,39 +202,6 @@ ruleTester.run('cspell', Rule.rules.spellchecker, {
],
});

const ruleTesterReact = new RuleTester({
files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
plugins: {
react,
},
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
globals: {
...globals.browser,
},
},
// ... others are omitted for brevity
});

ruleTesterReact.run('cspell with React', Rule.rules.spellchecker, {
valid: [readSample('react/sample.jsx'), readSample('react/sample.tsx')],
invalid: [
// cspell:ignore Welcomeeeee Summmer
readInvalid('with-errors/react/sample.jsx', ['Unknown word: "Welcomeeeee"', 'Unknown word: "Summmer"']),
readInvalid('with-errors/react/sample.tsx', ['Unknown word: "Welcomeeeee"', 'Unknown word: "Summmer"']),
readInvalid('with-errors/react/sample.tsx', ['Unknown word: "Summmer"'], {
checkJSXText: false,
}),
readInvalid('with-errors/react/sample.jsx', ['Unknown word: "Summmer"'], {
checkJSXText: false,
}),
],
});

function resolveFix(filename: string): string {
return path.resolve(fixturesDir, filename);
}
Expand Down
125 changes: 125 additions & 0 deletions packages/cspell-eslint-plugin/src/test/jsx.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';

import typeScriptParser from '@typescript-eslint/parser';
import { RuleTester } from 'eslint';
import react from 'eslint-plugin-react';
import globals from 'globals';

import type { Options as RuleOptions } from '../plugin/index.cjs';
import Rule from '../plugin/index.cjs';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const root = path.resolve(__dirname, '../..');
const fixturesDir = path.join(root, 'fixtures');

const parsers: Record<string, string | undefined | unknown> = {
// Note: it is possible for @typescript-eslint/parser to break the path
'.ts': typeScriptParser,
};

type ValidTestCase = RuleTester.ValidTestCase;
type Options = Partial<RuleOptions>;

const KnownErrors: TestCaseError[] = [ce('Unknown word: "Summmer"', 8)];

const ruleTesterReact = new RuleTester({
files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
plugins: {
react,
},
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
globals: {
...globals.browser,
},
},
// ... others are omitted for brevity
});

ruleTesterReact.run('cspell with React', Rule.rules.spellchecker, {
valid: [readSample('react/sample.jsx'), readSample('react/sample.tsx')],
invalid: [
// cspell:ignore Welcomeeeee Summmer
readInvalid('with-errors/react/sample.jsx', ['Unknown word: "Welcomeeeee"', 'Unknown word: "Summmer"']),
readInvalid('with-errors/react/sample.tsx', ['Unknown word: "Welcomeeeee"', 'Unknown word: "Summmer"']),
readInvalid('with-errors/react/sample.tsx', ['Unknown word: "Summmer"'], {
checkJSXText: false,
}),
readInvalid('with-errors/react/sample.jsx', ['Unknown word: "Summmer"'], {
checkJSXText: false,
}),
],
});

function resolveFix(filename: string): string {
return path.resolve(fixturesDir, filename);
}

interface ValidTestCaseEsLint9 extends ValidTestCase {
languageOptions?: {
parser?: unknown;
};
}

function readFix(filename: string, options?: Options): ValidTestCase {
const __filename = resolveFix(filename);
const code = fs.readFileSync(__filename, 'utf8');

const sample: ValidTestCaseEsLint9 = {
code,
filename: __filename,
};
if (options) {
sample.options = [options];
}

const parser = parsers[path.extname(__filename)];
if (parser) {
sample.languageOptions ??= {};
sample.languageOptions.parser = parser;
}

return sample;
}

function readSample(sampleFile: string, options?: Options) {
return readFix(path.join('samples', sampleFile), options);
}

interface TestCaseError {
message?: string | RegExp | undefined;
messageId?: string | undefined;
type?: string | undefined;
data?: unknown | undefined;
line?: number | undefined;
column?: number | undefined;
endLine?: number | undefined;
endColumn?: number | undefined;
suggestions?: RuleTester.SuggestionOutput[] | undefined | number;
}

type InvalidTestCaseError = RuleTester.TestCaseError | TestCaseError | string;

function readInvalid(filename: string, errors: (TestCaseError | string)[], options?: Options) {
const sample = readFix(filename, options);
return {
...sample,
errors: errors.map((err) => csError(err)),
};
}

function ce(message: string, suggestions?: number): RuleTester.TestCaseError {
return { message, suggestions } as RuleTester.TestCaseError;
}

function csError(error: InvalidTestCaseError, suggestions?: number): RuleTester.TestCaseError {
if (error && typeof error === 'object') return error as RuleTester.TestCaseError;
const found = KnownErrors.find((e) => e.message === error) as RuleTester.TestCaseError | undefined;
return found || ce(error, suggestions);
}
Loading

0 comments on commit cea5ce9

Please sign in to comment.