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
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group=io.ballerina.scan
version=0.11.1-SNAPSHOT
version=0.12.0-SNAPSHOT

# Plugin versions
spotbugsPluginVersion=6.1.5
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();
}
}
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.12.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 @@ -33,7 +33,7 @@
* */
public class IssueImpl implements Issue {
private final BLangDiagnosticLocation location;
private final RuleImpl rule;
private final Rule rule;
private final Source source;
private final String fileName;
private final String filePath;
Expand All @@ -48,7 +48,7 @@ public class IssueImpl implements Issue {
this.location = new BLangDiagnosticLocation(lineRange.fileName(), lineRange.startLine().line(),
lineRange.endLine().line(), lineRange.startLine().offset(), lineRange.endLine().offset(),
textRange.startOffset(), textRange.length());
this.rule = (RuleImpl) rule;
this.rule = rule;
this.source = source;
this.fileName = fileName;
this.filePath = filePath;
Expand Down
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<>());
Comment on lines +142 to +143
Copy link

Copilot AI Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When pluginDesc is null, the current code uses pkgDependency.manifest().compilerPluginDescriptor().toString() as the key for externalAnalyzers. This key may be ambiguous or lead to unexpected behavior; consider using a more explicit fallback or skip adding an entry for dependencies without a valid descriptor.

Suggested change
externalAnalyzers.put(pkgDependency.manifest().compilerPluginDescriptor().toString(),
new ArrayList<>());
// Skip adding an entry for dependencies without a valid descriptor

Copilot uses AI. Check for mistakes.
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);
Copy link

Copilot AI Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be useful to log or handle the case where no RuleProvider implementations are found, so that the absence of external rules does not go unnoticed during runtime.

Copilot uses AI. Check for mistakes.
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
* Represents the implementation of the {@link Reporter} interface.
*
* @since 0.1.0
* */
*/
public class ReporterImpl implements Reporter {
private final List<Issue> issues = new ArrayList<>();
private final Map<Integer, Rule> rules = new HashMap<>();
Expand All @@ -65,7 +65,7 @@ public void reportIssue(Document reportedDocument, Location location, int ruleId

@Override
public void reportIssue(Document reportedDocument, Location location, Rule rule) {
issues.add(createIssue(reportedDocument, location, rule));
issues.add(createIssue(reportedDocument, location, rules.get(rule.numericId())));
}

private Issue createIssue(Document reportedDocument, Location location, Rule rule) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,13 @@
* @since 0.1.0
*/
public class ScanToolConstants {
static final String SCAN_COMMAND = "scan";
static final String BALLERINA_RULE_PREFIX = "ballerina:";
static final String BALLERINA_ORG = "ballerina";
static final String BALLERINAI_ORG = "ballerinai";
static final String BALLERINAX_ORG = "ballerinax";
static final String USE_IMPORT_AS_UNDERSCORE = " as _;";
static final String IMPORT_GENERATOR_FILE = "scan_file";
static final String RULES_FILE = "rules.json";
static final String RULE_KIND = "kind";
static final String BUG = "BUG";
static final String VULNERABILITY = "VULNERABILITY";
static final String CODE_SMELL = "CODE_SMELL";
static final String RULE_ID = "id";
static final String RULE_DESCRIPTION = "description";

public static final String SCAN_COMMAND = "scan";
public static final String BALLERINA_RULE_PREFIX = "ballerina:";
public static final String BALLERINA_ORG = "ballerina";
public static final String BALLERINAI_ORG = "ballerinai";
public static final String BALLERINAX_ORG = "ballerinax";
public static final String USE_IMPORT_AS_UNDERSCORE = " as _;";
public static final String IMPORT_GENERATOR_FILE = "scan_file";
public static final String SCANNER_CONTEXT = "ScannerContext";
public static final String FORWARD_SLASH = "/";

Expand Down
Loading
Loading