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
Lines changed: 156 additions & 0 deletions
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+
}

eslint.config.js

Lines changed: 3 additions & 0 deletions
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)
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