first commit

This commit is contained in:
monjack
2025-06-20 18:01:48 +08:00
commit 6daa6d65c1
24611 changed files with 2512443 additions and 0 deletions

21
app_vue/node_modules/eslint-plugin-vue/LICENSE generated vendored Normal file
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.

49
app_vue/node_modules/eslint-plugin-vue/README.md generated vendored Normal file
View File

@ -0,0 +1,49 @@
# eslint-plugin-vue
[![NPM version](https://img.shields.io/npm/v/eslint-plugin-vue.svg?style=flat)](https://npmjs.org/package/eslint-plugin-vue)
[![NPM downloads](https://img.shields.io/npm/dm/eslint-plugin-vue.svg?style=flat)](https://npmjs.org/package/eslint-plugin-vue)
[![CircleCI](https://img.shields.io/circleci/project/github/vuejs/eslint-plugin-vue/master.svg?style=flat)](https://circleci.com/gh/vuejs/eslint-plugin-vue)
[![License](https://img.shields.io/github/license/vuejs/eslint-plugin-vue.svg?style=flat)](https://github.com/vuejs/eslint-plugin-vue/blob/master/LICENSE)
> Official ESLint plugin for Vue.js
## :book: Documentation
Please refer to the [official website](https://eslint.vuejs.org).
## :anchor: Versioning Policy
This plugin follows [Semantic Versioning](https://semver.org) and [ESLint's Semantic Versioning Policy](https://github.com/eslint/eslint#semantic-versioning-policy).
## :newspaper: Releases
This project uses [GitHub Releases](https://github.com/vuejs/eslint-plugin-vue/releases).
## :beers: Contribution Guide
Contributing is welcome! See the [ESLint Vue Plugin Developer Guide](https://eslint.vuejs.org/developer-guide).
### Working With Rules
Be sure to read the [official ESLint guide](https://eslint.org/docs/developer-guide/working-with-rules) before you start writing a new rule.
To see what an abstract syntax tree (AST) of your code looks like, you may use [AST Explorer](https://astexplorer.net). After opening [AST Explorer](https://astexplorer.net), select `Vue` as the syntax and `vue-eslint-parser` as the parser.
The default JavaScript parser must be replaced because [Vue.js single file components](https://vuejs.org/guide/scaling-up/sfc.html) are not plain JavaScript, but a custom file format. [`vue-eslint-parser`](https://github.com/vuejs/vue-eslint-parser) is a replacement parser that generates an enhanced AST with nodes that represent specific parts of the template syntax, as well as the contents of the `<script>` tag.
To learn more about certain nodes in a produced AST, see the [ESTree project page](https://github.com/estree/estree) and the [vue-eslint-parser AST documentation](https://github.com/vuejs/vue-eslint-parser/blob/master/docs/ast.md).
`vue-eslint-parser` provides a few useful parser services to help traverse the produced AST and access template tokens:
- `context.parserServices.defineTemplateBodyVisitor(visitor, scriptVisitor)`
- `context.parserServices.getTemplateBodyTokenStore()`
Check out an [example rule](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/mustache-interpolation-spacing.js) to see usage of these services.
Be aware that depending on the code samples you write in tests, the `RuleTester` parser property must be set accordingly (this can be done on a test by test basis). See an [example here](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/attribute-hyphenation.js#L19).
If you're stuck, remember there are many rules available for reference. If you can't find the right solution, don't hesitate to reach out in [issues](https://github.com/vuejs/eslint-plugin-vue/issues) we're happy to help!
## :lock: License
See the [LICENSE](LICENSE) file for license rights and limitations (MIT).

View File

@ -0,0 +1,22 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update its content execute "npm run update"
*/
module.exports = {
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module'
},
env: {
browser: true,
es6: true
},
plugins: ['vue'],
rules: {
'vue/comment-directive': 'error',
'vue/jsx-uses-vars': 'error',
'vue/script-setup-uses-vars': 'error'
}
}

View File

@ -0,0 +1,62 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update its content execute "npm run update"
*/
module.exports = {
extends: require.resolve('./base'),
rules: {
'vue/multi-word-component-names': 'error',
'vue/no-arrow-functions-in-watch': 'error',
'vue/no-async-in-computed-properties': 'error',
'vue/no-computed-properties-in-data': 'error',
'vue/no-custom-modifiers-on-v-model': 'error',
'vue/no-dupe-keys': 'error',
'vue/no-dupe-v-else-if': 'error',
'vue/no-duplicate-attributes': 'error',
'vue/no-multiple-template-root': 'error',
'vue/no-mutating-props': 'error',
'vue/no-parsing-error': 'error',
'vue/no-reserved-keys': 'error',
'vue/no-reserved-props': [
'error',
{
vueVersion: 2
}
],
'vue/no-shared-component-data': 'error',
'vue/no-side-effects-in-computed-properties': 'error',
'vue/no-template-key': 'error',
'vue/no-textarea-mustache': 'error',
'vue/no-unused-components': 'error',
'vue/no-unused-vars': 'error',
'vue/no-use-v-if-with-v-for': 'error',
'vue/no-useless-template-attributes': 'error',
'vue/no-v-for-template-key': 'error',
'vue/no-v-model-argument': 'error',
'vue/require-component-is': 'error',
'vue/require-prop-type-constructor': 'error',
'vue/require-render-return': 'error',
'vue/require-v-for-key': 'error',
'vue/require-valid-default-prop': 'error',
'vue/return-in-computed-property': 'error',
'vue/use-v-on-exact': 'error',
'vue/valid-next-tick': 'error',
'vue/valid-template-root': 'error',
'vue/valid-v-bind-sync': '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-slot': 'error',
'vue/valid-v-text': 'error'
}
}

View File

@ -0,0 +1,52 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update its content execute "npm run update"
*/
module.exports = {
rules: {
'vue/array-bracket-newline': 'off',
'vue/array-bracket-spacing': 'off',
'vue/arrow-spacing': 'off',
'vue/block-spacing': 'off',
'vue/block-tag-newline': 'off',
'vue/brace-style': 'off',
'vue/comma-dangle': 'off',
'vue/comma-spacing': 'off',
'vue/comma-style': 'off',
'vue/define-macros-order': 'off',
'vue/dot-location': 'off',
'vue/first-attribute-linebreak': 'off',
'vue/func-call-spacing': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/html-closing-bracket-spacing': 'off',
'vue/html-comment-content-newline': 'off',
'vue/html-comment-content-spacing': 'off',
'vue/html-comment-indent': 'off',
'vue/html-indent': 'off',
'vue/html-quotes': 'off',
'vue/html-self-closing': 'off',
'vue/key-spacing': 'off',
'vue/keyword-spacing': 'off',
'vue/max-attributes-per-line': 'off',
'vue/max-len': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/mustache-interpolation-spacing': 'off',
'vue/new-line-between-multi-line-property': 'off',
'vue/no-extra-parens': 'off',
'vue/no-multi-spaces': 'off',
'vue/no-spaces-around-equal-signs-in-attribute': 'off',
'vue/object-curly-newline': 'off',
'vue/object-curly-spacing': 'off',
'vue/object-property-newline': 'off',
'vue/operator-linebreak': 'off',
'vue/padding-line-between-blocks': 'off',
'vue/script-indent': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/space-in-parens': 'off',
'vue/space-infix-ops': 'off',
'vue/space-unary-ops': 'off',
'vue/template-curly-spacing': 'off',
'vue/v-for-delimiter-style': 'off'
}
}

View File

@ -0,0 +1,17 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update its content execute "npm run update"
*/
module.exports = {
extends: require.resolve('./strongly-recommended'),
rules: {
'vue/attributes-order': 'warn',
'vue/component-tags-order': 'warn',
'vue/no-lone-template': 'warn',
'vue/no-multiple-slot-args': 'warn',
'vue/no-v-html': 'warn',
'vue/order-in-components': 'warn',
'vue/this-in-template': 'warn'
}
}

View File

@ -0,0 +1,33 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update its content execute "npm run update"
*/
module.exports = {
extends: require.resolve('./essential'),
rules: {
'vue/attribute-hyphenation': 'warn',
'vue/component-definition-name-casing': 'warn',
'vue/first-attribute-linebreak': 'warn',
'vue/html-closing-bracket-newline': 'warn',
'vue/html-closing-bracket-spacing': 'warn',
'vue/html-end-tags': 'warn',
'vue/html-indent': 'warn',
'vue/html-quotes': 'warn',
'vue/html-self-closing': 'warn',
'vue/max-attributes-per-line': 'warn',
'vue/multiline-html-element-content-newline': 'warn',
'vue/mustache-interpolation-spacing': 'warn',
'vue/no-multi-spaces': 'warn',
'vue/no-spaces-around-equal-signs-in-attribute': 'warn',
'vue/no-template-shadow': 'warn',
'vue/one-component-per-file': 'warn',
'vue/prop-name-casing': 'warn',
'vue/require-default-prop': 'warn',
'vue/require-prop-types': 'warn',
'vue/singleline-html-element-content-newline': 'warn',
'vue/v-bind-style': 'warn',
'vue/v-on-style': 'warn',
'vue/v-slot-style': 'warn'
}
}

View File

@ -0,0 +1,84 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update its content execute "npm run update"
*/
module.exports = {
extends: require.resolve('./base'),
rules: {
'vue/multi-word-component-names': 'error',
'vue/no-arrow-functions-in-watch': 'error',
'vue/no-async-in-computed-properties': 'error',
'vue/no-computed-properties-in-data': 'error',
'vue/no-deprecated-data-object-declaration': 'error',
'vue/no-deprecated-destroyed-lifecycle': 'error',
'vue/no-deprecated-dollar-listeners-api': 'error',
'vue/no-deprecated-dollar-scopedslots-api': 'error',
'vue/no-deprecated-events-api': 'error',
'vue/no-deprecated-filter': 'error',
'vue/no-deprecated-functional-template': 'error',
'vue/no-deprecated-html-element-is': 'error',
'vue/no-deprecated-inline-template': 'error',
'vue/no-deprecated-props-default-this': 'error',
'vue/no-deprecated-router-link-tag-prop': 'error',
'vue/no-deprecated-scope-attribute': 'error',
'vue/no-deprecated-slot-attribute': 'error',
'vue/no-deprecated-slot-scope-attribute': 'error',
'vue/no-deprecated-v-bind-sync': 'error',
'vue/no-deprecated-v-is': 'error',
'vue/no-deprecated-v-on-native-modifier': 'error',
'vue/no-deprecated-v-on-number-modifiers': 'error',
'vue/no-deprecated-vue-config-keycodes': 'error',
'vue/no-dupe-keys': 'error',
'vue/no-dupe-v-else-if': 'error',
'vue/no-duplicate-attributes': 'error',
'vue/no-export-in-script-setup': 'error',
'vue/no-lifecycle-after-await': 'error',
'vue/no-mutating-props': 'error',
'vue/no-parsing-error': 'error',
'vue/no-ref-as-operand': 'error',
'vue/no-reserved-keys': 'error',
'vue/no-reserved-props': 'error',
'vue/no-setup-props-destructure': 'error',
'vue/no-shared-component-data': 'error',
'vue/no-side-effects-in-computed-properties': 'error',
'vue/no-template-key': 'error',
'vue/no-textarea-mustache': 'error',
'vue/no-unused-components': 'error',
'vue/no-unused-vars': 'error',
'vue/no-use-v-if-with-v-for': 'error',
'vue/no-useless-template-attributes': 'error',
'vue/no-v-for-template-key-on-child': 'error',
'vue/no-watch-after-await': 'error',
'vue/require-component-is': 'error',
'vue/require-prop-type-constructor': 'error',
'vue/require-render-return': 'error',
'vue/require-slots-as-functions': 'error',
'vue/require-toggle-inside-transition': 'error',
'vue/require-v-for-key': 'error',
'vue/require-valid-default-prop': 'error',
'vue/return-in-computed-property': 'error',
'vue/return-in-emits-validator': 'error',
'vue/use-v-on-exact': 'error',
'vue/valid-define-emits': 'error',
'vue/valid-define-props': 'error',
'vue/valid-next-tick': '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-is': 'error',
'vue/valid-v-memo': '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-slot': 'error',
'vue/valid-v-text': 'error'
}
}

View File

@ -0,0 +1,17 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update its content execute "npm run update"
*/
module.exports = {
extends: require.resolve('./vue3-strongly-recommended'),
rules: {
'vue/attributes-order': 'warn',
'vue/component-tags-order': 'warn',
'vue/no-lone-template': 'warn',
'vue/no-multiple-slot-args': 'warn',
'vue/no-v-html': 'warn',
'vue/order-in-components': 'warn',
'vue/this-in-template': 'warn'
}
}

View File

@ -0,0 +1,35 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update its content execute "npm run update"
*/
module.exports = {
extends: require.resolve('./vue3-essential'),
rules: {
'vue/attribute-hyphenation': 'warn',
'vue/component-definition-name-casing': 'warn',
'vue/first-attribute-linebreak': 'warn',
'vue/html-closing-bracket-newline': 'warn',
'vue/html-closing-bracket-spacing': 'warn',
'vue/html-end-tags': 'warn',
'vue/html-indent': 'warn',
'vue/html-quotes': 'warn',
'vue/html-self-closing': 'warn',
'vue/max-attributes-per-line': 'warn',
'vue/multiline-html-element-content-newline': 'warn',
'vue/mustache-interpolation-spacing': 'warn',
'vue/no-multi-spaces': 'warn',
'vue/no-spaces-around-equal-signs-in-attribute': 'warn',
'vue/no-template-shadow': 'warn',
'vue/one-component-per-file': 'warn',
'vue/prop-name-casing': 'warn',
'vue/require-default-prop': 'warn',
'vue/require-explicit-emits': 'warn',
'vue/require-prop-types': 'warn',
'vue/singleline-html-element-content-newline': 'warn',
'vue/v-bind-style': 'warn',
'vue/v-on-event-hyphenation': 'warn',
'vue/v-on-style': 'warn',
'vue/v-slot-style': 'warn'
}
}

248
app_vue/node_modules/eslint-plugin-vue/lib/index.js generated vendored Normal file
View File

@ -0,0 +1,248 @@
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update its content execute "npm run update"
*/
'use strict'
module.exports = {
rules: {
'array-bracket-newline': require('./rules/array-bracket-newline'),
'array-bracket-spacing': require('./rules/array-bracket-spacing'),
'arrow-spacing': require('./rules/arrow-spacing'),
'attribute-hyphenation': require('./rules/attribute-hyphenation'),
'attributes-order': require('./rules/attributes-order'),
'block-lang': require('./rules/block-lang'),
'block-spacing': require('./rules/block-spacing'),
'block-tag-newline': require('./rules/block-tag-newline'),
'brace-style': require('./rules/brace-style'),
camelcase: require('./rules/camelcase'),
'comma-dangle': require('./rules/comma-dangle'),
'comma-spacing': require('./rules/comma-spacing'),
'comma-style': require('./rules/comma-style'),
'comment-directive': require('./rules/comment-directive'),
'component-api-style': require('./rules/component-api-style'),
'component-definition-name-casing': require('./rules/component-definition-name-casing'),
'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
'component-options-name-casing': require('./rules/component-options-name-casing'),
'component-tags-order': require('./rules/component-tags-order'),
'custom-event-name-casing': require('./rules/custom-event-name-casing'),
'define-macros-order': require('./rules/define-macros-order'),
'dot-location': require('./rules/dot-location'),
'dot-notation': require('./rules/dot-notation'),
eqeqeq: require('./rules/eqeqeq'),
'experimental-script-setup-vars': require('./rules/experimental-script-setup-vars'),
'first-attribute-linebreak': require('./rules/first-attribute-linebreak'),
'func-call-spacing': require('./rules/func-call-spacing'),
'html-button-has-type': require('./rules/html-button-has-type'),
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
'html-comment-content-newline': require('./rules/html-comment-content-newline'),
'html-comment-content-spacing': require('./rules/html-comment-content-spacing'),
'html-comment-indent': require('./rules/html-comment-indent'),
'html-end-tags': require('./rules/html-end-tags'),
'html-indent': require('./rules/html-indent'),
'html-quotes': require('./rules/html-quotes'),
'html-self-closing': require('./rules/html-self-closing'),
'jsx-uses-vars': require('./rules/jsx-uses-vars'),
'key-spacing': require('./rules/key-spacing'),
'keyword-spacing': require('./rules/keyword-spacing'),
'match-component-file-name': require('./rules/match-component-file-name'),
'match-component-import-name': require('./rules/match-component-import-name'),
'max-attributes-per-line': require('./rules/max-attributes-per-line'),
'max-len': require('./rules/max-len'),
'multi-word-component-names': require('./rules/multi-word-component-names'),
'multiline-html-element-content-newline': require('./rules/multiline-html-element-content-newline'),
'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),
'name-property-casing': require('./rules/name-property-casing'),
'new-line-between-multi-line-property': require('./rules/new-line-between-multi-line-property'),
'next-tick-style': require('./rules/next-tick-style'),
'no-arrow-functions-in-watch': require('./rules/no-arrow-functions-in-watch'),
'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'),
'no-bare-strings-in-template': require('./rules/no-bare-strings-in-template'),
'no-boolean-default': require('./rules/no-boolean-default'),
'no-child-content': require('./rules/no-child-content'),
'no-computed-properties-in-data': require('./rules/no-computed-properties-in-data'),
'no-confusing-v-for-v-if': require('./rules/no-confusing-v-for-v-if'),
'no-constant-condition': require('./rules/no-constant-condition'),
'no-custom-modifiers-on-v-model': require('./rules/no-custom-modifiers-on-v-model'),
'no-deprecated-data-object-declaration': require('./rules/no-deprecated-data-object-declaration'),
'no-deprecated-destroyed-lifecycle': require('./rules/no-deprecated-destroyed-lifecycle'),
'no-deprecated-dollar-listeners-api': require('./rules/no-deprecated-dollar-listeners-api'),
'no-deprecated-dollar-scopedslots-api': require('./rules/no-deprecated-dollar-scopedslots-api'),
'no-deprecated-events-api': require('./rules/no-deprecated-events-api'),
'no-deprecated-filter': require('./rules/no-deprecated-filter'),
'no-deprecated-functional-template': require('./rules/no-deprecated-functional-template'),
'no-deprecated-html-element-is': require('./rules/no-deprecated-html-element-is'),
'no-deprecated-inline-template': require('./rules/no-deprecated-inline-template'),
'no-deprecated-props-default-this': require('./rules/no-deprecated-props-default-this'),
'no-deprecated-router-link-tag-prop': require('./rules/no-deprecated-router-link-tag-prop'),
'no-deprecated-scope-attribute': require('./rules/no-deprecated-scope-attribute'),
'no-deprecated-slot-attribute': require('./rules/no-deprecated-slot-attribute'),
'no-deprecated-slot-scope-attribute': require('./rules/no-deprecated-slot-scope-attribute'),
'no-deprecated-v-bind-sync': require('./rules/no-deprecated-v-bind-sync'),
'no-deprecated-v-is': require('./rules/no-deprecated-v-is'),
'no-deprecated-v-on-native-modifier': require('./rules/no-deprecated-v-on-native-modifier'),
'no-deprecated-v-on-number-modifiers': require('./rules/no-deprecated-v-on-number-modifiers'),
'no-deprecated-vue-config-keycodes': require('./rules/no-deprecated-vue-config-keycodes'),
'no-dupe-keys': require('./rules/no-dupe-keys'),
'no-dupe-v-else-if': require('./rules/no-dupe-v-else-if'),
'no-duplicate-attr-inheritance': require('./rules/no-duplicate-attr-inheritance'),
'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
'no-empty-component-block': require('./rules/no-empty-component-block'),
'no-empty-pattern': require('./rules/no-empty-pattern'),
'no-export-in-script-setup': require('./rules/no-export-in-script-setup'),
'no-expose-after-await': require('./rules/no-expose-after-await'),
'no-extra-parens': require('./rules/no-extra-parens'),
'no-invalid-model-keys': require('./rules/no-invalid-model-keys'),
'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'),
'no-lone-template': require('./rules/no-lone-template'),
'no-loss-of-precision': require('./rules/no-loss-of-precision'),
'no-multi-spaces': require('./rules/no-multi-spaces'),
'no-multiple-objects-in-class': require('./rules/no-multiple-objects-in-class'),
'no-multiple-slot-args': require('./rules/no-multiple-slot-args'),
'no-multiple-template-root': require('./rules/no-multiple-template-root'),
'no-mutating-props': require('./rules/no-mutating-props'),
'no-parsing-error': require('./rules/no-parsing-error'),
'no-potential-component-option-typo': require('./rules/no-potential-component-option-typo'),
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
'no-reserved-keys': require('./rules/no-reserved-keys'),
'no-reserved-props': require('./rules/no-reserved-props'),
'no-restricted-block': require('./rules/no-restricted-block'),
'no-restricted-call-after-await': require('./rules/no-restricted-call-after-await'),
'no-restricted-class': require('./rules/no-restricted-class'),
'no-restricted-component-options': require('./rules/no-restricted-component-options'),
'no-restricted-custom-event': require('./rules/no-restricted-custom-event'),
'no-restricted-html-elements': require('./rules/no-restricted-html-elements'),
'no-restricted-props': require('./rules/no-restricted-props'),
'no-restricted-static-attribute': require('./rules/no-restricted-static-attribute'),
'no-restricted-syntax': require('./rules/no-restricted-syntax'),
'no-restricted-v-bind': require('./rules/no-restricted-v-bind'),
'no-setup-props-destructure': require('./rules/no-setup-props-destructure'),
'no-shared-component-data': require('./rules/no-shared-component-data'),
'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'),
'no-spaces-around-equal-signs-in-attribute': require('./rules/no-spaces-around-equal-signs-in-attribute'),
'no-sparse-arrays': require('./rules/no-sparse-arrays'),
'no-static-inline-styles': require('./rules/no-static-inline-styles'),
'no-template-key': require('./rules/no-template-key'),
'no-template-shadow': require('./rules/no-template-shadow'),
'no-template-target-blank': require('./rules/no-template-target-blank'),
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
'no-this-in-before-route-enter': require('./rules/no-this-in-before-route-enter'),
'no-undef-components': require('./rules/no-undef-components'),
'no-undef-properties': require('./rules/no-undef-properties'),
'no-unregistered-components': require('./rules/no-unregistered-components'),
'no-unsupported-features': require('./rules/no-unsupported-features'),
'no-unused-components': require('./rules/no-unused-components'),
'no-unused-properties': require('./rules/no-unused-properties'),
'no-unused-refs': require('./rules/no-unused-refs'),
'no-unused-vars': require('./rules/no-unused-vars'),
'no-use-computed-property-like-method': require('./rules/no-use-computed-property-like-method'),
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
'no-useless-concat': require('./rules/no-useless-concat'),
'no-useless-mustaches': require('./rules/no-useless-mustaches'),
'no-useless-template-attributes': require('./rules/no-useless-template-attributes'),
'no-useless-v-bind': require('./rules/no-useless-v-bind'),
'no-v-for-template-key-on-child': require('./rules/no-v-for-template-key-on-child'),
'no-v-for-template-key': require('./rules/no-v-for-template-key'),
'no-v-html': require('./rules/no-v-html'),
'no-v-model-argument': require('./rules/no-v-model-argument'),
'no-v-text-v-html-on-component': require('./rules/no-v-text-v-html-on-component'),
'no-v-text': require('./rules/no-v-text'),
'no-watch-after-await': require('./rules/no-watch-after-await'),
'object-curly-newline': require('./rules/object-curly-newline'),
'object-curly-spacing': require('./rules/object-curly-spacing'),
'object-property-newline': require('./rules/object-property-newline'),
'object-shorthand': require('./rules/object-shorthand'),
'one-component-per-file': require('./rules/one-component-per-file'),
'operator-linebreak': require('./rules/operator-linebreak'),
'order-in-components': require('./rules/order-in-components'),
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'),
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
'prefer-template': require('./rules/prefer-template'),
'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'),
'prop-name-casing': require('./rules/prop-name-casing'),
'quote-props': require('./rules/quote-props'),
'require-component-is': require('./rules/require-component-is'),
'require-default-prop': require('./rules/require-default-prop'),
'require-direct-export': require('./rules/require-direct-export'),
'require-emit-validator': require('./rules/require-emit-validator'),
'require-explicit-emits': require('./rules/require-explicit-emits'),
'require-expose': require('./rules/require-expose'),
'require-name-property': require('./rules/require-name-property'),
'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
'require-prop-types': require('./rules/require-prop-types'),
'require-render-return': require('./rules/require-render-return'),
'require-slots-as-functions': require('./rules/require-slots-as-functions'),
'require-toggle-inside-transition': require('./rules/require-toggle-inside-transition'),
'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'),
'return-in-emits-validator': require('./rules/return-in-emits-validator'),
'script-indent': require('./rules/script-indent'),
'script-setup-uses-vars': require('./rules/script-setup-uses-vars'),
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
'sort-keys': require('./rules/sort-keys'),
'space-in-parens': require('./rules/space-in-parens'),
'space-infix-ops': require('./rules/space-infix-ops'),
'space-unary-ops': require('./rules/space-unary-ops'),
'static-class-names-order': require('./rules/static-class-names-order'),
'template-curly-spacing': require('./rules/template-curly-spacing'),
'this-in-template': require('./rules/this-in-template'),
'use-v-on-exact': require('./rules/use-v-on-exact'),
'v-bind-style': require('./rules/v-bind-style'),
'v-for-delimiter-style': require('./rules/v-for-delimiter-style'),
'v-on-event-hyphenation': require('./rules/v-on-event-hyphenation'),
'v-on-function-call': require('./rules/v-on-function-call'),
'v-on-style': require('./rules/v-on-style'),
'v-slot-style': require('./rules/v-slot-style'),
'valid-define-emits': require('./rules/valid-define-emits'),
'valid-define-props': require('./rules/valid-define-props'),
'valid-next-tick': require('./rules/valid-next-tick'),
'valid-template-root': require('./rules/valid-template-root'),
'valid-v-bind-sync': require('./rules/valid-v-bind-sync'),
'valid-v-bind': require('./rules/valid-v-bind'),
'valid-v-cloak': require('./rules/valid-v-cloak'),
'valid-v-else-if': require('./rules/valid-v-else-if'),
'valid-v-else': require('./rules/valid-v-else'),
'valid-v-for': require('./rules/valid-v-for'),
'valid-v-html': require('./rules/valid-v-html'),
'valid-v-if': require('./rules/valid-v-if'),
'valid-v-is': require('./rules/valid-v-is'),
'valid-v-memo': require('./rules/valid-v-memo'),
'valid-v-model': require('./rules/valid-v-model'),
'valid-v-on': require('./rules/valid-v-on'),
'valid-v-once': require('./rules/valid-v-once'),
'valid-v-pre': require('./rules/valid-v-pre'),
'valid-v-show': require('./rules/valid-v-show'),
'valid-v-slot': require('./rules/valid-v-slot'),
'valid-v-text': require('./rules/valid-v-text')
},
configs: {
base: require('./configs/base'),
essential: require('./configs/essential'),
'no-layout-rules': require('./configs/no-layout-rules'),
recommended: require('./configs/recommended'),
'strongly-recommended': require('./configs/strongly-recommended'),
'vue3-essential': require('./configs/vue3-essential'),
'vue3-recommended': require('./configs/vue3-recommended'),
'vue3-strongly-recommended': require('./configs/vue3-strongly-recommended')
},
processors: {
'.vue': require('./processor')
},
environments: {
'setup-compiler-macros': {
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly'
}
}
}
}

165
app_vue/node_modules/eslint-plugin-vue/lib/processor.js generated vendored Normal file
View File

@ -0,0 +1,165 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/
'use strict'
/**
* @typedef {import('eslint').Linter.LintMessage} LintMessage
*/
/**
* @typedef {object} GroupState
* @property {Set<string>} GroupState.disableAllKeys
* @property {Map<string, string[]>} GroupState.disableRuleKeys
*/
module.exports = {
/** @param {string} code */
preprocess(code) {
return [code]
},
/**
* @param {LintMessage[][]} messages
* @returns {LintMessage[]}
*/
postprocess(messages) {
const state = {
/** @type {GroupState} */
block: {
disableAllKeys: new Set(),
disableRuleKeys: new Map()
},
/** @type {GroupState} */
line: {
disableAllKeys: new Set(),
disableRuleKeys: new Map()
}
}
/** @type {string[]} */
const usedDisableDirectiveKeys = []
/** @type {Map<string,LintMessage>} */
const unusedDisableDirectiveReports = new Map()
// Filter messages which are in disabled area.
const filteredMessages = messages[0].filter((message) => {
if (message.ruleId === 'vue/comment-directive') {
const directiveType = message.messageId
const data = message.message.split(' ')
switch (directiveType) {
case 'disableBlock':
state.block.disableAllKeys.add(data[1])
break
case 'disableLine':
state.line.disableAllKeys.add(data[1])
break
case 'enableBlock':
state.block.disableAllKeys.clear()
break
case 'enableLine':
state.line.disableAllKeys.clear()
break
case 'disableBlockRule':
addDisableRule(state.block.disableRuleKeys, data[1], data[2])
break
case 'disableLineRule':
addDisableRule(state.line.disableRuleKeys, data[1], data[2])
break
case 'enableBlockRule':
state.block.disableRuleKeys.delete(data[1])
break
case 'enableLineRule':
state.line.disableRuleKeys.delete(data[1])
break
case 'clear':
state.block.disableAllKeys.clear()
state.block.disableRuleKeys.clear()
state.line.disableAllKeys.clear()
state.line.disableRuleKeys.clear()
break
default:
// unused eslint-disable comments report
unusedDisableDirectiveReports.set(messageToKey(message), message)
break
}
return false
} else {
const disableDirectiveKeys = []
if (state.block.disableAllKeys.size) {
disableDirectiveKeys.push(...state.block.disableAllKeys)
}
if (state.line.disableAllKeys.size) {
disableDirectiveKeys.push(...state.line.disableAllKeys)
}
if (message.ruleId) {
const block = state.block.disableRuleKeys.get(message.ruleId)
if (block) {
disableDirectiveKeys.push(...block)
}
const line = state.line.disableRuleKeys.get(message.ruleId)
if (line) {
disableDirectiveKeys.push(...line)
}
}
if (disableDirectiveKeys.length) {
// Store used eslint-disable comment key
usedDisableDirectiveKeys.push(...disableDirectiveKeys)
return false
} else {
return true
}
}
})
if (unusedDisableDirectiveReports.size) {
for (const key of usedDisableDirectiveKeys) {
// Remove used eslint-disable comments
unusedDisableDirectiveReports.delete(key)
}
// Reports unused eslint-disable comments
filteredMessages.push(...unusedDisableDirectiveReports.values())
filteredMessages.sort(compareLocations)
}
return filteredMessages
},
supportsAutofix: true
}
/**
* @param {Map<string, string[]>} disableRuleKeys
* @param {string} rule
* @param {string} key
*/
function addDisableRule(disableRuleKeys, rule, key) {
let keys = disableRuleKeys.get(rule)
if (keys) {
keys.push(key)
} else {
keys = [key]
disableRuleKeys.set(rule, keys)
}
}
/**
* @param {LintMessage} message
* @returns {string} message key
*/
function messageToKey(message) {
return `line:${message.line},column${
// -1 because +1 by ESLint's `report-translator`.
message.column - 1
}`
}
/**
* Compares the locations of two objects in a source file
* @param {Position} itemA The first object
* @param {Position} itemB The second object
* @returns {number} A value less than 1 if itemA appears before itemB in the source file, greater than 1 if
* itemA appears after itemB in the source file, or 0 if itemA and itemB have the same location.
*/
function compareLocations(itemA, itemB) {
return itemA.line - itemB.line || itemA.column - itemB.column
}

View File

@ -0,0 +1,11 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('array-bracket-newline', {
skipDynamicArguments: true
})

View File

@ -0,0 +1,11 @@
/**
* @author Toru Nagashima
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('array-bracket-spacing', {
skipDynamicArguments: true
})

View File

@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('arrow-spacing')

View File

@ -0,0 +1,128 @@
/**
* @fileoverview Define a style for the props casing in templates.
* @author Armano
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
const svgAttributes = require('../utils/svg-attributes-weird-case.json')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'enforce attribute naming style on custom components in template',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/attribute-hyphenation.html'
},
fixable: 'code',
schema: [
{
enum: ['always', 'never']
},
{
type: 'object',
properties: {
ignore: {
type: 'array',
items: {
allOf: [
{ type: 'string' },
{ not: { type: 'string', pattern: ':exit$' } },
{ not: { type: 'string', pattern: '^\\s*$' } }
]
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
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'].concat(
svgAttributes
)
if (optionsPayload && optionsPayload.ignore) {
ignoredAttributes = ignoredAttributes.concat(optionsPayload.ignore)
}
const caseConverter = casing.getExactConverter(
useHyphenated ? 'kebab-case' : 'camelCase'
)
/**
* @param {VDirective | VAttribute} node
* @param {string} name
*/
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)))
})
}
/**
* @param {string} value
*/
function isIgnoredAttribute(value) {
const isIgnored = ignoredAttributes.some((attr) => {
return value.includes(attr)
})
if (isIgnored) {
return true
}
return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.defineTemplateBodyVisitor(context, {
VAttribute(node) {
if (
!utils.isCustomComponent(node.parent.parent) &&
node.parent.parent.name !== 'slot'
)
return
const name = !node.directive
? node.key.rawName
: node.key.name.name === 'bind'
? node.key.argument &&
node.key.argument.type === 'VIdentifier' &&
node.key.argument.rawName
: /* otherwise */ false
if (!name || isIgnoredAttribute(name)) return
reportIssue(node, name)
}
})
}
}

View File

@ -0,0 +1,399 @@
/**
* @fileoverview enforce ordering of attributes
* @author Erin Depew
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
/**
* @typedef { VDirective & { key: VDirectiveKey & { name: VIdentifier & { name: 'bind' } } } } VBindDirective
*/
const ATTRS = {
DEFINITION: 'DEFINITION',
LIST_RENDERING: 'LIST_RENDERING',
CONDITIONALS: 'CONDITIONALS',
RENDER_MODIFIERS: 'RENDER_MODIFIERS',
GLOBAL: 'GLOBAL',
UNIQUE: 'UNIQUE',
SLOT: 'SLOT',
TWO_WAY_BINDING: 'TWO_WAY_BINDING',
OTHER_DIRECTIVES: 'OTHER_DIRECTIVES',
OTHER_ATTR: 'OTHER_ATTR',
EVENTS: 'EVENTS',
CONTENT: 'CONTENT'
}
/**
* Check whether the given attribute is `v-bind` directive.
* @param {VAttribute | VDirective | undefined | null} node
* @returns { node is VBindDirective }
*/
function isVBind(node) {
return Boolean(node && node.directive && node.key.name.name === 'bind')
}
/**
* Check whether the given attribute is `v-model` directive.
* @param {VAttribute | VDirective | undefined | null} node
* @returns { node is VDirective }
*/
function isVModel(node) {
return Boolean(node && node.directive && node.key.name.name === 'model')
}
/**
* Check whether the given attribute is plain attribute.
* @param {VAttribute | VDirective | undefined | null} node
* @returns { node is VAttribute }
*/
function isVAttribute(node) {
return Boolean(node && !node.directive)
}
/**
* Check whether the given attribute is plain attribute, `v-bind` directive or `v-model` directive.
* @param {VAttribute | VDirective | undefined | null} node
* @returns { node is VAttribute }
*/
function isVAttributeOrVBindOrVModel(node) {
return isVAttribute(node) || isVBind(node) || isVModel(node)
}
/**
* Check whether the given attribute is `v-bind="..."` directive.
* @param {VAttribute | VDirective | undefined | null} node
* @returns { node is VBindDirective }
*/
function isVBindObject(node) {
return isVBind(node) && node.key.argument == null
}
/**
* @param {VAttribute | VDirective} attribute
* @param {SourceCode} sourceCode
*/
function getAttributeName(attribute, sourceCode) {
if (attribute.directive) {
if (isVBind(attribute)) {
return attribute.key.argument
? sourceCode.getText(attribute.key.argument)
: ''
} else {
return getDirectiveKeyName(attribute.key, sourceCode)
}
} else {
return attribute.key.name
}
}
/**
* @param {VDirectiveKey} directiveKey
* @param {SourceCode} sourceCode
*/
function getDirectiveKeyName(directiveKey, sourceCode) {
let text = `v-${directiveKey.name.name}`
if (directiveKey.argument) {
text += `:${sourceCode.getText(directiveKey.argument)}`
}
for (const modifier of directiveKey.modifiers) {
text += `.${modifier.name}`
}
return text
}
/**
* @param {VAttribute | VDirective} attribute
*/
function getAttributeType(attribute) {
let propName
if (attribute.directive) {
if (!isVBind(attribute)) {
const name = attribute.key.name.name
if (name === 'for') {
return ATTRS.LIST_RENDERING
} else if (
name === 'if' ||
name === 'else-if' ||
name === 'else' ||
name === 'show' ||
name === 'cloak'
) {
return ATTRS.CONDITIONALS
} else if (name === 'pre' || name === 'once') {
return ATTRS.RENDER_MODIFIERS
} else if (name === 'model') {
return ATTRS.TWO_WAY_BINDING
} else if (name === 'on') {
return ATTRS.EVENTS
} else if (name === 'html' || name === 'text') {
return ATTRS.CONTENT
} else if (name === 'slot') {
return ATTRS.SLOT
} else if (name === 'is') {
return ATTRS.DEFINITION
} else {
return ATTRS.OTHER_DIRECTIVES
}
}
propName =
attribute.key.argument && attribute.key.argument.type === 'VIdentifier'
? attribute.key.argument.rawName
: ''
} else {
propName = attribute.key.name
}
if (propName === 'is') {
return ATTRS.DEFINITION
} else if (propName === 'id') {
return ATTRS.GLOBAL
} else if (propName === 'ref' || propName === 'key') {
return ATTRS.UNIQUE
} else if (propName === 'slot' || propName === 'slot-scope') {
return ATTRS.SLOT
} else {
return ATTRS.OTHER_ATTR
}
}
/**
* @param {VAttribute | VDirective} attribute
* @param { { [key: string]: number } } attributePosition
* @returns {number | null} If the value is null, the order is omitted. Do not force the order.
*/
function getPosition(attribute, attributePosition) {
const attributeType = getAttributeType(attribute)
return attributePosition[attributeType] != null
? attributePosition[attributeType]
: null
}
/**
* @param {VAttribute | VDirective} prevNode
* @param {VAttribute | VDirective} currNode
* @param {SourceCode} sourceCode
*/
function isAlphabetical(prevNode, currNode, sourceCode) {
const prevName = getAttributeName(prevNode, sourceCode)
const currName = getAttributeName(currNode, sourceCode)
if (prevName === currName) {
const prevIsBind = isVBind(prevNode)
const currIsBind = isVBind(currNode)
return prevIsBind <= currIsBind
}
return prevName < currName
}
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
function create(context) {
const sourceCode = context.getSourceCode()
let attributeOrder = [
ATTRS.DEFINITION,
ATTRS.LIST_RENDERING,
ATTRS.CONDITIONALS,
ATTRS.RENDER_MODIFIERS,
ATTRS.GLOBAL,
[ATTRS.UNIQUE, ATTRS.SLOT],
ATTRS.TWO_WAY_BINDING,
ATTRS.OTHER_DIRECTIVES,
ATTRS.OTHER_ATTR,
ATTRS.EVENTS,
ATTRS.CONTENT
]
if (context.options[0] && context.options[0].order) {
attributeOrder = context.options[0].order
}
const alphabetical = Boolean(
context.options[0] && context.options[0].alphabetical
)
/** @type { { [key: string]: number } } */
const attributePosition = {}
attributeOrder.forEach((item, i) => {
if (Array.isArray(item)) {
for (const attr of item) {
attributePosition[attr] = i
}
} else attributePosition[item] = i
})
/**
* @param {VAttribute | VDirective} node
* @param {VAttribute | VDirective} previousNode
*/
function reportIssue(node, previousNode) {
const currentNode = sourceCode.getText(node.key)
const prevNode = sourceCode.getText(previousNode.key)
context.report({
node,
message: `Attribute "${currentNode}" should go before "${prevNode}".`,
data: {
currentNode
},
fix(fixer) {
const attributes = node.parent.attributes
/** @type { (node: VAttribute | VDirective | undefined) => boolean } */
let isMoveUp
if (isVBindObject(node)) {
// prev, v-bind:foo, v-bind -> v-bind:foo, v-bind, prev
isMoveUp = isVAttributeOrVBindOrVModel
} else if (isVAttributeOrVBindOrVModel(node)) {
// prev, v-bind, v-bind:foo -> v-bind, v-bind:foo, prev
isMoveUp = isVBindObject
} else {
isMoveUp = () => false
}
const previousNodes = attributes.slice(
attributes.indexOf(previousNode),
attributes.indexOf(node)
)
const moveNodes = [node]
for (const node of previousNodes) {
if (isMoveUp(node)) {
moveNodes.unshift(node)
} else {
moveNodes.push(node)
}
}
return moveNodes.map((moveNode, index) => {
const text = sourceCode.getText(moveNode)
return fixer.replaceText(previousNodes[index] || node, text)
})
}
})
}
return utils.defineTemplateBodyVisitor(context, {
VStartTag(node) {
const attributeAndPositions = getAttributeAndPositionList(node)
if (attributeAndPositions.length <= 1) {
return
}
let { attr: previousNode, position: previousPosition } =
attributeAndPositions[0]
for (let index = 1; index < attributeAndPositions.length; index++) {
const { attr, position } = attributeAndPositions[index]
let valid = previousPosition <= position
if (valid && alphabetical && previousPosition === position) {
valid = isAlphabetical(previousNode, attr, sourceCode)
}
if (valid) {
previousNode = attr
previousPosition = position
} else {
reportIssue(attr, previousNode)
}
}
}
})
/**
* @param {VStartTag} node
* @returns { { attr: ( VAttribute | VDirective ), position: number }[] }
*/
function getAttributeAndPositionList(node) {
const attributes = node.attributes.filter((node, index, attributes) => {
if (
isVBindObject(node) &&
(isVAttributeOrVBindOrVModel(attributes[index - 1]) ||
isVAttributeOrVBindOrVModel(attributes[index + 1]))
) {
// In Vue 3, ignore `v-bind="object"`, which is
// a pair of `v-bind:foo="..."` and `v-bind="object"` and
// a pair of `v-model="..."` and `v-bind="object"`,
// because changing the order behaves differently.
return false
}
return true
})
const results = []
for (let index = 0; index < attributes.length; index++) {
const attr = attributes[index]
const position = getPositionFromAttrIndex(index)
if (position == null) {
// The omitted order is skipped.
continue
}
results.push({ attr, position })
}
return results
/**
* @param {number} index
* @returns {number | null}
*/
function getPositionFromAttrIndex(index) {
const node = attributes[index]
if (isVBindObject(node)) {
// node is `v-bind ="object"` syntax
// In Vue 3, if change the order of `v-bind:foo="..."`, `v-model="..."` and `v-bind="object"`,
// the behavior will be different, so adjust so that there is no change in behavior.
const len = attributes.length
for (let nextIndex = index + 1; nextIndex < len; nextIndex++) {
const next = attributes[nextIndex]
if (isVAttributeOrVBindOrVModel(next) && !isVBindObject(next)) {
// It is considered to be in the same order as the next bind prop node.
return getPositionFromAttrIndex(nextIndex)
}
}
}
return getPosition(node, attributePosition)
}
}
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce order of attributes',
categories: ['vue3-recommended', 'recommended'],
url: 'https://eslint.vuejs.org/rules/attributes-order.html'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
order: {
type: 'array',
items: {
anyOf: [
{ enum: Object.values(ATTRS) },
{
type: 'array',
items: {
enum: Object.values(ATTRS),
uniqueItems: true,
additionalItems: false
}
}
]
},
uniqueItems: true,
additionalItems: false
},
alphabetical: { type: 'boolean' }
},
additionalProperties: false
}
]
},
create
}

