Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ project(':scan-command') {
dependsOn ':test-compiler-plugins:exampleOrg-plugin:build'
dependsOn ':test-compiler-plugins:ballerina-plugin:build'
dependsOn ':test-compiler-plugins:ballerinax-plugin:build'
dependsOn ':test-compiler-plugins:invalid-rules-plugin:build'
dependsOn ':test-compiler-plugins:invalid-ruleformat-plugin:build'
dependsOn ':test-compiler-plugins:invalid-rulekind-plugin:build'
dependsOn ':test-static-code-analysis-platform-plugins:exampleOrg-static-code-analysis-platform-plugin:build'
}
}
Expand All @@ -56,24 +53,6 @@ project(':test-compiler-plugins:ballerinax-plugin') {
}
}

project(':test-compiler-plugins:invalid-rules-plugin') {
dependencies {
implementation project(':scan-command')
}
}

project(':test-compiler-plugins:invalid-ruleformat-plugin') {
dependencies {
implementation project(':scan-command')
}
}

project(':test-compiler-plugins:invalid-rulekind-plugin') {
dependencies {
implementation project(':scan-command')
}
}

project(':test-static-code-analysis-platform-plugins:exampleOrg-static-code-analysis-platform-plugin') {
dependencies {
implementation project(':scan-command')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class TestReporter extends ReporterImpl {
}

@Override
protected List<Issue> getIssues() {
public List<Issue> getIssues() {
return super.getIssues();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org)
*
* 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;

import io.ballerina.projects.plugins.CodeAnalyzer;

/**
* This class provides a convenient abstraction for creating external analyzers
* that need to provide rules through the service loading mechanism.
*
* <p>
* External analyzer implementations should extend this class and provide
* implementations for the abstract methods. The class handles the integration
* with the Ballerina compiler plugin framework and the static code analysis
* tool.
* </p>
*
* @since 0.10.0
*/
public abstract class ExternalCodeAnalyzer extends CodeAnalyzer implements RuleProvider {
/**
* Default constructor for the external code analyzer.
* This constructor is used for service loading and should not be called directly.
*/
protected ExternalCodeAnalyzer() {
// Default constructor for service loading
}
}
35 changes: 35 additions & 0 deletions scan-command/src/main/java/io/ballerina/scan/RuleProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org)
*
* 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;

/**
* Service provider interface for discovering rules from code analyzers.
* This interface should be implemented by classes that extend CodeAnalyzer
* and want to provide rules through the service loading mechanism.
*
* @since 0.10.0
*/
public interface RuleProvider {
/**
* Returns all Rule instances provided by this analyzer.
*
* @return an iterable of rules provided by this analyzer
*/
Iterable<Rule> getRules();
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@

package io.ballerina.scan.internal;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.ballerina.compiler.api.SemanticModel;
import io.ballerina.projects.CompilerPluginCache;
import io.ballerina.projects.Document;
Expand All @@ -37,55 +33,41 @@
import io.ballerina.projects.internal.model.CompilerPluginDescriptor;
import io.ballerina.scan.Issue;
import io.ballerina.scan.Rule;
import io.ballerina.scan.RuleKind;
import io.ballerina.scan.RuleProvider;
import io.ballerina.scan.ScannerContext;
import io.ballerina.scan.utils.DiagnosticCode;
import io.ballerina.scan.utils.DiagnosticLog;
import io.ballerina.scan.utils.ScanTomlFile;
import io.ballerina.scan.utils.ScanToolException;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static io.ballerina.projects.util.ProjectConstants.IMPORT_PREFIX;
import static io.ballerina.scan.internal.ScanToolConstants.BUG;
import static io.ballerina.scan.internal.ScanToolConstants.CODE_SMELL;
import static io.ballerina.scan.internal.ScanToolConstants.FORWARD_SLASH;
import static io.ballerina.scan.internal.ScanToolConstants.IMPORT_GENERATOR_FILE;
import static io.ballerina.scan.internal.ScanToolConstants.RULES_FILE;
import static io.ballerina.scan.internal.ScanToolConstants.RULE_DESCRIPTION;
import static io.ballerina.scan.internal.ScanToolConstants.RULE_ID;
import static io.ballerina.scan.internal.ScanToolConstants.RULE_KIND;
import static io.ballerina.scan.internal.ScanToolConstants.SCANNER_CONTEXT;
import static io.ballerina.scan.internal.ScanToolConstants.USE_IMPORT_AS_UNDERSCORE;
import static io.ballerina.scan.internal.ScanToolConstants.VULNERABILITY;

/**
* Represents the project analyzer used for analyzing projects.
*
* @since 0.1.0
* */
*/
public class ProjectAnalyzer {
private final Project project;
private final ScanTomlFile scanTomlFile;
private final Gson gson = new Gson();
private String pluginImportsDocumentName;

protected ProjectAnalyzer(Project project, ScanTomlFile scanTomlFile) {
Expand Down Expand Up @@ -144,26 +126,66 @@ Map<String, List<Rule>> getExternalAnalyzers() {
ResolvedPackageDependency rootPkgNode = new ResolvedPackageDependency(project.currentPackage(),
PackageDependencyScope.DEFAULT);
Map<String, List<Rule>> externalAnalyzers = new HashMap<>();

packageResolution.dependencyGraph().getDirectDependencies(rootPkgNode).stream()
.map(ResolvedPackageDependency::packageInstance).forEach(pkgDependency -> {
PackageManifest pkgManifest = pkgDependency.manifest();
String org = pkgManifest.org().value();
String name = pkgManifest.name().value();
String pluginName = org + FORWARD_SLASH + name;
if (pkgManifest.compilerPluginDescriptor().isEmpty()) {
return;
}
CompilerPluginDescriptor pluginDesc = pkgManifest.compilerPluginDescriptor().get();
Optional<String> ruleFileContent = loadRuleFileContent(pluginName, pluginDesc);
if (ruleFileContent.isEmpty()) {

CompilerPluginDescriptor pluginDesc = pkgDependency.manifest().compilerPluginDescriptor()
.orElse(null);
if (pluginDesc == null) {
externalAnalyzers.put(pkgDependency.manifest().compilerPluginDescriptor().toString(),
new ArrayList<>());
return;
}
List<Rule> externalRules = loadExternalRules(org, name, pluginName, ruleFileContent.get());
List<Rule> externalRules;
try {
externalRules = loadRulesFromEnum(pluginDesc, org, name);
} catch (IOException e) {
DiagnosticLog.error(DiagnosticCode.FAILED_TO_LOAD_COMPILER_PLUGIN,
"IOException occurred while loading rules: " + e.getMessage());
externalRules = new ArrayList<>();
} catch (ScanToolException e) {
DiagnosticLog.error(DiagnosticCode.FAILED_TO_LOAD_COMPILER_PLUGIN,
"ScanToolException occurred while loading rules: " + e.getMessage());
externalRules = new ArrayList<>();
}
externalAnalyzers.put(pluginDesc.plugin().getClassName(), externalRules);
});
return externalAnalyzers;
}

private List<Rule> loadRulesFromEnum(CompilerPluginDescriptor pluginDesc, String org, String name)
throws IOException {
List<URL> jarUrls = pluginDesc.dependencies().stream().map(dependency -> {
try {
return Path.of(dependency.getPath()).toUri().toURL();
} catch (MalformedURLException ex) {
throw new ScanToolException(
DiagnosticLog.error(DiagnosticCode.FAILED_TO_LOAD_COMPILER_PLUGIN, ex.getMessage()));
}
}).toList();

try (URLClassLoader ucl = URLClassLoader.newInstance(jarUrls.toArray(URL[]::new),
this.getClass().getClassLoader())) {
ServiceLoader<RuleProvider> loader = ServiceLoader.load(RuleProvider.class, ucl);
List<Rule> rules = new ArrayList<>();
for (RuleProvider provider : loader) {
for (Rule r : provider.getRules()) {
Rule inMemoryRule = RuleFactory.createRule(r.numericId(),
r.description(), r.kind(), org, name);
rules.add(inMemoryRule);
}
}
return rules;
}
}

private void extractAnalyzerImportsAndDependencies(ScanTomlFile.Analyzer analyzer, StringBuilder imports,
StringBuilder dependencies) {
String org = analyzer.org();
Expand All @@ -190,104 +212,12 @@ private void buildStringWithNewLine(StringBuilder stringBuilder, String content)
stringBuilder.append(content).append(System.lineSeparator());
}

private Optional<String> loadRuleFileContent(String pluginName, CompilerPluginDescriptor pluginDesc) {
InputStream resource = loadResource(pluginDesc);
if (resource == null) {
return Optional.empty();
}

String resourceContent;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource,
StandardCharsets.UTF_8))) {
resourceContent = reader.lines().collect(Collectors.joining());
} catch (IOException ex) {
throw new ScanToolException(DiagnosticLog.error(DiagnosticCode.READING_RULES_FILE, RULES_FILE, pluginName,
ex.getMessage()));
}
return Optional.of(resourceContent);
}

private InputStream loadResource(CompilerPluginDescriptor pluginDesc) {
List<URL> jarUrls = pluginDesc.dependencies().stream().map(dependency -> {
try {
return Path.of(dependency.getPath()).toUri().toURL();
} catch (MalformedURLException ex) {
throw new ScanToolException(
DiagnosticLog.error(DiagnosticCode.FAILED_TO_LOAD_COMPILER_PLUGIN,
ex.getMessage()));
}
}).toList();

URLClassLoader ucl = URLClassLoader.newInstance(jarUrls.toArray(URL[]::new),
this.getClass().getClassLoader());
return ucl.getResourceAsStream(RULES_FILE);
}

private List<Rule> loadExternalRules(String org, String name, String pluginName, String ruleFileContent) {
List<Rule> rules = new ArrayList<>();
JsonArray ruleArray = getRuleJsonArray(pluginName, ruleFileContent);
for (JsonElement rule : ruleArray) {
JsonObject ruleObject = getRuleObject(pluginName, rule);
RuleKind ruleKind = getRuleKind(pluginName, ruleObject.get(RULE_KIND).getAsString());
Rule inMemoryRule = RuleFactory.createRule(ruleObject.get(RULE_ID).getAsInt(),
ruleObject.get(RULE_DESCRIPTION).getAsString(), ruleKind, org, name);
rules.add(inMemoryRule);
}
return rules;
}

private JsonArray getRuleJsonArray(String pluginName, String ruleFileContent) {
Gson gson = new Gson();
JsonElement element = gson.fromJson(ruleFileContent, JsonElement.class);
if (!element.isJsonArray()) {
throw new ScanToolException(DiagnosticLog.error(DiagnosticCode.INVALID_JSON_FORMAT, RULES_FILE,
pluginName, gson.toJson(element)));
}
return element.getAsJsonArray();
}

private JsonObject getRuleObject(String pluginName, JsonElement rule) {
JsonObject ruleObject = rule.getAsJsonObject();
if (!isValidRule(ruleObject)) {
throw new ScanToolException(DiagnosticLog.error(DiagnosticCode.INVALID_JSON_FORMAT_RULE,
pluginName, gson.toJson(ruleObject)));
}
return ruleObject;
}

private boolean isValidRule(JsonObject ruleObject) {
return ruleObject.has(RULE_ID) &&
ruleObject.get(RULE_ID).isJsonPrimitive() &&
ruleObject.get(RULE_ID).getAsJsonPrimitive().isNumber() &&
ruleObject.has(RULE_KIND) &&
ruleObject.has(RULE_DESCRIPTION);
}

private RuleKind getRuleKind(String pluginName, String kind) {
switch (kind) {
case BUG -> {
return RuleKind.BUG;
}
case VULNERABILITY -> {
return RuleKind.VULNERABILITY;
}
case CODE_SMELL -> {
return RuleKind.CODE_SMELL;
}
default -> {
throw new ScanToolException(DiagnosticLog.error(DiagnosticCode.INVALID_JSON_FORMAT_RULE_KIND,
pluginName, Arrays.toString(RuleKind.values()), kind));
}
}
}

List<Issue> runExternalAnalyzers(Map<String, List<Rule>> externalAnalyzers) {
List<ScannerContext> scannerContextList = new ArrayList<>(externalAnalyzers.size());
for (Map.Entry<String, List<Rule>> externalAnalyzer : externalAnalyzers.entrySet()) {
ScannerContext scannerContext = getScannerContext(externalAnalyzer.getValue());
scannerContextList.add(scannerContext);

// Save the scanner context to plugin cache for the compiler plugin to use during package compilation
Map<String, Object> pluginProperties = new HashMap<>();
pluginProperties.put(SCANNER_CONTEXT, scannerContext);
project.projectEnvironmentContext()
Expand Down
1 change: 1 addition & 0 deletions scan-command/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module io.ballerina.scan {
uses io.ballerina.scan.StaticCodeAnalysisPlatformPlugin;
uses io.ballerina.scan.RuleProvider;
requires io.ballerina.cli;
requires io.ballerina.lang;
requires io.ballerina.parser;
Expand Down
Loading
Loading