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

antebudimir / eslint-plugin-vanilla-extract / 19830657381

01 Dec 2025 04:54PM UTC coverage: 92.646% (+0.07%) from 92.574%
19830657381

push

github

antebudimir
feat 🥁: add no-unitless-values rule

- Disallow unitless numeric values for CSS properties that require units (e.g., width: 100 should be width: 100px)
- Allow zero values and unitless-valid properties (opacity, zIndex, lineHeight)
- Support both numeric literals and string literals with unitless numbers
- Configurable allowlist via 'allow' option

1094 of 1240 branches covered (88.23%)

Branch coverage included in aggregate %.

368 of 388 new or added lines in 3 files covered. (94.85%)

4298 of 4580 relevant lines covered (93.84%)

314.59 hits per line

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

95.51
/src/css-rules/no-unitless-values/unitless-value-processor.ts
1
import type { Rule } from 'eslint';
2
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
1✔
3

4
/**
5
 * CSS properties that require units for length/dimension values.
6
 * These properties should not have unitless numeric values (except 0).
7
 */
8
const PROPERTIES_REQUIRING_UNITS = new Set([
1✔
9
  // Box model
10
  'width',
1✔
11
  'height',
1✔
12
  'minWidth',
1✔
13
  'maxWidth',
1✔
14
  'minHeight',
1✔
15
  'maxHeight',
1✔
16
  'min-width',
1✔
17
  'max-width',
1✔
18
  'min-height',
1✔
19
  'max-height',
1✔
20

21
  // Spacing
22
  'margin',
1✔
23
  'marginTop',
1✔
24
  'marginRight',
1✔
25
  'marginBottom',
1✔
26
  'marginLeft',
1✔
27
  'marginBlock',
1✔
28
  'marginBlockStart',
1✔
29
  'marginBlockEnd',
1✔
30
  'marginInline',
1✔
31
  'marginInlineStart',
1✔
32
  'marginInlineEnd',
1✔
33
  'margin-top',
1✔
34
  'margin-right',
1✔
35
  'margin-bottom',
1✔
36
  'margin-left',
1✔
37
  'margin-block',
1✔
38
  'margin-block-start',
1✔
39
  'margin-block-end',
1✔
40
  'margin-inline',
1✔
41
  'margin-inline-start',
1✔
42
  'margin-inline-end',
1✔
43

44
  'padding',
1✔
45
  'paddingTop',
1✔
46
  'paddingRight',
1✔
47
  'paddingBottom',
1✔
48
  'paddingLeft',
1✔
49
  'paddingBlock',
1✔
50
  'paddingBlockStart',
1✔
51
  'paddingBlockEnd',
1✔
52
  'paddingInline',
1✔
53
  'paddingInlineStart',
1✔
54
  'paddingInlineEnd',
1✔
55
  'padding-top',
1✔
56
  'padding-right',
1✔
57
  'padding-bottom',
1✔
58
  'padding-left',
1✔
59
  'padding-block',
1✔
60
  'padding-block-start',
1✔
61
  'padding-block-end',
1✔
62
  'padding-inline',
1✔
63
  'padding-inline-start',
1✔
64
  'padding-inline-end',
1✔
65

66
  // Positioning
67
  'top',
1✔
68
  'right',
1✔
69
  'bottom',
1✔
70
  'left',
1✔
71
  'inset',
1✔
72
  'insetBlock',
1✔
73
  'insetBlockStart',
1✔
74
  'insetBlockEnd',
1✔
75
  'insetInline',
1✔
76
  'insetInlineStart',
1✔
77
  'insetInlineEnd',
1✔
78
  'inset-block',
1✔
79
  'inset-block-start',
1✔
80
  'inset-block-end',
1✔
81
  'inset-inline',
1✔
82
  'inset-inline-start',
1✔
83
  'inset-inline-end',
1✔
84

85
  // Border
86
  'borderWidth',
1✔
87
  'borderTopWidth',
1✔
88
  'borderRightWidth',
1✔
89
  'borderBottomWidth',
1✔
90
  'borderLeftWidth',
1✔
91
  'borderBlockWidth',
1✔
92
  'borderBlockStartWidth',
1✔
93
  'borderBlockEndWidth',
1✔
94
  'borderInlineWidth',
1✔
95
  'borderInlineStartWidth',
1✔
96
  'borderInlineEndWidth',
1✔
97
  'border-width',
1✔
98
  'border-top-width',
1✔
99
  'border-right-width',
1✔
100
  'border-bottom-width',
1✔
101
  'border-left-width',
1✔
102
  'border-block-width',
1✔
103
  'border-block-start-width',
1✔
104
  'border-block-end-width',
1✔
105
  'border-inline-width',
1✔
106
  'border-inline-start-width',
1✔
107
  'border-inline-end-width',
1✔
108

109
  'borderRadius',
1✔
110
  'borderTopLeftRadius',
1✔
111
  'borderTopRightRadius',
1✔
112
  'borderBottomLeftRadius',
1✔
113
  'borderBottomRightRadius',
1✔
114
  'borderStartStartRadius',
1✔
115
  'borderStartEndRadius',
1✔
116
  'borderEndStartRadius',
1✔
117
  'borderEndEndRadius',
1✔
118
  'border-radius',
1✔
119
  'border-top-left-radius',
1✔
120
  'border-top-right-radius',
1✔
121
  'border-bottom-left-radius',
1✔
122
  'border-bottom-right-radius',
1✔
123
  'border-start-start-radius',
1✔
124
  'border-start-end-radius',
1✔
125
  'border-end-start-radius',
1✔
126
  'border-end-end-radius',
1✔
127

128
  // Typography
129
  'fontSize',
1✔
130
  'font-size',
1✔
131
  'letterSpacing',
1✔
132
  'letter-spacing',
1✔
133
  'wordSpacing',
1✔
134
  'word-spacing',
1✔
135
  'textIndent',
1✔
136
  'text-indent',
1✔
137

138
  // Flexbox/Grid
139
  'gap',
1✔
140
  'rowGap',
1✔
141
  'columnGap',
1✔
142
  'row-gap',
1✔
143
  'column-gap',
1✔
144
  'flexBasis',
1✔
145
  'flex-basis',
1✔
146

147
  // Outline
148
  'outlineWidth',
1✔
149
  'outline-width',
1✔
150
  'outlineOffset',
1✔
151
  'outline-offset',
1✔
152

153
  // Other
154
  'blockSize',
1✔
155
  'inlineSize',
1✔
156
  'minBlockSize',
1✔
157
  'maxBlockSize',
1✔
158
  'minInlineSize',
1✔
159
  'maxInlineSize',
1✔
160
  'block-size',
1✔
161
  'inline-size',
1✔
162
  'min-block-size',
1✔
163
  'max-block-size',
1✔
164
  'min-inline-size',
1✔
165
  'max-inline-size',
1✔
166
]);
1✔
167

