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

Add vue/no-child-content rule #1707

Merged
merged 7 commits into from Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -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 `<template>` | |
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
| [vue/no-child-content](./no-child-content.md) | disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text` | |
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
| [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty | |
| [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | require valid keys in model option | |
Expand Down
52 changes: 52 additions & 0 deletions docs/rules/no-child-content.md
@@ -0,0 +1,52 @@
---
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: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

## :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.

<eslint-code-block :rules="{'vue/no-child-content': ['error']}">

```vue
<template>
<!-- ✓ GOOD -->
<div>child content</div>
<div v-html="replacesChildContent"></div>

<!-- ✗ BAD -->
<div v-html="replacesChildContent">child content</div>
</template>
```

</eslint-code-block>

## :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)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -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'),
Expand Down
93 changes: 93 additions & 0 deletions 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: undefined,
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)
}
}
}
]
})
}
}
})
}
}
172 changes: 172 additions & 0 deletions 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: '<template><div>foo</div></template>'
},
{
// element with unknown directive
filename: 'test.vue',
code: '<template><div v-foo="bar" /></template>'
},
{
// self-closing element with v-html directive
filename: 'test.vue',
code: '<template><div v-html="foo" /></template>'
},
{
// self-closing element with v-text directive
filename: 'test.vue',
code: '<template><div v-text="foo" /></template>'
},
{
// empty element with v-html directive
filename: 'test.vue',
code: '<template><div v-html="foo"></div></template>'
},
{
// self-closing element with v-t directive
filename: 'test.vue',
options: [{ additionalDirectives: ['t'] }],
code: '<template><div v-t="foo" /></template>'
}
],
invalid: [
{
// v-html directive and text content
filename: 'test.vue',
code: '<template><div v-html="foo">bar</div></template>',
errors: [
{
message:
'Child content is disallowed because it will be overwritten by the v-html directive.',
column: 29,
endColumn: 32,
suggestions: [
{ output: '<template><div v-html="foo"></div></template>' }
]
}
]
},
{
// v-html directive and whitespace-only text content
filename: 'test.vue',
code: '<template><div v-html="foo"> </div></template>',
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
errors: [
{
message:
'Child content is disallowed because it will be overwritten by the v-html directive.',
column: 29,
endColumn: 30,
suggestions: [
{ output: '<template><div v-html="foo"></div></template>' }
]
}
]
},
{
// v-html directive and text expression content
filename: 'test.vue',
code: '<template><div v-html="foo">{{ bar }}</div></template>',
errors: [
{
message:
'Child content is disallowed because it will be overwritten by the v-html directive.',
column: 29,
endColumn: 38,
suggestions: [
{ output: '<template><div v-html="foo"></div></template>' }
]
}
]
},
{
// v-html directive and child element
filename: 'test.vue',
code: '<template><div v-html="foo"><span /></div></template>',
errors: [
{
message:
'Child content is disallowed because it will be overwritten by the v-html directive.',
column: 29,
endColumn: 37,
suggestions: [
{ output: '<template><div v-html="foo"></div></template>' }
]
}
]
},
{
// v-text directive and text content
filename: 'test.vue',
code: '<template><div v-text="foo">bar</div></template>',
errors: [
{
message:
'Child content is disallowed because it will be overwritten by the v-text directive.',
column: 29,
endColumn: 32,
suggestions: [
{ output: '<template><div v-text="foo"></div></template>' }
]
}
]
},
{
// v-t directive and text content
filename: 'test.vue',
options: [{ additionalDirectives: ['t'] }],
code: '<template><div v-t="foo">bar</div></template>',
errors: [
{
message:
'Child content is disallowed because it will be overwritten by the v-t directive.',
column: 26,
endColumn: 29,
suggestions: [
{ output: '<template><div v-t="foo"></div></template>' }
]
}
]
},
{
// v-html directive and text content while v-t directive is configured
filename: 'test.vue',
options: [{ additionalDirectives: ['t'] }],
code: '<template><div v-html="foo">baz</div></template>',
errors: [
{
message:
'Child content is disallowed because it will be overwritten by the v-html directive.',
column: 29,
endColumn: 32,
suggestions: [
{ output: '<template><div v-html="foo"></div></template>' }
]
}
]
}
]
})