-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCheckJavaSyntax.java
More file actions
172 lines (167 loc) · 7.42 KB
/
Copy pathCheckJavaSyntax.java
File metadata and controls
172 lines (167 loc) · 7.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
// Compile and execute each given Java source file with the in-process
// JavaCompiler API and a private URLClassLoader so the compiler and JVM
// stay warm across fixtures instead of cold-starting a JVM per file like
// the previous `javac` bash loop did. Fixtures either expose a
// `public static void main()` method (wrapping the literal in a method
// body) or declare `my_data` as a field on `class Main`; either way the
// host triggers static-init, constructs a `Main` instance to run any
// instance-field initializers, and invokes `main()` when present, so
// runtime errors (e.g. a bad `Instant.parse` argument) surface here
// instead of passing silently.
public class CheckJavaSyntax {
public static void main(final String[] args) throws IOException {
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
System.err.println("No JDK Java compiler available.");
System.exit(2);
}
if (args.length < 2) {
System.err.println("Usage: CheckJavaSyntax <release> <file>...");
System.exit(2);
}
// The `--release` value is supplied as the first argument by the
// `Lint Java` step in `.github/workflows/lint.yml`, which passes
// the literal matching each fixture's `@jdk_<release>` golden tag
// (`11` for `JDK_11`, `16` for `JDK_16` in `Java.VersionFormats`
// in `src/literalizer/languages/java.py`). Keep them in sync.
final String release = args[0];
// Optional `LITERALIZER_LINT_CLASSPATH` (colon-separated, with
// `dir/*` wildcards expanded to every jar in `dir/`) is added to
// both the compiler classpath and the per-fixture URLClassLoader
// so json_type fixtures resolve against Jackson at lint time.
// See `Lint Java` in `.github/workflows/lint.yml` for the wiring.
final List<Path> extraClasspath = resolveExtraClasspath();
final String classpathArg = classpathArg(extraClasspath);
boolean failed = false;
try (final StandardJavaFileManager fileManager =
compiler.getStandardFileManager(null, null, null)) {
for (int i = 1; i < args.length; i++) {
final String filename = args[i];
// Each fixture declares `class Main`, so give every file
// its own output directory to avoid class-name collisions.
final Path classDir = Files.createTempDirectory("javac");
final Iterable<? extends JavaFileObject> units =
fileManager.getJavaFileObjectsFromStrings(List.of(filename));
final List<String> options = new ArrayList<>(List.of(
"-d", classDir.toString(), "-proc:none", "--release", release));
if (!classpathArg.isEmpty()) {
options.add("-classpath");
options.add(classpathArg);
}
final JavaCompiler.CompilationTask task = compiler.getTask(
null,
fileManager,
null,
options,
null,
units);
if (!task.call()) {
System.err.println(filename + ": javac failed");
failed = true;
continue;
}
if (!runFixture(filename, classDir, extraClasspath)) {
failed = true;
}
}
}
System.exit(failed ? 1 : 0);
}
private static List<Path> resolveExtraClasspath() throws IOException {
final String env = System.getenv("LITERALIZER_LINT_CLASSPATH");
if (env == null || env.isEmpty()) {
return List.of();
}
final List<Path> jars = new ArrayList<>();
for (final String entry : env.split(":")) {
if (entry.isEmpty()) {
continue;
}
if (entry.endsWith("/*")) {
final Path dir = Paths.get(entry.substring(0, entry.length() - 2));
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(dir, "*.jar")) {
for (final Path jar : stream) {
jars.add(jar);
}
}
} else {
jars.add(Paths.get(entry));
}
}
return jars;
}
private static String classpathArg(final List<Path> jars) {
if (jars.isEmpty()) {
return "";
}
final List<String> parts = new ArrayList<>(jars.size());
for (final Path jar : jars) {
parts.add(jar.toString());
}
return String.join(":", parts);
}
// Load `Main` from a private class loader rooted at *classDir*,
// construct an instance (forcing both static- and instance-field
// initializers), and invoke `main()` if the fixture defines one.
// `Main` is package-private in every fixture, so `setAccessible`
// is required before invoking members reflectively.
private static boolean runFixture(
final String filename,
final Path classDir,
final List<Path> extraClasspath) {
final List<URL> urlList = new ArrayList<>();
try {
urlList.add(classDir.toUri().toURL());
for (final Path jar : extraClasspath) {
urlList.add(jar.toUri().toURL());
}
} catch (final java.net.MalformedURLException e) {
System.err.println(filename + ": " + e);
return false;
}
final URL[] urls = urlList.toArray(new URL[0]);
try (final URLClassLoader loader = new URLClassLoader(urls)) {
final Class<?> checkClass = Class.forName("Main", true, loader);
final Constructor<?> constructor = checkClass.getDeclaredConstructor();
constructor.setAccessible(true);
final Object instance = constructor.newInstance();
final Method checkMethod;
try {
checkMethod = checkClass.getDeclaredMethod("main");
} catch (final NoSuchMethodException e) {
// Field-only fixtures — constructing `Main` above
// already ran the field initializer.
return true;
}
checkMethod.setAccessible(true);
checkMethod.invoke(instance);
return true;
} catch (final InvocationTargetException e) {
System.err.println(filename + ": " + e.getCause());
return false;
} catch (final Throwable t) {
// Includes `ExceptionInInitializerError` (extends `Error`)
// raised when a static field initializer throws during
// `Class.forName`, e.g. a bad `Instant.parse` argument.
System.err.println(filename + ": " + t);
return false;
}
}
}