node_modules

This commit is contained in:
saeid
2019-07-15 13:07:31 +04:30
parent d3735c6d30
commit ce1a3ebf9c
35713 changed files with 3639219 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.
+293
View File
@@ -0,0 +1,293 @@
# 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://circleci.com/gh/vuejs/eslint-plugin-vue.svg?style=svg)](https://circleci.com/gh/vuejs/eslint-plugin-vue)
> Official ESLint plugin for Vue.js
## :art: Playground on the Web
You can try this plugin on the Web.
- https://mysticatea.github.io/vue-eslint-demo/
## :grey_exclamation: Requirements
- [ESLint](http://eslint.org/) `>=3.18.0`.
- `>=4.7.0` to use `eslint --fix`.
- `>=4.14.0` to use with `babel-eslint`.
- Node.js `>=4.0.0`
## :cd: Installation
```bash
npm install --save-dev eslint eslint-plugin-vue
```
## :rocket: Usage
Create `.eslintrc.*` file to configure rules. See also: [http://eslint.org/docs/user-guide/configuring](http://eslint.org/docs/user-guide/configuring).
Example **.eslintrc.js**:
```js
module.exports = {
extends: [
// add more generic rulesets here, such as:
// 'eslint:recommended',
'plugin:vue/essential'
],
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
}
}
```
### Single File Components
ESLint only targets `.js` files by default. You must include the `.vue` extension using [the `--ext` option](https://eslint.org/docs/user-guide/configuring#specifying-file-extensions-to-lint) or a glob pattern.
Examples:
```bash
eslint --ext .js,.vue src
eslint src/**/*.{js,vue}
```
### Attention
All component-related rules are being applied to code that passes any of the following checks:
* `Vue.component()` expression
* `Vue.extend()` expression
* `Vue.mixin()` expression
* `export default {}` in `.vue` or `.jsx` file
If you however want to take advantage of our rules in any of your custom objects that are Vue components, you might need to use special comment `// @vue/component` that marks object in the next line as a Vue component in any file, e.g.:
```js
// @vue/component
const CustomComponent = {
name: 'custom-component',
template: '<div></div>'
}
```
```js
Vue.component('AsyncComponent', (resolve, reject) => {
setTimeout(() => {
// @vue/component
resolve({
name: 'async-component',
template: '<div></div>'
})
}, 500)
})
```
### `eslint-disable` functionality in `<template>`
You can use `<!-- eslint-disable -->`-like HTML comments in `<template>` of `.vue` files. For example:
```html
<template>
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
<div a="1" b="2" c="3" d="4">
</div>
</template>
```
If you want to disallow `eslint-disable` functionality, please disable [vue/comment-directive](./docs/rules/comment-directive.md) rule.
## :gear: Configs
This plugin provides four predefined configs:
- `plugin:vue/base` - Settings and rules to enable correct ESLint parsing
- `plugin:vue/essential` - Above, plus rules to prevent errors or unintended behavior
- `plugin:vue/strongly-recommended` - Above, plus rules to considerably improve code readability and/or dev experience
- `plugin:vue/recommended` - Above, plus rules to enforce subjective community defaults to ensure consistency
## :bulb: Rules
Rules are grouped by priority to help you understand their purpose. The `--fix` option on the command line automatically fixes problems reported by rules which have a wrench :wrench: below.
<!--RULES_TABLE_START-->
### Base Rules (Enabling Correct ESLint Parsing)
Enforce all the rules in this category, as well as all higher priority rules, with:
```json
{
"extends": "plugin:vue/base"
}
```
| | Rule ID | Description |
|:---|:--------|:------------|
| | [vue/comment-directive](./docs/rules/comment-directive.md) | support comment-directives in `<template>` |
| | [vue/jsx-uses-vars](./docs/rules/jsx-uses-vars.md) | prevent variables used in JSX to be marked as unused |
### Priority A: Essential (Error Prevention)
Enforce all the rules in this category, as well as all higher priority rules, with:
```json
{
"extends": "plugin:vue/essential"
}
```
| | Rule ID | Description |
|:---|:--------|:------------|
| | [vue/no-async-in-computed-properties](./docs/rules/no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties |
| | [vue/no-dupe-keys](./docs/rules/no-dupe-keys.md) | disallow duplication of field names |
| | [vue/no-duplicate-attributes](./docs/rules/no-duplicate-attributes.md) | disallow duplication of attributes |
| | [vue/no-parsing-error](./docs/rules/no-parsing-error.md) | disallow parsing errors in `<template>` |
| | [vue/no-reserved-keys](./docs/rules/no-reserved-keys.md) | disallow overwriting reserved keys |
| :wrench: | [vue/no-shared-component-data](./docs/rules/no-shared-component-data.md) | enforce component's data property to be a function |
| | [vue/no-side-effects-in-computed-properties](./docs/rules/no-side-effects-in-computed-properties.md) | disallow side effects in computed properties |
| | [vue/no-template-key](./docs/rules/no-template-key.md) | disallow `key` attribute on `<template>` |
| | [vue/no-textarea-mustache](./docs/rules/no-textarea-mustache.md) | disallow mustaches in `<textarea>` |
| | [vue/no-unused-vars](./docs/rules/no-unused-vars.md) | disallow unused variable definitions of v-for directives or scope attributes |
| | [vue/require-component-is](./docs/rules/require-component-is.md) | require `v-bind:is` of `<component>` elements |
| | [vue/require-render-return](./docs/rules/require-render-return.md) | enforce render function to always return value |
| | [vue/require-v-for-key](./docs/rules/require-v-for-key.md) | require `v-bind:key` with `v-for` directives |
| | [vue/require-valid-default-prop](./docs/rules/require-valid-default-prop.md) | enforce props default values to be valid |
| | [vue/return-in-computed-property](./docs/rules/return-in-computed-property.md) | enforce that a return statement is present in computed property |
| | [vue/valid-template-root](./docs/rules/valid-template-root.md) | enforce valid template root |
| | [vue/valid-v-bind](./docs/rules/valid-v-bind.md) | enforce valid `v-bind` directives |
| | [vue/valid-v-cloak](./docs/rules/valid-v-cloak.md) | enforce valid `v-cloak` directives |
| | [vue/valid-v-else-if](./docs/rules/valid-v-else-if.md) | enforce valid `v-else-if` directives |
| | [vue/valid-v-else](./docs/rules/valid-v-else.md) | enforce valid `v-else` directives |
| | [vue/valid-v-for](./docs/rules/valid-v-for.md) | enforce valid `v-for` directives |
| | [vue/valid-v-html](./docs/rules/valid-v-html.md) | enforce valid `v-html` directives |
| | [vue/valid-v-if](./docs/rules/valid-v-if.md) | enforce valid `v-if` directives |
| | [vue/valid-v-model](./docs/rules/valid-v-model.md) | enforce valid `v-model` directives |
| | [vue/valid-v-on](./docs/rules/valid-v-on.md) | enforce valid `v-on` directives |
| | [vue/valid-v-once](./docs/rules/valid-v-once.md) | enforce valid `v-once` directives |
| | [vue/valid-v-pre](./docs/rules/valid-v-pre.md) | enforce valid `v-pre` directives |
| | [vue/valid-v-show](./docs/rules/valid-v-show.md) | enforce valid `v-show` directives |
| | [vue/valid-v-text](./docs/rules/valid-v-text.md) | enforce valid `v-text` directives |
### Priority B: Strongly Recommended (Improving Readability)
Enforce all the rules in this category, as well as all higher priority rules, with:
```json
{
"extends": "plugin:vue/strongly-recommended"
}
```
| | Rule ID | Description |
|:---|:--------|:------------|
| :wrench: | [vue/attribute-hyphenation](./docs/rules/attribute-hyphenation.md) | enforce attribute naming style on custom components in template |
| :wrench: | [vue/html-end-tags](./docs/rules/html-end-tags.md) | enforce end tag style |
| :wrench: | [vue/html-indent](./docs/rules/html-indent.md) | enforce consistent indentation in `<template>` |
| :wrench: | [vue/html-self-closing](./docs/rules/html-self-closing.md) | enforce self-closing style |
| :wrench: | [vue/max-attributes-per-line](./docs/rules/max-attributes-per-line.md) | enforce the maximum number of attributes per line |
| :wrench: | [vue/mustache-interpolation-spacing](./docs/rules/mustache-interpolation-spacing.md) | enforce unified spacing in mustache interpolations |
| :wrench: | [vue/name-property-casing](./docs/rules/name-property-casing.md) | enforce specific casing for the name property in Vue components |
| :wrench: | [vue/no-multi-spaces](./docs/rules/no-multi-spaces.md) | disallow multiple spaces |
| | [vue/require-default-prop](./docs/rules/require-default-prop.md) | require default value for props |
| | [vue/require-prop-types](./docs/rules/require-prop-types.md) | require type definitions in props |
| :wrench: | [vue/v-bind-style](./docs/rules/v-bind-style.md) | enforce `v-bind` directive style |
| :wrench: | [vue/v-on-style](./docs/rules/v-on-style.md) | enforce `v-on` directive style |
### Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
Enforce all the rules in this category, as well as all higher priority rules, with:
```json
{
"extends": "plugin:vue/recommended"
}
```
| | Rule ID | Description |
|:---|:--------|:------------|
| :wrench: | [vue/attributes-order](./docs/rules/attributes-order.md) | enforce order of attributes |
| :wrench: | [vue/html-quotes](./docs/rules/html-quotes.md) | enforce quotes style of HTML attributes |
| | [vue/no-confusing-v-for-v-if](./docs/rules/no-confusing-v-for-v-if.md) | disallow confusing `v-for` and `v-if` on the same element |
| :wrench: | [vue/order-in-components](./docs/rules/order-in-components.md) | enforce order of properties in components |
| | [vue/this-in-template](./docs/rules/this-in-template.md) | enforce usage of `this` in template |
### Uncategorized
| | Rule ID | Description |
|:---|:--------|:------------|
| :wrench: | [vue/html-closing-bracket-newline](./docs/rules/html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets |
| :wrench: | [vue/html-closing-bracket-spacing](./docs/rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets |
| | [vue/no-use-v-if-with-v-for](./docs/rules/no-use-v-if-with-v-for.md) | disallow use v-if on the same element as v-for |
| | [vue/no-v-html](./docs/rules/no-v-html.md) | disallow use of v-html to prevent XSS attack |
| :wrench: | [vue/prop-name-casing](./docs/rules/prop-name-casing.md) | enforce specific casing for the Prop name in Vue components |
| :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `<script>` |
<!--RULES_TABLE_END-->
## :couple: FAQ
### What is the "Use the latest vue-eslint-parser" error?
The most rules of `eslint-plugin-vue` require `vue-eslint-parser` to check `<template>` ASTs.
Make sure you have one of the following settings in your **.eslintrc**:
- `"extends": ["plugin:vue/recommended"]`
- `"extends": ["plugin:vue/base"]`
If you already use other parser (e.g. `"parser": "babel-eslint"`), please move it into `parserOptions`, so it doesn't collide with the `vue-eslint-parser` used by this plugin's configuration:
```diff
- "parser": "babel-eslint",
"parserOptions": {
+ "parser": "babel-eslint",
"ecmaVersion": 2017,
"sourceType": "module"
}
```
The `vue-eslint-parser` uses the parser which is set by `parserOptions.parser` to parse scripts.
### Why doesn't it work on .vue file?
1. Make sure you don't have `eslint-plugin-html` in your config. The `eslint-plugin-html` extracts the content from `<script>` tags, but `eslint-plugin-vue` requires `<script>` tags and `<template>` tags in order to distinguish template and script in single file components.
```diff
"plugins": [
"vue",
- "html"
]
```
2. Make sure your tool is set to lint `.vue` files.
- CLI targets only `.js` files by default. You have to specify additional extensions by `--ext` option or glob patterns. E.g. `eslint "src/**/*.{js,vue}"` or `eslint src --ext .vue`.
- VSCode targets only JavaScript or HTML files by default. You have to add `"vue"` to the `"eslint.validate"` array in vscode settings. e.g. `"eslint.validate": [ "javascript", "javascriptreact", "vue" ]`
## :anchor: Semantic Versioning Policy
This plugin follows [semantic versioning](http://semver.org/) and [ESLint's Semantic Versioning Policy](https://github.com/eslint/eslint#semantic-versioning-policy).
## :newspaper: Changelog
We're using [GitHub Releases](https://github.com/vuejs/eslint-plugin-vue/releases).
## :beers: Contribution guide
In order to add a new rule, you should:
- Create issue on GH with description of proposed rule
- Generate a new rule using the [official yeoman generator](https://github.com/eslint/generator-eslint)
- Run `npm start`
- Write test scenarios & implement logic
- Describe the rule in the generated `docs` file
- Make sure all tests are passing
- Run `npm run update` in order to update readme and recommended configuration
- Create PR and link created issue in description
We're more than happy to see potential contributions, so don't hesitate. If you have any suggestions, ideas or problems feel free to add new [issue](https://github.com/vuejs/eslint-plugin-vue/issues), but first please make sure your question does not repeat previous ones.
## :lock: License
See the [LICENSE](LICENSE) file for license rights and limitations (MIT).
+28
View File
@@ -0,0 +1,28 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update it's content execute "npm run update"
*/
module.exports = {
root: true,
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2015,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
experimentalObjectRestSpread: true
}
},
env: {
browser: true,
es6: true
},
plugins: [
'vue'
],
rules: {
'vue/comment-directive': 'error',
'vue/jsx-uses-vars': 'error'
}
}
+39
View File
@@ -0,0 +1,39 @@
/*
* 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-vars': 'error',
'vue/require-component-is': '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/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'
}
}
+15
View File
@@ -0,0 +1,15 @@
/*
* 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': 'error',
'vue/html-quotes': 'error',
'vue/no-confusing-v-for-v-if': 'error',
'vue/order-in-components': 'error',
'vue/this-in-template': 'error'
}
}
+22
View File
@@ -0,0 +1,22 @@
/*
* 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': 'error',
'vue/html-end-tags': 'error',
'vue/html-indent': 'error',
'vue/html-self-closing': 'error',
'vue/max-attributes-per-line': 'error',
'vue/mustache-interpolation-spacing': 'error',
'vue/name-property-casing': 'error',
'vue/no-multi-spaces': 'error',
'vue/require-default-prop': 'error',
'vue/require-prop-types': 'error',
'vue/v-bind-style': 'error',
'vue/v-on-style': 'error'
}
}
+74
View File
@@ -0,0 +1,74 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update it's content execute "npm run update"
*/
'use strict'
module.exports = {
rules: {
'attribute-hyphenation': require('./rules/attribute-hyphenation'),
'attributes-order': require('./rules/attributes-order'),
'comment-directive': require('./rules/comment-directive'),
'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'),
'max-attributes-per-line': require('./rules/max-attributes-per-line'),
'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-confusing-v-for-v-if': require('./rules/no-confusing-v-for-v-if'),
'no-dupe-keys': require('./rules/no-dupe-keys'),
'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
'no-multi-spaces': require('./rules/no-multi-spaces'),
'no-parsing-error': require('./rules/no-parsing-error'),
'no-reserved-keys': require('./rules/no-reserved-keys'),
'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-template-key': require('./rules/no-template-key'),
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
'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'),
'order-in-components': require('./rules/order-in-components'),
'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-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'),
'this-in-template': require('./rules/this-in-template'),
'v-bind-style': require('./rules/v-bind-style'),
'v-on-style': require('./rules/v-on-style'),
'valid-template-root': require('./rules/valid-template-root'),
'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-text': require('./rules/valid-v-text')
},
configs: {
'base': require('./configs/base'),
'essential': require('./configs/essential'),
'strongly-recommended': require('./configs/strongly-recommended'),
'recommended': require('./configs/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
}
+101
View File
@@ -0,0 +1,101 @@
/**
* @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: {
docs: {
description: 'enforce attribute naming style on custom components in template',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/attribute-hyphenation.md'
},
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 === 'bind' ? node.key.raw.argument : false
if (!name || isIgnoredAttribute(name)) return
reportIssue(node, name)
}
})
}
}
+129
View File
@@ -0,0 +1,129 @@
/**
* @fileoverview enforce ordering of attributes
* @author Erin Depew
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function getAttributeType (name, isDirective) {
if (isDirective) {
if (name === 'for') {
return 'LIST_RENDERING'
} else if (name === 'if' || name === 'else-if' || name === 'else' || name === 'show' || name === 'cloak') {
return 'CONDITIONALS'
} else if (name === 'pre' || name === 'once') {
return 'RENDER_MODIFIERS'
} else if (name === 'model' || name === 'bind') {
return 'BINDING'
} else if (name === 'on') {
return 'EVENTS'
} else if (name === 'html' || name === 'text') {
return 'CONTENT'
}
} else {
if (name === 'is') {
return 'DEFINITION'
} else if (name === 'id') {
return 'GLOBAL'
} else if (name === 'ref' || name === 'key' || name === 'slot' || name === 'slot-scope') {
return 'UNIQUE'
} else {
return 'OTHER_ATTR'
}
}
}
function getPosition (attribute, attributeOrder) {
const attributeType = getAttributeType(attribute.key.name, attribute.directive)
return attributeOrder.indexOf(attributeType)
}
function create (context) {
const sourceCode = context.getSourceCode()
let attributeOrder = ['DEFINITION', 'LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'OTHER_ATTR', 'EVENTS', 'CONTENT']
if (context.options[0] && context.options[0].order) {
attributeOrder = context.options[0].order
}
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)
// If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by:
// return shiftAttrs.map((attr, i) => {
// const text = attr === previousNode ? sourceCode.getText(node) : sourceCode.getText(shiftAttrs[i - 1])
// return fixer.replaceText(attr, text)
// })
const replaceDataList = shiftAttrs.map((attr, i) => {
const text = attr === previousNode ? sourceCode.getText(node) : sourceCode.getText(shiftAttrs[i - 1])
return {
range: attr.range,
text
}
})
const replaceRange = [previousNode.range[0], node.range[1]]
let text = sourceCode.text.slice(replaceRange[0], replaceRange[1])
replaceDataList.reverse().forEach((data) => {
const textRange = data.range.map(r => r - replaceRange[0])
text = text.slice(0, textRange[0]) + data.text + text.slice(textRange[1], text.length)
})
return fixer.replaceTextRange(replaceRange, text)
}
})
}
return utils.defineTemplateBodyVisitor(context, {
'VStartTag' () {
currentPosition = -1
previousNode = null
},
'VAttribute' (node) {
if ((currentPosition === -1) || (currentPosition <= getPosition(node, attributeOrder))) {
currentPosition = getPosition(node, attributeOrder)
previousNode = node
} else {
reportIssue(node, previousNode)
}
}
})
}
module.exports = {
meta: {
docs: {
description: 'enforce order of attributes',
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.5.0/docs/rules/attributes-order.md'
},
fixable: 'code',
schema: {
type: 'array',
properties: {
order: {
items: {
type: 'string'
},
maxItems: 10,
minItems: 10
}
}
}
},
create
}
+138
View File
@@ -0,0 +1,138 @@
/**
* @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: {
docs: {
description: 'support comment-directives in `<template>`',
category: 'base',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/comment-directive.md'
},
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,86 @@
/**
* @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: {
docs: {
description: "require or disallow a line break before tag's closing brackets",
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-closing-bracket-newline.md'
},
fixable: 'whitespace',
schema: [{
type: 'object',
properties: {
'singleline': { enum: ['always', 'never'] },
'multiline': { enum: ['always', 'never'] }
},
additionalProperties: false
}]
},
create (context) {
const options = 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,113 @@
/**
* @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: {
docs: {
description: 'require or disallow a space before tag\'s closing brackets',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-closing-bracket-spacing.md'
},
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]])
})
}
}
})
}
}
+59
View File
@@ -0,0 +1,59 @@
/**
* @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: {
docs: {
description: 'enforce end tag style',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-end-tags.md'
},
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)
}
})
}
}
+66
View File
@@ -0,0 +1,66 @@
/**
* @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: {
docs: {
description: 'enforce consistent indentation in `<template>`',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-indent.md'
},
fixable: 'whitespace',
schema: [
{
anyOf: [
{ type: 'integer', minimum: 1 },
{ enum: ['tab'] }
]
},
{
type: 'object',
properties: {
'attribute': { 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
}
]
}
}
+69
View File
@@ -0,0 +1,69 @@
/**
* @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: {
docs: {
description: 'enforce quotes style of HTML attributes',
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-quotes.md'
},
fixable: 'code',
schema: [
{ enum: ['double', 'single'] }
]
},
create (context) {
const sourceCode = context.getSourceCode()
const double = context.options[0] !== 'single'
const quoteChar = double ? '"' : "'"
const quoteName = double ? 'double quotes' : 'single quotes'
const quotePattern = double ? /"/g : /'/g
const quoteEscaped = double ? '&quot;' : '&apos;'
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) {
context.report({
node: node.value,
loc: node.value.loc,
message: 'Expected to be enclosed by {{kind}}.',
data: { kind: quoteName },
fix (fixer) {
const contentText = (firstChar === "'" || firstChar === '"') ? text.slice(1, -1) : text
const replacement = quoteChar + contentText.replace(quotePattern, quoteEscaped) + quoteChar
return fixer.replaceText(node.value, replacement)
}
})
}
}
}, {
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
})
}
}
+179
View File
@@ -0,0 +1,179 @@
/**
* @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: {
docs: {
description: 'enforce self-closing style',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/html-self-closing.md'
},
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, '>')
}
return fixer.replaceText(close, `></${node.rawName}>`)
}
})
}
}
}, {
Program (node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
})
}
}
+69
View File
@@ -0,0 +1,69 @@
// 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: {
docs: {
description: 'prevent variables used in JSX to be marked as unused', // eslint-disable-line consistent-docs-description
category: 'base',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/jsx-uses-vars.md'
},
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)
}
}
}
}
+160
View File
@@ -0,0 +1,160 @@
/**
* @fileoverview Define the number of attributes allows per line
* @author Filipa Lacerda
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const utils = require('../utils')
module.exports = {
meta: {
docs: {
description: 'enforce the maximum number of attributes per line',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/max-attributes-per-line.md'
},
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 configuration = parseOptions(context.options[0])
const multilineMaximum = configuration.multiline
const singlelinemMaximum = configuration.singleline
const canHaveFirstLine = configuration.allowFirstLine
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), node)
}
if (!utils.isSingleLine(node)) {
if (!canHaveFirstLine && node.attributes[0].loc.start.line === node.loc.start.line) {
showErrors([node.attributes[0]], node)
}
groupAttrsByLine(node.attributes)
.filter(attrs => attrs.length > multilineMaximum)
.forEach(attrs => showErrors(attrs.splice(multilineMaximum), node))
}
}
})
// ----------------------------------------------------------------------
// 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, node) {
attributes.forEach((prop, i) => {
context.report({
node: prop,
loc: prop.loc,
message: 'Attribute "{{propName}}" should be on a new line.',
data: {
propName: prop.key.name
},
fix: i === 0 ? (fixer) => fixer.insertTextBefore(prop, '\n') : undefined
})
})
}
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
}
}
}
@@ -0,0 +1,99 @@
/**
* @fileoverview enforce unified spacing in mustache interpolations.
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce unified spacing in mustache interpolations',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/mustache-interpolation-spacing.md'
},
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]])
})
}
}
}
})
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* @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: {
docs: {
description: 'enforce specific casing for the name property in Vue components',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/name-property-casing.md'
},
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,156 @@
/**
* @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: {
docs: {
description: 'disallow asynchronous actions in computed properties',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-async-in-computed-properties.md'
},
fixable: null,
schema: []
},
create (context) {
const forbiddenNodes = []
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'
})
}
}
return Object.assign({},
{
FunctionDeclaration: onFunctionEnter,
FunctionExpression: onFunctionEnter,
ArrowFunctionExpression: onFunctionEnter,
NewExpression (node) {
if (node.callee.name === 'Promise') {
forbiddenNodes.push({
node: node,
type: 'new'
})
}
},
CallExpression (node) {
if (isPromise(node)) {
forbiddenNodes.push({
node: node,
type: 'promise'
})
}
if (isTimedFunction(node)) {
forbiddenNodes.push({
node: node,
type: 'timed'
})
}
},
AwaitExpression (node) {
forbiddenNodes.push({
node: node,
type: 'await'
})
}
},
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
) {
context.report({
node: el.node,
message: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
data: {
expressionName: expressionTypes[el.type],
propertyName: cp.key
}
})
}
})
})
})
)
}
}
+63
View File
@@ -0,0 +1,63 @@
/**
* @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: {
docs: {
description: 'disallow confusing `v-for` and `v-if` on the same element',
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-confusing-v-for-v-if.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.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."
})
}
}
})
}
}
+63
View File
@@ -0,0 +1,63 @@
/**
* @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: {
docs: {
description: 'disallow duplication of field names',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-dupe-keys.md'
},
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)
}
})
}
}
+104
View File
@@ -0,0 +1,104 @@
/**
* @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 === 'bind') {
return attribute.key.argument || null
}
return null
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow duplication of attributes',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-duplicate-attributes.md'
},
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)
}
}
})
}
}
+69
View File
@@ -0,0 +1,69 @@
/**
* @fileoverview This rule warns about the usage of extra whitespaces between attributes
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow multiple spaces',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-multi-spaces.md'
},
fixable: 'whitespace', // or "code" or "whitespace"
schema: []
},
/**
* @param {RuleContext} context - The rule context.
* @returns {Object} AST event handlers.
*/
create (context) {
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
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://github.com/vuejs/eslint-plugin-vue#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]
if (spaces > 1 && token.loc.start.line === prevToken.loc.start.line) {
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
}
}
}
}
}
+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'
// ------------------------------------------------------------------------------
// 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: {
docs: {
description: 'disallow parsing errors in `<template>`',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-parsing-error.md'
},
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
}
})
}
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
/**
* @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: {
docs: {
description: 'disallow overwriting reserved keys',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-reserved-keys.md'
},
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
}
})
}
}
})
}
}
+84
View File
@@ -0,0 +1,84 @@
/**
* @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: {
docs: {
description: "enforce component's data property to be a function",
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-shared-component-data.md'
},
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)
// If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by:
// return [
// fixer.insertTextBefore(tokens.first, 'function() {\nreturn '),
// fixer.insertTextAfter(tokens.last, ';\n}')
// ]
// See: https://eslint.org/blog/2017/06/eslint-v4.1.0-released#applying-multiple-autofixes-simultaneously
const range = [tokens.first.range[0], tokens.last.range[1]]
const valueText = sourceCode.text.slice(range[0], range[1])
const replacement = `function() {\nreturn ${valueText};\n}`
return fixer.replaceTextRange(range, replacement)
}
})
})
})
}
}
@@ -0,0 +1,73 @@
/**
* @fileoverview Don't introduce side effects in computed properties
* @author Michał Sajnóg
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow side effects in computed properties',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-side-effects-in-computed-properties.md'
},
fixable: null,
schema: []
},
create (context) {
const forbiddenNodes = []
return Object.assign({},
{
// this.xxx <=|+=|-=>
'AssignmentExpression' (node) {
if (node.left.type !== 'MemberExpression') return
if (utils.parseMemberExpression(node.left)[0] === 'this') {
forbiddenNodes.push(node)
}
},
// this.xxx <++|-->
'UpdateExpression > MemberExpression' (node) {
if (utils.parseMemberExpression(node)[0] === 'this') {
forbiddenNodes.push(node)
}
},
// 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)
}
}
},
utils.executeOnVue(context, (obj) => {
const computedProperties = utils.getComputedProperties(obj)
computedProperties.forEach(cp => {
forbiddenNodes.forEach(node => {
if (
cp.value &&
node.loc.start.line >= cp.value.loc.start.line &&
node.loc.end.line <= cp.value.loc.end.line
) {
context.report({
node: node,
message: 'Unexpected side effect in "{{key}}" computed property.',
data: { key: cp.key }
})
}
})
})
})
)
}
}
+42
View File
@@ -0,0 +1,42 @@
/**
* @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: {
docs: {
description: 'disallow `key` attribute on `<template>`',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-template-key.md'
},
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."
})
}
}
})
}
}
+44
View File
@@ -0,0 +1,44 @@
/**
* @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: {
docs: {
description: 'disallow mustaches in `<textarea>`',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-textarea-mustache.md'
},
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."
})
}
})
}
}
+45
View File
@@ -0,0 +1,45 @@
/**
* @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: {
docs: {
description: 'disallow unused variable definitions of v-for directives or scope attributes',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-unused-vars.md'
},
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
})
}
}
})
}
}
+99
View File
@@ -0,0 +1,99 @@
/**
* @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.id.parent
}
}
return undefined
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow use v-if on the same element as v-for',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-use-v-if-with-v-for.md'
},
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='if']" (node) {
const element = node.parent.parent
if (utils.hasDirective(element, 'for')) {
if (isUsingIterationVar(node)) {
if (!allowUsingIterationVar) {
const vFor = getVForUsingIterationVar(node)
context.report({
node,
loc: node.loc,
message: "The '{{iteratorName}}' variable 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: vFor.right.name
}
})
}
} else {
context.report({
node,
loc: node.loc,
message: "This 'v-if' should be moved to the wrapper element."
})
}
}
}
})
}
}
+33
View File
@@ -0,0 +1,33 @@
/**
* @fileoverview Restrict or warn use of v-html to prevent XSS attack
* @author Nathan Zeplowitz
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definitionutilu
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'disallow use of v-html to prevent XSS attack',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/no-v-html.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='html']" (node) {
context.report({
node,
loc: node.loc,
message: "'v-html' directive can lead to XSS attack."
})
}
})
}
}
+222
View File
@@ -0,0 +1,222 @@
/**
* @fileoverview Keep order of properties in components
* @author Michał Sajnóg
*/
'use strict'
const utils = require('../utils')
const Traverser = require('eslint/lib/util/traverser')
const defaultOrder = [
'el',
'name',
'parent',
'functional',
['delimiters', 'comments'],
['components', 'directives', 'filters'],
'extends',
'mixins',
'inheritAttrs',
'model',
['props', 'propsData'],
'data',
'computed',
'watch',
'LIFECYCLE_HOOKS',
'methods',
['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
new Traverser().traverse(node, {
visitorKeys,
enter (node, parent) {
if (
node.type === 'FunctionExpression' ||
node.type === 'Identifier' ||
node.type === 'Literal' ||
// es2015
node.type === 'ArrowFunctionExpression' ||
node.type === 'TemplateElement'
) {
// no side effects node
this.skip()
} else if (
node.type !== 'Property' &&
node.type !== 'ObjectExpression' &&
node.type !== 'ArrayExpression' &&
(node.type !== 'UnaryExpression' || ['!', '~', '+', '-', 'typeof'].indexOf(node.operator) < 0) &&
(node.type !== 'BinaryExpression' || ALL_BINARY_OPERATORS.indexOf(node.operator) < 0) &&
(node.type !== 'LogicalExpression' || LOGICAL_OPERATORS.indexOf(node.operator) < 0) &&
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
this.break()
}
}
})
return result
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce order of properties in components',
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/order-in-components.md'
},
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))
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 comma = sourceCode.getTokenAfter(propertyNode)
const hasAfterComma = isComma(comma)
const codeStart = sourceCode.getTokenBefore(propertyNode).range[1] // to include comments
const codeEnd = hasAfterComma ? comma.range[1] : propertyNode.range[1]
const propertyCode = sourceCode.text.slice(codeStart, codeEnd) + (hasAfterComma ? '' : ',')
const insertTarget = sourceCode.getTokenBefore(firstUnorderedPropertyNode)
// If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by:
// return [
// fixer.removeRange([codeStart, codeEnd]),
// fixer.insertTextAfter(insertTarget, propertyCode)
// ]
const insertStart = insertTarget.range[1]
const newCode = propertyCode + sourceCode.text.slice(insertStart, codeStart)
return fixer.replaceTextRange([insertStart, codeEnd], newCode)
}
})
}
})
}
return utils.executeOnVue(context, (obj) => {
checkOrder(obj.properties, orderMap)
})
}
}
+105
View File
@@ -0,0 +1,105 @@
/**
* @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']
function canFixPropertyName (node, originalName) {
// Can not fix of computed property names & shorthand
if (node.computed || node.shorthand) {
return false
}
const key = node.key
// Can not fix of unknown types
if (key.type !== 'Literal' && key.type !== 'Identifier') {
return false
}
// Can fix of ASCII printable characters
return originalName.match(/[ -~]+/)
}
// ------------------------------------------------------------------------------
// 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 node = obj.properties.find(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'props' &&
(p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression')
)
if (!node) return
const items = node.value.type === 'ObjectExpression' ? node.value.properties : node.value.elements
for (const item of items) {
if (item.type !== 'Property') {
return
}
if (item.computed) {
if (item.key.type !== 'Literal') {
// TemplateLiteral | Identifier(variable) | Expression(s)
return
}
if (typeof item.key.value !== 'string') {
// (boolean | null | number | RegExp) Literal
return
}
}
const propName = item.key.type === 'Literal' ? item.key.value : item.key.name
const convertedName = converter(propName)
if (convertedName !== propName) {
context.report({
node: item,
message: 'Prop "{{name}}" is not in {{caseType}}.',
data: {
name: propName,
caseType: caseType
},
fix: canFixPropertyName(item, propName) ? fixer => {
return item.key.type === 'Literal'
? fixer.replaceText(item.key, item.key.raw.replace(item.key.value, convertedName))
: fixer.replaceText(item.key, convertedName)
} : undefined
})
}
}
})
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce specific casing for the Prop name in Vue components',
category: undefined, // 'strongly-recommended'
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.5.0/docs/rules/prop-name-casing.md'
},
fixable: 'code', // null or "code" or "whitespace"
schema: [
{
enum: allowedCaseOptions
}
]
},
create
}
+42
View File
@@ -0,0 +1,42 @@
/**
* @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: {
docs: {
description: 'require `v-bind:is` of `<component>` elements',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-component-is.md'
},
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."
})
}
}
})
}
}
+106
View File
@@ -0,0 +1,106 @@
/**
* @fileoverview Require default value for props
* @author Michał Sajnóg <msajnog93@gmail.com> (http://github.com/michalsnik)
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'require default value for props',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-default-prop.md'
},
fixable: null, // or "code" or "whitespace"
schema: []
},
create: function (context) {
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
/**
* Checks if the passed prop is required
* @param {Property} prop - Property AST node for a single prop
* @return {boolean}
*/
function propIsRequired (prop) {
const propRequiredNode = prop.value.properties
.find(p =>
p.type === 'Property' &&
p.key.name === 'required' &&
p.value.type === 'Literal' &&
p.value.value === true
)
return Boolean(propRequiredNode)
}
/**
* Checks if the passed prop has a default value
* @param {Property} prop - Property AST node for a single prop
* @return {boolean}
*/
function propHasDefault (prop) {
const propDefaultNode = prop.value.properties
.find(p =>
p.key &&
(p.key.name === 'default' || p.key.value === 'default')
)
return Boolean(propDefaultNode)
}
/**
* Finds all props that don't have a default value set
* @param {Property} propsNode - Vue component's "props" node
* @return {boolean}
*/
function findPropsWithoutDefaultValue (propsNode) {
return propsNode.value.properties
.filter(prop => prop.type === 'Property')
.filter(prop => {
if (prop.value.type !== 'ObjectExpression') {
return true
}
return !propIsRequired(prop) && !propHasDefault(prop)
})
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const propsNode = obj.properties
.find(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'props' &&
p.value.type === 'ObjectExpression'
)
if (!propsNode) return
const propsWithoutDefault = findPropsWithoutDefaultValue(propsNode)
propsWithoutDefault.forEach(prop => {
context.report({
node: prop,
message: `Prop '{{propName}}' requires default value to be set.`,
data: {
propName: prop.key.name
}
})
})
})
}
}
+96
View File
@@ -0,0 +1,96 @@
/**
* @fileoverview Prop definitions should be detailed
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'require type definitions in props',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-prop-types.md'
},
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 checkProperties (items) {
for (const cp of items) {
if (cp.type !== 'Property') {
return
}
let hasType = true
if (cp.value.type === 'ObjectExpression') { // foo: {
hasType = objectHasType(cp.value)
} else if (cp.value.type === 'ArrayExpression') { // foo: [
hasType = cp.value.elements.length > 0
} else if (cp.value.type === 'FunctionExpression' || cp.value.type === 'ArrowFunctionExpression') {
hasType = false
}
if (!hasType) {
context.report({
node: cp,
message: 'Prop "{{name}}" should define at least its type.',
data: {
name: cp.key.name
}
})
}
}
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const node = obj.properties
.find(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'props'
)
if (!node) return
if (node.value.type === 'ObjectExpression') {
checkProperties(node.value.properties)
}
if (node.value.type === 'ArrayExpression') {
context.report({
node,
message: 'Props should at least define their types.'
})
}
})
}
}
+53
View File
@@ -0,0 +1,53 @@
/**
* @fileoverview Enforces render function to always return value.
* @author Armano
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce render function to always return value',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-render-return.md'
},
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.'
})
}
})
})
)
}
}
+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: {
docs: {
description: 'require `v-bind:key` with `v-for` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-v-for-key.md'
},
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='for']" (node) {
checkKey(node.parent.parent)
}
})
}
}
+131
View File
@@ -0,0 +1,131 @@
/**
* @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: {
docs: {
description: 'enforce props default values to be valid',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/require-valid-default-prop.md'
},
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 = obj.properties.find(p =>
isPropertyIdentifier(p) &&
p.key.name === 'props' &&
p.value.type === 'ObjectExpression'
)
if (!props) return
const properties = props.value.properties.filter(p =>
isPropertyIdentifier(p) &&
p.value.type === 'ObjectExpression'
)
for (const prop of properties) {
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
context.report({
node: def,
message: "Type of the default value for '{{name}}' prop must be a {{types}}.",
data: {
name: prop.key.name,
types: Array.from(typeNames).join(' or ').toLowerCase()
}
})
}
})
}
}
@@ -0,0 +1,67 @@
/**
* @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: {
docs: {
description: 'enforce that a return statement is present in computed property',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/return-in-computed-property.md'
},
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
}
})
}
})
})
})
)
}
}
+57
View File
@@ -0,0 +1,57 @@
/**
* @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: {
docs: {
description: 'enforce consistent indentation in `<script>`',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/script-indent.md'
},
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(), {})
}
}
+101
View File
@@ -0,0 +1,101 @@
/**
* @fileoverview enforce 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: {
docs: {
description: 'enforce usage of `this` in template',
category: 'recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/this-in-template.md'
},
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.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'."
})
}
}
}
}
}
))
}
}
+53
View File
@@ -0,0 +1,53 @@
/**
* @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: {
docs: {
description: 'enforce `v-bind` directive style',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/v-bind-style.md'
},
fixable: 'code',
schema: [
{ enum: ['shorthand', 'longform'] }
]
},
create (context) {
const shorthand = context.options[0] !== 'longform'
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='bind'][key.argument!=null]" (node) {
if (node.key.shorthand === shorthand) {
return
}
context.report({
node,
loc: node.loc,
message: shorthand
? "Unexpected 'v-bind' before ':'."
: "Expected 'v-bind' before ':'.",
fix: (fixer) => shorthand
? fixer.removeRange([node.range[0], node.range[0] + 6])
: fixer.insertTextBefore(node, 'v-bind')
})
}
})
}
}
+54
View File
@@ -0,0 +1,54 @@
/**
* @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: {
docs: {
description: 'enforce `v-on` directive style',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/v-on-style.md'
},
fixable: 'code',
schema: [
{ enum: ['shorthand', 'longform'] }
]
},
create (context) {
const shorthand = context.options[0] !== 'longform'
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='on'][key.argument!=null]" (node) {
if (node.key.shorthand === shorthand) {
return
}
const pos = node.range[0]
context.report({
node,
loc: node.loc,
message: shorthand
? "Expected '@' instead of 'v-on:'."
: "Expected 'v-on:' instead of '@'.",
fix: (fixer) => shorthand
? fixer.replaceTextRange([pos, pos + 5], '@')
: fixer.replaceTextRange([pos, pos + 1], 'v-on:')
})
}
})
}
}
+111
View File
@@ -0,0 +1,111 @@
/**
* @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: {
docs: {
description: 'enforce valid template root',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-template-root.md'
},
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."
})
}
}
}
}
}
}
}
+59
View File
@@ -0,0 +1,59 @@
/**
* @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
// ------------------------------------------------------------------------------
const VALID_MODIFIERS = new Set(['prop', 'camel', 'sync'])
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-bind` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-bind.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='bind']" (node) {
for (const modifier of node.key.modifiers) {
if (!VALID_MODIFIERS.has(modifier)) {
context.report({
node,
loc: node.key.loc,
message: "'v-bind' directives don't support the modifier '{{name}}'.",
data: { name: modifier }
})
}
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-bind' directives require an attribute value."
})
}
}
})
}
}
+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: {
docs: {
description: 'enforce valid `v-cloak` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-cloak.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='cloak']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-cloak' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-cloak' directives require no modifier."
})
}
if (node.value) {
context.report({
node,
loc: node.loc,
message: "'v-cloak' directives require no attribute value."
})
}
}
})
}
}
+79
View File
@@ -0,0 +1,79 @@
/**
* @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: {
docs: {
description: 'enforce valid `v-else-if` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-else-if.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='else-if']" (node) {
const element = node.parent.parent
if (!utils.prevElementHasIf(element)) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."
})
}
if (utils.hasDirective(element, 'if')) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' and 'v-if' directives can't exist on the same element."
})
}
if (utils.hasDirective(element, 'else')) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' and 'v-else' directives can't exist on the same element."
})
}
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-else-if' directives require that attribute value."
})
}
}
})
}
}
+79
View File
@@ -0,0 +1,79 @@
/**
* @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: {
docs: {
description: 'enforce valid `v-else` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-else.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='else']" (node) {
const element = node.parent.parent
if (!utils.prevElementHasIf(element)) {
context.report({
node,
loc: node.loc,
message: "'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else' directive."
})
}
if (utils.hasDirective(element, 'if')) {
context.report({
node,
loc: node.loc,
message: "'v-else' and 'v-if' directives can't exist on the same element. You may want 'v-else-if' directives."
})
}
if (utils.hasDirective(element, 'else-if')) {
context.report({
node,
loc: node.loc,
message: "'v-else' and 'v-else-if' directives can't exist on the same element."
})
}
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-else' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-else' directives require no modifier."
})
}
if (utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-else' directives require no attribute value."
})
}
}
})
}
}
+191
View File
@@ -0,0 +1,191 @@
/**
* @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 attribute is using the variables which are defined by `v-for` directives.
* @param {ASTNode} vFor The attribute node of `v-for` to check.
* @param {ASTNode} vBindKey The attribute node of `v-bind:key` to check.
* @returns {boolean} `true` if the node is using the variables which are defined by `v-for` directives.
*/
function isUsingIterationVar (vFor, vBindKey) {
if (vBindKey.value == null) {
return false
}
const references = vBindKey.value.references
const variables = vFor.parent.parent.variables
return references.some(reference =>
variables.some(variable =>
variable.id.name === reference.id.name &&
variable.kind === 'v-for'
)
)
}
/**
* Check the child element in tempalte v-for about `v-bind:key` attributes.
* @param {RuleContext} context The rule context to report.
* @param {ASTNode} vFor The attribute node of `v-for` to check.
* @param {ASTNode} child The child node to check.
*/
function checkChildKey (context, vFor, child) {
const childFor = utils.getDirective(child, 'for')
// if child has v-for, check if parent iterator is used in v-for
if (childFor != null) {
const childForRefs = childFor.value.references
const variables = vFor.parent.parent.variables
const usedInFor = childForRefs.some(cref =>
variables.some(variable =>
cref.id.name === variable.id.name &&
variable.kind === 'v-for'
)
)
// if parent iterator is used, skip other checks
// iterator usage will be checked later by child v-for
if (usedInFor) {
return
}
}
// otherwise, check if parent iterator is directly used in child's key
checkKey(context, vFor, child)
}
/**
* Check the given element about `v-bind:key` attributes.
* @param {RuleContext} context The rule context to report.
* @param {ASTNode} vFor The attribute node of `v-for` to check.
* @param {ASTNode} element The element node to check.
*/
function checkKey (context, vFor, element) {
if (element.name === 'template') {
for (const child of element.children) {
if (child.type === 'VElement') {
checkChildKey(context, vFor, child)
}
}
return
}
const vBindKey = utils.getDirective(element, 'bind', 'key')
if (utils.isCustomComponent(element) && vBindKey == null) {
context.report({
node: element.startTag,
loc: element.startTag.loc,
message: "Custom elements in iteration require 'v-bind:key' directives."
})
}
if (vBindKey != null && !isUsingIterationVar(vFor, vBindKey)) {
context.report({
node: vBindKey,
loc: vBindKey.loc,
message: "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."
})
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-for` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-for.md'
},
fixable: null,
schema: []
},
create (context) {
const sourceCode = context.getSourceCode()
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='for']" (node) {
const element = node.parent.parent
checkKey(context, node, element)
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-for' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-for' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-for' directives require that attribute value."
})
return
}
const expr = node.value.expression
if (expr == null) {
return
}
if (expr.type !== 'VForExpression') {
context.report({
node: node.value,
loc: node.value.loc,
message: "'v-for' directives require the special syntax '<alias> in <expression>'."
})
return
}
const lhs = expr.left
const value = lhs[0]
const key = lhs[1]
const index = lhs[2]
if (value === null) {
context.report({
node: value || expr,
loc: value && value.loc,
message: "Invalid alias ''."
})
}
if (key !== undefined && (!key || key.type !== 'Identifier')) {
context.report({
node: key || expr,
loc: key && key.loc,
message: "Invalid alias '{{text}}'.",
data: { text: key ? sourceCode.getText(key) : '' }
})
}
if (index !== undefined && (!index || index.type !== 'Identifier')) {
context.report({
node: index || expr,
loc: index && index.loc,
message: "Invalid alias '{{text}}'.",
data: { text: index ? sourceCode.getText(index) : '' }
})
}
}
})
}
}
+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: {
docs: {
description: 'enforce valid `v-html` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-html.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='html']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-html' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-html' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-html' directives require that attribute value."
})
}
}
})
}
}
+72
View File
@@ -0,0 +1,72 @@
/**
* @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: {
docs: {
description: 'enforce valid `v-if` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-if.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='if']" (node) {
const element = node.parent.parent
if (utils.hasDirective(element, 'else')) {
context.report({
node,
loc: node.loc,
message: "'v-if' and 'v-else' directives can't exist on the same element. You may want 'v-else-if' directives."
})
}
if (utils.hasDirective(element, 'else-if')) {
context.report({
node,
loc: node.loc,
message: "'v-if' and 'v-else-if' directives can't exist on the same element."
})
}
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-if' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-if' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-if' directives require that attribute value."
})
}
}
})
}
}
+167
View File
@@ -0,0 +1,167 @@
/**
* @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
// ------------------------------------------------------------------------------
const VALID_MODIFIERS = new Set(['lazy', 'number', 'trim'])
/**
* 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) {
const name = node.name
return (
name === 'input' ||
name === 'select' ||
name === 'textarea' ||
(
name !== 'keep-alive' &&
name !== 'slot' &&
name !== 'transition' &&
name !== 'transition-group' &&
utils.isCustomComponent(node)
)
)
}
/**
* 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 node != null && (
node.type === 'Identifier' ||
node.type === 'MemberExpression'
)
}
/**
* Get the variable by names.
* @param {string} name The variable name to find.
* @param {ASTNode} leafNode The node to look up.
* @returns {Variable|null} The found variable or null.
*/
function getVariable (name, leafNode) {
let node = leafNode
while (node != null) {
const variables = node.variables
const variable = variables && variables.find(v => v.id.name === name)
if (variable != null) {
return variable
}
node = node.parent
}
return null
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-model` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-model.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='model']" (node) {
const element = node.parent.parent
const name = element.name
if (!isValidElement(element)) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives aren't supported on <{{name}}> elements.",
data: { name }
})
}
if (name === 'input' && utils.hasAttribute(element, 'type', 'file')) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives don't support 'file' input type."
})
}
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives require no argument."
})
}
for (const modifier of node.key.modifiers) {
if (!VALID_MODIFIERS.has(modifier)) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives don't support the modifier '{{name}}'.",
data: { name: modifier }
})
}
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives require that attribute value."
})
}
if (node.value) {
if (!isLhs(node.value.expression)) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives require the attribute value which is valid as LHS."
})
}
for (const reference of node.value.references) {
const id = reference.id
if (id.parent.type === 'MemberExpression' || id.parent.type === 'BinaryExpression') {
continue
}
const variable = getVariable(id.name, element)
if (variable != null) {
context.report({
node,
loc: node.loc,
message: "'v-model' directives cannot update the iteration variable 'x' itself."
})
}
}
}
}
})
}
}
+147
View File
@@ -0,0 +1,147 @@
/**
* @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
// ------------------------------------------------------------------------------
const VALID_MODIFIERS = new Set([
'stop', 'prevent', 'capture', 'self', 'ctrl', 'shift', 'alt', 'meta',
'native', 'once', 'left', 'right', 'middle', 'passive', 'esc', 'tab',
'enter', 'space', 'up', 'left', 'right', 'down', 'delete', 'exact'
])
const VERB_MODIFIERS = new Set([
'stop', 'prevent'
])
// https://www.w3.org/TR/uievents-key/
const KEY_ALIASES = new Set([
'unidentified', 'alt', 'alt-graph', 'caps-lock', 'control', 'fn', 'fn-lock',
'meta', 'num-lock', 'scroll-lock', 'shift', 'symbol', 'symbol-lock', 'hyper',
'super', 'enter', 'tab', 'arrow-down', 'arrow-left', 'arrow-right',
'arrow-up', 'end', 'home', 'page-down', 'page-up', 'backspace', 'clear',
'copy', 'cr-sel', 'cut', 'delete', 'erase-eof', 'ex-sel', 'insert', 'paste',
'redo', 'undo', 'accept', 'again', 'attn', 'cancel', 'context-menu', 'escape',
'execute', 'find', 'help', 'pause', 'select', 'zoom-in', 'zoom-out',
'brightness-down', 'brightness-up', 'eject', 'log-off', 'power',
'print-screen', 'hibernate', 'standby', 'wake-up', 'all-candidates',
'alphanumeric', 'code-input', 'compose', 'convert', 'dead', 'final-mode',
'group-first', 'group-last', 'group-next', 'group-previous', 'mode-change',
'next-candidate', 'non-convert', 'previous-candidate', 'process',
'single-candidate', 'hangul-mode', 'hanja-mode', 'junja-mode', 'eisu',
'hankaku', 'hiragana', 'hiragana-katakana', 'kana-mode', 'kanji-mode',
'katakana', 'romaji', 'zenkaku', 'zenkaku-hankaku', 'f1', 'f2', 'f3', 'f4',
'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', 'soft1', 'soft2', 'soft3',
'soft4', 'channel-down', 'channel-up', 'close', 'mail-forward', 'mail-reply',
'mail-send', 'media-close', 'media-fast-forward', 'media-pause',
'media-play-pause', 'media-record', 'media-rewind', 'media-stop',
'media-track-next', 'media-track-previous', 'new', 'open', 'print', 'save',
'spell-check', 'key11', 'key12', 'audio-balance-left', 'audio-balance-right',
'audio-bass-boost-down', 'audio-bass-boost-toggle', 'audio-bass-boost-up',
'audio-fader-front', 'audio-fader-rear', 'audio-surround-mode-next',
'audio-treble-down', 'audio-treble-up', 'audio-volume-down',
'audio-volume-up', 'audio-volume-mute', 'microphone-toggle',
'microphone-volume-down', 'microphone-volume-up', 'microphone-volume-mute',
'speech-correction-list', 'speech-input-toggle', 'launch-application1',
'launch-application2', 'launch-calendar', 'launch-contacts', 'launch-mail',
'launch-media-player', 'launch-music-player', 'launch-phone',
'launch-screen-saver', 'launch-spreadsheet', 'launch-web-browser',
'launch-web-cam', 'launch-word-processor', 'browser-back',
'browser-favorites', 'browser-forward', 'browser-home', 'browser-refresh',
'browser-search', 'browser-stop', 'app-switch', 'call', 'camera',
'camera-focus', 'end-call', 'go-back', 'go-home', 'headset-hook',
'last-number-redial', 'notification', 'manner-mode', 'voice-dial', 't-v',
't-v3-d-mode', 't-v-antenna-cable', 't-v-audio-description',
't-v-audio-description-mix-down', 't-v-audio-description-mix-up',
't-v-contents-menu', 't-v-data-service', 't-v-input', 't-v-input-component1',
't-v-input-component2', 't-v-input-composite1', 't-v-input-composite2',
't-v-input-h-d-m-i1', 't-v-input-h-d-m-i2', 't-v-input-h-d-m-i3',
't-v-input-h-d-m-i4', 't-v-input-v-g-a1', 't-v-media-context', 't-v-network',
't-v-number-entry', 't-v-power', 't-v-radio-service', 't-v-satellite',
't-v-satellite-b-s', 't-v-satellite-c-s', 't-v-satellite-toggle',
't-v-terrestrial-analog', 't-v-terrestrial-digital', 't-v-timer',
'a-v-r-input', 'a-v-r-power', 'color-f0-red', 'color-f1-green',
'color-f2-yellow', 'color-f3-blue', 'color-f4-grey', 'color-f5-brown',
'closed-caption-toggle', 'dimmer', 'display-swap', 'd-v-r', 'exit',
'favorite-clear0', 'favorite-clear1', 'favorite-clear2', 'favorite-clear3',
'favorite-recall0', 'favorite-recall1', 'favorite-recall2',
'favorite-recall3', 'favorite-store0', 'favorite-store1', 'favorite-store2',
'favorite-store3', 'guide', 'guide-next-day', 'guide-previous-day', 'info',
'instant-replay', 'link', 'list-program', 'live-content', 'lock',
'media-apps', 'media-last', 'media-skip-backward', 'media-skip-forward',
'media-step-backward', 'media-step-forward', 'media-top-menu', 'navigate-in',
'navigate-next', 'navigate-out', 'navigate-previous', 'next-favorite-channel',
'next-user-profile', 'on-demand', 'pairing', 'pin-p-down', 'pin-p-move',
'pin-p-toggle', 'pin-p-up', 'play-speed-down', 'play-speed-reset',
'play-speed-up', 'random-toggle', 'rc-low-battery', 'record-speed-next',
'rf-bypass', 'scan-channels-toggle', 'screen-mode-next', 'settings',
'split-screen-toggle', 's-t-b-input', 's-t-b-power', 'subtitle', 'teletext',
'video-mode-next', 'wink', 'zoom-toggle', 'audio-volume-down',
'audio-volume-up', 'audio-volume-mute', 'browser-back', 'browser-forward',
'channel-down', 'channel-up', 'context-menu', 'eject', 'end', 'enter', 'home',
'media-fast-forward', 'media-play', 'media-play-pause', 'media-record',
'media-rewind', 'media-stop', 'media-next-track', 'media-pause',
'media-previous-track', 'power', 'unidentified'
])
function isValidModifier (modifier) {
return (
// built-in aliases
VALID_MODIFIERS.has(modifier) ||
// keyCode
Number.isInteger(parseInt(modifier, 10)) ||
// keyAlias (an Unicode character)
Array.from(modifier).length === 1 ||
// keyAlias (special keys)
KEY_ALIASES.has(modifier)
)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'enforce valid `v-on` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-on.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='on']" (node) {
for (const modifier of node.key.modifiers) {
if (!isValidModifier(modifier)) {
context.report({
node,
loc: node.loc,
message: "'v-on' directives don't support the modifier '{{modifier}}'.",
data: { modifier }
})
}
}
if (!utils.hasAttributeValue(node) && !node.key.modifiers.some(VERB_MODIFIERS.has, VERB_MODIFIERS)) {
context.report({
node,
loc: node.loc,
message: "'v-on' directives require that attribute value or verb modifiers."
})
}
}
})
}
}
+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: {
docs: {
description: 'enforce valid `v-once` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-once.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='once']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-once' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-once' directives require no modifier."
})
}
if (node.value) {
context.report({
node,
loc: node.loc,
message: "'v-once' directives require no attribute value."
})
}
}
})
}
}
+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: {
docs: {
description: 'enforce valid `v-pre` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-pre.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='pre']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-pre' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-pre' directives require no modifier."
})
}
if (node.value) {
context.report({
node,
loc: node.loc,
message: "'v-pre' directives require no attribute value."
})
}
}
})
}
}
+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: {
docs: {
description: 'enforce valid `v-show` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-show.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='show']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-show' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-show' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-show' directives require that attribute value."
})
}
}
})
}
}
+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: {
docs: {
description: 'enforce valid `v-text` directives',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.7.1/docs/rules/valid-v-text.md'
},
fixable: null,
schema: []
},
create (context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='text']" (node) {
if (node.key.argument) {
context.report({
node,
loc: node.loc,
message: "'v-text' directives require no argument."
})
}
if (node.key.modifiers.length > 0) {
context.report({
node,
loc: node.loc,
message: "'v-text' directives require no modifier."
})
}
if (!utils.hasAttributeValue(node)) {
context.report({
node,
loc: node.loc,
message: "'v-text' directives require that attribute value."
})
}
}
})
}
}
+79
View File
@@ -0,0 +1,79 @@
const assert = require('assert')
const invalidChars = /[^a-zA-Z0-9:]+/g
/**
* Convert text to kebab-case
* @param {string} str Text to be converted
* @return {string}
*/
function kebabCase (str) {
return str
.replace(/([a-z])([A-Z])/g, match => match[0] + '-' + match[1])
.replace(invalidChars, '-')
.toLowerCase()
}
/**
* Convert text to snake_case
* @param {string} str Text to be converted
* @return {string}
*/
function snakeCase (str) {
return str
.replace(/([a-z])([A-Z])/g, match => match[0] + '_' + match[1])
.replace(invalidChars, '_')
.toLowerCase()
}
/**
* Convert text to camelCase
* @param {string} str Text to be converted
* @return {string} Converted string
*/
function camelCase (str) {
return str
.replace(/_/g, (_, index) => index === 0 ? _ : '-')
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) =>
index === 0 ? letter.toLowerCase() : letter.toUpperCase()
)
.replace(invalidChars, '')
}
/**
* Convert text to PascalCase
* @param {string} str Text to be converted
* @return {string} Converted string
*/
function pascalCase (str) {
return str
.replace(/_/g, (_, index) => index === 0 ? _ : '-')
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => letter.toUpperCase())
.replace(invalidChars, '')
}
const convertersMap = {
'kebab-case': kebabCase,
'snake_case': snakeCase,
'camelCase': camelCase,
'PascalCase': pascalCase
}
module.exports = {
allowedCaseOptions: [
'camelCase',
'kebab-case',
'PascalCase'
],
/**
* Return case converter
* @param {string} name type of converter to return ('camelCase', 'kebab-case', 'PascalCase')
* @return {kebabCase|camelCase|pascalCase}
*/
getConverter (name) {
assert(typeof name === 'string')
return convertersMap[name] || pascalCase
}
}
+1
View File
@@ -0,0 +1 @@
["html","body","base","head","link","meta","style","title","address","article","aside","footer","header","h1","h2","h3","h4","h5","h6","hgroup","nav","section","div","dd","dl","dt","figcaption","figure","hr","img","li","main","ol","p","pre","ul","a","b","abbr","bdi","bdo","br","cite","code","data","dfn","em","i","kbd","mark","q","rp","rt","rtc","ruby","s","samp","small","span","strong","sub","sup","time","u","var","wbr","area","audio","map","track","video","embed","object","param","source","canvas","script","noscript","del","ins","caption","col","colgroup","table","thead","tbody","tfoot","td","th","tr","button","datalist","fieldset","form","input","label","legend","meter","optgroup","option","output","progress","select","textarea","details","dialog","menu","menuitem","summary","content","element","shadow","template","slot","blockquote","iframe","noframes","picture"]
File diff suppressed because it is too large Load Diff
+659
View File
@@ -0,0 +1,659 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
const HTML_ELEMENT_NAMES = new Set(require('./html-elements.json'))
const VOID_ELEMENT_NAMES = new Set(require('./void-elements.json'))
const assert = require('assert')
const vueEslintParser = require('vue-eslint-parser')
// ------------------------------------------------------------------------------
// Exports
// ------------------------------------------------------------------------------
module.exports = {
/**
* Register the given visitor to parser services.
* If the parser service of `vue-eslint-parser` was not found,
* this generates a warning.
*
* @param {RuleContext} context The rule context to use parser services.
* @param {Object} templateBodyVisitor The visitor to traverse the template body.
* @param {Object} scriptVisitor The visitor to traverse the script.
* @returns {Object} The merged visitor.
*/
defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor) {
if (context.parserServices.defineTemplateBodyVisitor == null) {
context.report({
loc: { line: 1, column: 0 },
message: 'Use the latest vue-eslint-parser. See also https://github.com/vuejs/eslint-plugin-vue#what-is-the-use-the-latest-vue-eslint-parser-error'
})
return {}
}
return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
},
/**
* Check whether the given node is the root element or not.
* @param {ASTNode} node The element node to check.
* @returns {boolean} `true` if the node is the root element.
*/
isRootElement (node) {
assert(node && node.type === 'VElement')
return (
node.parent.type === 'VDocumentFragment' ||
node.parent.parent.type === 'VDocumentFragment'
)
},
/**
* Get the previous sibling element of the given element.
* @param {ASTNode} node The element node to get the previous sibling element.
* @returns {ASTNode|null} The previous sibling element.
*/
prevSibling (node) {
assert(node && node.type === 'VElement')
let prevElement = null
for (const siblingNode of (node.parent && node.parent.children) || []) {
if (siblingNode === node) {
return prevElement
}
if (siblingNode.type === 'VElement') {
prevElement = siblingNode
}
}
return null
},
/**
* Check whether the given start tag has specific directive.
* @param {ASTNode} node The start tag node to check.
* @param {string} name The attribute name to check.
* @param {string} [value] The attribute value to check.
* @returns {boolean} `true` if the start tag has the directive.
*/
hasAttribute (node, name, value) {
assert(node && node.type === 'VElement')
return node.startTag.attributes.some(a =>
!a.directive &&
a.key.name === name &&
(
value === undefined ||
(a.value != null && a.value.value === value)
)
)
},
/**
* Check whether the given start tag has specific directive.
* @param {ASTNode} node The start tag node to check.
* @param {string} name The directive name to check.
* @param {string} [argument] The directive argument to check.
* @returns {boolean} `true` if the start tag has the directive.
*/
hasDirective (node, name, argument) {
assert(node && node.type === 'VElement')
return node.startTag.attributes.some(a =>
a.directive &&
a.key.name === name &&
(argument === undefined || a.key.argument === argument)
)
},
/**
* Check whether the given attribute has their attribute value.
* @param {ASTNode} node The attribute node to check.
* @returns {boolean} `true` if the attribute has their value.
*/
hasAttributeValue (node) {
assert(node && node.type === 'VAttribute')
return (
node.value != null &&
(node.value.expression != null || node.value.syntaxError != null)
)
},
/**
* Get the attribute which has the given name.
* @param {ASTNode} node The start tag node to check.
* @param {string} name The attribute name to check.
* @param {string} [value] The attribute value to check.
* @returns {ASTNode} The found attribute.
*/
getAttribute (node, name, value) {
assert(node && node.type === 'VElement')
return node.startTag.attributes.find(a =>
!a.directive &&
a.key.name === name &&
(
value === undefined ||
(a.value != null && a.value.value === value)
)
)
},
/**
* Get the directive which has the given name.
* @param {ASTNode} node The start tag node to check.
* @param {string} name The directive name to check.
* @param {string} [argument] The directive argument to check.
* @returns {ASTNode} The found directive.
*/
getDirective (node, name, argument) {
assert(node && node.type === 'VElement')
return node.startTag.attributes.find(a =>
a.directive &&
a.key.name === name &&
(argument === undefined || a.key.argument === argument)
)
},
/**
* Check whether the previous sibling element has `if` or `else-if` directive.
* @param {ASTNode} node The element node to check.
* @returns {boolean} `true` if the previous sibling element has `if` or `else-if` directive.
*/
prevElementHasIf (node) {
assert(node && node.type === 'VElement')
const prev = this.prevSibling(node)
return (
prev != null &&
prev.startTag.attributes.some(a =>
a.directive &&
(a.key.name === 'if' || a.key.name === 'else-if')
)
)
},
/**
* Check whether the given node is a custom component or not.
* @param {ASTNode} node The start tag node to check.
* @returns {boolean} `true` if the node is a custom component.
*/
isCustomComponent (node) {
assert(node && node.type === 'VElement')
return (
(this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.name)) ||
this.hasAttribute(node, 'is') ||
this.hasDirective(node, 'bind', 'is')
)
},
/**
* Check whether the given node is a HTML element or not.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is a HTML element.
*/
isHtmlElementNode (node) {
assert(node && node.type === 'VElement')
return node.namespace === vueEslintParser.AST.NS.HTML
},
/**
* Check whether the given node is a SVG element or not.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the name is a SVG element.
*/
isSvgElementNode (node) {
assert(node && node.type === 'VElement')
return node.namespace === vueEslintParser.AST.NS.SVG
},
/**
* Check whether the given name is a MathML element or not.
* @param {ASTNode} name The node to check.
* @returns {boolean} `true` if the node is a MathML element.
*/
isMathMLElementNode (node) {
assert(node && node.type === 'VElement')
return node.namespace === vueEslintParser.AST.NS.MathML
},
/**
* Check whether the given name is an well-known element or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is an well-known element name.
*/
isHtmlWellKnownElementName (name) {
assert(typeof name === 'string')
return HTML_ELEMENT_NAMES.has(name.toLowerCase())
},
/**
* Check whether the given name is a void element name or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is a void element name.
*/
isHtmlVoidElementName (name) {
assert(typeof name === 'string')
return VOID_ELEMENT_NAMES.has(name.toLowerCase())
},
/**
* Parse member expression node to get array with all of its parts
* @param {ASTNode} MemberExpression
* @returns {Array}
*/
parseMemberExpression (node) {
const members = []
let memberExpression
if (node.type === 'MemberExpression') {
memberExpression = node
while (memberExpression.type === 'MemberExpression') {
if (memberExpression.property.type === 'Identifier') {
members.push(memberExpression.property.name)
}
memberExpression = memberExpression.object
}
if (memberExpression.type === 'ThisExpression') {
members.push('this')
} else if (memberExpression.type === 'Identifier') {
members.push(memberExpression.name)
}
}
return members.reverse()
},
/**
* Gets the property name of a given node.
* @param {ASTNode} node - The node to get.
* @return {string|null} The property name if static. Otherwise, null.
*/
getStaticPropertyName (node) {
let prop
switch (node && node.type) {
case 'Property':
case 'MethodDefinition':
prop = node.key
break
case 'MemberExpression':
prop = node.property
break
case 'Literal':
case 'TemplateLiteral':
case 'Identifier':
prop = node
break
// no default
}
switch (prop && prop.type) {
case 'Literal':
return String(prop.value)
case 'TemplateLiteral':
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
return prop.quasis[0].value.cooked
}
break
case 'Identifier':
if (!node.computed) {
return prop.name
}
break
// no default
}
return null
},
/**
* Get all computed properties by looking at all component's properties
* @param {ObjectExpression} Object with component definition
* @return {Array} Array of computed properties in format: [{key: String, value: ASTNode}]
*/
getComputedProperties (componentObject) {
const computedPropertiesNode = componentObject.properties
.find(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'computed' &&
p.value.type === 'ObjectExpression'
)
if (!computedPropertiesNode) { return [] }
return computedPropertiesNode.value.properties
.filter(cp => cp.type === 'Property')
.map(cp => {
const key = cp.key.name
let value
if (cp.value.type === 'FunctionExpression') {
value = cp.value.body
} else if (cp.value.type === 'ObjectExpression') {
value = cp.value.properties
.filter(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'get' &&
p.value.type === 'FunctionExpression'
)
.map(p => p.value.body)[0]
}
return { key, value }
})
},
/**
* Check whether the given node is a Vue component based
* on the filename and default export type
* export default {} in .vue || .jsx
* @param {ASTNode} node Node to check
* @param {string} path File name with extension
* @returns {boolean}
*/
isVueComponentFile (node, path) {
const isVueFile = path.endsWith('.vue') || path.endsWith('.jsx')
return isVueFile &&
node.type === 'ExportDefaultDeclaration' &&
node.declaration.type === 'ObjectExpression'
},
/**
* Check whether given node is Vue component
* Vue.component('xxx', {}) || component('xxx', {})
* @param {ASTNode} node Node to check
* @returns {boolean}
*/
isVueComponent (node) {
const callee = node.callee
const isFullVueComponent = node.type === 'CallExpression' &&
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === 'Vue' &&
callee.property.type === 'Identifier' &&
['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 &&
node.arguments.length >= 1 &&
node.arguments.slice(-1)[0].type === 'ObjectExpression'
const isDestructedVueComponent = node.type === 'CallExpression' &&
callee.type === 'Identifier' &&
callee.name === 'component' &&
node.arguments.length >= 1 &&
node.arguments.slice(-1)[0].type === 'ObjectExpression'
return isFullVueComponent || isDestructedVueComponent
},
/**
* Check whether given node is new Vue instance
* new Vue({})
* @param {ASTNode} node Node to check
* @returns {boolean}
*/
isVueInstance (node) {
const callee = node.callee
return node.type === 'NewExpression' &&
callee.type === 'Identifier' &&
callee.name === 'Vue' &&
node.arguments.length &&
node.arguments[0].type === 'ObjectExpression'
},
/**
* Check if current file is a Vue instance or component and call callback
* @param {RuleContext} context The ESLint rule context object.
* @param {Function} cb Callback function
*/
executeOnVue (context, cb) {
return Object.assign(
this.executeOnVueComponent(context, cb),
this.executeOnVueInstance(context, cb)
)
},
/**
* Check if current file is a Vue instance (new Vue) and call callback
* @param {RuleContext} context The ESLint rule context object.
* @param {Function} cb Callback function
*/
executeOnVueInstance (context, cb) {
const _this = this
return {
'NewExpression:exit' (node) {
// new Vue({})
if (!_this.isVueInstance(node)) return
cb(node.arguments[0])
}
}
},
/**
* Check if current file is a Vue component and call callback
* @param {RuleContext} context The ESLint rule context object.
* @param {Function} cb Callback function
*/
executeOnVueComponent (context, cb) {
const filePath = context.getFilename()
const sourceCode = context.getSourceCode()
const _this = this
const componentComments = sourceCode.getAllComments().filter(comment => /@vue\/component/g.test(comment.value))
const foundNodes = []
const isDuplicateNode = (node) => {
if (foundNodes.some(el => el.loc.start.line === node.loc.start.line)) return true
foundNodes.push(node)
return false
}
return {
'ObjectExpression:exit' (node) {
if (!componentComments.some(el => el.loc.end.line === node.loc.start.line - 1) || isDuplicateNode(node)) return
cb(node)
},
'ExportDefaultDeclaration:exit' (node) {
// export default {} in .vue || .jsx
if (!_this.isVueComponentFile(node, filePath) || isDuplicateNode(node.declaration)) return
cb(node.declaration)
},
'CallExpression:exit' (node) {
// Vue.component('xxx', {}) || component('xxx', {})
if (!_this.isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) return
cb(node.arguments.slice(-1)[0])
}
}
},
/**
* Return generator with all properties
* @param {ASTNode} node Node to check
* @param {string} groupName Name of parent group
*/
* iterateProperties (node, groups) {
const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(this.getStaticPropertyName(p.key)))
for (const item of nodes) {
const name = this.getStaticPropertyName(item.key)
if (!name) continue
if (item.value.type === 'ArrayExpression') {
yield * this.iterateArrayExpression(item.value, name)
} else if (item.value.type === 'ObjectExpression') {
yield * this.iterateObjectExpression(item.value, name)
} else if (item.value.type === 'FunctionExpression') {
yield * this.iterateFunctionExpression(item.value, name)
}
}
},
/**
* Return generator with all elements inside ArrayExpression
* @param {ASTNode} node Node to check
* @param {string} groupName Name of parent group
*/
* iterateArrayExpression (node, groupName) {
assert(node.type === 'ArrayExpression')
for (const item of node.elements) {
const name = this.getStaticPropertyName(item)
if (name) {
const obj = { name, groupName, node: item }
yield obj
}
}
},
/**
* Return generator with all elements inside ObjectExpression
* @param {ASTNode} node Node to check
* @param {string} groupName Name of parent group
*/
* iterateObjectExpression (node, groupName) {
assert(node.type === 'ObjectExpression')
for (const item of node.properties) {
const name = this.getStaticPropertyName(item)
if (name) {
const obj = { name, groupName, node: item.key }
yield obj
}
}
},
/**
* Return generator with all elements inside FunctionExpression
* @param {ASTNode} node Node to check
* @param {string} groupName Name of parent group
*/
* iterateFunctionExpression (node, groupName) {
assert(node.type === 'FunctionExpression')
if (node.body.type === 'BlockStatement') {
for (const item of node.body.body) {
if (item.type === 'ReturnStatement' && item.argument && item.argument.type === 'ObjectExpression') {
yield * this.iterateObjectExpression(item.argument, groupName)
}
}
}
},
/**
* Find all functions which do not always return values
* @param {boolean} treatUndefinedAsUnspecified
* @param {Function} cb Callback function
*/
executeOnFunctionsWithoutReturn (treatUndefinedAsUnspecified, cb) {
let funcInfo = {
funcInfo: null,
codePath: null,
hasReturn: false,
hasReturnValue: false,
node: null
}
function isValidReturn () {
if (!funcInfo.hasReturn) {
return false
}
return !treatUndefinedAsUnspecified || funcInfo.hasReturnValue
}
return {
onCodePathStart (codePath, node) {
funcInfo = {
codePath,
funcInfo: funcInfo,
hasReturn: false,
hasReturnValue: false,
node
}
},
onCodePathEnd () {
funcInfo = funcInfo.funcInfo
},
ReturnStatement (node) {
funcInfo.hasReturn = true
funcInfo.hasReturnValue = Boolean(node.argument)
},
'ArrowFunctionExpression:exit' (node) {
if (!isValidReturn() && !node.expression) {
cb(funcInfo.node)
}
},
'FunctionExpression:exit' (node) {
if (!isValidReturn()) {
cb(funcInfo.node)
}
}
}
},
/**
* Check whether the component is declared in a single line or not.
* @param {ASTNode} node
* @returns {boolean}
*/
isSingleLine (node) {
return node.loc.start.line === node.loc.end.line
},
/**
* Check whether the templateBody of the program has invalid EOF or not.
* @param {Program} node The program node to check.
* @returns {boolean} `true` if it has invalid EOF.
*/
hasInvalidEOF (node) {
const body = node.templateBody
if (body == null || body.errors == null) {
return
}
return body.errors.some(error => typeof error.code === 'string' && error.code.startsWith('eof-'))
},
/**
* Parse CallExpression or MemberExpression to get simplified version without arguments
*
* @param {Object} node The node to parse (MemberExpression | CallExpression)
* @return {String} eg. 'this.asd.qwe().map().filter().test.reduce()'
*/
parseMemberOrCallExpression (node) {
const parsedCallee = []
let n = node
let isFunc
while (n.type === 'MemberExpression' || n.type === 'CallExpression') {
if (n.type === 'CallExpression') {
n = n.callee
isFunc = true
} else {
if (n.computed) {
parsedCallee.push('[]')
} else if (n.property.type === 'Identifier') {
parsedCallee.push(n.property.name + (isFunc ? '()' : ''))
}
isFunc = false
n = n.object
}
}
if (n.type === 'Identifier') {
parsedCallee.push(n.name)
}
if (n.type === 'ThisExpression') {
parsedCallee.push('this')
}
return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
}
}
+18
View File
@@ -0,0 +1,18 @@
[
"abstract", "arguments", "await", "boolean",
"break", "byte", "case", "catch",
"char", "class", "const", "continue",
"debugger", "default", "delete", "do",
"double", "else", "enum", "eval",
"export", "extends", "false", "final",
"finally", "float", "for", "function",
"goto", "if", "implements", "import",
"in", "instanceof", "int", "interface",
"let", "long", "native", "new",
"null", "package", "private", "protected",
"public", "return", "short", "static",
"super", "switch", "synchronized", "this",
"throw", "throws", "transient", "true",
"try", "typeof", "var", "void",
"volatile", "while", "with", "yield"
]
+1
View File
@@ -0,0 +1 @@
["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"]
+4
View File
@@ -0,0 +1,4 @@
[
"$data", "$props", "$el", "$options", "$parent", "$root", "$children", "$slots", "$scopedSlots", "$refs", "$isServer", "$attrs", "$listeners",
"$watch", "$set", "$delete", "$on", "$once", "$off", "$emit", "$mount", "$forceUpdate", "$nextTick", "$destroy"
]
+93
View File
@@ -0,0 +1,93 @@
{
"_from": "eslint-plugin-vue@^4.7.1",
"_id": "eslint-plugin-vue@4.7.1",
"_inBundle": false,
"_integrity": "sha512-esETKhVMI7Vdli70Wt4bvAwnZBJeM0pxVX9Yb0wWKxdCJc2EADalVYK/q2FzMw8oKN0wPMdqVCKS8kmR89recA==",
"_location": "/eslint-plugin-vue",
"_phantomChildren": {},
"_requested": {
"type": "range",
"registry": true,
"raw": "eslint-plugin-vue@^4.7.1",
"name": "eslint-plugin-vue",
"escapedName": "eslint-plugin-vue",
"rawSpec": "^4.7.1",
"saveSpec": null,
"fetchSpec": "^4.7.1"
},
"_requiredBy": [
"/@vue/cli-plugin-eslint"
],
"_resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-4.7.1.tgz",
"_shasum": "c829b9fc62582c1897b5a0b94afd44ecca511e63",
"_spec": "eslint-plugin-vue@^4.7.1",
"_where": "D:\\Atom\\Git\\Laravel_CSSd\\node_modules\\@vue\\cli-plugin-eslint",
"author": {
"name": "Toru Nagashima",
"url": "https://github.com/mysticatea"
},
"bugs": {
"url": "https://github.com/vuejs/eslint-plugin-vue/issues"
},
"bundleDependencies": false,
"contributors": [
{
"name": "Michał Sajnóg",
"email": "msajnog93@gmail.com",
"url": "http://github.com/michalsnik"
}
],
"dependencies": {
"vue-eslint-parser": "^2.0.3"
},
"deprecated": false,
"description": "Official ESLint plugin for Vue.js",
"devDependencies": {
"@types/node": "^4.2.16",
"babel-eslint": "^8.2.2",
"chai": "^4.1.0",
"eslint": "^4.14.0",
"eslint-plugin-eslint-plugin": "^0.8.0",
"eslint-plugin-html": "^4.0.1",
"eslint-plugin-vue-libs": "^2.0.0",
"lodash": "^4.17.4",
"mocha": "^3.2.0",
"nyc": "^11.1.0"
},
"engines": {
"node": ">=4"
},
"files": [
"lib"
],
"homepage": "https://github.com/vuejs/eslint-plugin-vue#readme",
"keywords": [
"eslint",
"eslint-plugin",
"eslint-config",
"vue",
"vuejs",
"rules"
],
"license": "MIT",
"main": "lib/index.js",
"name": "eslint-plugin-vue",
"peerDependencies": {
"eslint": "^3.18.0 || ^4.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/eslint-plugin-vue.git"
},
"scripts": {
"lint": "eslint . --rulesdir eslint-internal-rules",
"pretest": "npm run lint",
"preversion": "npm test && npm run update && git add .",
"start": "npm run test:base -- --watch --growl",
"test": "nyc npm run test:base -- \"tests/integrations/*.js\" --timeout 60000",
"test:base": "mocha \"tests/lib/**/*.js\" --reporter dot",
"update": "node ./tools/update.js",
"version": "npm run lint -- --fix && git add ."
},
"version": "4.7.1"
}