From a441eabb0f89aba5b90950246a384d04191d41c9 Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Fri, 26 Nov 2021 13:02:21 +0100 Subject: [PATCH 1/8] Add new rule `vue/prefer-separate-static-class` --- docs/rules/README.md | 1 + docs/rules/prefer-separate-static-class.md | 42 ++++ lib/index.js | 1 + lib/rules/prefer-separate-static-class.js | 115 +++++++++ .../lib/rules/prefer-separate-static-class.js | 218 ++++++++++++++++++ 5 files changed, 377 insertions(+) create mode 100644 docs/rules/prefer-separate-static-class.md create mode 100644 lib/rules/prefer-separate-static-class.js create mode 100644 tests/lib/rules/prefer-separate-static-class.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 5794cc3ed..9d9d4b75b 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -350,6 +350,7 @@ For example: | [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | | [vue/no-v-text](./no-v-text.md) | disallow use of v-text | | | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | +| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | | [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: | diff --git a/docs/rules/prefer-separate-static-class.md b/docs/rules/prefer-separate-static-class.md new file mode 100644 index 000000000..31acc8540 --- /dev/null +++ b/docs/rules/prefer-separate-static-class.md @@ -0,0 +1,42 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/prefer-separate-static-class +description: require static class names in template to be in a separate `class` attribute +--- +# vue/prefer-separate-static-class + +> require static class names in template to be in a separate `class` attribute + +- :exclamation: ***This rule has not been released yet.*** + +## :book: Rule Details + +This rule reports static class names in dynamic class attributes. + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-separate-static-class.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-separate-static-class.js) diff --git a/lib/index.js b/lib/index.js index c9969917c..50a190bae 100644 --- a/lib/index.js +++ b/lib/index.js @@ -153,6 +153,7 @@ module.exports = { 'operator-linebreak': require('./rules/operator-linebreak'), 'order-in-components': require('./rules/order-in-components'), 'padding-line-between-blocks': require('./rules/padding-line-between-blocks'), + 'prefer-separate-static-class': require('./rules/prefer-separate-static-class'), 'prefer-template': require('./rules/prefer-template'), 'prop-name-casing': require('./rules/prop-name-casing'), 'require-component-is': require('./rules/require-component-is'), diff --git a/lib/rules/prefer-separate-static-class.js b/lib/rules/prefer-separate-static-class.js new file mode 100644 index 000000000..4b26e3d7b --- /dev/null +++ b/lib/rules/prefer-separate-static-class.js @@ -0,0 +1,115 @@ +/** + * @author Flo Edelmann + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const { defineTemplateBodyVisitor, getStringLiteralValue } = require('../utils') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * @param {ASTNode} node + * @returns {node is Literal | TemplateLiteral} + */ +function isStringLiteral(node) { + return ( + (node.type === 'Literal' && typeof node.value === 'string') || + (node.type === 'TemplateLiteral' && node.expressions.length === 0) + ) +} + +/** + * @param {Expression | VForExpression | VOnExpression | VSlotScopeExpression | VFilterSequenceExpression} expressionNode + * @returns {(Literal | TemplateLiteral)[]} + */ +function findStaticClasses(expressionNode) { + if (isStringLiteral(expressionNode)) { + return [expressionNode] + } + + if (expressionNode.type === 'ArrayExpression') { + return expressionNode.elements.flatMap((element) => { + if (element === null || element.type === 'SpreadElement') { + return [] + } + return findStaticClasses(element) + }) + } + + if (expressionNode.type === 'ObjectExpression') { + return expressionNode.properties.flatMap((property) => { + if ( + property.type === 'Property' && + property.value.type === 'Literal' && + property.value.value === true && + isStringLiteral(property.key) + ) { + return [property.key] + } + return [] + }) + } + + return [] +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: + 'require static class names in template to be in a separate `class` attribute', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/prefer-separate-static-class.html' + }, + fixable: null, + schema: [], + messages: { + preferSeparateStaticClass: + 'Static class "{{className}}" should be in a static `class` attribute.' + } + }, + /** @param {RuleContext} context */ + create(context) { + return defineTemplateBodyVisitor(context, { + /** @param {VDirectiveKey} directiveKeyNode */ + "VAttribute[directive=true] > VDirectiveKey[name.name='bind'][argument.name='class']"( + directiveKeyNode + ) { + const attributeNode = directiveKeyNode.parent + if (!attributeNode.value || !attributeNode.value.expression) { + return + } + + const staticClassNameNodes = findStaticClasses( + attributeNode.value.expression + ) + + for (const staticClassNameNode of staticClassNameNodes) { + const className = getStringLiteralValue(staticClassNameNode, true) + + if (className === null) { + continue + } + + context.report({ + node: staticClassNameNode, + messageId: 'preferSeparateStaticClass', + data: { className } + }) + } + } + }) + } +} diff --git a/tests/lib/rules/prefer-separate-static-class.js b/tests/lib/rules/prefer-separate-static-class.js new file mode 100644 index 000000000..7807ea459 --- /dev/null +++ b/tests/lib/rules/prefer-separate-static-class.js @@ -0,0 +1,218 @@ +/** + * @author Flo Edelmann + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/prefer-separate-static-class') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('prefer-separate-static-class', rule, { + valid: [ + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + }, + { + filename: 'test.vue', + code: `` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: + 'Static class "static-class" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 30, + endColumn: 44 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: + 'Static class "static-class" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 24, + endColumn: 38 + } + ] + }, + { + filename: 'test.vue', + code: '', + errors: [ + { + message: + 'Static class "static-class" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 24, + endColumn: 38 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: + 'Static class "static-class" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 24, + endColumn: 38 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: + 'Static class "static-class" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 25, + endColumn: 39 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: + 'Static class "static-class" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 25, + endColumn: 39 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: + 'Static class "static-class" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 26, + endColumn: 40 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: + 'Static class "static-class" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 25, + endColumn: 39 + } + ] + }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: + 'Static class "static-class" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 25, + endColumn: 39 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: + 'Static class "static-class-a" should be in a static `class` attribute.', + line: 7, + endLine: 7, + column: 15, + endColumn: 31 + }, + { + message: + 'Static class "static-class-b" should be in a static `class` attribute.', + line: 8, + endLine: 8, + column: 16, + endColumn: 32 + } + ] + } + ] +}) From b5dc1f37eb8d78505d1796e12c697cf22bf1e297 Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Fri, 26 Nov 2021 13:03:14 +0100 Subject: [PATCH 2/8] Also find static identifier object keys --- lib/rules/prefer-separate-static-class.js | 28 +++++++++++++++---- .../lib/rules/prefer-separate-static-class.js | 14 ++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/lib/rules/prefer-separate-static-class.js b/lib/rules/prefer-separate-static-class.js index 4b26e3d7b..06d0a91fc 100644 --- a/lib/rules/prefer-separate-static-class.js +++ b/lib/rules/prefer-separate-static-class.js @@ -25,11 +25,21 @@ function isStringLiteral(node) { ) } +/** + * @param {VReference[]} references + * @param {Identifier} identifier + * @returns {boolean} + */ +function referencesInclude(references, identifier) { + return references.some((reference) => reference.id === identifier) +} + /** * @param {Expression | VForExpression | VOnExpression | VSlotScopeExpression | VFilterSequenceExpression} expressionNode - * @returns {(Literal | TemplateLiteral)[]} + * @param {VReference[]} references + * @returns {(Literal | TemplateLiteral | Identifier)[]} */ -function findStaticClasses(expressionNode) { +function findStaticClasses(expressionNode, references) { if (isStringLiteral(expressionNode)) { return [expressionNode] } @@ -39,7 +49,7 @@ function findStaticClasses(expressionNode) { if (element === null || element.type === 'SpreadElement') { return [] } - return findStaticClasses(element) + return findStaticClasses(element, references) }) } @@ -49,7 +59,9 @@ function findStaticClasses(expressionNode) { property.type === 'Property' && property.value.type === 'Literal' && property.value.value === true && - isStringLiteral(property.key) + (isStringLiteral(property.key) || + (property.key.type === 'Identifier' && + !referencesInclude(references, property.key))) ) { return [property.key] } @@ -93,11 +105,15 @@ module.exports = { } const staticClassNameNodes = findStaticClasses( - attributeNode.value.expression + attributeNode.value.expression, + attributeNode.value.references ) for (const staticClassNameNode of staticClassNameNodes) { - const className = getStringLiteralValue(staticClassNameNode, true) + const className = + staticClassNameNode.type === 'Identifier' + ? staticClassNameNode.name + : getStringLiteralValue(staticClassNameNode, true) if (className === null) { continue diff --git a/tests/lib/rules/prefer-separate-static-class.js b/tests/lib/rules/prefer-separate-static-class.js index 7807ea459..b9cf70835 100644 --- a/tests/lib/rules/prefer-separate-static-class.js +++ b/tests/lib/rules/prefer-separate-static-class.js @@ -139,6 +139,20 @@ tester.run('prefer-separate-static-class', rule, { } ] }, + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: + 'Static class "foo" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 25, + endColumn: 28 + } + ] + }, { filename: 'test.vue', code: ``, From 18ab74bc390f795c94330c8181a9217bf9d1fc15 Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Fri, 26 Nov 2021 16:49:06 +0100 Subject: [PATCH 3/8] Add auto-fix --- docs/rules/README.md | 2 +- docs/rules/prefer-separate-static-class.md | 3 +- lib/rules/prefer-separate-static-class.js | 116 +++++++++++++++++- .../lib/rules/prefer-separate-static-class.js | 86 +++++++++++++ 4 files changed, 202 insertions(+), 5 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index 9d9d4b75b..e3c1d19e7 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -350,7 +350,7 @@ For example: | [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | | [vue/no-v-text](./no-v-text.md) | disallow use of v-text | | | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | -| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | | +| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | | [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: | diff --git a/docs/rules/prefer-separate-static-class.md b/docs/rules/prefer-separate-static-class.md index 31acc8540..0220822e2 100644 --- a/docs/rules/prefer-separate-static-class.md +++ b/docs/rules/prefer-separate-static-class.md @@ -9,12 +9,13 @@ description: require static class names in template to be in a separate `class` > require static class names in template to be in a separate `class` attribute - :exclamation: ***This rule has not been released yet.*** +- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. ## :book: Rule Details This rule reports static class names in dynamic class attributes. - + ```vue `, + output: ` + + `, errors: [ { message: From 4bf2645468fc5539ae63b9ad4486d88419238d79 Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Wed, 1 Dec 2021 19:01:36 +0100 Subject: [PATCH 4/8] Fix removing whole class directive if it's not empty --- lib/rules/prefer-separate-static-class.js | 16 ++++++++++------ tests/lib/rules/prefer-separate-static-class.js | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/rules/prefer-separate-static-class.js b/lib/rules/prefer-separate-static-class.js index f80bf287e..052aef914 100644 --- a/lib/rules/prefer-separate-static-class.js +++ b/lib/rules/prefer-separate-static-class.js @@ -196,17 +196,21 @@ module.exports = { ? listNode.properties : listNode.elements - if (elements.length === 1) { + if (elements.length === 1 && listNode === expressionNode) { yield fixer.remove(attributeNode) dynamicClassDirectiveRemoved = true return } - yield* removeNodeWithComma( - fixer, - context.parserServices.getTemplateBodyTokenStore(), - listElement - ) + const tokenStore = + context.parserServices.getTemplateBodyTokenStore() + + if (elements.length === 1) { + yield* removeNodeWithComma(fixer, tokenStore, listNode) + return + } + + yield* removeNodeWithComma(fixer, tokenStore, listElement) } } diff --git a/tests/lib/rules/prefer-separate-static-class.js b/tests/lib/rules/prefer-separate-static-class.js index ba031d3f2..498fc90dd 100644 --- a/tests/lib/rules/prefer-separate-static-class.js +++ b/tests/lib/rules/prefer-separate-static-class.js @@ -235,6 +235,21 @@ tester.run('prefer-separate-static-class', rule, { } ] }, + { + filename: 'test.vue', + code: ``, + output: ``, + errors: [ + { + message: + 'Static class "staticClass" should be in a static `class` attribute.', + line: 1, + endLine: 1, + column: 40, + endColumn: 51 + } + ] + }, { filename: 'test.vue', code: ` From bd51d1e88b00eef1b71bdd8afe1fc14c79ff7f4b Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Fri, 3 Dec 2021 08:46:54 +0100 Subject: [PATCH 5/8] Simplify check with `property.computed` --- lib/rules/prefer-separate-static-class.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/rules/prefer-separate-static-class.js b/lib/rules/prefer-separate-static-class.js index 052aef914..30a744fc2 100644 --- a/lib/rules/prefer-separate-static-class.js +++ b/lib/rules/prefer-separate-static-class.js @@ -25,15 +25,6 @@ function isStringLiteral(node) { ) } -/** - * @param {VReference[]} references - * @param {Identifier} identifier - * @returns {boolean} - */ -function referencesInclude(references, identifier) { - return references.some((reference) => reference.id === identifier) -} - /** * @param {Expression | VForExpression | VOnExpression | VSlotScopeExpression | VFilterSequenceExpression} expressionNode * @param {VReference[]} references @@ -60,8 +51,7 @@ function findStaticClasses(expressionNode, references) { property.value.type === 'Literal' && property.value.value === true && (isStringLiteral(property.key) || - (property.key.type === 'Identifier' && - !referencesInclude(references, property.key))) + (property.key.type === 'Identifier' && !property.computed)) ) { return [property.key] } From ad77407ae1708c62e7fdc2f413319fa79e88738a Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Fri, 3 Dec 2021 08:47:48 +0100 Subject: [PATCH 6/8] Change rule type to `suggestion` --- lib/rules/prefer-separate-static-class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/prefer-separate-static-class.js b/lib/rules/prefer-separate-static-class.js index 30a744fc2..943892b8e 100644 --- a/lib/rules/prefer-separate-static-class.js +++ b/lib/rules/prefer-separate-static-class.js @@ -109,7 +109,7 @@ function* removeNodeWithComma(fixer, tokenStore, node) { module.exports = { meta: { - type: 'problem', + type: 'suggestion', docs: { description: 'require static class names in template to be in a separate `class` attribute', From 031ba99c1fc830383418592d9478fbf743f815cd Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Fri, 3 Dec 2021 08:48:33 +0100 Subject: [PATCH 7/8] Make rule docs more consistent --- docs/rules/prefer-separate-static-class.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rules/prefer-separate-static-class.md b/docs/rules/prefer-separate-static-class.md index 0220822e2..155c4fb5c 100644 --- a/docs/rules/prefer-separate-static-class.md +++ b/docs/rules/prefer-separate-static-class.md @@ -19,12 +19,12 @@ This rule reports static class names in dynamic class attributes. ```vue