Skip to content

Commit

Permalink
Update some rules to support style CSS vars injection (#1574)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ota-meshi committed Jul 18, 2021
1 parent 9dc78d0 commit 23be6e4
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 2 deletions.
3 changes: 3 additions & 0 deletions docs/rules/no-unsupported-features.md
Expand Up @@ -32,6 +32,7 @@ The `"ignores"` option accepts an array of the following strings.
- Vue.js 3.1.0+
- `"is-attribute-with-vue-prefix"` ... [`is` attribute with `vue:` prefix](https://v3.vuejs.org/api/special-attributes.html#is)
- Vue.js 3.0.0+
- `"style-css-vars-injection"` ... [SFC CSS variable injection][Vue RFCs - 0043-sfc-style-variables]
- `"script-setup"` ... [`<script setup>`][Vue RFCs - 0040-script-setup]
- `"v-model-argument"` ... [argument on `v-model`][Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
- `"v-model-custom-modifiers"` ... [custom modifiers on `v-model`][Vue RFCs - 0011-v-model-api-change]
Expand Down Expand Up @@ -107,6 +108,7 @@ The `"ignores"` option accepts an array of the following strings.
- [Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
- [Vue RFCs - 0011-v-model-api-change]
- [Vue RFCs - 0040-script-setup]
- [Vue RFCs - 0043-sfc-style-variables]
- [Vue RFCs - v-bind .prop shorthand proposal]

[Vue RFCs - 0001-new-slot-syntax]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md
Expand All @@ -115,6 +117,7 @@ The `"ignores"` option accepts an array of the following strings.
[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
[Vue RFCs - 0011-v-model-api-change]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0011-v-model-api-change.md
[Vue RFCs - 0040-script-setup]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md
[Vue RFCs - 0043-sfc-style-variables]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0043-sfc-style-variables.md

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

Expand Down
3 changes: 3 additions & 0 deletions lib/rules/no-unsupported-features.js
Expand Up @@ -27,6 +27,7 @@ const FEATURES = {
'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers'),
'v-is': require('./syntaxes/v-is'),
'script-setup': require('./syntaxes/script-setup'),
'style-css-vars-injection': require('./syntaxes/style-css-vars-injection'),
// Vue.js 3.1.0+
'is-attribute-with-vue-prefix': require('./syntaxes/is-attribute-with-vue-prefix')
}
Expand Down Expand Up @@ -103,6 +104,8 @@ module.exports = {
forbiddenVIs: '`v-is` are not supported until Vue.js "3.0.0".',
forbiddenScriptSetup:
'`<script setup>` are not supported until Vue.js "3.0.0".',
forbiddenStyleCssVarsInjection:
'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
// Vue.js 3.1.0+
forbiddenIsAttributeWithVuePrefix:
'`is="vue:"` are not supported until Vue.js "3.1.0".'
Expand Down
10 changes: 10 additions & 0 deletions lib/rules/no-unused-properties.js
Expand Up @@ -10,6 +10,7 @@

const utils = require('../utils')
const eslintUtils = require('eslint-utils')
const { getStyleVariablesContext } = require('../utils/style-variables')

/**
* @typedef {import('../utils').GroupName} GroupName
Expand Down Expand Up @@ -1056,6 +1057,15 @@ module.exports = {
{
/** @param {Program} node */
'Program:exit'(node) {
const styleVars = getStyleVariablesContext(context)
if (styleVars) {
for (const { id } of styleVars.references) {
templatePropertiesContainer.usedProperties.addUsed(
id.name,
(context) => extractPatternOrThisProperties(id, context, true)
)
}
}
if (!node.templateBody) {
reportUnusedProperties()
}
Expand Down
12 changes: 11 additions & 1 deletion lib/rules/script-setup-uses-vars.js
Expand Up @@ -4,6 +4,7 @@
*/
'use strict'

const { getStyleVariablesContext } = require('../utils/style-variables')
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
Expand Down Expand Up @@ -112,7 +113,16 @@ module.exports = {
}
}
},
undefined,
{
Program() {
const styleVars = getStyleVariablesContext(context)
if (styleVars) {
for (const ref of styleVars.references) {
context.markVariableAsUsed(ref.id.name)
}
}
}
},
{
templateBodyTriggerSelector: 'Program'
}
Expand Down
28 changes: 28 additions & 0 deletions lib/rules/syntaxes/style-css-vars-injection.js
@@ -0,0 +1,28 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

const { getStyleVariablesContext } = require('../../utils/style-variables')

module.exports = {
supported: '>=3.0.3',
/** @param {RuleContext} context @returns {TemplateListener} */
createScriptVisitor(context) {
const styleVars = getStyleVariablesContext(context)
if (!styleVars) {
return {}
}
return {
Program() {
for (const vBind of styleVars.vBinds) {
context.report({
node: vBind,
messageId: 'forbiddenStyleCssVarsInjection'
})
}
}
}
}
}
61 changes: 61 additions & 0 deletions lib/utils/style-variables/index.js
@@ -0,0 +1,61 @@
const { isVElement } = require('..')

module.exports = {
getStyleVariablesContext
}

class StyleVariablesContext {
/**
* @param {RuleContext} context
* @param {VElement[]} styles
*/
constructor(context, styles) {
this.context = context
this.styles = styles
/** @type {VReference[]} */
this.references = []
/** @type {VExpressionContainer[]} */
this.vBinds = []
for (const style of styles) {
for (const node of style.children) {
if (node.type === 'VExpressionContainer') {
this.vBinds.push(node)
for (const ref of node.references.filter(
(ref) => ref.variable == null
)) {
this.references.push(ref)
}
}
}
}
}
}

/** @type {Map<VElement, StyleVariablesContext} */
const cache = new Map()
/**
* Get the style vars context
* @param {RuleContext} context
* @returns {StyleVariablesContext | null}
*/
function getStyleVariablesContext(context) {
const df =
context.parserServices.getDocumentFragment &&
context.parserServices.getDocumentFragment()
if (!df) {
return null
}
const styles = df.children
.filter(isVElement)
.filter((e) => e.name === 'style')
if (!styles.length) {
return null
}
let ctx = cache.get(styles[0])
if (ctx) {
return ctx
}
ctx = new StyleVariablesContext(context, styles)
cache.set(styles[0], ctx)
return ctx
}
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -56,7 +56,7 @@
"eslint-utils": "^2.1.0",
"natural-compare": "^1.4.0",
"semver": "^6.3.0",
"vue-eslint-parser": "^7.8.0"
"vue-eslint-parser": "^7.9.0"
},
"devDependencies": {
"@types/eslint": "^7.2.0",
Expand Down
26 changes: 26 additions & 0 deletions tests/lib/rules/no-parsing-error.js
Expand Up @@ -615,6 +615,32 @@ tester.run('no-parsing-error', rule, {
{
code: '<template><div xmlns=""></template>',
errors: ['Parsing error: x-invalid-namespace.']
},

//style vars
{
filename: 'test.vue',
code: `
<template></template>
<style>
.text {
color: v-bind(color.);
font-size: v-bind('font size');
}
</style>
`,
errors: [
{
message: 'Parsing error: Unexpected end of expression.',
line: 5,
column: 33
},
{
message: 'Parsing error: Unexpected token size.',
line: 6,
column: 37
}
]
}
]
})
118 changes: 118 additions & 0 deletions tests/lib/rules/no-unsupported-features/style-css-vars-injection.js
@@ -0,0 +1,118 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

const RuleTester = require('eslint').RuleTester
const rule = require('../../../../lib/rules/no-unsupported-features')
const utils = require('./utils')

const buildOptions = utils.optionsBuilder('style-css-vars-injection', '^3.0.3')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2019,
sourceType: 'module'
}
})

