import com.github.javaparser.Position;
import com.github.javaparser.Range;
import spoon.reflect.code.*;
import spoon.reflect.cu.SourcePosition;
import spoon.reflect.cu.position.NoSourcePosition;
import spoon.reflect.declaration.*;
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.visitor.CtScanner;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
public class DocumentIndexer {
private String projectRoot;
private boolean contents;
private String pathname;
private CtElement spoonElement;
private String projectId;
private Emitter emitter;
private Map indexers;
private String documentId;
private Set rangeIds = new HashSet<>();
private Map definitions = new HashMap<>();
public DocumentIndexer(
String projectRoot,
boolean contents,
String pathname,
CtElement spoonElement,
String projectId,
Emitter emitter,
Map indexers
) {
this.projectRoot = projectRoot;
this.contents = contents;
this.pathname = pathname;
this.spoonElement = spoonElement;
this.projectId = projectId;
this.emitter = emitter;
this.indexers = indexers;
}
public int numDefinitions() {
return definitions.size();
}
public void visitDefinitions() {
this.spoonElement.accept(new DefinitionsVisitor());
}
public void visitReferences() {
this.spoonElement.accept(new ReferencesVisitor());
}
public void preIndex() {
Map args = Util.mapOf(
"languageId", "java",
"uri", String.format("file://%s", Paths.get(pathname).toAbsolutePath().toString())
);
if (contents) {
try {
List lines = Files.readAllLines(Paths.get(pathname), StandardCharsets.UTF_8);
args = Util.union(args, Util.mapOf("contents", String.join("\n", lines)));
} catch (IOException ex) {
throw new RuntimeException(String.format("Failed to read file %s", pathname));
}
}
this.documentId = emitter.emitVertex("document", args);
}
public void postIndex() {
for (DefinitionMeta meta : definitions.values()) {
linkUses(meta, documentId);
}
emitter.emitEdge("contains", Util.mapOf("outV", projectId, "inVs", new String[]{documentId}));
emitter.emitEdge("contains", Util.mapOf("outV", documentId, "inVs", rangeIds.stream().sorted().toArray()));
}
private void linkUses(DefinitionMeta meta, String documentId) {
String resultId = emitter.emitVertex("referenceResult", Util.mapOf());
emitter.emitEdge("textDocument/references", Util.mapOf("outV", meta.resultSetId, "inV", resultId));
emitter.emitEdge("item", Util.mapOf(
"property", "definitions",
"outV", resultId,
"inVs", new String[]{meta.rangeId},
"document", documentId
));
for (Map.Entry> entry : meta.referenceRangeIds.entrySet()) {
emitter.emitEdge("item", Util.mapOf(
"property", "references",
"outV", resultId,
"inVs", entry.getValue().stream().sorted().toArray(),
"document", entry.getKey()
));
}
}
// TODO add support for more language constructs:
// https://github.com/INRIA/spoon/blob/master/src/main/java/spoon/reflect/visitor/CtScanner.java
private class DefinitionsVisitor extends CtScanner {
@Override
public void visitCtParameter(CtParameter el) {
super.visitCtParameter(el);
if (el.getPosition() instanceof NoSourcePosition) {
return;
}
emitDefinition(mkRange(el.getPosition()), mkDoc(el.getType(), el.getDocComment()));
}
@Override
public void visitCtLocalVariable(CtLocalVariable el) {
super.visitCtLocalVariable(el);
emitDefinition(mkRange(el.getPosition()), mkDoc(el.getType(), el.getDocComment()));
}
@Override
public void visitCtCatchVariable(CtCatchVariable el) {
super.visitCtCatchVariable(el);
emitDefinition(mkRange(el.getPosition()), mkDoc(el.getType(), el.getDocComment()));
}
@Override
public void visitCtMethod(CtMethod el) {
super.visitCtMethod(el);
emitDefinition(nameRange(el), mkDoc(el.getType(), el.getDocComment()));
}
@Override
public void visitCtField(CtField el) {
super.visitCtField(el);
emitDefinition(nameRange(el), mkDoc(el.getType(), el.getDocComment()));
}
@Override
public void visitCtClass(CtClass el) {
super.visitCtClass(el);
emitDefinition(nameRange(el), el.getDocComment());
}
private void emitDefinition(Range range, String doc) {
System.out.println("DEF " + pathname + ":" + humanRange(range));
String hoverId = emitter.emitVertex("hoverResult", Util.mapOf(
"result", Util.mapOf(
"contents", Util.mapOf(
"kind", "markdown",
"value", doc
)
)
));
String resultSetId = emitter.emitVertex("resultSet", Util.mapOf());
emitter.emitEdge("textDocument/hover", Util.mapOf("outV", resultSetId, "inV", hoverId));
String rangeId = emitter.emitVertex("range", createRange(range));
emitter.emitEdge("next", Util.mapOf("outV", rangeId, "inV", resultSetId));
rangeIds.add(rangeId);
definitions.put(range, new DefinitionMeta(rangeId, resultSetId)); // + contents?
}
}
private class ReferencesVisitor extends CtScanner {
@Override
public void visitCtVariableRead(CtVariableRead el) {
super.visitCtVariableRead(el);
if (el.getVariable().getDeclaration() == null) {
return;
}
if (el.getPosition() instanceof NoSourcePosition) {
return;
}
emitUse(
mkRange(el.getPosition()),
mkRange(el.getVariable().getDeclaration().getPosition()),
el.getVariable().getDeclaration().getPosition().getFile().getPath()
);
}
@Override
public void visitCtInvocation(CtInvocation el) {
super.visitCtInvocation(el);
if (el.getPosition() instanceof NoSourcePosition) {
return;
}
if (el.getExecutable() == null || el.getExecutable().getDeclaration() == null || el.getExecutable().getDeclaration().getPosition() instanceof NoSourcePosition) {
return;
}
Range useRange = identifierRange(el, el.getExecutable().getSimpleName());
emitUse(
useRange,
nameRange(el.getExecutable().getDeclaration()),
el.getExecutable().getDeclaration().getPosition().getFile().getPath()
);
}
@Override
public void visitCtFieldRead(CtFieldRead el) {
super.visitCtFieldRead(el);
if (el.getPosition() instanceof NoSourcePosition) {
return;
}
Range useRange = identifierRange(el, el.getVariable().getSimpleName());
CtField decl = el.getVariable().getDeclaration();
if (decl == null) {
return;
}
emitUse(
useRange,
nameRange(decl),
decl.getPosition().getFile().getPath()
);
}
@Override
public void visitCtTypeReference(CtTypeReference el) {
super.visitCtTypeReference(el);
if (el.getPosition() instanceof NoSourcePosition) {
return;
}
CtType decl = el.getDeclaration();
if (decl == null) {
return;
}
emitUse(
mkRange(el.getPosition()),
nameRange(decl),
decl.getPosition().getFile().getPath()
);
}
private void emitUse(Range use, Range def, String defPath) {
DocumentIndexer indexer = indexers.get(defPath);
String link = pathname + ":" + humanRange(use) + " -> " + defPath + ":" + humanRange(def);
System.out.println("Linking use to definition: " + link);
DefinitionMeta meta = indexer.definitions.get(def);
if (meta == null) {
System.out.println("WARNING Skipping linking use to definition: " + link);
return;
}
String rangeId = emitter.emitVertex("range", createRange(use));
emitter.emitEdge("next", Util.mapOf("outV", rangeId, "inV", meta.resultSetId));
if (meta.definitionResultId == null) {
String resultId = emitter.emitVertex("definitionResult", Util.mapOf());
emitter.emitEdge("textDocument/definition", Util.mapOf("outV", meta.resultSetId, "inV", resultId));
meta.definitionResultId = resultId;
}
emitter.emitEdge("item", Util.mapOf(
"outV", meta.definitionResultId,
"inVs", new String[]{meta.rangeId},
"document", indexer.documentId
));
rangeIds.add(rangeId);
Set set = meta.referenceRangeIds.getOrDefault(documentId, new HashSet<>());
set.add(rangeId);
meta.referenceRangeIds.put(documentId, set);
}
}
private Range nameRange(CtNamedElement a) {
return nameRange(a.getPosition(), a.getSimpleName());
}
private Range nameRange(SourcePosition a, String name) {
return Range.range(
a.getLine(),
a.getColumn(),
a.getLine(),
a.getColumn() + name.length()
);
}
private Range nameRange(Range a, String name) {
return Range.range(
a.begin.line,
a.begin.column,
a.end.line,
a.end.column + name.length()
);
}
private String mkDoc(CtTypeReference t, String docComment) {
return "```java\n" + t + "\n```" + (docComment.equals("") ? "" : "\n---\n" + docComment);
}
private String humanRange(Range r) {
return r.begin.line + ":" + r.begin.column + "-" + r.end.line + ":" + r.end.column;
}
private Range mkRange(SourcePosition pos) {
return Range.range(pos.getLine(), pos.getColumn(), pos.getEndLine(), pos.getEndColumn());
}
private Map createRange(Range range) {
return Util.mapOf("start", createPosition(range.begin), "end", createPosition(range.end));
}
private Map createPosition(Position position) {
return Util.mapOf("line", position.line - 1, "character", position.column - 1);
}
private Range identifierRange(CtTargetedExpression a, String b) {
// Ugh, we only want the range for the identifier, not the whole expression.
// This will probably break on fields/methods that are spread across lines.
// +1 for `.`, +1 because (I'm guessing) end is inclusive
if (a.getTarget() == null || a.getTarget().getPosition() instanceof NoSourcePosition) {
return mkRange(a.getPosition());
}
SourcePosition p = a.getTarget().getPosition();
return Range.range(p.getEndLine(), p.getEndColumn() + 1 + 1, p.getEndLine(), p.getEndColumn() + 1 + 1 + b.length());
}
}