View File

@ -0,0 +1,224 @@
/**
* @fileoverview Disallow use other than available `lang`
* @author Yosuke Ota
*/
'use strict'
const utils = require('../utils')
/**
* @typedef {object} BlockOptions
* @property {Set<string>} lang
* @property {boolean} allowNoLang
*/
/**
* @typedef { { [element: string]: BlockOptions | undefined } } Options
*/
/**
* @typedef {object} UserBlockOptions
* @property {string[] | string} [lang]
* @property {boolean} [allowNoLang]
*/
/**
* @typedef { { [element: string]: UserBlockOptions | undefined } } UserOptions
*/
/**
* https://vuejs.github.io/vetur/guide/highlighting.html
* <template lang="html"></template>
* <style lang="css"></style>
* <script lang="js"></script>
* <script lang="javascript"></script>
* @type {Record<string, string[] | undefined>}
*/
const DEFAULT_LANGUAGES = {
template: ['html'],
style: ['css'],
script: ['js', 'javascript']
}
/**
* @param {NonNullable<BlockOptions['lang']>} lang
*/
function getAllowsLangPhrase(lang) {
const langs = [...lang].map((s) => `"${s}"`)
switch (langs.length) {
case 1:
return langs[0]
default:
return `${langs.slice(0, -1).join(', ')}, and ${langs[langs.length - 1]}`
}
}
/**
* Normalizes a given option.
* @param {string} blockName The block name.
* @param { UserBlockOptions } option An option to parse.
* @returns {BlockOptions} Normalized option.
*/
function normalizeOption(blockName, option) {
const lang = new Set(
Array.isArray(option.lang) ? option.lang : option.lang ? [option.lang] : []
)
let hasDefault = false
for (const def of DEFAULT_LANGUAGES[blockName] || []) {
if (lang.has(def)) {
lang.delete(def)
hasDefault = true
}
}
if (lang.size === 0) {
return {
lang,
allowNoLang: true
}
}
return {
lang,
allowNoLang: hasDefault || Boolean(option.allowNoLang)
}
}
/**
* Normalizes a given options.
* @param { UserOptions } options An option to parse.
* @returns {Options} Normalized option.
*/
function normalizeOptions(options) {
if (!options) {
return {}
}
/** @type {Options} */
const normalized = {}
for (const blockName of Object.keys(options)) {
const value = options[blockName]
if (value) {
normalized[blockName] = normalizeOption(blockName, value)
}
}
return normalized
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow use other than available `lang`',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/block-lang.html'
},
schema: [
{
type: 'object',
patternProperties: {
'^(?:\\S+)$': {
oneOf: [
{
type: 'object',
properties: {
lang: {
anyOf: [
{ type: 'string' },
{
type: 'array',
items: {
type: 'string'
},
uniqueItems: true,
additionalItems: false
}
]
},
allowNoLang: { type: 'boolean' }
},
additionalProperties: false
}
]
}
},
minProperties: 1,
additionalProperties: false
}
],
messages: {
expected:
"Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'.",
missing: "The 'lang' attribute of '<{{tag}}>' is missing.",
unexpected: "Do not specify the 'lang' attribute of '<{{tag}}>'.",
useOrNot:
"Only {{allows}} can be used for the 'lang' attribute of '<{{tag}}>'. Or, not specifying the `lang` attribute is allowed.",
unexpectedDefault:
"Do not explicitly specify the default language for the 'lang' attribute of '<{{tag}}>'."
}
},
/** @param {RuleContext} context */
create(context) {
const options = normalizeOptions(
context.options[0] || {
script: { allowNoLang: true },
template: { allowNoLang: true },
style: { allowNoLang: true }
}
)
if (!Object.keys(options).length) {
// empty
return {}
}
/**
* @param {VElement} element
* @returns {void}
*/
function verify(element) {
const tag = element.name
const option = options[tag]
if (!option) {
return
}
const lang = utils.getAttribute(element, 'lang')
if (lang == null || lang.value == null) {
if (!option.allowNoLang) {
context.report({
node: element.startTag,
messageId: 'missing',
data: {
tag
}
})
}
return
}
if (!option.lang.has(lang.value.value)) {
let messageId
if (!option.allowNoLang) {
messageId = 'expected'
} else if (option.lang.size === 0) {
if ((DEFAULT_LANGUAGES[tag] || []).includes(lang.value.value)) {
messageId = 'unexpectedDefault'
} else {
messageId = 'unexpected'
}
} else {
messageId = 'useOrNot'
}
context.report({
node: lang,
messageId,
data: {
tag,
allows: getAllowsLangPhrase(option.lang)
}
})
}
}
return utils.defineDocumentVisitor(context, {
'VDocumentFragment > VElement': verify
})
}
}

View File

@ -0,0 +1,11 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('block-spacing', {
skipDynamicArguments: true
})

View File

