Skip to content

Commit

Permalink
Improve HTML comment directives.
Browse files Browse the repository at this point in the history
- Add the support of descriptions in directive comments.
- Add the support for block-level directive comments.
  • Loading branch information
ota-meshi committed May 10, 2020
1 parent 59727a4 commit 0fa1de2
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 18 deletions.
53 changes: 50 additions & 3 deletions docs/rules/comment-directive.md
Expand Up @@ -9,7 +9,7 @@ description: support comment-directives in `<template>`
- :gear: This rule is included in all of `"plugin:vue/base"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-essential"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/recommended"` and `"plugin:vue/vue3-recommended"`.

Sole purpose of this rule is to provide `eslint-disable` functionality in `<template>`.
Sole purpose of this rule is to provide `eslint-disable` functionality in the `<template>` and in the block level.
It supports usage of the following comments:

- `eslint-disable`
Expand All @@ -34,8 +34,55 @@ This rule sends all `eslint-disable`-like comments as errors to the post-process
```vue
<template>
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
<div a="1" b="2" c="3" d="4">
</div>
<div a="1" b="2" c="3" d="4" />
</template>
```

</eslint-code-block>

The `eslint-disable`-like comments can be used in the `<template>` and in the block level.

<eslint-code-block :rules="{'vue/comment-directive': ['error'], 'vue/max-attributes-per-line': ['error'], 'vue/component-tags-order': ['error'] }">

```vue
<template>
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
<div a="1" b="2" c="3" d="4" />
</template>
<!-- eslint-disable-next-line vue/component-tags-order -->
<script>
</script>
```

</eslint-code-block>

The `eslint-disable` comments has no effect after one block.

<eslint-code-block :rules="{'vue/comment-directive': ['error'], 'vue/max-attributes-per-line': ['error'], 'vue/component-tags-order': ['error'] }">

```vue
<template>
</template>
<!-- eslint-disable vue/component-tags-order -->
<style> /* <- Warning has been disabled. */
</style>
<script> /* <- Warning are not disabled. */
</script>
```

</eslint-code-block>

The `eslint-disable`-like comments can include descriptions to explain why the comment is necessary. The description must occur after the directive and is separated from the directive by two or more consecutive `-` characters. For example:

<eslint-code-block :rules="{'vue/comment-directive': ['error'], 'vue/max-attributes-per-line': ['error']}">

```vue
<template>
<!-- eslint-disable-next-line vue/max-attributes-per-line -- Here's a description about why this disabling is necessary. -->
<div a="1" b="2" c="3" d="4" />
</template>
```

Expand Down
88 changes: 73 additions & 15 deletions lib/rules/comment-directive.js
Expand Up @@ -12,14 +12,23 @@
const COMMENT_DIRECTIVE_B = /^\s*(eslint-(?:en|dis)able)(?:\s+(\S|\S[\s\S]*\S))?\s*$/
const COMMENT_DIRECTIVE_L = /^\s*(eslint-disable(?:-next)?-line)(?:\s+(\S|\S[\s\S]*\S))?\s*$/

/**
* Remove the ignored part from a given directive comment and trim it.
* @param {string} value The comment text to strip.
* @returns {string} The stripped text.
*/
function stripDirectiveComment (value) {
return value.split(/\s-{2,}\s/u)[0].trim()
}

