Skip to content
Open
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
3 changes: 3 additions & 0 deletions compiler-plugin-tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ dependencies {
testImplementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}"
testImplementation group: 'org.ballerinalang', name: 'ballerina-tools-api', version: "${ballerinaLangVersion}"
testImplementation group: 'org.ballerinalang', name: 'ballerina-parser', version: "${ballerinaLangVersion}"
testImplementation group: 'io.ballerina.scan', name: 'scan-command', version: "${balScanVersion}"
testImplementation group: 'io.ballerina.scan', name: 'scan-command-test-utils', version: "${balScanVersion}"
testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "${jacksonDatabindVersion}"

implementation group: 'org.testng', name: 'testng', version: "${testngVersion}"
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,105 +18,159 @@

package io.ballerina.stdlib.mysql.compiler.staticcodeanalyzer;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.ballerina.projects.Project;
import io.ballerina.projects.ProjectEnvironmentBuilder;
import io.ballerina.projects.directory.BuildProject;
import io.ballerina.projects.environment.Environment;
import io.ballerina.projects.environment.EnvironmentBuilder;
import io.ballerina.scan.Issue;
import io.ballerina.scan.Rule;
import io.ballerina.scan.Source;
import io.ballerina.scan.test.Assertions;
import io.ballerina.scan.test.TestOptions;
import io.ballerina.scan.test.TestRunner;
import org.testng.Assert;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Test;
import org.testng.internal.ExitCode;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static io.ballerina.scan.RuleKind.VULNERABILITY;
import static io.ballerina.stdlib.mysql.compiler.staticcodeanalyzer.MySQLRule.USE_SECURE_PASSWORD;
import static java.nio.charset.StandardCharsets.UTF_8;

public class StaticCodeAnalyzerTest {
private static final Path RESOURCE_PACKAGES_DIRECTORY = Paths
.get("src", "test", "resources", "static_code_analyzer", "ballerina_packages").toAbsolutePath();
private static final Path EXPECTED_JSON_OUTPUT_DIRECTORY = Paths
private static final Path EXPECTED_OUTPUT_DIRECTORY = Paths
.get("src", "test", "resources", "static_code_analyzer", "expected_output").toAbsolutePath();
private static final Path BALLERINA_PATH = getBalCommandPath();
private static final Path JSON_RULES_FILE_PATH = Paths
.get("../", "compiler-plugin", "src", "main", "resources", "rules.json").toAbsolutePath();
private static final String SCAN_COMMAND = "scan";

private static Path getBalCommandPath() {
String balCommand = isWindows() ? "bal.bat" : "bal";
return Paths.get("../", "target", "ballerina-runtime", "bin", balCommand).toAbsolutePath();
}

@BeforeSuite
public void pullScanTool() throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder(BALLERINA_PATH.toString(), "tool", "pull", SCAN_COMMAND);
ProcessOutputGobbler output = getOutput(processBuilder.start());
if (Pattern.compile("tool 'scan:.+\\..+\\..+' successfully set as the active version\\.")
.matcher(output.getOutput()).find() || Pattern.compile("tool 'scan:.+\\..+\\..+' is already active\\.")
.matcher(output.getOutput()).find()) {
return;
}
Assert.assertFalse(ExitCode.hasFailure(output.getExitCode()));
}
private static final Path DISTRIBUTION_PATH = Paths.get("../", "target", "ballerina-runtime");
private static final String MODULE_BALLERINAX_MYSQL = "module-ballerinax-mysql";

@Test
public void validateRulesJson() throws IOException {
String expectedRules = "[" + Arrays.stream(MySQLRule.values())
.map(MySQLRule::toString).collect(Collectors.joining(",")) + "]";
String actualRules = Files.readString(JSON_RULES_FILE_PATH);
assertJsonEqual(normalizeJson(actualRules), normalizeJson(expectedRules));
assertJsonEqual(actualRules, expectedRules);
}