@ -0,0 +1,375 @@
/**
* @fileoverview Enforce line breaks style after opening and before closing block-level tags.
* @author Yosuke Ota
*/
'use strict'
const utils = require('../utils')
/**
* @typedef { 'always' | 'never' | 'consistent' | 'ignore' } OptionType
* @typedef { { singleline?: OptionType, multiline?: OptionType, maxEmptyLines?: number } } ContentsOptions
* @typedef { ContentsOptions & { blocks?: { [element: string]: ContentsOptions } } } Options
* @typedef { Required<ContentsOptions> } ArgsOptions
*/
/**
* @param {string} text Source code as a string.
* @returns {number}
*/
function getLinebreakCount(text) {
return text.split(/\r\n|[\r\n\u2028\u2029]/gu).length - 1
}
/**
* @param {number} lineBreaks
*/
function getPhrase(lineBreaks) {
switch (lineBreaks) {
case 1:
return '1 line break'
default:
return `${lineBreaks} line breaks`
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const ENUM_OPTIONS = { enum: ['always', 'never', 'consistent', 'ignore'] }
module.exports = {
meta: {
type: 'layout',
docs: {
description:
'enforce line breaks after opening and before closing block-level tags',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/block-tag-newline.html'
},
fixable: 'whitespace',
schema: [
{
type: 'object',
properties: {
singleline: ENUM_OPTIONS,
multiline: ENUM_OPTIONS,
maxEmptyLines: { type: 'number', minimum: 0 },
blocks: {
type: 'object',
patternProperties: {
'^(?:\\S+)$': {
type: 'object',
properties: {
singleline: ENUM_OPTIONS,
multiline: ENUM_OPTIONS,
maxEmptyLines: { type: 'number', minimum: 0 }
},
additionalProperties: false
}
},
additionalProperties: false
}
},
additionalProperties: false
}
],
messages: {
unexpectedOpeningLinebreak:
"There should be no line break after '<{{tag}}>'.",
unexpectedClosingLinebreak:
"There should be no line break before '</{{tag}}>'.",
expectedOpeningLinebreak:
"Expected {{expected}} after '<{{tag}}>', but {{actual}} found.",
expectedClosingLinebreak:
"Expected {{expected}} before '</{{tag}}>', but {{actual}} found.",
missingOpeningLinebreak: "A line break is required after '<{{tag}}>'.",
missingClosingLinebreak: "A line break is required before '</{{tag}}>'."
}
},
/** @param {RuleContext} context */
create(context) {
const df =
context.parserServices.getDocumentFragment &&
context.parserServices.getDocumentFragment()
if (!df) {
return {}
}
const sourceCode = context.getSourceCode()
/**
* @param {VStartTag} startTag
* @param {string} beforeText
* @param {number} beforeLinebreakCount
* @param {'always' | 'never'} beforeOption
* @param {number} maxEmptyLines
* @returns {void}
*/
function verifyBeforeSpaces(
startTag,
beforeText,
beforeLinebreakCount,
beforeOption,
maxEmptyLines
) {
if (beforeOption === 'always') {
if (beforeLinebreakCount === 0) {
context.report({
loc: {
start: startTag.loc.end,
end: startTag.loc.end
},
messageId: 'missingOpeningLinebreak',
data: { tag: startTag.parent.name },
fix(fixer) {
return fixer.insertTextAfter(startTag, '\n')
}
})
} else if (maxEmptyLines < beforeLinebreakCount - 1) {
context.report({
loc: {
start: startTag.loc.end,
end: sourceCode.getLocFromIndex(
startTag.range[1] + beforeText.length
)
},
messageId: 'expectedOpeningLinebreak',
data: {
tag: startTag.parent.name,
expected: getPhrase(maxEmptyLines + 1),
actual: getPhrase(beforeLinebreakCount)
},
fix(fixer) {
return fixer.replaceTextRange(
[startTag.range[1], startTag.range[1] + beforeText.length],
'\n'.repeat(maxEmptyLines + 1)
)
}
})
}
} else {
if (beforeLinebreakCount > 0) {
context.report({
loc: {
start: startTag.loc.end,
end: sourceCode.getLocFromIndex(
startTag.range[1] + beforeText.length
)
},
messageId: 'unexpectedOpeningLinebreak',
data: { tag: startTag.parent.name },
fix(fixer) {
return fixer.removeRange([
startTag.range[1],
startTag.range[1] + beforeText.length
])
}
})
}
}
}
/**
* @param {VEndTag} endTag
* @param {string} afterText
* @param {number} afterLinebreakCount
* @param {'always' | 'never'} afterOption
* @param {number} maxEmptyLines
* @returns {void}
*/
function verifyAfterSpaces(
endTag,
afterText,
afterLinebreakCount,
afterOption,
maxEmptyLines
) {
if (afterOption === 'always') {
if (afterLinebreakCount === 0) {
context.report({
loc: {
start: endTag.loc.start,
end: endTag.loc.start
},
messageId: 'missingClosingLinebreak',
data: { tag: endTag.parent.name },
fix(fixer) {
return fixer.insertTextBefore(endTag, '\n')
}
})
} else if (maxEmptyLines < afterLinebreakCount - 1) {
context.report({
loc: {
start: sourceCode.getLocFromIndex(
endTag.range[0] - afterText.length
),
end: endTag.loc.start
},
messageId: 'expectedClosingLinebreak',
data: {
tag: endTag.parent.name,
expected: getPhrase(maxEmptyLines + 1),
actual: getPhrase(afterLinebreakCount)
},
fix(fixer) {
return fixer.replaceTextRange(
[endTag.range[0] - afterText.length, endTag.range[0]],
'\n'.repeat(maxEmptyLines + 1)
)
}
})
}
} else {
if (afterLinebreakCount > 0) {
context.report({
loc: {
start: sourceCode.getLocFromIndex(
endTag.range[0] - afterText.length
),
end: endTag.loc.start
},
messageId: 'unexpectedOpeningLinebreak',
data: { tag: endTag.parent.name },
fix(fixer) {
return fixer.removeRange([
endTag.range[0] - afterText.length,
endTag.range[0]
])
}
})
}
}
}
/**
* @param {VElement} element
* @param {ArgsOptions} options
* @returns {void}
*/
function verifyElement(element, options) {
const { startTag, endTag } = element
if (startTag.selfClosing || endTag == null) {
return
}
const text = sourceCode.text.slice(startTag.range[1], endTag.range[0])
const trimText = text.trim()
if (!trimText) {
return
}
const option =
options.multiline === options.singleline
? options.singleline
: /[\n\r\u2028\u2029]/u.test(text.trim())
? options.multiline
: options.singleline
if (option === 'ignore') {
return
}
const beforeText = /** @type {RegExpExecArray} */ (/^\s*/u.exec(text))[0]
const afterText = /** @type {RegExpExecArray} */ (/\s*$/u.exec(text))[0]
const beforeLinebreakCount = getLinebreakCount(beforeText)
const afterLinebreakCount = getLinebreakCount(afterText)
/** @type {'always' | 'never'} */
let beforeOption
/** @type {'always' | 'never'} */
let afterOption
if (option === 'always' || option === 'never') {
beforeOption = option
afterOption = option
} else {
// consistent
if (beforeLinebreakCount > 0 === afterLinebreakCount > 0) {
return
}
beforeOption = 'always'
afterOption = 'always'
}
verifyBeforeSpaces(
startTag,
beforeText,
beforeLinebreakCount,
beforeOption,
options.maxEmptyLines
)
verifyAfterSpaces(
endTag,
afterText,
afterLinebreakCount,
afterOption,
options.maxEmptyLines
)
}
/**
* Normalizes a given option value.
* @param { Options | undefined } option An option value to parse.
* @returns { (element: VElement) => void } Verify function.
*/
function normalizeOptionValue(option) {
if (!option) {
return normalizeOptionValue({})
}
/** @type {ContentsOptions} */
const contentsOptions = option
/** @type {ArgsOptions} */
const options = {
singleline: contentsOptions.singleline || 'consistent',
multiline: contentsOptions.multiline || 'always',
maxEmptyLines: contentsOptions.maxEmptyLines || 0
}
const { blocks } = option
if (!blocks) {
return (element) => verifyElement(element, options)
}
return (element) => {
const { name } = element
const elementsOptions = blocks[name]
if (!elementsOptions) {
verifyElement(element, options)
} else {
normalizeOptionValue({
singleline: elementsOptions.singleline || options.singleline,
multiline: elementsOptions.multiline || options.multiline,
maxEmptyLines:
elementsOptions.maxEmptyLines != null
? elementsOptions.maxEmptyLines
: options.maxEmptyLines
})(element)
}
}
}
const documentFragment = df
const verify = normalizeOptionValue(context.options[0])
/**
* @returns {VElement[]}
*/
function getTopLevelHTMLElements() {
return documentFragment.children.filter(utils.isVElement)
}
return utils.defineTemplateBodyVisitor(
context,
{},
{
/** @param {Program} node */
Program(node) {
if (utils.hasInvalidEOF(node)) {
return
}
for (const element of getTopLevelHTMLElements()) {
verify(element)
}
}
}
)
}
}

View File

@ -0,0 +1,11 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('brace-style', {
skipDynamicArguments: true
})

View File

@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('camelcase')

View File

@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('comma-dangle')

View File

@ -0,0 +1,13 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('comma-spacing', {
skipDynamicArguments: true,
skipDynamicArgumentsReport: true,
applyDocument: true
})

View File

@ -0,0 +1,20 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('comma-style', {
create(_context, { coreHandlers }) {
return {
VSlotScopeExpression(node) {
if (coreHandlers.FunctionExpression) {
// @ts-expect-error -- Process params of VSlotScopeExpression as FunctionExpression.
coreHandlers.FunctionExpression(node)
}
}
}
}
})

View File

@ -0,0 +1,366 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/
/* eslint-disable eslint-plugin/report-message-format */
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
/**
* @typedef {object} RuleAndLocation
* @property {string} RuleAndLocation.ruleId
* @property {number} RuleAndLocation.index
* @property {string} [RuleAndLocation.key]
*/
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
const COMMENT_DIRECTIVE_B = /^\s*(eslint-(?:en|dis)able)(?:\s+|$)/
const COMMENT_DIRECTIVE_L = /^\s*(eslint-disable(?:-next)?-line)(?:\s+|$)/
/**
* Remove the ignored part from a given directive comment and trim it.
* @param {string} value The comment text to strip.
* @returns {string} The stripped text.
*/
function stripDirectiveComment(value) {
return value.split(/\s-{2,}\s/u)[0]
}
/**
* Parse a given comment.
* @param {RegExp} pattern The RegExp pattern to parse.
* @param {string} comment The comment value to parse.
* @returns {({type:string,rules:RuleAndLocation[]})|null} The parsing result.
*/
function parse(pattern, comment) {
const text = stripDirectiveComment(comment)
const match = pattern.exec(text)
if (match == null) {
return null
}
const type = match[1]
/** @type {RuleAndLocation[]} */
const rules = []
const rulesRe = /([^,\s]+)[,\s]*/g
let startIndex = match[0].length
rulesRe.lastIndex = startIndex
let res
while ((res = rulesRe.exec(text))) {
const ruleId = res[1].trim()
rules.push({
ruleId,
index: startIndex
})
startIndex = rulesRe.lastIndex
}
return { type, rules }
}
/**
* Enable rules.
* @param {RuleContext} context The rule context.
* @param {{line:number,column:number}} loc The location information to enable.
* @param { 'block' | 'line' } group The group to enable.
* @param {string | null} rule The rule ID to enable.
* @returns {void}
*/
function enable(context, loc, group, rule) {
if (!rule) {
context.report({
loc,
messageId: group === 'block' ? 'enableBlock' : 'enableLine'
})
} else {
context.report({
loc,
messageId: group === 'block' ? 'enableBlockRule' : 'enableLineRule',
data: { rule }
})
}
}
/**
* Disable rules.
* @param {RuleContext} context The rule context.
* @param {{line:number,column:number}} loc The location information to disable.
* @param { 'block' | 'line' } group The group to disable.
* @param {string | null} rule The rule ID to disable.
* @param {string} key The disable directive key.
* @returns {void}
*/
function disable(context, loc, group, rule, key) {
if (!rule) {
context.report({
loc,
messageId: group === 'block' ? 'disableBlock' : 'disableLine',
data: { key }
})
} else {
context.report({
loc,
messageId: group === 'block' ? 'disableBlockRule' : 'disableLineRule',
data: { rule, key }
})
}
}
/**
* 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.
* @param {boolean} reportUnusedDisableDirectives To report unused eslint-disable comments.
* @returns {void}
*/
function processBlock(context, comment, reportUnusedDisableDirectives) {
const parsed = parse(COMMENT_DIRECTIVE_B, comment.value)
if (parsed != null) {
if (parsed.type === 'eslint-disable') {
if (parsed.rules.length) {
const rules = reportUnusedDisableDirectives
? reportUnusedRules(context, comment, parsed.type, parsed.rules)
: parsed.rules
for (const rule of rules) {
disable(
context,
comment.loc.start,
'block',
rule.ruleId,
rule.key || '*'
)
}
} else {
const key = reportUnusedDisableDirectives
? reportUnused(context, comment, parsed.type)
: ''
disable(context, comment.loc.start, 'block', null, key)
}
} else {
if (parsed.rules.length) {
for (const rule of parsed.rules) {
enable(context, comment.loc.start, 'block', rule.ruleId)
}
} else {
enable(context, comment.loc.start, 'block', null)
}
}
}
}
/**
* 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.
* @param {boolean} reportUnusedDisableDirectives To report unused eslint-disable comments.
* @returns {void}
*/
function processLine(context, comment, reportUnusedDisableDirectives) {
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
if (parsed.rules.length) {
const rules = reportUnusedDisableDirectives
? reportUnusedRules(context, comment, parsed.type, parsed.rules)
: parsed.rules
for (const rule of rules) {
disable(context, { line, column }, 'line', rule.ruleId, rule.key || '')
enable(context, { line: line + 1, column }, 'line', rule.ruleId)
}
} else {
const key = reportUnusedDisableDirectives
? reportUnused(context, comment, parsed.type)
: ''
disable(context, { line, column }, 'line', null, key)
enable(context, { line: line + 1, column }, 'line', null)
}
}
}
/**
* Reports unused disable directive.
* Do not check the use of directives here. Filter the directives used with postprocess.
* @param {RuleContext} context The rule context.
* @param {Token} comment The comment token to report.
* @param {string} kind The comment directive kind.
* @returns {string} The report key
*/
function reportUnused(context, comment, kind) {
const loc = comment.loc
context.report({
loc,
messageId: 'unused',
data: { kind }
})
return locToKey(loc.start)
}
/**
* Reports unused disable directive rules.
* Do not check the use of directives here. Filter the directives used with postprocess.
* @param {RuleContext} context The rule context.
* @param {Token} comment The comment token to report.
* @param {string} kind The comment directive kind.
* @param {RuleAndLocation[]} rules To report rule.
* @returns { { ruleId: string, key: string }[] }
*/
function reportUnusedRules(context, comment, kind, rules) {
const sourceCode = context.getSourceCode()
const commentStart = comment.range[0] + 4 /* <!-- */
return rules.map((rule) => {
const start = sourceCode.getLocFromIndex(commentStart + rule.index)
const end = sourceCode.getLocFromIndex(
commentStart + rule.index + rule.ruleId.length
)
context.report({
loc: { start, end },
messageId: 'unusedRule',
data: { rule: rule.ruleId, kind }
})
return {
ruleId: rule.ruleId,
key: locToKey(start)
}
})
}
/**
* Gets the key of location
* @param {Position} location The location
* @returns {string} The key
*/
function locToKey(location) {
return `line:${location.line},column${location.column}`
}
/**
* Extracts the top-level elements in document fragment.
* @param {VDocumentFragment} documentFragment The document fragment.
* @returns {VElement[]} The top-level elements
*/
function extractTopLevelHTMLElements(documentFragment) {
return documentFragment.children.filter(utils.isVElement)
}
/**
* Extracts the top-level comments in document fragment.
* @param {VDocumentFragment} documentFragment The document fragment.
* @returns {Token[]} The top-level comments
*/
function extractTopLevelDocumentFragmentComments(documentFragment) {
const elements = extractTopLevelHTMLElements(documentFragment)
return documentFragment.comments.filter((comment) =>
elements.every(
(element) =>
comment.range[1] <= element.range[0] ||
element.range[1] <= comment.range[0]
)
)
}
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'support comment-directives in `<template>`', // eslint-disable-line eslint-plugin/require-meta-docs-description
categories: ['base'],
url: 'https://eslint.vuejs.org/rules/comment-directive.html'
},
schema: [
{
type: 'object',
properties: {
reportUnusedDisableDirectives: {
type: 'boolean'
}
},
additionalProperties: false
}
],
messages: {
disableBlock: '--block {{key}}',
enableBlock: '++block',
disableLine: '--line {{key}}',
enableLine: '++line',
disableBlockRule: '-block {{rule}} {{key}}',
enableBlockRule: '+block {{rule}}',
disableLineRule: '-line {{rule}} {{key}}',
enableLineRule: '+line {{rule}}',
clear: 'clear',
unused: 'Unused {{kind}} directive (no problems were reported).',
unusedRule:
"Unused {{kind}} directive (no problems were reported from '{{rule}}')."
}
},
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
create(context) {
const options = context.options[0] || {}
/** @type {boolean} */
const reportUnusedDisableDirectives = options.reportUnusedDisableDirectives
const documentFragment =
context.parserServices.getDocumentFragment &&
context.parserServices.getDocumentFragment()
return {
Program(node) {
if (node.templateBody) {
// Send directives to the post-process.
for (const comment of node.templateBody.comments) {
processBlock(context, comment, reportUnusedDisableDirectives)
processLine(context, comment, reportUnusedDisableDirectives)
}
// Send a clear mark to the post-process.
context.report({
loc: node.templateBody.loc.end,
messageId: 'clear'
})
}
if (documentFragment) {
// Send directives to the post-process.
for (const comment of extractTopLevelDocumentFragmentComments(
documentFragment
)) {
processBlock(context, comment, reportUnusedDisableDirectives)
processLine(context, comment, reportUnusedDisableDirectives)
}
// Send a clear mark to the post-process.
for (const element of extractTopLevelHTMLElements(documentFragment)) {
context.report({
loc: element.loc.end,
messageId: 'clear'
})
}
}
}
}
}
}

View File

@ -0,0 +1,299 @@
/**
* @author Yosuke Ota <https://github.com/ota-meshi>
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
/**
* @typedef { 'script-setup' | 'composition' | 'composition-vue2' | 'options' } PreferOption
*
* @typedef {PreferOption[]} UserPreferOption
*
* @typedef {object} NormalizeOptions
* @property {object} allowsSFC
* @property {boolean} [allowsSFC.scriptSetup]
* @property {boolean} [allowsSFC.composition]
* @property {boolean} [allowsSFC.compositionVue2]
* @property {boolean} [allowsSFC.options]
* @property {object} allowsOther
* @property {boolean} [allowsOther.composition]
* @property {boolean} [allowsOther.compositionVue2]
* @property {boolean} [allowsOther.options]
*/
/** @type {PreferOption[]} */
const STYLE_OPTIONS = [
'script-setup',
'composition',
'composition-vue2',
'options'
]
/**
* Normalize options.
* @param {any[]} options The options user configured.
* @returns {NormalizeOptions} The normalized options.
*/
function parseOptions(options) {
/** @type {NormalizeOptions} */
const opts = { allowsSFC: {}, allowsOther: {} }
/** @type {UserPreferOption} */
const preferOptions = options[0] || ['script-setup', 'composition']
for (const prefer of preferOptions) {
if (prefer === 'script-setup') {
opts.allowsSFC.scriptSetup = true
} else if (prefer === 'composition') {
opts.allowsSFC.composition = true
opts.allowsOther.composition = true
} else if (prefer === 'composition-vue2') {
opts.allowsSFC.compositionVue2 = true
opts.allowsOther.compositionVue2 = true
} else if (prefer === 'options') {
opts.allowsSFC.options = true
opts.allowsOther.options = true
}
}
if (
!opts.allowsOther.composition &&
!opts.allowsOther.compositionVue2 &&
!opts.allowsOther.options
) {
opts.allowsOther.composition = true
opts.allowsOther.compositionVue2 = true
opts.allowsOther.options = true
}
return opts
}
const OPTIONS_API_OPTIONS = new Set([
'mixins',
'extends',
// state
'data',
'computed',
'methods',
'watch',
'provide',
'inject',
// lifecycle
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'activated',
'deactivated',
'beforeDestroy',
'beforeUnmount',
'destroyed',
'unmounted',
'render',
'renderTracked',
'renderTriggered',
'errorCaptured',
// public API
'expose'
])
const COMPOSITION_API_OPTIONS = new Set(['setup'])
const COMPOSITION_API_VUE2_OPTIONS = new Set([
'setup',
'render', // https://github.com/vuejs/composition-api#template-refs
'renderTracked', // https://github.com/vuejs/composition-api#missing-apis
'renderTriggered' // https://github.com/vuejs/composition-api#missing-apis
])
const LIFECYCLE_HOOK_OPTIONS = new Set([
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'activated',
'deactivated',
'beforeDestroy',
'beforeUnmount',
'destroyed',
'unmounted',
'renderTracked',
'renderTriggered',
'errorCaptured'
])
/**
* @typedef { 'script-setup' | 'composition' | 'options' } ApiStyle
*/
/**
* @param {object} allowsOpt
* @param {boolean} [allowsOpt.scriptSetup]
* @param {boolean} [allowsOpt.composition]
* @param {boolean} [allowsOpt.compositionVue2]
* @param {boolean} [allowsOpt.options]
*/
function buildAllowedPhrase(allowsOpt) {
const phrases = []
if (allowsOpt.scriptSetup) {
phrases.push('`<script setup>`')
}
if (allowsOpt.composition) {
phrases.push('Composition API')
}
if (allowsOpt.compositionVue2) {
phrases.push('Composition API (Vue 2)')
}
if (allowsOpt.options) {
phrases.push('Options API')
}
return phrases.length > 2
? `${phrases.slice(0, -1).join(',')} or ${phrases.slice(-1)[0]}`
: phrases.join(' or ')
}
/**
* @param {object} allowsOpt
* @param {boolean} [allowsOpt.scriptSetup]
* @param {boolean} [allowsOpt.composition]
* @param {boolean} [allowsOpt.compositionVue2]
* @param {boolean} [allowsOpt.options]
*/
function isPreferScriptSetup(allowsOpt) {
if (
!allowsOpt.scriptSetup ||
allowsOpt.composition ||
allowsOpt.compositionVue2 ||
allowsOpt.options
) {
return false
}
return true
}
/**
* @param {string} name
*/
function buildOptionPhrase(name) {
return LIFECYCLE_HOOK_OPTIONS.has(name)
? `\`${name}\` lifecycle hook`
: name === 'setup' || name === 'render'
? `\`${name}\` function`
: `\`${name}\` option`
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce component API style',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/component-api-style.html'
},
fixable: null,
schema: [
{
type: 'array',
items: {
enum: STYLE_OPTIONS,
uniqueItems: true,
additionalItems: false
},
minItems: 1
}
],
messages: {
disallowScriptSetup:
'`<script setup>` is not allowed in your project. Use {{allowedApis}} instead.',
disallowComponentOption:
'{{disallowedApi}} is not allowed in your project. {{optionPhrase}} is part of the {{disallowedApi}}. Use {{allowedApis}} instead.',
disallowComponentOptionPreferScriptSetup:
'{{disallowedApi}} is not allowed in your project. Use `<script setup>` instead.'
}
},
/** @param {RuleContext} context */
create(context) {
const options = parseOptions(context.options)
return utils.compositingVisitors(
{
Program() {
if (options.allowsSFC.scriptSetup) {
return
}
const scriptSetup = utils.getScriptSetupElement(context)
if (scriptSetup) {
context.report({
node: scriptSetup.startTag,
messageId: 'disallowScriptSetup',
data: {
allowedApis: buildAllowedPhrase(options.allowsSFC)
}
})
}
}
},
utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
const allows = utils.isSFCObject(context, node)
? options.allowsSFC
: options.allowsOther
if (
(allows.composition || allows.compositionVue2) &&
allows.options
) {
return
}
const apis = [
{
allow: allows.composition,
options: COMPOSITION_API_OPTIONS,
apiName: 'Composition API'
},
{
allow: allows.options,
options: OPTIONS_API_OPTIONS,
apiName: 'Options API'
},
{
allow: allows.compositionVue2,
options: COMPOSITION_API_VUE2_OPTIONS,
apiName: 'Composition API (Vue 2)'
}
]
for (const prop of node.properties) {
if (prop.type !== 'Property') {
continue
}
const name = utils.getStaticPropertyName(prop)
if (!name) {
continue
}
const disallowApi =
!apis.some((api) => api.allow && api.options.has(name)) &&
apis.find((api) => !api.allow && api.options.has(name))
if (disallowApi) {
context.report({
node: prop.key,
messageId: isPreferScriptSetup(allows)
? 'disallowComponentOptionPreferScriptSetup'
: 'disallowComponentOption',
data: {
disallowedApi: disallowApi.apiName,
optionPhrase: buildOptionPhrase(name),
allowedApis: buildAllowedPhrase(allows)
}
})
}
}
}
})
)
}
}

View File

@ -0,0 +1,107 @@
/**
* @fileoverview enforce specific casing for component definition name
* @author Armano
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
const allowedCaseOptions = ['PascalCase', 'kebab-case']
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce specific casing for component definition name',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/component-definition-name-casing.html'
},
fixable: 'code', // or "code" or "whitespace"
schema: [
{
enum: allowedCaseOptions
}
]
},
/** @param {RuleContext} context */
create(context) {
const options = context.options[0]
const caseType =
allowedCaseOptions.indexOf(options) !== -1 ? options : 'PascalCase'
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
/**
* @param {Literal | TemplateLiteral} node
*/
function convertName(node) {
/** @type {string} */
let nodeValue
/** @type {Range} */
let range
if (node.type === 'TemplateLiteral') {
const quasis = node.quasis[0]
nodeValue = quasis.value.cooked
range = quasis.range
} else {
nodeValue = `${node.value}`
range = node.range
}
if (!casing.getChecker(caseType)(nodeValue)) {
context.report({
node,
message: 'Property name "{{value}}" is not {{caseType}}.',
data: {
value: nodeValue,
caseType
},
fix: (fixer) =>
fixer.replaceTextRange(
[range[0] + 1, range[1] - 1],
casing.getExactConverter(caseType)(nodeValue)
)
})
}
}
/**
* @param {Expression | SpreadElement} node
* @returns {node is (Literal | TemplateLiteral)}
*/
function canConvert(node) {
return (
node.type === 'Literal' ||
(node.type === 'TemplateLiteral' &&
node.expressions.length === 0 &&
node.quasis.length === 1)
)
}
return Object.assign(
{},
utils.executeOnCallVueComponent(context, (node) => {
if (node.arguments.length === 2) {
const argument = node.arguments[0]
if (canConvert(argument)) {
convertName(argument)
}
}
}),
utils.executeOnVue(context, (obj) => {
const node = utils.findProperty(obj, 'name')
if (!node) return
if (!canConvert(node.value)) return
convertName(node.value)
})
)
}
}

View File

@ -0,0 +1,163 @@
/**
* @author Yosuke Ota
* issue https://github.com/vuejs/eslint-plugin-vue/issues/250
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const casing = require('../utils/casing')
const { toRegExp } = require('../utils/regexp')
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
const allowedCaseOptions = ['PascalCase', 'kebab-case']
const defaultCase = 'PascalCase'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'enforce specific casing for the component naming style in template',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/component-name-in-template-casing.html'
},
fixable: 'code',
schema: [
{
enum: allowedCaseOptions
},
{
type: 'object',
properties: {
ignores: {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
},
registeredComponentsOnly: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
create(context) {
const caseOption = context.options[0]
const options = context.options[1] || {}
const caseType =
allowedCaseOptions.indexOf(caseOption) !== -1 ? caseOption : defaultCase
/** @type {RegExp[]} */
const ignores = (options.ignores || []).map(toRegExp)
const registeredComponentsOnly = options.registeredComponentsOnly !== false
const tokens =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
/** @type { string[] } */
const registeredComponents = []
/**
* Checks whether the given node is the verification target node.
* @param {VElement} node element node
* @returns {boolean} `true` if the given node is the verification target node.
*/
function isVerifyTarget(node) {
if (ignores.some((re) => re.test(node.rawName))) {
// ignore
return false
}
if (!registeredComponentsOnly) {
// If the user specifies registeredComponentsOnly as false, it checks all component tags.
if (
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
utils.isHtmlWellKnownElementName(node.rawName) ||
utils.isSvgWellKnownElementName(node.rawName)
) {
return false
}
return true
}
// We only verify the components registered in the component.
if (
registeredComponents
.filter((name) => casing.isPascalCase(name)) // When defining a component with PascalCase, you can use either case
.some(
(name) =>
node.rawName === name || casing.pascalCase(node.rawName) === name
)
) {
return true
}
return false
}
let hasInvalidEOF = false
return utils.defineTemplateBodyVisitor(
context,
{
VElement(node) {
if (hasInvalidEOF) {
return
}
if (!isVerifyTarget(node)) {
return
}
const name = node.rawName
if (!casing.getChecker(caseType)(name)) {
const startTag = node.startTag
const open = tokens.getFirstToken(startTag)
const casingName = casing.getExactConverter(caseType)(name)
context.report({
node: open,
loc: open.loc,
message: 'Component name "{{name}}" is not {{caseType}}.',
data: {
name,
caseType
},
*fix(fixer) {
yield fixer.replaceText(open, `<${casingName}`)
const endTag = node.endTag
if (endTag) {
const endTagOpen = tokens.getFirstToken(endTag)
yield fixer.replaceText(endTagOpen, `</${casingName}`)
}
}
})
}
}
},
{
Program(node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
},
...(registeredComponentsOnly
? utils.executeOnVue(context, (obj) => {
registeredComponents.push(
...utils.getRegisteredComponents(obj).map((n) => n.name)
)
})
: {})
}
)
}
}

View File

@ -0,0 +1,115 @@
/**
* @author Pig Fang
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const casing = require('../utils/casing')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* @param {import('../../typings/eslint-plugin-vue/util-types/ast').Expression} node
* @returns {string | null}
*/
function getOptionsComponentName(node) {
if (node.type === 'Identifier') {
return node.name
}
if (node.type === 'Literal') {
return typeof node.value === 'string' ? node.value : null
}
return null
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'enforce the casing of component name in `components` options',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/component-options-name-casing.html'
},
fixable: 'code',
hasSuggestions: true,
schema: [{ enum: casing.allowedCaseOptions }],
messages: {
caseNotMatched: 'Component name "{{component}}" is not {{caseType}}.',
possibleRenaming: 'Rename component name to be in {{caseType}}.'
}
},
/** @param {RuleContext} context */
create(context) {
const caseType = context.options[0] || 'PascalCase'
const canAutoFix = caseType === 'PascalCase'
const checkCase = casing.getChecker(caseType)
const convert = casing.getConverter(caseType)
return utils.executeOnVue(context, (obj) => {
const node = utils.findProperty(obj, 'components')
if (!node || node.value.type !== 'ObjectExpression') {
return
}
node.value.properties.forEach((property) => {
if (property.type !== 'Property') {
return
}
const name = getOptionsComponentName(property.key)
if (!name || checkCase(name)) {
return
}
context.report({
node: property.key,
messageId: 'caseNotMatched',
data: {
component: name,
caseType
},
fix: canAutoFix
? (fixer) => {
const converted = convert(name)
return property.shorthand
? fixer.replaceText(property, `${converted}: ${name}`)
: fixer.replaceText(property.key, converted)
}
: undefined,
suggest: canAutoFix
? undefined
: [
{
messageId: 'possibleRenaming',
data: { caseType },
fix: (fixer) => {
const converted = convert(name)
if (caseType === 'kebab-case') {
return property.shorthand
? fixer.replaceText(property, `'${converted}': ${name}`)
: fixer.replaceText(property.key, `'${converted}'`)
}
return property.shorthand
? fixer.replaceText(property, `${converted}: ${name}`)
: fixer.replaceText(property.key, converted)
}
}
]
})
})
})
}
}

View File

@ -0,0 +1,195 @@
/**
* @author Yosuke Ota
* issue https://github.com/vuejs/eslint-plugin-vue/issues/140
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const { parseSelector } = require('../utils/selector')
/**
* @typedef {import('../utils/selector').VElementSelector} VElementSelector
*/
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style'])
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce order of component top-level elements',
categories: ['vue3-recommended', 'recommended'],
url: 'https://eslint.vuejs.org/rules/component-tags-order.html'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
order: {
type: 'array',
items: {
anyOf: [
{ type: 'string' },
{ type: 'array', items: { type: 'string' }, uniqueItems: true }
]
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
],
messages: {
unexpected:
"'<{{elementName}}{{elementAttributes}}>' should be above '<{{firstUnorderedName}}{{firstUnorderedAttributes}}>' on line {{line}}."
}
},
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
create(context) {
/**
* @typedef {object} OrderElement
* @property {string} selectorText
* @property {VElementSelector} selector
* @property {number} index
*/
/** @type {OrderElement[]} */
const orders = []
/** @type {(string|string[])[]} */
const orderOptions =
(context.options[0] && context.options[0].order) || DEFAULT_ORDER
orderOptions.forEach((selectorOrSelectors, index) => {
if (Array.isArray(selectorOrSelectors)) {
for (const selector of selectorOrSelectors) {
orders.push({
selectorText: selector,
selector: parseSelector(selector, context),
index
})
}
} else {
orders.push({
selectorText: selectorOrSelectors,
selector: parseSelector(selectorOrSelectors, context),
index
})
}
})
/**
* @param {VElement} element
* @return {string}
*/
function getAttributeString(element) {
return element.startTag.attributes
.map((attribute) => {
if (attribute.value && attribute.value.type !== 'VLiteral') {
return ''
}
return `${attribute.key.name}${
attribute.value && attribute.value.value
? `=${attribute.value.value}`
: ''
}`
})
.join(' ')
}
/**
* @param {VElement} element
*/
function getOrderElement(element) {
return orders.find((o) => o.selector.test(element))
}
const documentFragment =
context.parserServices.getDocumentFragment &&
context.parserServices.getDocumentFragment()
function getTopLevelHTMLElements() {
if (documentFragment) {
return documentFragment.children.filter(utils.isVElement)
}
return []
}
return {
Program(node) {
if (utils.hasInvalidEOF(node)) {
return
}
const elements = getTopLevelHTMLElements()
const elementWithOrders = elements.flatMap((element) => {
const order = getOrderElement(element)
return order ? [{ order, element }] : []
})
const sourceCode = context.getSourceCode()
elementWithOrders.forEach(({ order: expected, element }, index) => {
const firstUnordered = elementWithOrders
.slice(0, index)
.filter(({ order }) => expected.index < order.index)
.sort((e1, e2) => e1.order.index - e2.order.index)[0]
if (firstUnordered) {
const firstUnorderedAttributes = getAttributeString(
firstUnordered.element
)
const elementAttributes = getAttributeString(element)
context.report({
node: element,
loc: element.loc,
messageId: 'unexpected',
data: {
elementName: element.name,
elementAttributes: elementAttributes
? ` ${elementAttributes}`
: '',
firstUnorderedName: firstUnordered.element.name,
firstUnorderedAttributes: firstUnorderedAttributes
? ` ${firstUnorderedAttributes}`
: '',
line: firstUnordered.element.loc.start.line
},
*fix(fixer) {
// insert element before firstUnordered
const fixedElements = elements.flatMap((it) => {
if (it === firstUnordered.element) {
return [element, it]
} else if (it === element) {
return []
}
return [it]
})
for (let i = elements.length - 1; i >= 0; i--) {
if (elements[i] !== fixedElements[i]) {
yield fixer.replaceTextRange(
elements[i].range,
sourceCode.text.slice(...fixedElements[i].range)
)
}
}
}
})
}
})
}
}
}
}

View File

