@dialpad/eslint-plugin-dialtone 1.12.0 → 1.13.0-next.1

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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @fileoverview Detects usage of deprecated xxl/xxxl headline sizes which have been renamed to 2xl/3xl.
3
+ * @author Dialtone Team
4
+ */
5
+ 'use strict';
6
+
7
+ // ------------------------------------------------------------------------------
8
+ // Rule Definition
9
+ // ------------------------------------------------------------------------------
10
+
11
+ const SIZE_MAP = {
12
+ xxxl: '3xl',
13
+ xxl: '2xl',
14
+ };
15
+
16
+ module.exports = {
17
+ meta: {
18
+ type: 'suggestion',
19
+ docs: {
20
+ description: 'Headline sizes xxl/xxxl have been renamed to 2xl/3xl.',
21
+ recommended: false,
22
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-headline-sizes.md',
23
+ },
24
+ fixable: 'code',
25
+ schema: [],
26
+ messages: {
27
+ deprecatedSize: 'Headline size "{{oldSize}}" has been renamed to "{{newSize}}". Update size="{{oldSize}}" to size="{{newSize}}".',
28
+ deprecatedClass: 'CSS class "{{oldClass}}" has been renamed to "{{newClass}}". Update to use the new class name.',
29
+ },
30
+ },
31
+
32
+ create(context) {
33
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
34
+ return sourceCode.parserServices.defineTemplateBodyVisitor({
35
+ VAttribute(node) {
36
+ // Check size prop (e.g., size="xxl" or size="xxxl")
37
+ if (node.key.name === 'size' && node.value && node.value.value) {
38
+ const sizeValue = node.value.value;
39
+ if (SIZE_MAP[sizeValue]) {
40
+ context.report({
41
+ node,
42
+ messageId: 'deprecatedSize',
43
+ data: {
44
+ oldSize: sizeValue,
45
+ newSize: SIZE_MAP[sizeValue],
46
+ },
47
+ fix(fixer) {
48
+ return fixer.replaceText(node.value, `"${SIZE_MAP[sizeValue]}"`);
49
+ },
50
+ });
51
+ }
52
+ }
53
+
54
+ // Check class attributes for deprecated d-text-headline--xxl/xxxl classes
55
+ // Note: We only flag d-text-headline-- (current system), NOT d-headline-- (legacy system)
56
+ if (node.key.name === 'class' && node.value && node.value.value) {
57
+ const classValue = node.value.value;
58
+ // Match d-text-headline--xxl or d-text-headline--xxxl (but NOT d-headline--xxl)
59
+ const match = classValue.match(/\bd-text-headline--(xxx?l)\b/);
60
+ if (match && SIZE_MAP[match[1]]) {
61
+ const oldClass = `d-text-headline--${match[1]}`;
62
+ const newClass = `d-text-headline--${SIZE_MAP[match[1]]}`;
63
+ context.report({
64
+ node,
65
+ messageId: 'deprecatedClass',
66
+ data: { oldClass, newClass },
67
+ fix(fixer) {
68
+ const newValue = classValue.replace(oldClass, newClass);
69
+ return fixer.replaceText(node.value, `"${newValue}"`);
70
+ },
71
+ });
72
+ }
73
+ }
74
+ },
75
+ });
76
+ },
77
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @fileoverview Detect raw anchor / router-link with d-btn or d-link classes,
3
+ * and DtLink with d-td-* classes — all should migrate to DtButton/DtLink props
4
+ * via `npx dialtone-migrate-link-rendering`.
5
+ */
6
+ 'use strict';
7
+
8
+ //------------------------------------------------------------------------------
9
+ // Helpers
10
+ //------------------------------------------------------------------------------
11
+
12
+ function getStaticClassValue (node) {
13
+ for (const attr of node.startTag.attributes) {
14
+ if (attr.directive) continue;
15
+ const name = attr.key && (attr.key.rawName || attr.key.name);
16
+ if (name !== 'class' || !attr.value) continue;
17
+ return { value: attr.value.value, attr };
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function tokenize (classValue) {
23
+ return classValue.split(/\s+/).filter(Boolean);
24
+ }
25
+
26
+ const D_TD_TOKEN_RE = /^(?:[\w-]+:)?d-td-[\w-]+$/;
27
+
28
+ const RELEVANT_TAGS = new Set(['a', 'router-link', 'dt-link']);
29
+
30
+ const TAG_TOKEN_CHECKS = [
31
+ { tag: 'a', test: tokens => tokens.includes('d-btn'), messageId: 'anchorWithDBtn' },
32
+ { tag: 'a', test: tokens => tokens.includes('d-link'), messageId: 'anchorWithDLink' },
33
+ { tag: 'router-link', test: tokens => tokens.includes('d-btn'), messageId: 'routerLinkWithDBtn' },
34
+ { tag: 'router-link', test: tokens => tokens.includes('d-link'), messageId: 'routerLinkWithDLink' },
35
+ { tag: 'dt-link', test: tokens => tokens.some(t => D_TD_TOKEN_RE.test(t)), messageId: 'dtLinkWithDTd' },
36
+ ];
37
+
38
+ //------------------------------------------------------------------------------
39
+ // Rule Definition
40
+ //------------------------------------------------------------------------------
41
+
42
+ module.exports = {
43
+ meta: {
44
+ type: 'suggestion',
45
+ docs: {
46
+ description: 'Detect legacy d-btn / d-link / d-td-* class usage that should migrate to DtButton/DtLink props',
47
+ recommended: false,
48
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-link-styling-classes.md',
49
+ },
50
+ fixable: null,
51
+ schema: [],
52
+ messages: {
53
+ anchorWithDBtn:
54
+ '<a class="d-btn"> is deprecated. Use <dt-button href="…"> instead. ' +
55
+ 'Run `npx dialtone-migrate-link-rendering` to migrate.',
56
+ routerLinkWithDBtn:
57
+ '<router-link class="d-btn"> is deprecated. Use <dt-button :to="…"> instead. ' +
58
+ 'Run `npx dialtone-migrate-link-rendering` to migrate.',
59
+ anchorWithDLink:
60
+ '<a class="d-link"> is deprecated. Use <dt-link href="…"> instead. ' +
61
+ 'Run `npx dialtone-migrate-link-rendering` to migrate.',
62
+ routerLinkWithDLink:
63
+ '<router-link class="d-link"> is deprecated. Use <dt-link :to="…"> instead. ' +
64
+ 'Run `npx dialtone-migrate-link-rendering` to migrate.',
65
+ dtLinkWithDTd:
66
+ '<dt-link class="d-td-…"> is deprecated. Use the `underline` prop instead. ' +
67
+ 'Run `npx dialtone-migrate-link-rendering` to migrate.',
68
+ },
69
+ },
70
+
71
+ create (context) {
72
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
73
+ const defineTemplateBodyVisitor = sourceCode.parserServices?.defineTemplateBodyVisitor;
74
+ if (!defineTemplateBodyVisitor) return {};
75
+
76
+ return defineTemplateBodyVisitor({
77
+ VElement (node) {
78
+ const rawName = node.rawName || node.name;
79
+ // Normalize PascalCase Vue tags (RouterLink, DtLink) to kebab-case.
80
+ const tagName = rawName.includes('-')
81
+ ? rawName.toLowerCase()
82
+ : rawName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
83
+ if (!RELEVANT_TAGS.has(tagName)) return;
84
+
85
+ const result = getStaticClassValue(node);
86
+ if (!result) return;
87
+ const tokens = tokenize(result.value);
88
+
89
+ for (const check of TAG_TOKEN_CHECKS) {
90
+ if (check.tag !== tagName) continue;
91
+ if (check.test(tokens)) context.report({ node: result.attr, messageId: check.messageId });
92
+ }
93
+ },
94
+ });
95
+ },
96
+ };
@@ -0,0 +1,228 @@
1
+ /**
2
+ * @fileoverview Detect deprecated physical direction names in Dialtone component
3
+ * slots, props, prop values, and events. Suggests logical replacements.
4
+ */
5
+ 'use strict';
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Constants
9
+ //------------------------------------------------------------------------------
10
+
11
+ /**
12
+ * Maps component names to their deprecated slot names and replacements.
13
+ * null value = special handling (ambiguous, e.g. #icon on dt-button).
14
+ */
15
+ const DEPRECATED_SLOTS = {
16
+ 'dt-badge': { leftIcon: 'startIcon', rightIcon: 'endIcon' },
17
+ 'dt-button': { icon: null },
18
+ 'dt-input': { leftIcon: 'startIcon', rightIcon: 'endIcon' },
19
+ 'dt-tab': { leftIcon: 'startIcon' },
20
+ 'dt-split-button': { alphaIcon: 'startIcon', omegaIcon: 'endIcon', omega: 'end' },
21
+ 'dt-item-layout': { left: 'start', right: 'end', bottom: 'blockEnd' },
22
+ 'dt-recipe-callbox': { right: 'end', bottom: 'blockEnd' },
23
+ 'dt-recipe-contact-centers-row': { right: 'end' },
24
+ 'dt-recipe-general-row': { left: 'start' },
25
+ 'dt-recipe-top-banner-info': { left: 'start', right: 'end' },
26
+ 'dt-recipe-grouped-chip': {
27
+ leftIcon: 'startIcon',
28
+ rightIcon: 'endIcon',
29
+ leftContent: 'startContent',
30
+ rightContent: 'endContent',
31
+ },
32
+ };
33
+
34
+ /**
35
+ * Maps component names to their deprecated prop names (kebab-case) and replacements.
36
+ */
37
+ const DEPRECATED_PROPS = {
38
+ 'dt-item-layout': {
39
+ 'left-class': 'start-class',
40
+ 'right-class': 'end-class',
41
+ 'bottom-class': 'block-end-class',
42
+ },
43
+ 'dt-split-button': {
44
+ 'alpha-active': 'start-active',
45
+ 'alpha-aria-label': 'start-aria-label',
46
+ 'alpha-icon-position': 'start-icon-position',
47
+ 'alpha-leading-class': 'start-leading-class',
48
+ 'alpha-trailing-class': 'start-trailing-class',
49
+ 'alpha-label-class': 'start-label-class',
50
+ 'alpha-disabled': 'start-disabled',
51
+ 'alpha-loading': 'start-loading',
52
+ 'alpha-tooltip-text': 'start-tooltip-text',
53
+ 'omega-active': 'end-active',
54
+ 'omega-aria-label': 'end-aria-label',
55
+ 'omega-disabled': 'end-disabled',
56
+ 'omega-id': 'end-id',
57
+ 'omega-tooltip-text': 'end-tooltip-text',
58
+ },
59
+ };
60
+
61
+ /**
62
+ * Maps component names to props whose specific values are deprecated.
63
+ */
64
+ const DEPRECATED_PROP_VALUES = {
65
+ 'dt-button': {
66
+ 'icon-position': { left: 'start', right: 'end', top: 'blockStart', bottom: 'blockEnd' },
67
+ },
68
+ 'dt-root-layout': {
69
+ 'sidebar-position': { left: 'start', right: 'end' },
70
+ },
71
+ };
72
+
73
+ /**
74
+ * Maps component names to their deprecated event names and replacements.
75
+ */
76
+ const DEPRECATED_EVENTS = {
77
+ 'dt-split-button': { 'alpha-clicked': 'start-clicked', 'omega-clicked': 'end-clicked' },
78
+ };
79
+
80
+ //------------------------------------------------------------------------------
81
+ // Helpers
82
+ //------------------------------------------------------------------------------
83
+
84
+ /**
85
+ * Converts PascalCase or camelCase to kebab-case.
86
+ * e.g. 'DtBadge' → 'dt-badge', 'DtRecipeCallbox' → 'dt-recipe-callbox'
87
+ */
88
+ function toKebabCase (str) {
89
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
90
+ }
91
+
92
+ //------------------------------------------------------------------------------
93
+ // Rule Definition
94
+ //------------------------------------------------------------------------------
95
+
96
+ module.exports = {
97
+ meta: {
98
+ type: 'suggestion',
99
+ docs: {
100
+ description: 'Detect deprecated physical direction names in Dialtone component slots, props, prop values, and events',
101
+ recommended: false,
102
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-physical-naming.md',
103
+ },
104
+ fixable: null,
105
+ schema: [],
106
+ messages: {
107
+ deprecatedSlot:
108
+ '#{{ oldSlot }} on <{{ component }}> is deprecated. Use #{{ newSlot }} instead.',
109
+ deprecatedIconSlot:
110
+ 'The #icon slot on <dt-button> is deprecated. Use #startIcon, #endIcon, #blockStartIcon, or #blockEndIcon instead.',
111
+ deprecatedProp:
112
+ '{{ oldProp }} on <{{ component }}> is deprecated. Use {{ newProp }} instead.',
113
+ deprecatedPropValue:
114
+ '{{ prop }}="{{ oldValue }}" on <{{ component }}> is deprecated. Use {{ prop }}="{{ newValue }}" instead.',
115
+ deprecatedEvent:
116
+ '@{{ oldEvent }} on <{{ component }}> is deprecated. Use @{{ newEvent }} instead.',
117
+ },
118
+ },
119
+
120
+ create (context) {
121
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
122
+ return sourceCode.parserServices.defineTemplateBodyVisitor({
123
+
124
+ VElement (node) {
125
+ const rawName = node.rawName || node.name;
126
+ const elementName = rawName.includes('-') ? rawName : toKebabCase(rawName);
127
+
128
+ const slotsMap = DEPRECATED_SLOTS[elementName];
129
+ const propsMap = DEPRECATED_PROPS[elementName];
130
+ const propValuesMap = DEPRECATED_PROP_VALUES[elementName];
131
+ const eventsMap = DEPRECATED_EVENTS[elementName];
132
+
133
+ // Check attributes in a single pass (props, prop values, events)
134
+ if (propsMap || propValuesMap || eventsMap) {
135
+ for (const attr of node.startTag.attributes) {
136
+ if (!attr.directive) {
137
+ // Static props: alpha-active, left-class="x", icon-position="left"
138
+ const attrName = attr.key && (attr.key.rawName || attr.key.name);
139
+ if (attrName && propsMap && propsMap[attrName]) {
140
+ context.report({
141
+ node: attr,
142
+ messageId: 'deprecatedProp',
143
+ data: {
144
+ component: elementName,
145
+ oldProp: attrName,
146
+ newProp: propsMap[attrName],
147
+ },
148
+ });
149
+ }
150
+ if (attrName && propValuesMap && propValuesMap[attrName] && attr.value) {
151
+ const val = attr.value.value;
152
+ if (propValuesMap[attrName][val]) {
153
+ context.report({
154
+ node: attr,
155
+ messageId: 'deprecatedPropValue',
156
+ data: {
157
+ component: elementName,
158
+ prop: attrName,
159
+ oldValue: val,
160
+ newValue: propValuesMap[attrName][val],
161
+ },
162
+ });
163
+ }
164
+ }
165
+ } else if (attr.directive && attr.key.name.name === 'bind') {
166
+ // Dynamic props: :alpha-active="x", v-bind:omega-disabled="y"
167
+ const bindName = attr.key.argument && (attr.key.argument.rawName || attr.key.argument.name);
168
+ if (bindName && propsMap && propsMap[bindName]) {
169
+ context.report({
170
+ node: attr,
171
+ messageId: 'deprecatedProp',
172
+ data: {
173
+ component: elementName,
174
+ oldProp: bindName,
175
+ newProp: propsMap[bindName],
176
+ },
177
+ });
178
+ }
179
+ } else if (eventsMap && attr.key.name.name === 'on') {
180
+ const eventName = attr.key.argument && (attr.key.argument.rawName || attr.key.argument.name);
181
+ if (eventName && eventsMap[eventName]) {
182
+ context.report({
183
+ node: attr,
184
+ messageId: 'deprecatedEvent',
185
+ data: {
186
+ component: elementName,
187
+ oldEvent: eventName,
188
+ newEvent: eventsMap[eventName],
189
+ },
190
+ });
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ // Check child <template> elements for deprecated slot names
197
+ if (slotsMap) {
198
+ for (const child of node.children) {
199
+ if (child.type === 'VElement' && child.name === 'template') {
200
+ for (const attr of child.startTag.attributes) {
201
+ if (attr.directive && attr.key.name.name === 'slot') {
202
+ const slotName = attr.key.argument && (attr.key.argument.rawName || attr.key.argument.name);
203
+ if (slotName && slotName in slotsMap) {
204
+ if (slotsMap[slotName] === null) {
205
+ // #icon on dt-button is ambiguous — consumer must choose replacement
206
+ context.report({ node: attr, messageId: 'deprecatedIconSlot' });
207
+ } else {
208
+ context.report({
209
+ node: attr,
210
+ messageId: 'deprecatedSlot',
211
+ data: {
212
+ component: elementName,
213
+ oldSlot: slotName,
214
+ newSlot: slotsMap[slotName],
215
+ },
216
+ });
217
+ }
218
+ }
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ },
226
+ });
227
+ },
228
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @fileoverview Detects usage of pixel-based utility classes (d-h16, d-p8, d-m8, etc.)
3
+ * which should be replaced with token-stop-based equivalents (d-h-25, d-p-100, d-m-100).
4
+ * Autofixes via `lint-staged` — mirrors the `utility-class-to-token-stops` migration helper.
5
+ * @author Joshua Hynes
6
+ */
7
+ 'use strict';
8
+
9
+ const { START, END, buildDetectRegex, createClassAttributeRule } = require('../util/class-attribute-rule');
10
+
11
+ // MUST STAY IN SYNC with:
12
+ // - LAYOUT_STOPS, MARGIN_SIZES_SPACING, MARGIN_SIZES_LAYOUT in dialtone-css/postcss/constants.cjs
13
+ // - SIZING_MAP, SPACING_MAP, NEGATIVE_SPACING_MAP, SPACING_LAYOUT_MAP in
14
+ // dialtone-css/.../migration_helper/configs/utility-class-to-token-stops.mjs
15
+
16
+ // Ordered by descending string-length then descending value so regex alternation matches
17
+ // longest first (d-w1024 resolves `1024`, not `1`).
18
+ const SIZING_PIXELS = '1024|992|960|928|896|864|832|800|768|736|704|672|640|608|576|544|512|480|448|416|384|352|320|288|256|224|192|160|128|112|96|80|64|48|32|24|20|16|8|2|1';
19
+ const SPACING_PIXELS = '0|1|2|4|6|8|10|12|14|16|20|24|32|48|64|96|128';
20
+ const NEGATIVE_PIXELS = '1|2|4|6|8|10|12|14|16|20|24|32|48|64';
21
+
22
+ // Sizing autofix: scale-indexed layout stops + off-scale pixel-indexed exceptions (DLT-3330).
23
+ const SIZING_MAP = {
24
+ 1: '1px', 2: '2px', 8: '8px', 20: '20px', 24: '24px',
25
+ 16: '25', 32: '50', 48: '75', 64: '100', 80: '125', 96: '150',
26
+ 112: '175', 128: '200', 160: '250', 192: '300', 224: '350', 256: '400',
27
+ 288: '450', 320: '500', 352: '550', 384: '600', 416: '650', 448: '700',
28
+ 480: '750', 512: '800', 544: '850', 576: '900', 608: '950', 640: '1000',
29
+ 672: '1050', 704: '1100', 736: '1150', 768: '1200', 800: '1250',
30
+ 832: '1300', 864: '1350', 896: '1400', 928: '1450', 960: '1500',
31
+ 992: '1550', 1024: '1600',
32
+ };
33
+
34
+ const SPACING_MAP = {
35
+ 0: '0', 1: '1', 2: '25', 4: '50', 6: '75', 8: '100',
36
+ 10: '125', 12: '150', 14: '175', 16: '200', 20: '250', 24: '300',
37
+ 32: '400', 48: '600', 64: '800',
38
+ };
39
+
40
+ const NEGATIVE_SPACING_MAP = {
41
+ 1: '1', 2: '25', 4: '50', 6: '75', 8: '100',
42
+ 10: '125', 12: '150', 14: '175', 16: '200', 20: '250', 24: '300',
43
+ 32: '400', 48: '600', 64: '800',
44
+ };
45
+
46
+ const SPACING_LAYOUT_MAP = { 96: '150', 128: '200' };
47
+
48
+ // Per-category regexes with capture groups. Negative variants precede positive so `d-mtn8`
49
+ // matches the negative pattern (rule order is load-order in `rewriteClassString`).
50
+ const SIZING_RE = new RegExp(`${START}d-(h|w|hmn|hmx|wmn|wmx)(${SIZING_PIXELS})${END}`, 'g');
51
+ const NEGATIVE_MARGIN_RE = new RegExp(`${START}d-m(t|r|b|l|x|y)?n(${NEGATIVE_PIXELS})${END}`, 'g');
52
+ const MARGIN_RE = new RegExp(`${START}d-m(t|r|b|l|x|y)?(${SPACING_PIXELS})${END}`, 'g');
53
+ const PADDING_RE = new RegExp(`${START}d-p(t|r|b|l|x|y)?(${SPACING_PIXELS})${END}`, 'g');
54
+ const GAP_RE = new RegExp(`${START}d-(g|rg|cg)(${SPACING_PIXELS})${END}`, 'g');
55
+ const NEGATIVE_POS_RE = new RegExp(`${START}d-(t|r|b|l|x|y|all)n(${NEGATIVE_PIXELS})${END}`, 'g');
56
+ const POSITION_RE = new RegExp(`${START}d-(t|r|b|l|x|y|all)(${SPACING_PIXELS})${END}`, 'g');
57
+
58
+ const DETECT = buildDetectRegex([SIZING_RE, NEGATIVE_MARGIN_RE, MARGIN_RE, PADDING_RE, GAP_RE, NEGATIVE_POS_RE, POSITION_RE]);
59
+
60
+ /**
61
+ * Rewrite a class attribute string from legacy pixel-suffix to token-stop naming.
62
+ * Returns the input unchanged when no rewrites apply.
63
+ */
64
+ function rewriteClassString (input) {
65
+ return input
66
+ .replace(NEGATIVE_MARGIN_RE, (m, dir, px) => NEGATIVE_SPACING_MAP[px] ? `d-m${dir ?? ''}-n${NEGATIVE_SPACING_MAP[px]}` : m)
67
+ .replace(NEGATIVE_POS_RE, (m, dir, px) => NEGATIVE_SPACING_MAP[px] ? `d-${dir}-n${NEGATIVE_SPACING_MAP[px]}` : m)
68
+ .replace(SIZING_RE, (m, dir, px) => SIZING_MAP[px] ? `d-${dir}-${SIZING_MAP[px]}` : m)
69
+ .replace(MARGIN_RE, (m, dir, px) => { const stop = SPACING_MAP[px] ?? SPACING_LAYOUT_MAP[px]; return stop ? `d-m${dir ?? ''}-${stop}` : m; })
70
+ .replace(PADDING_RE, (m, dir, px) => { const stop = SPACING_MAP[px] ?? SPACING_LAYOUT_MAP[px]; return stop ? `d-p${dir ?? ''}-${stop}` : m; })
71
+ .replace(GAP_RE, (m, dir, px) => SPACING_MAP[px] ? `d-${dir}-${SPACING_MAP[px]}` : m)
72
+ .replace(POSITION_RE, (m, dir, px) => { const stop = SPACING_MAP[px] ?? SPACING_LAYOUT_MAP[px]; return stop ? `d-${dir}-${stop}` : m; });
73
+ }
74
+
75
+ module.exports = {
76
+ meta: {
77
+ type: 'suggestion',
78
+ docs: {
79
+ description: 'Pixel-based utility classes (d-h16, d-p8, d-m8) are deprecated. Use token-stop-based equivalents (d-h-25, d-p-100, d-m-100).',
80
+ recommended: false,
81
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-pixel-utility-classes.md',
82
+ },
83
+ fixable: 'code',
84
+ schema: [],
85
+ messages: {
86
+ deprecatedPixelClass: 'Pixel-based utility classes are deprecated. Use token-stop-based equivalents instead (e.g. d-h16 → d-h-25, d-p8 → d-p-100, d-w1 → d-w-1px).',
87
+ },
88
+ },
89
+
90
+ create: createClassAttributeRule({
91
+ detect: DETECT,
92
+ rewrite: rewriteClassString,
93
+ messageId: 'deprecatedPixelClass',
94
+ }),
95
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @fileoverview Detects usage of legacy border-radius utility classes (d-bar6, d-btr8, d-bbr-pill,
3
+ * etc.) and autofixes them to the new token-stop-indexed logical names (d-bar-350, d-bbsr-400,
4
+ * d-bber-pill).
5
+ */
6
+ 'use strict';
7
+
8
+ const { START, END, buildDetectRegex, createClassAttributeRule } = require('../util/class-attribute-rule');
9
+
10
+ // MUST STAY IN SYNC with:
11
+ // - RADIUS_STOPS in dialtone-css/postcss/constants.cjs
12
+ // - RADIUS_MAP / RADIUS_PAIR_PREFIX_MAP in dialtone-css/.../migration_helper/configs/utility-class-to-token-stops.mjs
13
+
14
+ const RADIUS_STOP_MAP = {
15
+ 0: '0', 1: '100', 2: '200', 4: '300', 6: '350',
16
+ 8: '400', 12: '450', 16: '500', 24: '550', 32: '600',
17
+ };
18
+
19
+ const PAIR_PREFIX_MAP = {
20
+ btr: 'bbsr', // top → block-start pair
21
+ bbr: 'bber', // bottom → block-end pair
22
+ blr: 'bisr', // left → inline-start pair
23
+ brr: 'bier', // right → inline-end pair
24
+ };
25
+
26
+ // Ordered by descending string length so regex alternation matches longest first
27
+ // (.d-bar32 resolves as `32`, not `3`).
28
+ const NUMERIC_SUFFIXES = Object.keys(RADIUS_STOP_MAP).sort((a, b) => b.length - a.length || Number(b) - Number(a)).join('|');
29
+ const PAIR_PREFIXES = Object.keys(PAIR_PREFIX_MAP).join('|');
30
+
31
+ const ALL_CORNERS_NUMERIC = new RegExp(`${START}d-bar(${NUMERIC_SUFFIXES})${END}`, 'g');
32
+ const PAIR_NUMERIC = new RegExp(`${START}d-(${PAIR_PREFIXES})(${NUMERIC_SUFFIXES})${END}`, 'g');
33
+ const PAIR_KEYWORD = new RegExp(`${START}d-(${PAIR_PREFIXES})-(pill|circle)${END}`, 'g');
34
+
35
+ const DETECT = buildDetectRegex([ALL_CORNERS_NUMERIC, PAIR_NUMERIC, PAIR_KEYWORD]);
36
+
37
+ function rewriteClassString (input) {
38
+ return input
39
+ .replace(ALL_CORNERS_NUMERIC, (_, px) => `d-bar-${RADIUS_STOP_MAP[px]}`)
40
+ .replace(PAIR_NUMERIC, (_, legacyPrefix, px) => `d-${PAIR_PREFIX_MAP[legacyPrefix]}-${RADIUS_STOP_MAP[px]}`)
41
+ .replace(PAIR_KEYWORD, (_, legacyPrefix, keyword) => `d-${PAIR_PREFIX_MAP[legacyPrefix]}-${keyword}`);
42
+ }
43
+
44
+ module.exports = {
45
+ meta: {
46
+ type: 'suggestion',
47
+ docs: {
48
+ description: 'Legacy border-radius utility classes (d-bar6, d-btr8, d-bbr-pill) are deprecated. Use token-stop-indexed logical names (d-bar-350, d-bbsr-400, d-bber-pill).',
49
+ recommended: false,
50
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-radius-utility-classes.md',
51
+ },
52
+ fixable: 'code',
53
+ schema: [],
54
+ messages: {
55
+ deprecatedRadiusClass: 'Legacy border-radius utility classes are deprecated. Use token-stop-indexed logical names instead (e.g. d-bar6 → d-bar-350, d-btr6 → d-bbsr-350, d-btr-pill → d-bbsr-pill).',
56
+ },
57
+ },
58
+
59
+ create: createClassAttributeRule({
60
+ detect: DETECT,
61
+ rewrite: rewriteClassString,
62
+ messageId: 'deprecatedRadiusClass',
63
+ }),
64
+ };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @fileoverview Detects usage of deprecated `success`-named color utility classes
3
+ * (`d-bgc-success*`, `d-bc-success*`, `d-fc-success*`) and recommends the
4
+ * `positive`-named replacements. The foreground (`d-fc-success*`) variants are
5
+ * already deprecated as part of the base-color cleanup but are still in active
6
+ * use, so this rule covers them too.
7
+ * @author Dialtone Team
8
+ */
9
+ 'use strict';
10
+
11
+ // ------------------------------------------------------------------------------
12
+ // Rule Definition
13
+ // ------------------------------------------------------------------------------
14
+
15
+ // Anchor at token boundaries. `\b` would slice through
16
+ // `d-bgc-success-strong-inverted-foo`, so we use explicit lookarounds.
17
+ // TOKEN_END mirrors the `success-to-positive` migration helper's CLASS_BOUNDARY
18
+ // so every lint finding is auto-fixable by the CLI the rule's message points to.
19
+ const TOKEN_START = '(?<![A-Za-z0-9_-])';
20
+ const TOKEN_END = '(?=$|[\\s"\'><:=,;{}()\\[\\]!`./])';
21
+
22
+ // Suffixes that follow `success` in the deprecated namespace. Order longest
23
+ // first so alternation matches greedily where needed (regex matching is
24
+ // leftmost-first, but we still keep this ordered for clarity).
25
+ const SUCCESS_SUFFIXES = [
26
+ '-subtle-opaque-inverted',
27
+ '-strong-inverted',
28
+ '-subtle-inverted',
29
+ '-opaque-inverted',
30
+ '-inverted-hover',
31
+ '-subtle-opaque',
32
+ '-inverted',
33
+ '-subtle',
34
+ '-strong',
35
+ '-opaque',
36
+ '-hover',
37
+ ].join('|');
38
+
39
+ // Per-role detect regexes. Each matches `d-{role}-success` optionally followed
40
+ // by one of the known suffixes — and ONLY by one of the known suffixes (the
41
+ // trailing `TOKEN_END` rejects unrelated continuations like
42
+ // `d-bgc-success-foo`).
43
+ const BG_SUCCESS_RE = new RegExp(`${TOKEN_START}d-bgc-success(?:${SUCCESS_SUFFIXES})?${TOKEN_END}`);
44
+ const FG_SUCCESS_RE = new RegExp(`${TOKEN_START}d-fc-success(?:${SUCCESS_SUFFIXES})?${TOKEN_END}`);
45
+ const BORDER_SUCCESS_RE = new RegExp(`${TOKEN_START}d-bc-success(?:${SUCCESS_SUFFIXES})?${TOKEN_END}`);
46
+
47
+ // Generic detect for the script-string visitor (any role).
48
+ const ANY_SUCCESS_RE = new RegExp(`${TOKEN_START}d-(?:bgc|bc|fc)-success(?:${SUCCESS_SUFFIXES})?${TOKEN_END}`);
49
+
50
+ const DOCS_BACKGROUND = 'https://dialtone.dialpad.com/utilities/backgrounds/color.html';
51
+ const DOCS_FOREGROUND = 'https://dialtone.dialpad.com/utilities/typography/font-color.html';
52
+ const DOCS_BORDER = 'https://dialtone.dialpad.com/utilities/borders/color.html';
53
+
54
+ // Build per-role messages following the precedent of deprecated-base-color-classes
55
+ // (separate messageId per role family).
56
+ const MESSAGE_PREFIX = '`d-{role}-success-...` is deprecated. Use `d-{role}-positive-...` instead. Run `npx dialtone-migration-helper --config success-to-positive` to migrate automatically.';
57
+ const buildMessage = (role, docs) =>
58
+ MESSAGE_PREFIX.replace(/\{role\}/g, role) + ` See the utility docs: ${docs}`;
59
+
60
+ function checkClassString (context, node, classes) {
61
+ if (BG_SUCCESS_RE.test(classes)) {
62
+ context.report({ node, messageId: 'deprecatedBackgroundSuccess' });
63
+ }
64
+ if (FG_SUCCESS_RE.test(classes)) {
65
+ context.report({ node, messageId: 'deprecatedForegroundSuccess' });
66
+ }
67
+ if (BORDER_SUCCESS_RE.test(classes)) {
68
+ context.report({ node, messageId: 'deprecatedBorderSuccess' });
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Walk every Literal/TemplateElement string descendant of an expression node
74
+ * (used for `:class="[...]"` / `:class="{...}"` / template-literal bindings).
75
+ * Best-effort — fully dynamic class names (`d-bgc-' + variant`) cannot be
76
+ * matched without runtime knowledge.
77
+ */
78
+ function findStringLiterals (n, out) {
79
+ if (!n || typeof n !== 'object') return;
80
+ if (n.type === 'Literal' && typeof n.value === 'string') {
81
+ out.push({ node: n, value: n.value });
82
+ } else if (n.type === 'TemplateElement' && n.value && typeof n.value.cooked === 'string') {
83
+ out.push({ node: n, value: n.value.cooked });
84
+ }
85
+ for (const key of Object.keys(n)) {
86
+ if (key === 'parent') continue;
87
+ const child = n[key];
88
+ if (child && typeof child === 'object') {
89
+ if (Array.isArray(child)) {
90
+ for (const c of child) findStringLiterals(c, out);
91
+ } else {
92
+ findStringLiterals(child, out);
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ module.exports = {
99
+ meta: {
100
+ type: 'suggestion',
101
+ docs: {
102
+ description:
103
+ '`d-bgc-success*`, `d-bc-success*`, and `d-fc-success*` utility classes are deprecated. Use the `positive`-named replacements.',
104
+ recommended: false,
105
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-success-color-classes.md',
106
+ },
107
+ fixable: null,
108
+ schema: [],
109
+ messages: {
110
+ deprecatedBackgroundSuccess: buildMessage('bgc', DOCS_BACKGROUND),
111
+ deprecatedForegroundSuccess: buildMessage('fc', DOCS_FOREGROUND),
112
+ deprecatedBorderSuccess: buildMessage('bc', DOCS_BORDER),
113
+ },
114
+ },
115
+
116
+ create (context) {
117
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
118
+ const defineTemplateBodyVisitor = sourceCode.parserServices?.defineTemplateBodyVisitor;
119
+
120
+ // Visitor that scans all string-typed AST nodes inside `<script>` or in
121
+ // plain `.js`/`.ts` files for class-name strings containing the deprecated
122
+ // tokens. This catches things like `:class="['d-bgc-success']"` (the array
123
+ // literal lives in the script-side AST that vue-eslint-parser exposes via
124
+ // the standard ESLint visitor) and `const c = 'd-bgc-success';`.
125
+ const scriptVisitor = {
126
+ Literal (node) {
127
+ if (typeof node.value !== 'string') return;
128
+ if (!ANY_SUCCESS_RE.test(node.value)) return;
129
+ checkClassString(context, node, node.value);
130
+ },
131
+ TemplateElement (node) {
132
+ const cooked = node.value && node.value.cooked;
133
+ if (typeof cooked !== 'string') return;
134
+ if (!ANY_SUCCESS_RE.test(cooked)) return;
135
+ checkClassString(context, node, cooked);
136
+ },
137
+ };
138
+
139
+ if (!defineTemplateBodyVisitor) {
140
+ // Plain JS/TS file — no Vue template, only the script visitor applies.
141
+ return scriptVisitor;
142
+ }
143
+
144
+ return defineTemplateBodyVisitor(
145
+ // Template visitor — handles `class="..."` static attributes and the
146
+ // string-literal halves of `:class="..."` dynamic bindings.
147
+ {
148
+ VAttribute (node) {
149
+ // Static `class="..."` attribute.
150
+ if (!node.directive && node.key.name === 'class' && node.value && typeof node.value.value === 'string') {
151
+ checkClassString(context, node, node.value.value);
152
+ return;
153
+ }
154
+ // Dynamic `:class="..."` / `v-bind:class="..."` binding.
155
+ if (
156
+ node.directive &&
157
+ node.key.name &&
158
+ node.key.name.name === 'bind' &&
159
+ node.key.argument &&
160
+ (node.key.argument.rawName === 'class' || node.key.argument.name === 'class') &&
161
+ node.value &&
162
+ node.value.expression
163
+ ) {
164
+ const literals = [];
165
+ findStringLiterals(node.value.expression, literals);
166
+ for (const { node: litNode, value } of literals) {
167
+ if (ANY_SUCCESS_RE.test(value)) {
168
+ checkClassString(context, litNode, value);
169
+ }
170
+ }
171
+ }
172
+ },
173
+ },
174
+ // ESLint visitor for the script half (i.e., the surrounding JS/TS that
175
+ // vue-eslint-parser exposes through the regular AST traversal). Plain
176
+ // `.js`/`.ts` files also fall through here.
177
+ scriptVisitor,
178
+ );
179
+ },
180
+ };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @fileoverview Detects usage of deprecated t-shirt size props (xs, sm, md, lg, xl) on Dialtone
3
+ * components and suggests numeric equivalents (100, 200, 300, 400, 500).
4
+ * @author Dialtone Team
5
+ */
6
+ 'use strict';
7
+
8
+ // ------------------------------------------------------------------------------
9
+ // Rule Definition
10
+ // ------------------------------------------------------------------------------
11
+
12
+ const SIZE_MAP = {
13
+ xs: '100',
14
+ sm: '200',
15
+ md: '300',
16
+ lg: '400',
17
+ xl: '500',
18
+ '2xl': '600',
19
+ '3xl': '700',
20
+ };
21
+
22
+ const TSHIRT_VALUES = new Set(Object.keys(SIZE_MAP));
23
+
24
+ // Props that accept the component size scale
25
+ const SIZE_PROPS = ['size', 'label-size', 'labelSize'];
26
+
27
+ // Speed prop on motion-text also uses the same scale
28
+ const SPEED_PROPS = ['speed'];
29
+
30
+ // All size-related prop names
31
+ const ALL_SIZE_PROPS = [...SIZE_PROPS, ...SPEED_PROPS];
32
+
33
+ // Only flag on Dialtone components (dt-* or Dt*)
34
+ function isDialtoneComponent (node) {
35
+ const parent = node.parent;
36
+ if (!parent || parent.type !== 'VStartTag') return false;
37
+ const element = parent.parent;
38
+ if (!element || element.type !== 'VElement') return false;
39
+ const name = element.rawName || element.name || '';
40
+ return name.startsWith('dt-') || name.startsWith('Dt');
41
+ }
42
+
43
+ module.exports = {
44
+ meta: {
45
+ type: 'suggestion',
46
+ docs: {
47
+ description: 'T-shirt sizes (xs, sm, md, lg, xl) are deprecated. Use numeric scale (100, 200, 300, 400, 500) instead.',
48
+ recommended: false,
49
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-tshirt-sizes.md',
50
+ },
51
+ fixable: 'code',
52
+ schema: [],
53
+ messages: {
54
+ deprecatedSize: 'Size "{{oldSize}}" is deprecated. Use :{{prop}}="{{newSize}}" instead.',
55
+ deprecatedSizeInBinding: 'T-shirt size "{{oldSize}}" in dynamic binding is deprecated. Use numeric {{newSize}} instead.',
56
+ },
57
+ },
58
+
59
+ create (context) {
60
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
61
+ return sourceCode.parserServices.defineTemplateBodyVisitor({
62
+ VAttribute (node) {
63
+ if (!isDialtoneComponent(node)) return;
64
+
65
+ // Get the prop name and check if it's a size-related prop
66
+ const isDirective = node.directive;
67
+ const propName = isDirective
68
+ ? (node.key.argument && node.key.argument.name)
69
+ : node.key.name;
70
+
71
+ if (!propName || !ALL_SIZE_PROPS.includes(propName)) return;
72
+
73
+ // --- Static attributes: size="sm" → auto-fixable ---
74
+ if (!isDirective && node.value && node.value.value) {
75
+ const sizeValue = node.value.value;
76
+ if (SIZE_MAP[sizeValue]) {
77
+ context.report({
78
+ node,
79
+ messageId: 'deprecatedSize',
80
+ data: {
81
+ oldSize: sizeValue,
82
+ newSize: SIZE_MAP[sizeValue],
83
+ prop: propName,
84
+ },
85
+ fix (fixer) {
86
+ const newAttr = `:${propName}="${SIZE_MAP[sizeValue]}"`;
87
+ return fixer.replaceTextRange(
88
+ [node.range[0], node.range[1]],
89
+ newAttr,
90
+ );
91
+ },
92
+ });
93
+ }
94
+ return;
95
+ }
96
+
97
+ // --- Dynamic bindings: :size="'sm'" or :size="x ? 'sm' : 'md'" ---
98
+ if (isDirective && node.value && node.value.expression) {
99
+ // Walk the expression tree for string literals with t-shirt values
100
+ const expression = node.value.expression;
101
+ const literals = [];
102
+
103
+ (function findLiterals (n) {
104
+ if (!n) return;
105
+ if (n.type === 'Literal' && typeof n.value === 'string' && TSHIRT_VALUES.has(n.value)) {
106
+ literals.push(n);
107
+ }
108
+ // Walk child nodes
109
+ for (const key of Object.keys(n)) {
110
+ if (key === 'parent') continue;
111
+ const child = n[key];
112
+ if (child && typeof child === 'object') {
113
+ if (Array.isArray(child)) {
114
+ child.forEach(c => { if (c && c.type) findLiterals(c); });
115
+ } else if (child.type) {
116
+ findLiterals(child);
117
+ }
118
+ }
119
+ }
120
+ })(expression);
121
+
122
+ for (const literal of literals) {
123
+ context.report({
124
+ node: literal,
125
+ messageId: 'deprecatedSizeInBinding',
126
+ data: {
127
+ oldSize: literal.value,
128
+ newSize: SIZE_MAP[literal.value],
129
+ },
130
+ });
131
+ }
132
+ }
133
+ },
134
+ });
135
+ },
136
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @fileoverview Warns when v-dt-focusgroup is used without an accessible label on the same element.
3
+ * @author Dialtone
4
+ */
5
+ 'use strict';
6
+
7
+ module.exports = {
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description:
12
+ 'Warns when v-dt-focusgroup is used on an element without aria-label or aria-labelledby. ' +
13
+ 'Screen readers need an accessible name to identify the widget.',
14
+ recommended: false,
15
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/focusgroup-requires-label.md',
16
+ },
17
+ fixable: null,
18
+ schema: [],
19
+ messages: {
20
+ missingLabel:
21
+ 'v-dt-focusgroup requires an accessible name via "aria-label" or "aria-labelledby" ' +
22
+ 'so screen readers can identify the widget.',
23
+ },
24
+ },
25
+
26
+ create (context) {
27
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
28
+ return sourceCode.parserServices.defineTemplateBodyVisitor({
29
+ VAttribute (node) {
30
+ if (!node.directive) return;
31
+ if (node.key.name.name !== 'dt-focusgroup') return;
32
+
33
+ const element = node.parent;
34
+ const hasLabel = element.attributes.some(
35
+ attr =>
36
+ (!attr.directive && (
37
+ attr.key.name === 'aria-label' ||
38
+ attr.key.name === 'aria-labelledby'
39
+ )) ||
40
+ (attr.directive && attr.key.name.name === 'bind' && (
41
+ attr.key.argument?.name === 'aria-label' ||
42
+ attr.key.argument?.name === 'aria-labelledby'
43
+ )),
44
+ );
45
+
46
+ if (!hasLabel) {
47
+ context.report({ node, messageId: 'missingLabel' });
48
+ }
49
+ },
50
+ });
51
+ },
52
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @fileoverview Warns when v-dt-focusgroup is used without a role attribute on the same element.
3
+ * @author Dialtone
4
+ */
5
+ 'use strict';
6
+
7
+ module.exports = {
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description:
12
+ 'Warns when v-dt-focusgroup is used on an element without a role attribute. ' +
13
+ 'Screen readers need a role to announce the widget correctly.',
14
+ recommended: false,
15
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/focusgroup-requires-role.md',
16
+ },
17
+ fixable: null,
18
+ schema: [],
19
+ messages: {
20
+ missingRole:
21
+ 'v-dt-focusgroup requires a "role" attribute (e.g. toolbar, tablist, listbox, radiogroup, menu) ' +
22
+ 'so screen readers can announce the widget correctly.',
23
+ },
24
+ },
25
+
26
+ create (context) {
27
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
28
+ return sourceCode.parserServices.defineTemplateBodyVisitor({
29
+ VAttribute (node) {
30
+ if (!node.directive) return;
31
+ if (node.key.name.name !== 'dt-focusgroup') return;
32
+
33
+ const element = node.parent;
34
+ const hasRole = element.attributes.some(
35
+ attr =>
36
+ (!attr.directive && attr.key.name === 'role') ||
37
+ (attr.directive && attr.key.name.name === 'bind' && attr.key.argument?.name === 'role'),
38
+ );
39
+
40
+ if (!hasRole) {
41
+ context.report({ node, messageId: 'missingRole' });
42
+ }
43
+ },
44
+ });
45
+ },
46
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Helpers for ESLint rules that autofix deprecated CSS utility classes in
3
+ * Vue template class attributes.
4
+ */
5
+ 'use strict';
6
+
7
+ // Token-boundary anchors. `\b` treats `-` as a non-word char, so `\bd-h16\b`
8
+ // would match inside `foo-d-h16` — use whitespace/start/end instead.
9
+ const START = '(?<=^|\\s)';
10
+ const END = '(?=$|\\s)';
11
+
12
+ function buildDetectRegex (regexes) {
13
+ return new RegExp(regexes.map(r => r.source).join('|'));
14
+ }
15
+
16
+ /**
17
+ * Returns a `create` function for a rule that detects and autofixes deprecated
18
+ * class names in `class` attributes on Vue template nodes. Preserves the
19
+ * attribute's quoting style (double, single, or unquoted).
20
+ */
21
+ function createClassAttributeRule ({ detect, rewrite, messageId }) {
22
+ return (context) => {
23
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
24
+ const defineTemplateBodyVisitor = sourceCode.parserServices?.defineTemplateBodyVisitor;
25
+ if (!defineTemplateBodyVisitor) return {};
26
+
27
+ return defineTemplateBodyVisitor({
28
+ VAttribute (node) {
29
+ if (node.key.name !== 'class') return;
30
+ const classes = node.value?.value;
31
+ if (!classes || !detect.test(classes)) return;
32
+
33
+ context.report({
34
+ node,
35
+ messageId,
36
+ fix (fixer) {
37
+ const rewritten = rewrite(classes);
38
+ if (rewritten === classes) return null;
39
+ const firstChar = sourceCode.getText(node.value)[0];
40
+ const quote = firstChar === '"' || firstChar === '\'' ? firstChar : '';
41
+ return fixer.replaceText(node.value, `${quote}${rewritten}${quote}`);
42
+ },
43
+ });
44
+ },
45
+ });
46
+ };
47
+ }
48
+
49
+ module.exports = { START, END, buildDetectRegex, createClassAttributeRule };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dialpad/eslint-plugin-dialtone",
3
- "version": "1.12.0",
3
+ "version": "1.13.0-next.1",
4
4
  "description": "dialtone eslint plugin",
5
5
  "keywords": [
6
6
  "Dialpad",