Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,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` | :bulb: |
| [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
53 changes: 53 additions & 0 deletions docs/rules/no-child-content.md
Original file line number Diff line number Diff line change
@@ -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: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).

## :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
Original file line number Diff line number Diff line change
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
157 changes: 157 additions & 0 deletions lib/rules/no-child-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* @author Flo Edelmann
* See LICENSE file in root directory for full license.
*/
'use strict'
const { defineTemplateBodyVisitor, inRange } = require('../utils')

/**
* @typedef {object} RuleOption
* @property {string[]} additionalDirectives
*/

/**
* @param {VNode | Token} node
* @returns {boolean}
*/
function isWhiteSpaceTextNode(node) {
return node.type === 'VText' && node.value.trim() === ''
}

/**
* @param {Position} pos1
* @param {Position} pos2
* @returns {'less' | 'equal' | 'greater'}
*/
function comparePositions(pos1, pos2) {
if (
pos1.line < pos2.line ||
(pos1.line === pos2.line && pos1.column < pos2.column)
) {
return 'less'
}

if (
pos1.line > pos2.line ||
(pos1.line === pos2.line && pos1.column > pos2.column)
) {
return 'greater'
}

return 'equal'
}

/**
* @param {(VNode | Token)[]} nodes
* @returns {SourceLocation | undefined}
*/
function getLocationRange(nodes) {
/** @type {Position | undefined} */
let start
/** @type {Position | undefined} */
let end

for (const node of nodes) {
if (!start || comparePositions(node.loc.start, start) === 'less') {
start = node.loc.start
}

if (!end || comparePositions(node.loc.end, end) === 'greater') {
end = node.loc.end
}
}

if (start === undefined || end === undefined) {
return undefined
}

return { start, end }
}

// ------------------------------------------------------------------------------
// 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)
}
}

const templateBody = context.getSourceCode().ast.templateBody
const templateComments = templateBody ? templateBody.comments : []

return defineTemplateBodyVisitor(context, {
/** @param {VDirective} directiveNode */
'VAttribute[directive=true]'(directiveNode) {
const directiveName = directiveNode.key.name.name
const elementNode = directiveNode.parent.parent

const elementComments = templateComments.filter((comment) =>
inRange(elementNode.range, comment)
)

const childNodes = [...elementNode.children, ...elementComments]

if (
directives.has(directiveName) &&
childNodes.length > 0 &&
childNodes.some((childNode) => !isWhiteSpaceTextNode(childNode))
) {
context.report({
node: elementNode,
loc: getLocationRange(childNodes),
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 childNodes) {
yield fixer.remove(childNode)
}
}
}
]
})
}
}
})
}
}
2 changes: 1 addition & 1 deletion lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1695,7 +1695,7 @@ module.exports = {
/**
* Checks whether the target node is within the given range.
* @param { [number, number] } range
* @param {ASTNode} target
* @param {ASTNode | Token} target
*/
inRange(range, target) {
return range[0] <= target.range[0] && target.range[1] <= range[1]
Expand Down
Loading