This commit is contained in:
darenhsu
2022-07-17 13:16:16 +08:00
parent 84759556ff
commit befd344ab0
28070 changed files with 4008428 additions and 1 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Toru Nagashima
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+54
View File
@@ -0,0 +1,54 @@
# eslint-plugin-vue
[![NPM version](https://img.shields.io/npm/v/eslint-plugin-vue.svg?style=flat)](https://npmjs.org/package/eslint-plugin-vue)
[![NPM downloads](https://img.shields.io/npm/dm/eslint-plugin-vue.svg?style=flat)](https://npmjs.org/package/eslint-plugin-vue)
[![CircleCI](https://img.shields.io/circleci/project/github/vuejs/eslint-plugin-vue/master.svg?style=flat)](https://circleci.com/gh/vuejs/eslint-plugin-vue)
[![License](https://img.shields.io/github/license/vuejs/eslint-plugin-vue.svg?style=flat)](https://github.com/vuejs/eslint-plugin-vue/blob/master/LICENSE.md)
> Official ESLint plugin for Vue.js
## :book: Documentation
See [the official website](https://eslint.vuejs.org).
## :anchor: Versioning Policy
This plugin is following [Semantic Versioning](https://semver.org/) and [ESLint's Semantic Versioning Policy](https://github.com/eslint/eslint#semantic-versioning-policy).
## :newspaper: Changelog
This project uses [GitHub Releases](https://github.com/vuejs/eslint-plugin-vue/releases).
## :beers: Contribution Guide
Contribution is welcome!
See [The ESLint Vue Plugin Developer Guide](https://eslint.vuejs.org/developer-guide/).
### Working with Rules
Before you start writing a new rule, please read [the official ESLint guide](https://eslint.org/docs/developer-guide/working-with-rules).
Next, in order to get an idea how does the AST of the code that you want to check looks like, use one of the following applications:
- [astexplorer.net](https://astexplorer.net/) - the best tool to inspect ASTs, but it doesn't support Vue template yet
- [ast.js.org](https://ast.js.org/) - not fully featured, but supports Vue template syntax
Since single file components in Vue are not plain JavaScript, the default parser couldn't be used, so a new one was introduced. `vue-eslint-parser` generates enhanced AST with nodes that represent specific parts of the template syntax, as well as what's inside the `<script>` tag.
To know more about certain nodes in produced ASTs, go here:
- [ESTree docs](https://github.com/estree/estree)
- [vue-eslint-parser AST docs](https://github.com/mysticatea/vue-eslint-parser/blob/master/docs/ast.md)
The `vue-eslint-parser` provides a few useful parser services that help traverse the produced AST and access tokens of the template:
- `context.parserServices.defineTemplateBodyVisitor(visitor, scriptVisitor)`
- `context.parserServices.getTemplateBodyTokenStore()`
Check out [an example rule](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/mustache-interpolation-spacing.js) to get a better understanding of how these work.
Please be aware that regarding what kind of code examples you'll write in tests, you'll have to accordingly set up the parser in `RuleTester` (you can do it on a per test case basis). See an example [here](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/attribute-hyphenation.js#L19).
If you'll stuck, remember there are plenty of rules you can learn from already. If you can't find the right solution, don't hesitate to reach out in [issues](https://github.com/vuejs/eslint-plugin-vue/issues) we're happy to help!
## :lock: License
See the [LICENSE](LICENSE) file for license rights and limitations (MIT).
BIN
View File
Binary file not shown.
+26
View File
@@ -0,0 +1,26 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update it's content execute "npm run update"
*/
module.exports = {
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
env: {
browser: true,
es6: true
},
plugins: [
'vue'
],
rules: {
'vue/comment-directive': 'error',
'vue/jsx-uses-vars': 'error'
}
}
+43
View File
@@ -0,0 +1,43 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update it's content execute "npm run update"
*/
module.exports = {
extends: require.resolve('./base'),
rules: {
'vue/no-async-in-computed-properties': 'error',
'vue/no-dupe-keys': 'error',
'vue/no-duplicate-attributes': 'error',
'vue/no-parsing-error': 'error',
'vue/no-reserved-keys': 'error',
'vue/no-shared-component-data': 'error',
'vue/no-side-effects-in-computed-properties': 'error',
'vue/no-template-key': 'error',
'vue/no-textarea-mustache': 'error',
'vue/no-unused-components': 'error',
'vue/no-unused-vars': 'error',
'vue/no-use-v-if-with-v-for': 'error',
'vue/require-component-is': 'error',
'vue/require-prop-type-constructor': 'error',
'vue/require-render-return': 'error',
'vue/require-v-for-key': 'error',
'vue/require-valid-default-prop': 'error',
'vue/return-in-computed-property': 'error',
'vue/use-v-on-exact': 'error',
'vue/valid-template-root': 'error',
'vue/valid-v-bind': 'error',
'vue/valid-v-cloak': 'error',
'vue/valid-v-else-if': 'error',
'vue/valid-v-else': 'error',
'vue/valid-v-for': 'error',
'vue/valid-v-html': 'error',
'vue/valid-v-if': 'error',
'vue/valid-v-model': 'error',
'vue/valid-v-on': 'error',
'vue/valid-v-once': 'error',
'vue/valid-v-pre': 'error',
'vue/valid-v-show': 'error',
'vue/valid-v-text': 'error'
}
}
+34
View File
@@ -0,0 +1,34 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update it's content execute "npm run update"
*/
module.exports = {
rules: {
'vue/array-bracket-spacing': 'off',
'vue/arrow-spacing': 'off',
'vue/block-spacing': 'off',
'vue/brace-style': 'off',
'vue/comma-dangle': 'off',
'vue/dot-location': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/html-closing-bracket-spacing': 'off',
'vue/html-indent': 'off',
'vue/html-quotes': 'off',
'vue/html-self-closing': 'off',
'vue/key-spacing': 'off',
'vue/keyword-spacing': 'off',
'vue/max-attributes-per-line': 'off',
'vue/max-len': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/mustache-interpolation-spacing': 'off',
'vue/no-multi-spaces': 'off',
'vue/no-spaces-around-equal-signs-in-attribute': 'off',
'vue/object-curly-spacing': 'off',
'vue/padding-line-between-blocks': 'off',
'vue/script-indent': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/space-infix-ops': 'off',
'vue/space-unary-ops': 'off'
}
}
+14
View File
@@ -0,0 +1,14 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update it's content execute "npm run update"
*/
module.exports = {
extends: require.resolve('./strongly-recommended'),
rules: {
'vue/attributes-order': 'warn',
'vue/no-v-html': 'warn',
'vue/order-in-components': 'warn',
'vue/this-in-template': 'warn'
}
}
+30
View File
@@ -0,0 +1,30 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update it's content execute "npm run update"
*/
module.exports = {
extends: require.resolve('./essential'),
rules: {
'vue/attribute-hyphenation': 'warn',
'vue/html-closing-bracket-newline': 'warn',
'vue/html-closing-bracket-spacing': 'warn',
'vue/html-end-tags': 'warn',
'vue/html-indent': 'warn',
'vue/html-quotes': 'warn',
'vue/html-self-closing': 'warn',
'vue/max-attributes-per-line': 'warn',
'vue/multiline-html-element-content-newline': 'warn',
'vue/mustache-interpolation-spacing': 'warn',
'vue/name-property-casing': 'warn',
'vue/no-multi-spaces': 'warn',
'vue/no-spaces-around-equal-signs-in-attribute': 'warn',
'vue/no-template-shadow': 'warn',
'vue/prop-name-casing': 'warn',
'vue/require-default-prop': 'warn',
'vue/require-prop-types': 'warn',
'vue/singleline-html-element-content-newline': 'warn',
'vue/v-bind-style': 'warn',
'vue/v-on-style': 'warn'
}
}
+119
View File
@@ -0,0 +1,119 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update it's content execute "npm run update"
*/
'use strict'
module.exports = {
rules: {
'array-bracket-spacing': require('./rules/array-bracket-spacing'),
'arrow-spacing': require('./rules/arrow-spacing'),
'attribute-hyphenation': require('./rules/attribute-hyphenation'),
'attributes-order': require('./rules/attributes-order'),
'block-spacing': require('./rules/block-spacing'),
'brace-style': require('./rules/brace-style'),
'camelcase': require('./rules/camelcase'),
'comma-dangle': require('./rules/comma-dangle'),
'comment-directive': require('./rules/comment-directive'),
'component-definition-name-casing': require('./rules/component-definition-name-casing'),
'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
'component-tags-order': require('./rules/component-tags-order'),
'dot-location': require('./rules/dot-location'),
'eqeqeq': require('./rules/eqeqeq'),
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
'html-end-tags': require('./rules/html-end-tags'),
'html-indent': require('./rules/html-indent'),
'html-quotes': require('./rules/html-quotes'),
'html-self-closing': require('./rules/html-self-closing'),
'jsx-uses-vars': require('./rules/jsx-uses-vars'),
'key-spacing': require('./rules/key-spacing'),
'keyword-spacing': require('./rules/keyword-spacing'),
'match-component-file-name': require('./rules/match-component-file-name'),
'max-attributes-per-line': require('./rules/max-attributes-per-line'),
'max-len': require('./rules/max-len'),
'multiline-html-element-content-newline': require('./rules/multiline-html-element-content-newline'),
'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),
'name-property-casing': require('./rules/name-property-casing'),
'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'),
'no-boolean-default': require('./rules/no-boolean-default'),
'no-confusing-v-for-v-if': require('./rules/no-confusing-v-for-v-if'),
'no-deprecated-scope-attribute': require('./rules/no-deprecated-scope-attribute'),
'no-deprecated-slot-attribute': require('./rules/no-deprecated-slot-attribute'),
'no-deprecated-slot-scope-attribute': require('./rules/no-deprecated-slot-scope-attribute'),
'no-dupe-keys': require('./rules/no-dupe-keys'),
'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
'no-empty-pattern': require('./rules/no-empty-pattern'),
'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
'no-multi-spaces': require('./rules/no-multi-spaces'),
'no-parsing-error': require('./rules/no-parsing-error'),
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
'no-reserved-keys': require('./rules/no-reserved-keys'),
'no-restricted-syntax': require('./rules/no-restricted-syntax'),
'no-shared-component-data': require('./rules/no-shared-component-data'),
'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'),
'no-spaces-around-equal-signs-in-attribute': require('./rules/no-spaces-around-equal-signs-in-attribute'),
'no-static-inline-styles': require('./rules/no-static-inline-styles'),
'no-template-key': require('./rules/no-template-key'),
'no-template-shadow': require('./rules/no-template-shadow'),
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
'no-unsupported-features': require('./rules/no-unsupported-features'),
'no-unused-components': require('./rules/no-unused-components'),
'no-unused-vars': require('./rules/no-unused-vars'),
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
'no-v-html': require('./rules/no-v-html'),
'object-curly-spacing': require('./rules/object-curly-spacing'),
'order-in-components': require('./rules/order-in-components'),
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
'prop-name-casing': require('./rules/prop-name-casing'),
'require-component-is': require('./rules/require-component-is'),
'require-default-prop': require('./rules/require-default-prop'),
'require-direct-export': require('./rules/require-direct-export'),
'require-name-property': require('./rules/require-name-property'),
'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
'require-prop-types': require('./rules/require-prop-types'),
'require-render-return': require('./rules/require-render-return'),
'require-v-for-key': require('./rules/require-v-for-key'),
'require-valid-default-prop': require('./rules/require-valid-default-prop'),
'return-in-computed-property': require('./rules/return-in-computed-property'),
'script-indent': require('./rules/script-indent'),
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
'sort-keys': require('./rules/sort-keys'),
'space-infix-ops': require('./rules/space-infix-ops'),
'space-unary-ops': require('./rules/space-unary-ops'),
'static-class-names-order': require('./rules/static-class-names-order'),
'this-in-template': require('./rules/this-in-template'),
'use-v-on-exact': require('./rules/use-v-on-exact'),
'v-bind-style': require('./rules/v-bind-style'),
'v-on-function-call': require('./rules/v-on-function-call'),
'v-on-style': require('./rules/v-on-style'),
'v-slot-style': require('./rules/v-slot-style'),
'valid-template-root': require('./rules/valid-template-root'),
'valid-v-bind-sync': require('./rules/valid-v-bind-sync'),
'valid-v-bind': require('./rules/valid-v-bind'),
'valid-v-cloak': require('./rules/valid-v-cloak'),
'valid-v-else-if': require('./rules/valid-v-else-if'),
'valid-v-else': require('./rules/valid-v-else'),
'valid-v-for': require('./rules/valid-v-for'),
'valid-v-html': require('./rules/valid-v-html'),
'valid-v-if': require('./rules/valid-v-if'),
'valid-v-model': require('./rules/valid-v-model'),
'valid-v-on': require('./rules/valid-v-on'),
'valid-v-once': require('./rules/valid-v-once'),
'valid-v-pre': require('./rules/valid-v-pre'),
'valid-v-show': require('./rules/valid-v-show'),
'valid-v-slot': require('./rules/valid-v-slot'),
'valid-v-text': require('./rules/valid-v-text')
},
configs: {
'base': require('./configs/base'),
'essential': require('./configs/essential'),
'no-layout-rules': require('./configs/no-layout-rules'),
'recommended': require('./configs/recommended'),
'strongly-recommended': require('./configs/strongly-recommended')
},
processors: {
'.vue': require('./processor')
}
}
+66
View File
@@ -0,0 +1,66 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/
'use strict'
module.exports = {
preprocess (code) {
return [code]
},
postprocess (messages) {
const state = {
block: {
disableAll: false,
disableRules: new Set()
},
line: {
disableAll: false,
disableRules: new Set()
}
}
// Filter messages which are in disabled area.
return messages[0].filter(message => {
if (message.ruleId === 'vue/comment-directive') {
const rules = message.message.split(' ')
const type = rules.shift()
const group = rules.shift()
switch (type) {
case '--':
state[group].disableAll = true
break
case '++':
state[group].disableAll = false
break
case '-':
for (const rule of rules) {
state[group].disableRules.add(rule)
}
break
case '+':
for (const rule of rules) {
state[group].disableRules.delete(rule)
}
break
case 'clear':
state.block.disableAll = false
state.block.disableRules.clear()
state.line.disableAll = false
state.line.disableRules.clear()
break
}
return false
} else {
return !(
state.block.disableAll ||
state.line.disableAll ||
state.block.disableRules.has(message.ruleId) ||
state.line.disableRules.has(message.ruleId)
)
}
})
},
supportsAutofix: true
}
+12
View File
@@ -0,0 +1,12 @@
/**
* @author Toru Nagashima
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta
module.exports = wrapCoreRule(
require('eslint/lib/rules/array-bracket-spacing'),
{ skipDynamicArguments: true }
)
+9
View File
@@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line
module.exports = wrapCoreRule(require('eslint/lib/rules/arrow-spacing'))
+105
View File
@@ -0,0 +1,105 @@
/**
* @fileoverview Define a style for the props casing in templates.
* @author Armano
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce attribute naming style on custom components in template',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/attribute-hyphenation.html'
},
fixable: 'code',
schema: [
{
enum: ['always', 'never']
},
{
type: 'object',
properties: {
'ignore': {
type: 'array',
items: {
allOf: [
{ type: 'string' },
{ not: { type: 'string', pattern: ':exit$' }},
{ not: { type: 'string', pattern: '^\\s*$' }}
]
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
]
},
create (context) {
const sourceCode = context.getSourceCode()
const option = context.options[0]
const optionsPayload = context.options[1]
const useHyphenated = option !== 'never'
let ignoredAttributes = ['data-', 'aria-', 'slot-scope']
if (optionsPayload && optionsPayload.ignore) {
ignoredAttributes = ignoredAttributes.concat(optionsPayload.ignore)
}
const caseConverter = casing.getConverter(useHyphenated ? 'kebab-case' : 'camelCase')
function reportIssue (node, name) {
const text = sourceCode.getText(node.key)
context.report({
node: node.key,
loc: node.loc,
message: useHyphenated ? "Attribute '{{text}}' must be hyphenated." : "Attribute '{{text}}' can't be hyphenated.",
data: {
text
},
fix: fixer => fixer.replaceText(node.key, text.replace(name, caseConverter(name)))
})
}
function isIgnoredAttribute (value) {
const isIgnored = ignoredAttributes.some(function (attr) {
return value.indexOf(attr) !== -1
})
if (isIgnored) {
return true
}
return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.defineTemplateBodyVisitor(context, {
VAttribute (node) {
if (!utils.isCustomComponent(node.parent.parent)) return
const name =
!node.directive ? node.key.rawName
: node.key.name.name === 'bind' ? node.key.argument && node.key.argument.rawName
: /* otherwise */ false
if (!name || isIgnoredAttribute(name)) return
reportIssue(node, name)
}
})
}
}
+183
View File
@@ -0,0 +1,183 @@
/**
* @fileoverview enforce ordering of attributes
* @author Erin Depew
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const ATTRS = {
DEFINITION: 'DEFINITION',
LIST_RENDERING: 'LIST_RENDERING',
CONDITIONALS: 'CONDITIONALS',
RENDER_MODIFIERS: 'RENDER_MODIFIERS',
GLOBAL: 'GLOBAL',
UNIQUE: 'UNIQUE',
TWO_WAY_BINDING: 'TWO_WAY_BINDING',
OTHER_DIRECTIVES: 'OTHER_DIRECTIVES',
OTHER_ATTR: 'OTHER_ATTR',
EVENTS: 'EVENTS',
CONTENT: 'CONTENT'
}
function getAttributeName (attribute, sourceCode) {
const isBind = attribute.directive && attribute.key.name.name === 'bind'
return isBind
? (attribute.key.argument ? sourceCode.getText(attribute.key.argument) : '')
: (attribute.directive ? getDirectiveKeyName(attribute.key, sourceCode) : attribute.key.name)
}
function getDirectiveKeyName (directiveKey, sourceCode) {
let text = 'v-' + directiveKey.name.name
if (directiveKey.argument) {
text += ':' + sourceCode.getText(directiveKey.argument)
}
for (const modifier of directiveKey.modifiers) {
text += '.' + modifier.name
}
return text
}
function getAttributeType (attribute, sourceCode) {
const isBind = attribute.directive && attribute.key.name.name === 'bind'
const name = isBind
? (attribute.key.argument ? sourceCode.getText(attribute.key.argument) : '')
: (attribute.directive ? attribute.key.name.name : attribute.key.name)
if (attribute.directive && !isBind) {
if (name === 'for') {
return ATTRS.LIST_RENDERING
} else if (name === 'if' || name === 'else-if' || name === 'else' || name === 'show' || name === 'cloak') {
return ATTRS.CONDITIONALS
} else if (name === 'pre' || name === 'once') {
return ATTRS.RENDER_MODIFIERS
} else if (name === 'model') {
return ATTRS.TWO_WAY_BINDING
} else if (name === 'on') {
return ATTRS.EVENTS
} else if (name === 'html' || name === 'text') {
return ATTRS.CONTENT
} else if (name === 'slot') {
return ATTRS.UNIQUE
} else {
return ATTRS.OTHER_DIRECTIVES
}
} else {
if (name === 'is') {
return ATTRS.DEFINITION
} else if (name === 'id') {
return ATTRS.GLOBAL
} else if (name === 'ref' || name === 'key' || name === 'slot' || name === 'slot-scope') {
return ATTRS.UNIQUE
} else {
return ATTRS.OTHER_ATTR
}
}
}
function getPosition (attribute, attributePosition, sourceCode) {
const attributeType = getAttributeType(attribute, sourceCode)
return attributePosition.hasOwnProperty(attributeType) ? attributePosition[attributeType] : -1
}
function isAlphabetical (prevNode, currNode, sourceCode) {
const isSameType = getAttributeType(prevNode, sourceCode) === getAttributeType(currNode, sourceCode)
if (isSameType) {
const prevName = getAttributeName(prevNode, sourceCode)
const currName = getAttributeName(currNode, sourceCode)
if (prevName === currName) {
const prevIsBind = Boolean(prevNode.directive && prevNode.key.name.name === 'bind')
const currIsBind = Boolean(currNode.directive && currNode.key.name.name === 'bind')
return prevIsBind <= currIsBind
}
return prevName < currName
}
return true
}
function create (context) {
const sourceCode = context.getSourceCode()
let attributeOrder = [ATTRS.DEFINITION, ATTRS.LIST_RENDERING, ATTRS.CONDITIONALS, ATTRS.RENDER_MODIFIERS, ATTRS.GLOBAL, ATTRS.UNIQUE, ATTRS.TWO_WAY_BINDING, ATTRS.OTHER_DIRECTIVES, ATTRS.OTHER_ATTR, ATTRS.EVENTS, ATTRS.CONTENT]
if (context.options[0] && context.options[0].order) {
attributeOrder = context.options[0].order
}
const attributePosition = {}
attributeOrder.forEach((item, i) => {
if (item instanceof Array) {
item.forEach((attr) => {
attributePosition[attr] = i
})
} else attributePosition[item] = i
})
let currentPosition
let previousNode
function reportIssue (node, previousNode) {
const currentNode = sourceCode.getText(node.key)
const prevNode = sourceCode.getText(previousNode.key)
context.report({
node: node.key,
loc: node.loc,
message: `Attribute "${currentNode}" should go before "${prevNode}".`,
data: {
currentNode
},
fix (fixer) {
const attributes = node.parent.attributes
const shiftAttrs = attributes.slice(attributes.indexOf(previousNode), attributes.indexOf(node) + 1)
return shiftAttrs.map((attr, i) => {
const text = attr === previousNode ? sourceCode.getText(node) : sourceCode.getText(shiftAttrs[i - 1])
return fixer.replaceText(attr, text)
})
}
})
}
return utils.defineTemplateBodyVisitor(context, {
'VStartTag' () {
currentPosition = -1
previousNode = null
},
'VAttribute' (node) {
let inAlphaOrder = true
if (currentPosition !== -1 && (context.options[0] && context.options[0].alphabetical)) {
inAlphaOrder = isAlphabetical(previousNode, node, sourceCode)
}
if ((currentPosition === -1) || ((currentPosition <= getPosition(node, attributePosition, sourceCode)) && inAlphaOrder)) {
currentPosition = getPosition(node, attributePosition, sourceCode)
previousNode = node
} else {
reportIssue(node, previousNode)
}
}
})
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce order of attributes',
category: 'recommended',
url: 'https://eslint.vuejs.org/rules/attributes-order.html'
},
fixable: 'code',
schema: {
type: 'array',
properties: {
order: {
items: {
type: 'string'
},
maxItems: 10,
minItems: 10
}
}
}
},
create
}
+12
View File
@@ -0,0 +1,12 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta
module.exports = wrapCoreRule(
require('eslint/lib/rules/block-spacing'),
{ skipDynamicArguments: true }
)
+13
View File
@@ -0,0 +1,13 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta
module.exports = wrapCoreRule(
require('eslint/lib/rules/brace-style'),
{ skipDynamicArguments: true }
)
+9
View File
@@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line
module.exports = wrapCoreRule(require('eslint/lib/rules/camelcase'))
+9
View File
@@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line
module.exports = wrapCoreRule(require('eslint/lib/rules/comma-dangle'))
+139
View File
@@ -0,0 +1,139 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/
/* eslint-disable eslint-plugin/report-message-format, consistent-docs-description */
'use strict'
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
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*$/
/**
* 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)
if (match == null) {
return null
}
const type = match[1]
const rules = (match[2] || '')
.split(',')
.map(s => s.trim())
.filter(Boolean)
return { type, rules }
}
/**
* Enable rules.
* @param {RuleContext} context The rule context.
* @param {{line:number,column:number}} loc The location information to enable.
* @param {string} group The group to enable.
* @param {string[]} rules The rule IDs to enable.
* @returns {void}
*/
function enable (context, loc, group, rules) {
if (rules.length === 0) {
context.report({ loc, message: '++ {{group}}', data: { group }})
} else {
context.report({ loc, message: '+ {{group}} {{rules}}', data: { group, rules: rules.join(' ') }})
}
}
/**
* Disable rules.
* @param {RuleContext} context The rule context.
* @param {{line:number,column:number}} loc The location information to disable.
* @param {string} group The group to disable.
* @param {string[]} rules The rule IDs to disable.
* @returns {void}
*/
function disable (context, loc, group, rules) {
if (rules.length === 0) {
context.report({ loc, message: '-- {{group}}', data: { group }})
} else {
context.report({ loc, message: '- {{group}} {{rules}}', data: { group, rules: rules.join(' ') }})
}
}
/**
* Process a given comment token.
* If the comment is `eslint-disable` or `eslint-enable` then it reports the comment.
* @param {RuleContext} context The rule context.
* @param {Token} comment The comment token to process.
* @returns {void}
*/
function processBlock (context, comment) {
const parsed = parse(COMMENT_DIRECTIVE_B, comment.value)
if (parsed != null) {
if (parsed.type === 'eslint-disable') {
disable(context, comment.loc.start, 'block', parsed.rules)
} else {
enable(context, comment.loc.start, 'block', parsed.rules)
}
}
}
/**
* Process a given comment token.
* If the comment is `eslint-disable-line` or `eslint-disable-next-line` then it reports the comment.
* @param {RuleContext} context The rule context.
* @param {Token} comment The comment token to process.
* @returns {void}
*/
function processLine (context, comment) {
const parsed = parse(COMMENT_DIRECTIVE_L, comment.value)
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
disable(context, { line, column }, 'line', parsed.rules)
enable(context, { line: line + 1, column }, 'line', parsed.rules)
}
}
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'support comment-directives in `<template>`',
category: 'base',
url: 'https://eslint.vuejs.org/rules/comment-directive.html'
},
schema: []
},
create (context) {
return {
Program (node) {
if (!node.templateBody) {
return
}
// 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'
})
}
}
}
}
@@ -0,0 +1,98 @@
/**
* @fileoverview enforce specific casing for component definition name
* @author Armano
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
const allowedCaseOptions = ['PascalCase', 'kebab-case']
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce specific casing for component definition name',
category: undefined,
// TODO Change with major version.
// category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/component-definition-name-casing.html'
},
fixable: 'code', // or "code" or "whitespace"
schema: [
{
enum: allowedCaseOptions
}
]
},
create (context) {
const options = context.options[0]
const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'PascalCase'
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
function convertName (node) {
let nodeValue
let range
if (node.type === 'TemplateLiteral') {
const quasis = node.quasis[0]
nodeValue = quasis.value.cooked
range = quasis.range
} else {
nodeValue = node.value
range = node.range
}
const value = casing.getConverter(caseType)(nodeValue)
if (value !== nodeValue) {
context.report({
node: node,
message: 'Property name "{{value}}" is not {{caseType}}.',
data: {
value: nodeValue,
caseType: caseType
},
fix: fixer => fixer.replaceTextRange([range[0] + 1, range[1] - 1], value)
})
}
}
function canConvert (node) {
return node.type === 'Literal' || (
node.type === 'TemplateLiteral' &&
node.expressions.length === 0 &&
node.quasis.length === 1
)
}
return Object.assign({},
utils.executeOnCallVueComponent(context, (node) => {
if (node.arguments.length === 2) {
const argument = node.arguments[0]
if (canConvert(argument)) {
convertName(argument)
}
}
}),
utils.executeOnVue(context, (obj) => {
const node = obj.properties
.find(item => (
item.type === 'Property' &&
item.key.name === 'name' &&
canConvert(item.value)
))
if (!node) return
convertName(node.value)
})
)
}
}
@@ -0,0 +1,152 @@
/**
* @author Yosuke Ota
* issue https://github.com/vuejs/eslint-plugin-vue/issues/250
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const casing = require('../utils/casing')
const { toRegExp } = require('../utils/regexp')
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
const allowedCaseOptions = ['PascalCase', 'kebab-case']
const defaultCase = 'PascalCase'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce specific casing for the component naming style in template',
category: undefined,
url: 'https://eslint.vuejs.org/rules/component-name-in-template-casing.html'
},
fixable: 'code',
schema: [
{
enum: allowedCaseOptions
},
{
type: 'object',
properties: {
ignores: {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
},
registeredComponentsOnly: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
create (context) {
const caseOption = context.options[0]
const options = context.options[1] || {}
const caseType = allowedCaseOptions.indexOf(caseOption) !== -1 ? caseOption : defaultCase
const ignores = (options.ignores || []).map(toRegExp)
const registeredComponentsOnly = options.registeredComponentsOnly !== false
const tokens = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
const registeredComponents = []
/**
* Checks whether the given node is the verification target node.
* @param {VElement} node element node
* @returns {boolean} `true` if the given node is the verification target node.
*/
function isVerifyTarget (node) {
if (ignores.some(re => re.test(node.rawName))) {
// ignore
return false
}
if (!registeredComponentsOnly) {
// If the user specifies registeredComponentsOnly as false, it checks all component tags.
if ((!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
utils.isHtmlWellKnownElementName(node.rawName) ||
utils.isSvgWellKnownElementName(node.rawName)
) {
return false
}
return true
}
// We only verify the components registered in the component.
if (registeredComponents
.filter(name => casing.pascalCase(name) === name) // When defining a component with PascalCase, you can use either case
.some(name => node.rawName === name || casing.pascalCase(node.rawName) === name)) {
return true
}
return false
}
let hasInvalidEOF = false
return utils.defineTemplateBodyVisitor(context, {
'VElement' (node) {
if (hasInvalidEOF) {
return
}
if (!isVerifyTarget(node)) {
return
}
const name = node.rawName
const casingName = casing.getConverter(caseType)(name)
if (casingName !== name) {
const startTag = node.startTag
const open = tokens.getFirstToken(startTag)
context.report({
node: open,
loc: open.loc,
message: 'Component name "{{name}}" is not {{caseType}}.',
data: {
name,
caseType
},
fix: fixer => {
const endTag = node.endTag
if (!endTag) {
return fixer.replaceText(open, `<${casingName}`)
}
const endTagOpen = tokens.getFirstToken(endTag)
return [
fixer.replaceText(open, `<${casingName}`),
fixer.replaceText(endTagOpen, `</${casingName}`)
]
}
})
}
}
},
Object.assign(
{
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
},
registeredComponentsOnly
? utils.executeOnVue(context, (obj) => {
registeredComponents.push(...utils.getRegisteredComponents(obj).map(n => n.name))
})
: {}
))
}
}
+95
View File
@@ -0,0 +1,95 @@
/**
* @author Yosuke Ota
* issue https://github.com/vuejs/eslint-plugin-vue/issues/140
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const DEFAULT_ORDER = Object.freeze(['script', 'template', 'style'])
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce order of component top-level elements',
category: undefined,
// TODO Change with major version.
// category: 'recommended',
url: 'https://eslint.vuejs.org/rules/component-tags-order.html'
},
fixable: null,
schema: {
type: 'array',
properties: {
order: {
type: 'array'
}
}
},
messages: {
unexpected: 'The <{{name}}> should be above the <{{firstUnorderedName}}> on line {{line}}.'
}
},
create (context) {
const order = (context.options[0] && context.options[0].order) || DEFAULT_ORDER
const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()
function getTopLevelHTMLElements () {
if (documentFragment) {
return documentFragment.children.filter(e => e.type === 'VElement')
}
return []
}
function report (element, firstUnorderedElement) {
context.report({
node: element,
loc: element.loc,
messageId: 'unexpected',
data: {
name: element.name,
firstUnorderedName: firstUnorderedElement.name,
line: firstUnorderedElement.loc.start.line
}
})
}
return utils.defineTemplateBodyVisitor(
context,
{},
{
Program (node) {
if (utils.hasInvalidEOF(node)) {
return
}
const elements = getTopLevelHTMLElements()
elements.forEach((element, index) => {
const expectedIndex = order.indexOf(element.name)
if (expectedIndex < 0) {
return
}
const firstUnordered = elements
.slice(0, index)
.filter(e => expectedIndex < order.indexOf(e.name))
.sort(
(e1, e2) => order.indexOf(e1.name) - order.indexOf(e2.name)
)[0]
if (firstUnordered) {
report(element, firstUnordered)
}
})
}
}
)
}
}
+9
View File
@@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line
module.exports = wrapCoreRule(require('eslint/lib/rules/dot-location'))
+9
View File
@@ -0,0 +1,9 @@
/**
* @author Toru Nagashima
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line
module.exports = wrapCoreRule(require('eslint/lib/rules/eqeqeq'))
@@ -0,0 +1,90 @@
/**
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
function getPhrase (lineBreaks) {
switch (lineBreaks) {
case 0: return 'no line breaks'
case 1: return '1 line break'
default: return `${lineBreaks} line breaks`
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: "require or disallow a line break before tag's closing brackets",
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/html-closing-bracket-newline.html'
},
fixable: 'whitespace',
schema: [{
type: 'object',
properties: {
'singleline': { enum: ['always', 'never'] },
'multiline': { enum: ['always', 'never'] }
},
additionalProperties: false
}]
},
create (context) {
const options = Object.assign({}, {
singleline: 'never',
multiline: 'always'
}, context.options[0] || {})
const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
return utils.defineTemplateBodyVisitor(context, {
'VStartTag, VEndTag' (node) {
const closingBracketToken = template.getLastToken(node)
if (closingBracketToken.type !== 'HTMLSelfClosingTagClose' && closingBracketToken.type !== 'HTMLTagClose') {
return
}
const prevToken = template.getTokenBefore(closingBracketToken)
const type = (node.loc.start.line === prevToken.loc.end.line) ? 'singleline' : 'multiline'
const expectedLineBreaks = (options[type] === 'always') ? 1 : 0
const actualLineBreaks = (closingBracketToken.loc.start.line - prevToken.loc.end.line)
if (actualLineBreaks !== expectedLineBreaks) {
context.report({
node,
loc: {
start: prevToken.loc.end,
end: closingBracketToken.loc.start
},
message: 'Expected {{expected}} before closing bracket, but {{actual}} found.',
data: {
expected: getPhrase(expectedLineBreaks),
actual: getPhrase(actualLineBreaks)
},
fix (fixer) {
const range = [prevToken.range[1], closingBracketToken.range[0]]
const text = '\n'.repeat(expectedLineBreaks)
return fixer.replaceTextRange(range, text)
}
})
}
}
})
}
}
@@ -0,0 +1,114 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/
'use strict'
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
const utils = require('../utils')
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
/**
* Normalize options.
* @param {{startTag?:"always"|"never",endTag?:"always"|"never",selfClosingTag?:"always"|"never"}} options The options user configured.
* @param {TokenStore} tokens The token store of template body.
* @returns {{startTag:"always"|"never",endTag:"always"|"never",selfClosingTag:"always"|"never"}} The normalized options.
*/
function parseOptions (options, tokens) {
return Object.assign({
startTag: 'never',
endTag: 'never',
selfClosingTag: 'always',
detectType (node) {
const openType = tokens.getFirstToken(node).type
const closeType = tokens.getLastToken(node).type
if (openType === 'HTMLEndTagOpen' && closeType === 'HTMLTagClose') {
return this.endTag
}
if (openType === 'HTMLTagOpen' && closeType === 'HTMLTagClose') {
return this.startTag
}
if (openType === 'HTMLTagOpen' && closeType === 'HTMLSelfClosingTagClose') {
return this.selfClosingTag
}
return null
}
}, options)
}
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'require or disallow a space before tag\'s closing brackets',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/html-closing-bracket-spacing.html'
},
schema: [{
type: 'object',
properties: {
startTag: { enum: ['always', 'never'] },
endTag: { enum: ['always', 'never'] },
selfClosingTag: { enum: ['always', 'never'] }
},
additionalProperties: false
}],
fixable: 'whitespace'
},
create (context) {
const sourceCode = context.getSourceCode()
const tokens =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
const options = parseOptions(context.options[0], tokens)
return utils.defineTemplateBodyVisitor(context, {
'VStartTag, VEndTag' (node) {
const type = options.detectType(node)
const lastToken = tokens.getLastToken(node)
const prevToken = tokens.getLastToken(node, 1)
// Skip if EOF exists in the tag or linebreak exists before `>`.
if (type == null || prevToken == null || prevToken.loc.end.line !== lastToken.loc.start.line) {
return
}
// Check and report.
const hasSpace = (prevToken.range[1] !== lastToken.range[0])
if (type === 'always' && !hasSpace) {
context.report({
node,
loc: lastToken.loc,
message: "Expected a space before '{{bracket}}', but not found.",
data: { bracket: sourceCode.getText(lastToken) },
fix: (fixer) => fixer.insertTextBefore(lastToken, ' ')
})
} else if (type === 'never' && hasSpace) {
context.report({
node,
loc: {
start: prevToken.loc.end,
end: lastToken.loc.end
},
message: "Expected no space before '{{bracket}}', but found.",
data: { bracket: sourceCode.getText(lastToken) },
fix: (fixer) => fixer.removeRange([prevToken.range[1], lastToken.range[0]])
})
}
}
})
}
}
+60
View File
@@ -0,0 +1,60 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce end tag style',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/html-end-tags.html'
},
fixable: 'code',
schema: []
},
create (context) {
let hasInvalidEOF = false
return utils.defineTemplateBodyVisitor(context, {
VElement (node) {
if (hasInvalidEOF) {
return
}
const name = node.name
const isVoid = utils.isHtmlVoidElementName(name)
const isSelfClosing = node.startTag.selfClosing
const hasEndTag = node.endTag != null
if (!isVoid && !hasEndTag && !isSelfClosing) {
context.report({
node: node.startTag,
loc: node.startTag.loc,
message: "'<{{name}}>' should have end tag.",
data: { name },
fix: (fixer) => fixer.insertTextAfter(node, `</${name}>`)
})
}
}
}, {
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
})
}
}
+68
View File
@@ -0,0 +1,68 @@
/**
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const indentCommon = require('../utils/indent-common')
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
create (context) {
const tokenStore =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
const visitor = indentCommon.defineVisitor(context, tokenStore, { baseIndent: 1 })
return utils.defineTemplateBodyVisitor(context, visitor)
},
meta: {
type: 'layout',
docs: {
description: 'enforce consistent indentation in `<template>`',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/html-indent.html'
},
fixable: 'whitespace',
schema: [
{
anyOf: [
{ type: 'integer', minimum: 1 },
{ enum: ['tab'] }
]
},
{
type: 'object',
properties: {
'attribute': { type: 'integer', minimum: 0 },
'baseIndent': { type: 'integer', minimum: 0 },
'closeBracket': { type: 'integer', minimum: 0 },
'switchCase': { type: 'integer', minimum: 0 },
'alignAttributesVertically': { type: 'boolean' },
'ignores': {
type: 'array',
items: {
allOf: [
{ type: 'string' },
{ not: { type: 'string', pattern: ':exit$' }},
{ not: { type: 'string', pattern: '^\\s*$' }}
]
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
]
}
}
+99
View File
@@ -0,0 +1,99 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce quotes style of HTML attributes',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/html-quotes.html'
},
fixable: 'code',
schema: [
{ enum: ['double', 'single'] },
{
type: 'object',
properties: {
avoidEscape: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
create (context) {
const sourceCode = context.getSourceCode()
const double = context.options[0] !== 'single'
const avoidEscape = context.options[1] && context.options[1].avoidEscape === true
const quoteChar = double ? '"' : "'"
const quoteName = double ? 'double quotes' : 'single quotes'
let hasInvalidEOF
return utils.defineTemplateBodyVisitor(context, {
'VAttribute[value!=null]' (node) {
if (hasInvalidEOF) {
return
}
const text = sourceCode.getText(node.value)
const firstChar = text[0]
if (firstChar !== quoteChar) {
const quoted = (firstChar === "'" || firstChar === '"')
if (avoidEscape && quoted) {
const contentText = text.slice(1, -1)
if (contentText.includes(quoteChar)) {
return
}
}
context.report({
node: node.value,
loc: node.value.loc,
message: 'Expected to be enclosed by {{kind}}.',
data: { kind: quoteName },
fix (fixer) {
const contentText = quoted ? text.slice(1, -1) : text
const fixToDouble = avoidEscape && !quoted && contentText.includes(quoteChar)
? (
double
? contentText.includes("'")
: !contentText.includes('"')
)
: double
const quotePattern = fixToDouble ? /"/g : /'/g
const quoteEscaped = fixToDouble ? '&quot;' : '&apos;'
const fixQuoteChar = fixToDouble ? '"' : "'"
const replacement = fixQuoteChar + contentText.replace(quotePattern, quoteEscaped) + fixQuoteChar
return fixer.replaceText(node.value, replacement)
}
})
}
}
}, {
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
})
}
}
+184
View File
@@ -0,0 +1,184 @@
/**
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* These strings wil be displayed in error messages.
*/
const ELEMENT_TYPE = Object.freeze({
NORMAL: 'HTML elements',
VOID: 'HTML void elements',
COMPONENT: 'Vue.js custom components',
SVG: 'SVG elements',
MATH: 'MathML elements'
})
/**
* Normalize the given options.
* @param {Object|undefined} options The raw options object.
* @returns {Object} Normalized options.
*/
function parseOptions (options) {
return {
[ELEMENT_TYPE.NORMAL]: (options && options.html && options.html.normal) || 'always',
[ELEMENT_TYPE.VOID]: (options && options.html && options.html.void) || 'never',
[ELEMENT_TYPE.COMPONENT]: (options && options.html && options.html.component) || 'always',
[ELEMENT_TYPE.SVG]: (options && options.svg) || 'always',
[ELEMENT_TYPE.MATH]: (options && options.math) || 'always'
}
}
/**
* Get the elementType of the given element.
* @param {VElement} node The element node to get.
* @returns {string} The elementType of the element.
*/
function getElementType (node) {
if (utils.isCustomComponent(node)) {
return ELEMENT_TYPE.COMPONENT
}
if (utils.isHtmlElementNode(node)) {
if (utils.isHtmlVoidElementName(node.name)) {
return ELEMENT_TYPE.VOID
}
return ELEMENT_TYPE.NORMAL
}
if (utils.isSvgElementNode(node)) {
return ELEMENT_TYPE.SVG
}
if (utils.isMathMLElementNode(node)) {
return ELEMENT_TYPE.MATH
}
return 'unknown elements'
}
/**
* Check whether the given element is empty or not.
* This ignores whitespaces, doesn't ignore comments.
* @param {VElement} node The element node to check.
* @param {SourceCode} sourceCode The source code object of the current context.
* @returns {boolean} `true` if the element is empty.
*/
function isEmpty (node, sourceCode) {
const start = node.startTag.range[1]
const end = (node.endTag != null) ? node.endTag.range[0] : node.range[1]
return sourceCode.text.slice(start, end).trim() === ''
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce self-closing style',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/html-self-closing.html'
},
fixable: 'code',
schema: {
definitions: {
optionValue: {
enum: ['always', 'never', 'any']
}
},
type: 'array',
items: [{
type: 'object',
properties: {
html: {
type: 'object',
properties: {
normal: { $ref: '#/definitions/optionValue' },
void: { $ref: '#/definitions/optionValue' },
component: { $ref: '#/definitions/optionValue' }
},
additionalProperties: false
},
svg: { $ref: '#/definitions/optionValue' },
math: { $ref: '#/definitions/optionValue' }
},
additionalProperties: false
}],
maxItems: 1
}
},
create (context) {
const sourceCode = context.getSourceCode()
const options = parseOptions(context.options[0])
let hasInvalidEOF = false
return utils.defineTemplateBodyVisitor(context, {
'VElement' (node) {
if (hasInvalidEOF) {
return
}
const elementType = getElementType(node)
const mode = options[elementType]
if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node, sourceCode)) {
context.report({
node,
loc: node.loc,
message: 'Require self-closing on {{elementType}} (<{{name}}>).',
data: { elementType, name: node.rawName },
fix: (fixer) => {
const tokens = context.parserServices.getTemplateBodyTokenStore()
const close = tokens.getLastToken(node.startTag)
if (close.type !== 'HTMLTagClose') {
return null
}
return fixer.replaceTextRange([close.range[0], node.range[1]], '/>')
}
})
}
if (mode === 'never' && node.startTag.selfClosing) {
context.report({
node,
loc: node.loc,
message: 'Disallow self-closing on {{elementType}} (<{{name}}/>).',
data: { elementType, name: node.rawName },
fix: (fixer) => {
const tokens = context.parserServices.getTemplateBodyTokenStore()
const close = tokens.getLastToken(node.startTag)
if (close.type !== 'HTMLSelfClosingTagClose') {
return null
}
if (elementType === ELEMENT_TYPE.VOID) {
return fixer.replaceText(close, '>')
}
// If only `close` is targeted for replacement, it conflicts with `component-name-in-template-casing`,
// so replace the entire element.
// return fixer.replaceText(close, `></${node.rawName}>`)
const elementPart = sourceCode.text.slice(node.range[0], close.range[0])
return fixer.replaceText(node, elementPart + `></${node.rawName}>`)
}
})
}
}
}, {
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
})
}
}
+70
View File
@@ -0,0 +1,70 @@
// the following rule is based on yannickcr/eslint-plugin-react
/**
The MIT License (MIT)
Copyright (c) 2014 Yannick Croissant
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @fileoverview Prevent variables used in JSX to be marked as unused
* @author Yannick Croissant
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'prevent variables used in JSX to be marked as unused', // eslint-disable-line consistent-docs-description
category: 'base',
url: 'https://eslint.vuejs.org/rules/jsx-uses-vars.html'
},
schema: []
},
create (context) {
return {
JSXOpeningElement (node) {
let name
if (node.name.name) {
// <Foo>
name = node.name.name
} else if (node.name.object) {
// <Foo...Bar>
let parent = node.name.object
while (parent.object) {
parent = parent.object
}
name = parent.name
} else {
return
}
context.markVariableAsUsed(name)
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
/**
* @author Toru Nagashima
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta
module.exports = wrapCoreRule(
require('eslint/lib/rules/key-spacing'),
{ skipDynamicArguments: true }
)
+12
View File
@@ -0,0 +1,12 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta
module.exports = wrapCoreRule(
require('eslint/lib/rules/keyword-spacing'),
{ skipDynamicArguments: true }
)
+135
View File
@@ -0,0 +1,135 @@
/**
* @fileoverview Require component name property to match its file name
* @author Rodrigo Pedra Brum <rodrigo.pedra@gmail.com>
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const casing = require('../utils/casing')
const path = require('path')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require component name property to match its file name',
category: undefined,
url: 'https://eslint.vuejs.org/rules/match-component-file-name.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
extensions: {
type: 'array',
items: {
type: 'string'
},
uniqueItems: true,
additionalItems: false
},
shouldMatchCase: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
create (context) {
const options = context.options[0]
const shouldMatchCase = (options && options.shouldMatchCase) || false
const extensionsArray = options && options.extensions
const allowedExtensions = Array.isArray(extensionsArray) ? extensionsArray : ['jsx']
const extension = path.extname(context.getFilename())
const filename = path.basename(context.getFilename(), extension)
const errors = []
let componentCount = 0
if (!allowedExtensions.includes(extension.replace(/^\./, ''))) {
return {}
}
// ----------------------------------------------------------------------
// Private
// ----------------------------------------------------------------------
function compareNames (name, filename) {
if (shouldMatchCase) {
return name === filename
}
return casing.pascalCase(name) === filename || casing.kebabCase(name) === filename
}
function verifyName (node) {
let name
if (node.type === 'TemplateLiteral') {
const quasis = node.quasis[0]
name = quasis.value.cooked
} else {
name = node.value
}
if (!compareNames(name, filename)) {
errors.push({
node: node,
message: 'Component name `{{name}}` should match file name `{{filename}}`.',
data: { filename, name }
})
}
}
function canVerify (node) {
return node.type === 'Literal' || (
node.type === 'TemplateLiteral' &&
node.expressions.length === 0 &&
node.quasis.length === 1
)
}
return Object.assign({},
utils.executeOnCallVueComponent(context, (node) => {
if (node.arguments.length === 2) {
const argument = node.arguments[0]
if (canVerify(argument)) {
verifyName(argument)
}
}
}),
utils.executeOnVue(context, (object) => {
const node = object.properties
.find(item => (
item.type === 'Property' &&
item.key.name === 'name' &&
canVerify(item.value)
))
componentCount++
if (!node) return
verifyName(node.value)
}),
{
'Program:exit' () {
if (componentCount > 1) return
errors.forEach((error) => context.report(error))
}
}
)
}
}
+175
View File
@@ -0,0 +1,175 @@
/**
* @fileoverview Define the number of attributes allows per line
* @author Filipa Lacerda
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const utils = require('../utils')
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce the maximum number of attributes per line',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/max-attributes-per-line.html'
},
fixable: 'whitespace', // or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
singleline: {
anyOf: [
{
type: 'number',
minimum: 1
},
{
type: 'object',
properties: {
max: {
type: 'number',
minimum: 1
}
},
additionalProperties: false
}
]
},
multiline: {
anyOf: [
{
type: 'number',
minimum: 1
},
{
type: 'object',
properties: {
max: {
type: 'number',
minimum: 1
},
allowFirstLine: {
type: 'boolean'
}
},
additionalProperties: false
}
]
}
}
}
]
},
create: function (context) {
const sourceCode = context.getSourceCode()
const configuration = parseOptions(context.options[0])
const multilineMaximum = configuration.multiline
const singlelinemMaximum = configuration.singleline
const canHaveFirstLine = configuration.allowFirstLine
const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
return utils.defineTemplateBodyVisitor(context, {
'VStartTag' (node) {
const numberOfAttributes = node.attributes.length
if (!numberOfAttributes) return
if (utils.isSingleLine(node) && numberOfAttributes > singlelinemMaximum) {
showErrors(node.attributes.slice(singlelinemMaximum))
}
if (!utils.isSingleLine(node)) {
if (!canHaveFirstLine && node.attributes[0].loc.start.line === node.loc.start.line) {
showErrors([node.attributes[0]])
}
groupAttrsByLine(node.attributes)
.filter(attrs => attrs.length > multilineMaximum)
.forEach(attrs => showErrors(attrs.splice(multilineMaximum)))
}
}
})
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
function parseOptions (options) {
const defaults = {
singleline: 1,
multiline: 1,
allowFirstLine: false
}
if (options) {
if (typeof options.singleline === 'number') {
defaults.singleline = options.singleline
} else if (options.singleline && options.singleline.max) {
defaults.singleline = options.singleline.max
}
if (options.multiline) {
if (typeof options.multiline === 'number') {
defaults.multiline = options.multiline
} else if (typeof options.multiline === 'object') {
if (options.multiline.max) {
defaults.multiline = options.multiline.max
}
if (options.multiline.allowFirstLine) {
defaults.allowFirstLine = options.multiline.allowFirstLine
}
}
}
}
return defaults
}
function showErrors (attributes) {
attributes.forEach((prop, i) => {
const fix = (fixer) => {
if (i !== 0) return null
// Find the closest token before the current prop
// that is not a white space
const prevToken = template.getTokenBefore(prop, {
filter: (token) => token.type !== 'HTMLWhitespace'
})
const range = [prevToken.range[1], prop.range[0]]
return fixer.replaceTextRange(range, '\n')
}
context.report({
node: prop,
loc: prop.loc,
message: '\'{{name}}\' should be on a new line.',
data: { name: sourceCode.getText(prop.key) },
fix
})
})
}
function groupAttrsByLine (attributes) {
const propsPerLine = [[attributes[0]]]
attributes.reduce((previous, current) => {
if (previous.loc.end.line === current.loc.start.line) {
propsPerLine[propsPerLine.length - 1].push(current)
} else {
propsPerLine.push([current])
}
return current
})
return propsPerLine
}
}
}
+494
View File
@@ -0,0 +1,494 @@
/**
* @author Yosuke Ota
* @fileoverview Rule to check for max length on a line of Vue file.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const OPTIONS_SCHEMA = {
type: 'object',
properties: {
code: {
type: 'integer',
minimum: 0
},
template: {
type: 'integer',
minimum: 0
},
comments: {
type: 'integer',
minimum: 0
},
tabWidth: {
type: 'integer',
minimum: 0
},
ignorePattern: {
type: 'string'
},
ignoreComments: {
type: 'boolean'
},
ignoreTrailingComments: {
type: 'boolean'
},
ignoreUrls: {
type: 'boolean'
},
ignoreStrings: {
type: 'boolean'
},
ignoreTemplateLiterals: {
type: 'boolean'
},
ignoreRegExpLiterals: {
type: 'boolean'
},
ignoreHTMLAttributeValues: {
type: 'boolean'
},
ignoreHTMLTextContents: {
type: 'boolean'
}
},
additionalProperties: false
}
const OPTIONS_OR_INTEGER_SCHEMA = {
anyOf: [
OPTIONS_SCHEMA,
{
type: 'integer',
minimum: 0
}
]
}
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
/**
* Computes the length of a line that may contain tabs. The width of each
* tab will be the number of spaces to the next tab stop.
* @param {string} line The line.
* @param {int} tabWidth The width of each tab stop in spaces.
* @returns {int} The computed line length.
* @private
*/
function computeLineLength (line, tabWidth) {
let extraCharacterCount = 0
line.replace(/\t/gu, (match, offset) => {
const totalOffset = offset + extraCharacterCount
const previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0
const spaceCount = tabWidth - previousTabStopOffset
extraCharacterCount += spaceCount - 1 // -1 for the replaced tab
})
return Array.from(line).length + extraCharacterCount
}
/**
* Tells if a given comment is trailing: it starts on the current line and
* extends to or past the end of the current line.
* @param {string} line The source line we want to check for a trailing comment on
* @param {number} lineNumber The one-indexed line number for line
* @param {ASTNode} comment The comment to inspect
* @returns {boolean} If the comment is trailing on the given line
*/
function isTrailingComment (line, lineNumber, comment) {
return comment &&
(comment.loc.start.line === lineNumber && lineNumber <= comment.loc.end.line) &&
(comment.loc.end.line > lineNumber || comment.loc.end.column === line.length)
}
/**
* Tells if a comment encompasses the entire line.
* @param {string} line The source line with a trailing comment
* @param {number} lineNumber The one-indexed line number this is on
* @param {ASTNode} comment The comment to remove
* @returns {boolean} If the comment covers the entire line
*/
function isFullLineComment (line, lineNumber, comment) {
const start = comment.loc.start
const end = comment.loc.end
const isFirstTokenOnLine = !line.slice(0, comment.loc.start.column).trim()
return comment &&
(start.line < lineNumber || (start.line === lineNumber && isFirstTokenOnLine)) &&
(end.line > lineNumber || (end.line === lineNumber && end.column === line.length))
}
/**
* Gets the line after the comment and any remaining trailing whitespace is
* stripped.
* @param {string} line The source line with a trailing comment
* @param {ASTNode} comment The comment to remove
* @returns {string} Line without comment and trailing whitepace
*/
function stripTrailingComment (line, comment) {
// loc.column is zero-indexed
return line.slice(0, comment.loc.start.column).replace(/\s+$/u, '')
}
/**
* Ensure that an array exists at [key] on `object`, and add `value` to it.
*
* @param {Object} object the object to mutate
* @param {string} key the object's key
* @param {*} value the value to add
* @returns {void}
* @private
*/
function ensureArrayAndPush (object, key, value) {
if (!Array.isArray(object[key])) {
object[key] = []
}
object[key].push(value)
}
/**
* A reducer to group an AST node by line number, both start and end.
*
* @param {Object} acc the accumulator
* @param {ASTNode} node the AST node in question
* @returns {Object} the modified accumulator
* @private
*/
function groupByLineNumber (acc, node) {
for (let i = node.loc.start.line; i <= node.loc.end.line; ++i) {
ensureArrayAndPush(acc, i, node)
}
return acc
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce a maximum line length',
category: undefined,
url: 'https://eslint.vuejs.org/rules/max-len.html'
},
schema: [
OPTIONS_OR_INTEGER_SCHEMA,
OPTIONS_OR_INTEGER_SCHEMA,
OPTIONS_SCHEMA
],
messages: {
max: 'This line has a length of {{lineLength}}. Maximum allowed is {{maxLength}}.',
maxComment: 'This line has a comment length of {{lineLength}}. Maximum allowed is {{maxCommentLength}}.'
}
},
create (context) {
/*
* Inspired by http://tools.ietf.org/html/rfc3986#appendix-B, however:
* - They're matching an entire string that we know is a URI
* - We're matching part of a string where we think there *might* be a URL
* - We're only concerned about URLs, as picking out any URI would cause
* too many false positives
* - We don't care about matching the entire URL, any small segment is fine
*/
const URL_REGEXP = /[^:/?#]:\/\/[^?#]/u
const sourceCode = context.getSourceCode()
const tokens = []
const comments = []
const htmlAttributeValues = []
// The options object must be the last option specified…
const options = Object.assign({}, context.options[context.options.length - 1])
// …but max code length…
if (typeof context.options[0] === 'number') {
options.code = context.options[0]
}
// …and tabWidth can be optionally specified directly as integers.
if (typeof context.options[1] === 'number') {
options.tabWidth = context.options[1]
}
const scriptMaxLength = typeof options.code === 'number' ? options.code : 80
const tabWidth = typeof options.tabWidth === 'number' ? options.tabWidth : 2// default value of `vue/html-indent`
const templateMaxLength = typeof options.template === 'number' ? options.template : scriptMaxLength
const ignoreComments = !!options.ignoreComments
const ignoreStrings = !!options.ignoreStrings
const ignoreTemplateLiterals = !!options.ignoreTemplateLiterals
const ignoreRegExpLiterals = !!options.ignoreRegExpLiterals
const ignoreTrailingComments = !!options.ignoreTrailingComments || !!options.ignoreComments
const ignoreUrls = !!options.ignoreUrls
const ignoreHTMLAttributeValues = !!options.ignoreHTMLAttributeValues
const ignoreHTMLTextContents = !!options.ignoreHTMLTextContents
const maxCommentLength = options.comments
let ignorePattern = options.ignorePattern || null
if (ignorePattern) {
ignorePattern = new RegExp(ignorePattern, 'u')
}
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
/**
* Retrieves an array containing all strings (" or ') in the source code.
*
* @returns {ASTNode[]} An array of string nodes.
*/
function getAllStrings () {
return tokens.filter(token => (token.type === 'String' ||
(token.type === 'JSXText' && sourceCode.getNodeByRangeIndex(token.range[0] - 1).type === 'JSXAttribute')))
}
/**
* Retrieves an array containing all template literals in the source code.
*
* @returns {ASTNode[]} An array of template literal nodes.
*/
function getAllTemplateLiterals () {
return tokens.filter(token => token.type === 'Template')
}
/**
* Retrieves an array containing all RegExp literals in the source code.
*
* @returns {ASTNode[]} An array of RegExp literal nodes.
*/
function getAllRegExpLiterals () {
return tokens.filter(token => token.type === 'RegularExpression')
}
/**
* Retrieves an array containing all HTML texts in the source code.
*
* @returns {ASTNode[]} An array of HTML text nodes.
*/
function getAllHTMLTextContents () {
return tokens.filter(token => token.type === 'HTMLText')
}
/**
* Check the program for max length
* @param {ASTNode} node Node to examine
* @returns {void}
* @private
*/
function checkProgramForMaxLength (node) {
const programNode = node
const templateBody = node.templateBody
// setup tokens
const scriptTokens = sourceCode.ast.tokens
const scriptComments = sourceCode.getAllComments()
if (context.parserServices.getTemplateBodyTokenStore && templateBody) {
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const templateTokens = tokenStore.getTokens(templateBody, { includeComments: true })
if (templateBody.range[0] < programNode.range[0]) {
tokens.push(...templateTokens, ...scriptTokens)
} else {
tokens.push(...scriptTokens, ...templateTokens)
}
} else {
tokens.push(...scriptTokens)
}
if (ignoreComments || maxCommentLength || ignoreTrailingComments) {
// list of comments to ignore
if (templateBody) {
if (templateBody.range[0] < programNode.range[0]) {
comments.push(...templateBody.comments, ...scriptComments)
} else {
comments.push(...scriptComments, ...templateBody.comments)
}
} else {
comments.push(...scriptComments)
}
}
let scriptLinesRange
if (scriptTokens.length) {
if (scriptComments.length) {
scriptLinesRange = [
Math.min(scriptTokens[0].loc.start.line, scriptComments[0].loc.start.line),
Math.max(scriptTokens[scriptTokens.length - 1].loc.end.line, scriptComments[scriptComments.length - 1].loc.end.line)
]
} else {
scriptLinesRange = [
scriptTokens[0].loc.start.line,
scriptTokens[scriptTokens.length - 1].loc.end.line
]
}
} else if (scriptComments.length) {
scriptLinesRange = [
scriptComments[0].loc.start.line,
scriptComments[scriptComments.length - 1].loc.end.line
]
}
const templateLinesRange = templateBody && [templateBody.loc.start.line, templateBody.loc.end.line]
// split (honors line-ending)
const lines = sourceCode.lines
const strings = getAllStrings()
const stringsByLine = strings.reduce(groupByLineNumber, {})
const templateLiterals = getAllTemplateLiterals()
const templateLiteralsByLine = templateLiterals.reduce(groupByLineNumber, {})
const regExpLiterals = getAllRegExpLiterals()
const regExpLiteralsByLine = regExpLiterals.reduce(groupByLineNumber, {})
const htmlAttributeValuesByLine = htmlAttributeValues.reduce(groupByLineNumber, {})
const htmlTextContents = getAllHTMLTextContents()
const htmlTextContentsByLine = htmlTextContents.reduce(groupByLineNumber, {})
const commentsByLine = comments.reduce(groupByLineNumber, {})
lines.forEach((line, i) => {
// i is zero-indexed, line numbers are one-indexed
const lineNumber = i + 1
const inScript = (scriptLinesRange && scriptLinesRange[0] <= lineNumber && lineNumber <= scriptLinesRange[1])
const inTemplate = (templateLinesRange && templateLinesRange[0] <= lineNumber && lineNumber <= templateLinesRange[1])
// check if line is inside a script or template.
if (!inScript && !inTemplate) {
// out of range.
return
}
const maxLength = inScript && inTemplate
? Math.max(scriptMaxLength, templateMaxLength)
: inScript
? scriptMaxLength
: templateMaxLength
if (
(ignoreStrings && stringsByLine[lineNumber]) ||
(ignoreTemplateLiterals && templateLiteralsByLine[lineNumber]) ||
(ignoreRegExpLiterals && regExpLiteralsByLine[lineNumber]) ||
(ignoreHTMLAttributeValues && htmlAttributeValuesByLine[lineNumber]) ||
(ignoreHTMLTextContents && htmlTextContentsByLine[lineNumber])
) {
// ignore this line
return
}
/*
* if we're checking comment length; we need to know whether this
* line is a comment
*/
let lineIsComment = false
let textToMeasure
/*
* comments to check.
*/
if (commentsByLine[lineNumber]) {
const commentList = [...commentsByLine[lineNumber]]
let comment = commentList.pop()
if (isFullLineComment(line, lineNumber, comment)) {
lineIsComment = true
textToMeasure = line
} else if (ignoreTrailingComments && isTrailingComment(line, lineNumber, comment)) {
textToMeasure = stripTrailingComment(line, comment)
// ignore multiple trailing comments in the same line
comment = commentList.pop()
while (isTrailingComment(textToMeasure, lineNumber, comment)) {
textToMeasure = stripTrailingComment(textToMeasure, comment)
}
} else {
textToMeasure = line
}
} else {
textToMeasure = line
}
if ((ignorePattern && ignorePattern.test(textToMeasure)) ||
(ignoreUrls && URL_REGEXP.test(textToMeasure))) {
// ignore this line
return
}
const lineLength = computeLineLength(textToMeasure, tabWidth)
const commentLengthApplies = lineIsComment && maxCommentLength
if (lineIsComment && ignoreComments) {
return
}
if (commentLengthApplies) {
if (lineLength > maxCommentLength) {
context.report({
node,
loc: { line: lineNumber, column: 0 },
messageId: 'maxComment',
data: {
lineLength,
maxCommentLength
}
})
}
} else if (lineLength > maxLength) {
context.report({
node,
loc: { line: lineNumber, column: 0 },
messageId: 'max',
data: {
lineLength,
maxLength
}
})
}
})
}
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
const bodyVisitor = utils.defineTemplateBodyVisitor(context,
{
'VAttribute[directive=false] > VLiteral' (node) {
htmlAttributeValues.push(node)
}
}
)
return Object.assign({}, bodyVisitor,
{
'Program:exit' (node) {
if (bodyVisitor['Program:exit']) {
bodyVisitor['Program:exit'](node)
}
checkProgramForMaxLength(node)
}
}
)
}
}
@@ -0,0 +1,193 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const casing = require('../utils/casing')
const INLINE_ELEMENTS = require('../utils/inline-non-void-elements.json')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
function isMultilineElement (element) {
return element.loc.start.line < element.endTag.loc.start.line
}
function parseOptions (options) {
return Object.assign({
ignores: ['pre', 'textarea'].concat(INLINE_ELEMENTS),
ignoreWhenEmpty: true,
allowEmptyLines: false
}, options)
}
function getPhrase (lineBreaks) {
switch (lineBreaks) {
case 0: return 'no'
default: return `${lineBreaks}`
}
}
/**
* Check whether the given element is empty or not.
* This ignores whitespaces, doesn't ignore comments.
* @param {VElement} node The element node to check.
* @param {SourceCode} sourceCode The source code object of the current context.
* @returns {boolean} `true` if the element is empty.
*/
function isEmpty (node, sourceCode) {
const start = node.startTag.range[1]
const end = node.endTag.range[0]
return sourceCode.text.slice(start, end).trim() === ''
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'require a line break before and after the contents of a multiline element',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/multiline-html-element-content-newline.html'
},
fixable: 'whitespace',
schema: [{
type: 'object',
properties: {
ignoreWhenEmpty: {
type: 'boolean'
},
ignores: {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
},
allowEmptyLines: {
type: 'boolean'
}
},
additionalProperties: false
}],
messages: {
unexpectedAfterClosingBracket: 'Expected 1 line break after opening tag (`<{{name}}>`), but {{actual}} line breaks found.',
unexpectedBeforeOpeningBracket: 'Expected 1 line break before closing tag (`</{{name}}>`), but {{actual}} line breaks found.'
}
},
create (context) {
const options = parseOptions(context.options[0])
const ignores = options.ignores
const ignoreWhenEmpty = options.ignoreWhenEmpty
const allowEmptyLines = options.allowEmptyLines
const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
const sourceCode = context.getSourceCode()
let inIgnoreElement
function isIgnoredElement (node) {
return ignores.includes(node.name) ||
ignores.includes(casing.pascalCase(node.rawName)) ||
ignores.includes(casing.kebabCase(node.rawName))
}
function isInvalidLineBreaks (lineBreaks) {
if (allowEmptyLines) {
return lineBreaks === 0
} else {
return lineBreaks !== 1
}
}
return utils.defineTemplateBodyVisitor(context, {
'VElement' (node) {
if (inIgnoreElement) {
return
}
if (isIgnoredElement(node)) {
// ignore element name
inIgnoreElement = node
return
}
if (node.startTag.selfClosing || !node.endTag) {
// self closing
return
}
if (!isMultilineElement(node)) {
return
}
const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' }
if (
ignoreWhenEmpty &&
node.children.length === 0 &&
template.getFirstTokensBetween(node.startTag, node.endTag, getTokenOption).length === 0
) {
return
}
const contentFirst = template.getTokenAfter(node.startTag, getTokenOption)
const contentLast = template.getTokenBefore(node.endTag, getTokenOption)
const beforeLineBreaks = contentFirst.loc.start.line - node.startTag.loc.end.line
const afterLineBreaks = node.endTag.loc.start.line - contentLast.loc.end.line
if (isInvalidLineBreaks(beforeLineBreaks)) {
context.report({
node: template.getLastToken(node.startTag),
loc: {
start: node.startTag.loc.end,
end: contentFirst.loc.start
},
messageId: 'unexpectedAfterClosingBracket',
data: {
name: node.rawName,
actual: getPhrase(beforeLineBreaks)
},
fix (fixer) {
const range = [node.startTag.range[1], contentFirst.range[0]]
return fixer.replaceTextRange(range, '\n')
}
})
}
if (isEmpty(node, sourceCode)) {
return
}
if (isInvalidLineBreaks(afterLineBreaks)) {
context.report({
node: template.getFirstToken(node.endTag),
loc: {
start: contentLast.loc.end,
end: node.endTag.loc.start
},
messageId: 'unexpectedBeforeOpeningBracket',
data: {
name: node.name,
actual: getPhrase(afterLineBreaks)
},
fix (fixer) {
const range = [contentLast.range[1], node.endTag.range[0]]
return fixer.replaceTextRange(range, '\n')
}
})
}
},
'VElement:exit' (node) {
if (inIgnoreElement === node) {
inIgnoreElement = null
}
}
})
}
}
@@ -0,0 +1,100 @@
/**
* @fileoverview enforce unified spacing in mustache interpolations.
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce unified spacing in mustache interpolations',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/mustache-interpolation-spacing.html'
},
fixable: 'whitespace',
schema: [
{
enum: ['always', 'never']
}
]
},
create (context) {
const options = context.options[0] || 'always'
const template =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.defineTemplateBodyVisitor(context, {
'VExpressionContainer[expression!=null]' (node) {
const openBrace = template.getFirstToken(node)
const closeBrace = template.getLastToken(node)
if (
!openBrace ||
!closeBrace ||
openBrace.type !== 'VExpressionStart' ||
closeBrace.type !== 'VExpressionEnd'
) {
return
}
const firstToken = template.getTokenAfter(openBrace, { includeComments: true })
const lastToken = template.getTokenBefore(closeBrace, { includeComments: true })
if (options === 'always') {
if (openBrace.range[1] === firstToken.range[0]) {
context.report({
node: openBrace,
message: "Expected 1 space after '{{', but not found.",
fix: (fixer) => fixer.insertTextAfter(openBrace, ' ')
})
}
if (closeBrace.range[0] === lastToken.range[1]) {
context.report({
node: closeBrace,
message: "Expected 1 space before '}}', but not found.",
fix: (fixer) => fixer.insertTextBefore(closeBrace, ' ')
})
}
} else {
if (openBrace.range[1] !== firstToken.range[0]) {
context.report({
loc: {
start: openBrace.loc.start,
end: firstToken.loc.start
},
message: "Expected no space after '{{', but found.",
fix: (fixer) => fixer.removeRange([openBrace.range[1], firstToken.range[0]])
})
}
if (closeBrace.range[0] !== lastToken.range[1]) {
context.report({
loc: {
start: lastToken.loc.end,
end: closeBrace.loc.end
},
message: "Expected no space before '}}', but found.",
fix: (fixer) => fixer.removeRange([lastToken.range[1], closeBrace.range[0]])
})
}
}
}
})
}
}
+65
View File
@@ -0,0 +1,65 @@
/**
* @fileoverview Requires specific casing for the name property in Vue components
* @author Armano
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
const allowedCaseOptions = ['PascalCase', 'kebab-case']
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce specific casing for the name property in Vue components',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/name-property-casing.html'
},
// deprecated: true, // TODO Change with major version.
// replacedBy: ['component-definition-name-casing'], // TODO Change with major version.
fixable: 'code', // or "code" or "whitespace"
schema: [
{
enum: allowedCaseOptions
}
]
},
create (context) {
const options = context.options[0]
const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'PascalCase'
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const node = obj.properties
.find(item => (
item.type === 'Property' &&
item.key.name === 'name' &&
item.value.type === 'Literal'
))
if (!node) return
const value = casing.getConverter(caseType)(node.value.value)
if (value !== node.value.value) {
context.report({
node: node.value,
message: 'Property name "{{value}}" is not {{caseType}}.',
data: {
value: node.value.value,
caseType: caseType
},
fix: fixer => fixer.replaceText(node.value, node.value.raw.replace(node.value.value, value))
})
}
})
}
}
@@ -0,0 +1,165 @@
/**
* @fileoverview Check if there are no asynchronous actions inside computed properties.
* @author Armano
*/
'use strict'
const utils = require('../utils')
const PROMISE_FUNCTIONS = [
'then',
'catch',
'finally'
]
const PROMISE_METHODS = [
'all',
'race',
'reject',
'resolve'
]
const TIMED_FUNCTIONS = [
'setTimeout',
'setInterval',
'setImmediate',
'requestAnimationFrame'
]
function isTimedFunction (node) {
return ((
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
TIMED_FUNCTIONS.indexOf(node.callee.name) !== -1
) || (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'window' && (
TIMED_FUNCTIONS.indexOf(node.callee.property.name) !== -1
)
)) && node.arguments.length
}
function isPromise (node) {
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {
return ( // hello.PROMISE_FUNCTION()
node.callee.property.type === 'Identifier' &&
PROMISE_FUNCTIONS.indexOf(node.callee.property.name) !== -1
) || ( // Promise.PROMISE_METHOD()
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'Promise' &&
PROMISE_METHODS.indexOf(node.callee.property.name) !== -1
)
}
return false
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow asynchronous actions in computed properties',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-async-in-computed-properties.html'
},
fixable: null,
schema: []
},
create (context) {
const forbiddenNodes = []
let scopeStack = { upper: null, body: null }
const expressionTypes = {
promise: 'asynchronous action',
await: 'await operator',
async: 'async function declaration',
new: 'Promise object',
timed: 'timed function'
}
function onFunctionEnter (node) {
if (node.async) {
forbiddenNodes.push({
node: node,
type: 'async',
targetBody: node.body
})
}
scopeStack = { upper: scopeStack, body: node.body }
}
function onFunctionExit () {
scopeStack = scopeStack.upper
}
return Object.assign({},
{
':function': onFunctionEnter,
':function:exit': onFunctionExit,
NewExpression (node) {
if (node.callee.name === 'Promise') {
forbiddenNodes.push({
node: node,
type: 'new',
targetBody: scopeStack.body
})
}
},
CallExpression (node) {
if (isPromise(node)) {
forbiddenNodes.push({
node: node,
type: 'promise',
targetBody: scopeStack.body
})
} else if (isTimedFunction(node)) {
forbiddenNodes.push({
node: node,
type: 'timed',
targetBody: scopeStack.body
})
}
},
AwaitExpression (node) {
forbiddenNodes.push({
node: node,
type: 'await',
targetBody: scopeStack.body
})
}
},
utils.executeOnVue(context, (obj) => {
const computedProperties = utils.getComputedProperties(obj)
computedProperties.forEach(cp => {
forbiddenNodes.forEach(el => {
if (
cp.value &&
el.node.loc.start.line >= cp.value.loc.start.line &&
el.node.loc.end.line <= cp.value.loc.end.line &&
el.targetBody === cp.value
) {
context.report({
node: el.node,
message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
data: {
expressionName: expressionTypes[el.type],
propertyName: cp.key
}
})
}
})
})
})
)
}
}
+95
View File
@@ -0,0 +1,95 @@
/**
* @fileoverview Prevents boolean defaults from being set
* @author Hiroki Osame
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function isBooleanProp (prop) {
return (
prop.type === 'Property' &&
prop.key.type === 'Identifier' &&
prop.key.name === 'type' &&
prop.value.type === 'Identifier' &&
prop.value.name === 'Boolean'
)
}
function getBooleanProps (props) {
return props
.filter(prop => (
prop.value &&
prop.value.properties &&
prop.value.properties.find(isBooleanProp)
))
}
function getDefaultNode (propDef) {
return propDef.value.properties.find(p => {
return (
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'default'
)
})
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow boolean defaults',
category: undefined,
url: 'https://eslint.vuejs.org/rules/no-boolean-default.html'
},
fixable: 'code',
schema: [
{
enum: ['default-false', 'no-default']
}
]
},
create (context) {
return utils.executeOnVueComponent(context, (obj) => {
const props = utils.getComponentProps(obj)
const booleanProps = getBooleanProps(props)
if (!booleanProps.length) return
const booleanType = context.options[0] || 'no-default'
booleanProps.forEach((propDef) => {
const defaultNode = getDefaultNode(propDef)
switch (booleanType) {
case 'no-default':
if (defaultNode) {
context.report({
node: defaultNode,
message: 'Boolean prop should not set a default (Vue defaults it to false).'
})
}
break
case 'default-false':
if (
defaultNode &&
defaultNode.value.value !== false
) {
context.report({
node: defaultNode,
message: 'Boolean prop should only be defaulted to false.'
})
}
break
}
})
})
}
}
+66
View File
@@ -0,0 +1,66 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Check whether the given `v-if` node is using the variable which is defined by the `v-for` directive.
* @param {ASTNode} vIf The `v-if` attribute node to check.
* @returns {boolean} `true` if the `v-if` is using the variable which is defined by the `v-for` directive.
*/
function isUsingIterationVar (vIf) {
const element = vIf.parent.parent
return vIf.value.references.some(reference =>
element.variables.some(variable =>
variable.id.name === reference.id.name &&
variable.kind === 'v-for'
)
)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow confusing `v-for` and `v-if` on the same element',
category: 'recommended',
url: 'https://eslint.vuejs.org/rules/no-confusing-v-for-v-if.html',
replacedBy: ['no-use-v-if-with-v-for']
},
deprecated: true,
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='if']" (node) {
const element = node.parent.parent
if (utils.hasDirective(element, 'for') && !isUsingIterationVar(node)) {
context.report({
node,
loc: node.loc,
message: "This 'v-if' should be moved to the wrapper element."
})
}
}
})
}
}
@@ -0,0 +1,28 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
const scopeAttribute = require('./syntaxes/scope-attribute')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow deprecated `scope` attribute (in Vue.js 2.5.0+)',
category: undefined,
url: 'https://eslint.vuejs.org/rules/no-deprecated-scope-attribute.html'
},
fixable: 'code',
schema: [],
messages: {
forbiddenScopeAttribute: '`scope` attributes are deprecated.'
}
},
create (context) {
const templateBodyVisitor = scopeAttribute.createTemplateBodyVisitor(context)
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
}
}
@@ -0,0 +1,28 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
const slotAttribute = require('./syntaxes/slot-attribute')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow deprecated `slot` attribute (in Vue.js 2.6.0+)',
category: undefined,
url: 'https://eslint.vuejs.org/rules/no-deprecated-slot-attribute.html'
},
fixable: 'code',
schema: [],
messages: {
forbiddenSlotAttribute: '`slot` attributes are deprecated.'
}
},
create (context) {
const templateBodyVisitor = slotAttribute.createTemplateBodyVisitor(context)
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
}
}
@@ -0,0 +1,28 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
const slotScopeAttribute = require('./syntaxes/slot-scope-attribute')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+)',
category: undefined,
url: 'https://eslint.vuejs.org/rules/no-deprecated-slot-scope-attribute.html'
},
fixable: 'code',
schema: [],
messages: {
forbiddenSlotScopeAttribute: '`slot-scope` are deprecated.'
}
},
create (context) {
const templateBodyVisitor = slotScopeAttribute.createTemplateBodyVisitor(context, { fixToUpgrade: true })
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
}
}
+64
View File
@@ -0,0 +1,64 @@
/**
* @fileoverview Prevents duplication of field names.
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const GROUP_NAMES = ['props', 'computed', 'data', 'methods']
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow duplication of field names',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-dupe-keys.html'
},
fixable: null, // or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
groups: {
type: 'array'
}
},
additionalProperties: false
}
]
},
create (context) {
const options = context.options[0] || {}
const groups = new Set(GROUP_NAMES.concat(options.groups || []))
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const usedNames = []
const properties = utils.iterateProperties(obj, groups)
for (const o of properties) {
if (usedNames.indexOf(o.name) !== -1) {
context.report({
node: o.node,
message: "Duplicated key '{{name}}'.",
data: {
name: o.name
}
})
}
usedNames.push(o.name)
}
})
}
}
+105
View File
@@ -0,0 +1,105 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Get the name of the given attribute node.
* @param {ASTNode} attribute The attribute node to get.
* @returns {string} The name of the attribute.
*/
function getName (attribute) {
if (!attribute.directive) {
return attribute.key.name
}
if (attribute.key.name.name === 'bind') {
return (attribute.key.argument && attribute.key.argument.name) || null
}
return null
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow duplication of attributes',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-duplicate-attributes.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
allowCoexistClass: {
type: 'boolean'
},
allowCoexistStyle: {
type: 'boolean'
}
}
}
]
},
create (context) {
const options = context.options[0] || {}
const allowCoexistStyle = options.allowCoexistStyle !== false
const allowCoexistClass = options.allowCoexistClass !== false
const directiveNames = new Set()
const attributeNames = new Set()
function isDuplicate (name, isDirective) {
if ((allowCoexistStyle && name === 'style') || (allowCoexistClass && name === 'class')) {
return isDirective ? directiveNames.has(name) : attributeNames.has(name)
}
return directiveNames.has(name) || attributeNames.has(name)
}
return utils.defineTemplateBodyVisitor(context, {
'VStartTag' () {
directiveNames.clear()
attributeNames.clear()
},
'VAttribute' (node) {
const name = getName(node)
if (name == null) {
return
}
if (isDuplicate(name, node.directive)) {
context.report({
node,
loc: node.loc,
message: "Duplicate attribute '{{name}}'.",
data: { name }
})
}
if (node.directive) {
directiveNames.add(name)
} else {
attributeNames.add(name)
}
}
})
}
}
+9
View File
@@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line
module.exports = wrapCoreRule(require('eslint/lib/rules/no-empty-pattern'))
+234
View File
@@ -0,0 +1,234 @@
/**
* @author Yosuke Ota
* @fileoverview Rule to disalow whitespace that is not a tab or space, whitespace inside strings and comments are allowed
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const ALL_IRREGULARS = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000\u2028\u2029]/u
const IRREGULAR_WHITESPACE = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000]+/mgu
const IRREGULAR_LINE_TERMINATORS = /[\u2028\u2029]/mgu
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow irregular whitespace',
category: undefined,
url: 'https://eslint.vuejs.org/rules/no-irregular-whitespace.html'
},
schema: [
{
type: 'object',
properties: {
skipComments: {
type: 'boolean',
default: false
},
skipStrings: {
type: 'boolean',
default: true
},
skipTemplates: {
type: 'boolean',
default: false
},
skipRegExps: {
type: 'boolean',
default: false
},
skipHTMLAttributeValues: {
type: 'boolean',
default: false
},
skipHTMLTextContents: {
type: 'boolean',
default: false
}
},
additionalProperties: false
}
],
messages: {
disallow: 'Irregular whitespace not allowed.'
}
},
create (context) {
// Module store of error indexes that we have found
let errorIndexes = []
// Lookup the `skipComments` option, which defaults to `false`.
const options = context.options[0] || {}
const skipComments = !!options.skipComments
const skipStrings = options.skipStrings !== false
const skipRegExps = !!options.skipRegExps
const skipTemplates = !!options.skipTemplates
const skipHTMLAttributeValues = !!options.skipHTMLAttributeValues
const skipHTMLTextContents = !!options.skipHTMLTextContents
const sourceCode = context.getSourceCode()
/**
* Removes errors that occur inside a string node
* @param {ASTNode} node to check for matching errors.
* @returns {void}
* @private
*/
function removeWhitespaceError (node) {
const [startIndex, endIndex] = node.range
errorIndexes = errorIndexes
.filter(errorIndex => errorIndex < startIndex || endIndex <= errorIndex)
}
/**
* Checks literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
* @param {ASTNode} node to check for matching errors.
* @returns {void}
* @private
*/
function removeInvalidNodeErrorsInLiteral (node) {
const shouldCheckStrings = skipStrings && (typeof node.value === 'string')
const shouldCheckRegExps = skipRegExps && Boolean(node.regex)
if (shouldCheckStrings || shouldCheckRegExps) {
// If we have irregular characters remove them from the errors list
if (ALL_IRREGULARS.test(node.raw)) {
removeWhitespaceError(node)
}
}
}
/**
* Checks template string literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
* @param {ASTNode} node to check for matching errors.
* @returns {void}
* @private
*/
function removeInvalidNodeErrorsInTemplateLiteral (node) {
if (ALL_IRREGULARS.test(node.value.raw)) {
removeWhitespaceError(node)
}
}
/**
* Checks HTML attribute value nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
* @param {ASTNode} node to check for matching errors.
* @returns {void}
* @private
*/
function removeInvalidNodeErrorsInHTMLAttributeValue (node) {
if (ALL_IRREGULARS.test(sourceCode.getText(node))) {
removeWhitespaceError(node)
}
}
/**
* Checks HTML text content nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
* @param {ASTNode} node to check for matching errors.
* @returns {void}
* @private
*/
function removeInvalidNodeErrorsInHTMLTextContent (node) {
if (ALL_IRREGULARS.test(sourceCode.getText(node))) {
removeWhitespaceError(node)
}
}
/**
* Checks comment nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
* @param {ASTNode} node to check for matching errors.
* @returns {void}
* @private
*/
function removeInvalidNodeErrorsInComment (node) {
if (ALL_IRREGULARS.test(node.value)) {
removeWhitespaceError(node)
}
}
/**
* Checks the program source for irregular whitespaces and irregular line terminators
* @returns {void}
* @private
*/
function checkForIrregularWhitespace () {
const source = sourceCode.getText()
let match
while ((match = IRREGULAR_WHITESPACE.exec(source)) !== null) {
errorIndexes.push(match.index)
}
while ((match = IRREGULAR_LINE_TERMINATORS.exec(source)) !== null) {
errorIndexes.push(match.index)
}
}
checkForIrregularWhitespace()
if (!errorIndexes.length) {
return {}
}
const bodyVisitor = utils.defineTemplateBodyVisitor(context,
{
...(skipHTMLAttributeValues ? { 'VAttribute[directive=false] > VLiteral': removeInvalidNodeErrorsInHTMLAttributeValue } : {}),
...(skipHTMLTextContents ? { VText: removeInvalidNodeErrorsInHTMLTextContent } : {}),
// inline scripts
Literal: removeInvalidNodeErrorsInLiteral,
...(skipTemplates ? { TemplateElement: removeInvalidNodeErrorsInTemplateLiteral } : {})
}
)
return {
...bodyVisitor,
Literal: removeInvalidNodeErrorsInLiteral,
...(skipTemplates ? { TemplateElement: removeInvalidNodeErrorsInTemplateLiteral } : {}),
'Program:exit' (node) {
if (bodyVisitor['Program:exit']) {
bodyVisitor['Program:exit'](node)
}
const templateBody = node.templateBody
if (skipComments) {
// First strip errors occurring in comment nodes.
sourceCode.getAllComments().forEach(removeInvalidNodeErrorsInComment)
if (templateBody) {
templateBody.comments.forEach(removeInvalidNodeErrorsInComment)
}
}
// Removes errors that occur outside script and template
const [scriptStart, scriptEnd] = node.range
const [templateStart, templateEnd] = templateBody ? templateBody.range : [0, 0]
errorIndexes = errorIndexes
.filter(errorIndex =>
(scriptStart <= errorIndex && errorIndex < scriptEnd) ||
(templateStart <= errorIndex && errorIndex < templateEnd)
)
// If we have any errors remaining report on them
errorIndexes.forEach(errorIndex => {
context.report({
loc: sourceCode.getLocFromIndex(errorIndex),
messageId: 'disallow'
})
})
}
}
}
}
+85
View File
@@ -0,0 +1,85 @@
/**
* @fileoverview This rule warns about the usage of extra whitespaces between attributes
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const isProperty = (context, node) => {
const sourceCode = context.getSourceCode()
return node.type === 'Punctuator' && sourceCode.getText(node) === ':'
}
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'disallow multiple spaces',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/no-multi-spaces.html'
},
fixable: 'whitespace', // or "code" or "whitespace"
schema: [{
type: 'object',
properties: {
ignoreProperties: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
/**
* @param {RuleContext} context - The rule context.
* @returns {Object} AST event handlers.
*/
create (context) {
const options = context.options[0] || {}
const ignoreProperties = options.ignoreProperties === true
return {
Program (node) {
if (context.parserServices.getTemplateBodyTokenStore == null) {
context.report({
loc: { line: 1, column: 0 },
message: 'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.'
})
return
}
if (!node.templateBody) {
return
}
const sourceCode = context.getSourceCode()
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const tokens = tokenStore.getTokens(node.templateBody, { includeComments: true })
let prevToken = tokens.shift()
for (const token of tokens) {
const spaces = token.range[0] - prevToken.range[1]
const shouldIgnore = ignoreProperties && (
isProperty(context, token) || isProperty(context, prevToken)
)
if (spaces > 1 && token.loc.start.line === prevToken.loc.start.line && !shouldIgnore) {
context.report({
node: token,
loc: {
start: prevToken.loc.end,
end: token.loc.start
},
message: "Multiple spaces found before '{{displayValue}}'.",
fix: (fixer) => fixer.replaceTextRange([prevToken.range[1], token.range[0]], ' '),
data: {
displayValue: sourceCode.getText(token)
}
})
}
prevToken = token
}
}
}
}
}
+106
View File
@@ -0,0 +1,106 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
// https://html.spec.whatwg.org/multipage/parsing.html#parse-errors
const DEFAULT_OPTIONS = Object.freeze(Object.assign(Object.create(null), {
'abrupt-closing-of-empty-comment': true,
'absence-of-digits-in-numeric-character-reference': true,
'cdata-in-html-content': true,
'character-reference-outside-unicode-range': true,
'control-character-in-input-stream': true,
'control-character-reference': true,
'eof-before-tag-name': true,
'eof-in-cdata': true,
'eof-in-comment': true,
'eof-in-tag': true,
'incorrectly-closed-comment': true,
'incorrectly-opened-comment': true,
'invalid-first-character-of-tag-name': true,
'missing-attribute-value': true,
'missing-end-tag-name': true,
'missing-semicolon-after-character-reference': true,
'missing-whitespace-between-attributes': true,
'nested-comment': true,
'noncharacter-character-reference': true,
'noncharacter-in-input-stream': true,
'null-character-reference': true,
'surrogate-character-reference': true,
'surrogate-in-input-stream': true,
'unexpected-character-in-attribute-name': true,
'unexpected-character-in-unquoted-attribute-value': true,
'unexpected-equals-sign-before-attribute-name': true,
'unexpected-null-character': true,
'unexpected-question-mark-instead-of-tag-name': true,
'unexpected-solidus-in-tag': true,
'unknown-named-character-reference': true,
'end-tag-with-attributes': true,
'duplicate-attribute': true,
'end-tag-with-trailing-solidus': true,
'non-void-html-element-start-tag-with-trailing-solidus': false,
'x-invalid-end-tag': true,
'x-invalid-namespace': true
}))
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow parsing errors in `<template>`',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-parsing-error.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: Object.keys(DEFAULT_OPTIONS).reduce((ret, code) => {
ret[code] = { type: 'boolean' }
return ret
}, {}),
additionalProperties: false
}
]
},
create (context) {
const options = Object.assign({}, DEFAULT_OPTIONS, context.options[0] || {})
return {
Program (program) {
const node = program.templateBody
if (node == null || node.errors == null) {
return
}
for (const error of node.errors) {
if (error.code && !options[error.code]) {
continue
}
context.report({
node,
loc: { line: error.lineNumber, column: error.column },
message: 'Parsing error: {{message}}.',
data: {
message: error.message.endsWith('.')
? error.message.slice(0, -1)
: error.message
}
})
}
}
}
}
}
+116
View File
@@ -0,0 +1,116 @@
/**
* @fileoverview disallow the use of reserved names in component definitions
* @author Jake Hassel <https://github.com/shadskii>
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
const htmlElements = require('../utils/html-elements.json')
const deprecatedHtmlElements = require('../utils/deprecated-html-elements.json')
const svgElements = require('../utils/svg-elements.json')
const kebabCaseElements = [
'annotation-xml',
'color-profile',
'font-face',
'font-face-src',
'font-face-uri',
'font-face-format',
'font-face-name',
'missing-glyph'
]
const isLowercase = (word) => /^[a-z]*$/.test(word)
const capitalizeFirstLetter = (word) => word[0].toUpperCase() + word.substring(1, word.length)
const RESERVED_NAMES = new Set(
[
...kebabCaseElements,
...kebabCaseElements.map(casing.pascalCase),
...htmlElements,
...htmlElements.map(capitalizeFirstLetter),
...deprecatedHtmlElements,
...deprecatedHtmlElements.map(capitalizeFirstLetter),
...svgElements,
...svgElements.filter(isLowercase).map(capitalizeFirstLetter)
])
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow the use of reserved names in component definitions',
category: undefined, // 'essential'
url: 'https://eslint.vuejs.org/rules/no-reserved-component-names.html'
},
fixable: null,
schema: []
},
create (context) {
function canVerify (node) {
return node.type === 'Literal' || (
node.type === 'TemplateLiteral' &&
node.expressions.length === 0 &&
node.quasis.length === 1
)
}
function reportIfInvalid (node) {
let name
if (node.type === 'TemplateLiteral') {
const quasis = node.quasis[0]
name = quasis.value.cooked
} else {
name = node.value
}
if (RESERVED_NAMES.has(name)) {
report(node, name)
}
}
function report (node, name) {
context.report({
node: node,
message: 'Name "{{name}}" is reserved.',
data: {
name: name
}
})
}
return Object.assign({},
utils.executeOnCallVueComponent(context, (node) => {
if (node.arguments.length === 2) {
const argument = node.arguments[0]
if (canVerify(argument)) {
reportIfInvalid(argument)
}
}
}),
utils.executeOnVue(context, (obj) => {
// Report if a component has been registered locally with a reserved name.
utils.getRegisteredComponents(obj)
.filter(({ name }) => RESERVED_NAMES.has(name))
.forEach(({ node, name }) => report(node, name))
const node = obj.properties
.find(item => (
item.type === 'Property' &&
item.key.name === 'name' &&
canVerify(item.value)
))
if (!node) return
reportIfInvalid(node.value)
})
)
}
}
+73
View File
@@ -0,0 +1,73 @@
/**
* @fileoverview Prevent overwrite reserved keys
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const RESERVED_KEYS = require('../utils/vue-reserved.json')
const GROUP_NAMES = ['props', 'computed', 'data', 'methods']
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow overwriting reserved keys',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-reserved-keys.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
reserved: {
type: 'array'
},
groups: {
type: 'array'
}
},
additionalProperties: false
}
]
},
create (context) {
const options = context.options[0] || {}
const reservedKeys = new Set(RESERVED_KEYS.concat(options.reserved || []))
const groups = new Set(GROUP_NAMES.concat(options.groups || []))
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const properties = utils.iterateProperties(obj, groups)
for (const o of properties) {
if (o.groupName === 'data' && o.name[0] === '_') {
context.report({
node: o.node,
message: "Keys starting with with '_' are reserved in '{{name}}' group.",
data: {
name: o.name
}
})
} else if (reservedKeys.has(o.name)) {
context.report({
node: o.node,
message: "Key '{{name}}' is reserved.",
data: {
name: o.name
}
})
}
}
})
}
}
+9
View File
@@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line
module.exports = wrapCoreRule(require('eslint/lib/rules/no-restricted-syntax'))
+79
View File
@@ -0,0 +1,79 @@
/**
* @fileoverview Enforces component's data property to be a function.
* @author Armano
*/
'use strict'
const utils = require('../utils')
function isOpenParen (token) {
return token.type === 'Punctuator' && token.value === '('
}
function isCloseParen (token) {
return token.type === 'Punctuator' && token.value === ')'
}
function getFirstAndLastTokens (node, sourceCode) {
let first = sourceCode.getFirstToken(node)
let last = sourceCode.getLastToken(node)
// If the value enclosed by parentheses, update the 'first' and 'last' by the parentheses.
while (true) {
const prev = sourceCode.getTokenBefore(first)
const next = sourceCode.getTokenAfter(last)
if (isOpenParen(prev) && isCloseParen(next)) {
first = prev
last = next
} else {
return { first, last }
}
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: "enforce component's data property to be a function",
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-shared-component-data.html'
},
fixable: 'code',
schema: []
},
create (context) {
const sourceCode = context.getSourceCode()
return utils.executeOnVueComponent(context, (obj) => {
obj.properties
.filter(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'data' &&
p.value.type !== 'FunctionExpression' &&
p.value.type !== 'ArrowFunctionExpression' &&
p.value.type !== 'Identifier'
)
.forEach(p => {
context.report({
node: p,
message: '`data` property in component must be a function.',
fix (fixer) {
const tokens = getFirstAndLastTokens(p.value, sourceCode)
return [
fixer.insertTextBefore(tokens.first, 'function() {\nreturn '),
fixer.insertTextAfter(tokens.last, ';\n}')
]
}
})
})
})
}
}
@@ -0,0 +1,96 @@
/**
* @fileoverview Don't introduce side effects in computed properties
* @author Michał Sajnóg
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow side effects in computed properties',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-side-effects-in-computed-properties.html'
},
fixable: null,
schema: []
},
create (context) {
const forbiddenNodes = []
let scopeStack = { upper: null, body: null }
function onFunctionEnter (node) {
scopeStack = { upper: scopeStack, body: node.body }
}
function onFunctionExit () {
scopeStack = scopeStack.upper
}
return Object.assign({},
{
':function': onFunctionEnter,
':function:exit': onFunctionExit,
// this.xxx <=|+=|-=>
'AssignmentExpression' (node) {
if (node.left.type !== 'MemberExpression') return
if (utils.parseMemberExpression(node.left)[0] === 'this') {
forbiddenNodes.push({
node,
targetBody: scopeStack.body
})
}
},
// this.xxx <++|-->
'UpdateExpression > MemberExpression' (node) {
if (utils.parseMemberExpression(node)[0] === 'this') {
forbiddenNodes.push({
node,
targetBody: scopeStack.body
})
}
},
// this.xxx.func()
'CallExpression' (node) {
const code = utils.parseMemberOrCallExpression(node)
const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g
if (MUTATION_REGEX.test(code)) {
forbiddenNodes.push({
node,
targetBody: scopeStack.body
})
}
}
},
utils.executeOnVue(context, (obj) => {
const computedProperties = utils.getComputedProperties(obj)
computedProperties.forEach(cp => {
forbiddenNodes.forEach(({ node, targetBody }) => {
if (
cp.value &&
node.loc.start.line >= cp.value.loc.start.line &&
node.loc.end.line <= cp.value.loc.end.line &&
targetBody === cp.value
) {
context.report({
node: node,
message: 'Unexpected side effect in "{{key}}" computed property.',
data: { key: cp.key }
})
}
})
})
})
)
}
}
@@ -0,0 +1,55 @@
/**
* @author Yosuke Ota
* issue https://github.com/vuejs/eslint-plugin-vue/issues/460
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'disallow spaces around equal signs in attribute',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/no-spaces-around-equal-signs-in-attribute.html'
},
fixable: 'whitespace',
schema: []
},
create (context) {
const sourceCode = context.getSourceCode()
return utils.defineTemplateBodyVisitor(context, {
'VAttribute' (node) {
if (!node.value) {
return
}
const range = [node.key.range[1], node.value.range[0]]
const eqText = sourceCode.text.slice(range[0], range[1])
const expect = eqText.trim()
if (eqText !== expect) {
context.report({
node: node.key,
loc: {
start: node.key.loc.end,
end: node.value.loc.start
},
message: 'Unexpected spaces found around equal signs.',
data: {},
fix: fixer => fixer.replaceTextRange(range, expect)
})
}
}
})
}
}
+138
View File
@@ -0,0 +1,138 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow static inline `style` attributes',
category: undefined,
url: 'https://eslint.vuejs.org/rules/no-static-inline-styles.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
allowBinding: {
type: 'boolean'
}
},
additionalProperties: false
}
],
messages: {
forbiddenStaticInlineStyle: 'Static inline `style` are forbidden.',
forbiddenStyleAttr: '`style` attributes are forbidden.'
}
},
create (context) {
/**
* Checks whether if the given property node is a static value.
* @param {AssignmentProperty} prop property node to check
* @returns {boolean} `true` if the given property node is a static value.
*/
function isStaticValue (prop) {
return (
!prop.computed &&
prop.value.type === 'Literal' &&
(prop.key.type === 'Identifier' || prop.key.type === 'Literal')
)
}
/**
* Gets the static properties of a given expression node.
* - If `SpreadElement` or computed property exists, it gets only the static properties before it.
* `:style="{ color: 'red', display: 'flex', ...spread, width: '16px' }"`
* ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
* - If non-static object exists, it gets only the static properties up to that object.
* `:style="[ { color: 'red' }, { display: 'flex', color, width: '16px' }, { height: '16px' } ]"`
* ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
* - If all properties are static properties, it returns one root node.
* `:style="[ { color: 'red' }, { display: 'flex', width: '16px' } ]"`
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* @param {VAttribute} node `:style` node to check
* @returns {AssignmentProperty[] | [VAttribute]} the static properties.
*/
function getReportNodes (node) {
const { value } = node
if (!value) {
return []
}
const { expression } = value
if (!expression) {
return []
}
let elements
if (expression.type === 'ObjectExpression') {
elements = [expression]
} else if (expression.type === 'ArrayExpression') {
elements = expression.elements
} else {
return []
}
const staticProperties = []
for (const element of elements) {
if (!element) {
continue
}
if (element.type !== 'ObjectExpression') {
return staticProperties
}
let isAllStatic = true
for (const prop of element.properties) {
if (prop.type === 'SpreadElement' || prop.computed) {
// If `SpreadElement` or computed property exists, it gets only the static properties before it.
return staticProperties
}
if (isStaticValue(prop)) {
staticProperties.push(prop)
} else {
isAllStatic = false
}
}
if (!isAllStatic) {
// If non-static object exists, it gets only the static properties up to that object.
return staticProperties
}
}
// If all properties are static properties, it returns one root node.
return [node]
}
/**
* Reports if the value is static.
* @param {VAttribute} node `:style` node to check
*/
function verifyVBindStyle (node) {
for (const n of getReportNodes(node)) {
context.report({
node: n,
messageId: 'forbiddenStaticInlineStyle'
})
}
}
const visitor = {
"VAttribute[directive=false][key.name='style']" (node) {
context.report({
node,
messageId: 'forbiddenStyleAttr'
})
}
}
if (!context.options[0] || !context.options[0].allowBinding) {
visitor[
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='style']"
] = verifyVBindStyle
}
return utils.defineTemplateBodyVisitor(context, visitor)
}
}
+43
View File
@@ -0,0 +1,43 @@
/**
* @author Toru Nagashima
* @copyright 2016 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow `key` attribute on `<template>`',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-template-key.html'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VElement[name='template']" (node) {
if (utils.hasAttribute(node, 'key') || utils.hasDirective(node, 'bind', 'key')) {
context.report({
node: node,
loc: node.loc,
message: "'<template>' cannot be keyed. Place the key on real elements instead."
})
}
}
})
}
}
+77
View File
@@ -0,0 +1,77 @@
/**
* @fileoverview Disallow variable declarations from shadowing variables declared in the outer scope.
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const GROUP_NAMES = ['props', 'computed', 'data', 'methods']
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow variable declarations from shadowing variables declared in the outer scope',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/no-template-shadow.html'
},
fixable: null,
schema: []
},
create (context) {
const jsVars = new Set()
let scope = {
parent: null,
nodes: []
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.defineTemplateBodyVisitor(context, {
VElement (node) {
scope = {
parent: scope,
nodes: scope.nodes.slice() // make copy
}
if (node.variables) {
for (const variable of node.variables) {
const varNode = variable.id
const name = varNode.name
if (scope.nodes.some(node => node.name === name) || jsVars.has(name)) {
context.report({
node: varNode,
loc: varNode.loc,
message: "Variable '{{name}}' is already declared in the upper scope.",
data: {
name
}
})
} else {
scope.nodes.push(varNode)
}
}
}
},
'VElement:exit' (node) {
scope = scope.parent
}
}, utils.executeOnVue(context, (obj) => {
const properties = Array.from(utils.iterateProperties(obj, new Set(GROUP_NAMES)))
for (const node of properties) {
jsVars.add(node.name)
}
}))
}
}
+45
View File
@@ -0,0 +1,45 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow mustaches in `<textarea>`',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-textarea-mustache.html'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VElement[name='textarea'] VExpressionContainer" (node) {
if (node.parent.type !== 'VElement') {
return
}
context.report({
node,
loc: node.loc,
message: "Unexpected mustache. Use 'v-model' instead."
})
}
})
}
}
+143
View File
@@ -0,0 +1,143 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const { Range } = require('semver')
const utils = require('../utils')
const FEATURES = {
// Vue.js 2.5.0+
'slot-scope-attribute': require('./syntaxes/slot-scope-attribute'),
// Vue.js 2.6.0+
'dynamic-directive-arguments': require('./syntaxes/dynamic-directive-arguments'),
'v-slot': require('./syntaxes/v-slot'),
// >=2.6.0-beta.1 <=2.6.0-beta.3
'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand')
}
const cache = new Map()
/**
* Get the `semver.Range` object of a given range text.
* @param {string} x The text expression for a semver range.
* @returns {Range|null} The range object of a given range text.
* It's null if the `x` is not a valid range text.
*/
function getSemverRange (x) {
const s = String(x)
let ret = cache.get(s) || null
if (!ret) {
try {
ret = new Range(s)
} catch (_error) {
// Ignore parsing error.
}
cache.set(s, ret)
}
return ret
}
/**
* Merge two visitors.
* @param {Visitor} x The visitor which is assigned.
* @param {Visitor} y The visitor which is assigning.
* @returns {Visitor} `x`.
*/
function merge (x, y) {
for (const key of Object.keys(y)) {
if (typeof x[key] === 'function') {
if (x[key]._handlers == null) {
const fs = [x[key], y[key]]
x[key] = node => fs.forEach(h => h(node))
x[key]._handlers = fs
} else {
x[key]._handlers.push(y[key])
}
} else {
x[key] = y[key]
}
}
return x
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow unsupported Vue.js syntax on the specified version',
category: undefined,
url: 'https://eslint.vuejs.org/rules/no-unsupported-features.html'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
version: {
type: 'string'
},
ignores: {
type: 'array',
items: {
enum: Object.keys(FEATURES)
},
uniqueItems: true
}
},
additionalProperties: false
}
],
messages: {
// Vue.js 2.5.0+
forbiddenSlotScopeAttribute: '`slot-scope` are not supported until Vue.js "2.5.0".',
// Vue.js 2.6.0+
forbiddenDynamicDirectiveArguments: 'Dynamic arguments are not supported until Vue.js "2.6.0".',
forbiddenVSlot: '`v-slot` are not supported until Vue.js "2.6.0".',
// >=2.6.0-beta.1 <=2.6.0-beta.3
forbiddenVBindPropModifierShorthand: '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".'
}
},
create (context) {
const { version, ignores } = Object.assign(
{
version: null,
ignores: []
},
context.options[0] || {}
)
if (!version) {
// version is not set.
return {}
}
const versionRange = getSemverRange(version)
/**
* Check whether a given case object is full-supported on the configured node version.
* @param {{supported:string}} aCase The case object to check.
* @returns {boolean} `true` if it's supporting.
*/
function isNotSupportingVersion (aCase) {
if (typeof aCase.supported === 'function') {
return !aCase.supported(versionRange)
}
return versionRange.intersects(getSemverRange(`<${aCase.supported}`))
}
const templateBodyVisitor = Object.keys(FEATURES)
.filter(syntaxName => !ignores.includes(syntaxName))
.filter(syntaxName => isNotSupportingVersion(FEATURES[syntaxName]))
.reduce((result, syntaxName) => {
const visitor = FEATURES[syntaxName].createTemplateBodyVisitor(context)
if (visitor) {
merge(result, visitor)
}
return result
}, {})
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
}
}
+112
View File
@@ -0,0 +1,112 @@
/**
* @fileoverview Report used components
* @author Michał Sajnóg
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const casing = require('../utils/casing')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow registering components that are not used inside templates',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-unused-components.html'
},
fixable: null,
schema: [{
type: 'object',
properties: {
ignoreWhenBindingPresent: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create (context) {
const options = context.options[0] || {}
const ignoreWhenBindingPresent = options.ignoreWhenBindingPresent !== undefined ? options.ignoreWhenBindingPresent : true
const usedComponents = new Set()
let registeredComponents = []
let ignoreReporting = false
let templateLocation
return utils.defineTemplateBodyVisitor(context, {
VElement (node) {
if (
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
utils.isHtmlWellKnownElementName(node.rawName) ||
utils.isSvgWellKnownElementName(node.rawName)
) {
return
}
usedComponents.add(node.rawName)
},
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']" (node) {
if (
!node.value || // `<component :is>`
node.value.type !== 'VExpressionContainer' ||
!node.value.expression // `<component :is="">`
) return
if (node.value.expression.type === 'Literal') {
usedComponents.add(node.value.expression.value)
} else if (ignoreWhenBindingPresent) {
ignoreReporting = true
}
},
"VAttribute[directive=false][key.name='is']" (node) {
usedComponents.add(node.value.value)
},
"VElement[name='template']" (rootNode) {
templateLocation = templateLocation || rootNode.loc.start
},
"VElement[name='template']:exit" (rootNode) {
if (
rootNode.loc.start !== templateLocation ||
ignoreReporting ||
utils.hasAttribute(rootNode, 'src')
) return
registeredComponents
.filter(({ name }) => {
// If the component name is PascalCase or camelCase
// it can be used in various of ways inside template,
// like "theComponent", "The-component" etc.
// but except snake_case
if (casing.pascalCase(name) === name || casing.camelCase(name) === name) {
return ![...usedComponents].some(n => {
return n.indexOf('_') === -1 && (name === casing.pascalCase(n) || casing.camelCase(n) === name)
})
} else {
// In any other case the used component name must exactly match
// the registered name
return !usedComponents.has(name)
}
})
.forEach(({ node, name }) => context.report({
node,
message: 'The "{{name}}" component has been registered but not used.',
data: {
name
}
}))
}
}, utils.executeOnVue(context, (obj) => {
registeredComponents = utils.getRegisteredComponents(obj)
}))
}
}
+46
View File
@@ -0,0 +1,46 @@
/**
* @fileoverview disallow unused variable definitions of v-for directives or scope attributes.
* @author 薛定谔的猫<hh_2013@foxmail.com>
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow unused variable definitions of v-for directives or scope attributes',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-unused-vars.html'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
VElement (node) {
const variables = node.variables
for (
let i = variables.length - 1;
i >= 0 && !variables[i].references.length;
i--
) {
const variable = variables[i]
context.report({
node: variable.id,
loc: variable.id.loc,
message: `'{{name}}' is defined but never used.`,
data: variable.id
})
}
}
})
}
}
+107
View File
@@ -0,0 +1,107 @@
/**
* @author Yosuke Ota
*
* issue https://github.com/vuejs/eslint-plugin-vue/issues/403
* Style guide: https://vuejs.org/v2/style-guide/#Avoid-v-if-with-v-for-essential
*
* I implemented it with reference to `no-confusing-v-for-v-if`
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Check whether the given `v-if` node is using the variable which is defined by the `v-for` directive.
* @param {ASTNode} vIf The `v-if` attribute node to check.
* @returns {boolean} `true` if the `v-if` is using the variable which is defined by the `v-for` directive.
*/
function isUsingIterationVar (vIf) {
return !!getVForUsingIterationVar(vIf)
}
function getVForUsingIterationVar (vIf) {
const element = vIf.parent.parent
for (var i = 0; i < vIf.value.references.length; i++) {
const reference = vIf.value.references[i]
const targetVFor = element.variables.find(variable =>
variable.id.name === reference.id.name &&
variable.kind === 'v-for'
)
if (targetVFor) {
return targetVFor
}
}
return undefined
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow use v-if on the same element as v-for',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/no-use-v-if-with-v-for.html'
},
fixable: null,
schema: [{
type: 'object',
properties: {
allowUsingIterationVar: {
type: 'boolean'
}
}
}]
},
create (context) {
const options = context.options[0] || {}
const allowUsingIterationVar = options.allowUsingIterationVar === true // default false
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='if']" (node) {
const element = node.parent.parent
if (utils.hasDirective(element, 'for')) {
if (isUsingIterationVar(node)) {
if (!allowUsingIterationVar) {
const vForVar = getVForUsingIterationVar(node)
let targetVForExpr = vForVar.id.parent
while (targetVForExpr.type !== 'VForExpression') {
targetVForExpr = targetVForExpr.parent
}
const iteratorNode = targetVForExpr.right
context.report({
node,
loc: node.loc,
message: "The '{{iteratorName}}' {{kind}} inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'.",
data: {
iteratorName: iteratorNode.type === 'Identifier' ? iteratorNode.name : context.getSourceCode().getText(iteratorNode),
kind: iteratorNode.type === 'Identifier' ? 'variable' : 'expression'
}
})
}
} else {
context.report({
node,
loc: node.loc,
message: "This 'v-if' should be moved to the wrapper element."
})
}
}
}
})
}
}
+34
View File
@@ -0,0 +1,34 @@
/**
* @fileoverview Restrict or warn use of v-html to prevent XSS attack
* @author Nathan Zeplowitz
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow use of v-html to prevent XSS attack',
category: 'recommended',
url: 'https://eslint.vuejs.org/rules/no-v-html.html'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='html']" (node) {
context.report({
node,
loc: node.loc,
message: "'v-html' directive can lead to XSS attack."
})
}
})
}
}
+12
View File
@@ -0,0 +1,12 @@
/**
* @author Toru Nagashima
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta
module.exports = wrapCoreRule(
require('eslint/lib/rules/object-curly-spacing'),
{ skipDynamicArguments: true }
)
+234
View File
@@ -0,0 +1,234 @@
/**
* @fileoverview Keep order of properties in components
* @author Michał Sajnóg
*/
'use strict'
const utils = require('../utils')
const traverseNodes = require('vue-eslint-parser').AST.traverseNodes
const defaultOrder = [
'el',
'name',
'parent',
'functional',
['delimiters', 'comments'],
['components', 'directives', 'filters'],
'extends',
'mixins',
'inheritAttrs',
'model',
['props', 'propsData'],
'fetch',
'asyncData',
'data',
'computed',
'watch',
'LIFECYCLE_HOOKS',
'methods',
'head',
['template', 'render'],
'renderError'
]
const groups = {
LIFECYCLE_HOOKS: [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'activated',
'deactivated',
'beforeDestroy',
'destroyed'
]
}
function getOrderMap (order) {
const orderMap = new Map()
order.forEach((property, i) => {
if (Array.isArray(property)) {
property.forEach(p => orderMap.set(p, i))
} else {
orderMap.set(property, i)
}
})
return orderMap
}
function isComma (node) {
return node.type === 'Punctuator' && node.value === ','
}
const ARITHMETIC_OPERATORS = ['+', '-', '*', '/', '%', '**']
const BITWISE_OPERATORS = ['&', '|', '^', '~', '<<', '>>', '>>>']
const COMPARISON_OPERATORS = ['==', '!=', '===', '!==', '>', '>=', '<', '<=']
const RELATIONAL_OPERATORS = ['in', 'instanceof']
const ALL_BINARY_OPERATORS = [].concat(
ARITHMETIC_OPERATORS,
BITWISE_OPERATORS,
COMPARISON_OPERATORS,
RELATIONAL_OPERATORS
)
const LOGICAL_OPERATORS = ['&&', '||']
/*
* Result `true` if the node is sure that there are no side effects
*
* Currently known side effects types
*
* node.type === 'CallExpression'
* node.type === 'NewExpression'
* node.type === 'UpdateExpression'
* node.type === 'AssignmentExpression'
* node.type === 'TaggedTemplateExpression'
* node.type === 'UnaryExpression' && node.operator === 'delete'
*
* @param {ASTNode} node target node
* @param {Object} visitorKeys sourceCode.visitorKey
* @returns {Boolean} no side effects
*/
function isNotSideEffectsNode (node, visitorKeys) {
let result = true
const noSideEffectsNodes = new Set()
traverseNodes(node, {
visitorKeys,
enterNode (node, parent) {
if (!result) {
return
}
if (
// parent has no side effects
noSideEffectsNodes.has(parent) ||
// no side effects node
node.type === 'FunctionExpression' ||
node.type === 'Identifier' ||
node.type === 'Literal' ||
// es2015
node.type === 'ArrowFunctionExpression' ||
node.type === 'TemplateElement'
) {
noSideEffectsNodes.add(node)
} else if (
node.type !== 'Property' &&
node.type !== 'ObjectExpression' &&
node.type !== 'ArrayExpression' &&
(node.type !== 'UnaryExpression' || !['!', '~', '+', '-', 'typeof'].includes(node.operator)) &&
(node.type !== 'BinaryExpression' || !ALL_BINARY_OPERATORS.includes(node.operator)) &&
(node.type !== 'LogicalExpression' || !LOGICAL_OPERATORS.includes(node.operator)) &&
node.type !== 'MemberExpression' &&
node.type !== 'ConditionalExpression' &&
// es2015
node.type !== 'SpreadElement' &&
node.type !== 'TemplateLiteral'
) {
// Can not be sure that a node has no side effects
result = false
}
},
leaveNode () {}
})
return result
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce order of properties in components',
category: 'recommended',
url: 'https://eslint.vuejs.org/rules/order-in-components.html'
},
fixable: 'code', // null or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
order: {
type: 'array'
}
},
additionalProperties: false
}
]
},
create (context) {
const options = context.options[0] || {}
const order = options.order || defaultOrder
const extendedOrder = order.map(property => groups[property] || property)
const orderMap = getOrderMap(extendedOrder)
const sourceCode = context.getSourceCode()
function checkOrder (propertiesNodes, orderMap) {
const properties = propertiesNodes
.filter(property => property.type === 'Property')
.map(property => property.key)
properties.forEach((property, i) => {
const propertiesAbove = properties.slice(0, i)
const unorderedProperties = propertiesAbove
.filter(p => orderMap.get(p.name) > orderMap.get(property.name))
.sort((p1, p2) => orderMap.get(p1.name) > orderMap.get(p2.name) ? 1 : -1)
const firstUnorderedProperty = unorderedProperties[0]
if (firstUnorderedProperty) {
const line = firstUnorderedProperty.loc.start.line
context.report({
node: property,
message: `The "{{name}}" property should be above the "{{firstUnorderedPropertyName}}" property on line {{line}}.`,
data: {
name: property.name,
firstUnorderedPropertyName: firstUnorderedProperty.name,
line
},
fix (fixer) {
const propertyNode = property.parent
const firstUnorderedPropertyNode = firstUnorderedProperty.parent
const hasSideEffectsPossibility = propertiesNodes
.slice(
propertiesNodes.indexOf(firstUnorderedPropertyNode),
propertiesNodes.indexOf(propertyNode) + 1
)
.some((property) => !isNotSideEffectsNode(property, sourceCode.visitorKeys))
if (hasSideEffectsPossibility) {
return undefined
}
const afterComma = sourceCode.getTokenAfter(propertyNode)
const hasAfterComma = isComma(afterComma)
const beforeComma = sourceCode.getTokenBefore(propertyNode)
const codeStart = beforeComma.range[1] // to include comments
const codeEnd = hasAfterComma ? afterComma.range[1] : propertyNode.range[1]
const propertyCode = sourceCode.text.slice(codeStart, codeEnd) + (hasAfterComma ? '' : ',')
const insertTarget = sourceCode.getTokenBefore(firstUnorderedPropertyNode)
const removeStart = hasAfterComma ? codeStart : beforeComma.range[0]
return [
fixer.removeRange([removeStart, codeEnd]),
fixer.insertTextAfter(insertTarget, propertyCode)
]
}
})
}
})
}
return utils.executeOnVue(context, (obj) => {
checkOrder(obj.properties, orderMap)
})
}
}
+194
View File
@@ -0,0 +1,194 @@
/**
* @fileoverview Require or disallow padding lines between blocks
* @author Yosuke Ota
*/
'use strict'
const utils = require('../utils')
/**
* Split the source code into multiple lines based on the line delimiters.
* @param {string} text Source code as a string.
* @returns {string[]} Array of source code lines.
*/
function splitLines (text) {
return text.split(/\r\n|[\r\n\u2028\u2029]/gu)
}
/**
* Check and report blocks for `never` configuration.
* This autofix removes blank lines between the given 2 blocks.
* @param {RuleContext} context The rule context to report.
* @param {VElement} prevBlock The previous block to check.
* @param {VElement} nextBlock The next block to check.
* @param {Token[]} betweenTokens The array of tokens between blocks.
* @returns {void}
* @private
*/
function verifyForNever (context, prevBlock, nextBlock, betweenTokens) {
if (prevBlock.loc.end.line === nextBlock.loc.start.line) {
// same line
return
}
const tokenOrNodes = [...betweenTokens, nextBlock]
let prev = prevBlock
const paddingLines = []
for (const tokenOrNode of tokenOrNodes) {
const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line
if (numOfLineBreaks > 1) {
paddingLines.push([prev, tokenOrNode])
}
prev = tokenOrNode
}
if (!paddingLines.length) {
return
}
context.report({
node: nextBlock,
messageId: 'never',
fix (fixer) {
return paddingLines.map(([prevToken, nextToken]) => {
const start = prevToken.range[1]
const end = nextToken.range[0]
const paddingText = context.getSourceCode().text
.slice(start, end)
const lastSpaces = splitLines(paddingText).pop()
return fixer.replaceTextRange([start, end], '\n' + lastSpaces)
})
}
})
}
/**
* Check and report blocks for `always` configuration.
* This autofix inserts a blank line between the given 2 blocks.
* @param {RuleContext} context The rule context to report.
* @param {VElement} prevBlock The previous block to check.
* @param {VElement} nextBlock The next block to check.
* @param {Token[]} betweenTokens The array of tokens between blocks.
* @returns {void}
* @private
*/
function verifyForAlways (context, prevBlock, nextBlock, betweenTokens) {
const tokenOrNodes = [...betweenTokens, nextBlock]
let prev = prevBlock
let linebreak
for (const tokenOrNode of tokenOrNodes) {
const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line
if (numOfLineBreaks > 1) {
// Already padded.
return
}
if (!linebreak && numOfLineBreaks > 0) {
linebreak = prev
}
prev = tokenOrNode
}
context.report({
node: nextBlock,
messageId: 'always',
fix (fixer) {
if (linebreak) {
return fixer.insertTextAfter(linebreak, '\n')
}
return fixer.insertTextAfter(prevBlock, '\n\n')
}
})
}
/**
* Types of blank lines.
* `never` and `always` are defined.
* Those have `verify` method to check and report statements.
* @private
*/
const PaddingTypes = {
never: { verify: verifyForNever },
always: { verify: verifyForAlways }
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'require or disallow padding lines between blocks',
category: undefined,
url: 'https://eslint.vuejs.org/rules/padding-line-between-blocks.html'
},
fixable: 'whitespace',
schema: [
{
enum: Object.keys(PaddingTypes)
}
],
messages: {
never: 'Unexpected blank line before this block.',
always: 'Expected blank line before this block.'
}
},
create (context) {
const paddingType = PaddingTypes[context.options[0] || 'always']
const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()
let tokens
function getTopLevelHTMLElements () {
if (documentFragment) {
return documentFragment.children.filter(e => e.type === 'VElement')
}
return []
}
function getTokenAndCommentsBetween (prev, next) {
// When there is no <template>, tokenStore.getTokensBetween cannot be used.
if (!tokens) {
tokens = [
...documentFragment.tokens
.filter(token => token.type !== 'HTMLWhitespace'),
...documentFragment.comments
].sort((a, b) => a.range[0] > b.range[0] ? 1 : a.range[0] < b.range[0] ? -1 : 0)
}
let token = tokens.shift()
const results = []
while (token) {
if (prev.range[1] <= token.range[0]) {
if (next.range[0] <= token.range[0]) {
tokens.unshift(token)
break
} else {
results.push(token)
}
}
token = tokens.shift()
}
return results
}
return utils.defineTemplateBodyVisitor(
context,
{},
{
Program (node) {
if (utils.hasInvalidEOF(node)) {
return
}
const elements = [...getTopLevelHTMLElements()]
let prev = elements.shift()
for (const element of elements) {
const betweenTokens = getTokenAndCommentsBetween(prev, element)
paddingType.verify(context, prev, element, betweenTokens)
prev = element
}
}
}
)
}
}
+69
View File
@@ -0,0 +1,69 @@
/**
* @fileoverview Requires specific casing for the Prop name in Vue components
* @author Yu Kimura
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
const allowedCaseOptions = ['camelCase', 'snake_case']
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function create (context) {
const options = context.options[0]
const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'camelCase'
const converter = casing.getConverter(caseType)
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const props = utils.getComponentProps(obj)
.filter(prop => prop.key && (prop.key.type === 'Literal' || (prop.key.type === 'Identifier' && !prop.node.computed)))
for (const item of props) {
const propName = item.key.type === 'Literal' ? item.key.value : item.key.name
if (typeof propName !== 'string') {
// (boolean | null | number | RegExp) Literal
continue
}
const convertedName = converter(propName)
if (convertedName !== propName) {
context.report({
node: item.node,
message: 'Prop "{{name}}" is not in {{caseType}}.',
data: {
name: propName,
caseType: caseType
}
})
}
}
})
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce specific casing for the Prop name in Vue components',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/prop-name-casing.html'
},
fixable: null, // null or "code" or "whitespace"
schema: [
{
enum: allowedCaseOptions
}
]
},
create
}
+43
View File
@@ -0,0 +1,43 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'require `v-bind:is` of `<component>` elements',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/require-component-is.html'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VElement[name='component']" (node) {
if (!utils.hasDirective(node, 'bind', 'is')) {
context.report({
node,
loc: node.loc,
message: "Expected '<component>' elements to have 'v-bind:is' attribute."
})
}
}
})
}
}
+167
View File
@@ -0,0 +1,167 @@
/**
* @fileoverview Require default value for props
* @author Michał Sajnóg <msajnog93@gmail.com> (https://github.com/michalsnik)
*/
'use strict'
/**
* @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
* @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression
* @typedef {import('vue-eslint-parser').AST.ESLintPattern} Pattern
*/
/**
* @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
* @typedef {ComponentObjectProp & { value: ObjectExpression} } ComponentObjectPropObject
*/
const utils = require('../utils')
const NATIVE_TYPES = new Set([
'String',
'Number',
'Boolean',
'Function',
'Object',
'Array',
'Symbol'
])
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require default value for props',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/require-default-prop.html'
},
fixable: null, // or "code" or "whitespace"
schema: []
},
create (context) {
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
/**
* Checks if the passed prop is required
* @param {ComponentObjectPropObject} prop - Property AST node for a single prop
* @return {boolean}
*/
function propIsRequired (prop) {
const propRequiredNode = prop.value.properties
.find(p =>
p.type === 'Property' &&
utils.getStaticPropertyName(p) === 'required' &&
p.value.type === 'Literal' &&
p.value.value === true
)
return Boolean(propRequiredNode)
}
/**
* Checks if the passed prop has a default value
* @param {ComponentObjectPropObject} prop - Property AST node for a single prop
* @return {boolean}
*/
function propHasDefault (prop) {
const propDefaultNode = prop.value.properties
.find(p =>
p.type === 'Property' && utils.getStaticPropertyName(p) === 'default'
)
return Boolean(propDefaultNode)
}
/**
* Finds all props that don't have a default value set
* @param {ComponentObjectProp[]} props - Vue component's "props" node
* @return {ComponentObjectProp[]} Array of props without "default" value
*/
function findPropsWithoutDefaultValue (props) {
return props
.filter(prop => {
if (prop.value.type !== 'ObjectExpression') {
return (prop.value.type !== 'CallExpression' && prop.value.type !== 'Identifier') ||
(prop.value.type === 'Identifier' && NATIVE_TYPES.has(prop.value.name))
}
return !propIsRequired(/** @type {ComponentObjectPropObject} */(prop)) && !propHasDefault(/** @type {ComponentObjectPropObject} */(prop))
})
}
/**
* Detects whether given value node is a Boolean type
* @param {Expression | Pattern} value
* @return {Boolean}
*/
function isValueNodeOfBooleanType (value) {
return (
value.type === 'Identifier' &&
value.name === 'Boolean'
) || (
value.type === 'ArrayExpression' &&
value.elements.length === 1 &&
value.elements[0].type === 'Identifier' &&
value.elements[0].name === 'Boolean'
)
}
/**
* Detects whether given prop node is a Boolean
* @param {ComponentObjectProp} prop
* @return {Boolean}
*/
function isBooleanProp (prop) {
const value = utils.unwrapTypes(prop.value)
return isValueNodeOfBooleanType(value) || (
value.type === 'ObjectExpression' &&
value.properties.some(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'type' &&
isValueNodeOfBooleanType(p.value)
)
)
}
/**
* Excludes purely Boolean props from the Array
* @param {ComponentObjectProp[]} props - Array with props
* @return {ComponentObjectProp[]}
*/
function excludeBooleanProps (props) {
return props.filter(prop => !isBooleanProp(prop))
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const props = utils.getComponentProps(obj)
.filter(prop => prop.key && prop.value && !(prop.node.type === 'Property' && prop.node.shorthand))
const propsWithoutDefault = findPropsWithoutDefaultValue(/** @type {ComponentObjectProp[]} */(props))
const propsToReport = excludeBooleanProps(propsWithoutDefault)
for (const prop of propsToReport) {
const propName = prop.propName != null ? prop.propName : `[${context.getSourceCode().getText(prop.key)}]`
context.report({
node: prop.node,
message: `Prop '{{propName}}' requires default value to be set.`,
data: {
propName
}
})
}
})
}
}
+46
View File
@@ -0,0 +1,46 @@
/**
* @fileoverview require the component to be directly exported
* @author Hiroki Osame <hiroki.osame@gmail.com>
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require the component to be directly exported',
category: undefined,
url: 'https://eslint.vuejs.org/rules/require-direct-export.html'
},
fixable: null, // or "code" or "whitespace"
schema: []
},
create (context) {
const filePath = context.getFilename()
return {
'ExportDefaultDeclaration:exit' (node) {
if (!utils.isVueFile(filePath)) return
const isObjectExpression = (
node.type === 'ExportDefaultDeclaration' &&
node.declaration.type === 'ObjectExpression'
)
if (!isObjectExpression) {
context.report({
node,
message: `Expected the component literal to be directly exported.`
})
}
}
}
}
}
+37
View File
@@ -0,0 +1,37 @@
/**
* @fileoverview Require a name property in Vue components
* @author LukeeeeBennett
*/
'use strict'
const utils = require('../utils')
function isNameProperty (node) {
return node.type === 'Property' &&
node.key.name === 'name' &&
!node.computed
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require a name property in Vue components',
category: undefined,
url: 'https://eslint.vuejs.org/rules/require-name-property.html'
},
fixable: null,
schema: []
},
create (context) {
return utils.executeOnVue(context, component => {
if (component.properties.some(isNameProperty)) return
context.report({
node: component,
message: 'Required name property is not set.'
})
})
}
}
@@ -0,0 +1,102 @@
/**
* @fileoverview require prop type to be a constructor
* @author Michał Sajnóg
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const message = 'The "{{name}}" property should be a constructor.'
const forbiddenTypes = [
'Literal',
'TemplateLiteral',
'BinaryExpression',
'UpdateExpression'
]
const isForbiddenType = node => forbiddenTypes.indexOf(node.type) > -1 && node.raw !== 'null'
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require prop type to be a constructor',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/require-prop-type-constructor.html'
},
fixable: 'code', // or "code" or "whitespace"
schema: []
},
create (context) {
const fix = node => fixer => {
let newText
if (node.type === 'Literal') {
if (typeof node.value !== 'string') {
return undefined
}
newText = node.value
} else if (
node.type === 'TemplateLiteral' &&
node.expressions.length === 0 &&
node.quasis.length === 1
) {
newText = node.quasis[0].value.cooked
} else {
return undefined
}
if (newText) {
return fixer.replaceText(node, newText)
}
}
const checkPropertyNode = (key, node) => {
if (isForbiddenType(node)) {
context.report({
node: node,
message,
data: {
name: utils.getStaticPropertyName(key)
},
fix: fix(node)
})
} else if (node.type === 'ArrayExpression') {
node.elements
.filter(prop => prop && isForbiddenType(prop))
.forEach(prop => context.report({
node: prop,
message,
data: {
name: utils.getStaticPropertyName(key)
},
fix: fix(prop)
}))
}
}
return utils.executeOnVueComponent(context, (obj) => {
const props = utils.getComponentProps(obj)
.filter(prop => prop.key && prop.value)
for (const prop of props) {
if (isForbiddenType(prop.value) || prop.value.type === 'ArrayExpression') {
checkPropertyNode(prop.key, prop.value)
} else if (prop.value.type === 'ObjectExpression') {
const typeProperty = prop.value.properties.find(property =>
property.type === 'Property' &&
property.key.name === 'type'
)
if (!typeProperty) continue
checkPropertyNode(prop.key, typeProperty.value)
}
}
})
}
}
+81
View File
@@ -0,0 +1,81 @@
/**
* @fileoverview Prop definitions should be detailed
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require type definitions in props',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/require-prop-types.html'
},
fixable: null, // or "code" or "whitespace"
schema: [
// fill in your schema
]
},
create (context) {
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
function objectHasType (node) {
const typeProperty = node.properties
.find(p =>
utils.getStaticPropertyName(p.key) === 'type' &&
(
p.value.type !== 'ArrayExpression' ||
p.value.elements.length > 0
)
)
const validatorProperty = node.properties
.find(p => utils.getStaticPropertyName(p.key) === 'validator')
return Boolean(typeProperty || validatorProperty)
}
function checkProperty (key, value, node) {
let hasType = true
if (!value) {
hasType = false
} else if (value.type === 'ObjectExpression') { // foo: {
hasType = objectHasType(value)
} else if (value.type === 'ArrayExpression') { // foo: [
hasType = value.elements.length > 0
} else if (value.type === 'FunctionExpression' || value.type === 'ArrowFunctionExpression') {
hasType = false
}
if (!hasType) {
context.report({
node,
message: 'Prop "{{name}}" should define at least its type.',
data: {
name: utils.getStaticPropertyName(key || node) || 'Unknown prop'
}
})
}
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const props = utils.getComponentProps(obj)
for (const prop of props) {
checkProperty(prop.key, prop.value, prop.node)
}
})
}
}
+54
View File
@@ -0,0 +1,54 @@
/**
* @fileoverview Enforces render function to always return value.
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce render function to always return value',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/require-render-return.html'
},
fixable: null, // or "code" or "whitespace"
schema: []
},
create (context) {
const forbiddenNodes = []
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return Object.assign({},
utils.executeOnFunctionsWithoutReturn(true, node => {
forbiddenNodes.push(node)
}),
utils.executeOnVue(context, obj => {
const node = obj.properties.find(item => item.type === 'Property' &&
utils.getStaticPropertyName(item) === 'render' &&
(item.value.type === 'ArrowFunctionExpression' || item.value.type === 'FunctionExpression')
)
if (!node) return
forbiddenNodes.forEach(el => {
if (node.value === el) {
context.report({
node: node.key,
message: 'Expected to return a value in render function.'
})
}
})
})
)
}
}
+57
View File
@@ -0,0 +1,57 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'require `v-bind:key` with `v-for` directives',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/require-v-for-key.html'
},
fixable: null,
schema: []
},
create (context) {
/**
* Check the given element about `v-bind:key` attributes.
* @param {ASTNode} element The element node to check.
*/
function checkKey (element) {
if (element.name === 'template' || element.name === 'slot') {
for (const child of element.children) {
if (child.type === 'VElement') {
checkKey(child)
}
}
} else if (!utils.isCustomComponent(element) && !utils.hasDirective(element, 'bind', 'key')) {
context.report({
node: element.startTag,
loc: element.startTag.loc,
message: "Elements in iteration expect to have 'v-bind:key' directives."
})
}
}
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='for']" (node) {
checkKey(node.parent.parent)
}
})
}
}
+124
View File
@@ -0,0 +1,124 @@
/**
* @fileoverview Enforces props default values to be valid.
* @author Armano
*/
'use strict'
const utils = require('../utils')
const NATIVE_TYPES = new Set([
'String',
'Number',
'Boolean',
'Function',
'Object',
'Array',
'Symbol'
])
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce props default values to be valid',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/require-valid-default-prop.html'
},
fixable: null,
schema: []
},
create (context) {
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
function isPropertyIdentifier (node) {
return node.type === 'Property' && node.key.type === 'Identifier'
}
function getPropertyNode (obj, name) {
return obj.properties.find(p =>
isPropertyIdentifier(p) &&
p.key.name === name
)
}
function getTypes (node) {
if (node.type === 'Identifier') {
return [node.name]
} else if (node.type === 'ArrayExpression') {
return node.elements
.filter(item => item.type === 'Identifier')
.map(item => item.name)
}
return []
}
function ucFirst (text) {
return text[0].toUpperCase() + text.slice(1)
}
function getValueType (node) {
if (node.type === 'CallExpression') { // Symbol(), Number() ...
if (node.callee.type === 'Identifier' && NATIVE_TYPES.has(node.callee.name)) {
return node.callee.name
}
} else if (node.type === 'TemplateLiteral') { // String
return 'String'
} else if (node.type === 'Literal') { // String, Boolean, Number
if (node.value === null) return null
const type = ucFirst(typeof node.value)
if (NATIVE_TYPES.has(type)) {
return type
}
} else if (node.type === 'ArrayExpression') { // Array
return 'Array'
} else if (node.type === 'ObjectExpression') { // Object
return 'Object'
}
// FunctionExpression, ArrowFunctionExpression
return null
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, obj => {
const props = utils.getComponentProps(obj)
.filter(prop => prop.key && prop.value && prop.value.type === 'ObjectExpression')
for (const prop of props) {
const type = getPropertyNode(prop.value, 'type')
if (!type) continue
const typeNames = new Set(getTypes(type.value)
.map(item => item === 'Object' || item === 'Array' ? 'Function' : item) // Object and Array require function
.filter(item => NATIVE_TYPES.has(item)))
// There is no native types detected
if (typeNames.size === 0) continue
const def = getPropertyNode(prop.value, 'default')
if (!def) continue
const defType = getValueType(def.value)
if (!defType || typeNames.has(defType)) continue
const propName = prop.propName != null ? prop.propName : `[${context.getSourceCode().getText(prop.key)}]`
context.report({
node: def,
message: "Type of the default value for '{{name}}' prop must be a {{types}}.",
data: {
name: propName,
types: Array.from(typeNames).join(' or ').toLowerCase()
}
})
}
})
}
}
@@ -0,0 +1,68 @@
/**
* @fileoverview Enforces that a return statement is present in computed property (return-in-computed-property)
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce that a return statement is present in computed property',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/return-in-computed-property.html'
},
fixable: null, // or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
treatUndefinedAsUnspecified: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
create (context) {
const options = context.options[0] || {}
const treatUndefinedAsUnspecified = !(options.treatUndefinedAsUnspecified === false)
const forbiddenNodes = []
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return Object.assign({},
utils.executeOnFunctionsWithoutReturn(treatUndefinedAsUnspecified, node => {
forbiddenNodes.push(node)
}),
utils.executeOnVue(context, properties => {
const computedProperties = utils.getComputedProperties(properties)
computedProperties.forEach(cp => {
forbiddenNodes.forEach(el => {
if (cp.value && cp.value.parent === el) {
context.report({
node: el,
message: 'Expected to return a value in "{{name}}" computed property.',
data: {
name: cp.key
}
})
}
})
})
})
)
}
}
+58
View File
@@ -0,0 +1,58 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const indentCommon = require('../utils/indent-common')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce consistent indentation in `<script>`',
category: undefined,
url: 'https://eslint.vuejs.org/rules/script-indent.html'
},
fixable: 'whitespace',
schema: [
{
anyOf: [
{ type: 'integer', minimum: 1 },
{ enum: ['tab'] }
]
},
{
type: 'object',
properties: {
'baseIndent': { type: 'integer', minimum: 0 },
'switchCase': { type: 'integer', minimum: 0 },
'ignores': {
type: 'array',
items: {
allOf: [
{ type: 'string' },
{ not: { type: 'string', pattern: ':exit$' }},
{ not: { type: 'string', pattern: '^\\s*$' }}
]
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
]
},
create (context) {
return indentCommon.defineVisitor(context, context.getSourceCode(), {})
}
}
@@ -0,0 +1,174 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const casing = require('../utils/casing')
const INLINE_ELEMENTS = require('../utils/inline-non-void-elements.json')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
function isSinglelineElement (element) {
return element.loc.start.line === element.endTag.loc.start.line
}
function parseOptions (options) {
return Object.assign({
ignores: ['pre', 'textarea'].concat(INLINE_ELEMENTS),
ignoreWhenNoAttributes: true,
ignoreWhenEmpty: true
}, options)
}
/**
* Check whether the given element is empty or not.
* This ignores whitespaces, doesn't ignore comments.
* @param {VElement} node The element node to check.
* @param {SourceCode} sourceCode The source code object of the current context.
* @returns {boolean} `true` if the element is empty.
*/
function isEmpty (node, sourceCode) {
const start = node.startTag.range[1]
const end = node.endTag.range[0]
return sourceCode.text.slice(start, end).trim() === ''
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'require a line break before and after the contents of a singleline element',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/singleline-html-element-content-newline.html'
},
fixable: 'whitespace',
schema: [{
type: 'object',
properties: {
ignoreWhenNoAttributes: {
type: 'boolean'
},
ignoreWhenEmpty: {
type: 'boolean'
},
ignores: {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}],
messages: {
unexpectedAfterClosingBracket: 'Expected 1 line break after opening tag (`<{{name}}>`), but no line breaks found.',
unexpectedBeforeOpeningBracket: 'Expected 1 line break before closing tag (`</{{name}}>`), but no line breaks found.'
}
},
create (context) {
const options = parseOptions(context.options[0])
const ignores = options.ignores
const ignoreWhenNoAttributes = options.ignoreWhenNoAttributes
const ignoreWhenEmpty = options.ignoreWhenEmpty
const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
const sourceCode = context.getSourceCode()
let inIgnoreElement
function isIgnoredElement (node) {
return ignores.includes(node.name) ||
ignores.includes(casing.pascalCase(node.rawName)) ||
ignores.includes(casing.kebabCase(node.rawName))
}
return utils.defineTemplateBodyVisitor(context, {
'VElement' (node) {
if (inIgnoreElement) {
return
}
if (isIgnoredElement(node)) {
// ignore element name
inIgnoreElement = node
return
}
if (node.startTag.selfClosing || !node.endTag) {
// self closing
return
}
if (!isSinglelineElement(node)) {
return
}
if (ignoreWhenNoAttributes && node.startTag.attributes.length === 0) {
return
}
const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' }
if (
ignoreWhenEmpty &&
node.children.length === 0 &&
template.getFirstTokensBetween(node.startTag, node.endTag, getTokenOption).length === 0
) {
return
}
const contentFirst = template.getTokenAfter(node.startTag, getTokenOption)
const contentLast = template.getTokenBefore(node.endTag, getTokenOption)
context.report({
node: template.getLastToken(node.startTag),
loc: {
start: node.startTag.loc.end,
end: contentFirst.loc.start
},
messageId: 'unexpectedAfterClosingBracket',
data: {
name: node.rawName
},
fix (fixer) {
const range = [node.startTag.range[1], contentFirst.range[0]]
return fixer.replaceTextRange(range, '\n')
}
})
if (isEmpty(node, sourceCode)) {
return
}
context.report({
node: template.getFirstToken(node.endTag),
loc: {
start: contentLast.loc.end,
end: node.endTag.loc.start
},
messageId: 'unexpectedBeforeOpeningBracket',
data: {
name: node.rawName
},
fix (fixer) {
const range = [contentLast.range[1], node.endTag.range[0]]
return fixer.replaceTextRange(range, '\n')
}
})
},
'VElement:exit' (node) {
if (inIgnoreElement === node) {
inIgnoreElement = null
}
}
})
}
}
+265
View File
@@ -0,0 +1,265 @@
/**
* @fileoverview enforce sort-keys in a manner that is compatible with order-in-components
* @author Loren Klingman
* Original ESLint sort-keys by Toru Nagashima
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const naturalCompare = require('natural-compare')
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Gets the property name of the given `Property` node.
*
* - If the property's key is an `Identifier` node, this returns the key's name
* whether it's a computed property or not.
* - If the property has a static name, this returns the static name.
* - Otherwise, this returns null.
* @param {ASTNode} node The `Property` node to get.
* @returns {string|null} The property name or null.
* @private
*/
function getPropertyName (node) {
const staticName = utils.getStaticPropertyName(node)
if (staticName !== null) {
return staticName
}
return node.key.name || null
}
/**
* Functions which check that the given 2 names are in specific order.
*
* Postfix `I` is meant insensitive.
* Postfix `N` is meant natual.
* @private
*/
const isValidOrders = {
asc (a, b) {
return a <= b
},
ascI (a, b) {
return a.toLowerCase() <= b.toLowerCase()
},
ascN (a, b) {
return naturalCompare(a, b) <= 0
},
ascIN (a, b) {
return naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0
},
desc (a, b) {
return isValidOrders.asc(b, a)
},
descI (a, b) {
return isValidOrders.ascI(b, a)
},
descN (a, b) {
return isValidOrders.ascN(b, a)
},
descIN (a, b) {
return isValidOrders.ascIN(b, a)
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce sort-keys in a manner that is compatible with order-in-components',
category: null,
recommended: false,
url: 'https://eslint.vuejs.org/rules/sort-keys.html'
},
fixable: null,
schema: [
{
enum: ['asc', 'desc']
},
{
type: 'object',
properties: {
caseSensitive: {
type: 'boolean',
default: true
},
ignoreChildrenOf: {
type: 'array'
},
ignoreGrandchildrenOf: {
type: 'array'
},
minKeys: {
type: 'integer',
minimum: 2,
default: 2
},
natural: {
type: 'boolean',
default: false
},
runOutsideVue: {
type: 'boolean',
default: true
}
},
additionalProperties: false
}
]
},
create (context) {
// Parse options.
const options = context.options[1]
const order = context.options[0] || 'asc'
const ignoreGrandchildrenOf = (options && options.ignoreGrandchildrenOf) || ['computed', 'directives', 'inject', 'props', 'watch']
const ignoreChildrenOf = (options && options.ignoreChildrenOf) || ['model']
const insensitive = options && options.caseSensitive === false
const minKeys = options && options.minKeys
const natual = options && options.natural
const isValidOrder = isValidOrders[
order + (insensitive ? 'I' : '') + (natual ? 'N' : '')
]
// The stack to save the previous property's name for each object literals.
let stack = null
let errors = []
const names = {}
const reportErrors = (isVue) => {
if (isVue) {
errors = errors.filter((error) => {
let parentIsRoot = !error.hasUpper
let grandParentIsRoot = !error.grandparent
let greatGrandparentIsRoot = !error.greatGrandparent
const stackPrevChar = stack && stack.prevChar
if (stackPrevChar) {
parentIsRoot = stackPrevChar === error.parent
grandParentIsRoot = stackPrevChar === error.grandparent
greatGrandparentIsRoot = stackPrevChar === error.greatGrandparent
}
if (parentIsRoot) {
return false
} else if (grandParentIsRoot) {
return !error.parentIsProperty || !ignoreChildrenOf.includes(names[error.parent])
} else if (greatGrandparentIsRoot) {
return !error.parentIsProperty || !ignoreGrandchildrenOf.includes(names[error.grandparent])
}
return true
})
}
errors.forEach((error) => error.errors.forEach((e) => context.report(e)))
errors = []
}
const sortTests = {
ObjectExpression (node) {
if (!stack) {
reportErrors(false)
}
stack = {
upper: stack,
prevChar: null,
prevName: null,
numKeys: node.properties.length,
parentIsProperty: node.parent.type === 'Property',
errors: []
}
},
'ObjectExpression:exit' (node) {
errors.push({
errors: stack.errors,
hasUpper: !!stack.upper,
parentIsProperty: node.parent.type === 'Property',
parent: stack.upper && stack.upper.prevChar,
grandparent: stack.upper && stack.upper.upper && stack.upper.upper.prevChar,
greatGrandparent: stack.upper && stack.upper.upper && stack.upper.upper.upper && stack.upper.upper.upper.prevChar
})
stack = stack.upper
},
SpreadElement (node) {
if (node.parent.type === 'ObjectExpression') {
stack.prevName = null
stack.prevChar = null
}
},
'Program:exit' () {
reportErrors(false)
},
Property (node) {
if (node.parent.type === 'ObjectPattern') {
return
}
const prevName = stack.prevName
const numKeys = stack.numKeys
const thisName = getPropertyName(node)
if (thisName !== null) {
stack.prevName = thisName
stack.prevChar = node.range[0]
if (Object.prototype.hasOwnProperty.call(names, node.range[0])) {
throw new Error('Name clash')
}
names[node.range[0]] = thisName
}
if (prevName === null || thisName === null || numKeys < minKeys) {
return
}
if (!isValidOrder(prevName, thisName)) {
stack.errors.push({
node,
loc: node.key.loc,
message: "Expected object keys to be in {{natual}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'.",
data: {
thisName,
prevName,
order,
insensitive: insensitive ? 'insensitive ' : '',
natual: natual ? 'natural ' : ''
}
})
}
}
}
const execOnVue = utils.executeOnVue(context, (obj) => {
reportErrors(true)
})
const result = { ...sortTests }
Object.keys(execOnVue).forEach((key) => {
// Ensure we call both the callback from sortTests and execOnVue if they both use the same key
if (Object.prototype.hasOwnProperty.call(sortTests, key)) {
result[key] = (node) => {
sortTests[key](node)
execOnVue[key](node)
}
} else {
result[key] = execOnVue[key]
}
})
return result
}
}
+12
View File
@@ -0,0 +1,12 @@
/**
* @author Toru Nagashima
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta
module.exports = wrapCoreRule(
require('eslint/lib/rules/space-infix-ops'),
{ skipDynamicArguments: true }
)
+12
View File
@@ -0,0 +1,12 @@
/**
* @author Toru Nagashima
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta
module.exports = wrapCoreRule(
require('eslint/lib/rules/space-unary-ops'),
{ skipDynamicArguments: true }
)
+55
View File
@@ -0,0 +1,55 @@
/**
* @fileoverview Alphabetizes static class names.
* @author Maciej Chmurski
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const { defineTemplateBodyVisitor } = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
url: 'https://eslint.vuejs.org/rules/static-class-names-order.html',
description: 'enforce static class names order',
category: undefined
},
fixable: 'code',
schema: []
},
create: context => {
return defineTemplateBodyVisitor(context, {
"VAttribute[directive=false][key.name='class']" (node) {
const classList = node.value.value
const classListWithWhitespace = classList.split(/(\s+)/)
// Detect and reuse any type of whitespace.
let divider = ''
if (classListWithWhitespace.length > 1) {
divider = classListWithWhitespace[1]
}
const classListNoWhitespace = classListWithWhitespace.filter(className => className.trim() !== '')
const classListSorted = classListNoWhitespace.sort().join(divider)
if (classList !== classListSorted) {
context.report({
node,
loc: node.loc,
message: 'Classes should be ordered alphabetically.',
fix: (fixer) => fixer.replaceTextRange(
[node.value.range[0], node.value.range[1]], `"${classListSorted}"`
)
})
}
}
})
}
}
@@ -0,0 +1,25 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
supported: '2.6.0',
createTemplateBodyVisitor (context) {
/**
* Reports dynamic argument node
* @param {VExpressionContainer} dinamicArgument node of dynamic argument
* @returns {void}
*/
function reportDynamicArgument (dinamicArgument) {
context.report({
node: dinamicArgument,
messageId: 'forbiddenDynamicDirectiveArguments'
})
}
return {
'VAttribute[directive=true] > VDirectiveKey > VExpressionContainer': reportDynamicArgument
}
}
}
+27
View File
@@ -0,0 +1,27 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
deprecated: '2.5.0',
createTemplateBodyVisitor (context) {
/**
* Reports `scope` node
* @param {VDirectiveKey} scopeKey node of `scope`
* @returns {void}
*/
function reportScope (scopeKey) {
context.report({
node: scopeKey,
messageId: 'forbiddenScopeAttribute',
// fix to use `slot-scope`
fix: fixer => fixer.replaceText(scopeKey, 'slot-scope')
})
}
return {
"VAttribute[directive=true] > VDirectiveKey[name.name='scope']": reportScope
}
}
}
+128
View File
@@ -0,0 +1,128 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
deprecated: '2.6.0',
createTemplateBodyVisitor (context) {
const sourceCode = context.getSourceCode()
/**
* Checks whether the given node can convert to the `v-slot`.
* @param {VAttribute} slotAttr node of `slot`
* @returns {boolean} `true` if the given node can convert to the `v-slot`
*/
function canConvertFromSlotToVSlot (slotAttr) {
if (slotAttr.parent.parent.name !== 'template') {
return false
}
if (!slotAttr.value) {
return true
}
const slotName = slotAttr.value.value
// If non-Latin characters are included it can not be converted.
return !/[^a-z]/i.test(slotName)
}
/**
* Checks whether the given node can convert to the `v-slot`.
* @param {VAttribute} slotAttr node of `v-bind:slot`
* @returns {boolean} `true` if the given node can convert to the `v-slot`
*/
function canConvertFromVBindSlotToVSlot (slotAttr) {
if (slotAttr.parent.parent.name !== 'template') {
return false
}
if (!slotAttr.value) {
return true
}
if (!slotAttr.value.expression) {
// parse error or empty expression
return false
}
const slotName = sourceCode.getText(slotAttr.value.expression).trim()
// If non-Latin characters are included it can not be converted.
// It does not check the space only because `a>b?c:d` should be rejected.
return !/[^a-z]/i.test(slotName)
}
/**
* Convert to `v-slot`.
* @param {object} fixer fixer
* @param {VAttribute} slotAttr node of `slot`
* @param {string | null} slotName name of `slot`
* @param {boolean} vBind `true` if `slotAttr` is `v-bind:slot`
* @returns {*} fix data
*/
function fixSlotToVSlot (fixer, slotAttr, slotName, vBind) {
const element = slotAttr.parent
const scopeAttr = element.attributes
.find(attr => attr.directive === true && attr.key.name && (
attr.key.name.name === 'slot-scope' ||
attr.key.name.name === 'scope'
))
const nameArgument = slotName ? (vBind ? `:[${slotName}]` : `:${slotName}`) : ''
const scopeValue = scopeAttr && scopeAttr.value
? `=${sourceCode.getText(scopeAttr.value)}`
: ''
const replaceText = `v-slot${nameArgument}${scopeValue}`
const fixers = [
fixer.replaceText(slotAttr || scopeAttr, replaceText)
]
if (slotAttr && scopeAttr) {
fixers.push(fixer.remove(scopeAttr))
}
return fixers
}
/**
* Reports `slot` node
* @param {VAttribute} slotAttr node of `slot`
* @returns {void}
*/
function reportSlot (slotAttr) {
context.report({
node: slotAttr.key,
messageId: 'forbiddenSlotAttribute',
// fix to use `v-slot`
fix (fixer) {
if (!canConvertFromSlotToVSlot(slotAttr)) {
return null
}
const slotName = slotAttr.value &&
slotAttr.value.value
return fixSlotToVSlot(fixer, slotAttr, slotName, false)
}
})
}
/**
* Reports `v-bind:slot` node
* @param {VAttribute} slotAttr node of `v-bind:slot`
* @returns {void}
*/
function reportVBindSlot (slotAttr) {
context.report({
node: slotAttr.key,
messageId: 'forbiddenSlotAttribute',
// fix to use `v-slot`
fix (fixer) {
if (!canConvertFromVBindSlotToVSlot(slotAttr)) {
return null
}
const slotName = slotAttr.value &&
slotAttr.value.expression &&
sourceCode.getText(slotAttr.value.expression).trim()
return fixSlotToVSlot(fixer, slotAttr, slotName, true)
}
})
}
return {
"VAttribute[directive=false][key.name='slot']": reportSlot,
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='slot']": reportVBindSlot
}
}
}
@@ -0,0 +1,84 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
deprecated: '2.6.0',
supported: '2.5.0',
createTemplateBodyVisitor (context, { fixToUpgrade } = {}) {
const sourceCode = context.getSourceCode()
/**
* Checks whether the given node can convert to the `v-slot`.
* @param {VStartTag} startTag node of `<element v-slot ... >`
* @returns {boolean} `true` if the given node can convert to the `v-slot`
*/
function canConvertToVSlot (startTag) {
if (startTag.parent.name !== 'template') {
return false
}
const slotAttr = startTag.attributes
.find(attr => attr.directive === false && attr.key.name === 'slot')
if (slotAttr) {
// if the element have `slot` it can not be converted.
// Conversion of `slot` is done with `vue/no-deprecated-slot-attribute`.
return false
}
const vBindSlotAttr = startTag.attributes
.find(attr =>
attr.directive === true &&
attr.key.name.name === 'bind' &&
attr.key.argument &&
attr.key.argument.name === 'slot')
if (vBindSlotAttr) {
// if the element have `v-bind:slot` it can not be converted.
// Conversion of `v-bind:slot` is done with `vue/no-deprecated-slot-attribute`.
return false
}
return true
}
/**
* Convert to `v-slot`.
* @param {object} fixer fixer
* @param {VAttribute | null} scopeAttr node of `slot-scope`
* @returns {*} fix data
*/
function fixSlotScopeToVSlot (fixer, scopeAttr) {
const scopeValue = scopeAttr && scopeAttr.value
? `=${sourceCode.getText(scopeAttr.value)}`
: ''
const replaceText = `v-slot${scopeValue}`
return fixer.replaceText(scopeAttr, replaceText)
}
/**
* Reports `slot-scope` node
* @param {VAttribute} scopeAttr node of `slot-scope`
* @returns {void}
*/
function reportSlotScope (scopeAttr) {
context.report({
node: scopeAttr.key,
messageId: 'forbiddenSlotScopeAttribute',
fix: fixToUpgrade
// fix to use `v-slot`
? (fixer) => {
const startTag = scopeAttr.parent
if (!canConvertToVSlot(startTag)) {
return null
}
return fixSlotScopeToVSlot(fixer, scopeAttr)
}
: null
})
}
return {
"VAttribute[directive=true][key.name.name='slot-scope']": reportSlotScope
}
}
}
@@ -0,0 +1,33 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const { Range } = require('semver')
const unsupported = new Range('<=2.5 || >=2.6.0')
module.exports = {
// >=2.6.0-beta.1 <=2.6.0-beta.3
supported: (versionRange) => {
return !versionRange.intersects(unsupported)
},
createTemplateBodyVisitor (context) {
/**
* Reports `.prop` shorthand node
* @param {VDirectiveKey} bindPropKey node of `.prop` shorthand
* @returns {void}
*/
function reportPropModifierShorthand (bindPropKey) {
context.report({
node: bindPropKey,
messageId: 'forbiddenVBindPropModifierShorthand',
// fix to use `:x.prop` (downgrade)
fix: fixer => fixer.replaceText(bindPropKey, `:${bindPropKey.argument.rawName}.prop`)
})
}
return {
"VAttribute[directive=true] > VDirectiveKey[name.name='bind'][name.rawName='.']": reportPropModifierShorthand
}
}
}
+83
View File
@@ -0,0 +1,83 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
supported: '2.6.0',
createTemplateBodyVisitor (context) {
const sourceCode = context.getSourceCode()
/**
* Checks whether the given node can convert to the `slot`.
* @param {VAttribute} vSlotAttr node of `v-slot`
* @returns {boolean} `true` if the given node can convert to the `slot`
*/
function canConvertToSlot (vSlotAttr) {
if (vSlotAttr.parent.parent.name !== 'template') {
return false
}
return true
}
/**
* Convert to `slot` and `slot-scope`.
* @param {object} fixer fixer
* @param {VAttribute} vSlotAttr node of `v-slot`
* @returns {*} fix data
*/
function fixVSlotToSlot (fixer, vSlotAttr) {
const key = vSlotAttr.key
if (key.modifiers.length) {
// unknown modifiers
return null
}
const attrs = []
const argument = key.argument
if (argument) {
if (argument.type === 'VIdentifier') {
const name = argument.rawName
attrs.push(`slot="${name}"`)
} else if (argument.type === 'VExpressionContainer' && argument.expression) {
const expression = sourceCode.getText(argument.expression)
attrs.push(`:slot="${expression}"`)
} else {
// unknown or syntax error
return null
}
}
const scopedValueNode = vSlotAttr.value
if (scopedValueNode) {
attrs.push(
`slot-scope=${sourceCode.getText(scopedValueNode)}`
)
}
if (!attrs.length) {
attrs.push('slot') // useless
}
return fixer.replaceText(vSlotAttr, attrs.join(' '))
}
/**
* Reports `v-slot` node
* @param {VAttribute} vSlotAttr node of `v-slot`
* @returns {void}
*/
function reportVSlot (vSlotAttr) {
context.report({
node: vSlotAttr.key,
messageId: 'forbiddenVSlot',
// fix to use `slot` (downgrade)
fix: fixer => {
if (!canConvertToSlot(vSlotAttr)) {
return null
}
return fixVSlotToSlot(fixer, vSlotAttr)
}
})
}
return {
"VAttribute[directive=true][key.name.name='slot']": reportVSlot
}
}
}
+107
View File
@@ -0,0 +1,107 @@
/**
* @fileoverview disallow usage of `this` in template.
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const RESERVED_NAMES = new Set(require('../utils/js-reserved.json'))
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow usage of `this` in template',
category: 'recommended',
url: 'https://eslint.vuejs.org/rules/this-in-template.html'
},
fixable: null,
schema: [
{
enum: ['always', 'never']
}
]
},
/**
* Creates AST event handlers for this-in-template.
*
* @param {RuleContext} context - The rule context.
* @returns {Object} AST event handlers.
*/
create (context) {
const options = context.options[0] !== 'always' ? 'never' : 'always'
let scope = {
parent: null,
nodes: []
}
return utils.defineTemplateBodyVisitor(context, Object.assign({
VElement (node) {
scope = {
parent: scope,
nodes: scope.nodes.slice() // make copy
}
if (node.variables) {
for (const variable of node.variables) {
const varNode = variable.id
const name = varNode.name
if (!scope.nodes.some(node => node.name === name)) { // Prevent adding duplicates
scope.nodes.push(varNode)
}
}
}
},
'VElement:exit' (node) {
scope = scope.parent
}
}, options === 'never'
? {
'VExpressionContainer MemberExpression > ThisExpression' (node) {
const propertyName = utils.getStaticPropertyName(node.parent.property)
if (!propertyName ||
scope.nodes.some(el => el.name === propertyName) ||
RESERVED_NAMES.has(propertyName) || // this.class | this['class']
/^[0-9].*$|[^a-zA-Z0-9_]/.test(propertyName) // this['0aaaa'] | this['foo-bar bas']
) {
return
}
context.report({
node,
loc: node.loc,
message: "Unexpected usage of 'this'."
})
}
}
: {
'VExpressionContainer' (node) {
if (node.parent.type === 'VDirectiveKey') {
// We cannot use `.` in dynamic arguments because the right of the `.` becomes a modifier.
// For example, In `:[this.prop]` case, `:[this` is an argument and `prop]` is a modifier.
return
}
if (node.references) {
for (const reference of node.references) {
if (!scope.nodes.some(el => el.name === reference.id.name)) {
context.report({
node: reference.id,
loc: reference.id.loc,
message: "Expected 'this'."
})
}
}
}
}
}
))
}
}
+219
View File
@@ -0,0 +1,219 @@
/**
* @fileoverview enforce usage of `exact` modifier on `v-on`.
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const SYSTEM_MODIFIERS = new Set(['ctrl', 'shift', 'alt', 'meta'])
const GLOBAL_MODIFIERS = new Set(['stop', 'prevent', 'capture', 'self', 'once', 'passive', 'native'])
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Finds and returns all keys for event directives
*
* @param {array} attributes Element attributes
* @param {SourceCode} sourceCode The source code object.
* @returns {array[object]} [{ name, node, modifiers }]
*/
function getEventDirectives (attributes, sourceCode) {
return attributes
.filter(attribute =>
attribute.directive &&
attribute.key.name.name === 'on'
)
.map(attribute => ({
name: attribute.key.argument ? sourceCode.getText(attribute.key.argument) : '',
node: attribute.key,
modifiers: attribute.key.modifiers.map(modifier => modifier.name)
}))
}
/**
* Checks whether given modifier is key modifier
*
* @param {string} modifier
* @returns {boolean}
*/
function isKeyModifier (modifier) {
return !GLOBAL_MODIFIERS.has(modifier) && !SYSTEM_MODIFIERS.has(modifier)
}
/**
* Checks whether given modifier is system one
*
* @param {string} modifier
* @returns {boolean}
*/
function isSystemModifier (modifier) {
return SYSTEM_MODIFIERS.has(modifier)
}
/**
* Checks whether given any of provided modifiers
* has system modifier
*
* @param {array} modifiers
* @returns {boolean}
*/
function hasSystemModifier (modifiers) {
return modifiers.some(isSystemModifier)
}
/**
* Groups all events in object,
* with keys represinting each event name
*
* @param {array} events
* @returns {object} { click: [], keypress: [] }
*/
function groupEvents (events) {
return events.reduce((acc, event) => {
if (acc[event.name]) {
acc[event.name].push(event)
} else {
acc[event.name] = [event]
}
return acc
}, {})
}
/**
* Creates alphabetically sorted string with system modifiers
*
* @param {array[string]} modifiers
* @returns {string} e.g. "alt,ctrl,del,shift"
*/
function getSystemModifiersString (modifiers) {
return modifiers.filter(isSystemModifier).sort().join(',')
}
/**
* Creates alphabetically sorted string with key modifiers
*
* @param {array[string]} modifiers
* @returns {string} e.g. "enter,tab"
*/
function getKeyModifiersString (modifiers) {
return modifiers.filter(isKeyModifier).sort().join(',')
}
/**
* Compares two events based on their modifiers
* to detect possible event leakeage
*
* @param {object} baseEvent
* @param {object} event
* @returns {boolean}
*/
function hasConflictedModifiers (baseEvent, event) {
if (
event.node === baseEvent.node ||
event.modifiers.includes('exact')
) return false
const eventKeyModifiers = getKeyModifiersString(event.modifiers)
const baseEventKeyModifiers = getKeyModifiersString(baseEvent.modifiers)
if (
eventKeyModifiers && baseEventKeyModifiers &&
eventKeyModifiers !== baseEventKeyModifiers
) return false
const eventSystemModifiers = getSystemModifiersString(event.modifiers)
const baseEventSystemModifiers = getSystemModifiersString(baseEvent.modifiers)
return (
baseEvent.modifiers.length >= 1 &&
baseEventSystemModifiers !== eventSystemModifiers &&
baseEventSystemModifiers.indexOf(eventSystemModifiers) > -1
)
}
/**
* Searches for events that might conflict with each other
*
* @param {array} events
* @returns {array} conflicted events, without duplicates
*/
function findConflictedEvents (events) {
return events.reduce((acc, event) => {
return [
...acc,
...events
.filter(evt => !acc.find(e => evt === e)) // No duplicates
.filter(hasConflictedModifiers.bind(null, event))
]
}, [])
}
// ------------------------------------------------------------------------------
// Rule details
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce usage of `exact` modifier on `v-on`',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/use-v-on-exact.html'
},
fixable: null,
schema: []
},
/**
* Creates AST event handlers for use-v-on-exact.
*
* @param {RuleContext} context - The rule context.
* @returns {Object} AST event handlers.
*/
create (context) {
const sourceCode = context.getSourceCode()
return utils.defineTemplateBodyVisitor(context, {
VStartTag (node) {
if (node.attributes.length === 0) return
const isCustomComponent = utils.isCustomComponent(node.parent)
let events = getEventDirectives(node.attributes, sourceCode)
if (isCustomComponent) {
// For components consider only events with `native` modifier
events = events.filter(event => event.modifiers.includes('native'))
}
const grouppedEvents = groupEvents(events)
Object.keys(grouppedEvents).forEach(eventName => {
const eventsInGroup = grouppedEvents[eventName]
const hasEventWithKeyModifier = eventsInGroup.some(event =>
hasSystemModifier(event.modifiers)
)
if (!hasEventWithKeyModifier) return
const conflictedEvents = findConflictedEvents(eventsInGroup)
conflictedEvents.forEach(e => {
context.report({
node: e.node,
loc: e.node.loc,
message: "Consider to use '.exact' modifier."
})
})
})
}
})
}
}
+73
View File
@@ -0,0 +1,73 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce `v-bind` directive style',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/v-bind-style.html'
},
fixable: 'code',
schema: [
{ enum: ['shorthand', 'longform'] }
]
},
create (context) {
const preferShorthand = context.options[0] !== 'longform'
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='bind'][key.argument!=null]" (node) {
const shorthandProp = node.key.name.rawName === '.'
const shorthand = node.key.name.rawName === ':' || shorthandProp
if (shorthand === preferShorthand) {
return
}
context.report({
node,
loc: node.loc,
message:
preferShorthand ? "Unexpected 'v-bind' before ':'."
: shorthandProp ? "Expected 'v-bind:' instead of '.'."
/* otherwise */ : "Expected 'v-bind' before ':'.",
* fix (fixer) {
if (preferShorthand) {
yield fixer.remove(node.key.name)
} else {
yield fixer.insertTextBefore(node, 'v-bind')
if (shorthandProp) {
// Replace `.` by `:`.
yield fixer.replaceText(node.key.name, ':')
// Insert `.prop` modifier if it doesn't exist.
const modifier = node.key.modifiers[0]
const isAutoGeneratedPropModifier = modifier.name === 'prop' && modifier.rawName === ''
if (isAutoGeneratedPropModifier) {
yield fixer.insertTextBefore(modifier, '.prop')
}
}
}
}
})
}
})
}
}
+60
View File
@@ -0,0 +1,60 @@
/**
* @author Niklas Higi
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce or forbid parentheses after method calls without arguments in `v-on` directives',
category: undefined,
url: 'https://eslint.vuejs.org/rules/v-on-function-call.html'
},
fixable: 'code',
schema: [
{ enum: ['always', 'never'] }
]
},
create (context) {
const always = context.options[0] === 'always'
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer > Identifier" (node) {
if (!always) return
context.report({
node,
loc: node.loc,
message: "Method calls inside of 'v-on' directives must have parentheses."
})
},
"VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression > ExpressionStatement > *" (node) {
if (!always && node.type === 'CallExpression' && node.arguments.length === 0) {
context.report({
node,
loc: node.loc,
message: "Method calls without arguments inside of 'v-on' directives must not have parentheses.",
fix: fixer => {
const nodeString = context.getSourceCode().getText().substring(node.range[0], node.range[1])
// This ensures that parens are also removed if they contain whitespace
const parensLength = nodeString.match(/\(\s*\)\s*$/)[0].length
return fixer.removeRange([node.end - parensLength, node.end])
}
})
}
}
})
}
}
+56
View File
@@ -0,0 +1,56 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce `v-on` directive style',
category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/v-on-style.html'
},
fixable: 'code',
schema: [
{ enum: ['shorthand', 'longform'] }
]
},
create (context) {
const preferShorthand = context.options[0] !== 'longform'
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='on'][key.argument!=null]" (node) {
const shorthand = node.key.name.rawName === '@'
if (shorthand === preferShorthand) {
return
}
const pos = node.range[0]
context.report({
node,
loc: node.loc,
message: preferShorthand
? "Expected '@' instead of 'v-on:'."
: "Expected 'v-on:' instead of '@'.",
fix: (fixer) => preferShorthand
? fixer.replaceTextRange([pos, pos + 5], '@')
: fixer.replaceTextRange([pos, pos + 1], 'v-on:')
})
}
})
}
}
+148
View File
@@ -0,0 +1,148 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
'use strict'
const { pascalCase } = require('../utils/casing')
const utils = require('../utils')
/**
* @typedef {Object} Options
* @property {"shorthand" | "longform" | "v-slot"} atComponent The style for the default slot at a custom component directly.
* @property {"shorthand" | "longform" | "v-slot"} default The style for the default slot at a template wrapper.
* @property {"shorthand" | "longform"} named The style for named slots at a template wrapper.
*/
/**
* Normalize options.
* @param {any} options The raw options to normalize.
* @returns {Options} The normalized options.
*/
function normalizeOptions (options) {
const normalized = {
atComponent: 'v-slot',
default: 'shorthand',
named: 'shorthand'
}
if (typeof options === 'string') {
normalized.atComponent = normalized.default = normalized.named = options
} else if (options != null) {
for (const key of ['atComponent', 'default', 'named']) {
if (options[key] != null) {
normalized[key] = options[key]
}
}
}
return normalized
}
/**
* Get the expected style.
* @param {Options} options The options that defined expected types.
* @param {VAttribute} node The `v-slot` node to check.
* @returns {"shorthand" | "longform" | "v-slot"} The expected style.
*/
function getExpectedStyle (options, node) {
const { argument } = node.key
if (argument == null || (argument.type === 'VIdentifier' && argument.name === 'default')) {
const element = node.parent.parent
return element.name === 'template' ? options.default : options.atComponent
}
return options.named
}
/**
* Get the expected style.
* @param {VAttribute} node The `v-slot` node to check.
* @returns {"shorthand" | "longform" | "v-slot"} The expected style.
*/
function getActualStyle (node) {
const { name, argument } = node.key
if (name.rawName === '#') {
return 'shorthand'
}
if (argument != null) {
return 'longform'
}
return 'v-slot'
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce `v-slot` directive style',
category: undefined, // strongly-recommended
// TODO Change with major version.
// category: 'strongly-recommended',
url: 'https://eslint.vuejs.org/rules/v-slot-style.html'
},
fixable: 'code',
schema: [
{
anyOf: [
{ enum: ['shorthand', 'longform'] },
{
type: 'object',
properties: {
atComponent: { enum: ['shorthand', 'longform', 'v-slot'] },
default: { enum: ['shorthand', 'longform', 'v-slot'] },
named: { enum: ['shorthand', 'longform'] }
},
additionalProperties: false
}
]
}
],
messages: {
expectedShorthand: "Expected '#{{argument}}' instead of '{{actual}}'.",
expectedLongform: "Expected 'v-slot:{{argument}}' instead of '{{actual}}'.",
expectedVSlot: "Expected 'v-slot' instead of '{{actual}}'."
}
},
create (context) {
const sourceCode = context.getSourceCode()
const options = normalizeOptions(context.options[0])
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='slot']" (node) {
const expected = getExpectedStyle(options, node)
const actual = getActualStyle(node)
if (actual === expected) {
return
}
const { name, argument } = node.key
const range = [name.range[0], (argument || name).range[1]]
const argumentText = argument ? sourceCode.getText(argument) : 'default'
context.report({
node,
messageId: `expected${pascalCase(expected)}`,
data: {
actual: sourceCode.text.slice(range[0], range[1]),
argument: argumentText
},
fix (fixer) {
switch (expected) {
case 'shorthand':
return fixer.replaceTextRange(range, `#${argumentText}`)
case 'longform':
return fixer.replaceTextRange(range, `v-slot:${argumentText}`)
case 'v-slot':
return fixer.replaceTextRange(range, 'v-slot')
default:
return null
}
}
})
}
})
}
}
+112
View File
@@ -0,0 +1,112 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid template root',
category: 'essential',
url: 'https://eslint.vuejs.org/rules/valid-template-root.html'
},
fixable: null,
schema: []
},
create (context) {
const sourceCode = context.getSourceCode()
return {
Program (program) {
const element = program.templateBody
if (element == null) {
return
}
const hasSrc = utils.hasAttribute(element, 'src')
const rootElements = []
let extraText = null
let extraElement = null
let vIf = false
for (const child of element.children) {
if (child.type === 'VElement') {
if (rootElements.length === 0 && !hasSrc) {
rootElements.push(child)
vIf = utils.hasDirective(child, 'if')
} else if (vIf && utils.hasDirective(child, 'else-if')) {
rootElements.push(child)
} else if (vIf && utils.hasDirective(child, 'else')) {
rootElements.push(child)
vIf = false
} else {
extraElement = child
}
} else if (sourceCode.getText(child).trim() !== '') {
extraText = child
}
}
if (hasSrc && (extraText != null || extraElement != null)) {
context.report({
node: extraText || extraElement,
loc: (extraText || extraElement).loc,
message: "The template root with 'src' attribute is required to be empty."
})
} else if (extraText != null) {
context.report({
node: extraText,
loc: extraText.loc,
message: 'The template root requires an element rather than texts.'
})
} else if (extraElement != null) {
context.report({
node: extraElement,
loc: extraElement.loc,
message: 'The template root requires exactly one element.'
})
} else if (rootElements.length === 0 && !hasSrc) {
context.report({
node: element,
loc: element.loc,
message: 'The template root requires exactly one element.'
})
} else {
for (const element of rootElements) {
const tag = element.startTag
const name = element.name
if (name === 'template' || name === 'slot') {
context.report({
node: tag,
loc: tag.loc,
message: "The template root disallows '<{{name}}>' elements.",
data: { name }
})
}
if (utils.hasDirective(element, 'for')) {
context.report({
node: tag,
loc: tag.loc,
message: "The template root disallows 'v-for' directives."
})
}
}
}
}
}
}
}
+115
View File
@@ -0,0 +1,115 @@
/**
* @fileoverview enforce valid `.sync` modifier on `v-bind` directives
* @author Yosuke Ota
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Check whether the given node is valid or not.
* @param {ASTNode} node The element node to check.
* @returns {boolean} `true` if the node is valid.
*/
function isValidElement (node) {
if (
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
utils.isHtmlWellKnownElementName(node.rawName) ||
utils.isSvgWellKnownElementName(node.rawName)
) {
// non Vue-component
return false
}
return true
}
/**
* Check whether the given node can be LHS.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node can be LHS.
*/
function isLhs (node) {
return Boolean(node) && (
node.type === 'Identifier' ||
node.type === 'MemberExpression'
)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `.sync` modifier on `v-bind` directives',
category: undefined,
// TODO Change with major version.
// category: 'essential',
url: 'https://eslint.vuejs.org/rules/valid-v-bind-sync.html'
},
fixable: null,
schema: [],
messages: {
unexpectedInvalidElement: "'.sync' modifiers aren't supported on <{{name}}> non Vue-components.",
unexpectedNonLhsExpression: "'.sync' modifiers require the attribute value which is valid as LHS.",
unexpectedUpdateIterationVariable: "'.sync' modifiers cannot update the iteration variable '{{varName}}' itself."
}
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='bind']" (node) {
if (!node.key.modifiers.map(mod => mod.name).includes('sync')) {
return
}
const element = node.parent.parent
const name = element.name
if (!isValidElement(element)) {
context.report({
node,
loc: node.loc,
messageId: 'unexpectedInvalidElement',
data: { name }
})
}
if (node.value) {
if (!isLhs(node.value.expression)) {
context.report({
node,
loc: node.loc,
messageId: 'unexpectedNonLhsExpression'
})
}
for (const reference of node.value.references) {
const id = reference.id
if (id.parent.type !== 'VExpressionContainer') {
continue
}
const variable = reference.variable
if (variable) {
context.report({
node,
loc: node.loc,
messageId: 'unexpectedUpdateIterationVariable',
data: { varName: id.name }
})
}
}
}
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More