tester.run('no-unsupported-features/style-css-vars-injection', rule, {
valid: [
{
code: `
<template>
<div class="text">hello</div>
</template>
<script>
export default {
data() {
return {
color: 'red',
font: {
size: '2em',
},
}
},
}
</script>
<style>
.text {
color: v-bind(color);
/* expressions (wrap in quotes) */
font-size: v-bind('font.size');
}
</style>`,
options: buildOptions()
},
{
code: `
<template>
<div class="text">hello</div>
</template>
<script>
</script>
<style>
.text {
color: red;
font-size: 2em;
}
</style>`,
options: buildOptions({ version: '^2.6.0' })
}
],
invalid: [
{
code: `
<template>
<div class="text">hello</div>
</template>
<script>
export default {
data() {
return {
color: 'red',
font: {
size: '2em',
},
}
},
}
</script>
<style>
.text {
color: v-bind(color);
/* expressions (wrap in quotes) */
font-size: v-bind('font.size');
}
</style>`,
options: buildOptions({ version: '^3.0.0' }),
errors: [
{
message:
'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
line: 21,
column: 18,
endLine: 21,
endColumn: 31
},
{
message:
'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
line: 24,
column: 22,
endLine: 24,
endColumn: 41
}
]
}
]
})
32 changes: 32 additions & 0 deletions tests/lib/rules/no-unused-properties.js
Expand Up @@ -1562,6 +1562,38 @@ tester.run('no-unused-properties', rule, {
}
</script>`,
options: allOptions
},

//style vars
{
filename: 'test.vue',
code: `
<template>
<div class="text">hello</div>
</template>
<script>
export default {
data() {
return {
color: 'red',
font: {
size: '2em',
},
}
},
}
</script>
<style>
.text {
color: v-bind(color);
/* expressions (wrap in quotes) */
font-size: v-bind('font.size');
}
</style>
`
}
],

Expand Down

0 comments on commit 23be6e4

Please sign in to comment.