168
/**
169
 * CSS properties that accept unitless numeric values.
170
 * These properties should NOT be flagged when they have numeric values.
171
 */
172
const UNITLESS_VALID_PROPERTIES = new Set([
1✔
173
  'opacity',
1✔
174
  'zIndex',
1✔
175
  'z-index',
1✔
176
  'lineHeight',
1✔
177
  'line-height',
1✔
178
  'flexGrow',
1✔
179
  'flex-grow',
1✔
180
  'flexShrink',
1✔
181
  'flex-shrink',
1✔
182
  'order',
1✔
183
  'fontWeight',
1✔
184
  'font-weight',
1✔
185
  'zoom',
1✔
186
  'animationIterationCount',
1✔
187
  'animation-iteration-count',
1✔
188
  'columnCount',
1✔
189
  'column-count',
1✔
190
  'gridColumn',
1✔
191
  'grid-column',
1✔
192
  'gridColumnEnd',
1✔
193
  'grid-column-end',
1✔
194
  'gridColumnStart',
1✔
195
  'grid-column-start',
1✔
196
  'gridRow',
1✔
197
  'grid-row',
1✔
198
  'gridRowEnd',
1✔
199
  'grid-row-end',
1✔
200
  'gridRowStart',
1✔
201
  'grid-row-start',
1✔
202
  'orphans',
1✔
203
  'widows',
1✔
204
  'fillOpacity',
1✔
205
  'fill-opacity',
1✔
206
  'strokeOpacity',
1✔
207
  'stroke-opacity',
1✔
208
  'strokeMiterlimit',
1✔
209
  'stroke-miterlimit',
1✔
210
]);
1✔
211

212
export interface NoUnitlessValuesOptions {
213
  allow?: string[];
214
}
215

216
/**
217
 * Checks if a property name requires units for numeric values.
218
 */
219
const requiresUnits = (propertyName: string, allow: string[] = []): boolean => {
1✔
220
  if (allow.includes(propertyName)) {
186✔
221
    return false;
2✔
222
  }
2✔
223

224
  if (UNITLESS_VALID_PROPERTIES.has(propertyName)) {
186✔
225
    return false;
34✔
226
  }
34✔
227

228
  return PROPERTIES_REQUIRING_UNITS.has(propertyName);
150✔
229
};
150✔
230

231
/**
232
 * Gets the property name from a Property node.
233
 */
234
const getPropertyName = (property: TSESTree.Property): string | null => {
1✔
235
  if (property.key.type === AST_NODE_TYPES.Identifier) {
194✔
236
    return property.key.name;
166✔
237
  }
166✔
238
  if (property.key.type === AST_NODE_TYPES.Literal && typeof property.key.value === 'string') {
194✔
239
    return property.key.value;
28✔
240
  }
28!
NEW
241
  return null;
×
NEW
242
};
×
243

244
/**
245
 * Recursively processes a style object, reporting instances of unitless numeric values for properties that require units.
246
 *
247
 * @param ruleContext The ESLint rule context.
248
 * @param node The ObjectExpression node representing the style object to be processed.
249
 * @param options Rule options including allow list.
250
 */
