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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,32 +103,24 @@ public static Target local(int port, Duration timeout) {
}
}

record Source(
URI uri,
String binaryName,
String contents,
SourceLineMap lineMap,
SourceVariableNames variableNames
) {
record Source(URI uri, com.github.minecraft_ta.totalDebugCompanion.source.SourceDocument document) {
public Source {
Objects.requireNonNull(uri, "uri");
if (!uri.isAbsolute()) {
throw new IllegalArgumentException("Debug source URI must be absolute");
}
if (binaryName == null || binaryName.isBlank()) {
throw new IllegalArgumentException("Debug source binary name must not be blank");
}
Objects.requireNonNull(contents, "contents");
Objects.requireNonNull(lineMap, "lineMap");
Objects.requireNonNull(variableNames, "variableNames");
if (!uri.isAbsolute()) throw new IllegalArgumentException("Debug source URI must be absolute");
Objects.requireNonNull(document);
}
public String binaryName() { return document.binaryName(); }
public String contents() { return document.contents(); }
public SourceLineMap lineMap() { return document.lineMap(); }
public SourceVariableNames variableNames() { return document.variableNames(); }
public Source(URI uri, String binaryName, String contents, SourceLineMap lineMap, SourceVariableNames names) {
this(uri, new com.github.minecraft_ta.totalDebugCompanion.source.SourceDocument(binaryName, contents, lineMap, names, List.of()));
}

public Source(URI uri, String binaryName, String contents, SourceLineMap lineMap) {
this(uri, binaryName, contents, lineMap, SourceVariableNames.empty());
}

public Source(URI uri, String binaryName, String contents) {
this(uri, binaryName, contents, SourceLineMap.empty(), SourceVariableNames.empty());
this(uri, new com.github.minecraft_ta.totalDebugCompanion.source.SourceDocument(binaryName, contents));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,64 +1,27 @@
package com.github.minecraft_ta.totalDebugCompanion.debugger;

import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst;
import com.github.minecraft_ta.totalDebugCompanion.jdt.symbol.CodeSymbol;
import com.github.minecraft_ta.totalDebugCompanion.jdt.symbol.JavaSymbolResolver;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;

import java.util.Objects;
import java.util.Optional;

/** Resolves displayed source lines identically for the editor and remote debugger callers. */
/** Breakpoint policy over the same source scopes used by navigation and MCP. */
public final class DebuggerBreakpointResolver {
private DebuggerBreakpointResolver() {
}

public static Optional<DebugEngine.SourceBreakpoint> resolve(
DebugEngine.Source source, int line, String condition, String hitCondition
) {
return resolve(source, JavaAst.parse("DebuggerBreakpoint", source.contents()),
line, condition, hitCondition);
}

private DebuggerBreakpointResolver() {}
public static Optional<DebugEngine.SourceBreakpoint> resolve(
DebugEngine.Source source, CompilationUnit unit, int line, String condition, String hitCondition
) {
Objects.requireNonNull(source, "source");
Objects.requireNonNull(unit, "unit");
DebugEngine.Source source, int line, String condition, String hitCondition) {
Objects.requireNonNull(source);
if (line < 1 || line > source.contents().lines().count()) {
throw new IllegalArgumentException("Source has no displayed line " + line);
}
if (source.lineMap().isEmpty()) {
return Optional.of(new DebugEngine.SourceBreakpoint(line, condition, hitCondition));
if (source.lineMap().isEmpty()) return Optional.of(new DebugEngine.SourceBreakpoint(line, condition, hitCondition));
var scope = source.document().methodAtLine(line);
if (scope.isEmpty()) {
return source.lineMap().containsDisplayedLine(line)
? Optional.of(new DebugEngine.SourceBreakpoint(line, condition, hitCondition)) : Optional.empty();
}
MethodDeclaration[] selected = new MethodDeclaration[1];
unit.accept(new ASTVisitor() {
@Override
public boolean visit(MethodDeclaration declaration) {
if (selected[0] == null && unit.getLineNumber(declaration.getName().getStartPosition()) == line) {
selected[0] = declaration;
}
return true;
}
});
MethodDeclaration declaration = selected[0];
if (declaration == null) {
if (!source.lineMap().containsDisplayedLine(line)) {
return Optional.empty();
}
return Optional.of(new DebugEngine.SourceBreakpoint(line, condition, hitCondition));
}
int endLine = unit.getLineNumber(declaration.getStartPosition() + declaration.getLength() - 1);
var debuggerLine = source.lineMap().firstMappedDisplayedLine(line, endLine);
var debuggerLine = source.lineMap().firstMappedDisplayedLine(line, scope.get().lastLine());
if (debuggerLine.isEmpty()) return Optional.empty();
var binding = declaration.resolveBinding();
if (binding == null || !(JavaSymbolResolver.trySymbolForBinding(binding) instanceof CodeSymbol.MethodSymbol method)) {
throw new IllegalArgumentException("Cannot resolve the runtime method declared at line " + line);
}
var method = scope.get().method();
return Optional.of(DebugEngine.SourceBreakpoint.methodEntry(line, debuggerLine.getAsInt(),
new DebugEngine.MethodTarget(method.ownerClassName(), method.name(), method.descriptor()),
condition, hitCondition));
new DebugEngine.MethodTarget(method.ownerClassName(), method.name(), method.descriptor()), condition, hitCondition));
}
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
package com.github.minecraft_ta.totalDebugCompanion.debugger;

import com.github.minecraft_ta.totalDebugCompanion.debugger.expression.DebuggerTypeScope;
import com.github.minecraft_ta.totalDebugCompanion.jdt.JdtConfiguration;
import com.github.minecraft_ta.totalDebugCompanion.source.SourceVariableNames;
import com.microsoft.java.debug.core.JavaBreakpointLocation;
import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider;
import com.microsoft.java.debug.core.adapter.SourceType;
import com.microsoft.java.debug.core.protocol.Types;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;

import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/** Source lookup and exact source-line ownership for the Microsoft Java debug adapter. */
final class MicrosoftSourceRegistry implements ISourceLookUpProvider {
Expand Down Expand Up @@ -269,134 +256,12 @@ static URI sourceUri(Types.Source source) {
}
}

private record RegisteredSource(
DebugEngine.Source source,
DebuggerTypeScope typeScope,
List<TypeRegion> typeRegions
) {
private record RegisteredSource(DebugEngine.Source source, DebuggerTypeScope typeScope) {
static RegisteredSource parse(DebugEngine.Source source) {
ASTParser parser = JdtConfiguration.createParser();
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setSource(source.contents().toCharArray());
parser.setStatementsRecovery(true);
CompilationUnit unit = (CompilationUnit) parser.createAST(null);
String parseErrors = Arrays.stream(unit.getProblems())
.filter(org.eclipse.jdt.core.compiler.IProblem::isError)
.map(problem -> "line " + problem.getSourceLineNumber() + ": " + problem.getMessage())
.collect(Collectors.joining("; "));
if (!parseErrors.isEmpty()) {
throw new IllegalArgumentException(
"Unable to parse debugger source " + source.uri() + ": " + parseErrors
);
}

String packageName = unit.getPackage() == null
? packageName(source.binaryName())
: unit.getPackage().getName().getFullyQualifiedName();
Map<AbstractTypeDeclaration, String> names = new HashMap<>();
List<TypeRegion> regions = new ArrayList<>();
unit.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public void preVisit(ASTNode node) {
if (!(node instanceof AbstractTypeDeclaration declaration) || isLocalType(declaration)) {
return;
}
AbstractTypeDeclaration parent = enclosingNamedType(declaration.getParent());
String simpleName = declaration.getName().getIdentifier();
String binaryName;
if (parent != null) {
String parentName = names.get(parent);
if (parentName == null) {
return;
}
binaryName = parentName + "$" + simpleName;
} else if (simpleBinaryName(source.binaryName()).equals(simpleName)) {
binaryName = source.binaryName();
} else {
binaryName = packageName.isBlank() ? simpleName : packageName + "." + simpleName;
}
names.put(declaration, binaryName);
int firstLine = unit.getLineNumber(declaration.getStartPosition());
int lastLine = unit.getLineNumber(
declaration.getStartPosition() + Math.max(0, declaration.getLength() - 1)
);
if (firstLine > 0 && lastLine >= firstLine) {
regions.add(new TypeRegion(firstLine, lastLine, binaryName));
}
}
});
regions.sort(Comparator.comparingInt(TypeRegion::span));
return new RegisteredSource(
source,
DebuggerTypeScope.parse(source),
List.copyOf(regions)
);
}

String binaryNameAt(int line) {
return this.typeRegions.stream()
.filter(region -> region.contains(line))
.map(TypeRegion::binaryName)
.findFirst()
.orElse(this.source.binaryName());
}

List<String> binaryNames() {
List<String> result = new ArrayList<>(this.typeRegions.size() + 1);
result.add(this.source.binaryName());
this.typeRegions.stream()
.map(TypeRegion::binaryName)
.filter(name -> !result.contains(name))
.forEach(result::add);
return result;
}

private static boolean isLocalType(AbstractTypeDeclaration declaration) {
for (ASTNode current = declaration.getParent(); current != null; current = current.getParent()) {
if (current instanceof AbstractTypeDeclaration) {
return false;
}
if (current instanceof MethodDeclaration
|| current instanceof Initializer
|| current instanceof LambdaExpression
|| current instanceof AnonymousClassDeclaration) {
return true;
}
}
return false;
}

private static AbstractTypeDeclaration enclosingNamedType(ASTNode node) {
for (ASTNode current = node; current != null; current = current.getParent()) {
if (current instanceof AbstractTypeDeclaration declaration) {
return declaration;
}
}
return null;
}

private static String packageName(String binaryName) {
String topLevelName = binaryName.substring(0, binaryName.indexOf('$') < 0
? binaryName.length()
: binaryName.indexOf('$'));
int separator = topLevelName.lastIndexOf('.');
return separator < 0 ? "" : topLevelName.substring(0, separator);
}

private static String simpleBinaryName(String binaryName) {
int packageSeparator = binaryName.lastIndexOf('.');
int nestedSeparator = binaryName.lastIndexOf('$');
return binaryName.substring(Math.max(packageSeparator, nestedSeparator) + 1);
}
}

private record TypeRegion(int firstLine, int lastLine, String binaryName) {
boolean contains(int line) {
return line >= this.firstLine && line <= this.lastLine;
}

int span() {
return this.lastLine - this.firstLine;
source.document().binaryNames(); // Validate the source before publishing a registration.
return new RegisteredSource(source, DebuggerTypeScope.from(source.document()));
}
String binaryNameAt(int line) { return source.document().ownerAtLine(line); }
List<String> binaryNames() { return source.document().binaryNames(); }
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package com.github.minecraft_ta.totalDebugCompanion.debugger.expression;

import com.github.minecraft_ta.totalDebugCompanion.debugger.DebugEngine;
import com.github.minecraft_ta.totalDebugCompanion.jdt.JdtConfiguration;
import com.github.minecraft_ta.totalDebugCompanion.source.SourceDocument;

import com.sun.jdi.ReferenceType;
import com.sun.jdi.VirtualMachine;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ImportDeclaration;

import java.util.ArrayList;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -36,30 +32,18 @@ private DebuggerTypeScope(
this.onDemandImports = List.copyOf(onDemandImports);
}

public static DebuggerTypeScope parse(DebugEngine.Source source) {
Objects.requireNonNull(source, "source");
ASTParser parser = JdtConfiguration.createParser();
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setSource(source.contents().toCharArray());
parser.setStatementsRecovery(true);
CompilationUnit unit = (CompilationUnit) parser.createAST(null);

String packageName = unit.getPackage() == null
? packageName(source.binaryName())
: unit.getPackage().getName().getFullyQualifiedName();
public static DebuggerTypeScope from(SourceDocument document) {
Objects.requireNonNull(document);
String packageName = document.packageName().isEmpty() ? packageName(document.binaryName()) : document.packageName();
Map<String, String> singleImports = new LinkedHashMap<>();
List<String> onDemandImports = new ArrayList<>();
for (Object value : unit.imports()) {
ImportDeclaration declaration = (ImportDeclaration) value;
String importedName = declaration.getName().getFullyQualifiedName();
if (declaration.isOnDemand()) {
onDemandImports.add(importedName);
} else {
singleImports.put(simpleName(importedName), importedName);
}
}
List<String> compilerImports = new ArrayList<>();
for (Object declaration : unit.imports()) compilerImports.add(declaration.toString());
for (String imported : document.imports()) {
compilerImports.add("import " + imported + ";\n");
String name = imported.startsWith("static ") ? imported.substring(7) : imported;
if (name.endsWith(".*")) onDemandImports.add(name.substring(0, name.length() - 2));
else singleImports.put(simpleName(name), name);
}
return new DebuggerTypeScope(packageName, singleImports, onDemandImports, compilerImports);
}

Expand Down
Loading