Skip to content

Commit e1cf1cd

Browse files
authored
Update core wrapper rules to support style CSS vars injection (#1576)
* Update core wrapper rules to support style CSS vars injection * update vue/comma-spacing, and vue/eqeqeq * update vue/no-extra-parens, vue/no-useless-concat, and vue/prefer-template * Update vue/keyword-spacing, vue/space-in-parens, vue/space-infix-ops, and vue/space-unary-ops rules * fix * update vue/func-call-spacing, vue/no-restricted-syntax, and vue/template-curly-spacing * upgrade vue-eslint-parser
1 parent c08be31 commit e1cf1cd

28 files changed

+435
-29
lines changed

lib/rules/comma-spacing.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ const { wrapCoreRule } = require('../utils')
88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
99
module.exports = wrapCoreRule('comma-spacing', {
1010
skipDynamicArguments: true,
11-
skipDynamicArgumentsReport: true
11+
skipDynamicArgumentsReport: true,
12+
applyDocument: true
1213
})

lib/rules/dot-notation.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
const { wrapCoreRule } = require('../utils')
77

88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
9-
module.exports = wrapCoreRule('dot-notation')
9+
module.exports = wrapCoreRule('dot-notation', {
10+
applyDocument: true
11+
})

lib/rules/eqeqeq.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
const { wrapCoreRule } = require('../utils')
77

88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
9-
module.exports = wrapCoreRule('eqeqeq')
9+
module.exports = wrapCoreRule('eqeqeq', {
10+
applyDocument: true
11+
})

lib/rules/func-call-spacing.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ const { wrapCoreRule } = require('../utils')
77

88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
99
module.exports = wrapCoreRule('func-call-spacing', {
10-
skipDynamicArguments: true
10+
skipDynamicArguments: true,
11+
applyDocument: true
1112
})

lib/rules/no-extra-parens.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const { wrapCoreRule } = require('../utils')
99
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
1010
module.exports = wrapCoreRule('no-extra-parens', {
1111
skipDynamicArguments: true,
12+
applyDocument: true,
1213
create: createForVueSyntax
1314
})
1415

lib/rules/no-restricted-syntax.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
const { wrapCoreRule } = require('../utils')
77

88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
9-
module.exports = wrapCoreRule('no-restricted-syntax')
9+
module.exports = wrapCoreRule('no-restricted-syntax', {
10+
applyDocument: true
11+
})

lib/rules/no-useless-concat.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
const { wrapCoreRule } = require('../utils')
77

88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
9-
module.exports = wrapCoreRule('no-useless-concat')
9+
module.exports = wrapCoreRule('no-useless-concat', {
10+
applyDocument: true
11+
})

lib/rules/prefer-template.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
const { wrapCoreRule } = require('../utils')
77

88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
9-
module.exports = wrapCoreRule('prefer-template')
9+
module.exports = wrapCoreRule('prefer-template', {
10+
applyDocument: true
11+
})

lib/rules/space-in-parens.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ const { wrapCoreRule } = require('../utils')
88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
99
module.exports = wrapCoreRule('space-in-parens', {
1010
skipDynamicArguments: true,
11-
skipDynamicArgumentsReport: true
11+
skipDynamicArgumentsReport: true,
12+
applyDocument: true
1213
})

lib/rules/space-infix-ops.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ const { wrapCoreRule } = require('../utils')
77

88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
99
module.exports = wrapCoreRule('space-infix-ops', {
10-
skipDynamicArguments: true
10+
skipDynamicArguments: true,
11+
applyDocument: true
1112
})

lib/rules/space-unary-ops.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ const { wrapCoreRule } = require('../utils')
77

88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
99
module.exports = wrapCoreRule('space-unary-ops', {
10-
skipDynamicArguments: true
10+
skipDynamicArguments: true,
11+
applyDocument: true
1112
})

lib/rules/template-curly-spacing.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ const { wrapCoreRule } = require('../utils')
77

88
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
99
module.exports = wrapCoreRule('template-curly-spacing', {
10-
skipDynamicArguments: true
10+
skipDynamicArguments: true,
11+
applyDocument: true
1112
})

lib/utils/index.js

+54-11
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,24 @@ function getCoreRule(name) {
7878
* Wrap the rule context object to override methods which access to tokens (such as getTokenAfter).
7979
* @param {RuleContext} context The rule context object.
8080
* @param {ParserServices.TokenStore} tokenStore The token store object for template.
81+
* @param {Object} options The option of this rule.
82+
* @param {boolean} [options.applyDocument] If `true`, apply check to document fragment.
8183
* @returns {RuleContext}
8284
*/
83-
function wrapContextToOverrideTokenMethods(context, tokenStore) {
85+
function wrapContextToOverrideTokenMethods(context, tokenStore, options) {
8486
const eslintSourceCode = context.getSourceCode()
87+
const rootNode = options.applyDocument
88+
? context.parserServices.getDocumentFragment &&
89+
context.parserServices.getDocumentFragment()
90+
: eslintSourceCode.ast.templateBody
8591
/** @type {Token[] | null} */
8692
let tokensAndComments = null
8793
function getTokensAndComments() {
8894
if (tokensAndComments) {
8995
return tokensAndComments
9096
}
91-
const templateBody = eslintSourceCode.ast.templateBody
92-
tokensAndComments = templateBody
93-
? tokenStore.getTokens(templateBody, {
97+
tokensAndComments = rootNode
98+
? tokenStore.getTokens(rootNode, {
9499
includeComments: true
95100
})
96101
: []
@@ -99,8 +104,7 @@ function wrapContextToOverrideTokenMethods(context, tokenStore) {
99104

100105
/** @param {number} index */
101106
function getNodeByRangeIndex(index) {
102-
const templateBody = eslintSourceCode.ast.templateBody
103-
if (!templateBody) {
107+
if (!rootNode) {
104108
return eslintSourceCode.ast
105109
}
106110

@@ -110,7 +114,7 @@ function wrapContextToOverrideTokenMethods(context, tokenStore) {
110114
const skipNodes = []
111115
let breakFlag = false
112116

113-
traverseNodes(templateBody, {
117+
traverseNodes(rootNode, {
114118
enterNode(node, parent) {
115119
if (breakFlag) {
116120
return
@@ -242,6 +246,7 @@ module.exports = {
242246
* @param {string[]} [options.categories] The categories of this rule.
243247
* @param {boolean} [options.skipDynamicArguments] If `true`, skip validation within dynamic arguments.
244248
* @param {boolean} [options.skipDynamicArgumentsReport] If `true`, skip report within dynamic arguments.
249+
* @param {boolean} [options.applyDocument] If `true`, apply check to document fragment.
245250
* @param { (context: RuleContext, options: { coreHandlers: RuleListener }) => TemplateListener } [options.create] If define, extend core rule.
246251
* @returns {RuleModule} The wrapped rule implementation.
247252
*/
@@ -251,6 +256,7 @@ module.exports = {
251256
categories,
252257
skipDynamicArguments,
253258
skipDynamicArgumentsReport,
259+
applyDocument,
254260
create
255261
} = options || {}
256262
return {
@@ -262,7 +268,9 @@ module.exports = {
262268
// The `context.getSourceCode()` cannot access the tokens of templates.
263269
// So override the methods which access to tokens by the `tokenStore`.
264270
if (tokenStore) {
265-
context = wrapContextToOverrideTokenMethods(context, tokenStore)
271+
context = wrapContextToOverrideTokenMethods(context, tokenStore, {
272+
applyDocument
273+
})
266274
}
267275

268276
if (skipDynamicArgumentsReport) {
@@ -277,12 +285,19 @@ module.exports = {
277285
Object.assign({}, coreHandlers)
278286
)
279287
if (handlers.Program) {
280-
handlers["VElement[parent.type!='VElement']"] = handlers.Program
288+
handlers[
289+
applyDocument
290+
? 'VDocumentFragment'
291+
: "VElement[parent.type!='VElement']"
292+
] = /** @type {any} */ (handlers.Program)
281293
delete handlers.Program
282294
}
283295
if (handlers['Program:exit']) {
284-
handlers["VElement[parent.type!='VElement']:exit"] =
285-
handlers['Program:exit']
296+
handlers[
297+
applyDocument
298+
? 'VDocumentFragment:exit'
299+
: "VElement[parent.type!='VElement']:exit"
300+
] = /** @type {any} */ (handlers['Program:exit'])
286301
delete handlers['Program:exit']
287302
}
288303

@@ -309,6 +324,10 @@ module.exports = {
309324
compositingVisitors(handlers, create(context, { coreHandlers }))
310325
}
311326

327+
if (applyDocument) {
328+
// Apply the handlers to document.
329+
return defineDocumentVisitor(context, handlers)
330+
}
312331
// Apply the handlers to templates.
313332
return defineTemplateBodyVisitor(context, handlers)
314333
},
@@ -1812,6 +1831,30 @@ function defineTemplateBodyVisitor(
18121831
options
18131832
)
18141833
}
1834+
/**
1835+
* Register the given visitor to parser services.
1836+
* If the parser service of `vue-eslint-parser` was not found,
1837+
* this generates a warning.
1838+
*
1839+
* @param {RuleContext} context The rule context to use parser services.
1840+
* @param {TemplateListener} documentVisitor The visitor to traverse the document.
1841+
* @param { { triggerSelector: "Program" | "Program:exit" } } [options] The options.
1842+
* @returns {RuleListener} The merged visitor.
1843+
*/
1844+
function defineDocumentVisitor(context, documentVisitor, options) {
1845+
if (context.parserServices.defineDocumentVisitor == null) {
1846+
const filename = context.getFilename()
1847+
if (path.extname(filename) === '.vue') {
1848+
context.report({
1849+
loc: { line: 1, column: 0 },
1850+
message:
1851+
'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.'
1852+
})
1853+
}
1854+
return {}
1855+
}
1856+
return context.parserServices.defineDocumentVisitor(documentVisitor, options)
1857+
}
18151858

18161859
/**
18171860
* @template T

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"eslint-utils": "^2.1.0",
5757
"natural-compare": "^1.4.0",
5858
"semver": "^6.3.0",
59-
"vue-eslint-parser": "^7.9.0"
59+
"vue-eslint-parser": "^7.10.0"
6060
},
6161
"devDependencies": {
6262
"@types/eslint": "^7.2.0",

tests/lib/rules/comma-spacing.js

+28-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,14 @@ tester.run('comma-spacing', rule, {
6161
`<script>
6262
fn = (a,b) => {}
6363
</script>`,
64-
`fn = (a,b) => {}`
64+
`fn = (a,b) => {}`,
65+
// CSS vars injection
66+
`
67+
<style>
68+
.text {
69+
color: v-bind('foo(a, b)')
70+
}
71+
</style>`
6572
],
6673
invalid: [
6774
{
@@ -278,6 +285,26 @@ tester.run('comma-spacing', rule, {
278285
line: 4
279286
}
280287
]
288+
},
289+
{
290+
code: `
291+
<style>
292+
.text {
293+
color: v-bind('foo(a,b)')
294+
}
295+
</style>`,
296+
output: `
297+
<style>
298+
.text {
299+
color: v-bind('foo(a, b)')
300+
}
301+
</style>`,
302+
errors: [
303+
{
304+
message: "A space is required after ','.",
305+
line: 4
306+
}
307+
]
281308
}
282309
]
283310
})

tests/lib/rules/dot-notation.js

+39-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ tester.run('dot-notation', rule, {
1717
'<template><div attr="foo[\'bar\']" /></template>',
1818
`<template><div :[foo.bar]="a" /></template>`,
1919
`<template><div :attr="foo[bar]" /></template>`,
20-
`<template><div :[foo[bar]]="a" /></template>`
20+
`<template><div :[foo[bar]]="a" /></template>`,
21+
// CSS vars injection
22+
`
23+
<style>
24+
.text {
25+
color: v-bind(foo.bar)
26+
}
27+
</style>`
2128
],
2229
invalid: [
2330
{
@@ -29,6 +36,37 @@ tester.run('dot-notation', rule, {
2936
code: `<template><div :[foo[\`bar\`]]="a" /></template>`,
3037
output: `<template><div :[foo.bar]="a" /></template>`,
3138
errors: ['[`bar`] is better written in dot notation.']
39+
},
40+
// CSS vars injection
41+
{
42+
code: `
43+
<style>
44+
.text {
45+
color: v-bind(foo[\`bar\`])
46+
}
47+
</style>`,
48+
output: `
49+
<style>
50+
.text {
51+
color: v-bind(foo.bar)
52+
}
53+
</style>`,
54+
errors: ['[`bar`] is better written in dot notation.']
55+
},
56+
{
57+
code: `
58+
<style>
59+
.text {
60+
color: v-bind("foo[\`bar\`]")
61+
}
62+
</style>`,
63+
output: `
64+
<style>
65+
.text {
66+
color: v-bind("foo.bar")
67+
}
68+
</style>`,
69+
errors: ['[`bar`] is better written in dot notation.']
3270
}
3371
]
3472
})

tests/lib/rules/eqeqeq.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,30 @@ const tester = new RuleTester({
1212
})
1313

1414
tester.run('eqeqeq', rule, {
15-
valid: ['<template><div :attr="a === 1" /></template>'],
15+
valid: [
16+
'<template><div :attr="a === 1" /></template>',
17+
// CSS vars injection
18+
`
19+
<style>
20+
.text {
21+
color: v-bind(a === 1 ? 'red' : 'blue')
22+
}
23+
</style>`
24+
],
1625
invalid: [
1726
{
1827
code: '<template><div :attr="a == 1" /></template>',
1928
errors: ["Expected '===' and instead saw '=='."]
29+
},
30+
// CSS vars injection
31+
{
32+
code: `
33+
<style>
34+
.text {
35+
color: v-bind(a == 1 ? 'red' : 'blue')
36+
}
37+
</style>`,
38+
errors: ["Expected '===' and instead saw '=='."]
2039
}
2140
]
2241
})

0 commit comments

Comments
 (0)