diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index e2f586f6981b..272fd56e0b2d 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -61,6 +61,7 @@ "@types/marked": "*", "@types/natural-compare-lite": "^1.4.0", "@types/prettier": "*", + "ajv": "^6.12.6", "chalk": "^5.0.1", "cross-fetch": "^3.1.5", "grapheme-splitter": "^1.0.4", diff --git a/packages/eslint-plugin/src/rules/array-type.ts b/packages/eslint-plugin/src/rules/array-type.ts index f353207e7d5d..dcfcc9e3040d 100644 --- a/packages/eslint-plugin/src/rules/array-type.ts +++ b/packages/eslint-plugin/src/rules/array-type.ts @@ -111,8 +111,10 @@ export default util.createRule({ enum: ['array', 'generic', 'array-simple'], }, }, - prefixItems: [ + items: [ { + type: 'object', + additionalProperties: false, properties: { default: { $ref: '#/$defs/arrayOption', @@ -124,7 +126,6 @@ export default util.createRule({ 'The array type expected for readonly cases. If omitted, the value for `default` will be used.', }, }, - type: 'object', }, ], type: 'array', diff --git a/packages/eslint-plugin/src/rules/ban-ts-comment.ts b/packages/eslint-plugin/src/rules/ban-ts-comment.ts index 511a951280e7..b09cfc457268 100644 --- a/packages/eslint-plugin/src/rules/ban-ts-comment.ts +++ b/packages/eslint-plugin/src/rules/ban-ts-comment.ts @@ -59,8 +59,10 @@ export default util.createRule<[Options], MessageIds>({ ], }, }, - prefixItems: [ + items: [ { + type: 'object', + additionalProperties: false, properties: { 'ts-expect-error': { $ref: '#/$defs/directiveConfigSchema', @@ -73,7 +75,6 @@ export default util.createRule<[Options], MessageIds>({ default: defaultMinimumDescriptionLength, }, }, - additionalProperties: false, }, ], type: 'array', diff --git a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts index 3fc42a956f68..9174a4c83c94 100644 --- a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts +++ b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts @@ -67,7 +67,7 @@ export default util.createRule({ $defs: { accessibilityLevel, }, - prefixItems: [ + items: [ { type: 'object', properties: { diff --git a/packages/eslint-plugin/src/rules/lines-between-class-members.ts b/packages/eslint-plugin/src/rules/lines-between-class-members.ts index 2f37b365daea..301407d45ae8 100644 --- a/packages/eslint-plugin/src/rules/lines-between-class-members.ts +++ b/packages/eslint-plugin/src/rules/lines-between-class-members.ts @@ -9,16 +9,20 @@ const baseRule = getESLintCoreRule('lines-between-class-members'); type Options = util.InferOptionsTypeFromRule; type MessageIds = util.InferMessageIdsTypeFromRule; -const schema = util.deepMerge( - { ...baseRule.meta.schema }, - { - 1: { - exceptAfterOverload: { - type: 'boolean', - default: true, +const schema = Object.values( + util.deepMerge( + { ...baseRule.meta.schema }, + { + 1: { + properties: { + exceptAfterOverload: { + type: 'boolean', + default: true, + }, + }, }, }, - }, + ), ); export default util.createRule({ diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 8a7f2875170f..69eb6b41d008 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -83,6 +83,7 @@ export default util.createRule({ schema: [ { type: 'object', + additionalProperties: false, properties: { checksConditionals: { type: 'boolean', diff --git a/packages/eslint-plugin/src/rules/no-parameter-properties.ts b/packages/eslint-plugin/src/rules/no-parameter-properties.ts index 3952dfc581f1..f7ff81f8f510 100644 --- a/packages/eslint-plugin/src/rules/no-parameter-properties.ts +++ b/packages/eslint-plugin/src/rules/no-parameter-properties.ts @@ -51,7 +51,6 @@ export default util.createRule({ 'public readonly', ], }, - minItems: 1, }, }, additionalProperties: false, diff --git a/packages/eslint-plugin/src/rules/no-restricted-imports.ts b/packages/eslint-plugin/src/rules/no-restricted-imports.ts index c2c2b54aa721..0a1a2b6c2050 100644 --- a/packages/eslint-plugin/src/rules/no-restricted-imports.ts +++ b/packages/eslint-plugin/src/rules/no-restricted-imports.ts @@ -5,12 +5,13 @@ import type { } from 'eslint/lib/rules/no-restricted-imports'; import type { Ignore } from 'ignore'; import ignore from 'ignore'; +import type { JSONSchema4 } from 'json-schema'; import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; -import { createRule, deepMerge } from '../util'; +import { createRule } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('no-restricted-imports'); @@ -18,48 +19,146 @@ const baseRule = getESLintCoreRule('no-restricted-imports'); export type Options = InferOptionsTypeFromRule; export type MessageIds = InferMessageIdsTypeFromRule; -const allowTypeImportsOptionSchema = { +// In some versions of eslint, the base rule has a completely incompatible schema +// This helper function is to safely try to get parts of the schema. If it's not +// possible, we'll fallback to less strict checks. +const tryAccess = (getter: () => T, fallback: T): T => { + try { + return getter(); + } catch { + return fallback; + } +}; + +const baseSchema = baseRule.meta.schema as { + anyOf: [ + unknown, + { + type: 'array'; + items: [ + { + type: 'object'; + properties: { + paths: { + type: 'array'; + items: { + anyOf: [ + { type: 'string' }, + { + type: 'object'; + properties: JSONSchema4['properties']; + required: string[]; + }, + ]; + }; + }; + patterns: { + anyOf: [ + { type: 'array'; items: { type: 'string' } }, + { + type: 'array'; + items: { + type: 'object'; + properties: JSONSchema4['properties']; + required: string[]; + }; + }, + ]; + }; + }; + }, + ]; + }, + ]; +}; + +const allowTypeImportsOptionSchema: JSONSchema4['properties'] = { allowTypeImports: { type: 'boolean', - default: false, }, }; -const schemaForMergeArrayOfStringsOrObjects = { + +const arrayOfStringsOrObjects: JSONSchema4 = { + type: 'array', items: { anyOf: [ - {}, + { type: 'string' }, { - properties: allowTypeImportsOptionSchema, + type: 'object', + properties: { + ...tryAccess( + () => + baseSchema.anyOf[1].items[0].properties.paths.items.anyOf[1] + .properties, + undefined, + ), + ...allowTypeImportsOptionSchema, + }, + required: tryAccess( + () => + baseSchema.anyOf[1].items[0].properties.paths.items.anyOf[1] + .required, + undefined, + ), }, ], }, + uniqueItems: true, }; -const schemaForMergeArrayOfStringsOrObjectPatterns = { + +const arrayOfStringsOrObjectPatterns: JSONSchema4 = { anyOf: [ - {}, { + type: 'array', items: { - properties: allowTypeImportsOptionSchema, + type: 'string', }, + uniqueItems: true, + }, + { + type: 'array', + items: { + type: 'object', + properties: { + ...tryAccess( + () => + baseSchema.anyOf[1].items[0].properties.patterns.anyOf[1].items + .properties, + undefined, + ), + ...allowTypeImportsOptionSchema, + }, + required: tryAccess( + () => + baseSchema.anyOf[1].items[0].properties.patterns.anyOf[1].items + .required, + [], + ), + }, + uniqueItems: true, }, ], }; -const schema = deepMerge( - { ...baseRule.meta.schema }, - { - anyOf: [ - schemaForMergeArrayOfStringsOrObjects, - { - items: { + +const schema: JSONSchema4 = { + anyOf: [ + arrayOfStringsOrObjects, + { + type: 'array', + items: [ + { + type: 'object', properties: { - paths: schemaForMergeArrayOfStringsOrObjects, - patterns: schemaForMergeArrayOfStringsOrObjectPatterns, + paths: arrayOfStringsOrObjects, + patterns: arrayOfStringsOrObjectPatterns, }, + additionalProperties: false, }, - }, - ], - }, -); + ], + additionalItems: false, + }, + ], +}; function isObjectOfPaths( obj: unknown, diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts index 38248f311235..4d311a3a8cb0 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -37,6 +37,7 @@ export default util.createRule({ schema: [ { type: 'object', + additionalProperties: false, properties: { typesToIgnore: { description: 'A list of type names to ignore.', diff --git a/packages/eslint-plugin/src/rules/parameter-properties.ts b/packages/eslint-plugin/src/rules/parameter-properties.ts index 32547d9650fc..2c2d18484205 100644 --- a/packages/eslint-plugin/src/rules/parameter-properties.ts +++ b/packages/eslint-plugin/src/rules/parameter-properties.ts @@ -52,22 +52,21 @@ export default util.createRule({ ], }, }, - prefixItems: [ + items: [ { type: 'object', + additionalProperties: false, properties: { allow: { type: 'array', items: { $ref: '#/$defs/modifier', }, - minItems: 1, }, prefer: { enum: ['class-property', 'parameter-property'], }, }, - additionalProperties: false, }, ], type: 'array', diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index 3a9e6cdca660..8a7aca65d62d 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -36,13 +36,13 @@ export default util.createRule({ }, schema: [ { - allowAdditionalProperties: false, + type: 'object', + additionalProperties: false, properties: { onlyInlineLambdas: { type: 'boolean', }, }, - type: 'object', }, ], type: 'suggestion', diff --git a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts index 1a9a8b795654..84c3cd92c7a2 100644 --- a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts +++ b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts @@ -31,6 +31,7 @@ export default util.createRule({ schema: [ { type: 'object', + additionalProperties: false, properties: { ignoreStringArrays: { description: diff --git a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts index 485ba42378bc..e465f3b13651 100644 --- a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts +++ b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts @@ -33,6 +33,7 @@ export default util.createRule({ schema: [ { type: 'object', + additionalProperties: false, properties: { allowNumber: { description: diff --git a/packages/eslint-plugin/src/rules/sort-type-constituents.ts b/packages/eslint-plugin/src/rules/sort-type-constituents.ts index 848b2ce0722e..b1159cef8ad9 100644 --- a/packages/eslint-plugin/src/rules/sort-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/sort-type-constituents.ts @@ -124,6 +124,7 @@ export default util.createRule({ schema: [ { type: 'object', + additionalProperties: false, properties: { checkIntersections: { description: 'Whether to check intersection types.', diff --git a/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts b/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts index cbfa7a515940..c4e04b75d2e4 100644 --- a/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts +++ b/packages/eslint-plugin/src/rules/sort-type-union-intersection-members.ts @@ -126,6 +126,7 @@ export default util.createRule({ schema: [ { type: 'object', + additionalProperties: false, properties: { checkIntersections: { description: 'Whether to check intersection types.', diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts index dd1f6ed871b8..d9a0b8dbce6f 100644 --- a/packages/eslint-plugin/src/rules/typedef.ts +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -32,6 +32,7 @@ export default util.createRule<[Options], MessageIds>({ schema: [ { type: 'object', + additionalProperties: false, properties: { [OptionKeys.ArrayDestructuring]: { type: 'boolean' }, [OptionKeys.ArrowParameter]: { type: 'boolean' }, diff --git a/packages/eslint-plugin/tests/areOptionsValid.test.ts b/packages/eslint-plugin/tests/areOptionsValid.test.ts new file mode 100644 index 000000000000..1efdfabb0ae3 --- /dev/null +++ b/packages/eslint-plugin/tests/areOptionsValid.test.ts @@ -0,0 +1,34 @@ +import * as util from '../src/util'; +import { areOptionsValid } from './areOptionsValid'; + +const exampleRule = util.createRule<['value-a' | 'value-b'], never>({ + name: 'space-infix-ops', + meta: { + type: 'layout', + docs: { + description: 'Require spacing around infix operators', + recommended: false, + extendsBaseRule: true, + }, + schema: [{ enum: ['value-a', 'value-b'] }], + messages: {}, + }, + defaultOptions: ['value-a'], + create() { + return {}; + }, +}); + +test('returns true for valid options', () => { + expect(areOptionsValid(exampleRule, ['value-a'])).toBe(true); +}); + +describe('returns false for invalid options', () => { + test('bad enum value', () => { + expect(areOptionsValid(exampleRule, ['value-c'])).toBe(false); + }); + + test('bad type', () => { + expect(areOptionsValid(exampleRule, [true])).toBe(false); + }); +}); diff --git a/packages/eslint-plugin/tests/areOptionsValid.ts b/packages/eslint-plugin/tests/areOptionsValid.ts new file mode 100644 index 000000000000..e67a582d28c5 --- /dev/null +++ b/packages/eslint-plugin/tests/areOptionsValid.ts @@ -0,0 +1,43 @@ +import type { RuleModule } from '@typescript-eslint/utils/src/ts-eslint'; +import Ajv from 'ajv'; +import type { JSONSchema4 } from 'json-schema'; + +const ajv = new Ajv({ async: false }); + +export function areOptionsValid( + rule: RuleModule, + options: unknown, +): boolean { + const normalizedSchema = normalizeSchema(rule.meta.schema); + + const valid = ajv.validate(normalizedSchema, options); + if (typeof valid !== 'boolean') { + // Schema could not validate options synchronously. This is not allowed for ESLint rules. + return false; + } + + return valid; +} + +function normalizeSchema( + schema: JSONSchema4 | readonly JSONSchema4[], +): JSONSchema4 { + if (!Array.isArray(schema)) { + return schema; + } + + if (schema.length === 0) { + return { + type: 'array', + minItems: 0, + maxItems: 0, + }; + } + + return { + type: 'array', + items: schema, + minItems: 0, + maxItems: schema.length, + }; +} diff --git a/packages/eslint-plugin/tests/rules/array-type.test.ts b/packages/eslint-plugin/tests/rules/array-type.test.ts index 04ab47d0b8a0..0e0e6a992649 100644 --- a/packages/eslint-plugin/tests/rules/array-type.test.ts +++ b/packages/eslint-plugin/tests/rules/array-type.test.ts @@ -3,6 +3,7 @@ import { TSESLint } from '@typescript-eslint/utils'; import type { OptionString } from '../../src/rules/array-type'; import rule from '../../src/rules/array-type'; +import { areOptionsValid } from '../areOptionsValid'; import { RuleTester } from '../RuleTester'; const ruleTester = new RuleTester({ @@ -2156,3 +2157,19 @@ type BrokenArray = { ); }); }); + +describe('schema validation', () => { + // https://github.com/typescript-eslint/typescript-eslint/issues/6852 + test("array-type does not accept 'simple-array' option", () => { + if (areOptionsValid(rule, [{ default: 'simple-array' }])) { + throw new Error(`Options succeeded validation for bad options`); + } + }); + + // https://github.com/typescript-eslint/typescript-eslint/issues/6892 + test('array-type does not accept non object option', () => { + if (areOptionsValid(rule, ['array'])) { + throw new Error(`Options succeeded validation for bad options`); + } + }); +}); diff --git a/packages/eslint-plugin/tests/schemas.test.ts b/packages/eslint-plugin/tests/schemas.test.ts new file mode 100644 index 000000000000..86e6b32a1a93 --- /dev/null +++ b/packages/eslint-plugin/tests/schemas.test.ts @@ -0,0 +1,26 @@ +import eslintPlugin from '../src'; +import { areOptionsValid } from './areOptionsValid'; + +describe("Validating rule's schemas", () => { + // These two have defaults which cover multiple arguments that are incompatible + const overrideOptions: Record = { + semi: ['never'], + 'func-call-spacing': ['never'], + }; + + for (const [ruleName, rule] of Object.entries(eslintPlugin.rules)) { + test(`${ruleName} must accept valid arguments`, () => { + if ( + !areOptionsValid(rule, overrideOptions[ruleName] ?? rule.defaultOptions) + ) { + throw new Error(`Options failed validation against rule's schema`); + } + }); + + test(`${ruleName} rejects arbitrary arguments`, () => { + if (areOptionsValid(rule, [{ 'arbitrary-schemas.test.ts': true }])) { + throw new Error(`Options succeeded validation for arbitrary options`); + } + }); + } +}); diff --git a/packages/utils/src/ts-eslint/Rule.ts b/packages/utils/src/ts-eslint/Rule.ts index ff3053863219..2b9b5c3f1be0 100644 --- a/packages/utils/src/ts-eslint/Rule.ts +++ b/packages/utils/src/ts-eslint/Rule.ts @@ -74,8 +74,11 @@ interface RuleMetaData { replacedBy?: readonly string[]; /** * The options schema. Supply an empty array if there are no options. + * $ref is not supported in arrays, so is omitted. + * See: https://github.com/typescript-eslint/typescript-eslint/pull/5531 + * See: https://eslint.org/docs/latest/extend/custom-rules#options-schemas */ - schema: JSONSchema4 | readonly JSONSchema4[]; + schema: JSONSchema4 | readonly Omit[]; } interface RuleFix { diff --git a/packages/website/plugins/generated-rule-docs.ts b/packages/website/plugins/generated-rule-docs.ts index 2e5a32587003..aecfaf897b54 100644 --- a/packages/website/plugins/generated-rule-docs.ts +++ b/packages/website/plugins/generated-rule-docs.ts @@ -265,7 +265,9 @@ export const generatedRuleDocs: Plugin = () => { ...(meta.schema.$defs ? { $defs: (meta.schema as JSONSchema7).$defs } : {}), - ...(meta.schema.prefixItems as [JSONSchema])[0], + ...(meta.schema.items + ? (meta.schema.items[0] as JSONSchema) + : undefined), } : meta.schema; diff --git a/tests/integration/integration-test-base.ts b/tests/integration/integration-test-base.ts index f49a4e90a232..a460ec563cb9 100644 --- a/tests/integration/integration-test-base.ts +++ b/tests/integration/integration-test-base.ts @@ -100,6 +100,7 @@ export function integrationTest(testFilename: string, filesGlob: string): void { // lint, outputting to a JSON file const outFile = await tmpFile(); + let stderr = ''; try { await execFile( 'yarn', @@ -120,6 +121,11 @@ export function integrationTest(testFilename: string, filesGlob: string): void { ); } catch (ex) { // we expect eslint will "fail" because we have intentional lint errors + + // useful for debugging + if (typeof ex === 'object' && ex != null && 'stderr' in ex) { + stderr = String(ex.stderr); + } } // console.log('Lint complete.'); @@ -134,7 +140,9 @@ export function integrationTest(testFilename: string, filesGlob: string): void { const lintOutput = JSON.parse(lintOutputRAW); expect(lintOutput).toMatchSnapshot(); } catch { - throw lintOutputRAW; + throw new Error( + `Lint output could not be parsed as JSON: \`${lintOutputRAW}\`. The error logs from eslint were: \`${stderr}\``, + ); } }); diff --git a/yarn.lock b/yarn.lock index 62ada2f23453..6ae4be438b34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4302,7 +4302,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@~6.12.6: +ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6, ajv@~6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy