diff --git a/CHANGELOG.md b/CHANGELOG.md index 549e22a..5d22fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- [#84](https://github.com/green-code-initiative/creedengo-javascript/pull/84) Add rule GCI535 "No imported number format library" + ## [3.1.0] - 2026-05-10 ### Added diff --git a/eslint-plugin/README.md b/eslint-plugin/README.md index 52f0aff..8bf01c4 100644 --- a/eslint-plugin/README.md +++ b/eslint-plugin/README.md @@ -90,6 +90,7 @@ export default [ | [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..332a003 --- /dev/null +++ b/eslint-plugin/docs/rules/no-imported-number-format-library.md @@ -0,0 +1,63 @@ +# @creedengo/no-imported-number-format-library + +📝 You should not format number with an external library. + +⚠️ This rule _warns_ in the ✅ `recommended` config. + + + +## 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 (not compliant) +import numbro from "numbro"; + +numbro.setLanguage("en-GB"); +var string = numbro(1000).format({ + thousandSeparated: true, +}); // '1,000' + +// 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://www.npmjs.com/package/numbro) +- [numerable](https://www.npmjs.com/package/numerable) + +Some candidates for the future developments are : + +- [javascript-number-formatter](https://www.npmjs.com/package/number-format.js) +- [numeraljs](https://www.npmjs.com/package/numeral) +- [formatjs](https://www.npmjs.com/package/@formatjs/intl) + +It’s more likely this rule won’t ever be exhaustive. + +## 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..0cb16da --- /dev/null +++ b/eslint-plugin/lib/rules/no-imported-number-format-library.js @@ -0,0 +1,85 @@ +/* + * 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" && + node.id.type === "Identifier" + ) { + 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)); + } + }, + 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.test.js b/eslint-plugin/tests/lib/rules/no-imported-number-format-library.test.js new file mode 100644 index 0000000..f9b79e6 --- /dev/null +++ b/eslint-plugin/tests/lib/rules/no-imported-number-format-library.test.js @@ -0,0 +1,95 @@ +/* + * 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"); +const { describe, it } = require("node:test"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 6, + sourceType: "module", + }, +}); + +const expectedIdentifierError = { + messageId: "ShouldNotUseImportedNumberFormatLibrary", +}; +const expectedImportError = { + messageId: "ShouldNotUseImportedNumberFormatLibrary", +}; + +const tests = { + valid: [ + "new Intl.NumberFormat().format(1000);", + "numbro(1000).add(5);", + ` + const number = numbro(1000); + 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: [expectedIdentifierError], + }, + { + code: ` + const number = numbro(1000); + number.format({thousandSeparated: true}); + `, + errors: [expectedIdentifierError], + }, + { + code: ` + const number = numbro(1000); + const number2 = numbro(2000); + number.format({thousandSeparated: true}); + `, + errors: [expectedIdentifierError], + }, + { + code: "import { format } from 'numerable';", + errors: [expectedImportError], + }, + { + code: "import { format as myFormat} from 'numerable';", + errors: [expectedImportError], + }, + ], +}; + +describe("no-imported-number-format-library", () => { + it("no-imported-number-format-library", () => { + ruleTester.run("no-imported-number-format-library", rule, tests); + }); +}); 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 97e36ce..820272a 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> getAllHooks() { 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..5af104d --- /dev/null +++ b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/checks/NoImportedNumberFormatLibrary.java @@ -0,0 +1,37 @@ +/* + * 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.sonar.check.Rule; +import org.sonar.plugins.javascript.api.EslintHook; +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 EslintHook { + + 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..c542671 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 @@ -13,6 +13,7 @@ "GCI36", "GCI505", "GCI523", - "GCI530" + "GCI530", + "GCI535" ] } 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..664c2e4 --- /dev/null +++ b/test-project/src/no-imported-number-format-library.js @@ -0,0 +1,14 @@ +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 + +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 + +format(28); +parse("29"); // Compliant