diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d22fe0..d03e85c 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 - [#84](https://github.com/green-code-initiative/creedengo-javascript/pull/84) Add rule GCI535 "No imported number format library" +- Add SonarQube-only rule GCI2536 "Optimize the browserslist tag in package.json" ## [3.1.0] - 2026-05-10 diff --git a/sonar-plugin/pom.xml b/sonar-plugin/pom.xml index 712eec7..8053cff 100644 --- a/sonar-plugin/pom.xml +++ b/sonar-plugin/pom.xml @@ -48,7 +48,7 @@ ${encoding} ${encoding} - 3.0.0 + main-SNAPSHOT 13.0.0.3026 11.8.0.37897 1.25.1.3002 diff --git a/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/JavaScriptPlugin.java b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/JavaScriptPlugin.java index 01f9fb9..3ff6f63 100644 --- a/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/JavaScriptPlugin.java +++ b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/JavaScriptPlugin.java @@ -28,6 +28,7 @@ public void define(Context context) { context.addExtensions( ESLintRulesBundle.class, JavaScriptRulesDefinition.class, + OptimizeBrowserslistTagInPackageJsonSensor.class, JavaScriptRuleRepository.class, TypeScriptRulesDefinition.class, TypeScriptRuleRepository.class diff --git a/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/JavaScriptRulesDefinition.java b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/JavaScriptRulesDefinition.java index 871afa0..dff9428 100644 --- a/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/JavaScriptRulesDefinition.java +++ b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/JavaScriptRulesDefinition.java @@ -24,6 +24,8 @@ import org.sonar.api.server.rule.RulesDefinition; import org.sonarsource.analyzer.commons.RuleMetadataLoader; +import static java.util.List.of; + public class JavaScriptRulesDefinition implements RulesDefinition { private static final String METADATA_LOCATION = "org/green-code-initiative/rules/javascript"; @@ -49,6 +51,8 @@ public void define(Context context) { ruleMetadataLoader.addRulesByAnnotatedClass(repository, checks); DeprecatedEcoCodeRule.addOnRepository(repository, JavaScriptRuleRepository.OLD_KEY, checks); + ruleMetadataLoader.addRulesByAnnotatedClass(repository, of(OptimizeBrowserslistTagInPackageJsonRule.class)); + repository.done(); } diff --git a/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonRule.java b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonRule.java new file mode 100644 index 0000000..13a29f6 --- /dev/null +++ b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonRule.java @@ -0,0 +1,65 @@ +/* + * 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; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.sonar.check.Rule; + +@Rule(key = OptimizeBrowserslistTagInPackageJsonRule.KEY) +public final class OptimizeBrowserslistTagInPackageJsonRule { + + public static final String KEY = "GCI2536"; + + static final String ISSUE_MESSAGE = "Move the browserslist configuration to a \"production\" target."; + + private static final Pattern BROWSERSLIST_PATTERN = Pattern.compile( + "\"browserslist\"\\s*:\\s*(\\{.*?\\}|\\[.*?\\]|\".*?\")", Pattern.DOTALL); + + private OptimizeBrowserslistTagInPackageJsonRule() { + } + + static boolean isNonCompliant(String packageJsonContents) { + Matcher matcher = BROWSERSLIST_PATTERN.matcher(packageJsonContents); + if (!matcher.find()) { + return false; + } + + String browserslistConfiguration = matcher.group(1).trim(); + if (browserslistConfiguration.startsWith("{")) { + return !browserslistConfiguration.contains("\"production\""); + } + + return true; + } + + static int browserslistLineNumber(String packageJsonContents) { + Matcher matcher = BROWSERSLIST_PATTERN.matcher(packageJsonContents); + if (!matcher.find()) { + return 1; + } + + return lineNumberAt(packageJsonContents, matcher.start()); + } + + private static int lineNumberAt(String text, int index) { + return 1 + (int) text.substring(0, index).chars().filter(c -> c == '\n').count(); + } + +} diff --git a/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonSensor.java b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonSensor.java new file mode 100644 index 0000000..b94e5cb --- /dev/null +++ b/sonar-plugin/src/main/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonSensor.java @@ -0,0 +1,79 @@ +/* + * 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; + +import java.io.IOException; + +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.scanner.sensor.ProjectSensor; + +public class OptimizeBrowserslistTagInPackageJsonSensor implements ProjectSensor { + + private static final RuleKey RULE_KEY = RuleKey.of(JavaScriptRuleRepository.KEY, OptimizeBrowserslistTagInPackageJsonRule.KEY); + + private final FileSystem fileSystem; + + public OptimizeBrowserslistTagInPackageJsonSensor(FileSystem fileSystem) { + this.fileSystem = fileSystem; + } + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor + .name("Optimize browserslist tag in package.json") + .createIssuesForRuleRepository(JavaScriptRuleRepository.KEY); + } + + @Override + public void execute(SensorContext context) { + InputFile packageJson = findPackageJson(); + if (packageJson == null || packageJson.isEmpty()) { + return; + } + + try { + String contents = packageJson.contents(); + if (!OptimizeBrowserslistTagInPackageJsonRule.isNonCompliant(contents)) { + return; + } + + NewIssue issue = context.newIssue().forRule(RULE_KEY); + NewIssueLocation location = issue.newLocation() + .on(packageJson) + .at(packageJson.selectLine(OptimizeBrowserslistTagInPackageJsonRule.browserslistLineNumber(contents))) + .message(OptimizeBrowserslistTagInPackageJsonRule.ISSUE_MESSAGE); + + issue.at(location).save(); + } catch (IOException exception) { + throw new IllegalStateException("Unable to read package.json", exception); + } + } + + private InputFile findPackageJson() { + FilePredicate packageJsonPredicate = fileSystem.predicates().hasRelativePath("package.json"); + return fileSystem.inputFile(packageJsonPredicate); + } + +} \ No newline at end of file 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 c542671..a411343 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 @@ -14,6 +14,7 @@ "GCI505", "GCI523", "GCI530", - "GCI535" + "GCI535", + "GCI2536" ] } diff --git a/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/JavaScriptPluginTest.java b/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/JavaScriptPluginTest.java index c6d07b9..5c842fb 100644 --- a/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/JavaScriptPluginTest.java +++ b/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/JavaScriptPluginTest.java @@ -31,7 +31,7 @@ void extensions() { SonarRuntime sonarRuntime = mock(SonarRuntime.class); Plugin.Context context = new Plugin.Context(sonarRuntime); new JavaScriptPlugin().define(context); - assertThat(context.getExtensions()).hasSize(5); + assertThat(context.getExtensions()).hasSize(6); } } diff --git a/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/JavaScriptRulesDefinitionTest.java b/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/JavaScriptRulesDefinitionTest.java index bdc3546..09c948d 100644 --- a/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/JavaScriptRulesDefinitionTest.java +++ b/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/JavaScriptRulesDefinitionTest.java @@ -41,6 +41,7 @@ void createRepository() { assertThat(repository.isExternal()).isFalse(); assertThat(repository.language()).isEqualTo("js"); assertThat(repository.key()).isEqualTo("creedengo-javascript"); + assertThat(repository.rule(OptimizeBrowserslistTagInPackageJsonRule.KEY)).isNotNull(); } } diff --git a/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonRuleTest.java b/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonRuleTest.java new file mode 100644 index 0000000..ac92d2c --- /dev/null +++ b/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonRuleTest.java @@ -0,0 +1,101 @@ +/* + * 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; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class OptimizeBrowserslistTagInPackageJsonRuleTest { + + @Test + void detectNonCompliantBrowserslistArray() { + String packageJson = """ + { + "name": "app", + "browserslist": [ + "defaults" + ] + } + """; + + assertThat(OptimizeBrowserslistTagInPackageJsonRule.isNonCompliant(packageJson)).isTrue(); + assertThat(OptimizeBrowserslistTagInPackageJsonRule.browserslistLineNumber(packageJson)).isEqualTo(3); + } + + @Test + void acceptProductionBrowserslistObject() { + String packageJson = """ + { + "name": "app", + "browserslist": { + "production": [ + "last 2 Chrome versions" + ], + "development": [ + "last 1 Chrome version" + ] + } + } + """; + + assertThat(OptimizeBrowserslistTagInPackageJsonRule.isNonCompliant(packageJson)).isFalse(); + } + + @Test + void acceptPackageJsonWithoutBrowserslist() { + String packageJson = """ + { + "name": "app" + } + """; + + assertThat(OptimizeBrowserslistTagInPackageJsonRule.isNonCompliant(packageJson)).isFalse(); + assertThat(OptimizeBrowserslistTagInPackageJsonRule.browserslistLineNumber(packageJson)).isEqualTo(1); + } + + @Test + void detectNonCompliantBrowserslistObjectWithoutProductionKey() { + String packageJson = """ + { + "name": "app", + "browserslist": { + "development": [ + "last 1 Chrome version" + ] + } + } + """; + + assertThat(OptimizeBrowserslistTagInPackageJsonRule.isNonCompliant(packageJson)).isTrue(); + } + + @Test + void detectNonCompliantBrowserslistString() { + String packageJson = """ + { + "name": "app", + "browserslist": "defaults and > 0.5%" + } + """; + + assertThat(OptimizeBrowserslistTagInPackageJsonRule.isNonCompliant(packageJson)).isTrue(); + assertThat(OptimizeBrowserslistTagInPackageJsonRule.browserslistLineNumber(packageJson)).isEqualTo(3); + } + +} \ No newline at end of file diff --git a/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonSensorTest.java b/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonSensorTest.java new file mode 100644 index 0000000..ef24f28 --- /dev/null +++ b/sonar-plugin/src/test/java/org/greencodeinitiative/creedengo/javascript/OptimizeBrowserslistTagInPackageJsonSensorTest.java @@ -0,0 +1,143 @@ +/* + * 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; + +import org.junit.jupiter.api.Test; +import org.mockito.Answers; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.FilePredicates; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.TextRange; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class OptimizeBrowserslistTagInPackageJsonSensorTest { + + @Test + void reportIssueForNonCompliantPackageJson() throws Exception { + String packageJson = """ + { + "name": "app", + "browserslist": [ + "defaults" + ] + } + """; + + FileSystem fileSystem = mock(FileSystem.class); + FilePredicates predicates = mock(FilePredicates.class); + FilePredicate predicate = mock(FilePredicate.class); + InputFile inputFile = mock(InputFile.class); + SensorContext context = mock(SensorContext.class); + NewIssue issue = mock(NewIssue.class, Answers.RETURNS_SELF); + NewIssueLocation location = mock(NewIssueLocation.class, Answers.RETURNS_SELF); + TextRange textRange = mock(TextRange.class); + + when(fileSystem.predicates()).thenReturn(predicates); + when(predicates.hasRelativePath("package.json")).thenReturn(predicate); + when(fileSystem.inputFile(predicate)).thenReturn(inputFile); + when(inputFile.isEmpty()).thenReturn(false); + when(inputFile.contents()).thenReturn(packageJson); + when(inputFile.selectLine(3)).thenReturn(textRange); + when(context.newIssue()).thenReturn(issue); + when(issue.newLocation()).thenReturn(location); + + new OptimizeBrowserslistTagInPackageJsonSensor(fileSystem).execute(context); + + verify(context).newIssue(); + verify(issue).forRule(eq(org.sonar.api.rule.RuleKey.of(JavaScriptRuleRepository.KEY, OptimizeBrowserslistTagInPackageJsonRule.KEY))); + verify(location).on(inputFile); + verify(location).at(textRange); + verify(location).message(OptimizeBrowserslistTagInPackageJsonRule.ISSUE_MESSAGE); + verify(issue).save(); + } + + @Test + void doNotReportIssueWhenPackageJsonIsCompliant() throws Exception { + String packageJson = """ + { + "name": "app", + "browserslist": { + "production": [ + "last 2 Chrome versions" + ] + } + } + """; + + FileSystem fileSystem = mock(FileSystem.class); + FilePredicates predicates = mock(FilePredicates.class); + FilePredicate predicate = mock(FilePredicate.class); + InputFile inputFile = mock(InputFile.class); + SensorContext context = mock(SensorContext.class); + + when(fileSystem.predicates()).thenReturn(predicates); + when(predicates.hasRelativePath("package.json")).thenReturn(predicate); + when(fileSystem.inputFile(predicate)).thenReturn(inputFile); + when(inputFile.isEmpty()).thenReturn(false); + when(inputFile.contents()).thenReturn(packageJson); + + assertThatCode(() -> new OptimizeBrowserslistTagInPackageJsonSensor(fileSystem).execute(context)).doesNotThrowAnyException(); + + verify(context, never()).newIssue(); + } + + @Test + void doNotReportIssueWhenPackageJsonNotFound() { + FileSystem fileSystem = mock(FileSystem.class); + FilePredicates predicates = mock(FilePredicates.class); + FilePredicate predicate = mock(FilePredicate.class); + SensorContext context = mock(SensorContext.class); + + when(fileSystem.predicates()).thenReturn(predicates); + when(predicates.hasRelativePath("package.json")).thenReturn(predicate); + when(fileSystem.inputFile(predicate)).thenReturn(null); + + assertThatCode(() -> new OptimizeBrowserslistTagInPackageJsonSensor(fileSystem).execute(context)).doesNotThrowAnyException(); + + verify(context, never()).newIssue(); + } + + @Test + void doNotReportIssueWhenPackageJsonIsEmpty() { + FileSystem fileSystem = mock(FileSystem.class); + FilePredicates predicates = mock(FilePredicates.class); + FilePredicate predicate = mock(FilePredicate.class); + InputFile inputFile = mock(InputFile.class); + SensorContext context = mock(SensorContext.class); + + when(fileSystem.predicates()).thenReturn(predicates); + when(predicates.hasRelativePath("package.json")).thenReturn(predicate); + when(fileSystem.inputFile(predicate)).thenReturn(inputFile); + when(inputFile.isEmpty()).thenReturn(true); + + assertThatCode(() -> new OptimizeBrowserslistTagInPackageJsonSensor(fileSystem).execute(context)).doesNotThrowAnyException(); + + verify(context, never()).newIssue(); + } + +} \ No newline at end of file diff --git a/test-project/package.json b/test-project/package.json index 48eccd6..6693dc6 100644 --- a/test-project/package.json +++ b/test-project/package.json @@ -13,6 +13,9 @@ "lint:report": "eslint src/. -f json -o eslint-report.json", "sonar": "sonar" }, + "browserslist": [ + "defaults" + ], "devDependencies": { "@creedengo/eslint-plugin": "../eslint-plugin", "@eslint/js": "^10.0.1", diff --git a/test-project/sonar-project.properties b/test-project/sonar-project.properties index 764219c..1866cc0 100644 --- a/test-project/sonar-project.properties +++ b/test-project/sonar-project.properties @@ -1,3 +1,3 @@ sonar.exclusions=node_modules -sonar.sources=src +sonar.sources=src,package.json sonar.projectKey=creedengo-javascript-test-project diff --git a/test-project/yarn.lock b/test-project/yarn.lock index 4cd76f6..4b25748 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=30e64e&locator=creedengo-javascript-test-project%40workspace%3A." + resolution: "@creedengo/eslint-plugin@file:../eslint-plugin#../eslint-plugin::hash=b22b11&locator=creedengo-javascript-test-project%40workspace%3A." peerDependencies: eslint: ^9.0.0 || ^10.0.0 - checksum: 10c0/35357906578cb26b758f44fb8fdb12656de3a6d3297d97002f3a1c755066b81e1321badd1f4a01a9e9d156b0545b5611774c2ed83e80600168c134f0dc0846fd + checksum: 10c0/b46e04628e19ea19cec1c78d01cbed468c031304326eedb1a30d39aa8451e1347daee2c7fd823a509f14df81d2801fba82e6d4a0fcecfad832b5d69eeed7bd17 languageName: node linkType: hard