Skip to content

Commit fbd0166

Browse files
committed
Implement Java compiler plugin that generates SemanticDB.
1 parent 520def5 commit fbd0166

File tree

15 files changed

+1087
-0
lines changed

15 files changed

+1087
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.sourcegraph.semanticdb_javac;
2+
3+
/**
4+
* Describes how to convert a compiler position into SemanticDB Range.
5+
*
6+
* <p>A Java compiler position has tree parts: "start", "point" and "end".
7+
*
8+
* <pre>
9+
* public static void main(String[] args) { }
10+
* ^ start
11+
* ^ point (aka. "preferred position")
12+
* ^ end
13+
* </pre>
14+
*
15+
* A SemanticDB range has four parts: "startLine", "startCharacter", "endLine", "endCharacter".
16+
*/
17+
public enum CompilerRange {
18+
/** Map the compiler start/end positions to SemanticDB start/end positions. */
19+
FROM_START_TO_END,
20+
21+
/**
22+
* Map the compiler point position to SemanticDB start and use (point + symbol name length) for
23+
* the SemanticDB end position.
24+
*/
25+
FROM_POINT_TO_SYMBOL_NAME,
26+
27+
/**
28+
* Map the compiler (point + 1) position to SemanticDB start and use (point + symbol name length +
29+
* 1) for the SemanticDB end position.
30+
*/
31+
FROM_POINT_TO_SYMBOL_NAME_PLUS_ONE;
32+
33+
public boolean isFromPoint() {
34+
switch (this) {
35+
case FROM_POINT_TO_SYMBOL_NAME:
36+
case FROM_POINT_TO_SYMBOL_NAME_PLUS_ONE:
37+
return true;
38+
default:
39+
return false;
40+
}
41+
}
42+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.sourcegraph.semanticdb_javac;
2+
3+
/** Utility methods for debugging purposes. */
4+
public final class Debugging {
5+
public static void pprint(Object any) {
6+
StackTraceElement trace = new Exception().getStackTrace()[1];
7+
if (any instanceof String) {
8+
any = String.format("\"%s\"", any);
9+
}
10+
System.out.printf("%s:%s %s%n", trace.getFileName(), trace.getLineNumber(), any);
11+
}
12+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.sourcegraph.semanticdb_javac;
2+
3+
import com.sun.tools.javac.tree.EndPosTable;
4+
import com.sun.tools.javac.tree.JCTree;
5+
import com.sun.tools.javac.util.Position;
6+
7+
/** A fallback implementation of EndPosTable when it's missing from the compiler. */
8+
public final class EmptyEndPosTable implements EndPosTable {
9+
10+
@Override
11+
public int getEndPos(JCTree tree) {
12+
return Position.NOPOS;
13+
}
14+
15+
@Override
16+
public void storeEnd(JCTree tree, int endpos) {}
17+
18+
@Override
19+
public int replaceTree(JCTree oldtree, JCTree newtree) {
20+
return Position.NOPOS;
21+
}
22+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package com.sourcegraph.semanticdb_javac;
2+
3+
import com.sun.tools.javac.code.Kinds;
4+
import com.sun.tools.javac.code.Scope;
5+
import com.sun.tools.javac.code.Symbol;
6+
7+
import java.util.ArrayList;
8+
import java.util.Collections;
9+
import java.util.IdentityHashMap;
10+
11+
import static com.sourcegraph.semanticdb_javac.Debugging.pprint;
12+
13+
/** Cache of SemanticDB symbols that can be referenced between files. */
14+
public final class GlobalSymbolsCache {
15+
16+
private final IdentityHashMap<Symbol, String> globals = new IdentityHashMap<>();
17+
private final SemanticdbOptions options;
18+
19+
public GlobalSymbolsCache(SemanticdbOptions options) {
20+
this.options = options;
21+
}
22+
23+
public String semanticdbSymbol(Symbol sym, LocalSymbolsCache locals) {
24+
String result = globals.get(sym);
25+
if (result != null) return result;
26+
String localResult = locals.get(sym);
27+
if (localResult != null) return localResult;
28+
result = uncachedSemanticdbSymbol(sym, locals);
29+
if (SemanticdbSymbols.isGlobal(result)) {
30+
globals.put(sym, result);
31+
}
32+
return result;
33+
}
34+
35+
public boolean isNone(Symbol sym) {
36+
return sym == null || sym.kind == Kinds.NIL || (sym.kind & Kinds.ERRONEOUS) != 0;
37+
}
38+
39+
private String uncachedSemanticdbSymbol(Symbol sym, LocalSymbolsCache locals) {
40+
if (isNone(sym)) return SemanticdbSymbols.NONE;
41+
String owner = semanticdbSymbol(sym.owner, locals);
42+
if (owner.equals(SemanticdbSymbols.NONE)) {
43+
return SemanticdbSymbols.ROOT_PACKAGE;
44+
} else if (sym instanceof Symbol.VarSymbol && sym.isLocal()) {
45+
return locals.put(sym);
46+
}
47+
SemanticdbSymbols.Descriptor desc = semanticdbDescriptor(sym);
48+
if (options.verboseEnabled && desc.kind == SemanticdbSymbols.Descriptor.Kind.None) {
49+
pprint(sym.name.toString());
50+
pprint(sym.kind);
51+
pprint(
52+
String.format(
53+
"sym: %s (%s - superclass %s)", sym, sym.getClass(), sym.getClass().getSuperclass()));
54+
}
55+
return SemanticdbSymbols.global(owner, desc);
56+
}
57+
58+
private SemanticdbSymbols.Descriptor semanticdbDescriptor(Symbol sym) {
59+
if (sym instanceof Symbol.ClassSymbol) {
60+
return new SemanticdbSymbols.Descriptor(
61+
SemanticdbSymbols.Descriptor.Kind.Type, sym.name.toString());
62+
} else if (sym instanceof Symbol.MethodSymbol) {
63+
return new SemanticdbSymbols.Descriptor(
64+
SemanticdbSymbols.Descriptor.Kind.Method,
65+
sym.name.toString(),
66+
methodDisambiguator((Symbol.MethodSymbol) sym));
67+
} else if (sym instanceof Symbol.PackageSymbol) {
68+
return new SemanticdbSymbols.Descriptor(
69+
SemanticdbSymbols.Descriptor.Kind.Package, sym.name.toString());
70+
} else if (sym instanceof Symbol.TypeVariableSymbol) {
71+
return new SemanticdbSymbols.Descriptor(
72+
SemanticdbSymbols.Descriptor.Kind.TypeParameter, sym.name.toString());
73+
} else if (sym instanceof Symbol.VarSymbol) {
74+
return new SemanticdbSymbols.Descriptor(
75+
SemanticdbSymbols.Descriptor.Kind.Term, sym.name.toString());
76+
} else {
77+
return SemanticdbSymbols.Descriptor.NONE;
78+
}
79+
}
80+
81+
/**
82+
* Computes the method "disambiguator" according to the SemanticDB spec.
83+
*
84+
* <p><quote> Concatenation of a left parenthesis ("("), a tag and a right parenthesis (")"). If
85+
* the definition is not overloaded, the tag is empty. If the definition is overloaded, the tag is
86+
* computed depending on where the definition appears in the following order:
87+
*
88+
* <ul>
89+
* <li>non-static overloads first, following the same order as they appear in the original
90+
* source,
91+
* <li>static overloads secondly, following the same order as they appear in the original source
92+
* </ul>
93+
*
94+
* </quote>
95+
*
96+
* <p><a href="https://scalameta.org/docs/semanticdb/specification.html#symbol-2">Link to
97+
* SemanticDB spec</a>.
98+
*/
99+
private String methodDisambiguator(Symbol.MethodSymbol sym) {
100+
Scope.Entry lookup =
101+
sym.owner.members().lookup(sym.name, s -> s instanceof Symbol.MethodSymbol);
102+
ArrayList<Symbol> peers = new ArrayList<>();
103+
while (lookup != null) {
104+
if (lookup.sym != null) {
105+
peers.add(lookup.sym);
106+
}
107+
lookup = lookup.next();
108+
}
109+
// NOTE(olafur): reverse the iteration from `Scope.Entry` to get order in which the symbols are
110+
// defined in source.
111+
Collections.reverse(peers);
112+
// NOTE(olafur): sort static methods last, according to the spec. Historical note: this
113+
// requirement is
114+
// part of the SemanticDB spec because static methods and non-static methods have a different
115+
// "owner" symbol.
116+
// There is no way to recover the definition order for a mix of static nnon-static method
117+
// definitions.
118+
// In practice, it's unusual to mix static and non-static methods so this shouldn't be a big
119+
// issue.
120+
peers.sort((a, b) -> Boolean.compare(a.isStatic(), b.isStatic()));
121+
int index = peers.indexOf(sym);
122+
if (index == 0) return "()";
123+
return String.format("(+%d)", index);
124+
}
125+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.sourcegraph.semanticdb_javac;
2+
3+
import com.sun.tools.javac.code.Symbol;
4+
5+
import java.util.IdentityHashMap;
6+
7+
/** Cache of SemanticDB symbols that are local to a single file. */
8+
public final class LocalSymbolsCache {
9+
10+
private final IdentityHashMap<Symbol, String> symbols = new IdentityHashMap<>();
11+
private int localsCounter = -1;
12+
13+
public String get(Symbol sym) {
14+
return symbols.get(sym);
15+
}
16+
17+
public String put(Symbol sym) {
18+
localsCounter++;
19+
String result = SemanticdbSymbols.local(localsCounter);
20+
symbols.put(sym, result);
21+
return result;
22+
}
23+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.sourcegraph.semanticdb_javac;
2+
3+
import java.nio.CharBuffer;
4+
import java.nio.charset.StandardCharsets;
5+
import java.security.MessageDigest;
6+
import java.security.NoSuchAlgorithmException;
7+
8+
/** Utility to compute MD5 checksums of strings. */
9+
public final class MD5 {
10+
private static final char[] HEX_ARRAY;
11+
12+
static {
13+
HEX_ARRAY = "0123456789ABCDEF".toCharArray();
14+
}
15+
16+
private static String bytesToHex(byte[] bytes) {
17+
char[] hexChars = new char[bytes.length * 2];
18+
int j = 0;
19+
while (j < bytes.length) {
20+
int v = bytes[j] & 0xFF;
21+
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
22+
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
23+
j += 1;
24+
}
25+
return new String(hexChars);
26+
}
27+
28+
public static String digest(CharSequence chars) throws NoSuchAlgorithmException {
29+
CharBuffer buf = CharBuffer.wrap(chars);
30+
byte[] bytes = StandardCharsets.UTF_8.encode(buf).array();
31+
MessageDigest md5 = MessageDigest.getInstance("MD5");
32+
md5.digest(bytes);
33+
return bytesToHex(md5.digest());
34+
}
35+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.sourcegraph.semanticdb_javac;
2+
3+
import java.util.NoSuchElementException;
4+
import java.util.Objects;
5+
import java.util.function.Function;
6+
7+
/**
8+
* A Java implementation of Rust's <code>Result[T, E]</code> type, or Scala's <code>Either[A, B]
9+
* </code>.
10+
*
11+
* @param <T> The type of a successful value.
12+
* @param <E> The type of the error value.
13+
*/
14+
public final class Result<T, E> {
15+
private enum Kind {
16+
Ok,
17+
Error;
18+
}
19+
20+
private Kind kind;
21+
private final T ok;
22+
private final E error;
23+
24+
private Result(Kind kind, T ok, E error) {
25+
if (kind == Kind.Ok && ok == null)
26+
throw new IllegalArgumentException("ok must not be null when kind == Kind.Ok");
27+
if (kind == Kind.Error && error == null)
28+
throw new IllegalArgumentException("error must not be null when kind == Kind.Error");
29+
this.kind = kind;
30+
this.error = error;
31+
this.ok = ok;
32+
}
33+
34+
@Override
35+
public boolean equals(Object o) {
36+
if (this == o) return true;
37+
if (o == null || getClass() != o.getClass()) return false;
38+
Result<?, ?> result = (Result<?, ?>) o;
39+
return kind == result.kind
40+
&& Objects.equals(error, result.error)
41+
&& Objects.equals(ok, result.ok);
42+
}
43+
44+
@Override
45+
public int hashCode() {
46+
return Objects.hash(kind, error, ok);
47+
}
48+
49+
@Override
50+
public String toString() {
51+
switch (kind) {
52+
case Ok:
53+
return "Error(" + error + ")";
54+
case Error:
55+
return "Ok(" + ok + ")";
56+
default:
57+
return "Result{" + "kind=" + kind + ", error=" + error + ", ok=" + ok + '}';
58+
}
59+
}
60+
61+
public <C> C fold(Function<T, C> onOk, Function<E, C> onError) {
62+
switch (kind) {
63+
case Ok:
64+
return onOk.apply(ok);
65+
case Error:
66+
return onError.apply(error);
67+
default:
68+
throw new IllegalArgumentException(this.toString());
69+
}
70+
}
71+
72+
public <C> Result<C, E> map(Function<T, C> fn) {
73+
return this.fold(left -> Result.ok(fn.apply(left)), Result::error);
74+
}
75+
76+
public boolean isOk() {
77+
return kind == Kind.Ok;
78+
}
79+
80+
public boolean isError() {
81+
return kind == Kind.Error;
82+
}
83+
84+
public T getOrThrow() {
85+
if (kind == Kind.Ok) {
86+
return ok;
87+
} else {
88+
throw new NoSuchElementException("no left value on " + this.toString());
89+
}
90+
}
91+
92+
public E getErrorOrThrow() {
93+
if (kind == Kind.Error) {
94+
return error;
95+
} else {
96+
throw new NoSuchElementException("no left value on " + this.toString());
97+
}
98+
}
99+
100+
public static <T, E> Result<T, E> ok(T value) {
101+
return new Result<>(Kind.Ok, value, null);
102+
}
103+
104+
public static <T, E> Result<T, E> error(E value) {
105+
return new Result<>(Kind.Error, null, value);
106+
}
107+
}

0 commit comments

Comments
 (0)