Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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,145 @@

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 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 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 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 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:
Assert.assertEquals(issues.size(), 2);
Assertions.assertIssue(issues, 0, "ballerinax/mysql:1", "named_arg.bal",
24, 30, Source.BUILT_IN);
Assertions.assertIssue(issues, 1, "ballerinax/mysql:1", "pos_arg.bal",
24, 30, Source.BUILT_IN);
break;
default:
Assert.fail("Unhandled rule in validateIssues: " + rule);
break;
}
}

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 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