@Test
public void testStaticCodeRules() throws IOException, InterruptedException {
public void testStaticCodeRulesWithAPI() throws IOException {
ByteArrayOutputStream console = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(console, true, UTF_8);

for (MySQLRule rule : MySQLRule.values()) {
String targetPackageName = "rule" + rule.getId();
String actualJsonReport = StaticCodeAnalyzerTest.executeScanProcess(targetPackageName);
String expectedJsonReport = Files
.readString(EXPECTED_JSON_OUTPUT_DIRECTORY.resolve(targetPackageName + ".json"));
assertJsonEqual(actualJsonReport, expectedJsonReport);
testIndividualRule(rule, console, printStream);
}
}

private void testIndividualRule(MySQLRule rule, ByteArrayOutputStream console, PrintStream printStream)
throws IOException {
String targetPackageName = "rule" + rule.getId();
Path targetPackagePath = RESOURCE_PACKAGES_DIRECTORY.resolve(targetPackageName);

TestRunner testRunner = setupTestRunner(targetPackagePath, printStream);
testRunner.performScan();

validateRules(testRunner.getRules());
validateIssues(rule, testRunner.getIssues());
validateOutput(console, targetPackageName);

console.reset();
}

private TestRunner setupTestRunner(Path targetPackagePath, PrintStream printStream) {
Project project = BuildProject.load(getEnvironmentBuilder(), targetPackagePath);
TestOptions options = TestOptions.builder(project).setOutputStream(printStream).build();
return new TestRunner(options);
}

private void validateRules(List<Rule> rules) {
Assertions.assertRule(
rules,
"ballerinax/mysql:1",
USE_SECURE_PASSWORD.getDescription(),
VULNERABILITY);
}

private void validateIssues(MySQLRule rule, List<Issue> issues) {
switch (rule) {
case USE_SECURE_PASSWORD:
// We expect 11 issues based on the rule1 package
Assert.assertEquals(issues.size(), 11);

// custom_prefix.bal (5 issues)
Assertions.assertIssue(issues, 0, "ballerinax/mysql:1", "custom_prefix.bal", 25, 30, Source.BUILT_IN);
Assertions.assertIssue(issues, 1, "ballerinax/mysql:1", "custom_prefix.bal", 36, 36, Source.BUILT_IN);
Assertions.assertIssue(issues, 2, "ballerinax/mysql:1", "custom_prefix.bal", 42, 42, Source.BUILT_IN);
Assertions.assertIssue(issues, 3, "ballerinax/mysql:1", "custom_prefix.bal", 47, 47, Source.BUILT_IN);
Assertions.assertIssue(issues, 4, "ballerinax/mysql:1", "custom_prefix.bal", 48, 48, Source.BUILT_IN);

// named_arg.bal (3 issues)
Assertions.assertIssue(issues, 5, "ballerinax/mysql:1", "named_arg.bal", 25, 31, Source.BUILT_IN);
Assertions.assertIssue(issues, 6, "ballerinax/mysql:1", "named_arg.bal", 37, 42, Source.BUILT_IN);
Assertions.assertIssue(issues, 7, "ballerinax/mysql:1", "named_arg.bal", 48, 52, Source.BUILT_IN);

// pos_arg.bal (3 issues)
Assertions.assertIssue(issues, 8, "ballerinax/mysql:1", "pos_arg.bal", 25, 31, Source.BUILT_IN);
Assertions.assertIssue(issues, 9, "ballerinax/mysql:1", "pos_arg.bal", 37, 37, Source.BUILT_IN);
Assertions.assertIssue(issues, 10, "ballerinax/mysql:1", "pos_arg.bal", 43, 43, Source.BUILT_IN);
break;
default:
Assert.fail("Unhandled rule in validateIssues: " + rule);
break;
}
}

private static String executeScanProcess(String targetPackage) throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder(BALLERINA_PATH.toString(), SCAN_COMMAND);
processBuilder.directory(RESOURCE_PACKAGES_DIRECTORY.resolve(targetPackage).toFile());
ProcessOutputGobbler output = getOutput(processBuilder.start());
Assert.assertFalse(ExitCode.hasFailure(output.getExitCode()));
return Files.readString(RESOURCE_PACKAGES_DIRECTORY.resolve(targetPackage)
.resolve("target").resolve("report").resolve("scan_results.json"));
private void validateOutput(ByteArrayOutputStream console, String targetPackageName) throws IOException {
String output = console.toString(UTF_8);
String jsonOutput = extractJson(output);
String expectedOutput = Files.readString(EXPECTED_OUTPUT_DIRECTORY.resolve(targetPackageName + ".json"));
assertJsonEqual(jsonOutput, expectedOutput);
}

private static ProjectEnvironmentBuilder getEnvironmentBuilder() {
Environment environment = EnvironmentBuilder.getBuilder().setBallerinaHome(DISTRIBUTION_PATH).build();
return ProjectEnvironmentBuilder.getBuilder(environment);
}

private static ProcessOutputGobbler getOutput(Process process) throws InterruptedException {
ProcessOutputGobbler outputGobbler = new ProcessOutputGobbler(process.getInputStream());
ProcessOutputGobbler errorGobbler = new ProcessOutputGobbler(process.getErrorStream());
Thread outputThread = new Thread(outputGobbler);
Thread errorThread = new Thread(errorGobbler);
outputThread.start();
errorThread.start();
int exitCode = process.waitFor();
outputGobbler.setExitCode(exitCode);
errorGobbler.setExitCode(exitCode);
outputThread.join();
errorThread.join();
return outputGobbler;
private String extractJson(String consoleOutput) {
int startIndex = consoleOutput.indexOf("[");
int endIndex = consoleOutput.lastIndexOf("]");
if (startIndex == -1 || endIndex == -1) {
return "";
}
return consoleOutput.substring(startIndex, endIndex + 1);
}

private void assertJsonEqual(String actual, String expected) {
Assert.assertEquals(normalizeJson(actual), normalizeJson(expected));
Assert.assertEquals(normalizeString(actual), normalizeString(expected));
}

private static String normalizeJson(String json) {
String normalizedJson = json.replaceAll("\\s*\"\\s*", "\"")
.replaceAll("\\s*:\\s*", ":")
.replaceAll("\\s*,\\s*", ",")
.replaceAll("\\s*\\{\\s*", "{")
.replaceAll("\\s*}\\s*", "}")
.replaceAll("\\s*\\[\\s*", "[")
.replaceAll("\\s*]\\s*", "]")
.replaceAll("\n", "")
.replaceAll(":\".*module-ballerina-mysql", ":\"module-ballerina-mysql");
return isWindows() ? normalizedJson.replaceAll("/", "\\\\\\\\") : normalizedJson;
private static String normalizeString(String json) {
try {
ObjectMapper mapper = new ObjectMapper().configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
JsonNode node = mapper.readTree(json);
String normalizedJson = mapper.writeValueAsString(node)
.replaceAll(":\".*" + MODULE_BALLERINAX_MYSQL, ":\"" + MODULE_BALLERINAX_MYSQL);
return isWindows() ? normalizedJson.replace("/", "\\\\") : normalizedJson;
} catch (Exception ignore) {
return json;
}
}

private static boolean isWindows() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2026, 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.

import ballerinax/mysql as m;

configurable string host = ?;
configurable string user = ?;
configurable int port = ?;
configurable string database = ?;

// Case 1: Custom prefix 'm' with Named Argument (Empty string)
public isolated function customPrefixNamed() {
m:Client|error dbClient = new (
host = host,
user = user,
password = "",
database = database
);
}

// Case 2: Custom prefix 'm' with Positional Arguments
// This proves that even with an alias, the 3rd index is correctly identified
public isolated function customPrefixPositional() {
m:Client|error dbClient = new (host, user, "admin123", database);
}

// Case 3: Using 'check' with the custom prefix
// This ensures Semantic API resolves the type during a check expression
public isolated function customPrefixCheck() returns error? {
m:Client dbClient = check new (host, user, "password", database);
}

// Case 4: Multiple initializations in one function with custom prefix
public isolated function multipleInit() {
m:Client|error db1 = new (host, user, "", database);
m:Client|error db2 = new (host = host, user = user, password = "123");
}

This file was deleted.

Loading
Loading