• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

antebudimir / eslint-plugin-vanilla-extract / 18703859060

22 Oct 2025 02:52AM UTC coverage: 93.866% (+0.06%) from 93.81%
18703859060

push

github

antebudimir
feat 🥁: add no-trailing-zero rule

- New rule that flags and fixes unnecessary trailing zeros in numeric values
- Handles various CSS units, negative numbers, and decimal values
- Preserves non-trailing zeros in numbers like 11.01rem and 2.05em
- Includes comprehensive test coverage for edge cases

583 of 630 branches covered (92.54%)

Branch coverage included in aggregate %.

207 of 217 new or added lines in 3 files covered. (95.39%)

2340 of 2484 relevant lines covered (94.2%)

531.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.22
/src/css-rules/no-trailing-zero/trailing-zero-processor.ts
1
import type { Rule } from 'eslint';
2
import { TSESTree } from '@typescript-eslint/utils';
1✔
3

4
/**
5
 * Regex to match numbers with trailing zeros.
6
 * Matches patterns like:
7
 * - 1.0, 2.50, 0.0, 0.50
8
 * - 1.0px, 2.50rem, 0.0em
9
 * - -1.0, -2.50px
10
 *
11
 * Groups:
12
 * 1: Optional minus sign
13
 * 2: Integer part
14
 * 3: Significant fractional digits (optional)
15
 * 4: Trailing zeros
16
 * 5: Optional unit
17
 */
18
const TRAILING_ZERO_REGEX = /^(-?)(\d+)\.(\d*[1-9])?(0+)([a-z%]+)?$/i;
1✔
19

20
/**
21
 * Checks if a value has trailing zeros and returns the fixed value if needed.
22
 *
23
 * @param value The string value to check
24
 * @returns Object with hasTrailingZero flag and fixed value, or null if no trailing zeros
25
 */
26
export const checkTrailingZero = (value: string): { hasTrailingZero: boolean; fixed: string } | null => {
1✔
27
  const trimmedValue = value.trim();
400✔
28
  const match = trimmedValue.match(TRAILING_ZERO_REGEX);
400✔
29

30
  if (!match) {
400✔
31
    return null;
324✔
32
  }
324✔
33

34
  const [, minus = '', integerPart, significantFractional = '', , unit = ''] = match;
76✔
35

36
  // Handle special case: 0.0 or 0.00 etc. should become just "0"
37
  if (integerPart === '0' && !significantFractional) {
400✔
38
    return {
10✔
39
      hasTrailingZero: true,
10✔
40
      fixed: '0',
10✔
41
    };
10✔
42
  }
10✔
43

44
  // If there's no significant fractional part (e.g., "1.0" -> "1")
45
  if (!significantFractional) {
400✔
46
    return {
38✔
47
      hasTrailingZero: true,
38✔
48
      fixed: `${minus}${integerPart}${unit}`,
38✔
49
    };
38✔
50
  }
38✔
51

52
  // If there's a significant fractional part (e.g., "1.50" -> "1.5")
53
  return {
28✔
54
    hasTrailingZero: true,
28✔
55
    fixed: `${minus}${integerPart}.${significantFractional}${unit}`,
28✔
56
  };
28✔
57
};
28✔
58

59
/**
60
 * Processes a single string value and checks for trailing zeros in all numeric values.
61
 * Handles strings with multiple numeric values (e.g., "1.0px 2.50em").
62
 * Also handles values within function calls (e.g., "rotate(45.0deg)").
63
 *
64
 * @param value The string value to process
65
 * @returns Object with hasTrailingZero flag and fixed value, or null if no trailing zeros
66
 */
