@d-zero/stylelint-rules 5.0.0-alpha.67 → 5.0.0-alpha.69

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/dist/index.js CHANGED
@@ -1,3 +1,10 @@
1
1
  import component from './rules/component/index.js';
2
2
  import declarationValueTypeDisallowedList from './rules/declaration-value-type-disallowed-list/index.js';
3
- export default [component, declarationValueTypeDisallowedList];
3
+ import preferIndividualTransformProperties from './rules/prefer-individual-transform-properties/index.js';
4
+ import shorthandPropertyUseLogical from './rules/shorthand-property-use-logical/index.js';
5
+ export default [
6
+ component,
7
+ declarationValueTypeDisallowedList,
8
+ preferIndividualTransformProperties,
9
+ shorthandPropertyUseLogical,
10
+ ];
@@ -0,0 +1,83 @@
1
+ import postcssValueParser from 'postcss-value-parser';
2
+ import stylelint from 'stylelint';
3
+ import { createRule } from '../../utils/create-rule.js';
4
+ /**
5
+ * Transform functions that can be replaced with individual properties
6
+ */
7
+ const REPLACEABLE_TRANSFORM_FUNCTIONS = {
8
+ // cspell:disable-next-line
9
+ translate: ['translate', 'translatex', 'translatey', 'translate3d'],
10
+ // cspell:disable-next-line
11
+ rotate: ['rotate', 'rotatex', 'rotatey', 'rotatez', 'rotate3d'],
12
+ // cspell:disable-next-line
13
+ scale: ['scale', 'scalex', 'scaley', 'scale3d'],
14
+ };
15
+ /**
16
+ * Check if a transform value contains only functions that can be replaced
17
+ * @param value
18
+ */
19
+ function canBeReplacedWithIndividualProperties(value) {
20
+ const parsed = postcssValueParser(value);
21
+ const suggestions = [];
22
+ const foundTransformTypes = new Set(); // Track types of transforms found
23
+ let hasReplaceableFunction = false;
24
+ let hasNonReplaceableFunction = false;
25
+ parsed.walk((node) => {
26
+ if (node.type === 'function') {
27
+ const functionName = node.value.toLowerCase();
28
+ let isReplaceable = false;
29
+ // Check each category of replaceable functions
30
+ for (const [property, functions] of Object.entries(REPLACEABLE_TRANSFORM_FUNCTIONS)) {
31
+ if (functions.includes(functionName)) {
32
+ isReplaceable = true;
33
+ hasReplaceableFunction = true;
34
+ foundTransformTypes.add(property);
35
+ // Generate suggestion based on function type
36
+ const args = postcssValueParser.stringify(node.nodes);
37
+ suggestions.push(`${property}: ${args}`);
38
+ // Don't walk into the arguments of transform functions
39
+ return false;
40
+ }
41
+ }
42
+ if (!isReplaceable) {
43
+ hasNonReplaceableFunction = true;
44
+ }
45
+ }
46
+ return true;
47
+ });
48
+ // Only suggest replacement if:
49
+ // 1. We found replaceable functions
50
+ // 2. We found no non-replaceable functions
51
+ // 3. We only found ONE type of transform (translate, rotate, or scale)
52
+ const canReplace = hasReplaceableFunction &&
53
+ !hasNonReplaceableFunction &&
54
+ foundTransformTypes.size === 1;
55
+ return {
56
+ canReplace,
57
+ suggestions: canReplace ? suggestions : [],
58
+ };
59
+ }
60
+ export default createRule({
61
+ name: 'prefer-individual-transform-properties',
62
+ rejected: (value, suggestions) => `Use individual transform properties instead of "transform: ${value}". Consider: ${suggestions}`,
63
+ rule: (ruleName, messages) => () => {
64
+ return (root, result) => {
65
+ root.walkDecls((decl) => {
66
+ // Only check transform property
67
+ if (decl.prop.toLowerCase() !== 'transform') {
68
+ return;
69
+ }
70
+ const { canReplace, suggestions } = canBeReplacedWithIndividualProperties(decl.value);
71
+ // If we can replace and have suggestions, report the issue
72
+ if (canReplace && suggestions.length > 0) {
73
+ stylelint.utils.report({
74
+ result,
75
+ ruleName,
76
+ message: messages.rejected(decl.value, suggestions.join(', ')),
77
+ node: decl,
78
+ });
79
+ }
80
+ });
81
+ };
82
+ },
83
+ });
@@ -0,0 +1,137 @@
1
+ import stylelint from 'stylelint';
2
+ import { describe, test, expect } from 'vitest';
3
+ import plugin from './index.js';
4
+ const ruleName = '@d-zero/prefer-individual-transform-properties';
5
+ /**
6
+ * Helper function to run stylelint with the plugin
7
+ * @param code
8
+ * @param options
9
+ */
10
+ async function lint(code, options) {
11
+ const result = await stylelint.lint({
12
+ code,
13
+ config: {
14
+ plugins: [plugin],
15
+ rules: {
16
+ [ruleName]: options || true,
17
+ },
18
+ },
19
+ });
20
+ return result.results[0]?.warnings || [];
21
+ }
22
+ describe('prefer-individual-transform-properties', () => {
23
+ test('should flag simple translate function', async () => {
24
+ const warnings = await lint(`
25
+ .element {
26
+ transform: translate(10px, 20px);
27
+ }
28
+ `);
29
+ expect(warnings).toHaveLength(1);
30
+ expect(warnings[0]?.text).toContain('translate: 10px, 20px');
31
+ });
32
+ test('should flag simple rotate function', async () => {
33
+ const warnings = await lint(`
34
+ .element {
35
+ transform: rotate(45deg);
36
+ }
37
+ `);
38
+ expect(warnings).toHaveLength(1);
39
+ expect(warnings[0]?.text).toContain('rotate: 45deg');
40
+ });
41
+ test('should flag simple scale function', async () => {
42
+ const warnings = await lint(`
43
+ .element {
44
+ transform: scale(1.5);
45
+ }
46
+ `);
47
+ expect(warnings).toHaveLength(1);
48
+ expect(warnings[0]?.text).toContain('scale: 1.5');
49
+ });
50
+ test('should flag translateX function', async () => {
51
+ const warnings = await lint(`
52
+ .element {
53
+ transform: translateX(10px);
54
+ }
55
+ `);
56
+ expect(warnings).toHaveLength(1);
57
+ expect(warnings[0]?.text).toContain('translate: 10px');
58
+ });
59
+ test('should flag rotateY function', async () => {
60
+ const warnings = await lint(`
61
+ .element {
62
+ transform: rotateY(90deg);
63
+ }
64
+ `);
65
+ expect(warnings).toHaveLength(1);
66
+ expect(warnings[0]?.text).toContain('rotate: 90deg');
67
+ });
68
+ test('should flag scaleX function', async () => {
69
+ const warnings = await lint(`
70
+ .element {
71
+ transform: scaleX(2);
72
+ }
73
+ `);
74
+ expect(warnings).toHaveLength(1);
75
+ expect(warnings[0]?.text).toContain('scale: 2');
76
+ });
77
+ test('should not flag complex transforms with multiple different functions', async () => {
78
+ const warnings = await lint(`
79
+ .element {
80
+ transform: translate(10px, 20px) rotate(45deg) scale(1.5);
81
+ }
82
+ `);
83
+ // This contains multiple transform types, so it cannot be easily replaced
84
+ expect(warnings).toHaveLength(0);
85
+ });
86
+ test('should not flag transforms with matrix functions', async () => {
87
+ const warnings = await lint(`
88
+ .element {
89
+ transform: matrix(1, 0, 0, 1, 10, 20);
90
+ }
91
+ `);
92
+ expect(warnings).toHaveLength(0);
93
+ });
94
+ test('should not flag transforms with skew functions', async () => {
95
+ const warnings = await lint(`
96
+ .element {
97
+ transform: skew(20deg, 10deg);
98
+ }
99
+ `);
100
+ expect(warnings).toHaveLength(0);
101
+ });
102
+ test('should not flag transforms mixing replaceable and non-replaceable functions', async () => {
103
+ const warnings = await lint(`
104
+ .element {
105
+ transform: translate(10px, 20px) skew(20deg);
106
+ }
107
+ `);
108
+ expect(warnings).toHaveLength(0);
109
+ });
110
+ test('should not flag non-transform properties', async () => {
111
+ const warnings = await lint(`
112
+ .element {
113
+ transition: transform 0.3s ease;
114
+ will-change: transform;
115
+ }
116
+ `);
117
+ expect(warnings).toHaveLength(0);
118
+ });
119
+ test('should handle CSS variables in transform values', async () => {
120
+ const warnings = await lint(`
121
+ .element {
122
+ transform: translate(var(--x), var(--y));
123
+ }
124
+ `);
125
+ expect(warnings).toHaveLength(1);
126
+ expect(warnings[0]?.text).toContain('translate: var(--x), var(--y)');
127
+ });
128
+ test('should handle calc() in transform values', async () => {
129
+ const warnings = await lint(`
130
+ .element {
131
+ transform: translate(calc(100% - 20px), 0);
132
+ }
133
+ `);
134
+ expect(warnings).toHaveLength(1);
135
+ expect(warnings[0]?.text).toContain('translate: calc(100% - 20px), 0');
136
+ });
137
+ });
@@ -0,0 +1,92 @@
1
+ import postcssValueParser from 'postcss-value-parser';
2
+ import stylelint from 'stylelint';
3
+ // @ts-ignore
4
+ import validateOptions from 'stylelint/lib/utils/validateOptions.mjs';
5
+ // @ts-ignore
6
+ import { isString, isPlainObject } from 'stylelint/lib/utils/validateTypes.mjs';
7
+ import { createRule } from '../../utils/create-rule.js';
8
+ // Properties that have logical equivalents and can use shorthand syntax
9
+ const SHORTHAND_PROPERTIES_WITH_LOGICAL = [
10
+ 'padding',
11
+ 'margin',
12
+ 'border-width',
13
+ 'border-style',
14
+ 'border-color',
15
+ 'scroll-padding',
16
+ 'scroll-margin',
17
+ 'border-radius',
18
+ ];
19
+ // Mapping from physical shorthand to logical equivalents
20
+ const LOGICAL_PROPERTY_MAP = {
21
+ padding: ['padding-block', 'padding-inline'],
22
+ margin: ['margin-block', 'margin-inline'],
23
+ 'border-width': ['border-block-width', 'border-inline-width'],
24
+ 'border-style': ['border-block-style', 'border-inline-style'],
25
+ 'border-color': ['border-block-color', 'border-inline-color'],
26
+ 'scroll-padding': ['scroll-padding-block', 'scroll-padding-inline'],
27
+ 'scroll-margin': ['scroll-margin-block', 'scroll-margin-inline'],
28
+ 'border-radius': [
29
+ 'border-start-start-radius',
30
+ 'border-start-end-radius',
31
+ 'border-end-start-radius',
32
+ 'border-end-end-radius',
33
+ ],
34
+ };
35
+ /**
36
+ *
37
+ * @param value
38
+ */
39
+ function hasMultipleValues(value) {
40
+ const parsed = postcssValueParser(value);
41
+ const values = parsed.nodes.filter((node) => node.type === 'word' || node.type === 'function');
42
+ return values.length > 1;
43
+ }
44
+ export default createRule({
45
+ name: 'shorthand-property-use-logical',
46
+ rejected: (property, logicalProperties) => `Unexpected shorthand property "${property}" with multiple values. Consider using logical properties: ${logicalProperties}`,
47
+ rule: (ruleName, messages) => (primary) => {
48
+ return (root, result) => {
49
+ const validOptions = validateOptions(result, ruleName, {
50
+ actual: primary,
51
+ possible: [
52
+ true,
53
+ false,
54
+ (value) => {
55
+ if (!isPlainObject(value))
56
+ return false;
57
+ const obj = value;
58
+ return (!('properties' in obj) ||
59
+ (Array.isArray(obj.properties) && obj.properties.every(isString)));
60
+ },
61
+ ],
62
+ });
63
+ if (!validOptions || primary === false) {
64
+ return;
65
+ }
66
+ const enabledProperties = typeof primary === 'object' && primary.properties
67
+ ? primary.properties
68
+ : [...SHORTHAND_PROPERTIES_WITH_LOGICAL];
69
+ root.walkDecls((decl) => {
70
+ // Only check properties that are in our enabled list
71
+ if (!enabledProperties.includes(decl.prop)) {
72
+ return;
73
+ }
74
+ // Check if the property has multiple values
75
+ if (!hasMultipleValues(decl.value)) {
76
+ return;
77
+ }
78
+ // Get logical property suggestions
79
+ const logicalProperties = LOGICAL_PROPERTY_MAP[decl.prop];
80
+ if (!logicalProperties) {
81
+ return;
82
+ }
83
+ stylelint.utils.report({
84
+ result,
85
+ ruleName,
86
+ message: messages.rejected(decl.prop, logicalProperties.join(', ')),
87
+ node: decl,
88
+ });
89
+ });
90
+ };
91
+ },
92
+ });
@@ -0,0 +1,162 @@
1
+ import stylelint from 'stylelint';
2
+ import { describe, test, expect } from 'vitest';
3
+ import rule from './index.js';
4
+ const { lint } = stylelint;
5
+ const config = (settings = true) => ({
6
+ plugins: [rule],
7
+ rules: {
8
+ // @ts-ignore
9
+ [rule.ruleName]: settings,
10
+ },
11
+ });
12
+ describe('shorthand-property-use-logical', () => {
13
+ describe('padding', () => {
14
+ test('single value - should not warn', async () => {
15
+ const {
16
+ // @ts-ignore
17
+ results: [{ warnings, parseErrors }], } = await lint({
18
+ code: '* { padding: 2rem }',
19
+ config: config(),
20
+ });
21
+ expect(parseErrors).toHaveLength(0);
22
+ expect(warnings).toHaveLength(0);
23
+ });
24
+ test('two values - should warn', async () => {
25
+ const {
26
+ // @ts-ignore
27
+ results: [{ warnings, parseErrors }], } = await lint({
28
+ code: '* { padding: 2rem 1rem }',
29
+ config: config(),
30
+ });
31
+ expect(parseErrors).toHaveLength(0);
32
+ expect(warnings).toStrictEqual([
33
+ {
34
+ rule: '@d-zero/shorthand-property-use-logical',
35
+ severity: 'error',
36
+ line: 1,
37
+ endLine: 1,
38
+ column: 5,
39
+ endColumn: 23,
40
+ text: 'Unexpected shorthand property "padding" with multiple values. Consider using logical properties: padding-block, padding-inline (@d-zero/shorthand-property-use-logical)',
41
+ url: undefined,
42
+ fix: undefined,
43
+ },
44
+ ]);
45
+ });
46
+ test('three values - should warn', async () => {
47
+ const {
48
+ // @ts-ignore
49
+ results: [{ warnings, parseErrors }], } = await lint({
50
+ code: '* { padding: 2rem 0 0 }',
51
+ config: config(),
52
+ });
53
+ expect(parseErrors).toHaveLength(0);
54
+ expect(warnings).toHaveLength(1);
55
+ expect(warnings[0].text).toContain('Unexpected shorthand property "padding"');
56
+ });
57
+ test('four values - should warn', async () => {
58
+ const {
59
+ // @ts-ignore
60
+ results: [{ warnings, parseErrors }], } = await lint({
61
+ code: '* { padding: 1rem 2rem 3rem 4rem }',
62
+ config: config(),
63
+ });
64
+ expect(parseErrors).toHaveLength(0);
65
+ expect(warnings).toHaveLength(1);
66
+ expect(warnings[0].text).toContain('Unexpected shorthand property "padding"');
67
+ });
68
+ });
69
+ describe('margin', () => {
70
+ test('single value - should not warn', async () => {
71
+ const {
72
+ // @ts-ignore
73
+ results: [{ warnings, parseErrors }], } = await lint({
74
+ code: '* { margin: auto }',
75
+ config: config(),
76
+ });
77
+ expect(parseErrors).toHaveLength(0);
78
+ expect(warnings).toHaveLength(0);
79
+ });
80
+ test('two values - should warn', async () => {
81
+ const {
82
+ // @ts-ignore
83
+ results: [{ warnings, parseErrors }], } = await lint({
84
+ code: '* { margin: 1rem 2rem }',
85
+ config: config(),
86
+ });
87
+ expect(parseErrors).toHaveLength(0);
88
+ expect(warnings).toHaveLength(1);
89
+ expect(warnings[0].text).toContain('margin-block, margin-inline');
90
+ });
91
+ });
92
+ describe('border-width', () => {
93
+ test('single value - should not warn', async () => {
94
+ const {
95
+ // @ts-ignore
96
+ results: [{ warnings, parseErrors }], } = await lint({
97
+ code: '* { border-width: 1px }',
98
+ config: config(),
99
+ });
100
+ expect(parseErrors).toHaveLength(0);
101
+ expect(warnings).toHaveLength(0);
102
+ });
103
+ test('multiple values - should warn', async () => {
104
+ const {
105
+ // @ts-ignore
106
+ results: [{ warnings, parseErrors }], } = await lint({
107
+ code: '* { border-width: 1px 2px }',
108
+ config: config(),
109
+ });
110
+ expect(parseErrors).toHaveLength(0);
111
+ expect(warnings).toHaveLength(1);
112
+ expect(warnings[0].text).toContain('border-block-width, border-inline-width');
113
+ });
114
+ });
115
+ describe('limited properties configuration', () => {
116
+ test('only check specified properties', async () => {
117
+ const {
118
+ // @ts-ignore
119
+ results: [{ warnings, parseErrors }], } = await lint({
120
+ code: '* { padding: 1rem 2rem; margin: 1rem 2rem; }',
121
+ config: config({ properties: ['padding'] }),
122
+ });
123
+ expect(parseErrors).toHaveLength(0);
124
+ expect(warnings).toHaveLength(1);
125
+ expect(warnings[0].text).toContain('padding');
126
+ });
127
+ });
128
+ describe('unsupported properties', () => {
129
+ test('should not check properties not in the list', async () => {
130
+ const {
131
+ // @ts-ignore
132
+ results: [{ warnings, parseErrors }], } = await lint({
133
+ code: '* { background: url(a.png) no-repeat }',
134
+ config: config(),
135
+ });
136
+ expect(parseErrors).toHaveLength(0);
137
+ expect(warnings).toHaveLength(0);
138
+ });
139
+ });
140
+ describe('complex values', () => {
141
+ test('calc() function with multiple values', async () => {
142
+ const {
143
+ // @ts-ignore
144
+ results: [{ warnings, parseErrors }], } = await lint({
145
+ code: '* { padding: calc(1rem + 2px) 1rem }',
146
+ config: config(),
147
+ });
148
+ expect(parseErrors).toHaveLength(0);
149
+ expect(warnings).toHaveLength(1);
150
+ });
151
+ test('var() function with multiple values', async () => {
152
+ const {
153
+ // @ts-ignore
154
+ results: [{ warnings, parseErrors }], } = await lint({
155
+ code: '* { margin: var(--spacing) 2rem }',
156
+ config: config(),
157
+ });
158
+ expect(parseErrors).toHaveLength(0);
159
+ expect(warnings).toHaveLength(1);
160
+ });
161
+ });
162
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d-zero/stylelint-rules",
3
- "version": "5.0.0-alpha.67",
3
+ "version": "5.0.0-alpha.69",
4
4
  "description": "Rules of Stylelint for D-ZERO",
5
5
  "repository": "https://github.com/d-zero-dev/linters.git",
6
6
  "author": "D-ZERO Co., Ltd.",
@@ -22,14 +22,14 @@
22
22
  "build": "tsc"
23
23
  },
24
24
  "dependencies": {
25
- "@d-zero/csstree-scss-syntax": "5.0.0-alpha.67",
25
+ "@d-zero/csstree-scss-syntax": "5.0.0-alpha.69",
26
26
  "css-tree": "3.1.0",
27
27
  "postcss-selector-parser": "7.1.0",
28
28
  "postcss-value-parser": "4.2.0",
29
- "stylelint": "16.21.0"
29
+ "stylelint": "16.21.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "postcss": "8.5.6"
33
33
  },
34
- "gitHead": "b0981c8d25ed356c5abaf5ab43d92401b6d55667"
34
+ "gitHead": "8527568567fd3b4030906fbdebe06720e606f7b8"
35
35
  }