From 8a046e7d2670d0394ccef791fcd238aba2d1bf82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabien=20B=C3=A9zagu?= Date: Wed, 21 May 2025 15:09:41 +0200 Subject: [PATCH 1/5] Add rule GCI535 "No imported number format library" ...Handle numbro library only --- CHANGELOG.md | 1 + eslint-plugin/README.md | 1 + .../no-imported-number-format-library.md | 40 ++++++++++ .../no-imported-number-format-library.js | 69 +++++++++++++++++ .../no-imported-number-format-library.js | 74 +++++++++++++++++++ .../creedengo/javascript/CheckList.java | 1 + .../checks/NoImportedNumberFormatLibrary.java | 38 ++++++++++ .../profiles/javascript_profile.json | 1 + .../src/no-imported-number-format-library.js | 10 +++ 9 files changed, 235 insertions(+) create mode 100644 eslint-plugin/docs/rules/no-imported-number-format-library.md create mode 100644 eslint-plugin/lib/rules/no-imported-number-format-library.js create mode 100644 eslint-plugin/tests/lib/rules/no-imported-number-format-library.js create mode 100644 sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/checks/NoImportedNumberFormatLibrary.java create mode 100644 test-project/src/no-imported-number-format-library.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b23234..66b9bf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [#51](https://github.com/green-code-initiative/creedengo-javascript/pull/51) Add ESLint v9 and flat config support +- [#84](https://github.com/green-code-initiative/creedengo-javascript/pull/84) Add rule GCI535 "No imported number format library" ### Changed diff --git a/eslint-plugin/README.md b/eslint-plugin/README.md index f4cba47..2d27c72 100644 --- a/eslint-plugin/README.md +++ b/eslint-plugin/README.md @@ -119,6 +119,7 @@ If your project uses a legacy ESLint version, it may use as well the now depreca | [limit-db-query-results](docs/rules/limit-db-query-results.md) | Should limit the number of returns for a SQL query | ✅ | | [no-empty-image-src-attribute](docs/rules/no-empty-image-src-attribute.md) | Disallow usage of image with empty source attribute | ✅ | | [no-import-all-from-library](docs/rules/no-import-all-from-library.md) | Should not import all from library | ✅ | +| [no-imported-number-format-library](docs/rules/no-imported-number-format-library.md) | You should not format number with an external library | ✅ | | [no-multiple-access-dom-element](docs/rules/no-multiple-access-dom-element.md) | Disallow multiple access of same DOM element | ✅ | | [no-multiple-style-changes](docs/rules/no-multiple-style-changes.md) | Disallow multiple style changes at once | ✅ | | [no-torch](docs/rules/no-torch.md) | Should not programmatically enable torch mode | ✅ | diff --git a/eslint-plugin/docs/rules/no-imported-number-format-library.md b/eslint-plugin/docs/rules/no-imported-number-format-library.md new file mode 100644 index 0000000..91070b7 --- /dev/null +++ b/eslint-plugin/docs/rules/no-imported-number-format-library.md @@ -0,0 +1,40 @@ +# You should not format number with an external library (`@creedengo/no-imported-number-format-library`) + +⚠️ This rule _warns_ in the following configs: ✅ `flat/recommended`, ✅ `recommended`. + +## Why is this an issue? + +Importing an external library for lightweight operations increases overall size of the program. +Using native methods instead reduces the amount of memory and storage to run and store the application. +This is especially critical in environments with limited resources, such as on mobile devices or in web applications +where bandwidth and download times matter. + +Smaller programs generally have better runtime performance. +Reducing the number of unnecessary modules minimizes the amount of code that needs to be interpreted or compiled, +leading to faster execution and improved overall performance. + +Depending on less external dependencies also increases the maintainability and security of your program. + +## Examples + +**Example with the [numbro](https://numbrojs.com/) library, when you use +`format` method.** + +```js +// Example with numbro +import numbro from "numbro"; + +numbro.setLanguage('en-GB'); +var string = numbro(1000).format({ + thousandSeparated: true, +}); // '1,000' + +// Example with Intl +new Intl.NumberFormat("en-GB").format(1000); // '1,000' +``` + +## Resources + +### Documentation + +- [Mozilla Web Technology for Developers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) - diff --git a/eslint-plugin/lib/rules/no-imported-number-format-library.js b/eslint-plugin/lib/rules/no-imported-number-format-library.js new file mode 100644 index 0000000..1f29bdf --- /dev/null +++ b/eslint-plugin/lib/rules/no-imported-number-format-library.js @@ -0,0 +1,69 @@ +/* + * 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"; + +/** @type {import("eslint").Rule.RuleModule} */ +module.exports = { + meta: { + type: "suggestion", + docs: { + description: "You should not format number with an external library", + category: "eco-design", + recommended: "warn", + }, + messages: { + ShouldNotUseImportedNumberFormatLibrary: + "You should not format number with an external library", + }, + schema: [], + }, + + create: function (context) { + let variablesNumbro = []; + + const errorReport = (node) => ({ + node, + messageId: "ShouldNotUseImportedNumberFormatLibrary", + }); + + return { + VariableDeclarator(node) { + if (node.init.callee?.name === "numbro") { + variablesNumbro.push(node.id.name); + } + }, + CallExpression(node) { + const formatIsCalledOnANumbroTypeVariable = + node.callee.type === "MemberExpression" && + variablesNumbro.includes(node.callee.object.name) && + node.callee.property.name === "format"; + if (formatIsCalledOnANumbroTypeVariable) { + context.report(errorReport(node.callee.property)); + } + let formatIsCalledOnNumbroInstance = + node.parent.type === "MemberExpression" && + node.callee.name === "numbro" && + node.parent.property.name === "format"; + if (formatIsCalledOnNumbroInstance) { + context.report(errorReport(node.parent.property)); + } + }, + }; + }, +}; diff --git a/eslint-plugin/tests/lib/rules/no-imported-number-format-library.js b/eslint-plugin/tests/lib/rules/no-imported-number-format-library.js new file mode 100644 index 0000000..a33f5c8 --- /dev/null +++ b/eslint-plugin/tests/lib/rules/no-imported-number-format-library.js @@ -0,0 +1,74 @@ +/* + * 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"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-imported-number-format-library"); +const RuleTester = require("eslint").RuleTester; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 6, + sourceType: "module", + }, +}); +const expectedError = { + messageId: "ShouldNotUseImportedNumberFormatLibrary", + type: "Identifier", +}; + +ruleTester.run("no-imported-number-format-library", rule, { + valid: [ + "new Intl.NumberFormat().format(1000);", + "numbro(1000).add(5);", + ` + const number = numbro(1000); + const number2 = numbro(2000); + number2.add(1000); + `, + ], + invalid: [ + { + code: "numbro(1000).format({thousandSeparated: true});", + errors: [expectedError], + }, + { + code: ` + const number = numbro(1000); + number.format({thousandSeparated: true}); + `, + errors: [expectedError], + }, + { + code: ` + const number = numbro(1000); + const number2 = numbro(2000); + number.format({thousandSeparated: true}); + `, + errors: [expectedError], + }, + ], +}); diff --git a/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/CheckList.java b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/CheckList.java index 8ed5b81..4c2ba7b 100644 --- a/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/CheckList.java +++ b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/CheckList.java @@ -42,6 +42,7 @@ public static List> getAllChecks() { LimitDbQueryResult.class, NoEmptyImageSrcAttribute.class, NoImportAllFromLibrary.class, + NoImportedNumberFormatLibrary.class, NoMultipleAccessDomElement.class, NoMultipleStyleChanges.class, NoTorch.class, diff --git a/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/checks/NoImportedNumberFormatLibrary.java b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/checks/NoImportedNumberFormatLibrary.java new file mode 100644 index 0000000..6afd905 --- /dev/null +++ b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/checks/NoImportedNumberFormatLibrary.java @@ -0,0 +1,38 @@ +/* + * 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 . + */ +package org.greencodeinitiative.creedengo.javascript.checks; + +import org.greencodeinitiative.creedengo.javascript.DeprecatedEcoCodeRule; +import org.sonar.check.Rule; +import org.sonar.plugins.javascript.api.EslintBasedCheck; +import org.sonar.plugins.javascript.api.JavaScriptRule; +import org.sonar.plugins.javascript.api.TypeScriptRule; + +@JavaScriptRule +@TypeScriptRule +@Rule(key = NoImportedNumberFormatLibrary.RULE_KEY) +public class NoImportedNumberFormatLibrary implements EslintBasedCheck { + + public static final String RULE_KEY = "GCI535"; + + @Override + public String eslintKey() { + return "@creedengo/no-imported-number-format-library"; + } + +} diff --git a/sonar-plugin/src/main/resources/org/greencodeinitiative/creedengo/profiles/javascript_profile.json b/sonar-plugin/src/main/resources/org/greencodeinitiative/creedengo/profiles/javascript_profile.json index af1a0b3..224851c 100644 --- a/sonar-plugin/src/main/resources/org/greencodeinitiative/creedengo/profiles/javascript_profile.json +++ b/sonar-plugin/src/main/resources/org/greencodeinitiative/creedengo/profiles/javascript_profile.json @@ -1,6 +1,7 @@ { "name": "Creedengo", "ruleKeys": [ + "GCI535", "GCI9", "GCI11", "GCI12", diff --git a/test-project/src/no-imported-number-format-library.js b/test-project/src/no-imported-number-format-library.js new file mode 100644 index 0000000..80757cb --- /dev/null +++ b/test-project/src/no-imported-number-format-library.js @@ -0,0 +1,10 @@ +import numbro from "numbro"; + +numbro(1000).format({thousandSeparated: true}); // Non-compliant: usage of external library to format number + +let variable = numbro(1000); +variable.format({thousandSeparated: true}); // Non-compliant: usage of external library to format number + +numbro(2000).add(1000); // Compliant + +new Intl.NumberFormat().format(1000); // Compliant From ef9138f5f44e60c40a4028f8fc57bbff2bf21234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9fanie=20LOISELEUR?= Date: Tue, 20 May 2025 17:44:20 +0200 Subject: [PATCH 2/5] Add `test:watch` target --- eslint-plugin/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint-plugin/package.json b/eslint-plugin/package.json index 1a363d9..8479303 100644 --- a/eslint-plugin/package.json +++ b/eslint-plugin/package.json @@ -29,6 +29,7 @@ "lint:fix": "eslint . --fix", "pack:sonar": "npm pkg set main=\"./dist/rules.js\" && mkdirp dist/pack && yarn pack -o dist/pack/creedengo-eslint-plugin.tgz && npm pkg set main=\"./lib/standalone.js\"", "test": "mocha tests --recursive", + "test:watch": "mocha tests --watch --recursive", "test:cov": "nyc --reporter=lcov --reporter=text mocha tests --recursive", "update:eslint-docs": "eslint-doc-generator" }, From efb9a690f3ffebdf6b88f2265d8bd73ab61dab18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9fanie=20LOISELEUR?= Date: Tue, 20 May 2025 18:23:08 +0200 Subject: [PATCH 3/5] Improve documentation - install the project - run Sonar scanner manually with proper host --- CONTRIBUTING.md | 7 ++++--- test-project/README.md | 2 +- test-project/tool_send_to_sonar.sh | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b552cd4..36761ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,9 +42,10 @@ But it can be useful to prepare a test project to check the correct execution of ## Installation 1. Clone the Git repository -2. Run `yarn install` inside **eslint-plugin** directory -3. Synchronize dependencies using Maven inside **sonar-plugin** directory -4. You are good to go! 🚀 +1. Inside **eslint-plugin** directory, run `yarn install` +1. Inside **sonar-plugin** directory, synchronize dependencies using Maven with `mvn clean install -DskipTests` +1. Inside root directory, initialize docker with `docker compose up --build -d` +1. You are good to go! 🚀 ## Create a rule diff --git a/test-project/README.md b/test-project/README.md index 7fc6f5d..2db85f0 100644 --- a/test-project/README.md +++ b/test-project/README.md @@ -28,7 +28,7 @@ Use the following Shell script which will do the job for you: Or you can manually run these commands: - Install dependencies: `yarn install` -- Start Sonar Scanner: `yarn sonar -Dsonar.token=MY_SONAR_TOKEN` +- Start Sonar Scanner: `yarn sonar -Dsonar.host.url=http://127.0.0.1:9000 -Dsonar.token=MY_SONAR_TOKEN` ### 3. Check errors diff --git a/test-project/tool_send_to_sonar.sh b/test-project/tool_send_to_sonar.sh index dcbc22b..174c810 100755 --- a/test-project/tool_send_to_sonar.sh +++ b/test-project/tool_send_to_sonar.sh @@ -7,4 +7,4 @@ yarn install # sending to Sonar phase -yarn sonar -Dsonar.host.url=http://0.0.0.0:9000 -Dsonar.token=$1 +yarn sonar -Dsonar.host.url=http://127.0.0.1:9000 -Dsonar.token=$1 From 156f1a940a05de1799e62255f6948651d69d01a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabien=20B=C3=A9zagu?= Date: Wed, 21 May 2025 13:47:37 +0200 Subject: [PATCH 4/5] GCI535: add support for the numerable library --- .../no-imported-number-format-library.js | 9 +++++++ .../no-imported-number-format-library.js | 24 +++++++++++++++---- .../src/no-imported-number-format-library.js | 4 ++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/eslint-plugin/lib/rules/no-imported-number-format-library.js b/eslint-plugin/lib/rules/no-imported-number-format-library.js index 1f29bdf..be8c048 100644 --- a/eslint-plugin/lib/rules/no-imported-number-format-library.js +++ b/eslint-plugin/lib/rules/no-imported-number-format-library.js @@ -64,6 +64,15 @@ module.exports = { context.report(errorReport(node.parent.property)); } }, + ImportDeclaration(node) { + const importedLibraryName = node.source.value; + if (importedLibraryName === 'numerable') { + const formatSpecifier = node.specifiers.find(specifier => specifier.type === 'ImportSpecifier' && specifier.imported.name === 'format'); + if (formatSpecifier) { + context.report(errorReport(formatSpecifier)); + } + } + } }; }, }; diff --git a/eslint-plugin/tests/lib/rules/no-imported-number-format-library.js b/eslint-plugin/tests/lib/rules/no-imported-number-format-library.js index a33f5c8..2f8719d 100644 --- a/eslint-plugin/tests/lib/rules/no-imported-number-format-library.js +++ b/eslint-plugin/tests/lib/rules/no-imported-number-format-library.js @@ -35,11 +35,14 @@ const ruleTester = new RuleTester({ sourceType: "module", }, }); -const expectedError = { +const expectedIdentifierError = { messageId: "ShouldNotUseImportedNumberFormatLibrary", type: "Identifier", }; - +const expectedImportError = { + messageId: "ShouldNotUseImportedNumberFormatLibrary", + type: "ImportSpecifier", +}; ruleTester.run("no-imported-number-format-library", rule, { valid: [ "new Intl.NumberFormat().format(1000);", @@ -49,18 +52,21 @@ ruleTester.run("no-imported-number-format-library", rule, { const number2 = numbro(2000); number2.add(1000); `, + "import { parse } from 'numerable';", + "import { format } from 'date-fns';", + "import mysql from 'mysql2';" ], invalid: [ { code: "numbro(1000).format({thousandSeparated: true});", - errors: [expectedError], + errors: [expectedIdentifierError], }, { code: ` const number = numbro(1000); number.format({thousandSeparated: true}); `, - errors: [expectedError], + errors: [expectedIdentifierError], }, { code: ` @@ -68,7 +74,15 @@ ruleTester.run("no-imported-number-format-library", rule, { const number2 = numbro(2000); number.format({thousandSeparated: true}); `, - errors: [expectedError], + errors: [expectedIdentifierError], + }, + { + code: "import { format } from 'numerable';", + errors: [expectedImportError], + }, + { + code: "import { format as myFormat} from 'numerable';", + errors: [expectedImportError], }, ], }); diff --git a/test-project/src/no-imported-number-format-library.js b/test-project/src/no-imported-number-format-library.js index 80757cb..664c2e4 100644 --- a/test-project/src/no-imported-number-format-library.js +++ b/test-project/src/no-imported-number-format-library.js @@ -1,4 +1,5 @@ import numbro from "numbro"; +import { parse, format } from "numerable"; // Non-compliant: usage of external library to format number numbro(1000).format({thousandSeparated: true}); // Non-compliant: usage of external library to format number @@ -8,3 +9,6 @@ variable.format({thousandSeparated: true}); // Non-compliant: usage of external numbro(2000).add(1000); // Compliant new Intl.NumberFormat().format(1000); // Compliant + +format(28); +parse("29"); // Compliant From 80220c703aec2365ed39182c80ad9bcbd4a8772a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabien=20B=C3=A9zagu?= Date: Wed, 21 May 2025 14:44:52 +0200 Subject: [PATCH 5/5] GCI535[doc]: add examples, limitations and ideas for the next developments --- .../no-imported-number-format-library.md | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/eslint-plugin/docs/rules/no-imported-number-format-library.md b/eslint-plugin/docs/rules/no-imported-number-format-library.md index 91070b7..bd1d44b 100644 --- a/eslint-plugin/docs/rules/no-imported-number-format-library.md +++ b/eslint-plugin/docs/rules/no-imported-number-format-library.md @@ -21,7 +21,7 @@ Depending on less external dependencies also increases the maintainability and s `format` method.** ```js -// Example with numbro +// Example with numbro (not compliant) import numbro from "numbro"; numbro.setLanguage('en-GB'); @@ -29,10 +29,26 @@ var string = numbro(1000).format({ thousandSeparated: true, }); // '1,000' -// Example with Intl +// Example with numerable (not compliant) +import { format } from "numerable"; +format(1000, '0,0'); + +// Example with Intl (compliant) new Intl.NumberFormat("en-GB").format(1000); // '1,000' ``` +## Limitations +As for now, only two libraries are handled by this rule : +- [numbro](https://numbrojs.com/) +- [numerable](https://numerablejs.com/lander) + +Some candidates for the future developments are : +- [javascript-number-formatter](https://github.com/Mottie/javascript-number-formatter) +- [numeraljs](https://www.npmjs.com/package/numerable) +- [formatjs](https://formatjs.github.io/) + +It’s more likely this rule won’t ever be exhaustive. + ## Resources ### Documentation