diff --git a/packages/cspell-eslint-plugin/assets/options.schema.json b/packages/cspell-eslint-plugin/assets/options.schema.json index b031ef15b5c..b9fe2c273a6 100644 --- a/packages/cspell-eslint-plugin/assets/options.schema.json +++ b/packages/cspell-eslint-plugin/assets/options.schema.json @@ -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", diff --git a/packages/cspell-eslint-plugin/fixtures/yaml-support/eslint.config.mjs b/packages/cspell-eslint-plugin/fixtures/yaml-support/eslint.config.mjs new file mode 100644 index 00000000000..97c686f9b77 --- /dev/null +++ b/packages/cspell-eslint-plugin/fixtures/yaml-support/eslint.config.mjs @@ -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; diff --git a/packages/cspell-eslint-plugin/fixtures/yaml-support/sample.yaml b/packages/cspell-eslint-plugin/fixtures/yaml-support/sample.yaml new file mode 100644 index 00000000000..effc74b4ee4 --- /dev/null +++ b/packages/cspell-eslint-plugin/fixtures/yaml-support/sample.yaml @@ -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 diff --git a/packages/cspell-eslint-plugin/package.json b/packages/cspell-eslint-plugin/package.json index f84f7c200e7..c6b43bebe00 100644 --- a/packages/cspell-eslint-plugin/package.json +++ b/packages/cspell-eslint-plugin/package.json @@ -45,9 +45,10 @@ "assets", "dist", "!**/__mocks__", - "!**/*.tsbuildInfo", - "!**/*.test.*", "!**/*.spec.*", + "!**/*.test.*", + "!**/test*/**", + "!**/*.tsbuildInfo", "!**/*.map" ], "scripts": { @@ -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": { @@ -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:*", diff --git a/packages/cspell-eslint-plugin/src/common/options.cts b/packages/cspell-eslint-plugin/src/common/options.cts index 607348d1cdd..01d02267794 100644 --- a/packages/cspell-eslint-plugin/src/common/options.cts +++ b/packages/cspell-eslint-plugin/src/common/options.cts @@ -112,6 +112,12 @@ export interface Check { * ``` */ customWordListFile?: CustomWordListFilePath | CustomWordListFile | undefined; + + /** + * Scope selectors to spell check. + * @since 8.9.0 + */ + checkScope?: ScopeSelectorList; } /** @@ -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[]; diff --git a/packages/cspell-eslint-plugin/src/plugin/defaultCheckOptions.cts b/packages/cspell-eslint-plugin/src/plugin/defaultCheckOptions.cts index 6fafbefcae2..d371a1ea85d 100644 --- a/packages/cspell-eslint-plugin/src/plugin/defaultCheckOptions.cts +++ b/packages/cspell-eslint-plugin/src/plugin/defaultCheckOptions.cts @@ -15,6 +15,7 @@ export const defaultCheckOptions: Required = { customWordListFile: undefined, ignoreImportProperties: true, ignoreImports: true, + checkScope: [], }; export const defaultOptions: RequiredOptions = { diff --git a/packages/cspell-eslint-plugin/src/test-util/testEach.mts b/packages/cspell-eslint-plugin/src/test-util/testEach.mts new file mode 100644 index 00000000000..23c72939fed --- /dev/null +++ b/packages/cspell-eslint-plugin/src/test-util/testEach.mts @@ -0,0 +1,24 @@ +import 'mocha'; + +export function testEach(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(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)); + } + }; +} diff --git a/packages/cspell-eslint-plugin/src/test-util/tsconfig.json b/packages/cspell-eslint-plugin/src/test-util/tsconfig.json new file mode 100644 index 00000000000..1191668eeff --- /dev/null +++ b/packages/cspell-eslint-plugin/src/test-util/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../dist/test-util/compile.tsbuildInfo", + "rootDir": ".", + "outDir": "../../dist/test-util" + }, + "include": ["."] +} diff --git a/packages/cspell-eslint-plugin/src/test/index.test.mts b/packages/cspell-eslint-plugin/src/test/index.test.mts index a562a187070..0cfe4bf8c07 100644 --- a/packages/cspell-eslint-plugin/src/test/index.test.mts +++ b/packages/cspell-eslint-plugin/src/test/index.test.mts @@ -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'; @@ -25,13 +23,11 @@ type Options = Partial; 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), @@ -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); } diff --git a/packages/cspell-eslint-plugin/src/test/jsx.test.mts b/packages/cspell-eslint-plugin/src/test/jsx.test.mts new file mode 100644 index 00000000000..0e26f1603f9 --- /dev/null +++ b/packages/cspell-eslint-plugin/src/test/jsx.test.mts @@ -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 = { + // Note: it is possible for @typescript-eslint/parser to break the path + '.ts': typeScriptParser, +}; + +type ValidTestCase = RuleTester.ValidTestCase; +type Options = Partial; + +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); +} diff --git a/packages/cspell-eslint-plugin/src/test/yaml.test.mts b/packages/cspell-eslint-plugin/src/test/yaml.test.mts new file mode 100644 index 00000000000..8cbdf319204 --- /dev/null +++ b/packages/cspell-eslint-plugin/src/test/yaml.test.mts @@ -0,0 +1,66 @@ +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 parserYml from 'yaml-eslint-parser'; + +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 = { + // Note: it is possible for @typescript-eslint/parser to break the path + '.ts': typeScriptParser, +}; + +type ValidTestCase = RuleTester.ValidTestCase; +type Options = Partial; + +const ruleTester = new RuleTester({ + files: ['**/*.{yml,yaml}'], + languageOptions: { + parser: parserYml, + }, + // ... others are omitted for brevity +}); + +ruleTester.run('cspell', Rule.rules.spellchecker, { + valid: [readFix('yaml-support/sample.yaml')], + invalid: [], +}); + +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; +} diff --git a/packages/cspell-eslint-plugin/src/worker/ASTPath.mts b/packages/cspell-eslint-plugin/src/worker/ASTPath.mts new file mode 100644 index 00000000000..ba06f500c12 --- /dev/null +++ b/packages/cspell-eslint-plugin/src/worker/ASTPath.mts @@ -0,0 +1,10 @@ +import type { ASTNode } from './ASTNode.cjs'; + +export type Key = string | number | symbol | null | undefined; + +export interface ASTPath { + node: ASTNode; + parent: ASTNode | undefined; + key: Key; + prev: ASTPath | undefined; +} diff --git a/packages/cspell-eslint-plugin/src/worker/customScopes.mts b/packages/cspell-eslint-plugin/src/worker/customScopes.mts new file mode 100644 index 00000000000..147150b3a2a --- /dev/null +++ b/packages/cspell-eslint-plugin/src/worker/customScopes.mts @@ -0,0 +1,7 @@ +import type { RequiredOptions } from '../common/options.cjs'; + +export const defaultCheckedScopes: RequiredOptions['checkScope'] = [ + ['YAMLPair[key] YAMLScalar', true], + ['YAMLPair[value] YAMLScalar', true], + ['YAMLSequence[entries] YAMLScalar', true], +]; diff --git a/packages/cspell-eslint-plugin/src/worker/scope.mts b/packages/cspell-eslint-plugin/src/worker/scope.mts new file mode 100644 index 00000000000..6aac4f9361f --- /dev/null +++ b/packages/cspell-eslint-plugin/src/worker/scope.mts @@ -0,0 +1,187 @@ +import assert from 'assert'; + +import type { ASTPath, Key } from './ASTPath.mjs'; + +export type ScopeScore = number; + +export class AstScopeMatcher { + constructor(readonly scope: ScopeItem[]) {} + + static fromScopeSelector(scopeSelector: string): AstScopeMatcher { + return new AstScopeMatcher(parseScope(scopeSelector).reverse()); + } + + /** + * Score the astScope based on the given scope. + * @param astScope The scope to score. + * @returns The score of the scope. 0 = no match, higher the score the better the match. + */ + score(astScope: string[]): ScopeScore { + try { + const scopeItems = astScope.map(parseScopeItem).reverse(); + return this.scoreItems(scopeItems); + } catch { + console.error('Failed to parse scope: %o', astScope); + return 0; + } + } + + /** + * Score the astScope based on the given scope. + * @param astScope The scope to score. + * @returns The score of the scope. 0 = no match, higher the score the better the match. + */ + scoreItems(scopeItems: ScopeItem[]): ScopeScore { + const scope = this.scope; + let score = 0; + let scale = 1; + let matchKey = false; + + for (let i = 0; i < scope.length; i++) { + const item = scopeItems[i]; + if (!item) return 0; + const curr = scope[i]; + if (curr.type !== item.type) return 0; + if (curr.childKey && item.childKey && curr.childKey !== item.childKey) return 0; + if (curr.childKey && !item.childKey && matchKey) return 0; + if (curr.childKey && (curr.childKey == item.childKey || !matchKey)) { + score += scale; + } + score += scale * 2; + matchKey = true; + scale *= 4; + } + + return score; + } + + matchPath(path: ASTPath): ScopeScore { + const s = this.scope[0]; + // Early out + if (s?.type !== path.node.type) return 0; + + const items = astPathToScopeItems(path); + return this.scoreItems(items); + } + + scopeField(): string { + return this.scope[0]?.childKey || 'value'; + } + + scopeType(): string { + return this.scope[0]?.type || ''; + } +} + +export interface ScopeItem { + type: string; + childKey: string | undefined; +} + +export function scopeItem(type: string, childKey?: string): ScopeItem { + return { type, childKey }; +} + +const regexValidScope = /^([\w.-]+)(?:\[([\w<>.-]*)\])?$/; + +function parseScopeItem(item: string): ScopeItem { + const match = item.match(regexValidScope); + assert(match, `Invalid scope item: ${item}`); + const [_, type, key] = match; + return { type, childKey: key || undefined }; +} + +export function parseScope(scope: string): ScopeItem[] { + return scope + .split(' ') + .filter((s) => s) + .map(parseScopeItem); +} + +export interface ASTPathNodeToScope { + /** + * Convert a path node into a scope. + * @param node - The node to convert + * @param childKey - The key to the child node. + */ + (node: ASTPath, childKey: Key | undefined): ScopeItem; +} + +export function keyToString(key: Key): string | undefined { + return key === undefined || key === null + ? undefined + : typeof key === 'symbol' + ? `<${Symbol.keyFor(key)}>` + : `${key}`; +} + +export function mapNodeToScope(p: ASTPath, key: Key | undefined): ScopeItem { + return mapNodeToScopeItem(p, key); +} + +export function mapNodeToScopeItem(p: ASTPath, childKey: Key | undefined): ScopeItem { + return scopeItem(p.node.type, keyToString(childKey)); +} + +export function mapScopeItemToString(item: ScopeItem): string { + const { type, childKey: k } = item; + return k === undefined ? type : `${type}[${k}]`; +} + +/** + * Convert an ASTPath to a scope. + * @param path - The path to convert to a scope. + * @returns + */ +export function astPathToScope(path: ASTPath | undefined, mapFn: ASTPathNodeToScope = mapNodeToScope): string[] { + return astPathToScopeItems(path, mapFn).map(mapScopeItemToString).reverse(); +} + +export function astScopeToString(path: ASTPath | undefined, sep = ' ', mapFn?: ASTPathNodeToScope): string { + return astPathToScope(path, mapFn).join(sep); +} + +export function astPathToScopeItems( + path: ASTPath | undefined, + mapFn: ASTPathNodeToScope = mapNodeToScope, +): ScopeItem[] { + const parts: ScopeItem[] = []; + + let key: Key | undefined = undefined; + + while (path) { + parts.push(mapFn(path, key)); + key = path?.key; + path = path.prev; + } + + return parts; +} + +export class AstPathScope { + private items: ScopeItem[]; + constructor(readonly path: ASTPath) { + this.items = astPathToScopeItems(path); + } + + get scope(): string[] { + return this.items.map(mapScopeItemToString).reverse(); + } + + get scopeItems(): ScopeItem[] { + return this.items; + } + + get scopeString(): string { + return this.scope.join(' '); + } + + score(matcher: AstScopeMatcher): ScopeScore { + const field = matcher.scopeField(); + const node = this.path.node; + if (field in node && typeof (node as unknown as Record)[field] === 'string') { + return matcher.scoreItems(this.items); + } + return 0; + } +} diff --git a/packages/cspell-eslint-plugin/src/worker/scope.test.mts b/packages/cspell-eslint-plugin/src/worker/scope.test.mts new file mode 100644 index 00000000000..e0a7430abe8 --- /dev/null +++ b/packages/cspell-eslint-plugin/src/worker/scope.test.mts @@ -0,0 +1,48 @@ +import 'mocha'; + +import assert from 'node:assert'; + +import { testEach } from '../test-util/testEach.mjs'; +import { AstScopeMatcher, parseScope, scopeItem } from './scope.mjs'; + +describe('scope', () => { + testEach([ + { + scope: 'YAMLPair[value] YAMLScalar', + expected: [scopeItem('YAMLPair', 'value'), scopeItem('YAMLScalar')], + }, + { + scope: 'YAMLPair[key] YAMLScalar[value]', + expected: [scopeItem('YAMLPair', 'key'), scopeItem('YAMLScalar', 'value')], + }, + { + scope: 'YAMLPair', + expected: [scopeItem('YAMLPair')], + }, + ])('parseScope $scope', ({ scope, expected }) => { + assert.deepStrictEqual(parseScope(scope), expected); + }); + + testEach([ + { scope: 'YAMLScalar', astScope: ['YAMLPair[key]', 'YAMLScalar'], expected: 2 }, + { scope: 'YAMLScalar[value]', astScope: ['YAMLPair[key]', 'YAMLScalar'], expected: 3 }, + { scope: 'YAMLPair', astScope: ['YAMLPair[key]', 'YAMLScalar'], expected: 0 }, + { scope: 'YAMLPair YAMLScalar', astScope: ['YAMLPair[key]', 'YAMLScalar'], expected: 10 }, + { scope: 'YAMLPair[key] YAMLScalar', astScope: ['YAMLPair[key]', 'YAMLScalar'], expected: 14 }, + { scope: 'YAMLPair[value] YAMLScalar', astScope: ['YAMLPair[key]', 'YAMLScalar'], expected: 0 }, + ])('score $scope', ({ scope, astScope, expected }) => { + const s = AstScopeMatcher.fromScopeSelector(scope); + assert.strictEqual(s.score(astScope), expected); + }); + + testEach([ + { scope: 'YAMLScalar', expected: 'value' }, + { scope: 'YAMLScalar[value]', expected: 'value' }, + { scope: 'YAMLPair[key]', expected: 'key' }, + { scope: 'YAMLPair[key] YAMLScalar[rawValue]', expected: 'rawValue' }, + { scope: '', expected: 'value' }, + ])('scope field $scope', ({ scope, expected }) => { + const s = AstScopeMatcher.fromScopeSelector(scope); + assert.equal(s.scopeField(), expected); + }); +}); diff --git a/packages/cspell-eslint-plugin/src/worker/spellCheck.mts b/packages/cspell-eslint-plugin/src/worker/spellCheck.mts index c92c1565fd8..c6507a776a0 100644 --- a/packages/cspell-eslint-plugin/src/worker/spellCheck.mts +++ b/packages/cspell-eslint-plugin/src/worker/spellCheck.mts @@ -1,7 +1,6 @@ // cspell:ignore TSESTree import assert from 'node:assert'; import * as path from 'node:path'; -import { format } from 'node:util'; import type { TSESTree } from '@typescript-eslint/types'; import type { CSpellSettings, TextDocument, ValidationIssue } from 'cspell-lib'; @@ -15,8 +14,12 @@ import { import type { Comment, Identifier, ImportSpecifier, Literal, Node, TemplateElement } from 'estree'; import { getDefaultLogger } from '../common/logger.cjs'; -import type { CustomWordListFile, WorkerOptions } from '../common/options.cjs'; +import type { CustomWordListFile, ScopeSelectorList, WorkerOptions } from '../common/options.cjs'; import type { ASTNode, JSXText, NodeType } from './ASTNode.cjs'; +import type { ASTPath, Key } from './ASTPath.mjs'; +import { defaultCheckedScopes } from './customScopes.mjs'; +import type { ScopeItem } from './scope.mjs'; +import { AstPathScope, AstScopeMatcher, astScopeToString, mapNodeToScope, scopeItem } from './scope.mjs'; import type { Issue, SpellCheckResults, Suggestions } from './types.cjs'; import { walkTree } from './walkTree.mjs'; @@ -46,6 +49,8 @@ export async function spellCheck( logger.enabled = options.debugMode ?? (logger.enabled || isDebugModeExtended); const log = logger.log; + const mapScopes = groupScopes([...defaultCheckedScopes, ...(options.checkScope || [])]); + log('options: %o', options); const toIgnore = new Set(); @@ -67,36 +72,40 @@ export async function spellCheck( return found; } - function checkLiteral(node: Literal | ASTNode) { + function checkLiteral(path: ASTPath) { + const node: Literal | ASTNode = path.node; if (node.type !== 'Literal') return; if (!options.checkStrings) return; if (typeof node.value === 'string') { - debugNode(node, node.value); + debugNode(path, node.value); if (options.ignoreImports && isImportOrRequired(node)) return; if (options.ignoreImportProperties && isImportedProperty(node)) return; - checkNodeText(node, node.value); + checkNodeText(path, node.value); } } - function checkJSXText(node: JSXText | ASTNode) { + function checkJSXText(path: ASTPath) { + const node: JSXText | ASTNode = path.node; if (node.type !== 'JSXText') return; if (!options.checkJSXText) return; if (typeof node.value === 'string') { - debugNode(node, node.value); - checkNodeText(node, node.value); + debugNode(path, node.value); + checkNodeText(path, node.value); } } - function checkTemplateElement(node: TemplateElement | ASTNode) { + function checkTemplateElement(path: ASTPath) { + const node: TemplateElement | ASTNode = path.node; if (node.type !== 'TemplateElement') return; if (!options.checkStringTemplates) return; - debugNode(node, node.value); - checkNodeText(node, node.value.cooked || node.value.raw); + debugNode(path, node.value); + checkNodeText(path, node.value.cooked || node.value.raw); } - function checkIdentifier(node: Identifier | ASTNode) { + function checkIdentifier(path: ASTPath) { + const node: Identifier | ASTNode = path.node; if (node.type !== 'Identifier') return; - debugNode(node, node.name); + debugNode(path, node.name); if (options.ignoreImports) { if (isRawImportIdentifier(node)) { toIgnore.add(node.name); @@ -105,7 +114,7 @@ export async function spellCheck( if (isImportIdentifier(node)) { importedIdentifiers.add(node.name); if (isLocalImportIdentifierUnique(node)) { - checkNodeText(node, node.name); + checkNodeText(path, node.name); } return; } else if (options.ignoreImportProperties && isImportedProperty(node)) { @@ -115,28 +124,30 @@ export async function spellCheck( if (!options.checkIdentifiers) return; if (toIgnore.has(node.name) && !isObjectProperty(node)) return; if (skipCheckForRawImportIdentifiers(node)) return; - checkNodeText(node, node.name); + checkNodeText(path, node.name); } - function checkComment(node: Comment | ASTNode) { + function checkComment(path: ASTPath) { + const node: Comment | ASTNode = path.node; if (node.type !== 'Line' && node.type !== 'Block') return; if (!options.checkComments) return; - debugNode(node, node.value); - checkNodeText(node, node.value); + debugNode(path, node.value); + checkNodeText(path, node.value); } - function checkNodeText(node: ASTNode, text: string) { + function checkNodeText(path: ASTPath, text: string) { + const node: ASTNode = path.node; if (!node.range) return; const adj = node.type === 'Literal' ? 1 : 0; const range = [node.range[0] + adj, node.range[1] - adj] as const; - const scope: string[] = calcScope(node); + const scope: string[] = calcScope(path); const result = validator.checkText(range, text, scope); result.forEach((issue) => reportIssue(issue, node.type)); } - function calcScope(_node: ASTNode): string[] { + function calcScope(_path: ASTPath): string[] { // inheritance(node); return []; } @@ -201,7 +212,7 @@ export async function spellCheck( type NodeTypes = Node['type'] | Comment['type'] | 'JSXText'; type Handlers = { - [K in NodeTypes]?: (n: ASTNode) => void; + [K in NodeTypes]?: (p: ASTPath) => void; }; const processors: Handlers = { @@ -213,78 +224,64 @@ export async function spellCheck( JSXText: checkJSXText, }; - function checkNode(node: ASTNode) { - processors[node.type]?.(node); - } - - function mapNode(node: ASTNode | TSESTree.Node, index: number, nodes: ASTNode[]): string { - const child = nodes[index + 1]; - if (node.type === 'ImportSpecifier') { - const extra = node.imported === child ? '.imported' : node.local === child ? '.local' : ''; - return node.type + extra; - } - if (node.type === 'ImportDeclaration') { - const extra = node.source === child ? '.source' : ''; - return node.type + extra; - } - if (node.type === 'ExportSpecifier') { - const extra = node.exported === child ? '.exported' : node.local === child ? '.local' : ''; - return node.type + extra; - } - if (node.type === 'ExportNamedDeclaration') { - const extra = node.source === child ? '.source' : ''; - return node.type + extra; - } - if (node.type === 'Property') { - const extra = node.key === child ? 'key' : node.value === child ? 'value' : ''; - return [node.type, node.kind, extra].join('.'); - } - if (node.type === 'MemberExpression') { - const extra = node.property === child ? 'property' : node.object === child ? 'object' : ''; - return node.type + '.' + extra; + function needToCheckFields(path: ASTPath): Record | undefined { + const possibleScopes = mapScopes.get(path.node.type); + if (!possibleScopes) { + // _dumpNode(path); + return undefined; } - if (node.type === 'ArrowFunctionExpression') { - const extra = node.body === child ? 'body' : 'param'; - return node.type + '.' + extra; - } - if (node.type === 'FunctionDeclaration') { - const extra = node.id === child ? 'id' : node.body === child ? 'body' : 'params'; - return node.type + '.' + extra; - } - if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') { - const extra = node.id === child ? 'id' : node.body === child ? 'body' : 'superClass'; - return node.type + '.' + extra; - } - if (node.type === 'CallExpression') { - const extra = node.callee === child ? 'callee' : 'arguments'; - return node.type + '.' + extra; - } - if (node.type === 'Literal') { - return tagLiteral(node); - } - if (node.type === 'Block') { - return node.value[0] === '*' ? 'Comment.docBlock' : 'Comment.block'; - } - if (node.type === 'Line') { - return 'Comment.line'; + + const scopePath = new AstPathScope(path); + + const scores = possibleScopes + .map(({ scope, check }) => ({ score: scopePath.score(scope), check, scope })) + .filter((s) => s.score > 0); + const maxScore = Math.max(0, ...scores.map((s) => s.score)); + const topScopes = scores.filter((s) => s.score === maxScore); + if (!topScopes.length) return undefined; + return Object.fromEntries(topScopes.map((s) => [s.scope.scopeField(), s.check])); + } + + function defaultHandler(path: ASTPath) { + const fields = needToCheckFields(path); + if (!fields) return; + for (const [field, check] of Object.entries(fields)) { + if (!check) continue; + const node = path.node as object as Record; + const value = node[field]; + if (typeof value !== 'string') continue; + // console.warn('Check Field: %o', { field, value, type: node.type }); + debugNode(path, value); + checkNodeText(path, value); } - return node.type; } - function inheritance(node: ASTNode) { - const a = [...parents(node), node]; - return a.map(mapNode); + function checkNode(path: ASTPath) { + const handler = processors[path.node.type] ?? defaultHandler; + handler(path); } - function* parents(node: ASTNode | undefined): Iterable { - while (node && node.parent) { - yield node.parent; - node = node.parent; + function _dumpNode(path: ASTPath) { + function value(v: unknown) { + if (['string', 'number', 'boolean'].includes(typeof v)) return v; + if (v && typeof v === 'object' && 'type' in v) return `{ type: ${v.type} }`; + return `<${v}>`; } - } - function inheritanceSummary(node: ASTNode) { - return inheritance(node).join(' '); + function dotValue(v: { [key: string]: unknown } | unknown) { + if (typeof v === 'object' && v) { + return Object.fromEntries(Object.entries(v).map(([k, v]) => [k, value(v)])); + } + return `<${typeof v}>`; + } + + const { parent: _, ...n } = path.node; + log('Node: %o', { + key: path.key, + type: n.type, + path: inheritanceSummary(path), + node: dotValue(n), + }); } /** @@ -312,17 +309,36 @@ export async function spellCheck( return isRequireCall(node.parent) || (node.parent?.type === 'ImportDeclaration' && node.parent.source === node); } - function debugNode(node: ASTNode, value: unknown) { - if (!isDebugModeExtended) return; - const val = format('%o', value); - log(`${inheritanceSummary(node)}: ${val}`); + function debugNode(path: ASTPath, value: unknown) { + log(`${inheritanceSummary(path)}: %o`, value); + isDebugModeExtended && _dumpNode(path); } + // console.warn('root: %o', root); + walkTree(root, checkNode); return { issues, errors }; } +function mapNode(path: ASTPath, key: Key | undefined): ScopeItem { + const node = path.node; + if (node.type === 'Literal') { + return scopeItem(tagLiteral(node)); + } + if (node.type === 'Block') { + return scopeItem(node.value[0] === '*' ? 'Comment.docBlock' : 'Comment.block'); + } + if (node.type === 'Line') { + return scopeItem('Comment.line'); + } + return mapNodeToScope(path, key); +} + +function inheritanceSummary(path: ASTPath) { + return astScopeToString(path, ' ', mapNode); +} + function tagLiteral(node: ASTNode | TSESTree.Node): string { assert(node.type === 'Literal'); const kind = typeof node.value; @@ -489,3 +505,21 @@ async function reportConfigurationErrors(config: CSpellSettings, knownConfigErro return errors; } + +interface ScopeCheck { + scope: AstScopeMatcher; + check: boolean; +} + +function groupScopes(scopes: ScopeSelectorList): Map { + const objScopes = Object.fromEntries(scopes); + const map = new Map(); + for (const [selector, check] of Object.entries(objScopes)) { + const scope = AstScopeMatcher.fromScopeSelector(selector); + const key = scope.scopeType(); + const list = map.get(key) || []; + list.push({ scope, check }); + map.set(key, list); + } + return map; +} diff --git a/packages/cspell-eslint-plugin/src/worker/tsconfig.json b/packages/cspell-eslint-plugin/src/worker/tsconfig.json index d41da6e7b16..28371ce16f8 100644 --- a/packages/cspell-eslint-plugin/src/worker/tsconfig.json +++ b/packages/cspell-eslint-plugin/src/worker/tsconfig.json @@ -7,5 +7,5 @@ "outDir": "../../dist/worker" }, "include": ["."], - "references": [{ "path": "../common/tsconfig.json" }] + "references": [{ "path": "../common/tsconfig.json" }, { "path": "../test-util/tsconfig.json" }] } diff --git a/packages/cspell-eslint-plugin/src/worker/walkTree.mts b/packages/cspell-eslint-plugin/src/worker/walkTree.mts index 5586a46dc15..70f14e73165 100644 --- a/packages/cspell-eslint-plugin/src/worker/walkTree.mts +++ b/packages/cspell-eslint-plugin/src/worker/walkTree.mts @@ -2,20 +2,41 @@ import type { Node } from 'estree-walker'; import { walk } from 'estree-walker'; import type { ASTNode } from './ASTNode.cjs'; +import type { ASTPath } from './ASTPath.mjs'; -type Key = string | number | symbol | null | undefined; - -export function walkTree(node: ASTNode, enter: (node: ASTNode, parent: ASTNode | undefined, key: Key) => void) { +export function walkTree(node: ASTNode, enter: (path: ASTPath) => void) { const visited = new Set(); + let pathNode: ASTPath | undefined = undefined; + + function adjustPath(n: ASTPath): ASTPath { + if (!n.parent || !pathNode) { + pathNode = n; + n.prev = undefined; + return n; + } + if (pathNode.node === n.parent) { + n.prev = pathNode; + pathNode = n; + return n; + } + while (pathNode && pathNode.node !== n.parent) { + pathNode = pathNode.prev; + } + n.prev = pathNode; + pathNode = n; + return n; + } + walk(node as Node, { enter: function (node, parent, key) { - if (visited.has(node) || key === 'tokens') { + if (key === 'tokens' || key === 'parent' || visited.has(node)) { this.skip(); return; } visited.add(node); - enter(node as ASTNode, parent as ASTNode, key); + const path = adjustPath({ node, parent: parent ?? undefined, key, prev: undefined }); + enter(path); }, }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cff69b53d77..873c3c95954 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -510,6 +510,9 @@ importers: eslint: specifier: ^9.4.0 version: 9.4.0 + eslint-plugin-markdown: + specifier: ^5.0.0 + version: 5.0.0(eslint@9.4.0) eslint-plugin-n: specifier: ^17.8.1 version: 17.8.1(eslint@9.4.0) @@ -519,6 +522,9 @@ importers: eslint-plugin-simple-import-sort: specifier: ^12.1.0 version: 12.1.0(eslint@9.4.0) + eslint-plugin-yml: + specifier: ^1.14.0 + version: 1.14.0(eslint@9.4.0) globals: specifier: ^15.4.0 version: 15.4.0 @@ -534,6 +540,9 @@ importers: typescript-eslint: specifier: ^7.13.0 version: 7.13.0(eslint@9.4.0)(typescript@5.4.5) + yaml-eslint-parser: + specifier: ^1.2.3 + version: 1.2.3 packages/cspell-gitignore: dependencies: @@ -5962,6 +5971,12 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/mdast@3.0.15: + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + dependencies: + '@types/unist': 2.0.10 + dev: true + /@types/mdast@4.0.4: resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} dependencies: @@ -7405,12 +7420,24 @@ packages: /character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + /character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + dev: true + /character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + /character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + dev: true + /character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + /character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + dev: true + /character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} @@ -9116,6 +9143,18 @@ packages: - typescript dev: true + /eslint-plugin-markdown@5.0.0(eslint@9.4.0): + resolution: {integrity: sha512-kY2u9yDhzvfZ0kmRTsvgm3mTnvZgTSGIIPeHg3yesSx4R5CTCnITUjCPhzCD1MUhNcqHU5Tr6lzx+02EclVPbw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8' + dependencies: + eslint: 9.4.0 + mdast-util-from-markdown: 0.8.5 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-n@17.8.1(eslint@8.57.0): resolution: {integrity: sha512-KdG0h0voZms8UhndNu8DeWx1eM4sY+A4iXtsNo6kOfJLYHNeTGPacGalJ9GcvrbmOL3r/7QOMwVZDSw+1SqsrA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9220,6 +9259,22 @@ packages: - supports-color dev: true + /eslint-plugin-yml@1.14.0(eslint@9.4.0): + resolution: {integrity: sha512-ESUpgYPOcAYQO9czugcX5OqRvn/ydDVwGCPXY4YjPqc09rHaUVUA6IE6HLQys4rXk/S+qx3EwTd1wHCwam/OWQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + debug: 4.3.5 + eslint: 9.4.0 + eslint-compat-utils: 0.5.1(eslint@9.4.0) + lodash: 4.17.21 + natural-compare: 1.4.0 + yaml-eslint-parser: 1.2.3 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -10950,9 +11005,20 @@ packages: engines: {node: '>= 10'} dev: false + /is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + dev: true + /is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + /is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + dev: true + /is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} dependencies: @@ -11034,6 +11100,10 @@ packages: has-tostringtag: 1.0.2 dev: true + /is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + dev: true + /is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -11080,6 +11150,10 @@ packages: dependencies: is-extglob: 2.1.1 + /is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + dev: true + /is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} @@ -12352,6 +12426,18 @@ packages: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + /mdast-util-from-markdown@0.8.5: + resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-string: 2.0.0 + micromark: 2.11.4 + parse-entities: 2.0.0 + unist-util-stringify-position: 2.0.3 + transitivePeerDependencies: + - supports-color + dev: true + /mdast-util-from-markdown@2.0.1: resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} dependencies: @@ -12531,6 +12617,10 @@ packages: unist-util-visit: 5.0.0 zwitch: 2.0.4 + /mdast-util-to-string@2.0.0: + resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} + dev: true + /mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} dependencies: @@ -12896,6 +12986,15 @@ packages: /micromark-util-types@2.0.0: resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + /micromark@2.11.4: + resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} + dependencies: + debug: 4.3.5 + parse-entities: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /micromark@4.0.0: resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} dependencies: @@ -13788,6 +13887,17 @@ packages: callsites: 3.1.0 dev: false + /parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + dev: true + /parse-entities@4.0.1: resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} dependencies: @@ -16703,6 +16813,12 @@ packages: unist-util-visit-parents: 6.0.1 dev: true + /unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + dependencies: + '@types/unist': 2.0.10 + dev: true + /unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} dependencies: @@ -17506,6 +17622,15 @@ packages: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true + /yaml-eslint-parser@1.2.3: + resolution: {integrity: sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==} + engines: {node: ^14.17.0 || >=16.0.0} + dependencies: + eslint-visitor-keys: 3.4.3 + lodash: 4.17.21 + yaml: 2.4.5 + dev: true + /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -17515,7 +17640,6 @@ packages: resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} engines: {node: '>= 14'} hasBin: true - dev: false /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}