@dialpad/eslint-plugin-dialtone 1.12.0-next.3 → 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,100 @@
1
+ /**
2
+ * @fileoverview Detects usages of deprecated Dialtone components that should be replaced by newer alternatives.
3
+ * @author Brad Paugh
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Rule Definition
9
+ //------------------------------------------------------------------------------
10
+
11
+ /** @type {import('eslint').Rule.RuleModule} */
12
+ module.exports = {
13
+ meta: {
14
+ type: 'suggestion', // `problem`, `suggestion`, or `layout`
15
+ docs: {
16
+ description: "Detects usages of deprecated Dialtone components that should be replaced by newer alternatives",
17
+ recommended: false,
18
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-dialtone-component.md',
19
+ },
20
+ fixable: null, // Or `code` or `whitespace`
21
+ schema: [], // Add a schema if the rule has options
22
+ messages: {
23
+ deprecatedDialtoneComponent: '{{ componentName }} is deprecated. Replace with {{ replacement }} from {{ package }}.',
24
+ deprecatedDtIcon: 'DtIcon is deprecated. Import icon components directly instead, for example: import { DtIconPhoneHangUp } from \'@dialpad/dialtone-icons/vue3\'.',
25
+ }
26
+ },
27
+
28
+ create(context) {
29
+ const deprecatedIconComponent = 'DtIcon';
30
+
31
+ const deprecatedComponents = [
32
+ { componentName: 'DtRecipeComboboxMultiSelect', replacement: 'DtComboboxMultiSelect', package: '@dialpad/dialtone' },
33
+ { componentName: 'DtRecipeComboboxWithPopover', replacement: 'DtComboboxWithPopover', package: '@dialpad/dialtone' },
34
+ { componentName: 'DtRecipeMotionText', replacement: 'DtMotionText', package: '@dialpad/dialtone' },
35
+ { componentName: 'DtRecipeCallbarButton', replacement: 'DpCallbarButton', package: '@dialpad/callbarkit' },
36
+ { componentName: 'DtRecipeCallbarButtonWithPopover', replacement: 'DpCallbarButtonWithPopover', package: '@dialpad/callbarkit' },
37
+ { componentName: 'DtRecipeCallbarButtonWithDropdown', replacement: 'DpCallbarButtonWithDropdown', package: '@dialpad/callbarkit' },
38
+ { componentName: 'DtRecipeGroupedChip', replacement: 'DpGroupedChip', package: '@dialpad/callbarkit' },
39
+ { componentName: 'DtRecipeTopBannerInfo', replacement: 'DpTopBannerInfo', package: '@dialpad/callbarkit' },
40
+ { componentName: 'DtRecipeAttachmentCarousel', replacement: 'DpAttachmentCarousel', package: '@dialpad/chatkit' },
41
+ { componentName: 'DtRecipeMessageInput', replacement: 'DpMessageInput', package: '@dialpad/chatkit' },
42
+ { componentName: 'DtRecipeContactInfo', replacement: 'DpContactInfo', package: '@dialpad/chatkit' },
43
+ { componentName: 'DtRecipeEditor', replacement: 'DpEditor', package: '@dialpad/chatkit' },
44
+ { componentName: 'DtRecipeEmojiRow', replacement: 'DpEmojiRow', package: '@dialpad/chatkit' },
45
+ { componentName: 'DtRecipeFeedItemPill', replacement: 'DpFeedItemPill', package: '@dialpad/chatkit' },
46
+ { componentName: 'DtRecipeFeedItemRow', replacement: 'DpFeedItemRow', package: '@dialpad/chatkit' },
47
+ { componentName: 'DtRecipeContactCentersRow', replacement: 'DpContactCentersRow', package: '@dialpad/navigationkit' },
48
+ { componentName: 'DtRecipeContactRow', replacement: 'DpContactRow', package: '@dialpad/navigationkit' },
49
+ { componentName: 'DtRecipeGeneralRow', replacement: 'DpGeneralRow', package: '@dialpad/navigationkit' },
50
+ { componentName: 'DtRecipeGroupRow', replacement: 'DpGroupRow', package: '@dialpad/navigationkit' },
51
+ { componentName: 'DtRecipeUnreadPill', replacement: 'DpUnreadPill', package: '@dialpad/navigationkit' },
52
+ { componentName: 'DtRecipeCallbox', replacement: 'DpCallbox', package: '@dialpad/navigationkit' },
53
+ { componentName: 'DtRecipeSettingsMenuButton', replacement: 'DpSettingsMenuButton', package: '@dialpad/navigationkit' },
54
+ { componentName: 'DtRecipeIvrNode', replacement: 'DpIvrNode', package: '@dialpad/workflowkit' },
55
+ ];
56
+
57
+ //----------------------------------------------------------------------
58
+ // Public
59
+ //----------------------------------------------------------------------
60
+
61
+ return {
62
+ ImportDeclaration(node) {
63
+ const importPath = node.source.value;
64
+
65
+ const isDialtoneSource = /^@dialpad\/dialtone(?:-vue)?(?:$|\/)/.test(importPath);
66
+ if (!isDialtoneSource) {
67
+ return;
68
+ }
69
+
70
+ node.specifiers.forEach(specifier => {
71
+ if (specifier.type !== 'ImportSpecifier') return;
72
+
73
+ const importedName = specifier.imported.name;
74
+
75
+ if (importedName === deprecatedIconComponent) {
76
+ context.report({
77
+ node: specifier,
78
+ messageId: 'deprecatedDtIcon',
79
+ });
80
+ return;
81
+ }
82
+
83
+ const found = deprecatedComponents.find(item => item.componentName === importedName);
84
+
85
+ if (found) {
86
+ context.report({
87
+ node: specifier,
88
+ messageId: 'deprecatedDialtoneComponent',
89
+ data: {
90
+ componentName: found.componentName,
91
+ replacement: found.replacement,
92
+ package: found.package,
93
+ }
94
+ });
95
+ }
96
+ });
97
+ },
98
+ };
99
+ },
100
+ };
@@ -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
+ };
@@ -1,73 +1,95 @@
1
1
  /**
2
2
  * @fileoverview Detects usage of pixel-based utility classes (d-h16, d-p8, d-m8, etc.)
3
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.
4
5
  * @author Joshua Hynes
5
6
  */
