Skip to content

Commit 33ffc91

Browse files
committed
Add internal rule that enforces valid default options
1 parent 1a508c0 commit 33ffc91

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed
+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* @fileoverview Internal rule to enforce valid default options.
3+
* @author Flo Edelmann
4+
*/
5+
6+
'use strict'
7+
8+
const Ajv = require('ajv')
9+
const metaSchema = require('ajv/lib/refs/json-schema-draft-04.json')
10+
11+
// from https://github.com/eslint/eslint/blob/main/lib/shared/ajv.js
12+
const ajv = new Ajv({
13+
meta: false,
14+
useDefaults: true,
15+
validateSchema: false,
16+
missingRefs: 'ignore',
17+
verbose: true,
18+
schemaId: 'auto'
19+
})
20+
ajv.addMetaSchema(metaSchema)
21+
ajv._opts.defaultMeta = metaSchema.id
22+
23+
// from https://github.com/eslint/eslint/blob/main/lib/config/flat-config-helpers.js
24+
const noOptionsSchema = Object.freeze({
25+
type: 'array',
26+
minItems: 0,
27+
maxItems: 0
28+
})
29+
function getRuleOptionsSchema(schema) {
30+
if (schema === false || typeof schema !== 'object' || schema === null) {
31+
return null
32+
}
33+
34+
if (!Array.isArray(schema)) {
35+
return schema
36+
}
37+
38+
if (schema.length === 0) {
39+
return { ...noOptionsSchema }
40+
}
41+
42+
return {
43+
type: 'array',
44+
items: schema,
45+
minItems: 0,
46+
maxItems: schema.length
47+
}
48+
}
49+
50+
/**
51+
* @param {RuleContext} context
52+
* @param {ASTNode} node
53+
* @returns {any}
54+
*/
55+
function getNodeValue(context, node) {
56+
try {
57+
// eslint-disable-next-line no-eval
58+
return eval(context.getSourceCode().getText(node))
59+
} catch (error) {
60+
return undefined
61+
}
62+
}
63+
64+
/**
65+
* Gets the property of the Object node passed in that has the name specified.
66+
*
67+
* @param {string} propertyName Name of the property to return.
68+
* @param {ASTNode} node The ObjectExpression node.
69+
* @returns {ASTNode} The Property node or null if not found.
70+
*/
71+
function getPropertyFromObject(propertyName, node) {
72+
if (node && node.type === 'ObjectExpression') {
73+
for (const property of node.properties) {
74+
if (property.type === 'Property' && property.key.name === propertyName) {
75+
return property
76+
}
77+
}
78+
}
79+
return null
80+
}
81+
82+
module.exports = {
83+
meta: {
84+
type: 'problem',
85+
docs: {
86+
description: 'enforce correct use of `meta` property in core rules',
87+
categories: ['Internal']
88+
},
89+
schema: [],
90+
messages: {
91+
defaultOptionsNotMatchingSchema:
92+
'Default options do not match the schema.'
93+
}
94+
},
95+
96+
create(context) {
97+
/** @type {ASTNode} */
98+
let exportsNode
99+
100+
return {
101+
AssignmentExpression(node) {
102+
if (
103+
node.left &&
104+
node.right &&
105+
node.left.type === 'MemberExpression' &&
106+
node.left.object.name === 'module' &&
107+
node.left.property.name === 'exports'
108+
) {
109+
exportsNode = node.right
110+
}
111+
},
112+
113+
'Program:exit'() {
114+
const metaProperty = getPropertyFromObject('meta', exportsNode)
115+
if (!metaProperty) {
116+
return
117+
}
118+
119+
const metaSchema = getPropertyFromObject('schema', metaProperty.value)
120+
const metaDefaultOptions = getPropertyFromObject(
121+
'defaultOptions',
122+
metaProperty.value
123+
)
124+
125+
if (
126+
!metaSchema ||
127+
!metaDefaultOptions ||
128+
metaDefaultOptions.value.type !== 'ArrayExpression'
129+
) {
130+
return
131+
}
132+
133+
const defaultOptions = getNodeValue(context, metaDefaultOptions.value)
134+
const schema = getNodeValue(context, metaSchema.value)
135+
136+
if (!defaultOptions || !schema) {
137+
return
138+
}
139+
140+
let validate
141+
try {
142+
validate = ajv.compile(getRuleOptionsSchema(schema))
143+
} catch (error) {
144+
return
145+
}
146+
147+
if (!validate(defaultOptions)) {
148+
context.report({
149+
node: metaDefaultOptions.value,
150+
messageId: 'defaultOptionsNotMatchingSchema'
151+
})
152+
}
153+
}
154+
}
155+
}
156+
}

Diff for: eslint.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module.exports = [
3535
internal: {
3636
rules: {
3737
'no-invalid-meta': require('./eslint-internal-rules/no-invalid-meta'),
38+
'no-invalid-meta-default-options': require('./eslint-internal-rules/no-invalid-meta-default-options'),
3839
'no-invalid-meta-docs-categories': require('./eslint-internal-rules/no-invalid-meta-docs-categories'),
3940
'require-eslint-community': require('./eslint-internal-rules/require-eslint-community')
4041
}
@@ -224,6 +225,7 @@ module.exports = [
224225
{ pattern: 'https://eslint.vuejs.org/rules/{{name}}.html' }
225226
],
226227
'internal/no-invalid-meta': 'error',
228+
'internal/no-invalid-meta-default-options': 'error',
227229
'internal/no-invalid-meta-docs-categories': 'error'
228230
}
229231
},
@@ -232,6 +234,7 @@ module.exports = [
232234
rules: {
233235
'eslint-plugin/require-meta-docs-url': 'off',
234236
'internal/no-invalid-meta': 'error',
237+
'internal/no-invalid-meta-default-options': 'error',
235238
'internal/no-invalid-meta-docs-categories': 'error'
236239
}
237240
},

0 commit comments

Comments
 (0)