From b6dea9f18351511664571ce6886e6807a5957823 Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Fri, 12 Nov 2021 14:14:12 +0100 Subject: [PATCH 1/6] Add `vue/no-child-content` rule --- docs/rules/no-child-content.md | 53 +++++++++ lib/index.js | 1 + lib/rules/no-child-content.js | 93 +++++++++++++++ tests/lib/rules/no-child-content.js | 172 ++++++++++++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 docs/rules/no-child-content.md create mode 100644 lib/rules/no-child-content.js create mode 100644 tests/lib/rules/no-child-content.js diff --git a/docs/rules/no-child-content.md b/docs/rules/no-child-content.md new file mode 100644 index 000000000..f4aef38fe --- /dev/null +++ b/docs/rules/no-child-content.md @@ -0,0 +1,53 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-child-content +description: disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text` +--- +# vue/no-child-content + +> disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text` + +- :exclamation: ***This rule has not been released yet.*** +- :gear: This rule is included in . + +## :book: Rule Details + +This rule reports child content of elements that have a directive which overwrites that child content. By default, those are `v-html` and `v-text`, additional ones (e.g. [Vue I18n's `v-t` directive](https://vue-i18n.intlify.dev/api/directive.html)) can be configured manually. + + + +```vue + +``` + + + +## :wrench: Options + +```json +{ + "vue/no-child-content": ["error", { + "additionalDirectives": ["foo"] // checks v-foo directive + }] +} +``` + +- `additionalDirectives` ... An array of additional directives to check, without the `v-` prefix. Empty by default; `v-html` and `v-text` are always checked. + +## :books: Further Reading + +- [`v-html` directive](https://v3.vuejs.org/api/directives.html#v-html) +- [`v-text` directive](https://v3.vuejs.org/api/directives.html#v-text) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-child-content.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-child-content.js) diff --git a/lib/index.js b/lib/index.js index 60d7aac0e..437ed3e49 100644 --- a/lib/index.js +++ b/lib/index.js @@ -58,6 +58,7 @@ module.exports = { 'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'), 'no-bare-strings-in-template': require('./rules/no-bare-strings-in-template'), 'no-boolean-default': require('./rules/no-boolean-default'), + 'no-child-content': require('./rules/no-child-content'), 'no-computed-properties-in-data': require('./rules/no-computed-properties-in-data'), 'no-confusing-v-for-v-if': require('./rules/no-confusing-v-for-v-if'), 'no-constant-condition': require('./rules/no-constant-condition'), diff --git a/lib/rules/no-child-content.js b/lib/rules/no-child-content.js new file mode 100644 index 000000000..d13e3bf7d --- /dev/null +++ b/lib/rules/no-child-content.js @@ -0,0 +1,93 @@ +/** + * @author Flo Edelmann + * See LICENSE file in root directory for full license. + */ +'use strict' +const utils = require('../utils') + +/** + * @typedef {object} RuleOption + * @property {string[]} additionalDirectives + */ + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + hasSuggestions: true, + type: 'problem', + docs: { + description: + "disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text`", + categories: [], + url: 'https://eslint.vuejs.org/rules/no-child-content.html' + }, + fixable: null, + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + additionalDirectives: { + type: 'array', + uniqueItems: true, + minItems: 1, + items: { + type: 'string' + } + } + }, + required: ['additionalDirectives'] + } + ] + }, + /** @param {RuleContext} context */ + create(context) { + const directives = new Set(['html', 'text']) + + /** @type {RuleOption | undefined} */ + const option = context.options[0] + if (option !== undefined) { + for (const directive of option.additionalDirectives) { + directives.add(directive) + } + } + + return utils.defineTemplateBodyVisitor(context, { + /** @param {VDirective} directiveNode */ + 'VAttribute[directive=true]'(directiveNode) { + const directiveName = directiveNode.key.name.name + const elementNode = directiveNode.parent.parent + + if (directives.has(directiveName) && elementNode.children.length > 0) { + const firstChildNode = elementNode.children[0] + const lastChildNode = + elementNode.children[elementNode.children.length - 1] + + context.report({ + node: elementNode, + loc: { + start: firstChildNode.loc.start, + end: lastChildNode.loc.end + }, + message: + 'Child content is disallowed because it will be overwritten by the v-{{ directiveName }} directive.', + data: { directiveName }, + suggest: [ + { + desc: 'Remove child content.', + *fix(fixer) { + for (const childNode of elementNode.children) { + yield fixer.remove(childNode) + } + } + } + ] + }) + } + } + }) + } +} diff --git a/tests/lib/rules/no-child-content.js b/tests/lib/rules/no-child-content.js new file mode 100644 index 000000000..23343304e --- /dev/null +++ b/tests/lib/rules/no-child-content.js @@ -0,0 +1,172 @@ +/** + * @author Flo Edelmann + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-child-content') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ +const ruleTester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2015 } +}) + +ruleTester.run('no-child-content', rule, { + valid: [ + { + // element without directive + filename: 'test.vue', + code: '' + }, + { + // element with unknown directive + filename: 'test.vue', + code: '' + }, + { + // self-closing element with v-html directive + filename: 'test.vue', + code: '' + }, + { + // self-closing element with v-text directive + filename: 'test.vue', + code: '' + }, + { + // empty element with v-html directive + filename: 'test.vue', + code: '' + }, + { + // self-closing element with v-t directive + filename: 'test.vue', + options: [{ additionalDirectives: ['t'] }], + code: '' + } + ], + invalid: [ + { + // v-html directive and text content + filename: 'test.vue', + code: '', + errors: [ + { + message: + 'Child content is disallowed because it will be overwritten by the v-html directive.', + column: 29, + endColumn: 32, + suggestions: [ + { output: '' } + ] + } + ] + }, + { + // v-html directive and whitespace-only text content + filename: 'test.vue', + code: '', + errors: [ + { + message: + 'Child content is disallowed because it will be overwritten by the v-html directive.', + column: 29, + endColumn: 30, + suggestions: [ + { output: '' } + ] + } + ] + }, + { + // v-html directive and text expression content + filename: 'test.vue', + code: '', + errors: [ + { + message: + 'Child content is disallowed because it will be overwritten by the v-html directive.', + column: 29, + endColumn: 38, + suggestions: [ + { output: '' } + ] + } + ] + }, + { + // v-html directive and child element + filename: 'test.vue', + code: '', + errors: [ + { + message: + 'Child content is disallowed because it will be overwritten by the v-html directive.', + column: 29, + endColumn: 37, + suggestions: [ + { output: '' } + ] + } + ] + }, + { + // v-text directive and text content + filename: 'test.vue', + code: '', + errors: [ + { + message: + 'Child content is disallowed because it will be overwritten by the v-text directive.', + column: 29, + endColumn: 32, + suggestions: [ + { output: '' } + ] + } + ] + }, + { + // v-t directive and text content + filename: 'test.vue', + options: [{ additionalDirectives: ['t'] }], + code: '', + errors: [ + { + message: + 'Child content is disallowed because it will be overwritten by the v-t directive.', + column: 26, + endColumn: 29, + suggestions: [ + { output: '' } + ] + } + ] + }, + { + // v-html directive and text content while v-t directive is configured + filename: 'test.vue', + options: [{ additionalDirectives: ['t'] }], + code: '', + errors: [ + { + message: + 'Child content is disallowed because it will be overwritten by the v-html directive.', + column: 29, + endColumn: 32, + suggestions: [ + { output: '' } + ] + } + ] + } + ] +}) From 31a2c6f06e5ba75947f1623265f71b7e3320e3e3 Mon Sep 17 00:00:00 2001 From: Flo Edelmann Date: Fri, 12 Nov 2021 14:29:26 +0100 Subject: [PATCH 2/6] Fix rule category --- docs/rules/README.md | 1 + docs/rules/no-child-content.md | 1 - lib/rules/no-child-content.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index 8619d88f1..524476c3a 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -319,6 +319,7 @@ For example: | [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: | | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `