From fda2c704763b2aa681e6285cf7e0a68c7ed99d81 Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 14 Apr 2023 15:20:49 +0200 Subject: [PATCH 1/6] chore(website): validate rule options in editor --- .../src/components/editor/LoadedEditor.tsx | 9 ++- .../website/src/components/lib/jsonSchema.ts | 61 +++++++++++++++---- .../src/components/linter/createLinter.ts | 13 +++- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 994561e7c318..9c728648a17f 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -147,16 +147,23 @@ export const LoadedEditor: React.FC = ({ }, [webLinter, onEsASTChange, onScopeChange, onTsASTChange]); useEffect(() => { + const createRuleUri = (name: string): string => + monaco.Uri.parse(`/rules/${name.replace('@', '')}.json`).toString(); + // configure the JSON language support with schemas and schema associations monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ validate: true, enableSchemaRequest: false, allowComments: true, schemas: [ + ...Array.from(webLinter.rules.values()).map(rule => ({ + uri: createRuleUri(rule.name), + schema: rule.schema, + })), { uri: monaco.Uri.file('eslint-schema.json').toString(), // id of the first schema fileMatch: ['/.eslintrc'], // associate with our model - schema: getEslintJsonSchema(webLinter), + schema: getEslintJsonSchema(webLinter, createRuleUri), }, { uri: monaco.Uri.file('ts-schema.json').toString(), // id of the first schema diff --git a/packages/website/src/components/lib/jsonSchema.ts b/packages/website/src/components/lib/jsonSchema.ts index b43b6ad72cb9..d54f92900dfb 100644 --- a/packages/website/src/components/lib/jsonSchema.ts +++ b/packages/website/src/components/lib/jsonSchema.ts @@ -3,31 +3,70 @@ import type * as ts from 'typescript'; import type { CreateLinter } from '../linter/createLinter'; +const defaultRuleSchema: JSONSchema4 = { + type: ['string', 'number'], + enum: ['off', 'warn', 'error', 0, 1, 2], +}; + /** * Get the JSON schema for the eslint config * Currently we only support the rules and extends */ -export function getEslintJsonSchema(linter: CreateLinter): JSONSchema4 { +export function getEslintJsonSchema( + linter: CreateLinter, + createRef: (name: string) => string, +): JSONSchema4 { const properties: Record = {}; for (const [, item] of linter.rules) { + const ruleRefName = createRef(item.name); + const schemaItemOptions: JSONSchema4[] = [defaultRuleSchema]; + let additionalItems: JSONSchema4 | false = false; + + // if the rule has options, add them to the schema + // if you encounter issues with rule schema validation you can check the schema by using the following code in the console: + // monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.find(item => item.uri === 'file:///rules/typescript-eslint/consistent-type-imports.json') + // monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.filter(item => item.schema.type === 'array') + if (Array.isArray(item.schema)) { + // example @typescript-eslint/consistent-type-imports + for (let index = 0; index < item.schema?.length; ++index) { + schemaItemOptions.push({ + $ref: `${ruleRefName}#/${index}`, + }); + } + } else { + const name = item.schema.items ? 'items' : 'prefixItems'; + const items = item.schema[name] as + | JSONSchema4[] + | JSONSchema4 + | undefined; + if (items) { + if (Array.isArray(items)) { + // example array-element-newline + for (let index = 0; index < items.length; ++index) { + schemaItemOptions.push({ + $ref: `${ruleRefName}#/${name}/${index}`, + }); + } + } else { + // example @typescript-eslint/naming-convention + additionalItems = { + $ref: `${ruleRefName}#/${name}`, + }; + } + } + } + properties[item.name] = { description: `${item.description}\n ${item.url}`, title: item.name.startsWith('@typescript') ? 'Rules' : 'Core rules', default: 'off', oneOf: [ - { - type: ['string', 'number'], - enum: ['off', 'warn', 'error', 0, 1, 2], - }, + defaultRuleSchema, { type: 'array', - items: [ - { - type: ['string', 'number'], - enum: ['off', 'warn', 'error', 0, 1, 2], - }, - ], + items: schemaItemOptions, + additionalItems: additionalItems, }, ], }; diff --git a/packages/website/src/components/linter/createLinter.ts b/packages/website/src/components/linter/createLinter.ts index 813fd10eaf24..30ba20fe1a43 100644 --- a/packages/website/src/components/linter/createLinter.ts +++ b/packages/website/src/components/linter/createLinter.ts @@ -1,5 +1,5 @@ import type * as tsvfs from '@site/src/vendor/typescript-vfs'; -import type { TSESLint } from '@typescript-eslint/utils'; +import type { JSONSchema, TSESLint } from '@typescript-eslint/utils'; import type * as ts from 'typescript'; import { createCompilerOptions } from '../lib/createCompilerOptions'; @@ -15,7 +15,15 @@ import type { } from './types'; export interface CreateLinter { - rules: Map; + rules: Map< + string, + { + name: string; + description?: string; + url?: string; + schema: JSONSchema.JSONSchema4; + } + >; configs: string[]; triggerFix(filename: string): TSESLint.Linter.FixReport | undefined; triggerLint(filename: string): void; @@ -56,6 +64,7 @@ export function createLinter( name: name, description: item.meta?.docs?.description, url: item.meta?.docs?.url, + schema: item.meta?.schema ?? [], }); }); From 01bd2a861a0a9de0b747a991d5d0171870b12093 Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 14 Apr 2023 17:54:57 +0200 Subject: [PATCH 2/6] chore(website): add support for oneOf and anyOf --- .../src/components/editor/LoadedEditor.tsx | 3 +- .../editor/createProvideCodeActions.ts | 4 +- .../website/src/components/lib/jsonSchema.ts | 124 +++++++++++------- 3 files changed, 82 insertions(+), 49 deletions(-) diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 9c728648a17f..4d852ab19698 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -8,6 +8,7 @@ import { createCompilerOptions } from '../lib/createCompilerOptions'; import { debounce } from '../lib/debounce'; import { getEslintJsonSchema, + getRuleJsonSchemaWithErrorLevel, getTypescriptJsonSchema, } from '../lib/jsonSchema'; import { parseTSConfig, tryParseEslintModule } from '../lib/parseConfig'; @@ -158,7 +159,7 @@ export const LoadedEditor: React.FC = ({ schemas: [ ...Array.from(webLinter.rules.values()).map(rule => ({ uri: createRuleUri(rule.name), - schema: rule.schema, + schema: getRuleJsonSchemaWithErrorLevel(rule.schema), })), { uri: monaco.Uri.file('eslint-schema.json').toString(), // id of the first schema diff --git a/packages/website/src/components/editor/createProvideCodeActions.ts b/packages/website/src/components/editor/createProvideCodeActions.ts index 7f26faec7044..a5eae9f849a9 100644 --- a/packages/website/src/components/editor/createProvideCodeActions.ts +++ b/packages/website/src/components/editor/createProvideCodeActions.ts @@ -34,9 +34,9 @@ export function createProvideCodeActions( edits: [ { resource: model.uri, - // @ts-expect-error monaco for ts >= 4.8 + // monaco for ts >= 4.8 textEdit: editOperation, - // monaco for ts < 4.8 + // @ts-expect-error monaco for ts < 4.8 edit: editOperation, }, ], diff --git a/packages/website/src/components/lib/jsonSchema.ts b/packages/website/src/components/lib/jsonSchema.ts index d54f92900dfb..96d62666cf2b 100644 --- a/packages/website/src/components/lib/jsonSchema.ts +++ b/packages/website/src/components/lib/jsonSchema.ts @@ -8,6 +8,83 @@ const defaultRuleSchema: JSONSchema4 = { enum: ['off', 'warn', 'error', 0, 1, 2], }; +/** + * Add the error level to the rule schema items + * + * if you encounter issues with rule schema validation you can check the schema by using the following code in the console: + * monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.find(item => item.uri.includes('typescript-eslint/consistent-type-imports')) + * monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.find(item => item.uri.includes('no-unused-labels')) + * monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.filter(item => item.schema.type === 'array') + */ +export function getRuleJsonSchemaWithErrorLevel( + ruleSchema: JSONSchema4 | JSONSchema4[], +): JSONSchema4 { + if (Array.isArray(ruleSchema)) { + return { + type: 'array', + items: [defaultRuleSchema, ...ruleSchema], + additionalItems: false, + }; + } + // TODO: delete this once we update schemas + // example: ban-ts-comments + if (Array.isArray(ruleSchema.prefixItems)) { + const { prefixItems, ...rest } = ruleSchema; + return { + ...rest, + items: [defaultRuleSchema, ...(prefixItems as JSONSchema4[])], + maxItems: ruleSchema.maxItems ? ruleSchema.maxItems + 1 : undefined, + minItems: ruleSchema.minItems ? ruleSchema.minItems + 1 : 1, + additionalItems: false, + }; + } + // example: @typescript-eslint/explicit-member-accessibility + if (Array.isArray(ruleSchema.items)) { + return { + ...ruleSchema, + items: [defaultRuleSchema, ...ruleSchema.items], + maxItems: ruleSchema.maxItems ? ruleSchema.maxItems + 1 : undefined, + minItems: ruleSchema.minItems ? ruleSchema.minItems + 1 : 1, + additionalItems: false, + }; + } + if (typeof ruleSchema.items === 'object' && ruleSchema.items) { + // this is a workaround for @typescript-eslint/naming-convention rule + if (ruleSchema.items.oneOf) { + return { + ...ruleSchema, + items: [defaultRuleSchema], + additionalItems: ruleSchema.items, + }; + } + // example: @typescript-eslint/padding-line-between-statements + return { + ...ruleSchema, + items: [defaultRuleSchema, ruleSchema.items], + additionalItems: false, + }; + } + // example eqeqeq + if (Array.isArray(ruleSchema.anyOf)) { + return { + ...ruleSchema, + anyOf: ruleSchema.anyOf.map(item => + getRuleJsonSchemaWithErrorLevel(item), + ), + }; + } + // example logical-assignment-operators + if (Array.isArray(ruleSchema.oneOf)) { + return { + ...ruleSchema, + oneOf: ruleSchema.oneOf.map(item => + getRuleJsonSchemaWithErrorLevel(item), + ), + }; + } + return ruleSchema; +} + /** * Get the JSON schema for the eslint config * Currently we only support the rules and extends @@ -19,56 +96,11 @@ export function getEslintJsonSchema( const properties: Record = {}; for (const [, item] of linter.rules) { - const ruleRefName = createRef(item.name); - const schemaItemOptions: JSONSchema4[] = [defaultRuleSchema]; - let additionalItems: JSONSchema4 | false = false; - - // if the rule has options, add them to the schema - // if you encounter issues with rule schema validation you can check the schema by using the following code in the console: - // monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.find(item => item.uri === 'file:///rules/typescript-eslint/consistent-type-imports.json') - // monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.filter(item => item.schema.type === 'array') - if (Array.isArray(item.schema)) { - // example @typescript-eslint/consistent-type-imports - for (let index = 0; index < item.schema?.length; ++index) { - schemaItemOptions.push({ - $ref: `${ruleRefName}#/${index}`, - }); - } - } else { - const name = item.schema.items ? 'items' : 'prefixItems'; - const items = item.schema[name] as - | JSONSchema4[] - | JSONSchema4 - | undefined; - if (items) { - if (Array.isArray(items)) { - // example array-element-newline - for (let index = 0; index < items.length; ++index) { - schemaItemOptions.push({ - $ref: `${ruleRefName}#/${name}/${index}`, - }); - } - } else { - // example @typescript-eslint/naming-convention - additionalItems = { - $ref: `${ruleRefName}#/${name}`, - }; - } - } - } - properties[item.name] = { description: `${item.description}\n ${item.url}`, title: item.name.startsWith('@typescript') ? 'Rules' : 'Core rules', default: 'off', - oneOf: [ - defaultRuleSchema, - { - type: 'array', - items: schemaItemOptions, - additionalItems: additionalItems, - }, - ], + oneOf: [defaultRuleSchema, { $ref: createRef(item.name) }], }; } From b830f93baef4613cb8d543758abf58a9bebe342f Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 14 Apr 2023 19:45:01 +0200 Subject: [PATCH 3/6] chore: add logging for unsupported schemas / invalid, and add fallback --- .../src/components/editor/LoadedEditor.tsx | 2 +- .../website/src/components/lib/jsonSchema.ts | 22 +++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 4d852ab19698..f8c88d3d5f0a 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -159,7 +159,7 @@ export const LoadedEditor: React.FC = ({ schemas: [ ...Array.from(webLinter.rules.values()).map(rule => ({ uri: createRuleUri(rule.name), - schema: getRuleJsonSchemaWithErrorLevel(rule.schema), + schema: getRuleJsonSchemaWithErrorLevel(rule.name, rule.schema), })), { uri: monaco.Uri.file('eslint-schema.json').toString(), // id of the first schema diff --git a/packages/website/src/components/lib/jsonSchema.ts b/packages/website/src/components/lib/jsonSchema.ts index 96d62666cf2b..bab51924e991 100644 --- a/packages/website/src/components/lib/jsonSchema.ts +++ b/packages/website/src/components/lib/jsonSchema.ts @@ -17,17 +17,19 @@ const defaultRuleSchema: JSONSchema4 = { * monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.filter(item => item.schema.type === 'array') */ export function getRuleJsonSchemaWithErrorLevel( + name: string, ruleSchema: JSONSchema4 | JSONSchema4[], ): JSONSchema4 { if (Array.isArray(ruleSchema)) { return { type: 'array', items: [defaultRuleSchema, ...ruleSchema], + minItems: 1, additionalItems: false, }; } // TODO: delete this once we update schemas - // example: ban-ts-comments + // example: ban-ts-comment if (Array.isArray(ruleSchema.prefixItems)) { const { prefixItems, ...rest } = ruleSchema; return { @@ -38,7 +40,7 @@ export function getRuleJsonSchemaWithErrorLevel( additionalItems: false, }; } - // example: @typescript-eslint/explicit-member-accessibility + // example: explicit-member-accessibility if (Array.isArray(ruleSchema.items)) { return { ...ruleSchema, @@ -49,7 +51,7 @@ export function getRuleJsonSchemaWithErrorLevel( }; } if (typeof ruleSchema.items === 'object' && ruleSchema.items) { - // this is a workaround for @typescript-eslint/naming-convention rule + // this is a workaround for naming-convention rule if (ruleSchema.items.oneOf) { return { ...ruleSchema, @@ -57,7 +59,7 @@ export function getRuleJsonSchemaWithErrorLevel( additionalItems: ruleSchema.items, }; } - // example: @typescript-eslint/padding-line-between-statements + // example: padding-line-between-statements return { ...ruleSchema, items: [defaultRuleSchema, ruleSchema.items], @@ -69,7 +71,7 @@ export function getRuleJsonSchemaWithErrorLevel( return { ...ruleSchema, anyOf: ruleSchema.anyOf.map(item => - getRuleJsonSchemaWithErrorLevel(item), + getRuleJsonSchemaWithErrorLevel(name, item), ), }; } @@ -78,11 +80,17 @@ export function getRuleJsonSchemaWithErrorLevel( return { ...ruleSchema, oneOf: ruleSchema.oneOf.map(item => - getRuleJsonSchemaWithErrorLevel(item), + getRuleJsonSchemaWithErrorLevel(name, item), ), }; } - return ruleSchema; + console.log('unsupported rule schema', name, ruleSchema); + return { + type: 'array', + items: [defaultRuleSchema], + minItems: 1, + additionalItems: false, + }; } /** From 7c699d373879f209364fa08ee2fce68b153ad9c9 Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 14 Apr 2023 20:12:32 +0200 Subject: [PATCH 4/6] chore: do not show messages for empty objects --- packages/website/src/components/lib/jsonSchema.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/website/src/components/lib/jsonSchema.ts b/packages/website/src/components/lib/jsonSchema.ts index bab51924e991..7dc094a51fda 100644 --- a/packages/website/src/components/lib/jsonSchema.ts +++ b/packages/website/src/components/lib/jsonSchema.ts @@ -84,7 +84,9 @@ export function getRuleJsonSchemaWithErrorLevel( ), }; } - console.log('unsupported rule schema', name, ruleSchema); + if (typeof ruleSchema !== 'object' || Object.keys(ruleSchema).length) { + console.error('unsupported rule schema', name, ruleSchema); + } return { type: 'array', items: [defaultRuleSchema], From 664cc78f3c0b9ef39b39f0556b14befebfbe5551 Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 14 Apr 2023 23:09:23 +0200 Subject: [PATCH 5/6] fix: remove invalid condition --- packages/website/src/components/lib/jsonSchema.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/website/src/components/lib/jsonSchema.ts b/packages/website/src/components/lib/jsonSchema.ts index 7dc094a51fda..db11292c0847 100644 --- a/packages/website/src/components/lib/jsonSchema.ts +++ b/packages/website/src/components/lib/jsonSchema.ts @@ -51,19 +51,11 @@ export function getRuleJsonSchemaWithErrorLevel( }; } if (typeof ruleSchema.items === 'object' && ruleSchema.items) { - // this is a workaround for naming-convention rule - if (ruleSchema.items.oneOf) { - return { - ...ruleSchema, - items: [defaultRuleSchema], - additionalItems: ruleSchema.items, - }; - } - // example: padding-line-between-statements + // example: naming-convention rule return { ...ruleSchema, - items: [defaultRuleSchema, ruleSchema.items], - additionalItems: false, + items: [defaultRuleSchema], + additionalItems: ruleSchema.items, }; } // example eqeqeq From 3a5b13a531e9e34725559d806237e8aeadff666b Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 16 Apr 2023 19:50:33 +0200 Subject: [PATCH 6/6] fix: filter out invalid error codes --- packages/website/src/components/editor/useSandboxServices.ts | 1 + packages/website/src/components/linter/utils.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 21a7f51ea471..027e12d918cf 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -125,6 +125,7 @@ export const useSandboxServices = ( }; // colorMode and jsx can't be reactive here because we don't want to force a recreation // updating of colorMode and jsx is handled in LoadedEditor + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return services; diff --git a/packages/website/src/components/linter/utils.ts b/packages/website/src/components/linter/utils.ts index e2a4b3ed1c72..474e28cca8fb 100644 --- a/packages/website/src/components/linter/utils.ts +++ b/packages/website/src/components/linter/utils.ts @@ -102,8 +102,9 @@ export function parseMarkers( result[group].items.push({ message: - (marker.owner !== 'eslint' && code ? `${code.value}: ` : '') + - marker.message, + (marker.owner !== 'eslint' && marker.owner !== 'json' && code.value + ? `${code.value}: ` + : '') + marker.message, location: `${marker.startLineNumber}:${marker.startColumn} - ${marker.endLineNumber}:${marker.endColumn}`, severity: marker.severity, fixer: fixers.find(item => item.isPreferred),