6
- "use strict";
7
+ 'use strict';
7
8
 
8
- //------------------------------------------------------------------------------
9
- // Rule Definition
10
- //------------------------------------------------------------------------------
9
+ const { START, END, buildDetectRegex, createClassAttributeRule } = require('../util/class-attribute-rule');
11
10
 
12
- // Pixel values that have token-stop equivalents
13
- // MUST STAY IN SYNC with WIDTH_HEIGHTS_LAYOUT, MARGIN_SIZES_SPACING, MARGIN_SIZES_LAYOUT,
14
- // and NEGATIVE_SPACING_MAP in dialtone-css/postcss/constants.cjs
15
- // Sizing: layout stops (16px+)
16
- const SIZING_PIXELS = '16|32|48|64|80|96|112|128|160|192|224|256|288|320|352|384|416|448|480|512|544|576|608|640|672|704|736|768|800|832|864|896|928|960|992|1024';
17
- // Spacing: spacing stops (0-64px) + layout stops for margin/padding (96, 128)
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';
18
19
  const SPACING_PIXELS = '0|1|2|4|6|8|10|12|14|16|20|24|32|48|64|96|128';
19
- // Negative spacing
20
- const NEGATIVE_PIXELS = '1|2|4|6|8|10|12|14|16|24|32|48|64';
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');
21
57
 
22
- // Build patterns for each category
23
- // Sizing: d-h16, d-w64, d-hmn96, d-hmx128, d-wmn32, d-wmx512
24
- const SIZING_PATTERN = `d-(?:h|w|hmn|hmx|wmn|wmx)(?:${SIZING_PIXELS})\\b`;
25
- // Margin: d-m8, d-mt16, d-mr8, d-mb8, d-ml8, d-mx8, d-my8
26
- const MARGIN_PATTERN = `d-m(?:t|r|b|l|x|y)?(?:${SPACING_PIXELS})\\b`;
27
- // Negative margin: d-mtn8, d-mrn8, d-mbn8, d-mln8, d-mxn8, d-myn8, d-mn8
28
- const NEGATIVE_MARGIN_PATTERN = `d-m(?:t|r|b|l|x|y)?n(?:${NEGATIVE_PIXELS})\\b`;
29
- // Padding: d-p8, d-pt16, d-pr8, d-pb8, d-pl8, d-px8, d-py8
30
- const PADDING_PATTERN = `d-p(?:t|r|b|l|x|y)?(?:${SPACING_PIXELS})\\b`;
31
- // Gap: d-g8, d-rg8, d-cg8
32
- const GAP_PATTERN = `d-(?:g|rg|cg)(?:${SPACING_PIXELS})\\b`;
33
- // Position: d-t8, d-r8, d-b8, d-l8, d-x8, d-y8, d-all8
34
- const POSITION_PATTERN = `d-(?:t|r|b|l|x|y|all)(?:${SPACING_PIXELS})\\b`;
35
- // Negative position: d-tn8, d-rn8, d-bn8, d-ln8, d-xn8, d-yn8, d-alln8
36
- const NEGATIVE_POSITION_PATTERN = `d-(?:t|r|b|l|x|y|all)n(?:${NEGATIVE_PIXELS})\\b`;
58
+ const DETECT = buildDetectRegex([SIZING_RE, NEGATIVE_MARGIN_RE, MARGIN_RE, PADDING_RE, GAP_RE, NEGATIVE_POS_RE, POSITION_RE]);
37
59
 
38
- const COMBINED_PATTERN = new RegExp(
39
- `(?:${SIZING_PATTERN}|${NEGATIVE_MARGIN_PATTERN}|${MARGIN_PATTERN}|${PADDING_PATTERN}|${GAP_PATTERN}|${NEGATIVE_POSITION_PATTERN}|${POSITION_PATTERN})`
40
- );
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
+ }
41
74
 
42
75
  module.exports = {
43
76
  meta: {
44
77
  type: 'suggestion',
45
78
  docs: {
46
- 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).",
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).',
47
80
  recommended: false,
48
81
  url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-pixel-utility-classes.md',
49
82
  },
50
- fixable: null,
83
+ fixable: 'code',
51
84
  schema: [],
52
85
  messages: {
53
- 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). Run the "utility-class-to-token-stops" migration helper to update automatically.`,
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).',
54
87
  },
55
88
  },
56
89
 
57
- create (context) {
58
- const sourceCode = context.sourceCode ?? context.getSourceCode();
59
- return sourceCode.parserServices.defineTemplateBodyVisitor({
60
- VAttribute (node) {
61
- if (node.key.name === 'class') {
62
- const classes = node.value.value;
63
- if (COMBINED_PATTERN.test(classes)) {
64
- context.report({
65
- node,
66
- messageId: 'deprecatedPixelClass',
67
- });
68
- }
69
- }
70
- },
71
- });
72
- },
90
+ create: createClassAttributeRule({
91
+ detect: DETECT,
92
+ rewrite: rewriteClassString,
93
+ messageId: 'deprecatedPixelClass',
94
+ }),
73
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,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-next.3",
3
+ "version": "1.13.0-next.1",
4
4
  "description": "dialtone eslint plugin",
5
5
  "keywords": [
6
6
  "Dialpad",