@ -0,0 +1,307 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const { findVariable } = require('eslint-utils')
const utils = require('../utils')
const casing = require('../utils/casing')
const { toRegExp } = require('../utils/regexp')
/**
* @typedef {import('../utils').VueObjectData} VueObjectData
*/
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
const ALLOWED_CASE_OPTIONS = ['kebab-case', 'camelCase']
const DEFAULT_CASE = 'kebab-case'
/**
* Get the name param node from the given CallExpression
* @param {CallExpression} node CallExpression
* @returns { Literal & { value: string } | null }
*/
function getNameParamNode(node) {
const nameLiteralNode = node.arguments[0]
if (
!nameLiteralNode ||
nameLiteralNode.type !== 'Literal' ||
typeof nameLiteralNode.value !== 'string'
) {
// cannot check
return null
}
return /** @type {Literal & { value: string }} */ (nameLiteralNode)
}
/**
* Get the callee member node from the given CallExpression
* @param {CallExpression} node CallExpression
*/
function getCalleeMemberNode(node) {
const callee = utils.skipChainExpression(node.callee)
if (callee.type === 'MemberExpression') {
const name = utils.getStaticPropertyName(callee)
if (name) {
return { name, member: callee }
}
}
return null
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const OBJECT_OPTION_SCHEMA = {
type: 'object',
properties: {
ignores: {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce specific casing for custom event name',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/custom-event-name-casing.html'
},
fixable: null,
schema: {
anyOf: [
{
type: 'array',
items: [
{
enum: ALLOWED_CASE_OPTIONS
},
OBJECT_OPTION_SCHEMA
]
},
// For backward compatibility
{
type: 'array',
items: [OBJECT_OPTION_SCHEMA]
}
]
},
messages: {
unexpected: "Custom event name '{{name}}' must be {{caseType}}."
}
},
/** @param {RuleContext} context */
create(context) {
/** @type {Map<ObjectExpression|Program, {contextReferenceIds:Set<Identifier>,emitReferenceIds:Set<Identifier>}>} */
const setupContexts = new Map()
const options =
context.options.length === 1 && typeof context.options[0] !== 'string'
? // For backward compatibility
[undefined, context.options[0]]
: context.options
const caseType = options[0] || DEFAULT_CASE
const objectOption = options[1] || {}
const caseChecker = casing.getChecker(caseType)
/** @type {RegExp[]} */
const ignores = (objectOption.ignores || []).map(toRegExp)
/**
* Check whether the given event name is valid.
* @param {string} name The name to check.
* @returns {boolean} `true` if the given event name is valid.
*/
function isValidEventName(name) {
return caseChecker(name) || name.startsWith('update:')
}
/**
* @param { Literal & { value: string } } nameLiteralNode
*/
function verify(nameLiteralNode) {
const name = nameLiteralNode.value
if (isValidEventName(name) || ignores.some((re) => re.test(name))) {
return
}
context.report({
node: nameLiteralNode,
messageId: 'unexpected',
data: {
name,
caseType
}
})
}
const programNode = context.getSourceCode().ast
const callVisitor = {
/**
* @param {CallExpression} node
* @param {VueObjectData} [info]
*/
CallExpression(node, info) {
const nameLiteralNode = getNameParamNode(node)
if (!nameLiteralNode) {
// cannot check
return
}
// verify setup context
const setupContext = setupContexts.get(info ? info.node : programNode)
if (setupContext) {
const { contextReferenceIds, emitReferenceIds } = setupContext
if (
node.callee.type === 'Identifier' &&
emitReferenceIds.has(node.callee)
) {
// verify setup(props,{emit}) {emit()}
verify(nameLiteralNode)
} else {
const emit = getCalleeMemberNode(node)
if (
emit &&
emit.name === 'emit' &&
emit.member.object.type === 'Identifier' &&
contextReferenceIds.has(emit.member.object)
) {
// verify setup(props,context) {context.emit()}
verify(nameLiteralNode)
}
}
}
}
}
return utils.defineTemplateBodyVisitor(
context,
{
CallExpression(node) {
const callee = node.callee
const nameLiteralNode = getNameParamNode(node)
if (!nameLiteralNode) {
// cannot check
return
}
if (callee.type === 'Identifier' && callee.name === '$emit') {
verify(nameLiteralNode)
}
}
},
utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
onDefineEmitsEnter(node) {
if (
!node.parent ||
node.parent.type !== 'VariableDeclarator' ||
node.parent.init !== node
) {
return
}
const emitParam = node.parent.id
if (emitParam.type !== 'Identifier') {
return
}
// const emit = defineEmits()
const variable = findVariable(context.getScope(), emitParam)
if (!variable) {
return
}
const emitReferenceIds = new Set()
for (const reference of variable.references) {
emitReferenceIds.add(reference.identifier)
}
setupContexts.set(programNode, {
contextReferenceIds: new Set(),
emitReferenceIds
})
},
...callVisitor
}),
utils.defineVueVisitor(context, {
onSetupFunctionEnter(node, { node: vueNode }) {
const contextParam = utils.skipDefaultParamValue(node.params[1])
if (!contextParam) {
// no arguments
return
}
if (
contextParam.type === 'RestElement' ||
contextParam.type === 'ArrayPattern'
) {
// cannot check
return
}
const contextReferenceIds = new Set()
const emitReferenceIds = new Set()
if (contextParam.type === 'ObjectPattern') {
const emitProperty = utils.findAssignmentProperty(
contextParam,
'emit'
)
if (!emitProperty || emitProperty.value.type !== 'Identifier') {
return
}
const emitParam = emitProperty.value
// `setup(props, {emit})`
const variable = findVariable(context.getScope(), emitParam)
if (!variable) {
return
}
for (const reference of variable.references) {
emitReferenceIds.add(reference.identifier)
}
} else {
// `setup(props, context)`
const variable = findVariable(context.getScope(), contextParam)
if (!variable) {
return
}
for (const reference of variable.references) {
contextReferenceIds.add(reference.identifier)
}
}
setupContexts.set(vueNode, {
contextReferenceIds,
emitReferenceIds
})
},
...callVisitor,
onVueObjectExit(node) {
setupContexts.delete(node)
}
}),
{
CallExpression(node) {
const nameLiteralNode = getNameParamNode(node)
if (!nameLiteralNode) {
// cannot check
return
}
const emit = getCalleeMemberNode(node)
// verify $emit
if (emit && emit.name === '$emit') {
// verify this.$emit()
verify(nameLiteralNode)
}
}
}
)
)
}
}

View File

@ -0,0 +1,307 @@
/**
* @author Eduard Deisling
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
const MACROS_EMITS = 'defineEmits'
const MACROS_PROPS = 'defineProps'
const ORDER = [MACROS_EMITS, MACROS_PROPS]
const DEFAULT_ORDER = [MACROS_PROPS, MACROS_EMITS]
/**
* @param {VElement} scriptSetup
* @param {ASTNode} node
*/
function inScriptSetup(scriptSetup, node) {
return (
scriptSetup.range[0] <= node.range[0] &&
node.range[1] <= scriptSetup.range[1]
)
}
/**
* @param {ASTNode} node
*/
function isUseStrictStatement(node) {
return (
node.type === 'ExpressionStatement' &&
node.expression.type === 'Literal' &&
node.expression.value === 'use strict'
)
}
/**
* Get an index of the first statement after imports and interfaces in order
* to place defineEmits and defineProps before this statement
* @param {VElement} scriptSetup
* @param {Program} program
*/
function getTargetStatementPosition(scriptSetup, program) {
const skipStatements = new Set([
'ImportDeclaration',
'TSInterfaceDeclaration',
'TSTypeAliasDeclaration',
'DebuggerStatement',
'EmptyStatement'
])
for (const [index, item] of program.body.entries()) {
if (
inScriptSetup(scriptSetup, item) &&
!skipStatements.has(item.type) &&
!isUseStrictStatement(item)
) {
return index
}
}
return -1
}
/**
* We need to handle cases like "const props = defineProps(...)"
* Define macros must be used only on top, so we can look for "Program" type
* inside node.parent.type
* @param {CallExpression|ASTNode} node
* @return {ASTNode}
*/
function getDefineMacrosStatement(node) {
if (!node.parent) {
throw new Error('Node has no parent')
}
if (node.parent.type === 'Program') {
return node
}
return getDefineMacrosStatement(node.parent)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
/** @param {RuleContext} context */
function create(context) {
const scriptSetup = utils.getScriptSetupElement(context)
if (!scriptSetup) {
return {}
}
const sourceCode = context.getSourceCode()
const options = context.options
/** @type {[string, string]} */
const order = (options[0] && options[0].order) || DEFAULT_ORDER
/** @type {Map<string, ASTNode>} */
const macrosNodes = new Map()
return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
onDefinePropsExit(node) {
macrosNodes.set(MACROS_PROPS, getDefineMacrosStatement(node))
},
onDefineEmitsExit(node) {
macrosNodes.set(MACROS_EMITS, getDefineMacrosStatement(node))
}
}),
{
'Program:exit'(program) {
const shouldFirstNode = macrosNodes.get(order[0])
const shouldSecondNode = macrosNodes.get(order[1])
const firstStatementIndex = getTargetStatementPosition(
scriptSetup,
program
)
const firstStatement = program.body[firstStatementIndex]
// have both defineEmits and defineProps
if (shouldFirstNode && shouldSecondNode) {
const secondStatement = program.body[firstStatementIndex + 1]
// need move only first
if (firstStatement === shouldSecondNode) {
reportNotOnTop(order[0], shouldFirstNode, firstStatement)
return
}
// need move both defineEmits and defineProps
if (firstStatement !== shouldFirstNode) {
reportBothNotOnTop(
shouldFirstNode,
shouldSecondNode,
firstStatement
)
return
}
// need move only second
if (secondStatement !== shouldSecondNode) {
reportNotOnTop(order[1], shouldSecondNode, shouldFirstNode)
}
return
}
// have only first and need to move it
if (shouldFirstNode && firstStatement !== shouldFirstNode) {
reportNotOnTop(order[0], shouldFirstNode, firstStatement)
return
}
// have only second and need to move it
if (shouldSecondNode && firstStatement !== shouldSecondNode) {
reportNotOnTop(order[1], shouldSecondNode, firstStatement)
}
}
}
)
/**
* @param {ASTNode} shouldFirstNode
* @param {ASTNode} shouldSecondNode
* @param {ASTNode} before
*/
function reportBothNotOnTop(shouldFirstNode, shouldSecondNode, before) {
context.report({
node: shouldFirstNode,
loc: shouldFirstNode.loc,
messageId: 'macrosNotOnTop',
data: {
macro: order[0]
},
fix(fixer) {
return [
...moveNodeBefore(fixer, shouldFirstNode, before),
...moveNodeBefore(fixer, shouldSecondNode, before)
]
}
})
}
/**
* @param {string} macro
* @param {ASTNode} node
* @param {ASTNode} before
*/
function reportNotOnTop(macro, node, before) {
context.report({
node,
loc: node.loc,
messageId: 'macrosNotOnTop',
data: {
macro
},
fix(fixer) {
return moveNodeBefore(fixer, node, before)
}
})
}
/**
* Move all lines of "node" with its comments to before the "target"
* @param {RuleFixer} fixer
* @param {ASTNode} node
* @param {ASTNode} target
*/
function moveNodeBefore(fixer, node, target) {
// get comments under tokens(if any)
const beforeNodeToken = sourceCode.getTokenBefore(node)
const nodeComment = sourceCode.getTokenAfter(beforeNodeToken, {
includeComments: true
})
const nextNodeComment = sourceCode.getTokenAfter(node, {
includeComments: true
})
// get positions of what we need to remove
const cutStart = getLineStartIndex(nodeComment, beforeNodeToken)
const cutEnd = getLineStartIndex(nextNodeComment, node)
// get space before target
const beforeTargetToken = sourceCode.getTokenBefore(target)
const targetComment = sourceCode.getTokenAfter(beforeTargetToken, {
includeComments: true
})
const textSpace = getTextBetweenTokens(beforeTargetToken, targetComment)
// make insert text: comments + node + space before target
const textNode = sourceCode.getText(
node,
node.range[0] - nodeComment.range[0]
)
const insertText = textNode + textSpace
return [
fixer.insertTextBefore(targetComment, insertText),
fixer.removeRange([cutStart, cutEnd])
]
}
/**
* @param {ASTNode} tokenBefore
* @param {ASTNode} tokenAfter
*/
function getTextBetweenTokens(tokenBefore, tokenAfter) {
return sourceCode.text.slice(tokenBefore.range[1], tokenAfter.range[0])
}
/**
* Get position of the beginning of the token's line(or prevToken end if no line)
* @param {ASTNode} token
* @param {ASTNode} prevToken
*/
function getLineStartIndex(token, prevToken) {
// if we have next token on the same line - get index right before that token
if (token.loc.start.line === prevToken.loc.end.line) {
return prevToken.range[1]
}
return sourceCode.getIndexFromLoc({
line: token.loc.start.line,
column: 0
})
}
}
module.exports = {
meta: {
type: 'layout',
docs: {
description:
'enforce order of `defineEmits` and `defineProps` compiler macros',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
order: {
type: 'array',
items: {
enum: Object.values(ORDER)
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
],
messages: {
macrosNotOnTop:
'{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).'
}
},
create
}

View File

@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('dot-location')

View File

@ -0,0 +1,11 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('dot-notation', {
applyDocument: true
})

View File

@ -0,0 +1,11 @@
/**
* @author Toru Nagashima
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('eqeqeq', {
applyDocument: true
})

View File

@ -0,0 +1,230 @@
/**
* @fileoverview prevent variables defined in `<script setup>` to be marked as undefined
* @author Yosuke Ota
*/
'use strict'
const Module = require('module')
const path = require('path')
const utils = require('../utils')
const AST = require('vue-eslint-parser').AST
const ecmaVersion = 2020
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'prevent variables defined in `<script setup>` to be marked as undefined', // eslint-disable-line eslint-plugin/require-meta-docs-description
categories: undefined,
url: 'https://eslint.vuejs.org/rules/experimental-script-setup-vars.html'
},
deprecated: true,
schema: []
},
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
create(context) {
const documentFragment =
context.parserServices.getDocumentFragment &&
context.parserServices.getDocumentFragment()
if (!documentFragment) {
return {}
}
const sourceCode = context.getSourceCode()
const scriptElement = documentFragment.children
.filter(utils.isVElement)
.find(
(element) =>
element.name === 'script' &&
element.range[0] <= sourceCode.ast.range[0] &&
sourceCode.ast.range[1] <= element.range[1]
)
if (!scriptElement) {
return {}
}
const setupAttr = utils.getAttribute(scriptElement, 'setup')
if (!setupAttr || !setupAttr.value) {
return {}
}
const value = setupAttr.value.value
let eslintScope
try {
eslintScope = getESLintModule('eslint-scope', () =>
// @ts-ignore
require('eslint-scope')
)
} catch (_e) {
context.report({
node: setupAttr,
message: 'Can not be resolved eslint-scope.'
})
return {}
}
let espree
try {
espree = getESLintModule('espree', () =>
// @ts-ignore
require('espree')
)
} catch (_e) {
context.report({
node: setupAttr,
message: 'Can not be resolved espree.'
})
return {}
}
const globalScope = sourceCode.scopeManager.scopes[0]
/** @type {string[]} */
let vars
try {
vars = parseSetup(value, espree, eslintScope)
} catch (_e) {
context.report({
node: setupAttr.value,
message: 'Parsing error.'
})
return {}
}
// Define configured global variables.
for (const id of vars) {
const tempVariable = globalScope.set.get(id)
/** @type {Variable} */
let variable
if (!tempVariable) {
variable = new eslintScope.Variable(id, globalScope)
globalScope.variables.push(variable)
globalScope.set.set(id, variable)
} else {
variable = tempVariable
}
variable.eslintImplicitGlobalSetting = 'readonly'
variable.eslintExplicitGlobal = undefined
variable.eslintExplicitGlobalComments = undefined
variable.writeable = false
}
/*
* "through" contains all references which definitions cannot be found.
* Since we augment the global scope using configuration, we need to update
* references and remove the ones that were added by configuration.
*/
globalScope.through = globalScope.through.filter((reference) => {
const name = reference.identifier.name
const variable = globalScope.set.get(name)
if (variable) {
/*
* Links the variable and the reference.
* And this reference is removed from `Scope#through`.
*/
reference.resolved = variable
variable.references.push(reference)
return false
}
return true
})
return {}
}
}
/**
* @param {string} code
* @param {any} espree
* @param {any} eslintScope
* @returns {string[]}
*/
function parseSetup(code, espree, eslintScope) {
/** @type {Program} */
const ast = espree.parse(`(${code})=>{}`, { ecmaVersion })
const result = eslintScope.analyze(ast, {
ignoreEval: true,
nodejsScope: false,
ecmaVersion,
sourceType: 'script',
fallback: AST.getFallbackKeys
})
const variables = /** @type {Variable[]} */ (
result.globalScope.childScopes[0].variables
)
return variables.map((v) => v.name)
}
const createRequire =
// Added in v12.2.0
Module.createRequire ||
// Added in v10.12.0, but deprecated in v12.2.0.
Module.createRequireFromPath ||
// Polyfill - This is not executed on the tests on node@>=10.
/**
* @param {string} filename
*/
function (filename) {
const mod = new Module(filename)
mod.filename = filename
// @ts-ignore
mod.paths = Module._nodeModulePaths(path.dirname(filename))
// @ts-ignore
mod._compile('module.exports = require;', filename)
return mod.exports
}
/** @type { { 'espree'?: any, 'eslint-scope'?: any } } */
const modulesCache = {}
/**
* @param {string} p
*/
function isLinterPath(p) {
return (
// ESLint 6 and above
p.includes(`eslint${path.sep}lib${path.sep}linter${path.sep}linter.js`) ||
// ESLint 5
p.includes(`eslint${path.sep}lib${path.sep}linter.js`)
)
}
/**
* Load module from the loaded ESLint.
* If the loaded ESLint was not found, just returns `fallback()`.
* @param {'espree' | 'eslint-scope'} name
* @param { () => any } fallback
*/
function getESLintModule(name, fallback) {
if (!modulesCache[name]) {
// Lookup the loaded eslint
const linterPath = Object.keys(require.cache).find(isLinterPath)
if (linterPath) {
try {
modulesCache[name] = createRequire(linterPath)(name)
} catch (_e) {
// ignore
}
}
if (!modulesCache[name]) {
modulesCache[name] = fallback()
}
}
return modulesCache[name]
}

View File

@ -0,0 +1,98 @@
/**
* @fileoverview Enforce the location of first attribute
* @author Yosuke Ota
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const utils = require('../utils')
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce the location of first attribute',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/first-attribute-linebreak.html'
},
fixable: 'whitespace', // or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
multiline: { enum: ['below', 'beside', 'ignore'] },
singleline: { enum: ['below', 'beside', 'ignore'] }
},
additionalProperties: false
}
],
messages: {
expected: 'Expected a linebreak before this attribute.',
unexpected: 'Expected no linebreak before this attribute.'
}
},
/** @param {RuleContext} context */
create(context) {
/** @type {"below" | "beside" | "ignore"} */
const singleline =
(context.options[0] && context.options[0].singleline) || 'ignore'
/** @type {"below" | "beside" | "ignore"} */
const multiline =
(context.options[0] && context.options[0].multiline) || 'below'
const template =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
/**
* Report attribute
* @param {VAttribute | VDirective} firstAttribute
* @param { "below" | "beside"} location
*/
function report(firstAttribute, location) {
context.report({
node: firstAttribute,
messageId: location === 'beside' ? 'unexpected' : 'expected',
fix(fixer) {
const prevToken = template.getTokenBefore(firstAttribute, {
includeComments: true
})
return fixer.replaceTextRange(
[prevToken.range[1], firstAttribute.range[0]],
location === 'beside' ? ' ' : '\n'
)
}
})
}
return utils.defineTemplateBodyVisitor(context, {
VStartTag(node) {
const firstAttribute = node.attributes[0]
if (!firstAttribute) return
const lastAttribute = node.attributes[node.attributes.length - 1]
const location =
firstAttribute.loc.start.line === lastAttribute.loc.end.line
? singleline
: multiline
if (location === 'ignore') {
return
}
if (location === 'beside') {
if (node.loc.start.line === firstAttribute.loc.start.line) {
return
}
} else {
if (node.loc.start.line < firstAttribute.loc.start.line) {
return
}
}
report(firstAttribute, location)
}
})
}
}

View File

@ -0,0 +1,12 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('func-call-spacing', {
skipDynamicArguments: true,
applyDocument: true
})

View File

@ -0,0 +1,146 @@
/**
* @fileoverview Disallow usage of button without an explicit type attribute
* @author Jonathan Santerre <jonathan@santerre.dev>
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const optionDefaults = {
button: true,
submit: true,
reset: true
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'disallow usage of button without an explicit type attribute',
categories: null,
url: 'https://eslint.vuejs.org/rules/html-button-has-type.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
button: {
default: optionDefaults.button,
type: 'boolean'
},
submit: {
default: optionDefaults.submit,
type: 'boolean'
},
reset: {
default: optionDefaults.reset,
type: 'boolean'
}
},
additionalProperties: false
}
],
messages: {
missingTypeAttribute: 'Missing an explicit type attribute for button.',
invalidTypeAttribute:
'{{value}} is an invalid value for button type attribute.',
forbiddenTypeAttribute:
'{{value}} is a forbidden value for button type attribute.',
emptyTypeAttribute: 'A value must be set for button type attribute.'
}
},
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
create(context) {
/**
* @typedef {object} Configuration
* @property {boolean} button
* @property {boolean} submit
* @property {boolean} reset
*/
/** @type {Configuration} */
const configuration = Object.assign({}, optionDefaults, context.options[0])
/**
*
* @param {string} type
* @returns {type is 'button' | 'submit' | 'reset'}
*/
function isButtonType(type) {
return type === 'button' || type === 'submit' || type === 'reset'
}
/**
* @param {ASTNode} node
* @param {string} messageId
* @param {any} [data]
*/
function report(node, messageId, data) {
context.report({
node,
messageId,
data
})
}
/**
* @param {VAttribute} attribute
*/
function validateAttribute(attribute) {
const value = attribute.value
if (!value || !value.value) {
report(value || attribute, 'emptyTypeAttribute')
return
}
const strValue = value.value
if (!isButtonType(strValue)) {
report(value, 'invalidTypeAttribute', { value: strValue })
} else if (!configuration[strValue]) {
report(value, 'forbiddenTypeAttribute', { value: strValue })
}
}
/**
* @param {VDirective} directive
*/
function validateDirective(directive) {
const value = directive.value
if (!value || !value.expression) {
report(value || directive, 'emptyTypeAttribute')
}
}
return utils.defineTemplateBodyVisitor(context, {
/**
* @param {VElement} node
*/
"VElement[rawName='button']"(node) {
const typeAttr = utils.getAttribute(node, 'type')
if (typeAttr) {
validateAttribute(typeAttr)
return
}
const typeDir = utils.getDirective(node, 'bind', 'type')
if (typeDir) {
validateDirective(typeDir)
return
}
report(node.startTag, 'missingTypeAttribute')
}
})
}
}

View File

@ -0,0 +1,115 @@
/**
* @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
// ------------------------------------------------------------------------------
/**
* @param {number} lineBreaks
*/
function getPhrase(lineBreaks) {
switch (lineBreaks) {
case 0:
return 'no line breaks'
case 1:
return '1 line break'
default:
return `${lineBreaks} line breaks`
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description:
"require or disallow a line break before tag's closing brackets",
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/html-closing-bracket-newline.html'
},
fixable: 'whitespace',
schema: [
{
type: 'object',
properties: {
singleline: { enum: ['always', 'never'] },
multiline: { enum: ['always', 'never'] }
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
create(context) {
const options = Object.assign(
{},
{
singleline: 'never',
multiline: 'always'
},
context.options[0] || {}
)
const template =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
return utils.defineTemplateBodyVisitor(context, {
/** @param {VStartTag | VEndTag} node */
'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) {
/** @type {Range} */
const range = [prevToken.range[1], closingBracketToken.range[0]]
const text = '\n'.repeat(expectedLineBreaks)
return fixer.replaceTextRange(range, text)
}
})
}
}
})
}
}

View File

@ -0,0 +1,137 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/
'use strict'
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
const utils = require('../utils')
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
/**
* @typedef { {startTag?:"always"|"never",endTag?:"always"|"never",selfClosingTag?:"always"|"never"} } Options
*/
/**
* Normalize options.
* @param {Options} options The options user configured.
* @param {ParserServices.TokenStore} tokens The token store of template body.
* @returns {Options & { detectType: (node: VStartTag | VEndTag) => 'never' | 'always' | null }} The normalized options.
*/
function parseOptions(options, tokens) {
const opts = Object.assign(
{
startTag: 'never',
endTag: 'never',
selfClosingTag: 'always'
},
options
)
return Object.assign(opts, {
/**
* @param {VStartTag | VEndTag} node
* @returns {'never' | 'always' | null}
*/
detectType(node) {
const openType = tokens.getFirstToken(node).type
const closeType = tokens.getLastToken(node).type
if (openType === 'HTMLEndTagOpen' && closeType === 'HTMLTagClose') {
return opts.endTag
}
if (openType === 'HTMLTagOpen' && closeType === 'HTMLTagClose') {
return opts.startTag
}
if (
openType === 'HTMLTagOpen' &&
closeType === 'HTMLSelfClosingTagClose'
) {
return opts.selfClosingTag
}
return null
}
})
}
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: "require or disallow a space before tag's closing brackets",
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/html-closing-bracket-spacing.html'
},
schema: [
{
type: 'object',
properties: {
startTag: { enum: ['always', 'never'] },
endTag: { enum: ['always', 'never'] },
selfClosingTag: { enum: ['always', 'never'] }
},
additionalProperties: false
}
],
fixable: 'whitespace'
},
/** @param {RuleContext} context */
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, {
/** @param {VStartTag | VEndTag} node */
'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]])
})
}
}
})
}
}

View File

@ -0,0 +1,215 @@
/**
* @author Yosuke ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
const htmlComments = require('../utils/html-comments')
/**
* @typedef { import('../utils/html-comments').ParsedHTMLComment } ParsedHTMLComment
*/
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* @param {any} param
*/
function parseOption(param) {
if (param && typeof param === 'string') {
return {
singleline: param,
multiline: param
}
}
return Object.assign(
{
singleline: 'never',
multiline: 'always'
},
param
)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce unified line brake in HTML comments',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/html-comment-content-newline.html'
},
fixable: 'whitespace',
schema: [
{
anyOf: [
{
enum: ['always', 'never']
},
{
type: 'object',
properties: {
singleline: { enum: ['always', 'never', 'ignore'] },
multiline: { enum: ['always', 'never', 'ignore'] }
},
additionalProperties: false
}
]
},
{
type: 'object',
properties: {
exceptions: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}
],
messages: {
expectedAfterHTMLCommentOpen: "Expected line break after '<!--'.",
expectedBeforeHTMLCommentOpen: "Expected line break before '-->'.",
expectedAfterExceptionBlock: 'Expected line break after exception block.',
expectedBeforeExceptionBlock:
'Expected line break before exception block.',
unexpectedAfterHTMLCommentOpen: "Unexpected line breaks after '<!--'.",
unexpectedBeforeHTMLCommentOpen: "Unexpected line breaks before '-->'."
}
},
/** @param {RuleContext} context */
create(context) {
const option = parseOption(context.options[0])
return htmlComments.defineVisitor(
context,
context.options[1],
(comment) => {
const { value, openDecoration, closeDecoration } = comment
if (!value) {
return
}
const startLine = openDecoration
? openDecoration.loc.end.line
: value.loc.start.line
const endLine = closeDecoration
? closeDecoration.loc.start.line
: value.loc.end.line
const newlineType =
startLine === endLine ? option.singleline : option.multiline
if (newlineType === 'ignore') {
return
}
checkCommentOpen(comment, newlineType !== 'never')
checkCommentClose(comment, newlineType !== 'never')
}
)
/**
* Reports the newline before the contents of a given comment if it's invalid.
* @param {ParsedHTMLComment} comment - comment data.
* @param {boolean} requireNewline - `true` if line breaks are required.
* @returns {void}
*/
function checkCommentOpen(comment, requireNewline) {
const { value, openDecoration, open } = comment
if (!value) {
return
}
const beforeToken = openDecoration || open
if (requireNewline) {
if (beforeToken.loc.end.line < value.loc.start.line) {
// Is valid
return
}
context.report({
loc: {
start: beforeToken.loc.end,
end: value.loc.start
},
messageId: openDecoration
? 'expectedAfterExceptionBlock'
: 'expectedAfterHTMLCommentOpen',
fix: openDecoration
? undefined
: (fixer) => fixer.insertTextAfter(beforeToken, '\n')
})
} else {
if (beforeToken.loc.end.line === value.loc.start.line) {
// Is valid
return
}
context.report({
loc: {
start: beforeToken.loc.end,
end: value.loc.start
},
messageId: 'unexpectedAfterHTMLCommentOpen',
fix: (fixer) =>
fixer.replaceTextRange([beforeToken.range[1], value.range[0]], ' ')
})
}
}
/**
* Reports the space after the contents of a given comment if it's invalid.
* @param {ParsedHTMLComment} comment - comment data.
* @param {boolean} requireNewline - `true` if line breaks are required.
* @returns {void}
*/
function checkCommentClose(comment, requireNewline) {
const { value, closeDecoration, close } = comment
if (!value) {
return
}
const afterToken = closeDecoration || close
if (requireNewline) {
if (value.loc.end.line < afterToken.loc.start.line) {
// Is valid
return
}
context.report({
loc: {
start: value.loc.end,
end: afterToken.loc.start
},
messageId: closeDecoration
? 'expectedBeforeExceptionBlock'
: 'expectedBeforeHTMLCommentOpen',
fix: closeDecoration
? undefined
: (fixer) => fixer.insertTextBefore(afterToken, '\n')
})
} else {
if (value.loc.end.line === afterToken.loc.start.line) {
// Is valid
return
}
context.report({
loc: {
start: value.loc.end,
end: afterToken.loc.start
},
messageId: 'unexpectedBeforeHTMLCommentOpen',
fix: (fixer) =>
fixer.replaceTextRange([value.range[1], afterToken.range[0]], ' ')
})
}
}
}
}

