eslint-plugin-vuetify 2.0.4 → 2.1.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.
@@ -10,6 +10,7 @@ module.exports = {
10
10
  'vuetify/no-deprecated-colors': 'error',
11
11
  'vuetify/no-deprecated-components': 'error',
12
12
  'vuetify/no-deprecated-events': 'error',
13
- 'vuetify/no-deprecated-props': 'error'
13
+ 'vuetify/no-deprecated-props': 'error',
14
+ 'vuetify/no-deprecated-slots': 'error'
14
15
  }
15
16
  };
@@ -17,7 +17,7 @@ const replacements = new Map([[/^rounded-(r|l|tr|tl|br|bl)(-.*)?$/, ([side, rest
17
17
  l: 's'
18
18
  }[side];
19
19
  return `border-${side}${rest}`;
20
- }], [/^text-xs-(left|right|center|justify)$/, ([align]) => `text-${align}`], ['scroll-y', 'overflow-y-auto'], ['hide-overflow', 'overflow-hidden'], ['show-overflow', 'overflow-visible'], ['no-wrap', 'text-no-wrap'], ['ellipsis', 'text-truncate'], ['left', 'float-left'], ['right', 'float-right'], ['display-4', 'text-h1'], ['display-3', 'text-h2'], ['display-2', 'text-h3'], ['display-1', 'text-h4'], ['headline', 'text-h5'], ['title', 'text-h6'], ['subtitle-1', 'text-subtitle-1'], ['subtitle-2', 'text-subtitle-2'], ['body-1', 'text-body-1'], ['body-2', 'text-body-2'], ['caption', 'text-caption'], ['caption', 'text-caption'], ['overline', 'text-overline'], [/^transition-(fast-out-slow-in|linear-out-slow-in|fast-out-linear-in|ease-in-out|fast-in-fast-out|swing)$/, false]]);
20
+ }], [/^text-xs-(left|right|center|justify)$/, ([align]) => `text-${align}`], [/hidden-(xs|sm|md|lg|xl)-only/, ([breakpoint]) => `hidden-${breakpoint}`], ['scroll-y', 'overflow-y-auto'], ['hide-overflow', 'overflow-hidden'], ['show-overflow', 'overflow-visible'], ['no-wrap', 'text-no-wrap'], ['ellipsis', 'text-truncate'], ['left', 'float-left'], ['right', 'float-right'], ['display-4', 'text-h1'], ['display-3', 'text-h2'], ['display-2', 'text-h3'], ['display-1', 'text-h4'], ['headline', 'text-h5'], ['title', 'text-h6'], ['subtitle-1', 'text-subtitle-1'], ['subtitle-2', 'text-subtitle-2'], ['body-1', 'text-body-1'], ['body-2', 'text-body-2'], ['caption', 'text-caption'], ['caption', 'text-caption'], ['overline', 'text-overline'], [/^transition-(fast-out-slow-in|linear-out-slow-in|fast-out-linear-in|ease-in-out|fast-in-fast-out|swing)$/, false]]);
21
21
 
22
22
  // ------------------------------------------------------------------------------
23
23
  // Rule Definition
@@ -14,6 +14,8 @@ const replacements = {
14
14
  VListTileSubTitle: 'v-list-item-subtitle',
15
15
  VJumbotron: false,
16
16
  VToolbarSideIcon: 'v-app-bar-nav-icon',
17
+ VExpansionPanelHeader: 'v-expansion-panel-title',
18
+ VExpansionPanelContent: 'v-expansion-panel-text',
17
19
  // Possible typos
18
20
  VListItemSubTitle: 'v-list-item-subtitle',
19
21
  VListTileSubtitle: 'v-list-item-subtitle',
@@ -24,17 +26,18 @@ const replacements = {
24
26
  VListItemAvatar: false,
25
27
  VListItemContent: false,
26
28
  VListItemIcon: false,
27
- VOtpInput: false,
28
29
  VOverflowBtn: false,
29
30
  VPicker: false,
30
31
  VSimpleCheckbox: 'v-checkbox-btn',
31
32
  VSparkline: false,
32
33
  VSpeedDial: false,
33
- VStepper: false,
34
34
  VSubheader: {
35
35
  custom: 'v-list-subheader or class="text-subheader-2"'
36
36
  },
37
37
  VSimpleTable: 'v-table',
38
+ VTabsSlider: false,
39
+ VTabsItems: false,
40
+ VTabItem: false,
38
41
  VTimePicker: false,
39
42
  VTreeview: false
40
43
  };
@@ -32,6 +32,10 @@ const sizes = {
32
32
  value: 'x-small'
33
33
  }
34
34
  };
