Skip to content

Commit 23be6e4

Browse files
authored
Update some rules to support style CSS vars injection (#1574)
* Update `vue/no-unused-properties` and `vue/script-setup-uses-vars` rule to support style CSS vars * Add testcase * upgrade vue-eslint-parser * update test * Update no-unsupported-features
1 parent 9dc78d0 commit 23be6e4

11 files changed

+333
-2
lines changed

Diff for: docs/rules/no-unsupported-features.md

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ The `"ignores"` option accepts an array of the following strings.
3232
- Vue.js 3.1.0+
3333
- `"is-attribute-with-vue-prefix"` ... [`is` attribute with `vue:` prefix](https://v3.vuejs.org/api/special-attributes.html#is)
3434
- Vue.js 3.0.0+
35+
- `"style-css-vars-injection"` ... [SFC CSS variable injection][Vue RFCs - 0043-sfc-style-variables]
3536
- `"script-setup"` ... [`<script setup>`][Vue RFCs - 0040-script-setup]
3637
- `"v-model-argument"` ... [argument on `v-model`][Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
3738
- `"v-model-custom-modifiers"` ... [custom modifiers on `v-model`][Vue RFCs - 0011-v-model-api-change]
@@ -107,6 +108,7 @@ The `"ignores"` option accepts an array of the following strings.
107108
- [Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
108109
- [Vue RFCs - 0011-v-model-api-change]
109110
- [Vue RFCs - 0040-script-setup]
111+
- [Vue RFCs - 0043-sfc-style-variables]
110112
- [Vue RFCs - v-bind .prop shorthand proposal]
111113

112114
[Vue RFCs - 0001-new-slot-syntax]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md
@@ -115,6 +117,7 @@ The `"ignores"` option accepts an array of the following strings.
115117
[Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0005-replace-v-bind-sync-with-v-model-argument.md
116118
[Vue RFCs - 0011-v-model-api-change]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0011-v-model-api-change.md
117119
[Vue RFCs - 0040-script-setup]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md
120+
[Vue RFCs - 0043-sfc-style-variables]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0043-sfc-style-variables.md
118121

119122
[Vue RFCs - v-bind .prop shorthand proposal]: https://github.com/vuejs/rfcs/pull/18
120123

Diff for: lib/rules/no-unsupported-features.js

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const FEATURES = {
2727
'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers'),
2828
'v-is': require('./syntaxes/v-is'),
2929
'script-setup': require('./syntaxes/script-setup'),
30+
'style-css-vars-injection': require('./syntaxes/style-css-vars-injection'),
3031
// Vue.js 3.1.0+
3132
'is-attribute-with-vue-prefix': require('./syntaxes/is-attribute-with-vue-prefix')
3233
}
@@ -103,6 +104,8 @@ module.exports = {
103104
forbiddenVIs: '`v-is` are not supported until Vue.js "3.0.0".',
104105
forbiddenScriptSetup:
105106
'`<script setup>` are not supported until Vue.js "3.0.0".',
107+
forbiddenStyleCssVarsInjection:
108+
'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
106109
// Vue.js 3.1.0+
107110
forbiddenIsAttributeWithVuePrefix:
108111
'`is="vue:"` are not supported until Vue.js "3.1.0".'

Diff for: lib/rules/no-unused-properties.js

+10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
const utils = require('../utils')
1212
const eslintUtils = require('eslint-utils')
13+
const { getStyleVariablesContext } = require('../utils/style-variables')
1314

1415
/**
1516
* @typedef {import('../utils').GroupName} GroupName
@@ -1056,6 +1057,15 @@ module.exports = {
10561057
{
10571058
/** @param {Program} node */
10581059
'Program:exit'(node) {
1060+
const styleVars = getStyleVariablesContext(context)
1061+
if (styleVars) {
1062+
for (const { id } of styleVars.references) {
1063+
templatePropertiesContainer.usedProperties.addUsed(
1064+
id.name,
1065+
(context) => extractPatternOrThisProperties(id, context, true)
1066+
)
1067+
}
1068+
}
10591069
if (!node.templateBody) {
10601070
reportUnusedProperties()
10611071
}

Diff for: lib/rules/script-setup-uses-vars.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
'use strict'
66

7+
const { getStyleVariablesContext } = require('../utils/style-variables')
78
// ------------------------------------------------------------------------------
89
// Requirements
910
// ------------------------------------------------------------------------------
@@ -112,7 +113,16 @@ module.exports = {
112113
}
113114
}
114115
},
115-
undefined,
116+
{
117+
Program() {
118+
const styleVars = getStyleVariablesContext(context)
119+
if (styleVars) {
120+
for (const ref of styleVars.references) {
121+
context.markVariableAsUsed(ref.id.name)
122+
}
123+
}
124+
}
125+
},
116126
{
117127
templateBodyTriggerSelector: 'Program'
118128
}

Diff for: lib/rules/syntaxes/style-css-vars-injection.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const { getStyleVariablesContext } = require('../../utils/style-variables')
8+
9+
module.exports = {
10+
supported: '>=3.0.3',
11+
/** @param {RuleContext} context @returns {TemplateListener} */
12+
createScriptVisitor(context) {
13+
const styleVars = getStyleVariablesContext(context)
14+
if (!styleVars) {
15+
return {}
16+
}
17+
return {
18+
Program() {
19+
for (const vBind of styleVars.vBinds) {
20+
context.report({
21+
node: vBind,
22+
messageId: 'forbiddenStyleCssVarsInjection'
23+
})
24+
}
25+
}
26+
}
27+
}
28+
}

Diff for: lib/utils/style-variables/index.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const { isVElement } = require('..')
2+
3+
module.exports = {
4+
getStyleVariablesContext
5+
}
6+
7+
class StyleVariablesContext {
8+
/**
9+
* @param {RuleContext} context
10+
* @param {VElement[]} styles
11+
*/
12+
constructor(context, styles) {
13+
this.context = context
14+
this.styles = styles
15+
/** @type {VReference[]} */
16+
this.references = []
17+
/** @type {VExpressionContainer[]} */
18+
this.vBinds = []
19+
for (const style of styles) {
20+
for (const node of style.children) {
21+
if (node.type === 'VExpressionContainer') {
22+
this.vBinds.push(node)
23+
for (const ref of node.references.filter(
24+
(ref) => ref.variable == null
25+
)) {
26+
this.references.push(ref)
27+
}
28+
}
29+
}
30+
}
31+
}
32+
}
33+
34+
/** @type {Map<VElement, StyleVariablesContext} */
35+
const cache = new Map()
36+
/**
37+
* Get the style vars context
38+
* @param {RuleContext} context
39+
* @returns {StyleVariablesContext | null}
40+
*/
41+
function getStyleVariablesContext(context) {
42+
const df =
43+
context.parserServices.getDocumentFragment &&
44+
context.parserServices.getDocumentFragment()
45+
if (!df) {
46+
return null
47+
}
48+
const styles = df.children
49+
.filter(isVElement)
50+
.filter((e) => e.name === 'style')
51+
if (!styles.length) {
52+
return null
53+
}
54+
let ctx = cache.get(styles[0])
55+
if (ctx) {
56+
return ctx
57+
}
58+
ctx = new StyleVariablesContext(context, styles)
59+
cache.set(styles[0], ctx)
60+
return ctx
61+
}

Diff for: 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.8.0"
59+
"vue-eslint-parser": "^7.9.0"
6060
},
6161
"devDependencies": {
6262
"@types/eslint": "^7.2.0",

Diff for: tests/lib/rules/no-parsing-error.js

+26
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,32 @@ tester.run('no-parsing-error', rule, {
615615
{
616616
code: '<template><div xmlns=""></template>',
617617
errors: ['Parsing error: x-invalid-namespace.']
618+
},
619+
620+
//style vars
621+
{
622+
filename: 'test.vue',
623+
code: `
624+
<template></template>
625+
<style>
626+
.text {
627+
color: v-bind(color.);
628+
font-size: v-bind('font size');
629+
}
630+
</style>
631+
`,
632+
errors: [
633+
{
634+
message: 'Parsing error: Unexpected end of expression.',
635+
line: 5,
636+
column: 33
637+
},
638+
{
639+
message: 'Parsing error: Unexpected token size.',
640+
line: 6,
641+
column: 37
642+
}
643+
]
618644
}
619645
]
620646
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const RuleTester = require('eslint').RuleTester
8+
const rule = require('../../../../lib/rules/no-unsupported-features')
9+
const utils = require('./utils')
10+
11+
const buildOptions = utils.optionsBuilder('style-css-vars-injection', '^3.0.3')
12+
const tester = new RuleTester({
13+
parser: require.resolve('vue-eslint-parser'),
14+
parserOptions: {
15+
ecmaVersion: 2019,
16+
sourceType: 'module'
17+
}
18+
})
19+
20+
tester.run('no-unsupported-features/style-css-vars-injection', rule, {
21+
valid: [
22+
{
23+
code: `
24+
<template>
25+
<div class="text">hello</div>
26+
</template>
27+
28+
<script>
29+
export default {
30+
data() {
31+
return {
32+
color: 'red',
33+
font: {
34+
size: '2em',
35+
},
36+
}
37+
},
38+
}
39+
</script>
40+
41+
<style>
42+
.text {
43+
color: v-bind(color);
44+
45+
/* expressions (wrap in quotes) */
46+
font-size: v-bind('font.size');
47+
}
48+
</style>`,
49+
options: buildOptions()
50+
},
51+
{
52+
code: `
53+
<template>
54+
<div class="text">hello</div>
55+
</template>
56+
57+
<script>
58+
</script>
59+
60+
<style>
61+
.text {
62+
color: red;
63+
font-size: 2em;
64+
}
65+
</style>`,
66+
options: buildOptions({ version: '^2.6.0' })
67+
}
68+
],
69+
invalid: [
70+
{
71+
code: `
72+
<template>
73+
<div class="text">hello</div>
74+
</template>
75+
76+
<script>
77+
export default {
78+
data() {
79+
return {
80+
color: 'red',
81+
font: {
82+
size: '2em',
83+
},
84+
}
85+
},
86+
}
87+
</script>
88+
89+
<style>
90+
.text {
91+
color: v-bind(color);
92+
93+
/* expressions (wrap in quotes) */
94+
font-size: v-bind('font.size');
95+
}
96+
</style>`,
97+
options: buildOptions({ version: '^3.0.0' }),
98+
errors: [
99+
{
100+
message:
101+
'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
102+
line: 21,
103+
column: 18,
104+
endLine: 21,
105+
endColumn: 31
106+
},
107+
{
108+
message:
109+
'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
110+
line: 24,
111+
column: 22,
112+
endLine: 24,
113+
endColumn: 41
114+
}
115+
]
116+
}
117+
]
118+
})

Diff for: tests/lib/rules/no-unused-properties.js

+32
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,38 @@ tester.run('no-unused-properties', rule, {
15621562
}
15631563
</script>`,
15641564
options: allOptions
1565+
},
1566+
1567+
//style vars
1568+
{
1569+
filename: 'test.vue',
1570+
code: `
1571+
<template>
1572+
<div class="text">hello</div>
1573+
</template>
1574+
1575+
<script>
1576+
export default {
1577+
data() {
1578+
return {
1579+
color: 'red',
1580+
font: {
1581+
size: '2em',
1582+
},
1583+
}
1584+
},
1585+
}
1586+
</script>
1587+
1588+
<style>
1589+
.text {
1590+
color: v-bind(color);
1591+
1592+
/* expressions (wrap in quotes) */
1593+
font-size: v-bind('font.size');
1594+
}
1595+
</style>
1596+
`
15651597
}
15661598
],
15671599

0 commit comments

Comments
 (0)