From d317fbce07461180e1aa3fd9262e70050a6cb141 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Thu, 13 Apr 2023 08:48:52 +0930 Subject: [PATCH] fix(eslint-plugin): refactor schemas that use `$ref` --- .../eslint-plugin/src/rules/array-type.ts | 39 ++--- .../eslint-plugin/src/rules/ban-ts-comment.ts | 67 ++++---- .../eslint-plugin/src/rules/comma-dangle.ts | 19 ++- .../rules/explicit-member-accessibility.ts | 76 ++++----- .../src/rules/func-call-spacing.ts | 1 + .../src/rules/lines-around-comment.ts | 159 +++++++++--------- .../rules/padding-line-between-statements.ts | 1 + .../src/rules/parameter-properties.ts | 57 +++---- .../website/plugins/generated-rule-docs.ts | 74 ++++++-- 9 files changed, 254 insertions(+), 239 deletions(-) diff --git a/packages/eslint-plugin/src/rules/array-type.ts b/packages/eslint-plugin/src/rules/array-type.ts index f353207e7d5d..aef7c9124a1e 100644 --- a/packages/eslint-plugin/src/rules/array-type.ts +++ b/packages/eslint-plugin/src/rules/array-type.ts @@ -105,30 +105,27 @@ export default util.createRule({ errorStringGenericSimple: "Array type using '{{readonlyPrefix}}{{type}}[]' is forbidden for non-simple types. Use '{{className}}<{{type}}>' instead.", }, - schema: { - $defs: { - arrayOption: { - enum: ['array', 'generic', 'array-simple'], + schema: [ + { + $defs: { + arrayOption: { + enum: ['array', 'generic', 'array-simple'], + }, }, - }, - prefixItems: [ - { - properties: { - default: { - $ref: '#/$defs/arrayOption', - description: 'The array type expected for mutable cases...', - }, - readonly: { - $ref: '#/$defs/arrayOption', - description: - 'The array type expected for readonly cases. If omitted, the value for `default` will be used.', - }, + properties: { + default: { + $ref: '#/items/0/$defs/arrayOption', + description: 'The array type expected for mutable cases...', + }, + readonly: { + $ref: '#/items/0/$defs/arrayOption', + description: + 'The array type expected for readonly cases. If omitted, the value for `default` will be used.', }, - type: 'object', }, - ], - type: 'array', - }, + type: 'object', + }, + ], }, defaultOptions: [ { diff --git a/packages/eslint-plugin/src/rules/ban-ts-comment.ts b/packages/eslint-plugin/src/rules/ban-ts-comment.ts index 511a951280e7..1092756d8354 100644 --- a/packages/eslint-plugin/src/rules/ban-ts-comment.ts +++ b/packages/eslint-plugin/src/rules/ban-ts-comment.ts @@ -39,45 +39,42 @@ export default util.createRule<[Options], MessageIds>({ tsDirectiveCommentDescriptionNotMatchPattern: 'The description for the "@ts-{{directive}}" directive must match the {{format}} format.', }, - schema: { - $defs: { - directiveConfigSchema: { - oneOf: [ - { - type: 'boolean', - default: true, - }, - { - enum: ['allow-with-description'], - }, - { - type: 'object', - properties: { - descriptionFormat: { type: 'string' }, + schema: [ + { + $defs: { + directiveConfigSchema: { + oneOf: [ + { + type: 'boolean', + default: true, }, - }, - ], + { + enum: ['allow-with-description'], + }, + { + type: 'object', + properties: { + descriptionFormat: { type: 'string' }, + }, + }, + ], + }, }, - }, - prefixItems: [ - { - properties: { - 'ts-expect-error': { - $ref: '#/$defs/directiveConfigSchema', - }, - 'ts-ignore': { $ref: '#/$defs/directiveConfigSchema' }, - 'ts-nocheck': { $ref: '#/$defs/directiveConfigSchema' }, - 'ts-check': { $ref: '#/$defs/directiveConfigSchema' }, - minimumDescriptionLength: { - type: 'number', - default: defaultMinimumDescriptionLength, - }, + properties: { + 'ts-expect-error': { + $ref: '#/items/0/$defs/directiveConfigSchema', + }, + 'ts-ignore': { $ref: '#/items/0/$defs/directiveConfigSchema' }, + 'ts-nocheck': { $ref: '#/items/0/$defs/directiveConfigSchema' }, + 'ts-check': { $ref: '#/items/0/$defs/directiveConfigSchema' }, + minimumDescriptionLength: { + type: 'number', + default: defaultMinimumDescriptionLength, }, - additionalProperties: false, }, - ], - type: 'array', - }, + additionalProperties: false, + }, + ], }, defaultOptions: [ { diff --git a/packages/eslint-plugin/src/rules/comma-dangle.ts b/packages/eslint-plugin/src/rules/comma-dangle.ts index 149218c4e7e5..95e4fab22704 100644 --- a/packages/eslint-plugin/src/rules/comma-dangle.ts +++ b/packages/eslint-plugin/src/rules/comma-dangle.ts @@ -47,6 +47,7 @@ export default util.createRule({ recommended: false, extendsBaseRule: true, }, + // intentionally a non-array schema to mirror the base rule schema: { $defs: { value: { @@ -61,19 +62,19 @@ export default util.createRule({ { oneOf: [ { - $ref: '#/$defs/value', + $ref: '#/items/0/$defs/value', }, { type: 'object', properties: { - arrays: { $ref: '#/$defs/valueWithIgnore' }, - objects: { $ref: '#/$defs/valueWithIgnore' }, - imports: { $ref: '#/$defs/valueWithIgnore' }, - exports: { $ref: '#/$defs/valueWithIgnore' }, - functions: { $ref: '#/$defs/valueWithIgnore' }, - enums: { $ref: '#/$defs/valueWithIgnore' }, - generics: { $ref: '#/$defs/valueWithIgnore' }, - tuples: { $ref: '#/$defs/valueWithIgnore' }, + arrays: { $ref: '#/items/0/$defs/valueWithIgnore' }, + objects: { $ref: '#/items/0/$defs/valueWithIgnore' }, + imports: { $ref: '#/items/0/$defs/valueWithIgnore' }, + exports: { $ref: '#/items/0/$defs/valueWithIgnore' }, + functions: { $ref: '#/items/0/$defs/valueWithIgnore' }, + enums: { $ref: '#/items/0/$defs/valueWithIgnore' }, + generics: { $ref: '#/items/0/$defs/valueWithIgnore' }, + tuples: { $ref: '#/items/0/$defs/valueWithIgnore' }, }, additionalProperties: false, }, diff --git a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts index 3fc42a956f68..c70cdb6300a3 100644 --- a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts +++ b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts @@ -27,23 +27,6 @@ type MessageIds = | 'missingAccessibility' | 'addExplicitAccessibility'; -const accessibilityLevel = { - oneOf: [ - { - const: 'explicit', - description: 'Always require an accessor.', - }, - { - const: 'no-public', - description: 'Require an accessor except when public.', - }, - { - const: 'off', - description: 'Never check whether there is an accessor.', - }, - ], -}; - export default util.createRule({ name: 'explicit-member-accessibility', meta: { @@ -63,41 +46,40 @@ export default util.createRule({ 'Public accessibility modifier on {{type}} {{name}}.', addExplicitAccessibility: "Add '{{ type }}' accessibility modifier", }, - schema: { - $defs: { - accessibilityLevel, - }, - prefixItems: [ - { - type: 'object', - properties: { - accessibility: { $ref: '#/$defs/accessibilityLevel' }, - overrides: { - type: 'object', - properties: { - accessors: { $ref: '#/$defs/accessibilityLevel' }, - constructors: { $ref: '#/$defs/accessibilityLevel' }, - methods: { $ref: '#/$defs/accessibilityLevel' }, - properties: { $ref: '#/$defs/accessibilityLevel' }, - parameterProperties: { - $ref: '#/$defs/accessibilityLevel', - }, + schema: [ + { + $defs: { + accessibilityLevel: { + enum: ['explicit', 'no-public', 'off'], + }, + }, + type: 'object', + properties: { + accessibility: { $ref: '#/items/0/$defs/accessibilityLevel' }, + overrides: { + type: 'object', + properties: { + accessors: { $ref: '#/items/0/$defs/accessibilityLevel' }, + constructors: { $ref: '#/items/0/$defs/accessibilityLevel' }, + methods: { $ref: '#/items/0/$defs/accessibilityLevel' }, + properties: { $ref: '#/items/0/$defs/accessibilityLevel' }, + parameterProperties: { + $ref: '#/items/0/$defs/accessibilityLevel', }, - - additionalProperties: false, }, - ignoredMethodNames: { - type: 'array', - items: { - type: 'string', - }, + + additionalProperties: false, + }, + ignoredMethodNames: { + type: 'array', + items: { + type: 'string', }, }, - additionalProperties: false, }, - ], - type: 'array', - }, + additionalProperties: false, + }, + ], }, defaultOptions: [{ accessibility: 'explicit' }], create(context, [option]) { diff --git a/packages/eslint-plugin/src/rules/func-call-spacing.ts b/packages/eslint-plugin/src/rules/func-call-spacing.ts index 31d8fa41046e..20a0e5f1e678 100644 --- a/packages/eslint-plugin/src/rules/func-call-spacing.ts +++ b/packages/eslint-plugin/src/rules/func-call-spacing.ts @@ -24,6 +24,7 @@ export default util.createRule({ extendsBaseRule: true, }, fixable: 'whitespace', + // intentionally a non-array schema to mirror the base rule schema: { anyOf: [ { diff --git a/packages/eslint-plugin/src/rules/lines-around-comment.ts b/packages/eslint-plugin/src/rules/lines-around-comment.ts index 478667040c5a..5e1632a6e8fb 100644 --- a/packages/eslint-plugin/src/rules/lines-around-comment.ts +++ b/packages/eslint-plugin/src/rules/lines-around-comment.ts @@ -51,89 +51,86 @@ export default util.createRule({ recommended: false, extendsBaseRule: true, }, - schema: { - type: 'array', - items: [ - { - type: 'object', - properties: { - beforeBlockComment: { - type: 'boolean', - default: true, - }, - afterBlockComment: { - type: 'boolean', - default: false, - }, - beforeLineComment: { - type: 'boolean', - default: false, - }, - afterLineComment: { - type: 'boolean', - default: false, - }, - allowBlockStart: { - type: 'boolean', - default: false, - }, - allowBlockEnd: { - type: 'boolean', - default: false, - }, - allowClassStart: { - type: 'boolean', - }, - allowClassEnd: { - type: 'boolean', - }, - allowObjectStart: { - type: 'boolean', - }, - allowObjectEnd: { - type: 'boolean', - }, - allowArrayStart: { - type: 'boolean', - }, - allowArrayEnd: { - type: 'boolean', - }, - allowInterfaceStart: { - type: 'boolean', - }, - allowInterfaceEnd: { - type: 'boolean', - }, - allowTypeStart: { - type: 'boolean', - }, - allowTypeEnd: { - type: 'boolean', - }, - allowEnumStart: { - type: 'boolean', - }, - allowEnumEnd: { - type: 'boolean', - }, - allowModuleStart: { - type: 'boolean', - }, - allowModuleEnd: { - type: 'boolean', - }, - ignorePattern: { - type: 'string', - }, - applyDefaultIgnorePatterns: { - type: 'boolean', - }, + schema: [ + { + type: 'object', + properties: { + beforeBlockComment: { + type: 'boolean', + default: true, + }, + afterBlockComment: { + type: 'boolean', + default: false, + }, + beforeLineComment: { + type: 'boolean', + default: false, + }, + afterLineComment: { + type: 'boolean', + default: false, + }, + allowBlockStart: { + type: 'boolean', + default: false, + }, + allowBlockEnd: { + type: 'boolean', + default: false, + }, + allowClassStart: { + type: 'boolean', + }, + allowClassEnd: { + type: 'boolean', + }, + allowObjectStart: { + type: 'boolean', + }, + allowObjectEnd: { + type: 'boolean', + }, + allowArrayStart: { + type: 'boolean', + }, + allowArrayEnd: { + type: 'boolean', + }, + allowInterfaceStart: { + type: 'boolean', + }, + allowInterfaceEnd: { + type: 'boolean', + }, + allowTypeStart: { + type: 'boolean', + }, + allowTypeEnd: { + type: 'boolean', + }, + allowEnumStart: { + type: 'boolean', + }, + allowEnumEnd: { + type: 'boolean', + }, + allowModuleStart: { + type: 'boolean', + }, + allowModuleEnd: { + type: 'boolean', + }, + ignorePattern: { + type: 'string', + }, + applyDefaultIgnorePatterns: { + type: 'boolean', }, - additionalProperties: false, }, - ], - }, + additionalProperties: false, + }, + ], fixable: baseRule.meta.fixable, hasSuggestions: baseRule.meta.hasSuggestions, messages: baseRule.meta.messages, diff --git a/packages/eslint-plugin/src/rules/padding-line-between-statements.ts b/packages/eslint-plugin/src/rules/padding-line-between-statements.ts index f9b97096afc8..3ab861ef68ae 100644 --- a/packages/eslint-plugin/src/rules/padding-line-between-statements.ts +++ b/packages/eslint-plugin/src/rules/padding-line-between-statements.ts @@ -595,6 +595,7 @@ export default util.createRule({ }, fixable: 'whitespace', hasSuggestions: false, + // intentionally a non-array schema to mirror the base rule schema: { $defs: { paddingType: { diff --git a/packages/eslint-plugin/src/rules/parameter-properties.ts b/packages/eslint-plugin/src/rules/parameter-properties.ts index 32547d9650fc..ddbf52400182 100644 --- a/packages/eslint-plugin/src/rules/parameter-properties.ts +++ b/packages/eslint-plugin/src/rules/parameter-properties.ts @@ -38,40 +38,37 @@ export default util.createRule({ preferParameterProperty: 'Property {{parameter}} should be declared as a parameter property.', }, - schema: { - $defs: { - modifier: { - enum: [ - 'readonly', - 'private', - 'protected', - 'public', - 'private readonly', - 'protected readonly', - 'public readonly', - ], + schema: [ + { + $defs: { + modifier: { + enum: [ + 'readonly', + 'private', + 'protected', + 'public', + 'private readonly', + 'protected readonly', + 'public readonly', + ], + }, }, - }, - prefixItems: [ - { - type: 'object', - properties: { - allow: { - type: 'array', - items: { - $ref: '#/$defs/modifier', - }, - minItems: 1, - }, - prefer: { - enum: ['class-property', 'parameter-property'], + type: 'object', + properties: { + allow: { + type: 'array', + items: { + $ref: '#/items/0/$defs/modifier', }, + minItems: 1, + }, + prefer: { + enum: ['class-property', 'parameter-property'], }, - additionalProperties: false, }, - ], - type: 'array', - }, + additionalProperties: false, + }, + ], }, defaultOptions: [ { diff --git a/packages/website/plugins/generated-rule-docs.ts b/packages/website/plugins/generated-rule-docs.ts index 2e5a32587003..13957eefb5ba 100644 --- a/packages/website/plugins/generated-rule-docs.ts +++ b/packages/website/plugins/generated-rule-docs.ts @@ -1,14 +1,14 @@ import * as eslintPlugin from '@typescript-eslint/eslint-plugin'; import * as tseslintParser from '@typescript-eslint/parser'; import * as fs from 'fs'; -import type { JSONSchema7 } from 'json-schema'; +import type { JSONSchema4Type } from 'json-schema'; import type { JSONSchema } from 'json-schema-to-typescript'; import { compile } from 'json-schema-to-typescript'; import * as lz from 'lzstring.ts'; import type * as mdast from 'mdast'; import { EOL } from 'os'; import * as path from 'path'; -import { format } from 'prettier'; +import { format, resolveConfig } from 'prettier'; import type { Plugin } from 'unified'; import type * as unist from 'unist'; @@ -34,6 +34,59 @@ function nodeIsParent(node: unist.Node): node is unist.Parent { return 'children' in node; } +const prettierConfig = resolveConfig.sync(__dirname); + +function correctSchemaForCompile( + schema: JSONSchema | JSONSchema[], +): JSONSchema { + if (Array.isArray(schema)) { + if (schema.length > 1) { + throw new Error( + 'Expected schema to only have one element for documentation generation. If this was intentional, we will need to update the documentation generator.', + ); + } + + if (schema[0].$defs != null) { + correctSchema(schema[0]); + } + return schema[0]; + } + + return schema; + + /** + * ESLint will automatically wrap an array schema in its own schema + * [schema] -> { type: 'array', items: [schema] } + * + * So to make refs work we prefix the path with `items/0/`. + * When we're generating the TS types though we don't do this + */ + function correctSchema(subschema: JSONSchema4Type): void { + if (typeof subschema !== 'object' || subschema == null) { + return; + } + + if (Array.isArray(subschema)) { + subschema.forEach(correctSchema); + return; + } + + for (const [key, value] of Object.entries(subschema)) { + switch (typeof value) { + case 'string': + if (value.startsWith('#/items/0/')) { + subschema[key] = value.replace('#/items/0/', '#/'); + } + break; + + case 'object': + correctSchema(value); + break; + } + } + } +} + export const generatedRuleDocs: Plugin = () => { return async (root, file) => { if (!nodeIsParent(root) || file.stem == null) { @@ -254,20 +307,7 @@ export const generatedRuleDocs: Plugin = () => { type: 'paragraph', } as mdast.Paragraph); } else if (!COMPLICATED_RULE_OPTIONS.has(file.stem)) { - const optionsSchema: JSONSchema = - meta.schema instanceof Array - ? meta.schema[0] - : meta.schema.type === 'array' - ? { - ...(meta.schema.definitions - ? { definitions: meta.schema.definitions } - : {}), - ...(meta.schema.$defs - ? { $defs: (meta.schema as JSONSchema7).$defs } - : {}), - ...(meta.schema.prefixItems as [JSONSchema])[0], - } - : meta.schema; + const optionsSchema = correctSchemaForCompile(meta.schema); children.splice( optionsH2Index + 1, @@ -300,6 +340,8 @@ export const generatedRuleDocs: Plugin = () => { additionalProperties: false, bannerComment: '', declareExternallyReferenced: true, + format: true, + style: prettierConfig ?? {}, }, ) ).replace(/^export /gm, ''),