View File

@ -0,0 +1,179 @@
/**
* @author Yosuke ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
const htmlComments = require('../utils/html-comments')
/**
* @typedef { import('../utils/html-comments').ParsedHTMLComment } ParsedHTMLComment
*/
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce unified spacing in HTML comments',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/html-comment-content-spacing.html'
},
fixable: 'whitespace',
schema: [
{
enum: ['always', 'never']
},
{
type: 'object',
properties: {
exceptions: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}
],
messages: {
expectedAfterHTMLCommentOpen: "Expected space after '<!--'.",
expectedBeforeHTMLCommentOpen: "Expected space before '-->'.",
expectedAfterExceptionBlock: 'Expected space after exception block.',
expectedBeforeExceptionBlock: 'Expected space before exception block.',
unexpectedAfterHTMLCommentOpen: "Unexpected space after '<!--'.",
unexpectedBeforeHTMLCommentOpen: "Unexpected space before '-->'."
}
},
/** @param {RuleContext} context */
create(context) {
// Unless the first option is never, require a space
const requireSpace = context.options[0] !== 'never'
return htmlComments.defineVisitor(
context,
context.options[1],
(comment) => {
checkCommentOpen(comment)
checkCommentClose(comment)
},
{ includeDirectives: true }
)
/**
* Reports the space before the contents of a given comment if it's invalid.
* @param {ParsedHTMLComment} comment - comment data.
* @returns {void}
*/
function checkCommentOpen(comment) {
const { value, openDecoration, open } = comment
if (!value) {
return
}
const beforeToken = openDecoration || open
if (beforeToken.loc.end.line !== value.loc.start.line) {
// Ignore newline
return
}
if (requireSpace) {
if (beforeToken.range[1] < value.range[0]) {
// Is valid
return
}
context.report({
loc: {
start: beforeToken.loc.end,
end: value.loc.start
},
messageId: openDecoration
? 'expectedAfterExceptionBlock'
: 'expectedAfterHTMLCommentOpen',
fix: openDecoration
? undefined
: (fixer) => fixer.insertTextAfter(beforeToken, ' ')
})
} else {
if (openDecoration) {
// Ignore expection block
return
}
if (beforeToken.range[1] === value.range[0]) {
// Is valid
return
}
context.report({
loc: {
start: beforeToken.loc.end,
end: value.loc.start
},
messageId: 'unexpectedAfterHTMLCommentOpen',
fix: (fixer) =>
fixer.removeRange([beforeToken.range[1], value.range[0]])
})
}
}
/**
* Reports the space after the contents of a given comment if it's invalid.
* @param {ParsedHTMLComment} comment - comment data.
* @returns {void}
*/
function checkCommentClose(comment) {
const { value, closeDecoration, close } = comment
if (!value) {
return
}
const afterToken = closeDecoration || close
if (value.loc.end.line !== afterToken.loc.start.line) {
// Ignore newline
return
}
if (requireSpace) {
if (value.range[1] < afterToken.range[0]) {
// Is valid
return
}
context.report({
loc: {
start: value.loc.end,
end: afterToken.loc.start
},
messageId: closeDecoration
? 'expectedBeforeExceptionBlock'
: 'expectedBeforeHTMLCommentOpen',
fix: closeDecoration
? undefined
: (fixer) => fixer.insertTextBefore(afterToken, ' ')
})
} else {
if (closeDecoration) {
// Ignore expection block
return
}
if (value.range[1] === afterToken.range[0]) {
// Is valid
return
}
context.report({
loc: {
start: value.loc.end,
end: afterToken.loc.start
},
messageId: 'unexpectedBeforeHTMLCommentOpen',
fix: (fixer) =>
fixer.removeRange([value.range[1], afterToken.range[0]])
})
}
}
}
}

View File

@ -0,0 +1,258 @@
/**
* @author Yosuke ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
const htmlComments = require('../utils/html-comments')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Normalize options.
* @param {number|"tab"|undefined} type The type of indentation.
* @returns { { indentChar: string, indentSize: number, indentText: string } } Normalized options.
*/
function parseOptions(type) {
const ret = {
indentChar: ' ',
indentSize: 2,
indentText: ''
}
if (Number.isSafeInteger(type)) {
ret.indentSize = Number(type)
} else if (type === 'tab') {
ret.indentChar = '\t'
ret.indentSize = 1
}
ret.indentText = ret.indentChar.repeat(ret.indentSize)
return ret
}
/**
* @param {string} s
* @param {string} [unitChar]
*/
function toDisplay(s, unitChar) {
if (s.length === 0 && unitChar) {
return `0 ${toUnit(unitChar)}s`
}
const char = s[0]
if (char === ' ' || char === '\t') {
if (s.split('').every((c) => c === char)) {
return `${s.length} ${toUnit(char)}${s.length === 1 ? '' : 's'}`
}
}
return JSON.stringify(s)
}
/** @param {string} char */
function toUnit(char) {
if (char === '\t') {
return 'tab'
}
if (char === ' ') {
return 'space'
}
return JSON.stringify(char)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce consistent indentation in HTML comments',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/html-comment-indent.html'
},
fixable: 'whitespace',
schema: [
{
anyOf: [{ type: 'integer', minimum: 0 }, { enum: ['tab'] }]
}
],
messages: {
unexpectedBaseIndentation:
'Expected base point indentation of {{expected}}, but found {{actual}}.',
missingBaseIndentation:
'Expected base point indentation of {{expected}}, but not found.',
unexpectedIndentationCharacter:
'Expected {{expected}} character, but found {{actual}} character.',
unexpectedIndentation:
'Expected indentation of {{expected}} but found {{actual}}.',
unexpectedRelativeIndentation:
'Expected relative indentation of {{expected}} but found {{actual}}.'
}
},
/** @param {RuleContext} context */
create(context) {
const options = parseOptions(context.options[0])
const sourceCode = context.getSourceCode()
return htmlComments.defineVisitor(
context,
null,
(comment) => {
const baseIndentText = getLineIndentText(comment.open.loc.start.line)
let endLine
if (comment.value) {
const startLine = comment.value.loc.start.line
endLine = comment.value.loc.end.line
const checkStartLine =
comment.open.loc.end.line === startLine ? startLine + 1 : startLine
for (let line = checkStartLine; line <= endLine; line++) {
validateIndentForLine(line, baseIndentText, 1)
}
} else {
endLine = comment.open.loc.end.line
}
if (endLine < comment.close.loc.start.line) {
// `-->`
validateIndentForLine(comment.close.loc.start.line, baseIndentText, 0)
}
},
{ includeDirectives: true }
)
/**
* Checks whether the given line is a blank line.
* @param {number} line The number of line. Begins with 1.
* @returns {boolean} `true` if the given line is a blank line
*/
function isEmptyLine(line) {
const lineText = sourceCode.getLines()[line - 1]
return !lineText.trim()
}
/**
* Get the actual indentation of the given line.
* @param {number} line The number of line. Begins with 1.
* @returns {string} The actual indentation text
*/
function getLineIndentText(line) {
const lineText = sourceCode.getLines()[line - 1]
const charIndex = lineText.search(/\S/)
// already checked
// if (charIndex < 0) {
// return lineText
// }
return lineText.slice(0, charIndex)
}
/**
* Define the function which fixes the problem.
* @param {number} line The number of line.
* @param {string} actualIndentText The actual indentation text.
* @param {string} expectedIndentText The expected indentation text.
* @returns { (fixer: RuleFixer) => Fix } The defined function.
*/
function defineFix(line, actualIndentText, expectedIndentText) {
return (fixer) => {
const start = sourceCode.getIndexFromLoc({
line,
column: 0
})
return fixer.replaceTextRange(
[start, start + actualIndentText.length],
expectedIndentText
)
}
}
/**
* Validate the indentation of a line.
* @param {number} line The number of line. Begins with 1.
* @param {string} baseIndentText The expected base indentation text.
* @param {number} offset The number of the indentation offset.
*/
function validateIndentForLine(line, baseIndentText, offset) {
if (isEmptyLine(line)) {
return
}
const actualIndentText = getLineIndentText(line)
const expectedOffsetIndentText = options.indentText.repeat(offset)
const expectedIndentText = baseIndentText + expectedOffsetIndentText
// validate base indent
if (
baseIndentText &&
(actualIndentText.length < baseIndentText.length ||
!actualIndentText.startsWith(baseIndentText))
) {
context.report({
loc: {
start: { line, column: 0 },
end: { line, column: actualIndentText.length }
},
messageId: actualIndentText
? 'unexpectedBaseIndentation'
: 'missingBaseIndentation',
data: {
expected: toDisplay(baseIndentText),
actual: toDisplay(actualIndentText.slice(0, baseIndentText.length))
},
fix: defineFix(line, actualIndentText, expectedIndentText)
})
return
}
const actualOffsetIndentText = actualIndentText.slice(
baseIndentText.length
)
// validate indent charctor
for (let i = 0; i < actualOffsetIndentText.length; ++i) {
if (actualOffsetIndentText[i] !== options.indentChar) {
context.report({
loc: {
start: { line, column: baseIndentText.length + i },
end: { line, column: baseIndentText.length + i + 1 }
},
messageId: 'unexpectedIndentationCharacter',
data: {
expected: toUnit(options.indentChar),
actual: toUnit(actualOffsetIndentText[i])
},
fix: defineFix(line, actualIndentText, expectedIndentText)
})
return
}
}
// validate indent length
if (actualOffsetIndentText.length !== expectedOffsetIndentText.length) {
context.report({
loc: {
start: { line, column: baseIndentText.length },
end: { line, column: actualIndentText.length }
},
messageId: baseIndentText
? 'unexpectedRelativeIndentation'
: 'unexpectedIndentation',
data: {
expected: toDisplay(expectedOffsetIndentText, options.indentChar),
actual: toDisplay(actualOffsetIndentText, options.indentChar)
},
fix: defineFix(line, actualIndentText, expectedIndentText)
})
}
}
}
}

View File

@ -0,0 +1,64 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce end tag style',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/html-end-tags.html'
},
fixable: 'code',
schema: []
},
/** @param {RuleContext} context */
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)
}
}
)
}
}

View File

@ -0,0 +1,81 @@
/**
* @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 = {
/** @param {RuleContext} context */
create(context) {
const tokenStore =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
const visitor = indentCommon.defineVisitor(context, tokenStore, {
baseIndent: 1
})
return utils.defineTemplateBodyVisitor(context, visitor)
},
meta: {
type: 'layout',
docs: {
description: 'enforce consistent indentation in `<template>`',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/html-indent.html'
},
fixable: 'whitespace',
schema: [
{
anyOf: [{ type: 'integer', minimum: 1 }, { enum: ['tab'] }]
},
{
type: 'object',
properties: {
attribute: { type: 'integer', minimum: 0 },
baseIndent: { type: 'integer', minimum: 0 },
closeBracket: {
anyOf: [
{ type: 'integer', minimum: 0 },
{
type: 'object',
properties: {
startTag: { type: 'integer', minimum: 0 },
endTag: { type: 'integer', minimum: 0 },
selfClosingTag: { type: 'integer', minimum: 0 }
},
additionalProperties: false
}
]
},
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
}
]
}
}

View File

@ -0,0 +1,107 @@
/**
* @author Toru Nagashima
* @copyright 2017 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce quotes style of HTML attributes',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/html-quotes.html'
},
fixable: 'code',
schema: [
{ enum: ['double', 'single'] },
{
type: 'object',
properties: {
avoidEscape: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
const double = context.options[0] !== 'single'
const avoidEscape =
context.options[1] && context.options[1].avoidEscape === true
const quoteChar = double ? '"' : "'"
const quoteName = double ? 'double quotes' : 'single quotes'
/** @type {boolean} */
let hasInvalidEOF
return utils.defineTemplateBodyVisitor(
context,
{
'VAttribute[value!=null]'(node) {
if (hasInvalidEOF) {
return
}
const text = sourceCode.getText(node.value)
const firstChar = text[0]
if (firstChar !== quoteChar) {
const quoted = firstChar === "'" || firstChar === '"'
if (avoidEscape && quoted) {
const contentText = text.slice(1, -1)
if (contentText.includes(quoteChar)) {
return
}
}
context.report({
node: node.value,
loc: node.value.loc,
message: 'Expected to be enclosed by {{kind}}.',
data: { kind: quoteName },
fix(fixer) {
const contentText = quoted ? text.slice(1, -1) : text
const fixToDouble =
avoidEscape && !quoted && contentText.includes(quoteChar)
? double
? contentText.includes("'")
: !contentText.includes('"')
: double
const quotePattern = fixToDouble ? /"/g : /'/g
const quoteEscaped = fixToDouble ? '&quot;' : '&apos;'
const fixQuoteChar = fixToDouble ? '"' : "'"
const replacement =
fixQuoteChar +
contentText.replace(quotePattern, quoteEscaped) +
fixQuoteChar
return fixer.replaceText(node.value, replacement)
}
})
}
}
},
{
Program(node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
}
)
}
}

View File

@ -0,0 +1,224 @@
/**
* @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_MESSAGES = Object.freeze({
NORMAL: 'HTML elements',
VOID: 'HTML void elements',
COMPONENT: 'Vue.js custom components',
SVG: 'SVG elements',
MATH: 'MathML elements',
UNKNOWN: 'unknown elements'
})
/**
* @typedef {object} Options
* @property {'always' | 'never'} NORMAL
* @property {'always' | 'never'} VOID
* @property {'always' | 'never'} COMPONENT
* @property {'always' | 'never'} SVG
* @property {'always' | 'never'} MATH
* @property {null} UNKNOWN
*/
/**
* Normalize the given options.
* @param {any} options The raw options object.
* @returns {Options} Normalized options.
*/
function parseOptions(options) {
return {
NORMAL: (options && options.html && options.html.normal) || 'always',
VOID: (options && options.html && options.html.void) || 'never',
COMPONENT: (options && options.html && options.html.component) || 'always',
SVG: (options && options.svg) || 'always',
MATH: (options && options.math) || 'always',
UNKNOWN: null
}
}
/**
* Get the elementType of the given element.
* @param {VElement} node The element node to get.
* @returns {keyof Options} The elementType of the element.
*/
function getElementType(node) {
if (utils.isCustomComponent(node)) {
return 'COMPONENT'
}
if (utils.isHtmlElementNode(node)) {
if (utils.isHtmlVoidElementName(node.name)) {
return 'VOID'
}
return 'NORMAL'
}
if (utils.isSvgElementNode(node)) {
return 'SVG'
}
if (utils.isMathMLElementNode(node)) {
return 'MATH'
}
return 'UNKNOWN'
}
/**
* Check whether the given element is empty or not.
* This ignores whitespaces, doesn't ignore comments.
* @param {VElement} node The element node to check.
* @param {SourceCode} sourceCode The source code object of the current context.
* @returns {boolean} `true` if the element is empty.
*/
function isEmpty(node, sourceCode) {
const start = node.startTag.range[1]
const end = node.endTag != null ? node.endTag.range[0] : node.range[1]
return sourceCode.text.slice(start, end).trim() === ''
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce self-closing style',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/html-self-closing.html'
},
fixable: 'code',
schema: {
definitions: {
optionValue: {
enum: ['always', 'never', 'any']
}
},
type: 'array',
items: [
{
type: 'object',
properties: {
html: {
type: 'object',
properties: {
normal: { $ref: '#/definitions/optionValue' },
void: { $ref: '#/definitions/optionValue' },
component: { $ref: '#/definitions/optionValue' }
},
additionalProperties: false
},
svg: { $ref: '#/definitions/optionValue' },
math: { $ref: '#/definitions/optionValue' }
},
additionalProperties: false
}
],
maxItems: 1
}
},
/** @param {RuleContext} context */
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: ELEMENT_TYPE_MESSAGES[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: ELEMENT_TYPE_MESSAGES[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 === 'VOID') {
return fixer.replaceText(close, '>')
}
// If only `close` is targeted for replacement, it conflicts with `component-name-in-template-casing`,
// so replace the entire element.
// return fixer.replaceText(close, `></${node.rawName}>`)
const elementPart = sourceCode.text.slice(
node.range[0],
close.range[0]
)
return fixer.replaceText(
node,
`${elementPart}></${node.rawName}>`
)
}
})
}
}
},
{
Program(node) {
hasInvalidEOF = utils.hasInvalidEOF(node)
}
}
)
}
}

View File

@ -0,0 +1,73 @@
// the following rule is based on yannickcr/eslint-plugin-react
/**
The MIT License (MIT)
Copyright (c) 2014 Yannick Croissant
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @fileoverview Prevent variables used in JSX to be marked as unused
* @author Yannick Croissant
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'prevent variables used in JSX to be marked as unused', // eslint-disable-line eslint-plugin/require-meta-docs-description
categories: ['base'],
url: 'https://eslint.vuejs.org/rules/jsx-uses-vars.html'
},
schema: []
},
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
create(context) {
return {
JSXOpeningElement(node) {
let name
if (node.name.type === 'JSXIdentifier') {
// <Foo>
name = node.name.name
} else if (node.name.type === 'JSXMemberExpression') {
// <Foo...Bar>
let parent = node.name.object
while (parent.type === 'JSXMemberExpression') {
parent = parent.object
}
name = parent.name
} else {
return
}
context.markVariableAsUsed(name)
}
}
}
}

View File

@ -0,0 +1,11 @@
/**
* @author Toru Nagashima
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('key-spacing', {
skipDynamicArguments: true
})

View File

@ -0,0 +1,11 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('keyword-spacing', {
skipDynamicArguments: true
})

View File

@ -0,0 +1,151 @@
/**
* @fileoverview Require component name property to match its file name
* @author Rodrigo Pedra Brum <rodrigo.pedra@gmail.com>
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const casing = require('../utils/casing')
const path = require('path')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require component name property to match its file name',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/match-component-file-name.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
extensions: {
type: 'array',
items: {
type: 'string'
},
uniqueItems: true,
additionalItems: false
},
shouldMatchCase: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
create(context) {
const options = context.options[0]
const shouldMatchCase = (options && options.shouldMatchCase) || false
const extensionsArray = options && options.extensions
const allowedExtensions = Array.isArray(extensionsArray)
? extensionsArray
: ['jsx']
const extension = path.extname(context.getFilename())
const filename = path.basename(context.getFilename(), extension)
/** @type {Rule.ReportDescriptor[]} */
const errors = []
let componentCount = 0
if (!allowedExtensions.includes(extension.replace(/^\./, ''))) {
return {}
}
// ----------------------------------------------------------------------
// Private
// ----------------------------------------------------------------------
/**
* @param {string} name
* @param {string} filename
*/
function compareNames(name, filename) {
if (shouldMatchCase) {
return name === filename
}
return (
casing.pascalCase(name) === filename ||
casing.kebabCase(name) === filename
)
}
/**
* @param {Literal | TemplateLiteral} node
*/
function verifyName(node) {
let name
if (node.type === 'TemplateLiteral') {
const quasis = node.quasis[0]
name = quasis.value.cooked
} else {
name = `${node.value}`
}
if (!compareNames(name, filename)) {
errors.push({
node,
message:
'Component name `{{name}}` should match file name `{{filename}}`.',
data: { filename, name }
})
}
}
/**
* @param {Expression | SpreadElement} node
* @returns {node is (Literal | TemplateLiteral)}
*/
function canVerify(node) {
return (
node.type === 'Literal' ||
(node.type === 'TemplateLiteral' &&
node.expressions.length === 0 &&
node.quasis.length === 1)
)
}
return Object.assign(
{},
utils.executeOnCallVueComponent(context, (node) => {
if (node.arguments.length === 2) {
const argument = node.arguments[0]
if (canVerify(argument)) {
verifyName(argument)
}
}
}),
utils.executeOnVue(context, (object) => {
const node = utils.findProperty(object, 'name')
componentCount++
if (!node) return
if (!canVerify(node.value)) return
verifyName(node.value)
}),
{
'Program:exit'() {
if (componentCount > 1) return
errors.forEach((error) => context.report(error))
}
}
)
}
}

View File

@ -0,0 +1,79 @@
/**
* @author Doug Wade <douglas.b.wade@gmail.com>
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
module.exports = {
meta: {
type: 'problem',
schema: [],
docs: {
description:
'require the registered component name to match the imported component name',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/match-component-import-name.html'
},
fixable: null,
messages: {
unexpected:
'Component alias {{importedName}} should be one of: {{expectedName}}.'
}
},
/**
* @param {RuleContext} context
* @returns {RuleListener}
*/
create(context) {
/**
* @param {Identifier} identifier
* @return {Array<String>}
*/
function getExpectedNames(identifier) {
return [
casing.pascalCase(identifier.name),
casing.kebabCase(identifier.name)
]
}
return utils.executeOnVueComponent(context, (obj) => {
const components = utils.findProperty(obj, 'components')
if (
!components ||
!components.value ||
components.value.type !== 'ObjectExpression'
) {
return
}
components.value.properties.forEach(
/** @param {Property | SpreadElement} property */
(property) => {
if (
property.type === 'SpreadElement' ||
property.value.type !== 'Identifier' ||
property.computed === true
) {
return
}
const importedName = utils.getStaticPropertyName(property) || ''
const expectedNames = getExpectedNames(property.value)
if (!expectedNames.includes(importedName)) {
context.report({
node: property,
messageId: 'unexpected',
data: {
importedName,
expectedName: expectedNames.join(', ')
}
})
}
}
)
})
}
}

View File

@ -0,0 +1,179 @@
/**
* @fileoverview Define the number of attributes allows per line
* @author Filipa Lacerda
*/
'use strict'
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const utils = require('../utils')
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce the maximum number of attributes per line',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/max-attributes-per-line.html'
},
fixable: 'whitespace', // or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
singleline: {
anyOf: [
{
type: 'number',
minimum: 1
},
{
type: 'object',
properties: {
max: {
type: 'number',
minimum: 1
}
},
additionalProperties: false
}
]
},
multiline: {
anyOf: [
{
type: 'number',
minimum: 1
},
{
type: 'object',
properties: {
max: {
type: 'number',
minimum: 1
}
},
additionalProperties: false
}
]
}
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
const configuration = parseOptions(context.options[0])
const multilineMaximum = configuration.multiline
const singlelinemMaximum = configuration.singleline
const template =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
return utils.defineTemplateBodyVisitor(context, {
VStartTag(node) {
const numberOfAttributes = node.attributes.length
if (!numberOfAttributes) return
if (utils.isSingleLine(node)) {
if (numberOfAttributes > singlelinemMaximum) {
showErrors(node.attributes.slice(singlelinemMaximum))
}
}
if (!utils.isSingleLine(node)) {
groupAttrsByLine(node.attributes)
.filter((attrs) => attrs.length > multilineMaximum)
.forEach((attrs) => showErrors(attrs.splice(multilineMaximum)))
}
}
})
// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------
/**
* @param {any} options
*/
function parseOptions(options) {
const defaults = {
singleline: 1,
multiline: 1
}
if (options) {
if (typeof options.singleline === 'number') {
defaults.singleline = options.singleline
} else if (typeof options.singleline === 'object') {
if (typeof options.singleline.max === 'number') {
defaults.singleline = options.singleline.max
}
}
if (options.multiline) {
if (typeof options.multiline === 'number') {
defaults.multiline = options.multiline
} else if (typeof options.multiline === 'object') {
if (typeof options.multiline.max === 'number') {
defaults.multiline = options.multiline.max
}
}
}
}
return defaults
}
/**
* @param {(VDirective | VAttribute)[]} attributes
*/
function showErrors(attributes) {
attributes.forEach((prop, i) => {
context.report({
node: prop,
loc: prop.loc,
message: "'{{name}}' should be on a new line.",
data: { name: sourceCode.getText(prop.key) },
fix(fixer) {
if (i !== 0) return null
// Find the closest token before the current prop
// that is not a white space
const prevToken = /** @type {Token} */ (
template.getTokenBefore(prop, {
filter: (token) => token.type !== 'HTMLWhitespace'
})
)
/** @type {Range} */
const range = [prevToken.range[1], prop.range[0]]
return fixer.replaceTextRange(range, '\n')
}
})
})
}
/**
* @param {(VDirective | VAttribute)[]} attributes
*/
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
}
}
}

View File