67
export const processStringValue = (value: string): { hasTrailingZero: boolean; fixed: string } | null => {
1✔
68
  // First, try to match the entire value
69
  const directMatch = checkTrailingZero(value);
208✔
70
  if (directMatch?.hasTrailingZero) {
208✔
71
    return directMatch;
60✔
72
  }
60✔
73

74
  // Split by whitespace to handle multiple values
75
  const parts = value.split(/(\s+)/);
148✔
76
  let hasAnyTrailingZero = false;
148✔
77

78
  const fixedParts = parts.map((part) => {
148✔
79
    // Preserve whitespace
80
    if (/^\s+$/.test(part)) {
204✔
81
      return part;
28✔
82
    }
28✔
83

84
    // Try to match the whole part first
85
    const result = checkTrailingZero(part);
176✔
86
    if (result?.hasTrailingZero) {
204✔
87
      hasAnyTrailingZero = true;
12✔
88
      return result.fixed;
12✔
89
    }
12✔
90

91
    // If no match, try to find and replace numbers within the part (e.g., inside function calls)
92
    const regex = /(-?\d+)\.(\d*[1-9])?(0+)(?![0-9])([a-z%]+)?/gi;
164✔
93
    const fixedPart = part.replace(
164✔
94
      regex,
164✔
95
      (_: string, integerWithSign: string, significantFractional: string, __: string, unit: string) => {
164✔
96
        // Reconstruct the number without trailing zeros
97
        const integerPart = integerWithSign;
12✔
98
        const sig = significantFractional || '';
12✔
99
        const u = unit || '';
12!
100

101
        // Handle 0.0 case - if it's zero and no unit, return just '0', otherwise keep the unit
102
        if (integerPart === '0' && !sig) {
12✔
103
          hasAnyTrailingZero = true;
2✔
104
          return u ? `0${u}` : '0';
2!
105
        }
2✔
106

107
        // Handle X.0 case
108
        if (!sig) {
10✔
109
          hasAnyTrailingZero = true;
10✔
110
          return `${integerPart}${u}`;
10✔
111
        }
10!
112

113
        // Handle X.Y0 case
NEW
114
        hasAnyTrailingZero = true;
×
NEW
115
        return `${integerPart}.${sig}${u}`;
×
116
      },
12✔
117
    );
164✔
118

119
    return fixedPart;
164✔
120
  });
148✔
121

122
  if (!hasAnyTrailingZero) {
208✔
123
    return null;
130✔
124
  }
130✔
125

126
  return {
18✔
127
    hasTrailingZero: true,
18✔
128
    fixed: fixedParts.join(''),
18✔
129
  };
18✔
130
};
18✔
131

132
/**
133
 * Recursively processes a style object, reporting and fixing instances of trailing zeros in numeric values.
134
 *
135
 * @param ruleContext The ESLint rule context.
136
 * @param node The ObjectExpression node representing the style object to be processed.
137
 */
138
export const processTrailingZeroInStyleObject = (
1✔
139
  ruleContext: Rule.RuleContext,
154✔
140
  node: TSESTree.ObjectExpression,
154✔
141
): void => {
154✔
142
  node.properties.forEach((property) => {
154✔
143
    if (property.type !== 'Property') {
266✔
144
      return;
2✔
145
    }
2✔
146

147
    // Process direct string literal values
148
    if (property.value.type === 'Literal' && typeof property.value.value === 'string') {
266✔
149
      const result = processStringValue(property.value.value);
208✔
150

151
      if (result?.hasTrailingZero) {
208✔
152
        ruleContext.report({
78✔
153
          node: property.value,
78✔
154
          messageId: 'trailingZero',
78✔
155
          data: {
78✔
156
            value: property.value.value,
78✔
157
            fixed: result.fixed,
78✔
158
          },
78✔
159
          fix: (fixer) => fixer.replaceText(property.value, `'${result.fixed}'`),
78✔
160
        });
78✔
161
      }
78✔
162
    }
208✔
163

164
    // Process numeric literal values (e.g., margin: 1.0)
165
    if (property.value.type === 'Literal' && typeof property.value.value === 'number') {
266✔
166
      // Use the raw property to get the original source text (which preserves trailing zeros)
167
      const rawValue = property.value.raw || property.value.value.toString();
16!
168
      const result = checkTrailingZero(rawValue);
16✔
169

170
      if (result?.hasTrailingZero) {
16✔
171
        ruleContext.report({
4✔
172
          node: property.value,
4✔
173
          messageId: 'trailingZero',
4✔
174
          data: {
4✔
175
            value: rawValue,
4✔
176
            fixed: result.fixed,
4✔
177
          },
4✔
178
          fix: (fixer) => fixer.replaceText(property.value, result.fixed),
4✔
179
        });
4✔
180
      }
4✔
181
    }
16✔
182

183
    // Process nested objects (selectors, media queries, etc.)
184
    if (property.value.type === 'ObjectExpression') {
266✔
185
      processTrailingZeroInStyleObject(ruleContext, property.value);
34✔
186
    }
34✔
187

188
    // Process arrays (for styleVariants with array values)
189
    if (property.value.type === 'ArrayExpression') {
266!
NEW
190
      property.value.elements.forEach((element) => {
×
NEW
191
        if (element && element.type === 'ObjectExpression') {
×
NEW
192
          processTrailingZeroInStyleObject(ruleContext, element);
×
NEW
193
        }
×
NEW
194
      });
×
NEW
195
    }
×
196
  });
154✔
197
};
154✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc