diff --git a/CHANGELOG.md b/CHANGELOG.md index 7697812..1c8a3b6 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" + ### Fixed - [#101](https://github.com/green-code-initiative/creedengo-javascript/pull/101) Fix spread style attributes in GCI26 and GCI29 rules diff --git a/eslint-plugin/README.md b/eslint-plugin/README.md index ea5b643..676b79d 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..d19c6e2 --- /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://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 + +- [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..be8c048 --- /dev/null +++ b/eslint-plugin/lib/rules/no-imported-number-format-library.js @@ -0,0 +1,78 @@ +/* + * 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)); + } + }, + 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/package.json b/eslint-plugin/package.json index 5ecea5f..e313c79 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" }, 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..d9c2a5e --- /dev/null +++ b/eslint-plugin/tests/lib/rules/no-imported-number-format-library.js @@ -0,0 +1,88 @@ +/* + * 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({ + languageOptions: { + ecmaVersion: 6, + sourceType: "module", + }, +}); +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);", + "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], + }, + ], +}); 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..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..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