@ -0,0 +1,560 @@
/**
* @author Yosuke Ota
* @fileoverview Rule to check for max length on a line of Vue file.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const OPTIONS_SCHEMA = {
type: 'object',
properties: {
code: {
type: 'integer',
minimum: 0
},
template: {
type: 'integer',
minimum: 0
},
comments: {
type: 'integer',
minimum: 0
},
tabWidth: {
type: 'integer',
minimum: 0
},
ignorePattern: {
type: 'string'
},
ignoreComments: {
type: 'boolean'
},
ignoreTrailingComments: {
type: 'boolean'
},
ignoreUrls: {
type: 'boolean'
},
ignoreStrings: {
type: 'boolean'
},
ignoreTemplateLiterals: {
type: 'boolean'
},
ignoreRegExpLiterals: {
type: 'boolean'
},
ignoreHTMLAttributeValues: {
type: 'boolean'
},
ignoreHTMLTextContents: {
type: 'boolean'
}
},
additionalProperties: false
}
const OPTIONS_OR_INTEGER_SCHEMA = {
anyOf: [
OPTIONS_SCHEMA,
{
type: 'integer',
minimum: 0
}
]
}
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
/**
* Computes the length of a line that may contain tabs. The width of each
* tab will be the number of spaces to the next tab stop.
* @param {string} line The line.
* @param {number} tabWidth The width of each tab stop in spaces.
* @returns {number} The computed line length.
* @private
*/
function computeLineLength(line, tabWidth) {
let extraCharacterCount = 0
const re = /\t/gu
let ret
while ((ret = re.exec(line))) {
const offset = ret.index
const totalOffset = offset + extraCharacterCount
const previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0
const spaceCount = tabWidth - previousTabStopOffset
extraCharacterCount += spaceCount - 1 // -1 for the replaced tab
}
return Array.from(line).length + extraCharacterCount
}
/**
* Tells if a given comment is trailing: it starts on the current line and
* extends to or past the end of the current line.
* @param {string} line The source line we want to check for a trailing comment on
* @param {number} lineNumber The one-indexed line number for line
* @param {Token | null} comment The comment to inspect
* @returns {comment is Token} If the comment is trailing on the given line
*/
function isTrailingComment(line, lineNumber, comment) {
return Boolean(
comment &&
comment.loc.start.line === lineNumber &&
lineNumber <= comment.loc.end.line &&
(comment.loc.end.line > lineNumber ||
comment.loc.end.column === line.length)
)
}
/**
* Tells if a comment encompasses the entire line.
* @param {string} line The source line with a trailing comment
* @param {number} lineNumber The one-indexed line number this is on
* @param {Token | null} comment The comment to remove
* @returns {boolean} If the comment covers the entire line
*/
function isFullLineComment(line, lineNumber, comment) {
if (!comment) {
return false
}
const start = comment.loc.start
const end = comment.loc.end
const isFirstTokenOnLine = !line.slice(0, comment.loc.start.column).trim()
return (
comment &&
(start.line < lineNumber ||
(start.line === lineNumber && isFirstTokenOnLine)) &&
(end.line > lineNumber ||
(end.line === lineNumber && end.column === line.length))
)
}
/**
* Gets the line after the comment and any remaining trailing whitespace is
* stripped.
* @param {string} line The source line with a trailing comment
* @param {Token} comment The comment to remove
* @returns {string} Line without comment and trailing whitepace
*/
function stripTrailingComment(line, comment) {
// loc.column is zero-indexed
return line.slice(0, comment.loc.start.column).replace(/\s+$/u, '')
}
/**
* Ensure that an array exists at [key] on `object`, and add `value` to it.
*
* @param { { [key: number]: Token[] } } object the object to mutate
* @param {number} key the object's key
* @param {Token} value the value to add
* @returns {void}
* @private
*/
function ensureArrayAndPush(object, key, value) {
if (!Array.isArray(object[key])) {
object[key] = []
}
object[key].push(value)
}
/**
* A reducer to group an AST node by line number, both start and end.
*
* @param { { [key: number]: Token[] } } acc the accumulator
* @param {Token} node the AST node in question
* @returns { { [key: number]: Token[] } } the modified accumulator
* @private
*/
function groupByLineNumber(acc, node) {
for (let i = node.loc.start.line; i <= node.loc.end.line; ++i) {
ensureArrayAndPush(acc, i, node)
}
return acc
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce a maximum line length in `.vue` files',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/max-len.html',
extensionRule: true,
coreRuleUrl: 'https://eslint.org/docs/rules/max-len'
},
schema: [
OPTIONS_OR_INTEGER_SCHEMA,
OPTIONS_OR_INTEGER_SCHEMA,
OPTIONS_SCHEMA
],
messages: {
max: 'This line has a length of {{lineLength}}. Maximum allowed is {{maxLength}}.',
maxComment:
'This line has a comment length of {{lineLength}}. Maximum allowed is {{maxCommentLength}}.'
}
},
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
create(context) {
/*
* Inspired by http://tools.ietf.org/html/rfc3986#appendix-B, however:
* - They're matching an entire string that we know is a URI
* - We're matching part of a string where we think there *might* be a URL
* - We're only concerned about URLs, as picking out any URI would cause
* too many false positives
* - We don't care about matching the entire URL, any small segment is fine
*/
const URL_REGEXP = /[^:/?#]:\/\/[^?#]/u
const sourceCode = context.getSourceCode()
/** @type {Token[]} */
const tokens = []
/** @type {(HTMLComment | HTMLBogusComment | Comment)[]} */
const comments = []
/** @type {VLiteral[]} */
const htmlAttributeValues = []
// The options object must be the last option specified…
const options = Object.assign(
{},
context.options[context.options.length - 1]
)
// …but max code length…
if (typeof context.options[0] === 'number') {
options.code = context.options[0]
}
// …and tabWidth can be optionally specified directly as integers.
if (typeof context.options[1] === 'number') {
options.tabWidth = context.options[1]
}
/** @type {number} */
const scriptMaxLength = typeof options.code === 'number' ? options.code : 80
/** @type {number} */
const tabWidth = typeof options.tabWidth === 'number' ? options.tabWidth : 2 // default value of `vue/html-indent`
/** @type {number} */
const templateMaxLength =
typeof options.template === 'number' ? options.template : scriptMaxLength
const ignoreComments = !!options.ignoreComments
const ignoreStrings = !!options.ignoreStrings
const ignoreTemplateLiterals = !!options.ignoreTemplateLiterals
const ignoreRegExpLiterals = !!options.ignoreRegExpLiterals
const ignoreTrailingComments =
!!options.ignoreTrailingComments || !!options.ignoreComments
const ignoreUrls = !!options.ignoreUrls
const ignoreHTMLAttributeValues = !!options.ignoreHTMLAttributeValues
const ignoreHTMLTextContents = !!options.ignoreHTMLTextContents
/** @type {number} */
const maxCommentLength = options.comments
/** @type {RegExp} */
let ignorePattern = options.ignorePattern || null
if (ignorePattern) {
ignorePattern = new RegExp(ignorePattern, 'u')
}
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
/**
* Retrieves an array containing all strings (" or ') in the source code.
*
* @returns {Token[]} An array of string nodes.
*/
function getAllStrings() {
return tokens.filter(
(token) =>
token.type === 'String' ||
(token.type === 'JSXText' &&
sourceCode.getNodeByRangeIndex(token.range[0] - 1).type ===
'JSXAttribute')
)
}
/**
* Retrieves an array containing all template literals in the source code.
*
* @returns {Token[]} An array of template literal nodes.
*/
function getAllTemplateLiterals() {
return tokens.filter((token) => token.type === 'Template')
}
/**
* Retrieves an array containing all RegExp literals in the source code.
*
* @returns {Token[]} An array of RegExp literal nodes.
*/
function getAllRegExpLiterals() {
return tokens.filter((token) => token.type === 'RegularExpression')
}
/**
* Retrieves an array containing all HTML texts in the source code.
*
* @returns {Token[]} An array of HTML text nodes.
*/
function getAllHTMLTextContents() {
return tokens.filter((token) => token.type === 'HTMLText')
}
/**
* Check the program for max length
* @param {Program} node Node to examine
* @returns {void}
* @private
*/
function checkProgramForMaxLength(node) {
const programNode = node
const templateBody = node.templateBody
// setup tokens
const scriptTokens = sourceCode.ast.tokens
const scriptComments = sourceCode.getAllComments()
if (context.parserServices.getTemplateBodyTokenStore && templateBody) {
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const templateTokens = tokenStore.getTokens(templateBody, {
includeComments: true
})
if (templateBody.range[0] < programNode.range[0]) {
tokens.push(...templateTokens, ...scriptTokens)
} else {
tokens.push(...scriptTokens, ...templateTokens)
}
} else {
tokens.push(...scriptTokens)
}
if (ignoreComments || maxCommentLength || ignoreTrailingComments) {
// list of comments to ignore
if (templateBody) {
if (templateBody.range[0] < programNode.range[0]) {
comments.push(...templateBody.comments, ...scriptComments)
} else {
comments.push(...scriptComments, ...templateBody.comments)
}
} else {
comments.push(...scriptComments)
}
}
/** @type {Range} */
let scriptLinesRange
if (scriptTokens.length) {
if (scriptComments.length) {
scriptLinesRange = [
Math.min(
scriptTokens[0].loc.start.line,
scriptComments[0].loc.start.line
),
Math.max(
scriptTokens[scriptTokens.length - 1].loc.end.line,
scriptComments[scriptComments.length - 1].loc.end.line
)
]
} else {
scriptLinesRange = [
scriptTokens[0].loc.start.line,
scriptTokens[scriptTokens.length - 1].loc.end.line
]
}
} else if (scriptComments.length) {
scriptLinesRange = [
scriptComments[0].loc.start.line,
scriptComments[scriptComments.length - 1].loc.end.line
]
}
const templateLinesRange = templateBody && [
templateBody.loc.start.line,
templateBody.loc.end.line
]
// split (honors line-ending)
const lines = sourceCode.lines
const strings = getAllStrings()
const stringsByLine = strings.reduce(groupByLineNumber, {})
const templateLiterals = getAllTemplateLiterals()
const templateLiteralsByLine = templateLiterals.reduce(
groupByLineNumber,
{}
)
const regExpLiterals = getAllRegExpLiterals()
const regExpLiteralsByLine = regExpLiterals.reduce(groupByLineNumber, {})
const htmlAttributeValuesByLine = htmlAttributeValues.reduce(
groupByLineNumber,
{}
)
const htmlTextContents = getAllHTMLTextContents()
const htmlTextContentsByLine = htmlTextContents.reduce(
groupByLineNumber,
{}
)
const commentsByLine = comments.reduce(groupByLineNumber, {})
lines.forEach((line, i) => {
// i is zero-indexed, line numbers are one-indexed
const lineNumber = i + 1
const inScript =
scriptLinesRange &&
scriptLinesRange[0] <= lineNumber &&
lineNumber <= scriptLinesRange[1]
const inTemplate =
templateLinesRange &&
templateLinesRange[0] <= lineNumber &&
lineNumber <= templateLinesRange[1]
// check if line is inside a script or template.
if (!inScript && !inTemplate) {
// out of range.
return
}
const maxLength =
inScript && inTemplate
? Math.max(scriptMaxLength, templateMaxLength)
: inScript
? scriptMaxLength
: templateMaxLength
if (
(ignoreStrings && stringsByLine[lineNumber]) ||
(ignoreTemplateLiterals && templateLiteralsByLine[lineNumber]) ||
(ignoreRegExpLiterals && regExpLiteralsByLine[lineNumber]) ||
(ignoreHTMLAttributeValues &&
htmlAttributeValuesByLine[lineNumber]) ||
(ignoreHTMLTextContents && htmlTextContentsByLine[lineNumber])
) {
// ignore this line
return
}
/*
* if we're checking comment length; we need to know whether this
* line is a comment
*/
let lineIsComment = false
let textToMeasure
/*
* comments to check.
*/
if (commentsByLine[lineNumber]) {
const commentList = [...commentsByLine[lineNumber]]
let comment = commentList.pop() || null
if (isFullLineComment(line, lineNumber, comment)) {
lineIsComment = true
textToMeasure = line
} else if (
ignoreTrailingComments &&
isTrailingComment(line, lineNumber, comment)
) {
textToMeasure = stripTrailingComment(line, comment)
// ignore multiple trailing comments in the same line
comment = commentList.pop() || null
while (isTrailingComment(textToMeasure, lineNumber, comment)) {
textToMeasure = stripTrailingComment(textToMeasure, comment)
}
} else {
textToMeasure = line
}
} else {
textToMeasure = line
}
if (
(ignorePattern && ignorePattern.test(textToMeasure)) ||
(ignoreUrls && URL_REGEXP.test(textToMeasure))
) {
// ignore this line
return
}
const lineLength = computeLineLength(textToMeasure, tabWidth)
const commentLengthApplies = lineIsComment && maxCommentLength
if (lineIsComment && ignoreComments) {
return
}
if (commentLengthApplies) {
if (lineLength > maxCommentLength) {
context.report({
node,
loc: { line: lineNumber, column: 0 },
messageId: 'maxComment',
data: {
lineLength,
maxCommentLength
}
})
}
} else if (lineLength > maxLength) {
context.report({
node,
loc: { line: lineNumber, column: 0 },
messageId: 'max',
data: {
lineLength,
maxLength
}
})
}
})
}
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
return utils.compositingVisitors(
utils.defineTemplateBodyVisitor(context, {
/** @param {VLiteral} node */
'VAttribute[directive=false] > VLiteral'(node) {
htmlAttributeValues.push(node)
}
}),
{
'Program:exit'(node) {
checkProgramForMaxLength(node)
}
}
)
}
}

View File

@ -0,0 +1,132 @@
/**
* @author Marton Csordas
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const path = require('path')
const casing = require('../utils/casing')
const utils = require('../utils')
const RESERVED_NAMES_IN_VUE3 = new Set(
require('../utils/vue3-builtin-components')
)
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require component names to be always multi-word',
categories: ['vue3-essential', 'essential'],
url: 'https://eslint.vuejs.org/rules/multi-word-component-names.html'
},
schema: [
{
type: 'object',
properties: {
ignores: {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
],
messages: {
unexpected: 'Component name "{{value}}" should always be multi-word.'
}
},
/** @param {RuleContext} context */
create(context) {
/** @type {Set<string>} */
const ignores = new Set()
ignores.add('App')
ignores.add('app')
for (const ignore of (context.options[0] && context.options[0].ignores) ||
[]) {
ignores.add(ignore)
if (casing.isPascalCase(ignore)) {
// PascalCase
ignores.add(casing.kebabCase(ignore))
}
}
let hasVue = utils.isScriptSetup(context)
let hasName = false
/**
* Returns true if the given component name is valid, otherwise false.
* @param {string} name
* */
function isValidComponentName(name) {
if (ignores.has(name) || RESERVED_NAMES_IN_VUE3.has(name)) {
return true
}
const elements = casing.kebabCase(name).split('-')
return elements.length > 1
}
/**
* @param {Expression | SpreadElement} nameNode
*/
function validateName(nameNode) {
if (nameNode.type !== 'Literal') return
const componentName = `${nameNode.value}`
if (!isValidComponentName(componentName)) {
context.report({
node: nameNode,
messageId: 'unexpected',
data: {
value: componentName
}
})
}
}
return utils.compositingVisitors(
utils.executeOnCallVueComponent(context, (node) => {
hasVue = true
if (node.arguments.length !== 2) return
hasName = true
validateName(node.arguments[0])
}),
utils.executeOnVue(context, (obj) => {
hasVue = true
const node = utils.findProperty(obj, 'name')
if (!node) return
hasName = true
validateName(node.value)
}),
{
/** @param {Program} node */
'Program:exit'(node) {
if (hasName) return
if (!hasVue && node.body.length > 0) return
const fileName = context.getFilename()
const componentName = path.basename(fileName, path.extname(fileName))
if (
utils.isVueFile(fileName) &&
!isValidComponentName(componentName)
) {
context.report({
messageId: 'unexpected',
data: {
value: componentName
},
loc: { line: 1, column: 0 }
})
}
}
}
)
}
}

View File

@ -0,0 +1,243 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const casing = require('../utils/casing')
const INLINE_ELEMENTS = require('../utils/inline-non-void-elements.json')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* @param {VElement & { endTag: VEndTag }} element
*/
function isMultilineElement(element) {
return element.loc.start.line < element.endTag.loc.start.line
}
/**
* @param {any} options
*/
function parseOptions(options) {
return Object.assign(
{
ignores: ['pre', 'textarea'].concat(INLINE_ELEMENTS),
ignoreWhenEmpty: true,
allowEmptyLines: false
},
options
)
}
/**
* @param {number} lineBreaks
*/
function getPhrase(lineBreaks) {
switch (lineBreaks) {
case 0:
return 'no'
default:
return `${lineBreaks}`
}
}
/**
* Check whether the given element is empty or not.
* This ignores whitespaces, doesn't ignore comments.
* @param {VElement & { endTag: VEndTag }} node The element node to check.
* @param {SourceCode} sourceCode The source code object of the current context.
* @returns {boolean} `true` if the element is empty.
*/
function isEmpty(node, sourceCode) {
const start = node.startTag.range[1]
const end = node.endTag.range[0]
return sourceCode.text.slice(start, end).trim() === ''
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description:
'require a line break before and after the contents of a multiline element',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/multiline-html-element-content-newline.html'
},
fixable: 'whitespace',
schema: [
{
type: 'object',
properties: {
ignoreWhenEmpty: {
type: 'boolean'
},
ignores: {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
},
allowEmptyLines: {
type: 'boolean'
}
},
additionalProperties: false
}
],
messages: {
unexpectedAfterClosingBracket:
'Expected 1 line break after opening tag (`<{{name}}>`), but {{actual}} line breaks found.',
unexpectedBeforeOpeningBracket:
'Expected 1 line break before closing tag (`</{{name}}>`), but {{actual}} line breaks found.'
}
},
/** @param {RuleContext} context */
create(context) {
const options = parseOptions(context.options[0])
const ignores = options.ignores
const ignoreWhenEmpty = options.ignoreWhenEmpty
const allowEmptyLines = options.allowEmptyLines
const template =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
const sourceCode = context.getSourceCode()
/** @type {VElement | null} */
let inIgnoreElement = null
/**
* @param {VElement} node
*/
function isIgnoredElement(node) {
return (
ignores.includes(node.name) ||
ignores.includes(casing.pascalCase(node.rawName)) ||
ignores.includes(casing.kebabCase(node.rawName))
)
}
/**
* @param {number} lineBreaks
*/
function isInvalidLineBreaks(lineBreaks) {
if (allowEmptyLines) {
return lineBreaks === 0
} else {
return lineBreaks !== 1
}
}
return utils.defineTemplateBodyVisitor(context, {
VElement(node) {
if (inIgnoreElement) {
return
}
if (isIgnoredElement(node)) {
// ignore element name
inIgnoreElement = node
return
}
if (node.startTag.selfClosing || !node.endTag) {
// self closing
return
}
const element = /** @type {VElement & { endTag: VEndTag }} */ (node)
if (!isMultilineElement(element)) {
return
}
/**
* @type {SourceCode.CursorWithCountOptions}
*/
const getTokenOption = {
includeComments: true,
filter: (token) => token.type !== 'HTMLWhitespace'
}
if (
ignoreWhenEmpty &&
element.children.length === 0 &&
template.getFirstTokensBetween(
element.startTag,
element.endTag,
getTokenOption
).length === 0
) {
return
}
const contentFirst = /** @type {Token} */ (
template.getTokenAfter(element.startTag, getTokenOption)
)
const contentLast = /** @type {Token} */ (
template.getTokenBefore(element.endTag, getTokenOption)
)
const beforeLineBreaks =
contentFirst.loc.start.line - element.startTag.loc.end.line
const afterLineBreaks =
element.endTag.loc.start.line - contentLast.loc.end.line
if (isInvalidLineBreaks(beforeLineBreaks)) {
context.report({
node: template.getLastToken(element.startTag),
loc: {
start: element.startTag.loc.end,
end: contentFirst.loc.start
},
messageId: 'unexpectedAfterClosingBracket',
data: {
name: element.rawName,
actual: getPhrase(beforeLineBreaks)
},
fix(fixer) {
/** @type {Range} */
const range = [element.startTag.range[1], contentFirst.range[0]]
return fixer.replaceTextRange(range, '\n')
}
})
}
if (isEmpty(element, sourceCode)) {
return
}
if (isInvalidLineBreaks(afterLineBreaks)) {
context.report({
node: template.getFirstToken(element.endTag),
loc: {
start: contentLast.loc.end,
end: element.endTag.loc.start
},
messageId: 'unexpectedBeforeOpeningBracket',
data: {
name: element.name,
actual: getPhrase(afterLineBreaks)
},
fix(fixer) {
/** @type {Range} */
const range = [contentLast.range[1], element.endTag.range[0]]
return fixer.replaceTextRange(range, '\n')
}
})
}
},
'VElement:exit'(node) {
if (inIgnoreElement === node) {
inIgnoreElement = null
}
}
})
}
}

View File

@ -0,0 +1,107 @@
/**
* @fileoverview enforce unified spacing in mustache interpolations.
* @author Armano
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'enforce unified spacing in mustache interpolations',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/mustache-interpolation-spacing.html'
},
fixable: 'whitespace',
schema: [
{
enum: ['always', 'never']
}
]
},
/** @param {RuleContext} context */
create(context) {
const options = context.options[0] || 'always'
const template =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.defineTemplateBodyVisitor(context, {
/** @param {VExpressionContainer} node */
'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]])
})
}
}
}
})
}
}

View File

@ -0,0 +1,71 @@
/**
* @fileoverview Requires specific casing for the name property in Vue components
* @author Armano
*/
'use strict'
const utils = require('../utils')
const casing = require('../utils/casing')
const allowedCaseOptions = ['PascalCase', 'kebab-case']
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'enforce specific casing for the name property in Vue components',
categories: ['vue3-strongly-recommended', 'strongly-recommended'],
url: 'https://eslint.vuejs.org/rules/name-property-casing.html'
},
deprecated: true,
replacedBy: ['component-definition-name-casing'],
fixable: 'code', // or "code" or "whitespace"
schema: [
{
enum: allowedCaseOptions
}
]
},
/** @param {RuleContext} context */
create(context) {
const options = context.options[0]
const caseType =
allowedCaseOptions.indexOf(options) !== -1 ? options : 'PascalCase'
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const node = utils.findProperty(obj, 'name')
if (!node) return
const valueNode = node.value
if (valueNode.type !== 'Literal') return
if (!casing.getChecker(caseType)(`${valueNode.value}`)) {
const value = casing.getExactConverter(caseType)(`${valueNode.value}`)
context.report({
node: valueNode,
message: 'Property name "{{value}}" is not {{caseType}}.',
data: {
value: `${valueNode.value}`,
caseType
},
fix: (fixer) =>
fixer.replaceText(
valueNode,
context
.getSourceCode()
.getText(valueNode)
.replace(`${valueNode.value}`, value)
)
})
}
})
}
}

View File

@ -0,0 +1,154 @@
/**
* @fileoverview Enforce new lines between multi-line properties in Vue components.
* @author IWANABETHATGUY
*/
'use strict'
const utils = require('../utils')
/**
* @param {Token} node
*/
function isComma(node) {
return node.type === 'Punctuator' && node.value === ','
}
/**
* Check whether the between given nodes has empty line.
* @param {SourceCode} sourceCode
* @param {ASTNode} pre
* @param {ASTNode} cur
*/
function* iterateBetweenTokens(sourceCode, pre, cur) {
yield sourceCode.getLastToken(pre)
yield* sourceCode.getTokensBetween(pre, cur, {
includeComments: true
})
yield sourceCode.getFirstToken(cur)
}
/**
* Check whether the between given nodes has empty line.
* @param {SourceCode} sourceCode
* @param {ASTNode} pre
* @param {ASTNode} cur
*/
function hasEmptyLine(sourceCode, pre, cur) {
/** @type {Token|null} */
let preToken = null
for (const token of iterateBetweenTokens(sourceCode, pre, cur)) {
if (preToken && token.loc.start.line - preToken.loc.end.line >= 2) {
return true
}
preToken = token
}
return false
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'layout',
docs: {
description:
'enforce new lines between multi-line properties in Vue components',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/new-line-between-multi-line-property.html'
},
fixable: 'whitespace', // or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
// number of line you want to insert after multi-line property
minLineOfMultilineProperty: {
type: 'number',
minimum: 2
}
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
create(context) {
let minLineOfMultilineProperty = 2
if (
context.options &&
context.options[0] &&
context.options[0].minLineOfMultilineProperty
) {
minLineOfMultilineProperty = context.options[0].minLineOfMultilineProperty
}
/** @type {CallExpression[]} */
const callStack = []
const sourceCode = context.getSourceCode()
return Object.assign(
utils.defineVueVisitor(context, {
CallExpression(node) {
callStack.push(node)
},
'CallExpression:exit'() {
callStack.pop()
},
/**
* @param {ObjectExpression} node
*/
ObjectExpression(node) {
if (callStack.length) {
return
}
const properties = node.properties
for (let i = 1; i < properties.length; i++) {
const cur = properties[i]
const pre = properties[i - 1]
const lineCountOfPreProperty =
pre.loc.end.line - pre.loc.start.line + 1
if (lineCountOfPreProperty < minLineOfMultilineProperty) {
continue
}
if (hasEmptyLine(sourceCode, pre, cur)) {
continue
}
context.report({
node: pre,
loc: {
start: pre.loc.end,
end: cur.loc.start
},
message:
'Enforce new lines between multi-line properties in Vue components.',
fix(fixer) {
/** @type {Token|null} */
let preToken = null
for (const token of iterateBetweenTokens(
sourceCode,
pre,
cur
)) {
if (
preToken &&
preToken.loc.end.line < token.loc.start.line
) {
return fixer.insertTextAfter(preToken, '\n')
}
preToken = token
}
const commaToken = sourceCode.getTokenAfter(pre, isComma)
return fixer.insertTextAfter(commaToken || pre, '\n\n')
}
})
}
}
})
)
}
}

View File

@ -0,0 +1,146 @@
/**
* @fileoverview enforce Promise or callback style in `nextTick`
* @author Flo Edelmann
* @copyright 2020 Flo Edelmann. All rights reserved.
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const { findVariable } = require('eslint-utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* @param {Identifier} identifier
* @param {RuleContext} context
* @returns {CallExpression|undefined}
*/
function getVueNextTickCallExpression(identifier, context) {
// Instance API: this.$nextTick()
if (
identifier.name === '$nextTick' &&
identifier.parent.type === 'MemberExpression' &&
utils.isThis(identifier.parent.object, context) &&
identifier.parent.parent.type === 'CallExpression' &&
identifier.parent.parent.callee === identifier.parent
) {
return identifier.parent.parent
}
// Vue 2 Global API: Vue.nextTick()
if (
identifier.name === 'nextTick' &&
identifier.parent.type === 'MemberExpression' &&
identifier.parent.object.type === 'Identifier' &&
identifier.parent.object.name === 'Vue' &&
identifier.parent.parent.type === 'CallExpression' &&
identifier.parent.parent.callee === identifier.parent
) {
return identifier.parent.parent
}
// Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
if (
identifier.parent.type === 'CallExpression' &&
identifier.parent.callee === identifier
) {
const variable = findVariable(context.getScope(), identifier)
if (variable != null && variable.defs.length === 1) {
const def = variable.defs[0]
if (
def.type === 'ImportBinding' &&
def.node.type === 'ImportSpecifier' &&
def.node.imported.type === 'Identifier' &&
def.node.imported.name === 'nextTick' &&
def.node.parent.type === 'ImportDeclaration' &&
def.node.parent.source.value === 'vue'
) {
return identifier.parent
}
}
}
return undefined
}
/**
* @param {CallExpression} callExpression
* @returns {boolean}
*/
function isAwaitedPromise(callExpression) {
return (
callExpression.parent.type === 'AwaitExpression' ||
(callExpression.parent.type === 'MemberExpression' &&
callExpression.parent.property.type === 'Identifier' &&
callExpression.parent.property.name === 'then')
)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce Promise or callback style in `nextTick`',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/next-tick-style.html'
},
fixable: 'code',
schema: [{ enum: ['promise', 'callback'] }]
},
/** @param {RuleContext} context */
create(context) {
const preferredStyle =
/** @type {string|undefined} */ (context.options[0]) || 'promise'
return utils.defineVueVisitor(context, {
/** @param {Identifier} node */
Identifier(node) {
const callExpression = getVueNextTickCallExpression(node, context)
if (!callExpression) {
return
}
if (preferredStyle === 'callback') {
if (
callExpression.arguments.length !== 1 ||
isAwaitedPromise(callExpression)
) {
context.report({
node,
message:
'Pass a callback function to `nextTick` instead of using the returned Promise.'
})
}
return
}
if (
callExpression.arguments.length !== 0 ||
!isAwaitedPromise(callExpression)
) {
context.report({
node,
message:
'Use the Promise returned by `nextTick` instead of passing a callback function.',
fix(fixer) {
return fixer.insertTextAfter(node, '().then')
}
})
}
}
})
}
}

View File

@ -0,0 +1,47 @@
/**
* @author Sosuke Suzuki
*/
'use strict'
const utils = require('../utils')
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow using arrow functions to define watcher',
categories: ['vue3-essential', 'essential'],
url: 'https://eslint.vuejs.org/rules/no-arrow-functions-in-watch.html'
},
fixable: null,
schema: []
},
/** @param {RuleContext} context */
create(context) {
return utils.executeOnVue(context, (obj) => {
const watchNode = utils.findProperty(obj, 'watch')
if (watchNode == null) {
return
}
const watchValue = watchNode.value
if (watchValue.type !== 'ObjectExpression') {
return
}
for (const property of watchValue.properties) {
if (property.type !== 'Property') {
continue
}
for (const handler of utils.iterateWatchHandlerValues(property)) {
if (handler.type === 'ArrowFunctionExpression') {
context.report({
node: handler,
message:
'You should not use an arrow function to define a watcher.'
})
}
}
}
})
}
}

View File

