Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- [#107](https://github.com/green-code-initiative/creedengo-javascript/pull/107) Move to creedengo-rules-specifications v3
- [#115](https://github.com/green-code-initiative/creedengo-javascript/pull/115) Add Vue SFC template support to JSX‑based rules (avoid-autoplay, no-empty-image-src-attribute, prefer-lighter-formats-for-image-files, avoid-css-animations, prefer-shorthand-css-notations)
- [#115](https://github.com/green-code-initiative/creedengo-javascript/pull/115) Add Vue RuleTester cases and test-project Vue examples for the updated rules

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ services:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
POSTGRES_DB: sonarqube
PGDATA: pg_data:/var/lib/postgresql/data/pgdata
PGDATA: /var/lib/postgresql/data/pgdata
healthcheck:
test: ["CMD-SHELL", "pg_isready -U sonar -d sonarqube"]
interval: 5s
Expand Down
7 changes: 7 additions & 0 deletions eslint-plugin/docs/rules/avoid-autoplay.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ return (

This rule is build for [React](https://react.dev/) and JSX.

```jsx
<template>
Comment thread
neptia marked this conversation as resolved.
<video autoplay></video> <!-- Non-compliant -->
<video preload="none"></video> <!-- Compliant -->
</template>
```

## Resources

### Documentation
Expand Down
9 changes: 9 additions & 0 deletions eslint-plugin/docs/rules/avoid-css-animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ Limiting the usage of CSS animations helps in creating a more energy-efficient a
<div style={{ border: "1px solid black" }} /> // Compliant
```

```jsx
<template>
<div style="border: 1px solid black; transition: border 2s ease;"></div> <!-- Non-compliant -->
<div style="border: 1px solid black;"></div> <!-- Compliant -->
</template>
```

Vue support only checks static style attributes; :style bindings are not validated.

It's important to note that while limiting animations is generally advisable for certain scenarios, there are cases
where animations contribute positively to the user experience and overall design.
In this case they should be limited to the CSS properties `opacity` and `transform` with it's associated
Expand Down
8 changes: 8 additions & 0 deletions eslint-plugin/docs/rules/no-empty-image-src-attribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ return (

This rule is build for [React](https://react.dev/) and JSX.

```jsx
<template>
<img src="" /> <!-- Non-compliant -->
<img /> <!-- Non-compliant -->
<img src="./logo.svg" /> <!-- Compliant -->
</template>
```

## Resources

### Documentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ is supported, the image.webp image will be downloaded; otherwise, image.jpg imag
</picture>
```

```jsx
<template>
<img src="./assets/cat.jpg" alt="Unoptimized image of a cat" /> <!-- Non-compliant -->
<img src="./assets/cat.webp" alt="Optimized image of a cat" /> <!-- Compliant -->
</template>
```

Also remember to consider browser compatibility.
Older browsers may not recognize .webp/.avif images and fail to display them.
To address this issue, you can supply multiple formats for the same image.
Expand Down
9 changes: 9 additions & 0 deletions eslint-plugin/docs/rules/prefer-shorthand-css-notations.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ For example, if you only want to set the left margin, you must continue to use `
</div>
```

```jsx
<template>
<div style="margin-top: 1em; margin-right: 0; margin-bottom: 2em; margin-left: 0.5em;"></div> <!-- Non-compliant -->
<div style="margin: 1em 0 2em 0.5em;"></div> <!-- Compliant -->
</template>
```

Vue support only checks static style attributes; :style bindings are not validated.

This optimization works for a number of
properties [listed here](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties#see_also).

Expand Down
78 changes: 56 additions & 22 deletions eslint-plugin/lib/rules/avoid-autoplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,59 @@ module.exports = {
schema: [],
},
create(context) {
const reportAutoplay = (autoplayAttr, preloadAttr, preloadValue, fallback) => {
if (autoplayAttr && preloadValue !== "none") {
context.report({
node: autoplayAttr || preloadAttr,
messageId: "NoAutoplayAndEnforcePreloadNone",
});
return;
}

if (autoplayAttr) {
context.report({
node: autoplayAttr,
messageId: "NoAutoplay",
});
}

if (!preloadAttr || preloadValue !== "none") {
context.report({
node: preloadAttr || fallback,
messageId: "EnforcePreloadNone",
});
}
};

const parserServices =
context.parserServices || context.sourceCode?.parserServices;

const vueTemplateVisitor = parserServices?.defineTemplateBodyVisitor
? parserServices.defineTemplateBodyVisitor({
VElement(node) {
const rawName =
typeof node.name === "string"
? node.name
: node.name?.name || node.rawName;
const name = rawName?.toLowerCase();
if (name !== "video" && name !== "audio") return;

const getAttr = (attrName) =>
node.startTag.attributes.find(
(attr) =>
attr.type === "VAttribute" &&
attr.key?.name?.toLowerCase() === attrName,
);

const autoplayAttr = getAttr("autoplay");
const preloadAttr = getAttr("preload");
const preloadValue = preloadAttr?.value?.value;

reportAutoplay(autoplayAttr, preloadAttr, preloadValue, node);
},
})
: {};

return {
JSXOpeningElement(node) {
if (node.name.name === "video" || node.name.name === "audio") {
Expand All @@ -45,31 +98,12 @@ module.exports = {
const preloadAttr = node.attributes.find(
(attr) => attr.name?.name.toLowerCase() === "preload",
);
if (
autoplayAttr &&
(!preloadAttr || preloadAttr.value.value !== "none")
) {
context.report({
node: autoplayAttr || preloadAttr,
messageId: "NoAutoplayAndEnforcePreloadNone",
});
} else {
if (autoplayAttr) {
context.report({
node: autoplayAttr,
messageId: "NoAutoplay",
});
}
const preloadValue = preloadAttr?.value?.value;

if (!preloadAttr || preloadAttr.value.value !== "none") {
context.report({
node: preloadAttr || node,
messageId: "EnforcePreloadNone",
});
}
}
reportAutoplay(autoplayAttr, preloadAttr, preloadValue, node);
}
},
...vueTemplateVisitor,
};
},
};
30 changes: 29 additions & 1 deletion eslint-plugin/lib/rules/avoid-css-animations.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,35 @@ module.exports = {
},
schema: [],
},
create(context) {
create(context) {
const forbiddenProperties = ["transition", "animation"];

const parserServices =
context.parserServices || context.sourceCode?.parserServices;

const vueTemplateVisitor = parserServices?.defineTemplateBodyVisitor
? parserServices.defineTemplateBodyVisitor({
VElement(node) {
const styleAttr = node.startTag.attributes.find(
(attr) => attr.type === "VAttribute" && attr.key?.name === "style",
);
const styleValue = styleAttr?.value?.value;
if (!styleValue) return;

const matched = forbiddenProperties.find((prop) =>
new RegExp(`(^|;)\\s*${prop}\\s*:`, "i").test(styleValue),
);
if (!matched) return;

context.report({
node: styleAttr,
messageId: "AvoidCSSAnimations",
data: { attribute: matched },
});
},
})
: {};

return {
JSXOpeningElement(node) {
const styleAttribute = node.attributes.find(
Expand All @@ -60,6 +87,7 @@ module.exports = {
}
}
},
...vueTemplateVisitor,
};
},
};
29 changes: 29 additions & 0 deletions eslint-plugin/lib/rules/no-empty-image-src-attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,34 @@ module.exports = {
schema: [],
},
create(context) {
const parserServices =
context.parserServices || context.sourceCode?.parserServices;

const vueTemplateVisitor = parserServices?.defineTemplateBodyVisitor
? parserServices.defineTemplateBodyVisitor({
VElement(node) {
const rawName =
typeof node.name === "string"
? node.name
: node.name?.name || node.rawName;
const name = rawName?.toLowerCase();
if (name !== "img") return;

const srcAttr = node.startTag.attributes.find(
(attr) => attr.type === "VAttribute" && attr.key?.name === "src",
);
const srcValue = srcAttr?.value?.value;

if (srcValue === "" || !srcAttr) {
context.report({
node: srcAttr || node,
messageId: "SpecifySrcAttribute",
});
}
},
})
: {};

return {
JSXOpeningElement(node) {
if (node.name.name === "img") {
Expand All @@ -55,6 +83,7 @@ module.exports = {
}
}
},
...vueTemplateVisitor,
};
},
};
41 changes: 38 additions & 3 deletions eslint-plugin/lib/rules/prefer-lighter-formats-for-image-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,42 @@ module.exports = {
create(context) {
const eligibleExtensions = ["webp", "avif", "svg", "jxl"];

return {
const parserServices = context.parserServices || context.sourceCode?.parserServices;

const vueTemplateVisitor = parserServices?.defineTemplateBodyVisitor
? parserServices.defineTemplateBodyVisitor({
VElement(node) {
const name =
typeof node.name === "string" ? node.name : node.name?.name;
if (name?.toLowerCase() !== "img") return;

const parent = node.parent?.type === "VElement" ? node.parent : null;
const parentName =
typeof parent?.name === "string" ? parent.name : parent?.name?.name;
if (parentName?.toLowerCase() === "picture") return;

const srcAttr = node.startTag.attributes.find(
(attr) => attr.type === "VAttribute" && attr.key?.name === "src",
);
const srcValue = srcAttr?.value?.value;
if (!srcValue) return;

const fileName = srcValue.substring(srcValue.lastIndexOf("/") + 1);
const dotIndex = fileName.lastIndexOf(".");
if (dotIndex === -1) return;

const imgExtension = fileName.substring(dotIndex + 1);
if (eligibleExtensions.includes(imgExtension.toLowerCase())) return;

context.report({
node,
messageId: "PreferLighterFormatsForImageFiles",
data: { eligibleExtensions: eligibleExtensions.join(", ") },
});
},
})
: {};
return {
JSXOpeningElement(node) {
const tagName = node.name.name;
if (tagName?.toLowerCase() !== "img") return;
Expand Down Expand Up @@ -66,7 +101,7 @@ module.exports = {
messageId: "PreferLighterFormatsForImageFiles",
data: { eligibleExtensions: eligibleExtensions.join(", ") },
});
},
};
}
, ...vueTemplateVisitor };
},
};
49 changes: 48 additions & 1 deletion eslint-plugin/lib/rules/prefer-shorthand-css-notations.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ module.exports = {
},
],
},
create: function (context) {
create: function (context) {
const shorthandProperties = {
animation: ["animationName", "animationDuration"],
background: [
Expand Down Expand Up @@ -87,6 +87,52 @@ module.exports = {

const disabledProperties = context.options?.[0]?.disableProperties ?? [];

const parserServices =
context.parserServices || context.sourceCode?.parserServices;

const toCamelCase = (value) =>
value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());

const parseCssProperties = (styleValue) =>
styleValue
.split(";")
.map((part) => part.trim())
.filter(Boolean)
.map((part) => part.split(":")[0].trim())
.filter(Boolean)
.map(toCamelCase);

const vueTemplateVisitor = parserServices?.defineTemplateBodyVisitor
? parserServices.defineTemplateBodyVisitor({
VElement(node) {
const styleAttr = node.startTag.attributes.find(
(attr) => attr.type === "VAttribute" && attr.key?.name === "style",
);
const styleValue = styleAttr?.value?.value;
if (!styleValue) return;

const nodePropertyNames = parseCssProperties(styleValue);

for (const [shorthandProp, matchProperties] of Object.entries(
shorthandProperties,
)) {
if (
!disabledProperties.includes(shorthandProp) &&
matchProperties.every((prop) =>
nodePropertyNames.includes(prop),
)
) {
return context.report({
node: styleAttr,
messageId: "PreferShorthandCSSNotation",
data: { property: shorthandProp },
});
}
}
},
})
: {};

return {
JSXOpeningElement(node) {
const styleAttribute = node.attributes.find(
Expand All @@ -113,6 +159,7 @@ module.exports = {
}
}
},
...vueTemplateVisitor,
};
},
};
Loading