Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update core wrapper rules to support style CSS vars injection #1576

Merged
merged 7 commits into from Jul 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 lib/utils/style-variables/index.js
Expand Up @@ -31,7 +31,7 @@ class StyleVariablesContext {
}
}

/** @type {Map<VElement, StyleVariablesContext} */
/** @type {Map<VElement, StyleVariablesContext>} */
const cache = new Map()
/**
* Get the style vars context
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 '=='."]
}
]
})