Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2026, 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.flowmodelgenerator.core;

import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Utility class to replace generic lang type parameters with their super types.
*
* @since 1.7.0
*/
public class TypeParameterReplacer {

private static final Map<String, String> TYPE_PARAMETER_REPLACEMENTS = new HashMap<>();

static {
// lang.array type parameter replacements
TYPE_PARAMETER_REPLACEMENTS.put("array:Type1", "(any|error)");
TYPE_PARAMETER_REPLACEMENTS.put("array:Type", "(any|error)");
TYPE_PARAMETER_REPLACEMENTS.put("array:AnydataType", "(anydata|error)");

// lang.error type parameter replacements
TYPE_PARAMETER_REPLACEMENTS.put("error:DetailType", "error:Detail");

// lang.map type parameter replacements
TYPE_PARAMETER_REPLACEMENTS.put("map:Type1", "map<any|error>");
TYPE_PARAMETER_REPLACEMENTS.put("map:Type", "map<any|error>");

// lang.stream type parameter replacements
TYPE_PARAMETER_REPLACEMENTS.put("stream:Type1", "(any|error)");
TYPE_PARAMETER_REPLACEMENTS.put("stream:Type", "(any|error)");
TYPE_PARAMETER_REPLACEMENTS.put("stream:ErrorType", "error");
TYPE_PARAMETER_REPLACEMENTS.put("stream:CompletionType", "error");

// lang.xml type parameter replacements
TYPE_PARAMETER_REPLACEMENTS.put("xml:XmlType", "xml");
TYPE_PARAMETER_REPLACEMENTS.put("xml:ItemType",
"(xml:Element|xml:Comment|xml:ProcessingInstruction|xml:Text)");

// lang.table type parameter replacements
TYPE_PARAMETER_REPLACEMENTS.put("table:MapType1", "map<any|error>");
TYPE_PARAMETER_REPLACEMENTS.put("table:MapType", "map<any|error>");
TYPE_PARAMETER_REPLACEMENTS.put("table:KeyType", "anydata");
TYPE_PARAMETER_REPLACEMENTS.put("table:Type", "(any|error)");

// lang.value type parameter replacements
TYPE_PARAMETER_REPLACEMENTS.put("value:AnydataType", "anydata");
TYPE_PARAMETER_REPLACEMENTS.put("value:Type", "(any|error)");
}

public static List<String> getSortedPlaceholderValues() {
return Set.copyOf(TYPE_PARAMETER_REPLACEMENTS.values()).stream()
.sorted(Comparator.comparingInt(String::length).reversed())
.toList();
}

/**
* Replaces type parameters in a string value.
*
* @param original the original string
* @return the string with type parameters replaced
*/
public static String replaceTypeParameters(String original) {
if (original == null || original.isEmpty()) {
return original;
}

String result = original;
List<Map.Entry<String, String>> entries = TYPE_PARAMETER_REPLACEMENTS.entrySet().stream()
.sorted(Comparator.comparingInt((Map.Entry<String, String> e) -> e.getKey().length()).reversed())
.toList();

for (Map.Entry<String, String> entry : entries) {
result = result.replace(entry.getKey(), entry.getValue());
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ protected Set<Diagnostic> getSemanticDiagnostics(ExpressionEditorContext context
}
symbolStream = semanticModel.get()
.visibleSymbols(document.get(), context.info().startLine()).parallelStream()
.filter(symbol -> symbol.kind() == SymbolKind.VARIABLE);
.filter(symbol -> symbol.kind() == SymbolKind.VARIABLE
|| symbol.kind() == SymbolKind.WORKER);
}

// Check for redeclared symbols
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.ballerina.compiler.syntax.tree.TypedBindingPatternNode;
import io.ballerina.flowmodelgenerator.core.Constants;
import io.ballerina.flowmodelgenerator.core.DiagnosticHandler;
import io.ballerina.flowmodelgenerator.core.TypeParameterReplacer;
import io.ballerina.flowmodelgenerator.core.model.node.DataMapperBuilder;
import io.ballerina.flowmodelgenerator.core.model.node.ExpressionBuilder;
import io.ballerina.flowmodelgenerator.core.model.node.FunctionDefinitionBuilder;
Expand Down Expand Up @@ -216,6 +217,7 @@ public FormBuilder<T> type(String typeName, String label, boolean editable, Bool

public FormBuilder<T> type(String typeName, String label, boolean editable, Boolean modified, LineRange lineRange,
String importStatements, boolean hidden) {
String replacedTypeName = TypeParameterReplacer.replaceTypeParameters(typeName);
propertyBuilder
.metadata()
.label(label)
Expand All @@ -224,9 +226,9 @@ public FormBuilder<T> type(String typeName, String label, boolean editable, Bool
.codedata()
.stepOut()
.placeholder("var")
.value(typeName)
.value(replacedTypeName)
.imports(importStatements)
.hidden(hidden)
.hidden(hidden || !replacedTypeName.equals(typeName))
.type()
.fieldType(Property.ValueType.TYPE)
.selected(true)
Expand All @@ -244,7 +246,7 @@ public FormBuilder<T> returnType(String value, String typeConstraint, boolean op
.label(Property.RETURN_TYPE_LABEL)
.description(Property.RETURN_TYPE_DOC)
.stepOut()
.value(value == null ? "" : value)
.value(TypeParameterReplacer.replaceTypeParameters(value == null ? "" : value))
.type()
.fieldType(Property.ValueType.TYPE)
.ballerinaType(typeConstraint)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.ballerina.compiler.api.symbols.UnionTypeSymbol;
import io.ballerina.compiler.syntax.tree.Node;
import io.ballerina.flowmodelgenerator.core.DiagnosticHandler;
import io.ballerina.flowmodelgenerator.core.TypeParameterReplacer;
import io.ballerina.modelgenerator.commons.CommonUtils;
import io.ballerina.modelgenerator.commons.ModuleInfo;
import io.ballerina.modelgenerator.commons.ParameterMemberTypeData;
Expand Down Expand Up @@ -456,7 +457,7 @@ public TypeBuilder fieldType(Property.ValueType fieldType) {
}

public TypeBuilder ballerinaType(String ballerinaType) {
this.ballerinaType = ballerinaType;
this.ballerinaType = TypeParameterReplacer.replaceTypeParameters(ballerinaType);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ public SourceBuilder newVariableWithInferredType() {
return this;
}

public SourceBuilder newVariableWithType(String resolvedType) {
Optional<Property> variable = getProperty(Property.VARIABLE_KEY);
if (variable.isEmpty()) {
return this;
}
tokenBuilder.expressionWithType(resolvedType, variable.get()).keyword(SyntaxKind.EQUAL_TOKEN);
return this;
}

public SourceBuilder newVariable(String typeKey) {
Optional<Property> type = getProperty(typeKey);
Optional<Property> variable = getProperty(Property.VARIABLE_KEY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,22 @@
package io.ballerina.flowmodelgenerator.core.model.node;

import io.ballerina.compiler.api.SemanticModel;
import io.ballerina.compiler.api.symbols.ArrayTypeSymbol;
import io.ballerina.compiler.api.symbols.Symbol;
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.VariableSymbol;
import io.ballerina.flowmodelgenerator.core.AiUtils;
import io.ballerina.flowmodelgenerator.core.TypeParameterReplacer;
import io.ballerina.flowmodelgenerator.core.model.Codedata;
import io.ballerina.flowmodelgenerator.core.model.FlowNode;
import io.ballerina.flowmodelgenerator.core.model.FormBuilder;
import io.ballerina.flowmodelgenerator.core.model.NodeBuilder;
import io.ballerina.flowmodelgenerator.core.model.NodeKind;
import io.ballerina.flowmodelgenerator.core.model.Property;
import io.ballerina.flowmodelgenerator.core.model.PropertyType;
import io.ballerina.flowmodelgenerator.core.utils.FileSystemUtils;
import io.ballerina.flowmodelgenerator.core.utils.FlowNodeUtil;
import io.ballerina.flowmodelgenerator.core.utils.ParamUtils;
import io.ballerina.modelgenerator.commons.CommonUtils;
Expand All @@ -34,14 +43,20 @@
import io.ballerina.modelgenerator.commons.ModuleInfo;
import io.ballerina.modelgenerator.commons.PackageUtil;
import io.ballerina.modelgenerator.commons.ParameterData;
import io.ballerina.projects.Document;
import io.ballerina.projects.PackageDescriptor;
import io.ballerina.projects.Project;
import io.ballerina.projects.ProjectException;
import io.ballerina.tools.text.LinePosition;
import org.ballerinalang.langserver.common.utils.CommonUtil;
import org.ballerinalang.langserver.commons.eventsync.exceptions.EventSyncException;
import org.ballerinalang.langserver.commons.workspace.WorkspaceDocumentException;
import org.ballerinalang.langserver.commons.workspace.WorkspaceManager;

import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* Abstract base class for function-like builders (functions, methods, resource actions).
Expand Down Expand Up @@ -250,6 +265,121 @@ protected void setExpressionProperty(Codedata codedata) {
.addProperty(Property.CONNECTION_KEY);
}

private static boolean isLangLibFunction(Codedata codedata) {
return codedata != null
&& CommonUtil.BALLERINA_ORG_NAME.equals(codedata.org())
&& codedata.module() != null
&& codedata.module().startsWith("lang.");
}

protected static String resolveLangLibReturnType(WorkspaceManager workspaceManager, Path filePath,
FlowNode flowNode) {
if (!isLangLibFunction(flowNode.codedata())) {
return null;
}

Optional<Property> typeProperty = flowNode.getProperty(Property.TYPE_KEY);
if (typeProperty.isEmpty()) {
return null;
}

String typeName = typeProperty.get().value().toString();

// Find the first (longest) placeholder present in the return type
String matchedPlaceholder = null;
for (String placeholder : TypeParameterReplacer.getSortedPlaceholderValues()) {
if (typeName.contains(placeholder)) {
matchedPlaceholder = placeholder;
break;
}
}
if (matchedPlaceholder == null) {
return null;
}

// Find a REQUIRED parameter whose ballerinaType template matches the placeholder structure
Map<String, Property> properties = flowNode.properties();
if (properties == null) {
return null;
}

final String placeholder = matchedPlaceholder;
for (Property prop : properties.values()) {
if (prop.codedata() == null || prop.codedata().kind() == null) {
continue;
}
if (!prop.codedata().kind().equals(ParameterData.Kind.REQUIRED.name())) {
continue;
}
if (prop.value() == null || prop.value().toString().isBlank()) {
continue;
}
if (prop.types() == null) {
continue;
}

for (PropertyType pt : prop.types()) {
String template = pt.ballerinaType();
if (template == null) {
continue;
}
String varName = prop.value().toString().trim();
String resolved = resolveConcreteType(workspaceManager, filePath, flowNode,
varName, template, placeholder);
if (resolved != null) {
return typeName.replace(placeholder, resolved);
}
}
}
return null;
}

private static String resolveConcreteType(WorkspaceManager workspaceManager, Path filePath,
FlowNode flowNode, String varName,
String template, String placeholder) {
try {
workspaceManager.loadProject(filePath);
SemanticModel semanticModel = FileSystemUtils.getSemanticModel(workspaceManager, filePath);
Document document = FileSystemUtils.getDocument(workspaceManager, filePath);

LinePosition position = flowNode.codedata().lineRange().startLine();

List<Symbol> visibleSymbols = semanticModel.visibleSymbols(document, position);
Optional<Symbol> matchingSymbol = visibleSymbols.stream()
.filter(s -> s.getName().map(name -> name.equals(varName)).orElse(false))
.findFirst();

if (matchingSymbol.isEmpty() || !(matchingSymbol.get() instanceof VariableSymbol variableSymbol)) {
return null;
}

TypeSymbol typeSymbol = variableSymbol.typeDescriptor();
if (typeSymbol.typeKind() == TypeDescKind.TYPE_REFERENCE) {
typeSymbol = ((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor();
}

ModuleInfo moduleInfo = ModuleInfo.from(document.module().descriptor());

// Case 1: template IS the placeholder — use the actual type directly
if (template.equals(placeholder)) {
return CommonUtils.getTypeSignature(semanticModel, typeSymbol, true, moduleInfo);
}

// Case 2: template is placeholder + [] — extract the array element type
if (template.equals(placeholder + "[]")) {
if (typeSymbol.typeKind() != TypeDescKind.ARRAY) {
return null;
}
TypeSymbol elementType = ((ArrayTypeSymbol) typeSymbol).memberTypeDescriptor();
return CommonUtils.getTypeSignature(semanticModel, elementType, true, moduleInfo);
}

return null;
} catch (WorkspaceDocumentException | EventSyncException | ProjectException e) {
return null;
}
}

protected static boolean isLocalFunction(WorkspaceManager workspaceManager, Path filePath, Codedata codedata) {
if (codedata.org() == null || codedata.module() == null) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ public Map<Path, List<TextEdit>> toSource(SourceBuilder sourceBuilder) {
sourceBuilder.token().keyword(SyntaxKind.FINAL_KEYWORD);
}

sourceBuilder.newVariableWithInferredType();
String resolvedReturnType = resolveLangLibReturnType(sourceBuilder.workspaceManager,
sourceBuilder.filePath, flowNode);
if (resolvedReturnType != null) {
sourceBuilder.newVariableWithType(resolvedReturnType);
} else {
sourceBuilder.newVariableWithInferredType();
}
if (FlowNodeUtil.hasCheckKeyFlagSet(flowNode)) {
sourceBuilder.token().keyword(SyntaxKind.CHECK_KEYWORD);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ public class MethodCall extends CallBuilder {

@Override
public Map<Path, List<TextEdit>> toSource(SourceBuilder sourceBuilder) {
sourceBuilder.newVariableWithInferredType();
FlowNode flowNode = sourceBuilder.flowNode;
String resolvedReturnType = resolveLangLibReturnType(sourceBuilder.workspaceManager,
sourceBuilder.filePath, flowNode);
if (resolvedReturnType != null) {
sourceBuilder.newVariableWithType(resolvedReturnType);
} else {
sourceBuilder.newVariableWithInferredType();
}

if (FlowNodeUtil.hasCheckKeyFlagSet(flowNode)) {
sourceBuilder.token().keyword(SyntaxKind.CHECK_KEYWORD);
Expand Down
Loading
Loading