diff --git a/flow-model-generator/modules/flow-model-generator-core/src/main/java/io/ballerina/flowmodelgenerator/core/DataMapManager.java b/flow-model-generator/modules/flow-model-generator-core/src/main/java/io/ballerina/flowmodelgenerator/core/DataMapManager.java index 875bf6ed14..2dd62dd601 100644 --- a/flow-model-generator/modules/flow-model-generator-core/src/main/java/io/ballerina/flowmodelgenerator/core/DataMapManager.java +++ b/flow-model-generator/modules/flow-model-generator-core/src/main/java/io/ballerina/flowmodelgenerator/core/DataMapManager.java @@ -33,6 +33,7 @@ import io.ballerina.compiler.api.symbols.RecordTypeSymbol; import io.ballerina.compiler.api.symbols.Symbol; import io.ballerina.compiler.api.symbols.SymbolKind; +import io.ballerina.compiler.api.symbols.TupleTypeSymbol; import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol; import io.ballerina.compiler.api.symbols.TypeDescKind; import io.ballerina.compiler.api.symbols.TypeSymbol; @@ -227,9 +228,10 @@ public JsonElement getMappings(SemanticModel semanticModel, JsonElement cd, Line .toList(); Map references = new HashMap<>(); RefType refType; + TypeSymbol targetTypeSymbol; try { - TypeSymbol targetTypeSymbol = targetNode.typeSymbol(); - TypeSymbol rawtargetTypeSymbol = CommonUtils.getRawType(targetNode.typeSymbol()); + targetTypeSymbol = targetNode.typeSymbol(); + TypeSymbol rawtargetTypeSymbol = CommonUtils.getRawType(targetTypeSymbol); if (rawtargetTypeSymbol.typeKind() == TypeDescKind.UNION) { targetTypeSymbol = filterErrorOrNil(semanticModel, (UnionTypeSymbol) rawtargetTypeSymbol, new ArrayList<>()); @@ -365,7 +367,7 @@ public JsonElement getMappings(SemanticModel semanticModel, JsonElement cd, Line functionDocument, dataMappingDocument, enumPorts, inputPorts); } else if (expr.kind() == SyntaxKind.LIST_CONSTRUCTOR) { genMapping((ListConstructorExpressionNode) expr, mappings, name, semanticModel, functionDocument, - dataMappingDocument, enumPorts, inputPorts); + dataMappingDocument, enumPorts, inputPorts, targetTypeSymbol); } else { genMapping(expr, name, mappings, semanticModel, functionDocument, dataMappingDocument, enumPorts); } @@ -765,7 +767,7 @@ private void genMapping(MappingConstructorExpressionNode mappingCtrExpr, List mappings, String name, SemanticModel semanticModel, Document functionDocument, Document dataMappingDocument, - List enumPorts, List inputPorts) { + List enumPorts, List inputPorts, TypeSymbol targetType) { SeparatedNodeList expressions = listCtrExpr.expressions(); int size = expressions.size(); @@ -799,18 +801,42 @@ private void genMapping(ListConstructorExpressionNode listCtrExpr, List } } + // Use bracket notation for tuples, dot notation for arrays + // First try to use the target type, then fall back to semantic model + boolean isTuple = false; + if (targetType != null) { + TypeSymbol rawTargetType = CommonUtils.getRawType(targetType); + isTuple = rawTargetType.typeKind() == TypeDescKind.TUPLE; + } + if (!isTuple) { + isTuple = semanticModel.typeOf(listCtrExpr) + .map(t -> CommonUtils.getRawType(t).typeKind() == TypeDescKind.TUPLE) + .orElse(false); + } + List mappingElements = new ArrayList<>(); + // Get tuple member types if available + List memberTypes = null; + if (isTuple && targetType != null) { + TypeSymbol rawTargetType = CommonUtils.getRawType(targetType); + if (rawTargetType.typeKind() == TypeDescKind.TUPLE) { + memberTypes = ((TupleTypeSymbol) rawTargetType).memberTypeDescriptors(); + } + } + for (int i = 0; i < size; i++) { List elements = new ArrayList<>(); Node expr = expressions.get(i); + String elementPath = isTuple ? name + "[" + i + "]" : name + "." + i; + TypeSymbol memberType = (memberTypes != null && i < memberTypes.size()) ? memberTypes.get(i) : null; if (expr.kind() == SyntaxKind.MAPPING_CONSTRUCTOR) { - genMapping((MappingConstructorExpressionNode) expr, elements, name + "." + i, semanticModel, + genMapping((MappingConstructorExpressionNode) expr, elements, elementPath, semanticModel, functionDocument, dataMappingDocument, enumPorts, inputPorts); } else if (expr.kind() == SyntaxKind.LIST_CONSTRUCTOR) { - genMapping((ListConstructorExpressionNode) expr, elements, name + "." + i, semanticModel, - functionDocument, dataMappingDocument, enumPorts, inputPorts); + genMapping((ListConstructorExpressionNode) expr, elements, elementPath, semanticModel, + functionDocument, dataMappingDocument, enumPorts, inputPorts, memberType); } else { - genMapping(expr, name + "." + i, elements, semanticModel, functionDocument, dataMappingDocument, + genMapping(expr, elementPath, elements, semanticModel, functionDocument, dataMappingDocument, enumPorts); } mappingElements.add(new MappingElements(elements)); @@ -823,11 +849,11 @@ private void genMapping(ListConstructorExpressionNode listCtrExpr, List private void genMapping(Node expr, String name, List elements, SemanticModel semanticModel, Document functionDocument, Document dataMappingDocument, List enumPorts) { List inputs = new ArrayList<>(); - expr.accept(new GenInputsVisitor(inputs, enumPorts)); + expr.accept(new GenInputsVisitor(inputs, enumPorts, semanticModel)); LineRange customFunctionRange = getCustomFunctionRange(expr, functionDocument, dataMappingDocument); List elementAccessIndex = null; if (containsArrayAccess(expr)) { - elementAccessIndex = extractArrayIndices(expr); + elementAccessIndex = extractArrayIndices(expr, semanticModel); } Mapping mapping = new Mapping(name, inputs, expr.toSourceCode(), getDiagnostics(expr.lineRange(), semanticModel), new ArrayList<>(), @@ -905,9 +931,9 @@ private List> getDiagnostics(LineRange lineRange, SemanticMo .toList(); } - private List extractArrayIndices(Node expr) { + private List extractArrayIndices(Node expr, SemanticModel semanticModel) { List indices = new ArrayList<>(); - ArrayIndexExtractorVisitor visitor = new ArrayIndexExtractorVisitor(indices); + ArrayIndexExtractorVisitor visitor = new ArrayIndexExtractorVisitor(indices, semanticModel); expr.accept(visitor); return indices.isEmpty() ? null : indices; } @@ -1267,8 +1293,14 @@ private MappingPort handleTupleType(String id, String name, String typeName, Ref MappingTuplePort tuplePort = new MappingTuplePort(id, name, typeName, "tuple", type.key); tuplePort.typeInfo = isExternalType(type) ? createTypeInfo(type) : null; - for (RefType member : ((RefTupleType) type).memberTypes) { - MappingPort memberPort = getRefMappingPort(id, name, member, visitedTypes, references); + List memberTypes = ((RefTupleType) type).memberTypes; + for (int i = 0; i < memberTypes.size(); i++) { + RefType member = memberTypes.get(i); + String memberName = "[" + i + "]"; + MappingPort memberPort = getRefMappingPort(memberName, name + memberName, member, visitedTypes, references); + if (memberPort.displayName == null) { + memberPort.displayName = name + "[" + i + "]"; + } tuplePort.members.add(memberPort); } @@ -1451,7 +1483,8 @@ private boolean isExternalType(RefType refType) { !refType.moduleInfo.packageName.equals(currentModuleInfo.packageName()); } - public JsonElement getSource(Path filePath, JsonElement cd, JsonElement mp, String targetField) { + public JsonElement getSource(SemanticModel semanticModel, Path filePath, JsonElement cd, JsonElement mp, + String targetField) { Codedata codedata = gson.fromJson(cd, Codedata.class); Mapping mapping = gson.fromJson(mp, Mapping.class); NonTerminalNode node = getNode(codedata.lineRange()); @@ -1461,15 +1494,28 @@ public JsonElement getSource(Path filePath, JsonElement cd, JsonElement mp, Stri textEditsMap.put(filePath, textEdits); ExpressionNode expr = null; + TypeSymbol targetTypeSymbol = null; if (node.kind() == SyntaxKind.LOCAL_VAR_DECL) { VariableDeclarationNode varDecl = (VariableDeclarationNode) node; expr = varDecl.initializer().orElseThrow(); + targetTypeSymbol = semanticModel.symbol(varDecl) + .filter(s -> s.kind() == SymbolKind.VARIABLE) + .map(s -> ((VariableSymbol) s).typeDescriptor()) + .orElse(null); } else if (node.kind() == SyntaxKind.MODULE_VAR_DECL) { ModuleVariableDeclarationNode moduleVarDecl = (ModuleVariableDeclarationNode) node; expr = moduleVarDecl.initializer().orElseThrow(); + targetTypeSymbol = semanticModel.symbol(moduleVarDecl) + .filter(s -> s.kind() == SymbolKind.VARIABLE) + .map(s -> ((VariableSymbol) s).typeDescriptor()) + .orElse(null); } else if (node.kind() == SyntaxKind.LET_VAR_DECL) { LetVariableDeclarationNode varDecl = (LetVariableDeclarationNode) node; expr = varDecl.expression(); + targetTypeSymbol = semanticModel.symbol(varDecl) + .filter(s -> s.kind() == SymbolKind.VARIABLE) + .map(s -> ((VariableSymbol) s).typeDescriptor()) + .orElse(null); } else if (node.kind() == SyntaxKind.FUNCTION_DEFINITION) { FunctionDefinitionNode funcDefNode = (FunctionDefinitionNode) node; FunctionBodyNode funcBodyNode = funcDefNode.functionBody(); @@ -1477,6 +1523,14 @@ public JsonElement getSource(Path filePath, JsonElement cd, JsonElement mp, Stri ExpressionFunctionBodyNode exprFuncBodyNode = (ExpressionFunctionBodyNode) funcBodyNode; expr = exprFuncBodyNode.expression(); } + Optional returnTypeSymbol = semanticModel.symbol(node) + .filter(s -> s.kind() == SymbolKind.FUNCTION) + .flatMap(s -> ((FunctionSymbol) s).typeDescriptor().returnTypeDescriptor()); + if (returnTypeSymbol.isPresent() && + returnTypeSymbol.get().typeKind() == TypeDescKind.TUPLE) { + targetTypeSymbol = returnTypeSymbol.get(); + } + } if (expr != null) { @@ -1484,13 +1538,26 @@ public JsonElement getSource(Path filePath, JsonElement cd, JsonElement mp, Stri expr = ((LetExpressionNode) expr).expression(); } String output = mapping.output(); - String[] splits = output.split(DOT); + // Normalize bracket notation to dot notation (e.g., result[0] -> result.0) + String normalizedOutput = output.replaceAll("\\[(\\d+)]", ".$1"); + String[] splits = normalizedOutput.split(DOT); StringBuilder sb = new StringBuilder(); MatchingNode targetMappingExpr = getTargetMappingExpr(expr, targetField); if (targetMappingExpr != null) { expr = targetMappingExpr.expr(); } - genSource(expr, splits, 1, sb, mapping.expression(), null, textEdits); + // Get resolved type for targetField navigation (needed for tuple field mappings) + if (targetField != null) { + TargetNode targetNode = getTargetNode(node, targetField, semanticModel); + if (targetNode != null) { + targetTypeSymbol = targetNode.typeSymbol; + TypeSymbol rawType = CommonUtils.getRawType(targetTypeSymbol); + if (rawType.typeKind() == TypeDescKind.ARRAY) { + targetTypeSymbol = CommonUtils.getRawType(((ArrayTypeSymbol) rawType).memberTypeDescriptor()); + } + } + } + genSource(expr, splits, 1, sb, mapping.expression(), null, textEdits, targetTypeSymbol); } setImportStatements(mapping.imports(), textEdits); @@ -1507,7 +1574,9 @@ public JsonElement deleteMapping(SemanticModel semanticModel, Path filePath, Jso List textEdits = new ArrayList<>(); textEditsMap.put(filePath, textEdits); String output = mapping.output(); - String[] splits = output.split(DOT); + // Normalize bracket notation to dot notation (e.g., data[0] -> data.0) + String normalizedOutput = output.replaceAll("\\[(\\d+)]", ".$1"); + String[] splits = normalizedOutput.split(DOT); if (targetNode != null && targetNode.matchingNode != null && targetNode.matchingNode.expr() != null) { ExpressionNode expr = targetNode.matchingNode.expr(); genDeleteMappingSource(semanticModel, expr, splits, 1, textEdits, targetNode.typeSymbol); @@ -1539,22 +1608,33 @@ private ExpressionNode getMappingExpr(Node node) { private void genSource(ExpressionNode expr, String[] names, int idx, StringBuilder stringBuilder, - String mappingExpr, LinePosition position, List textEdits) { + String mappingExpr, LinePosition position, List textEdits, + TypeSymbol targetTypeSymbol) { if (idx == names.length) { textEdits.add(new TextEdit(CommonUtils.toRange(expr.lineRange()), mappingExpr)); } else if (expr == null) { String name = names[idx]; if (name.matches("\\d+")) { - stringBuilder.append(mappingExpr); + // Check if we're creating a tuple literal + TypeSymbol rawType = targetTypeSymbol != null ? CommonUtils.getRawType(targetTypeSymbol) : null; + if (rawType != null && rawType.typeKind() == TypeDescKind.TUPLE) { + int index; + try { + index = Integer.parseInt(name); + } catch (NumberFormatException e) { + stringBuilder.append(mappingExpr); + textEdits.add(new TextEdit(CommonUtils.toRange(position), stringBuilder.toString())); + return; + } + stringBuilder.append(generateTupleExpression((TupleTypeSymbol) rawType, index, mappingExpr, + names, idx)); + } else { + stringBuilder.append(mappingExpr); + } } else { stringBuilder.append(name).append(": "); - for (int i = idx + 1; i < names.length; i++) { - stringBuilder.append("{").append(names[i]).append(": "); - } - stringBuilder.append(mappingExpr); - for (int i = idx + 1; i < names.length; i++) { - stringBuilder.append("}"); - } + String nestedExpr = generateNestedExpression(names, idx + 1, mappingExpr, targetTypeSymbol); + stringBuilder.append(nestedExpr); } textEdits.add(new TextEdit(CommonUtils.toRange(position), stringBuilder.toString())); } else if (expr.kind() == SyntaxKind.MAPPING_CONSTRUCTOR) { @@ -1562,6 +1642,7 @@ private void genSource(ExpressionNode expr, String[] names, int idx, StringBuild MappingConstructorExpressionNode mappingCtrExpr = (MappingConstructorExpressionNode) expr; Map mappingFields = convertMappingFieldsToMap(mappingCtrExpr); SpecificFieldNode mappingFieldNode = mappingFields.get(name); + TypeSymbol fieldType = getFieldType(targetTypeSymbol, name); if (mappingFieldNode == null) { LinePosition insertPosition; if (!mappingFields.isEmpty()) { @@ -1571,17 +1652,25 @@ private void genSource(ExpressionNode expr, String[] names, int idx, StringBuild } else { insertPosition = mappingCtrExpr.closeBrace().lineRange().startLine(); } - genSource(null, names, idx, stringBuilder, mappingExpr, insertPosition, textEdits); + genSource(null, names, idx, stringBuilder, mappingExpr, insertPosition, textEdits, fieldType); } else { genSource(mappingFieldNode.valueExpr().orElseThrow(), names, idx + 1, stringBuilder, mappingExpr, - null, textEdits); + null, textEdits, fieldType); } } else if (expr.kind() == SyntaxKind.LIST_CONSTRUCTOR) { ListConstructorExpressionNode listCtrExpr = (ListConstructorExpressionNode) expr; String name = names[idx]; if (name.matches("\\d+")) { int index = Integer.parseInt(name); - if (index >= listCtrExpr.expressions().size()) { + TypeSymbol rawType = targetTypeSymbol != null ? CommonUtils.getRawType(targetTypeSymbol) : null; + + // Handle tuple types with empty constructor + if (rawType != null && rawType.typeKind() == TypeDescKind.TUPLE + && listCtrExpr.expressions().isEmpty()) { + String tupleExpr = generateTupleExpression((TupleTypeSymbol) rawType, index, mappingExpr, + names, idx); + textEdits.add(new TextEdit(CommonUtils.toRange(listCtrExpr.lineRange()), tupleExpr)); + } else if (index >= listCtrExpr.expressions().size()) { if (idx > 0) { stringBuilder.append(", "); } @@ -1592,10 +1681,10 @@ private void genSource(ExpressionNode expr, String[] names, int idx, StringBuild } else { insertPosition = listCtrExpr.closeBracket().lineRange().startLine(); } - genSource(null, names, idx, stringBuilder, mappingExpr, insertPosition, textEdits); + genSource(null, names, idx, stringBuilder, mappingExpr, insertPosition, textEdits, null); } else { genSource((ExpressionNode) listCtrExpr.expressions().get(index), names, idx + 1, stringBuilder, - mappingExpr, null, textEdits); + mappingExpr, null, textEdits, null); } } } else if (expr.kind() == SyntaxKind.QUERY_EXPRESSION) { @@ -1605,8 +1694,102 @@ private void genSource(ExpressionNode expr, String[] names, int idx, StringBuild } else { expr = ((CollectClauseNode) clauseNode).expression(); } - genSource(expr, names, idx, stringBuilder, mappingExpr, position, textEdits); + genSource(expr, names, idx, stringBuilder, mappingExpr, position, textEdits, targetTypeSymbol); + } + } + + private String generateTupleExpression(TupleTypeSymbol tupleType, int mappedIndex, String mappingExpr, + String[] names, int nameIdx) { + List memberTypes = tupleType.memberTypeDescriptors(); + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < memberTypes.size(); i++) { + if (i > 0) { + sb.append(", "); + } + if (i == mappedIndex) { + // Check if there are more field names after the index (nested field access) + if (nameIdx + 1 < names.length) { + // Generate nested expression for the tuple member + TypeSymbol memberType = memberTypes.get(i); + sb.append(generateNestedExpression(names, nameIdx + 1, mappingExpr, memberType)); + } else { + sb.append(mappingExpr); + } + } else { + sb.append(getDefaultValue(memberTypes.get(i))); + } + } + sb.append("]"); + return sb.toString(); + } + + private String getDefaultValue(TypeSymbol typeSymbol) { + TypeSymbol rawType = CommonUtils.getRawType(typeSymbol); + TypeDescKind kind = rawType.typeKind(); + return switch (kind) { + case INT, BYTE -> "0"; + case FLOAT -> "0.0"; + case DECIMAL -> "0d"; + case STRING, STRING_CHAR -> "\"\""; + case BOOLEAN -> "false"; + case NIL -> "()"; + case ARRAY -> "[]"; + case MAP, RECORD -> "{}"; + default -> "()"; + }; + } + + private TypeSymbol getFieldType(TypeSymbol parentType, String fieldName) { + if (parentType == null) { + return null; + } + TypeSymbol rawType = CommonUtils.getRawType(parentType); + if (rawType.typeKind() == TypeDescKind.RECORD) { + RecordFieldSymbol fieldSymbol = ((RecordTypeSymbol) rawType).fieldDescriptors().get(fieldName); + return fieldSymbol != null ? fieldSymbol.typeDescriptor() : null; + } + return null; + } + + private String generateNestedExpression(String[] names, int startIdx, String mappingExpr, TypeSymbol typeSymbol) { + StringBuilder sb = new StringBuilder(); + TypeSymbol currentType = typeSymbol; + + for (int i = startIdx; i < names.length; i++) { + String name = names[i]; + if (name.matches("\\d+")) { + // This is a tuple/array index + TypeSymbol rawType = currentType != null ? CommonUtils.getRawType(currentType) : null; + if (rawType != null && rawType.typeKind() == TypeDescKind.TUPLE) { + try { + int index = Integer.parseInt(name); + sb.append(generateTupleExpression((TupleTypeSymbol) rawType, index, mappingExpr, names, i)); + return sb.toString(); + } catch (NumberFormatException e) { + // Fallback: treat as a regular array access and just append the expression + sb.append(mappingExpr); + return sb.toString(); + } + } else { + // Regular array - just append the expression + sb.append(mappingExpr); + return sb.toString(); + } + } else { + // This is a record field + sb.append("{").append(name).append(": "); + currentType = getFieldType(currentType, name); + } } + + // If we get here, append the mapping expression and close braces + sb.append(mappingExpr); + for (int i = startIdx; i < names.length; i++) { + if (!names[i].matches("\\d+")) { + sb.append("}"); + } + } + return sb.toString(); } private void genDeleteMappingSource(SemanticModel semanticModel, ExpressionNode expr, String[] names, int idx, @@ -1710,7 +1893,38 @@ private void genDeleteMappingSource(SemanticModel semanticModel, ExpressionNode } } - if (expressions.size() == 1) { + // Check if this is a tuple type - if so, replace with default value instead of deleting + // First try semantic model, then fall back to targetSymbol + boolean isTuple = false; + TypeSymbol tupleTypeSymbol = null; + + // Try semantic model first + Optional semanticType = semanticModel.typeOf(listCtrExpr); + if (semanticType.isPresent()) { + TypeSymbol rawType = CommonUtils.getRawType(semanticType.get()); + if (rawType.typeKind() == TypeDescKind.TUPLE) { + isTuple = true; + tupleTypeSymbol = rawType; + } + } + + // Fall back to targetSymbol if semantic model didn't work + if (!isTuple && targetSymbol != null) { + TypeSymbol rawType = CommonUtils.getRawType(targetSymbol); + if (rawType.typeKind() == TypeDescKind.TUPLE) { + isTuple = true; + tupleTypeSymbol = rawType; + } + } + + if (isTuple && tupleTypeSymbol != null) { + // For tuples, replace with default value for the member type + List memberTypes = ((TupleTypeSymbol) tupleTypeSymbol).memberTypeDescriptors(); + if (memberIdx < memberTypes.size()) { + String defaultVal = getDefaultValue(memberTypes.get(memberIdx)); + textEdits.add(new TextEdit(CommonUtils.toRange(expr.lineRange()), defaultVal)); + } + } else if (expressions.size() == 1) { textEdits.add(new TextEdit(CommonUtils.toRange(expr.lineRange()), "")); } else { if (memberIdx + 1 == expressions.size()) { @@ -1777,8 +1991,10 @@ private void genDeleteMappingSource(SemanticModel semanticModel, ExpressionNode Map mappingFields = convertMappingFieldsToMap(mappingCtrExpr); SpecificFieldNode mappingFieldNode = mappingFields.get(name); if (mappingFieldNode != null) { + // Get the field type for the next level + TypeSymbol fieldType = getFieldType(targetSymbol, name); genDeleteMappingSource(semanticModel, mappingFieldNode.valueExpr().orElseThrow(), names, idx + 1, - textEdits, targetSymbol); + textEdits, fieldType); } } else if (expr.kind() == SyntaxKind.LIST_CONSTRUCTOR) { ListConstructorExpressionNode listCtrExpr = (ListConstructorExpressionNode) expr; @@ -1786,6 +2002,8 @@ private void genDeleteMappingSource(SemanticModel semanticModel, ExpressionNode if (name.matches("\\d+")) { int index = Integer.parseInt(name); if (index < listCtrExpr.expressions().size()) { + // For tuples, pass the tuple type (not member type) so the base case can determine + // if it needs to replace with default value genDeleteMappingSource(semanticModel, (ExpressionNode) listCtrExpr.expressions().get(index), names, idx + 1, textEdits, targetSymbol); } @@ -1986,13 +2204,13 @@ public JsonElement getQuery(SemanticModel semanticModel, JsonElement cd, JsonEle if (clauseType.equals("collect")) { String query = getQuerySource(mapping.expression(), "collect", targetTypeSymbol); - genSource(expr, mapping.output().split(DOT), 1, new StringBuilder(), query, null, textEdits); + genSource(expr, mapping.output().split(DOT), 1, new StringBuilder(), query, null, textEdits, null); } else { if (targetTypeSymbol.typeKind() == TypeDescKind.ARRAY) { TypeSymbol typeSymbol = CommonUtils.getRawType(((ArrayTypeSymbol) targetTypeSymbol).memberTypeDescriptor()); String query = getQuerySource(mapping.expression(), "select", typeSymbol); - genSource(expr, mapping.output().split(DOT), 1, new StringBuilder(), query, null, textEdits); + genSource(expr, mapping.output().split(DOT), 1, new StringBuilder(), query, null, textEdits, null); } } return gson.toJsonTree(textEditsMap); @@ -2617,7 +2835,7 @@ public JsonElement genMappingFunction(WorkspaceManager workspaceManager, Semanti filePath, functionMetadata, textEditsMap, semanticModel, isCustomFunction); List textEdits = new ArrayList<>(); genSource(targetNode.matchingNode().expr(), mapping.output().split(DOT), 1, new StringBuilder(), - functionName + "(" + mapping.expression() + ")", null, textEdits); + functionName + "(" + mapping.expression() + ")", null, textEdits, null); textEditsMap.computeIfAbsent(filePath, k -> new ArrayList<>()) .addAll(textEdits); return gson.toJsonTree(textEditsMap); @@ -3201,14 +3419,27 @@ public record TypeInfo(String orgName, String moduleName) { private static class GenInputsVisitor extends NodeVisitor { private final List inputs; private final List enumPorts; + private final SemanticModel semanticModel; - GenInputsVisitor(List inputs, List enumPorts) { + GenInputsVisitor(List inputs, List enumPorts, + SemanticModel semanticModel) { this.inputs = inputs; this.enumPorts = enumPorts; + this.semanticModel = semanticModel; } @Override public void visit(FieldAccessExpressionNode node) { + ExpressionNode baseExpr = node.expression(); + // Check if the base expression involves a tuple indexed access + if (baseExpr.kind() == SyntaxKind.INDEXED_EXPRESSION) { + IndexedExpressionNode indexedExpr = (IndexedExpressionNode) baseExpr; + if (isTupleType(indexedExpr.containerExpression())) { + // For tuple field access like input[2].id, preserve the full path + addInput(node.toSourceCode().trim()); + return; + } + } String source = node.toSourceCode().trim(); String[] split = source.split("\\["); if (split.length > 1) { @@ -3262,6 +3493,12 @@ public void visit(IndexedExpressionNode node) { ExpressionNode containerExpr = node.containerExpression(); SyntaxKind containerKind = containerExpr.kind(); + if (isTupleType(containerExpr)) { + // For tuple element access, use the full indexed expression + addInput(node.toSourceCode().trim()); + return; + } + if (containerKind == SyntaxKind.FIELD_ACCESS || containerKind == SyntaxKind.INDEXED_EXPRESSION) { String source = node.toSourceCode().trim(); String openBraceRemoved = source.replace("[", "."); @@ -3278,6 +3515,15 @@ public void visit(IndexedExpressionNode node) { } } + private boolean isTupleType(ExpressionNode expr) { + if (semanticModel == null) { + return false; + } + return semanticModel.typeOf(expr) + .map(typeSymbol -> CommonUtils.getRawType(typeSymbol).typeKind() == TypeDescKind.TUPLE) + .orElse(false); + } + @Override public void visit(QueryExpressionNode node) { node.queryPipeline().fromClause().accept(this); @@ -3336,15 +3582,26 @@ private void addInput(String input) { private static class ArrayIndexExtractorVisitor extends NodeVisitor { private final List indices; + private final SemanticModel semanticModel; - ArrayIndexExtractorVisitor(List indices) { + ArrayIndexExtractorVisitor(List indices, SemanticModel semanticModel) { this.indices = indices; + this.semanticModel = semanticModel; } @Override public void visit(IndexedExpressionNode node) { node.containerExpression().accept(this); + // Skip tuple member access - only include array indices + Optional containerType = semanticModel.typeOf(node.containerExpression()); + if (containerType.isPresent()) { + TypeSymbol rawType = CommonUtils.getRawType(containerType.get()); + if (rawType.typeKind() == TypeDescKind.TUPLE) { + return; + } + } + SeparatedNodeList keyExpressions = node.keyExpression(); for (ExpressionNode keyExpr : keyExpressions) { String indexValue = extractIndexValue(keyExpr); diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/DataMapperService.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/DataMapperService.java index aac65f9d3d..adfa404c29 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/DataMapperService.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/DataMapperService.java @@ -142,14 +142,15 @@ public CompletableFuture getSource(DataMapperSourceReq try { Path filePath = Path.of(request.filePath()); this.workspaceManager.loadProject(filePath); + Optional semanticModel = this.workspaceManager.semanticModel(filePath); Optional document = this.workspaceManager.document(filePath); - if (document.isEmpty()) { + if (document.isEmpty() || semanticModel.isEmpty()) { return response; } DataMapManager dataMapManager = new DataMapManager(document.get()); - response.setTextEdits(dataMapManager.getSource(filePath, request.codedata(), request.mapping(), - request.targetField())); + response.setTextEdits(dataMapManager.getSource(semanticModel.get(), filePath, request.codedata(), + request.mapping(), request.targetField())); } catch (Throwable e) { response.setError(e); } diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingDeleteTest.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingDeleteTest.java index cc0f0e41db..eefe541dcd 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingDeleteTest.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingDeleteTest.java @@ -68,8 +68,8 @@ protected Object[] getConfigsList() { {Path.of("variable10.json")}, {Path.of("variable11.json")}, {Path.of("query_collect.json")}, - {Path.of("query_collect2.json")} - + {Path.of("query_collect2.json")}, + {Path.of("tuple_member.json")} }; } diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingModelTest.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingModelTest.java index 63892132de..1e0c1fb78b 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingModelTest.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingModelTest.java @@ -163,6 +163,9 @@ protected Object[] getConfigsList() { {Path.of("query29.json")}, {Path.of("variable58.json")}, {Path.of("variable59.json")}, + {Path.of("tuple.json")}, + {Path.of("tuple_based_transformation.json")}, + {Path.of("tuple_based_transformation2.json")}, }; } diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingSourceTest.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingSourceTest.java index df1a1133ed..e839a1d664 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingSourceTest.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/DataMappingSourceTest.java @@ -105,6 +105,10 @@ protected Object[] getConfigsList() { {Path.of("query7.json")}, {Path.of("query8.json")}, {Path.of("variable20.json")}, + {Path.of("tuple_mapping.json")}, + {Path.of("tuple_mapping2.json")}, + {Path.of("tuple_transformation.json")}, + {Path.of("tuple_based_query.json")} }; } diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/tuple.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/tuple.json new file mode 100644 index 0000000000..a7f0baef10 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/tuple.json @@ -0,0 +1,125 @@ +{ + "source": "variable50.bal", + "description": "Sample diagram node", + "codedata": { + "node": "VARIABLE", + "lineRange": { + "fileName": "variable50.bal", + "startLine": { + "line": 17, + "offset": 8 + }, + "endLine": { + "line": 17, + "offset": 39 + } + }, + "sourceCode": "Bar[] bars = [\n {a: 1, b: 3},\n (from var baz in bazs\n select {a: baz.x, b: baz.y})[0]\n ];" + }, + "position": { + "line": 17, + "offset": 8 + }, + "targetField": "person", + "model": { + "inputs": [ + { + "members": [ + { + "name": "[0]", + "displayName": "data[0]", + "typeName": "int", + "kind": "int" + }, + { + "name": "[1]", + "displayName": "data[1]", + "typeName": "string", + "kind": "string" + }, + { + "name": "[2]", + "displayName": "data[2]", + "typeName": "float", + "kind": "float" + } + ], + "name": "data", + "displayName": "data", + "typeName": "[int, string, float]", + "kind": "tuple", + "category": "local-variable" + } + ], + "output": { + "fields": [], + "name": "person", + "displayName": "person", + "typeName": "Person", + "kind": "record", + "ref": "887620673" + }, + "mappings": [ + { + "output": "person.age", + "inputs": [ + "data[0]" + ], + "expression": "data[0]", + "diagnostics": [], + "elements": [], + "isQueryExpression": false, + "isFunctionCall": false + } + ], + "refs": { + "887620673": { + "fields": [ + { + "name": "name", + "displayName": "name", + "typeName": "string", + "kind": "string", + "optional": false + }, + { + "name": "age", + "displayName": "age", + "typeName": "int", + "kind": "int", + "optional": false + }, + { + "members": [ + { + "name": "[0]", + "displayName": "data[0]", + "typeName": "int", + "kind": "int" + }, + { + "name": "[1]", + "displayName": "data[1]", + "typeName": "string", + "kind": "string" + }, + { + "name": "[2]", + "displayName": "data[2]", + "typeName": "float", + "kind": "float" + } + ], + "name": "data", + "displayName": "data", + "typeName": "[int, string, float]", + "kind": "tuple", + "optional": false + } + ], + "typeName": "Person", + "kind": "record" + } + } + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/tuple_based_transformation.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/tuple_based_transformation.json new file mode 100644 index 0000000000..e68293d48c --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/tuple_based_transformation.json @@ -0,0 +1,185 @@ +{ + "source": "tupleBasedTransformation.bal", + "description": "Sample diagram node", + "codedata": { + "node": "VARIABLE", + "lineRange": { + "fileName": "tupleBasedTransformation.bal", + "startLine": { + "line": 28, + "offset": 0 + }, + "endLine": { + "line": 28, + "offset": 123 + } + }, + "sourceCode": "Bar[] bars = [\n {a: 1, b: 3},\n (from var baz in bazs\n select {a: baz.x, b: baz.y})[0]\n ];" + }, + "position": { + "line": 28, + "offset": 91 + }, + "targetField": "transform1", + "model": { + "inputs": [ + { + "fields": [], + "name": "course", + "displayName": "course", + "typeName": "Course", + "kind": "record", + "category": "parameter", + "ref": "524765447" + }, + { + "fields": [], + "name": "person", + "displayName": "person", + "typeName": "DetailedPerson", + "kind": "record", + "category": "parameter", + "ref": "-692431599" + } + ], + "output": { + "members": [ + { + "name": "[0]", + "displayName": "transform1[0]", + "typeName": "int", + "kind": "int" + }, + { + "name": "[1]", + "displayName": "transform1[1]", + "typeName": "string", + "kind": "string" + }, + { + "fields": [], + "name": "[2]", + "displayName": "transform1[2]", + "typeName": "Course", + "kind": "record", + "ref": "524765447" + } + ], + "name": "transform1", + "displayName": "transform1", + "typeName": "[int, string, Course]", + "kind": "tuple" + }, + "mappings": [ + { + "output": "transform1", + "inputs": [], + "expression": "[0, person.id, {id: person.id}]", + "diagnostics": [ + { + "message": "missing non-defaultable required record field 'credits'", + "code": "BCE2520" + }, + { + "message": "missing non-defaultable required record field 'name'", + "code": "BCE2520" + } + ], + "elements": [ + { + "mappings": [ + { + "output": "transform1[0]", + "inputs": [], + "expression": "0", + "diagnostics": [], + "elements": [], + "isQueryExpression": false, + "isFunctionCall": false + } + ] + }, + { + "mappings": [ + { + "output": "transform1[1]", + "inputs": [ + "person.id" + ], + "expression": "person.id", + "diagnostics": [], + "elements": [], + "isQueryExpression": false, + "isFunctionCall": false + } + ] + }, + { + "mappings": [ + { + "output": "transform1[2].id", + "inputs": [ + "person.id" + ], + "expression": "person.id", + "diagnostics": [], + "elements": [], + "isQueryExpression": false, + "isFunctionCall": false + } + ] + } + ] + } + ], + "refs": { + "-692431599": { + "fields": [ + { + "name": "id", + "displayName": "id", + "typeName": "string", + "kind": "string", + "optional": false + }, + { + "name": "name", + "displayName": "name", + "typeName": "string", + "kind": "string", + "optional": false + } + ], + "typeName": "DetailedPerson", + "kind": "record" + }, + "524765447": { + "fields": [ + { + "name": "id", + "displayName": "id", + "typeName": "string", + "kind": "string", + "optional": false + }, + { + "name": "name", + "displayName": "name", + "typeName": "string", + "kind": "string", + "optional": false + }, + { + "name": "credits", + "displayName": "credits", + "typeName": "float", + "kind": "float", + "optional": false + } + ], + "typeName": "Course", + "kind": "record" + } + } + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/tuple_based_transformation2.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/tuple_based_transformation2.json new file mode 100644 index 0000000000..65138ec41b --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/tuple_based_transformation2.json @@ -0,0 +1,174 @@ +{ + "source": "tupleBasedTransformation2.bal", + "description": "Sample diagram node", + "codedata": { + "node": "VARIABLE", + "lineRange": { + "fileName": "tupleBasedTransformation2.bal", + "startLine": { + "line": 28, + "offset": 0 + }, + "endLine": { + "line": 28, + "offset": 109 + } + }, + "sourceCode": "Bar[] bars = [\n {a: 1, b: 3},\n (from var baz in bazs\n select {a: baz.x, b: baz.y})[0]\n ];" + }, + "position": { + "line": 28, + "offset": 82 + }, + "targetField": "transform1", + "model": { + "inputs": [ + { + "members": [ + { + "name": "[0]", + "displayName": "input[0]", + "typeName": "int", + "kind": "int" + }, + { + "name": "[1]", + "displayName": "input[1]", + "typeName": "string", + "kind": "string" + }, + { + "fields": [], + "name": "[2]", + "displayName": "input[2]", + "typeName": "Course", + "kind": "record", + "ref": "524765447" + } + ], + "name": "input", + "displayName": "input", + "typeName": "[int, string, Course]", + "kind": "tuple", + "category": "parameter" + } + ], + "output": { + "members": [ + { + "name": "[0]", + "displayName": "transform1[0]", + "typeName": "int", + "kind": "int" + }, + { + "name": "[1]", + "displayName": "transform1[1]", + "typeName": "string", + "kind": "string" + }, + { + "fields": [], + "name": "[2]", + "displayName": "transform1[2]", + "typeName": "Course", + "kind": "record", + "ref": "524765447" + } + ], + "name": "transform1", + "displayName": "transform1", + "typeName": "[int, string, Course]", + "kind": "tuple" + }, + "mappings": [ + { + "output": "transform1", + "inputs": [], + "expression": "[0, \"\", {id: input[2].id}]", + "diagnostics": [ + { + "message": "missing non-defaultable required record field 'credits'", + "code": "BCE2520" + }, + { + "message": "missing non-defaultable required record field 'name'", + "code": "BCE2520" + } + ], + "elements": [ + { + "mappings": [ + { + "output": "transform1[0]", + "inputs": [], + "expression": "0", + "diagnostics": [], + "elements": [], + "isQueryExpression": false, + "isFunctionCall": false + } + ] + }, + { + "mappings": [ + { + "output": "transform1[1]", + "inputs": [], + "expression": "\"\"", + "diagnostics": [], + "elements": [], + "isQueryExpression": false, + "isFunctionCall": false + } + ] + }, + { + "mappings": [ + { + "output": "transform1[2].id", + "inputs": [ + "input[2].id" + ], + "expression": "input[2].id", + "diagnostics": [], + "elements": [], + "isQueryExpression": false, + "isFunctionCall": false + } + ] + } + ] + } + ], + "refs": { + "524765447": { + "fields": [ + { + "name": "id", + "displayName": "id", + "typeName": "string", + "kind": "string", + "optional": false + }, + { + "name": "name", + "displayName": "name", + "typeName": "string", + "kind": "string", + "optional": false + }, + { + "name": "credits", + "displayName": "credits", + "typeName": "float", + "kind": "float", + "optional": false + } + ], + "typeName": "Course", + "kind": "record" + } + } + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/variable54.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/variable54.json index 04bf11c203..84e2d423e3 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/variable54.json +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/config/variable54.json @@ -27,16 +27,16 @@ "members": [ { "fields": [], - "name": "a", - "displayName": "a", + "name": "[0]", + "displayName": "a[0]", "typeName": "Student", "kind": "record", "ref": "545732303" }, { "fields": [], - "name": "a", - "displayName": "a", + "name": "[1]", + "displayName": "a[1]", "typeName": "Person", "kind": "record", "ref": "887620673" diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/source/tupleBasedTransformation.bal b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/source/tupleBasedTransformation.bal new file mode 100644 index 0000000000..2c6987ec6f --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/source/tupleBasedTransformation.bal @@ -0,0 +1,29 @@ +type Student record { + string id; + string name; +}; + +type DetailedPerson record { + string id; + string name; +}; + +type CourseDetails record { + string courseId; + string courseName; + decimal credits; + string grade; +}; + +type DetailedStudent record { + Course[] courses; +}; + +type Course record { + string id; + string name; + float credits; +}; + +// Case 01 +function transform1(DetailedPerson person, Course course) returns [int, string, Course] => [0, person.id, {id: person.id}]; diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/source/tupleBasedTransformation2.bal b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/source/tupleBasedTransformation2.bal new file mode 100644 index 0000000000..26a3bb99ba --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/source/tupleBasedTransformation2.bal @@ -0,0 +1,29 @@ +type Student record { + string id; + string name; +}; + +type DetailedPerson record { + string id; + string name; +}; + +type CourseDetails record { + string courseId; + string courseName; + decimal credits; + string grade; +}; + +type DetailedStudent record { + Course[] courses; +}; + +type Course record { + string id; + string name; + float credits; +}; + +// Case 01 +function transform1([int, string, Course] input) returns [int, string, Course] => [0, "", {id: input[2].id}]; diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/source/variable50.bal b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/source/variable50.bal new file mode 100644 index 0000000000..0c3035745b --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_model/source/variable50.bal @@ -0,0 +1,23 @@ +import ballerina/log; + +public type Person record { + string name; + int age; + [int, string, float] data; +}; + +public function main() returns error? { + do { + // [int, string, float] data = [1, "Alice", 99.5]; + // [int, string, float] result = []; + // int[] numbers = [10, 20, 30]; + // Person person = {name: "Alice" + data[1], age: 30}; + // Person updatedPErson = {age: numbers[1]}; + + [int, string, float] data = [1, "Alice", 99.5]; + Person person = {age: data[0]}; + } on fail error e { + log:printError("Error occurred", 'error = e); + return e; + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_based_query.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_based_query.json new file mode 100644 index 0000000000..ac012c58b7 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_based_query.json @@ -0,0 +1,41 @@ +{ + "source": "tupleBasedQuery.bal", + "description": "Tuple mapping", + "codedata": { + "node": "VARIABLE", + "lineRange": { + "fileName": "tupleBasedQuery.bal", + "startLine": { + "line": 14, + "offset": 8 + }, + "endLine": { + "line": 15, + "offset": 22 + } + }, + "sourceCode": "[int, string, float] result = [];" + }, + "mapping": { + "output": "persons2.data[0]", + "expression": "personsItem.data[0]" + }, + "targetField": "persons2.0", + "output": { + "tupleBasedQuery.bal": [ + { + "range": { + "start": { + "line": 15, + "character": 20 + }, + "end": { + "line": 15, + "character": 20 + } + }, + "newText": "data: [personsItem.data[0], \"\", 0.0]" + } + ] + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_mapping.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_mapping.json new file mode 100644 index 0000000000..e29429f2c8 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_mapping.json @@ -0,0 +1,41 @@ +{ + "source": "tupleMapping.bal", + "description": "Tuple mapping", + "codedata": { + "node": "VARIABLE", + "lineRange": { + "fileName": "tupleMapping.bal", + "startLine": { + "line": 13, + "offset": 8 + }, + "endLine": { + "line": 13, + "offset": 41 + } + }, + "sourceCode": "[int, string, float] result = [];" + }, + "mapping": { + "output": "result[0]", + "expression": "data[0]" + }, + "targetField": "result", + "output": { + "tupleMapping.bal": [ + { + "range": { + "start": { + "line": 13, + "character": 38 + }, + "end": { + "line": 13, + "character": 40 + } + }, + "newText": "[data[0], \"\", 0.0]" + } + ] + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_mapping2.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_mapping2.json new file mode 100644 index 0000000000..cdd03e53e6 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_mapping2.json @@ -0,0 +1,41 @@ +{ + "source": "tupleMapping.bal", + "description": "Tuple mapping", + "codedata": { + "node": "VARIABLE", + "lineRange": { + "fileName": "tupleMapping.bal", + "startLine": { + "line": 15, + "offset": 8 + }, + "endLine": { + "line": 15, + "offset": 63 + } + }, + "sourceCode": "[int, string, float] result = [];" + }, + "mapping": { + "output": "person2.data[0]", + "expression": "result[0]" + }, + "targetField": "person2", + "output": { + "tupleMapping.bal": [ + { + "range": { + "start": { + "line": 15, + "character": 61 + }, + "end": { + "line": 15, + "character": 61 + } + }, + "newText": ", data: [result[0], \"\", 0.0]" + } + ] + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_transformation.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_transformation.json new file mode 100644 index 0000000000..8aeb7348ba --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/config/tuple_transformation.json @@ -0,0 +1,41 @@ +{ + "source": "tupleTransformation.bal", + "description": "Tuple transformation mapping", + "codedata": { + "node": "VARIABLE", + "lineRange": { + "fileName": "tupleTransformation.bal", + "startLine": { + "line": 28, + "offset": 0 + }, + "endLine": { + "line": 28, + "offset": 94 + } + }, + "sourceCode": "[int, string, float] result = [];" + }, + "mapping": { + "output": "transform1[2].id", + "expression": "course.id" + }, + "targetField": "transform1", + "output": { + "tupleTransformation.bal": [ + { + "range": { + "start": { + "line": 28, + "character": 91 + }, + "end": { + "line": 28, + "character": 93 + } + }, + "newText": "[0, \"\", {id: course.id}]" + } + ] + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/source/tupleBasedQuery.bal b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/source/tupleBasedQuery.bal new file mode 100644 index 0000000000..abcf231ef1 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/source/tupleBasedQuery.bal @@ -0,0 +1,26 @@ +import ballerina/log; + +type TupleType [int, string, float]; + +public type Person record { + string name; + int age; + TupleType data; +}; + +public function main() returns error? { + do { + [int, string, float] data = [1, "Alice", 99.5]; + Person[] persons = []; + Person[] persons2 = from var personsItem in persons + select {}; + [int, string, float][] result = []; //Case 01 + [int, string, float][] result2 = from var resultItem in result + select [0, "", 0.0]; //Case 01 + Person person1 = {name: "Bob", age: 30, data: data}; + Person person2 = persons[0]; //Case 02 + } on fail error e { + log:printError("Error occurred", 'error = e); + return e; + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/source/tupleMapping.bal b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/source/tupleMapping.bal new file mode 100644 index 0000000000..2474e9640b --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/source/tupleMapping.bal @@ -0,0 +1,21 @@ +import ballerina/log; + +type TupleType [int, string, float]; + +public type Person record { + string name; + int age; + TupleType data; +}; + +public function main() returns error? { + do { + [int, string, float] data = [1, "Alice", 99.5]; + [int, string, float] result = []; + Person person1 = {name: "Bob", age: 30, data: data}; + Person person2 = {age: data[0], name: person1.data[1]}; //Case 02 + } on fail error e { + log:printError("Error occurred", 'error = e); + return e; + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/source/tupleTransformation.bal b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/source/tupleTransformation.bal new file mode 100644 index 0000000000..13e8c7e3b7 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/data_mapper_source/source/tupleTransformation.bal @@ -0,0 +1,29 @@ +type Student record { + string id; + string name; +}; + +type DetailedPerson record { + string id; + string name; +}; + +type CourseDetails record { + string courseId; + string courseName; + decimal credits; + string grade; +}; + +type DetailedStudent record { + Course[] courses; +}; + +type Course record { + string id; + string name; + float credits; +}; + +// Case 01 +function transform1(DetailedPerson person, Course course) returns [int, string, Course] => []; diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/delete_mapping/config/tuple_member.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/delete_mapping/config/tuple_member.json new file mode 100644 index 0000000000..8cfa47f441 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/delete_mapping/config/tuple_member.json @@ -0,0 +1,50 @@ +{ + "source": "tupleMember.bal", + "description": "", + "codedata": { + "node": "VARIABLE", + "lineRange": { + "fileName": "tupleMember.bal", + "startLine": { + "line": 15, + "offset": 8 + }, + "endLine": { + "line": 15, + "offset": 54 + } + } + }, + "mapping": { + "output": "person2.data[0]", + "inputs": [ + "result[0]" + ], + "expression": "result[0]", + "diagnostics": [], + "elements": [], + "isQueryExpression": false, + "isFunctionCall": false, + "elementAccessIndex": [ + "0" + ] + }, + "targetField": "person2", + "output": { + "tupleMember.bal": [ + { + "range": { + "start": { + "line": 15, + "character": 33 + }, + "end": { + "line": 15, + "character": 42 + } + }, + "newText": "0" + } + ] + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/delete_mapping/source/tupleMember.bal b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/delete_mapping/source/tupleMember.bal new file mode 100644 index 0000000000..99e8bbdc50 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/delete_mapping/source/tupleMember.bal @@ -0,0 +1,21 @@ +import ballerina/log; + +type TupleType [int, string, float]; + +public type Person record { + string name; + int age; + TupleType data; +}; + +public function main() returns error? { + do { + [int, string, float] data = [1, "Alice", 99.5]; + [int, string, float] result = []; //Case 01 + Person person1 = {name: "Bob", age: 30, data: data}; + Person person2 = {data: [result[0], "", 0.0]}; //Case 02 + } on fail error e { + log:printError("Error occurred", 'error = e); + return e; + } +} diff --git a/misc/diagram-util/src/main/java/org/ballerinalang/diagramutil/connector/models/connector/ReferenceType.java b/misc/diagram-util/src/main/java/org/ballerinalang/diagramutil/connector/models/connector/ReferenceType.java index e96d465fbc..a9af5f6213 100644 --- a/misc/diagram-util/src/main/java/org/ballerinalang/diagramutil/connector/models/connector/ReferenceType.java +++ b/misc/diagram-util/src/main/java/org/ballerinalang/diagramutil/connector/models/connector/ReferenceType.java @@ -273,12 +273,20 @@ public static RefType fromSemanticSymbol(TypeSymbol symbol, String name, ModuleI } else if (kind == TypeDescKind.TUPLE) { TupleTypeSymbol typeSymbol = (TupleTypeSymbol) symbol; RefTupleType tupleType = new RefTupleType(name); + StringBuilder tupleNameBuilder = new StringBuilder(); + tupleNameBuilder.append("["); for (TypeSymbol memberTypeSymbol : typeSymbol.memberTypeDescriptors()) { - String memberTypeName = memberTypeSymbol.getName().orElse(""); + String memberTypeName = memberTypeSymbol.getName().orElse(memberTypeSymbol.signature()); ModuleID memberModuleId = getModuleID(memberTypeSymbol, moduleID); RefType refType = fromSemanticSymbol(memberTypeSymbol, memberTypeName, memberModuleId, typeDefSymbols); + if (tupleNameBuilder.length() > 1) { + tupleNameBuilder.append(", ").append(memberTypeName); + } else { + tupleNameBuilder.append(memberTypeName); + } tupleType.memberTypes.add(refType); } + tupleType.name = tupleNameBuilder.append("]").toString(); return tupleType; } else if (kind == TypeDescKind.REGEXP) { return new RefType("regexp:RegExp"); diff --git a/misc/diagram-util/src/test/resources/RefTypeTest/BalProject7/main.bal b/misc/diagram-util/src/test/resources/RefTypeTest/BalProject7/main.bal index ba7c64e1ae..4d78276c9b 100644 --- a/misc/diagram-util/src/test/resources/RefTypeTest/BalProject7/main.bal +++ b/misc/diagram-util/src/test/resources/RefTypeTest/BalProject7/main.bal @@ -11,3 +11,8 @@ public type RecordWithStream record {| string id; SimpleStream data; |}; + +public type PersonWithTuple record {| + string name; + [int, Person] info; +|};