Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
fix(eslint-plugin): fix schemas across several rules and add schema tests#6947
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
8efa5a8
509b162
d716278
ccab82e
3d0ea94
d577ef8
f2e3cda
730821b
f0b2278
55a5ffb
218f866
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
import type { TSESTree } from '@typescript-eslint/utils'; | ||
import type { | ||
JSONSchema4AnyOfSchema, | ||
JSONSchema4ArraySchema, | ||
JSONSchema4ObjectSchema, | ||
} from '@typescript-eslint/utils/json-schema'; | ||
import type { | ||
ArrayOfStringOrObject, | ||
ArrayOfStringOrObjectPatterns, | ||
@@ -19,38 +23,96 @@ const baseRule = getESLintCoreRule('no-restricted-imports'); | ||
export type Options = InferOptionsTypeFromRule<typeof baseRule>; | ||
export type MessageIds = InferMessageIdsTypeFromRule<typeof baseRule>; | ||
// 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. question: why did you switch back to basing off of the base rule's schema here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. No strong reason: just to dedupe things / keep things in sync. | ||
anyOf: [ | ||
unknown, | ||
{ | ||
type: 'array'; | ||
items: [ | ||
{ | ||
type: 'object'; | ||
properties: { | ||
paths: { | ||
type: 'array'; | ||
items: { | ||
anyOf: [ | ||
{ type: 'string' }, | ||
{ | ||
type: 'object'; | ||
properties: JSONSchema4ObjectSchema['properties']; | ||
required: string[]; | ||
}, | ||
]; | ||
}; | ||
}; | ||
patterns: { | ||
anyOf: [ | ||
{ type: 'array'; items: { type: 'string' } }, | ||
{ | ||
type: 'array'; | ||
items: { | ||
type: 'object'; | ||
properties: JSONSchema4ObjectSchema['properties']; | ||
required: string[]; | ||
}; | ||
}, | ||
]; | ||
}; | ||
}; | ||
}, | ||
]; | ||
}, | ||
]; | ||
}; | ||
const allowTypeImportsOptionSchema: JSONSchema4ObjectSchema['properties'] = { | ||
allowTypeImports: { | ||
type: 'boolean', | ||
description: 'Disallow value imports, but allow type-only imports.', | ||
}, | ||
}; | ||
const arrayOfStringsOrObjects: JSONSchema4ArraySchema = { | ||
type: 'array', | ||
items: { | ||
anyOf: [ | ||
{ type: 'string' }, | ||
{ | ||
type: 'object', | ||
additionalProperties: false, | ||
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 arrayOfStringsOrObjectPatterns: JSONSchema4AnyOfSchema = { | ||
anyOf: [ | ||
{ | ||
type: 'array', | ||
@@ -63,43 +125,48 @@ const arrayOfStringsOrObjectPatterns: JSONSchema4 = { | ||
type: 'array', | ||
items: { | ||
type: 'object', | ||
additionalProperties: false, | ||
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: JSONSchema4AnyOfSchema = { | ||
anyOf: [ | ||
arrayOfStringsOrObjects, | ||
{ | ||
type: 'array', | ||
items: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
paths: arrayOfStringsOrObjects, | ||
patterns: arrayOfStringsOrObjectPatterns, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
additionalItems: false, | ||
}, | ||
], | ||
}; | ||
function isObjectOfPaths( | ||
obj: unknown, | ||
): obj is { paths: ArrayOfStringOrObject } { | ||
@@ -153,25 +220,7 @@ export default createRule<Options, MessageIds>({ | ||
}, | ||
messages: baseRule.meta.messages, | ||
fixable: baseRule.meta.fixable, | ||
schema, | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -60,7 +60,6 @@ export default util.createRule<Options, MessageIds>({ | ||
items: { | ||
$ref: '#/items/0/$defs/modifier', | ||
}, | ||
bradzacher marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
}, | ||
prefer: { | ||
type: 'string', | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import * as util from '../src/util'; | ||
import { areOptionsValid } from './areOptionsValid'; | ||
const exampleRule = util.createRule<['value-a' | 'value-b'], never>({ | ||
name: 'my-example-rule', | ||
meta: { | ||
type: 'layout', | ||
docs: { | ||
description: 'Detects something or other', | ||
}, | ||
schema: [{ type: 'string', 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); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { TSUtils } from '@typescript-eslint/utils'; | ||
import type { RuleModule } from '@typescript-eslint/utils/ts-eslint'; | ||
import Ajv from 'ajv'; | ||
import type { JSONSchema4 } from 'json-schema'; | ||
const ajv = new Ajv({ async: false }); | ||
export function areOptionsValid( | ||
rule: RuleModule<string, readonly unknown[]>, | ||
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 (!TSUtils.isArray(schema)) { | ||
return schema; | ||
} | ||
if (schema.length === 0) { | ||
return { | ||
type: 'array', | ||
minItems: 0, | ||
maxItems: 0, | ||
}; | ||
} | ||
return { | ||
type: 'array', | ||
items: schema as JSONSchema4[], | ||
minItems: 0, | ||
maxItems: schema.length, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -4,6 +4,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'; | ||
const ruleTester = new RuleTester({ | ||
parser: '@typescript-eslint/parser', | ||
@@ -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`); | ||
} | ||
}); | ||
}); | ||
Comment on lines +2161 to +2175 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I wonder if we should do a rule-tester style API here to remove the boilerplate. ruleOptionsTester(rule,{valid:[ ...],invalid:[[{default:'simple-array'}],['array'],],}); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Yeah, I think something like this would be neat if we end up doing a lot of these kinds of tests. For now I think it's simple enough that we can leave this to be done in future if necessary? Member
|
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.