@ -0,0 +1,293 @@
/**
* @fileoverview Check if there are no asynchronous actions inside computed properties.
* @author Armano
*/
'use strict'
const { ReferenceTracker } = require('eslint-utils')
const utils = require('../utils')
/**
* @typedef {import('../utils').VueObjectData} VueObjectData
* @typedef {import('../utils').VueVisitor} VueVisitor
* @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty
*/
const PROMISE_FUNCTIONS = new Set(['then', 'catch', 'finally'])
const PROMISE_METHODS = new Set(['all', 'race', 'reject', 'resolve'])
const TIMED_FUNCTIONS = new Set([
'setTimeout',
'setInterval',
'setImmediate',
'requestAnimationFrame'
])
/**
* @param {CallExpression} node
*/
function isTimedFunction(node) {
const callee = utils.skipChainExpression(node.callee)
return (
((callee.type === 'Identifier' && TIMED_FUNCTIONS.has(callee.name)) ||
(callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === 'window' &&
TIMED_FUNCTIONS.has(utils.getStaticPropertyName(callee) || ''))) &&
node.arguments.length > 0
)
}
/**
* @param {CallExpression} node
*/
function isPromise(node) {
const callee = utils.skipChainExpression(node.callee)
if (callee.type === 'MemberExpression') {
const name = utils.getStaticPropertyName(callee)
return (
name &&
// hello.PROMISE_FUNCTION()
(PROMISE_FUNCTIONS.has(name) ||
// Promise.PROMISE_METHOD()
(callee.object.type === 'Identifier' &&
callee.object.name === 'Promise' &&
PROMISE_METHODS.has(name)))
)
}
return false
}
/**
* @param {CallExpression} node
* @param {RuleContext} context
*/
function isNextTick(node, context) {
const callee = utils.skipChainExpression(node.callee)
if (callee.type === 'MemberExpression') {
const name = utils.getStaticPropertyName(callee)
return (
(utils.isThis(callee.object, context) && name === '$nextTick') ||
(callee.object.type === 'Identifier' &&
callee.object.name === 'Vue' &&
name === 'nextTick')
)
}
return false
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow asynchronous actions in computed properties',
categories: ['vue3-essential', 'essential'],
url: 'https://eslint.vuejs.org/rules/no-async-in-computed-properties.html'
},
fixable: null,
schema: []
},
/** @param {RuleContext} context */
create(context) {
/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
const computedPropertiesMap = new Map()
/** @type {(FunctionExpression | ArrowFunctionExpression)[]} */
const computedFunctionNodes = []
/**
* @typedef {object} ScopeStack
* @property {ScopeStack | null} upper
* @property {BlockStatement | Expression} body
*/
/** @type {ScopeStack | null} */
let scopeStack = null
const expressionTypes = {
promise: 'asynchronous action',
nextTick: 'asynchronous action',
await: 'await operator',
async: 'async function declaration',
new: 'Promise object',
timed: 'timed function'
}
/**
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
* @param {VueObjectData|undefined} [info]
*/
function onFunctionEnter(node, info) {
if (node.async) {
verify(
node,
node.body,
'async',
info ? computedPropertiesMap.get(info.node) : null
)
}
scopeStack = {
upper: scopeStack,
body: node.body
}
}
function onFunctionExit() {
scopeStack = scopeStack && scopeStack.upper
}
/**
* @param {ESNode} node
* @param {BlockStatement | Expression} targetBody
* @param {keyof expressionTypes} type
* @param {ComponentComputedProperty[]|undefined|null} computedProperties
*/
function verify(node, targetBody, type, computedProperties) {
for (const cp of computedProperties || []) {
if (
cp.value &&
node.loc.start.line >= cp.value.loc.start.line &&
node.loc.end.line <= cp.value.loc.end.line &&
targetBody === cp.value
) {
context.report({
node,
message:
'Unexpected {{expressionName}} in "{{propertyName}}" computed property.',
data: {
expressionName: expressionTypes[type],
propertyName: cp.key || 'unknown'
}
})
return
}
}
for (const cf of computedFunctionNodes) {
if (
node.loc.start.line >= cf.body.loc.start.line &&
node.loc.end.line <= cf.body.loc.end.line &&
targetBody === cf.body
) {
context.report({
node,
message: 'Unexpected {{expressionName}} in computed function.',
data: {
expressionName: expressionTypes[type]
}
})
return
}
}
}
const nodeVisitor = {
':function': onFunctionEnter,
':function:exit': onFunctionExit,
/**
* @param {NewExpression} node
* @param {VueObjectData|undefined} [info]
*/
NewExpression(node, info) {
if (!scopeStack) {
return
}
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'Promise'
) {
verify(
node,
scopeStack.body,
'new',
info ? computedPropertiesMap.get(info.node) : null
)
}
},
/**
* @param {CallExpression} node
* @param {VueObjectData|undefined} [info]
*/
CallExpression(node, info) {
if (!scopeStack) {
return
}
if (isPromise(node)) {
verify(
node,
scopeStack.body,
'promise',
info ? computedPropertiesMap.get(info.node) : null
)
} else if (isTimedFunction(node)) {
verify(
node,
scopeStack.body,
'timed',
info ? computedPropertiesMap.get(info.node) : null
)
} else if (isNextTick(node, context)) {
verify(
node,
scopeStack.body,
'nextTick',
info ? computedPropertiesMap.get(info.node) : null
)
}
},
/**
* @param {AwaitExpression} node
* @param {VueObjectData|undefined} [info]
*/
AwaitExpression(node, info) {
if (!scopeStack) {
return
}
verify(
node,
scopeStack.body,
'await',
info ? computedPropertiesMap.get(info.node) : null
)
}
}
return utils.compositingVisitors(
{
Program() {
const tracker = new ReferenceTracker(context.getScope())
const traceMap = utils.createCompositionApiTraceMap({
[ReferenceTracker.ESM]: true,
computed: {
[ReferenceTracker.CALL]: true
}
})
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
if (node.type !== 'CallExpression') {
continue
}
const getter = utils.getGetterBodyFromComputedFunction(node)
if (getter) {
computedFunctionNodes.push(getter)
}
}
}
},
utils.isScriptSetup(context)
? utils.defineScriptSetupVisitor(context, nodeVisitor)
: utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
computedPropertiesMap.set(node, utils.getComputedProperties(node))
},
...nodeVisitor
})
)
}
}

View File

@ -0,0 +1,276 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const regexp = require('../utils/regexp')
const casing = require('../utils/casing')
/**
* @typedef { { names: { [tagName in string]: Set<string> }, regexps: { name: RegExp, attrs: Set<string> }[], cache: { [tagName in string]: Set<string> } } } TargetAttrs
*/
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
// https://dev.w3.org/html5/html-author/charref
const DEFAULT_ALLOWLIST = [
'(',
')',
',',
'.',
'&',
'+',
'-',
'=',
'*',
'/',
'#',
'%',
'!',
'?',
':',
'[',
']',
'{',
'}',
'<',
'>',
'\u00b7', // "·"
'\u2022', // "•"
'\u2010', // ""
'\u2013', // ""
'\u2014', // "—"
'\u2212', // ""
'|'
]
const DEFAULT_ATTRIBUTES = {
'/.+/': [
'title',
'aria-label',
'aria-placeholder',
'aria-roledescription',
'aria-valuetext'
],
input: ['placeholder'],
img: ['alt']
}
const DEFAULT_DIRECTIVES = ['v-text']
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
/**
* Parse attributes option
* @param {any} options
* @returns {TargetAttrs}
*/
function parseTargetAttrs(options) {
/** @type {TargetAttrs} */
const result = { names: {}, regexps: [], cache: {} }
for (const tagName of Object.keys(options)) {
/** @type { Set<string> } */
const attrs = new Set(options[tagName])
if (regexp.isRegExp(tagName)) {
result.regexps.push({
name: regexp.toRegExp(tagName),
attrs
})
} else {
result.names[tagName] = attrs
}
}
return result
}
/**
* Get a string from given expression container node
* @param {VExpressionContainer} value
* @returns { string | null }
*/
function getStringValue(value) {
const expression = value.expression
if (!expression) {
return null
}
if (expression.type !== 'Literal') {
return null
}
if (typeof expression.value === 'string') {
return expression.value
}
return null
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow the use of bare strings in `<template>`',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-bare-strings-in-template.html'
},
schema: [
{
type: 'object',
properties: {
allowlist: {
type: 'array',
items: { type: 'string' },
uniqueItems: true
},
attributes: {
type: 'object',
patternProperties: {
'^(?:\\S+|/.*/[a-z]*)$': {
type: 'array',
items: { type: 'string' },
uniqueItems: true
}
},
additionalProperties: false
},
directives: {
type: 'array',
items: { type: 'string', pattern: '^v-' },
uniqueItems: true
}
},
additionalProperties: false
}
],
messages: {
unexpected: 'Unexpected non-translated string used.',
unexpectedInAttr: 'Unexpected non-translated string used in `{{attr}}`.'
}
},
/** @param {RuleContext} context */
create(context) {
/**
* @typedef { { upper: ElementStack | null, name: string, attrs: Set<string> } } ElementStack
*/
const opts = context.options[0] || {}
/** @type {string[]} */
const allowlist = opts.allowlist || DEFAULT_ALLOWLIST
const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES)
const directives = opts.directives || DEFAULT_DIRECTIVES
const allowlistRe = new RegExp(
allowlist.map((w) => regexp.escape(w)).join('|'),
'gu'
)
/** @type {ElementStack | null} */
let elementStack = null
/**
* Gets the bare string from given string
* @param {string} str
*/
function getBareString(str) {
return str.trim().replace(allowlistRe, '').trim()
}
/**
* Get the attribute to be verified from the element name.
* @param {string} tagName
* @returns {Set<string>}
*/
function getTargetAttrs(tagName) {
if (attributes.cache[tagName]) {
return attributes.cache[tagName]
}
/** @type {string[]} */
const result = []
if (attributes.names[tagName]) {
result.push(...attributes.names[tagName])
}
for (const { name, attrs } of attributes.regexps) {
name.lastIndex = 0
if (name.test(tagName)) {
result.push(...attrs)
}
}
if (casing.isKebabCase(tagName)) {
result.push(...getTargetAttrs(casing.pascalCase(tagName)))
}
return (attributes.cache[tagName] = new Set(result))
}
return utils.defineTemplateBodyVisitor(context, {
/** @param {VText} node */
VText(node) {
if (getBareString(node.value)) {
context.report({
node,
messageId: 'unexpected'
})
}
},
/**
* @param {VElement} node
*/
VElement(node) {
elementStack = {
upper: elementStack,
name: node.rawName,
attrs: getTargetAttrs(node.rawName)
}
},
'VElement:exit'() {
elementStack = elementStack && elementStack.upper
},
/** @param {VAttribute|VDirective} node */
VAttribute(node) {
if (!node.value || !elementStack) {
return
}
if (node.directive === false) {
const attrs = elementStack.attrs
if (!attrs.has(node.key.rawName)) {
return
}
if (getBareString(node.value.value)) {
context.report({
node: node.value,
messageId: 'unexpectedInAttr',
data: {
attr: node.key.rawName
}
})
}
} else {
const directive = `v-${node.key.name.name}`
if (!directives.includes(directive)) {
return
}
const str = getStringValue(node.value)
if (str && getBareString(str)) {
context.report({
node: node.value,
messageId: 'unexpectedInAttr',
data: {
attr: directive
}
})
}
}
}
})
}
}

View File

@ -0,0 +1,128 @@
/**
* @fileoverview Prevents boolean defaults from being set
* @author Hiroki Osame
*/
'use strict'
const utils = require('../utils')
/**
* @typedef {import('../utils').ComponentProp} ComponentProp
*/
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
/**
* @param {Property | SpreadElement} prop
*/
function isBooleanProp(prop) {
return (
prop.type === 'Property' &&
prop.key.type === 'Identifier' &&
prop.key.name === 'type' &&
prop.value.type === 'Identifier' &&
prop.value.name === 'Boolean'
)
}
/**
* @param {ObjectExpression} propDefValue
*/
function getDefaultNode(propDefValue) {
return utils.findProperty(propDefValue, 'default')
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow boolean defaults',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-boolean-default.html'
},
fixable: 'code',
schema: [
{
enum: ['default-false', 'no-default']
}
]
},
/** @param {RuleContext} context */
create(context) {
const booleanType = context.options[0] || 'no-default'
/**
* @param {ComponentProp} prop
* @param { { [key: string]: Expression | undefined } } [withDefaultsExpressions]
*/
function processProp(prop, withDefaultsExpressions) {
if (prop.type === 'object') {
if (prop.value.type !== 'ObjectExpression') {
return
}
if (!prop.value.properties.some(isBooleanProp)) {
return
}
const defaultNode = getDefaultNode(prop.value)
if (!defaultNode) {
return
}
verifyDefaultExpression(defaultNode.value)
} else if (prop.type === 'type') {
if (prop.types.length !== 1 || prop.types[0] !== 'Boolean') {
return
}
const defaultNode =
withDefaultsExpressions && withDefaultsExpressions[prop.propName]
if (!defaultNode) {
return
}
verifyDefaultExpression(defaultNode)
}
}
/**
* @param {ComponentProp[]} props
* @param { { [key: string]: Expression | undefined } } [withDefaultsExpressions]
*/
function processProps(props, withDefaultsExpressions) {
for (const prop of props) {
processProp(prop, withDefaultsExpressions)
}
}
/**
* @param {Expression} defaultNode
*/
function verifyDefaultExpression(defaultNode) {
switch (booleanType) {
case 'no-default':
context.report({
node: defaultNode,
message:
'Boolean prop should not set a default (Vue defaults it to false).'
})
break
case 'default-false':
if (defaultNode.type !== 'Literal' || defaultNode.value !== false) {
context.report({
node: defaultNode,
message: 'Boolean prop should only be defaulted to false.'
})
}
break
}
}
return utils.compositingVisitors(
utils.executeOnVueComponent(context, (obj) => {
processProps(utils.getComponentPropsFromOptions(obj))
}),
utils.defineScriptSetupVisitor(context, {
onDefinePropsEnter(node, props) {
processProps(props, utils.getWithDefaultsPropExpressions(node))
}
})
)
}
}

View File

@ -0,0 +1,164 @@
/**
* @author Flo Edelmann
* See LICENSE file in root directory for full license.
*/
'use strict'
const { defineTemplateBodyVisitor } = require('../utils')
/**
* @typedef {object} RuleOption
* @property {string[]} additionalDirectives
*/
/**
* @param {VNode | Token} node
* @returns {boolean}
*/
function isWhiteSpaceTextNode(node) {
return node.type === 'VText' && node.value.trim() === ''
}
/**
* @param {Position} pos1
* @param {Position} pos2
* @returns {'less' | 'equal' | 'greater'}
*/
function comparePositions(pos1, pos2) {
if (
pos1.line < pos2.line ||
(pos1.line === pos2.line && pos1.column < pos2.column)
) {
return 'less'
}
if (
pos1.line > pos2.line ||
(pos1.line === pos2.line && pos1.column > pos2.column)
) {
return 'greater'
}
return 'equal'
}
/**
* @param {(VNode | Token)[]} nodes
* @returns {SourceLocation | undefined}
*/
function getLocationRange(nodes) {
/** @type {Position | undefined} */
let start
/** @type {Position | undefined} */
let end
for (const node of nodes) {
if (!start || comparePositions(node.loc.start, start) === 'less') {
start = node.loc.start
}
if (!end || comparePositions(node.loc.end, end) === 'greater') {
end = node.loc.end
}
}
if (start === undefined || end === undefined) {
return undefined
}
return { start, end }
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
hasSuggestions: true,
type: 'problem',
docs: {
description:
"disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text`",
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-child-content.html'
},
fixable: null,
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
additionalDirectives: {
type: 'array',
uniqueItems: true,
minItems: 1,
items: {
type: 'string'
}
}
},
required: ['additionalDirectives']
}
]
},
/** @param {RuleContext} context */
create(context) {
const directives = new Set(['html', 'text'])
/** @type {RuleOption | undefined} */
const option = context.options[0]
if (option !== undefined) {
for (const directive of option.additionalDirectives) {
directives.add(directive)
}
}
return defineTemplateBodyVisitor(context, {
/** @param {VDirective} directiveNode */
'VAttribute[directive=true]'(directiveNode) {
const directiveName = directiveNode.key.name.name
const elementNode = directiveNode.parent.parent
if (elementNode.endTag === null) {
return
}
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const elementComments = tokenStore.getTokensBetween(
elementNode.startTag,
elementNode.endTag,
{
includeComments: true,
filter: (token) => token.type === 'HTMLComment'
}
)
const childNodes = [...elementNode.children, ...elementComments]
if (
directives.has(directiveName) &&
childNodes.length > 0 &&
childNodes.some((childNode) => !isWhiteSpaceTextNode(childNode))
) {
context.report({
node: elementNode,
loc: getLocationRange(childNodes),
message:
'Child content is disallowed because it will be overwritten by the v-{{ directiveName }} directive.',
data: { directiveName },
suggest: [
{
desc: 'Remove child content.',
*fix(fixer) {
for (const childNode of childNodes) {
yield fixer.remove(childNode)
}
}
}
]
})
}
}
})
}
}

View File

@ -0,0 +1,108 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
/**
* @typedef {import('../utils').VueObjectData} VueObjectData
*/
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow accessing computed properties in `data`.',
categories: ['vue3-essential', 'essential'],
url: 'https://eslint.vuejs.org/rules/no-computed-properties-in-data.html'
},
fixable: null,
schema: [],
messages: {
cannotBeUsed:
'The computed property cannot be used in `data()` because it is before initialization.'
}
},
/** @param {RuleContext} context */
create(context) {
/** @type {Map<ObjectExpression, {data: FunctionExpression | ArrowFunctionExpression, computedNames:Set<string>}>} */
const contextMap = new Map()
/**
* @typedef {object} ScopeStack
* @property {ScopeStack | null} upper
* @property {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
*/
/** @type {ScopeStack | null} */
let scopeStack = null
return utils.compositingVisitors(
{
/**
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
*/
':function'(node) {
scopeStack = {
upper: scopeStack,
node
}
},
':function:exit'() {
scopeStack = scopeStack && scopeStack.upper
}
},
utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
const dataProperty = utils.findProperty(node, 'data')
if (
!dataProperty ||
(dataProperty.value.type !== 'FunctionExpression' &&
dataProperty.value.type !== 'ArrowFunctionExpression')
) {
return
}
const computedNames = new Set()
for (const computed of utils.iterateProperties(
node,
new Set(['computed'])
)) {
computedNames.add(computed.name)
}
contextMap.set(node, { data: dataProperty.value, computedNames })
},
/**
* @param {MemberExpression} node
* @param {VueObjectData} vueData
*/
MemberExpression(node, vueData) {
if (!scopeStack || !utils.isThis(node.object, context)) {
return
}
const ctx = contextMap.get(vueData.node)
if (!ctx || ctx.data !== scopeStack.node) {
return
}
const name = utils.getStaticPropertyName(node)
if (!name || !ctx.computedNames.has(name)) {
return
}
context.report({
node,
messageId: 'cannotBeUsed'
})
}
})
)
}
}

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')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Check whether the given `v-if` node is using the variable which is defined by the `v-for` directive.
* @param {VDirective} 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 Boolean(
vIf.value &&
vIf.value.references.some((reference) =>
element.variables.some(
(variable) =>
variable.id.name === reference.id.name && variable.kind === 'v-for'
)
)
)
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow confusing `v-for` and `v-if` on the same element',
categories: ['vue3-recommended', 'recommended'],
url: 'https://eslint.vuejs.org/rules/no-confusing-v-for-v-if.html'
},
deprecated: true,
replacedBy: ['no-use-v-if-with-v-for'],
fixable: null,
schema: []
},
/** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='if']"(node) {
const element = node.parent.parent
if (utils.hasDirective(element, 'for') && !isUsingIterationVar(node)) {
context.report({
node,
loc: node.loc,
message: "This 'v-if' should be moved to the wrapper element."
})
}
}
})
}
}

View File

@ -0,0 +1,29 @@
/**
* @author Flo Edelmann
*/
'use strict'
const { wrapCoreRule } = require('../utils')
const conditionalDirectiveNames = new Set(['v-show', 'v-if', 'v-else-if'])
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('no-constant-condition', {
create(_context, { coreHandlers }) {
return {
VDirectiveKey(node) {
if (
conditionalDirectiveNames.has(`v-${node.name.name}`) &&
node.parent.value &&
node.parent.value.expression &&
coreHandlers.IfStatement
) {
coreHandlers.IfStatement({
// @ts-expect-error -- Process expression of VExpressionContainer as IfStatement.
test: node.parent.value.expression
})
}
}
}
}
})

View File

@ -0,0 +1,59 @@
/**
* @author Przemyslaw Falowski (@przemkow)
* @fileoverview This rule checks whether v-model used on the component do not have custom modifiers
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
const VALID_MODIFIERS = new Set(['lazy', 'number', 'trim'])
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow custom modifiers on v-model used on the component',
categories: ['essential'],
url: 'https://eslint.vuejs.org/rules/no-custom-modifiers-on-v-model.html'
},
fixable: null,
schema: [],
messages: {
notSupportedModifier:
"'v-model' directives don't support the modifier '{{name}}'."
}
},
/** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='model']"(node) {
const element = node.parent.parent
if (utils.isCustomComponent(element)) {
for (const modifier of node.key.modifiers) {
if (!VALID_MODIFIERS.has(modifier.name)) {
context.report({
node,
loc: node.loc,
messageId: 'notSupportedModifier',
data: { name: modifier.name }
})
}
}
}
}
})
}
}

View File

@ -0,0 +1,94 @@
/**
* @fileoverview disallow using deprecated object declaration on data
* @author yoyo930021
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
/** @param {Token} token */
function isOpenParen(token) {
return token.type === 'Punctuator' && token.value === '('
}
/** @param {Token} token */
function isCloseParen(token) {
return token.type === 'Punctuator' && token.value === ')'
}
/**
* @param {Expression} node
* @param {SourceCode} sourceCode
*/
function getFirstAndLastTokens(node, sourceCode) {
let first = sourceCode.getFirstToken(node)
let last = sourceCode.getLastToken(node)
// If the value enclosed by parentheses, update the 'first' and 'last' by the parentheses.
while (true) {
const prev = sourceCode.getTokenBefore(first)
const next = sourceCode.getTokenAfter(last)
if (isOpenParen(prev) && isCloseParen(next)) {
first = prev
last = next
} else {
return { first, last }
}
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated object declaration on data (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-data-object-declaration.html'
},
fixable: 'code',
schema: [],
messages: {
objectDeclarationIsDeprecated:
"Object declaration on 'data' property is deprecated. Using function declaration instead."
}
},
/** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
return utils.executeOnVue(context, (obj) => {
const invalidData = utils.findProperty(
obj,
'data',
(p) =>
p.value.type !== 'FunctionExpression' &&
p.value.type !== 'ArrowFunctionExpression' &&
p.value.type !== 'Identifier'
)
if (invalidData) {
context.report({
node: invalidData,
messageId: 'objectDeclarationIsDeprecated',
fix(fixer) {
const tokens = getFirstAndLastTokens(invalidData.value, sourceCode)
return [
fixer.insertTextBefore(tokens.first, 'function() {\nreturn '),
fixer.insertTextAfter(tokens.last, ';\n}')
]
}
})
}
})
}
}

View File

@ -0,0 +1,86 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-destroyed-lifecycle.html'
},
fixable: 'code',
schema: [],
messages: {
deprecatedDestroyed:
'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
deprecatedBeforeDestroy:
'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.'
}
},
/** @param {RuleContext} context */
create(context) {
return utils.executeOnVue(context, (obj) => {
const destroyed = utils.findProperty(obj, 'destroyed')
if (destroyed) {
context.report({
node: destroyed.key,
messageId: 'deprecatedDestroyed',
fix(fixer) {
return fix(fixer, destroyed, 'unmounted')
}
})
}
const beforeDestroy = utils.findProperty(obj, 'beforeDestroy')
if (beforeDestroy) {
context.report({
node: beforeDestroy.key,
messageId: 'deprecatedBeforeDestroy',
fix(fixer) {
return fix(fixer, beforeDestroy, 'beforeUnmount')
}
})
}
/**
* @param {RuleFixer} fixer
* @param {Property} property
* @param {string} newName
*/
function fix(fixer, property, newName) {
if (property.computed) {
if (
property.key.type === 'Literal' ||
property.key.type === 'TemplateLiteral'
) {
return fixer.replaceTextRange(
[property.key.range[0] + 1, property.key.range[1] - 1],
newName
)
}
return null
}
if (property.shorthand) {
return fixer.insertTextBefore(property.key, `${newName}:`)
}
return fixer.replaceText(property.key, newName)
}
})
}
}

View File

@ -0,0 +1,71 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow using deprecated `$listeners` (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-dollar-listeners-api.html'
},
fixable: null,
schema: [],
messages: {
deprecated: 'The `$listeners` is deprecated.'
}
},
/** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(
context,
{
VExpressionContainer(node) {
for (const reference of node.references) {
if (reference.variable != null) {
// Not vm reference
continue
}
if (reference.id.name === '$listeners') {
context.report({
node: reference.id,
messageId: 'deprecated'
})
}
}
}
},
utils.defineVueVisitor(context, {
MemberExpression(node) {
if (
node.property.type !== 'Identifier' ||
node.property.name !== '$listeners'
) {
return
}
if (!utils.isThis(node.object, context)) {
return
}
context.report({
node: node.property,
messageId: 'deprecated'
})
}
})
)
}
}

View File

@ -0,0 +1,78 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-dollar-scopedslots-api.html'
},
fixable: 'code',
schema: [],
messages: {
deprecated: 'The `$scopedSlots` is deprecated.'
}
},
/** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(
context,
{
VExpressionContainer(node) {
for (const reference of node.references) {
if (reference.variable != null) {
// Not vm reference
continue
}
if (reference.id.name === '$scopedSlots') {
context.report({
node: reference.id,
messageId: 'deprecated',
fix(fixer) {
return fixer.replaceText(reference.id, '$slots')
}
})
}
}
}
},
utils.defineVueVisitor(context, {
MemberExpression(node) {
if (
node.property.type !== 'Identifier' ||
node.property.name !== '$scopedSlots'
) {
return
}
if (!utils.isThis(node.object, context)) {
return
}
context.report({
node: node.property,
messageId: 'deprecated',
fix(fixer) {
return fixer.replaceText(node.property, '$slots')
}
})
}
})
)
}
}

View File

@ -0,0 +1,69 @@
/**
* @fileoverview disallow using deprecated events api
* @author yoyo930021
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow using deprecated events api (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-events-api.html'
},
fixable: null,
schema: [],
messages: {
noDeprecatedEventsApi:
'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.'
}
},
/** @param {RuleContext} context */
create(context) {
return utils.defineVueVisitor(context, {
/** @param {MemberExpression & ({parent: CallExpression} | {parent: ChainExpression & {parent: CallExpression}})} node */
'CallExpression > MemberExpression, CallExpression > ChainExpression > MemberExpression'(
node
) {
const call =
node.parent.type === 'ChainExpression'
? node.parent.parent
: node.parent
if (call.optional) {
// It is OK because checking whether it is deprecated.
// e.g. `this.$on?.()`
return
}
if (
utils.skipChainExpression(call.callee) !== node ||
!['$on', '$off', '$once'].includes(
utils.getStaticPropertyName(node) || ''
)
) {
return
}
if (!utils.isThis(node.object, context)) {
return
}
context.report({
node: node.property,
messageId: 'noDeprecatedEventsApi'
})
}
})
}
}

View File

@ -0,0 +1,44 @@
/**
* @author Przemyslaw Falowski (@przemkow)
* @fileoverview disallow using deprecated filters syntax
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated filters syntax (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-filter.html'
},
fixable: null,
schema: [],
messages: {
noDeprecatedFilter: 'Filters are deprecated.'
}
},
/** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
VFilterSequenceExpression(node) {
context.report({
node,
loc: node.loc,
messageId: 'noDeprecatedFilter'
})
}
})
}
}

View File

@ -0,0 +1,55 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated the `functional` template (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-functional-template.html'
},
fixable: null,
schema: [],
messages: {
unexpected: 'The `functional` template are deprecated.'
}
},
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
create(context) {
return {
Program(program) {
const element = program.templateBody
if (element == null) {
return
}
const functional = utils.getAttribute(element, 'functional')
if (functional) {
context.report({
node: functional,
messageId: 'unexpected'
})
}
}
}
}
}

View File

@ -0,0 +1,72 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated the `is` attribute on HTML elements (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-html-element-is.html'
},
fixable: null,
schema: [],
messages: {
unexpected: 'The `is` attribute on HTML element are deprecated.'
}
},
/** @param {RuleContext} context */
create(context) {
/** @param {VElement} node */
function isValidElement(node) {
return (
!utils.isHtmlWellKnownElementName(node.rawName) &&
!utils.isSvgWellKnownElementName(node.rawName)
)
}
return utils.defineTemplateBodyVisitor(context, {
/** @param {VDirective} node */
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']"(
node
) {
if (isValidElement(node.parent.parent)) {
return
}
context.report({
node,
loc: node.loc,
messageId: 'unexpected'
})
},
/** @param {VAttribute} node */
"VAttribute[directive=false][key.name='is']"(node) {
if (isValidElement(node.parent.parent)) {
return
}
if (node.value && node.value.value.startsWith('vue:')) {
// Usage on native elements 3.1+
return
}
context.report({
node,
loc: node.loc,
messageId: 'unexpected'
})
}
})
}
}

View File

@ -0,0 +1,47 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated `inline-template` attribute (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-inline-template.html'
},
fixable: null,
schema: [],
messages: {
unexpected: '`inline-template` are deprecated.'
}
},
/** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
/** @param {VIdentifier} node */
"VAttribute[directive=false] > VIdentifier[rawName='inline-template']"(
node
) {
context.report({
node,
loc: node.loc,
messageId: 'unexpected'
})
}
})
}
}

View File

