From e67eb215ef45b9d81b890eb8af30ad35c64d5de7 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Thu, 6 Jun 2024 19:49:48 +0530 Subject: [PATCH] Add compiler support --- ballerina/http_annotation.bal | 19 +- ballerina/http_types.bal | 5 + .../stdlib/http/compiler/Constants.java | 8 + .../http/compiler/HttpCompilerPlugin.java | 8 +- .../http/compiler/HttpDiagnosticCodes.java | 23 +- .../HttpInterceptorResourceValidator.java | 3 +- .../compiler/HttpResourceFunctionNode.java | 91 +++++ .../http/compiler/HttpResourceValidator.java | 44 +- .../http/compiler/HttpServiceAnalyzer.java | 1 + .../HttpServiceContractResourceValidator.java | 205 ++++++++++ .../compiler/HttpServiceObjTypeAnalyzer.java | 113 ++++++ .../http/compiler/HttpServiceValidator.java | 377 ++++++++++++++++-- .../codeaction/AddBasePathCodeAction.java | 100 +++++ .../codeaction/ChangeBasePathCodeAction.java | 101 +++++ .../http/compiler/codeaction/Constants.java | 1 + .../codeaction/ImplementServiceContract.java | 172 ++++++++ .../HttpPayloadParamIdentifier.java | 42 +- .../codemodifier/HttpServiceModifier.java | 3 +- .../PayloadAnnotationModifierTask.java | 28 +- .../codemodifier/ServiceTypeModifierTask.java | 170 ++++++++ 20 files changed, 1440 insertions(+), 74 deletions(-) create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceFunctionNode.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceContractResourceValidator.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceObjTypeAnalyzer.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddBasePathCodeAction.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeBasePathCodeAction.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ImplementServiceContract.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/ServiceTypeModifierTask.java diff --git a/ballerina/http_annotation.bal b/ballerina/http_annotation.bal index 9eb58f8b3c..e625bb0498 100644 --- a/ballerina/http_annotation.bal +++ b/ballerina/http_annotation.bal @@ -25,6 +25,7 @@ # + treatNilableAsOptional - Treat Nilable parameters as optional # + openApiDefinition - The generated OpenAPI definition for the HTTP service. This is auto-generated at compile-time if OpenAPI doc auto generation is enabled # + validation - Enables the inbound payload validation functionalty which provided by the constraint package. Enabled by default +# + serviceType - The service object type which defines the service contract public type HttpServiceConfig record {| string host = "b7a.default"; CompressionConfig compression = {}; @@ -35,6 +36,7 @@ public type HttpServiceConfig record {| boolean treatNilableAsOptional = true; byte[] openApiDefinition = []; boolean validation = true; + typedesc serviceType?; |}; # Configurations for CORS support. @@ -55,7 +57,7 @@ public type CorsConfig record {| |}; # The annotation which is used to configure an HTTP service. -public annotation HttpServiceConfig ServiceConfig on service; +public annotation HttpServiceConfig ServiceConfig on service, type; # Configuration for an HTTP resource. # @@ -87,7 +89,7 @@ public type HttpPayload record {| |}; # The annotation which is used to define the Payload resource signature parameter and return parameter. -public annotation HttpPayload Payload on parameter, return; +public const annotation HttpPayload Payload on parameter, return; # Configures the typing details type of the Caller resource signature parameter. # @@ -108,13 +110,13 @@ public type HttpHeader record {| |}; # The annotation which is used to define the Header resource signature parameter. -public annotation HttpHeader Header on parameter; +public const annotation HttpHeader Header on parameter; # Defines the query resource signature parameter. public type HttpQuery record {||}; # The annotation which is used to define the query resource signature parameter. -public annotation HttpQuery Query on parameter; +public const annotation HttpQuery Query on parameter; # Defines the HTTP response cache configuration. By default the `no-cache` directive is setted to the `cache-control` # header. In addition to that `etag` and `last-modified` headers are also added for cache validation. @@ -152,3 +154,12 @@ public type HttpCacheConfig record {| # Success(2XX) `StatusCodeResponses` return types. Default annotation adds `must-revalidate,public,max-age=3600` as # `cache-control` header in addition to `etag` and `last-modified` headers. public annotation HttpCacheConfig Cache on return; + +# Service contract configuration +# + basePath - Base path for generated service contract +public type ServiceContractConfiguration record {| + string basePath; +|}; + +# Annotation for mapping service contract information to a Ballerina service type. +public const annotation ServiceContractConfiguration ServiceContractConfig on type; diff --git a/ballerina/http_types.bal b/ballerina/http_types.bal index f4d95bab04..560111173f 100644 --- a/ballerina/http_types.bal +++ b/ballerina/http_types.bal @@ -28,6 +28,11 @@ public type Service distinct service object { }; +# The HTTP service contract type. +public type ServiceContract distinct service object { + *Service; +}; + # The types of data values that are expected by the HTTP `client` to return after the data binding operation. public type TargetType typedesc; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java index 17f43a34ee..93268428e9 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java @@ -26,6 +26,10 @@ private Constants() {} public static final String BALLERINA = "ballerina"; public static final String HTTP = "http"; + public static final String SERVICE_CONTRACT_CONFIG = "ServiceContractConfiguration"; + public static final String SERVICE_CONTRACT_TYPE = "ServiceContract"; + public static final String HTTP_SERVICE_TYPE = "Service"; + public static final String SERVICE_TYPE= "serviceType"; public static final String SERVICE_KEYWORD = "service"; public static final String REMOTE_KEYWORD = "remote"; public static final String RESOURCE_KEYWORD = "resource"; @@ -72,8 +76,12 @@ private Constants() {} public static final String OBJECT = "object"; public static final String HEADER_OBJ_NAME = "Headers"; public static final String PAYLOAD_ANNOTATION = "Payload"; + public static final String HEADER_ANNOTATION = "Header"; + public static final String QUERY_ANNOTATION = "Query"; + public static final String CALLER_ANNOTATION = "Caller"; public static final String CACHE_ANNOTATION = "Cache"; public static final String SERVICE_CONFIG_ANNOTATION = "ServiceConfig"; + public static final String SERVICE_CONTRACT_CONFIG_ANNOTATION = "ServiceContractConfig"; public static final String MEDIA_TYPE_SUBTYPE_PREFIX = "mediaTypeSubtypePrefix"; public static final String INTERCEPTABLE_SERVICE = "InterceptableService"; public static final String RESOURCE_CONFIG_ANNOTATION = "ResourceConfig"; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java index 38e59beadb..4944e51e8a 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java @@ -23,15 +23,18 @@ import io.ballerina.projects.plugins.CompilerPluginContext; import io.ballerina.projects.plugins.codeaction.CodeAction; import io.ballerina.projects.plugins.completion.CompletionProvider; +import io.ballerina.stdlib.http.compiler.codeaction.AddBasePathCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.AddHeaderParameterCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.AddInterceptorRemoteMethodCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.AddInterceptorResourceMethodCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.AddPayloadParameterCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.AddResponseCacheConfigCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.AddResponseContentTypeCodeAction; +import io.ballerina.stdlib.http.compiler.codeaction.ChangeBasePathCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.ChangeHeaderParamTypeToStringArrayCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.ChangeHeaderParamTypeToStringCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.ChangeReturnTypeWithCallerCodeAction; +import io.ballerina.stdlib.http.compiler.codeaction.ImplementServiceContract; import io.ballerina.stdlib.http.compiler.codemodifier.HttpServiceModifier; import io.ballerina.stdlib.http.compiler.completion.HttpServiceBodyContextProvider; @@ -60,7 +63,10 @@ private List getCodeActions() { new AddResponseContentTypeCodeAction(), new AddResponseCacheConfigCodeAction(), new AddInterceptorResourceMethodCodeAction(), - new AddInterceptorRemoteMethodCodeAction() + new AddInterceptorRemoteMethodCodeAction(), + new ImplementServiceContract(), + new AddBasePathCodeAction(), + new ChangeBasePathCodeAction() ); } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpDiagnosticCodes.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpDiagnosticCodes.java index 25f39a65c7..e1b038a5c0 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpDiagnosticCodes.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpDiagnosticCodes.java @@ -33,8 +33,6 @@ public enum HttpDiagnosticCodes { HTTP_101("HTTP_101", "remote methods are not allowed in http:Service", ERROR), HTTP_102("HTTP_102", "invalid resource method return type: expected '" + ALLOWED_RETURN_UNION + "', but found '%s'", ERROR), - HTTP_103("HTTP_103", "invalid resource method annotation type: expected 'http:" + RESOURCE_CONFIG_ANNOTATION + - "', but found '%s'", ERROR), HTTP_104("HTTP_104", "invalid annotation type on param '%s': expected one of the following types: " + "'http:Payload', 'http:CallerInfo', 'http:Header', 'http:Query'", ERROR), HTTP_105("HTTP_105", "invalid resource parameter '%s'", ERROR), @@ -110,11 +108,30 @@ public enum HttpDiagnosticCodes { HTTP_151("HTTP_151", "ambiguous types for parameter '%s' and '%s'. Use annotations to avoid ambiguity", ERROR), HTTP_152("HTTP_152", "invalid union type for default payload param: '%s'. Use basic structured anydata types", ERROR), + HTTP_153("HTTP_153", "'http:ServiceConfig' annotation is not allowed for service declaration implemented via the " + + "'http:ServiceContract' type. The HTTP annotations are inferred from the service contract type", ERROR), + HTTP_154("HTTP_154", "base path not found for the service defined with 'http:ServiceContract' type. " + + "Expected base path is '%s'", ERROR), + HTTP_155("HTTP_155", "invalid base path found for the service defined with 'http:ServiceContract' type." + + " Expected base path is '%s', but found '%s'", ERROR), + HTTP_156("HTTP_156", "invalid service type descriptor found in `http:ServiceConfig' annotation. " + + "Expected service type: '%s' but found: '%s'", ERROR), + HTTP_157("HTTP_157", "`serviceType` is not allowed in the service which is not implemented " + + "via the 'http:ServiceContract' type", ERROR), + HTTP_158("HTTP_158", "resource function which is not defined in the service contract type: '%s'," + + " is not allowed", ERROR), + HTTP_159("HTTP_159", "'http:ResourceConfig' annotation is not allowed for resource function implemented via the " + + "'http:ServiceContract' type. The HTTP annotations are inferred from the service contract type", ERROR), + HTTP_160("HTTP_160", "'%s' annotation is not allowed for resource function implemented via the " + + "'http:ServiceContract' type. The HTTP annotations are inferred from the service contract type", ERROR), + HTTP_161("HTTP_161", "'http:ServiceContractConfig' annotation is only allowed for service object type " + + "including 'http:ServiceContract' type", ERROR), HTTP_HINT_101("HTTP_HINT_101", "Payload annotation can be added", INTERNAL), HTTP_HINT_102("HTTP_HINT_102", "Header annotation can be added", INTERNAL), HTTP_HINT_103("HTTP_HINT_103", "Response content-type can be added", INTERNAL), - HTTP_HINT_104("HTTP_HINT_104", "Response cache configuration can be added", INTERNAL); + HTTP_HINT_104("HTTP_HINT_104", "Response cache configuration can be added", INTERNAL), + HTTP_HINT_105("HTTP_HINT_105", "Service contract: '%s', can be implemented", INTERNAL); private final String code; private final String message; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorResourceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorResourceValidator.java index 7c54ae8bc2..f7a83a077f 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorResourceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorResourceValidator.java @@ -44,7 +44,8 @@ public static void validateResource(SyntaxNodeAnalysisContext ctx, FunctionDefin if (isRequestErrorInterceptor(type)) { extractAndValidateMethodAndPath(ctx, member); } - HttpResourceValidator.extractInputParamTypeAndValidate(ctx, member, isRequestErrorInterceptor(type), + HttpResourceFunctionNode functionNode = new HttpResourceFunctionNode(member); + HttpResourceValidator.extractInputParamTypeAndValidate(ctx, functionNode, isRequestErrorInterceptor(type), typeSymbols); HttpCompilerPluginUtil.extractInterceptorReturnTypeAndValidate(ctx, typeSymbols, member, HttpDiagnosticCodes.HTTP_126); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceFunctionNode.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceFunctionNode.java new file mode 100644 index 0000000000..9d59521ca8 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceFunctionNode.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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.stdlib.http.compiler; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.FunctionSignatureNode; +import io.ballerina.compiler.syntax.tree.IdentifierToken; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.NodeVisitor; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.tools.diagnostics.Location; + +import java.util.Optional; + +public class HttpResourceFunctionNode { + + Node functionNode; + Optional metadata; + NodeList resourcePath; + FunctionSignatureNode functionSignatureNode; + IdentifierToken functionName; + + public HttpResourceFunctionNode(FunctionDefinitionNode functionDefinitionNode) { + functionNode = functionDefinitionNode; + metadata = functionDefinitionNode.metadata(); + resourcePath = functionDefinitionNode.relativeResourcePath(); + functionSignatureNode = functionDefinitionNode.functionSignature(); + functionName = functionDefinitionNode.functionName(); + } + + public HttpResourceFunctionNode(MethodDeclarationNode methodDeclarationNode) { + functionNode = methodDeclarationNode; + metadata = methodDeclarationNode.metadata(); + resourcePath = methodDeclarationNode.relativeResourcePath(); + functionSignatureNode = methodDeclarationNode.methodSignature(); + functionName = methodDeclarationNode.methodName(); + } + + public void accept(NodeVisitor nodeVisitor) { + functionNode.accept(nodeVisitor); + } + + public Optional metadata() { + return metadata; + } + + public NodeList relativeResourcePath() { + return resourcePath; + } + + public FunctionSignatureNode functionSignature() { + return functionSignatureNode; + } + + public IdentifierToken functionName() { + return functionName; + } + + public Location location() { + return functionNode.location(); + } + + public Optional getFunctionDefinitionNode() { + return functionNode.kind().equals(SyntaxKind.FUNCTION_DEFINITION) ? + Optional.of((FunctionDefinitionNode) functionNode) : Optional.empty(); + } + + public Optional getSymbol(SemanticModel semanticModel) { + return semanticModel.symbol(functionNode); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceValidator.java index 8250cfc39e..349d8553a5 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceValidator.java @@ -43,6 +43,7 @@ import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingFieldNode; import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; import io.ballerina.compiler.syntax.tree.NameReferenceNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; @@ -134,14 +135,23 @@ private HttpResourceValidator() {} static void validateResource(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, LinksMetaData linksMetaData, Map typeSymbols) { - extractResourceAnnotationAndValidate(ctx, member, linksMetaData); - extractInputParamTypeAndValidate(ctx, member, false, typeSymbols); - extractReturnTypeAndValidate(ctx, member, typeSymbols); + HttpResourceFunctionNode functionNode = new HttpResourceFunctionNode(member); + extractResourceAnnotationAndValidate(ctx, functionNode, linksMetaData); + extractInputParamTypeAndValidate(ctx, functionNode, false, typeSymbols); + extractReturnTypeAndValidate(ctx, functionNode, typeSymbols); validateHttpCallerUsage(ctx, member); } + static void validateResource(SyntaxNodeAnalysisContext ctx, MethodDeclarationNode member, + LinksMetaData linksMetaData, Map typeSymbols) { + HttpResourceFunctionNode functionNode = new HttpResourceFunctionNode(member); + extractResourceAnnotationAndValidate(ctx, functionNode, linksMetaData); + extractInputParamTypeAndValidate(ctx, functionNode, false, typeSymbols); + extractReturnTypeAndValidate(ctx, functionNode, typeSymbols); + } + private static void extractResourceAnnotationAndValidate(SyntaxNodeAnalysisContext ctx, - FunctionDefinitionNode member, + HttpResourceFunctionNode member, LinksMetaData linksMetaData) { Optional metadataNodeOptional = member.metadata(); if (metadataNodeOptional.isEmpty()) { @@ -155,14 +165,12 @@ private static void extractResourceAnnotationAndValidate(SyntaxNodeAnalysisConte String[] strings = annotName.split(Constants.COLON); if (RESOURCE_CONFIG_ANNOTATION.equals(strings[strings.length - 1].trim())) { validateLinksInResourceConfig(ctx, member, annotation, linksMetaData); - continue; } } - reportInvalidResourceAnnotation(ctx, annotReference.location(), annotName); } } - private static void validateLinksInResourceConfig(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + private static void validateLinksInResourceConfig(SyntaxNodeAnalysisContext ctx, HttpResourceFunctionNode member, AnnotationNode annotation, LinksMetaData linksMetaData) { Optional optionalMapping = annotation.annotValue(); if (optionalMapping.isEmpty()) { @@ -185,7 +193,7 @@ private static void validateLinksInResourceConfig(SyntaxNodeAnalysisContext ctx, } } - private static void validateResourceNameField(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + private static void validateResourceNameField(SyntaxNodeAnalysisContext ctx, HttpResourceFunctionNode member, SpecificFieldNode field, LinksMetaData linksMetaData) { Optional fieldValueExpression = field.valueExpr(); if (fieldValueExpression.isEmpty()) { @@ -206,7 +214,7 @@ private static void validateResourceNameField(SyntaxNodeAnalysisContext ctx, Fun } } - private static String getRelativePathFromFunctionNode(FunctionDefinitionNode member) { + private static String getRelativePathFromFunctionNode(HttpResourceFunctionNode member) { NodeList nodes = member.relativeResourcePath(); String path = EMPTY; for (Node node : nodes) { @@ -282,7 +290,7 @@ private static void populateLinkedToResources(SyntaxNodeAnalysisContext ctx, } } - public static void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + public static void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ctx, HttpResourceFunctionNode member, boolean isErrorInterceptor, Map typeSymbols) { boolean callerPresent = false; @@ -292,7 +300,7 @@ public static void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ct boolean errorPresent = false; boolean payloadAnnotationPresent = false; boolean headerAnnotationPresent = false; - Optional resourceMethodSymbolOptional = ctx.semanticModel().symbol(member); + Optional resourceMethodSymbolOptional = member.getSymbol(ctx.semanticModel()); Location paramLocation = member.location(); if (resourceMethodSymbolOptional.isEmpty()) { return; @@ -426,7 +434,10 @@ public static void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ct updateDiagnostic(ctx, paramLocation, HttpDiagnosticCodes.HTTP_115, paramName); } else { callerPresent = true; - extractCallerInfoValueAndValidate(ctx, member, paramIndex); + Optional functionDefNode = member.getFunctionDefinitionNode(); + if (functionDefNode.isPresent()) { + extractCallerInfoValueAndValidate(ctx, functionDefNode.get(), paramIndex); + } } } else { reportInvalidCallerParameterType(ctx, paramLocation, paramName); @@ -767,7 +778,7 @@ private static List getRespondParamNode(SyntaxNodeAnalys return respondNodeVisitor.getRespondStatementNodes(); } - private static void extractReturnTypeAndValidate(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + private static void extractReturnTypeAndValidate(SyntaxNodeAnalysisContext ctx, HttpResourceFunctionNode member, Map typeSymbols) { Optional returnTypeDescriptorNode = member.functionSignature().returnTypeDesc(); if (returnTypeDescriptorNode.isEmpty()) { @@ -775,7 +786,7 @@ private static void extractReturnTypeAndValidate(SyntaxNodeAnalysisContext ctx, } Node returnTypeNode = returnTypeDescriptorNode.get().type(); String returnTypeStringValue = HttpCompilerPluginUtil.getReturnTypeDescription(returnTypeDescriptorNode.get()); - Optional functionSymbol = ctx.semanticModel().symbol(member); + Optional functionSymbol = member.getSymbol(ctx.semanticModel()); if (functionSymbol.isEmpty()) { return; } @@ -923,11 +934,6 @@ private static boolean isValidReturnTypeWithCaller(TypeSymbol returnTypeDescript } } - private static void reportInvalidResourceAnnotation(SyntaxNodeAnalysisContext ctx, Location location, - String annotName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_103, annotName); - } - private static void reportInvalidParameterAnnotation(SyntaxNodeAnalysisContext ctx, Location location, String paramName) { updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_104, paramName); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceAnalyzer.java index e7d1041ac2..4bf17a52ab 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceAnalyzer.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceAnalyzer.java @@ -28,6 +28,7 @@ public class HttpServiceAnalyzer extends CodeAnalyzer { @Override public void init(CodeAnalysisContext codeAnalysisContext) { + codeAnalysisContext.addSyntaxNodeAnalysisTask(new HttpServiceObjTypeAnalyzer(), SyntaxKind.OBJECT_TYPE_DESC); codeAnalysisContext.addSyntaxNodeAnalysisTask(new HttpServiceValidator(), SyntaxKind.SERVICE_DECLARATION); codeAnalysisContext.addSyntaxNodeAnalysisTask(new HttpInterceptorServiceValidator(), SyntaxKind.CLASS_DEFINITION); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceContractResourceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceContractResourceValidator.java new file mode 100644 index 0000000000..1bc6114f29 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceContractResourceValidator.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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.stdlib.http.compiler; + +import io.ballerina.compiler.api.symbols.ResourceMethodSymbol; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.resourcepath.PathSegmentList; +import io.ballerina.compiler.api.symbols.resourcepath.ResourcePath; +import io.ballerina.compiler.api.symbols.resourcepath.util.PathSegment; +import io.ballerina.compiler.syntax.tree.AbstractNodeFactory; +import io.ballerina.compiler.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.DefaultableParameterNode; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.IncludedRecordParameterNode; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ParameterNode; +import io.ballerina.compiler.syntax.tree.RequiredParameterNode; +import io.ballerina.compiler.syntax.tree.RestParameterNode; +import io.ballerina.compiler.syntax.tree.ReturnTypeDescriptorNode; +import io.ballerina.compiler.syntax.tree.SeparatedNodeList; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.tools.diagnostics.Location; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static io.ballerina.stdlib.http.compiler.Constants.CACHE_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.CALLER_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.COLON; +import static io.ballerina.stdlib.http.compiler.Constants.HEADER_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.HTTP; +import static io.ballerina.stdlib.http.compiler.Constants.PAYLOAD_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.QUERY_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.RESOURCE_CONFIG_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.updateDiagnostic; + +/** + * Validates a ballerina http resource implemented via the service contract type + */ +public final class HttpServiceContractResourceValidator { + + private HttpServiceContractResourceValidator() { + } + + public static void validateResource(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + Set resourcesFromServiceType, String serviceTypeName) { + Optional functionDefinitionSymbol = ctx.semanticModel().symbol(member); + if (functionDefinitionSymbol.isEmpty() || + !(functionDefinitionSymbol.get() instanceof ResourceMethodSymbol resourceMethodSymbol)) { + return; + } + + ResourcePath resourcePath = resourceMethodSymbol.resourcePath(); + String resourceName = resourceMethodSymbol.getName().orElse("") + " " + constructResourcePathName(resourcePath); + if (!resourcesFromServiceType.contains(resourceName)) { + reportResourceFunctionNotAllowed(ctx, serviceTypeName, member.location()); + } + + validateAnnotationUsages(ctx, member); + } + + public static void validateAnnotationUsages(SyntaxNodeAnalysisContext ctx, + FunctionDefinitionNode resourceFunction) { + validateAnnotationUsagesOnResourceFunction(ctx, resourceFunction); + validateAnnotationUsagesOnInputParams(ctx, resourceFunction); + validateAnnotationUsagesOnReturnType(ctx, resourceFunction); + } + + public static void validateAnnotationUsagesOnResourceFunction(SyntaxNodeAnalysisContext ctx, + FunctionDefinitionNode resourceFunction) { + Optional metadataNodeOptional = resourceFunction.metadata(); + if (metadataNodeOptional.isEmpty()) { + return; + } + NodeList annotations = metadataNodeOptional.get().annotations(); + for (AnnotationNode annotation : annotations) { + Node annotReference = annotation.annotReference(); + String annotName = annotReference.toString(); + if (annotReference.kind() == SyntaxKind.QUALIFIED_NAME_REFERENCE) { + String[] annotStrings = annotName.split(Constants.COLON); + if (RESOURCE_CONFIG_ANNOTATION.equals(annotStrings[annotStrings.length - 1].trim()) + && HTTP.equals(annotStrings[0].trim())) { + reportResourceConfigAnnotationNotAllowed(ctx, annotation.location()); + } + } + } + } + + public static void validateAnnotationUsagesOnInputParams(SyntaxNodeAnalysisContext ctx, + FunctionDefinitionNode resourceFunction) { + SeparatedNodeList parameters = resourceFunction.functionSignature().parameters(); + for (ParameterNode parameter : parameters) { + NodeList annotations = getAnnotationsFromParameter(parameter); + for (AnnotationNode annotation : annotations) { + Node annotReference = annotation.annotReference(); + String annotName = annotReference.toString(); + if (annotReference.kind() == SyntaxKind.QUALIFIED_NAME_REFERENCE) { + String[] annotationStrings = annotName.split(COLON); + String annotationName = annotationStrings[annotationStrings.length - 1].trim(); + if (HTTP.equals(annotationStrings[0].trim()) && + (annotationName.equals(PAYLOAD_ANNOTATION) || annotationName.equals(HEADER_ANNOTATION) || + annotationName.equals(QUERY_ANNOTATION) || annotationName.equals(CALLER_ANNOTATION))) { + reportAnnotationNotAllowed(ctx, annotation.location(), HTTP + COLON + annotationName); + } + } + } + } + } + + public static NodeList getAnnotationsFromParameter(ParameterNode parameter) { + if (parameter instanceof RequiredParameterNode parameterNode) { + return parameterNode.annotations(); + } else if (parameter instanceof DefaultableParameterNode parameterNode) { + return parameterNode.annotations(); + } else if (parameter instanceof IncludedRecordParameterNode parameterNode) { + return parameterNode.annotations(); + } else if (parameter instanceof RestParameterNode parameterNode) { + return parameterNode.annotations(); + } else { + return AbstractNodeFactory.createEmptyNodeList(); + } + } + + public static void validateAnnotationUsagesOnReturnType(SyntaxNodeAnalysisContext ctx, + FunctionDefinitionNode resourceFunction) { + Optional returnTypeDescriptorNode = resourceFunction.functionSignature(). + returnTypeDesc(); + if (returnTypeDescriptorNode.isEmpty()) { + return; + } + + NodeList annotations = returnTypeDescriptorNode.get().annotations(); + for (AnnotationNode annotation : annotations) { + Node annotReference = annotation.annotReference(); + String annotName = annotReference.toString(); + if (annotReference.kind() == SyntaxKind.QUALIFIED_NAME_REFERENCE) { + String[] annotationStrings = annotName.split(COLON); + String annotationName = annotationStrings[annotationStrings.length - 1].trim(); + if (HTTP.equals(annotationStrings[0].trim()) && + (annotationName.equals(PAYLOAD_ANNOTATION) || annotationName.equals(CACHE_ANNOTATION))) { + reportAnnotationNotAllowed(ctx, annotation.location(), HTTP + COLON + annotationName); + } + } + } + } + + public static String constructResourcePathName(ResourcePath resourcePath) { + return switch (resourcePath.kind()) { + case DOT_RESOURCE_PATH -> "."; + case PATH_SEGMENT_LIST -> constructResourcePathNameFromSegList((PathSegmentList) resourcePath); + default -> "^^"; + }; + } + + public static String constructResourcePathNameFromSegList(PathSegmentList pathSegmentList) { + List resourcePaths = new ArrayList<>(); + for (PathSegment pathSegment : pathSegmentList.list()) { + switch (pathSegment.pathSegmentKind()) { + case NAMED_SEGMENT: + resourcePaths.add(pathSegment.getName().orElse("")); + break; + case PATH_PARAMETER: + resourcePaths.add("^"); + break; + default: + resourcePaths.add("^^"); + } + } + return resourcePaths.isEmpty() ? "" : String.join("/", resourcePaths); + } + + private static void reportResourceFunctionNotAllowed(SyntaxNodeAnalysisContext ctx, String serviceContractType, + Location location) { + updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_158, serviceContractType); + } + + private static void reportResourceConfigAnnotationNotAllowed(SyntaxNodeAnalysisContext ctx, Location location) { + updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_159); + } + + private static void reportAnnotationNotAllowed(SyntaxNodeAnalysisContext ctx, Location location, + String annotationName) { + updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_160, annotationName); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceObjTypeAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceObjTypeAnalyzer.java new file mode 100644 index 0000000000..d94707a0c8 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceObjTypeAnalyzer.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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.stdlib.http.compiler; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.tools.diagnostics.Diagnostic; +import io.ballerina.tools.diagnostics.DiagnosticSeverity; + +import java.util.List; +import java.util.Optional; + +import static io.ballerina.stdlib.http.compiler.Constants.BALLERINA; +import static io.ballerina.stdlib.http.compiler.Constants.EMPTY; +import static io.ballerina.stdlib.http.compiler.Constants.HTTP; +import static io.ballerina.stdlib.http.compiler.Constants.HTTP_SERVICE_TYPE; +import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_CONTRACT_TYPE; + +public class HttpServiceObjTypeAnalyzer extends HttpServiceValidator { + + @Override + public void perform(SyntaxNodeAnalysisContext context) { + List diagnostics = context.semanticModel().diagnostics(); + if (diagnostics.stream().anyMatch(d -> DiagnosticSeverity.ERROR.equals(d.diagnosticInfo().severity()))) { + return; + } + + Node typeNode = context.node(); + if (!isHttpServiceType(context.semanticModel(), typeNode)) { + return; + } + + ObjectTypeDescriptorNode serviceObjectType = (ObjectTypeDescriptorNode) typeNode; + Optional metadataNodeOptional = ((TypeDefinitionNode) serviceObjectType.parent()).metadata(); + metadataNodeOptional.ifPresent(metadataNode -> validateServiceAnnotation(context, metadataNode, null, + isServiceContractType(context.semanticModel(), serviceObjectType))); + + NodeList members = serviceObjectType.members(); + validateResources(context, members); + } + + public static boolean isServiceObjectType(Node typeNode) { + if (!typeNode.kind().equals(SyntaxKind.OBJECT_TYPE_DESC)) { + return false; + } + + return ((ObjectTypeDescriptorNode) typeNode).objectTypeQualifiers().stream().anyMatch( + qualifier -> qualifier.kind().equals(SyntaxKind.SERVICE_KEYWORD)); + } + + public static boolean isHttpServiceType(SemanticModel semanticModel, Node typeNode) { + if (!isServiceObjectType(typeNode)) { + return false; + } + + ObjectTypeDescriptorNode serviceObjType = (ObjectTypeDescriptorNode) typeNode; + Optional serviceObjSymbol = semanticModel.symbol(serviceObjType.parent()); + if (serviceObjSymbol.isEmpty() || + (!(serviceObjSymbol.get() instanceof TypeDefinitionSymbol serviceObjTypeDef))) { + return false; + } + + Optional serviceContractType = semanticModel.types().getTypeByName(BALLERINA, HTTP, EMPTY, + HTTP_SERVICE_TYPE); + if (serviceContractType.isEmpty() || + !(serviceContractType.get() instanceof TypeDefinitionSymbol serviceContractTypeDef)) { + return false; + } + + return serviceObjTypeDef.typeDescriptor().subtypeOf(serviceContractTypeDef.typeDescriptor()); + } + + private static boolean isServiceContractType(SemanticModel semanticModel, + ObjectTypeDescriptorNode serviceObjType) { + Optional serviceObjSymbol = semanticModel.symbol(serviceObjType.parent()); + if (serviceObjSymbol.isEmpty() || + (!(serviceObjSymbol.get() instanceof TypeDefinitionSymbol serviceObjTypeDef))) { + return false; + } + + Optional serviceContractType = semanticModel.types().getTypeByName(BALLERINA, HTTP, EMPTY, + SERVICE_CONTRACT_TYPE); + if (serviceContractType.isEmpty() || + !(serviceContractType.get() instanceof TypeDefinitionSymbol serviceContractTypeDef)) { + return false; + } + + return serviceObjTypeDef.typeDescriptor().subtypeOf(serviceContractTypeDef.typeDescriptor()); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceValidator.java index 4e1caa9dad..138e178a66 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceValidator.java @@ -18,44 +18,62 @@ package io.ballerina.stdlib.http.compiler; +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.AnnotationAttachmentSymbol; +import io.ballerina.compiler.api.symbols.ObjectTypeSymbol; import io.ballerina.compiler.api.symbols.ServiceDeclarationSymbol; import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol; import io.ballerina.compiler.api.symbols.TypeDescKind; import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; import io.ballerina.compiler.api.symbols.TypeSymbol; import io.ballerina.compiler.api.symbols.UnionTypeSymbol; +import io.ballerina.compiler.api.values.ConstantValue; import io.ballerina.compiler.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.BasicLiteralNode; import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingFieldNode; import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; -import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; +import io.ballerina.compiler.syntax.tree.NodeLocation; import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.Token; +import io.ballerina.compiler.syntax.tree.TypeDescriptorNode; import io.ballerina.projects.plugins.AnalysisTask; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; import io.ballerina.tools.diagnostics.Diagnostic; import io.ballerina.tools.diagnostics.DiagnosticFactory; import io.ballerina.tools.diagnostics.DiagnosticInfo; import io.ballerina.tools.diagnostics.DiagnosticSeverity; +import io.ballerina.tools.diagnostics.Location; +import org.wso2.ballerinalang.compiler.diagnostic.BLangDiagnosticLocation; + +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import static io.ballerina.stdlib.http.compiler.Constants.BALLERINA; import static io.ballerina.stdlib.http.compiler.Constants.COLON; import static io.ballerina.stdlib.http.compiler.Constants.DEFAULT; +import static io.ballerina.stdlib.http.compiler.Constants.EMPTY; import static io.ballerina.stdlib.http.compiler.Constants.HTTP; -import static io.ballerina.stdlib.http.compiler.Constants.INTERCEPTABLE_SERVICE; import static io.ballerina.stdlib.http.compiler.Constants.MEDIA_TYPE_SUBTYPE_PREFIX; import static io.ballerina.stdlib.http.compiler.Constants.MEDIA_TYPE_SUBTYPE_REGEX; import static io.ballerina.stdlib.http.compiler.Constants.PLUS; import static io.ballerina.stdlib.http.compiler.Constants.REMOTE_KEYWORD; import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_CONFIG_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_CONTRACT_CONFIG_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_CONTRACT_TYPE; +import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_TYPE; +import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_CONTRACT_CONFIG; import static io.ballerina.stdlib.http.compiler.Constants.SUFFIX_SEPARATOR_REGEX; import static io.ballerina.stdlib.http.compiler.Constants.UNNECESSARY_CHARS_REGEX; import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.getCtxTypes; @@ -72,6 +90,7 @@ public class HttpServiceValidator implements AnalysisTask serviceTypeDesc = getServiceContractTypeDesc( + syntaxNodeAnalysisContext.semanticModel(), serviceDeclarationNode); + + if (serviceTypeDesc.isPresent() && !validateBasePathFromServiceType(syntaxNodeAnalysisContext, + serviceTypeDesc.get(), serviceDeclarationNode)) { + return; + } + + Optional metadataNodeOptional = serviceDeclarationNode.metadata(); + metadataNodeOptional.ifPresent(metadataNode -> validateServiceAnnotation(syntaxNodeAnalysisContext, + metadataNode, serviceTypeDesc.orElse(null), false)); - LinksMetaData linksMetaData = new LinksMetaData(); NodeList members = serviceDeclarationNode.members(); + if (serviceTypeDesc.isPresent()) { + Set resourcesFromServiceType = extractMethodsFromServiceType(serviceTypeDesc.get(), + syntaxNodeAnalysisContext.semanticModel()); + validateServiceContractResources(syntaxNodeAnalysisContext, resourcesFromServiceType, members, + serviceTypeDesc.get().toString().trim()); + } else { + validateResources(syntaxNodeAnalysisContext, members); + } + } + + public static boolean isServiceContractImplementation(SemanticModel semanticModel, Node node) { + ServiceDeclarationNode serviceDeclarationNode = getServiceDeclarationNode(node, semanticModel); + if (serviceDeclarationNode == null) { + return false; + } + + return getServiceContractTypeDesc(semanticModel, serviceDeclarationNode).isPresent(); + } + + private static Optional getServiceContractTypeDesc(SemanticModel semanticModel, Node node) { + ServiceDeclarationNode serviceDeclarationNode = getServiceDeclarationNode(node, semanticModel); + if (serviceDeclarationNode == null) { + return Optional.empty(); + } + + return getServiceContractTypeDesc(semanticModel, serviceDeclarationNode); + } + + public static Optional getServiceContractTypeDesc(SemanticModel semanticModel, + ServiceDeclarationNode serviceDeclaration) { + Optional serviceTypeDesc = serviceDeclaration.typeDescriptor(); + if (serviceTypeDesc.isEmpty()) { + return Optional.empty(); + } + + Optional serviceTypeSymbol = semanticModel.symbol(serviceTypeDesc.get()); + if (serviceTypeSymbol.isEmpty() || + !(serviceTypeSymbol.get() instanceof TypeReferenceTypeSymbol serviceTypeRef)) { + return Optional.empty(); + } + + Optional serviceContractType = semanticModel.types().getTypeByName(BALLERINA, HTTP, EMPTY, + SERVICE_CONTRACT_TYPE); + if (serviceContractType.isEmpty() || + !(serviceContractType.get() instanceof TypeDefinitionSymbol serviceContractTypeDef)) { + return Optional.empty(); + } + + if (serviceTypeRef.subtypeOf(serviceContractTypeDef.typeDescriptor())) { + return serviceTypeDesc; + } + return Optional.empty(); + } + + private static Set extractMethodsFromServiceType(TypeDescriptorNode serviceTypeDesc, + SemanticModel semanticModel) { + Optional serviceTypeSymbol = semanticModel.symbol(serviceTypeDesc); + if (serviceTypeSymbol.isEmpty() || + !(serviceTypeSymbol.get() instanceof TypeReferenceTypeSymbol serviceTypeRef)) { + return Collections.emptySet(); + } + + TypeSymbol serviceTypeRefSymbol = serviceTypeRef.typeDescriptor(); + if (!(serviceTypeRefSymbol instanceof ObjectTypeSymbol serviceObjTypeSymbol)) { + return Collections.emptySet(); + } + + return serviceObjTypeSymbol.methods().keySet(); + } + + private static boolean validateBasePathFromServiceType(SyntaxNodeAnalysisContext ctx, + TypeDescriptorNode serviceTypeDesc, + ServiceDeclarationNode serviceDeclarationNode) { + SemanticModel semanticModel = ctx.semanticModel(); + Optional serviceTypeSymbol = semanticModel.symbol(serviceTypeDesc); + if (serviceTypeSymbol.isEmpty() || + !(serviceTypeSymbol.get() instanceof TypeReferenceTypeSymbol serviceTypeRef)) { + return true; + } + + Symbol serviceTypeDef = serviceTypeRef.definition(); + if (Objects.isNull(serviceTypeDef) || !(serviceTypeDef instanceof TypeDefinitionSymbol serviceType)) { + return true; + } + + Optional serviceTypeInfo = serviceType.annotAttachments().stream().filter( + annotationAttachment -> isOpenServiceTypeInfoAnnotation(annotationAttachment, semanticModel) + ).findFirst(); + if (serviceTypeInfo.isEmpty() || !serviceTypeInfo.get().isConstAnnotation()) { + return true; + } + + Optional expectedBasePathOpt = getBasePathFromServiceTypeInfo(serviceTypeInfo.get()); + if (expectedBasePathOpt.isEmpty()) { + return true; + } + + String expectedBasePath = expectedBasePathOpt.get().trim(); + + NodeList nodes = serviceDeclarationNode.absoluteResourcePath(); + if (nodes.isEmpty()) { + if (!expectedBasePath.equals("/")) { + reportBasePathNotFound(ctx, expectedBasePath, serviceTypeDesc.location()); + return false; + } + return true; + } + + String actualBasePath = constructBasePathFormNodeList(nodes); + if (!actualBasePath.equals(expectedBasePath)) { + reportInvalidBasePathFound(ctx, expectedBasePath, actualBasePath, nodes); + return false; + } + return true; + } + + private static String constructBasePathFormNodeList(NodeList nodes) { + // Handle string literal values + if (nodes.size() == 1 && nodes.get(0).kind().equals(SyntaxKind.STRING_LITERAL)) { + String basicLiteralText = ((BasicLiteralNode) nodes.get(0)).literalToken().text(); + return basicLiteralText.substring(1, basicLiteralText.length() - 1); + } + + StringBuilder basePath = new StringBuilder(); + for (Node node : nodes) { + if (node.kind().equals(SyntaxKind.SLASH_TOKEN)) { + basePath.append("/"); + } else if (node.kind().equals(SyntaxKind.IDENTIFIER_TOKEN)) { + basePath.append(((Token) node).text().replaceAll("\\\\", "").replaceAll("'", "")); + } + } + return basePath.toString(); + } + + private static boolean isOpenServiceTypeInfoAnnotation(AnnotationAttachmentSymbol annotationAttachmentSymbol, + SemanticModel semanticModel) { + Optional serviceTypeInfo = semanticModel.types().getTypeByName(BALLERINA, HTTP, EMPTY, + SERVICE_CONTRACT_CONFIG); + Optional annotationDescType = annotationAttachmentSymbol.typeDescriptor().typeDescriptor(); + if (annotationDescType.isPresent() && serviceTypeInfo.isPresent() && + serviceTypeInfo.get() instanceof TypeDefinitionSymbol serviceTypeInfoSymbol) { + return annotationDescType.get().subtypeOf(serviceTypeInfoSymbol.typeDescriptor()); + } + return false; + } + + private static Optional getBasePathFromServiceTypeInfo(AnnotationAttachmentSymbol serviceTypeInfo) { + Optional serviceTypeInfoValue = serviceTypeInfo.attachmentValue(); + if (serviceTypeInfoValue.isEmpty()) { + return Optional.empty(); + } + Object serviceTypeInfoMapObject = serviceTypeInfoValue.get().value(); + if (serviceTypeInfoMapObject instanceof Map serviceTypeInfoMap) { + Object basePath = serviceTypeInfoMap.get("basePath"); + if (Objects.nonNull(basePath) && basePath instanceof ConstantValue basePathConstant) { + Object basePathString = basePathConstant.value(); + if (Objects.nonNull(basePathString) && basePathString instanceof String basePathStrValue) { + return Optional.of(basePathStrValue); + } + } + } + return Optional.empty(); + } + + protected static void validateResources(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, + NodeList members) { + LinksMetaData linksMetaData = new LinksMetaData(); for (Node member : members) { if (member.kind() == SyntaxKind.OBJECT_METHOD_DEFINITION) { FunctionDefinitionNode node = (FunctionDefinitionNode) member; @@ -99,21 +294,73 @@ public void perform(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext) { } else if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { HttpResourceValidator.validateResource(syntaxNodeAnalysisContext, (FunctionDefinitionNode) member, linksMetaData, getCtxTypes(syntaxNodeAnalysisContext)); + } else if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DECLARATION) { + HttpResourceValidator.validateResource(syntaxNodeAnalysisContext, (MethodDeclarationNode) member, + linksMetaData, getCtxTypes(syntaxNodeAnalysisContext)); } } validateResourceLinks(syntaxNodeAnalysisContext, linksMetaData); } + private static void validateServiceContractResources(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, + Set resourcesFromServiceType, NodeList members, + String serviceTypeName) { + for (Node member : members) { + if (member.kind() == SyntaxKind.OBJECT_METHOD_DEFINITION) { + FunctionDefinitionNode node = (FunctionDefinitionNode) member; + NodeList tokens = node.qualifierList(); + if (tokens.isEmpty()) { + // Object methods are allowed. + continue; + } + if (tokens.stream().anyMatch(token -> token.text().equals(REMOTE_KEYWORD))) { + reportInvalidFunctionType(syntaxNodeAnalysisContext, node); + } + } else if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { + // Only resources defined in the serviceTypeDes is allowed + // No annotations are allowed in either in resource function or in the parameters + HttpServiceContractResourceValidator.validateResource(syntaxNodeAnalysisContext, + (FunctionDefinitionNode) member, resourcesFromServiceType, serviceTypeName); + } + } + } + + private static void checkForServiceImplementationErrors(SyntaxNodeAnalysisContext context) { + Node node = context.node(); + Optional serviceContractTypeDesc = getServiceContractTypeDesc(context.semanticModel(), + node); + if (serviceContractTypeDesc.isEmpty()) { + return; + } + String serviceType = serviceContractTypeDesc.get().toString().trim(); + + NodeLocation location = node.location(); + for (Diagnostic diagnostic : context.semanticModel().diagnostics()) { + Location diagnosticLocation = diagnostic.location(); + String diagnosticCode = diagnostic.diagnosticInfo().code(); + + if (diagnosticCode.equals("BCE4006") && diagnosticLocation.textRange().equals(location.textRange()) && + diagnosticLocation.lineRange().equals(location.lineRange())) { + enableImplementServiceContractCodeAction(context, serviceType, location); + return; + } + } + } + public static boolean diagnosticContainsErrors(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext) { List diagnostics = syntaxNodeAnalysisContext.semanticModel().diagnostics(); return diagnostics.stream() .anyMatch(d -> DiagnosticSeverity.ERROR.equals(d.diagnosticInfo().severity())); } - public static ServiceDeclarationNode getServiceDeclarationNode(SyntaxNodeAnalysisContext context) { - ServiceDeclarationNode serviceDeclarationNode = (ServiceDeclarationNode) context.node(); - Optional serviceSymOptional = context.semanticModel().symbol(serviceDeclarationNode); + public static ServiceDeclarationNode getServiceDeclarationNode(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext) { + return getServiceDeclarationNode(syntaxNodeAnalysisContext.node(), syntaxNodeAnalysisContext.semanticModel()); + } + + public static ServiceDeclarationNode getServiceDeclarationNode(Node node, SemanticModel semanticModel) { + ServiceDeclarationNode serviceDeclarationNode = (ServiceDeclarationNode) node; + Optional serviceSymOptional = semanticModel.symbol(serviceDeclarationNode); if (serviceSymOptional.isPresent()) { List listenerTypes = ((ServiceDeclarationSymbol) serviceSymOptional.get()).listenerTypes(); if (listenerTypes.stream().noneMatch(HttpServiceValidator::isListenerBelongsToHttpModule)) { @@ -146,7 +393,7 @@ public static TypeDescKind getReferencedTypeDescKind(TypeSymbol typeSymbol) { return kind; } - private void validateResourceLinks(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, + private static void validateResourceLinks(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, LinksMetaData linksMetaData) { if (!linksMetaData.hasNameReferenceObjects()) { for (Map linkedToResourceMap : linksMetaData.getLinkedToResourceMaps()) { @@ -159,7 +406,7 @@ private void validateResourceLinks(SyntaxNodeAnalysisContext syntaxNodeAnalysisC } } - private void checkLinkedResourceExistence(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, + private static void checkLinkedResourceExistence(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, LinksMetaData linksMetaData, LinkedToResource linkedToResource) { if (linksMetaData.getLinkedResourcesMap().containsKey(linkedToResource.getName())) { List linkedResources = @@ -195,14 +442,10 @@ private void checkLinkedResourceExistence(SyntaxNodeAnalysisContext syntaxNodeAn } } - private static void extractServiceAnnotationAndValidate(SyntaxNodeAnalysisContext ctx, - ServiceDeclarationNode serviceDeclarationNode) { - Optional metadataNodeOptional = serviceDeclarationNode.metadata(); - - if (metadataNodeOptional.isEmpty()) { - return; - } - NodeList annotations = metadataNodeOptional.get().annotations(); + protected static void validateServiceAnnotation(SyntaxNodeAnalysisContext ctx, MetadataNode metadataNode, + TypeDescriptorNode serviceTypeDesc, + boolean isServiceContractType) { + NodeList annotations = metadataNode.annotations(); for (AnnotationNode annotation : annotations) { Node annotReference = annotation.annotReference(); String annotName = annotReference.toString(); @@ -212,24 +455,53 @@ private static void extractServiceAnnotationAndValidate(SyntaxNodeAnalysisContex } String[] annotStrings = annotName.split(COLON); if (SERVICE_CONFIG_ANNOTATION.equals(annotStrings[annotStrings.length - 1].trim()) - && (annotValue.isPresent())) { - boolean isInterceptableService = false; - for (Node child:serviceDeclarationNode.children()) { - if (child.kind() == SyntaxKind.QUALIFIED_NAME_REFERENCE && - ((QualifiedNameReferenceNode) child).modulePrefix().text().equals(HTTP) && - ((QualifiedNameReferenceNode) child).identifier().text().equals(INTERCEPTABLE_SERVICE)) { - isInterceptableService = true; - break; - } + && HTTP.equals(annotStrings[0].trim())) { + if (Objects.nonNull(serviceTypeDesc)) { + validateAnnotationUsageForServiceContractType(ctx, annotation, annotValue.orElse(null), + serviceTypeDesc); + return; + } + if (annotValue.isPresent()) { + validateServiceConfigAnnotation(ctx, annotValue); + } + } + if (SERVICE_CONTRACT_CONFIG_ANNOTATION.equals(annotStrings[annotStrings.length - 1].trim()) + && HTTP.equals(annotStrings[0].trim()) && !isServiceContractType) { + reportServiceContractTypeAnnotationNotAllowedFound(ctx, annotation.location()); + } + } + } + + private static void validateAnnotationUsageForServiceContractType(SyntaxNodeAnalysisContext ctx, + AnnotationNode annotation, + MappingConstructorExpressionNode annotValue, + TypeDescriptorNode typeDescriptorNode) { + if (Objects.isNull(annotValue) || annotValue.fields().isEmpty() || annotValue.fields().size() > 1) { + reportInvalidServiceConfigAnnotationUsage(ctx, annotation.location()); + return; + } + + MappingFieldNode field = annotValue.fields().get(0); + String fieldString = field.toString(); + fieldString = fieldString.trim().replaceAll(UNNECESSARY_CHARS_REGEX, ""); + if (field.kind().equals(SyntaxKind.SPECIFIC_FIELD)) { + String[] strings = fieldString.split(COLON, 2); + if (SERVICE_TYPE.equals(strings[0].trim())) { + String expectedServiceType = typeDescriptorNode.toString().trim(); + String actualServiceType = strings[1].trim(); + if (!actualServiceType.equals(expectedServiceType)) { + reportInvalidServiceContractType(ctx, expectedServiceType, actualServiceType, + annotation.location()); } - validateServiceConfigAnnotation(ctx, annotValue, isInterceptableService); + return; } } + + reportInvalidServiceConfigAnnotationUsage(ctx, annotation.location()); } - private static void validateServiceConfigAnnotation(SyntaxNodeAnalysisContext ctx, - Optional maps, - boolean isInterceptableService) { + protected static void validateServiceConfigAnnotation(SyntaxNodeAnalysisContext ctx, + Optional maps) { MappingConstructorExpressionNode mapping = maps.get(); for (MappingFieldNode field : mapping.fields()) { String fieldName = field.toString(); @@ -239,19 +511,20 @@ private static void validateServiceConfigAnnotation(SyntaxNodeAnalysisContext ct if (MEDIA_TYPE_SUBTYPE_PREFIX.equals(strings[0].trim())) { if (!(strings[1].trim().matches(MEDIA_TYPE_SUBTYPE_REGEX))) { reportInvalidMediaTypeSubtype(ctx, strings[1].trim(), field); - break; + continue; } if (strings[1].trim().contains(PLUS)) { String suffix = strings[1].trim().split(SUFFIX_SEPARATOR_REGEX, 2)[1]; reportErrorMediaTypeSuffix(ctx, suffix.trim(), field); - break; } + } else if (SERVICE_TYPE.equals(strings[0].trim())) { + reportServiceTypeNotAllowedFound(ctx, field.location()); } } } } - private void reportInvalidFunctionType(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode node) { + private static void reportInvalidFunctionType(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode node) { DiagnosticInfo diagnosticInfo = new DiagnosticInfo(HTTP_101.getCode(), HTTP_101.getMessage(), HTTP_101.getSeverity()); ctx.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, node.location())); @@ -284,4 +557,42 @@ private static void reportUnresolvedLinkedResourceWithMethod(SyntaxNodeAnalysisC updateDiagnostic(ctx, resource.getNode().location(), HttpDiagnosticCodes.HTTP_150, resource.getMethod(), resource.getName()); } + + private static void reportInvalidServiceConfigAnnotationUsage(SyntaxNodeAnalysisContext ctx, Location location) { + updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_153); + } + + private static void reportInvalidServiceContractType(SyntaxNodeAnalysisContext ctx, String expectedServiceType, + String actualServiceType, Location location) { + updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_156, expectedServiceType, actualServiceType); + } + + private static void reportBasePathNotFound(SyntaxNodeAnalysisContext ctx, String expectedBasePath, + Location location) { + updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_154, expectedBasePath); + } + + private static void reportInvalidBasePathFound(SyntaxNodeAnalysisContext ctx, String expectedBasePath, + String actualBasePath, NodeList nodes) { + Location startLocation = nodes.get(0).location(); + Location endLocation = nodes.get(nodes.size() - 1).location(); + BLangDiagnosticLocation location = new BLangDiagnosticLocation(startLocation.lineRange().fileName(), + startLocation.lineRange().startLine().line(), startLocation.lineRange().endLine().line(), + startLocation.lineRange().startLine().offset(), endLocation.lineRange().endLine().offset(), 0, 0); + updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_155, expectedBasePath, actualBasePath); + } + + private static void reportServiceTypeNotAllowedFound(SyntaxNodeAnalysisContext ctx, NodeLocation location) { + updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_157); + } + + private static void reportServiceContractTypeAnnotationNotAllowedFound(SyntaxNodeAnalysisContext ctx, + NodeLocation location) { + updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_161); + } + + private static void enableImplementServiceContractCodeAction(SyntaxNodeAnalysisContext ctx, String serviceType, + NodeLocation location) { + updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_HINT_105, serviceType); + } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddBasePathCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddBasePathCodeAction.java new file mode 100644 index 0000000000..0138b085bb --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddBasePathCodeAction.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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.stdlib.http.compiler.codeaction; + +import io.ballerina.compiler.syntax.tree.NonTerminalNode; +import io.ballerina.compiler.syntax.tree.SyntaxTree; +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.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.tools.text.LineRange; +import io.ballerina.tools.text.TextDocument; +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 java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.ballerina.stdlib.http.compiler.codeaction.Constants.EXPECTED_BASE_PATH; +import static io.ballerina.stdlib.http.compiler.codeaction.Constants.NODE_LOCATION_KEY; + +public class AddBasePathCodeAction implements CodeAction { + @Override + public List supportedDiagnosticCodes() { + return List.of(HttpDiagnosticCodes.HTTP_154.getCode()); + } + + @Override + public Optional codeActionInfo(CodeActionContext context) { + NonTerminalNode node = CodeActionUtil.findNode(context.currentDocument().syntaxTree(), + context.diagnostic().location().lineRange()); + String diagnosticMsg = context.diagnostic().message(); + Pattern pattern = Pattern.compile("Expected base path is (.*)"); + Matcher matcher = pattern.matcher(diagnosticMsg); + String basePath = ""; + if (matcher.find()) { + basePath = matcher.group(1); + } + CodeActionArgument basePathArg = CodeActionArgument.from(EXPECTED_BASE_PATH, basePath); + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION_KEY, node.location().lineRange()); + return Optional.of(CodeActionInfo.from("Add base path from the service contract", + List.of(locationArg, basePathArg))); + } + + @Override + public List execute(CodeActionExecutionContext context) { + LineRange lineRange = null; + String basePath = ""; + for (CodeActionArgument argument : context.arguments()) { + if (NODE_LOCATION_KEY.equals(argument.key())) { + lineRange = argument.valueAs(LineRange.class); + } + if (EXPECTED_BASE_PATH.equals(argument.key())) { + basePath = argument.valueAs(String.class); + } + } + + if (lineRange == null || basePath.isEmpty()) { + return Collections.emptyList(); + } + + SyntaxTree syntaxTree = context.currentDocument().syntaxTree(); + TextDocument textDocument = syntaxTree.textDocument(); + int end = textDocument.textPositionFrom(lineRange.endLine()); + + List textEdits = new ArrayList<>(); + textEdits.add(TextEdit.from(TextRange.from(end, 0), " \"" + basePath + "\"")); + TextDocumentChange change = TextDocumentChange.from(textEdits.toArray(new TextEdit[0])); + TextDocument modifiedTextDocument = syntaxTree.textDocument().apply(change); + return Collections.singletonList(new DocumentEdit(context.fileUri(), SyntaxTree.from(modifiedTextDocument))); + } + + @Override + public String name() { + return "ADD_BASE_PATH"; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeBasePathCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeBasePathCodeAction.java new file mode 100644 index 0000000000..d81fc995d4 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeBasePathCodeAction.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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.stdlib.http.compiler.codeaction; + +import io.ballerina.compiler.syntax.tree.NonTerminalNode; +import io.ballerina.compiler.syntax.tree.SyntaxTree; +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.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.tools.text.LineRange; +import io.ballerina.tools.text.TextDocument; +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 java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.ballerina.stdlib.http.compiler.codeaction.Constants.EXPECTED_BASE_PATH; +import static io.ballerina.stdlib.http.compiler.codeaction.Constants.NODE_LOCATION_KEY; + +public class ChangeBasePathCodeAction implements CodeAction { + @Override + public List supportedDiagnosticCodes() { + return List.of(HttpDiagnosticCodes.HTTP_155.getCode()); + } + + @Override + public Optional codeActionInfo(CodeActionContext context) { + NonTerminalNode node = CodeActionUtil.findNode(context.currentDocument().syntaxTree(), + context.diagnostic().location().lineRange()); + String diagnosticMsg = context.diagnostic().message(); + Pattern pattern = Pattern.compile("Expected base path is (.*),"); + Matcher matcher = pattern.matcher(diagnosticMsg); + String basePath = ""; + if (matcher.find()) { + basePath = matcher.group(1); + } + CodeActionArgument basePathArg = CodeActionArgument.from(EXPECTED_BASE_PATH, basePath); + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION_KEY, node.location().lineRange()); + return Optional.of(CodeActionInfo.from("Change base path according to the service contract", + List.of(locationArg, basePathArg))); + } + + @Override + public List execute(CodeActionExecutionContext context) { + LineRange lineRange = null; + String basePath = ""; + for (CodeActionArgument argument : context.arguments()) { + if (NODE_LOCATION_KEY.equals(argument.key())) { + lineRange = argument.valueAs(LineRange.class); + } + if (EXPECTED_BASE_PATH.equals(argument.key())) { + basePath = argument.valueAs(String.class); + } + } + + if (lineRange == null || basePath.isEmpty()) { + return Collections.emptyList(); + } + + SyntaxTree syntaxTree = context.currentDocument().syntaxTree(); + TextDocument textDocument = syntaxTree.textDocument(); + int start = textDocument.textPositionFrom(lineRange.startLine()); + int end = textDocument.textPositionFrom(lineRange.endLine()); + + List textEdits = new ArrayList<>(); + textEdits.add(TextEdit.from(TextRange.from(start, end - start), "\"" + basePath + "\"")); + TextDocumentChange change = TextDocumentChange.from(textEdits.toArray(new TextEdit[0])); + TextDocument modifiedTextDocument = syntaxTree.textDocument().apply(change); + return Collections.singletonList(new DocumentEdit(context.fileUri(), SyntaxTree.from(modifiedTextDocument))); + } + + @Override + public String name() { + return "CHANGE_BASE_PATH"; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/Constants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/Constants.java index d1cd8715bf..9bbcdc9eea 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/Constants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/Constants.java @@ -26,6 +26,7 @@ private Constants () {} public static final String NODE_LOCATION_KEY = "node.location"; public static final String IS_ERROR_INTERCEPTOR_TYPE = "node.errorInterceptor"; + public static final String EXPECTED_BASE_PATH = "expectedBasePath"; public static final String REMOTE = "remote"; public static final String RESOURCE = "resource"; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ImplementServiceContract.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ImplementServiceContract.java new file mode 100644 index 0000000000..4bef3f1a65 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ImplementServiceContract.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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.stdlib.http.compiler.codeaction; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.MethodSymbol; +import io.ballerina.compiler.api.symbols.ObjectTypeSymbol; +import io.ballerina.compiler.api.symbols.Qualifier; +import io.ballerina.compiler.api.symbols.ResourceMethodSymbol; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; +import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.ballerina.compiler.api.symbols.resourcepath.ResourcePath; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.NonTerminalNode; +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.compiler.syntax.tree.TypeDescriptorNode; +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.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.tools.text.LineRange; +import io.ballerina.tools.text.TextDocument; +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.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static io.ballerina.stdlib.http.compiler.HttpServiceContractResourceValidator.constructResourcePathName; +import static io.ballerina.stdlib.http.compiler.HttpServiceValidator.getServiceContractTypeDesc; +import static io.ballerina.stdlib.http.compiler.codeaction.Constants.LS; +import static io.ballerina.stdlib.http.compiler.codeaction.Constants.NODE_LOCATION_KEY; + +public class ImplementServiceContract implements CodeAction { + @Override + public List supportedDiagnosticCodes() { + return List.of(HttpDiagnosticCodes.HTTP_HINT_105.getCode()); + } + + @Override + public Optional codeActionInfo(CodeActionContext context) { + NonTerminalNode node = CodeActionUtil.findNode(context.currentDocument().syntaxTree(), + context.diagnostic().location().lineRange()); + if (!node.kind().equals(SyntaxKind.SERVICE_DECLARATION)) { + return Optional.empty(); + } + + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION_KEY, node.location().lineRange()); + return Optional.of(CodeActionInfo.from("Implement all the methods from service contract", + List.of(locationArg))); + } + + @Override + public List execute(CodeActionExecutionContext context) { + LineRange lineRange = null; + for (CodeActionArgument argument : context.arguments()) { + if (NODE_LOCATION_KEY.equals(argument.key())) { + lineRange = argument.valueAs(LineRange.class); + } + } + + if (lineRange == null) { + return Collections.emptyList(); + } + + SyntaxTree syntaxTree = context.currentDocument().syntaxTree(); + SemanticModel semanticModel = context.currentSemanticModel(); + NonTerminalNode node = CodeActionUtil.findNode(syntaxTree, lineRange); + if (!node.kind().equals(SyntaxKind.SERVICE_DECLARATION)) { + return Collections.emptyList(); + } + + Optional serviceTypeDesc = getServiceContractTypeDesc(semanticModel, + (ServiceDeclarationNode) node); + if (serviceTypeDesc.isEmpty()) { + return Collections.emptyList(); + } + + Optional serviceTypeSymbol = semanticModel.symbol(serviceTypeDesc.get()); + if (serviceTypeSymbol.isEmpty() || + !(serviceTypeSymbol.get() instanceof TypeReferenceTypeSymbol serviceTypeRef)) { + return Collections.emptyList(); + } + + TypeSymbol serviceTypeRefSymbol = serviceTypeRef.typeDescriptor(); + if (!(serviceTypeRefSymbol instanceof ObjectTypeSymbol serviceObjTypeSymbol)) { + return Collections.emptyList(); + } + + NodeList members = ((ServiceDeclarationNode) node).members(); + List existingMethods = new ArrayList<>(); + for (Node member : members) { + if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { + Optional functionDefinitionSymbol = semanticModel.symbol(member); + if (functionDefinitionSymbol.isEmpty() || + !(functionDefinitionSymbol.get() instanceof ResourceMethodSymbol resourceMethodSymbol)) { + continue; + } + ResourcePath resourcePath = resourceMethodSymbol.resourcePath(); + existingMethods.add(resourceMethodSymbol.getName().orElse("") + " " + + constructResourcePathName(resourcePath)); + } + } + + Map methodSymbolMap = serviceObjTypeSymbol.methods(); + StringBuilder methods = new StringBuilder(); + for (Map.Entry entry : methodSymbolMap.entrySet()) { + if (existingMethods.contains(entry.getKey())) { + continue; + } + MethodSymbol methodSymbol = entry.getValue(); + if (methodSymbol instanceof ResourceMethodSymbol resourceMethodSymbol) { + methods.append(getMethodSignature(resourceMethodSymbol)); + } + } + + TextRange textRange = TextRange.from(((ServiceDeclarationNode) node).closeBraceToken().textRange().startOffset(), 0); + List textEdits = new ArrayList<>(); + textEdits.add(TextEdit.from(textRange, methods.toString())); + TextDocumentChange change = TextDocumentChange.from(textEdits.toArray(new TextEdit[0])); + TextDocument modifiedTextDocument = syntaxTree.textDocument().apply(change); + return Collections.singletonList(new DocumentEdit(context.fileUri(), SyntaxTree.from(modifiedTextDocument))); + } + + private String getMethodSignature(ResourceMethodSymbol resourceMethodSymbol) { + return LS + "\t" + sanitizePackageNames(resourceMethodSymbol.signature()) + " {" + LS + LS + "\t}" + LS; + } + + private String sanitizePackageNames(String input) { + Pattern pattern = Pattern.compile("(\\w+)/(\\w+:)(\\d+.\\d+.\\d+):"); + Matcher matcher = pattern.matcher(input); + return matcher.replaceAll("$2"); + } + + @Override + public String name() { + return "IMPLEMENT_SERVICE_CONTRACT"; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpPayloadParamIdentifier.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpPayloadParamIdentifier.java index 7bf304e6a6..832b837d79 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpPayloadParamIdentifier.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpPayloadParamIdentifier.java @@ -27,8 +27,10 @@ import io.ballerina.compiler.api.symbols.UnionTypeSymbol; import io.ballerina.compiler.syntax.tree.ClassDefinitionNode; import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.Token; @@ -37,7 +39,9 @@ import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; import io.ballerina.stdlib.http.compiler.Constants; import io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.stdlib.http.compiler.HttpResourceFunctionNode; import io.ballerina.stdlib.http.compiler.HttpResourceValidator; +import io.ballerina.stdlib.http.compiler.HttpServiceObjTypeAnalyzer; import io.ballerina.stdlib.http.compiler.HttpServiceValidator; import io.ballerina.stdlib.http.compiler.codemodifier.context.DocumentContext; import io.ballerina.stdlib.http.compiler.codemodifier.context.ParamAvailability; @@ -69,6 +73,8 @@ import static io.ballerina.stdlib.http.compiler.HttpResourceValidator.getEffectiveType; import static io.ballerina.stdlib.http.compiler.HttpResourceValidator.isValidBasicParamType; import static io.ballerina.stdlib.http.compiler.HttpResourceValidator.isValidNilableBasicParamType; +import static io.ballerina.stdlib.http.compiler.HttpServiceObjTypeAnalyzer.isHttpServiceType; +import static io.ballerina.stdlib.http.compiler.HttpServiceObjTypeAnalyzer.isServiceObjectType; /** @@ -94,6 +100,9 @@ public void perform(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext) { validateServiceDeclaration(syntaxNodeAnalysisContext, typeSymbols); } else if (kind == SyntaxKind.CLASS_DEFINITION) { validateClassDefinition(syntaxNodeAnalysisContext, typeSymbols); + } else if (kind == SyntaxKind.OBJECT_TYPE_DESC && isHttpServiceType(syntaxNodeAnalysisContext.semanticModel(), + syntaxNodeAnalysisContext.node())) { + validateServiceObjDefinition(syntaxNodeAnalysisContext, typeSymbols); } } @@ -105,9 +114,27 @@ private void validateServiceDeclaration(SyntaxNodeAnalysisContext syntaxNodeAnal } NodeList members = serviceDeclarationNode.members(); ServiceContext serviceContext = new ServiceContext(serviceDeclarationNode.hashCode()); + validateResources(syntaxNodeAnalysisContext, typeSymbols, members, serviceContext); + } + + private void validateServiceObjDefinition(SyntaxNodeAnalysisContext context, Map typeSymbols) { + ObjectTypeDescriptorNode serviceObjType = (ObjectTypeDescriptorNode) context.node(); + NodeList members = serviceObjType.members(); + ServiceContext serviceContext = new ServiceContext(serviceObjType.hashCode()); + validateResources(context, typeSymbols, members, serviceContext); + } + + private void validateResources(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, + Map typeSymbols, NodeList members, + ServiceContext serviceContext) { for (Node member : members) { if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { - validateResource(syntaxNodeAnalysisContext, (FunctionDefinitionNode) member, serviceContext, + validateResource(syntaxNodeAnalysisContext, + new HttpResourceFunctionNode((FunctionDefinitionNode) member), serviceContext, + typeSymbols); + } else if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DECLARATION) { + validateResource(syntaxNodeAnalysisContext, + new HttpResourceFunctionNode((MethodDeclarationNode) member), serviceContext, typeSymbols); } } @@ -141,24 +168,19 @@ private void validateClassDefinition(SyntaxNodeAnalysisContext syntaxNodeAnalysi } } if (proceed) { - for (Node member : members) { - if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { - validateResource(syntaxNodeAnalysisContext, (FunctionDefinitionNode) member, serviceContext, - typeSymbols); - } - } + validateResources(syntaxNodeAnalysisContext, typeSymbols, members, serviceContext); } } - void validateResource(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, ServiceContext serviceContext, + void validateResource(SyntaxNodeAnalysisContext ctx, HttpResourceFunctionNode member, ServiceContext serviceContext, Map typeSymbols) { extractInputParamTypeAndValidate(ctx, member, serviceContext, typeSymbols); } - void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ctx, HttpResourceFunctionNode member, ServiceContext serviceContext, Map typeSymbols) { - Optional resourceMethodSymbolOptional = ctx.semanticModel().symbol(member); + Optional resourceMethodSymbolOptional = member.getSymbol(ctx.semanticModel()); int resourceId = member.hashCode(); if (resourceMethodSymbolOptional.isEmpty()) { return; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpServiceModifier.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpServiceModifier.java index 4aa6227431..c2ad4ea204 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpServiceModifier.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpServiceModifier.java @@ -44,7 +44,8 @@ public HttpServiceModifier() { public void init(CodeModifierContext codeModifierContext) { codeModifierContext.addSyntaxNodeAnalysisTask( new HttpPayloadParamIdentifier(this.payloadParamContextMap), - List.of(SyntaxKind.SERVICE_DECLARATION, SyntaxKind.CLASS_DEFINITION)); + List.of(SyntaxKind.SERVICE_DECLARATION, SyntaxKind.CLASS_DEFINITION, SyntaxKind.OBJECT_TYPE_DESC)); codeModifierContext.addSourceModifierTask(new PayloadAnnotationModifierTask(this.payloadParamContextMap)); + codeModifierContext.addSourceModifierTask(new ServiceTypeModifierTask()); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/PayloadAnnotationModifierTask.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/PayloadAnnotationModifierTask.java index 86f7d9242b..1f85927e68 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/PayloadAnnotationModifierTask.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/PayloadAnnotationModifierTask.java @@ -29,6 +29,7 @@ import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeFactory; import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.ParameterNode; import io.ballerina.compiler.syntax.tree.RequiredParameterNode; import io.ballerina.compiler.syntax.tree.SeparatedNodeList; @@ -37,6 +38,7 @@ import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.SyntaxTree; import io.ballerina.compiler.syntax.tree.Token; +import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; import io.ballerina.projects.Document; import io.ballerina.projects.DocumentId; import io.ballerina.projects.Module; @@ -54,6 +56,9 @@ import java.util.List; import java.util.Map; +import static io.ballerina.stdlib.http.compiler.HttpServiceObjTypeAnalyzer.isHttpServiceType; +import static io.ballerina.stdlib.http.compiler.HttpServiceValidator.isServiceContractImplementation; + /** * {@code HttpPayloadParamIdentifier} injects the @http:Payload annotation to the Payload param which found during the * initial analysis. @@ -108,7 +113,8 @@ private NodeList updateMemberNodes(NodeList members; - if (memberNode.kind() == SyntaxKind.SERVICE_DECLARATION) { + if (memberNode.kind() == SyntaxKind.SERVICE_DECLARATION && + !isServiceContractImplementation(documentContext.getContext().semanticModel(), memberNode)) { ServiceDeclarationNode serviceNode = (ServiceDeclarationNode) memberNode; serviceId = serviceNode.hashCode(); members = serviceNode.members(); @@ -116,6 +122,12 @@ private NodeList updateMemberNodes(NodeList updateMemberNodes(NodeList { + + @Override + public void modify(SourceModifierContext modifierContext) { + boolean erroneousCompilation = modifierContext.compilation().diagnosticResult() + .diagnostics().stream() + .anyMatch(d -> DiagnosticSeverity.ERROR.equals(d.diagnosticInfo().severity())); + if (erroneousCompilation) { + return; + } + + modifyServiceDeclarationNodes(modifierContext); + } + + private void modifyServiceDeclarationNodes(SourceModifierContext modifierContext) { + Package currentPackage = modifierContext.currentPackage(); + for (ModuleId moduleId : currentPackage.moduleIds()) { + modifyServiceDeclarationsPerModule(modifierContext, moduleId, currentPackage); + } + } + + private void modifyServiceDeclarationsPerModule(SourceModifierContext modifierContext, ModuleId moduleId, + Package currentPackage) { + Module currentModule = currentPackage.module(moduleId); + for (DocumentId documentId : currentModule.documentIds()) { + modifyServiceDeclarationsPerDocument(modifierContext, documentId, currentModule); + } + } + + private void modifyServiceDeclarationsPerDocument(SourceModifierContext modifierContext, DocumentId documentId, + Module currentModule) { + Document currentDoc = currentModule.document(documentId); + ModulePartNode rootNode = currentDoc.syntaxTree().rootNode(); + NodeList newMembers = updateMemberNodes(rootNode.members()); + ModulePartNode newModulePart = rootNode.modify(rootNode.imports(), newMembers, rootNode.eofToken()); + SyntaxTree updatedSyntaxTree = currentDoc.syntaxTree().modifyWith(newModulePart); + TextDocument textDocument = updatedSyntaxTree.textDocument(); + if (currentModule.documentIds().contains(documentId)) { + modifierContext.modifySourceFile(textDocument, documentId); + } else { + modifierContext.modifyTestSourceFile(textDocument, documentId); + } + } + + private NodeList updateMemberNodes(NodeList oldMembers) { + List updatedMembers = new ArrayList<>(); + for (ModuleMemberDeclarationNode memberNode : oldMembers) { + if (memberNode.kind().equals(SyntaxKind.SERVICE_DECLARATION)) { + updatedMembers.add(updateServiceDeclarationNode((ServiceDeclarationNode) memberNode)); + } else { + updatedMembers.add(memberNode); + } + } + return AbstractNodeFactory.createNodeList(updatedMembers); + } + + private ServiceDeclarationNode updateServiceDeclarationNode(ServiceDeclarationNode serviceDeclarationNode) { + Optional serviceTypeDesc = serviceDeclarationNode.typeDescriptor(); + if (serviceTypeDesc.isEmpty()) { + return serviceDeclarationNode; + } + + Optional metadataNodeOptional = serviceDeclarationNode.metadata(); + if (metadataNodeOptional.isEmpty()) { + return addServiceConfigAnnotation(serviceTypeDesc.get(), serviceDeclarationNode); + } + + NodeList annotations = metadataNodeOptional.get().annotations(); + for (AnnotationNode annotation : annotations) { + Node annotReference = annotation.annotReference(); + String annotName = annotReference.toString(); + if (annotReference.kind() != SyntaxKind.QUALIFIED_NAME_REFERENCE) { + continue; + } + String[] annotStrings = annotName.split(COLON); + if (SERVICE_CONFIG_ANNOTATION.equals(annotStrings[annotStrings.length - 1].trim()) + && HTTP.equals(annotStrings[0].trim())) { + return serviceDeclarationNode; + } + } + + return addServiceConfigAnnotation(serviceTypeDesc.get(), serviceDeclarationNode); + } + + private ServiceDeclarationNode addServiceConfigAnnotation(TypeDescriptorNode serviceTypeDesc, + ServiceDeclarationNode serviceDeclarationNode) { + SpecificFieldNode serviceTypeField = createSpecificFieldNode(null, createIdentifierToken("serviceType"), + createToken(COLON_TOKEN), serviceTypeDesc); + MappingConstructorExpressionNode serviceConfigConstruct = createMappingConstructorExpressionNode(createToken(SyntaxKind.OPEN_BRACE_TOKEN), + createSeparatedNodeList(serviceTypeField), createToken(SyntaxKind.CLOSE_BRACE_TOKEN)); + AnnotationNode serviceConfigAnnotation = createAnnotationNode(createToken(AT_TOKEN), + createQualifiedNameReferenceNode(createIdentifierToken(HTTP), createToken(COLON_TOKEN), + createIdentifierToken(SERVICE_CONFIG_ANNOTATION)), serviceConfigConstruct); + Optional metadata = serviceDeclarationNode.metadata(); + MetadataNode metadataNode; + if (metadata.isEmpty()) { + metadataNode = createMetadataNode(null, createNodeList(serviceConfigAnnotation)); + } else { + NodeList annotations = metadata.get().annotations().add(serviceConfigAnnotation); + metadataNode = metadata.get().modify().withAnnotations(annotations).apply(); + } + return serviceDeclarationNode.modify().withMetadata(metadataNode).apply(); + } +}