Skip to content

Commit 08ad152

Browse files
authored
Merge pull request #20 from elisecodedestrucs/prefer-shorthand-css-notations
add rule prefer-shorthand-css-notations
2 parents f301597 + 4b5c7d0 commit 08ad152

File tree

5 files changed

+347
-0
lines changed

5 files changed

+347
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- [#21](https://github.com/green-code-initiative/ecoCode-javascript/pull/21) Add rule `@ecocode/avoid-css-animations`
1414
- [#18](https://github.com/green-code-initiative/ecoCode-javascript/pull/18) Add rule `@ecocode/limit-db-query-results`
1515
- [#19](https://github.com/green-code-initiative/ecoCode-javascript/pull/19) Add rule `@ecocode/no-empty-image-src-attribute`
16+
- [#20](https://github.com/green-code-initiative/ecoCode-javascript/pull/20) Add rule `@ecocode/prefer-shorthand-css-notations`
1617
- [#22](https://github.com/green-code-initiative/ecoCode-javascript/pull/22) Add rule `@ecocode/provide-print-css`
1718
- [#25](https://github.com/green-code-initiative/ecoCode-javascript/pull/25) Add license headers
1819
- [ecoCode#207](https://github.com/green-code-initiative/ecoCode/issues/207) Add release tag analyzis on SonarCloud

eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Add `@ecocode` to the `plugins` section of your `.eslintrc`, followed by rules c
6666
| [no-multiple-access-dom-element](docs/rules/no-multiple-access-dom-element.md) | Disallow multiple access of same DOM element. ||
6767
| [no-multiple-style-changes](docs/rules/no-multiple-style-changes.md) | Disallow multiple style changes at once. ||
6868
| [prefer-collections-with-pagination](docs/rules/prefer-collections-with-pagination.md) | Prefer API collections with pagination. ||
69+
| [prefer-shorthand-css-notations](docs/rules/prefer-shorthand-css-notations.md) | Encourage usage of shorthand CSS notations ||
6970
| [provide-print-css](docs/rules/provide-print-css.md) | Enforce providing a print stylesheet ||
7071

7172
<!-- end auto-generated rules list -->
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Encourage usage of shorthand CSS notations (`@ecocode/prefer-shorthand-css-notations`)
2+
3+
⚠️ This rule _warns_ in the ✅ `recommended` config.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
## Rule Details
8+
9+
This rule aims to encourage the use of shorthand CSS notations to reduce stylesheets size.
10+
11+
## Options
12+
13+
You can disable specific properties from being scanned if it does not match your needs.
14+
To disable properties you need to modify your .eslintrc.js by adding the following rule configuration:
15+
16+
```js
17+
module.exports = {
18+
...yourConf,
19+
rules: {
20+
"prefer-shorthand-css-notations": [
21+
"warn",
22+
// disable analyze of "animation-*" properties
23+
{ disableProperties: ["animation"] },
24+
],
25+
},
26+
};
27+
```
28+
29+
## Examples
30+
31+
Examples of **non-compliant** code for this rule:
32+
33+
```js
34+
<div
35+
style={{
36+
marginTop: "1em",
37+
marginRight: 0,
38+
marginBottom: "2em",
39+
marginLeft: "0.5em",
40+
}}
41+
>
42+
{/* Your content here */}
43+
</div>
44+
```
45+
46+
```js
47+
<div
48+
style={{
49+
transitionProperty: "width",
50+
transitionDuration: "35s",
51+
transitionTimingFunction: "ease-in-out",
52+
transitionDelay: "0s",
53+
}}
54+
>
55+
{/* Your content here */}
56+
</div>
57+
```
58+
59+
Examples of **compliant** code for this rule:
60+
61+
```js
62+
<div style={{ margin: "1em 0 2em 0.5em" }}>{/* Your content here */}</div>
63+
```
64+
65+
```js
66+
<div style={{ transition: "width 35s ease-in-out 0s" }}>
67+
{/* Your content here */}
68+
</div>
69+
```
70+
71+
## Further reading
72+
73+
- [cnumr/best-practices/Use abbreviated CSS notations](https://github.com/cnumr/best-practices/blob/fc5a1f865bafb196e4775cce8835393751d40ed8/chapters/BP_026_en.md)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* ecoCode JavaScript plugin - Provides rules to reduce the environmental footprint of your JavaScript programs
3+
* Copyright © 2023 Green Code Initiative (https://www.ecocode.io)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
"use strict";
20+
21+
/** @type {import('eslint').Rule.RuleModule} */
22+
module.exports = {
23+
meta: {
24+
type: "suggestion",
25+
docs: {
26+
description: "Encourage usage of shorthand CSS notations",
27+
category: "eco-design",
28+
recommended: "warn",
29+
},
30+
messages: {
31+
PreferShorthandCSSNotation:
32+
"Prefer the shorthand CSS notation {{property}}",
33+
},
34+
schema: [
35+
{
36+
type: "object",
37+
properties: {
38+
disableProperties: {
39+
type: "array",
40+
items: {
41+
type: "string",
42+
},
43+
},
44+
},
45+
additionalProperties: false,
46+
},
47+
],
48+
},
49+
create: function (context) {
50+
const shorthandProperties = {
51+
animation: ["animationName", "animationDuration"],
52+
background: [
53+
"backgroundColor",
54+
"backgroundImage",
55+
"backgroundPosition",
56+
"backgroundRepeat",
57+
],
58+
border: ["borderColor", "borderStyle", "borderWidth"],
59+
column: ["columnCount", "columnWidth"],
60+
columnRule: ["columnRuleColor", "columnRuleStyle", "columnRuleWidth"],
61+
flex: ["flexBasis", "flexGrow", "flexShrink"],
62+
font: ["fontFamily", "fontSize", "fontStyle"],
63+
grid: ["gridAutoColumns", "gridAutoFlow", "gridAutoRows"],
64+
gridTemplate: ["gridTemplateColumns", "gridTemplateRows"],
65+
listStyle: ["listStyleImage", "listStylePosition", "listStyleType"],
66+
margin: ["marginTop", "marginRight", "marginBottom", "marginLeft"],
67+
offset: ["offsetPath", "offsetPosition"],
68+
outline: ["outlineStyle", "outlineWidth", "outlineColor"],
69+
overflow: ["overflowX", "overflowY"],
70+
padding: ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"],
71+
placeContent: ["alignContent", "justifyContent"],
72+
placeItems: ["alignItems", "justifyItems"],
73+
placeSelf: ["alignSelf", "justifySelf"],
74+
textDecoration: [
75+
"textDecorationColor",
76+
"textDecorationLine",
77+
"textDecorationStyle",
78+
],
79+
transition: ["transitionProperty", "transitionDuration"],
80+
};
81+
82+
const disabledProperties = context.options?.[0]?.disableProperties ?? [];
83+
84+
return {
85+
JSXOpeningElement(node) {
86+
const styleAttribute = node.attributes.find(
87+
(attr) => attr.name.name === "style",
88+
);
89+
if (styleAttribute) {
90+
const nodePropertyNames =
91+
styleAttribute.value.expression.properties.map(
92+
(property) => property.key.name,
93+
);
94+
95+
for (const [shorthandProp, matchProperties] of Object.entries(
96+
shorthandProperties,
97+
)) {
98+
if (
99+
!disabledProperties.includes(shorthandProp) &&
100+
matchProperties.every((prop) => nodePropertyNames.includes(prop))
101+
) {
102+
return context.report({
103+
node: styleAttribute,
104+
messageId: "PreferShorthandCSSNotation",
105+
data: { property: shorthandProp },
106+
});
107+
}
108+
}
109+
}
110+
},
111+
};
112+
},
113+
};
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* ecoCode JavaScript plugin - Provides rules to reduce the environmental footprint of your JavaScript programs
3+
* Copyright © 2023 Green Code Initiative (https://www.ecocode.io)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
"use strict";
20+
21+
//------------------------------------------------------------------------------
22+
// Requirements
23+
//------------------------------------------------------------------------------
24+
25+
const rule = require("../../../lib/rules/prefer-shorthand-css-notations");
26+
const RuleTester = require("eslint").RuleTester;
27+
28+
//------------------------------------------------------------------------------
29+
// Tests
30+
//------------------------------------------------------------------------------
31+
32+
const ruleTester = new RuleTester({
33+
parserOptions: {
34+
ecmaVersion: 2021,
35+
sourceType: "module",
36+
ecmaFeatures: {
37+
jsx: true,
38+
},
39+
},
40+
});
41+
42+
const createError = (property) => ({
43+
messageId: "PreferShorthandCSSNotation",
44+
data: { property },
45+
type: "JSXAttribute",
46+
});
47+
48+
ruleTester.run("prefer-shorthand-css-notations", rule, {
49+
valid: [
50+
"<div class='my-class'/>",
51+
"<div style={{ animation: 'example 5s linear 2s infinite alternate' }}/>",
52+
"<div style={{ background: 'border-box red' }}/>",
53+
"<div style={{ border: '2px dotted' }}/>",
54+
"<div style={{ column: '3 100px' }}/>",
55+
"<div style={{ columnRule: '4px double #ff00ff' }}/>",
56+
"<div style={{ flex: '1 0 auto' }}/>",
57+
"<h1 style={{ font: 'italic bold 18px/150% Arial, sans-serif' }}/>",
58+
"<div style={{ grid: '1fr 2fr / row minmax(100px, auto) 200px' }}/>",
59+
"<div style={{ gridTemplate: '1fr 2fr' }}/>",
60+
"<div style={{ justifyItems: 'stretch' }}/>",
61+
"<div style={{ listStyle: 'georgian inside' }}/>",
62+
"<div style={{ margin: '10px 3px 8px 5px' }}/>",
63+
"<div style={{ offset: 'path(M 50 80 C 150 -20 250 180 350 80) 150px' }}/>",
64+
"<div style={{ outline: 'inset thick' }}/>",
65+
"<div style={{ overflow: 'visible hidden' }}/>",
66+
"<div style={{ padding: '10px 3px 8px 5px' }}/>",
67+
"<div style={{ placeContent: 'end stretch' }}/>",
68+
"<div style={{ placeItems: 'end stretch' }}/>",
69+
"<div style={{ placeSelf: 'end stretch' }}/>",
70+
"<div style={{ textDecoration: 'underline solid #f00' }}/>",
71+
"<div style={{ transition: 'width 35s ease-in-out 0s' }}/>",
72+
{
73+
code: "<div style={{ animationName: 'example', animationDuration: '5s' }}/>",
74+
options: [{ disableProperties: ["animation"] }],
75+
},
76+
],
77+
invalid: [
78+
{
79+
code: "<div style={{ animationName: 'example', animationDuration: '5s' }}/>",
80+
errors: [createError("animation")],
81+
},
82+
{
83+
code: "<div style={{ backgroundColor:'#000', backgroundImage: 'url(images/bg.png)', backgroundRepeat: 'no-repeat', backgroundPosition:'left top' }}/>",
84+
errors: [createError("background")],
85+
},
86+
{
87+
code: "<div style={{ borderWidth: 1, borderStyle: 'solid', borderColor: '#000000' }}/>",
88+
errors: [createError("border")],
89+
},
90+
{
91+
code: "<div style={{ columnWidth: '100px', columnCount: 3 }}/>",
92+
errors: [createError("column")],
93+
},
94+
{
95+
code: "<div style={{ columnRuleWidth: '4px', columnRuleStyle: 'double', columnRuleColor:'#ff00ff'}}/>",
96+
errors: [createError("columnRule")],
97+
},
98+
{
99+
code: "<div style={{ flexGrow: 1, flexShrink: 0, flexBasis: 'auto' }}/>",
100+
errors: [createError("flex")],
101+
},
102+
{
103+
code: "<h1 style={{ fontStyle: 'normal', fontWeight: 'bold', fontSize: 18, lineHeight: '150%', fontFamily: 'Arial,sans-serif' }}/>",
104+
errors: [createError("font")],
105+
},
106+
{
107+
code: "<div style={{ gridTemplateColumns: '1fr', gridAutoFlow: 'row', gridAutoRows:'minmax(100px, auto)', gridAutoColumns: '200px' }}/>",
108+
errors: [createError("grid")],
109+
},
110+
{
111+
code: "<div style={{ gridTemplateColumns: '1fr', gridTemplateRows: '2fr' }}/>",
112+
errors: [createError("gridTemplate")],
113+
},
114+
{
115+
code: "<ul style={{ listStyleType: 'disc', listStylePosition: 'inside', listStyleImage: 'url(disc.png)' }}/>",
116+
errors: [createError("listStyle")],
117+
},
118+
{
119+
code: "<div style={{ marginTop: 10, marginBottom: 8, marginRight: 3, marginLeft: 5 }}/>",
120+
errors: [createError("margin")],
121+
},
122+
{
123+
code: "<h1 style={{ offsetPath: 'path(M 50 80 C 150 -20 250 180 350 80)', offsetPosition: '50%' }}/>",
124+
errors: [createError("offset")],
125+
},
126+
{
127+
code: " <div style={{ outlineWidth: 1, outlineStyle: 'solid', outlineColor: '#000000' }}/>",
128+
errors: [createError("outline")],
129+
},
130+
{
131+
code: "<div style={{ overflowX: 'visible', overflowY: 'hidden' }}/>",
132+
errors: [createError("overflow")],
133+
},
134+
{
135+
code: "<div style={{ paddingTop: 10, paddingBottom: 8, paddingRight: 3, paddingLeft: 5 }}/>",
136+
errors: [createError("padding")],
137+
},
138+
{
139+
code: "<div style={{ alignContent: 'end', justifyContent: 'stretch' }}/>",
140+
errors: [createError("placeContent")],
141+
},
142+
{
143+
code: "<div style={{ alignItems: 'end', justifyItems: 'stretch' }}/>",
144+
errors: [createError("placeItems")],
145+
},
146+
{
147+
code: "<div style={{ alignSelf: 'end', justifySelf: 'stretch' }}/>",
148+
errors: [createError("placeSelf")],
149+
},
150+
{
151+
code: "<div style={{ textDecorationStyle: 'solid', textDecorationColor: '#f00', textDecorationLine: 'underline' }}/>",
152+
errors: [createError("textDecoration")],
153+
},
154+
{
155+
code: "<div style={{ transitionProperty: 'width', transitionDuration:'35s' }}/>",
156+
errors: [createError("transition")],
157+
},
158+
],
159+
});

0 commit comments

Comments
 (0)