251
export const processUnitlessValueInStyleObject = (
1✔
252
  ruleContext: Rule.RuleContext,
86✔
253
  node: TSESTree.ObjectExpression,
86✔
254
  options: NoUnitlessValuesOptions = {},
86✔
255
): void => {
86✔
256
  const allow = options.allow || [];
86✔
257

258
  node.properties.forEach((property) => {
86✔
259
    if (property.type !== AST_NODE_TYPES.Property) {
194!
NEW
260
      return;
×
NEW
261
    }
×
262

263
    const propertyName = getPropertyName(property);
194✔
264
    if (!propertyName) {
194!
NEW
265
      return;
×
NEW
266
    }
×
267

268
    // Skip special nested structures like @media, selectors, etc.
269
    // These will be processed recursively
270
    if (propertyName.startsWith('@') || propertyName.startsWith(':') || propertyName === 'selectors') {
194✔
271
      if (property.value.type === AST_NODE_TYPES.ObjectExpression) {
8✔
272
        if (propertyName === '@media' || propertyName === 'selectors') {
8✔
273
          property.value.properties.forEach((nestedProperty) => {
4✔
274
            if (
4✔
275
              nestedProperty.type === AST_NODE_TYPES.Property &&
4✔
276
              nestedProperty.value.type === AST_NODE_TYPES.ObjectExpression
4✔
277
            ) {
4✔
278
              processUnitlessValueInStyleObject(ruleContext, nestedProperty.value, options);
4✔
279
            }
4✔
280
          });
4✔
281
        } else {
4✔
282
          // For pseudo-selectors and other nested objects, process directly
283
          processUnitlessValueInStyleObject(ruleContext, property.value, options);
4✔
284
        }
4✔
285
      }
8✔
286
      return;
8✔
287
    }
8✔
288

289
    // Check if this property requires units
290
    if (!requiresUnits(propertyName, allow)) {
194✔
291
      // Still need to process nested objects for non-CSS properties
292
      if (property.value.type === AST_NODE_TYPES.ObjectExpression) {
60✔
293
        processUnitlessValueInStyleObject(ruleContext, property.value, options);
18✔
294
      }
18✔
295
      return;
60✔
296
    }
60✔
297

298
    // Check for unitless numeric literal values (e.g., width: 100)
299
    if (property.value.type === AST_NODE_TYPES.Literal && typeof property.value.value === 'number') {
194✔
300
      // Allow 0 without units (valid CSS), including -0
301
      if (property.value.value === 0 || property.value.value === -0) {
80✔
302
        return;
14✔
303
      }
14✔
304

305
      // Report unitless numeric value
306
      ruleContext.report({
66✔
307
        node: property.value,
66✔
308
        messageId: 'noUnitlessValue',
66✔
309
        data: {
66✔
310
          property: propertyName,
66✔
311
          value: String(property.value.value),
66✔
312
        },
66✔
313
      });
66✔
314
    }
66✔
315

316
    // Check for string literals that are unitless numbers (e.g., width: '100')
317
    if (property.value.type === AST_NODE_TYPES.Literal && typeof property.value.value === 'string') {
194✔
318
      const stringValue = property.value.value.trim();
38✔
319

320
      // Check if the string is a pure number (with optional negative sign and decimals)
321
      // This regex matches: -10, 10, 10.5, -10.5, but not 10px, 10rem, etc.
322
      const unitlessNumberRegex = /^-?\d+(\.\d+)?$/;
38✔
323

324
      if (unitlessNumberRegex.test(stringValue)) {
38✔
325
        // Allow '0' and '-0' without units
326
        const numValue = parseFloat(stringValue);
18✔
327
        if (numValue === 0 || numValue === -0) {
18✔
328
          return;
8✔
329
        }
8✔
330

331
        // Report unitless string numeric value
332
        ruleContext.report({
10✔
333
          node: property.value,
10✔
334
          messageId: 'noUnitlessValue',
10✔
335
          data: {
10✔
336
            property: propertyName,
10✔
337
            value: stringValue,
10✔
338
          },
10✔
339
        });
10✔
340
      }
10✔
341
    }
38✔
342

343
    // Check for unary expressions (e.g., -10)
344
    if (property.value.type === AST_NODE_TYPES.UnaryExpression && property.value.operator === '-') {
194✔
345
      if (
4✔
346
        property.value.argument.type === AST_NODE_TYPES.Literal &&
4✔
347
        typeof property.value.argument.value === 'number'
4✔
348
      ) {
4✔
349
        // Allow -0 without units
350
        if (property.value.argument.value === 0) {
4!
NEW
351
          return;
×
NEW
352
        }
×
353

354
        // Report unitless numeric value
355
        ruleContext.report({
4✔
356
          node: property.value as unknown as Rule.Node,
4✔
357
          messageId: 'noUnitlessValue',
4✔
358
          data: {
4✔
359
            property: propertyName,
4✔
360
            value: `-${property.value.argument.value}`,
4✔
361
          },
4✔
362
        });
4✔
363
      }
4✔
364
    }
4✔
365

366
    // Process nested objects (for complex selectors, etc.)
367
    if (property.value.type === AST_NODE_TYPES.ObjectExpression) {
194!
NEW
368
      processUnitlessValueInStyleObject(ruleContext, property.value, options);
×
NEW
369
    }
×
370
  });
86✔
371
};
86✔
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