Skip to content

Commit a9bf7b2

Browse files
authored
Share document accumulator between Java/Kotlin plugins (#900)
1 parent 68ae888 commit a9bf7b2

5 files changed

Lines changed: 118 additions & 150 deletions

File tree

semanticdb-javac/src/main/java/com/sourcegraph/semanticdb_javac/SemanticdbTaskListener.java

Lines changed: 26 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.sourcegraph.semanticdb_javac;
22

33
import com.sourcegraph.semanticdb.Semanticdb;
4+
import com.sourcegraph.semanticdb.SemanticdbDocumentBuilder;
45
import com.sourcegraph.semanticdb.SemanticdbPaths;
56
import com.sourcegraph.semanticdb.SemanticdbWriter;
67

@@ -12,15 +13,16 @@
1213
import javax.lang.model.util.Types;
1314

1415
import javax.tools.JavaFileObject;
15-
import java.io.*;
16+
import java.io.ByteArrayOutputStream;
17+
import java.io.IOException;
18+
import java.io.PrintWriter;
1619
import java.net.URI;
1720
import java.nio.file.Files;
1821
import java.nio.file.Path;
1922
import java.nio.file.Paths;
20-
import java.util.HashSet;
23+
import java.util.HashMap;
24+
import java.util.Map;
2125
import java.util.Optional;
22-
import java.util.Set;
23-
import java.util.stream.Collectors;
2426

2527
/**
2628
* Callback hook that generates SemanticDB when the compiler has completed typechecking a Java
@@ -33,6 +35,8 @@ public final class SemanticdbTaskListener implements TaskListener {
3335
private final Types types;
3436
private final Trees trees;
3537
private final Elements elements;
38+
// Javac fires ANALYZE once per top-level type; accumulate across rounds per output path.
39+
private final Map<Path, PerSourceState> perSourceState = new HashMap<>();
3640
private int noRelativePathCounter = 0;
3741

3842
public SemanticdbTaskListener(
@@ -58,8 +62,10 @@ public void started(TaskEvent e) {
5862
inferBazelSourceroot(e.getSourceFile());
5963
Result<Path, String> semanticdbPath = semanticdbOutputPath(options, e);
6064
if (semanticdbPath.isOk()) {
65+
Path output = semanticdbPath.getOrThrow();
66+
perSourceState.remove(output);
6167
try {
62-
Files.deleteIfExists(semanticdbPath.getOrThrow());
68+
Files.deleteIfExists(output);
6369
} catch (IOException ex) {
6470
this.reportException(ex, e);
6571
}
@@ -116,12 +122,20 @@ private void onFinishedAnalyze(TaskEvent e) {
116122
Result<Path, String> path = semanticdbOutputPath(options, e);
117123
if (path != null) {
118124
if (path.isOk()) {
125+
Path output = path.getOrThrow();
126+
PerSourceState state = perSourceState.computeIfAbsent(output, k -> new PerSourceState());
119127
Semanticdb.TextDocument textDocument =
120-
new SemanticdbVisitor(globals, e.getCompilationUnit(), options, types, trees, elements)
128+
new SemanticdbVisitor(
129+
globals,
130+
state.locals,
131+
e.getCompilationUnit(),
132+
options,
133+
types,
134+
trees,
135+
elements,
136+
state.documentBuilder)
121137
.buildTextDocument(e.getCompilationUnit());
122-
Path output = path.getOrThrow();
123-
if (Files.exists(output)) appendSemanticdb(e, output, textDocument);
124-
else writeSemanticdb(e, output, textDocument);
138+
writeSemanticdb(e, output, textDocument);
125139
} else {
126140
reporter.error(path.getErrorOrThrow(), e);
127141
}
@@ -136,76 +150,9 @@ private void writeSemanticdb(TaskEvent event, Path output, Semanticdb.TextDocume
136150
}
137151
}
138152

139-
private void appendSemanticdb(
140-
TaskEvent event, Path output, Semanticdb.TextDocument textDocument) {
141-
/*
142-
* If there already is a semanticdb file at the given path,
143-
* we do the following:
144-
* - Read a documents collection
145-
* - Try to find the document with the matching relative path (matching the incoming textDocument)
146-
* - Then, depending on whether a matching document already exists in the collection:
147-
* - if YES, mutate it in place to only add entries from the incoming document
148-
* - if NO, simply add the incoming text document to the collection
149-
* - Write the collection back to disk
150-
* */
151-
Semanticdb.TextDocument document = null;
152-
int documentIndex = -1;
153-
Semanticdb.TextDocuments documents = null;
154-
155-
try (InputStream is = Files.newInputStream(output.toFile().toPath())) {
156-
documents = Semanticdb.TextDocuments.parseFrom(is);
157-
158-
for (int i = 0; i < documents.getDocumentsCount(); i++) {
159-
Semanticdb.TextDocument candidate = documents.getDocuments(i);
160-
if (document == null && candidate.getUri().equals(textDocument.getUri())) {
161-
document = candidate;
162-
documentIndex = i;
163-
}
164-
}
165-
166-
} catch (IOException e) {
167-
this.reportException(e, event);
168-
return;
169-
}
170-
171-
if (document != null) {
172-
// If there is a previous semanticdb document at this path, we need
173-
// to deduplicate symbols and occurrences and mutate the document in place
174-
Set<Semanticdb.SymbolInformation> symbols = new HashSet<>(textDocument.getSymbolsList());
175-
Set<Semanticdb.SymbolOccurrence> occurrences =
176-
new HashSet<>(textDocument.getOccurrencesList());
177-
Set<Semanticdb.Synthetic> synthetics = new HashSet<>(textDocument.getSyntheticsList());
178-
179-
symbols.addAll(document.getSymbolsList());
180-
occurrences.addAll(document.getOccurrencesList());
181-
synthetics.addAll(document.getSyntheticsList());
182-
183-
documents
184-
.toBuilder()
185-
.addDocuments(
186-
documentIndex,
187-
document
188-
.toBuilder()
189-
.clearOccurrences()
190-
.addAllOccurrences(occurrences)
191-
.clearSymbols()
192-
.addAllSymbols(symbols)
193-
.clearSynthetics()
194-
.addAllSynthetics(synthetics));
195-
196-
} else {
197-
// If no prior document was found, we can just add the incoming one to the collection
198-
documents = documents.toBuilder().addDocuments(textDocument).build();
199-
}
200-
201-
byte[] bytes = documents.toByteArray();
202-
203-
try {
204-
Files.createDirectories(output.getParent());
205-
Files.write(output, bytes);
206-
} catch (IOException e) {
207-
this.reportException(e, event);
208-
}
153+
private static final class PerSourceState {
154+
final SemanticdbDocumentBuilder documentBuilder = new SemanticdbDocumentBuilder();
155+
final LocalSymbolsCache locals = new LocalSymbolsCache();
209156
}
210157

211158
public static Path absolutePathFromUri(SemanticdbJavacOptions options, JavaFileObject file) {

semanticdb-javac/src/main/java/com/sourcegraph/semanticdb_javac/SemanticdbVisitor.java

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.sourcegraph.semanticdb.Semanticdb;
44

5+
import com.sourcegraph.semanticdb.SemanticdbDocumentBuilder;
56
import com.sourcegraph.semanticdb.SemanticdbPaths;
67
import com.sourcegraph.semanticdb.SemanticdbSymbols;
78

@@ -69,29 +70,29 @@ public class SemanticdbVisitor extends TreePathScanner<Void, Void> {
6970
private final CompilationUnitTree compUnitTree;
7071
private final Elements elements;
7172
private final SemanticdbJavacOptions options;
72-
private final ArrayList<Semanticdb.SymbolOccurrence> occurrences;
73-
private final ArrayList<Semanticdb.SymbolInformation> symbolInfos;
73+
private final SemanticdbDocumentBuilder documentBuilder;
7474
private String source;
7575
private String uri;
7676

7777
private final LinkedHashMap<Tree, TreePath> nodes;
7878

7979
public SemanticdbVisitor(
8080
GlobalSymbolsCache globals,
81+
LocalSymbolsCache locals,
8182
CompilationUnitTree compUnitTree,
8283
SemanticdbJavacOptions options,
8384
Types types,
8485
Trees trees,
85-
Elements elements) {
86-
this.globals = globals; // Reused cache between compilation units.
87-
this.locals = new LocalSymbolsCache(); // Fresh cache per compilation unit.
86+
Elements elements,
87+
SemanticdbDocumentBuilder documentBuilder) {
88+
this.globals = globals;
89+
this.locals = locals;
8890
this.options = options;
8991
this.types = types;
9092
this.elements = elements;
9193
this.trees = trees;
9294
this.compUnitTree = compUnitTree;
93-
this.occurrences = new ArrayList<>();
94-
this.symbolInfos = new ArrayList<>();
95+
this.documentBuilder = documentBuilder;
9596
this.source = semanticdbText();
9697
this.uri = semanticdbUri(compUnitTree, options);
9798
this.nodes = new LinkedHashMap<>();
@@ -102,15 +103,8 @@ public Semanticdb.TextDocument buildTextDocument(CompilationUnitTree tree) {
102103

103104
resolveNodes();
104105

105-
return Semanticdb.TextDocument.newBuilder()
106-
.setSchema(Semanticdb.Schema.SEMANTICDB4)
107-
.setLanguage(Semanticdb.Language.JAVA)
108-
.setUri(uri)
109-
.setText(options.includeText ? this.source : "")
110-
.setMd5(semanticdbMd5())
111-
.addAllOccurrences(occurrences)
112-
.addAllSymbols(symbolInfos)
113-
.build();
106+
return documentBuilder.build(
107+
Semanticdb.Language.JAVA, uri, options.includeText ? this.source : "", semanticdbMd5());
114108
}
115109

116110
private Optional<Semanticdb.Range> emitSymbolOccurrence(
@@ -135,7 +129,7 @@ private void emitSymbolOccurrence(
135129
if (sym == null) return;
136130
Optional<Semanticdb.SymbolOccurrence> occ =
137131
semanticdbOccurrence(sym, range, role, enclosingRange);
138-
occ.ifPresent(occurrences::add);
132+
occ.ifPresent(documentBuilder::addOccurrence);
139133
}
140134

141135
private void emitSymbolInformation(Element sym, Tree tree) {
@@ -201,7 +195,7 @@ private void emitSymbolInformation(Element sym, Tree tree) {
201195

202196
Semanticdb.SymbolInformation info = builder.build();
203197

204-
symbolInfos.add(info);
198+
documentBuilder.addSymbol(info);
205199
}
206200

207201
void resolveNodes() {

semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SemanticdbTextDocumentBuilder.kt

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.sourcegraph.semanticdb_kotlinc
33
import com.sourcegraph.semanticdb.Semanticdb
44

55
import com.sourcegraph.semanticdb.Semanticdb.SymbolOccurrence.Role
6+
import com.sourcegraph.semanticdb.SemanticdbDocumentBuilder
67
import com.sourcegraph.semanticdb.SemanticdbPaths
78
import java.nio.file.Path
89
import java.nio.file.Paths
@@ -33,21 +34,12 @@ class SemanticdbTextDocumentBuilder(
3334
private val lineMap: LineMap,
3435
private val cache: SymbolsCache,
3536
) {
36-
private val occurrences = mutableListOf<Semanticdb.SymbolOccurrence>()
37-
private val symbols = mutableListOf<Semanticdb.SymbolInformation>()
37+
private val documentBuilder = SemanticdbDocumentBuilder()
3838
private val fileText = file.getContentsAsStream().reader().readText()
3939
private val semanticMd5 = semanticdbMD5()
4040

41-
fun build() = TextDocument {
42-
this.text = fileText
43-
this.uri = semanticdbURI()
44-
this.md5 = semanticMd5
45-
this.schema = Semanticdb.Schema.SEMANTICDB4
46-
this.language = Semanticdb.Language.KOTLIN
47-
occurrences.sortWith(compareBy({ it.range.startLine }, { it.range.startCharacter }))
48-
this.addAllOccurrences(occurrences)
49-
this.addAllSymbols(symbols)
50-
}
41+
fun build(): Semanticdb.TextDocument =
42+
documentBuilder.build(Semanticdb.Language.KOTLIN, semanticdbURI(), fileText, semanticMd5)
5143

5244
fun emitSemanticdbData(
5345
firBasedSymbol: FirBasedSymbol<*>?,
@@ -57,14 +49,10 @@ class SemanticdbTextDocumentBuilder(
5749
context: CheckerContext,
5850
enclosingSource: KtSourceElement? = null,
5951
) {
60-
symbolOccurrence(symbol, element, role, enclosingSource).let {
61-
if (!occurrences.contains(it)) {
62-
occurrences.add(it)
63-
}
52+
documentBuilder.addOccurrence(symbolOccurrence(symbol, element, role, enclosingSource))
53+
if (role == Role.DEFINITION) {
54+
documentBuilder.addSymbol(symbolInformation(firBasedSymbol, symbol, element, context))
6455
}
65-
val symbolInformation = symbolInformation(firBasedSymbol, symbol, element, context)
66-
if (role == Role.DEFINITION && !symbols.contains(symbolInformation))
67-
symbols.add(symbolInformation)
6856
}
6957

7058
@OptIn(SymbolInternals::class)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.sourcegraph.semanticdb;
2+
3+
import java.util.ArrayList;
4+
import java.util.Comparator;
5+
import java.util.LinkedHashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.Objects;
9+
10+
/**
11+
* Accumulator for {@link Semanticdb.SymbolOccurrence}/{@link Semanticdb.SymbolInformation} that
12+
* assembles a final {@link Semanticdb.TextDocument}. First emission wins: occurrences are
13+
* deduplicated by {@code (range, symbol, role)}, symbols by {@code symbol}; occurrences are sorted
14+
* by start position. Contains no compiler-API dependencies.
15+
*/
16+
public final class SemanticdbDocumentBuilder {
17+
private static final Comparator<Semanticdb.SymbolOccurrence> OCCURRENCE_ORDER =
18+
Comparator.<Semanticdb.SymbolOccurrence>comparingInt(o -> o.getRange().getStartLine())
19+
.thenComparingInt(o -> o.getRange().getStartCharacter());
20+
21+
private final Map<OccurrenceKey, Semanticdb.SymbolOccurrence> occurrences = new LinkedHashMap<>();
22+
private final Map<String, Semanticdb.SymbolInformation> symbols = new LinkedHashMap<>();
23+
24+
public void addOccurrence(Semanticdb.SymbolOccurrence occurrence) {
25+
occurrences.putIfAbsent(new OccurrenceKey(occurrence), occurrence);
26+
}
27+
28+
public void addSymbol(Semanticdb.SymbolInformation symbol) {
29+
symbols.putIfAbsent(symbol.getSymbol(), symbol);
30+
}
31+
32+
public Semanticdb.TextDocument build(
33+
Semanticdb.Language language, String uri, String text, String md5) {
34+
List<Semanticdb.SymbolOccurrence> sortedOccurrences = new ArrayList<>(occurrences.values());
35+
sortedOccurrences.sort(OCCURRENCE_ORDER);
36+
return Semanticdb.TextDocument.newBuilder()
37+
.setSchema(Semanticdb.Schema.SEMANTICDB4)
38+
.setLanguage(language)
39+
.setUri(uri)
40+
.setText(text)
41+
.setMd5(md5)
42+
.addAllOccurrences(sortedOccurrences)
43+
.addAllSymbols(symbols.values())
44+
.build();
45+
}
46+
47+
private static final class OccurrenceKey {
48+
private final Semanticdb.Range range;
49+
private final String symbol;
50+
private final Semanticdb.SymbolOccurrence.Role role;
51+
52+
OccurrenceKey(Semanticdb.SymbolOccurrence occurrence) {
53+
this.range = occurrence.hasRange() ? occurrence.getRange() : null;
54+
this.symbol = occurrence.getSymbol();
55+
this.role = occurrence.getRole();
56+
}
57+
58+
@Override
59+
public boolean equals(Object other) {
60+
if (this == other) return true;
61+
if (!(other instanceof OccurrenceKey)) return false;
62+
OccurrenceKey that = (OccurrenceKey) other;
63+
return role == that.role
64+
&& Objects.equals(range, that.range)
65+
&& Objects.equals(symbol, that.symbol);
66+
}
67+
68+
@Override
69+
public int hashCode() {
70+
return Objects.hash(range, symbol, role);
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)