Skip to content

Commit

Permalink
Update core wrapper rules to support style CSS vars injection (#1576)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ota-meshi committed Jul 29, 2021
1 parent c08be31 commit e1cf1cd
Show file tree
Hide file tree
Showing 28 changed files with 435 additions and 29 deletions.
3 changes: 2 additions & 1 deletion lib/rules/comma-spacing.js
Expand Up @@ -8,5 +8,6 @@ const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('comma-spacing', {
skipDynamicArguments: true,
skipDynamicArgumentsReport: true
skipDynamicArgumentsReport: true,
applyDocument: true
})
4 changes: 3 additions & 1 deletion lib/rules/dot-notation.js
Expand Up @@ -6,4 +6,6 @@
const { wrapCoreRule } = require('../utils')

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('dot-notation')
module.exports = wrapCoreRule('dot-notation', {
applyDocument: true
})
4 changes: 3 additions & 1 deletion lib/rules/eqeqeq.js
Expand Up @@ -6,4 +6,6 @@
const { wrapCoreRule } = require('../utils')

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('eqeqeq')
module.exports = wrapCoreRule('eqeqeq', {
applyDocument: true
})
3 changes: 2 additions & 1 deletion lib/rules/func-call-spacing.js
Expand Up @@ -7,5 +7,6 @@ const { wrapCoreRule } = require('../utils')

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('func-call-spacing', {
skipDynamicArguments: true
skipDynamicArguments: true,
applyDocument: true
})
1 change: 1 addition & 0 deletions lib/rules/no-extra-parens.js
Expand Up @@ -9,6 +9,7 @@ const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('no-extra-parens', {
skipDynamicArguments: true,
applyDocument: true,
create: createForVueSyntax
})

Expand Down
4 changes: 3 additions & 1 deletion lib/rules/no-restricted-syntax.js
Expand Up @@ -6,4 +6,6 @@
const { wrapCoreRule } = require('../utils')

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('no-restricted-syntax')
module.exports = wrapCoreRule('no-restricted-syntax', {
applyDocument: true
})
4 changes: 3 additions & 1 deletion lib/rules/no-useless-concat.js
Expand Up @@ -6,4 +6,6 @@
const { wrapCoreRule } = require('../utils')

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('no-useless-concat')
module.exports = wrapCoreRule('no-useless-concat', {
applyDocument: true
})
4 changes: 3 additions & 1 deletion lib/rules/prefer-template.js
Expand Up @@ -6,4 +6,6 @@
const { wrapCoreRule } = require('../utils')

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('prefer-template')
module.exports = wrapCoreRule('prefer-template', {
applyDocument: true
})
3 changes: 2 additions & 1 deletion lib/rules/space-in-parens.js
Expand Up @@ -8,5 +8,6 @@ const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('space-in-parens', {
skipDynamicArguments: true,
skipDynamicArgumentsReport: true
skipDynamicArgumentsReport: true,
applyDocument: true
})
3 changes: 2 additions & 1 deletion lib/rules/space-infix-ops.js
Expand Up @@ -7,5 +7,6 @@ const { wrapCoreRule } = require('../utils')

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('space-infix-ops', {
skipDynamicArguments: true
skipDynamicArguments: true,
applyDocument: true
})
3 changes: 2 additions & 1 deletion lib/rules/space-unary-ops.js
Expand Up @@ -7,5 +7,6 @@ const { wrapCoreRule } = require('../utils')

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('space-unary-ops', {
skipDynamicArguments: true
skipDynamicArguments: true,
applyDocument: true
})
3 changes: 2 additions & 1 deletion lib/rules/template-curly-spacing.js
Expand Up @@ -7,5 +7,6 @@ const { wrapCoreRule } = require('../utils')

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('template-curly-spacing', {
skipDynamicArguments: true
skipDynamicArguments: true,
applyDocument: true
})
65 changes: 54 additions & 11 deletions lib/utils/index.js
Expand Up @@ -78,19 +78,24 @@ function getCoreRule(name) {
* Wrap the rule context object to override methods which access to tokens (such as getTokenAfter).
* @param {RuleContext} context The rule context object.
* @param {ParserServices.TokenStore} tokenStore The token store object for template.
* @param {Object} options The option of this rule.
* @param {boolean} [options.applyDocument] If `true`, apply check to document fragment.
* @returns {RuleContext}
*/
function wrapContextToOverrideTokenMethods(context, tokenStore) {
function wrapContextToOverrideTokenMethods(context, tokenStore, options) {
const eslintSourceCode = context.getSourceCode()
const rootNode = options.applyDocument
? context.parserServices.getDocumentFragment &&
context.parserServices.getDocumentFragment()
: eslintSourceCode.ast.templateBody
/** @type {Token[] | null} */
let tokensAndComments = null
function getTokensAndComments() {
if (tokensAndComments) {
return tokensAndComments
}
const templateBody = eslintSourceCode.ast.templateBody
tokensAndComments = templateBody
? tokenStore.getTokens(templateBody, {
tokensAndComments = rootNode
? tokenStore.getTokens(rootNode, {
includeComments: true
})
: []
Expand All @@ -99,8 +104,7 @@ function wrapContextToOverrideTokenMethods(context, tokenStore) {

/** @param {number} index */
function getNodeByRangeIndex(index) {
const templateBody = eslintSourceCode.ast.templateBody
if (!templateBody) {
if (!rootNode) {
return eslintSourceCode.ast
}

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

traverseNodes(templateBody, {
traverseNodes(rootNode, {
enterNode(node, parent) {
if (breakFlag) {
return
Expand Down Expand Up @@ -242,6 +246,7 @@ module.exports = {
* @param {string[]} [options.categories] The categories of this rule.
* @param {boolean} [options.skipDynamicArguments] If `true`, skip validation within dynamic arguments.
* @param {boolean} [options.skipDynamicArgumentsReport] If `true`, skip report within dynamic arguments.
* @param {boolean} [options.applyDocument] If `true`, apply check to document fragment.
* @param { (context: RuleContext, options: { coreHandlers: RuleListener }) => TemplateListener } [options.create] If define, extend core rule.
* @returns {RuleModule} The wrapped rule implementation.
*/
Expand All @@ -251,6 +256,7 @@ module.exports = {
categories,
skipDynamicArguments,
skipDynamicArgumentsReport,
applyDocument,
create
} = options || {}
return {
Expand All @@ -262,7 +268,9 @@ module.exports = {
// The `context.getSourceCode()` cannot access the tokens of templates.
// So override the methods which access to tokens by the `tokenStore`.
if (tokenStore) {
context = wrapContextToOverrideTokenMethods(context, tokenStore)
context = wrapContextToOverrideTokenMethods(context, tokenStore, {
applyDocument
})
}

if (skipDynamicArgumentsReport) {
Expand All @@ -277,12 +285,19 @@ module.exports = {
Object.assign({}, coreHandlers)
)
if (handlers.Program) {
handlers["VElement[parent.type!='VElement']"] = handlers.Program
handlers[
applyDocument
? 'VDocumentFragment'
: "VElement[parent.type!='VElement']"
] = /** @type {any} */ (handlers.Program)
delete handlers.Program
}
if (handlers['Program:exit']) {
handlers["VElement[parent.type!='VElement']:exit"] =
handlers['Program:exit']
handlers[
applyDocument
? 'VDocumentFragment:exit'
: "VElement[parent.type!='VElement']:exit"
] = /** @type {any} */ (handlers['Program:exit'])
delete handlers['Program:exit']
}

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

if (applyDocument) {
// Apply the handlers to document.
return defineDocumentVisitor(context, handlers)
}
// Apply the handlers to templates.
return defineTemplateBodyVisitor(context, handlers)
},
Expand Down Expand Up @@ -1812,6 +1831,30 @@ function defineTemplateBodyVisitor(
options
)
}
/**
* Register the given visitor to parser services.
* If the parser service of `vue-eslint-parser` was not found,
* this generates a warning.
*
* @param {RuleContext} context The rule context to use parser services.
* @param {TemplateListener} documentVisitor The visitor to traverse the document.
* @param { { triggerSelector: "Program" | "Program:exit" } } [options] The options.
* @returns {RuleListener} The merged visitor.
*/
function defineDocumentVisitor(context, documentVisitor, options) {
if (context.parserServices.defineDocumentVisitor == null) {
const filename = context.getFilename()
if (path.extname(filename) === '.vue') {
context.report({
loc: { line: 1, column: 0 },
message:
'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.'
})
}
return {}
}
return context.parserServices.defineDocumentVisitor(documentVisitor, options)
}

/**
* @template T
Expand Down
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.9.0"
"vue-eslint-parser": "^7.10.0"
},
"devDependencies": {
"@types/eslint": "^7.2.0",
Expand Down
29 changes: 28 additions & 1 deletion tests/lib/rules/comma-spacing.js
Expand Up @@ -61,7 +61,14 @@ tester.run('comma-spacing', rule, {
`<script>
fn = (a,b) => {}
</script>`,
`fn = (a,b) => {}`
`fn = (a,b) => {}`,
// CSS vars injection
`
<style>
.text {
color: v-bind('foo(a, b)')
}
</style>`
],
invalid: [
{
Expand Down Expand Up @@ -278,6 +285,26 @@ tester.run('comma-spacing', rule, {
line: 4
}
]
},
{
code: `
<style>
.text {
color: v-bind('foo(a,b)')
}
</style>`,
output: `
<style>
.text {
color: v-bind('foo(a, b)')
}
</style>`,
errors: [
{
message: "A space is required after ','.",
line: 4
}
]
}
]
})
40 changes: 39 additions & 1 deletion tests/lib/rules/dot-notation.js
Expand Up @@ -17,7 +17,14 @@ tester.run('dot-notation', rule, {
'<template><div attr="foo[\'bar\']" /></template>',
`<template><div :[foo.bar]="a" /></template>`,
`<template><div :attr="foo[bar]" /></template>`,
`<template><div :[foo[bar]]="a" /></template>`
`<template><div :[foo[bar]]="a" /></template>`,
// CSS vars injection
`
<style>
.text {
color: v-bind(foo.bar)
}
</style>`
],
invalid: [
{
Expand All @@ -29,6 +36,37 @@ tester.run('dot-notation', rule, {
code: `<template><div :[foo[\`bar\`]]="a" /></template>`,
output: `<template><div :[foo.bar]="a" /></template>`,
errors: ['[`bar`] is better written in dot notation.']
},
// CSS vars injection
{
code: `
<style>
.text {
color: v-bind(foo[\`bar\`])
}
</style>`,
output: `
<style>
.text {
color: v-bind(foo.bar)
}
</style>`,
errors: ['[`bar`] is better written in dot notation.']
},
{
code: `
<style>
.text {
color: v-bind("foo[\`bar\`]")
}
</style>`,
output: `
<style>
.text {
color: v-bind("foo.bar")
}
</style>`,
errors: ['[`bar`] is better written in dot notation.']
}
]
})
21 changes: 20 additions & 1 deletion tests/lib/rules/eqeqeq.js
Expand Up @@ -12,11 +12,30 @@ const tester = new RuleTester({
})

tester.run('eqeqeq', rule, {
valid: ['<template><div :attr="a === 1" /></template>'],
valid: [
'<template><div :attr="a === 1" /></template>',
// CSS vars injection
`
<style>
.text {
color: v-bind(a === 1 ? 'red' : 'blue')
}
</style>`
],
invalid: [
{
code: '<template><div :attr="a == 1" /></template>',
errors: ["Expected '===' and instead saw '=='."]
},
// CSS vars injection
{
code: `
<style>
.text {
color: v-bind(a == 1 ? 'red' : 'blue')
}
</style>`,
errors: ["Expected '===' and instead saw '=='."]
}
]
})

0 comments on commit e1cf1cd

Please sign in to comment.