From aff1e5ec63517e38cffc307eca57e24e596b1110 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Thu, 15 Jan 2026 10:52:49 +0100 Subject: [PATCH] Add support of Kotlin's SourceDebugExtension In Kotlin some constructions remap source line beyond of the end of file (ex: stream operations, ...) which leads to incorrect line numbering in SymDB. The Kotlin classfiles contains information about this line remapping using the SourceDebugExtension (JSR-45). We are leveraging this extension to remap correctly the line numbers and send corrected lines to SymDB. We are introducing a SourceRemapper interface with one implementation for Kotlin for now, but keeping open door for other languages like Scala. --- .../CapturedContextInstrumenter.java | 1 + .../instrumentation/Instrumenter.java | 30 +-- .../debugger/symbol/SourceRemapper.java | 51 +++++ .../debugger/symbol/SymbolExtractor.java | 35 +++- .../datadog/debugger/util/JvmLanguage.java | 28 +++ .../debugger/agent/CapturingTestBase.java | 57 ------ .../datadog/debugger/agent/KotlinHelper.java | 62 ++++++ .../debugger/symbol/SourceRemapperTest.java | 31 +++ .../SymbolExtractionTransformerTest.java | 187 ++++++++++++++---- .../src/test/resources/CapturedSnapshot301.kt | 7 + .../debugger/symboltest/SymbolExtraction16.kt | 26 +++ 11 files changed, 379 insertions(+), 136 deletions(-) create mode 100644 dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SourceRemapper.java create mode 100644 dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/JvmLanguage.java create mode 100644 dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/KotlinHelper.java create mode 100644 dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SourceRemapperTest.java create mode 100644 dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/symboltest/SymbolExtraction16.kt diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumenter.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumenter.java index 2bdbfa8a684..eb18f813f78 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumenter.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumenter.java @@ -30,6 +30,7 @@ import com.datadog.debugger.probe.Where; import com.datadog.debugger.sink.Snapshot; import com.datadog.debugger.util.ClassFileLines; +import com.datadog.debugger.util.JvmLanguage; import datadog.trace.api.Config; import datadog.trace.bootstrap.debugger.Limits; import datadog.trace.bootstrap.debugger.MethodLocation; diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Instrumenter.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Instrumenter.java index 2a7408c0843..67146ec6561 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Instrumenter.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Instrumenter.java @@ -9,6 +9,7 @@ import com.datadog.debugger.instrumentation.DiagnosticMessage.Kind; import com.datadog.debugger.probe.ProbeDefinition; import com.datadog.debugger.util.ClassFileLines; +import com.datadog.debugger.util.JvmLanguage; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -69,7 +70,7 @@ public Instrumenter( argOffset += t.getSize(); } localVarsBySlotArray = extractLocalVariables(argTypes); - this.language = JvmLanguage.of(classNode); + this.language = JvmLanguage.of(classNode.sourceFile); } public abstract InstrumentationResult.Status instrument(); @@ -295,31 +296,4 @@ public FinallyBlock(LabelNode startLabel, LabelNode endLabel, LabelNode handlerL this.handlerLabel = handlerLabel; } } - - protected enum JvmLanguage { - JAVA, - KOTLIN, - SCALA, - GROOVY, - UNKNOWN; - - public static JvmLanguage of(ClassNode classNode) { - if (classNode.sourceFile == null) { - return UNKNOWN; - } - if (classNode.sourceFile.endsWith(".java")) { - return JAVA; - } - if (classNode.sourceFile.endsWith(".kt")) { - return KOTLIN; - } - if (classNode.sourceFile.endsWith(".scala")) { - return SCALA; - } - if (classNode.sourceFile.endsWith(".groovy")) { - return GROOVY; - } - return UNKNOWN; - } - } } diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SourceRemapper.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SourceRemapper.java new file mode 100644 index 00000000000..efedbb0d818 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SourceRemapper.java @@ -0,0 +1,51 @@ +package com.datadog.debugger.symbol; + +import com.datadog.debugger.util.JvmLanguage; +import datadog.trace.agent.tooling.stratum.SourceMap; +import datadog.trace.agent.tooling.stratum.StratumExt; +import datadog.trace.api.Pair; + +public interface SourceRemapper { + + int remapSourceLine(int line); + + static SourceRemapper getSourceRemapper(String sourceFile, SourceMap sourceMap) { + JvmLanguage jvmLanguage = JvmLanguage.of(sourceFile); + switch (jvmLanguage) { + case KOTLIN: + StratumExt stratum = sourceMap.getStratum("KotlinDebug"); + if (stratum == null) { + throw new IllegalArgumentException("No stratum found for KotlinDebug"); + } + return new KotlinSourceRemapper(stratum); + default: + return NOOP_REMAPPER; + } + } + + SourceRemapper NOOP_REMAPPER = new NoopSourceRemapper(); + + class NoopSourceRemapper implements SourceRemapper { + @Override + public int remapSourceLine(int line) { + return line; + } + } + + class KotlinSourceRemapper implements SourceRemapper { + private final StratumExt stratum; + + public KotlinSourceRemapper(StratumExt stratum) { + this.stratum = stratum; + } + + @Override + public int remapSourceLine(int line) { + Pair pair = stratum.getInputLine(line); + if (pair == null || pair.getRight() == null) { + return line; + } + return pair.getRight(); + } + } +} diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractor.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractor.java index 712e6e7c1fc..1e9f5e08358 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractor.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractor.java @@ -5,6 +5,8 @@ import static com.datadog.debugger.instrumentation.ASMHelper.sortLocalVariables; import com.datadog.debugger.instrumentation.ASMHelper; +import datadog.trace.agent.tooling.stratum.SourceMap; +import datadog.trace.agent.tooling.stratum.parser.Parser; import datadog.trace.util.Strings; import java.util.ArrayList; import java.util.Collection; @@ -28,9 +30,11 @@ import org.objectweb.asm.tree.LineNumberNode; import org.objectweb.asm.tree.LocalVariableNode; import org.objectweb.asm.tree.MethodNode; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SymbolExtractor { + private static final Logger LOGGER = LoggerFactory.getLogger(SymbolExtractor.class); public static Scope extract(byte[] classFileBuffer, String jarName) { ClassNode classNode = parseClassFile(classFileBuffer); @@ -40,7 +44,16 @@ public static Scope extract(byte[] classFileBuffer, String jarName) { private static Scope extractScopes(ClassNode classNode, String jarName) { try { String sourceFile = extractSourceFile(classNode); - List methodScopes = extractMethods(classNode, sourceFile); + SourceRemapper sourceRemapper = SourceRemapper.NOOP_REMAPPER; + if (classNode.sourceDebug != null) { + List sourceMaps = Parser.parse(classNode.sourceDebug); + if (sourceMaps.isEmpty()) { + throw new IllegalStateException("No source maps found for " + classNode.name); + } + SourceMap sourceMap = sourceMaps.get(0); + sourceRemapper = SourceRemapper.getSourceRemapper(classNode.sourceFile, sourceMap); + } + List methodScopes = extractMethods(classNode, sourceFile, sourceRemapper); int classStartLine = Integer.MAX_VALUE; int classEndLine = 0; for (Scope scope : methodScopes) { @@ -67,9 +80,8 @@ private static Scope extractScopes(ClassNode classNode, String jarName) { .scopes(new ArrayList<>(Collections.singletonList(classScope))) .build(); } catch (Exception ex) { - LoggerFactory.getLogger(SymbolExtractor.class) - .debug( - "Extracting scopes for class[{}] in jar[{}] failed: ", classNode.name, jarName, ex); + LOGGER.debug( + "Extracting scopes for class[{}] in jar[{}] failed: ", classNode.name, jarName, ex); return null; } } @@ -102,10 +114,11 @@ private static List extractFields(ClassNode classNode) { return fields; } - private static List extractMethods(ClassNode classNode, String sourceFile) { + private static List extractMethods( + ClassNode classNode, String sourceFile, SourceRemapper sourceRemapper) { List methodScopes = new ArrayList<>(); for (MethodNode method : classNode.methods) { - MethodLineInfo methodLineInfo = extractMethodLineInfo(method); + MethodLineInfo methodLineInfo = extractMethodLineInfo(method, sourceRemapper); List varScopes = new ArrayList<>(); List methodSymbols = new ArrayList<>(); int localVarBaseSlot = extractArgs(method, methodSymbols, methodLineInfo.start); @@ -464,7 +477,8 @@ static List buildRanges(List sortedLineNo) { return ranges; } - private static MethodLineInfo extractMethodLineInfo(MethodNode methodNode) { + private static MethodLineInfo extractMethodLineInfo( + MethodNode methodNode, SourceRemapper sourceRemapper) { Map map = new HashMap<>(); List lineNo = new ArrayList<>(); Set dedupSet = new HashSet<>(); @@ -473,10 +487,11 @@ private static MethodLineInfo extractMethodLineInfo(MethodNode methodNode) { while (node != null) { if (node.getType() == AbstractInsnNode.LINE) { LineNumberNode lineNumberNode = (LineNumberNode) node; - if (dedupSet.add(lineNumberNode.line)) { - lineNo.add(lineNumberNode.line); + int newLine = sourceRemapper.remapSourceLine(lineNumberNode.line); + if (dedupSet.add(newLine)) { + lineNo.add(newLine); } - maxLine = Math.max(lineNumberNode.line, maxLine); + maxLine = Math.max(newLine, maxLine); } if (node.getType() == AbstractInsnNode.LABEL) { if (node instanceof LabelNode) { diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/JvmLanguage.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/JvmLanguage.java new file mode 100644 index 00000000000..38dd63da0d7 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/JvmLanguage.java @@ -0,0 +1,28 @@ +package com.datadog.debugger.util; + +public enum JvmLanguage { + JAVA, + KOTLIN, + SCALA, + GROOVY, + UNKNOWN; + + public static JvmLanguage of(String sourceFile) { + if (sourceFile == null) { + return UNKNOWN; + } + if (sourceFile.endsWith(".java")) { + return JAVA; + } + if (sourceFile.endsWith(".kt")) { + return KOTLIN; + } + if (sourceFile.endsWith(".scala")) { + return SCALA; + } + if (sourceFile.endsWith(".groovy")) { + return GROOVY; + } + return UNKNOWN; + } +} diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturingTestBase.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturingTestBase.java index d1d5b9ff113..3af160c2e09 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturingTestBase.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturingTestBase.java @@ -25,30 +25,18 @@ import datadog.trace.bootstrap.debugger.ProbeId; import datadog.trace.bootstrap.debugger.ProbeRateLimiter; import datadog.trace.bootstrap.debugger.util.Redaction; -import java.io.File; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import net.bytebuddy.agent.ByteBuddyAgent; -import org.jetbrains.kotlin.cli.common.ExitCode; -import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments; -import org.jetbrains.kotlin.cli.common.messages.MessageRenderer; -import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector; -import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler; -import org.jetbrains.kotlin.config.Services; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -415,49 +403,4 @@ public void instrumentationResult(ProbeDefinition definition, InstrumentationRes results.put(definition.getId(), result); } } - - static class KotlinHelper { - public static Class compileAndLoad( - String className, String sourceFileName, List outputFilesToDelete) { - K2JVMCompiler compiler = new K2JVMCompiler(); - K2JVMCompilerArguments args = compiler.createArguments(); - args.setFreeArgs(Collections.singletonList(sourceFileName)); - String compilerOutputDir = "/tmp/" + CapturedSnapshotTest.class.getSimpleName() + "-kotlin"; - args.setDestination(compilerOutputDir); - args.setClasspath(System.getProperty("java.class.path")); - ExitCode exitCode = - compiler.exec( - new PrintingMessageCollector(System.out, MessageRenderer.WITHOUT_PATHS, true), - Services.EMPTY, - args); - - if (exitCode.getCode() != 0) { - throw new RuntimeException("Kotlin compilation failed"); - } - File compileOutputDirFile = new File(compilerOutputDir); - try { - URLClassLoader urlClassLoader = - new URLClassLoader(new URL[] {compileOutputDirFile.toURI().toURL()}); - return urlClassLoader.loadClass(className); - } catch (Exception ex) { - throw new RuntimeException(ex); - } finally { - registerFilesToDeleteDir(compileOutputDirFile, outputFilesToDelete); - } - } - - public static void registerFilesToDeleteDir(File dir, List outputFilesToDelete) { - if (!dir.exists()) { - return; - } - try { - Files.walk(dir.toPath()) - .sorted(Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(outputFilesToDelete::add); - } catch (IOException ex) { - ex.printStackTrace(); - } - } - } } diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/KotlinHelper.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/KotlinHelper.java new file mode 100644 index 00000000000..e389cd951b7 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/KotlinHelper.java @@ -0,0 +1,62 @@ +package com.datadog.debugger.agent; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import org.jetbrains.kotlin.cli.common.ExitCode; +import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments; +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer; +import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector; +import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler; +import org.jetbrains.kotlin.config.Services; + +public class KotlinHelper { + public static Class compileAndLoad( + String className, String sourceFileName, List outputFilesToDelete) { + K2JVMCompiler compiler = new K2JVMCompiler(); + K2JVMCompilerArguments args = compiler.createArguments(); + args.setFreeArgs(Collections.singletonList(sourceFileName)); + String compilerOutputDir = "/tmp/" + CapturedSnapshotTest.class.getSimpleName() + "-kotlin"; + args.setDestination(compilerOutputDir); + args.setClasspath(System.getProperty("java.class.path")); + ExitCode exitCode = + compiler.exec( + new PrintingMessageCollector(System.out, MessageRenderer.WITHOUT_PATHS, true), + Services.EMPTY, + args); + + if (exitCode.getCode() != 0) { + throw new RuntimeException("Kotlin compilation failed"); + } + File compileOutputDirFile = new File(compilerOutputDir); + try { + URLClassLoader urlClassLoader = + new URLClassLoader(new URL[] {compileOutputDirFile.toURI().toURL()}); + return urlClassLoader.loadClass(className); + } catch (Exception ex) { + throw new RuntimeException(ex); + } finally { + registerFilesToDeleteDir(compileOutputDirFile, outputFilesToDelete); + } + } + + public static void registerFilesToDeleteDir(File dir, List outputFilesToDelete) { + if (!dir.exists()) { + return; + } + try { + Files.walk(dir.toPath()) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(outputFilesToDelete::add); + } catch (IOException ex) { + ex.printStackTrace(); + } + } +} diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SourceRemapperTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SourceRemapperTest.java new file mode 100644 index 00000000000..b40b39a43e3 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SourceRemapperTest.java @@ -0,0 +1,31 @@ +package com.datadog.debugger.symbol; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.trace.agent.tooling.stratum.SourceMap; +import datadog.trace.agent.tooling.stratum.StratumExt; +import datadog.trace.api.Pair; +import org.junit.jupiter.api.Test; + +class SourceRemapperTest { + + @Test + public void noopSourceRemapper() { + assertEquals(SourceRemapper.NOOP_REMAPPER, SourceRemapper.getSourceRemapper("foo.java", null)); + assertEquals(SourceRemapper.NOOP_REMAPPER, SourceRemapper.getSourceRemapper("foo.dat", null)); + } + + @Test + public void kotlinSourceRemapper() { + SourceMap sourceMapMock = mock(SourceMap.class); + StratumExt stratumMock = mock(StratumExt.class); + when(sourceMapMock.getStratum(eq("KotlinDebug"))).thenReturn(stratumMock); + when(stratumMock.getInputLine(eq(42))).thenReturn(Pair.of("", 24)); + SourceRemapper sourceRemapper = SourceRemapper.getSourceRemapper("foo.kt", sourceMapMock); + assertTrue(sourceRemapper instanceof SourceRemapper.KotlinSourceRemapper); + assertEquals(24, sourceRemapper.remapSourceLine(42)); + } +} diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SymbolExtractionTransformerTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SymbolExtractionTransformerTest.java index 33a020bfaac..5ded8891352 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SymbolExtractionTransformerTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SymbolExtractionTransformerTest.java @@ -2,32 +2,40 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toSet; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import static utils.InstrumentationTestHelper.compileAndLoadClass; +import com.datadog.debugger.agent.CapturedSnapshotTest; +import com.datadog.debugger.agent.KotlinHelper; import com.datadog.debugger.sink.SymbolSink; import com.datadog.debugger.util.ClassNameFiltering; import datadog.trace.api.Config; +import java.io.File; import java.io.IOException; +import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.net.URISyntaxException; +import java.net.URL; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; import net.bytebuddy.agent.ByteBuddyAgent; import org.joor.Reflect; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; -import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; import org.mockito.Mockito; @@ -47,9 +55,10 @@ class SymbolExtractionTransformerTest { "org.omg.", "org.joor.", "com.datadog.debugger.") - .collect(Collectors.toSet()); + .collect(toSet()); private Instrumentation instr = ByteBuddyAgent.install(); + private ClassFileTransformer currentTransformer; private Config config; @BeforeEach @@ -58,6 +67,11 @@ public void setUp() { when(config.getFinalDebuggerSymDBUrl()).thenReturn("http://localhost:8126/symdb/v1/input"); } + @AfterEach + public void tearDown() { + instr.removeTransformer(currentTransformer); + } + @Test @DisabledIf( value = "datadog.environment.JavaVirtualMachine#isJ9", @@ -66,8 +80,8 @@ public void symbolExtraction01() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction01"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction01.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -135,8 +149,8 @@ public void symbolExtraction02() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction02"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction02.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -166,8 +180,8 @@ public void symbolExtraction03() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction03"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction03.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -237,8 +251,8 @@ public void symbolExtraction04() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction04"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction04.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -312,8 +326,8 @@ public void symbolExtraction05() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction05"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction05.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -361,8 +375,8 @@ public void symbolExtraction06() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction06"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction06.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -410,8 +424,8 @@ public void symbolExtraction07() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction07"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction07.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -445,8 +459,8 @@ public void symbolExtraction08() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction08"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction08.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -482,8 +496,8 @@ public void symbolExtraction09() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction09"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction09.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -581,8 +595,8 @@ public void symbolExtraction10() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction10"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction10.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock, 2); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock, 2, TRANSFORMER_EXCLUDES); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); assertEquals(2, symbolSinkMock.jarScopes.get(0).getScopes().size()); @@ -637,8 +651,8 @@ public void symbolExtraction11() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction11"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction11.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", 1).get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -674,8 +688,8 @@ public void symbolExtraction12() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction12"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction12.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", 1).get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -755,8 +769,8 @@ public void symbolExtraction12() throws IOException, URISyntaxException { public void symbolExtraction13() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction13"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -819,8 +833,8 @@ public void symbolExtraction13() throws IOException, URISyntaxException { public void symbolExtraction14() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction14"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -900,7 +914,7 @@ public void symbolExtraction14() throws IOException, URISyntaxException { } @Test - @EnabledOnJre({JRE.JAVA_17, JRE.JAVA_21, JRE.JAVA_24}) + @EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_25) @DisabledIf( value = "datadog.environment.JavaVirtualMachine#isJ9", disabledReason = "Flaky on J9 JVMs") @@ -908,8 +922,8 @@ public void symbolExtraction15() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction15"; final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction15.java"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME, "17"); Reflect.on(testClass).call("main", "1").get(); Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); @@ -953,6 +967,90 @@ public void symbolExtraction15() throws IOException, URISyntaxException { assertScope(ageMethodScope, ScopeType.METHOD, "age", 10, 10, SOURCE_FILE, 0, 0); } + @Test + @EnabledForJreRange(max = JRE.JAVA_25) + @DisabledIf( + value = "datadog.environment.JavaVirtualMachine#isJ9", + disabledReason = "Flaky on J9 JVMs") + public void symbolExtraction16() throws IOException, URISyntaxException { + final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction16"; + final String SOURCE_FILE = SYMBOL_PACKAGE_DIR + "SymbolExtraction16.kt"; + SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); + Set additionalExcludedPackages = + Stream.of( + "org.jetbrains.", + "kotlin.", + "kotlinx.", + "org.junit.", + "io.vavr.", + "com.intellij.", + "gnu.trove.") + .collect(toSet()); + currentTransformer = createTransformer(symbolSinkMock, additionalExcludedPackages); + instr.addTransformer(currentTransformer); + URL resource = CapturedSnapshotTest.class.getResource("/" + SOURCE_FILE); + assertNotNull(resource); + List filesToDelete = new ArrayList<>(); + try { + Class testClass = + KotlinHelper.compileAndLoad(CLASS_NAME, resource.getFile(), filesToDelete); + Object companion = Reflect.onClass(testClass).get("Companion"); + int result = Reflect.on(companion).call("main", "").get(); + assertEquals(48, result); + } finally { + filesToDelete.forEach(File::delete); + } + assertEquals(2, symbolSinkMock.jarScopes.size()); + Scope classScope = symbolSinkMock.jarScopes.get(0).getScopes().get(0); + assertScope(classScope, ScopeType.CLASS, CLASS_NAME, 0, 17, SOURCE_FILE, 4, 1); + assertLangSpecifics( + classScope.getLanguageSpecifics(), + asList("public", "final"), + asList("@kotlin.Metadata"), + "java.lang.Object", + null, + null); + assertSymbol( + classScope.getSymbols().get(0), + SymbolType.STATIC_FIELD, + "Companion", + CLASS_NAME + "$Companion", + 0); + assertScope(classScope.getScopes().get(0), ScopeType.METHOD, "", 3, 3, SOURCE_FILE, 0, 0); + Scope f1MethodScope = classScope.getScopes().get(1); + assertScope(f1MethodScope, ScopeType.METHOD, "f1", 6, 6, SOURCE_FILE, 0, 1); + assertSymbol( + f1MethodScope.getSymbols().get(0), SymbolType.ARG, "value", Integer.TYPE.getTypeName(), 6); + Scope f2MethodScope = classScope.getScopes().get(2); + assertScope(f2MethodScope, ScopeType.METHOD, "f2", 10, 17, SOURCE_FILE, 3, 1); + assertLineRanges(f2MethodScope, "10-10", "12-12", "14-14", "16-17"); + assertScope( + classScope.getScopes().get(3), ScopeType.METHOD, "", 0, 0, SOURCE_FILE, 0, 0); + + Scope companionClassScope = symbolSinkMock.jarScopes.get(1).getScopes().get(0); + assertScope( + companionClassScope, ScopeType.CLASS, CLASS_NAME + "$Companion", 0, 23, SOURCE_FILE, 3, 0); + assertLangSpecifics( + classScope.getLanguageSpecifics(), + asList("public", "final"), + asList("@kotlin.Metadata"), + "java.lang.Object", + null, + null); + assertScope( + companionClassScope.getScopes().get(0), + ScopeType.METHOD, + "", + 20, + 20, + SOURCE_FILE, + 0, + 0); + Scope mainMethodScope = companionClassScope.getScopes().get(1); + assertScope(mainMethodScope, ScopeType.METHOD, "main", 22, 23, SOURCE_FILE, 1, 1); + assertLineRanges(mainMethodScope, "22-23"); + } + @Test public void filterOutClassesFromExcludedPackages() throws IOException, URISyntaxException { config = Mockito.mock(Config.class); @@ -961,11 +1059,11 @@ public void filterOutClassesFromExcludedPackages() throws IOException, URISyntax SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); ClassNameFiltering classNameFiltering = new ClassNameFiltering(Collections.singleton(EXCLUDED_PACKAGE)); - SymbolExtractionTransformer transformer = + currentTransformer = new SymbolExtractionTransformer( new SymbolAggregator(classNameFiltering, emptyList(), symbolSinkMock, 1), classNameFiltering); - instr.addTransformer(transformer); + instr.addTransformer(currentTransformer); Class testClass = compileAndLoadClass(CLASS_NAME); Reflect.on(testClass).call("main", "1").get(); assertFalse( @@ -981,8 +1079,8 @@ public void filterOutClassesFromExcludedPackages() throws IOException, URISyntax public void duplicateClassThroughDifferentClassLoader() throws IOException, URISyntaxException { final String CLASS_NAME = SYMBOL_PACKAGE + "SymbolExtraction01"; SymbolSinkMock symbolSinkMock = new SymbolSinkMock(config); - SymbolExtractionTransformer transformer = createTransformer(symbolSinkMock); - instr.addTransformer(transformer); + currentTransformer = createTransformer(symbolSinkMock); + instr.addTransformer(currentTransformer); for (int i = 0; i < 10; i++) { // compile and load the class in a specific ClassLoader each time Class testClass = compileAndLoadClass(CLASS_NAME); @@ -1069,16 +1167,23 @@ private void assertLineRanges(Scope scope, String... expectedRanges) { } private SymbolExtractionTransformer createTransformer(SymbolSink symbolSink) { - return createTransformer(symbolSink, 1); + return createTransformer(symbolSink, 1, TRANSFORMER_EXCLUDES); + } + + private SymbolExtractionTransformer createTransformer( + SymbolSinkMock symbolSinkMock, Set additionalExcludedPackages) { + Set excludedPackages = new HashSet<>(TRANSFORMER_EXCLUDES); + excludedPackages.addAll(additionalExcludedPackages); + return createTransformer(symbolSinkMock, 1, excludedPackages); } private SymbolExtractionTransformer createTransformer( - SymbolSink symbolSink, int symbolFlushThreshold) { + SymbolSink symbolSink, int symbolFlushThreshold, Set excludedPackages) { return createTransformer( symbolSink, symbolFlushThreshold, new ClassNameFiltering( - TRANSFORMER_EXCLUDES, Collections.singleton(SYMBOL_PACKAGE), Collections.emptySet())); + excludedPackages, Collections.singleton(SYMBOL_PACKAGE), Collections.emptySet())); } private SymbolExtractionTransformer createTransformer( diff --git a/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot301.kt b/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot301.kt index 36a4492e181..74d82639acc 100644 --- a/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot301.kt +++ b/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot301.kt @@ -5,6 +5,13 @@ class CapturedSnapshot301 { } fun f2(value: Int): Int { + (1..3) + .filter { + it > 0 + } + .forEach { + println(it) + } return value } diff --git a/dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/symboltest/SymbolExtraction16.kt b/dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/symboltest/SymbolExtraction16.kt new file mode 100644 index 00000000000..47d04e74929 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/test/resources/com/datadog/debugger/symboltest/SymbolExtraction16.kt @@ -0,0 +1,26 @@ +package com.datadog.debugger.symboltest; + +class SymbolExtraction16 { + + fun f1(value: Int): Int { + return value // beae1817-f3b0-4ea8-a74f-000000000001 + } + + fun f2(value: Int): Int { + (1..3) + // filter in positive + .filter { it > 0 } + // filter out negative + .filterNot { it < 0 } + // print numbers + .forEach { println(it) } + return value + } + + companion object { + fun main(arg: String): Int { + val c = SymbolExtraction16() + return c.f1(31) + c.f2(17) + } + } +}