├── .github
├── FUNDING.yml
└── workflows
│ └── node.js.yml
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
└── pre-commit
├── LICENSE.md
├── README.md
├── babel.config.js
├── docs
└── rules
│ ├── grid-unknown-attributes.md
│ ├── icon-button-variant.md
│ ├── no-deprecated-classes.md
│ ├── no-deprecated-colors.md
│ ├── no-deprecated-components.md
│ ├── no-deprecated-events.md
│ ├── no-deprecated-props.md
│ └── no-deprecated-slots.md
├── eslint.config.js
├── package.json
├── pnpm-lock.yaml
├── renovate.json
├── scripts
├── lint-commit-message.js
└── warn-npm-install.js
├── src
├── configs
│ ├── base.js
│ ├── flat
│ │ ├── base.js
│ │ └── recommended.js
│ └── recommended.js
├── index.js
├── rules
│ ├── grid-unknown-attributes.js
│ ├── icon-button-variant.js
│ ├── no-deprecated-classes.js
│ ├── no-deprecated-colors.js
│ ├── no-deprecated-components.js
│ ├── no-deprecated-events.js
│ ├── no-deprecated-imports.js
│ ├── no-deprecated-props.js
│ └── no-deprecated-slots.js
└── util
│ ├── fixers.js
│ ├── get-installed-vuetify-version.js
│ ├── grid-attributes.js
│ └── helpers.js
└── tests
├── rules
├── grid-unknown-attributes.js
├── icon-button-variant.js
├── no-deprecated-classes.js
├── no-deprecated-colors.js
├── no-deprecated-components.js
├── no-deprecated-events.js
├── no-deprecated-imports.js
├── no-deprecated-props.js
└── no-deprecated-slots.js
└── setup.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: KaelWD
2 | patreon: kaelwd
3 | open_collective: vuetify
4 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | tags: [v*]
7 | pull_request:
8 | branches: [master]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node-version: [18.x, 20.x]
16 | steps:
17 | - uses: actions/checkout@v2
18 | - uses: pnpm/action-setup@v2
19 | with:
20 | version: 7
21 | - name: Use Node.js ${{ matrix.node-version }}
22 | uses: actions/setup-node@v1
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 | - run: pnpm i
26 | - run: pnpm lint
27 | - run: pnpm test:ci
28 |
29 | publish:
30 | needs: build
31 | runs-on: ubuntu-latest
32 | if: github.event_name == 'push' && startswith(github.ref, 'refs/tags/v') && github.repository_owner == 'vuetifyjs'
33 | steps:
34 | - uses: actions/checkout@v2
35 | with:
36 | fetch-depth: 0
37 | - uses: pnpm/action-setup@v2
38 | with:
39 | version: 7
40 | - run: pnpm i
41 | - run: npm config set //registry.npmjs.org/:_authToken ${NPM_API_KEY:?}
42 | env:
43 | NPM_API_KEY: ${{ secrets.NPM_TOKEN }}
44 | - run: npm publish
45 | - name: GitHub release
46 | run: pnpm conventional-github-releaser -p vuetify
47 | env:
48 | CONVENTIONAL_GITHUB_RELEASER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | .env
4 | .nyc_output/
5 | coverage/
6 |
7 | # IDE user config
8 | .vscode/
9 | .idea/
10 | .vs/
11 |
12 | # Logs
13 | yarn-error.log
14 | npm-debug.log
15 |
16 | # Build output
17 | lib/
18 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | node scripts/lint-commit-message.js $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | node scripts/warn-npm-install.js && yarn run lint && yarn run test
5 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # Released under MIT License
2 |
3 | Copyright (c) 2016-present Vuetify LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # eslint-plugin-vuetify
2 |
3 | This package is for migrating from Vuetify v2 to v3, use [eslint-plugin-vuetify@vuetify-2](https://www.npmjs.com/package/eslint-plugin-vuetify/v/vuetify-2) for v1 to v2.
4 |
5 |
6 |
7 |
Support the maintainer of this plugin:
8 | Kael Watts-Deuchar
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ## 💿 Install
17 |
18 | You should have [`eslint`](https://eslint.org/docs/latest/use/getting-started) and [`eslint-plugin-vue`](https://eslint.vuejs.org/user-guide/#installation) set up first.
19 |
20 | ```bash
21 | yarn add eslint-plugin-vuetify -D
22 | # OR
23 | npm install eslint-plugin-vuetify --save-dev
24 | ```
25 |
26 | ```js
27 | // eslint.config.js
28 | import vue from 'eslint-plugin-vue'
29 | import vuetify from 'eslint-plugin-vuetify'
30 |
31 | export default [
32 | ...vue.configs['flat/base'],
33 | ...vuetify.configs['flat/base'],
34 | ]
35 | ```
36 |
37 | Eslint 8 can alternatively use the older configuration format:
38 |
39 | ```js
40 | // .eslintrc.js
41 | module.exports = {
42 | extends: [
43 | 'plugin:vue/base',
44 | 'plugin:vuetify/base'
45 | ]
46 | }
47 | ```
48 |
49 | **NOTE** This plugin does not affect _**pug**_ templates due to [a limitation in vue-eslint-parser](https://github.com/mysticatea/vue-eslint-parser/issues/29). I suggest converting your pug templates to HTML with [pug-to-html](https://github.com/leo-buneev/pug-to-html) in order to use this plugin.
50 |
51 |
52 | ## Rules
53 |
54 | ### Deprecations
55 |
56 | These rules will help you avoid deprecated components, props, and classes. They are included in the `base` preset.
57 |
58 | - Prevent the use of components that have been removed from Vuetify ([`no-deprecated-components`])
59 | - Prevent the use of props that have been removed from Vuetify ([`no-deprecated-props`])
60 | - Prevent the use of events that have been removed from Vuetify ([`no-deprecated-events`])
61 | - Prevent the use of classes that have been removed from Vuetify ([`no-deprecated-classes`])
62 | - Prevent the use of the old theme class syntax ([`no-deprecated-colors`])
63 | - Prevent the use of deprecated import paths ([`no-deprecated-imports`])
64 | - Ensure icon buttons have a variant defined ([`icon-button-variant`])
65 |
66 | ### Grid system
67 |
68 | These rules are designed to help migrate to the new grid system in Vuetify v2. They are included in the `recommended` preset.
69 |
70 | - Warn about unknown attributes not being converted to classes on new grid components ([`grid-unknown-attributes`])
71 |
72 |
73 | [`grid-unknown-attributes`]: ./docs/rules/grid-unknown-attributes.md
74 | [`no-deprecated-components`]: ./docs/rules/no-deprecated-components.md
75 | [`no-deprecated-props`]: ./docs/rules/no-deprecated-props.md
76 | [`no-deprecated-events`]: ./docs/rules/no-deprecated-events.md
77 | [`no-deprecated-classes`]: ./docs/rules/no-deprecated-classes.md
78 | [`no-deprecated-colors`]: ./docs/rules/no-deprecated-colors.md
79 | [`no-deprecated-imports`]: ./docs/rules/no-deprecated-imports.md
80 | [`icon-button-variant`]: ./docs/rules/icon-button-variant.md
81 |
82 |
83 | ## 💪 Supporting Vuetify
84 | Vuetify is an open source MIT project that has been made possible due to the generous contributions by community backers. If you are interested in supporting this project, please consider:
85 |
86 |
105 |
106 | ### 📑 License
107 | [MIT](http://opensource.org/licenses/MIT)
108 |
109 | Copyright (c) 2016-present Vuetify LLC
110 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', { targets: { node: 'current' } }],
4 | ],
5 | }
6 |
--------------------------------------------------------------------------------
/docs/rules/grid-unknown-attributes.md:
--------------------------------------------------------------------------------
1 | # warn about unknown attributes not being converted to classes on new grid components (grid-unknown-attributes)
2 |
3 | :wrench: This rule is fixable with `eslint --fix`
4 |
5 | The new grid system in Vuetify v2 no longer converts unrecognised attributes into classes.
6 |
7 | ## Rule Details
8 |
9 | This rule prevents the use of non-prop attributes on `v-container`, `v-row`, and `v-col`. It will not transform legacy grid props on the new components, for that use the `no-legacy-grid` rule.
10 |
11 | Examples of **incorrect** code for this rule:
12 |
13 | ```html
14 |
15 | ```
16 |
17 | Examples of **correct** code for this rule:
18 |
19 | ```js
20 |
21 |
22 | ```
23 |
24 | ### Options
25 |
26 | This rule has no configuration options.
27 |
--------------------------------------------------------------------------------
/docs/rules/icon-button-variant.md:
--------------------------------------------------------------------------------
1 | # Ensure icon buttons have a variant defined (icon-button-variant)
2 |
3 | :wrench: This rule is fixable with `eslint --fix`
4 |
5 | ## Rule Details
6 |
7 | Buttons in Vuetify 3 no longer have a different variant applied automatically.
8 |
9 | Examples of **incorrect** code for this rule:
10 |
11 | ```html
12 |
13 |
14 | ```
15 |
16 | Examples of **correct** code for this rule:
17 |
18 | ```js
19 |
20 |
21 | ```
22 |
23 | ### Options
24 |
25 | A different variant other than `text` can be assigned:
26 |
27 | ```js
28 | {
29 | 'vuetify/icon-button-variant': ['error', 'plain']
30 | }
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/rules/no-deprecated-classes.md:
--------------------------------------------------------------------------------
1 | # Disallow the use of classes that have been removed from Vuetify (no-deprecated-classes)
2 |
3 | :wrench: This rule is fixable with `eslint --fix`
4 |
5 | Vuetify v2 and v2.3 changed several utility classes (documented [here](https://vuetifyjs.com/en/getting-started/upgrade-guide/#grid)).
6 |
7 |
8 | ## Rule Details
9 |
10 | This rule disallows the use of removed utility classes.
11 |
12 | Some of the changes cannot be detected automatically, you should apply these yourself:
13 |
14 | - Change spacing helpers to intervals of 4px
15 | - ma-3 -> ma-4
16 | - ma-4 -> ma-6
17 | - ma-5 -> ma-12
18 |
19 | Examples of **incorrect** code for this rule:
20 |
21 | ```html
22 |
23 |
24 |
25 | ```
26 |
27 | Examples of **correct** code for this rule:
28 |
29 | ```js
30 |
31 |
32 |
33 | ```
34 |
35 | ### Options
36 |
37 | This rule has no configuration options.
38 |
--------------------------------------------------------------------------------
/docs/rules/no-deprecated-colors.md:
--------------------------------------------------------------------------------
1 | # Disallow the use of the old color class structure (no-deprecated-colors)
2 |
3 | :wrench: This rule is fixable with `eslint --fix`
4 |
5 | ## Rule Details
6 |
7 | Examples of **incorrect** code for this rule:
8 |
9 | ```html
10 |
11 |
12 |
13 |
14 | ```
15 |
16 | Examples of **correct** code for this rule:
17 |
18 | ```js
19 |
20 |
21 |
22 |
23 |
24 | ```
25 |
26 | ### Options
27 |
28 | Additional custom theme colors can be added to the rule configuration:
29 |
30 | ```js
31 | {
32 | 'vuetify/no-deprecated-colors': ['error', {
33 | themeColors: ['tertiary']
34 | }]
35 | }
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/rules/no-deprecated-components.md:
--------------------------------------------------------------------------------
1 | # Disallow the use of components that have been removed from Vuetify (no-deprecated-components)
2 |
3 | :wrench: This rule is partially fixable with `eslint --fix`
4 |
5 | ## Rule Details
6 |
7 | This rule disallows the use of removed and deprecated components. Grid components are not included in this rule, use [`no-legacy-grid`](./no-legacy-grid.md) instead.
8 |
9 | Examples of **incorrect** code for this rule:
10 |
11 | ```html
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ```
20 |
21 | Examples of **correct** code for this rule:
22 |
23 | ```html
24 |
25 |
26 |
27 |
28 |
29 | ```
30 |
31 | ### Options
32 |
33 | This rule has no configuration options.
34 |
--------------------------------------------------------------------------------
/docs/rules/no-deprecated-events.md:
--------------------------------------------------------------------------------
1 | # Disallow the use of events that have been removed from Vuetify (no-deprecated-events)
2 |
3 | :wrench: This rule is partially fixable with `eslint --fix`
4 |
5 | ## Rule Details
6 |
7 | This rule disallows the use of removed and deprecated events.
8 |
9 | Examples of **incorrect** code for this rule:
10 |
11 | ```html
12 |
13 |
14 | ```
15 |
16 | Examples of **correct** code for this rule:
17 |
18 | ```html
19 |
20 |
21 | ```
22 |
23 | ### Options
24 |
25 | This rule has no configuration options.
26 |
--------------------------------------------------------------------------------
/docs/rules/no-deprecated-props.md:
--------------------------------------------------------------------------------
1 | # Prevent the use of removed and deprecated props (no-deprecated-props)
2 |
3 | :wrench: This rule is partially fixable with `eslint --fix`
4 |
5 | ## Rule Details
6 |
7 | Examples of **incorrect** code for this rule:
8 |
9 | ```html
10 |
11 | ```
12 |
13 | Examples of **correct** code for this rule:
14 |
15 | ```html
16 |
17 | ```
18 |
19 | ### Options
20 |
21 | This rule has no configuration options.
22 |
--------------------------------------------------------------------------------
/docs/rules/no-deprecated-slots.md:
--------------------------------------------------------------------------------
1 | # Prevent the use of removed slot variables (no-deprecated-slots)
2 |
3 | :wrench: This rule is partially fixable with `eslint --fix`
4 |
5 | ## Rule Details
6 |
7 | Examples of **incorrect** code for this rule:
8 |
9 | ```html
10 |
11 |
12 |
13 |
14 |
15 | ```
16 |
17 | Examples of **correct** code for this rule:
18 |
19 | ```html
20 |
21 |
22 |
23 |
24 |
25 | ```
26 |
27 | Variable shadowing is not currently handled, the following will produce incorrect output that must be fixed manually:
28 |
29 | ```html
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ```
40 |
41 | ### Options
42 |
43 | This rule has no configuration options.
44 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | const neostandard = require('neostandard')
2 |
3 | module.exports = [
4 | ...neostandard(),
5 | {
6 | rules: {
7 | 'no-template-curly-in-string': 'off',
8 |
9 | '@stylistic/quotes': ['error', 'single', {
10 | allowTemplateLiterals: true,
11 | }],
12 | '@stylistic/comma-dangle': ['error', {
13 | arrays: 'always-multiline',
14 | objects: 'always-multiline',
15 | imports: 'always-multiline',
16 | exports: 'always-multiline',
17 | functions: 'only-multiline',
18 | }],
19 | }
20 | }
21 | ]
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-vuetify",
3 | "version": "2.5.2",
4 | "description": "An eslint plugin for Vuetify",
5 | "main": "lib/index.js",
6 | "author": "Kael Watts-Deuchar ",
7 | "license": "MIT",
8 | "repository": "github:vuetifyjs/eslint-plugin-vuetify",
9 | "scripts": {
10 | "build": "rimraf lib && babel src --out-dir lib",
11 | "test": "mocha tests --recursive --reporter dot",
12 | "test:8": "ESLINT8=true mocha tests --recursive --reporter dot",
13 | "test:coverage": "nyc mocha tests --recursive --reporter dot",
14 | "test:ci": "nyc --reporter=lcov mocha tests --recursive --reporter dot",
15 | "lint": "eslint src tests",
16 | "prepublishOnly": "npm run build"
17 | },
18 | "files": [
19 | "lib"
20 | ],
21 | "homepage": "https://github.com/vuetifyjs/eslint-plugin-vuetify#readme",
22 | "dependencies": {
23 | "eslint-plugin-vue": ">=9.6.0",
24 | "requireindex": "^1.2.0"
25 | },
26 | "devDependencies": {
27 | "@babel/cli": "^7.19.3",
28 | "@babel/core": "^7.19.6",
29 | "@babel/preset-env": "^7.19.4",
30 | "@stylistic/eslint-plugin": "^2.10.1",
31 | "conventional-changelog-cli": "^2.2.2",
32 | "conventional-changelog-vuetify": "^1.1.0",
33 | "conventional-github-releaser": "^3.1.5",
34 | "eslint": "^9.22.0",
35 | "eslint8": "npm:eslint@8.57.1",
36 | "eslint-plugin-vue": "^10.0.0",
37 | "husky": "^8.0.1",
38 | "mocha": "^10.1.0",
39 | "neostandard": "^0.11.8",
40 | "nyc": "^15.1.0",
41 | "rimraf": "^3.0.2",
42 | "vue": "^3.5.13",
43 | "vue-eslint-parser": "^10.1.1",
44 | "vuetify": "^3.7.17"
45 | },
46 | "peerDependencies": {
47 | "eslint": "^8.0.0 || ^9.0.0",
48 | "vuetify": "^3.0.0"
49 | },
50 | "packageManager": "pnpm@9.13.2"
51 | }
52 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended",
5 | "group:allNonMajor"
6 | ],
7 | "automerge": true,
8 | "automergeSchedule": ["* * * * 2"]
9 | }
10 |
--------------------------------------------------------------------------------
/scripts/lint-commit-message.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 |
3 | const reset = '\x1b[0m'
4 | const red = '\x1b[31m'
5 |
6 | const [messageFile] = process.argv.slice(2)
7 | const currentMessage = fs.readFileSync(messageFile, 'utf8').replace(/^# ------------------------ >8 ------------------------[\s\S]*$|^#.*\n/gm, '')
8 |
9 | const errors = []
10 |
11 | function check (message, cb) {
12 | if (cb(currentMessage)) {
13 | errors.push(message)
14 | }
15 | }
16 |
17 | check('Whitespace at beginning of message', m => /^\s/.test(m))
18 | check('Title is too long. limit to 72 chars', m => m.trim().split(/\r?\n/, 1)[0].length > 72)
19 | check('Title and body must be separated by a blank line', m => {
20 | const s = m.trim().split(/\r?\n/, 3)
21 | return s[1] != null && !!s[1].length
22 | })
23 |
24 | if (errors.length) {
25 | const s = errors.length > 1 ? 's' : ''
26 | console.log()
27 | console.log(red + `Error${s} in commit message:` + reset)
28 | errors.forEach(err => {
29 | console.log(' - ' + err)
30 | })
31 | console.log()
32 | console.log('-'.repeat(72))
33 | console.log('Original message:')
34 | console.log('='.repeat(72))
35 | console.log(currentMessage.trimRight())
36 | console.log('='.repeat(72))
37 | process.exit(1)
38 | }
39 |
--------------------------------------------------------------------------------
/scripts/warn-npm-install.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 |
3 | const reset = '\x1b[0m'
4 | const red = '\x1b[31m'
5 | const bright = '\x1b[1m'
6 |
7 | if (fs.existsSync('package-lock.json') || fs.existsSync('yarn.lock')) {
8 | console.log()
9 | console.log(`${red}WARNING:${reset}`)
10 | console.log(`This project uses ${bright}PNPM${reset}. Installing its dependencies with ${bright}npm${reset} or ${bright}yarn${reset} may result in errors`)
11 | console.log(`Please remove ${bright}package-lock.json${reset} and ${bright}yarn.lock${reset} and try again, with PNPM this time`)
12 | console.log(`See ${bright}https://pnpm.io/${reset}`)
13 | console.log()
14 | process.exit(1)
15 | }
16 |
--------------------------------------------------------------------------------
/src/configs/base.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | plugins: [
5 | 'vuetify',
6 | ],
7 | rules: {
8 | 'vue/valid-v-slot': ['error', {
9 | allowModifiers: true,
10 | }],
11 |
12 | 'vuetify/no-deprecated-classes': 'error',
13 | 'vuetify/no-deprecated-colors': 'error',
14 | 'vuetify/no-deprecated-components': 'error',
15 | 'vuetify/no-deprecated-events': 'error',
16 | 'vuetify/no-deprecated-props': 'error',
17 | 'vuetify/no-deprecated-slots': 'error',
18 | 'vuetify/no-deprecated-imports': 'error',
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/src/configs/flat/base.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | plugins: {
4 | vue: require('eslint-plugin-vue'),
5 | get vuetify () {
6 | return require('../../index')
7 | },
8 | },
9 | rules: require('../base').rules,
10 | },
11 | ]
12 |
--------------------------------------------------------------------------------
/src/configs/flat/recommended.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | ...require('./base'),
3 | {
4 | plugins: {
5 | get vuetify () {
6 | return require('../../index')
7 | },
8 | },
9 | rules: require('../recommended').rules,
10 | },
11 | ]
12 |
--------------------------------------------------------------------------------
/src/configs/recommended.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | extends: require.resolve('./base'),
5 | rules: {
6 | 'vuetify/grid-unknown-attributes': 'error',
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const requireindex = require('requireindex')
4 |
5 | module.exports = {
6 | configs: {
7 | base: require('./configs/base'),
8 | recommended: require('./configs/recommended'),
9 |
10 | 'flat/base': require('./configs/flat/base'),
11 | 'flat/recommended': require('./configs/flat/recommended'),
12 | },
13 | rules: requireindex(path.join(__dirname, './rules')),
14 | }
15 |
--------------------------------------------------------------------------------
/src/rules/grid-unknown-attributes.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { hyphenate, classify, getAttributes, isVueTemplate } = require('../util/helpers')
4 | const { isGridAttribute } = require('../util/grid-attributes')
5 | const { addClass, removeAttr } = require('../util/fixers')
6 |
7 | const { components } = require('vuetify/dist/vuetify.js')
8 |
9 | const VGrid = {
10 | VContainer: components.VContainer,
11 | VRow: components.VRow,
12 | VCol: components.VCol,
13 | }
14 |
15 | const tags = Object.keys(VGrid).reduce((t, k) => {
16 | t[classify(k)] = Object.keys(VGrid[k].props).map(p => hyphenate(p)).sort()
17 |
18 | return t
19 | }, {})
20 |
21 | // ------------------------------------------------------------------------------
22 | // Rule Definition
23 | // ------------------------------------------------------------------------------
24 |
25 | module.exports = {
26 | meta: {
27 | docs: {
28 | description: 'warn about unknown attributes not being converted to classes on new grid components',
29 | category: 'recommended',
30 | },
31 | fixable: 'code',
32 | schema: [],
33 | },
34 | create (context) {
35 | if (!isVueTemplate(context)) return {}
36 |
37 | return context.sourceCode.parserServices.defineTemplateBodyVisitor({
38 | VElement (element) {
39 | const tag = classify(element.rawName)
40 | if (!Object.keys(tags).includes(tag)) return
41 |
42 | const attributes = getAttributes(element).filter(({ name }) => {
43 | return !tags[tag].includes(name) && !isGridAttribute(tag, name)
44 | })
45 |
46 | if (attributes.length) {
47 | context.report({
48 | node: element.startTag,
49 | loc: {
50 | start: attributes[0].node.loc.start,
51 | end: attributes[attributes.length - 1].node.loc.end,
52 | },
53 | message: 'Attributes are no longer converted into classes',
54 | fix (fixer) {
55 | const fixableAttrs = attributes.map(({ node }) => node)
56 | .filter(attr => !attr.directive)
57 |
58 | if (!fixableAttrs.length) return
59 |
60 | const className = fixableAttrs.map(node => node.key.rawName).join(' ')
61 | return [
62 | addClass(context, fixer, element, className),
63 | ...fixableAttrs.map(removeAttr.bind(this, context, fixer)),
64 | ]
65 | },
66 | })
67 | }
68 | },
69 | })
70 | },
71 | }
72 |
--------------------------------------------------------------------------------
/src/rules/icon-button-variant.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { classify, getAttributes, isVueTemplate } = require('../util/helpers')
4 |
5 | // ------------------------------------------------------------------------------
6 | // Rule Definition
7 | // ------------------------------------------------------------------------------
8 |
9 | module.exports = {
10 | meta: {
11 | docs: {
12 | description: 'Ensure icon buttons have a variant defined.',
13 | category: 'recommended',
14 | },
15 | fixable: 'code',
16 | schema: [
17 | { type: 'string' },
18 | ],
19 | messages: {
20 | needsVariant: 'Icon buttons should have {{ a }} defined.',
21 | },
22 | },
23 |
24 | create (context) {
25 | if (!isVueTemplate(context)) return {}
26 |
27 | return context.sourceCode.parserServices.defineTemplateBodyVisitor({
28 | VElement (element) {
29 | const tag = classify(element.rawName)
30 | if (tag !== 'VBtn') return
31 |
32 | const attributes = getAttributes(element)
33 | const iconAttribute = attributes.find(attr => attr.name === 'icon')
34 | if (!iconAttribute) return
35 | if (attributes.some(attr => attr.name === 'variant')) return
36 |
37 | const variant = `variant="${context.options[0] || 'text'}"`
38 |
39 | context.report({
40 | node: iconAttribute.node,
41 | messageId: 'needsVariant',
42 | data: { a: variant },
43 | fix (fixer) {
44 | return fixer.insertTextAfter(iconAttribute.node, ' ' + variant)
45 | },
46 | })
47 | },
48 | })
49 | },
50 | }
51 |
--------------------------------------------------------------------------------
/src/rules/no-deprecated-classes.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { isVueTemplate } = require('../util/helpers')
4 |
5 | /** @type {Map string | false) | false> | Map} */
6 | const replacements = new Map([
7 | [/^rounded-(r|l|tr|tl|br|bl)(-.*)?$/, ([side, rest]) => {
8 | side = {
9 | r: 'e',
10 | l: 's',
11 | tr: 'te',
12 | tl: 'ts',
13 | br: 'be',
14 | bl: 'bs',
15 | }[side]
16 | return `rounded-${side}${rest || ''}`
17 | }],
18 | [/^text-xs-(left|right|center|justify)$/, ([align]) => `text-${align}`],
19 | [/^hidden-(xs|sm|md|lg|xl)-only$/, ([breakpoint]) => `hidden-${breakpoint}`],
20 | ['scroll-y', 'overflow-y-auto'],
21 | ['hide-overflow', 'overflow-hidden'],
22 | ['show-overflow', 'overflow-visible'],
23 | ['no-wrap', 'text-no-wrap'],
24 | ['ellipsis', 'text-truncate'],
25 | ['left', 'float-left'],
26 | ['right', 'float-right'],
27 | ['display-4', 'text-h1'],
28 | ['display-3', 'text-h2'],
29 | ['display-2', 'text-h3'],
30 | ['display-1', 'text-h4'],
31 | ['headline', 'text-h5'],
32 | ['title', 'text-h6'],
33 | ['subtitle-1', 'text-subtitle-1'],
34 | ['subtitle-2', 'text-subtitle-2'],
35 | ['body-1', 'text-body-1'],
36 | ['body-2', 'text-body-2'],
37 | ['caption', 'text-caption'],
38 | ['caption', 'text-caption'],
39 | ['overline', 'text-overline'],
40 | [/^transition-(fast-out-slow-in|linear-out-slow-in|fast-out-linear-in|ease-in-out|fast-in-fast-out|swing)$/, false],
41 | ])
42 |
43 | // ------------------------------------------------------------------------------
44 | // Rule Definition
45 | // ------------------------------------------------------------------------------
46 |
47 | module.exports = {
48 | meta: {
49 | docs: {
50 | description: 'Disallow the use of classes that have been removed from Vuetify',
51 | },
52 | fixable: 'code',
53 | schema: [],
54 | messages: {
55 | replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`,
56 | removed: `'{{ name }}' has been removed`,
57 | },
58 | },
59 |
60 | create (context) {
61 | if (!isVueTemplate(context)) return {}
62 |
63 | return context.sourceCode.parserServices.defineTemplateBodyVisitor({
64 | 'VAttribute[key.name="class"]' (node) {
65 | if (!node.value || !node.value.value) return
66 |
67 | const classes = node.value.value.split(/\s+/).filter(s => !!s)
68 | const changed = []
69 | classes.forEach(className => {
70 | for (const replacer of replacements) {
71 | if (typeof replacer[0] === 'string' && replacer[0] === className) {
72 | return changed.push([className, replacer[1]])
73 | }
74 | if (replacer[0] instanceof RegExp) {
75 | const matches = (replacer[0].exec(className) || []).slice(1)
76 | const replace = replacer[1]
77 | if (matches.length) {
78 | if (typeof replace === 'function') {
79 | return changed.push([className, replace(matches)])
80 | } else {
81 | return changed.push([className, replace])
82 | }
83 | }
84 | }
85 | }
86 | })
87 |
88 | changed.forEach(change => {
89 | const idx = node.value.value.indexOf(change[0]) + 1
90 | const range = [
91 | node.value.range[0] + idx,
92 | node.value.range[0] + idx + change[0].length,
93 | ]
94 | const loc = {
95 | start: context.sourceCode.getLocFromIndex(range[0]),
96 | end: context.sourceCode.getLocFromIndex(range[1]),
97 | }
98 | if (change[1]) {
99 | context.report({
100 | loc,
101 | messageId: 'replacedWith',
102 | data: {
103 | a: change[0],
104 | b: change[1],
105 | },
106 | fix (fixer) {
107 | return fixer.replaceTextRange(range, change[1])
108 | },
109 | })
110 | } else {
111 | context.report({
112 | loc,
113 | messageId: 'removed',
114 | data: {
115 | name: change[0],
116 | },
117 | })
118 | }
119 | })
120 | },
121 | })
122 | },
123 | }
124 |
--------------------------------------------------------------------------------
/src/rules/no-deprecated-colors.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { isVueTemplate } = require('../util/helpers')
4 |
5 | const cssColors = [
6 | 'red', 'pink', 'purple', 'deep-purple',
7 | 'indigo', 'blue', 'light-blue', 'cyan',
8 | 'teal', 'green', 'light-green', 'lime',
9 | 'yellow', 'amber', 'orange', 'deep-orange',
10 | 'brown', 'blue-grey', 'grey', 'black',
11 | 'white', 'transparent',
12 | ]
13 | const cssTextColors = cssColors.map(v => `${v}--text`)
14 | const variants = [
15 | 'lighten-1', 'lighten-2', 'lighten-3', 'lighten-4', 'lighten-5',
16 | 'darken-1', 'darken-2', 'darken-3', 'darken-4',
17 | 'accent-1', 'accent-2', 'accent-3', 'accent-4',
18 | ]
19 | const textVariants = variants.map(v => `text--${v}`)
20 |
21 | // ------------------------------------------------------------------------------
22 | // Rule Definition
23 | // ------------------------------------------------------------------------------
24 |
25 | module.exports = {
26 | meta: {
27 | docs: {
28 | description: 'Disallow the use of classes that have been removed from Vuetify',
29 | },
30 | fixable: 'code',
31 | schema: [{
32 | type: 'object',
33 | properties: {
34 | themeColors: {
35 | type: 'array',
36 | items: {
37 | type: 'string',
38 | },
39 | },
40 | },
41 | additionalProperties: false,
42 | }],
43 | messages: {
44 | replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`,
45 | removed: `'{{ name }}' cannot be used alone, it must be combined with a color`,
46 | },
47 | },
48 |
49 | create (context) {
50 | if (!isVueTemplate(context)) return {}
51 |
52 | const themeColors = ['primary', 'secondary', 'accent', 'error', 'warning', 'info', 'success', ...(context.options[0]?.themeColors || [])]
53 | const themeTextColors = themeColors.map(v => `${v}--text`)
54 |
55 | function findColor (classes) {
56 | const base = classes.findIndex(t => themeColors.includes(t) || cssColors.includes(t))
57 | const variant = classes.findIndex(t => variants.includes(t))
58 |
59 | return [base, variant]
60 | }
61 | function findTextColor (classes) {
62 | const base = classes.findIndex(t => themeTextColors.includes(t) || cssTextColors.includes(t))
63 | const variant = classes.findIndex(t => textVariants.includes(t))
64 |
65 | return [base, variant]
66 | }
67 |
68 | return context.sourceCode.parserServices.defineTemplateBodyVisitor({
69 | 'VAttribute[key.name="color"]' (node) {
70 | if (!node.value || !node.value.value) return
71 |
72 | const color = node.value.value.split(/\s+/).filter(s => !!s)
73 | const [base, variant] = findColor(color)
74 | if (~base && ~variant) {
75 | context.report({
76 | node,
77 | messageId: 'replacedWith',
78 | data: {
79 | a: node.value.value,
80 | b: `${color[base]}-${color[variant]}`,
81 | },
82 | fix: fixer => fixer.replaceTextRange(node.value.range, `"${color[base]}-${color[variant]}"`),
83 | })
84 | }
85 | },
86 | 'VAttribute[key.name="class"]' (node) {
87 | if (!node.value || !node.value.value) return
88 |
89 | const classes = node.value.value.split(/\s+/).filter(s => !!s)
90 |
91 | for (const [find, prefix] of [[findColor, 'bg'], [findTextColor, 'text']]) {
92 | const [base, variant] = find(classes)
93 | if (~base || (~base && ~variant)) {
94 | const newColor = ~variant
95 | ? `${prefix}-${classes[base].replace('--text', '')}-${classes[variant].replace('text--', '')}`
96 | : `${prefix}-${classes[base].replace('--text', '')}`
97 |
98 | context.report({
99 | node,
100 | messageId: 'replacedWith',
101 | data: {
102 | a: ~variant ? `${classes[base]} ${classes[variant]}` : classes[base],
103 | b: newColor,
104 | },
105 | fix: fixer => {
106 | const newClasses = classes.slice()
107 | newClasses.splice(base, 1, newColor)
108 | if (~variant) newClasses.splice(variant, 1)
109 |
110 | return fixer.replaceTextRange(node.value.range, `"${newClasses.join(' ')}"`)
111 | },
112 | })
113 | } else if (~variant) {
114 | context.report({
115 | node,
116 | messageId: 'removed',
117 | data: {
118 | name: classes[variant],
119 | },
120 | })
121 | }
122 | }
123 | },
124 | })
125 | },
126 | }
127 |
--------------------------------------------------------------------------------
/src/rules/no-deprecated-components.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { hyphenate, classify, isVueTemplate } = require('../util/helpers')
4 |
5 | const replacements = {
6 | VListTile: 'v-list-item',
7 | VListTileAction: 'v-list-item-action',
8 | VListTileAvatar: false,
9 | VListTileActionText: 'v-list-item-action-text',
10 | VListTileContent: false,
11 | VListTileTitle: 'v-list-item-title',
12 | VListTileSubTitle: 'v-list-item-subtitle',
13 | VJumbotron: false,
14 | VToolbarSideIcon: 'v-app-bar-nav-icon',
15 | VExpansionPanelHeader: 'v-expansion-panel-title',
16 | VExpansionPanelContent: 'v-expansion-panel-text',
17 |
18 | // Possible typos
19 | VListItemSubTitle: 'v-list-item-subtitle',
20 | VListTileSubtitle: 'v-list-item-subtitle',
21 |
22 | VContent: 'v-main',
23 |
24 | VData: false,
25 | VListItemGroup: false,
26 | VListItemAvatar: { custom: '`v-list-item` with `avatar` props, or `v-avatar` in the list item append or prepend slot' },
27 | VListItemContent: false,
28 | VListItemIcon: { custom: '`v-list-item` with `icon` props, or `v-icon` in the list item append or prepend slot' },
29 | VOverflowBtn: false,
30 | VPicker: false,
31 | VSimpleCheckbox: 'v-checkbox-btn',
32 | VSubheader: { custom: 'v-list-subheader or class="text-subtitle-2"' },
33 | VSimpleTable: 'v-table',
34 | VTabsSlider: false,
35 | VTabsItems: false,
36 | VTabItem: false,
37 | }
38 |
39 | // ------------------------------------------------------------------------------
40 | // Rule Definition
41 | // ------------------------------------------------------------------------------
42 |
43 | module.exports = {
44 | meta: {
45 | docs: {
46 | description: 'Prevent the use of components that have been removed from Vuetify',
47 | category: 'recommended',
48 | },
49 | fixable: 'code',
50 | schema: [],
51 | messages: {
52 | replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`,
53 | replacedWithCustom: `'{{ a }}' has been replaced with {{ b }}`,
54 | removed: `'{{ name }}' has been removed`,
55 | },
56 | },
57 | create (context) {
58 | if (!isVueTemplate(context)) return {}
59 |
60 | return context.sourceCode.parserServices.defineTemplateBodyVisitor({
61 | VElement (element) {
62 | const tag = classify(element.rawName)
63 |
64 | const tokens = context.sourceCode.parserServices.getTemplateBodyTokenStore()
65 |
66 | if (Object.prototype.hasOwnProperty.call(replacements, tag)) {
67 | const replacement = replacements[tag]
68 | if (typeof replacement === 'object' && 'custom' in replacement) {
69 | context.report({
70 | node: element,
71 | messageId: 'replacedWithCustom',
72 | data: {
73 | a: hyphenate(tag),
74 | b: replacement.custom,
75 | },
76 | })
77 | } else if (typeof replacement === 'string') {
78 | context.report({
79 | node: element,
80 | messageId: 'replacedWith',
81 | data: {
82 | a: hyphenate(tag),
83 | b: replacement,
84 | },
85 | fix (fixer) {
86 | const open = tokens.getFirstToken(element.startTag)
87 | const endTag = element.endTag
88 | if (!endTag) {
89 | return fixer.replaceText(open, `<${replacement}`)
90 | }
91 | const endTagOpen = tokens.getFirstToken(endTag)
92 | return [
93 | fixer.replaceText(open, `<${replacement}`),
94 | fixer.replaceText(endTagOpen, `${replacement}`),
95 | ]
96 | },
97 | })
98 | } else if (!replacement) {
99 | context.report({
100 | node: element,
101 | messageId: 'removed',
102 | data: { name: hyphenate(tag) },
103 | fix (fixer) {
104 | if (tag === 'VListItemContent' && !element.startTag.attributes.length) {
105 | return element.children.length
106 | ? [fixer.remove(element.startTag), fixer.remove(element.endTag)]
107 | : fixer.remove(element)
108 | }
109 | },
110 | })
111 | }
112 | }
113 | },
114 | })
115 | },
116 | }
117 |
--------------------------------------------------------------------------------
/src/rules/no-deprecated-events.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { hyphenate, classify, isVueTemplate } = require('../util/helpers')
4 |
5 | const model = {
6 | input: 'update:modelValue',
7 | }
8 |
9 | const inputs = {
10 | ...model,
11 | 'click:appendOuter': 'click:append',
12 | 'update:error': false,
13 | }
14 |
15 | const combobox = {
16 | ...inputs,
17 | 'update:searchInput': 'update:search',
18 | 'update:listIndex': false,
19 | }
20 |
21 | const replacements = {
22 | VTextField: inputs,
23 | VTextarea: inputs,
24 | VFileInput: inputs,
25 | VCombobox: {
26 | ...combobox,
27 | change: false,
28 | },
29 | VAutocomplete: {
30 | ...combobox,
31 | change: 'update:modelValue',
32 | },
33 | VSelect: {
34 | ...combobox,
35 | change: 'update:modelValue',
36 | },
37 | VAlert: model,
38 | VTabs: {
39 | ...model,
40 | 'call:slider': false,
41 | change: 'update:modelValue',
42 | next: false,
43 | prev: false,
44 | },
45 | VTab: {
46 | change: 'group:selected',
47 | },
48 | VBottomNavigation: {
49 | change: 'update:modelValue',
50 | 'update:inputValue': false,
51 | },
52 | VBtn: {
53 | change: 'group:selected',
54 | },
55 | VBtnToggle: {
56 | change: 'update:modelValue',
57 | },
58 | VCarousel: {
59 | change: 'update:modelValue',
60 | },
61 | VCheckbox: {
62 | change: 'update:modelValue',
63 | 'update:error': false,
64 | },
65 | VChip: {
66 | ...model,
67 | 'update:active': 'update:modelValue',
68 | },
69 | VChipGroup: {
70 | change: 'update:modelValue',
71 | },
72 | VColorPicker: {
73 | ...model,
74 | 'update:color': 'update:modelValue',
75 | },
76 | VDialog: {
77 | ...model,
78 | 'update:returnValue': false,
79 | },
80 | VExpansionPanels: {
81 | change: 'update:modelValue',
82 | },
83 | VExpansionPanel: {
84 | change: 'group:selected',
85 | },
86 | VForm: {
87 | input: 'update:modelValue',
88 | },
89 | VHover: model,
90 | VItemGroup: {
91 | change: 'update:modelValue',
92 | },
93 | VMenu: {
94 | ...model,
95 | 'update:returnValue': false,
96 | },
97 | VNavigationDrawer: {
98 | ...model,
99 | 'update:miniVariant': false,
100 | },
101 | VPagination: {
102 | ...model,
103 | previous: 'prev',
104 | },
105 | VProgressLinear: {
106 | change: 'update:modelValue',
107 | },
108 | VRadioGroup: {
109 | change: 'update:modelValue',
110 | 'update:error': false,
111 | },
112 | VRadio: {
113 | change: 'update:modelValue',
114 | 'update:error': false,
115 | },
116 | VRangeSlider: {
117 | ...model,
118 | change: false,
119 | 'update:error': false,
120 | },
121 | VRating: model,
122 | VSlider: {
123 | ...model,
124 | change: false,
125 | 'update:error': false,
126 | },
127 | VSlideGroup: {
128 | change: 'update:modelValue',
129 | next: false,
130 | prev: false,
131 | },
132 | VSnackbar: model,
133 | VSwitch: {
134 | change: 'update:modelValue',
135 | 'update:error': false,
136 | },
137 | VWindow: {
138 | change: 'update:modelValue',
139 | },
140 | }
141 |
142 | // ------------------------------------------------------------------------------
143 | // Rule Definition
144 | // ------------------------------------------------------------------------------
145 |
146 | module.exports = {
147 | meta: {
148 | docs: {
149 | description: 'Prevent the use of removed and deprecated events.',
150 | category: 'recommended',
151 | },
152 | fixable: 'code',
153 | schema: [],
154 | messages: {
155 | replacedWith: `{{ tag }}: @{{ a }} has been replaced with @{{ b }}`,
156 | removed: `{{ tag }}: @{{ name }} has been removed`,
157 | },
158 | },
159 |
160 | create (context) {
161 | if (!isVueTemplate(context)) return {}
162 |
163 | return context.sourceCode.parserServices.defineTemplateBodyVisitor({
164 | VAttribute (attr) {
165 | if (!(attr.directive && attr.key.name.name === 'on' && attr.key.argument?.type === 'VIdentifier')) return
166 |
167 | const tag = classify(attr.parent.parent.rawName)
168 | if (!Object.keys(replacements).includes(tag)) return
169 |
170 | const eventNameNode = attr.key.argument
171 | const eventName = hyphenate(eventNameNode.rawName)
172 |
173 | Object.entries(replacements[tag]).forEach(([test, replace]) => {
174 | if (hyphenate(test) === eventName) {
175 | if (replace === false) {
176 | context.report({
177 | messageId: 'removed',
178 | data: { tag, name: eventName },
179 | node: eventNameNode,
180 | })
181 | } else if (typeof replace === 'string') {
182 | context.report({
183 | messageId: 'replacedWith',
184 | data: {
185 | tag,
186 | a: eventName,
187 | b: replace,
188 | },
189 | node: eventNameNode,
190 | fix (fixer) {
191 | return fixer.replaceText(eventNameNode, hyphenate(replace))
192 | },
193 | })
194 | } else if (typeof replace === 'object' && 'custom' in replace) {
195 | context.report({
196 | messageId: 'replacedWith',
197 | data: {
198 | a: eventName,
199 | b: replace.custom,
200 | },
201 | node: eventNameNode,
202 | })
203 | }
204 | }
205 | })
206 | },
207 | })
208 | },
209 | }
210 |
--------------------------------------------------------------------------------
/src/rules/no-deprecated-imports.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | meta: {
3 | type: 'problem',
4 | docs: {
5 | description: 'disallow import from "vuetify/lib/util/colors", suggest "vuetify/util/colors" instead',
6 | category: 'Best Practices',
7 | recommended: false,
8 | },
9 | fixable: 'code',
10 | schema: [],
11 | },
12 | create (context) {
13 | return {
14 | ImportDeclaration (node) {
15 | if (node.source.value === 'vuetify/lib/util/colors') {
16 | context.report({
17 | node,
18 | message: 'Import from "vuetify/lib/util/colors" is deprecated. Use "vuetify/util/colors" instead.',
19 | fix (fixer) {
20 | return fixer.replaceText(
21 | node.source,
22 | node.source.raw.replace('vuetify/lib/util/colors', 'vuetify/util/colors')
23 | )
24 | },
25 | })
26 | }
27 | },
28 | }
29 | },
30 | }
31 |
--------------------------------------------------------------------------------
/src/rules/no-deprecated-props.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { hyphenate, classify, isVueTemplate } = require('../util/helpers')
4 |
5 | const size = {
6 | maxHeight: false,
7 | maxWidth: false,
8 | minHeight: false,
9 | minWidth: false,
10 | }
11 |
12 | const sizes = {
13 | large: { name: 'size', value: 'large' },
14 | medium: { name: 'size', value: 'medium' },
15 | small: { name: 'size', value: 'small' },
16 | xLarge: { name: 'size', value: 'x-large' },
17 | xSmall: { name: 'size', value: 'x-small' },
18 | }
19 |
20 | const theme = {
21 | dark: false,
22 | light: false,
23 | }
24 |
25 | const inputs = {
26 | appendOuterIcon: 'append-icon',
27 | backgroundColor: 'bg-color',
28 | box: { name: 'variant', value: 'filled' },
29 | dense: { name: 'density', value: 'compact' },
30 | errorCount: 'max-errors',
31 | filled: { name: 'variant', value: 'filled' },
32 | fullWidth: false,
33 | height: false,
34 | loaderHeight: false,
35 | outline: { name: 'variant', value: 'outlined' },
36 | outlined: { name: 'variant', value: 'outlined' },
37 | shaped: false,
38 | solo: { name: 'variant', value: 'solo' },
39 | soloInverted: { name: 'variant', value: 'solo-inverted' },
40 | success: false,
41 | successMessages: false,
42 | validateOnBlur: { name: 'validate-on', value: 'blur' },
43 | value: 'model-value',
44 | ...theme,
45 | }
46 |
47 | const select = {
48 | allowOverflow: false,
49 | attach: { custom: ':menu-props="{ attach: true }"' },
50 | cacheItems: false,
51 | deletableChips: 'closable-chips',
52 | disableLookup: false,
53 | itemDisabled: { custom: 'item-props.disabled' },
54 | itemText: 'item-title',
55 | searchInput: 'search',
56 | smallChips: false,
57 | filter: 'customFilter',
58 | ...inputs,
59 | }
60 |
61 | const link = {
62 | append: false,
63 | exactActiveClass: false,
64 | exactPath: false,
65 | nuxt: false,
66 | }
67 |
68 | const overlay = {
69 | hideOverlay: { name: 'scrim', bind: true, value: false },
70 | internalActivator: false,
71 | overlayColor: 'scrim',
72 | overlayOpacity: false,
73 | value: 'model-value',
74 | returnValue: false,
75 | }
76 |
77 | const replacements = {
78 | VAppBar: {
79 | app: false,
80 | clippedLeft: false,
81 | clippedRight: false,
82 | collapseOnScroll: { name: 'scroll-behavior', value: 'collapse' },
83 | elevateOnScroll: { name: 'scroll-behavior', value: 'elevate' },
84 | fadeImgOnScroll: { name: 'scroll-behavior', value: 'fade-image' },
85 | fixed: false,
86 | hideOnScroll: { name: 'scroll-behavior', value: 'hide' },
87 | invertedScroll: { name: 'scroll-behavior', value: 'inverted' },
88 | outlined: 'border',
89 | prominent: false,
90 | scrollOffScreen: false,
91 | shaped: false,
92 | short: false,
93 | shrinkOnScroll: false,
94 | width: false,
95 | ...theme,
96 | ...size,
97 | },
98 | VAlert: {
99 | border: { name: 'border', value: value => ({ right: 'end', left: 'start' }[value] || value) },
100 | dense: { name: 'density', value: 'compact' },
101 | outline: { name: 'variant', value: 'outlined' },
102 | coloredBorder: { custom: 'border-color' },
103 | dismissible: 'closable',
104 | mode: false,
105 | origin: false,
106 | outlined: { name: 'variant', value: 'outlined' },
107 | shaped: false,
108 | transition: false,
109 | ...theme,
110 | },
111 | VAvatar: {
112 | height: { custom: 'size' },
113 | width: { custom: 'size' },
114 | left: 'start',
115 | right: 'end',
116 | ...size,
117 | },
118 | VBadge: {
119 | value: 'model-value',
120 | avatar: false,
121 | mode: false,
122 | origin: false,
123 | overlap: false,
124 | bottom: { name: 'location', value: 'bottom' },
125 | left: { name: 'location', value: 'left' },
126 | right: { name: 'location', value: 'right' },
127 | top: { name: 'location', value: 'top' },
128 | },
129 | VBanner: {
130 | app: false,
131 | iconColor: false,
132 | mobileBreakPoint: false,
133 | outlined: 'border',
134 | shaped: false,
135 | value: false,
136 | },
137 | VBottomNavigation: {
138 | activeClass: 'selected-class',
139 | app: false,
140 | fixed: false,
141 | hideOnScroll: false,
142 | inputValue: false,
143 | scrollTarget: false,
144 | scrollThreshold: false,
145 | width: false,
146 | value: 'model-value',
147 | ...size,
148 | },
149 | VBreadcrumbs: {
150 | large: false,
151 | ...theme,
152 | },
153 | VBreadcrumbsItem: {
154 | link: false,
155 | ripple: false,
156 | ...link,
157 | },
158 | VBtn: {
159 | activeClass: 'selected-class',
160 | bottom: { name: 'location', value: 'bottom' },
161 | depressed: { name: 'variant', value: 'flat' },
162 | fab: false,
163 | flat: { name: 'variant', value: 'flat' },
164 | inputValue: false,
165 | left: { name: 'location', value: 'left' },
166 | link: false,
167 | outline: { name: 'variant', value: 'outlined' },
168 | outlined: { name: 'variant', value: 'outlined' },
169 | plain: { name: 'variant', value: 'plain' },
170 | retainFocusOnClick: false,
171 | right: { name: 'location', value: 'right' },
172 | round: 'rounded',
173 | shaped: false,
174 | text: { name: 'variant', value: 'text' },
175 | top: { name: 'location', value: 'top' },
176 | ...link,
177 | ...theme,
178 | ...sizes,
179 | },
180 | VBtnToggle: {
181 | activeClass: 'selected-class',
182 | backgroundColor: false,
183 | borderless: false,
184 | dense: { name: 'density', value: 'compact' },
185 | shaped: false,
186 | value: 'model-value',
187 | valueComparator: false,
188 | ...theme,
189 | },
190 | VCard: {
191 | activeClass: false,
192 | loaderHeight: false,
193 | outlined: 'border',
194 | raised: { name: 'elevation', value: 8 },
195 | shaped: false,
196 | ...link,
197 | },
198 | VCarousel: {
199 | activeClass: 'selected-class',
200 | max: false,
201 | multiple: false,
202 | progressColor: { custom: 'progress=""' },
203 | showArrowsOnHover: { name: 'show-arrows', value: 'hover' },
204 | touchless: false,
205 | valueComparator: false,
206 | vertical: { name: 'direction', value: 'vertical' },
207 | value: 'model-value',
208 | ...theme,
209 | },
210 | VCarouselItem: {
211 | activeClass: 'selected-class',
212 | exact: false,
213 | href: false,
214 | link: false,
215 | replace: false,
216 | ripple: false,
217 | target: false,
218 | to: false,
219 | ...link,
220 | },
221 | VCheckbox: {
222 | backgroundColor: false,
223 | dense: false,
224 | errorCount: 'max-errors',
225 | hideSpinButtons: false,
226 | inputValue: 'model-value',
227 | offIcon: 'false-icon',
228 | onIcon: 'true-icon',
229 | offValue: 'false-value',
230 | onValue: 'true-value',
231 | success: false,
232 | successMessages: false,
233 | validateOnBlur: { name: 'validate-on', value: 'blur' },
234 | },
235 | VChip: {
236 | active: false,
237 | close: 'closable',
238 | inputValue: 'model-value',
239 | outline: { name: 'variant', value: 'outlined' },
240 | outlined: { name: 'variant', value: 'outlined' },
241 | selected: 'value',
242 | textColor: false,
243 | ...link,
244 | ...sizes,
245 | ...theme,
246 | },
247 | VChipGroup: {
248 | activeClass: 'selected-class',
249 | centerActive: false,
250 | mobileBreakPoint: false,
251 | nextIcon: false,
252 | prevIcon: false,
253 | showArrows: false,
254 | value: 'model-value',
255 | },
256 | VColorPicker: {
257 | flat: false,
258 | hideModeSwitch: false,
259 | value: 'model-value',
260 | },
261 | VDataTable: {
262 | serverItemsLength: { custom: '' },
263 | itemClass: { custom: 'row-props' },
264 | itemStyle: { custom: 'row-props' },
265 | sortDesc: { custom: 'sort-by' },
266 | groupDesc: { custom: 'group-by' },
267 | },
268 | VDatePicker: {
269 | activePicker: 'view-mode',
270 | pickerDate: { custom: 'separate month and year props' },
271 | locale: false,
272 | localeFirstDayOfYear: false,
273 | firstDayOfWeek: false,
274 | dayFormat: false,
275 | weekdayFormat: false,
276 | monthFormat: false,
277 | yearFormat: false,
278 | headerDateFormat: false,
279 | titleDateFormat: false,
280 | range: false,
281 | },
282 | VExpansionPanels: {
283 | accordion: { name: 'variant', value: 'accordion' },
284 | inset: { name: 'variant', value: 'inset' },
285 | popout: { name: 'variant', value: 'popout' },
286 | activeClass: 'selected-class',
287 | focusable: false,
288 | hover: false,
289 | value: 'model-value',
290 | valueComparator: false,
291 | },
292 | VTextField: {
293 | ...inputs,
294 | },
295 | VTextarea: {
296 | ...inputs,
297 | },
298 | VFileInput: {
299 | type: false,
300 | ...inputs,
301 | },
302 | VSelect: {
303 | ...select,
304 | },
305 | VAutocomplete: {
306 | ...select,
307 | },
308 | VCombobox: {
309 | ...select,
310 | },
311 | VInput: {
312 | ...inputs,
313 | },
314 | VDialog: {
315 | ...overlay,
316 | tile: { custom: 'apply border-radius changes to the root element of the `v-dialog`\'s content' },
317 | },
318 | VMenu: {
319 | allowOverflow: false,
320 | auto: false,
321 | bottom: { name: 'location', value: 'bottom' },
322 | closeOnClick: {
323 | name: 'persistent',
324 | bind: true,
325 | value: value => value ? `!(${value})` : false,
326 | },
327 | left: { name: 'location', value: 'left' },
328 | nudgeBottom: { custom: 'offset' },
329 | nudgeLeft: { custom: 'offset' },
330 | nudgeRight: { custom: 'offset' },
331 | nudgeTop: { custom: 'offset' },
332 | nudgeWidth: false,
333 | offsetOverflow: false,
334 | offsetX: false,
335 | offsetY: false,
336 | positionX: false,
337 | positionY: false,
338 | right: { name: 'location', value: 'right' },
339 | rounded: false,
340 | tile: { custom: 'apply border-radius changes to the root element of the `v-menu`\'s content' },
341 | top: { name: 'location', value: 'top' },
342 | value: 'model-value',
343 | ...overlay,
344 | },
345 | VFooter: {
346 | fixed: false,
347 | outlined: 'border',
348 | padless: false,
349 | shaped: false,
350 | width: false,
351 | ...size,
352 | },
353 | VForm: {
354 | value: 'model-value',
355 | lazyValidation: false,
356 | },
357 | VHover: {
358 | value: 'model-value',
359 | },
360 | VIcon: {
361 | dense: { name: 'size', value: 'small' },
362 | left: 'start',
363 | right: 'end',
364 | ...sizes,
365 | ...theme,
366 | },
367 | VImg: {
368 | contain: { custom: 'cover' },
369 | ...theme,
370 | },
371 | VItemGroup: {
372 | activeClass: 'selected-class',
373 | value: 'model-value',
374 | valueComparator: false,
375 | },
376 | VItem: {
377 | activeClass: 'selected-class',
378 | },
379 | VLazy: {
380 | value: 'model-value',
381 | },
382 | VList: {
383 | dense: { name: 'density', value: 'compact' },
384 | expand: false,
385 | flat: false,
386 | outlined: 'border',
387 | subheader: false,
388 | threeLine: { name: 'lines', value: 'three' },
389 | twoLine: { name: 'lines', value: 'two' },
390 | },
391 | VListGroup: {
392 | activeClass: false,
393 | disabled: false,
394 | eager: false,
395 | group: false,
396 | noAction: false,
397 | ripple: false,
398 | subGroup: false,
399 | },
400 | VListItem: {
401 | append: false,
402 | dense: { name: 'density', value: 'compact' },
403 | selectable: { custom: 'value' },
404 | threeLine: { name: 'lines', value: 'three' },
405 | twoLine: { name: 'lines', value: 'two' },
406 | inputValue: { custom: 'active' },
407 | ...link,
408 | },
409 | VNavigationDrawer: {
410 | app: false,
411 | bottom: { name: 'location', value: 'bottom' },
412 | clipped: false,
413 | fixed: false,
414 | height: false,
415 | hideOverlay: { name: 'scrim', bind: true, value: false },
416 | miniVariant: 'rail',
417 | miniVariantWidth: 'rail-width',
418 | mobileBreakPoint: false,
419 | overlayColor: 'scrim',
420 | overlayOpacity: false,
421 | right: { name: 'location', value: 'right' },
422 | src: 'image',
423 | stateless: false,
424 | value: 'model-value',
425 | ...theme,
426 | },
427 | VOverlay: {
428 | color: 'scrim',
429 | value: 'model-value',
430 | },
431 | VPagination: {
432 | circle: 'rounded',
433 | value: 'model-value',
434 | wrapperAriaLabel: 'aria-label',
435 | },
436 | VProgressCircular: {
437 | button: false,
438 | value: 'model-value',
439 | },
440 | VProgressLinear: {
441 | backgroundColor: 'bg-color',
442 | backgroundOpacity: 'bg-opacity',
443 | bottom: false,
444 | fixed: false,
445 | query: false,
446 | top: false,
447 | value: 'model-value',
448 | },
449 | VRadio: {
450 | inputValue: 'model-value',
451 | activeClass: 'false',
452 | offIcon: 'false-icon',
453 | onIcon: 'true-icon',
454 | offValue: 'false-value',
455 | onValue: 'true-value',
456 | },
457 | VRadioGroup: {
458 | activeClass: false,
459 | backgroundColor: false,
460 | row: 'inline',
461 | column: false,
462 | multiple: false,
463 | ...inputs,
464 | },
465 | VSlider: {
466 | backgroundColor: false,
467 | tickLabels: 'ticks',
468 | ticks: { custom: 'show-ticks' },
469 | vertical: { name: 'direction', value: 'vertical' },
470 | height: false,
471 | loading: false,
472 | inverseLabel: false,
473 | ...inputs,
474 | },
475 | VRangeSlider: {
476 | backgroundColor: false,
477 | tickLabels: 'ticks',
478 | ticks: { custom: 'show-ticks' },
479 | vertical: { name: 'direction', value: 'vertical' },
480 | height: false,
481 | loading: false,
482 | inverseLabel: false,
483 | ...inputs,
484 | },
485 | VRating: {
486 | backgroundColor: false,
487 | closeDelay: false,
488 | dense: { name: 'density', value: 'compact' },
489 | halfIcon: false,
490 | iconLabel: 'item-aria-label',
491 | large: false,
492 | openDelay: false,
493 | value: 'model-value',
494 | ...sizes,
495 | },
496 | VSheet: {
497 | outlined: 'border',
498 | shaped: false,
499 | },
500 | VSlideGroup: {
501 | activeClass: 'selected-class',
502 | mobileBreakPoint: false,
503 | value: 'model-value',
504 | valueComparator: false,
505 | ...theme,
506 | },
507 | VSnackbar: {
508 | app: false,
509 | bottom: { name: 'location', value: 'bottom' },
510 | centered: { custom: 'location' },
511 | elevation (attr) {
512 | if (attr.directive
513 | ? attr.value.type === 'VExpressionContainer' && attr.value.expression.type === 'Literal'
514 | : attr.value.type === 'VLiteral'
515 | ) {
516 | return { name: 'class', value: value => `elevation-${value}`, bind: false }
517 | } else if (attr.directive && attr.value.type === 'VExpressionContainer') {
518 | return { name: 'class', value: value => `\`elevation-$\{${value}}\``, bind: true }
519 | }
520 | return { name: 'class', custom: 'elevation-' }
521 | },
522 | left: { name: 'location', value: 'left' },
523 | outlined: { name: 'variant', value: 'outlined' },
524 | right: { name: 'location', value: 'right' },
525 | shaped: false,
526 | text: false,
527 | top: { name: 'location', value: 'top' },
528 | value: 'model-value',
529 | ...theme,
530 | },
531 | VSwitch: {
532 | ...inputs,
533 | inputValue: 'model-value',
534 | value: undefined,
535 | },
536 | VSystemBar: {
537 | app: false,
538 | fixed: false,
539 | lightsOut: false,
540 | },
541 | VTabs: {
542 | activeClass: false,
543 | alignWithTitle: { name: 'align-tabs', value: 'title' },
544 | backgroundColor: 'bg-color',
545 | centered: { name: 'align-tabs', value: 'center' },
546 | iconsAndText: 'stacked',
547 | right: { name: 'align-tabs', value: 'end' },
548 | value: 'model-value',
549 | vertical: { name: 'direction', value: 'vertical' },
550 | ...theme,
551 | },
552 | VTab: {
553 | activeClass: 'selected-class',
554 | link: false,
555 | ...link,
556 | },
557 | VThemeProvider: {
558 | root: false,
559 | },
560 | VTimeline: {
561 | alignTop: { name: 'align', value: 'top' },
562 | dense: { name: 'density', value: 'compact' },
563 | reverse: false,
564 | },
565 | VTimelineItem: {
566 | color: 'dot-color',
567 | left: false,
568 | right: false,
569 | ...theme,
570 | ...sizes,
571 | },
572 | VToolbar: {
573 | bottom: false,
574 | outlined: 'border',
575 | prominent: false,
576 | shaped: false,
577 | short: false,
578 | src: 'image',
579 | width: false,
580 | ...size,
581 | },
582 | VToolbarItems: {
583 | tag: false,
584 | },
585 | VTooltip: {
586 | allowOverflow: false,
587 | bottom: { name: 'location', value: 'bottom' },
588 | closeOnClick: { name: 'persistent', value: true },
589 | left: { name: 'location', value: 'left' },
590 | nudgeBottom: { custom: 'offset' },
591 | nudgeLeft: { custom: 'offset' },
592 | nudgeRight: { custom: 'offset' },
593 | nudgeTop: { custom: 'offset' },
594 | nudgeWidth: false,
595 | positionX: false,
596 | positionY: false,
597 | right: { name: 'location', value: 'right' },
598 | top: { name: 'location', value: 'top' },
599 | value: 'model-value',
600 | ...overlay,
601 | },
602 | VWindow: {
603 | activeClass: 'selected-class',
604 | showArrowsOnHover: false,
605 | touchless: false,
606 | value: 'model-value',
607 | valueComparator: false,
608 | vertical: { name: 'direction', value: 'vertical' },
609 | },
610 | VWindowItem: {
611 | activeClass: 'selected-class',
612 | },
613 | }
614 |
615 | // ------------------------------------------------------------------------------
616 | // Rule Definition
617 | // ------------------------------------------------------------------------------
618 |
619 | module.exports = {
620 | meta: {
621 | docs: {
622 | description: 'Prevent the use of removed and deprecated props.',
623 | category: 'recommended',
624 | },
625 | fixable: 'code',
626 | schema: [],
627 | messages: {
628 | replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`,
629 | removed: `'{{ name }}' has been removed`,
630 | combined: `multiple {{ a }} attributes have been combined`,
631 | },
632 | },
633 |
634 | create (context) {
635 | if (!isVueTemplate(context)) return {}
636 |
637 | return context.sourceCode.parserServices.defineTemplateBodyVisitor({
638 | VStartTag (tag) {
639 | const attrGroups = {}
640 | tag.attributes.forEach(attr => {
641 | if (['location'].includes(attr.key.name)) {
642 | attrGroups[attr.key.name] = attrGroups[attr.key.name] ?? []
643 | attrGroups[attr.key.name].push(attr)
644 | }
645 | })
646 | Object.values(attrGroups).forEach(attrGroup => {
647 | const [head, ...tail] = attrGroup
648 | if (!tail.length) return
649 | context.report({
650 | messageId: 'combined',
651 | data: {
652 | a: head.key.name,
653 | },
654 | node: head,
655 | fix (fixer) {
656 | return [
657 | fixer.replaceText(head.value, `"${attrGroup.map(a => a.value.value).join(' ')}"`),
658 | ...tail.map(a => fixer.remove(a)),
659 | ]
660 | },
661 | })
662 | })
663 | },
664 | VAttribute (attr) {
665 | if (
666 | attr.directive &&
667 | (attr.key.name.name !== 'bind' || !attr.key.argument)
668 | ) return
669 |
670 | const tag = classify(attr.parent.parent.rawName)
671 | if (!Object.keys(replacements).includes(tag)) return
672 |
673 | const propName = attr.directive
674 | ? hyphenate(attr.key.argument.rawName)
675 | : hyphenate(attr.key.rawName)
676 |
677 | const propNameNode = attr.directive
678 | ? attr.key.argument
679 | : attr.key
680 |
681 | Object.entries(replacements[tag]).forEach(([test, replace]) => {
682 | if (hyphenate(test) === propName) {
683 | if (typeof replace === 'function') {
684 | replace = replace(attr)
685 | }
686 |
687 | if (replace === false) {
688 | context.report({
689 | messageId: 'removed',
690 | data: { name: propName },
691 | node: propNameNode,
692 | })
693 | } else if (typeof replace === 'string') {
694 | context.report({
695 | messageId: 'replacedWith',
696 | data: {
697 | a: propName,
698 | b: replace,
699 | },
700 | node: propNameNode,
701 | fix (fixer) {
702 | return fixer.replaceText(propNameNode, replace)
703 | },
704 | })
705 | } else if (typeof replace === 'object' && 'name' in replace && 'value' in replace) {
706 | const oldValue = attr.directive ? context.sourceCode.getText(attr.value.expression) : attr.value?.value
707 | const value = typeof replace.value === 'function'
708 | ? replace.value(oldValue)
709 | : replace.value
710 | if (value == null || value === oldValue) return
711 | context.report({
712 | messageId: 'replacedWith',
713 | data: {
714 | a: propName,
715 | b: `${replace.name}="${value}"`,
716 | },
717 | node: propNameNode,
718 | fix (fixer) {
719 | if (attr.directive && replace.bind !== false) {
720 | if (replace.bind) {
721 | if (value === 'true' || value === '!(false)') {
722 | return fixer.replaceText(attr, replace.name)
723 | }
724 | return [fixer.replaceText(propNameNode, replace.name), fixer.replaceText(attr.value, `"${value}"`)]
725 | } else {
726 | const expression = context.sourceCode.getText(attr.value.expression)
727 | return [fixer.replaceText(propNameNode, replace.name), fixer.replaceText(attr.value, `"${expression} ? '${value}' : undefined"`)]
728 | }
729 | } else {
730 | return fixer.replaceText(attr, `${replace.bind ? ':' : ''}${replace.name}="${value}"`)
731 | }
732 | },
733 | })
734 | } else if (typeof replace === 'object' && 'custom' in replace) {
735 | context.report({
736 | messageId: 'replacedWith',
737 | data: {
738 | a: propName,
739 | b: replace.custom,
740 | },
741 | node: propNameNode,
742 | })
743 | }
744 | }
745 | })
746 | },
747 | })
748 | },
749 | }
750 |
--------------------------------------------------------------------------------
/src/rules/no-deprecated-slots.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { classify, getAttributes, isVueTemplate } = require('../util/helpers')
4 |
5 | const groups = [
6 | {
7 | components: ['VDialog', 'VMenu', 'VTooltip'],
8 | slots: ['activator'],
9 | handler (context, node, directive, param) {
10 | if (param.type === 'Identifier') {
11 | // #activator="data"
12 | const boundVariables = {}
13 | node.variables.find(variable => variable.id.name === param.name)?.references.forEach(ref => {
14 | if (ref.id.parent.type !== 'MemberExpression') return
15 | if (
16 | // v-bind="data.props"
17 | ref.id.parent.property.name === 'props' &&
18 | ref.id.parent.parent.parent.directive &&
19 | ref.id.parent.parent.parent.key.name.name === 'bind' &&
20 | !ref.id.parent.parent.parent.key.argument
21 | ) return
22 | if (ref.id.parent.property.name === 'on') {
23 | boundVariables.on = ref.id
24 | } else if (ref.id.parent.property.name === 'attrs') {
25 | boundVariables.attrs = ref.id
26 | }
27 | })
28 | if (boundVariables.on) {
29 | const ref = boundVariables.on
30 | context.report({
31 | node: ref,
32 | messageId: 'changedProps',
33 | data: {
34 | component: node.parent.name,
35 | slot: directive.key.argument.name,
36 | },
37 | fix (fixer) {
38 | return fixer.replaceText(ref.parent.parent.parent, `v-bind="${param.name}.props"`)
39 | },
40 | })
41 | }
42 | if (boundVariables.attrs) {
43 | const ref = boundVariables.attrs
44 | if (!boundVariables.on) {
45 | context.report({
46 | node: boundVariables.attrs,
47 | messageId: 'invalidProps',
48 | })
49 | } else {
50 | context.report({
51 | node: ref,
52 | messageId: 'changedProps',
53 | data: {
54 | component: node.parent.name,
55 | slot: directive.key.argument.name,
56 | },
57 | fix (fixer) {
58 | return fixer.removeRange([ref.parent.parent.parent.range[0] - 1, ref.parent.parent.parent.range[1]])
59 | },
60 | })
61 | }
62 | }
63 | } else if (param.type === 'ObjectPattern') {
64 | // #activator="{ on, attrs }"
65 | const boundVariables = {}
66 | param.properties.forEach(prop => {
67 | node.variables.find(variable => variable.id.name === prop.value.name)?.references.forEach(ref => {
68 | if (prop.key.name === 'on') {
69 | boundVariables.on = { prop, id: ref.id }
70 | } else if (prop.key.name === 'attrs') {
71 | boundVariables.attrs = { prop, id: ref.id }
72 | }
73 | })
74 | })
75 | if (boundVariables.on || boundVariables.attrs) {
76 | if (boundVariables.attrs && !boundVariables.on) {
77 | context.report({
78 | node: boundVariables.attrs.prop.key,
79 | messageId: 'invalidProps',
80 | })
81 | } else {
82 | context.report({
83 | node: param,
84 | messageId: 'changedProps',
85 | data: {
86 | component: node.parent.name,
87 | slot: directive.key.argument.name,
88 | },
89 | * fix (fixer) {
90 | if (boundVariables.on) {
91 | const ref = boundVariables.on
92 | yield fixer.replaceText(ref.prop, 'props')
93 | yield fixer.replaceText(ref.id.parent.parent, `v-bind="props"`)
94 | }
95 | if (boundVariables.attrs) {
96 | const template = context.sourceCode.parserServices.getTemplateBodyTokenStore()
97 | const ref = boundVariables.attrs
98 | const isLast = ref.prop === param.properties.at(-1)
99 | if (isLast) {
100 | const comma = template.getTokenBefore(ref.prop, { filter: token => token.value === ',' })
101 | if (comma) {
102 | yield fixer.removeRange([comma.range[0], ref.prop.range[1]])
103 | } else {
104 | yield fixer.removeRange([ref.prop.range[0] - 1, ref.prop.range[1]])
105 | }
106 | } else {
107 | const comma = template.getTokenAfter(ref.prop, { filter: token => token.value === ',' })
108 | if (comma) {
109 | yield fixer.removeRange([ref.prop.range[0] - 1, comma.range[1]])
110 | } else {
111 | yield fixer.removeRange([ref.prop.range[0] - 1, ref.prop.range[1]])
112 | }
113 | }
114 | yield fixer.removeRange([ref.id.parent.parent.range[0] - 1, ref.id.parent.parent.range[1]])
115 | }
116 | },
117 | })
118 | }
119 | }
120 | } else {
121 | context.report({
122 | node: directive,
123 | messageId: 'invalidProps',
124 | })
125 | }
126 | },
127 | },
128 | {
129 | components: ['VSelect', 'VAutocomplete', 'VCombobox'],
130 | slots: ['selection'],
131 | handler (context, node, directive, param) {
132 | if (!getAttributes(node.parent).some(attr => ['chips', 'closable-chips'].includes(attr.name))) return
133 |
134 | context.report({
135 | node: directive,
136 | messageId: 'renamed',
137 | data: {
138 | component: node.parent.name,
139 | slot: directive.key.argument.name,
140 | newSlot: 'chip',
141 | },
142 | fix (fixer) {
143 | return fixer.replaceText(directive.key.argument, 'chip')
144 | },
145 | })
146 | },
147 | },
148 | {
149 | components: ['VSnackbar'],
150 | slots: ['action'],
151 | handler (context, node, directive, param) {
152 | context.report({
153 | node: directive,
154 | messageId: 'renamed',
155 | data: {
156 | component: node.parent.name,
157 | slot: directive.key.argument.name,
158 | newSlot: 'actions',
159 | },
160 | fix (fixer) {
161 | return fixer.replaceText(directive.key.argument, 'action')
162 | },
163 | })
164 | },
165 | },
166 | ]
167 |
168 | // ------------------------------------------------------------------------------
169 | // Rule Definition
170 | // ------------------------------------------------------------------------------
171 |
172 | module.exports = {
173 | meta: {
174 | docs: {
175 | description: 'Prevent the use of removed and deprecated slots.',
176 | category: 'recommended',
177 | },
178 | fixable: 'code',
179 | schema: [],
180 | messages: {
181 | renamed: `{{ component }}'s '{{ slot }}' slot has been renamed to '{{ newSlot }}'`,
182 | changedProps: `{{ component }}'s '{{ slot }}' slot has changed props`,
183 | invalidProps: `Slot has invalid props`,
184 | },
185 | },
186 |
187 | create (context) {
188 | if (!isVueTemplate(context)) return {}
189 |
190 | let scopeStack
191 |
192 | return context.sourceCode.parserServices.defineTemplateBodyVisitor({
193 | VElement (node) {
194 | scopeStack = {
195 | parent: scopeStack,
196 | nodes: scopeStack ? [...scopeStack.nodes] : [],
197 | }
198 | for (const variable of node.variables) {
199 | scopeStack.nodes.push(variable.id)
200 | }
201 |
202 | if (node.name !== 'template' || node.parent.type !== 'VElement') return
203 |
204 | for (const group of groups) {
205 | if (
206 | !group.components.includes(classify(node.parent.name)) &&
207 | !group.components.includes(node.parent.name)
208 | ) continue
209 | const directive = node.startTag.attributes.find(attr => {
210 | return (
211 | attr.directive &&
212 | attr.key.name.name === 'slot' &&
213 | group.slots.includes(attr.key.argument?.name)
214 | )
215 | })
216 | if (
217 | !directive ||
218 | !directive.value ||
219 | directive.value.type !== 'VExpressionContainer' ||
220 | !directive.value.expression ||
221 | directive.value.expression.params.length !== 1
222 | ) continue
223 | const param = directive.value.expression.params[0]
224 | group.handler(context, node, directive, param)
225 | }
226 | },
227 | 'VElement:exit' () {
228 | scopeStack = scopeStack && scopeStack.parent
229 | },
230 | })
231 | },
232 | }
233 |
--------------------------------------------------------------------------------
/src/util/fixers.js:
--------------------------------------------------------------------------------
1 | function addClass (context, fixer, element, className) {
2 | const classNode = element.startTag.attributes.find(attr => attr.key.name === 'class')
3 |
4 | if (classNode && classNode.value) {
5 | // class=""
6 | return fixer.replaceText(classNode.value, `"${classNode.value.value} ${className}"`)
7 | } else if (classNode) {
8 | // class
9 | return fixer.insertTextAfter(classNode, `="${className}"`)
10 | } else {
11 | // nothing
12 | return fixer.insertTextAfter(
13 | context.sourceCode.parserServices.getTemplateBodyTokenStore().getFirstToken(element.startTag),
14 | ` class="${className}"`
15 | )
16 | }
17 | }
18 |
19 | function removeAttr (context, fixer, node) {
20 | const source = context.sourceCode.text
21 | let [start, end] = node.range
22 | // Remove extra whitespace before attributes
23 | start -= /\s*$/g.exec(source.substring(0, start))[0].length
24 | return fixer.removeRange([start, end])
25 | }
26 |
27 | module.exports = {
28 | addClass,
29 | removeAttr,
30 | }
31 |
--------------------------------------------------------------------------------
/src/util/get-installed-vuetify-version.js:
--------------------------------------------------------------------------------
1 | const { createRequire } = require('module')
2 | const path = require('path')
3 |
4 | function getInstalledVuetifyVersion () {
5 | try {
6 | const installedVuetify = createRequire(path.resolve(process.cwd(), 'package.json'))('vuetify/package.json')
7 | return installedVuetify.version
8 | } catch (e) {}
9 | }
10 |
11 | module.exports = {
12 | getInstalledVuetifyVersion,
13 | }
14 |
--------------------------------------------------------------------------------
/src/util/grid-attributes.js:
--------------------------------------------------------------------------------
1 | const alignmentClasses = [
2 | /^align-(content-)?(start|baseline|center|end|space-around|space-between)$/,
3 | /^justify-(start|center|end|space-around|space-between)$/,
4 | /^justify-between$/, // No idea where this was from or if it's a typo, but it's in the docs
5 | ]
6 |
7 | // These attributes have alternative props, so shouldn't be turned into classes by the fixer
8 | const noFix = {
9 | VContainer: [...alignmentClasses, /^grid-list-(xs|sm|md|lg|xl)$/],
10 | VRow: [...alignmentClasses, 'row', 'column', 'reverse', 'wrap'],
11 | VCol: [
12 | /^align-self-(start|baseline|center|end)$/,
13 | /^offset-(xs|sm|md|lg|xl)\d{1,2}$/,
14 | /^order-(xs|sm|md|lg|xl)\d{1,2}$/,
15 | /^(xs|sm|md|lg|xl)\d{1,2}$/,
16 | ],
17 | }
18 |
19 | function isGridAttribute (tag, name) {
20 | return noFix[tag] && noFix[tag].some(match => {
21 | return match instanceof RegExp ? match.test(name) : name === match
22 | })
23 | }
24 |
25 | module.exports = {
26 | isGridAttribute,
27 | }
28 |
--------------------------------------------------------------------------------
/src/util/helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('node:path')
4 |
5 | function hyphenate (
6 | /* istanbul ignore next */
7 | str = ''
8 | ) {
9 | return str.replace(/\B([A-Z])/g, '-$1').toLowerCase()
10 | }
11 |
12 | function classify (str) {
13 | return str
14 | .replace(/(?:^|[-_])(\w)/g, c => c.toUpperCase())
15 | .replace(/[-_]/g, '')
16 | }
17 |
18 | const specialAttrs = [
19 | 'style', 'class', 'id',
20 | 'contenteditable', 'draggable', 'spellcheck',
21 | 'key', 'ref', 'slot', 'is', 'slot-scope',
22 | ]
23 |
24 | function isBuiltinAttribute (name) {
25 | return specialAttrs.includes(name) ||
26 | name.startsWith('data-') ||
27 | name.startsWith('aria-')
28 | }
29 |
30 | function getAttributes (element) {
31 | const attrs = []
32 |
33 | element.startTag.attributes.forEach(node => {
34 | if (node.directive && (node.key.name.name !== 'bind' || !node.key.argument)) return
35 |
36 | const name = hyphenate(node.directive ? node.key.argument.name : node.key.rawName)
37 | if (!isBuiltinAttribute(name)) attrs.push({ name, node })
38 | })
39 |
40 | return attrs
41 | }
42 |
43 | function isObject (obj) {
44 | return obj !== null && typeof obj === 'object'
45 | }
46 |
47 | function mergeDeep (source, target) {
48 | for (const key in target) {
49 | const sourceProperty = source[key]
50 | const targetProperty = target[key]
51 |
52 | // Only continue deep merging if
53 | // both properties are objects
54 | if (
55 | isObject(sourceProperty) &&
56 | isObject(targetProperty)
57 | ) {
58 | source[key] = mergeDeep(sourceProperty, targetProperty)
59 |
60 | continue
61 | }
62 |
63 | source[key] = targetProperty
64 | }
65 |
66 | return source
67 | }
68 |
69 | function isVueTemplate (context) {
70 | if (context.sourceCode.parserServices.defineTemplateBodyVisitor == null) {
71 | return path.extname(context.getFilename()) === '.vue'
72 | }
73 | return true
74 | }
75 |
76 | module.exports = {
77 | hyphenate,
78 | classify,
79 | isBuiltinAttribute,
80 | getAttributes,
81 | isObject,
82 | mergeDeep,
83 | isVueTemplate,
84 | }
85 |
--------------------------------------------------------------------------------
/tests/rules/grid-unknown-attributes.js:
--------------------------------------------------------------------------------
1 | const { tester } = require('../setup')
2 | const rule = require('../../src/rules/grid-unknown-attributes')
3 |
4 | tester.run('grid-unknown-attributes', rule, {
5 | valid: [
6 | '',
7 | '',
8 | '',
9 | '',
10 | '',
11 | // https://github.com/vuetifyjs/eslint-plugin-vuetify/issues/19
12 | '',
13 | ],
14 | invalid: [
15 | {
16 | code: '',
17 | output: '',
18 | errors: ['Attributes are no longer converted into classes'],
19 | },
20 | {
21 | code: '',
22 | output: '',
23 | errors: ['Attributes are no longer converted into classes'],
24 | },
25 | {
26 | code: '',
27 | output: '',
28 | errors: ['Attributes are no longer converted into classes'],
29 | },
30 | {
31 | code: '',
32 | output: '',
33 | errors: ['Attributes are no longer converted into classes'],
34 | },
35 | {
36 | code: '',
37 | output: null,
38 | errors: ['Attributes are no longer converted into classes'],
39 | },
40 | // https://github.com/vuetifyjs/eslint-plugin-vuetify/issues/19
41 | {
42 | code: '',
43 | output: '',
44 | errors: ['Attributes are no longer converted into classes'],
45 | },
46 | ],
47 | })
48 |
--------------------------------------------------------------------------------
/tests/rules/icon-button-variant.js:
--------------------------------------------------------------------------------
1 | const { tester } = require('../setup')
2 | const rule = require('../../src/rules/icon-button-variant')
3 |
4 | tester.run('icon-button-variant', rule, {
5 | valid: [
6 | '',
7 | '',
8 | '',
9 | '',
10 | ],
11 | invalid: [
12 | {
13 | code: '',
14 | output: '',
15 | errors: [{ messageId: 'needsVariant' }],
16 | },
17 | {
18 | code: '',
19 | output: '',
20 | errors: [{ messageId: 'needsVariant' }],
21 | },
22 | ],
23 | })
24 |
--------------------------------------------------------------------------------
/tests/rules/no-deprecated-classes.js:
--------------------------------------------------------------------------------
1 | const { tester } = require('../setup')
2 | const rule = require('../../src/rules/no-deprecated-classes')
3 |
4 | tester.run('no-deprecated-classes', rule, {
5 | valid: [
6 | '',
7 | '',
8 | '',
9 | '',
10 | '',
11 | '',
12 | '',
13 | '',
14 | '',
15 | '',
16 | // https://github.com/vuetifyjs/eslint-plugin-vuetify/issues/2
17 | '',
18 | ],
19 | invalid: [
20 | {
21 | code: '',
22 | output: '',
23 | errors: [{ messageId: 'replacedWith' }],
24 | },
25 | {
26 | code: '',
27 | output: '',
28 | errors: [{ messageId: 'replacedWith' }],
29 | },
30 | {
31 | code: '',
32 | output: '',
33 | errors: [{ messageId: 'replacedWith' }],
34 | },
35 | {
36 | code: '',
37 | output: '',
38 | errors: [{ messageId: 'replacedWith' }],
39 | },
40 | {
41 | code: '',
42 | output: '',
43 | errors: [{ messageId: 'replacedWith' }],
44 | },
45 | {
46 | code: '',
47 | output: '',
48 | errors: [{ messageId: 'replacedWith' }],
49 | },
50 | {
51 | code: '',
52 | output: '',
53 | errors: [{ messageId: 'replacedWith' }],
54 | },
55 | {
56 | code: '',
57 | output: null,
58 | errors: [{ messageId: 'removed' }],
59 | },
60 | ],
61 | })
62 |
--------------------------------------------------------------------------------
/tests/rules/no-deprecated-colors.js:
--------------------------------------------------------------------------------
1 | const { tester } = require('../setup')
2 | const rule = require('../../src/rules/no-deprecated-colors')
3 |
4 | tester.run('no-deprecated-colors', rule, {
5 | valid: [
6 | '',
7 | '',
8 | '',
9 | '',
10 | '',
11 | ],
12 | invalid: [
13 | {
14 | code: '',
15 | output: '',
16 | errors: [{ messageId: 'replacedWith' }],
17 | },
18 | {
19 | code: '',
20 | output: '',
21 | errors: [{ messageId: 'replacedWith' }],
22 | },
23 | {
24 | code: '',
25 | output: '',
26 | errors: [{ messageId: 'replacedWith' }],
27 | },
28 | {
29 | code: '',
30 | output: '',
31 | errors: [{ messageId: 'replacedWith' }],
32 | },
33 | {
34 | code: '',
35 | output: '',
36 | errors: [{ messageId: 'replacedWith' }],
37 | },
38 | {
39 | code: '',
40 | output: null,
41 | errors: [{ messageId: 'removed' }],
42 | },
43 | {
44 | code: '',
45 | output: null,
46 | errors: [{ messageId: 'removed' }],
47 | },
48 | {
49 | code: '',
50 | output: '',
51 | errors: [{ messageId: 'replacedWith' }],
52 | options: [{ themeColors: ['asdfg'] }],
53 | },
54 | {
55 | code: '',
56 | output: '',
57 | errors: [{ messageId: 'replacedWith' }],
58 | options: [{ themeColors: ['asdfg'] }],
59 | },
60 | ],
61 | })
62 |
--------------------------------------------------------------------------------
/tests/rules/no-deprecated-components.js:
--------------------------------------------------------------------------------
1 | const { tester } = require('../setup')
2 | const rule = require('../../src/rules/no-deprecated-components')
3 |
4 | tester.run('no-deprecated-components', rule, {
5 | valid: [
6 | '',
7 | ],
8 | invalid: [
9 | {
10 | code: '',
11 | output: '',
12 | errors: [{ messageId: 'replacedWith' }],
13 | },
14 | {
15 | code: '',
16 | errors: [{ messageId: 'removed' }],
17 | },
18 | {
19 | code: '',
20 | output: null,
21 | errors: [{ messageId: 'replacedWithCustom' }],
22 | },
23 | {
24 | code: '',
25 | output: '',
26 | errors: [{ messageId: 'removed' }],
27 | },
28 | {
29 | code: '',
30 | output: '',
31 | errors: [{ messageId: 'removed' }],
32 | },
33 | {
34 | code: '',
35 | errors: [{ messageId: 'removed' }],
36 | },
37 | ],
38 | })
39 |
--------------------------------------------------------------------------------
/tests/rules/no-deprecated-events.js:
--------------------------------------------------------------------------------
1 | const { tester } = require('../setup')
2 | const rule = require('../../src/rules/no-deprecated-events')
3 |
4 | tester.run('no-deprecated-events', rule, {
5 | valid: [
6 | '',
7 | '',
8 | '',
9 | '',
10 | '',
11 | '',
12 | ],
13 |
14 | invalid: [
15 | {
16 | code: '',
17 | errors: [{ messageId: 'removed' }],
18 | },
19 | {
20 | code: '',
21 | output: '',
22 | errors: [{ messageId: 'replacedWith' }],
23 | },
24 | {
25 | code: '',
26 | output: '',
27 | errors: [{ messageId: 'replacedWith' }],
28 | },
29 | {
30 | code: '',
31 | output: '',
32 | errors: [{ messageId: 'replacedWith' }],
33 | },
34 | ],
35 | })
36 |
--------------------------------------------------------------------------------
/tests/rules/no-deprecated-imports.js:
--------------------------------------------------------------------------------
1 | const { tester } = require('../setup')
2 | const rule = require('../../src/rules/no-deprecated-imports')
3 |
4 | tester.run('no-deprecated-imports', rule, {
5 | valid: [
6 | 'import colors from "vuetify/util/colors"',
7 | `import colors from 'vuetify/util/colors'`,
8 | ],
9 | invalid: [
10 | {
11 | code: `import colors from 'vuetify/lib/util/colors'`,
12 | output: `import colors from 'vuetify/util/colors'`,
13 | errors: [{ message: 'Import from "vuetify/lib/util/colors" is deprecated. Use "vuetify/util/colors" instead.' }],
14 | },
15 | {
16 | code: `import colors from "vuetify/lib/util/colors"`,
17 | output: `import colors from "vuetify/util/colors"`,
18 | errors: [{ message: 'Import from "vuetify/lib/util/colors" is deprecated. Use "vuetify/util/colors" instead.' }],
19 | },
20 | ],
21 | })
22 |
--------------------------------------------------------------------------------
/tests/rules/no-deprecated-props.js:
--------------------------------------------------------------------------------
1 | const { tester } = require('../setup')
2 | const rule = require('../../src/rules/no-deprecated-props')
3 |
4 | tester.run('no-deprecated-props', rule, {
5 | valid: [
6 | // https://github.com/vuetifyjs/eslint-plugin-vuetify/issues/35
7 | '',
8 | '',
9 | '',
10 | '',
11 | ],
12 |
13 | invalid: [
14 | {
15 | code: '',
16 | output: '',
17 | errors: [{ messageId: 'replacedWith' }],
18 | },
19 | {
20 | code: '',
21 | output: ``,
22 | errors: [{ messageId: 'replacedWith' }],
23 | },
24 | {
25 | code: '',
26 | output: ``,
27 | errors: [{ messageId: 'replacedWith' }],
28 | },
29 | {
30 | code: '',
31 | output: '',
32 | errors: [{ messageId: 'replacedWith' }],
33 | },
34 | {
35 | code: '',
36 | output: '',
37 | errors: [{ messageId: 'replacedWith' }],
38 | },
39 | {
40 | code: '',
41 | output: '',
42 | errors: [{ messageId: 'replacedWith' }],
43 | },
44 | {
45 | code: '',
46 | output: '',
47 | errors: [{ messageId: 'replacedWith' }],
48 | },
49 | {
50 | code: '',
51 | output: '',
52 | errors: [{ messageId: 'replacedWith' }],
53 | },
54 | {
55 | code: '',
56 | output: '',
57 | errors: [{ messageId: 'replacedWith' }],
58 | },
59 | {
60 | code: '',
61 | output: '',
62 | errors: [{ messageId: 'replacedWith' }],
63 | },
64 | {
65 | code: '',
66 | output: '',
67 | errors: [{ messageId: 'replacedWith' }],
68 | },
69 | {
70 | code: '',
71 | output: ``,
72 | errors: [{ messageId: 'replacedWith' }],
73 | },
74 | {
75 | code: '',
76 | output: '',
77 | errors: [{ messageId: 'replacedWith' }, { messageId: 'replacedWith' }],
78 | },
79 | {
80 | code: '',
81 | output: '',
82 | errors: [{ messageId: 'replacedWith' }],
83 | },
84 | {
85 | code: '',
86 | output: '',
87 | errors: [{ messageId: 'replacedWith' }],
88 | },
89 | {
90 | code: '',
91 | output: '',
92 | errors: [{ messageId: 'replacedWith' }],
93 | },
94 | {
95 | code: '',
96 | output: '',
97 | errors: [{ messageId: 'replacedWith' }],
98 | },
99 | {
100 | code: '',
101 | output: '',
102 | errors: [{ messageId: 'replacedWith' }],
103 | },
104 | {
105 | code: '',
106 | output: '',
107 | errors: [{ messageId: 'replacedWith' }],
108 | },
109 | {
110 | code: '',
111 | output: '',
112 | errors: [{ messageId: 'combined' }],
113 | },
114 | ],
115 | })
116 |
--------------------------------------------------------------------------------
/tests/rules/no-deprecated-slots.js:
--------------------------------------------------------------------------------
1 | const { tester } = require('../setup')
2 | const rule = require('../../src/rules/no-deprecated-slots')
3 |
4 | tester.run('no-deprecated-slots', rule, {
5 | valid: [
6 | `
7 |
8 |
9 |
10 |
11 |
12 | `,
13 | `
14 |
15 |
16 |
17 |
18 |
19 | `,
20 | ],
21 |
22 | invalid: [
23 | {
24 | code:
25 | `
26 |
27 |
28 |
29 |
30 |
31 | `,
32 | output:
33 | `
34 |
35 |
36 |
37 |
38 |
39 | `,
40 | errors: [{ messageId: 'changedProps' }],
41 | },
42 | {
43 | code:
44 | `
45 |
46 |
47 |
48 |
49 |
50 | `,
51 | output:
52 | `
53 |
54 |
55 |
56 |
57 |
58 | `,
59 | errors: [{ messageId: 'changedProps' }],
60 | },
61 | {
62 | code:
63 | `
64 |
65 |
66 |
67 |
68 |
69 | `,
70 | output:
71 | `
72 |
73 |
74 |
75 |
76 |
77 | `,
78 | errors: [{ messageId: 'changedProps' }],
79 | },
80 | {
81 | code:
82 | `
83 |
84 |
85 |
86 |
87 |
88 | `,
89 | output: null,
90 | errors: [{ messageId: 'invalidProps' }],
91 | },
92 | {
93 | code:
94 | `
95 |
96 |
97 |
98 |
99 |
100 | `,
101 | output:
102 | `
103 |
104 |
105 |
106 |
107 |
108 | `,
109 | errors: [{ messageId: 'changedProps' }, { messageId: 'changedProps' }],
110 | },
111 | {
112 | code:
113 | `
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | `,
122 | output:
123 | `
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | `,
132 | errors: [{ messageId: 'changedProps' }],
133 | },
134 | // TODO: handle variable shadowing
135 | // {
136 | // code:
137 | // `
138 | //
139 | //
140 | //
141 | //
142 | //
143 | //
144 | //
145 | // `,
146 | // output:
147 | // `
148 | //
149 | //
150 | //
151 | //
152 | //
153 | //
154 | //
155 | // `,
156 | // errors: [{ messageId: 'changedProps' }],
157 | // },
158 | // {
159 | // output:
160 | // `
161 | //
162 | //
163 | //
164 | //
165 | //
166 | //
167 | //
168 | //
169 | //
170 | // `,
171 | // code:
172 | // `
173 | //
174 | //
175 | //
176 | //
177 | //
178 | //
179 | //
180 | //
181 | //
182 | // `,
183 | // errors: [{ messageId: 'changedProps' }],
184 | // },
185 | ],
186 | })
187 |
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
1 | const { RuleTester } = require(process.env.ESLINT8 ? 'eslint8' : 'eslint')
2 |
3 | const tester = new RuleTester(
4 | process.env.ESLINT8
5 | ? {
6 | parser: require.resolve('vue-eslint-parser'),
7 | parserOptions: { ecmaVersion: 2015, sourceType: 'module' },
8 | }
9 | : {
10 | languageOptions: {
11 | parser: require('vue-eslint-parser'),
12 | ecmaVersion: 2015,
13 | },
14 | }
15 | )
16 |
17 | module.exports = { tester }
18 |
--------------------------------------------------------------------------------