35
+ const theme = {
36
+ dark: false,
37
+ light: false
38
+ };
35
39
  const inputs = {
36
40
  appendOuterIcon: 'append-icon',
37
41
  backgroundColor: 'bg-color',
@@ -60,7 +64,6 @@ const inputs = {
60
64
  name: 'variant',
61
65
  value: 'outlined'
62
66
  },
63
- rounded: false,
64
67
  shaped: false,
65
68
  solo: {
66
69
  name: 'variant',
@@ -76,14 +79,14 @@ const inputs = {
76
79
  name: 'validate-on',
77
80
  value: 'blur'
78
81
  },
79
- value: 'model-value'
82
+ value: 'model-value',
83
+ ...theme
80
84
  };
81
85
  const select = {
82
86
  allowOverflow: false,
83
87
  attach: {
84
88
  custom: ':menu-props="{ attach: true }"'
85
89
  },
86
- autoSelectFirst: false,
87
90
  cacheItems: false,
88
91
  deletableChips: 'closable-chips',
89
92
  disableLookup: false,
@@ -101,10 +104,6 @@ const select = {
101
104
  ...inputs,
102
105
  dense: false
103
106
  };
104
- const theme = {
105
- dark: false,
106
- light: false
107
- };
108
107
  const link = {
109
108
  append: false,
110
109
  exactActiveClass: false,
@@ -196,7 +195,23 @@ const replacements = {
196
195
  avatar: false,
197
196
  mode: false,
198
197
  origin: false,
199
- overlap: false
198
+ overlap: false,
199
+ bottom: {
200
+ name: 'location',
201
+ value: 'bottom'
202
+ },
203
+ left: {
204
+ name: 'location',
205
+ value: 'left'
206
+ },
207
+ right: {
208
+ name: 'location',
209
+ value: 'right'
210
+ },
211
+ top: {
212
+ name: 'location',
213
+ value: 'top'
214
+ }
200
215
  },
201
216
  VBanner: {
202
217
  app: false,
@@ -377,7 +392,9 @@ const replacements = {
377
392
  },
378
393
  selected: 'value',
379
394
  textColor: false,
380
- ...link
395
+ ...link,
396
+ ...sizes,
397
+ ...theme
381
398
  },
382
399
  VChipGroup: {
383
400
  activeClass: 'selected-class',
@@ -393,6 +410,39 @@ const replacements = {
393
410
  hideModeSwitch: false,
394
411
  value: 'model-value'
395
412
  },
413
+ VDataTable: {
414
+ serverItemsLength: {
415
+ custom: '<v-data-table-server>'
416
+ },
417
+ itemClass: {
418
+ custom: 'row-props'
419
+ },
420
+ itemStyle: {
421
+ custom: 'row-props'
422
+ },
423
+ sortDesc: {
424
+ custom: 'sort-by'
425
+ },
426
+ groupDesc: {
427
+ custom: 'group-by'
428
+ }
429
+ },
430
+ VDatePicker: {
431
+ activePicker: 'view-mode',
432
+ pickerDate: {
433
+ custom: 'separate month and year props'
434
+ },
435
+ locale: false,
436
+ localeFirstDayOfYear: false,
437
+ firstDayOfWeek: false,
438
+ dayFormat: false,
439
+ weekdayFormat: false,
440
+ monthFormat: false,
441
+ yearFormat: false,
442
+ headerDateFormat: false,
443
+ titleDateFormat: false,
444
+ range: false
445
+ },
396
446
  VExpansionPanels: {
397
447
  accordion: {
398
448
  name: 'variant',
@@ -467,6 +517,7 @@ const replacements = {
467
517
  nudgeTop: {
468
518
  custom: 'offset'
469
519
  },
520
+ nudgeWidth: false,
470
521
  offsetOverflow: false,
471
522
  offsetX: false,
472
523
  offsetY: false,
@@ -641,6 +692,7 @@ const replacements = {
641
692
  VRadioGroup: {
642
693
  activeClass: false,
643
694
  backgroundColor: false,
695
+ row: 'inline',
644
696
  column: false,
645
697
  multiple: false,
646
698
  ...inputs
@@ -658,8 +710,7 @@ const replacements = {
658
710
  height: false,
659
711
  loading: false,
660
712
  inverseLabel: false,
661
- ...inputs,
662
- ...theme
713
+ ...inputs
663
714
  },
664
715
  VRangeSlider: {
665
716
  backgroundColor: false,
@@ -674,8 +725,7 @@ const replacements = {
674
725
  height: false,
675
726
  loading: false,
676
727
  inverseLabel: false,
677
- ...inputs,
678
- ...theme
728
+ ...inputs
679
729
  },
680
730
  VRating: {
681
731
  backgroundColor: false,
@@ -852,6 +902,7 @@ const replacements = {
852
902
  nudgeTop: {
853
903
  custom: 'offset'
854
904
  },
905
+ nudgeWidth: false,
855
906
  positionX: false,
856
907
  positionY: false,
857
908
  right: {
@@ -895,11 +946,35 @@ module.exports = {
895
946
  schema: [],
896
947
  messages: {
897
948
  replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`,
898
- removed: `'{{ name }}' has been removed`
949
+ removed: `'{{ name }}' has been removed`,
950
+ combined: `multiple {{ a }} attributes have been combined`
899
951
  }
900
952
  },
901
953
  create(context) {
902
954
  return context.parserServices.defineTemplateBodyVisitor({
955
+ VStartTag(tag) {
956
+ const attrGroups = {};
957
+ tag.attributes.forEach(attr => {
958
+ if (['location'].includes(attr.key.name)) {
959
+ attrGroups[attr.key.name] = attrGroups[attr.key.name] ?? [];
960
+ attrGroups[attr.key.name].push(attr);
961
+ }
962
+ });
963
+ Object.values(attrGroups).forEach(attrGroup => {
964
+ const [head, ...tail] = attrGroup;
965
+ if (!tail.length) return;
966
+ context.report({
967
+ messageId: 'combined',
968
+ data: {
969
+ a: head.key.name
970
+ },
971
+ node: head,
972
+ fix(fixer) {
973
+ return [fixer.replaceText(head.value, `"${attrGroup.map(a => a.value.value).join(' ')}"`), ...tail.map(a => fixer.remove(a))];
974
+ }
975
+ });
976
+ });
977
+ },
903
978
  VAttribute(attr) {
904
979
  if (attr.directive && (attr.key.name.name !== 'bind' || !attr.key.argument)) return;
905
980
  const tag = classify(attr.parent.parent.rawName);
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ classify,
5
+ getAttributes
6
+ } = require('../util/helpers');
7
+ const groups = [{
8
+ components: ['VDialog', 'VMenu', 'VTooltip'],
9
+ slots: ['activator'],
10
+ handler(context, node, directive, param) {
11
+ if (param.type === 'Identifier') {
12
+ // #activator="data"
13
+ const boundVariables = {};
14
+ node.variables.find(variable => variable.id.name === param.name)?.references.forEach(ref => {
15
+ if (ref.id.parent.type !== 'MemberExpression') return;
16
+ if (
17
+ // v-bind="data.props"
18
+ ref.id.parent.property.name === 'props' && ref.id.parent.parent.parent.directive && ref.id.parent.parent.parent.key.name.name === 'bind' && !ref.id.parent.parent.parent.key.argument) return;
19
+ if (ref.id.parent.property.name === 'on') {
20
+ boundVariables.on = ref.id;
21
+ } else if (ref.id.parent.property.name === 'attrs') {
22
+ boundVariables.attrs = ref.id;
23
+ }
24
+ });
25
+ if (boundVariables.on) {
26
+ const ref = boundVariables.on;
27
+ context.report({
28
+ node: ref,
29
+ messageId: 'changedProps',
30
+ data: {
31
+ component: node.parent.name,
32
+ slot: directive.key.argument.name
33
+ },
34
+ fix(fixer) {
35
+ return fixer.replaceText(ref.parent.parent.parent, `v-bind="${param.name}.props"`);
36
+ }
37
+ });
38
+ }
39
+ if (boundVariables.attrs) {
40
+ const ref = boundVariables.attrs;
41
+ if (!boundVariables.on) {
42
+ context.report({
43
+ node: boundVariables.attrs,
44
+ messageId: 'invalidProps'
45
+ });
46
+ } else {
47
+ context.report({
48
+ node: ref,
49
+ messageId: 'changedProps',
50
+ data: {
51
+ component: node.parent.name,
52
+ slot: directive.key.argument.name
53
+ },
54
+ fix(fixer) {
55
+ return fixer.removeRange([ref.parent.parent.parent.range[0] - 1, ref.parent.parent.parent.range[1]]);
56
+ }
57
+ });
58
+ }
59
+ }
60
+ } else if (param.type === 'ObjectPattern') {
61
+ // #activator="{ on, attrs }"
62
+ const boundVariables = {};
63
+ param.properties.forEach(prop => {
64
+ node.variables.find(variable => variable.id.name === prop.value.name)?.references.forEach(ref => {
65
+ if (prop.key.name === 'on') {
66
+ boundVariables.on = {
67
+ prop,
68
+ id: ref.id
69
+ };
70
+ } else if (prop.key.name === 'attrs') {
71
+ boundVariables.attrs = {
72
+ prop,
73
+ id: ref.id
74
+ };
75
+ }
76
+ });
77
+ });
78
+ if (boundVariables.on || boundVariables.attrs) {
79
+ if (boundVariables.attrs && !boundVariables.on) {
80
+ context.report({
81
+ node: boundVariables.attrs.prop.key,
82
+ messageId: 'invalidProps'
83
+ });
84
+ } else {
85
+ context.report({
86
+ node: param,
87
+ messageId: 'changedProps',
88
+ data: {
89
+ component: node.parent.name,
90
+ slot: directive.key.argument.name
91
+ },
92
+ *fix(fixer) {
93
+ if (boundVariables.on) {
94
+ const ref = boundVariables.on;
95
+ yield fixer.replaceText(ref.prop, 'props');
96
+ yield fixer.replaceText(ref.id.parent.parent, `v-bind="props"`);
97
+ }
98
+ if (boundVariables.attrs) {
99
+ const template = context.parserServices.getTemplateBodyTokenStore();
100
+ const ref = boundVariables.attrs;
101
+ const isLast = ref.prop === param.properties.at(-1);
102
+ if (isLast) {
103
+ const comma = template.getTokenBefore(ref.prop, {
104
+ filter: token => token.value === ','
105
+ });
106
+ if (comma) {
107
+ yield fixer.removeRange([comma.start, ref.prop.end]);
108
+ } else {
109
+ yield fixer.removeRange([ref.prop.start - 1, ref.prop.end]);
110
+ }
111
+ } else {
112
+ const comma = template.getTokenAfter(ref.prop, {
113
+ filter: token => token.value === ','
114
+ });
115
+ if (comma) {
116
+ yield fixer.removeRange([ref.prop.start - 1, comma.end]);
117
+ } else {
118
+ yield fixer.removeRange([ref.prop.start - 1, ref.prop.end]);
119
+ }
120
+ }
121
+ yield fixer.removeRange([ref.id.parent.parent.range[0] - 1, ref.id.parent.parent.range[1]]);
122
+ }
123
+ }
124
+ });
125
+ }
126
+ }
127
+ } else {
128
+ context.report({
129
+ node: directive,
130
+ messageId: 'invalidProps'
131
+ });
132
+ }
133
+ }
134
+ }, {
135
+ components: ['VSelect', 'VAutocomplete', 'VCombobox'],
136
+ slots: ['selection'],
137
+ handler(context, node, directive, param) {
138
+ if (!getAttributes(node.parent).some(attr => ['chips', 'closable-chips'].includes(attr.name))) return;
139
+ context.report({
140
+ node: directive,
141
+ messageId: 'renamed',
142
+ data: {
143
+ component: node.parent.name,
144
+ slot: directive.key.argument.name,
145
+ newSlot: 'chip'
146
+ },
147
+ fix(fixer) {
148
+ return fixer.replaceText(directive.key.argument, 'chip');
149
+ }
150
+ });
151
+ }
152
+ }];
153
+
154
+ // ------------------------------------------------------------------------------
155
+ // Rule Definition
156
+ // ------------------------------------------------------------------------------
157
+
158
+ module.exports = {
159
+ meta: {
160
+ docs: {
161
+ description: 'Prevent the use of removed and deprecated slots.',
162
+ category: 'recommended'
163
+ },
164
+ fixable: 'code',
165
+ schema: [],
166
+ messages: {
167
+ renamed: `{{ component }}'s '{{ slot }}' slot has been renamed to '{{ newSlot }}'`,
168
+ changedProps: `{{ component }}'s '{{ slot }}' slot has changed props`,
169
+ invalidProps: `Slot has invalid props`
170
+ }
171
+ },
172
+ create(context) {
173
+ let scopeStack;
174
+ return context.parserServices.defineTemplateBodyVisitor({
175
+ VElement(node) {
176
+ scopeStack = {
177
+ parent: scopeStack,
178
+ nodes: scopeStack ? [...scopeStack.nodes] : []
179
+ };
180
+ for (const variable of node.variables) {
181
+ scopeStack.nodes.push(variable.id);
182
+ }
183
+ if (node.name !== 'template' || node.parent.type !== 'VElement') return;
184
+ for (const group of groups) {
185
+ if (!group.components.includes(classify(node.parent.name)) && !group.components.includes(node.parent.name)) continue;
186
+ const directive = node.startTag.attributes.find(attr => {
187
+ return attr.directive && attr.key.name.name === 'slot' && group.slots.includes(attr.key.argument?.name);
188
+ });
189
+ if (!directive || !directive.value || directive.value.type !== 'VExpressionContainer' || !directive.value.expression || directive.value.expression.params.length !== 1) continue;
190
+ const param = directive.value.expression.params[0];
191
+ group.handler(context, node, directive, param);
192
+ }
193
+ },
194
+ 'VElement:exit'() {
195
+ scopeStack = scopeStack && scopeStack.parent;
196
+ }
197
+ });
198
+ }
199
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-vuetify",
3
- "version": "2.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "An eslint plugin for Vuetify",
5
5
  "main": "lib/index.js",
6
6
  "author": "Kael Watts-Deuchar <kaelwd@gmail.com>",