diff --git a/CHANGELOG.md b/CHANGELOG.md index e8767d7..8e5a519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#112](https://github.com/green-code-initiative/creedengo-javascript/pull/112) Clarify rule GCI9 "no-import-all-from-library" - [#113](https://github.com/green-code-initiative/creedengo-javascript/pull/113) Extend rule GCI530 "no-torch" to detect HTML5 Web API usage +- [#115](https://github.com/green-code-initiative/creedengo-javascript/pull/115) Add Vue SFC template support to JSX‑based rules ## [3.1.0] - 2026-05-10 diff --git a/README.md b/README.md index d69e101..deae27b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This project proposes rules for the following technologies: - NestJS - React (JSX) - React Native / Expo +- Vue ## 🔧 ESLint plugin diff --git a/eslint-plugin/docs/rules/avoid-autoplay.md b/eslint-plugin/docs/rules/avoid-autoplay.md index 24d9a32..ed650ba 100644 --- a/eslint-plugin/docs/rules/avoid-autoplay.md +++ b/eslint-plugin/docs/rules/avoid-autoplay.md @@ -17,6 +17,10 @@ Nevertheless, some parts of the video or audio files may be downloaded even if a will be unnecessarily downloaded even if users do not start the video playback. It is therefore necessary to force browsers not to preload anything by setting the `preload` attribute to `none`. +This rule supports both [React](https://react.dev/) (JSX) and [Vue](https://vuejs.org/) (template) syntax. + +### React (JSX) + ```jsx return ( <> @@ -28,7 +32,14 @@ return ( ); ``` -This rule is build for [React](https://react.dev/) and JSX. +### Vue + +```html + +``` ## Resources diff --git a/eslint-plugin/docs/rules/avoid-css-animations.md b/eslint-plugin/docs/rules/avoid-css-animations.md index 77ff251..5eb88df 100644 --- a/eslint-plugin/docs/rules/avoid-css-animations.md +++ b/eslint-plugin/docs/rules/avoid-css-animations.md @@ -16,6 +16,10 @@ On mobile devices, constant animations can contribute to increased power consump life. Limiting the usage of CSS animations helps in creating a more energy-efficient and mobile-friendly user experience. +This rule supports both [React](https://react.dev/) (JSX) and [Vue](https://vuejs.org/) (template) syntax. + +### React (JSX) + ```jsx
// Non-compliant ``` @@ -24,6 +28,17 @@ Limiting the usage of CSS animations helps in creating a more energy-efficient a
// Compliant ``` +### Vue + +```html + +``` + +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 diff --git a/eslint-plugin/docs/rules/no-empty-image-src-attribute.md b/eslint-plugin/docs/rules/no-empty-image-src-attribute.md index 4d8c10f..6ae0bec 100644 --- a/eslint-plugin/docs/rules/no-empty-image-src-attribute.md +++ b/eslint-plugin/docs/rules/no-empty-image-src-attribute.md @@ -17,6 +17,10 @@ Screen readers and other assistive technologies rely on valid image sources to p with disabilities. A missing src attribute can result in confusion and hinder accessibility. +This rule supports both [React](https://react.dev/) (JSX) and [Vue](https://vuejs.org/) (template) syntax. + +### React (JSX) + ```jsx return ( <> @@ -40,7 +44,15 @@ return ( ); ``` -This rule is build for [React](https://react.dev/) and JSX. +### Vue + +```html + +``` ## Resources diff --git a/eslint-plugin/docs/rules/prefer-lighter-formats-for-image-files.md b/eslint-plugin/docs/rules/prefer-lighter-formats-for-image-files.md index 9c9aa4c..0001b9e 100644 --- a/eslint-plugin/docs/rules/prefer-lighter-formats-for-image-files.md +++ b/eslint-plugin/docs/rules/prefer-lighter-formats-for-image-files.md @@ -19,6 +19,8 @@ We recommend using the following formats: - **SVG** (Scalable Vector Graphics) is a vector image format that is based on XML. Files are lightweight and can be scaled without loss of quality. +This rule supports plain HTML/JS, [React](https://react.dev/) (JSX), and [Vue](https://vuejs.org/) (template) syntax. + ```html Unoptimized image of a cat // Non-compliant @@ -72,6 +74,15 @@ is supported, the image.webp image will be downloaded; otherwise, image.jpg imag ``` +### Vue + +```html + +``` + 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. diff --git a/eslint-plugin/docs/rules/prefer-shorthand-css-notations.md b/eslint-plugin/docs/rules/prefer-shorthand-css-notations.md index 707c896..6de5979 100644 --- a/eslint-plugin/docs/rules/prefer-shorthand-css-notations.md +++ b/eslint-plugin/docs/rules/prefer-shorthand-css-notations.md @@ -36,6 +36,10 @@ module.exports = { For example, the `font` shorthand consolidates various font-related properties, and the `margin` shorthand streamlines the definition of margins around a box. +This rule supports both [React](https://react.dev/) (JSX) and [Vue](https://vuejs.org/) (template) syntax. + +### React (JSX) + ```jsx
``` +### Vue + +```html + +``` + +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). diff --git a/eslint-plugin/lib/rules/avoid-autoplay.js b/eslint-plugin/lib/rules/avoid-autoplay.js index 0cfc4fd..6e503d1 100644 --- a/eslint-plugin/lib/rules/avoid-autoplay.js +++ b/eslint-plugin/lib/rules/avoid-autoplay.js @@ -18,6 +18,12 @@ "use strict"; +const { + getVueElementName, + getVueAttribute, + defineVueTemplateVisitor, +} = require("../utils/vue-template"); + /** @type {import("eslint").Rule.RuleModule} */ module.exports = { meta: { @@ -36,6 +42,48 @@ 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 vueTemplateVisitor = defineVueTemplateVisitor(context, { + VElement(node) { + const name = getVueElementName(node); + if (name !== "video" && name !== "audio") return; + + const autoplayAttr = getVueAttribute(node, "autoplay"); + const preloadAttr = getVueAttribute(node, "preload"); + const preloadValue = preloadAttr?.value?.value; + + reportAutoplay(autoplayAttr, preloadAttr, preloadValue, node); + }, + }); + return { JSXOpeningElement(node) { if (node.name.name === "video" || node.name.name === "audio") { @@ -45,28 +93,12 @@ module.exports = { const preloadAttr = node.attributes.find( (attr) => attr.name?.name.toLowerCase() === "preload", ); - if (autoplayAttr && preloadAttr?.value.value !== "none") { - context.report({ - node: autoplayAttr, - messageId: "NoAutoplayAndEnforcePreloadNone", - }); - } else { - if (autoplayAttr) { - context.report({ - node: autoplayAttr, - messageId: "NoAutoplay", - }); - } + const preloadValue = preloadAttr?.value?.value; - if (preloadAttr?.value.value !== "none") { - context.report({ - node: preloadAttr || node, - messageId: "EnforcePreloadNone", - }); - } - } + reportAutoplay(autoplayAttr, preloadAttr, preloadValue, node); } }, + ...vueTemplateVisitor, }; }, }; diff --git a/eslint-plugin/lib/rules/avoid-css-animations.js b/eslint-plugin/lib/rules/avoid-css-animations.js index f181671..88bb1b5 100644 --- a/eslint-plugin/lib/rules/avoid-css-animations.js +++ b/eslint-plugin/lib/rules/avoid-css-animations.js @@ -18,6 +18,11 @@ "use strict"; +const { + getVueAttribute, + defineVueTemplateVisitor, +} = require("../utils/vue-template"); + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -34,6 +39,26 @@ module.exports = { }, create(context) { const forbiddenProperties = ["transition", "animation"]; + + const vueTemplateVisitor = defineVueTemplateVisitor(context, { + VElement(node) { + const styleAttr = getVueAttribute(node, "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( @@ -60,6 +85,7 @@ module.exports = { } } }, + ...vueTemplateVisitor, }; }, }; diff --git a/eslint-plugin/lib/rules/no-empty-image-src-attribute.js b/eslint-plugin/lib/rules/no-empty-image-src-attribute.js index ed0d148..52bf81f 100644 --- a/eslint-plugin/lib/rules/no-empty-image-src-attribute.js +++ b/eslint-plugin/lib/rules/no-empty-image-src-attribute.js @@ -18,6 +18,12 @@ "use strict"; +const { + getVueElementName, + getVueAttribute, + defineVueTemplateVisitor, +} = require("../utils/vue-template"); + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -34,6 +40,22 @@ module.exports = { schema: [], }, create(context) { + const vueTemplateVisitor = defineVueTemplateVisitor(context, { + VElement(node) { + if (getVueElementName(node) !== "img") return; + + const srcAttr = getVueAttribute(node, "src"); + const srcValue = srcAttr?.value?.value; + + if (srcValue === "" || !srcAttr) { + context.report({ + node: srcAttr || node, + messageId: "SpecifySrcAttribute", + }); + } + }, + }); + return { JSXOpeningElement(node) { if (node.name.name === "img") { @@ -55,6 +77,7 @@ module.exports = { } } }, + ...vueTemplateVisitor, }; }, }; diff --git a/eslint-plugin/lib/rules/prefer-lighter-formats-for-image-files.js b/eslint-plugin/lib/rules/prefer-lighter-formats-for-image-files.js index f3c9c85..1955a72 100644 --- a/eslint-plugin/lib/rules/prefer-lighter-formats-for-image-files.js +++ b/eslint-plugin/lib/rules/prefer-lighter-formats-for-image-files.js @@ -18,6 +18,12 @@ "use strict"; +const { + getVueElementName, + getVueAttribute, + defineVueTemplateVisitor, +} = require("../utils/vue-template"); + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -36,6 +42,31 @@ module.exports = { create(context) { const eligibleExtensions = ["webp", "avif", "svg", "jxl"]; + const vueTemplateVisitor = defineVueTemplateVisitor(context, { + VElement(node) { + if (getVueElementName(node) !== "img") return; + + const parent = node.parent?.type === "VElement" ? node.parent : null; + if (getVueElementName(parent) === "picture") return; + + const srcAttr = getVueAttribute(node, "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; @@ -67,6 +98,7 @@ module.exports = { data: { eligibleExtensions: eligibleExtensions.join(", ") }, }); }, + ...vueTemplateVisitor, }; }, }; diff --git a/eslint-plugin/lib/rules/prefer-shorthand-css-notations.js b/eslint-plugin/lib/rules/prefer-shorthand-css-notations.js index 92de908..118f655 100644 --- a/eslint-plugin/lib/rules/prefer-shorthand-css-notations.js +++ b/eslint-plugin/lib/rules/prefer-shorthand-css-notations.js @@ -18,6 +18,11 @@ "use strict"; +const { + getVueAttribute, + defineVueTemplateVisitor, +} = require("../utils/vue-template"); + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -87,6 +92,43 @@ module.exports = { const disabledProperties = context.options?.[0]?.disableProperties ?? []; + 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 = defineVueTemplateVisitor(context, { + VElement(node) { + const styleAttr = getVueAttribute(node, "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( @@ -115,6 +157,7 @@ module.exports = { } } }, + ...vueTemplateVisitor, }; }, }; diff --git a/eslint-plugin/lib/utils/vue-template.js b/eslint-plugin/lib/utils/vue-template.js new file mode 100644 index 0000000..4b04263 --- /dev/null +++ b/eslint-plugin/lib/utils/vue-template.js @@ -0,0 +1,76 @@ +/* + * creedengo JavaScript plugin - Provides rules to reduce the environmental footprint of your JavaScript programs + * Copyright © 2023 Green Code Initiative (https://green-code-initiative.org) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +"use strict"; + +/** + * Returns the lowercase tag name of a `vue-eslint-parser` template AST node + * (VElement), accounting for the various shapes `node.name` can take. + * + * @param {object} node - a VElement node, or its VElement parent + * @returns {string|undefined} + */ +function getVueElementName(node) { + const rawName = + typeof node?.name === "string" + ? node.name + : node?.name?.name || node?.rawName; + return rawName?.toLowerCase(); +} + +/** + * Finds a static `VAttribute` by name on a VElement's start tag. + * Directives and bindings (e.g. `:style`) are not matched since they are + * not `VAttribute` nodes. + * + * @param {object} node - a VElement node + * @param {string} attrName + * @returns {object|undefined} + */ +function getVueAttribute(node, attrName) { + return node.startTag.attributes.find( + (attr) => + attr.type === "VAttribute" && + attr.key?.name?.toLowerCase?.() === attrName, + ); +} + +/** + * Builds the `defineTemplateBodyVisitor` visitor object for a rule so Vue + * `.vue` templates are analyzed the same way as JSX. Returns an empty + * object when the current parser doesn't expose Vue template services, + * so it can always be spread into a rule's returned visitor. + * + * @param {import('eslint').Rule.RuleContext} context + * @param {object} visitor - template body AST visitor (e.g. `{ VElement(node) {} }`) + * @returns {object} + */ +function defineVueTemplateVisitor(context, visitor) { + const parserServices = + context.parserServices || context.sourceCode?.parserServices; + + return parserServices?.defineTemplateBodyVisitor + ? parserServices.defineTemplateBodyVisitor(visitor) + : {}; +} + +module.exports = { + getVueElementName, + getVueAttribute, + defineVueTemplateVisitor, +}; diff --git a/eslint-plugin/package.json b/eslint-plugin/package.json index 2b2eaf5..88be996 100644 --- a/eslint-plugin/package.json +++ b/eslint-plugin/package.json @@ -46,7 +46,8 @@ "mkdirp": "^3.0.1", "prettier": "^3.8.3", "rimraf": "^6.1.3", - "typescript": "~5.9.3" + "typescript": "~5.9.3", + "vue-eslint-parser": "^10.4.0" }, "engines": { "node": ">=22" diff --git a/eslint-plugin/tests/lib/rules/avoid-autoplay.test.js b/eslint-plugin/tests/lib/rules/avoid-autoplay.test.js index 8abb48e..f0bad41 100644 --- a/eslint-plugin/tests/lib/rules/avoid-autoplay.test.js +++ b/eslint-plugin/tests/lib/rules/avoid-autoplay.test.js @@ -24,7 +24,7 @@ const rule = require("../../../lib/rules/avoid-autoplay"); const { RuleTester } = require("eslint"); -const { describe, it } = require('node:test'); +const { describe, it } = require("node:test"); //------------------------------------------------------------------------------ // Tests @@ -42,6 +42,17 @@ const ruleTester = new RuleTester({ }, }); +const vueRuleTester = new RuleTester({ + languageOptions: { + parser: require("vue-eslint-parser"), + parserOptions: { + ecmaVersion: 2021, + sourceType: "module", + parser: require("@typescript-eslint/parser"), + }, + }, +}); + const noAutoplayError = { messageId: "NoAutoplay", }; @@ -86,9 +97,33 @@ const tests = { ], }; -describe('avoid-autoplay', () => { - it('autoplay-audio-video-attribute-not-present', () => { +const vueTests = { + valid: [ + '', + '', + ], + invalid: [ + { + code: "", + errors: [BothError], + }, + { + code: "", + errors: [BothError], + }, + { + code: '', + errors: [enforcePreloadNoneError], + }, + ], +}; + +describe("avoid-autoplay", () => { + it("React", () => { ruleTester.run("autoplay-audio-video-attribute-not-present", rule, tests); }); -}); + it("Vue", () => { + vueRuleTester.run("avoid-autoplay", rule, vueTests); + }); +}); diff --git a/eslint-plugin/tests/lib/rules/avoid-css-animations.test.js b/eslint-plugin/tests/lib/rules/avoid-css-animations.test.js index ea598d4..af6d2a7 100644 --- a/eslint-plugin/tests/lib/rules/avoid-css-animations.test.js +++ b/eslint-plugin/tests/lib/rules/avoid-css-animations.test.js @@ -42,6 +42,17 @@ const ruleTester = new RuleTester({ }, }); +const vueRuleTester = new RuleTester({ + languageOptions: { + parser: require("vue-eslint-parser"), + parserOptions: { + ecmaVersion: 2021, + sourceType: "module", + parser: require("@typescript-eslint/parser"), + }, + }, +}); + const tests = { valid: [ ` @@ -88,8 +99,33 @@ const tests = { ], }; +const vueTests = { + valid: [ + "", + "", + ], + invalid: [ + { + code: "", + errors: [ + { messageId: "AvoidCSSAnimations", data: { attribute: "transition" } }, + ], + }, + { + code: "", + errors: [ + { messageId: "AvoidCSSAnimations", data: { attribute: "animation" } }, + ], + }, + ], +}; + describe("avoid-css-animations", () => { - it("avoid-css-animations", () => { + it("React", () => { ruleTester.run("avoid-css-animations", rule, tests); }); + + it("Vue", () => { + vueRuleTester.run("avoid-css-animations", rule, vueTests); + }); }); diff --git a/eslint-plugin/tests/lib/rules/no-empty-image-src-attribute.test.js b/eslint-plugin/tests/lib/rules/no-empty-image-src-attribute.test.js index 8f8912d..29dee99 100644 --- a/eslint-plugin/tests/lib/rules/no-empty-image-src-attribute.test.js +++ b/eslint-plugin/tests/lib/rules/no-empty-image-src-attribute.test.js @@ -41,6 +41,18 @@ const ruleTester = new RuleTester({ }, }, }); + +const vueRuleTester = new RuleTester({ + languageOptions: { + parser: require("vue-eslint-parser"), + parserOptions: { + ecmaVersion: 2021, + sourceType: "module", + parser: require("@typescript-eslint/parser"), + }, + }, +}); + const expectedError1 = { messageId: "SpecifySrcAttribute", }; @@ -75,8 +87,26 @@ const tests = { ], }; +const vueTests = { + valid: [""], + invalid: [ + { + code: "", + errors: [expectedError1], + }, + { + code: "", + errors: [expectedError2], + }, + ], +}; + describe("no-empty-image-src-attribute", () => { - it("image-src-attribute-not-empty", () => { + it("React", () => { ruleTester.run("image-src-attribute-not-empty", rule, tests); }); + + it("Vue", () => { + vueRuleTester.run("no-empty-image-src-attribute", rule, vueTests); + }); }); diff --git a/eslint-plugin/tests/lib/rules/prefer-lighter-formats-for-image-files.test.js b/eslint-plugin/tests/lib/rules/prefer-lighter-formats-for-image-files.test.js index 2b32475..fde35a2 100644 --- a/eslint-plugin/tests/lib/rules/prefer-lighter-formats-for-image-files.test.js +++ b/eslint-plugin/tests/lib/rules/prefer-lighter-formats-for-image-files.test.js @@ -42,6 +42,17 @@ const ruleTester = new RuleTester({ }, }); +const vueRuleTester = new RuleTester({ + languageOptions: { + parser: require("vue-eslint-parser"), + parserOptions: { + ecmaVersion: 2021, + sourceType: "module", + parser: require("@typescript-eslint/parser"), + }, + }, +}); + const preferLighterFormatsForImageFilesError = { messageId: "PreferLighterFormatsForImageFiles", }; @@ -87,8 +98,32 @@ const tests = { ], }; +const vueTests = { + valid: [ + "", + "", + "", + "", + "", + ], + invalid: [ + { + code: "", + errors: [preferLighterFormatsForImageFilesError], + }, + { + code: "", + errors: [preferLighterFormatsForImageFilesError], + }, + ], +}; + describe("prefer-lighter-formats-for-image-files", () => { - it("prefer-lighter-formats-for-image-files", () => { + it("React", () => { ruleTester.run("prefer-lighter-formats-for-image-files", rule, tests); }); + + it("Vue", () => { + vueRuleTester.run("prefer-lighter-formats-for-image-files", rule, vueTests); + }); }); diff --git a/eslint-plugin/tests/lib/rules/prefer-shorthand-css-notations.test.js b/eslint-plugin/tests/lib/rules/prefer-shorthand-css-notations.test.js index b432a22..d9cbcbd 100644 --- a/eslint-plugin/tests/lib/rules/prefer-shorthand-css-notations.test.js +++ b/eslint-plugin/tests/lib/rules/prefer-shorthand-css-notations.test.js @@ -42,11 +42,27 @@ const ruleTester = new RuleTester({ }, }); +const vueRuleTester = new RuleTester({ + languageOptions: { + parser: require("vue-eslint-parser"), + parserOptions: { + ecmaVersion: 2021, + sourceType: "module", + parser: require("@typescript-eslint/parser"), + }, + }, +}); + const createError = (property) => ({ messageId: "PreferShorthandCSSNotation", data: { property }, }); +const vueCreateError = (property) => ({ + messageId: "PreferShorthandCSSNotation", + data: { property }, +}); + const tests = { valid: [ "
", @@ -164,8 +180,29 @@ const tests = { ], }; +const vueTests = { + valid: [ + "", + "", + ], + invalid: [ + { + code: "", + errors: [vueCreateError("margin")], + }, + { + code: "", + errors: [vueCreateError("border")], + }, + ], +}; + describe("prefer-shorthand-css-notations", () => { - it("prefer-shorthand-css-notations", () => { + it("React", () => { ruleTester.run("prefer-shorthand-css-notations", rule, tests); }); + + it("Vue", () => { + vueRuleTester.run("prefer-shorthand-css-notations", rule, vueTests); + }); }); diff --git a/eslint-plugin/yarn.lock b/eslint-plugin/yarn.lock index 2164d4b..918d65c 100644 --- a/eslint-plugin/yarn.lock +++ b/eslint-plugin/yarn.lock @@ -48,6 +48,7 @@ __metadata: prettier: "npm:^3.8.3" rimraf: "npm:^6.1.3" typescript: "npm:~5.9.3" + vue-eslint-parser: "npm:^10.4.0" peerDependencies: eslint: ^9.0.0 || ^10.0.0 languageName: unknown @@ -560,7 +561,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.4.3": +"debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.4.0, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -731,7 +732,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^9.1.2": +"eslint-scope@npm:^8.2.0 || ^9.0.0, eslint-scope@npm:^9.1.2": version: 9.1.2 resolution: "eslint-scope@npm:9.1.2" dependencies: @@ -750,7 +751,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^5.0.0, eslint-visitor-keys@npm:^5.0.1": +"eslint-visitor-keys@npm:^4.2.0 || ^5.0.0, eslint-visitor-keys@npm:^5.0.0, eslint-visitor-keys@npm:^5.0.1": version: 5.0.1 resolution: "eslint-visitor-keys@npm:5.0.1" checksum: 10/f9cc1a57b75e0ef949545cac33d01e8367e302de4c1483266ed4d8646ee5c306376660196bbb38b004e767b7043d1e661cb4336b49eff634a1bbe75c1db709ec @@ -802,7 +803,7 @@ __metadata: languageName: node linkType: hard -"espree@npm:^11.2.0": +"espree@npm:^10.3.0 || ^11.0.0, espree@npm:^11.2.0": version: 11.2.0 resolution: "espree@npm:11.2.0" dependencies: @@ -813,7 +814,7 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.7.0": +"esquery@npm:^1.6.0, esquery@npm:^1.7.0": version: 1.7.0 resolution: "esquery@npm:1.7.0" dependencies: @@ -1399,6 +1400,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.6.3": + version: 7.8.0 + resolution: "semver@npm:7.8.0" + bin: + semver: bin/semver.js + checksum: 10/039a8f68a581c03c1ac17c990316da57a79a93af9b109b712739c50cd4d464079f7e3fee31c008b472e390c7ba48a11ed2b86e91d8602bf06059d4a266db1426 + languageName: node + linkType: hard + "semver@npm:^7.7.2, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" @@ -1575,6 +1585,22 @@ __metadata: languageName: node linkType: hard +"vue-eslint-parser@npm:^10.4.0": + version: 10.4.0 + resolution: "vue-eslint-parser@npm:10.4.0" + dependencies: + debug: "npm:^4.4.0" + eslint-scope: "npm:^8.2.0 || ^9.0.0" + eslint-visitor-keys: "npm:^4.2.0 || ^5.0.0" + espree: "npm:^10.3.0 || ^11.0.0" + esquery: "npm:^1.6.0" + semver: "npm:^7.6.3" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + checksum: 10/b0d257ffc6afdc6fa47acd8633d3aa4959d1417fe182cd66c7fed02e0425dc9e38f6c4ee2182dbc098be14178801c3c731bd424eaad202b975ddf0756819259f + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" diff --git a/test-project/eslint.config.mjs b/test-project/eslint.config.mjs index ba13826..7e59b22 100644 --- a/test-project/eslint.config.mjs +++ b/test-project/eslint.config.mjs @@ -3,6 +3,7 @@ import eslint from "@eslint/js"; import { defineConfig } from "eslint/config"; import globals from "globals"; import tseslint from "typescript-eslint"; +import vueParser from "vue-eslint-parser"; export default defineConfig( eslint.configs.recommended, @@ -16,4 +17,16 @@ export default defineConfig( }, }, }, + { + files: ["**/*.vue"], + languageOptions: { + parser: vueParser, + parserOptions: { + ecmaVersion: 2021, + sourceType: "module", + parser: tseslint.parser, // use "espree" if not using TS + extraFileExtensions: [".vue"], + }, + }, + }, ); diff --git a/test-project/package.json b/test-project/package.json index 48eccd6..48c3a9e 100644 --- a/test-project/package.json +++ b/test-project/package.json @@ -21,7 +21,8 @@ "eslint": "^10.2.1", "globals": "^17.5.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.59.1" + "typescript-eslint": "^8.59.1", + "vue-eslint-parser": "^10.4.0" }, "packageManager": "yarn@4.14.1" } diff --git a/test-project/src/avoid-autoplay.js b/test-project/src/js/avoid-autoplay.js similarity index 100% rename from test-project/src/avoid-autoplay.js rename to test-project/src/js/avoid-autoplay.js diff --git a/test-project/src/avoid-brightness-override.js b/test-project/src/js/avoid-brightness-override.js similarity index 100% rename from test-project/src/avoid-brightness-override.js rename to test-project/src/js/avoid-brightness-override.js diff --git a/test-project/src/avoid-high-accuracy-geolocation.js b/test-project/src/js/avoid-high-accuracy-geolocation.js similarity index 100% rename from test-project/src/avoid-high-accuracy-geolocation.js rename to test-project/src/js/avoid-high-accuracy-geolocation.js diff --git a/test-project/src/avoid-keep-awake-react-native-fn.js b/test-project/src/js/avoid-keep-awake-react-native-fn.js similarity index 100% rename from test-project/src/avoid-keep-awake-react-native-fn.js rename to test-project/src/js/avoid-keep-awake-react-native-fn.js diff --git a/test-project/src/avoid-keep-awake-react-native-hook.js b/test-project/src/js/avoid-keep-awake-react-native-hook.js similarity index 100% rename from test-project/src/avoid-keep-awake-react-native-hook.js rename to test-project/src/js/avoid-keep-awake-react-native-hook.js diff --git a/test-project/src/import-all-from-library.js b/test-project/src/js/import-all-from-library.js similarity index 100% rename from test-project/src/import-all-from-library.js rename to test-project/src/js/import-all-from-library.js diff --git a/test-project/src/limit-db-query-results.js b/test-project/src/js/limit-db-query-results.js similarity index 100% rename from test-project/src/limit-db-query-results.js rename to test-project/src/js/limit-db-query-results.js diff --git a/test-project/src/modular-import-from-library.js b/test-project/src/js/modular-import-from-library.js similarity index 100% rename from test-project/src/modular-import-from-library.js rename to test-project/src/js/modular-import-from-library.js diff --git a/test-project/src/no-css-animations.js b/test-project/src/js/no-css-animations.js similarity index 100% rename from test-project/src/no-css-animations.js rename to test-project/src/js/no-css-animations.js diff --git a/test-project/src/no-empty-image-src-attribute.js b/test-project/src/js/no-empty-image-src-attribute.js similarity index 100% rename from test-project/src/no-empty-image-src-attribute.js rename to test-project/src/js/no-empty-image-src-attribute.js diff --git a/test-project/src/no-imported-number-format-library.js b/test-project/src/js/no-imported-number-format-library.js similarity index 100% rename from test-project/src/no-imported-number-format-library.js rename to test-project/src/js/no-imported-number-format-library.js diff --git a/test-project/src/no-multiple-access-dom-element.js b/test-project/src/js/no-multiple-access-dom-element.js similarity index 100% rename from test-project/src/no-multiple-access-dom-element.js rename to test-project/src/js/no-multiple-access-dom-element.js diff --git a/test-project/src/no-torch.js b/test-project/src/js/no-torch.js similarity index 100% rename from test-project/src/no-torch.js rename to test-project/src/js/no-torch.js diff --git a/test-project/src/prefer-lighter-formats-for-image-files.js b/test-project/src/js/prefer-lighter-formats-for-image-files.js similarity index 100% rename from test-project/src/prefer-lighter-formats-for-image-files.js rename to test-project/src/js/prefer-lighter-formats-for-image-files.js diff --git a/test-project/src/prefer-shorthand-css-notations.js b/test-project/src/js/prefer-shorthand-css-notations.js similarity index 100% rename from test-project/src/prefer-shorthand-css-notations.js rename to test-project/src/js/prefer-shorthand-css-notations.js diff --git a/test-project/src/provide-print-css.js b/test-project/src/js/provide-print-css.js similarity index 100% rename from test-project/src/provide-print-css.js rename to test-project/src/js/provide-print-css.js diff --git a/test-project/src/rule-no-multiple-style-changes.js b/test-project/src/js/rule-no-multiple-style-changes.js similarity index 100% rename from test-project/src/rule-no-multiple-style-changes.js rename to test-project/src/js/rule-no-multiple-style-changes.js diff --git a/test-project/src/prefer-collections-with-pagination.ts b/test-project/src/ts/prefer-collections-with-pagination.ts similarity index 100% rename from test-project/src/prefer-collections-with-pagination.ts rename to test-project/src/ts/prefer-collections-with-pagination.ts diff --git a/test-project/src/vue/avoid-autoplay.vue b/test-project/src/vue/avoid-autoplay.vue new file mode 100644 index 0000000..9343518 --- /dev/null +++ b/test-project/src/vue/avoid-autoplay.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test-project/src/vue/avoid-css-animations.vue b/test-project/src/vue/avoid-css-animations.vue new file mode 100644 index 0000000..ff040c8 --- /dev/null +++ b/test-project/src/vue/avoid-css-animations.vue @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/test-project/src/vue/no-empty-image-src-attribute.vue b/test-project/src/vue/no-empty-image-src-attribute.vue new file mode 100644 index 0000000..e9ddbef --- /dev/null +++ b/test-project/src/vue/no-empty-image-src-attribute.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test-project/src/vue/prefer-lighter-formats-for-image-files.vue b/test-project/src/vue/prefer-lighter-formats-for-image-files.vue new file mode 100644 index 0000000..6b459bc --- /dev/null +++ b/test-project/src/vue/prefer-lighter-formats-for-image-files.vue @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/test-project/src/vue/prefer-shorthand-css-notations.vue b/test-project/src/vue/prefer-shorthand-css-notations.vue new file mode 100644 index 0000000..56821bc --- /dev/null +++ b/test-project/src/vue/prefer-shorthand-css-notations.vue @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/test-project/yarn.lock b/test-project/yarn.lock index e25d90d..a7ce6cc 100644 --- a/test-project/yarn.lock +++ b/test-project/yarn.lock @@ -21,10 +21,10 @@ __metadata: "@creedengo/eslint-plugin@file:../eslint-plugin::locator=creedengo-javascript-test-project%40workspace%3A.": version: 3.1.0 - resolution: "@creedengo/eslint-plugin@file:../eslint-plugin#../eslint-plugin::hash=5bb3d4&locator=creedengo-javascript-test-project%40workspace%3A." + resolution: "@creedengo/eslint-plugin@file:../eslint-plugin#../eslint-plugin::hash=ed5cfe&locator=creedengo-javascript-test-project%40workspace%3A." peerDependencies: eslint: ^9.0.0 || ^10.0.0 - checksum: 10c0/bfe290b4ff43b71e4adcd9713fbd8f2fed54b34f5ca13db3c0d81225114f84a61bcb08d6756b2cedbdfc0ef19f2442011c1b771d2eae3885b031f74977ee03fc + checksum: 10c0/aa9089b62919097407454a8922f75530a349467f4d1c8b569485491f87cb383d7ff80cbb4fbb2ea931660043d49b8faad3516e87b3ed9207f1c757dc57156d57 languageName: node linkType: hard @@ -572,6 +572,7 @@ __metadata: globals: "npm:^17.5.0" typescript: "npm:~5.9.3" typescript-eslint: "npm:^8.59.1" + vue-eslint-parser: "npm:^10.4.0" languageName: unknown linkType: soft @@ -586,7 +587,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.4.3": +"debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.4.0, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -665,7 +666,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^9.1.2": +"eslint-scope@npm:^8.2.0 || ^9.0.0, eslint-scope@npm:^9.1.2": version: 9.1.2 resolution: "eslint-scope@npm:9.1.2" dependencies: @@ -684,7 +685,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^5.0.0, eslint-visitor-keys@npm:^5.0.1": +"eslint-visitor-keys@npm:^4.2.0 || ^5.0.0, eslint-visitor-keys@npm:^5.0.0, eslint-visitor-keys@npm:^5.0.1": version: 5.0.1 resolution: "eslint-visitor-keys@npm:5.0.1" checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678 @@ -736,7 +737,7 @@ __metadata: languageName: node linkType: hard -"espree@npm:^11.2.0": +"espree@npm:^10.3.0 || ^11.0.0, espree@npm:^11.2.0": version: 11.2.0 resolution: "espree@npm:11.2.0" dependencies: @@ -747,7 +748,7 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.7.0": +"esquery@npm:^1.6.0, esquery@npm:^1.7.0": version: 1.7.0 resolution: "esquery@npm:1.7.0" dependencies: @@ -1256,6 +1257,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.6.3": + version: 7.8.0 + resolution: "semver@npm:7.8.0" + bin: + semver: bin/semver.js + checksum: 10c0/8f096ca9b80ffd47b308d03f9ce8c873e27e2983f36023c559cdc92c51e8433fc23ebbfe57ec9623fc155636a6961ee989501099841ae4bb1babc8d2b3f048cd + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -1435,6 +1445,22 @@ __metadata: languageName: node linkType: hard +"vue-eslint-parser@npm:^10.4.0": + version: 10.4.0 + resolution: "vue-eslint-parser@npm:10.4.0" + dependencies: + debug: "npm:^4.4.0" + eslint-scope: "npm:^8.2.0 || ^9.0.0" + eslint-visitor-keys: "npm:^4.2.0 || ^5.0.0" + espree: "npm:^10.3.0 || ^11.0.0" + esquery: "npm:^1.6.0" + semver: "npm:^7.6.3" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + checksum: 10c0/ded1c52dfa6e08c384da43b8369814bd105e2dc3bbb04e95faa8365af0fcba70293ff8b3749824ae3cc98988aeaaa261d5a205febff23e15667176392dcfee4a + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2"