eslint-plugin-vuetify 2.5.3 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,22 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://cdn.vuetifyjs.com/docs/images/one/logos/veslplugin-logo-dark.png">
4
+ <img alt="Vuetify ESLint Plugin Logo" src="https://cdn.vuetifyjs.com/docs/images/one/logos/veslplugin-logo-light.png" height="100">
5
+ </picture>
6
+ </div>
7
+
8
+ <p align="center">
9
+ <a href="https://www.npmjs.com/package/eslint-plugin-vuetify"><img src="https://img.shields.io/npm/v/eslint-plugin-vuetify.svg" alt="npm version"></a>
10
+ <a href="https://npm.chart.dev/eslint-plugin-vuetify"><img src="https://img.shields.io/npm/dm/eslint-plugin-vuetify?color=blue" alt="npm downloads"></a>
11
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
12
+ <a href="https://community.vuetifyjs.com"><img src="https://discordapp.com/api/guilds/340160225338195969/widget.png" alt="Discord"></a>
13
+ </p>
14
+
1
15
  # eslint-plugin-vuetify
2
16
 
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.
17
+ This package helps migrate between Vuetify major versions. It includes rules for **v2 v3** and **v3 v4** migrations.
18
+
19
+ Use [eslint-plugin-vuetify@vuetify-2](https://www.npmjs.com/package/eslint-plugin-vuetify/v/vuetify-2) for v1 to v2.
4
20
 
5
21
  <br>
6
22
 
@@ -13,6 +29,56 @@ This package is for migrating from Vuetify v2 to v3, use [eslint-plugin-vuetify@
13
29
  </a>
14
30
  </p>
15
31
 
32
+ ## Vuetify 4 Migration
33
+
34
+ > [!IMPORTANT]
35
+ > We suggest running [vuetify-codemods](https://www.npmjs.com/package/vuetify-codemods) first to automatically apply most migration fixes
36
+
37
+ This plugin includes four new rules for migrating from Vuetify v3 to v4:
38
+
39
+ - **`no-deprecated-typography`** — replaces MD2 typography classes (`text-h1`) with MD3 equivalents (`text-display-large`)
40
+ - **`no-legacy-grid-props`** — converts removed `VRow`/`VCol` props (`align`, `justify`, `dense`) to utility classes or renamed props
41
+ - **`no-elevation-overflow`** — flags elevation classes and props above the MD3 maximum of 5
42
+ - **`no-deprecated-snackbar`** — fixes renamed `VSnackbarQueue` slots and replaced `VSnackbar` props
43
+
44
+ ### Using the recommended-v4 preset
45
+
46
+ Enable all v4 migration rules at once:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import vue from 'eslint-plugin-vue'
51
+ import vuetify from 'eslint-plugin-vuetify'
52
+
53
+ export default [
54
+ ...vue.configs['flat/base'],
55
+ ...vuetify.configs['flat/recommended-v4'],
56
+ ]
57
+ ```
58
+
59
+ ### Selecting individual rules
60
+
61
+ You can also enable rules selectively instead of using the preset:
62
+
63
+ ```js
64
+ // eslint.config.js
65
+ import vue from 'eslint-plugin-vue'
66
+ import vuetify from 'eslint-plugin-vuetify'
67
+
68
+ export default [
69
+ ...vue.configs['flat/base'],
70
+ ...vuetify.configs['flat/base'],
71
+ {
72
+ rules: {
73
+ 'vuetify/no-deprecated-typography': 'error',
74
+ 'vuetify/no-legacy-grid-props': 'error',
75
+ 'vuetify/no-elevation-overflow': 'error',
76
+ 'vuetify/no-deprecated-snackbar': 'error',
77
+ }
78
+ }
79
+ ]
80
+ ```
81
+
16
82
  ## 💿 Install
17
83
 
18
84
  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.
@@ -34,7 +100,7 @@ export default [
34
100
  ]
35
101
  ```
36
102
 
37
- Eslint 8 can alternatively use the older configuration format:
103
+ ESLint 8 can alternatively use the older configuration format:
38
104
 
39
105
  ```js
40
106
  // .eslintrc.js
@@ -46,7 +112,9 @@ module.exports = {
46
112
  }
47
113
  ```
48
114
 
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.
115
+ This plugin supports ESLint 8, 9, and 10. ESLint 10 only supports the flat config format (use `configs['flat/base']` or `configs['flat/recommended']`).
116
+
117
+ **NOTE** This plugin does not affect _**pug**_ templates due to [a limitation in vue-eslint-parser](https://github.com/vuejs/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
118
 
51
119
 
52
120
  ## Rules
@@ -60,15 +128,36 @@ These rules will help you avoid deprecated components, props, and classes. They
60
128
  - Prevent the use of events that have been removed from Vuetify ([`no-deprecated-events`])
61
129
  - Prevent the use of classes that have been removed from Vuetify ([`no-deprecated-classes`])
62
130
  - Prevent the use of the old theme class syntax ([`no-deprecated-colors`])
131
+ - Prevent the use of slots that have been removed from Vuetify ([`no-deprecated-slots`])
63
132
  - Prevent the use of deprecated import paths ([`no-deprecated-imports`])
133
+
134
+ Additional rule (not included in presets):
135
+
64
136
  - Ensure icon buttons have a variant defined ([`icon-button-variant`])
65
137
 
66
138
  ### Grid system
67
139
 
68
- These rules are designed to help migrate to the new grid system in Vuetify v2. They are included in the `recommended` preset.
140
+ These rules are designed to help migrate to the new grid system in Vuetify v3. They are included in the `recommended` preset.
69
141
 
70
142
  - Warn about unknown attributes not being converted to classes on new grid components ([`grid-unknown-attributes`])
71
143
 
144
+ ### Vuetify 4
145
+
146
+ These rules help migrate from Vuetify v3 to v4. They are included in the `recommended-v4` preset.
147
+
148
+ - Disallow deprecated MD2 typography classes ([`no-deprecated-typography`])
149
+ - Prevent the use of removed grid props ([`no-legacy-grid-props`])
150
+ - Disallow elevation classes above the MD3 maximum ([`no-elevation-overflow`])
151
+ - Disallow deprecated props and slots on snackbar components ([`no-deprecated-snackbar`])
152
+
153
+ ### Custom deprecations
154
+
155
+ User-configurable rules for deprecating components, props, events, and slots - helpful to enforce standardization.
156
+
157
+ - Disallow usage of specified components, with optional replacements ([`custom-deprecated-components`])
158
+ - Disallow usage of specified component props ([`custom-deprecated-props`])
159
+ - Disallow usage of specified component events ([`custom-deprecated-events`])
160
+ - Disallow usage of specified component slots ([`custom-deprecated-slots`])
72
161
 
73
162
  [`grid-unknown-attributes`]: ./docs/rules/grid-unknown-attributes.md
74
163
  [`no-deprecated-components`]: ./docs/rules/no-deprecated-components.md
@@ -76,8 +165,16 @@ These rules are designed to help migrate to the new grid system in Vuetify v2. T
76
165
  [`no-deprecated-events`]: ./docs/rules/no-deprecated-events.md
77
166
  [`no-deprecated-classes`]: ./docs/rules/no-deprecated-classes.md
78
167
  [`no-deprecated-colors`]: ./docs/rules/no-deprecated-colors.md
168
+ [`no-deprecated-slots`]: ./docs/rules/no-deprecated-slots.md
79
169
  [`no-deprecated-imports`]: ./docs/rules/no-deprecated-imports.md
80
170
  [`icon-button-variant`]: ./docs/rules/icon-button-variant.md
171
+ [`custom-deprecated-components`]: ./docs/rules/custom-deprecated-components.md
172
+ [`custom-deprecated-props`]: ./docs/rules/custom-deprecated-props.md
173
+ [`custom-deprecated-events`]: ./docs/rules/custom-deprecated-events.md
174
+ [`custom-deprecated-slots`]: ./docs/rules/custom-deprecated-slots.md
175
+ [`no-deprecated-typography`]: ./docs/rules/no-deprecated-typography.md
176
+ [`no-legacy-grid-props`]: ./docs/rules/no-legacy-grid-props.md
177
+ [`no-elevation-overflow`]: ./docs/rules/no-elevation-overflow.md
81
178
 
82
179
 
83
180
  ## 💪 Supporting Vuetify
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ module.exports = [...require('./base'), {
4
+ plugins: {
5
+ get vuetify() {
6
+ return require('../../index');
7
+ }
8
+ },
9
+ rules: require('../recommended-v4').rules
10
+ }];
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ module.exports = [...require('./base'), {
4
+ plugins: {
5
+ get vuetify() {
6
+ return require('../../index');
7
+ }
8
+ },
9
+ rules: require('../tailwindcss').rules
10
+ }];
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ module.exports = {
4
+ rules: {
5
+ 'vuetify/no-deprecated-snackbar': 'error',
6
+ 'vuetify/no-deprecated-typography': 'error',
7
+ 'vuetify/no-elevation-overflow': 'error',
8
+ 'vuetify/no-legacy-grid-props': 'error'
9
+ }
10
+ };
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ module.exports = {
4
+ rules: {
5
+ 'vuetify/no-elevation-prop': 'error',
6
+ 'vuetify/no-rounded-prop': 'error',
7
+ 'vuetify/no-border-prop': 'error',
8
+ 'vuetify/no-legacy-utilities': 'error'
9
+ }
10
+ };
@@ -0,0 +1,136 @@
1
+ /* eslint-disable */
2
+ /* prettier-ignore */
3
+ import type { Linter } from 'eslint'
4
+
5
+ export interface RuleOptions {
6
+ /**
7
+ * Disallow usage of specified components, with optional replacements
8
+ */
9
+ 'vuetify/custom-deprecated-components'?: Linter.RuleEntry<VuetifyCustomDeprecatedComponents>
10
+ /**
11
+ * Disallow usage of specified component events, with optional replacements
12
+ */
13
+ 'vuetify/custom-deprecated-events'?: Linter.RuleEntry<VuetifyCustomDeprecatedEvents>
14
+ /**
15
+ * Disallow usage of specified component props, with optional replacements
16
+ */
17
+ 'vuetify/custom-deprecated-props'?: Linter.RuleEntry<VuetifyCustomDeprecatedProps>
18
+ /**
19
+ * Disallow usage of specified component slots, with optional replacements
20
+ */
21
+ 'vuetify/custom-deprecated-slots'?: Linter.RuleEntry<VuetifyCustomDeprecatedSlots>
22
+ /**
23
+ * Warn about v1 grid attributes not being auto-converted to classes in v2.
24
+ */
25
+ 'vuetify/grid-unknown-attributes'?: Linter.RuleEntry<[]>
26
+ /**
27
+ * Ensure icon buttons have a variant defined.
28
+ */
29
+ 'vuetify/icon-button-variant'?: Linter.RuleEntry<VuetifyIconButtonVariant>
30
+ /**
31
+ * Disallow the `border` prop; use Tailwind border utilities instead.
32
+ */
33
+ 'vuetify/no-border-prop'?: Linter.RuleEntry<[]>
34
+ /**
35
+ * Disallow the use of classes that have been removed from Vuetify
36
+ */
37
+ 'vuetify/no-deprecated-classes'?: Linter.RuleEntry<[]>
38
+ /**
39
+ * Disallow the use of classes that have been removed from Vuetify
40
+ */
41
+ 'vuetify/no-deprecated-colors'?: Linter.RuleEntry<VuetifyNoDeprecatedColors>
42
+ /**
43
+ * Prevent the use of components that have been removed from Vuetify
44
+ */
45
+ 'vuetify/no-deprecated-components'?: Linter.RuleEntry<[]>
46
+ /**
47
+ * Prevent the use of removed and deprecated events.
48
+ */
49
+ 'vuetify/no-deprecated-events'?: Linter.RuleEntry<[]>
50
+ /**
51
+ * disallow import from "vuetify/lib/util/colors", suggest "vuetify/util/colors" instead
52
+ */
53
+ 'vuetify/no-deprecated-imports'?: Linter.RuleEntry<[]>
54
+ /**
55
+ * Prevent the use of removed and deprecated props.
56
+ */
57
+ 'vuetify/no-deprecated-props'?: Linter.RuleEntry<[]>
58
+ /**
59
+ * Prevent the use of removed and deprecated slots.
60
+ */
61
+ 'vuetify/no-deprecated-slots'?: Linter.RuleEntry<[]>
62
+ /**
63
+ * Disallow deprecated props and slots on Vuetify snackbar components.
64
+ */
65
+ 'vuetify/no-deprecated-snackbar'?: Linter.RuleEntry<[]>
66
+ /**
67
+ * Disallow deprecated MD2 typography classes, with configurable replacements.
68
+ */
69
+ 'vuetify/no-deprecated-typography'?: Linter.RuleEntry<VuetifyNoDeprecatedTypography>
70
+ /**
71
+ * Disallow elevation classes above the MD3 maximum (0–5).
72
+ */
73
+ 'vuetify/no-elevation-overflow'?: Linter.RuleEntry<[]>
74
+ /**
75
+ * Disallow the `elevation` prop; use Tailwind shadow utilities instead.
76
+ */
77
+ 'vuetify/no-elevation-prop'?: Linter.RuleEntry<[]>
78
+ /**
79
+ * Prevent the use of removed grid props.
80
+ */
81
+ 'vuetify/no-legacy-grid-props'?: Linter.RuleEntry<[]>
82
+ /**
83
+ * Disallow Vuetify utility classes; use Tailwind equivalents instead.
84
+ */
85
+ 'vuetify/no-legacy-utilities'?: Linter.RuleEntry<VuetifyNoLegacyUtilities>
86
+ /**
87
+ * Disallow the `rounded` prop; use Tailwind rounded utilities instead.
88
+ */
89
+ 'vuetify/no-rounded-prop'?: Linter.RuleEntry<[]>
90
+ }
91
+
92
+ /* ======= Declarations ======= */
93
+ // ----- vuetify/custom-deprecated-components -----
94
+ type VuetifyCustomDeprecatedComponents = []|[{
95
+ [k: string]: (string | false | {
96
+ message: string
97
+ }) | undefined
98
+ }]
99
+ // ----- vuetify/custom-deprecated-events -----
100
+ type VuetifyCustomDeprecatedEvents = []|[{
101
+ [k: string]: {
102
+ [k: string]: (string | false | {
103
+ message: string
104
+ }) | undefined
105
+ } | undefined
106
+ }]
107
+ // ----- vuetify/custom-deprecated-props -----
108
+ type VuetifyCustomDeprecatedProps = []|[{
109
+ [k: string]: {
110
+ [k: string]: (string | false | {
111
+ message: string
112
+ }) | undefined
113
+ } | undefined
114
+ }]
115
+ // ----- vuetify/custom-deprecated-slots -----
116
+ type VuetifyCustomDeprecatedSlots = []|[{
117
+ [k: string]: {
118
+ [k: string]: (string | false | {
119
+ message: string
120
+ }) | undefined
121
+ } | undefined
122
+ }]
123
+ // ----- vuetify/icon-button-variant -----
124
+ type VuetifyIconButtonVariant = []|[string]
125
+ // ----- vuetify/no-deprecated-colors -----
126
+ type VuetifyNoDeprecatedColors = []|[{
127
+ themeColors?: string[]
128
+ }]
129
+ // ----- vuetify/no-deprecated-typography -----
130
+ type VuetifyNoDeprecatedTypography = []|[{
131
+ [k: string]: (string | false) | undefined
132
+ }]
133
+ // ----- vuetify/no-legacy-utilities -----
134
+ type VuetifyNoLegacyUtilities = []|[{
135
+ [k: string]: (string | false) | undefined
136
+ }]
package/lib/index.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /// <reference path="./eslint-typegen.d.ts" />
2
+ import type { Linter } from 'eslint'
3
+
4
+ declare const vuetify: {
5
+ configs: {
6
+ base: Linter.LegacyConfig
7
+ recommended: Linter.LegacyConfig
8
+ 'recommended-v4': Linter.LegacyConfig
9
+
10
+ 'flat/base': Linter.FlatConfig[]
11
+ 'flat/recommended': Linter.FlatConfig[]
12
+ 'flat/recommended-v4': Linter.FlatConfig[]
13
+
14
+ tailwindcss: Linter.LegacyConfig
15
+ 'flat/tailwindcss': Linter.FlatConfig[]
16
+ }
17
+ rules: Record<string, any>
18
+ }
19
+
20
+ export = vuetify
package/lib/index.js CHANGED
@@ -6,8 +6,12 @@ module.exports = {
6
6
  configs: {
7
7
  base: require('./configs/base'),
8
8
  recommended: require('./configs/recommended'),
9
+ 'recommended-v4': require('./configs/recommended-v4'),
9
10
  'flat/base': require('./configs/flat/base'),
10
- 'flat/recommended': require('./configs/flat/recommended')
11
+ 'flat/recommended': require('./configs/flat/recommended'),
12
+ 'flat/recommended-v4': require('./configs/flat/recommended-v4'),
13
+ tailwindcss: require('./configs/tailwindcss'),
14
+ 'flat/tailwindcss': require('./configs/flat/tailwindcss')
11
15
  },
12
16
  rules: requireindex(path.join(__dirname, './rules'))
13
17
  };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ classify,
5
+ hyphenate,
6
+ isVueTemplate
7
+ } = require('../util/helpers');
8
+ module.exports = {
9
+ meta: {
10
+ docs: {
11
+ description: 'Disallow usage of specified components, with optional replacements'
12
+ },
13
+ fixable: 'code',
14
+ schema: [{
15
+ type: 'object',
16
+ additionalProperties: {
17
+ oneOf: [{
18
+ type: 'string'
19
+ }, {
20
+ type: 'boolean',
21
+ enum: [false]
22
+ }, {
23
+ type: 'object',
24
+ properties: {
25
+ message: {
26
+ type: 'string'
27
+ }
28
+ },
29
+ required: ['message'],
30
+ additionalProperties: false
31
+ }]
32
+ }
33
+ }],
34
+ messages: {
35
+ deprecated: `'{{ name }}' is deprecated`,
36
+ deprecatedWithMessage: `'{{ name }}' is deprecated: {{ message }}`,
37
+ deprecatedWithReplacement: `'{{ name }}' is deprecated, use '<{{ tag }}{{ classAttr }}>' instead`
38
+ }
39
+ },
40
+ create(context) {
41
+ if (!isVueTemplate(context)) return {};
42
+ const options = context.options[0];
43
+ if (!options || !Object.keys(options).length) return {};
44
+
45
+ // Normalize keys to PascalCase for lookup
46
+ const banned = new Map();
47
+ for (const [key, value] of Object.entries(options)) {
48
+ banned.set(classify(key), value);
49
+ }
50
+ return context.sourceCode.parserServices.defineTemplateBodyVisitor({
51
+ VElement(element) {
52
+ const tag = classify(element.rawName);
53
+ if (!banned.has(tag)) return;
54
+ const replacement = banned.get(tag);
55
+ const tokens = context.sourceCode.parserServices.getTemplateBodyTokenStore();
56
+ if (typeof replacement === 'string') {
57
+ // Parse replacement: tag part and optional classes (dot-separated)
58
+ const parts = replacement.split('.');
59
+ const replacementTag = parts[0];
60
+ const classes = parts.slice(1);
61
+ context.report({
62
+ node: element,
63
+ messageId: 'deprecatedWithReplacement',
64
+ data: {
65
+ name: hyphenate(tag),
66
+ tag: replacementTag,
67
+ classAttr: classes.length ? ` class="${classes.join(' ')}"` : ''
68
+ },
69
+ fix(fixer) {
70
+ const fixes = [];
71
+ const open = tokens.getFirstToken(element.startTag);
72
+ fixes.push(fixer.replaceText(open, `<${replacementTag}`));
73
+ if (element.endTag) {
74
+ const endTagOpen = tokens.getFirstToken(element.endTag);
75
+ fixes.push(fixer.replaceText(endTagOpen, `</${replacementTag}`));
76
+ }
77
+ if (classes.length) {
78
+ const classValue = classes.join(' ');
79
+ const classAttr = element.startTag.attributes.find(attr => !attr.directive && attr.key.rawName === 'class');
80
+ if (classAttr && classAttr.value) {
81
+ const existing = classAttr.value.value;
82
+ fixes.push(fixer.replaceText(classAttr.value, `"${existing} ${classValue}"`));
83
+ } else if (!classAttr) {
84
+ fixes.push(fixer.insertTextAfter(open, ` class="${classValue}"`));
85
+ }
86
+ }
87
+ return fixes;
88
+ }
89
+ });
90
+ } else if (typeof replacement === 'object' && replacement !== null) {
91
+ context.report({
92
+ node: element,
93
+ messageId: 'deprecatedWithMessage',
94
+ data: {
95
+ name: hyphenate(tag),
96
+ message: replacement.message
97
+ }
98
+ });
99
+ } else {
100
+ context.report({
101
+ node: element,
102
+ messageId: 'deprecated',
103
+ data: {
104
+ name: hyphenate(tag)
105
+ }
106
+ });
107
+ }
108
+ }
109
+ });
110
+ }
111
+ };
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ hyphenate,
5
+ classify,
6
+ isVueTemplate
7
+ } = require('../util/helpers');
8
+ module.exports = {
9
+ meta: {
10
+ docs: {
11
+ description: 'Disallow usage of specified component events, with optional replacements'
12
+ },
13
+ fixable: 'code',
14
+ schema: [{
15
+ type: 'object',
16
+ additionalProperties: {
17
+ type: 'object',
18
+ additionalProperties: {
19
+ oneOf: [{
20
+ type: 'string'
21
+ }, {
22
+ type: 'boolean',
23
+ enum: [false]
24
+ }, {
25
+ type: 'object',
26
+ properties: {
27
+ message: {
28
+ type: 'string'
29
+ }
30
+ },
31
+ required: ['message'],
32
+ additionalProperties: false
33
+ }]
34
+ }
35
+ }
36
+ }],
37
+ messages: {
38
+ replacedWith: `{{ tag }}: @{{ a }} has been replaced with @{{ b }}`,
39
+ removed: `{{ tag }}: @{{ name }} has been removed`,
40
+ removedWithMessage: `{{ tag }}: @{{ name }} has been removed: {{ message }}`
41
+ }
42
+ },
43
+ create(context) {
44
+ if (!isVueTemplate(context)) return {};
45
+ const options = context.options[0];
46
+ if (!options || !Object.keys(options).length) return {};
47
+
48
+ // Normalize keys to PascalCase
49
+ const replacements = new Map();
50
+ for (const [component, events] of Object.entries(options)) {
51
+ const normalizedEvents = {};
52
+ for (const [event, value] of Object.entries(events)) {
53
+ normalizedEvents[hyphenate(event)] = value;
54
+ }
55
+ replacements.set(classify(component), normalizedEvents);
56
+ }
57
+ return context.sourceCode.parserServices.defineTemplateBodyVisitor({
58
+ VAttribute(attr) {
59
+ if (!(attr.directive && attr.key.name.name === 'on' && attr.key.argument?.type === 'VIdentifier')) return;
60
+ const tag = classify(attr.parent.parent.rawName);
61
+ if (!replacements.has(tag)) return;
62
+ const eventNameNode = attr.key.argument;
63
+ const eventName = hyphenate(eventNameNode.rawName);
64
+ const events = replacements.get(tag);
65
+ const replace = events[eventName];
66
+ if (replace === undefined) return;
67
+ if (replace === false) {
68
+ context.report({
69
+ messageId: 'removed',
70
+ data: {
71
+ tag,
72
+ name: eventName
73
+ },
74
+ node: eventNameNode
75
+ });
76
+ } else if (typeof replace === 'string') {
77
+ context.report({
78
+ messageId: 'replacedWith',
79
+ data: {
80
+ tag,
81
+ a: eventName,
82
+ b: replace
83
+ },
84
+ node: eventNameNode,
85
+ fix(fixer) {
86
+ return fixer.replaceText(eventNameNode, hyphenate(replace));
87
+ }
88
+ });
89
+ } else if (typeof replace === 'object' && replace !== null) {
90
+ context.report({
91
+ messageId: 'removedWithMessage',
92
+ data: {
93
+ tag,
94
+ name: eventName,
95
+ message: replace.message
96
+ },
97
+ node: eventNameNode
98
+ });
99
+ }
100
+ }
101
+ });
102
+ }
103
+ };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ hyphenate,
5
+ classify,
6
+ isVueTemplate
7
+ } = require('../util/helpers');
8
+ module.exports = {
9
+ meta: {
10
+ docs: {
11
+ description: 'Disallow usage of specified component props, with optional replacements'
12
+ },
13
+ fixable: 'code',
14
+ schema: [{
15
+ type: 'object',
16
+ additionalProperties: {
17
+ type: 'object',
18
+ additionalProperties: {
19
+ oneOf: [{
20
+ type: 'string'
21
+ }, {
22
+ type: 'boolean',
23
+ enum: [false]
24
+ }, {
25
+ type: 'object',
26
+ properties: {
27
+ message: {
28
+ type: 'string'
29
+ }
30
+ },
31
+ required: ['message'],
32
+ additionalProperties: false
33
+ }]
34
+ }
35
+ }
36
+ }],
37
+ messages: {
38
+ replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`,
39
+ removed: `'{{ name }}' has been removed`,
40
+ removedWithMessage: `'{{ name }}' has been removed: {{ message }}`
41
+ }
42
+ },
43
+ create(context) {
44
+ if (!isVueTemplate(context)) return {};
45
+ const options = context.options[0];
46
+ if (!options || !Object.keys(options).length) return {};
47
+
48
+ // Normalize keys to PascalCase
49
+ const replacements = new Map();
50
+ for (const [component, props] of Object.entries(options)) {
51
+ const normalizedProps = {};
52
+ for (const [prop, value] of Object.entries(props)) {
53
+ normalizedProps[hyphenate(prop)] = value;
54
+ }
55
+ replacements.set(classify(component), normalizedProps);
56
+ }
57
+ return context.sourceCode.parserServices.defineTemplateBodyVisitor({
58
+ VAttribute(attr) {
59
+ if (attr.directive && (attr.key.name.name !== 'bind' || !attr.key.argument)) return;
60
+ const tag = classify(attr.parent.parent.rawName);
61
+ if (!replacements.has(tag)) return;
62
+ const propName = attr.directive ? hyphenate(attr.key.argument.rawName) : hyphenate(attr.key.rawName);
63
+ const propNameNode = attr.directive ? attr.key.argument : attr.key;
64
+ const props = replacements.get(tag);
65
+ const replace = props[propName];
66
+ if (replace === undefined) return;
67
+ if (replace === false) {
68
+ context.report({
69
+ messageId: 'removed',
70
+ data: {
71
+ name: propName
72
+ },
73
+ node: propNameNode
74
+ });
75
+ } else if (typeof replace === 'string') {
76
+ context.report({
77
+ messageId: 'replacedWith',
78
+ data: {
79
+ a: propName,
80
+ b: replace
81
+ },
82
+ node: propNameNode,
83
+ fix(fixer) {
84
+ return fixer.replaceText(propNameNode, replace);
85
+ }
86
+ });
87
+ } else if (typeof replace === 'object' && replace !== null) {
88
+ context.report({
89
+ messageId: 'removedWithMessage',
90
+ data: {
91
+ name: propName,
92
+ message: replace.message
93
+ },
94
+ node: propNameNode
95
+ });
96
+ }
97
+ }
98
+ });
99
+ }
100
+ };