Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(eslint-plugin): fix schemas across several rules and add schema tests #6894

Closed
wants to merge 14 commits into from
Closed
1 change: 1 addition & 0 deletions packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions packages/eslint-plugin/src/rules/array-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,10 @@ export default util.createRule<Options, MessageIds>({
enum: ['array', 'generic', 'array-simple'],
},
},
prefixItems: [
items: [
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
{
type: 'object',
additionalProperties: false,
properties: {
default: {
$ref: '#/$defs/arrayOption',
Expand All @@ -124,7 +126,6 @@ export default util.createRule<Options, MessageIds>({
'The array type expected for readonly cases. If omitted, the value for `default` will be used.',
},
},
type: 'object',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this up to the top for consistency with most of the other rules I saw.

},
],
type: 'array',
Expand Down
5 changes: 3 additions & 2 deletions packages/eslint-plugin/src/rules/ban-ts-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ export default util.createRule<[Options], MessageIds>({
],
},
},
prefixItems: [
items: [
{
type: 'object',
additionalProperties: false,
properties: {
'ts-expect-error': {
$ref: '#/$defs/directiveConfigSchema',
Expand All @@ -73,7 +75,6 @@ export default util.createRule<[Options], MessageIds>({
default: defaultMinimumDescriptionLength,
},
},
additionalProperties: false,
},
],
type: 'array',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default util.createRule<Options, MessageIds>({
$defs: {
accessibilityLevel,
},
prefixItems: [
items: [
{
type: 'object',
properties: {
Expand Down
20 changes: 12 additions & 8 deletions packages/eslint-plugin/src/rules/lines-between-class-members.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ const baseRule = getESLintCoreRule('lines-between-class-members');
type Options = util.InferOptionsTypeFromRule<typeof baseRule>;
type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;

const schema = util.deepMerge(
{ ...baseRule.meta.schema },
{
1: {
exceptAfterOverload: {
type: 'boolean',
default: true,
const schema = Object.values(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before, we had:

{ "0": { /* schema A */ }, "1": { /* schema B */ } }

AJV is actually not able to interpret this correctly, and no validation actually occurs.

This change fixes it to:

[{ /* schema A */ },  { /* schema B */ }]

Using Object.values here should be safe. The baseRule.meta.schema is an array itself, so will be spread in order (guaranteed by ECMA spec). deepMerge will merge the keys in the order of the first object. Object.values will take the values in order, so we'll get back the array in the right order.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Praise] Nifty trick to use Object.values to convert the string index style object to an array!

util.deepMerge(
{ ...baseRule.meta.schema },
{
1: {
properties: {
exceptAfterOverload: {
type: 'boolean',
default: true,
},
},
},
},
},
),
);

export default util.createRule<Options, MessageIds>({
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/rules/no-misused-promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export default util.createRule<Options, MessageId>({
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
checksConditionals: {
type: 'boolean',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ export default util.createRule<Options, MessageIds>({
'public readonly',
],
},
minItems: 1,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this constraint so the default is able to be validated against the schema.

Alternative could be to exclude this from the schema validation list, or have an 'override' schema for testing against the schema.

I don't think this matters for actual usage, and I think it's not harmful: in fact it might be useful in some configs to be able to specify some allowed things by default, and then specify none to be allowed in an override config explicitly to be clear about why it's there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't follow - why is this minItems not good? It's a correct restriction, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this PR, the main reason I did it is beacuse it would prefer the default options from validating against the schema itself, and this is a simple way of getting around that.

But also I think semantically it makes sense that it should accept an array of nothing for the exceptions allowed, meaning that no parameter properties are allowed.

},
},
additionalProperties: false,
Expand Down
143 changes: 121 additions & 22 deletions packages/eslint-plugin/src/rules/no-restricted-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,160 @@ 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');

export type Options = InferOptionsTypeFromRule<typeof baseRule>;
export type MessageIds = InferMessageIdsTypeFromRule<typeof baseRule>;

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 = <T>(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: [
{},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem incorrect and unnecessary. I think the intention was for this to merge with the base anyOf items, but deepMerge doesn't handle array items: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/src/eslint-utils/deepMerge.ts

Although thinking about this now, I'm not certain the updated rule is 100% correct, would appreciate careful eyes on it. Might need to refactor how this extends from the base.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have redone this. It's fairly horrible how it indexes into the baseRule - happy to take feedback on better approaches.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah seems like you're certainly following the standard practice: https://github.com/eslint/eslint/blob/1fea2797801a82a2718814c83dad641dab092bcc/lib/rules/no-restricted-imports.js#L19.

You might want to assert const baseSchema = baseRule.meta.schema as SomeTypeDescribingWhatWeKnow, since this is making an assumption on the base rule's schema type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, have done this.

{ 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default util.createRule<Options, MessageIds>({
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
typesToIgnore: {
description: 'A list of type names to ignore.',
Expand Down
5 changes: 2 additions & 3 deletions packages/eslint-plugin/src/rules/parameter-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,21 @@ export default util.createRule<Options, MessageIds>({
],
},
},
prefixItems: [
items: [
{
type: 'object',
additionalProperties: false,
properties: {
allow: {
type: 'array',
items: {
$ref: '#/$defs/modifier',
},
minItems: 1,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same minItems reasoning as before

},
prefer: {
enum: ['class-property', 'parameter-property'],
},
},
additionalProperties: false,
},
],
type: 'array',
Expand Down
4 changes: 2 additions & 2 deletions packages/eslint-plugin/src/rules/prefer-readonly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ export default util.createRule<Options, MessageIds>({
},
schema: [
{
allowAdditionalProperties: false,
type: 'object',
additionalProperties: false,
properties: {
onlyInlineLambdas: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default util.createRule<Options, MessageIds>({
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
ignoreStringArrays: {
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default util.createRule<Options, MessageId>({
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
allowNumber: {
description:
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/rules/sort-type-constituents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export default util.createRule<Options, MessageIds>({
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
checkIntersections: {
description: 'Whether to check intersection types.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export default util.createRule<Options, MessageIds>({
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
checkIntersections: {
description: 'Whether to check intersection types.',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/rules/typedef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default util.createRule<[Options], MessageIds>({
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
[OptionKeys.ArrayDestructuring]: { type: 'boolean' },
[OptionKeys.ArrowParameter]: { type: 'boolean' },
Expand Down