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/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/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..bd1d44b --- /dev/null +++ b/eslint-plugin/docs/rules/no-imported-number-format-library.md @@ -0,0 +1,56 @@ +# 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 (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 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" }, 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..2f8719d --- /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({ + parserOptions: { + 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 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/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/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 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