@ -0,0 +1,133 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow deprecated `this` access in props default function (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-props-default-this.html'
},
fixable: null,
schema: [],
messages: {
deprecated:
'Props default value factory functions no longer have access to `this`.'
}
},
/** @param {RuleContext} context */
create(context) {
/**
* @typedef {object} ScopeStack
* @property {ScopeStack | null} upper
* @property {FunctionExpression | FunctionDeclaration} node
* @property {boolean} propDefault
*/
/** @type {Set<FunctionExpression>} */
const propsDefault = new Set()
/** @type {ScopeStack | null} */
let scopeStack = null
/**
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
*/
function onFunctionEnter(node) {
if (node.type === 'ArrowFunctionExpression') {
return
}
if (scopeStack) {
scopeStack = {
upper: scopeStack,
node,
propDefault: false
}
} else if (node.type === 'FunctionExpression' && propsDefault.has(node)) {
scopeStack = {
upper: scopeStack,
node,
propDefault: true
}
}
}
/**
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
*/
function onFunctionExit(node) {
if (scopeStack && scopeStack.node === node) {
scopeStack = scopeStack.upper
}
}
/**
* @param {Expression|SpreadElement|null} node
*/
function isFunctionIdentifier(node) {
return node && node.type === 'Identifier' && node.name === 'Function'
}
/**
* @param {Expression} node
* @returns {boolean}
*/
function hasFunctionType(node) {
if (isFunctionIdentifier(node)) {
return true
}
if (node.type === 'ArrayExpression') {
return node.elements.some(isFunctionIdentifier)
}
return false
}
return utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
for (const prop of utils.getComponentPropsFromOptions(node)) {
if (prop.type !== 'object') {
continue
}
if (prop.value.type !== 'ObjectExpression') {
continue
}
const def = utils.findProperty(prop.value, 'default')
if (!def) {
continue
}
const type = utils.findProperty(prop.value, 'type')
if (type && hasFunctionType(type.value)) {
// ignore function type
continue
}
if (def.value.type !== 'FunctionExpression') {
continue
}
propsDefault.add(def.value)
}
},
':function': onFunctionEnter,
':function:exit': onFunctionExit,
ThisExpression(node) {
if (scopeStack && scopeStack.propDefault) {
context.report({
node,
messageId: 'deprecated'
})
}
}
})
}
}

View File

@ -0,0 +1,103 @@
/**
* @author Marton Csordas
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const casing = require('../utils/casing')
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
/** @param {RuleContext} context */
function getComponentNames(context) {
let components = ['RouterLink']
if (context.options[0] && context.options[0].components) {
components = context.options[0].components
}
return components.reduce((prev, curr) => {
prev.add(casing.kebabCase(curr))
prev.add(casing.pascalCase(curr))
return prev
}, new Set())
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated `tag` property on `RouterLink` (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-router-link-tag-prop.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
components: {
type: 'array',
items: {
type: 'string'
},
uniqueItems: true,
minItems: 1
}
}
}
],
messages: {
deprecated:
"'tag' property on '{{element}}' component is deprecated. Use scoped slots instead."
}
},
/** @param {RuleContext} context */
create(context) {
const components = getComponentNames(context)
return utils.defineTemplateBodyVisitor(context, {
VElement(node) {
if (!components.has(node.rawName)) return
/** @type VIdentifier | null */
let tagKey = null
const tagAttr = utils.getAttribute(node, 'tag')
if (tagAttr) {
tagKey = tagAttr.key
} else {
const directive = utils.getDirective(node, 'bind', 'tag')
if (directive) {
const arg = directive.key.argument
if (arg && arg.type === 'VIdentifier') {
tagKey = arg
}
}
}
if (tagKey) {
context.report({
node: tagKey,
messageId: 'deprecated',
data: {
element: node.rawName
}
})
}
}
})
}
}

View File

@ -0,0 +1,30 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
const scopeAttribute = require('./syntaxes/scope-attribute')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow deprecated `scope` attribute (in Vue.js 2.5.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-scope-attribute.html'
},
fixable: 'code',
schema: [],
messages: {
forbiddenScopeAttribute: '`scope` attributes are deprecated.'
}
},
/** @param {RuleContext} context */
create(context) {
const templateBodyVisitor =
scopeAttribute.createTemplateBodyVisitor(context)
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
}
}

View File

@ -0,0 +1,29 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
const slotAttribute = require('./syntaxes/slot-attribute')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow deprecated `slot` attribute (in Vue.js 2.6.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-slot-attribute.html'
},
fixable: 'code',
schema: [],
messages: {
forbiddenSlotAttribute: '`slot` attributes are deprecated.'
}
},
/** @param {RuleContext} context */
create(context) {
const templateBodyVisitor = slotAttribute.createTemplateBodyVisitor(context)
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
}
}

View File

@ -0,0 +1,33 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
const slotScopeAttribute = require('./syntaxes/slot-scope-attribute')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-slot-scope-attribute.html'
},
fixable: 'code',
schema: [],
messages: {
forbiddenSlotScopeAttribute: '`slot-scope` are deprecated.'
}
},
/** @param {RuleContext} context */
create(context) {
const templateBodyVisitor = slotScopeAttribute.createTemplateBodyVisitor(
context,
{ fixToUpgrade: true }
)
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
}
}

View File

@ -0,0 +1,62 @@
/**
* @author Przemyslaw Falowski (@przemkow)
* @fileoverview Disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+)
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-v-bind-sync.html'
},
fixable: 'code',
schema: [],
messages: {
syncModifierIsDeprecated:
"'.sync' modifier on 'v-bind' directive is deprecated. Use 'v-model:propName' instead."
}
},
/** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='bind']"(node) {
if (node.key.modifiers.map((mod) => mod.name).includes('sync')) {
context.report({
node,
loc: node.loc,
messageId: 'syncModifierIsDeprecated',
fix(fixer) {
if (node.key.argument == null) {
// is using spread syntax
return null
}
if (node.key.modifiers.length > 1) {
// has multiple modifiers
return null
}
const bindArgument = context
.getSourceCode()
.getText(node.key.argument)
return fixer.replaceText(node.key, `v-model:${bindArgument}`)
}
})
}
}
})
}
}

View File

@ -0,0 +1,29 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
const vIs = require('./syntaxes/v-is')
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow deprecated `v-is` directive (in Vue.js 3.1.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-v-is.html'
},
fixable: 'code',
schema: [],
messages: {
forbiddenVIs: '`v-is` directive is deprecated.'
}
},
/** @param {RuleContext} context */
create(context) {
const templateBodyVisitor = vIs.createTemplateBodyVisitor(context)
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
}
}

View File

@ -0,0 +1,49 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated `.native` modifiers (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-v-on-native-modifier.html'
},
fixable: null,
schema: [],
messages: {
deprecated: "'.native' modifier on 'v-on' directive is deprecated."
}
},
/** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
/** @param {VIdentifier & {parent:VDirectiveKey} } node */
"VAttribute[directive=true][key.name.name='on'] > VDirectiveKey > VIdentifier[name='native']"(
node
) {
const key = node.parent
if (!key.modifiers.includes(node)) return
context.report({
node,
messageId: 'deprecated'
})
}
})
}
}

View File

@ -0,0 +1,60 @@
/**
* @fileoverview disallow using deprecated number (keycode) modifiers
* @author yoyo930021
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const keyCodeToKey = require('../utils/keycode-to-key')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-v-on-number-modifiers.html'
},
fixable: 'code',
schema: [],
messages: {
numberModifierIsDeprecated:
"'KeyboardEvent.keyCode' modifier on 'v-on' directive is deprecated. Using 'KeyboardEvent.key' instead."
}
},
/** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
/** @param {VDirectiveKey} node */
"VAttribute[directive=true][key.name.name='on'] > VDirectiveKey"(node) {
const modifier = node.modifiers.find((mod) =>
Number.isInteger(parseInt(mod.name, 10))
)
if (!modifier) return
const keyCodes = parseInt(modifier.name, 10)
if (keyCodes > 9 || keyCodes < 0) {
context.report({
node: modifier,
messageId: 'numberModifierIsDeprecated',
fix(fixer) {
const key = keyCodeToKey[keyCodes]
if (!key) return null
return fixer.replaceText(modifier, `${key}`)
}
})
}
}
})
}
}

View File

@ -0,0 +1,52 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+)',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-deprecated-vue-config-keycodes.html'
},
fixable: null,
schema: [],
messages: {
unexpected: '`Vue.config.keyCodes` are deprecated.'
}
},
/** @param {RuleContext} context */
create(context) {
return {
/** @param {MemberExpression} node */
"MemberExpression[property.type='Identifier'][property.name='keyCodes']"(
node
) {
const config = utils.skipChainExpression(node.object)
if (
config.type !== 'MemberExpression' ||
config.property.type !== 'Identifier' ||
config.property.name !== 'config' ||
config.object.type !== 'Identifier' ||
config.object.name !== 'Vue'
) {
return
}
context.report({
node,
messageId: 'unexpected'
})
}
}
}
}

View File

@ -0,0 +1,68 @@
/**
* @fileoverview Prevents duplication of field names.
* @author Armano
*/
'use strict'
const utils = require('../utils')
/**
* @typedef {import('../utils').GroupName} GroupName
*/
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
/** @type {GroupName[]} */
const GROUP_NAMES = ['props', 'computed', 'data', 'methods', 'setup']
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow duplication of field names',
categories: ['vue3-essential', 'essential'],
url: 'https://eslint.vuejs.org/rules/no-dupe-keys.html'
},
fixable: null, // or "code" or "whitespace"
schema: [
{
type: 'object',
properties: {
groups: {
type: 'array'
}
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
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)
}
})
}
}

View File

@ -0,0 +1,193 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* @typedef {NonNullable<VExpressionContainer['expression']>} VExpression
*/
/**
* @typedef {object} OrOperands
* @property {VExpression} OrOperands.node
* @property {AndOperands[]} OrOperands.operands
*
* @typedef {object} AndOperands
* @property {VExpression} AndOperands.node
* @property {VExpression[]} AndOperands.operands
*/
/**
* Splits the given node by the given logical operator.
* @param {string} operator Logical operator `||` or `&&`.
* @param {VExpression} node The node to split.
* @returns {VExpression[]} Array of conditions that makes the node when joined by the operator.
*/
function splitByLogicalOperator(operator, node) {
if (node.type === 'LogicalExpression' && node.operator === operator) {
return [
...splitByLogicalOperator(operator, node.left),
...splitByLogicalOperator(operator, node.right)
]
}
return [node]
}
/**
* @param {VExpression} node
*/
function splitByOr(node) {
return splitByLogicalOperator('||', node)
}
/**
* @param {VExpression} node
*/
function splitByAnd(node) {
return splitByLogicalOperator('&&', node)
}
/**
* @param {VExpression} node
* @returns {OrOperands}
*/
function buildOrOperands(node) {
const orOperands = splitByOr(node)
return {
node,
operands: orOperands.map((orOperand) => {
const andOperands = splitByAnd(orOperand)
return {
node: orOperand,
operands: andOperands
}
})
}
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow duplicate conditions in `v-if` / `v-else-if` chains',
categories: ['vue3-essential', 'essential'],
url: 'https://eslint.vuejs.org/rules/no-dupe-v-else-if.html'
},
fixable: null,
schema: [],
messages: {
unexpected:
'This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain.'
}
},
/** @param {RuleContext} context */
create(context) {
const tokenStore =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
/**
* Determines whether the two given nodes are considered to be equal. In particular, given that the nodes
* represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators.
* @param {VExpression} a First node.
* @param {VExpression} b Second node.
* @returns {boolean} `true` if the nodes are considered to be equal.
*/
function equal(a, b) {
if (a.type !== b.type) {
return false
}
if (
a.type === 'LogicalExpression' &&
b.type === 'LogicalExpression' &&
(a.operator === '||' || a.operator === '&&') &&
a.operator === b.operator
) {
return (
(equal(a.left, b.left) && equal(a.right, b.right)) ||
(equal(a.left, b.right) && equal(a.right, b.left))
)
}
return utils.equalTokens(a, b, tokenStore)
}
/**
* Determines whether the first given AndOperands is a subset of the second given AndOperands.
*
* e.g. A: (a && b), B: (a && b && c): B is a subset of A.
*
* @param {AndOperands} operandsA The AndOperands to compare from.
* @param {AndOperands} operandsB The AndOperands to compare against.
* @returns {boolean} `true` if the `andOperandsA` is a subset of the `andOperandsB`.
*/
function isSubset(operandsA, operandsB) {
return operandsA.operands.every((operandA) =>
operandsB.operands.some((operandB) => equal(operandA, operandB))
)
}
return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='else-if']"(node) {
if (!node.value || !node.value.expression) {
return
}
const test = node.value.expression
const conditionsToCheck =
test.type === 'LogicalExpression' && test.operator === '&&'
? [...splitByAnd(test), test]
: [test]
const listToCheck = conditionsToCheck.map(buildOrOperands)
/** @type {VElement | null} */
let current = node.parent.parent
while (current && (current = utils.prevSibling(current))) {
const vIf = utils.getDirective(current, 'if')
const currentTestDir = vIf || utils.getDirective(current, 'else-if')
if (!currentTestDir) {
return
}
if (currentTestDir.value && currentTestDir.value.expression) {
const currentOrOperands = buildOrOperands(
currentTestDir.value.expression
)
for (const condition of listToCheck) {
const operands = (condition.operands = condition.operands.filter(
(orOperand) => {
return !currentOrOperands.operands.some((currentOrOperand) =>
isSubset(currentOrOperand, orOperand)
)
}
))
if (!operands.length) {
context.report({
node: condition.node,
messageId: 'unexpected'
})
return
}
}
}
if (vIf) {
return
}
}
}
})
}
}

View File

@ -0,0 +1,67 @@
/**
* @fileoverview Disable inheritAttrs when using v-bind="$attrs"
* @author Hiroki Osame
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"`',
categories: undefined,
recommended: false,
url: 'https://eslint.vuejs.org/rules/no-duplicate-attr-inheritance.html'
},
fixable: null,
schema: [
// fill in your schema
]
},
/** @param {RuleContext} context */
create(context) {
/** @type {string | number | boolean | RegExp | BigInt | null} */
let inheritsAttrs = true
return Object.assign(
utils.executeOnVue(context, (node) => {
const inheritAttrsProp = utils.findProperty(node, 'inheritAttrs')
if (inheritAttrsProp && inheritAttrsProp.value.type === 'Literal') {
inheritsAttrs = inheritAttrsProp.value.value
}
}),
utils.defineTemplateBodyVisitor(context, {
/** @param {VExpressionContainer} node */
"VAttribute[directive=true][key.name.name='bind'][key.argument=null] > VExpressionContainer"(
node
) {
if (!inheritsAttrs) {
return
}
const attrsRef = node.references.find((reference) => {
if (reference.variable != null) {
// Not vm reference
return false
}
return reference.id.name === '$attrs'
})
if (attrsRef) {
context.report({
node: attrsRef.id,
message: 'Set "inheritAttrs" to false.'
})
}
}
})
)
}
}

View File

@ -0,0 +1,120 @@
/**
* @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 {VAttribute | VDirective} attribute The attribute node to get.
* @returns {string | null} The name of the attribute.
*/
function getName(attribute) {
if (!attribute.directive) {
return attribute.key.name
}
if (attribute.key.name.name === 'bind') {
return (
(attribute.key.argument &&
attribute.key.argument.type === 'VIdentifier' &&
attribute.key.argument.name) ||
null
)
}
return null
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow duplication of attributes',
categories: ['vue3-essential', 'essential'],
url: 'https://eslint.vuejs.org/rules/no-duplicate-attributes.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
allowCoexistClass: {
type: 'boolean'
},
allowCoexistStyle: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},
/** @param {RuleContext} context */
create(context) {
const options = context.options[0] || {}
const allowCoexistStyle = options.allowCoexistStyle !== false
const allowCoexistClass = options.allowCoexistClass !== false
/** @type {Set<string>} */
const directiveNames = new Set()
/** @type {Set<string>} */
const attributeNames = new Set()
/**
* @param {string} name
* @param {boolean} isDirective
*/
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)
}
}
})
}
}

View File

@ -0,0 +1,109 @@
/**
* @author tyankatsu <https://github.com/tyankatsu0105>
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const { isVElement } = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
/**
* check whether has attribute `src`
* @param {VElement} componentBlock
*/
function hasAttributeSrc(componentBlock) {
const hasAttribute = componentBlock.startTag.attributes.length > 0
const hasSrc =
componentBlock.startTag.attributes.filter(
(attribute) =>
!attribute.directive &&
attribute.key.name === 'src' &&
attribute.value &&
attribute.value.value !== ''
).length > 0
return hasAttribute && hasSrc
}
/**
* check whether value under the component block is only whitespaces or break lines
* @param {VElement} componentBlock
*/
function isValueOnlyWhiteSpacesOrLineBreaks(componentBlock) {
return (
componentBlock.children.length === 1 &&
componentBlock.children[0].type === 'VText' &&
!componentBlock.children[0].value.trim()
)
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'disallow the `<template>` `<script>` `<style>` block to be empty',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-empty-component-block.html'
},
fixable: null,
schema: [],
messages: {
unexpected: '`<{{ blockName }}>` is empty. Empty block is not allowed.'
}
},
/**
* @param {RuleContext} context - The rule context.
* @returns {RuleListener} AST event handlers.
*/
create(context) {
if (!context.parserServices.getDocumentFragment) {
return {}
}
const documentFragment = context.parserServices.getDocumentFragment()
if (!documentFragment) {
return {}
}
const componentBlocks = documentFragment.children.filter(isVElement)
return {
Program() {
for (const componentBlock of componentBlocks) {
if (
componentBlock.name !== 'template' &&
componentBlock.name !== 'script' &&
componentBlock.name !== 'style'
)
continue
// https://vue-loader.vuejs.org/spec.html#src-imports
if (hasAttributeSrc(componentBlock)) continue
if (
isValueOnlyWhiteSpacesOrLineBreaks(componentBlock) ||
componentBlock.children.length === 0
) {
context.report({
node: componentBlock,
loc: componentBlock.loc,
messageId: 'unexpected',
data: {
blockName: componentBlock.name
}
})
}
}
}
}
}
}

View File

@ -0,0 +1,9 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { wrapCoreRule } = require('../utils')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('no-empty-pattern')

View File

@ -0,0 +1,60 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const utils = require('../utils')
/**
* @typedef {import('@typescript-eslint/types').TSESTree.ExportAllDeclaration} TSESTreeExportAllDeclaration
* @typedef {import('@typescript-eslint/types').TSESTree.ExportDefaultDeclaration} TSESTreeExportDefaultDeclaration
* @typedef {import('@typescript-eslint/types').TSESTree.ExportNamedDeclaration} TSESTreeExportNamedDeclaration
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow `export` in `<script setup>`',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-export-in-script-setup.html'
},
fixable: null,
schema: [],
messages: {
forbidden: '`<script setup>` cannot contain ES module exports.'
}
},
/** @param {RuleContext} context */
create(context) {
/** @param {ExportAllDeclaration | ExportDefaultDeclaration | ExportNamedDeclaration} node */
function verify(node) {
const tsNode =
/** @type {TSESTreeExportAllDeclaration | TSESTreeExportDefaultDeclaration | TSESTreeExportNamedDeclaration} */ (
node
)
if (tsNode.exportKind === 'type') {
return
}
if (tsNode.type === 'ExportNamedDeclaration') {
if (
tsNode.specifiers.length > 0 &&
tsNode.specifiers.every((spec) => spec.exportKind === 'type')
) {
return
}
}
context.report({
node,
messageId: 'forbidden'
})
}
return utils.defineScriptSetupVisitor(context, {
ExportAllDeclaration: verify,
ExportDefaultDeclaration: verify,
ExportNamedDeclaration: verify
})
}
}

View File

@ -0,0 +1,202 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const { findVariable } = require('eslint-utils')
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Get the callee member node from the given CallExpression
* @param {CallExpression} node CallExpression
*/
function getCalleeMemberNode(node) {
const callee = utils.skipChainExpression(node.callee)
if (callee.type === 'MemberExpression') {
const name = utils.getStaticPropertyName(callee)
if (name) {
return { name, member: callee }
}
}
return null
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow asynchronously registered `expose`',
categories: undefined,
// categories: ['vue3-essential'], TODO Change with the major version
url: 'https://eslint.vuejs.org/rules/no-expose-after-await.html'
},
fixable: null,
schema: [],
messages: {
forbidden: 'The `expose` after `await` expression are forbidden.'
}
},
/** @param {RuleContext} context */
create(context) {
/**
* @typedef {object} SetupScopeData
* @property {boolean} afterAwait
* @property {[number,number]} range
* @property {Set<Identifier>} exposeReferenceIds
* @property {Set<Identifier>} contextReferenceIds
*/
/**
* @typedef {object} ScopeStack
* @property {ScopeStack | null} upper
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} scopeNode
*/
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, SetupScopeData>} */
const setupScopes = new Map()
/** @type {ScopeStack | null} */
let scopeStack = null
return utils.defineVueVisitor(context, {
onSetupFunctionEnter(node) {
const contextParam = node.params[1]
if (!contextParam) {
// no arguments
return
}
if (contextParam.type === 'RestElement') {
// cannot check
return
}
if (contextParam.type === 'ArrayPattern') {
// cannot check
return
}
/** @type {Set<Identifier>} */
const contextReferenceIds = new Set()
/** @type {Set<Identifier>} */
const exposeReferenceIds = new Set()
if (contextParam.type === 'ObjectPattern') {
const exposeProperty = utils.findAssignmentProperty(
contextParam,
'expose'
)
if (!exposeProperty) {
return
}
const exposeParam = exposeProperty.value
// `setup(props, {emit})`
const variable =
exposeParam.type === 'Identifier'
? findVariable(context.getScope(), exposeParam)
: null
if (!variable) {
return
}
for (const reference of variable.references) {
if (!reference.isRead()) {
continue
}
exposeReferenceIds.add(reference.identifier)
}
} else if (contextParam.type === 'Identifier') {
// `setup(props, context)`
const variable = findVariable(context.getScope(), contextParam)
if (!variable) {
return
}
for (const reference of variable.references) {
if (!reference.isRead()) {
continue
}
contextReferenceIds.add(reference.identifier)
}
}
setupScopes.set(node, {
afterAwait: false,
range: node.range,
exposeReferenceIds,
contextReferenceIds
})
},
/**
* @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
*/
':function'(node) {
scopeStack = {
upper: scopeStack,
scopeNode: node
}
},
':function:exit'() {
scopeStack = scopeStack && scopeStack.upper
},
/** @param {AwaitExpression} node */
AwaitExpression(node) {
if (!scopeStack) {
return
}
const setupScope = setupScopes.get(scopeStack.scopeNode)
if (!setupScope || !utils.inRange(setupScope.range, node)) {
return
}
setupScope.afterAwait = true
},
/** @param {CallExpression} node */
CallExpression(node) {
if (!scopeStack) {
return
}
const setupScope = setupScopes.get(scopeStack.scopeNode)
if (
!setupScope ||
!setupScope.afterAwait ||
!utils.inRange(setupScope.range, node)
) {
return
}
const { contextReferenceIds, exposeReferenceIds } = setupScope
if (
node.callee.type === 'Identifier' &&
exposeReferenceIds.has(node.callee)
) {
// setup(props,{expose}) {expose()}
context.report({
node,
messageId: 'forbidden'
})
} else {
const expose = getCalleeMemberNode(node)
if (
expose &&
expose.name === 'expose' &&
expose.member.object.type === 'Identifier' &&
contextReferenceIds.has(expose.member.object)
) {
// setup(props,context) {context.emit()}
context.report({
node,
messageId: 'forbidden'
})
}
}
},
onSetupFunctionExit(node) {
setupScopes.delete(node)
}
})
}
}

View File

@ -0,0 +1,204 @@
/**
* @author Yosuke Ota
*/
'use strict'
const { isParenthesized } = require('eslint-utils')
const { wrapCoreRule } = require('../utils')
const { getStyleVariablesContext } = require('../utils/style-variables')
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule('no-extra-parens', {
skipDynamicArguments: true,
applyDocument: true,
create: createForVueSyntax
})
/**
* Check whether the given token is a left parenthesis.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a left parenthesis.
*/
function isLeftParen(token) {
return token.type === 'Punctuator' && token.value === '('
}
/**
* Check whether the given token is a right parenthesis.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a right parenthesis.
*/
function isRightParen(token) {
return token.type === 'Punctuator' && token.value === ')'
}
/**
* Check whether the given token is a left brace.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a left brace.
*/
function isLeftBrace(token) {
return token.type === 'Punctuator' && token.value === '{'
}
/**
* Check whether the given token is a right brace.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a right brace.
*/
function isRightBrace(token) {
return token.type === 'Punctuator' && token.value === '}'
}
/**
* Check whether the given token is a left bracket.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a left bracket.
*/
function isLeftBracket(token) {
return token.type === 'Punctuator' && token.value === '['
}
/**
* Check whether the given token is a right bracket.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a right bracket.
*/
function isRightBracket(token) {
return token.type === 'Punctuator' && token.value === ']'
}
/**
* Determines if a given expression node is an IIFE
* @param {Expression} node The node to check
* @returns {node is CallExpression & { callee: FunctionExpression } } `true` if the given node is an IIFE
*/
function isIIFE(node) {
return (
node.type === 'CallExpression' && node.callee.type === 'FunctionExpression'
)
}
/**
* @param {RuleContext} context - The rule context.
* @returns {TemplateListener} AST event handlers.
*/
function createForVueSyntax(context) {
if (!context.parserServices.getTemplateBodyTokenStore) {
return {}
}
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
/**
* Checks if the given node turns into a filter when unwraped.
* @param {Expression} expression node to evaluate
* @returns {boolean} `true` if the given node turns into a filter when unwraped.
*/
function isUnwrapChangeToFilter(expression) {
let parenStack = null
for (const token of tokenStore.getTokens(expression)) {
if (!parenStack) {
if (token.value === '|') {
return true
}
} else {
if (parenStack.isUpToken(token)) {
parenStack = parenStack.upper
continue
}
}
if (isLeftParen(token)) {
parenStack = { isUpToken: isRightParen, upper: parenStack }
} else if (isLeftBracket(token)) {
parenStack = { isUpToken: isRightBracket, upper: parenStack }
} else if (isLeftBrace(token)) {
parenStack = { isUpToken: isRightBrace, upper: parenStack }
}
}
return false
}
/**
* Checks if the given node is CSS v-bind() without quote.
* @param {VExpressionContainer} node
* @param {Expression} expression
*/
function isStyleVariableWithoutQuote(node, expression) {
const styleVars = getStyleVariablesContext(context)
if (!styleVars || !styleVars.vBinds.includes(node)) {
return false
}
const vBindToken = tokenStore.getFirstToken(node)
const tokens = tokenStore.getTokensBetween(vBindToken, expression)
return tokens.every(isLeftParen)
}
/**
* @param {VExpressionContainer & { expression: Expression | VFilterSequenceExpression | null }} node
*/
function verify(node) {
if (!node.expression) {
return
}
const expression =
node.expression.type === 'VFilterSequenceExpression'
? node.expression.expression
: node.expression
if (!isParenthesized(expression, tokenStore)) {
return
}
if (!isParenthesized(2, expression, tokenStore)) {
if (
isIIFE(expression) &&
!isParenthesized(expression.callee, tokenStore)
) {
return
}
if (isUnwrapChangeToFilter(expression)) {
return
}
if (isStyleVariableWithoutQuote(node, expression)) {
return
}
}
report(expression)
}
/**
* Report the node
* @param {Expression} node node to evaluate
* @returns {void}
* @private
*/
function report(node) {
const sourceCode = context.getSourceCode()
const leftParenToken = tokenStore.getTokenBefore(node)
const rightParenToken = tokenStore.getTokenAfter(node)
context.report({
node,
loc: leftParenToken.loc,
messageId: 'unexpected',
fix(fixer) {
const parenthesizedSource = sourceCode.text.slice(
leftParenToken.range[1],
rightParenToken.range[0]
)
return fixer.replaceTextRange(
[leftParenToken.range[0], rightParenToken.range[1]],
parenthesizedSource
)
}
})
}
return {
"VAttribute[directive=true][key.name.name='bind'] > VExpressionContainer":
verify,
'VElement > VExpressionContainer': verify
}
}

View File

@ -0,0 +1,57 @@
/**
* @fileoverview Requires valid keys in model option.
* @author Alex Sokolov
*/
'use strict'
const utils = require('../utils')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const VALID_MODEL_KEYS = ['prop', 'event']
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'require valid keys in model option',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-invalid-model-keys.html'
},
fixable: null,
schema: []
},
/** @param {RuleContext} context */
create(context) {
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return utils.executeOnVue(context, (obj) => {
const modelProperty = utils.findProperty(obj, 'model')
if (!modelProperty || modelProperty.value.type !== 'ObjectExpression') {
return
}
for (const p of modelProperty.value.properties) {
if (p.type !== 'Property') {
continue
}
const name = utils.getStaticPropertyName(p)
if (!name) {
continue
}
if (VALID_MODEL_KEYS.indexOf(name) === -1) {
context.report({
node: p,
message: "Invalid key '{{name}}' in model option.",
data: {
name
}
})
}
}
})
}
}

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