stylelint-plugin-defensive-css 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,9 @@ A Stylelint plugin to enforce defensive CSS best practices.
8
8
 
9
9
  > [Read more about Defensive CSS](https://defensivecss.dev/)
10
10
 
11
- ## 🚀 Version 1.0.0
11
+ ## 🚀 Version 1.1.0
12
12
 
13
- With the release of version 1.0.0 of the plugin, we now support Stylelint 16.
13
+ With the release of version 1.1.0 of the plugin, we now support Stylelint 17.
14
14
 
15
15
  ---
16
16
 
@@ -49,9 +49,10 @@ The plugin provides multiple rules that can be toggled on and off as needed.
49
49
  2. [Background-Repeat](#background-repeat)
50
50
  3. [Custom Property Fallbacks](#custom-property-fallbacks)
51
51
  4. [Flex Wrapping](#flex-wrapping)
52
- 5. [Scroll Chaining](#scroll-chaining)
53
- 6. [Scrollbar Gutter](#scrollbar-gutter)
54
- 7. [Vendor Prefix Grouping](#vendor-prefix-grouping)
52
+ 5. [Grid Line Names](#grid-line-names)
53
+ 6. [Scroll Chaining](#scroll-chaining)
54
+ 7. [Scrollbar Gutter](#scrollbar-gutter)
55
+ 8. [Vendor Prefix Grouping](#vendor-prefix-grouping)
55
56
 
56
57
  ---
57
58
 
@@ -146,6 +147,13 @@ div {
146
147
  background: url('some-image.jpg') black top center;
147
148
  background-repeat: no-repeat;
148
149
  }
150
+ div {
151
+ mask: url('some-image.jpg') repeat top center;
152
+ }
153
+ div {
154
+ mask: url('some-image.jpg') top center;
155
+ mask-repeat: no-repeat;
156
+ }
149
157
  ```
150
158
 
151
159
  #### ❌ Failing Examples
@@ -157,6 +165,12 @@ div {
157
165
  div {
158
166
  background-image: url('some-image.jpg');
159
167
  }
168
+ div {
169
+ mask: url('some-image.jpg') top center;
170
+ }
171
+ div {
172
+ mask-image: url('some-image.jpg');
173
+ }
160
174
  ```
161
175
 
162
176
  ### Custom Property Fallbacks
@@ -286,6 +300,118 @@ div {
286
300
  }
287
301
  ```
288
302
 
303
+ ### Grid Line Names
304
+
305
+ Require explicit named grid lines for tracks. When `grid-line-names` is enabled
306
+ the plugin validates `grid-template-columns`, `grid-template-rows`, and the
307
+ `grid` shorthand (the portion before/after the `/`) to ensure each track is
308
+ associated with a named line using the `[name]` syntax.
309
+
310
+ The rule supports configuring whether to validate columns and/or rows:
311
+
312
+ ```json
313
+ {
314
+ "rules": {
315
+ "plugin/use-defensive-css": [true, { "grid-line-names": true }]
316
+ }
317
+ }
318
+ ```
319
+
320
+ Or with explicit options:
321
+
322
+ ```json
323
+ {
324
+ "rules": {
325
+ "plugin/use-defensive-css": [
326
+ true,
327
+ { "grid-line-names": { "columns": true, "rows": false } }
328
+ ]
329
+ }
330
+ }
331
+ ```
332
+
333
+ - If `true` the rule validates both columns and rows.
334
+ - Pass an object with `columns` and/or `rows` set to `false` to disable one
335
+ side.
336
+
337
+ This rule helps avoid ambiguous layouts by rejecting unnamed tracks like
338
+ `1fr 1fr` and numeric `repeat(3, 1fr)` while allowing patterns that explicitly
339
+ name lines, e.g. `repeat(auto-fit, [name] 300px)` or bracketed names such as
340
+ `[a b] 1fr`.
341
+
342
+ #### ✅ Passing Examples
343
+
344
+ ```css
345
+ div {
346
+ grid-template-columns: [c-a] 1fr [c-b] 1fr;
347
+ }
348
+
349
+ div {
350
+ grid-template-rows: [r-a] 1fr [r-b] 2fr;
351
+ }
352
+
353
+ div {
354
+ grid-template-columns: [a] [b] 1fr [c] 2fr;
355
+ }
356
+
357
+ div {
358
+ grid-template-columns: repeat(auto-fit, [line-a line-b] 300px);
359
+ }
360
+
361
+ div {
362
+ grid-template-rows: repeat(auto-fill, [r1 r2] 100px);
363
+ }
364
+
365
+ div {
366
+ grid: [r-a] 1fr / [c-a] 1fr [c-b] 2fr;
367
+ }
368
+
369
+ div {
370
+ grid-template-columns: repeat(auto-fit, [a]300px);
371
+ }
372
+ ```
373
+
374
+ #### ❌ Failing Examples
375
+
376
+ ```css
377
+ div {
378
+ grid-template-columns: 1fr 1fr;
379
+ }
380
+
381
+ div {
382
+ grid-template-rows: 1fr 1fr;
383
+ }
384
+
385
+ div {
386
+ grid-template-columns: repeat(3, 1fr);
387
+ }
388
+
389
+ div {
390
+ grid-template-rows: repeat(3, 1fr);
391
+ }
392
+
393
+ div {
394
+ grid: auto / 1fr 1fr;
395
+ }
396
+
397
+ div {
398
+ grid: repeat(3, 1fr) / auto;
399
+ }
400
+
401
+ div {
402
+ grid-template-columns: 1fr [after] 1fr;
403
+ }
404
+
405
+ /* Reserved identifiers cannot be used as line names */
406
+ div {
407
+ grid-template-columns: [auto] 1fr;
408
+ }
409
+
410
+ div {
411
+ grid-template-rows: [span] 1fr;
412
+ }
413
+ ```
414
+
289
415
  ### Scroll Chaining
290
416
 
291
417
  > [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/scroll-chain/)
@@ -432,7 +558,6 @@ input::-webkit-input-placeholder {
432
558
  }
433
559
  input::-moz-placeholder {
434
560
  color: #222;
435
- }
436
561
  ```
437
562
 
438
563
  #### ❌ Failing Examples
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-defensive-css",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "A Stylelint plugin to enforce defensive CSS best practices.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -41,19 +41,19 @@
41
41
  "stylelint": "^14.0.0 || ^15.0.0 || ^16.0.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@commitlint/cli": "^18.4.3",
45
- "@commitlint/config-conventional": "^18.4.3",
46
- "cross-env": "^7.0.3",
47
- "eslint": "^8.35.0",
48
- "husky": "^8.0.3",
49
- "jest": "^29.4.3",
50
- "jest-cli": "^29.4.3",
51
- "jest-light-runner": "^0.6.0",
52
- "jest-preset-stylelint": "^7.0.0",
53
- "lint-staged": "^15.0.2",
54
- "prettier": "^3.0.3",
55
- "prettier-eslint": "^16.1.2",
56
- "stylelint": "^16.1.0"
44
+ "@commitlint/cli": "^20.4.0",
45
+ "@commitlint/config-conventional": "^20.4.0",
46
+ "cross-env": "^10.1.0",
47
+ "eslint": "^8.57.1",
48
+ "husky": "^9.1.7",
49
+ "jest": "^30.2.0",
50
+ "jest-cli": "^30.2.0",
51
+ "jest-light-runner": "^0.7.11",
52
+ "jest-preset-stylelint": "^9.1.0",
53
+ "lint-staged": "^16.2.7",
54
+ "prettier": "^3.8.1",
55
+ "prettier-eslint": "^16.4.2",
56
+ "stylelint": "^17.1.0"
57
57
  },
58
58
  "lint-staged": {
59
59
  "**/*.js|md|json": [
@@ -0,0 +1,40 @@
1
+ import stylelint from 'stylelint';
2
+ import { ruleMessages, ruleName } from '../base.js';
3
+
4
+ export function accidentalHover({ decl, result }) {
5
+ let isWrappedInHoverAtRule = false;
6
+
7
+ function traverseParentRules(parent) {
8
+ if (parent.parent.type === 'root') {
9
+ return;
10
+ }
11
+
12
+ if (parent.parent.params && /hover(: hover)?/.test(parent.parent.params)) {
13
+ isWrappedInHoverAtRule = true;
14
+ } else {
15
+ traverseParentRules(parent.parent);
16
+ }
17
+ }
18
+
19
+ const parent = decl.parent;
20
+ const selector = parent.selector;
21
+ const isHoverSelector = selector?.includes(':hover');
22
+
23
+ // If the :hover selector is inside a :not() selector, ignore it
24
+ if (/:not\(([^)]*:hover[^)]*)\)/g.test(selector)) {
25
+ return;
26
+ }
27
+
28
+ if (isHoverSelector) {
29
+ traverseParentRules(parent);
30
+
31
+ if (!isWrappedInHoverAtRule) {
32
+ stylelint.utils.report({
33
+ message: ruleMessages.accidentalHover(),
34
+ node: decl.parent,
35
+ result,
36
+ ruleName,
37
+ });
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,73 @@
1
+ import stylelint from 'stylelint';
2
+ import { ruleMessages, ruleName } from '../base.js';
3
+ import { findShorthandBackgroundRepeat } from './utils.js';
4
+
5
+ const defaultBackgroundRepeatProps = {
6
+ hasBackgroundImage: false,
7
+ isMissingBackgroundRepeat: true,
8
+ nodeToReport: undefined,
9
+ };
10
+
11
+ const defaultMaskRepeatProps = {
12
+ hasMaskImage: false,
13
+ isMissingMaskRepeat: true,
14
+ nodeToReport: undefined,
15
+ };
16
+
17
+ let backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
18
+ let maskRepeatProps = { ...defaultMaskRepeatProps };
19
+
20
+ export function backgroundRepeat({ decl, isLastStyleDeclaration, result }) {
21
+ const hasUrl = decl.value.includes('url(');
22
+ if (decl.prop === 'background' && hasUrl) {
23
+ backgroundRepeatProps.hasBackgroundImage = true;
24
+ backgroundRepeatProps.isMissingBackgroundRepeat =
25
+ !findShorthandBackgroundRepeat(decl.value);
26
+ backgroundRepeatProps.nodeToReport = decl;
27
+ }
28
+ if (decl.prop === 'mask' && hasUrl) {
29
+ maskRepeatProps.hasMaskImage = true;
30
+ maskRepeatProps.isMissingMaskRepeat = !findShorthandBackgroundRepeat(
31
+ decl.value,
32
+ );
33
+ maskRepeatProps.nodeToReport = decl;
34
+ }
35
+
36
+ if (decl.prop === 'background-image' && hasUrl) {
37
+ backgroundRepeatProps.hasBackgroundImage = true;
38
+ backgroundRepeatProps.nodeToReport = decl;
39
+ }
40
+ if (decl.prop === 'mask-image' && hasUrl) {
41
+ maskRepeatProps.hasMaskImage = true;
42
+ maskRepeatProps.nodeToReport = decl;
43
+ }
44
+
45
+ if (decl.prop === 'background-repeat') {
46
+ backgroundRepeatProps.isMissingBackgroundRepeat = false;
47
+ }
48
+ if (decl.prop === 'mask-repeat') {
49
+ maskRepeatProps.isMissingMaskRepeat = false;
50
+ }
51
+
52
+ if (isLastStyleDeclaration) {
53
+ if (Object.values(backgroundRepeatProps).every((prop) => prop)) {
54
+ stylelint.utils.report({
55
+ message: ruleMessages.backgroundRepeat(),
56
+ node: backgroundRepeatProps.nodeToReport,
57
+ result,
58
+ ruleName,
59
+ });
60
+ }
61
+ if (Object.values(maskRepeatProps).every((prop) => prop)) {
62
+ stylelint.utils.report({
63
+ message: ruleMessages.maskRepeat(),
64
+ node: maskRepeatProps.nodeToReport,
65
+ result,
66
+ ruleName,
67
+ });
68
+ }
69
+
70
+ backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
71
+ maskRepeatProps = { ...defaultMaskRepeatProps };
72
+ }
73
+ }
@@ -1,5 +1,5 @@
1
1
  const expression = /\b(repeat|repeat-x|repeat-y|space|round|no-repeat|)\b/g;
2
2
 
3
3
  export function findShorthandBackgroundRepeat(value) {
4
- return value.match(expression).some((val) => val);
4
+ return value.match(expression)?.some((val) => val) || false;
5
5
  }
@@ -15,6 +15,12 @@ export const ruleMessages = stylelint.utils.ruleMessages(ruleName, {
15
15
  flexWrapping() {
16
16
  return 'Whenever setting an element to `display: flex` a `flex-wrap` value must be defined. Set `flex-wrap: nowrap` for the default behavior. Learn more: https://defensivecss.dev/tip/flexbox-wrapping/';
17
17
  },
18
+ gridLineNames() {
19
+ return 'When defining grid tracks, name each grid line using the [name] syntax so each track is preceded by a named line. This applies to both rows and columns (depending on configuration). Avoid unnamed tracks like `1fr 1fr` or numeric repeat counts like `repeat(3, 1fr)`. Learn more: https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template';
20
+ },
21
+ maskRepeat() {
22
+ return 'Whenever setting a mask image, be sure to explicitly define a `mask-repeat` value. Learn more: https://defensivecss.dev/tip/bg-repeat/';
23
+ },
18
24
  scrollChaining() {
19
25
  return 'To prevent scroll chaining between contexts, any container with a scrollable overflow must have a `overscroll-behavior` value defined. Learn more: https://defensivecss.dev/tip/scroll-chain/';
20
26
  },
@@ -0,0 +1,35 @@
1
+ import stylelint from 'stylelint';
2
+ import { ruleMessages, ruleName } from '../base.js';
3
+ import { findCustomProperties } from './utils.js';
4
+
5
+ export function customPropertyFallbacks({ decl, options, result }) {
6
+ const propertiesWithoutFallback = findCustomProperties(decl.value);
7
+
8
+ if (propertiesWithoutFallback.length) {
9
+ if (Array.isArray(options?.['custom-property-fallbacks'])) {
10
+ if (options['custom-property-fallbacks'][0]) {
11
+ const patterns = options['custom-property-fallbacks'][1].ignore;
12
+ const patternMatched = propertiesWithoutFallback.some((property) => {
13
+ return patterns.some((pattern) =>
14
+ typeof pattern === 'string'
15
+ ? new RegExp(pattern).test(property)
16
+ : pattern.test(property),
17
+ );
18
+ });
19
+
20
+ if (patternMatched) {
21
+ return;
22
+ }
23
+ } else {
24
+ return;
25
+ }
26
+ }
27
+
28
+ stylelint.utils.report({
29
+ message: ruleMessages.customPropertyFallbacks(),
30
+ node: decl,
31
+ result,
32
+ ruleName,
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,47 @@
1
+ import stylelint from 'stylelint';
2
+ import { ruleMessages, ruleName } from '../base.js';
3
+
4
+ const defaultFlexWrappingProps = {
5
+ isDisplayFlex: false,
6
+ isFlexRow: true,
7
+ isMissingFlexWrap: true,
8
+ nodeToReport: undefined,
9
+ };
10
+
11
+ let flexWrappingProps = { ...defaultFlexWrappingProps };
12
+
13
+ export function flexWrapping({ decl, isLastStyleDeclaration, result }) {
14
+ if (decl.prop === 'display' && decl.value.includes('flex')) {
15
+ flexWrappingProps.isDisplayFlex = true;
16
+ flexWrappingProps.nodeToReport = decl;
17
+ }
18
+
19
+ if (decl.prop === 'flex-flow' && decl.value.includes('column')) {
20
+ flexWrappingProps.isFlexRow = false;
21
+ flexWrappingProps.isMissingFlexWrap = false;
22
+ }
23
+
24
+ if (decl.prop === 'flex-direction' && decl.value.includes('column')) {
25
+ flexWrappingProps.isFlexRow = false;
26
+ }
27
+
28
+ if (
29
+ decl.prop === 'flex-wrap' ||
30
+ (decl.prop === 'flex-flow' && decl.value.includes('wrap'))
31
+ ) {
32
+ flexWrappingProps.isMissingFlexWrap = false;
33
+ }
34
+
35
+ if (isLastStyleDeclaration) {
36
+ if (Object.values(flexWrappingProps).every((prop) => prop)) {
37
+ stylelint.utils.report({
38
+ message: ruleMessages.flexWrapping(),
39
+ node: flexWrappingProps.nodeToReport,
40
+ result,
41
+ ruleName,
42
+ });
43
+ }
44
+
45
+ flexWrappingProps = { ...defaultFlexWrappingProps };
46
+ }
47
+ }
@@ -0,0 +1,89 @@
1
+ import stylelint from 'stylelint';
2
+ import { ruleMessages, ruleName } from '../base.js';
3
+ import { isValueInvalidForLineNames, tokenizeGridColumns } from './utils.js';
4
+
5
+ export function gridLineNames({ decl, options, result }) {
6
+ const opt = options['grid-line-names'];
7
+ let validateColumns = false;
8
+ let validateRows = false;
9
+
10
+ if (opt === true) {
11
+ validateColumns = validateRows = true;
12
+ } else if (Array.isArray(opt)) {
13
+ const cfg = opt[1] || {};
14
+ validateColumns = cfg.columns !== false;
15
+ validateRows = cfg.rows !== false;
16
+ } else if (typeof opt === 'object') {
17
+ validateColumns = opt.columns !== false;
18
+ validateRows = opt.rows !== false;
19
+ } else {
20
+ // unknown shape — skip
21
+ return;
22
+ }
23
+
24
+ // Determine which declarations to validate
25
+ if (decl.prop === 'grid-template-columns' && validateColumns) {
26
+ if (isValueInvalidForLineNames(decl.value)) {
27
+ stylelint.utils.report({
28
+ message: ruleMessages.gridLineNames(),
29
+ node: decl,
30
+ result,
31
+ ruleName,
32
+ });
33
+ }
34
+ }
35
+
36
+ if (decl.prop === 'grid-template-rows' && validateRows) {
37
+ if (isValueInvalidForLineNames(decl.value)) {
38
+ stylelint.utils.report({
39
+ message: ruleMessages.gridLineNames(),
40
+ node: decl,
41
+ result,
42
+ ruleName,
43
+ });
44
+ }
45
+ }
46
+
47
+ if (decl.prop === 'grid') {
48
+ if (!validateColumns && !validateRows) return;
49
+
50
+ const value = decl.value;
51
+ const slashIndex = value.indexOf('/');
52
+
53
+ // If there's no slash we can't reliably extract rows/columns
54
+ if (slashIndex === -1) return;
55
+
56
+ const rowsValue = value.slice(0, slashIndex).trim();
57
+ const colsValue = value.slice(slashIndex + 1).trim();
58
+
59
+ let invalidRows = false;
60
+ let invalidCols = false;
61
+
62
+ if (validateRows) {
63
+ const rowTokens = tokenizeGridColumns(rowsValue || '');
64
+ const skipRowsValidation =
65
+ rowTokens.length === 1 &&
66
+ !/^\[[^\]]+\]$/.test(rowTokens[0]) &&
67
+ !/repeat\s*\(/i.test(rowTokens[0]);
68
+
69
+ if (!skipRowsValidation && isValueInvalidForLineNames(rowsValue)) {
70
+ invalidRows = true;
71
+ }
72
+ }
73
+
74
+ if (validateColumns) {
75
+ if (isValueInvalidForLineNames(colsValue)) {
76
+ invalidCols = true;
77
+ }
78
+ }
79
+
80
+ if (invalidRows || invalidCols) {
81
+ stylelint.utils.report({
82
+ message: ruleMessages.gridLineNames(),
83
+ node: decl,
84
+ result,
85
+ ruleName,
86
+ });
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,116 @@
1
+ export function tokenizeGridColumns(value) {
2
+ const tokens = [];
3
+ let cur = '';
4
+ let inBracket = false;
5
+ let inParens = false;
6
+
7
+ for (let i = 0; i < value.length; i++) {
8
+ const ch = value[i];
9
+
10
+ if (ch === '[') {
11
+ if (cur.trim()) {
12
+ tokens.push(cur.trim());
13
+ cur = '';
14
+ }
15
+ inBracket = true;
16
+ cur += ch;
17
+ continue;
18
+ }
19
+
20
+ if (ch === ']') {
21
+ cur += ch;
22
+ inBracket = false;
23
+ continue;
24
+ }
25
+
26
+ if (ch === '(') {
27
+ inParens = true;
28
+ cur += ch;
29
+ continue;
30
+ }
31
+
32
+ if (ch === ')') {
33
+ cur += ch;
34
+ inParens = false;
35
+ continue;
36
+ }
37
+
38
+ if (!inBracket && !inParens && /\s/.test(ch)) {
39
+ if (cur.trim()) {
40
+ tokens.push(cur.trim());
41
+ cur = '';
42
+ }
43
+ continue;
44
+ }
45
+
46
+ cur += ch;
47
+ }
48
+
49
+ if (cur.trim()) tokens.push(cur.trim());
50
+ return tokens;
51
+ }
52
+
53
+ // Extract names from the first bracketed group within a token, or `null`.
54
+ export function extractBracketNames(token) {
55
+ const m = token.match(/\[([^\]]+)\]/);
56
+ if (!m) return null;
57
+ return m[1].trim().split(/\s+/);
58
+ }
59
+
60
+ export function extractAllBracketNamesFromString(str) {
61
+ const names = [];
62
+ const re = /\[([^\]]+)\]/g;
63
+ let m;
64
+ while ((m = re.exec(str))) {
65
+ names.push(...m[1].trim().split(/\s+/));
66
+ }
67
+ return names;
68
+ }
69
+
70
+ export function buildTrackTokens(tokens) {
71
+ const trackTokens = [];
72
+ for (let i = 0; i < tokens.length; ) {
73
+ const token = tokens[i];
74
+
75
+ if (/^\[[^\]]+\]$/.test(token)) {
76
+ const brackets = [token];
77
+ i++;
78
+ while (i < tokens.length && /^\[[^\]]+\]$/.test(tokens[i])) {
79
+ brackets.push(tokens[i]);
80
+ i++;
81
+ }
82
+
83
+ if (i < tokens.length) {
84
+ trackTokens.push(`${brackets.join(' ')} ${tokens[i]}`);
85
+ i++;
86
+ } else {
87
+ trackTokens.push(brackets.join(' '));
88
+ }
89
+ } else {
90
+ trackTokens.push(token);
91
+ i++;
92
+ }
93
+ }
94
+ return trackTokens;
95
+ }
96
+
97
+ export function isValueInvalidForLineNames(value) {
98
+ const tokens = tokenizeGridColumns(value);
99
+ const trackTokens = buildTrackTokens(tokens);
100
+
101
+ if (!trackTokens.length) return false;
102
+
103
+ const invalidTrack = trackTokens.find((token) => {
104
+ let names = extractBracketNames(token);
105
+
106
+ if ((!names || !names.length) && /repeat\s*\(/i.test(token)) {
107
+ names = extractAllBracketNamesFromString(value);
108
+ }
109
+
110
+ if (!names || !names.length) return true;
111
+
112
+ return names.some((n) => ['span', 'auto'].includes(n));
113
+ });
114
+
115
+ return Boolean(invalidTrack);
116
+ }
@@ -1,58 +1,17 @@
1
1
  import stylelint from 'stylelint';
2
2
 
3
3
  import { ruleName, ruleMessages, ruleMeta } from './base.js';
4
- import { findShorthandBackgroundRepeat } from '../../utils/findShorthandBackgroundRepeat.js';
5
- import { findVendorPrefixes } from '../../utils/findVendorPrefixes.js';
6
- import { findCustomProperties } from '../../utils/findCustomProperties.js';
7
4
 
8
- const defaultBackgroundRepeatProps = {
9
- hasBackgroundImage: false,
10
- isMissingBackgroundRepeat: true,
11
- nodeToReport: undefined,
12
- };
13
- const defaultFlexWrappingProps = {
14
- isDisplayFlex: false,
15
- isFlexRow: true,
16
- isMissingFlexWrap: true,
17
- nodeToReport: undefined,
18
- };
19
- const defaultScrollbarGutterProps = {
20
- hasOverflow: false,
21
- hasScrollbarGutter: false,
22
- nodeToReport: undefined,
23
- };
24
- const defaultScrollChainingProps = {
25
- hasOverflow: false,
26
- hasOverscrollBehavior: false,
27
- nodeToReport: undefined,
28
- };
5
+ import { accidentalHover } from './accidental-hover/index.js';
6
+ import { backgroundRepeat } from './background-repeat/index.js';
7
+ import { customPropertyFallbacks } from './custom-property-fallbacks/index.js';
8
+ import { flexWrapping } from './flex-wrapping/index.js';
9
+ import { gridLineNames } from './grid-line-names/index.js';
10
+ import { scrollChaining } from './scroll-chaining/index.js';
11
+ import { scrollbarGutter } from './scrollbar-gutter/index.js';
12
+ import { vendorPrefixGrouping } from './vendor-prefix-grouping/index.js';
29
13
 
30
- let backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
31
- let flexWrappingProps = { ...defaultFlexWrappingProps };
32
- let scrollbarGutterProps = { ...defaultScrollbarGutterProps };
33
- let scrollChainingProps = { ...defaultScrollChainingProps };
34
14
  let isLastStyleDeclaration = false;
35
- let isWrappedInHoverAtRule = false;
36
-
37
- const overflowProperties = [
38
- 'overflow',
39
- 'overflow-x',
40
- 'overflow-y',
41
- 'overflow-inline',
42
- 'overflow-block',
43
- ];
44
-
45
- function traverseParentRules(parent) {
46
- if (parent.parent.type === 'root') {
47
- return;
48
- }
49
-
50
- if (parent.parent.params && /hover(: hover)?/.test(parent.parent.params)) {
51
- isWrappedInHoverAtRule = true;
52
- } else {
53
- traverseParentRules(parent.parent);
54
- }
55
- }
56
15
 
57
16
  const ruleFunction = (_, options) => {
58
17
  return (root, result) => {
@@ -69,208 +28,42 @@ const ruleFunction = (_, options) => {
69
28
 
70
29
  /* ACCIDENTAL HOVER */
71
30
  if (options?.['accidental-hover']) {
72
- const parent = decl.parent;
73
- const selector = parent.selector;
74
- const isHoverSelector = selector?.includes(':hover');
75
- isWrappedInHoverAtRule = false;
76
-
77
- // If the :hover selector is inside a :not() selector, ignore it
78
- if (/:not\(([^)]*:hover[^)]*)\)/g.test(selector)) {
79
- return;
80
- }
81
-
82
- if (isHoverSelector) {
83
- traverseParentRules(parent);
84
-
85
- if (!isWrappedInHoverAtRule) {
86
- stylelint.utils.report({
87
- message: ruleMessages.accidentalHover(),
88
- node: decl.parent,
89
- result,
90
- ruleName,
91
- });
92
- }
93
- }
31
+ accidentalHover({ decl, result });
94
32
  }
95
33
 
96
34
  /* BACKGROUND REPEAT */
97
35
  if (options?.['background-repeat']) {
98
- if (decl.prop === 'background' && decl.value.includes('url(')) {
99
- backgroundRepeatProps.hasBackgroundImage = true;
100
- backgroundRepeatProps.isMissingBackgroundRepeat =
101
- !findShorthandBackgroundRepeat(decl.value);
102
- backgroundRepeatProps.nodeToReport = decl;
103
- }
104
-
105
- if (decl.prop === 'background-image' && decl.value.includes('url(')) {
106
- backgroundRepeatProps.hasBackgroundImage = true;
107
- backgroundRepeatProps.nodeToReport = decl;
108
- }
109
-
110
- if (decl.prop === 'background-repeat') {
111
- backgroundRepeatProps.isMissingBackgroundRepeat = false;
112
- }
113
-
114
- if (isLastStyleDeclaration) {
115
- if (Object.values(backgroundRepeatProps).every((prop) => prop)) {
116
- stylelint.utils.report({
117
- message: ruleMessages.backgroundRepeat(),
118
- node: backgroundRepeatProps.nodeToReport,
119
- result,
120
- ruleName,
121
- });
122
- }
123
-
124
- backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
125
- }
36
+ backgroundRepeat({ decl, isLastStyleDeclaration, result });
126
37
  }
127
38
 
128
39
  /* CUSTOM PROPERTY FALLBACKS */
129
40
  if (options?.['custom-property-fallbacks']) {
130
- const propertiesWithoutFallback = findCustomProperties(decl.value);
131
-
132
- if (propertiesWithoutFallback.length) {
133
- if (Array.isArray(options?.['custom-property-fallbacks'])) {
134
- if (options['custom-property-fallbacks'][0]) {
135
- const patterns = options['custom-property-fallbacks'][1].ignore;
136
- const patternMatched = propertiesWithoutFallback.some(
137
- (property) => {
138
- return patterns.some((pattern) =>
139
- typeof pattern === 'string'
140
- ? new RegExp(pattern).test(property)
141
- : pattern.test(property),
142
- );
143
- },
144
- );
145
-
146
- if (patternMatched) {
147
- return;
148
- }
149
- } else {
150
- return;
151
- }
152
- }
153
-
154
- stylelint.utils.report({
155
- message: ruleMessages.customPropertyFallbacks(),
156
- node: decl,
157
- result,
158
- ruleName,
159
- });
160
- }
41
+ customPropertyFallbacks({ decl, options, result });
161
42
  }
162
43
 
163
44
  /* FLEX WRAPPING */
164
45
  if (options?.['flex-wrapping']) {
165
- if (decl.prop === 'display' && decl.value.includes('flex')) {
166
- flexWrappingProps.isDisplayFlex = true;
167
- flexWrappingProps.nodeToReport = decl;
168
- }
169
-
170
- if (decl.prop === 'flex-flow' && decl.value.includes('column')) {
171
- flexWrappingProps.isFlexRow = false;
172
- flexWrappingProps.isMissingFlexWrap = false;
173
- }
174
-
175
- if (decl.prop === 'flex-direction' && decl.value.includes('column')) {
176
- flexWrappingProps.isFlexRow = false;
177
- }
178
-
179
- if (
180
- decl.prop === 'flex-wrap' ||
181
- (decl.prop === 'flex-flow' && decl.value.includes('wrap'))
182
- ) {
183
- flexWrappingProps.isMissingFlexWrap = false;
184
- }
185
-
186
- if (isLastStyleDeclaration) {
187
- if (Object.values(flexWrappingProps).every((prop) => prop)) {
188
- stylelint.utils.report({
189
- message: ruleMessages.flexWrapping(),
190
- node: flexWrappingProps.nodeToReport,
191
- result,
192
- ruleName,
193
- });
194
- }
46
+ flexWrapping({ decl, isLastStyleDeclaration, result });
47
+ }
195
48
 
196
- flexWrappingProps = { ...defaultFlexWrappingProps };
197
- }
49
+ /* GRID LINE NAMES */
50
+ if (options?.['grid-line-names']) {
51
+ gridLineNames({ decl, options, result });
198
52
  }
199
53
 
200
54
  /* SCROLL CHAINING */
201
55
  if (options?.['scroll-chaining']) {
202
- if (
203
- overflowProperties.includes(decl.prop) &&
204
- (decl.value.includes('auto') || decl.value.includes('scroll'))
205
- ) {
206
- scrollChainingProps.hasOverflow = true;
207
- scrollChainingProps.nodeToReport = decl;
208
- }
209
-
210
- if (decl.prop.includes('overscroll-behavior')) {
211
- scrollChainingProps.hasOverscrollBehavior = true;
212
- }
213
-
214
- if (isLastStyleDeclaration) {
215
- if (
216
- scrollChainingProps.hasOverflow &&
217
- !scrollChainingProps.hasOverscrollBehavior
218
- ) {
219
- stylelint.utils.report({
220
- message: ruleMessages.scrollChaining(),
221
- node: scrollChainingProps.nodeToReport,
222
- result,
223
- ruleName,
224
- });
225
- }
226
-
227
- scrollChainingProps = { ...defaultScrollChainingProps };
228
- }
56
+ scrollChaining({ decl, isLastStyleDeclaration, result });
229
57
  }
230
58
 
231
59
  /* SCROLLBAR GUTTER */
232
60
  if (options?.['scrollbar-gutter']) {
233
- if (
234
- overflowProperties.includes(decl.prop) &&
235
- (decl.value.includes('auto') || decl.value.includes('scroll'))
236
- ) {
237
- scrollbarGutterProps.hasOverflow = true;
238
- scrollbarGutterProps.nodeToReport = decl;
239
- }
240
-
241
- if (decl.prop.includes('scrollbar-gutter')) {
242
- scrollbarGutterProps.hasScrollbarGutter = true;
243
- }
244
-
245
- if (isLastStyleDeclaration) {
246
- if (
247
- scrollbarGutterProps.hasOverflow &&
248
- !scrollbarGutterProps.hasScrollbarGutter
249
- ) {
250
- stylelint.utils.report({
251
- message: ruleMessages.scrollbarGutter(),
252
- node: scrollbarGutterProps.nodeToReport,
253
- result,
254
- ruleName,
255
- });
256
- }
257
-
258
- scrollbarGutterProps = { ...defaultScrollbarGutterProps };
259
- }
61
+ scrollbarGutter({ decl, isLastStyleDeclaration, result });
260
62
  }
261
63
 
262
64
  /* VENDOR PREFIX GROUPING */
263
65
  if (options?.['vendor-prefix-grouping']) {
264
- const hasMultiplePrefixes = findVendorPrefixes(decl.parent.selector);
265
-
266
- if (hasMultiplePrefixes) {
267
- stylelint.utils.report({
268
- message: ruleMessages.vendorPrefixWGrouping(),
269
- node: decl.parent,
270
- result,
271
- ruleName,
272
- });
273
- }
66
+ vendorPrefixGrouping({ decl, result });
274
67
  }
275
68
 
276
69
  return;
@@ -0,0 +1,41 @@
1
+ import stylelint from 'stylelint';
2
+ import { ruleMessages, ruleName } from '../base.js';
3
+ import { overflowProperties } from '../../../utils/overflow.js';
4
+
5
+ const defaultScrollChainingProps = {
6
+ hasOverflow: false,
7
+ hasOverscrollBehavior: false,
8
+ nodeToReport: undefined,
9
+ };
10
+
11
+ let scrollChainingProps = { ...defaultScrollChainingProps };
12
+
13
+ export function scrollChaining({ decl, isLastStyleDeclaration, result }) {
14
+ if (
15
+ overflowProperties.includes(decl.prop) &&
16
+ (decl.value.includes('auto') || decl.value.includes('scroll'))
17
+ ) {
18
+ scrollChainingProps.hasOverflow = true;
19
+ scrollChainingProps.nodeToReport = decl;
20
+ }
21
+
22
+ if (decl.prop.includes('overscroll-behavior')) {
23
+ scrollChainingProps.hasOverscrollBehavior = true;
24
+ }
25
+
26
+ if (isLastStyleDeclaration) {
27
+ if (
28
+ scrollChainingProps.hasOverflow &&
29
+ !scrollChainingProps.hasOverscrollBehavior
30
+ ) {
31
+ stylelint.utils.report({
32
+ message: ruleMessages.scrollChaining(),
33
+ node: scrollChainingProps.nodeToReport,
34
+ result,
35
+ ruleName,
36
+ });
37
+ }
38
+
39
+ scrollChainingProps = { ...defaultScrollChainingProps };
40
+ }
41
+ }
@@ -0,0 +1,41 @@
1
+ import stylelint from 'stylelint';
2
+ import { ruleMessages, ruleName } from '../base.js';
3
+ import { overflowProperties } from '../../../utils/overflow.js';
4
+
5
+ const defaultScrollbarGutterProps = {
6
+ hasOverflow: false,
7
+ hasScrollbarGutter: false,
8
+ nodeToReport: undefined,
9
+ };
10
+
11
+ let scrollbarGutterProps = { ...defaultScrollbarGutterProps };
12
+
13
+ export function scrollbarGutter({ decl, isLastStyleDeclaration, result }) {
14
+ if (
15
+ overflowProperties.includes(decl.prop) &&
16
+ (decl.value.includes('auto') || decl.value.includes('scroll'))
17
+ ) {
18
+ scrollbarGutterProps.hasOverflow = true;
19
+ scrollbarGutterProps.nodeToReport = decl;
20
+ }
21
+
22
+ if (decl.prop.includes('scrollbar-gutter')) {
23
+ scrollbarGutterProps.hasScrollbarGutter = true;
24
+ }
25
+
26
+ if (isLastStyleDeclaration) {
27
+ if (
28
+ scrollbarGutterProps.hasOverflow &&
29
+ !scrollbarGutterProps.hasScrollbarGutter
30
+ ) {
31
+ stylelint.utils.report({
32
+ message: ruleMessages.scrollbarGutter(),
33
+ node: scrollbarGutterProps.nodeToReport,
34
+ result,
35
+ ruleName,
36
+ });
37
+ }
38
+
39
+ scrollbarGutterProps = { ...defaultScrollbarGutterProps };
40
+ }
41
+ }
@@ -0,0 +1,16 @@
1
+ import stylelint from 'stylelint';
2
+ import { ruleMessages, ruleName } from '../base.js';
3
+ import { findVendorPrefixes } from './utils.js';
4
+
5
+ export function vendorPrefixGrouping({ decl, result }) {
6
+ const hasMultiplePrefixes = findVendorPrefixes(decl.parent.selector);
7
+
8
+ if (hasMultiplePrefixes) {
9
+ stylelint.utils.report({
10
+ message: ruleMessages.vendorPrefixWGrouping(),
11
+ node: decl.parent,
12
+ result,
13
+ ruleName,
14
+ });
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ export const overflowProperties = [
2
+ 'overflow',
3
+ 'overflow-x',
4
+ 'overflow-y',
5
+ 'overflow-inline',
6
+ 'overflow-block',
7
+ ];