Skip to content

Commit 2a440ce

Browse files
authored
feat: add prefer-logical-properties rule (#63)
* feat: add prefer-logical-properties rule * perf: move to maps in prefer-logical-properties rule * feat: add allowes properties and units option in prefer-logical-properties * refactor: rename options in prefer-logical-properties rule * docs: add options in prefer-logical-properties * refactor: define fixable value as const * docs: improve prefer-logical-property docs * fix: prevent testing in supports declaration
1 parent 3ea5047 commit 2a440ce

File tree

5 files changed

+403
-8
lines changed

5 files changed

+403
-8
lines changed

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,15 @@ export default [
5656

5757
<!-- Rule Table Start -->
5858

59-
| **Rule Name** | **Description** | **Recommended** |
60-
| :--------------------------------------------------------------- | :----------------------------------- | :-------------: |
61-
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
62-
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
63-
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
64-
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
65-
| [`require-baseline`](./docs/rules/require-baseline.md) | Enforce the use of baseline features | yes |
66-
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
59+
| **Rule Name** | **Description** | **Recommended** |
60+
| :------------------------------------------------------------------------- | :------------------------------------ | :-------------: |
61+
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
62+
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
63+
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
64+
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
65+
| [`prefer-logical-properties`](./docs//rules//prefer-logical-properties.md) | Enforce the use of logical properties | no |
66+
| [`require-baseline`](./docs/rules/require-baseline.md) | Enforce the use of baseline features | yes |
67+
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
6768

6869
<!-- Rule Table End -->
6970

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# prefer-logical-properties
2+
3+
Prefer logical properties over physical properties.
4+
5+
## Background
6+
7+
Logical properties are a set of CSS properties that map to their physical counterparts. They are designed to make it easier to create styles that work in both left-to-right and right-to-left languages. Logical properties are useful for creating styles that are more flexible and easier to maintain.
8+
9+
## Rule Details
10+
11+
This rule checks for the use of physical properties and suggests using their logical counterparts instead.
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```css
16+
/* incorrect use of physical properties */
17+
a {
18+
margin-left: 10px;
19+
}
20+
```
21+
22+
Examples of **correct** code for this rule:
23+
24+
```css
25+
a {
26+
margin-inline-start: 10px;
27+
}
28+
```
29+
30+
### Options
31+
32+
This rule accepts an option object with the following properties:
33+
34+
- `allowProperties` (default: `[]`) - Specify an array of physical properties that are allowed to be used.
35+
- `allowUnits` (default: `[]`) - Specify an array of physical units that are allowed to be used.
36+
37+
## When Not to Use It
38+
39+
If you aren't concerned with the use of logical properties, then you can safely disable this rule.
40+
41+
## Prior Art
42+
43+
- [stylelint-use-logical](https://github.com/csstools/stylelint-use-logical)

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import noEmptyBlocks from "./rules/no-empty-blocks.js";
1313
import noDuplicateImports from "./rules/no-duplicate-imports.js";
1414
import noInvalidProperties from "./rules/no-invalid-properties.js";
1515
import noInvalidAtRules from "./rules/no-invalid-at-rules.js";
16+
import preferLogicalProperties from "./rules/prefer-logical-properties.js";
1617
import useLayers from "./rules/use-layers.js";
1718
import requireBaseline from "./rules/require-baseline.js";
1819

@@ -33,6 +34,7 @@ const plugin = {
3334
"no-duplicate-imports": noDuplicateImports,
3435
"no-invalid-at-rules": noInvalidAtRules,
3536
"no-invalid-properties": noInvalidProperties,
37+
"prefer-logical-properties": preferLogicalProperties,
3638
"use-layers": useLayers,
3739
"require-baseline": requireBaseline,
3840
},
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
//-----------------------------------------------------------------------------
2+
// Helpers
3+
//-----------------------------------------------------------------------------
4+
5+
const propertiesReplacements = new Map([
6+
["bottom", "inset-block-end"],
7+
["border-bottom", "border-block-end"],
8+
["border-bottom-color", "border-block-end-color"],
9+
["border-bottom-left-radius", "border-end-start-radius"],
10+
["border-bottom-right-radius", "border-end-end-radius"],
11+
["border-bottom-style", "border-block-end-style"],
12+
["border-bottom-width", "border-block-end-width"],
13+
["border-left", "border-inline-start"],
14+
["border-left-color", "border-inline-start-color"],
15+
["border-left-style", "border-inline-start-style"],
16+
["border-left-width", "border-inline-start-width"],
17+
["border-right", "border-inline-end"],
18+
["border-right-color", "border-inline-end-color"],
19+
["border-right-style", "border-inline-end-style"],
20+
["border-right-width", "border-inline-end-width"],
21+
["border-top", "border-block-start"],
22+
["border-top-color", "border-block-start-color"],
23+
["border-top-left-radius", "border-start-start-radius"],
24+
["border-top-right-radius", "border-start-end-radius"],
25+
["border-top-style", "border-block-start-style"],
26+
["border-top-width", "border-block-start-width"],
27+
["contain-intrinsic-height", "contain-intrinsic-block-size"],
28+
["contain-intrinsic-width", "contain-intrinsic-inline-size"],
29+
["height", "block-size"],
30+
["left", "inset-inline-start"],
31+
["margin-bottom", "margin-block-end"],
32+
["margin-left", "margin-inline-start"],
33+
["margin-right", "margin-inline-end"],
34+
["margin-top", "margin-block-start"],
35+
["max-height", "max-block-size"],
36+
["max-width", "max-inline-size"],
37+
["min-height", "min-block-size"],
38+
["min-width", "min-inline-size"],
39+
["overflow-x", "overflow-inline"],
40+
["overflow-y", "overflow-block"],
41+
["overscroll-behavior-x", "overscroll-behavior-inline"],
42+
["overscroll-behavior-y", "overscroll-behavior-block"],
43+
["padding-bottom", "padding-block-end"],
44+
["padding-left", "padding-inline-start"],
45+
["padding-right", "padding-inline-end"],
46+
["padding-top", "padding-block-start"],
47+
["right", "inset-inline-end"],
48+
["scroll-margin-bottom", "scroll-margin-block-end"],
49+
["scroll-margin-left", "scroll-margin-inline-start"],
50+
["scroll-margin-right", "scroll-margin-inline-end"],
51+
["scroll-margin-top", "scroll-margin-block-start"],
52+
["scroll-padding-bottom", "scroll-padding-block-end"],
53+
["scroll-padding-left", "scroll-padding-inline-start"],
54+
["scroll-padding-right", "scroll-padding-inline-end"],
55+
["scroll-padding-top", "scroll-padding-block-start"],
56+
["top", "inset-block-start"],
57+
["width", "inline-size"],
58+
]);
59+
60+
const propertyValuesReplacements = new Map([
61+
[
62+
"text-align",
63+
{
64+
left: "start",
65+
right: "end",
66+
},
67+
],
68+
[
69+
"resize",
70+
{
71+
horizontal: "inline",
72+
vertical: "block",
73+
},
74+
],
75+
[
76+
"caption-side",
77+
{
78+
left: "inline-start",
79+
right: "inline-end",
80+
},
81+
],
82+
[
83+
"box-orient",
84+
{
85+
horizontal: "inline-axis",
86+
vertical: "block-axis",
87+
},
88+
],
89+
[
90+
"float",
91+
{
92+
left: "inline-start",
93+
right: "inline-end",
94+
},
95+
],
96+
[
97+
"clear",
98+
{
99+
left: "inline-start",
100+
right: "inline-end",
101+
},
102+
],
103+
]);
104+
105+
const unitReplacements = new Map([
106+
["cqh", "cqb"],
107+
["cqw", "cqi"],
108+
["dvh", "dvb"],
109+
["dvw", "dvi"],
110+
["lvh", "lvb"],
111+
["lvw", "lvi"],
112+
["svh", "svb"],
113+
["svw", "svi"],
114+
["vh", "vb"],
115+
["vw", "vi"],
116+
]);
117+
118+
//-----------------------------------------------------------------------------
119+
// Rule Definition
120+
//-----------------------------------------------------------------------------
121+
export default {
122+
meta: {
123+
type: /** @type {const} */ ("problem"),
124+
125+
fixable: /** @type {const} */ ("code"),
126+
127+
docs: {
128+
description: "Enforce the use of logical properties",
129+
url: "https://github.com/eslint/css/blob/main/docs/rules/prefer-logical-properties.md",
130+
},
131+
132+
schema: [
133+
{
134+
type: "object",
135+
properties: {
136+
allowProperties: {
137+
type: "array",
138+
items: {
139+
type: "string",
140+
},
141+
},
142+
allowUnits: {
143+
type: "array",
144+
items: {
145+
type: "string",
146+
},
147+
},
148+
},
149+
additionalProperties: false,
150+
},
151+
],
152+
153+
defaultOptions: [
154+
{
155+
allowProperties: [],
156+
allowUnits: [],
157+
},
158+
],
159+
160+
messages: {
161+
notLogicalProperty:
162+
"Expected logical property '{{replacement}}' instead of '{{property}}'.",
163+
notLogicalValue:
164+
"Expected logical value '{{replacement}}' instead of '{{value}}'.",
165+
notLogicalUnit:
166+
"Expected logical unit '{{replacement}}' instead of '{{unit}}'.",
167+
},
168+
},
169+
170+
create(context) {
171+
return {
172+
Declaration(node) {
173+
const parent = context.sourceCode.getParent(node);
174+
if (parent.type === "SupportsDeclaration") {
175+
return;
176+
}
177+
178+
const allowProperties = context.options[0].allowProperties;
179+
if (
180+
propertiesReplacements.get(node.property) &&
181+
!allowProperties.includes(node.property)
182+
) {
183+
context.report({
184+
loc: node.loc,
185+
messageId: "notLogicalProperty",
186+
data: {
187+
property: node.property,
188+
replacement: propertiesReplacements.get(
189+
node.property,
190+
),
191+
},
192+
});
193+
}
194+
195+
if (
196+
propertyValuesReplacements.get(node.property) &&
197+
node.value.children[0].type === "Identifier"
198+
) {
199+
const nodeValue = node.value.children[0].name;
200+
if (
201+
propertyValuesReplacements.get(node.property)[nodeValue]
202+
) {
203+
const replacement = propertyValuesReplacements.get(
204+
node.property,
205+
)[nodeValue];
206+
if (replacement) {
207+
context.report({
208+
loc: node.value.children[0].loc,
209+
messageId: "notLogicalValue",
210+
data: {
211+
value: nodeValue,
212+
replacement,
213+
},
214+
});
215+
}
216+
}
217+
}
218+
},
219+
Dimension(node) {
220+
const allowUnits = context.options[0].allowUnits;
221+
if (
222+
unitReplacements.get(node.unit) &&
223+
!allowUnits.includes(node.unit)
224+
) {
225+
context.report({
226+
loc: node.loc,
227+
messageId: "notLogicalUnit",
228+
data: {
229+
unit: node.unit,
230+
replacement: unitReplacements.get(node.unit),
231+
},
232+
});
233+
}
234+
},
235+
};
236+
},
237+
};

0 commit comments

Comments
 (0)