├── .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 | Become a Patron 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 | 14 | 15 | ``` 16 | 17 | Examples of **correct** code for this rule: 18 | 19 | ```html 20 | 21 | 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 | 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, ` { 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 | ``, 13 | ``, 20 | ], 21 | 22 | invalid: [ 23 | { 24 | code: 25 | ``, 32 | output: 33 | ``, 40 | errors: [{ messageId: 'changedProps' }], 41 | }, 42 | { 43 | code: 44 | ``, 51 | output: 52 | ``, 59 | errors: [{ messageId: 'changedProps' }], 60 | }, 61 | { 62 | code: 63 | ``, 70 | output: 71 | ``, 78 | errors: [{ messageId: 'changedProps' }], 79 | }, 80 | { 81 | code: 82 | ``, 89 | output: null, 90 | errors: [{ messageId: 'invalidProps' }], 91 | }, 92 | { 93 | code: 94 | ``, 101 | output: 102 | ``, 109 | errors: [{ messageId: 'changedProps' }, { messageId: 'changedProps' }], 110 | }, 111 | { 112 | code: 113 | ``, 122 | output: 123 | ``, 132 | errors: [{ messageId: 'changedProps' }], 133 | }, 134 | // TODO: handle variable shadowing 135 | // { 136 | // code: 137 | // ``, 146 | // output: 147 | // ``, 156 | // errors: [{ messageId: 'changedProps' }], 157 | // }, 158 | // { 159 | // output: 160 | // ``, 171 | // code: 172 | // ``, 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 | --------------------------------------------------------------------------------