diff --git a/gradle.properties b/gradle.properties index 4805b6b..d875f58 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=io.ballerina.scan -version=0.9.1-SNAPSHOT +version=0.10.0-SNAPSHOT # Plugin versions spotbugsPluginVersion=6.1.5 diff --git a/scan-command-test-utils/build.gradle b/scan-command-test-utils/build.gradle new file mode 100644 index 0000000..519e624 --- /dev/null +++ b/scan-command-test-utils/build.gradle @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import com.github.spotbugs.snom.Confidence +import com.github.spotbugs.snom.Effort + +plugins { + id 'java-library' + id 'checkstyle' + id 'maven-publish' + id 'com.github.spotbugs' + id 'de.undercouch.download' +} + +group = "${group}" +version = "${version}" + +repositories { + mavenLocal() + mavenCentral() + maven { + url = 'https://maven.pkg.github.com/ballerina-platform/*' + credentials { + username System.getenv("packageUser") + password System.getenv("packagePAT") + } + } +} + +dependencies { + implementation project(':scan-command') + implementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-cli', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-tools-api', version: "${ballerinaLangVersion}" + implementation group: 'org.testng', name: 'testng', version: "${testngVersion}" +} + +// Publish scan-command-test-utils package to GitHub packages +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + groupId group + artifactId project.name + version version + } + } + + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/ballerina-platform/static-code-analysis-tool") + credentials { + username = System.getenv("publishUser") + password = System.getenv("publishPAT") + } + } + } +} + +tasks.register('downloadCheckstyleRuleFiles', Download) { + src([ + 'https://raw.githubusercontent.com/wso2/code-quality-tools/v1.4/checkstyle/jdk-17/checkstyle.xml', + 'https://raw.githubusercontent.com/wso2/code-quality-tools/v1.4/checkstyle/jdk-17/suppressions.xml' + ]) + overwrite false + onlyIfNewer true + dest buildDir +} + +artifacts.add('default', file("${project.buildDir}/checkstyle.xml")) { + builtBy(downloadCheckstyleRuleFiles) +} + +artifacts.add('default', file("${project.buildDir}/suppressions.xml")) { + builtBy(downloadCheckstyleRuleFiles) +} + +def excludePattern = '**/module-info.java' +tasks.withType(Checkstyle).configureEach { + exclude excludePattern +} + +checkstyle { + toolVersion "${project.puppycrawlCheckstyleVersion}" + configFile rootProject.file("${project.buildDir}/checkstyle.xml") + configProperties = ["suppressionFile": file("${project.buildDir}/suppressions.xml")] +} + +checkstyleMain.dependsOn(downloadCheckstyleRuleFiles) +checkstyleTest.dependsOn(downloadCheckstyleRuleFiles) + +spotbugsMain { + effort = Effort.valueOf("MAX") + reportLevel = Confidence.valueOf("LOW") + + reportsDir = file("$project.buildDir/reports/spotbugs") + + reports { + html.required.set(true) + text.required.set(true) + } + + def excludeFile = file("${projectDir}/spotbugs-exclude.xml") + if (excludeFile.exists()) { + excludeFilter = excludeFile + } +} diff --git a/scan-command-test-utils/spotbugs-exclude.xml b/scan-command-test-utils/spotbugs-exclude.xml new file mode 100644 index 0000000..2a4c079 --- /dev/null +++ b/scan-command-test-utils/spotbugs-exclude.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/scan-command-test-utils/src/main/java/io/ballerina/scan/test/Assertions.java b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/Assertions.java new file mode 100644 index 0000000..39ecd15 --- /dev/null +++ b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/Assertions.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.scan.test; + +import io.ballerina.scan.Issue; +import io.ballerina.scan.Rule; +import io.ballerina.scan.RuleKind; +import io.ballerina.scan.Source; +import org.testng.Assert; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Test utility class to assert the issues found during a scan. + * + * @since 0.10.0 + */ +public class Assertions { + + private Assertions() { + } + + /** + * Assert that the list of issues contains an issue with the given rule ID, file name, start line, end line, + * and source at the given index. + * + * @param issues list of issues from which the issue should be found + * @param index index of the issue to check + * @param ruleId rule ID to check for + * @param fileName file name to check for + * @param startLine start line to check for + * @param endLine end line to check for + * @param source source to check for + */ + public static void assertIssue(List issues, + int index, + String ruleId, + String fileName, + int startLine, + int endLine, + Source source) { + Assert.assertTrue(index < issues.size(), "Index out of bounds for the issues list"); + Issue issue = issues.get(index); + Assert.assertEquals(issue.rule().id(), ruleId, "Rule ID mismatch"); + Assert.assertEquals(issue.source(), source, "Source mismatch"); + Assert.assertEquals(issue.location().lineRange().fileName(), fileName, "File name mismatch"); + Assert.assertEquals(issue.location().lineRange().startLine().line(), startLine, "Start line mismatch"); + Assert.assertEquals(issue.location().lineRange().endLine().line(), endLine, "End line mismatch"); + } + + /** + * Assert that the list of rules contains a rule with the given ID, description, and kind. + * + * @param rules list of rules from which the rule should be found + * @param id rule ID to check for + * @param description rule description to check for + * @param kind rule kind to check for + */ + public static void assertRule(List rules, String id, String description, RuleKind kind) { + boolean found = rules.stream().anyMatch(rule -> + rule.id().equals(id) && + rule.description().equals(description) && + rule.kind() == kind); + + if (!found) { + String summary = rules.stream() + .map(rule -> String.format("Rule ID: %s, Description: %s, Kind: %s", rule.id(), + rule.description(), rule.kind())) + .collect(Collectors.joining("\n")); + Assert.fail(String.format("Expected rule with ID '%s' not found.%nFound rules:%n%s", id, summary)); + } + } +} diff --git a/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestOptions.java b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestOptions.java new file mode 100644 index 0000000..deac2bb --- /dev/null +++ b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestOptions.java @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.scan.test; + +import io.ballerina.projects.Project; +import io.ballerina.scan.Rule; + +import java.io.PrintStream; +import java.util.Collections; +import java.util.List; + +/** + * Test utility class to hold scan options for testing purposes. + * + * @since 0.10.0 + */ +public class TestOptions { + private final Project project; + private final PrintStream outputStream; + private final boolean helpFlag; + private final boolean platformTriggered; + private final String targetDir; + private final boolean scanReport; + private final boolean listRules; + private final List includeRules; + private final List excludeRules; + private final List platforms; + + private TestOptions(Project project, PrintStream outputStream, boolean helpFlag, boolean platformTriggered, + String targetDir, boolean scanReport, boolean listRules, List includeRules, + List excludeRules, List platforms) { + this.project = project; + this.outputStream = outputStream; + this.helpFlag = helpFlag; + this.platformTriggered = platformTriggered; + this.targetDir = targetDir; + this.scanReport = scanReport; + this.listRules = listRules; + this.includeRules = includeRules; + this.excludeRules = excludeRules; + this.platforms = platforms; + } + + /** + * Create a new {@code TestOptionsBuilder} instance. + * + * @param project the project to be scanned + * @return a new {@code TestOptionsBuilder} instance + */ + public static TestOptionsBuilder builder(Project project) { + return new TestOptionsBuilder(project); + } + + /** + * Get the project to be scanned. + * + * @return the project to be scanned + */ + Project project() { + return project; + } + + /** + * Get the output stream. + * + * @return the output stream + */ + PrintStream outputStream() { + return outputStream; + } + + /** + * Get if the help flag is enabled or not. + * + * @return true if the help flag is enabled, false otherwise + */ + boolean helpFlag() { + return helpFlag; + } + + /** + * Get if the scan is triggered by a platform or not. + * + * @return true if the scan is triggered by a platform, false otherwise + */ + boolean platformTriggered() { + return platformTriggered; + } + + /** + * Get the target directory. + * + * @return the target directory + */ + String targetDir() { + return targetDir; + } + + /** + * Get if the scan report is enabled or not. + * + * @return true if the scan report is enabled, false otherwise + */ + boolean scanReport() { + return scanReport; + } + + /** + * Get if the rules should be listed or not. + * + * @return true if the rules should be listed, false otherwise + */ + boolean listRules() { + return listRules; + } + + /** + * Get the list of rules to be included. + * + * @return the list of rules to be included + */ + List includeRules() { + return includeRules; + } + + /** + * Get the list of rules to be excluded. + * + * @return the list of rules to be excluded + */ + List excludeRules() { + return excludeRules; + } + + /** + * Get the list of platforms. + * + * @return the list of platforms + */ + List platforms() { + return platforms; + } + + public static class TestOptionsBuilder { + private final Project project; + private PrintStream outputStream; + private boolean helpFlag; + private boolean platformTriggered; + private String targetDir; + private boolean scanReport; + private boolean listRules; + private List includeRules = List.of(); + private List excludeRules = List.of(); + private List platforms = List.of(); + + private TestOptionsBuilder(Project project) { + this.project = project; + } + + /** + * Set the output stream. + * + * @param outputStream the output stream + * @return this builder + */ + public TestOptionsBuilder setOutputStream(PrintStream outputStream) { + this.outputStream = outputStream; + return this; + } + + /** + * Set the help flag. + * + * @param helpFlag true if the help flag needs to be enabled, false otherwise + * @return this builder + */ + public TestOptionsBuilder setHelpFlag(boolean helpFlag) { + this.helpFlag = helpFlag; + return this; + } + + /** + * Set if the scan is triggered by a platform. + * + * @param platformTriggered true if the scan is triggered by a platform, false otherwise + * @return this builder + */ + public TestOptionsBuilder setPlatformTriggered(boolean platformTriggered) { + this.platformTriggered = platformTriggered; + return this; + } + + /** + * Set the target directory. + * + * @param targetDir the target directory + * @return this builder + */ + public TestOptionsBuilder setTargetDir(String targetDir) { + this.targetDir = targetDir; + return this; + } + + /** + * Set if the scan report needs to be enabled. + * + * @param scanReport true if the scan report needs to be enabled, false otherwise + * @return this builder + */ + public TestOptionsBuilder setScanReport(boolean scanReport) { + this.scanReport = scanReport; + return this; + } + + /** + * Set if the rules should be listed. + * + * @param listRules true if the rules should be listed, false otherwise + * @return this builder + */ + public TestOptionsBuilder setListRules(boolean listRules) { + this.listRules = listRules; + return this; + } + + /** + * Set the list of rules to be included. + * + * @param includeRules the list of rules to be included + * @return this builder + */ + public TestOptionsBuilder setIncludeRules(List includeRules) { + this.includeRules = Collections.unmodifiableList(includeRules); + return this; + } + + /** + * Set the list of rules to be excluded. + * + * @param excludeRules the list of rules to be excluded + * @return this builder + */ + public TestOptionsBuilder setExcludeRules(List excludeRules) { + this.excludeRules = Collections.unmodifiableList(excludeRules); + return this; + } + + /** + * Set the list of platforms. + * + * @param platforms the list of platforms + * @return this builder + */ + public TestOptionsBuilder setPlatforms(List platforms) { + this.platforms = Collections.unmodifiableList(platforms); + return this; + } + + /** + * Build the {@code TestOptions} instance. + * + * @return the built {@code TestOptions} instance + */ + public TestOptions build() { + return new TestOptions(project, outputStream, helpFlag, platformTriggered, + targetDir, scanReport, listRules, includeRules, excludeRules, platforms); + } + } +} diff --git a/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestProjectAnalyzer.java b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestProjectAnalyzer.java new file mode 100644 index 0000000..d194777 --- /dev/null +++ b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestProjectAnalyzer.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.scan.test; + +import io.ballerina.projects.Project; +import io.ballerina.scan.Rule; +import io.ballerina.scan.ScannerContext; +import io.ballerina.scan.internal.ProjectAnalyzer; +import io.ballerina.scan.utils.ScanTomlFile; + +import java.util.ArrayList; +import java.util.List; + +/** + * Test utility class to analyze a project and return the reporters. + * + * @since 0.10.0 + */ +class TestProjectAnalyzer extends ProjectAnalyzer { + private final List reporters; + + TestProjectAnalyzer(Project project, ScanTomlFile scanTomlFile) { + super(project, scanTomlFile); + this.reporters = new ArrayList<>(); + } + + @Override + protected ScannerContext getScannerContext(List rules) { + TestScannerContext scannerContext = new TestScannerContext(rules); + reporters.add(scannerContext.getReporter()); + return scannerContext; + } + + List getReporters() { + return reporters; + } +} diff --git a/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestReporter.java b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestReporter.java new file mode 100644 index 0000000..cecbdd2 --- /dev/null +++ b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestReporter.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.scan.test; + +import io.ballerina.scan.Issue; +import io.ballerina.scan.Rule; +import io.ballerina.scan.internal.ReporterImpl; + +import java.util.List; + +/** + * Test utility implementation of {@link ReporterImpl}. + * + * @since 0.10.0 + */ +class TestReporter extends ReporterImpl { + TestReporter(List rules) { + super(rules); + } + + @Override + protected List getIssues() { + return super.getIssues(); + } +} diff --git a/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestRunner.java b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestRunner.java new file mode 100644 index 0000000..2b5093e --- /dev/null +++ b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestRunner.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.scan.test; + +import io.ballerina.scan.Issue; +import io.ballerina.scan.Rule; + +import java.nio.file.Path; +import java.util.List; + +/** + * Test utility class to run a scan command and return the issues. + * + * @since 0.10.0 + */ +public class TestRunner { + private final TestScanCmd scanCmd; + + /** + * Create a new {@code ScanTestRunner} instance. + * + * @param testOptions options to perform the scan + */ + public TestRunner(TestOptions testOptions) { + scanCmd = new TestScanCmd(testOptions); + } + + /** + * Create a new {@code ScanTestRunner} instance. + * + * @param projectPath path to the project to be scanned + * @param distributionPath path to the Ballerina distribution to be used for the scan + */ + public TestRunner(Path projectPath, Path distributionPath) { + scanCmd = new TestScanCmd(projectPath, distributionPath); + } + + /** + * Perform a scan. + * + */ + public void performScan() { + scanCmd.execute(); + } + + /** + * Get the issues found during the scan. + * + * @return list of issues found during the scan + */ + public List getIssues() { + return scanCmd.getProjectAnalyzer().getReporters().stream() + .flatMap(reporter -> reporter.getIssues().stream()).toList(); + } + + /** + * Get the rules registered for the scan. + * + * @return list of rules registered for the scan + */ + public List getRules() { + return scanCmd.getAllRules(); + } +} diff --git a/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestScanCmd.java b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestScanCmd.java new file mode 100644 index 0000000..bac6b55 --- /dev/null +++ b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestScanCmd.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.scan.test; + +import io.ballerina.projects.Project; +import io.ballerina.projects.ProjectEnvironmentBuilder; +import io.ballerina.projects.directory.BuildProject; +import io.ballerina.projects.directory.SingleFileProject; +import io.ballerina.projects.environment.Environment; +import io.ballerina.projects.environment.EnvironmentBuilder; +import io.ballerina.scan.internal.ProjectAnalyzer; +import io.ballerina.scan.internal.ScanCmd; +import io.ballerina.scan.utils.ScanTomlFile; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Optional; + +/** + * TestScanCmd extends ScanCmd to extend it for testing purposes. + * + * @since 0.10.0 + */ +public class TestScanCmd extends ScanCmd { + private final Project project; + private TestProjectAnalyzer projectAnalyzer; + + TestScanCmd(TestOptions options) { + super( + options.project().sourceRoot(), + options.outputStream(), + options.helpFlag(), + options.platformTriggered(), + options.targetDir(), + options.scanReport(), + options.listRules(), + options.includeRules(), + options.excludeRules(), + options.platforms() + ); + this.project = options.project(); + } + + TestScanCmd(Path projectPath, Path distributionPath) { + super( + projectPath, + System.out, + false, + false, + null, + false, + false, + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ); + if (projectPath.toFile().isDirectory()) { + project = BuildProject.load(getEnvironmentBuilder(distributionPath), projectPath); + } else { + project = SingleFileProject.load(getEnvironmentBuilder(distributionPath), projectPath); + } + } + + @Override + protected Optional getProject() { + return Optional.of(this.project); + } + + @Override + protected ProjectAnalyzer getProjectAnalyzer(Project project, ScanTomlFile scanTomlFile) { + this.projectAnalyzer = new TestProjectAnalyzer(project, scanTomlFile); + return this.projectAnalyzer; + } + + TestProjectAnalyzer getProjectAnalyzer() { + return this.projectAnalyzer; + } + + private static ProjectEnvironmentBuilder getEnvironmentBuilder(Path distributionPath) { + Environment environment = EnvironmentBuilder.getBuilder().setBallerinaHome(distributionPath).build(); + return ProjectEnvironmentBuilder.getBuilder(environment); + } +} diff --git a/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestScannerContext.java b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestScannerContext.java new file mode 100644 index 0000000..86a5af6 --- /dev/null +++ b/scan-command-test-utils/src/main/java/io/ballerina/scan/test/TestScannerContext.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.scan.test; + +import io.ballerina.scan.Rule; +import io.ballerina.scan.ScannerContext; + +import java.util.List; + +/** + * Test utility subclass of {@link ScannerContext}. + * + * @since 0.10.0 + */ +class TestScannerContext implements ScannerContext { + private final TestReporter reporter; + + TestScannerContext(List rules) { + this.reporter = new TestReporter(rules); + } + + /** + * Returns the {@link TestReporter} to be used to report identified issues. + * + * @return reporter that needs to be used to report issues identified. + */ + @Override + public TestReporter getReporter() { + return reporter; + } +} diff --git a/scan-command-test-utils/src/main/java/module-info.java b/scan-command-test-utils/src/main/java/module-info.java new file mode 100644 index 0000000..2cf8292 --- /dev/null +++ b/scan-command-test-utils/src/main/java/module-info.java @@ -0,0 +1,8 @@ +module io.ballerina.scan.test { + requires io.ballerina.lang; + requires io.ballerina.tools.api; + requires org.testng; + requires io.ballerina.scan; + + exports io.ballerina.scan.test; +} diff --git a/scan-command/build.gradle b/scan-command/build.gradle index 3c23f66..3bea904 100644 --- a/scan-command/build.gradle +++ b/scan-command/build.gradle @@ -76,6 +76,7 @@ dependencies { // Required dependencies for running scan tool tests testImplementation group: 'org.testng', name: 'testng', version: "${testngVersion}" + testImplementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" // For including the scan report zip implementation files("${projectDir}/src/main/resources/report.zip") diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/ProjectAnalyzer.java b/scan-command/src/main/java/io/ballerina/scan/internal/ProjectAnalyzer.java index 84b7867..d68cbb6 100644 --- a/scan-command/src/main/java/io/ballerina/scan/internal/ProjectAnalyzer.java +++ b/scan-command/src/main/java/io/ballerina/scan/internal/ProjectAnalyzer.java @@ -82,13 +82,13 @@ * * @since 0.1.0 * */ -class ProjectAnalyzer { +public class ProjectAnalyzer { private final Project project; private final ScanTomlFile scanTomlFile; private final Gson gson = new Gson(); private String pluginImportsDocumentName; - ProjectAnalyzer(Project project, ScanTomlFile scanTomlFile) { + protected ProjectAnalyzer(Project project, ScanTomlFile scanTomlFile) { this.project = project; this.scanTomlFile = scanTomlFile; Module defaultModule = project.currentPackage().getDefaultModule(); @@ -284,7 +284,7 @@ private RuleKind getRuleKind(String pluginName, String kind) { List runExternalAnalyzers(Map> externalAnalyzers) { List scannerContextList = new ArrayList<>(externalAnalyzers.size()); for (Map.Entry> externalAnalyzer : externalAnalyzers.entrySet()) { - ScannerContextImpl scannerContext = new ScannerContextImpl(externalAnalyzer.getValue()); + ScannerContext scannerContext = getScannerContext(externalAnalyzer.getValue()); scannerContextList.add(scannerContext); // Save the scanner context to plugin cache for the compiler plugin to use during package compilation @@ -307,4 +307,8 @@ List runExternalAnalyzers(Map> externalAnalyzers) { } return externalIssues; } + + protected ScannerContext getScannerContext(List rules) { + return new ScannerContextImpl(rules); + } } diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/ReporterImpl.java b/scan-command/src/main/java/io/ballerina/scan/internal/ReporterImpl.java index 6efd325..c4be783 100644 --- a/scan-command/src/main/java/io/ballerina/scan/internal/ReporterImpl.java +++ b/scan-command/src/main/java/io/ballerina/scan/internal/ReporterImpl.java @@ -46,14 +46,12 @@ * * @since 0.1.0 * */ -class ReporterImpl implements Reporter { +public class ReporterImpl implements Reporter { private final List issues = new ArrayList<>(); private final Map rules = new HashMap<>(); - ReporterImpl(List rules) { - rules.forEach(rule -> { - this.rules.put(rule.numericId(), rule); - }); + protected ReporterImpl(List rules) { + rules.forEach(rule -> this.rules.put(rule.numericId(), rule)); } @Override @@ -96,7 +94,7 @@ private Issue createIssue(Document reportedDocument, Location location, Rule rul issuesFilePath.toString()); } - List getIssues() { + protected List getIssues() { return issues; } } diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/ScanCmd.java b/scan-command/src/main/java/io/ballerina/scan/internal/ScanCmd.java index 611fbe1..545720c 100644 --- a/scan-command/src/main/java/io/ballerina/scan/internal/ScanCmd.java +++ b/scan-command/src/main/java/io/ballerina/scan/internal/ScanCmd.java @@ -67,8 +67,8 @@ public class ScanCmd implements BLauncherCmd { private final PrintStream outputStream; - @CommandLine.Parameters(description = "Program arguments") - private final List argList = new ArrayList<>(); + @CommandLine.Parameters (arity = "0..1") + private final Path projectPath; @CommandLine.Option(names = {"--help", "-h", "?"}, hidden = true) private boolean helpFlag; @@ -103,12 +103,39 @@ public class ScanCmd implements BLauncherCmd { description = "Specify the comma separated list of static code analysis platforms to report issues") private List platforms = new ArrayList<>(); + private final List allRules = new ArrayList<>(); + public ScanCmd() { - this.outputStream = System.out; + this(System.out); } ScanCmd(PrintStream outputStream) { + this.projectPath = Paths.get(System.getProperty(ProjectConstants.USER_DIR)); + this.outputStream = outputStream; + } + + protected ScanCmd( + Path projectPath, + PrintStream outputStream, + boolean helpFlag, + boolean platformTriggered, + String targetDir, + boolean scanReport, + boolean listRules, + List includeRules, + List excludeRules, + List platforms + ) { + this.projectPath = projectPath; this.outputStream = outputStream; + this.helpFlag = helpFlag; + this.platformTriggered = platformTriggered; + this.targetDir = targetDir; + this.scanReport = scanReport; + this.listRules = listRules; + this.includeRules.addAll(includeRules.stream().map(Rule::id).toList()); + this.excludeRules.addAll(excludeRules.stream().map(Rule::id).toList()); + this.platforms.addAll(platforms); } @Override @@ -155,7 +182,7 @@ public void execute() { return; } - ProjectAnalyzer projectAnalyzer = new ProjectAnalyzer(project.get(), scanTomlFile.get()); + ProjectAnalyzer projectAnalyzer = getProjectAnalyzer(project.get(), scanTomlFile.get()); List coreRules = CoreRule.rules(); Map> externalAnalyzers; try { @@ -165,9 +192,10 @@ public void execute() { return; } + allRules.addAll(coreRules); + externalAnalyzers.values().forEach(allRules::addAll); if (listRules) { - externalAnalyzers.values().forEach(coreRules::addAll); - ScanUtils.printRulesToConsole(coreRules, outputStream); + ScanUtils.printRulesToConsole(allRules, outputStream); return; } @@ -275,33 +303,31 @@ private StringBuilder helpMessage() { return builder; } - private Optional getProject() { - if (argList.isEmpty()) { - try { - return Optional.of(BuildProject.load(Paths.get(System.getProperty(ProjectConstants.USER_DIR)))); - } catch (RuntimeException ex) { - outputStream.println(ex.getMessage()); - return Optional.empty(); - } - } - - if (argList.size() != 1) { - outputStream.println(DiagnosticLog.error(DiagnosticCode.INVALID_NUMBER_OF_ARGUMENTS, argList.size())); - return Optional.empty(); - } - - Path path = Paths.get(argList.get(0)); + protected Optional getProject() { try { - if (path.toFile().isDirectory()) { - return Optional.of(BuildProject.load(path)); + if (projectPath.toFile().isDirectory()) { + return Optional.of(BuildProject.load(projectPath)); } - return Optional.of(SingleFileProject.load(path)); + return Optional.of(SingleFileProject.load(projectPath)); } catch (RuntimeException ex) { outputStream.println(ex.getMessage()); return Optional.empty(); } } + protected ProjectAnalyzer getProjectAnalyzer(Project project, ScanTomlFile scanTomlFile) { + return new ProjectAnalyzer(project, scanTomlFile); + } + + /** + * Get the list of all rules available for the given project. + * + * @return an unmodifiable list of all rules + */ + public List getAllRules() { + return Collections.unmodifiableList(allRules); + } + private URLClassLoader loadPlatformPlugins(List jarPaths) { List jarUrls = new ArrayList<>(jarPaths.size()); jarPaths.forEach(jarPath -> { diff --git a/scan-command/src/main/java/io/ballerina/scan/internal/ScanToolConstants.java b/scan-command/src/main/java/io/ballerina/scan/internal/ScanToolConstants.java index d6efee1..592feca 100644 --- a/scan-command/src/main/java/io/ballerina/scan/internal/ScanToolConstants.java +++ b/scan-command/src/main/java/io/ballerina/scan/internal/ScanToolConstants.java @@ -26,7 +26,6 @@ public class ScanToolConstants { static final String SCAN_COMMAND = "scan"; static final String BALLERINA_RULE_PREFIX = "ballerina:"; - static final String FORWARD_SLASH = "/"; static final String BALLERINA_ORG = "ballerina"; static final String BALLERINAI_ORG = "ballerinai"; static final String BALLERINAX_ORG = "ballerinax"; @@ -40,7 +39,8 @@ public class ScanToolConstants { static final String RULE_ID = "id"; static final String RULE_DESCRIPTION = "description"; - static final String SCANNER_CONTEXT = "ScannerContext"; + public static final String SCANNER_CONTEXT = "ScannerContext"; + public static final String FORWARD_SLASH = "/"; private ScanToolConstants() { } diff --git a/scan-command/src/main/java/module-info.java b/scan-command/src/main/java/module-info.java index 79eb380..fccf3af 100644 --- a/scan-command/src/main/java/module-info.java +++ b/scan-command/src/main/java/module-info.java @@ -1,4 +1,4 @@ -module io.ballerina.scan{ +module io.ballerina.scan { uses io.ballerina.scan.StaticCodeAnalysisPlatformPlugin; requires io.ballerina.cli; requires io.ballerina.lang; @@ -10,4 +10,6 @@ requires org.apache.commons.io; exports io.ballerina.scan; + exports io.ballerina.scan.internal to io.ballerina.scan.test; + exports io.ballerina.scan.utils to io.ballerina.scan.test; } diff --git a/scan-command/src/test/java/io/ballerina/scan/internal/ScanCmdTest.java b/scan-command/src/test/java/io/ballerina/scan/internal/ScanCmdTest.java index 1a4eed5..e8a940c 100644 --- a/scan-command/src/test/java/io/ballerina/scan/internal/ScanCmdTest.java +++ b/scan-command/src/test/java/io/ballerina/scan/internal/ScanCmdTest.java @@ -105,8 +105,8 @@ void testScanCommandWithHelpFlag() throws IOException { @Test(description = "test scan command with Ballerina project") void testScanCommandProject() throws IOException { - ScanCmd scanCmd = new ScanCmd(printStream); System.setProperty("user.dir", validBalProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); scanCmd.execute(); System.setProperty("user.dir", userDir); String expected = "Running Scans"; @@ -116,8 +116,8 @@ void testScanCommandProject() throws IOException { @Test(description = "test scan command with an empty Ballerina project") void testScanCommandEmptyProject() throws IOException { Path emptyBalProject = testResources.resolve("test-resources").resolve("empty-bal-project"); - ScanCmd scanCmd = new ScanCmd(printStream); System.setProperty("user.dir", emptyBalProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); scanCmd.execute(); System.setProperty("user.dir", userDir); String expected = DiagnosticLog.error(DiagnosticCode.EMPTY_PACKAGE); @@ -161,8 +161,8 @@ void testScanCommandSingleFileProjectWithDirectoryAsArgument() throws IOExceptio @Test(description = "test scan command with single file project without arguments") void testScanCommandSingleFileProjectWithoutArgument() throws IOException { Path validBalProject = testResources.resolve("test-resources").resolve("valid-single-file-project"); - ScanCmd scanCmd = new ScanCmd(printStream); System.setProperty("user.dir", validBalProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); scanCmd.execute(); System.setProperty("user.dir", userDir); String expected = "Invalid Ballerina package directory: " + validBalProject + @@ -173,14 +173,18 @@ void testScanCommandSingleFileProjectWithoutArgument() throws IOException { @Test(description = "test scan command with single file project with too many arguments") void testScanCommandSingleFileProjectWithTooManyArguments() throws IOException { Path validBalProject = testResources.resolve("test-resources").resolve("valid-single-file-project"); + System.setProperty("user.dir", validBalProject.toString()); ScanCmd scanCmd = new ScanCmd(printStream); String[] args = {"main.bal", "argument2"}; - new CommandLine(scanCmd).parseArgs(args); - System.setProperty("user.dir", validBalProject.toString()); - scanCmd.execute(); + try { + new CommandLine(scanCmd).parseArgs(args); + Assert.fail("Expected CommandLine.UnmatchedArgumentException"); + } catch (CommandLine.UnmatchedArgumentException e) { + String expected = "picocli.CommandLine$UnmatchedArgumentException: " + + "Unmatched argument at index 1: 'argument2'"; + Assert.assertEquals(e.toString(), expected); + } System.setProperty("user.dir", userDir); - String expected = DiagnosticLog.error(DiagnosticCode.INVALID_NUMBER_OF_ARGUMENTS, 2); - Assert.assertEquals(readOutput(true).trim(), expected); } @Test(description = "test scan command with method for saving results to file when analysis issues are present") @@ -245,12 +249,12 @@ void testPrintRulesToConsole() throws IOException { @Test(description = "test scan command with list rules flag") void testScanCommandWithListRulesFlag() throws IOException { - ScanCmd scanCmd = new ScanCmd(printStream); - String[] args = {"--list-rules"}; - new CommandLine(scanCmd).parseArgs(args); Path ballerinaProject = testResources.resolve("test-resources") .resolve("bal-project-with-config-file"); System.setProperty("user.dir", ballerinaProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); + String[] args = {"--list-rules"}; + new CommandLine(scanCmd).parseArgs(args); scanCmd.execute(); System.setProperty("user.dir", userDir); String expected = getExpectedOutput("list-rules-output.txt"); @@ -335,12 +339,12 @@ Object[][] rulesProvider() { @Test(description = "test scan command with include rules flag") void testScanCommandWithIncludeRulesFlag() throws IOException { - ScanCmd scanCmd = new ScanCmd(printStream); - String[] args = {"--include-rules=ballerina:1"}; - new CommandLine(scanCmd).parseArgs(args); Path ballerinaProject = testResources.resolve("test-resources") .resolve("bal-project-with-analyzer-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); + String[] args = {"--include-rules=ballerina:1"}; + new CommandLine(scanCmd).parseArgs(args); scanCmd.execute(); System.setProperty("user.dir", userDir); String result = Files.readString(ballerinaProject.resolve("target").resolve("report") @@ -352,12 +356,12 @@ void testScanCommandWithIncludeRulesFlag() throws IOException { @Test(description = "test scan command with exclude rules flag") void testScanCommandWithExcludeRulesFlag() throws IOException { - ScanCmd scanCmd = new ScanCmd(printStream); - String[] args = {"--exclude-rules=ballerina:1"}; - new CommandLine(scanCmd).parseArgs(args); Path ballerinaProject = testResources.resolve("test-resources") .resolve("bal-project-with-analyzer-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); + String[] args = {"--exclude-rules=ballerina:1"}; + new CommandLine(scanCmd).parseArgs(args); scanCmd.execute(); System.setProperty("user.dir", userDir); String result = Files.readString(ballerinaProject.resolve("target").resolve("report") @@ -369,12 +373,12 @@ void testScanCommandWithExcludeRulesFlag() throws IOException { @Test(description = "test scan command with include and exclude rules flag") void testScanCommandWithIncludeAndExcludeRulesFlags() throws IOException { - ScanCmd scanCmd = new ScanCmd(printStream); - String[] args = {"--include-rules=ballerina:1", "--exclude-rules=ballerina:1"}; - new CommandLine(scanCmd).parseArgs(args); Path ballerinaProject = testResources.resolve("test-resources") .resolve("bal-project-with-analyzer-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); + String[] args = {"--include-rules=ballerina:1", "--exclude-rules=ballerina:1"}; + new CommandLine(scanCmd).parseArgs(args); scanCmd.execute(); System.setProperty("user.dir", userDir); String expected = getExpectedOutput("include-exclude-rules.txt"); @@ -383,10 +387,10 @@ void testScanCommandWithIncludeAndExcludeRulesFlags() throws IOException { @Test(description = "test scan command with include rules Scan.toml configurations") void testScanCommandWithIncludeRulesScanTomlConfigurations() throws IOException { - ScanCmd scanCmd = new ScanCmd(printStream); Path ballerinaProject = testResources.resolve("test-resources") .resolve("bal-project-with-include-rule-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); scanCmd.execute(); System.setProperty("user.dir", userDir); String result = Files.readString(ballerinaProject.resolve("target").resolve("report") @@ -398,12 +402,12 @@ void testScanCommandWithIncludeRulesScanTomlConfigurations() throws IOException @Test(description = "test scan command with exclude rules Scan.toml configurations") void testScanCommandWithExcludeRulesScanTomlConfigurations() throws IOException { - ScanCmd scanCmd = new ScanCmd(printStream); - String[] args = {"--exclude-rules=ballerina:1"}; - new CommandLine(scanCmd).parseArgs(args); Path ballerinaProject = testResources.resolve("test-resources") .resolve("bal-project-with-exclude-rule-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); + String[] args = {"--exclude-rules=ballerina:1"}; + new CommandLine(scanCmd).parseArgs(args); scanCmd.execute(); System.setProperty("user.dir", userDir); String result = Files.readString(ballerinaProject.resolve("target").resolve("report") @@ -415,12 +419,12 @@ void testScanCommandWithExcludeRulesScanTomlConfigurations() throws IOException @Test(description = "test scan command with include and exclude rules Scan.toml configurations") void testScanCommandWithIncludeAndExcludeRulesScanTomlConfigurations() throws IOException { - ScanCmd scanCmd = new ScanCmd(printStream); - String[] args = {"--include-rules=ballerina:1", "--exclude-rules=ballerina:1"}; - new CommandLine(scanCmd).parseArgs(args); Path ballerinaProject = testResources.resolve("test-resources") .resolve("bal-project-with-include-exclude-rule-configurations"); System.setProperty("user.dir", ballerinaProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); + String[] args = {"--include-rules=ballerina:1", "--exclude-rules=ballerina:1"}; + new CommandLine(scanCmd).parseArgs(args); scanCmd.execute(); System.setProperty("user.dir", userDir); String expected = getExpectedOutput("toml-include-exclude-rules.txt"); @@ -429,7 +433,6 @@ void testScanCommandWithIncludeAndExcludeRulesScanTomlConfigurations() throws IO @Test(description = "test scan command with platform plugin configurations") void testScanCommandWithPlatformPluginConfigurations() throws IOException { - ScanCmd scanCmd = new ScanCmd(printStream); Path ballerinaProject = testResources.resolve("test-resources") .resolve("bal-project-with-platform-configurations"); Path rootProject = Path.of(System.getProperty("user.dir")).getParent(); @@ -450,6 +453,7 @@ void testScanCommandWithPlatformPluginConfigurations() throws IOException { StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); System.setProperty("user.dir", ballerinaProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); scanCmd.execute(); System.setProperty("user.dir", userDir); @@ -471,7 +475,6 @@ void testScanCommandWithPlatformPluginConfigurations() throws IOException { @Test(description = "test scan command with invalid platform plugin configurations") void testScanCommandWithInvalidPlatformPluginConfigurations() throws IOException { - ScanCmd scanCmd = new ScanCmd(printStream); Path ballerinaProject = testResources.resolve("test-resources") .resolve("bal-project-with-invalid-platform-configurations"); Path rootProject = Path.of(System.getProperty("user.dir")).getParent(); @@ -491,6 +494,7 @@ void testScanCommandWithInvalidPlatformPluginConfigurations() throws IOException StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); System.setProperty("user.dir", ballerinaProject.toString()); + ScanCmd scanCmd = new ScanCmd(printStream); scanCmd.execute(); System.setProperty("user.dir", userDir); String expected = getExpectedOutput("invalid-platform-plugin-configurations.txt"); diff --git a/scan-command/tool-scan/BalTool.toml b/scan-command/tool-scan/BalTool.toml index f6d8fba..dd7229e 100644 --- a/scan-command/tool-scan/BalTool.toml +++ b/scan-command/tool-scan/BalTool.toml @@ -2,4 +2,4 @@ id = "scan" [[dependency]] -path = "../build/libs/scan-command-0.9.1-SNAPSHOT.jar" +path = "../build/libs/scan-command-0.10.0-SNAPSHOT.jar" diff --git a/scan-command/tool-scan/Ballerina.toml b/scan-command/tool-scan/Ballerina.toml index c2cba22..c7649f4 100644 --- a/scan-command/tool-scan/Ballerina.toml +++ b/scan-command/tool-scan/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "tool_scan" -version = "0.9.1-SNAPSHOT" +version = "0.10.0-SNAPSHOT" distribution = "2201.12.0" authors = ["Ballerina"] keywords = ["scan", "static code analysis"] diff --git a/scan-command/tool-scan/Dependencies.toml b/scan-command/tool-scan/Dependencies.toml index 45439e1..f6476c4 100644 --- a/scan-command/tool-scan/Dependencies.toml +++ b/scan-command/tool-scan/Dependencies.toml @@ -10,7 +10,7 @@ distribution-version = "2201.12.0" [[package]] org = "ballerina" name = "tool_scan" -version = "0.9.1-SNAPSHOT" +version = "0.10.0-SNAPSHOT" modules = [ {org = "ballerina", packageName = "tool_scan", moduleName = "tool_scan"} ] diff --git a/settings.gradle b/settings.gradle index 7f7194a..252f6c1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,6 +32,7 @@ pluginManagement { rootProject.name = 'static-code-analysis-tool' include 'scan-command' include 'static-code-analysis-report' +include 'scan-command-test-utils' include 'test-compiler-plugins:exampleOrg-plugin' include 'test-compiler-plugins:ballerina-plugin' include 'test-compiler-plugins:ballerinax-plugin'