diff --git a/lib/rules/comma-spacing.js b/lib/rules/comma-spacing.js index 7ac0245e8..6bad7e4b7 100644 --- a/lib/rules/comma-spacing.js +++ b/lib/rules/comma-spacing.js @@ -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 }) diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js index 712658967..bd72bd828 100644 --- a/lib/rules/dot-notation.js +++ b/lib/rules/dot-notation.js @@ -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 +}) diff --git a/lib/rules/eqeqeq.js b/lib/rules/eqeqeq.js index 39894e1fc..d918b4d80 100644 --- a/lib/rules/eqeqeq.js +++ b/lib/rules/eqeqeq.js @@ -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 +}) diff --git a/lib/rules/func-call-spacing.js b/lib/rules/func-call-spacing.js index 9fb96ab39..2054376c0 100644 --- a/lib/rules/func-call-spacing.js +++ b/lib/rules/func-call-spacing.js @@ -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 }) diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index 20c25938e..223bd630d 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -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 }) diff --git a/lib/rules/no-restricted-syntax.js b/lib/rules/no-restricted-syntax.js index a09ff6660..19feb14ef 100644 --- a/lib/rules/no-restricted-syntax.js +++ b/lib/rules/no-restricted-syntax.js @@ -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 +}) diff --git a/lib/rules/no-useless-concat.js b/lib/rules/no-useless-concat.js index 7379a1ee7..f2c51b908 100644 --- a/lib/rules/no-useless-concat.js +++ b/lib/rules/no-useless-concat.js @@ -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 +}) diff --git a/lib/rules/prefer-template.js b/lib/rules/prefer-template.js index 5cc11e2a5..6fac1a059 100644 --- a/lib/rules/prefer-template.js +++ b/lib/rules/prefer-template.js @@ -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 +}) diff --git a/lib/rules/space-in-parens.js b/lib/rules/space-in-parens.js index 29ba0403b..3a9a7e0dc 100644 --- a/lib/rules/space-in-parens.js +++ b/lib/rules/space-in-parens.js @@ -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 }) diff --git a/lib/rules/space-infix-ops.js b/lib/rules/space-infix-ops.js index 650b215e8..dd0a8af26 100644 --- a/lib/rules/space-infix-ops.js +++ b/lib/rules/space-infix-ops.js @@ -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 }) diff --git a/lib/rules/space-unary-ops.js b/lib/rules/space-unary-ops.js index 4920ca9e4..e3ece4c2f 100644 --- a/lib/rules/space-unary-ops.js +++ b/lib/rules/space-unary-ops.js @@ -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 }) diff --git a/lib/rules/template-curly-spacing.js b/lib/rules/template-curly-spacing.js index acefea6b5..549c790b6 100644 --- a/lib/rules/template-curly-spacing.js +++ b/lib/rules/template-curly-spacing.js @@ -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 }) diff --git a/lib/utils/index.js b/lib/utils/index.js index b0c189592..510e016a5 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -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 }) : [] @@ -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 } @@ -110,7 +114,7 @@ function wrapContextToOverrideTokenMethods(context, tokenStore) { const skipNodes = [] let breakFlag = false - traverseNodes(templateBody, { + traverseNodes(rootNode, { enterNode(node, parent) { if (breakFlag) { return @@ -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. */ @@ -251,6 +256,7 @@ module.exports = { categories, skipDynamicArguments, skipDynamicArgumentsReport, + applyDocument, create } = options || {} return { @@ -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) { @@ -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'] } @@ -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) }, @@ -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 diff --git a/package.json b/package.json index d55714151..7ceec76c6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/lib/rules/comma-spacing.js b/tests/lib/rules/comma-spacing.js index e5b74407f..68b70b324 100644 --- a/tests/lib/rules/comma-spacing.js +++ b/tests/lib/rules/comma-spacing.js @@ -61,7 +61,14 @@ tester.run('comma-spacing', rule, { ``, - `fn = (a,b) => {}` + `fn = (a,b) => {}`, + // CSS vars injection + ` + ` ], invalid: [ { @@ -278,6 +285,26 @@ tester.run('comma-spacing', rule, { line: 4 } ] + }, + { + code: ` + `, + output: ` + `, + errors: [ + { + message: "A space is required after ','.", + line: 4 + } + ] } ] }) diff --git a/tests/lib/rules/dot-notation.js b/tests/lib/rules/dot-notation.js index eea4c1928..ff32b85ef 100644 --- a/tests/lib/rules/dot-notation.js +++ b/tests/lib/rules/dot-notation.js @@ -17,7 +17,14 @@ tester.run('dot-notation', rule, { '', ``, ``, - `` + ``, + // CSS vars injection + ` + ` ], invalid: [ { @@ -29,6 +36,37 @@ tester.run('dot-notation', rule, { code: ``, output: ``, errors: ['[`bar`] is better written in dot notation.'] + }, + // CSS vars injection + { + code: ` + `, + output: ` + `, + errors: ['[`bar`] is better written in dot notation.'] + }, + { + code: ` + `, + output: ` + `, + errors: ['[`bar`] is better written in dot notation.'] } ] }) diff --git a/tests/lib/rules/eqeqeq.js b/tests/lib/rules/eqeqeq.js index baa6d4b6c..092c6f0d5 100644 --- a/tests/lib/rules/eqeqeq.js +++ b/tests/lib/rules/eqeqeq.js @@ -12,11 +12,30 @@ const tester = new RuleTester({ }) tester.run('eqeqeq', rule, { - valid: [''], + valid: [ + '', + // CSS vars injection + ` + ` + ], invalid: [ { code: '', errors: ["Expected '===' and instead saw '=='."] + }, + // CSS vars injection + { + code: ` + `, + errors: ["Expected '===' and instead saw '=='."] } ] }) diff --git a/tests/lib/rules/func-call-spacing.js b/tests/lib/rules/func-call-spacing.js index 93df1577c..d92502006 100644 --- a/tests/lib/rules/func-call-spacing.js +++ b/tests/lib/rules/func-call-spacing.js @@ -39,7 +39,14 @@ tester.run('func-call-spacing', rule, { `, options: ['always'] + }, + // CSS vars injection + ` + ` ], invalid: [ { @@ -80,6 +87,30 @@ tester.run('func-call-spacing', rule, { line: 3 } ] + }, + + // CSS vars injection + { + code: ` + `, + output: ` + `, + errors: [ + { + message: semver.lt(CLIEngine.version, '7.0.0') + ? 'Unexpected newline between function name and paren.' + : 'Unexpected whitespace between function name and paren.', + line: 4 + } + ] } ] }) diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index 1bb704bac..28f644a90 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -41,7 +41,14 @@ tester.run('no-extra-parens', rule, { '', '', '', - '' + '', + // CSS vars injection + ` + ` ], invalid: [ { @@ -192,6 +199,22 @@ tester.run('no-extra-parens', rule, { code: '', output: '', errors: [{ messageId: 'unexpected' }] + }, + // CSS vars injection + { + code: ` + `, + output: ` + `, + errors: [{ messageId: 'unexpected' }] } ] }) diff --git a/tests/lib/rules/no-restricted-syntax.js b/tests/lib/rules/no-restricted-syntax.js index 662c2296f..647bdc4b6 100644 --- a/tests/lib/rules/no-restricted-syntax.js +++ b/tests/lib/rules/no-restricted-syntax.js @@ -50,6 +50,21 @@ tester.run('no-restricted-syntax', rule, { message: 'Third argument of interpolate must be true' } ] + }, + // CSS vars injection + { + code: ` + `, + options: [ + { + selector: 'CallExpression', + message: 'Call expressions are not allowed.' + } + ] } ], invalid: [ @@ -183,6 +198,28 @@ tester.run('no-restricted-syntax', rule, { column: 48 } ] + }, + // CSS vars injection + { + code: ` + `, + options: [ + { + selector: 'CallExpression', + message: 'Call expressions are not allowed.' + } + ], + errors: [ + { + message: 'Call expressions are not allowed.', + line: 4, + column: 24 + } + ] } ] }) diff --git a/tests/lib/rules/no-useless-concat.js b/tests/lib/rules/no-useless-concat.js index 72e1ca551..28ac8240d 100644 --- a/tests/lib/rules/no-useless-concat.js +++ b/tests/lib/rules/no-useless-concat.js @@ -15,7 +15,14 @@ tester.run('no-useless-concat', rule, { valid: [ ``, '', - `` + ``, + // CSS vars injection + ` + ` ], invalid: [ { @@ -25,6 +32,16 @@ tester.run('no-useless-concat', rule, { { code: ``, errors: ['Unexpected string concatenation of literals.'] + }, + // CSS vars injection + { + code: ` + `, + errors: ['Unexpected string concatenation of literals.'] } ] }) diff --git a/tests/lib/rules/prefer-template.js b/tests/lib/rules/prefer-template.js index 73ad5e5b6..e35fa6d1f 100644 --- a/tests/lib/rules/prefer-template.js +++ b/tests/lib/rules/prefer-template.js @@ -22,7 +22,14 @@ tester.run('prefer-template', rule, { + `, + // CSS vars injection ` + ` ], invalid: [ { @@ -58,6 +65,27 @@ tester.run('prefer-template', rule, { line: 3 } ] + }, + // CSS vars injection + { + code: ` + `, + output: ` + `, + errors: [ + { + message: 'Unexpected string concatenation.', + line: 4 + } + ] } ] }) diff --git a/tests/lib/rules/space-in-parens.js b/tests/lib/rules/space-in-parens.js index 421fb4e95..57be0b52e 100644 --- a/tests/lib/rules/space-in-parens.js +++ b/tests/lib/rules/space-in-parens.js @@ -53,7 +53,14 @@ tester.run('space-in-parens', rule, { /> `, options: ['always'] + }, + // CSS vars injection + ` + ` ], invalid: [ { @@ -202,6 +209,32 @@ tester.run('space-in-parens', rule, { line: 4 }) ] + }, + + // CSS vars injection + { + code: ` + `, + output: ` + `, + errors: [ + errorMessage({ + messageId: 'rejectedOpeningSpace', + line: 4 + }), + errorMessage({ + messageId: 'rejectedClosingSpace', + line: 4 + }) + ] } ] }) diff --git a/tests/lib/rules/space-infix-ops.js b/tests/lib/rules/space-infix-ops.js index 427a8fdc5..26b008d86 100644 --- a/tests/lib/rules/space-infix-ops.js +++ b/tests/lib/rules/space-infix-ops.js @@ -20,7 +20,15 @@ tester.run('space-infix-ops', rule, { valid: [ '', '', - '' + '', + + // CSS vars injection + ` + ` ], invalid: [ { @@ -42,6 +50,23 @@ tester.run('space-infix-ops', rule, { code: '', output: '', errors: [message('+')] + }, + + // CSS vars injection + { + code: ` + `, + output: ` + `, + errors: [message('+'), message('+')] } ] }) diff --git a/tests/lib/rules/space-unary-ops.js b/tests/lib/rules/space-unary-ops.js index 9d74207c9..6b3e7c25f 100644 --- a/tests/lib/rules/space-unary-ops.js +++ b/tests/lib/rules/space-unary-ops.js @@ -23,7 +23,14 @@ tester.run('space-unary-ops', rule, { { code: '', options: [{ nonwords: true }] + }, + // CSS vars injection + ` + ` ], invalid: [ { @@ -46,6 +53,23 @@ tester.run('space-unary-ops', rule, { options: [{ nonwords: true }], output: '', errors: ["Unary operator '!' must be followed by whitespace."] + }, + + // CSS vars injection + { + code: ` + `, + output: ` + `, + errors: ["Unexpected space after unary operator '-'."] } ] }) diff --git a/tests/lib/rules/template-curly-spacing.js b/tests/lib/rules/template-curly-spacing.js index 02aeb43e5..6c4376969 100644 --- a/tests/lib/rules/template-curly-spacing.js +++ b/tests/lib/rules/template-curly-spacing.js @@ -38,6 +38,16 @@ tester.run('template-curly-spacing', rule, { `, options: ['always'] + }, + + // CSS vars injection + { + code: ` + ` } ], invalid: [ @@ -85,6 +95,32 @@ tester.run('template-curly-spacing', rule, { line: 3 } ] + }, + + // CSS vars injection + { + code: ` + `, + output: ` + `, + errors: [ + { + message: "Unexpected space(s) after '${'.", + line: 4 + }, + { + message: "Unexpected space(s) before '}'.", + line: 4 + } + ] } ] }) diff --git a/typings/eslint-plugin-vue/util-types/ast/ast.ts b/typings/eslint-plugin-vue/util-types/ast/ast.ts index e29c199fe..4356ebc55 100644 --- a/typings/eslint-plugin-vue/util-types/ast/ast.ts +++ b/typings/eslint-plugin-vue/util-types/ast/ast.ts @@ -145,6 +145,8 @@ export type VNodeListenerMap = { 'VFilterSequenceExpression:exit': V.VFilterSequenceExpression VFilter: V.VFilter 'VFilter:exit': V.VFilter + VDocumentFragment: V.VDocumentFragment + 'VDocumentFragment:exit': V.VDocumentFragment } & ESNodeListenerMap export type NodeListenerMap = { JSXAttribute: JSX.JSXAttribute diff --git a/typings/eslint-plugin-vue/util-types/parser-services.ts b/typings/eslint-plugin-vue/util-types/parser-services.ts index 183516770..4c938f33d 100644 --- a/typings/eslint-plugin-vue/util-types/parser-services.ts +++ b/typings/eslint-plugin-vue/util-types/parser-services.ts @@ -20,6 +20,12 @@ export interface ParserServices { templateBodyTriggerSelector: 'Program' | 'Program:exit' } ) => eslint.Rule.RuleListener + defineDocumentVisitor?: ( + documentVisitor: TemplateListener, + options?: { + triggerSelector: 'Program' | 'Program:exit' + } + ) => eslint.Rule.RuleListener getDocumentFragment?: () => VAST.VDocumentFragment | null } export namespace ParserServices {