diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 26594f2..ea731f3 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.12.3" +distribution-version = "2201.12.0" [[package]] org = "ballerina" diff --git a/ballerina/mssql_listener.bal b/ballerina/mssql_listener.bal index 0cdb479..31066e2 100644 --- a/ballerina/mssql_listener.bal +++ b/ballerina/mssql_listener.bal @@ -37,13 +37,13 @@ public isolated class MsSqlListener { # + return - An error if the service cannot be attached, or `()` if successful public isolated function attach(Service s, string[]|string? name = ()) returns Error? { lock { - if self.isStarted { - return error OperationNotPermittedError("Cannot attach CDC service to the listener once it is running."); - } + if self.isStarted { + return error OperationNotPermittedError("Cannot attach CDC service to the listener once it is running."); + } } check externAttach(self, s); lock { - self.hasAttachedService = true; + self.hasAttachedService = true; } } @@ -52,13 +52,13 @@ public isolated class MsSqlListener { # + return - An error if the listener cannot be started, or `()` if successful public isolated function 'start() returns Error? { lock { - if !self.hasAttachedService { - return error OperationNotPermittedError("Cannot start the listener without at least one attached service."); - } + if !self.hasAttachedService { + return error OperationNotPermittedError("Cannot start the listener without at least one attached service."); + } } check externStart(self, self.config); lock { - self.isStarted = true; + self.isStarted = true; } } @@ -68,16 +68,16 @@ public isolated class MsSqlListener { # + return - An error if the service cannot be detached, or `()` if successful public isolated function detach(Service s) returns Error? { lock { - if self.isStarted { - return error OperationNotPermittedError("Cannot detach a service from the listener once it is running."); - } + if self.isStarted { + return error OperationNotPermittedError("Cannot detach a service from the listener once it is running."); + } if !self.hasAttachedService { return; } } boolean result = check externDetach(self, s); lock { - self.hasAttachedService = result; + self.hasAttachedService = result; } } diff --git a/ballerina/mysql_listener.bal b/ballerina/mysql_listener.bal index 8e7dce5..8c459ec 100644 --- a/ballerina/mysql_listener.bal +++ b/ballerina/mysql_listener.bal @@ -36,13 +36,13 @@ public isolated class MySqlListener { # + return - An error if the service cannot be attached, or `()` if successful public isolated function attach(Service s, string[]|string? name = ()) returns Error? { lock { - if self.isStarted { - return error OperationNotPermittedError("Cannot attach CDC service to the listener once it is running."); - } + if self.isStarted { + return error OperationNotPermittedError("Cannot attach CDC service to the listener once it is running."); + } } check externAttach(self, s); lock { - self.hasAttachedService = true; + self.hasAttachedService = true; } } @@ -51,13 +51,13 @@ public isolated class MySqlListener { # + return - An error if the listener cannot be started, or `()` if successful public isolated function 'start() returns Error? { lock { - if !self.hasAttachedService { - return error OperationNotPermittedError("Cannot start the listener without at least one attached service."); - } + if !self.hasAttachedService { + return error OperationNotPermittedError("Cannot start the listener without at least one attached service."); + } } check externStart(self, self.config); lock { - self.isStarted = true; + self.isStarted = true; } } @@ -67,16 +67,16 @@ public isolated class MySqlListener { # + return - An error if the service cannot be detached, or `()` if successful public isolated function detach(Service s) returns Error? { lock { - if self.isStarted { - return error OperationNotPermittedError("Cannot detach a service from the listener once it is running."); - } + if self.isStarted { + return error OperationNotPermittedError("Cannot detach a service from the listener once it is running."); + } if !self.hasAttachedService { return; } } boolean result = check externDetach(self, s); lock { - self.hasAttachedService = result; + self.hasAttachedService = result; } } diff --git a/ballerina/oracledb_listener.bal b/ballerina/oracledb_listener.bal index 6610669..0350f7a 100644 --- a/ballerina/oracledb_listener.bal +++ b/ballerina/oracledb_listener.bal @@ -37,13 +37,13 @@ public isolated class OracleListener { # + return - An error if the service cannot be attached, or `()` if successful public isolated function attach(Service s, string[]|string? name = ()) returns Error? { lock { - if self.isStarted { - return error OperationNotPermittedError("Cannot attach CDC service to the listener once it is running."); - } + if self.isStarted { + return error OperationNotPermittedError("Cannot attach CDC service to the listener once it is running."); + } } check externAttach(self, s); lock { - self.hasAttachedService = true; + self.hasAttachedService = true; } } @@ -52,13 +52,13 @@ public isolated class OracleListener { # + return - An error if the listener cannot be started, or `()` if successful public isolated function 'start() returns Error? { lock { - if !self.hasAttachedService { - return error OperationNotPermittedError("Cannot start the listener without at least one attached service."); - } + if !self.hasAttachedService { + return error OperationNotPermittedError("Cannot start the listener without at least one attached service."); + } } check externStart(self, self.config); lock { - self.isStarted = true; + self.isStarted = true; } } @@ -68,16 +68,16 @@ public isolated class OracleListener { # + return - An error if the service cannot be detached, or `()` if successful public isolated function detach(Service s) returns Error? { lock { - if self.isStarted { - return error OperationNotPermittedError("Cannot detach a service from the listener once it is running."); - } + if self.isStarted { + return error OperationNotPermittedError("Cannot detach a service from the listener once it is running."); + } if !self.hasAttachedService { return; } } boolean result = check externDetach(self, s); lock { - self.hasAttachedService = result; + self.hasAttachedService = result; } } diff --git a/ballerina/postgresql_listener.bal b/ballerina/postgresql_listener.bal index 117a05f..c3d2f71 100644 --- a/ballerina/postgresql_listener.bal +++ b/ballerina/postgresql_listener.bal @@ -37,13 +37,13 @@ public isolated class PostgreSqlListener { # + return - An error if the service cannot be attached, or `()` if successful public isolated function attach(Service s, string[]|string? name = ()) returns Error? { lock { - if self.isStarted { - return error OperationNotPermittedError("Cannot attach CDC service to the listener once it is running."); - } + if self.isStarted { + return error OperationNotPermittedError("Cannot attach CDC service to the listener once it is running."); + } } check externAttach(self, s); lock { - self.hasAttachedService = true; + self.hasAttachedService = true; } } @@ -52,13 +52,13 @@ public isolated class PostgreSqlListener { # + return - An error if the listener cannot be started, or `()` if successful public isolated function 'start() returns Error? { lock { - if !self.hasAttachedService { - return error OperationNotPermittedError("Cannot start the listener without at least one attached service."); - } + if !self.hasAttachedService { + return error OperationNotPermittedError("Cannot start the listener without at least one attached service."); + } } check externStart(self, self.config); lock { - self.isStarted = true; + self.isStarted = true; } } @@ -68,16 +68,16 @@ public isolated class PostgreSqlListener { # + return - An error if the service cannot be detached, or `()` if successful public isolated function detach(Service s) returns Error? { lock { - if self.isStarted { - return error OperationNotPermittedError("Cannot detach a service from the listener once it is running."); - } + if self.isStarted { + return error OperationNotPermittedError("Cannot detach a service from the listener once it is running."); + } if !self.hasAttachedService { return; } } boolean result = check externDetach(self, s); lock { - self.hasAttachedService = result; + self.hasAttachedService = result; } } diff --git a/ballerina/utils.bal b/ballerina/utils.bal index ac21493..e09ff61 100644 --- a/ballerina/utils.bal +++ b/ballerina/utils.bal @@ -77,7 +77,7 @@ const string ORACLE_URL = "database.url"; const string ORACLE_PDB_NAME = "database.dbname"; const string ORACLE_CONNECTION_ADAPTER = "database.connection.adapter"; -isolated function getDebeziumProperties(MySqlListenerConfiguration|MsSqlListenerConfiguration|PostgresListenerConfiguration|OracleListenerConfiguration config) returns map & readonly{ +isolated function getDebeziumProperties(MySqlListenerConfiguration|MsSqlListenerConfiguration|PostgresListenerConfiguration|OracleListenerConfiguration config) returns map & readonly { map configMap = {}; // Common configurations diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/AbstractCodeActionTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/AbstractCodeActionTest.java deleted file mode 100644 index 58b732e..0000000 --- a/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/AbstractCodeActionTest.java +++ /dev/null @@ -1,142 +0,0 @@ -/** - * 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.lib.cdc.compiler; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import io.ballerina.projects.CodeActionManager; -import io.ballerina.projects.Document; -import io.ballerina.projects.DocumentId; -import io.ballerina.projects.Package; -import io.ballerina.projects.PackageCompilation; -import io.ballerina.projects.Project; -import io.ballerina.projects.directory.ProjectLoader; -import io.ballerina.projects.plugins.codeaction.CodeActionArgument; -import io.ballerina.projects.plugins.codeaction.CodeActionContextImpl; -import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; -import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContextImpl; -import io.ballerina.projects.plugins.codeaction.CodeActionInfo; -import io.ballerina.projects.plugins.codeaction.DocumentEdit; -import io.ballerina.tools.text.LinePosition; -import org.testng.Assert; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import static io.ballerina.lib.cdc.compiler.TestUtils.getEnvironmentBuilder; - -/** - * Abstract implementation of code action tests. - */ -public abstract class AbstractCodeActionTest { - - private static final Gson GSON = new Gson(); - - protected void performTest(Path filePath, LinePosition cursorPos, CodeActionInfo expected, Path expectedSrc) - throws IOException { - Project project = ProjectLoader.loadProject(filePath, getEnvironmentBuilder()); - List codeActions = getCodeActions(filePath, cursorPos, project); - - Assert.assertTrue(codeActions.size() > 1, "Expect at least 2 code actions"); - - Optional found = findCodeAction(codeActions, expected); - Assert.assertTrue(found.isPresent(), "Code action not found: " + GSON.toJson(expected)); - - List actualEdits = executeCodeAction(project, filePath, found.get()); - Assert.assertEquals(actualEdits.size(), 1, "Expected changes to 1 file"); - - String expectedFileUri = filePath.toUri().toString(); - Optional actualEdit = actualEdits.stream() - .filter(docEdit -> docEdit.getFileUri().equals(expectedFileUri)) - .findFirst(); - - Assert.assertTrue(actualEdit.isPresent(), "Edits not found for fileUri: " + expectedFileUri); - - String modifiedSourceCode = actualEdit.get().getModifiedSyntaxTree().toSourceCode(); - // Normalized actual to match Linux based expected source codes - String normalizedModifiedSourceCode = modifiedSourceCode.replace(System.lineSeparator(), "\n"); - - String expectedSourceCode = readExpectedSourceCode(expectedSrc); - Assert.assertEquals(normalizedModifiedSourceCode, expectedSourceCode, - "Actual source code didn't match expected source code"); - } - - private List getCodeActions(Path filePath, LinePosition cursorPos, Project project) { - Package currentPackage = project.currentPackage(); - PackageCompilation compilation = currentPackage.getCompilation(); - CodeActionManager codeActionManager = compilation.getCodeActionManager(); - - DocumentId documentId = project.documentId(filePath); - Document document = currentPackage.getDefaultModule().document(documentId); - - return compilation.diagnosticResult().diagnostics().stream() - .filter(diagnostic -> TestUtils.isWithinRange(diagnostic.location().lineRange(), cursorPos)) - .flatMap(diagnostic -> { - CodeActionContextImpl context = CodeActionContextImpl.from( - filePath.toUri().toString(), - filePath, - cursorPos, - document, - compilation.getSemanticModel(documentId.moduleId()), - diagnostic); - return codeActionManager.codeActions(context).getCodeActions().stream(); - }) - .collect(Collectors.toList()); - } - - private List executeCodeAction(Project project, Path filePath, CodeActionInfo codeAction) { - Package currentPackage = project.currentPackage(); - PackageCompilation compilation = currentPackage.getCompilation(); - - DocumentId documentId = project.documentId(filePath); - Document document = currentPackage.getDefaultModule().document(documentId); - - List codeActionArguments = codeAction.getArguments().stream() - .map(arg -> CodeActionArgument.from(GSON.toJsonTree(arg))) - .collect(Collectors.toList()); - - CodeActionExecutionContext executionContext = CodeActionExecutionContextImpl.from( - filePath.toUri().toString(), - filePath, - null, - document, - compilation.getSemanticModel(document.documentId().moduleId()), - codeActionArguments); - - return compilation.getCodeActionManager() - .executeCodeAction(codeAction.getProviderName(), executionContext); - } - - private Optional findCodeAction(List codeActions, CodeActionInfo expected) { - JsonObject expectedCodeAction = GSON.toJsonTree(expected).getAsJsonObject(); - return codeActions.stream() - .filter(codeActionInfo -> { - JsonObject actualCodeAction = GSON.toJsonTree(codeActionInfo).getAsJsonObject(); - return actualCodeAction.equals(expectedCodeAction); - }) - .findFirst(); - } - - private String readExpectedSourceCode(Path expectedSrc) throws IOException { - return Files.readString(expectedSrc); - } -} diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CodeActionTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CodeActionTest.java new file mode 100644 index 0000000..35c912f --- /dev/null +++ b/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CodeActionTest.java @@ -0,0 +1,320 @@ +/** + * 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.lib.cdc.compiler; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.ballerina.projects.CodeActionManager; +import io.ballerina.projects.Document; +import io.ballerina.projects.DocumentId; +import io.ballerina.projects.Package; +import io.ballerina.projects.PackageCompilation; +import io.ballerina.projects.Project; +import io.ballerina.projects.directory.ProjectLoader; +import io.ballerina.projects.plugins.codeaction.CodeActionArgument; +import io.ballerina.projects.plugins.codeaction.CodeActionContextImpl; +import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; +import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContextImpl; +import io.ballerina.projects.plugins.codeaction.CodeActionInfo; +import io.ballerina.projects.plugins.codeaction.DocumentEdit; +import io.ballerina.tools.text.LinePosition; +import io.ballerina.tools.text.LineRange; +import io.ballerina.tools.text.TextRange; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static io.ballerina.lib.cdc.compiler.DiagnosticCodes.EMPTY_SERVICE; +import static io.ballerina.lib.cdc.compiler.DiagnosticCodes.EMPTY_SERVICE_POSTGRESQL; +import static io.ballerina.lib.cdc.compiler.DiagnosticCodes.FUNCTION_SHOULD_BE_REMOTE; +import static io.ballerina.lib.cdc.compiler.DiagnosticCodes.INVALID_RETURN_TYPE_ERROR_OR_NIL; +import static io.ballerina.lib.cdc.compiler.TestUtils.getEnvironmentBuilder; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.CHANGE_RETURN_TYPE_TO_CDC_ERROR; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.CHANGE_RETURN_TYPE_TO_ERROR; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.CODE_TEMPLATE_NAME; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.CODE_TEMPLATE_NAME_WITH_TABLE_NAME; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.IS_POSTGRES_LISTENER; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.MAKE_FUNCTION_REMOTE; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.NODE_LOCATION; + +/** + * A class for testing code snippet generation code actions. + */ +public class CodeActionTest { + + private static final Gson GSON = new Gson(); + private static final Path RESOURCE_DIRECTORY = Paths.get("src", "test", "resources").toAbsolutePath(); + private static final Path BALLERINA_SOURCES = RESOURCE_DIRECTORY.resolve("code-action-source"); + private static final Path EXPECTED_SOURCES = RESOURCE_DIRECTORY.resolve("code-action-expected"); + private static final String EXPECTED_FILE_NAME = "result.bal"; + private static final String SOURCE_FILE_NAME = "service.bal"; + + @Test + public void testEmptyServiceCodeAction() throws IOException { + + LineRange lineRange = + LineRange.from(SOURCE_FILE_NAME, LinePosition.from(2, 0), LinePosition.from(6, 1)); + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, lineRange); + CodeActionArgument isPostgresListener = CodeActionArgument.from(IS_POSTGRES_LISTENER, false); + + CodeActionInfo expectedCodeAction = + CodeActionInfo.from("Add all functions", List.of(locationArg, isPostgresListener)); + expectedCodeAction.setProviderName(EMPTY_SERVICE.getCode() + "/ballerinax/cdc/" + CODE_TEMPLATE_NAME); + + performTest( + BALLERINA_SOURCES.resolve("snippet_gen_service_1").resolve(SOURCE_FILE_NAME), + LinePosition.from(6, 0), expectedCodeAction, + EXPECTED_SOURCES.resolve("service_1").resolve(EXPECTED_FILE_NAME) + ); + } + + @Test + public void testEmptyServiceCodeActionWithTableName() throws IOException { + LineRange lineRange = + LineRange.from(SOURCE_FILE_NAME, LinePosition.from(2, 0), LinePosition.from(6, 1)); + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, lineRange); + CodeActionArgument isPostgresListener = CodeActionArgument.from(IS_POSTGRES_LISTENER, false); + + CodeActionInfo expectedCodeAction = CodeActionInfo.from("Add all functions with tableName parameter", + List.of(locationArg, isPostgresListener)); + expectedCodeAction.setProviderName( + EMPTY_SERVICE.getCode() + "/ballerinax/cdc/" + CODE_TEMPLATE_NAME_WITH_TABLE_NAME); + + performTest( + BALLERINA_SOURCES.resolve("snippet_gen_service_1").resolve(SOURCE_FILE_NAME), + LinePosition.from(6, 0), expectedCodeAction, + EXPECTED_SOURCES.resolve("service_2").resolve(EXPECTED_FILE_NAME) + ); + } + + @Test + public void testServiceWithVariablesCodeAction() throws IOException { + + LineRange lineRange = LineRange.from(SOURCE_FILE_NAME, LinePosition.from(2, 0), LinePosition.from(8, 1)); + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, lineRange); + CodeActionArgument isPostgresListener = CodeActionArgument.from(IS_POSTGRES_LISTENER, false); + + CodeActionInfo expectedCodeAction = + CodeActionInfo.from("Add all functions", List.of(locationArg, isPostgresListener)); + expectedCodeAction.setProviderName(EMPTY_SERVICE.getCode() + "/ballerinax/cdc/" + CODE_TEMPLATE_NAME); + + performTest( + BALLERINA_SOURCES.resolve("snippet_gen_service_2").resolve(SOURCE_FILE_NAME), + LinePosition.from(8, 0), expectedCodeAction, + EXPECTED_SOURCES.resolve("service_3").resolve(EXPECTED_FILE_NAME) + ); + } + + @Test + public void testServiceWithVariablesCodeActionWithTableName() throws IOException { + + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, + LineRange.from(SOURCE_FILE_NAME, LinePosition.from(2, 0), LinePosition.from(8, 1))); + CodeActionArgument isPostgresListener = CodeActionArgument.from(IS_POSTGRES_LISTENER, false); + + CodeActionInfo expectedCodeAction = CodeActionInfo.from( + "Add all functions with tableName parameter", List.of(locationArg, isPostgresListener)); + expectedCodeAction.setProviderName( + EMPTY_SERVICE.getCode() + "/ballerinax/cdc/" + CODE_TEMPLATE_NAME_WITH_TABLE_NAME); + + performTest( + BALLERINA_SOURCES.resolve("snippet_gen_service_2").resolve(SOURCE_FILE_NAME), + LinePosition.from(8, 0), expectedCodeAction, + EXPECTED_SOURCES.resolve("service_4").resolve(EXPECTED_FILE_NAME) + ); + } + + @Test + public void testPostgresEmptyServiceCodeAction() throws IOException { + + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, + LineRange.from(SOURCE_FILE_NAME, LinePosition.from(2, 0), LinePosition.from(7, 1))); + CodeActionArgument isPostgresListener = CodeActionArgument.from(IS_POSTGRES_LISTENER, true); + + CodeActionInfo expectedCodeAction = + CodeActionInfo.from("Add all functions", List.of(locationArg, isPostgresListener)); + expectedCodeAction.setProviderName( + EMPTY_SERVICE_POSTGRESQL.getCode() + "/ballerinax/cdc/" + CODE_TEMPLATE_NAME); + + performTest( + BALLERINA_SOURCES.resolve("snippet_gen_service_3").resolve(SOURCE_FILE_NAME), + LinePosition.from(7, 0), expectedCodeAction, + EXPECTED_SOURCES.resolve("service_5").resolve(EXPECTED_FILE_NAME) + ); + } + + @Test + public void testPostgresEmptyServiceCodeActionWithTableName() throws IOException { + + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, + LineRange.from(SOURCE_FILE_NAME, LinePosition.from(2, 0), LinePosition.from(7, 1))); + CodeActionArgument isPostgresListener = CodeActionArgument.from(IS_POSTGRES_LISTENER, true); + + CodeActionInfo expectedCodeAction = CodeActionInfo.from( + "Add all functions with tableName parameter", List.of(locationArg, isPostgresListener)); + expectedCodeAction.setProviderName( + EMPTY_SERVICE_POSTGRESQL.getCode() + "/ballerinax/cdc/" + CODE_TEMPLATE_NAME_WITH_TABLE_NAME); + + performTest( + BALLERINA_SOURCES.resolve("snippet_gen_service_3").resolve(SOURCE_FILE_NAME), + LinePosition.from(7, 0), expectedCodeAction, + EXPECTED_SOURCES.resolve("service_6").resolve(EXPECTED_FILE_NAME) + ); + } + + @Test + public void testMakeFunctionRemote() throws IOException { + + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, 224); + CodeActionInfo expectedCodeAction = CodeActionInfo.from("Make the function remote", List.of(locationArg)); + expectedCodeAction.setProviderName( + FUNCTION_SHOULD_BE_REMOTE.getCode() + "/ballerinax/cdc/" + MAKE_FUNCTION_REMOTE); + + performTest( + BALLERINA_SOURCES.resolve("snippet_gen_service_4").resolve(SOURCE_FILE_NAME), + LinePosition.from(12, 9), expectedCodeAction, + EXPECTED_SOURCES.resolve("service_7").resolve(EXPECTED_FILE_NAME) + ); + } + + + @Test + public void testChangeReturnTypeToError() throws IOException { + + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, TextRange.from(173, 7)); + CodeActionInfo expectedCodeAction = + CodeActionInfo.from("Change return type to error?", List.of(locationArg)); + expectedCodeAction.setProviderName( + INVALID_RETURN_TYPE_ERROR_OR_NIL.getCode() + "/ballerinax/cdc/" + CHANGE_RETURN_TYPE_TO_ERROR); + + performTest( + BALLERINA_SOURCES.resolve("snippet_gen_service_4").resolve(SOURCE_FILE_NAME), + LinePosition.from(7, 57), expectedCodeAction, + EXPECTED_SOURCES.resolve("service_8").resolve(EXPECTED_FILE_NAME) + ); + } + + @Test + public void testChangeReturnTypeToCdcError() throws IOException { + + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, TextRange.from(173, 7)); + CodeActionInfo expectedCodeAction = + CodeActionInfo.from("Change return type to cdc:Error?", List.of(locationArg)); + expectedCodeAction.setProviderName( + INVALID_RETURN_TYPE_ERROR_OR_NIL.getCode() + "/ballerinax/cdc/" + CHANGE_RETURN_TYPE_TO_CDC_ERROR); + + performTest( + BALLERINA_SOURCES.resolve("snippet_gen_service_4").resolve(SOURCE_FILE_NAME), + LinePosition.from(7, 57), expectedCodeAction, + EXPECTED_SOURCES.resolve("service_9").resolve(EXPECTED_FILE_NAME) + ); + } + + private void performTest(Path srcPath, LinePosition cursorPos, CodeActionInfo expected, Path expectedSrc) + throws IOException { + Project project = ProjectLoader.loadProject(srcPath, getEnvironmentBuilder()); + List codeActions = getCodeActions(srcPath, cursorPos, project); + + Assert.assertFalse(codeActions.isEmpty(), "Expect at least 1 code actions"); + + Optional found = findCodeAction(codeActions, expected); + Assert.assertTrue(found.isPresent(), "Code action not found: " + GSON.toJson(expected)); + + List actualEdits = executeCodeAction(project, srcPath, found.get()); + Assert.assertEquals(actualEdits.size(), 1, "Expected changes to 1 file"); + + String expectedFileUri = srcPath.toUri().toString(); + Optional actualEdit = actualEdits.stream() + .filter(docEdit -> docEdit.getFileUri().equals(expectedFileUri)) + .findFirst(); + + Assert.assertTrue(actualEdit.isPresent(), "Edits not found for fileUri: " + expectedFileUri); + + String modifiedSourceCode = actualEdit.get().getModifiedSyntaxTree().toSourceCode(); + // Normalized actual to match Linux based expected source codes + String normalizedModifiedSourceCode = modifiedSourceCode.replace(System.lineSeparator(), "\n"); + + String expectedSourceCode = Files.readString(expectedSrc).replace(System.lineSeparator(), "\n"); + Assert.assertEquals(normalizedModifiedSourceCode, expectedSourceCode, + "Actual source code didn't match expected source code"); + } + + private List getCodeActions(Path filePath, LinePosition cursorPos, Project project) { + Package currentPackage = project.currentPackage(); + PackageCompilation compilation = currentPackage.getCompilation(); + CodeActionManager codeActionManager = compilation.getCodeActionManager(); + + DocumentId documentId = project.documentId(filePath); + Document document = currentPackage.getDefaultModule().document(documentId); + + return compilation.diagnosticResult().diagnostics().stream() + .filter(diagnostic -> TestUtils.isWithinRange(diagnostic.location().lineRange(), cursorPos)) + .flatMap(diagnostic -> { + CodeActionContextImpl context = CodeActionContextImpl.from( + filePath.toUri().toString(), + filePath, + cursorPos, + document, + compilation.getSemanticModel(documentId.moduleId()), + diagnostic); + return codeActionManager.codeActions(context).getCodeActions().stream(); + }) + .collect(Collectors.toList()); + } + + private List executeCodeAction(Project project, Path filePath, CodeActionInfo codeAction) { + Package currentPackage = project.currentPackage(); + PackageCompilation compilation = currentPackage.getCompilation(); + + DocumentId documentId = project.documentId(filePath); + Document document = currentPackage.getDefaultModule().document(documentId); + + List codeActionArguments = codeAction.getArguments().stream() + .map(arg -> CodeActionArgument.from(GSON.toJsonTree(arg))) + .collect(Collectors.toList()); + + CodeActionExecutionContext executionContext = CodeActionExecutionContextImpl.from( + filePath.toUri().toString(), + filePath, + null, + document, + compilation.getSemanticModel(document.documentId().moduleId()), + codeActionArguments); + + return compilation.getCodeActionManager() + .executeCodeAction(codeAction.getProviderName(), executionContext); + } + + private Optional findCodeAction(List codeActions, CodeActionInfo expected) { + JsonObject expectedCodeAction = GSON.toJsonTree(expected).getAsJsonObject(); + return codeActions.stream() + .filter(codeActionInfo -> { + JsonObject actualCodeAction = GSON.toJsonTree(codeActionInfo).getAsJsonObject(); + return actualCodeAction.equals(expectedCodeAction); + }) + .findFirst(); + } + +} diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CodeSnippetGenerationCodeActionTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CodeSnippetGenerationCodeActionTest.java deleted file mode 100644 index cf65fe4..0000000 --- a/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CodeSnippetGenerationCodeActionTest.java +++ /dev/null @@ -1,131 +0,0 @@ -/** - * 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.lib.cdc.compiler; - -import io.ballerina.projects.plugins.codeaction.CodeActionArgument; -import io.ballerina.projects.plugins.codeaction.CodeActionInfo; -import io.ballerina.tools.text.LinePosition; -import io.ballerina.tools.text.LineRange; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; - -import static io.ballerina.lib.cdc.compiler.Constants.CODE_TEMPLATE_NAME; -import static io.ballerina.lib.cdc.compiler.Constants.CODE_TEMPLATE_NAME_WITH_TABLE_NAME; -import static io.ballerina.lib.cdc.compiler.Constants.IS_POSTGRES_LISTENER; -import static io.ballerina.lib.cdc.compiler.Constants.NODE_LOCATION; - -/** - * A class for testing code snippet generation code actions. - */ -public class CodeSnippetGenerationCodeActionTest extends AbstractCodeActionTest { - - private static final Path RESOURCE_DIRECTORY = Paths.get("src", "test", "resources").toAbsolutePath(); - private static final String BALLERINA_SOURCES = "code-action-source"; - private static final String EXPECTED_SOURCES = "code-action-expected"; - private static final String EXPECTED_FILE_NAME = "result.bal"; - private static final String SOURCE_FILE_NAME = "service.bal"; - - @Test - public void testEmptyServiceCodeAction() throws IOException { - performTest( - getFilePath("snippet_gen_service_1"), - LinePosition.from(6, 0), - getExpectedCodeAction(6, "Add all functions", CODE_TEMPLATE_NAME, false), - getResultPath("service_1") - ); - } - - @Test - public void testEmptyServiceCodeActionWithTableName() throws IOException { - performTest( - getFilePath("snippet_gen_service_1"), - LinePosition.from(6, 0), - getExpectedCodeAction(6, "Add all functions with tableName parameter", - CODE_TEMPLATE_NAME_WITH_TABLE_NAME, false), - getResultPath("service_2") - ); - } - - @Test - public void testServiceWithVariablesCodeAction() throws IOException { - performTest( - getFilePath("snippet_gen_service_2"), - LinePosition.from(8, 0), - getExpectedCodeAction(8, "Add all functions", CODE_TEMPLATE_NAME, false), - getResultPath("service_3") - ); - } - - @Test - public void testServiceWithVariablesCodeActionWithTableName() throws IOException { - performTest( - getFilePath("snippet_gen_service_2"), - LinePosition.from(8, 0), - getExpectedCodeAction(8, "Add all functions with tableName parameter", - CODE_TEMPLATE_NAME_WITH_TABLE_NAME, false), - getResultPath("service_4") - ); - } - - @Test - public void testPostgresEmptyServiceCodeAction() throws IOException { - performTest( - getFilePath("snippet_gen_service_3"), - LinePosition.from(7, 0), - getExpectedCodeAction(7, "Add all functions", CODE_TEMPLATE_NAME, true), - getResultPath("service_5") - ); - } - - @Test - public void testPostgresEmptyServiceCodeActionWithTableName() throws IOException { - performTest( - getFilePath("snippet_gen_service_3"), - LinePosition.from(7, 0), - getExpectedCodeAction(7, "Add all functions with tableName parameter", - CODE_TEMPLATE_NAME_WITH_TABLE_NAME, true), - getResultPath("service_6") - ); - } - - private Path getFilePath(String directory) { - return RESOURCE_DIRECTORY.resolve(BALLERINA_SOURCES).resolve(directory).resolve(SOURCE_FILE_NAME); - } - - private Path getResultPath(String directory) { - return RESOURCE_DIRECTORY.resolve(EXPECTED_SOURCES).resolve(directory).resolve(EXPECTED_FILE_NAME); - } - - private CodeActionInfo getExpectedCodeAction(int line, String actionName, String templateName, - boolean isPostgresqlListener) { - LineRange lineRange = LineRange.from(SOURCE_FILE_NAME, LinePosition.from(2, 0), LinePosition.from(line, 1)); - CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, lineRange); - CodeActionArgument isPosgresListener = CodeActionArgument.from(IS_POSTGRES_LISTENER, isPostgresqlListener); - CodeActionInfo codeAction = CodeActionInfo.from(actionName, List.of(locationArg, isPosgresListener)); - if (isPostgresqlListener) { - codeAction.setProviderName("CDC_602/ballerinax/cdc/" + templateName); - } else { - codeAction.setProviderName("CDC_601/ballerinax/cdc/" + templateName); - } - return codeAction; - } -} diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CompilerPluginTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CompilerPluginTest.java index 10aee13..a67e32e 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CompilerPluginTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CompilerPluginTest.java @@ -43,8 +43,8 @@ */ public class CompilerPluginTest { - private static final Path RESOURCE_DIRECTORY = Paths.get("src", "test", "resources", "diagnostics") - .toAbsolutePath(); + private static final Path RESOURCE_DIRECTORY = + Paths.get("src", "test", "resources", "diagnostics").toAbsolutePath(); private PackageCompilation loadAndCompilePackage(String path) { Path projectDirPath = RESOURCE_DIRECTORY.resolve(path); @@ -56,13 +56,15 @@ private void assertDiagnostics(DiagnosticResult diagnosticResult, Object[][] exp Assert.assertEquals(diagnosticResult.errors().size(), expectedErrors.length); Diagnostic[] diagnostics = diagnosticResult.errors().toArray(new Diagnostic[0]); for (int i = 0; i < expectedErrors.length; i++) { - if (expectedErrors[i].length != 2) { - continue; - } - String expectedCode = ((DiagnosticCodes) expectedErrors[i][0]).getCode(); + String expectedCode = + expectedErrors[i][0] instanceof String ? + (String) expectedErrors[i][0] : + ((DiagnosticCodes) expectedErrors[i][0]).getCode(); String expectedMessage = (String) expectedErrors[i][1]; + String location = (String) expectedErrors[i][2]; Assert.assertEquals(diagnostics[i].diagnosticInfo().code(), expectedCode); Assert.assertEquals(diagnostics[i].diagnosticInfo().messageFormat(), expectedMessage); + Assert.assertEquals(diagnostics[i].location().lineRange().toString(), location); } } @@ -136,7 +138,7 @@ public void testValidService10() { Assert.assertEquals(diagnosticResult.errors().size(), 0); } - @Test(description = "Validate applying to only cdc listenerr") + @Test(description = "Validate applying to only cdc listener") public void testValidService11() { PackageCompilation currentPackage = loadAndCompilePackage("valid_service_11"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); @@ -148,12 +150,18 @@ public void testInvalidService1() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_1"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {NO_VALID_FUNCTION, - "Service must have at least one remote 'onRead', " + - "'onCreate', 'onUpdate' or 'onDelete' functions."}, - {NO_VALID_FUNCTION, - "Service must have at least one remote 'onRead', " + - "'onCreate', 'onUpdate', 'onDelete' or 'onTruncate' functions."} + { + NO_VALID_FUNCTION, + "missing valid remote function: expected at least one of " + + "''onRead'', ''onCreate'', ''onUpdate'' or ''onDelete'' functions", + "(23:0,24:1)" + }, + { + NO_VALID_FUNCTION, + "missing valid remote function: expected at least one of " + + "''onRead'', ''onCreate'', ''onUpdate'', ''onDelete'' or ''onTruncate'' functions", + "(32:0,33:1)" + } }); } @@ -162,9 +170,16 @@ public void testInvalidService2() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_2"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {}, - {} - }); + { + "BCE2063", + "missing.required.parameter", + "(18:41,21:2)" + }, + { + "BCE2039", + "undefined.parameter", + "(18:46,21:1)" + }}); } @Test(description = "Validate onRead without remote keyword") @@ -172,8 +187,11 @@ public void testInvalidService3() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_3"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {FUNCTION_SHOULD_BE_REMOTE, - "Invalid function: The function 'onRead' must be declared as a remote function."} + { + FUNCTION_SHOULD_BE_REMOTE, + "must be a ''remote'' function", + "(25:4,27:5)" + } }); } @@ -182,8 +200,16 @@ public void testInvalidService4() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_4"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_PARAM_TYPE, "Invalid parameter type: The parameter 'before' must be of type 'record'."}, - {INVALID_PARAM_TYPE, "Invalid parameter type: The parameter 'before' must be of type 'record'."} + { + INVALID_PARAM_TYPE, + "invalid type: expected ''record''", + "(25:27,25:33)" + }, + { + INVALID_PARAM_TYPE, + "invalid type: expected ''record''", + "(29:29,29:37)" + } }); } @@ -192,15 +218,46 @@ public void testInvalidService5() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_5"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_PARAM_TYPE, "Invalid parameter type: The parameter 'before' must be of type 'record'."}, - {INVALID_PARAM_TYPE, "Invalid parameter type: The parameter 'after' must be of type 'record'."}, - {INVALID_PARAM_TYPE, "Invalid parameter type: The parameter 'before' must be of type 'record'."}, - {INVALID_PARAM_TYPE, "Invalid parameter type: The parameter 'after' must be of type 'record'."}, - {INVALID_PARAM_TYPE, "Invalid parameter type: The parameter 'before' must be of type 'record'."}, - {INVALID_PARAM_TYPE, "Invalid parameter type: The parameter 'after' must be of type 'record'."}, - {INVALID_PARAM_TYPE, "Invalid parameter type: The parameter 'after' must be of type 'record'."}, - {NOT_OF_SAME_TYPE, - "Invalid parameter type: The function 'onUpdate' must have parameters of the same type."} + { + INVALID_PARAM_TYPE, + "invalid type: expected ''record''", + "(25:29,25:35)" + }, + { + INVALID_PARAM_TYPE, + "invalid type: expected ''record''", + "(31:49,31:55)" + }, + { + INVALID_PARAM_TYPE, + "invalid type: expected ''record''", + "(37:29,37:35)" + }, + { + INVALID_PARAM_TYPE, + "invalid type: expected ''record''", + "(37:44,37:50)" + }, + { + INVALID_PARAM_TYPE, + "invalid type: expected ''record''", + "(43:29,43:35)" + }, + { + INVALID_PARAM_TYPE, + "invalid type: expected ''record''", + "(43:44,43:52)" + }, + { + INVALID_PARAM_TYPE, + "invalid type: expected ''record''", + "(51:49,51:69)" + }, + { + NOT_OF_SAME_TYPE, + "invalid type: must be of the same type", + "(61:29,61:74)" + } }); } @@ -209,8 +266,11 @@ public void testInvalidService6() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_6"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_RETURN_TYPE_ERROR_OR_NIL, - "Invalid return type: The function 'onUpdate' must return either 'error?' or 'cdc:Error?'."} + { + INVALID_RETURN_TYPE_ERROR_OR_NIL, + "invalid return type: expected ''error?'' or ''cdc:Error?''", + "(24:76,24:82)" + } }); } @@ -219,7 +279,24 @@ public void testInvalidService7() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_7"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_PARAM_TYPE, "Invalid parameter type: The parameter 'tableName' must be of type 'string'."} + { + INVALID_PARAM_TYPE, + "invalid type: expected ''string''", + "(24:68,24:72)" + } + }); + } + + @Test(description = "Validate onUpdate has same type") + public void testInvalidService8() { + PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_8"); + DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); + assertDiagnostics(diagnosticResult, new Object[][]{ + { + NOT_OF_SAME_TYPE, + "invalid type: must be of the same type", + "(24:29,24:56)" + } }); } @@ -228,8 +305,11 @@ public void testInvalidService9() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_9"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_MULTIPLE_LISTENERS, - "Invalid service attachment: The service can only be attached to one 'cdc:Listener'."} + { + INVALID_MULTIPLE_LISTENERS, + "service can only be attached to one ''cdc:Listener''", + "(28:0,32:1)" + } }); } @@ -238,7 +318,11 @@ public void testInvalidService10() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_10"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_RESOURCE_FUNCTION, "Invalid resource function: Resource functions are not allowed."} + { + INVALID_RESOURCE_FUNCTION, + "resource functions are not allowed", + "(25:4,27:5)" + } }); } @@ -247,15 +331,24 @@ public void testInvalidService11() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_11"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_PARAM_COUNT, - "Invalid parameter count: The function 'onCreate' must have exactly one parameter of type " + - "'record' and may include an additional parameter of type 'string'."}, - {INVALID_PARAM_COUNT, - "Invalid parameter count: The function 'onUpdate' must have exactly two parameters of type " + - "'record' and may include an additional parameter of type 'string'."}, - {INVALID_PARAM_COUNT, - "Invalid parameter count: The function 'onUpdate' must have exactly two parameters of type " + - "'record' and may include an additional parameter of type 'string'."} + { + INVALID_PARAM_COUNT, + "invalid parameter count: expected one parameter of type ''record'' and " + + "may include an additional parameter of type ''string''", + "(24:28,24:30)" + }, + { + INVALID_PARAM_COUNT, + "invalid parameter count: expected two parameters of type ''record'' and " + + "may include an additional parameter of type ''string''", + "(27:28,27:30)" + }, + { + INVALID_PARAM_COUNT, + "invalid parameter count: expected two parameters of type ''record'' and " + + "may include an additional parameter of type ''string''", + "(32:28,32:48)" + } }); } @@ -264,7 +357,11 @@ public void testInvalidService12() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_12"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {MUST_BE_REQUIRED_PARAM, "Invalid parameter: The parameter 'tableName' must be a required parameter."} + { + MUST_BE_REQUIRED_PARAM, + "must be a required parameter", + "(24:47,24:68)" + } }); } @@ -273,8 +370,11 @@ public void testInvalidService14() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_14"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_PARAM_TYPE, - "Invalid parameter type: The parameter 'before' must be of type 'error?' or 'cdc:Error?'."} + { + INVALID_PARAM_TYPE, + "invalid type: expected ''error?'' or ''cdc:Error?''", + "(28:28,28:39)" + } }); } @@ -283,9 +383,11 @@ public void testInvalidService15() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_15"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_PARAM_COUNT, - "Invalid parameter count: The function 'onError' must have exactly one parameter of " + - "type 'error?' or 'cdc:Error?'."} + { + INVALID_PARAM_COUNT, + "invalid parameter count: expected one parameter of type ''error?'' or ''cdc:Error?''", + "(28:27,28:29)" + } }); } @@ -294,8 +396,11 @@ public void testInvalidService17() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_17"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {FUNCTION_SHOULD_BE_REMOTE, - "Invalid function: The function 'onError' must be declared as a remote function."} + { + FUNCTION_SHOULD_BE_REMOTE, + "must be a ''remote'' function", + "(28:4,30:5)" + } }); } @@ -304,7 +409,11 @@ public void testInvalidService18() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_18"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_RESOURCE_FUNCTION, "Invalid resource function: Resource functions are not allowed."} + { + INVALID_RESOURCE_FUNCTION, + "resource functions are not allowed", + "(28:4,29:5)" + } }); } @@ -313,15 +422,23 @@ public void testInvalidService19() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_19"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_PARAM_COUNT, - "Invalid parameter count: The function 'onCreate' must have exactly one parameter of type " + - "'record' and may include an additional parameter of type 'string'."}, - {INVALID_PARAM_COUNT, - "Invalid parameter count: The function 'onUpdate' must have exactly two parameters of type " + - "'record' and may include an additional parameter of type 'string'."}, - {INVALID_PARAM_COUNT, - "Invalid parameter count: The function 'onError' must have exactly one parameter of type " + - "'error?' or 'cdc:Error?'."} + { + INVALID_PARAM_COUNT, + "invalid parameter count: expected one parameter of type ''record'' and " + + "may include an additional parameter of type ''string''", + "(24:28,24:70)" + }, + { + INVALID_PARAM_COUNT, + "invalid parameter count: expected two parameters of type ''record'' and " + + "may include an additional parameter of type ''string''", + "(28:28,28:89)" + }, + { + INVALID_PARAM_COUNT, + "invalid parameter count: expected one parameter of type ''error?'' or ''cdc:Error?''", + "(32:27,32:58)" + } }); } @@ -330,15 +447,27 @@ public void testInvalidService20() { PackageCompilation currentPackage = loadAndCompilePackage("invalid_service_20"); DiagnosticResult diagnosticResult = currentPackage.diagnosticResult(); assertDiagnostics(diagnosticResult, new Object[][]{ - {INVALID_PARAM_COUNT, - "Invalid parameter count: The function 'onTruncate' must have no parameters or " + - "at most one optional parameter of type 'string'."}, - {INVALID_PARAM_TYPE, - "Invalid parameter type: The parameter 'abc' must be of type 'string'."}, - {INVALID_RETURN_TYPE_ERROR_OR_NIL, - "Invalid return type: The function 'onTruncate' must return either 'error?' or 'cdc:Error?'."}, - {INVALID_RETURN_TYPE_ERROR_OR_NIL, - "Invalid return type: The function 'onTruncate' must return either 'error?' or 'cdc:Error?'."} + { + INVALID_PARAM_COUNT, + "invalid parameter count: expected no parameters or " + + "at most one optional parameter of type ''string''", + "(38:30,38:55)" + }, + { + INVALID_PARAM_TYPE, + "invalid type: expected ''string''", + "(44:31,44:35)" + }, + { + INVALID_RETURN_TYPE_ERROR_OR_NIL, + "invalid return type: expected ''error?'' or ''cdc:Error?''", + "(64:51,64:67)" + }, + { + INVALID_RETURN_TYPE_ERROR_OR_NIL, + "invalid return type: expected ''error?'' or ''cdc:Error?''", + "(70:51,70:57)" + } }); } diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CompletionTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CompletionTest.java new file mode 100644 index 0000000..6c5e8e1 --- /dev/null +++ b/compiler-plugin-tests/src/test/java/io/ballerina/lib/cdc/compiler/CompletionTest.java @@ -0,0 +1,137 @@ +/** + * 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.lib.cdc.compiler; + +import com.google.gson.Gson; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.NonTerminalNode; +import io.ballerina.projects.CompletionManager; +import io.ballerina.projects.CompletionResult; +import io.ballerina.projects.Document; +import io.ballerina.projects.DocumentId; +import io.ballerina.projects.Module; +import io.ballerina.projects.PackageCompilation; +import io.ballerina.projects.Project; +import io.ballerina.projects.directory.ProjectLoader; +import io.ballerina.projects.plugins.completion.CompletionContext; +import io.ballerina.projects.plugins.completion.CompletionContextImpl; +import io.ballerina.projects.plugins.completion.CompletionException; +import io.ballerina.projects.plugins.completion.CompletionItem; +import io.ballerina.tools.text.LinePosition; +import io.ballerina.tools.text.TextRange; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +public class CompletionTest { + + private static final Gson GSON = new Gson(); + private static final Path RESOURCE_DIRECTORY = Paths.get("src", "test", "resources").toAbsolutePath(); + + @DataProvider(name = "completion-data-provider") + public Object[][] dataProvider() { + return new Object[][]{ + {"sample_1/main.bal", 9, 5, "completions.json"}, + {"sample_2/main.bal", 10, 5, "completions.json"}, + {"sample_2/main.bal", 25, 5, "completions.json"} + }; + } + + @Test(dataProvider = "completion-data-provider") + protected void test(String sourceFile, int line, int offset, String expectedFile) throws IOException { + Path sourceFilePath = RESOURCE_DIRECTORY.resolve("completions-source").resolve(sourceFile); + Path expectedFilePath = RESOURCE_DIRECTORY.resolve("completions-expected").resolve(expectedFile); + TestConfig expectedList = GSON.fromJson(Files.newBufferedReader(expectedFilePath), TestConfig.class); + + List expectedItems = expectedList.getItems(); + LinePosition cursorPos = LinePosition.from(line, offset); + + Project project = ProjectLoader.loadProject(sourceFilePath, TestUtils.getEnvironmentBuilder()); + CompletionResult completionResult = getCompletions(sourceFilePath, cursorPos, project); + List actualItems = completionResult.getCompletionItems(); + List errors = completionResult.getErrors(); + Assert.assertTrue(errors.isEmpty()); + Assert.assertTrue(compareCompletionItems(actualItems, expectedItems)); + + } + + private CompletionResult getCompletions(Path filePath, LinePosition cursorPos, Project project) { + + io.ballerina.projects.Package currentPackage = project.currentPackage(); + PackageCompilation compilation = currentPackage.getCompilation(); + CompletionManager completionManager = compilation.getCompletionManager(); + + DocumentId documentId = project.documentId(filePath); + Document document = currentPackage.getDefaultModule().document(documentId); + Module module = project.currentPackage().module(documentId.moduleId()); + + int cursorPositionInTree = document.textDocument().textPositionFrom(cursorPos); + TextRange range = TextRange.from(cursorPositionInTree, 0); + NonTerminalNode nodeAtCursor = ((ModulePartNode) document.syntaxTree().rootNode()).findNode(range); + + CompletionContext completionContext = CompletionContextImpl.from(filePath.toUri().toString(), + filePath, cursorPos, cursorPositionInTree, nodeAtCursor, document, + module.getCompilation().getSemanticModel()); + + return completionManager.completions(completionContext); + } + + private static boolean compareCompletionItems(List actualItems, + List expectedItems) { + List actualList = actualItems.stream() + .map(CompletionTest::getCompletionItemPropertyString) + .toList(); + List expectedList = expectedItems.stream() + .map(CompletionTest::getCompletionItemPropertyString) + .toList(); + return actualList.containsAll(expectedList) && actualItems.size() == expectedItems.size(); + } + + private static String getCompletionItemPropertyString(CompletionItem completionItem) { + // Here we replace the Windows specific \r\n to \n for evaluation only + String additionalTextEdits = ""; + if (completionItem.getAdditionalTextEdits() != null && !completionItem.getAdditionalTextEdits().isEmpty()) { + additionalTextEdits = "," + GSON.toJson(completionItem.getAdditionalTextEdits()); + } + return ("{" + + completionItem.getInsertText() + "," + + completionItem.getLabel() + "," + + completionItem.getPriority() + + additionalTextEdits + + "}").replace("\r\n", "\n").replace("\\r\\n", "\\n"); + } + + /** + * Represents the completion test config. + */ + public static class TestConfig { + private List items; + + public List getItems() { + return items; + } + } +} + + diff --git a/compiler-plugin-tests/src/test/resources/code-action-expected/service_1/result.bal b/compiler-plugin-tests/src/test/resources/code-action-expected/service_1/result.bal index 2f934be..a01d427 100644 --- a/compiler-plugin-tests/src/test/resources/code-action-expected/service_1/result.bal +++ b/compiler-plugin-tests/src/test/resources/code-action-expected/service_1/result.bal @@ -5,18 +5,14 @@ service cdc:Service on new cdc:MySqlListener(database = { password: "root" }) { remote function onRead(record {|anydata...;|} after) returns cdc:Error? { - } remote function onCreate(record {|anydata...;|} after) returns cdc:Error? { - } remote function onUpdate(record {|anydata...;|} before, record {|anydata...;|} after) returns cdc:Error? { - } remote function onDelete(record {|anydata...;|} before) returns cdc:Error? { - } } diff --git a/compiler-plugin-tests/src/test/resources/code-action-expected/service_2/result.bal b/compiler-plugin-tests/src/test/resources/code-action-expected/service_2/result.bal index 507016b..1b779a3 100644 --- a/compiler-plugin-tests/src/test/resources/code-action-expected/service_2/result.bal +++ b/compiler-plugin-tests/src/test/resources/code-action-expected/service_2/result.bal @@ -5,18 +5,14 @@ service cdc:Service on new cdc:MySqlListener(database = { password: "root" }) { remote function onRead(record {|anydata...;|} after, string tableName) returns cdc:Error? { - } remote function onCreate(record {|anydata...;|} after, string tableName) returns cdc:Error? { - } remote function onUpdate(record {|anydata...;|} before, record {|anydata...;|} after, string tableName) returns cdc:Error? { - } remote function onDelete(record {|anydata...;|} before, string tableName) returns cdc:Error? { - } } diff --git a/compiler-plugin-tests/src/test/resources/code-action-expected/service_3/result.bal b/compiler-plugin-tests/src/test/resources/code-action-expected/service_3/result.bal index 67641d1..4a3060b 100644 --- a/compiler-plugin-tests/src/test/resources/code-action-expected/service_3/result.bal +++ b/compiler-plugin-tests/src/test/resources/code-action-expected/service_3/result.bal @@ -7,18 +7,14 @@ service cdc:Service on new cdc:MySqlListener(database = { int x = 5; string y = "xx"; remote function onRead(record {|anydata...;|} after) returns cdc:Error? { - } remote function onCreate(record {|anydata...;|} after) returns cdc:Error? { - } remote function onUpdate(record {|anydata...;|} before, record {|anydata...;|} after) returns cdc:Error? { - } remote function onDelete(record {|anydata...;|} before) returns cdc:Error? { - } } diff --git a/compiler-plugin-tests/src/test/resources/code-action-expected/service_4/result.bal b/compiler-plugin-tests/src/test/resources/code-action-expected/service_4/result.bal index f862ba8..d716daa 100644 --- a/compiler-plugin-tests/src/test/resources/code-action-expected/service_4/result.bal +++ b/compiler-plugin-tests/src/test/resources/code-action-expected/service_4/result.bal @@ -7,18 +7,14 @@ service cdc:Service on new cdc:MySqlListener(database = { int x = 5; string y = "xx"; remote function onRead(record {|anydata...;|} after, string tableName) returns cdc:Error? { - } remote function onCreate(record {|anydata...;|} after, string tableName) returns cdc:Error? { - } remote function onUpdate(record {|anydata...;|} before, record {|anydata...;|} after, string tableName) returns cdc:Error? { - } remote function onDelete(record {|anydata...;|} before, string tableName) returns cdc:Error? { - } } diff --git a/compiler-plugin-tests/src/test/resources/code-action-expected/service_5/result.bal b/compiler-plugin-tests/src/test/resources/code-action-expected/service_5/result.bal index a7eaf49..8765b34 100644 --- a/compiler-plugin-tests/src/test/resources/code-action-expected/service_5/result.bal +++ b/compiler-plugin-tests/src/test/resources/code-action-expected/service_5/result.bal @@ -6,22 +6,17 @@ service cdc:Service on new cdc:PostgreSqlListener(database = { databaseName: "test" }) { remote function onRead(record {|anydata...;|} after) returns cdc:Error? { - } remote function onCreate(record {|anydata...;|} after) returns cdc:Error? { - } remote function onUpdate(record {|anydata...;|} before, record {|anydata...;|} after) returns cdc:Error? { - } remote function onDelete(record {|anydata...;|} before) returns cdc:Error? { - } remote function onTruncate() returns cdc:Error? { - } } diff --git a/compiler-plugin-tests/src/test/resources/code-action-expected/service_6/result.bal b/compiler-plugin-tests/src/test/resources/code-action-expected/service_6/result.bal index fd723aa..c0d5168 100644 --- a/compiler-plugin-tests/src/test/resources/code-action-expected/service_6/result.bal +++ b/compiler-plugin-tests/src/test/resources/code-action-expected/service_6/result.bal @@ -6,22 +6,17 @@ service cdc:Service on new cdc:PostgreSqlListener(database = { databaseName: "test" }) { remote function onRead(record {|anydata...;|} after, string tableName) returns cdc:Error? { - } remote function onCreate(record {|anydata...;|} after, string tableName) returns cdc:Error? { - } remote function onUpdate(record {|anydata...;|} before, record {|anydata...;|} after, string tableName) returns cdc:Error? { - } remote function onDelete(record {|anydata...;|} before, string tableName) returns cdc:Error? { - } remote function onTruncate(string tableName) returns cdc:Error? { - } } diff --git a/compiler-plugin-tests/src/test/resources/code-action-expected/service_7/result.bal b/compiler-plugin-tests/src/test/resources/code-action-expected/service_7/result.bal new file mode 100644 index 0000000..78e6f33 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/code-action-expected/service_7/result.bal @@ -0,0 +1,16 @@ +import ballerinax/cdc; + +service on new cdc:MySqlListener(database = { + username: "root", + password: "root" +}) { + + remote function onRead(record {||} after) returns string? { + return "Hello World"; + } + + remote function onCreate(record {||} after) { + + } + +} diff --git a/compiler-plugin-tests/src/test/resources/code-action-expected/service_8/result.bal b/compiler-plugin-tests/src/test/resources/code-action-expected/service_8/result.bal new file mode 100644 index 0000000..e0a0700 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/code-action-expected/service_8/result.bal @@ -0,0 +1,16 @@ +import ballerinax/cdc; + +service on new cdc:MySqlListener(database = { + username: "root", + password: "root" +}) { + + remote function onRead(record {||} after) returns error? { + return "Hello World"; + } + + function onCreate(record {||} after) { + + } + +} diff --git a/compiler-plugin-tests/src/test/resources/code-action-expected/service_9/result.bal b/compiler-plugin-tests/src/test/resources/code-action-expected/service_9/result.bal new file mode 100644 index 0000000..bbdd601 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/code-action-expected/service_9/result.bal @@ -0,0 +1,16 @@ +import ballerinax/cdc; + +service on new cdc:MySqlListener(database = { + username: "root", + password: "root" +}) { + + remote function onRead(record {||} after) returns cdc:Error? { + return "Hello World"; + } + + function onCreate(record {||} after) { + + } + +} diff --git a/compiler-plugin-tests/src/test/resources/code-action-source/snippet_gen_service_4/Ballerina.toml b/compiler-plugin-tests/src/test/resources/code-action-source/snippet_gen_service_4/Ballerina.toml new file mode 100644 index 0000000..49bcd56 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/code-action-source/snippet_gen_service_4/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "cdc_test" +name = "snippet_gen_4" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/code-action-source/snippet_gen_service_4/service.bal b/compiler-plugin-tests/src/test/resources/code-action-source/snippet_gen_service_4/service.bal new file mode 100644 index 0000000..e2e5812 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/code-action-source/snippet_gen_service_4/service.bal @@ -0,0 +1,16 @@ +import ballerinax/cdc; + +service on new cdc:MySqlListener(database = { + username: "root", + password: "root" +}) { + + remote function onRead(record {||} after) returns string? { + return "Hello World"; + } + + function onCreate(record {||} after) { + + } + +} diff --git a/compiler-plugin-tests/src/test/resources/completions-expected/completions.json b/compiler-plugin-tests/src/test/resources/completions-expected/completions.json new file mode 100644 index 0000000..d40313d --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/completions-expected/completions.json @@ -0,0 +1,29 @@ +{ + "items": [ + { + "label": "remote function onRead()", + "insertText": "remote function onRead(${1:record {|anydata...;|}} after) ${2}{\n\t${3}\n}", + "priority": "HIGH" + }, + { + "label": "remote function onCreate()", + "insertText": "remote function onCreate(${1:record {|anydata...;|}} after) ${2}{\n\t${3}\n}", + "priority": "HIGH" + }, + { + "label": "remote function onDelete()", + "insertText": "remote function onDelete(${1:record {|anydata...;|}} before) ${2}{\n\t${3}\n}", + "priority": "HIGH" + }, + { + "label": "remote function onUpdate()", + "insertText": "remote function onUpdate(${1:record {|anydata...;|}} before, ${2:record {|anydata...;|}} after) ${3}{\n\t${4}\n}", + "priority": "HIGH" + }, + { + "label": "remote function onTruncate()", + "insertText": "remote function onTruncate() ${1}{\n\t${2}\n}", + "priority": "HIGH" + } + ] +} diff --git a/compiler-plugin-tests/src/test/resources/completions-source/sample_1/Ballerina.toml b/compiler-plugin-tests/src/test/resources/completions-source/sample_1/Ballerina.toml new file mode 100644 index 0000000..408bf26 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/completions-source/sample_1/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "http_test" +name = "sample_completion_package_1" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/completions-source/sample_1/main.bal b/compiler-plugin-tests/src/test/resources/completions-source/sample_1/main.bal new file mode 100644 index 0000000..9457148 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/completions-source/sample_1/main.bal @@ -0,0 +1,11 @@ +import ballerinax/cdc; + +// This is added to test some auto generated code segments. +// Please ignore the indentation. + +service cdc:Service on new cdc:MySqlListener(database = { + username: "root", + password: "root" +}) { + r +} diff --git a/compiler-plugin-tests/src/test/resources/completions-source/sample_2/Ballerina.toml b/compiler-plugin-tests/src/test/resources/completions-source/sample_2/Ballerina.toml new file mode 100644 index 0000000..408bf26 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/completions-source/sample_2/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "http_test" +name = "sample_completion_package_1" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/completions-source/sample_2/main.bal b/compiler-plugin-tests/src/test/resources/completions-source/sample_2/main.bal new file mode 100644 index 0000000..ba56538 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/completions-source/sample_2/main.bal @@ -0,0 +1,27 @@ +import ballerinax/cdc; + +// This is added to test some auto generated code segments. +// Please ignore the indentation. + +service cdc:Service on new cdc:MySqlListener(database = { + username: "root", + password: "root" +}) { + + r + + remote function onRead(record {} after) { + + } +} + +service cdc:Service on new cdc:MySqlListener(database = { + username: "root", + password: "root" +}) { + remote function onRead(record {} after) { + + } + + r +} diff --git a/compiler-plugin-tests/src/test/resources/diagnostics/invalid_service_8/service.bal b/compiler-plugin-tests/src/test/resources/diagnostics/invalid_service_8/service.bal index 80e68db..5956219 100644 --- a/compiler-plugin-tests/src/test/resources/diagnostics/invalid_service_8/service.bal +++ b/compiler-plugin-tests/src/test/resources/diagnostics/invalid_service_8/service.bal @@ -22,14 +22,14 @@ listener cdc:MySqlListener cdcListener = new (database = { service cdc:Service on cdcListener { - remote function onUpdate(Table1 before, Table2 after, json tableName) { + remote function onUpdate(Table1 before, Table2 after, string tableName) { } } type Table1 record { string id; -} +}; type Table2 record { string id2; -} +}; diff --git a/compiler-plugin-tests/src/test/resources/testng.xml b/compiler-plugin-tests/src/test/resources/testng.xml index fb89440..867d592 100644 --- a/compiler-plugin-tests/src/test/resources/testng.xml +++ b/compiler-plugin-tests/src/test/resources/testng.xml @@ -23,7 +23,8 @@ - + + diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/CdcCodeAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/CdcCodeAnalyzer.java index c4ef5c4..64b4b04 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/CdcCodeAnalyzer.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/CdcCodeAnalyzer.java @@ -6,7 +6,7 @@ * in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/CdcCompilerPlugin.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/CdcCompilerPlugin.java index fb57004..f82f544 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/CdcCompilerPlugin.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/CdcCompilerPlugin.java @@ -17,10 +17,19 @@ */ package io.ballerina.lib.cdc.compiler; +import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.lib.cdc.compiler.codeaction.CdcCodeTemplate; import io.ballerina.lib.cdc.compiler.codeaction.CdcCodeTemplateWithTableName; +import io.ballerina.lib.cdc.compiler.codeaction.ChangeReturnTypeToCdcError; +import io.ballerina.lib.cdc.compiler.codeaction.ChangeReturnTypeToError; +import io.ballerina.lib.cdc.compiler.codeaction.ChangeToRemoteMethod; +import io.ballerina.lib.cdc.compiler.completion.CdcServiceBodyContextProvider; import io.ballerina.projects.plugins.CompilerPlugin; import io.ballerina.projects.plugins.CompilerPluginContext; +import io.ballerina.projects.plugins.codeaction.CodeAction; +import io.ballerina.projects.plugins.completion.CompletionProvider; + +import java.util.List; /** * This is the compiler plugin for Ballerina Cdc package. @@ -29,7 +38,20 @@ public class CdcCompilerPlugin extends CompilerPlugin { @Override public void init(CompilerPluginContext compilerPluginContext) { compilerPluginContext.addCodeAnalyzer(new CdcCodeAnalyzer()); - compilerPluginContext.addCodeAction(new CdcCodeTemplate()); - compilerPluginContext.addCodeAction(new CdcCodeTemplateWithTableName()); + getCodeActions().forEach(compilerPluginContext::addCodeAction); + getCompletionProviders().forEach(compilerPluginContext::addCompletionProvider); + } + + private List getCodeActions() { + return List.of( + new CdcCodeTemplate(), + new CdcCodeTemplateWithTableName(), + new ChangeToRemoteMethod(), + new ChangeReturnTypeToCdcError(), + new ChangeReturnTypeToError()); + } + + private List> getCompletionProviders() { + return List.of(new CdcServiceBodyContextProvider()); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/Constants.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/Constants.java index 7b2bb65..5d50689 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/Constants.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/Constants.java @@ -19,39 +19,32 @@ import java.util.List; -import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_CREATE; -import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_DELETE; -import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_READ; -import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_TRUNCATE; -import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_UPDATE; - -public class Constants { +public final class Constants { public static final String PACKAGE_ORG = "ballerinax"; public static final String PACKAGE_PREFIX = "cdc"; // Parameters public static final String ERROR_PARAM = "Error"; - // Code template related constants - public static final String NODE_LOCATION = "node.location"; - public static final String IS_POSTGRES_LISTENER = "is.postgres.listener"; - - public static final String LS = System.lineSeparator(); - public static final String CODE_TEMPLATE_NAME = "ADD_FUNCTIONS_CODE_SNIPPET"; - public static final String CODE_TEMPLATE_NAME_WITH_TABLE_NAME = "ADD_FUNCTIONS_W_TABLE_NAME_CODE_SNIPPET"; - public static final String POSTGRES_LISTENER_NAME = "PostgreSqlListener"; public static final List VALID_FUNCTIONS = List.of( - ON_READ, ON_CREATE, ON_DELETE, ON_UPDATE, ON_TRUNCATE + ServiceMethodNames.ON_READ, + ServiceMethodNames.ON_CREATE, + ServiceMethodNames.ON_DELETE, + ServiceMethodNames.ON_UPDATE, + ServiceMethodNames.ON_TRUNCATE ); public static final List VALID_FUNCTIONS_NON_POSTGRES = List.of( - ON_READ, ON_CREATE, ON_DELETE, ON_UPDATE + ServiceMethodNames.ON_READ, + ServiceMethodNames.ON_CREATE, + ServiceMethodNames.ON_DELETE, + ServiceMethodNames.ON_UPDATE ); private Constants() { } - public static class ServiceMethodNames { + public static final class ServiceMethodNames { public static final String ON_READ = "onRead"; public static final String ON_CREATE = "onCreate"; public static final String ON_UPDATE = "onUpdate"; diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/DiagnosticCodes.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/DiagnosticCodes.java index cb5b0e1..6a3797a 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/DiagnosticCodes.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/DiagnosticCodes.java @@ -23,31 +23,27 @@ import static io.ballerina.tools.diagnostics.DiagnosticSeverity.INTERNAL; public enum DiagnosticCodes { - NO_VALID_FUNCTION("CDC_101", ERROR, "Service must have at least one remote %s functions."), - INVALID_RESOURCE_FUNCTION("CDC_102", ERROR, "Invalid resource function: Resource functions are not allowed."), - FUNCTION_SHOULD_BE_REMOTE("CDC_103", ERROR, - "Invalid function: The function '%s' must be declared as a remote function."), - INVALID_PARAM_COUNT("CDC_104", ERROR, "Invalid parameter count: The function %s."), - MUST_BE_REQUIRED_PARAM("CDC_105", ERROR, "Invalid parameter: The parameter '%s' must be a required parameter."), - INVALID_PARAM_TYPE("CDC_106", ERROR, "Invalid parameter type: The parameter '%s' must be of type '%s'."), - NOT_OF_SAME_TYPE("CDC_107", ERROR, - "Invalid parameter type: The function '%s' must have parameters of the same type."), - INVALID_RETURN_TYPE_ERROR_OR_NIL("CDC_108", ERROR, - "Invalid return type: The function '%s' must return either 'error?' or 'cdc:Error?'."), - INVALID_MULTIPLE_LISTENERS("CDC_109", ERROR, - "Invalid service attachment: The service can only be attached to one 'cdc:Listener'."), + NO_VALID_FUNCTION("CDC_101", ERROR, "missing valid remote function: expected at least one of %s functions"), + INVALID_RESOURCE_FUNCTION("CDC_102", ERROR, "resource functions are not allowed"), + FUNCTION_SHOULD_BE_REMOTE("CDC_103", ERROR, "must be a ''remote'' function"), + INVALID_PARAM_COUNT("CDC_104", ERROR, "invalid parameter count: expected %s"), + MUST_BE_REQUIRED_PARAM("CDC_105", ERROR, "must be a required parameter"), + INVALID_PARAM_TYPE("CDC_106", ERROR, "invalid type: expected ''%s''"), + NOT_OF_SAME_TYPE("CDC_107", ERROR, "invalid type: must be of the same type"), + INVALID_RETURN_TYPE_ERROR_OR_NIL("CDC_108", ERROR, "invalid return type: expected ''error?'' or ''cdc:Error?''"), + INVALID_MULTIPLE_LISTENERS("CDC_109", ERROR, "service can only be attached to one ''cdc:Listener''"), // Internal diagnostics used to indicate empty service EMPTY_SERVICE("CDC_601", INTERNAL, ""), EMPTY_SERVICE_POSTGRESQL("CDC_602", INTERNAL, ""); - private final String message; - private final DiagnosticSeverity severity; private final String code; + private final DiagnosticSeverity severity; + private final String message; DiagnosticCodes(String code, DiagnosticSeverity severity, String message) { this.code = code; - this.message = message; this.severity = severity; + this.message = message; } public String getCode() { @@ -61,5 +57,4 @@ public DiagnosticSeverity getSeverity() { public String getMessage() { return message; } - } diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/Utils.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/Utils.java index 05c52a8..6774a05 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/Utils.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/Utils.java @@ -25,6 +25,7 @@ import io.ballerina.compiler.syntax.tree.ModulePartNode; import io.ballerina.compiler.syntax.tree.NonTerminalNode; import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; import io.ballerina.tools.diagnostics.Diagnostic; import io.ballerina.tools.diagnostics.DiagnosticFactory; import io.ballerina.tools.diagnostics.DiagnosticInfo; @@ -83,4 +84,12 @@ public static NonTerminalNode findNode(SyntaxTree syntaxTree, LineRange lineRang return ((ModulePartNode) syntaxTree.rootNode()).findNode(TextRange.from(start, end - start), true); } + public static T extractArgument(CodeActionExecutionContext context, String key, Class type, + T defaultValue) { + return context.arguments().stream() + .filter(arg -> key.equals(arg.key())) + .map(arg -> arg.valueAs(type)) + .findFirst() + .orElse(defaultValue); + } } diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/AbstractCdcCodeTemplate.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/AbstractCdcCodeTemplate.java index cd7e77f..9f9c72e 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/AbstractCdcCodeTemplate.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/AbstractCdcCodeTemplate.java @@ -38,9 +38,10 @@ import java.util.List; import java.util.Optional; -import static io.ballerina.lib.cdc.compiler.Constants.IS_POSTGRES_LISTENER; -import static io.ballerina.lib.cdc.compiler.Constants.NODE_LOCATION; +import static io.ballerina.lib.cdc.compiler.Utils.extractArgument; import static io.ballerina.lib.cdc.compiler.Utils.findNode; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.IS_POSTGRES_LISTENER; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.NODE_LOCATION; /** * Abstract class for CDC code templates to share common functionality. @@ -90,14 +91,6 @@ public List execute(CodeActionExecutionContext context) { SyntaxTree.from(syntaxTree, change))); } - protected T extractArgument(CodeActionExecutionContext context, String key, Class type, T defaultValue) { - return context.arguments().stream() - .filter(arg -> key.equals(arg.key())) - .map(arg -> arg.valueAs(type)) - .findFirst() - .orElse(defaultValue); - } - protected TextRange calculateTextRange(ServiceDeclarationNode serviceDeclarationNode) { if (serviceDeclarationNode.members().isEmpty()) { return TextRange.from(serviceDeclarationNode.openBraceToken().textRange().endOffset(), 1); diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/AbstractChangeReturnType.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/AbstractChangeReturnType.java new file mode 100644 index 0000000..072dbda --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/AbstractChangeReturnType.java @@ -0,0 +1,77 @@ +/** + * 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.lib.cdc.compiler.codeaction; + +import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.lib.cdc.compiler.DiagnosticCodes; +import io.ballerina.projects.plugins.codeaction.CodeAction; +import io.ballerina.projects.plugins.codeaction.CodeActionArgument; +import io.ballerina.projects.plugins.codeaction.CodeActionContext; +import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; +import io.ballerina.projects.plugins.codeaction.CodeActionInfo; +import io.ballerina.projects.plugins.codeaction.DocumentEdit; +import io.ballerina.tools.diagnostics.Diagnostic; +import io.ballerina.tools.text.TextDocumentChange; +import io.ballerina.tools.text.TextEdit; +import io.ballerina.tools.text.TextRange; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static io.ballerina.lib.cdc.compiler.Utils.extractArgument; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.NODE_LOCATION; + +public abstract class AbstractChangeReturnType implements CodeAction { + @Override + public List supportedDiagnosticCodes() { + return List.of(DiagnosticCodes.INVALID_RETURN_TYPE_ERROR_OR_NIL.getCode()); + } + + @Override + public Optional codeActionInfo(CodeActionContext codeActionContext) { + Diagnostic diagnostic = codeActionContext.diagnostic(); + if (diagnostic.location() == null) { + return Optional.empty(); + } + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, + diagnostic.location().textRange()); + return Optional.of(CodeActionInfo.from(getCodeActionDescription(), List.of(locationArg))); + } + + protected abstract String getCodeActionDescription(); + + @Override + public List execute(CodeActionExecutionContext context) { + TextRange textRange = extractArgument(context, NODE_LOCATION, TextRange.class, null); + + if (textRange == null) { + return Collections.emptyList(); + } + + List textEdits = new ArrayList<>(); + textEdits.add(TextEdit.from(textRange, getChangedReturnSignature())); + + TextDocumentChange change = TextDocumentChange.from(textEdits.toArray(new TextEdit[0])); + return List.of(new DocumentEdit(context.fileUri(), + SyntaxTree.from(context.currentDocument().syntaxTree(), change))); + } + + protected abstract String getChangedReturnSignature(); +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/CdcCodeTemplate.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/CdcCodeTemplate.java index fba8927..cadc682 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/CdcCodeTemplate.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/CdcCodeTemplate.java @@ -24,8 +24,9 @@ import java.util.ArrayList; import java.util.List; -import static io.ballerina.lib.cdc.compiler.Constants.CODE_TEMPLATE_NAME; -import static io.ballerina.lib.cdc.compiler.Constants.LS; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.CODE_TEMPLATE_NAME; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.LS; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.TAB; /** * Code action template for adding CDC-related functions to a service. @@ -33,20 +34,25 @@ public class CdcCodeTemplate extends AbstractCdcCodeTemplate { private static final String ON_READ_FUNCTION_TEXT = LS + - " remote function onRead(record {|anydata...;|} after) returns cdc:Error? {" + LS + LS + " }" + LS; + TAB + "remote function onRead(record {|anydata...;|} after) returns cdc:Error? {" + LS + + TAB + "}" + LS; private static final String ON_CREATE_FUNCTION_TEXT = LS + - " remote function onCreate(record {|anydata...;|} after) returns cdc:Error? {" + LS + LS + " }" + LS; + TAB + "remote function onCreate(record {|anydata...;|} after) returns cdc:Error? {" + LS + + TAB + "}" + LS; private static final String ON_UPDATE_FUNCTION_TEXT = LS + - " remote function onUpdate(record {|anydata...;|} before, record {|anydata...;|} after) " + - "returns cdc:Error? {" + LS + LS + " }" + LS; + TAB + "remote function onUpdate(record {|anydata...;|} before, record {|anydata...;|} after) " + + "returns cdc:Error? {" + LS + + TAB + "}" + LS; private static final String ON_DELETE_FUNCTION_TEXT = LS + - " remote function onDelete(record {|anydata...;|} before) returns cdc:Error? {" + LS + LS + " }" + LS; + TAB + "remote function onDelete(record {|anydata...;|} before) returns cdc:Error? {" + LS + + TAB + "}" + LS; private static final String ON_TRUNCATE_FUNCTION_TEXT = LS + - " remote function onTruncate() returns cdc:Error? {" + LS + LS + " }" + LS; + TAB + "remote function onTruncate() returns cdc:Error? {" + LS + + TAB + "}" + LS; @Override protected List generateTextEdits(ServiceDeclarationNode serviceDeclarationNode, diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/CdcCodeTemplateWithTableName.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/CdcCodeTemplateWithTableName.java index bd6fddf..c9d6ae4 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/CdcCodeTemplateWithTableName.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/CdcCodeTemplateWithTableName.java @@ -24,30 +24,33 @@ import java.util.ArrayList; import java.util.List; -import static io.ballerina.lib.cdc.compiler.Constants.CODE_TEMPLATE_NAME_WITH_TABLE_NAME; -import static io.ballerina.lib.cdc.compiler.Constants.LS; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.CODE_TEMPLATE_NAME_WITH_TABLE_NAME; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.LS; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.TAB; public class CdcCodeTemplateWithTableName extends AbstractCdcCodeTemplate { private static final String ON_READ_FUNCTION_TEXT = LS + - " remote function onRead(record {|anydata...;|} after, string tableName) returns cdc:Error? {" + - LS + LS + " }" + LS; + TAB + "remote function onRead(record {|anydata...;|} after, string tableName) returns cdc:Error? {" + LS + + TAB + "}" + LS; private static final String ON_CREATE_FUNCTION_TEXT = LS + - " remote function onCreate(record {|anydata...;|} after, string tableName) returns cdc:Error? {" + - LS + LS + " }" + LS; + TAB + "remote function onCreate(record {|anydata...;|} after, string tableName) returns cdc:Error? {" + LS + + TAB + "}" + LS; private static final String ON_UPDATE_FUNCTION_TEXT = LS + - " remote function onUpdate(record {|anydata...;|} before, record {|anydata...;|} after, " + - "string tableName) returns cdc:Error? {" + - LS + LS + " }" + LS; + TAB + "remote function onUpdate(record {|anydata...;|} before, record {|anydata...;|} after, " + + "string tableName) returns cdc:Error? {" + LS + + TAB + "}" + LS; private static final String ON_DELETE_FUNCTION_TEXT = LS + - " remote function onDelete(record {|anydata...;|} before, string tableName) returns cdc:Error? {" + - LS + LS + " }" + LS; + TAB + "remote function onDelete(record {|anydata...;|} before, string tableName) " + + "returns cdc:Error? {" + LS + + TAB + "}" + LS; private static final String ON_TRUNCATE_FUNCTION_TEXT = LS + - " remote function onTruncate(string tableName) returns cdc:Error? {" + LS + LS + " }" + LS; + TAB + "remote function onTruncate(string tableName) returns cdc:Error? {" + LS + + TAB + "}" + LS; @Override protected List generateTextEdits(ServiceDeclarationNode node, boolean isPostgresListener) { diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/ChangeReturnTypeToCdcError.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/ChangeReturnTypeToCdcError.java new file mode 100644 index 0000000..f6f6e11 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/ChangeReturnTypeToCdcError.java @@ -0,0 +1,37 @@ +/** + * 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.lib.cdc.compiler.codeaction; + +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.CHANGE_RETURN_TYPE_TO_CDC_ERROR; + +public class ChangeReturnTypeToCdcError extends AbstractChangeReturnType { + @Override + protected String getCodeActionDescription() { + return "Change return type to cdc:Error?"; + } + + @Override + protected String getChangedReturnSignature() { + return "cdc:Error?"; + } + + @Override + public String name() { + return CHANGE_RETURN_TYPE_TO_CDC_ERROR; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/ChangeReturnTypeToError.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/ChangeReturnTypeToError.java new file mode 100644 index 0000000..b3e994a --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/ChangeReturnTypeToError.java @@ -0,0 +1,37 @@ +/** + * 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.lib.cdc.compiler.codeaction; + +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.CHANGE_RETURN_TYPE_TO_ERROR; + +public class ChangeReturnTypeToError extends AbstractChangeReturnType { + @Override + protected String getCodeActionDescription() { + return "Change return type to error?"; + } + + @Override + protected String getChangedReturnSignature() { + return "error?"; + } + + @Override + public String name() { + return CHANGE_RETURN_TYPE_TO_ERROR; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/ChangeToRemoteMethod.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/ChangeToRemoteMethod.java new file mode 100644 index 0000000..afb56d2 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/ChangeToRemoteMethod.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.lib.cdc.compiler.codeaction; + +import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.lib.cdc.compiler.DiagnosticCodes; +import io.ballerina.projects.plugins.codeaction.CodeAction; +import io.ballerina.projects.plugins.codeaction.CodeActionArgument; +import io.ballerina.projects.plugins.codeaction.CodeActionContext; +import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; +import io.ballerina.projects.plugins.codeaction.CodeActionInfo; +import io.ballerina.projects.plugins.codeaction.DocumentEdit; +import io.ballerina.tools.diagnostics.Diagnostic; +import io.ballerina.tools.text.TextDocumentChange; +import io.ballerina.tools.text.TextEdit; +import io.ballerina.tools.text.TextRange; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static io.ballerina.lib.cdc.compiler.Utils.extractArgument; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.MAKE_FUNCTION_REMOTE; +import static io.ballerina.lib.cdc.compiler.codeaction.Constants.NODE_LOCATION; + +public class ChangeToRemoteMethod implements CodeAction { + @Override + public List supportedDiagnosticCodes() { + return List.of(DiagnosticCodes.FUNCTION_SHOULD_BE_REMOTE.getCode()); + } + + @Override + public Optional codeActionInfo(CodeActionContext codeActionContext) { + Diagnostic diagnostic = codeActionContext.diagnostic(); + if (diagnostic.location() == null) { + return Optional.empty(); + } + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION, + diagnostic.location().textRange().startOffset()); + return Optional.of(CodeActionInfo.from("Make the function remote", List.of(locationArg))); + } + + @Override + public List execute(CodeActionExecutionContext context) { + int functionStartOffset = extractArgument(context, NODE_LOCATION, Integer.class, 0); + + if (functionStartOffset == 0) { + return Collections.emptyList(); + } + + List textEdits = new ArrayList<>(); + TextRange resourceTextRange = TextRange.from(functionStartOffset, 0); + textEdits.add(TextEdit.from(resourceTextRange, "remote ")); + + TextDocumentChange change = TextDocumentChange.from(textEdits.toArray(new TextEdit[0])); + return List.of(new DocumentEdit(context.fileUri(), + SyntaxTree.from(context.currentDocument().syntaxTree(), change))); + } + + @Override + public String name() { + return MAKE_FUNCTION_REMOTE; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/Constants.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/Constants.java new file mode 100644 index 0000000..3bb75c1 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/codeaction/Constants.java @@ -0,0 +1,35 @@ +/** + * 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.lib.cdc.compiler.codeaction; + +public final class Constants { + + // Code template related constants + public static final String NODE_LOCATION = "node.location"; + public static final String IS_POSTGRES_LISTENER = "is.postgres.listener"; + public static final String LS = System.lineSeparator(); + public static final String TAB = " "; + public static final String CODE_TEMPLATE_NAME = "ADD_FUNCTIONS_CODE_SNIPPET"; + public static final String CODE_TEMPLATE_NAME_WITH_TABLE_NAME = "ADD_FUNCTIONS_W_TABLE_NAME_CODE_SNIPPET"; + public static final String MAKE_FUNCTION_REMOTE = "MAKE_FUNCTION_REMOTE"; + public static final String CHANGE_RETURN_TYPE_TO_CDC_ERROR = "CHANGE_RETURN_TYPE_CDC:ERROR?"; + public static final String CHANGE_RETURN_TYPE_TO_ERROR = "CHANGE_RETURN_TYPE_ERROR?"; + + private Constants() { // Prevent instantiation + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/completion/CdcServiceBodyContextProvider.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/completion/CdcServiceBodyContextProvider.java new file mode 100644 index 0000000..b757dff --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/completion/CdcServiceBodyContextProvider.java @@ -0,0 +1,92 @@ +/** + * 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.lib.cdc.compiler.completion; + +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.ballerina.lib.cdc.compiler.Constants; +import io.ballerina.projects.plugins.completion.CompletionContext; +import io.ballerina.projects.plugins.completion.CompletionException; +import io.ballerina.projects.plugins.completion.CompletionItem; +import io.ballerina.projects.plugins.completion.CompletionProvider; +import io.ballerina.projects.plugins.completion.CompletionUtil; + +import java.util.List; +import java.util.stream.Collectors; + +import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_CREATE; +import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_DELETE; +import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_ERROR; +import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_READ; +import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_TRUNCATE; +import static io.ballerina.lib.cdc.compiler.Constants.ServiceMethodNames.ON_UPDATE; + +public class CdcServiceBodyContextProvider implements CompletionProvider { + + private static final String COMPLETION_ACTION_NAME = "CdcServiceBodyProvider"; + + @Override + public List getCompletions(CompletionContext context, ServiceDeclarationNode serviceDeclarationNode) + throws CompletionException { + return Constants.VALID_FUNCTIONS.stream() + .filter(methodName -> !methodName.equals(ON_ERROR)) + .map(methodName -> { + String label = "remote function " + methodName + "()"; + String insertText = String.format("remote function %s(%s", + methodName, + getFunctionSignature(methodName) + ); + return new CompletionItem(label, insertText, CompletionItem.Priority.HIGH); + }).collect(Collectors.toList()); + } + + private String getFunctionSignature(String methodName) { + return switch (methodName) { + case ON_READ, ON_CREATE -> String.format("%s after) %s{%s}", + CompletionUtil.getPlaceHolderText(1, "record {|anydata...;|}"), + CompletionUtil.getPlaceHolderText(2), + CompletionUtil.LINE_BREAK + CompletionUtil.PADDING + CompletionUtil.getPlaceHolderText(3) + + CompletionUtil.LINE_BREAK); + case ON_DELETE -> String.format("%s before) %s{%s}", + CompletionUtil.getPlaceHolderText(1, "record {|anydata...;|}"), + CompletionUtil.getPlaceHolderText(2), + CompletionUtil.LINE_BREAK + CompletionUtil.PADDING + CompletionUtil.getPlaceHolderText(3) + + CompletionUtil.LINE_BREAK); + case ON_UPDATE -> String.format("%s before, %s after) %s{%s}", + CompletionUtil.getPlaceHolderText(1, "record {|anydata...;|}"), + CompletionUtil.getPlaceHolderText(2, "record {|anydata...;|}"), + CompletionUtil.getPlaceHolderText(3), + CompletionUtil.LINE_BREAK + CompletionUtil.PADDING + CompletionUtil.getPlaceHolderText(4) + + CompletionUtil.LINE_BREAK); + case ON_TRUNCATE -> String.format(") %s{%s}", + CompletionUtil.getPlaceHolderText(1), + CompletionUtil.LINE_BREAK + CompletionUtil.PADDING + CompletionUtil.getPlaceHolderText(2) + + CompletionUtil.LINE_BREAK); + default -> ""; + }; + } + + @Override + public List> getSupportedNodes() { + return List.of(ServiceDeclarationNode.class); + } + + @Override + public String name() { + return COMPLETION_ACTION_NAME; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcFunctionValidator.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcFunctionValidator.java index 804a588..05ce408 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcFunctionValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcFunctionValidator.java @@ -26,15 +26,18 @@ import io.ballerina.compiler.api.symbols.TypeSymbol; import io.ballerina.compiler.api.symbols.UnionTypeSymbol; import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; -import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.ParameterNode; import io.ballerina.compiler.syntax.tree.RequiredParameterNode; import io.ballerina.compiler.syntax.tree.ReturnTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.SeparatedNodeList; +import io.ballerina.compiler.syntax.tree.Token; import io.ballerina.lib.cdc.compiler.DiagnosticCodes; import io.ballerina.lib.cdc.compiler.Utils; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; import io.ballerina.tools.diagnostics.Location; +import io.ballerina.tools.text.LinePosition; +import io.ballerina.tools.text.LineRange; +import io.ballerina.tools.text.TextRange; import java.util.List; import java.util.Optional; @@ -95,9 +98,8 @@ private void validateEmptyParamFunction() { SeparatedNodeList parameters = functionDefNode.functionSignature().parameters(); if (parameters.size() > 1) { - reportErrorDiagnostics(INVALID_PARAM_COUNT, functionDefNode.functionSignature().location(), - "'" + functionName + "' must have no parameters or at most one optional " + - "parameter of type 'string'"); + reportErrorDiagnostics(INVALID_PARAM_COUNT, getParameterLocation(), + "no parameters or at most one optional parameter of type ''string''"); return; } @@ -115,9 +117,8 @@ private void validateSingleRecordParamFunction() { SeparatedNodeList parameters = functionDefNode.functionSignature().parameters(); if (parameters.isEmpty() || parameters.size() > 2) { - reportErrorDiagnostics(INVALID_PARAM_COUNT, functionDefNode.functionSignature().location(), - "'" + functionName + "' must have exactly one parameter of type 'record' and " + - "may include an additional parameter of type 'string'"); + reportErrorDiagnostics(INVALID_PARAM_COUNT, getParameterLocation(), "one parameter of type " + + "''record'' and may include an additional parameter of type ''string''"); return; } @@ -136,9 +137,8 @@ private void validateTwoRecordParamFunction() { SeparatedNodeList parameters = functionDefNode.functionSignature().parameters(); if (parameters.size() < 2 || parameters.size() > 3) { - reportErrorDiagnostics(INVALID_PARAM_COUNT, functionDefNode.functionSignature().location(), - "'" + functionName + "' must have exactly two parameters of type 'record' and " + - "may include an additional parameter of type 'string'"); + reportErrorDiagnostics(INVALID_PARAM_COUNT, getParameterLocation(), + "two parameters of type ''record'' and may include an additional parameter of type ''string''"); return; } @@ -166,7 +166,7 @@ private void validateRecordParametersAreSameType(ParameterNode firstParam, Param TypeSymbol firstTypeSymbol = ((ParameterSymbol) firstParamSymbolOpt.get()).typeDescriptor(); TypeSymbol secondTypeSymbol = ((ParameterSymbol) secondParamSymbolOpt.get()).typeDescriptor(); if (!firstTypeSymbol.equals(secondTypeSymbol)) { - reportErrorDiagnostics(NOT_OF_SAME_TYPE, functionDefNode.functionSignature().location(), functionName); + reportErrorDiagnostics(NOT_OF_SAME_TYPE, getFirstTwoParameterLocation(), functionName); } } @@ -177,8 +177,8 @@ private void validateOnErrorFunction() { SeparatedNodeList parameters = functionDefNode.functionSignature().parameters(); if (parameters.size() != 1) { - reportErrorDiagnostics(INVALID_PARAM_COUNT, functionDefNode.functionSignature().location(), - "'" + functionName + "' must have exactly one parameter of type 'error?' or 'cdc:Error?'"); + reportErrorDiagnostics(INVALID_PARAM_COUNT, getParameterLocation(), + "one parameter of type ''error?'' or ''cdc:Error?''"); return; } @@ -221,13 +221,11 @@ private boolean isValidRecordParameter(ParameterNode parameterNode) { .allMatch(memberType -> memberType.typeKind() == RECORD); if (!allMembersAreRecords) { - reportErrorDiagnostics(INVALID_PARAM_TYPE, requiredParam.location(), - requiredParam.paramName().map(Node::toString).orElse(""), RECORD.getName()); + reportErrorDiagnostics(INVALID_PARAM_TYPE, requiredParam.typeName().location(), RECORD.getName()); return false; } } else if (actualTypeDesc != RECORD) { - reportErrorDiagnostics(INVALID_PARAM_TYPE, requiredParam.typeName().location(), - requiredParam.paramName().map(Node::toString).orElse(""), RECORD.getName()); + reportErrorDiagnostics(INVALID_PARAM_TYPE, requiredParam.typeName().location(), RECORD.getName()); return false; } return true; @@ -247,8 +245,7 @@ private void validateStringParameter(ParameterNode parameterNode) { TypeSymbol typeSymbol = ((ParameterSymbol) typeSymbolOpt.get()).typeDescriptor(); TypeDescKind actualTypeDesc = typeSymbol.typeKind(); if (actualTypeDesc != STRING) { - reportErrorDiagnostics(INVALID_PARAM_TYPE, requiredParam.typeName().location(), - requiredParam.paramName().map(Node::toString).orElse(""), STRING.getName()); + reportErrorDiagnostics(INVALID_PARAM_TYPE, requiredParam.typeName().location(), STRING.getName()); } } @@ -267,14 +264,10 @@ private void validateErrorParameter(ParameterNode parameterNode) { if (typeSymbol.typeKind() == TYPE_REFERENCE) { if (!typeSymbol.getName().orElse("").equals(ERROR_PARAM) || !isCdcModule(typeSymbol.getModule().orElse(null))) { - reportErrorDiagnostics(INVALID_PARAM_TYPE, parameterNode.location(), - requiredParam.paramName().map(Node::toString).orElse(""), - "error' or 'cdc:Error"); + reportErrorDiagnostics(INVALID_PARAM_TYPE, parameterNode.location(), "error'' or ''cdc:Error"); } } else if (typeSymbol.typeKind() != ERROR) { - reportErrorDiagnostics(INVALID_PARAM_TYPE, parameterNode.location(), - requiredParam.paramName().map(Node::toString).orElse(""), - "error?' or 'cdc:Error?"); + reportErrorDiagnostics(INVALID_PARAM_TYPE, requiredParam.typeName().location(), "error?'' or ''cdc:Error?"); } } @@ -305,9 +298,10 @@ private void validateReturnTypeErrorOrNil() { returnTypeDescriptorNode.get().type().kind() == OPTIONAL_TYPE_DESC) { List returnTypeMembers = ((UnionTypeSymbol) returnTypeOpt.get()).memberTypeDescriptors(); returnTypeMembers.forEach( - member -> validateErrorReturnSymbol(member, returnTypeDescriptorNode.get().location())); + member -> validateErrorReturnSymbol(member, + returnTypeDescriptorNode.get().type().location())); } else { - validateErrorReturnSymbol(returnTypeOpt.get(), returnTypeDescriptorNode.get().location()); + validateErrorReturnSymbol(returnTypeOpt.get(), returnTypeDescriptorNode.get().type().location()); } } @@ -321,4 +315,53 @@ private void validateErrorReturnSymbol(TypeSymbol returnType, Location location) reportErrorDiagnostics(INVALID_RETURN_TYPE_ERROR_OR_NIL, location, functionName); } } + + private Location getParameterLocation() { + Token openParenToken = functionDefNode.functionSignature().openParenToken(); + Token closeParenToken = functionDefNode.functionSignature().closeParenToken(); + + LineRange lineRange = LineRange.from( + functionDefNode.lineRange().fileName(), + LinePosition.from( + openParenToken.lineRange().startLine().line(), + openParenToken.lineRange().startLine().offset() + ), + LinePosition.from( + closeParenToken.lineRange().endLine().line(), + closeParenToken.lineRange().endLine().offset() + ) + ); + + int textRangeStartOffset = openParenToken.textRange().startOffset(); + int textRangeEndOffset = closeParenToken.textRange().endOffset(); + TextRange textRange = TextRange.from(textRangeStartOffset, textRangeEndOffset - textRangeStartOffset); + + return new ParametersLocation(lineRange, textRange); + } + + private Location getFirstTwoParameterLocation() { + ParameterNode firstParam = functionDefNode.functionSignature().parameters().get(0); + ParameterNode secondParam = functionDefNode.functionSignature().parameters().get(1); + + LineRange lineRange = LineRange.from( + functionDefNode.lineRange().fileName(), + LinePosition.from( + firstParam.lineRange().startLine().line(), + firstParam.lineRange().startLine().offset() + ), + LinePosition.from( + secondParam.lineRange().endLine().line(), + secondParam.lineRange().endLine().offset() + ) + ); + + int textRangeStartOffset = firstParam.textRange().startOffset(); + int textRangeEndOffset = secondParam.textRange().endOffset(); + TextRange textRange = TextRange.from(textRangeStartOffset, textRangeEndOffset - textRangeStartOffset); + + return new ParametersLocation(lineRange, textRange); + } + + public record ParametersLocation(LineRange lineRange, TextRange textRange) implements Location { + } } diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcServiceAnalysisTask.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcServiceAnalysisTask.java index 3258c59..9b71c9a 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcServiceAnalysisTask.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcServiceAnalysisTask.java @@ -60,12 +60,10 @@ private boolean isCdcService(SyntaxNodeAnalysisContext context) { ServiceDeclarationSymbol serviceDeclarationSymbol = (ServiceDeclarationSymbol) symbol.get(); Optional serviceTypeSymbol = serviceDeclarationSymbol.typeDescriptor(); - if (serviceTypeSymbol.isEmpty() || serviceTypeSymbol.get().getModule().isEmpty()) { - return false; - } - - if (!Utils.isCdcModule(serviceTypeSymbol.get().getModule().get())) { - return false; + if (serviceTypeSymbol.isPresent() && serviceTypeSymbol.get().getModule().isEmpty()) { + if (!Utils.isCdcModule(serviceTypeSymbol.get().getModule().get())) { + return false; + } } List listeners = serviceDeclarationSymbol.listenerTypes(); diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcServiceValidator.java index ff173c5..7ae5a12 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/cdc/compiler/validator/CdcServiceValidator.java @@ -40,7 +40,6 @@ import static io.ballerina.lib.cdc.compiler.DiagnosticCodes.INVALID_RESOURCE_FUNCTION; import static io.ballerina.lib.cdc.compiler.DiagnosticCodes.NO_VALID_FUNCTION; import static io.ballerina.lib.cdc.compiler.Utils.getMethodSymbol; -import static io.ballerina.lib.cdc.compiler.Utils.isRemoteFunction; import static io.ballerina.tools.diagnostics.DiagnosticFactory.createDiagnostic; import static io.ballerina.tools.diagnostics.DiagnosticSeverity.INTERNAL; @@ -64,12 +63,12 @@ public void validate() { TypeSymbol listener = listenerOpt.get(); boolean isPostgresListener = isPostgresListener(listener); - boolean hasValidRemoteFunction = serviceDeclarationNode.members().stream() + boolean hasValidFunction = serviceDeclarationNode.members().stream() .filter(node -> node.kind() == OBJECT_METHOD_DEFINITION) .map(FunctionDefinitionNode.class::cast) - .anyMatch(functionNode -> isValidRemoteFunction(functionNode, isPostgresListener)); + .anyMatch(functionNode -> isValidFunction(functionNode, isPostgresListener)); - if (serviceDeclarationNode.members().isEmpty() || !hasValidRemoteFunction) { + if (serviceDeclarationNode.members().isEmpty() || !hasValidFunction) { reportEmptyServiceDiagnostics(serviceDeclarationNode, isPostgresListener); } @@ -86,9 +85,9 @@ private boolean isPostgresListener(TypeSymbol listener) { return listener.getName().map(POSTGRES_LISTENER_NAME::equals).orElse(false); } - private boolean isValidRemoteFunction(FunctionDefinitionNode functionNode, boolean isPostgresListener) { + private boolean isValidFunction(FunctionDefinitionNode functionNode, boolean isPostgresListener) { Optional methodSymbolOpt = getMethodSymbol(context.semanticModel(), functionNode); - if (methodSymbolOpt.isEmpty() || !isRemoteFunction(methodSymbolOpt.get())) { + if (methodSymbolOpt.isEmpty()) { return false; } @@ -107,8 +106,8 @@ private void reportEmptyServiceDiagnostics(ServiceDeclarationNode node, boolean context.reportDiagnostic(createDiagnostic(diagnosticInfo, node.location())); String validFunctions = isPostgresListener ? - "'onRead', 'onCreate', 'onUpdate', 'onDelete' or 'onTruncate'" : - "'onRead', 'onCreate', 'onUpdate' or 'onDelete'"; + "''onRead'', ''onCreate'', ''onUpdate'', ''onDelete'' or ''onTruncate''" : + "''onRead'', ''onCreate'', ''onUpdate'' or ''onDelete''"; context.reportDiagnostic(Utils.createDiagnostic(NO_VALID_FUNCTION, node.location(), validFunctions)); } diff --git a/native/src/main/java/io/ballerina/lib/cdc/Listener.java b/native/src/main/java/io/ballerina/lib/cdc/Listener.java index 70c7621..6e27e96 100644 --- a/native/src/main/java/io/ballerina/lib/cdc/Listener.java +++ b/native/src/main/java/io/ballerina/lib/cdc/Listener.java @@ -92,7 +92,7 @@ private static Map initializeServiceMap(Object serviceMap) { } private static void handleUnAnnotatedServiceAttachment(Object serviceMap, Map updatedServiceMap, - BObject service) { + BObject service) { if (serviceMap != null) { throw createError(BallerinaErrors.OPERATION_NOT_PERMITTED_ERROR, "The 'cdc:ServiceConfig' annotation " + "is mandatory when attaching multiple services to the 'cdc:Listener'."); diff --git a/native/src/main/java/io/ballerina/lib/cdc/utils/Constants.java b/native/src/main/java/io/ballerina/lib/cdc/utils/Constants.java index 86b0112..2359747 100644 --- a/native/src/main/java/io/ballerina/lib/cdc/utils/Constants.java +++ b/native/src/main/java/io/ballerina/lib/cdc/utils/Constants.java @@ -39,7 +39,7 @@ public final class Constants { //data.jsondata parseAsType constants public static final String ENABLE_CONSTRAINT_VALIDATION = "enableConstraintValidation"; public static final String ALLOW_DATA_PROJECTION = "allowDataProjection"; - public static final String PARSER_AS_TYPE_OPTIONS = "Options"; + public static final String PARSER_AS_TYPE_OPTIONS = "Options"; private Constants() { }