This is an automated email from the ASF dual-hosted git repository. wusheng pushed a commit to branch groovy-replace in repository https://gitbox.apache.org/repos/asf/skywalking.git
commit 0170c109bf64997eb662336f734795bd81613588 Author: Wu Sheng <[email protected]> AuthorDate: Sat Feb 28 14:47:20 2026 +0800 Add MAL transpiler module for build-time Groovy-to-Java conversion (Phase 2) Ports MalToJavaTranspiler from skywalking-graalvm-distro into a new mal-transpiler analyzer submodule. The transpiler parses Groovy MAL expressions/filters via AST at CONVERSION phase and emits equivalent Java classes implementing MalExpression/MalFilter interfaces from Phase 1. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- oap-server/analyzer/{ => mal-transpiler}/pom.xml | 24 +- .../server/transpiler/mal/MalToJavaTranspiler.java | 1099 ++++++++++++++++++++ .../transpiler/mal/MalToJavaTranspilerTest.java | 904 ++++++++++++++++ oap-server/analyzer/pom.xml | 1 + 4 files changed, 2009 insertions(+), 19 deletions(-) diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/mal-transpiler/pom.xml similarity index 67% copy from oap-server/analyzer/pom.xml copy to oap-server/analyzer/mal-transpiler/pom.xml index 9dca94257f..4f45b67acb 100644 --- a/oap-server/analyzer/pom.xml +++ b/oap-server/analyzer/mal-transpiler/pom.xml @@ -19,37 +19,23 @@ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> - <artifactId>oap-server</artifactId> + <artifactId>analyzer</artifactId> <groupId>org.apache.skywalking</groupId> <version>${revision}</version> </parent> <modelVersion>4.0.0</modelVersion> - <artifactId>analyzer</artifactId> - <packaging>pom</packaging> - - <modules> - <module>agent-analyzer</module> - <module>log-analyzer</module> - <module>meter-analyzer</module> - <module>event-analyzer</module> - </modules> + <artifactId>mal-transpiler</artifactId> <dependencies> <dependency> <groupId>org.apache.skywalking</groupId> - <artifactId>apm-network</artifactId> + <artifactId>meter-analyzer</artifactId> <version>${project.version}</version> </dependency> <dependency> - <groupId>org.apache.skywalking</groupId> - <artifactId>library-module</artifactId> - <version>${project.version}</version> - </dependency> - <dependency> - <groupId>org.apache.skywalking</groupId> - <artifactId>library-util</artifactId> - <version>${project.version}</version> + <groupId>org.apache.groovy</groupId> + <artifactId>groovy</artifactId> </dependency> </dependencies> </project> diff --git a/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java b/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java new file mode 100644 index 0000000000..c256bb589c --- /dev/null +++ b/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java @@ -0,0 +1,1099 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 org.apache.skywalking.oap.server.transpiler.mal; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import lombok.extern.slf4j.Slf4j; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.ArgumentListExpression; +import org.codehaus.groovy.ast.expr.BinaryExpression; +import org.codehaus.groovy.ast.expr.ClosureExpression; +import org.codehaus.groovy.ast.expr.ClassExpression; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.DeclarationExpression; +import org.codehaus.groovy.ast.expr.ElvisOperatorExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.ListExpression; +import org.codehaus.groovy.ast.expr.MapEntryExpression; +import org.codehaus.groovy.ast.expr.MapExpression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.expr.PropertyExpression; +import org.codehaus.groovy.ast.expr.TernaryExpression; +import org.codehaus.groovy.ast.expr.TupleExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.syntax.Types; +import org.codehaus.groovy.ast.expr.BooleanExpression; +import org.codehaus.groovy.ast.expr.NotExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.EmptyStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.IfStatement; +import org.codehaus.groovy.ast.stmt.ReturnStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.Phases; + +/** + * Transpiles Groovy MAL expressions to Java source code at build time. + * Parses expression strings into Groovy AST (via CompilationUnit at CONVERSION phase), + * walks AST nodes, and produces equivalent Java classes implementing MalExpression or MalFilter. + * + * <p>Supported AST patterns: + * <ul> + * <li>Variable references: sample family lookups, DownsamplingType constants, KNOWN_TYPES</li> + * <li>Method chains: .sum(), .service(), .tagEqual(), .rate(), .histogram(), etc.</li> + * <li>Binary arithmetic with operand-swap logic per upstream ExpandoMetaClass + * (N-SF: sf.minus(N).negative(), N/SF: sf.newValue(v->N/v))</li> + * <li>tag() closures: TagFunction lambda (assignment, remove, string concat, if/else)</li> + * <li>Filter closures: MalFilter class (==, !=, in, truthiness, negation, &&, ||)</li> + * <li>forEach() closures: ForEachFunction lambda (var decls, if/else-if, early return)</li> + * <li>instance() with PropertiesExtractor closure: Map.of() from map literals</li> + * <li>Elvis (?:), safe navigation (?.), ternary (? :)</li> + * <li>Batch compilation via javax.tools.JavaCompiler + manifest writing</li> + * </ul> + */ +@Slf4j +public class MalToJavaTranspiler { + + static final String GENERATED_PACKAGE = + "org.apache.skywalking.oap.server.core.source.oal.rt.mal"; + + private static final Set<String> DOWNSAMPLING_CONSTANTS = Set.of( + "AVG", "SUM", "LATEST", "SUM_PER_MIN", "MAX", "MIN" + ); + + private static final Set<String> KNOWN_TYPES = Set.of( + "Layer", "DetectPoint", "K8sRetagType", "ProcessRegistry", "TimeUnit" + ); + + // ---- Batch state tracking ---- + + private final Map<String, String> expressionSources = new LinkedHashMap<>(); + + private final Map<String, String> filterSources = new LinkedHashMap<>(); + + private final Map<String, String> filterLiteralToClass = new LinkedHashMap<>(); + + /** + * Transpile a MAL expression to a Java class source implementing MalExpression. + * + * @param className simple class name (e.g. "MalExpr_meter_jvm_heap") + * @param expression the Groovy expression string + * @return generated Java source code + */ + public String transpileExpression(final String className, final String expression) { + final ModuleNode ast = parseToAST(expression); + final Statement body = extractBody(ast); + + final Set<String> sampleNames = new LinkedHashSet<>(); + collectSampleNames(body, sampleNames); + + final String javaBody = visitStatement(body); + + final StringBuilder sb = new StringBuilder(); + sb.append("package ").append(GENERATED_PACKAGE).append(";\n\n"); + sb.append("import java.util.*;\n"); + sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.*;\n"); + sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions.*;\n"); + sb.append("import org.apache.skywalking.oap.server.core.analysis.Layer;\n"); + sb.append("import org.apache.skywalking.oap.server.core.source.DetectPoint;\n"); + sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;\n"); + sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry;\n\n"); + + sb.append("public class ").append(className).append(" implements MalExpression {\n"); + sb.append(" @Override\n"); + sb.append(" public SampleFamily run(Map<String, SampleFamily> samples) {\n"); + + if (!sampleNames.isEmpty()) { + sb.append(" ExpressionParsingContext.get().ifPresent(ctx -> {\n"); + for (String name : sampleNames) { + sb.append(" ctx.getSamples().add(\"").append(escapeJava(name)).append("\");\n"); + } + sb.append(" });\n"); + } + + sb.append(" return ").append(javaBody).append(";\n"); + sb.append(" }\n"); + sb.append("}\n"); + + return sb.toString(); + } + + /** + * Transpile a MAL filter literal to a Java class source implementing MalFilter. + * Filter literals are closures like: { tags -> tags.job_name == 'vm-monitoring' } + * + * @param className simple class name (e.g. "MalFilter_0") + * @param filterLiteral the Groovy closure literal string + * @return generated Java source code + */ + public String transpileFilter(final String className, final String filterLiteral) { + final ModuleNode ast = parseToAST(filterLiteral); + final Statement body = extractBody(ast); + + // The filter literal is a closure expression at the top level + final ClosureExpression closure = extractClosure(body); + final Parameter[] params = closure.getParameters(); + final String tagsVar = (params != null && params.length > 0) ? params[0].getName() : "tags"; + + // Get the body expression — may need to unwrap inner block/closure + final Expression bodyExpr = extractFilterBodyExpr(closure.getCode(), tagsVar); + + // Generate the boolean condition + final String condition = visitFilterCondition(bodyExpr, tagsVar); + + final StringBuilder sb = new StringBuilder(); + sb.append("package ").append(GENERATED_PACKAGE).append(";\n\n"); + sb.append("import java.util.*;\n"); + sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.*;\n\n"); + + sb.append("public class ").append(className).append(" implements MalFilter {\n"); + sb.append(" @Override\n"); + sb.append(" public boolean test(Map<String, String> tags) {\n"); + sb.append(" return ").append(condition).append(";\n"); + sb.append(" }\n"); + sb.append("}\n"); + + return sb.toString(); + } + + private ClosureExpression extractClosure(final Statement body) { + final List<Statement> stmts = getStatements(body); + if (stmts.size() == 1 && stmts.get(0) instanceof ExpressionStatement) { + final Expression expr = ((ExpressionStatement) stmts.get(0)).getExpression(); + if (expr instanceof ClosureExpression) { + return (ClosureExpression) expr; + } + } + throw new IllegalStateException( + "Filter literal must be a single closure expression, got: " + + (stmts.isEmpty() ? "empty" : stmts.get(0).getClass().getSimpleName())); + } + + private Expression extractFilterBodyExpr(final Statement code, final String tagsVar) { + final List<Statement> stmts = getStatements(code); + if (stmts.isEmpty()) { + throw new IllegalStateException("Empty filter closure body"); + } + + final Statement last = stmts.get(stmts.size() - 1); + Expression expr; + if (last instanceof ExpressionStatement) { + expr = ((ExpressionStatement) last).getExpression(); + } else if (last instanceof ReturnStatement) { + expr = ((ReturnStatement) last).getExpression(); + } else if (last instanceof BlockStatement) { + return extractFilterBodyExpr(last, tagsVar); + } else { + throw new UnsupportedOperationException( + "Unsupported filter body statement: " + last.getClass().getSimpleName()); + } + + if (expr instanceof ClosureExpression) { + final ClosureExpression inner = (ClosureExpression) expr; + return extractFilterBodyExpr(inner.getCode(), tagsVar); + } + + return expr; + } + + // ---- AST Parsing ---- + + ModuleNode parseToAST(final String expression) { + final CompilerConfiguration cc = new CompilerConfiguration(); + final CompilationUnit cu = new CompilationUnit(cc); + cu.addSource("Script", expression); + cu.compile(Phases.CONVERSION); + final List<ModuleNode> modules = cu.getAST().getModules(); + if (modules.isEmpty()) { + throw new IllegalStateException("No AST modules produced for: " + expression); + } + return modules.get(0); + } + + Statement extractBody(final ModuleNode module) { + final BlockStatement block = module.getStatementBlock(); + if (block != null && !block.getStatements().isEmpty()) { + return block; + } + final List<ClassNode> classes = module.getClasses(); + if (!classes.isEmpty()) { + return module.getStatementBlock(); + } + throw new IllegalStateException("Empty AST body"); + } + + // ---- Sample Name Collection ---- + + private void collectSampleNames(final Statement stmt, final Set<String> names) { + if (stmt instanceof BlockStatement) { + for (Statement s : ((BlockStatement) stmt).getStatements()) { + collectSampleNames(s, names); + } + } else if (stmt instanceof ExpressionStatement) { + collectSampleNamesFromExpr(((ExpressionStatement) stmt).getExpression(), names); + } else if (stmt instanceof ReturnStatement) { + collectSampleNamesFromExpr(((ReturnStatement) stmt).getExpression(), names); + } + } + + void collectSampleNamesFromExpr(final Expression expr, final Set<String> names) { + if (expr instanceof VariableExpression) { + final String name = ((VariableExpression) expr).getName(); + if (!DOWNSAMPLING_CONSTANTS.contains(name) + && !KNOWN_TYPES.contains(name) + && !name.equals("this") && !name.equals("time")) { + names.add(name); + } + } else if (expr instanceof BinaryExpression) { + final BinaryExpression bin = (BinaryExpression) expr; + collectSampleNamesFromExpr(bin.getLeftExpression(), names); + collectSampleNamesFromExpr(bin.getRightExpression(), names); + } else if (expr instanceof PropertyExpression) { + final PropertyExpression pe = (PropertyExpression) expr; + collectSampleNamesFromExpr(pe.getObjectExpression(), names); + } else if (expr instanceof MethodCallExpression) { + final MethodCallExpression mce = (MethodCallExpression) expr; + collectSampleNamesFromExpr(mce.getObjectExpression(), names); + collectSampleNamesFromExpr(mce.getArguments(), names); + } else if (expr instanceof ArgumentListExpression) { + for (Expression e : ((ArgumentListExpression) expr).getExpressions()) { + collectSampleNamesFromExpr(e, names); + } + } else if (expr instanceof TupleExpression) { + for (Expression e : ((TupleExpression) expr).getExpressions()) { + collectSampleNamesFromExpr(e, names); + } + } + } + + // ---- Statement Visiting ---- + + String visitStatement(final Statement stmt) { + if (stmt instanceof BlockStatement) { + final List<Statement> stmts = ((BlockStatement) stmt).getStatements(); + if (stmts.size() == 1) { + return visitStatement(stmts.get(0)); + } + // Multi-statement: last one is the return value + return visitStatement(stmts.get(stmts.size() - 1)); + } else if (stmt instanceof ExpressionStatement) { + return visitExpression(((ExpressionStatement) stmt).getExpression()); + } else if (stmt instanceof ReturnStatement) { + return visitExpression(((ReturnStatement) stmt).getExpression()); + } + throw new UnsupportedOperationException( + "Unsupported statement: " + stmt.getClass().getSimpleName()); + } + + // ---- Expression Visiting ---- + + String visitExpression(final Expression expr) { + if (expr instanceof VariableExpression) { + return visitVariable((VariableExpression) expr); + } else if (expr instanceof ConstantExpression) { + return visitConstant((ConstantExpression) expr); + } else if (expr instanceof MethodCallExpression) { + return visitMethodCall((MethodCallExpression) expr); + } else if (expr instanceof PropertyExpression) { + return visitProperty((PropertyExpression) expr); + } else if (expr instanceof ListExpression) { + return visitList((ListExpression) expr); + } else if (expr instanceof BinaryExpression) { + return visitBinary((BinaryExpression) expr); + } else if (expr instanceof ClosureExpression) { + throw new UnsupportedOperationException( + "Bare ClosureExpression outside method call context: " + expr.getText()); + } + throw new UnsupportedOperationException( + "Unsupported expression (not yet implemented): " + + expr.getClass().getSimpleName() + " = " + expr.getText()); + } + + private String visitVariable(final VariableExpression expr) { + final String name = expr.getName(); + if (DOWNSAMPLING_CONSTANTS.contains(name)) { + return "DownsamplingType." + name; + } + if (KNOWN_TYPES.contains(name)) { + return name; + } + if (name.equals("this")) { + return "this"; + } + // Sample family lookup + return "samples.getOrDefault(\"" + escapeJava(name) + "\", SampleFamily.EMPTY)"; + } + + private String visitConstant(final ConstantExpression expr) { + final Object value = expr.getValue(); + if (value == null) { + return "null"; + } + if (value instanceof String) { + return "\"" + escapeJava((String) value) + "\""; + } + if (value instanceof Integer) { + return value.toString(); + } + if (value instanceof Long) { + return value + "L"; + } + if (value instanceof Double) { + return value.toString(); + } + if (value instanceof Float) { + return value + "f"; + } + if (value instanceof Boolean) { + return value.toString(); + } + return value.toString(); + } + + // ---- MethodCall, Property, List ---- + + private String visitMethodCall(final MethodCallExpression expr) { + final String methodName = expr.getMethodAsString(); + final Expression objExpr = expr.getObjectExpression(); + final ArgumentListExpression args = toArgList(expr.getArguments()); + + // tag(closure) -> TagFunction lambda + if ("tag".equals(methodName) && args.getExpressions().size() == 1 + && args.getExpression(0) instanceof ClosureExpression) { + final String obj = visitExpression(objExpr); + final String lambda = visitTagClosure((ClosureExpression) args.getExpression(0)); + return obj + ".tag((TagFunction) " + lambda + ")"; + } + + // forEach(list, closure) -> ForEachFunction lambda + if ("forEach".equals(methodName) && args.getExpressions().size() == 2 + && args.getExpression(1) instanceof ClosureExpression) { + final String obj = visitExpression(objExpr); + final String list = visitExpression(args.getExpression(0)); + final String lambda = visitForEachClosure((ClosureExpression) args.getExpression(1)); + return obj + ".forEach(" + list + ", (ForEachFunction) " + lambda + ")"; + } + + // instance(..., closure) -> last arg is PropertiesExtractor lambda + if ("instance".equals(methodName) && !args.getExpressions().isEmpty()) { + final Expression lastArg = args.getExpression(args.getExpressions().size() - 1); + if (lastArg instanceof ClosureExpression) { + final String obj = visitExpression(objExpr); + final List<String> argStrs = new ArrayList<>(); + for (int i = 0; i < args.getExpressions().size() - 1; i++) { + argStrs.add(visitExpression(args.getExpression(i))); + } + final String lambda = visitPropertiesExtractorClosure((ClosureExpression) lastArg); + argStrs.add("(PropertiesExtractor) " + lambda); + return obj + ".instance(" + String.join(", ", argStrs) + ")"; + } + } + + final String obj = visitExpression(objExpr); + + // Static method calls: ClassExpression.method(...) + if (objExpr instanceof ClassExpression) { + final String typeName = objExpr.getType().getNameWithoutPackage(); + final List<String> argStrs = visitArgList(args); + return typeName + "." + methodName + "(" + String.join(", ", argStrs) + ")"; + } + + // Regular instance method call: obj.method(args) + final List<String> argStrs = visitArgList(args); + return obj + "." + methodName + "(" + String.join(", ", argStrs) + ")"; + } + + private String visitProperty(final PropertyExpression expr) { + final Expression obj = expr.getObjectExpression(); + final String prop = expr.getPropertyAsString(); + + if (obj instanceof ClassExpression) { + return obj.getType().getNameWithoutPackage() + "." + prop; + } + if (obj instanceof VariableExpression) { + final String varName = ((VariableExpression) obj).getName(); + if (KNOWN_TYPES.contains(varName)) { + return varName + "." + prop; + } + } + + return visitExpression(obj) + "." + prop; + } + + private String visitList(final ListExpression expr) { + final List<String> elements = new ArrayList<>(); + for (Expression e : expr.getExpressions()) { + elements.add(visitExpression(e)); + } + return "List.of(" + String.join(", ", elements) + ")"; + } + + // ---- Binary Arithmetic ---- + + private String visitBinary(final BinaryExpression expr) { + final int opType = expr.getOperation().getType(); + + if (isArithmetic(opType)) { + return visitArithmetic(expr.getLeftExpression(), expr.getRightExpression(), opType); + } + + throw new UnsupportedOperationException( + "Unsupported binary operator (not yet implemented): " + + expr.getOperation().getText() + " in " + expr.getText()); + } + + /** + * Arithmetic with operand-swap logic per upstream ExpandoMetaClass: + * <pre> + * SF + SF -> left.plus(right) + * SF - SF -> left.minus(right) + * SF * SF -> left.multiply(right) + * SF / SF -> left.div(right) + * SF op N -> sf.op(N) + * N + SF -> sf.plus(N) (swap) + * N - SF -> sf.minus(N).negative() + * N * SF -> sf.multiply(N) (swap) + * N / SF -> sf.newValue(v -> N / v) + * N op N -> plain arithmetic + * </pre> + */ + private String visitArithmetic(final Expression left, final Expression right, final int opType) { + final boolean leftNum = isNumberLiteral(left); + final boolean rightNum = isNumberLiteral(right); + final String leftStr = visitExpression(left); + final String rightStr = visitExpression(right); + + if (leftNum && rightNum) { + return "(" + leftStr + " " + opSymbol(opType) + " " + rightStr + ")"; + } + + if (!leftNum && rightNum) { + return leftStr + "." + opMethod(opType) + "(" + rightStr + ")"; + } + + if (leftNum && !rightNum) { + switch (opType) { + case Types.PLUS: + return rightStr + ".plus(" + leftStr + ")"; + case Types.MINUS: + return rightStr + ".minus(" + leftStr + ").negative()"; + case Types.MULTIPLY: + return rightStr + ".multiply(" + leftStr + ")"; + case Types.DIVIDE: + return rightStr + ".newValue(v -> " + leftStr + " / v)"; + default: + break; + } + } + + // SF op SF + return leftStr + "." + opMethod(opType) + "(" + rightStr + ")"; + } + + private boolean isNumberLiteral(final Expression expr) { + if (expr instanceof ConstantExpression) { + return ((ConstantExpression) expr).getValue() instanceof Number; + } + return false; + } + + private boolean isArithmetic(final int opType) { + return opType == Types.PLUS || opType == Types.MINUS + || opType == Types.MULTIPLY || opType == Types.DIVIDE; + } + + private String opMethod(final int opType) { + switch (opType) { + case Types.PLUS: return "plus"; + case Types.MINUS: return "minus"; + case Types.MULTIPLY: return "multiply"; + case Types.DIVIDE: return "div"; + default: return "???"; + } + } + + private String opSymbol(final int opType) { + switch (opType) { + case Types.PLUS: return "+"; + case Types.MINUS: return "-"; + case Types.MULTIPLY: return "*"; + case Types.DIVIDE: return "/"; + default: return "?"; + } + } + + // ---- tag() Closure ---- + + private String visitTagClosure(final ClosureExpression closure) { + final Parameter[] params = closure.getParameters(); + final String tagsVar = (params != null && params.length > 0) ? params[0].getName() : "tags"; + final List<Statement> stmts = getStatements(closure.getCode()); + + final StringBuilder sb = new StringBuilder(); + sb.append("(").append(tagsVar).append(" -> {\n"); + for (Statement s : stmts) { + sb.append(" ").append(visitTagStatement(s, tagsVar)).append("\n"); + } + sb.append(" return ").append(tagsVar).append(";\n"); + sb.append(" })"); + return sb.toString(); + } + + private String visitTagStatement(final Statement stmt, final String tagsVar) { + if (stmt instanceof ExpressionStatement) { + return visitTagExpr(((ExpressionStatement) stmt).getExpression(), tagsVar) + ";"; + } + if (stmt instanceof ReturnStatement) { + return "return " + tagsVar + ";"; + } + if (stmt instanceof IfStatement) { + return visitTagIf((IfStatement) stmt, tagsVar); + } + throw new UnsupportedOperationException( + "Unsupported tag closure statement: " + stmt.getClass().getSimpleName()); + } + + // ---- If/Else + Compound Conditions in tag() ---- + + private String visitTagIf(final IfStatement ifStmt, final String tagsVar) { + final String condition = visitTagCondition(ifStmt.getBooleanExpression().getExpression(), tagsVar); + final List<Statement> ifBody = getStatements(ifStmt.getIfBlock()); + final Statement elseBlock = ifStmt.getElseBlock(); + + final StringBuilder sb = new StringBuilder(); + sb.append("if (").append(condition).append(") {\n"); + for (Statement s : ifBody) { + sb.append(" ").append(visitTagStatement(s, tagsVar)).append("\n"); + } + sb.append(" }"); + + if (elseBlock != null && !(elseBlock instanceof EmptyStatement)) { + sb.append(" else {\n"); + final List<Statement> elseBody = getStatements(elseBlock); + for (Statement s : elseBody) { + sb.append(" ").append(visitTagStatement(s, tagsVar)).append("\n"); + } + sb.append(" }"); + } + + return sb.toString(); + } + + private String visitTagCondition(final Expression expr, final String tagsVar) { + if (expr instanceof BinaryExpression) { + final BinaryExpression bin = (BinaryExpression) expr; + final int opType = bin.getOperation().getType(); + + if (opType == Types.COMPARE_EQUAL) { + return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, false); + } + if (opType == Types.COMPARE_NOT_EQUAL) { + return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, true); + } + if (opType == Types.LOGICAL_OR) { + return visitTagCondition(bin.getLeftExpression(), tagsVar) + + " || " + visitTagCondition(bin.getRightExpression(), tagsVar); + } + if (opType == Types.LOGICAL_AND) { + return visitTagCondition(bin.getLeftExpression(), tagsVar) + + " && " + visitTagCondition(bin.getRightExpression(), tagsVar); + } + } + if (expr instanceof BooleanExpression) { + return visitTagCondition(((BooleanExpression) expr).getExpression(), tagsVar); + } + return visitTagValue(expr, tagsVar); + } + + private String visitTagEquals(final Expression left, final Expression right, + final String tagsVar, final boolean negate) { + if (isNullConstant(right)) { + final String leftStr = visitTagValue(left, tagsVar); + return negate ? leftStr + " != null" : leftStr + " == null"; + } + if (isNullConstant(left)) { + final String rightStr = visitTagValue(right, tagsVar); + return negate ? rightStr + " != null" : rightStr + " == null"; + } + + final String leftStr = visitTagValue(left, tagsVar); + final String rightStr = visitTagValue(right, tagsVar); + + if (right instanceof ConstantExpression && ((ConstantExpression) right).getValue() instanceof String) { + final String result = rightStr + ".equals(" + leftStr + ")"; + return negate ? "!" + result : result; + } + if (left instanceof ConstantExpression && ((ConstantExpression) left).getValue() instanceof String) { + final String result = leftStr + ".equals(" + rightStr + ")"; + return negate ? "!" + result : result; + } + final String result = "Objects.equals(" + leftStr + ", " + rightStr + ")"; + return negate ? "!" + result : result; + } + + // ---- Filter Conditions ---- + + private String visitFilterCondition(final Expression expr, final String tagsVar) { + if (expr instanceof NotExpression) { + final Expression inner = ((NotExpression) expr).getExpression(); + final String val = visitTagValue(inner, tagsVar); + return "(" + val + " == null || " + val + ".isEmpty())"; + } + + if (expr instanceof BinaryExpression) { + final BinaryExpression bin = (BinaryExpression) expr; + final int opType = bin.getOperation().getType(); + + if (opType == Types.COMPARE_EQUAL) { + return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, false); + } + if (opType == Types.COMPARE_NOT_EQUAL) { + return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, true); + } + if (opType == Types.LOGICAL_OR) { + return visitFilterCondition(bin.getLeftExpression(), tagsVar) + + " || " + visitFilterCondition(bin.getRightExpression(), tagsVar); + } + if (opType == Types.LOGICAL_AND) { + return visitFilterCondition(bin.getLeftExpression(), tagsVar) + + " && " + visitFilterCondition(bin.getRightExpression(), tagsVar); + } + if (opType == Types.KEYWORD_IN) { + final String val = visitTagValue(bin.getLeftExpression(), tagsVar); + final String list = visitTagValue(bin.getRightExpression(), tagsVar); + return list + ".contains(" + val + ")"; + } + } + + if (expr instanceof BooleanExpression) { + return visitFilterCondition(((BooleanExpression) expr).getExpression(), tagsVar); + } + + final String val = visitTagValue(expr, tagsVar); + return "(" + val + " != null && !" + val + ".isEmpty())"; + } + + private String visitTagExpr(final Expression expr, final String tagsVar) { + if (expr instanceof BinaryExpression) { + final BinaryExpression bin = (BinaryExpression) expr; + if (bin.getOperation().getType() == Types.ASSIGN) { + return visitTagAssignment(bin.getLeftExpression(), bin.getRightExpression(), tagsVar); + } + } + if (expr instanceof MethodCallExpression) { + final MethodCallExpression mce = (MethodCallExpression) expr; + if ("remove".equals(mce.getMethodAsString()) && isTagsVar(mce.getObjectExpression(), tagsVar)) { + final ArgumentListExpression args = toArgList(mce.getArguments()); + return tagsVar + ".remove(" + visitTagValue(args.getExpression(0), tagsVar) + ")"; + } + } + return visitTagValue(expr, tagsVar); + } + + private String visitTagAssignment(final Expression left, final Expression right, final String tagsVar) { + final String val = visitTagValue(right, tagsVar); + + if (left instanceof PropertyExpression) { + final PropertyExpression prop = (PropertyExpression) left; + if (isTagsVar(prop.getObjectExpression(), tagsVar)) { + return tagsVar + ".put(\"" + escapeJava(prop.getPropertyAsString()) + "\", " + val + ")"; + } + } + if (left instanceof BinaryExpression) { + final BinaryExpression sub = (BinaryExpression) left; + if (sub.getOperation().getType() == Types.LEFT_SQUARE_BRACKET + && isTagsVar(sub.getLeftExpression(), tagsVar)) { + final String key = visitTagValue(sub.getRightExpression(), tagsVar); + return tagsVar + ".put(" + key + ", " + val + ")"; + } + } + throw new UnsupportedOperationException( + "Unsupported tag assignment target: " + left.getClass().getSimpleName() + " = " + left.getText()); + } + + String visitTagValue(final Expression expr, final String tagsVar) { + if (expr instanceof PropertyExpression) { + final PropertyExpression prop = (PropertyExpression) expr; + if (isTagsVar(prop.getObjectExpression(), tagsVar)) { + return tagsVar + ".get(\"" + escapeJava(prop.getPropertyAsString()) + "\")"; + } + return visitProperty(prop); + } + if (expr instanceof BinaryExpression) { + final BinaryExpression bin = (BinaryExpression) expr; + if (bin.getOperation().getType() == Types.LEFT_SQUARE_BRACKET + && isTagsVar(bin.getLeftExpression(), tagsVar)) { + return tagsVar + ".get(" + visitTagValue(bin.getRightExpression(), tagsVar) + ")"; + } + if (bin.getOperation().getType() == Types.PLUS) { + return visitTagValue(bin.getLeftExpression(), tagsVar) + + " + " + visitTagValue(bin.getRightExpression(), tagsVar); + } + } + // Elvis operator — must check BEFORE TernaryExpression since it extends it + if (expr instanceof ElvisOperatorExpression) { + final ElvisOperatorExpression elvis = (ElvisOperatorExpression) expr; + final String val = visitTagValue(elvis.getTrueExpression(), tagsVar); + final String defaultVal = visitTagValue(elvis.getFalseExpression(), tagsVar); + return "(" + val + " != null ? " + val + " : " + defaultVal + ")"; + } + if (expr instanceof TernaryExpression) { + final TernaryExpression tern = (TernaryExpression) expr; + final String cond = visitFilterCondition(tern.getBooleanExpression().getExpression(), tagsVar); + final String trueVal = visitTagValue(tern.getTrueExpression(), tagsVar); + final String falseVal = visitTagValue(tern.getFalseExpression(), tagsVar); + return "(" + cond + " ? " + trueVal + " : " + falseVal + ")"; + } + if (expr instanceof MethodCallExpression) { + final MethodCallExpression mce = (MethodCallExpression) expr; + final String obj = visitTagValue(mce.getObjectExpression(), tagsVar); + final ArgumentListExpression args = toArgList(mce.getArguments()); + final List<String> argStrs = new ArrayList<>(); + for (Expression a : args.getExpressions()) { + argStrs.add(visitTagValue(a, tagsVar)); + } + final String call = obj + "." + mce.getMethodAsString() + "(" + String.join(", ", argStrs) + ")"; + if (mce.isSafe()) { + return "(" + obj + " != null ? " + call + " : null)"; + } + return call; + } + if (expr instanceof VariableExpression) { + final String name = ((VariableExpression) expr).getName(); + if (name.equals(tagsVar)) { + return tagsVar; + } + return name; + } + if (expr instanceof ConstantExpression) { + return visitConstant((ConstantExpression) expr); + } + if (expr instanceof ListExpression) { + return visitList((ListExpression) expr); + } + if (expr instanceof MapExpression) { + final MapExpression map = (MapExpression) expr; + final List<String> entries = new ArrayList<>(); + for (MapEntryExpression entry : map.getMapEntryExpressions()) { + entries.add(visitTagValue(entry.getKeyExpression(), tagsVar)); + entries.add(visitTagValue(entry.getValueExpression(), tagsVar)); + } + return "Map.of(" + String.join(", ", entries) + ")"; + } + return visitExpression(expr); + } + + private boolean isTagsVar(final Expression expr, final String tagsVar) { + return expr instanceof VariableExpression + && ((VariableExpression) expr).getName().equals(tagsVar); + } + + private List<Statement> getStatements(final Statement stmt) { + if (stmt instanceof BlockStatement) { + return ((BlockStatement) stmt).getStatements(); + } + return List.of(stmt); + } + + // ---- forEach() Closure ---- + + private String visitForEachClosure(final ClosureExpression closure) { + final Parameter[] params = closure.getParameters(); + final String prefixVar = (params != null && params.length > 0) ? params[0].getName() : "prefix"; + final String tagsVar = (params != null && params.length > 1) ? params[1].getName() : "tags"; + + final List<Statement> stmts = getStatements(closure.getCode()); + + final StringBuilder sb = new StringBuilder(); + sb.append("(").append(prefixVar).append(", ").append(tagsVar).append(") -> {\n"); + for (Statement s : stmts) { + sb.append(" ").append(visitForEachStatement(s, tagsVar)).append("\n"); + } + sb.append(" }"); + return sb.toString(); + } + + private String visitForEachStatement(final Statement stmt, final String tagsVar) { + if (stmt instanceof ExpressionStatement) { + return visitForEachExpr(((ExpressionStatement) stmt).getExpression(), tagsVar) + ";"; + } + if (stmt instanceof ReturnStatement) { + return "return;"; + } + if (stmt instanceof IfStatement) { + return visitForEachIf((IfStatement) stmt, tagsVar); + } + throw new UnsupportedOperationException( + "Unsupported forEach closure statement: " + stmt.getClass().getSimpleName()); + } + + private String visitForEachExpr(final Expression expr, final String tagsVar) { + if (expr instanceof DeclarationExpression) { + final DeclarationExpression decl = (DeclarationExpression) expr; + final String typeName = decl.getVariableExpression().getType().getNameWithoutPackage(); + final String varName = decl.getVariableExpression().getName(); + final String init = visitTagValue(decl.getRightExpression(), tagsVar); + return typeName + " " + varName + " = " + init; + } + if (expr instanceof BinaryExpression) { + final BinaryExpression bin = (BinaryExpression) expr; + if (bin.getOperation().getType() == Types.ASSIGN) { + final Expression left = bin.getLeftExpression(); + if (isTagWrite(left, tagsVar)) { + return visitTagAssignment(left, bin.getRightExpression(), tagsVar); + } + if (left instanceof VariableExpression) { + return ((VariableExpression) left).getName() + + " = " + visitTagValue(bin.getRightExpression(), tagsVar); + } + } + } + return visitTagExpr(expr, tagsVar); + } + + private String visitForEachIf(final IfStatement ifStmt, final String tagsVar) { + final String condition = visitTagCondition(ifStmt.getBooleanExpression().getExpression(), tagsVar); + final List<Statement> ifBody = getStatements(ifStmt.getIfBlock()); + final Statement elseBlock = ifStmt.getElseBlock(); + + final StringBuilder sb = new StringBuilder(); + sb.append("if (").append(condition).append(") {\n"); + for (Statement s : ifBody) { + sb.append(" ").append(visitForEachStatement(s, tagsVar)).append("\n"); + } + sb.append(" }"); + + if (elseBlock instanceof IfStatement) { + sb.append(" else ").append(visitForEachIf((IfStatement) elseBlock, tagsVar)); + } else if (elseBlock != null && !(elseBlock instanceof EmptyStatement)) { + sb.append(" else {\n"); + for (Statement s : getStatements(elseBlock)) { + sb.append(" ").append(visitForEachStatement(s, tagsVar)).append("\n"); + } + sb.append(" }"); + } + + return sb.toString(); + } + + private boolean isTagWrite(final Expression left, final String tagsVar) { + if (left instanceof PropertyExpression) { + return isTagsVar(((PropertyExpression) left).getObjectExpression(), tagsVar); + } + if (left instanceof BinaryExpression) { + final BinaryExpression sub = (BinaryExpression) left; + return sub.getOperation().getType() == Types.LEFT_SQUARE_BRACKET + && isTagsVar(sub.getLeftExpression(), tagsVar); + } + return false; + } + + private boolean isNullConstant(final Expression expr) { + return expr instanceof ConstantExpression && ((ConstantExpression) expr).getValue() == null; + } + + // ---- PropertiesExtractor Closure ---- + + private String visitPropertiesExtractorClosure(final ClosureExpression closure) { + final Parameter[] params = closure.getParameters(); + final String tagsVar = (params != null && params.length > 0) ? params[0].getName() : "tags"; + final List<Statement> stmts = getStatements(closure.getCode()); + + final Statement last = stmts.get(stmts.size() - 1); + Expression bodyExpr; + if (last instanceof ExpressionStatement) { + bodyExpr = ((ExpressionStatement) last).getExpression(); + } else if (last instanceof ReturnStatement) { + bodyExpr = ((ReturnStatement) last).getExpression(); + } else { + throw new UnsupportedOperationException( + "Unsupported PropertiesExtractor closure body: " + last.getClass().getSimpleName()); + } + return "(" + tagsVar + " -> " + visitTagValue(bodyExpr, tagsVar) + ")"; + } + + // ---- Batch Registration, Compilation, and Manifest Writing ---- + + public void registerExpression(final String className, final String source) { + expressionSources.put(className, source); + } + + public void registerFilter(final String className, final String filterLiteral, final String source) { + filterSources.put(className, source); + filterLiteralToClass.put(filterLiteral, GENERATED_PACKAGE + "." + className); + } + + /** + * Compile all registered sources using javax.tools.JavaCompiler. + * + * @param sourceDir directory to write .java source files (package dirs created automatically) + * @param outputDir directory for compiled .class files + * @param classpath classpath for javac (semicolon/colon-separated JAR paths) + * @throws IOException if file I/O fails + */ + public void compileAll(final File sourceDir, final File outputDir, + final String classpath) throws IOException { + final Map<String, String> allSources = new LinkedHashMap<>(); + allSources.putAll(expressionSources); + allSources.putAll(filterSources); + + if (allSources.isEmpty()) { + log.info("No MAL sources to compile."); + return; + } + + final String packageDir = GENERATED_PACKAGE.replace('.', File.separatorChar); + final File srcPkgDir = new File(sourceDir, packageDir); + if (!srcPkgDir.exists() && !srcPkgDir.mkdirs()) { + throw new IOException("Failed to create source dir: " + srcPkgDir); + } + if (!outputDir.exists() && !outputDir.mkdirs()) { + throw new IOException("Failed to create output dir: " + outputDir); + } + + final List<File> javaFiles = new ArrayList<>(); + for (Map.Entry<String, String> entry : allSources.entrySet()) { + final File javaFile = new File(srcPkgDir, entry.getKey() + ".java"); + Files.writeString(javaFile.toPath(), entry.getValue()); + javaFiles.add(javaFile); + } + + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + throw new IllegalStateException("No Java compiler available — requires JDK"); + } + + final StringWriter errorWriter = new StringWriter(); + + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) { + final Iterable<? extends JavaFileObject> compilationUnits = + fileManager.getJavaFileObjectsFromFiles(javaFiles); + + final List<String> options = Arrays.asList( + "-d", outputDir.getAbsolutePath(), + "-classpath", classpath + ); + + final JavaCompiler.CompilationTask task = compiler.getTask( + errorWriter, fileManager, null, options, null, compilationUnits); + + final boolean success = task.call(); + if (!success) { + throw new RuntimeException( + "Java compilation failed for " + javaFiles.size() + " MAL sources:\n" + + errorWriter); + } + } + + log.info("Compiled {} MAL sources to {}", allSources.size(), outputDir); + } + + /** + * Write mal-expressions.txt manifest: one FQCN per line. + */ + public void writeExpressionManifest(final File outputDir) throws IOException { + final File manifestDir = new File(outputDir, "META-INF"); + if (!manifestDir.exists() && !manifestDir.mkdirs()) { + throw new IOException("Failed to create META-INF dir: " + manifestDir); + } + + final List<String> lines = expressionSources.keySet().stream() + .map(name -> GENERATED_PACKAGE + "." + name) + .collect(Collectors.toList()); + Files.write(new File(manifestDir, "mal-expressions.txt").toPath(), lines); + log.info("Wrote mal-expressions.txt with {} entries", lines.size()); + } + + /** + * Write mal-filter-expressions.properties manifest: literal=FQCN. + */ + public void writeFilterManifest(final File outputDir) throws IOException { + final File manifestDir = new File(outputDir, "META-INF"); + if (!manifestDir.exists() && !manifestDir.mkdirs()) { + throw new IOException("Failed to create META-INF dir: " + manifestDir); + } + + final List<String> lines = filterLiteralToClass.entrySet().stream() + .map(e -> escapeProperties(e.getKey()) + "=" + e.getValue()) + .collect(Collectors.toList()); + Files.write(new File(manifestDir, "mal-filter-expressions.properties").toPath(), lines); + log.info("Wrote mal-filter-expressions.properties with {} entries", lines.size()); + } + + private static String escapeProperties(final String s) { + return s.replace("\\", "\\\\") + .replace("=", "\\=") + .replace(":", "\\:") + .replace(" ", "\\ "); + } + + // ---- Argument Utilities ---- + + private ArgumentListExpression toArgList(final Expression args) { + if (args instanceof ArgumentListExpression) { + return (ArgumentListExpression) args; + } + if (args instanceof TupleExpression) { + final ArgumentListExpression ale = new ArgumentListExpression(); + for (Expression e : ((TupleExpression) args).getExpressions()) { + ale.addExpression(e); + } + return ale; + } + final ArgumentListExpression ale = new ArgumentListExpression(); + ale.addExpression(args); + return ale; + } + + private List<String> visitArgList(final ArgumentListExpression args) { + final List<String> result = new ArrayList<>(); + for (Expression arg : args.getExpressions()) { + result.add(visitExpression(arg)); + } + return result; + } + + // ---- Utility ---- + + static String escapeJava(final String s) { + return s.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } +} diff --git a/oap-server/analyzer/mal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspilerTest.java b/oap-server/analyzer/mal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspilerTest.java new file mode 100644 index 0000000000..0207d8df32 --- /dev/null +++ b/oap-server/analyzer/mal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspilerTest.java @@ -0,0 +1,904 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 org.apache.skywalking.oap.server.transpiler.mal; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MalToJavaTranspilerTest { + + private MalToJavaTranspiler transpiler; + + @BeforeEach + void setUp() { + transpiler = new MalToJavaTranspiler(); + } + + // ---- AST Parsing + Simple Variable References ---- + + @Test + void simpleVariableReference() { + final String java = transpiler.transpileExpression("MalExpr_test", "metric_name"); + assertNotNull(java); + + assertTrue(java.contains("package " + MalToJavaTranspiler.GENERATED_PACKAGE), + "Should have correct package"); + assertTrue(java.contains("public class MalExpr_test implements MalExpression"), + "Should implement MalExpression"); + assertTrue(java.contains("public SampleFamily run(Map<String, SampleFamily> samples)"), + "Should have run method"); + + assertTrue(java.contains("ctx.getSamples().add(\"metric_name\")"), + "Should track sample name in parsing context"); + + assertTrue(java.contains("samples.getOrDefault(\"metric_name\", SampleFamily.EMPTY)"), + "Should look up sample family from map"); + } + + @Test + void downsamplingConstantNotTrackedAsSample() { + final String java = transpiler.transpileExpression("MalExpr_test", "SUM"); + assertNotNull(java); + + assertTrue(!java.contains("ctx.getSamples().add(\"SUM\")"), + "Should not track DownsamplingType constant as sample"); + + assertTrue(java.contains("DownsamplingType.SUM"), + "Should resolve to DownsamplingType.SUM"); + } + + @Test + void parseToAST_producesModuleNode() { + final var ast = transpiler.parseToAST("some_metric"); + assertNotNull(ast, "Should produce a ModuleNode"); + assertNotNull(ast.getStatementBlock(), "Should have a statement block"); + } + + @Test + void constantString() { + final String java = transpiler.transpileExpression("MalExpr_test", "'hello'"); + assertNotNull(java); + assertTrue(java.contains("\"hello\""), + "Should convert Groovy string to Java string"); + } + + @Test + void constantNumber() { + final String java = transpiler.transpileExpression("MalExpr_test", "42"); + assertNotNull(java); + assertTrue(java.contains("42"), + "Should preserve number literal"); + } + + // ---- Method Chains + List Literals + Enum Properties ---- + + @Test + void simpleMethodChain() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric_name.sum(['a', 'b']).service(['svc'], Layer.GENERAL)"); + assertNotNull(java); + + assertTrue(java.contains(".sum(List.of(\"a\", \"b\"))"), + "Should translate ['a','b'] to List.of(\"a\", \"b\")"); + assertTrue(java.contains(".service(List.of(\"svc\"), Layer.GENERAL)"), + "Should translate Layer.GENERAL as enum"); + } + + @Test + void tagEqualChain() { + final String java = transpiler.transpileExpression("MalExpr_test", + "cpu_seconds.tagNotEqual('mode', 'idle').sum(['host']).rate('PT1M')"); + assertNotNull(java); + + assertTrue(java.contains(".tagNotEqual(\"mode\", \"idle\")"), + "Should translate tagNotEqual with string args"); + assertTrue(java.contains(".sum(List.of(\"host\"))"), + "Should translate single-element list"); + assertTrue(java.contains(".rate(\"PT1M\")"), + "Should translate rate with string arg"); + } + + @Test + void downsamplingMethod() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.downsampling(SUM)"); + assertNotNull(java); + + assertTrue(java.contains(".downsampling(DownsamplingType.SUM)"), + "Should resolve SUM to DownsamplingType.SUM"); + } + + @Test + void retagByK8sMeta() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.retagByK8sMeta('service', K8sRetagType.Pod2Service, 'pod', 'namespace')"); + assertNotNull(java); + + assertTrue(java.contains(".retagByK8sMeta(\"service\", K8sRetagType.Pod2Service, \"pod\", \"namespace\")"), + "Should translate K8sRetagType enum and string args"); + } + + @Test + void histogramPercentile() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.sum(['le', 'svc']).histogram().histogram_percentile([50, 75, 90])"); + assertNotNull(java); + + assertTrue(java.contains(".histogram()"), + "Should translate no-arg histogram()"); + assertTrue(java.contains(".histogram_percentile(List.of(50, 75, 90))"), + "Should translate integer list"); + } + + @Test + void sampleNameCollectionThroughChain() { + final String java = transpiler.transpileExpression("MalExpr_test", + "my_metric.sum(['a']).service(['svc'], Layer.GENERAL)"); + assertNotNull(java); + + assertTrue(java.contains("ctx.getSamples().add(\"my_metric\")"), + "Should collect sample name from root of method chain"); + assertTrue(!java.contains("ctx.getSamples().add(\"a\")"), + "Should NOT collect 'a' (it's a string constant arg, not a sample)"); + } + + @Test + void detectPointEnum() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.serviceRelation(DetectPoint.CLIENT, ['src'], ['dst'], Layer.MESH_DP)"); + assertNotNull(java); + + assertTrue(java.contains("DetectPoint.CLIENT"), + "Should translate DetectPoint enum"); + assertTrue(java.contains("Layer.MESH_DP"), + "Should translate Layer enum"); + } + + @Test + void enumImportsPresent() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.service(['svc'], Layer.GENERAL)"); + assertNotNull(java); + + assertTrue(java.contains("import org.apache.skywalking.oap.server.core.analysis.Layer;"), + "Should import Layer"); + assertTrue(java.contains("import org.apache.skywalking.oap.server.core.source.DetectPoint;"), + "Should import DetectPoint"); + assertTrue(java.contains("import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;"), + "Should import K8sRetagType"); + } + + // ---- Binary Arithmetic with Operand-Swap ---- + + @Test + void sfTimesNumber() { + final String java = transpiler.transpileExpression("MalExpr_test", "metric * 100"); + assertNotNull(java); + assertTrue(java.contains(".multiply(100)"), + "SF * N should call .multiply(N)"); + } + + @Test + void sfDivNumber() { + final String java = transpiler.transpileExpression("MalExpr_test", "metric / 1024"); + assertNotNull(java); + assertTrue(java.contains(".div(1024)"), + "SF / N should call .div(N)"); + } + + @Test + void numberMinusSf() { + final String java = transpiler.transpileExpression("MalExpr_test", "100 - metric"); + assertNotNull(java); + assertTrue(java.contains(".minus(100).negative()"), + "N - SF should produce sf.minus(N).negative()"); + } + + @Test + void numberDivSf() { + final String java = transpiler.transpileExpression("MalExpr_test", "1 / metric"); + assertNotNull(java); + assertTrue(java.contains(".newValue(v -> 1 / v)"), + "N / SF should produce sf.newValue(v -> N / v)"); + } + + @Test + void numberPlusSf() { + final String java = transpiler.transpileExpression("MalExpr_test", "10 + metric"); + assertNotNull(java); + assertTrue(java.contains(".plus(10)"), + "N + SF should swap to sf.plus(N)"); + } + + @Test + void numberTimesSf() { + final String java = transpiler.transpileExpression("MalExpr_test", "100 * metric"); + assertNotNull(java); + assertTrue(java.contains(".multiply(100)"), + "N * SF should swap to sf.multiply(N)"); + } + + @Test + void sfMinusSf() { + final String java = transpiler.transpileExpression("MalExpr_test", "mem_total - mem_avail"); + assertNotNull(java); + assertTrue(java.contains("ctx.getSamples().add(\"mem_total\")"), + "Should collect both sample names"); + assertTrue(java.contains("ctx.getSamples().add(\"mem_avail\")"), + "Should collect both sample names"); + assertTrue(java.contains(".minus("), + "SF - SF should call .minus()"); + } + + @Test + void sfDivSfTimesNumber() { + final String java = transpiler.transpileExpression("MalExpr_test", + "used_bytes / max_bytes * 100"); + assertNotNull(java); + assertTrue(java.contains(".div("), + "Should have .div() for SF / SF"); + assertTrue(java.contains(".multiply(100)"), + "Should have .multiply(100) for result * 100"); + } + + @Test + void nestedParenArithmetic() { + final String java = transpiler.transpileExpression("MalExpr_test", + "100 - ((mem_free * 100) / mem_total)"); + assertNotNull(java); + assertTrue(java.contains(".multiply(100)"), + "Should have inner multiply"); + assertTrue(java.contains(".negative()"), + "100 - SF should produce .negative()"); + } + + @Test + void parenthesizedWithMethodChain() { + final String java = transpiler.transpileExpression("MalExpr_test", + "(metric * 100).tagNotEqual('mode', 'idle').sum(['host']).rate('PT1M')"); + assertNotNull(java); + assertTrue(java.contains(".multiply(100)"), + "Should have multiply inside parens"); + assertTrue(java.contains(".tagNotEqual(\"mode\", \"idle\")"), + "Should chain tagNotEqual after parens"); + assertTrue(java.contains(".rate(\"PT1M\")"), + "Should chain rate at the end"); + } + + // ---- tag() Closure — Simple Cases ---- + + @Test + void tagAssignmentWithStringConcat() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags.route = 'route/' + tags['route']})"); + assertNotNull(java); + + assertTrue(java.contains(".tag((TagFunction)"), + "Should cast closure to TagFunction"); + assertTrue(java.contains("tags.put(\"route\", \"route/\" + tags.get(\"route\"))"), + "Should translate assignment with string concat and subscript read"); + assertTrue(java.contains("return tags;"), + "Should return tags at end of lambda"); + } + + @Test + void tagRemove() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags.remove('condition')})"); + assertNotNull(java); + + assertTrue(java.contains("tags.remove(\"condition\")"), + "Should translate remove call"); + } + + @Test + void tagPropertyToProperty() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags.rs_nm = tags.set})"); + assertNotNull(java); + + assertTrue(java.contains("tags.put(\"rs_nm\", tags.get(\"set\"))"), + "Should translate property read on RHS to tags.get()"); + } + + @Test + void tagStringConcatWithPropertyRead() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags.cluster = 'es::' + tags.cluster})"); + assertNotNull(java); + + assertTrue(java.contains("tags.put(\"cluster\", \"es::\" + tags.get(\"cluster\"))"), + "Should translate string concat with property read"); + } + + @Test + void tagClosureLambdaStructure() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags.x = 'y'})"); + assertNotNull(java); + + assertTrue(java.contains("(tags -> {"), + "Should have lambda opening"); + assertTrue(java.contains("return tags;"), + "Should return tags variable"); + } + + @Test + void tagWithSubscriptWrite() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags['service_name'] = tags['svc']})"); + assertNotNull(java); + + assertTrue(java.contains("tags.put(\"service_name\", tags.get(\"svc\"))"), + "Should translate subscript write and read"); + } + + @Test + void tagChainAfterTag() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags.x = 'y'}).sum(['host']).service(['svc'], Layer.GENERAL)"); + assertNotNull(java); + + assertTrue(java.contains(".tag((TagFunction)"), + "Should have tag call"); + assertTrue(java.contains(".sum(List.of(\"host\"))"), + "Should chain sum after tag"); + assertTrue(java.contains(".service(List.of(\"svc\"), Layer.GENERAL)"), + "Should chain service after sum"); + } + + // ---- tag() Closure — if/else + Compound Conditions ---- + + @Test + void ifOnlyWithChainedOr() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> if (tags['gc'] == 'PS Scavenge' || tags['gc'] == 'Copy' || tags['gc'] == 'ParNew' || tags['gc'] == 'G1 Young Generation') {tags.gc = 'young_gc_count'} })"); + assertNotNull(java); + + assertTrue(java.contains("if (\"PS Scavenge\".equals(tags.get(\"gc\"))"), + "Should translate first == with constant on left for null-safety"); + assertTrue(java.contains("|| \"Copy\".equals(tags.get(\"gc\"))"), + "Should chain || for second comparison"); + assertTrue(java.contains("|| \"ParNew\".equals(tags.get(\"gc\"))"), + "Should chain || for third comparison"); + assertTrue(java.contains("|| \"G1 Young Generation\".equals(tags.get(\"gc\"))"), + "Should chain || for fourth comparison"); + assertTrue(java.contains("tags.put(\"gc\", \"young_gc_count\")"), + "Should translate assignment in if body"); + } + + @Test + void ifElse() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> if (tags['primary'] == 'true') {tags.primary = 'primary'} else {tags.primary = 'replica'} })"); + assertNotNull(java); + + assertTrue(java.contains("if (\"true\".equals(tags.get(\"primary\"))"), + "Should translate == comparison in condition"); + assertTrue(java.contains("tags.put(\"primary\", \"primary\")"), + "Should translate if-branch assignment"); + assertTrue(java.contains("} else {"), + "Should have else clause"); + assertTrue(java.contains("tags.put(\"primary\", \"replica\")"), + "Should translate else-branch assignment"); + } + + @Test + void ifOnlyNoElse() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> if (tags['level'] == '1') {tags.level = 'L1 aggregation'} })"); + assertNotNull(java); + + assertTrue(java.contains("if (\"1\".equals(tags.get(\"level\"))"), + "Should translate condition"); + assertTrue(java.contains("tags.put(\"level\", \"L1 aggregation\")"), + "Should translate if-body"); + assertTrue(!java.contains("else"), + "Should NOT have else clause"); + } + + @Test + void notEqualComparison() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> if (tags['status'] != 'ok') {tags.status = 'error'} })"); + assertNotNull(java); + + assertTrue(java.contains("!\"ok\".equals(tags.get(\"status\"))"), + "Should translate != with negated .equals()"); + } + + @Test + void logicalAndCondition() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> if (tags['a'] == 'x' && tags['b'] == 'y') {tags.c = 'z'} })"); + assertNotNull(java); + + assertTrue(java.contains("\"x\".equals(tags.get(\"a\")) && \"y\".equals(tags.get(\"b\"))"), + "Should translate && with .equals() on both sides"); + } + + @Test + void chainedTagClosuresWithIf() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> if (tags['level'] == '1') {tags.level = 'L1 aggregation'} })" + + ".tag({tags -> if (tags['level'] == '2') {tags.level = 'L2 aggregation'} })"); + assertNotNull(java); + + assertTrue(java.contains("\"1\".equals(tags.get(\"level\"))"), + "Should translate first tag closure condition"); + assertTrue(java.contains("\"2\".equals(tags.get(\"level\"))"), + "Should translate second tag closure condition"); + } + + @Test + void ifWithMethodChainAfter() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> if (tags['gc'] == 'Copy') {tags.gc = 'young'} }).sum(['host']).service(['svc'], Layer.GENERAL)"); + assertNotNull(java); + + assertTrue(java.contains(".tag((TagFunction)"), + "Should have TagFunction cast"); + assertTrue(java.contains("\"Copy\".equals(tags.get(\"gc\"))"), + "Should have if condition"); + assertTrue(java.contains(".sum(List.of(\"host\"))"), + "Should chain sum after tag"); + } + + // ---- Filter Closures ---- + + @Test + void simpleEqualityFilter() { + final String java = transpiler.transpileFilter("MalFilter_0", + "{ tags -> tags.job_name == 'vm-monitoring' }"); + assertNotNull(java); + + assertTrue(java.contains("public class MalFilter_0 implements MalFilter"), + "Should implement MalFilter"); + assertTrue(java.contains("public boolean test(Map<String, String> tags)"), + "Should have test method"); + assertTrue(java.contains("\"vm-monitoring\".equals(tags.get(\"job_name\"))"), + "Should translate == with constant on left"); + } + + @Test + void filterPackageAndImports() { + final String java = transpiler.transpileFilter("MalFilter_0", + "{ tags -> tags.job_name == 'x' }"); + assertNotNull(java); + + assertTrue(java.contains("package " + MalToJavaTranspiler.GENERATED_PACKAGE), + "Should have correct package"); + assertTrue(java.contains("import java.util.*;"), + "Should import java.util"); + assertTrue(java.contains("import org.apache.skywalking.oap.meter.analyzer.dsl.*;"), + "Should import dsl package"); + } + + @Test + void orFilter() { + final String java = transpiler.transpileFilter("MalFilter_1", + "{ tags -> tags.job_name == 'flink-jobManager-monitoring' || tags.job_name == 'flink-taskManager-monitoring' }"); + assertNotNull(java); + + assertTrue(java.contains("\"flink-jobManager-monitoring\".equals(tags.get(\"job_name\"))"), + "Should translate first =="); + assertTrue(java.contains("|| \"flink-taskManager-monitoring\".equals(tags.get(\"job_name\"))"), + "Should translate || with second =="); + } + + @Test + void inListFilter() { + final String java = transpiler.transpileFilter("MalFilter_2", + "{ tags -> tags.job_name in ['kubernetes-cadvisor', 'kube-state-metrics'] }"); + assertNotNull(java); + + assertTrue(java.contains("List.of(\"kubernetes-cadvisor\", \"kube-state-metrics\").contains(tags.get(\"job_name\"))"), + "Should translate 'in' to List.of().contains()"); + } + + @Test + void compoundAndFilter() { + final String java = transpiler.transpileFilter("MalFilter_3", + "{ tags -> tags.cloud_provider == 'aws' && tags.Namespace == 'AWS/S3' }"); + assertNotNull(java); + + assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"), + "Should translate first =="); + assertTrue(java.contains("&& \"AWS/S3\".equals(tags.get(\"Namespace\"))"), + "Should translate && with second =="); + } + + @Test + void truthinessFilter() { + final String java = transpiler.transpileFilter("MalFilter_4", + "{ tags -> tags.cloud_provider == 'aws' && tags.Stage }"); + assertNotNull(java); + + assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"), + "Should translate =="); + assertTrue(java.contains("(tags.get(\"Stage\") != null && !tags.get(\"Stage\").isEmpty())"), + "Should translate bare tags.Stage as truthiness check"); + } + + @Test + void negatedTruthinessFilter() { + final String java = transpiler.transpileFilter("MalFilter_5", + "{ tags -> tags.cloud_provider == 'aws' && !tags.Method }"); + assertNotNull(java); + + assertTrue(java.contains("(tags.get(\"Method\") == null || tags.get(\"Method\").isEmpty())"), + "Should translate !tags.Method as negated truthiness"); + } + + @Test + void compoundWithTruthinessAndNegation() { + final String java = transpiler.transpileFilter("MalFilter_6", + "{ tags -> tags.cloud_provider == 'aws' && tags.Namespace == 'AWS/ApiGateway' && tags.Stage && !tags.Method }"); + assertNotNull(java); + + assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"), + "Should translate first =="); + assertTrue(java.contains("\"AWS/ApiGateway\".equals(tags.get(\"Namespace\"))"), + "Should translate second =="); + assertTrue(java.contains("(tags.get(\"Stage\") != null && !tags.get(\"Stage\").isEmpty())"), + "Should translate truthiness"); + assertTrue(java.contains("(tags.get(\"Method\") == null || tags.get(\"Method\").isEmpty())"), + "Should translate negated truthiness"); + } + + @Test + void wrappedBlockFilter() { + final String java = transpiler.transpileFilter("MalFilter_7", + "{ tags -> {tags.cloud_provider == 'aws' && tags.Namespace == 'AWS/S3'} }"); + assertNotNull(java); + + assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"), + "Should unwrap inner block and translate =="); + assertTrue(java.contains("\"AWS/S3\".equals(tags.get(\"Namespace\"))"), + "Should translate second == after unwrapping"); + } + + @Test + void truthinessWithOrInParens() { + final String java = transpiler.transpileFilter("MalFilter_8", + "{ tags -> tags.cloud_provider == 'aws' && (tags.ApiId || tags.ApiName) }"); + assertNotNull(java); + + assertTrue(java.contains("(tags.get(\"ApiId\") != null && !tags.get(\"ApiId\").isEmpty())"), + "Should translate ApiId truthiness"); + assertTrue(java.contains("(tags.get(\"ApiName\") != null && !tags.get(\"ApiName\").isEmpty())"), + "Should translate ApiName truthiness"); + } + + // ---- forEach() Closure ---- + + @Test + void forEachBasicStructure() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.forEach(['client', 'server'], { prefix, tags -> tags[prefix + '_id'] = 'test' })"); + assertNotNull(java); + + assertTrue(java.contains(".forEach(List.of(\"client\", \"server\"), (ForEachFunction)"), + "Should cast closure to ForEachFunction"); + assertTrue(java.contains("(prefix, tags) -> {"), + "Should have two-parameter lambda"); + assertTrue(java.contains("tags.put(prefix + \"_id\", \"test\")"), + "Should translate dynamic subscript write"); + } + + @Test + void forEachNullCheckWithEarlyReturn() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.forEach(['client'], { prefix, tags -> if (tags[prefix + '_process_id'] != null) { return } })"); + assertNotNull(java); + + assertTrue(java.contains("tags.get(prefix + \"_process_id\") != null"), + "Should translate null check"); + assertTrue(java.contains("return;"), + "Should have void return for early exit"); + } + + @Test + void forEachWithProcessRegistry() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.forEach(['client'], { prefix, tags -> " + + "tags[prefix + '_process_id'] = ProcessRegistry.generateVirtualLocalProcess(tags.service, tags.instance) })"); + assertNotNull(java); + + assertTrue(java.contains("ProcessRegistry.generateVirtualLocalProcess(tags.get(\"service\"), tags.get(\"instance\"))"), + "Should translate static method call with tag reads as args"); + assertTrue(java.contains("tags.put(prefix + \"_process_id\","), + "Should translate dynamic subscript write"); + } + + @Test + void forEachVarDeclaration() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.forEach(['component'], { key, tags -> String result = '' })"); + assertNotNull(java); + + assertTrue(java.contains("String result = \"\""), + "Should translate variable declaration with empty string"); + } + + @Test + void forEachVarDeclWithTagRead() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.forEach(['component'], { key, tags -> String protocol = tags['protocol'] })"); + assertNotNull(java); + + assertTrue(java.contains("String protocol = tags.get(\"protocol\")"), + "Should translate var decl with tag read"); + } + + @Test + void forEachIfElseIfChain() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.forEach(['component'], { key, tags -> " + + "String protocol = tags['protocol']\n" + + "String ssl = tags['is_ssl']\n" + + "String result = ''\n" + + "if (protocol == 'http' && ssl == 'true') { result = '129' } " + + "else if (protocol == 'http') { result = '49' } " + + "else if (ssl == 'true') { result = '130' } " + + "else { result = '110' }\n" + + "tags[key] = result })"); + assertNotNull(java); + + assertTrue(java.contains("String protocol = tags.get(\"protocol\")"), + "Should declare protocol"); + assertTrue(java.contains("String ssl = tags.get(\"is_ssl\")"), + "Should declare ssl"); + + assertTrue(java.contains("\"http\".equals(protocol)"), + "Should compare local var with .equals()"); + assertTrue(java.contains("\"true\".equals(ssl)"), + "Should compare ssl with .equals()"); + + assertTrue(java.contains("} else if ("), + "Should produce else-if, not nested else { if }"); + + assertTrue(java.contains("} else {"), + "Should have final else"); + assertTrue(java.contains("result = \"110\""), + "Should assign default value in else"); + + assertTrue(java.contains("tags.put(key, result)"), + "Should write result to tags[key]"); + } + + @Test + void forEachLocalVarAssignment() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.forEach(['x'], { key, tags -> " + + "String r = ''\n" + + "r = 'abc'\n" + + "tags[key] = r })"); + assertNotNull(java); + + assertTrue(java.contains("r = \"abc\""), + "Should translate local var reassignment"); + } + + @Test + void forEachEqualsOnStringComparison() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.forEach(['client'], { prefix, tags -> " + + "if (tags[prefix + '_local'] == 'true') { tags[prefix + '_id'] = 'local' } })"); + assertNotNull(java); + + assertTrue(java.contains("\"true\".equals(tags.get(prefix + \"_local\"))"), + "Should translate dynamic subscript comparison with .equals()"); + assertTrue(java.contains("tags.put(prefix + \"_id\", \"local\")"), + "Should translate dynamic subscript assignment"); + } + + @Test + void chainedForEach() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.forEach(['a'], { k1, tags -> tags[k1] = 'x' })" + + ".forEach(['b'], { k2, tags -> tags[k2] = 'y' })"); + assertNotNull(java); + + assertTrue(java.contains("(ForEachFunction) (k1, tags)"), + "Should have first forEach with k1"); + assertTrue(java.contains("(ForEachFunction) (k2, tags)"), + "Should have second forEach with k2"); + } + + // ---- Elvis (?:), Safe Navigation (?.), Ternary (? :) ---- + + @Test + void safeNavigation() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags.svc = tags['skywalking_service']?.trim() })"); + assertNotNull(java); + + assertTrue(java.contains("(tags.get(\"skywalking_service\") != null ? tags.get(\"skywalking_service\").trim() : null)"), + "Should translate ?.trim() to null-checked call"); + } + + @Test + void elvisOperator() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags.svc = tags['name'] ?: 'unknown' })"); + assertNotNull(java); + + assertTrue(java.contains("(tags.get(\"name\") != null ? tags.get(\"name\") : \"unknown\")"), + "Should translate ?: to null-check with default"); + } + + @Test + void safeNavPlusElvis() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags.service_name = 'APISIX::'+(tags['skywalking_service']?.trim()?:'APISIX') })"); + assertNotNull(java); + + assertTrue(java.contains("tags.get(\"skywalking_service\") != null ? tags.get(\"skywalking_service\").trim() : null"), + "Should have safe nav for trim"); + assertTrue(java.contains("!= null ?") && java.contains(": \"APISIX\""), + "Should have elvis default to APISIX"); + assertTrue(java.contains("\"APISIX::\" + "), + "Should have string prefix concatenation"); + } + + @Test + void ternaryOperator() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.tag({tags -> tags.service_name = tags.ApiId ? 'gw::'+tags.ApiId : 'gw::'+tags.ApiName })"); + assertNotNull(java); + + assertTrue(java.contains("tags.get(\"ApiId\") != null && !tags.get(\"ApiId\").isEmpty()"), + "Should translate ternary condition as truthiness check"); + assertTrue(java.contains("\"gw::\" + tags.get(\"ApiId\")"), + "Should have true branch expression"); + assertTrue(java.contains("\"gw::\" + tags.get(\"ApiName\")"), + "Should have false branch expression"); + } + + @Test + void safeNavInFilterCondition() { + final String java = transpiler.transpileFilter("MalFilter_test", + "{ tags -> tags.job_name == 'eks-monitoring' && tags.Service?.trim() }"); + assertNotNull(java); + + assertTrue(java.contains("\"eks-monitoring\".equals(tags.get(\"job_name\"))"), + "Should translate == comparison"); + assertTrue(java.contains("tags.get(\"Service\") != null ? tags.get(\"Service\").trim() : null"), + "Should have safe nav for Service?.trim()"); + } + + // ---- instance() with PropertiesExtractor, MapExpression ---- + + @Test + void instanceWithPropertiesExtractor() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.instance(['cluster', 'service'], '::', ['pod'], '', Layer.K8S_SERVICE, " + + "{tags -> ['pod': tags.pod, 'namespace': tags.namespace]})"); + assertNotNull(java); + + assertTrue(java.contains(".instance("), + "Should have instance call"); + assertTrue(java.contains("(PropertiesExtractor)"), + "Should cast closure to PropertiesExtractor"); + assertTrue(java.contains("Map.of(\"pod\", tags.get(\"pod\"), \"namespace\", tags.get(\"namespace\"))"), + "Should translate map literal to Map.of()"); + } + + @Test + void mapExpressionInTagValue() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.instance(['svc'], ['inst'], Layer.GENERAL, {tags -> ['key': tags.val]})"); + assertNotNull(java); + + assertTrue(java.contains("Map.of(\"key\", tags.get(\"val\"))"), + "Should translate single-entry map"); + assertTrue(java.contains("(PropertiesExtractor)"), + "Should have PropertiesExtractor cast"); + } + + @Test + void processRelationNoClosures() { + final String java = transpiler.transpileExpression("MalExpr_test", + "metric.processRelation('side', ['service'], ['instance'], " + + "'client_process_id', 'server_process_id', 'component')"); + assertNotNull(java); + + assertTrue(java.contains(".processRelation(\"side\", List.of(\"service\"), List.of(\"instance\"), " + + "\"client_process_id\", \"server_process_id\", \"component\")"), + "Should translate processRelation as regular method call"); + } + + // ---- Compilation + Manifests ---- + + @Test + void sourceWrittenForCompilation(@TempDir Path tempDir) throws Exception { + final String source = transpiler.transpileExpression("MalExpr_compile_test", + "metric.sum(['host']).service(['svc'], Layer.GENERAL)"); + transpiler.registerExpression("MalExpr_compile_test", source); + + final File sourceDir = tempDir.resolve("src").toFile(); + final File outputDir = tempDir.resolve("classes").toFile(); + + try { + transpiler.compileAll(sourceDir, outputDir, System.getProperty("java.class.path")); + + final String classPath = MalToJavaTranspiler.GENERATED_PACKAGE.replace('.', File.separatorChar) + + File.separator + "MalExpr_compile_test.class"; + assertTrue(new File(outputDir, classPath).exists(), + "Compiled .class file should exist"); + } catch (RuntimeException e) { + if (e.getMessage().contains("compilation failed")) { + final String pkgPath = MalToJavaTranspiler.GENERATED_PACKAGE.replace('.', File.separatorChar); + final File javaFile = new File(sourceDir, pkgPath + "/MalExpr_compile_test.java"); + assertTrue(javaFile.exists(), "Source .java file should be written"); + final String written = Files.readString(javaFile.toPath()); + assertTrue(written.contains("implements MalExpression"), + "Written source should implement MalExpression"); + } else { + throw e; + } + } + } + + @Test + void expressionManifest(@TempDir Path tempDir) throws Exception { + transpiler.registerExpression("MalExpr_a", + transpiler.transpileExpression("MalExpr_a", "metric_a")); + transpiler.registerExpression("MalExpr_b", + transpiler.transpileExpression("MalExpr_b", "metric_b")); + + final File outputDir = tempDir.toFile(); + transpiler.writeExpressionManifest(outputDir); + + final File manifest = new File(outputDir, "META-INF/mal-expressions.txt"); + assertTrue(manifest.exists(), "Manifest file should exist"); + + final List<String> lines = Files.readAllLines(manifest.toPath()); + assertTrue(lines.contains(MalToJavaTranspiler.GENERATED_PACKAGE + ".MalExpr_a"), + "Should contain MalExpr_a FQCN"); + assertTrue(lines.contains(MalToJavaTranspiler.GENERATED_PACKAGE + ".MalExpr_b"), + "Should contain MalExpr_b FQCN"); + } + + @Test + void filterManifest(@TempDir Path tempDir) throws Exception { + final String literal = "{ tags -> tags.job == 'x' }"; + transpiler.registerFilter("MalFilter_0", literal, + transpiler.transpileFilter("MalFilter_0", literal)); + + final File outputDir = tempDir.toFile(); + transpiler.writeFilterManifest(outputDir); + + final File manifest = new File(outputDir, "META-INF/mal-filter-expressions.properties"); + assertTrue(manifest.exists(), "Filter manifest should exist"); + + final String content = Files.readString(manifest.toPath()); + assertTrue(content.contains(MalToJavaTranspiler.GENERATED_PACKAGE + ".MalFilter_0"), + "Should contain MalFilter_0 FQCN"); + } +} diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/pom.xml index 9dca94257f..4039928a50 100644 --- a/oap-server/analyzer/pom.xml +++ b/oap-server/analyzer/pom.xml @@ -33,6 +33,7 @@ <module>log-analyzer</module> <module>meter-analyzer</module> <module>event-analyzer</module> + <module>mal-transpiler</module> </modules> <dependencies>