/**
* Parse a given comment.
* @param {RegExp} pattern The RegExp pattern to parse.
* @param {string} comment The comment value to parse.
* @returns {({type:string,rules:string[]})|null} The parsing result.
*/
function parse (pattern, comment) {
const match = pattern.exec(comment)
const match = pattern.exec(stripDirectiveComment(comment))
if (match == null) {
return null
}
Expand Down Expand Up @@ -73,7 +82,7 @@ function disable (context, loc, group, rules) {
* @returns {void}
*/
function processBlock (context, comment) {
const parsed = parse(COMMENT_DIRECTIVE_B, comment.value)
const parsed = parse(COMMENT_DIRECTIVE_B, getCommentValue(context, comment))
if (parsed != null) {
if (parsed.type === 'eslint-disable') {
disable(context, comment.loc.start, 'block', parsed.rules)
Expand All @@ -91,7 +100,7 @@ function processBlock (context, comment) {
* @returns {void}
*/
function processLine (context, comment) {
const parsed = parse(COMMENT_DIRECTIVE_L, comment.value)
const parsed = parse(COMMENT_DIRECTIVE_L, getCommentValue(context, comment))
if (parsed != null && comment.loc.start.line === comment.loc.end.line) {
const line = comment.loc.start.line + (parsed.type === 'eslint-disable-line' ? 0 : 1)
const column = -1
Expand All @@ -100,6 +109,39 @@ function processLine (context, comment) {
}
}

/**
* Extracts the top-level elements in document fragment.
* @param {VDocumentFragment} documentFragment The document fragment.
* @returns {VElement[]} The top-level elements
*/
function extractTopLevelHTMLElements (documentFragment) {
return documentFragment.children.filter(e => e.type === 'VElement')
}
/**
* Extracts the top-level comments in document fragment.
* @param {VDocumentFragment} documentFragment The document fragment.
* @returns {Token[]} The top-level comments
*/
function extractTopLevelDocumentFragmentComments (documentFragment) {
const elements = extractTopLevelHTMLElements(documentFragment)

return documentFragment.comments.filter(comment =>
elements.every(element =>
comment.range[1] <= element.range[0] || element.range[1] <= comment.range[0]
))
}
/**
* Gets the comment contents value.
* @param {RuleContext} context The rule context.
* @param {Token} comment The comment token to process.
* @returns {string} The comment contents value.
*/
function getCommentValue (context, comment) {
// Note: `comment.value` doesn't handle two consecutive hyphens correctly.
// https://github.com/mysticatea/vue-eslint-parser/issues/72
return context.getSourceCode().text.slice(comment.range[0] + 4, comment.range[1] - 3)
}

// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
Expand All @@ -116,24 +158,40 @@ module.exports = {
},

create (context) {
const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()

return {
Program (node) {
if (!node.templateBody) {
return
}
if (node.templateBody) {
// Send directives to the post-process.
for (const comment of node.templateBody.comments) {
processBlock(context, comment)
processLine(context, comment)
}

// Send directives to the post-process.
for (const comment of node.templateBody.comments) {
processBlock(context, comment)
processLine(context, comment)
// Send a clear mark to the post-process.
context.report({
loc: node.templateBody.loc.end,
message: 'clear'
})
}
if (documentFragment) {
// Send directives to the post-process.
for (const comment of extractTopLevelDocumentFragmentComments(documentFragment)) {
processBlock(context, comment)
processLine(context, comment)
}

// Send a clear mark to the post-process.
context.report({
loc: node.templateBody.loc.end,
message: 'clear'
})
// Send a clear mark to the post-process.
for (const element of extractTopLevelHTMLElements(documentFragment)) {
context.report({
loc: element.loc.end,
message: 'clear'
})
}
}
}
}
}
}

126 changes: 126 additions & 0 deletions tests/lib/rules/comment-directive.js
Expand Up @@ -226,4 +226,130 @@ describe('comment-directive', () => {
assert.deepEqual(messages[1].line, 5)
})
})

describe('allow description', () => {
it('disable all rules if <!-- eslint-disable -- description -->', () => {
const code = `
<template>
<!-- eslint-disable -- description -->
<div id id="a">Hello</div>
</template>
`
const messages = linter.executeOnText(code, 'test.vue').results[0].messages

assert.deepEqual(messages.length, 0)
})

it('enable all rules if <!-- eslint-enable -- description -->', () => {
const code = `
<template>
<!-- eslint-disable -- description -->
<div id id="a">Hello</div>
<!-- eslint-enable -- description -->
<div id id="a">Hello</div>
</template>
`
const messages = linter.executeOnText(code, 'test.vue').results[0].messages

assert.deepEqual(messages.length, 2)
assert.deepEqual(messages[0].ruleId, 'vue/no-parsing-error')
assert.deepEqual(messages[0].line, 6)
assert.deepEqual(messages[1].ruleId, 'vue/no-duplicate-attributes')
assert.deepEqual(messages[1].line, 6)
})

it('enable specific rules if <!-- eslint-enable vue/no-duplicate-attributes -- description -->', () => {
const code = `
<template>
<!-- eslint-disable vue/no-parsing-error, vue/no-duplicate-attributes -- description -->
<div id id="a">Hello</div>
<!-- eslint-enable vue/no-duplicate-attributes -- description -->
<div id id="a">Hello</div>
</template>
`
const messages = linter.executeOnText(code, 'test.vue').results[0].messages

assert.deepEqual(messages.length, 1)
assert.deepEqual(messages[0].ruleId, 'vue/no-duplicate-attributes')
assert.deepEqual(messages[0].line, 6)
})

it('disable all rules if <!-- eslint-disable-line -- description -->', () => {
const code = `
<template>
<div id id="a">Hello</div> <!-- eslint-disable-line -- description -->
</template>
`
const messages = linter.executeOnText(code, 'test.vue').results[0].messages

assert.deepEqual(messages.length, 0)
})

it('disable specific rules if <!-- eslint-disable-line vue/no-duplicate-attributes -- description -->', () => {
const code = `
<template>
<div id id="a">Hello</div> <!-- eslint-disable-line vue/no-duplicate-attributes -- description -->
</template>
`
const messages = linter.executeOnText(code, 'test.vue').results[0].messages

assert.deepEqual(messages.length, 1)
assert.deepEqual(messages[0].ruleId, 'vue/no-parsing-error')
})

it('disable all rules if <!-- eslint-disable-next-line -- description -->', () => {
const code = `
<template>
<!-- eslint-disable-next-line -- description -->
<div id id="a">Hello</div>
</template>
`
const messages = linter.executeOnText(code, 'test.vue').results[0].messages

assert.deepEqual(messages.length, 0)
})

it('disable specific rules if <!-- eslint-disable-next-line vue/no-duplicate-attributes -->', () => {
const code = `
<template>
<!-- eslint-disable-next-line vue/no-duplicate-attributes -- description -->
<div id id="a">Hello</div>
</template>
`
const messages = linter.executeOnText(code, 'test.vue').results[0].messages

assert.deepEqual(messages.length, 1)
assert.deepEqual(messages[0].ruleId, 'vue/no-parsing-error')
})
})

describe('block level directive', () => {
it('disable all rules if <!-- eslint-disable -->', () => {
const code = `
<!-- eslint-disable -->
<template>
<div id id="a">Hello</div>
</template>
`
const messages = linter.executeOnText(code, 'test.vue').results[0].messages

assert.deepEqual(messages.length, 0)
})

it('don\'t disable rules if <!-- eslint-disable --> is on after block', () => {
const code = `
<!-- eslint-disable -->
<i18n>
</i18n>
<template>
<div id id="a">Hello</div>
</template>
`
const messages = linter.executeOnText(code, 'test.vue').results[0].messages

assert.deepEqual(messages.length, 2)
assert.deepEqual(messages[0].ruleId, 'vue/no-parsing-error')
assert.deepEqual(messages[1].ruleId, 'vue/no-duplicate-attributes')
})
})
})

0 comments on commit 0fa1de2

Please sign in to comment.