Skip to content

Commit 8f9c6c7

Browse files
authored
Add test for Java parsing coverage (#3916)
2 parents 2453255 + 877ea2c commit 8f9c6c7

99 files changed

Lines changed: 664 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/* This file is part of KeY - https://key-project.org
2+
* KeY is licensed under the GNU General Public License Version 2
3+
* SPDX-License-Identifier: GPL-2.0-only */
4+
package de.uka.ilkd.key.java;
5+
6+
import java.io.IOException;
7+
import java.net.URISyntaxException;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.nio.file.Paths;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
15+
16+
import de.uka.ilkd.key.control.KeYEnvironment;
17+
import de.uka.ilkd.key.speclang.PositionedString;
18+
import de.uka.ilkd.key.util.ExceptionTools;
19+
20+
import org.junit.jupiter.api.Assumptions;
21+
import org.junit.jupiter.api.DynamicTest;
22+
import org.junit.jupiter.api.TestFactory;
23+
24+
import static org.junit.jupiter.api.Assertions.fail;
25+
26+
/**
27+
* Pins down which Java language features KeY's front-end
28+
* ({@link de.uka.ilkd.key.java.loader.JP2KeYConverter} and the transformation pipeline) currently
29+
* accepts. This backs the "Java Coverage" support table in the user documentation.
30+
* <p>
31+
* The examples live under {@code src/test/resources/de/uka/ilkd/key/java/coverage} in two folders:
32+
* <ul>
33+
* <li>{@code supported/} — one directory per feature that KeY <em>parses</em> today. Each is loaded
34+
* and must keep loading; a failure here is a parser regression.</li>
35+
* <li>{@code rejected/} — one directory per feature that KeY does <em>not</em> parse today (it
36+
* throws at load time, usually "Unsupported element detected given by Java Parser"). Each must keep
37+
* being rejected; if one starts loading, the feature has become supported and the test (plus the
38+
* documentation table) must be updated.</li>
39+
* </ul>
40+
* Every example also compiles with {@code javac} 21, so a load failure reflects KeY's front-end and
41+
* not malformed Java. Note this checks <em>parseability</em>, not sound reasoning: a few
42+
* {@code supported/} features (e.g. autoboxing, try-with-resources) load but are not soundly
43+
* modelled, as noted in the documentation.
44+
*
45+
* @see de.uka.ilkd.key.java.loader.JP2KeYConverter
46+
*/
47+
public class JavaFeatureCoverageTest {
48+
49+
/** Each directory here must keep parsing; a throw is a front-end regression. */
50+
@TestFactory
51+
public Stream<DynamicTest> supportedFeaturesStayParseable() throws Exception {
52+
return featureDirs("supported")
53+
.map(dir -> DynamicTest.dynamicTest(dir.getFileName().toString(),
54+
() -> {
55+
Throwable error = tryLoad(dir);
56+
if (error != null) {
57+
fail("Regression: the Java feature '" + dir.getFileName()
58+
+ "' no longer parses, although it is listed as supported. "
59+
+ "If dropping it is intended, move it to 'coverage/rejected/' and update "
60+
+ "the Java Coverage documentation table. Load error: "
61+
+ firstMessage(error));
62+
}
63+
}));
64+
}
65+
66+
/**
67+
* Each directory here must keep being rejected. If one starts to parse, the feature is now
68+
* supported and both this test and the documentation must be updated.
69+
*/
70+
@TestFactory
71+
public Stream<DynamicTest> unsupportedFeaturesStayRejected() throws Exception {
72+
return featureDirs("rejected")
73+
.map(dir -> DynamicTest.dynamicTest(dir.getFileName().toString(),
74+
() -> {
75+
Throwable error = tryLoad(dir);
76+
if (error == null) {
77+
fail("'" + dir.getFileName()
78+
+ "' is now supported and the test should be "
79+
+ "modified to reflect the new addition: move it from 'coverage/rejected/' "
80+
+ "to 'coverage/supported/' and update the Java Coverage documentation "
81+
+ "table (mark the feature as supported).");
82+
}
83+
}));
84+
}
85+
86+
// --- helpers -----------------------------------------------------------------------------
87+
88+
private static Stream<Path> featureDirs(String kind) throws IOException, URISyntaxException {
89+
Path base = coverageRoot().resolve(kind);
90+
Assumptions.assumeTrue(Files.isDirectory(base),
91+
"coverage examples not found at " + base.toAbsolutePath());
92+
try (Stream<Path> s = Files.list(base)) {
93+
// Materialise so the directory stream is closed before the dynamic tests run.
94+
List<Path> dirs = s.filter(Files::isDirectory).sorted().collect(Collectors.toList());
95+
return dirs.stream();
96+
}
97+
}
98+
99+
private static Path coverageRoot() throws URISyntaxException {
100+
// Prefer the copied test resources on the classpath (location-independent),
101+
// fall back to the module-relative source path.
102+
var url = JavaFeatureCoverageTest.class.getResource("/de/uka/ilkd/key/java/coverage");
103+
if (url != null) {
104+
return Paths.get(url.toURI());
105+
}
106+
return Paths.get("src/test/resources/de/uka/ilkd/key/java/coverage");
107+
}
108+
109+
/** Loads the feature's {@code problem.key}; returns {@code null} on success, else the error. */
110+
private static Throwable tryLoad(Path featureDir) {
111+
Path problem = featureDir.resolve("problem.key");
112+
KeYEnvironment<?> env = null;
113+
try {
114+
env = KeYEnvironment.load(problem);
115+
return null;
116+
} catch (Throwable t) {
117+
return t;
118+
} finally {
119+
if (env != null) {
120+
try {
121+
env.dispose();
122+
} catch (Throwable ignore) {
123+
}
124+
}
125+
}
126+
}
127+
128+
private static String firstMessage(Throwable t) {
129+
List<String> msgs = new ArrayList<>();
130+
try {
131+
for (PositionedString ps : ExceptionTools.getMessages(t)) {
132+
msgs.add(ps.getText());
133+
}
134+
} catch (Throwable ignore) {
135+
}
136+
String joined = String.join(" | ", msgs);
137+
if (joined.isBlank()) {
138+
joined = t.getClass().getSimpleName()
139+
+ (t.getMessage() != null ? ": " + t.getMessage() : "");
140+
}
141+
return joined.replaceAll("\\s+", " ").trim();
142+
}
143+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Java feature coverage examples
2+
3+
Fixtures for `de.uka.ilkd.key.java.JavaFeatureCoverageTest`, which pins down
4+
which Java language features KeY's front-end currently parses. They also back
5+
the **Java Coverage** support table in the user documentation
6+
(*User Guide > Supported Language Features > Java Coverage*).
7+
8+
Layout: one directory per feature, each with a small `.java` file plus a
9+
`problem.key` (`\javaSource "."; \problem { true }`) that makes KeY parse it:
10+
11+
- `supported/`: features KeY **parses** today. The test loads each and fails
12+
if one stops parsing (a front-end regression).
13+
- `rejected/`: features KeY does **not** parse today (loading throws, usually
14+
*"Unsupported element detected given by Java Parser"*). The test fails if one
15+
starts parsing, which means the feature became supported and both the test
16+
and the documentation table must be updated.
17+
18+
Every `.java` file also compiles with `javac` 21, and uses only types available
19+
in KeY's default classpath (`JavaRedux`) or types it declares itself, so a load
20+
failure reflects KeY's front-end and not malformed Java or a missing type.
21+
22+
## Why each rejected example is rejected
23+
24+
Each example isolates a single construct, so its rejection is attributable to
25+
the feature under test (verified by loading each and inspecting the error):
26+
27+
| Example | Rejected because |
28+
|---|---|
29+
| `generics-*` (plain, bounded, wildcard, method, diamond) | converter aborts with *"Unexpected type to convert"* on the type parameter/argument |
30+
| `local-class` | `Unsupported element: LocalClassDeclarationStmt` |
31+
| `static-import` | `Unsupported element: ImportDeclaration` (the `static` import) |
32+
| `annotation-declaration` | `Unsupported element: AnnotationDeclaration` (the `@interface`) |
33+
| `multi-catch` | `Unsupported element: UnionType` |
34+
| `lambda` | `Unsupported element: LambdaExpr` |
35+
| `method-reference` | `Unsupported element: MethodReferenceExpr` |
36+
| `instanceof-pattern` | `Unsupported element: TypePatternExpr` (the bound pattern) |
37+
| `switch-expression` | parser rejects `yield` / arrow-switch (syntax error) |
38+
| `switch-pattern-type` | `Unsupported element: TypePatternExpr` (`case Integer i`) |
39+
| `switch-pattern-record` | `Unsupported element: TypePatternExpr` (record component of `case Point(...)`) |
40+
| `record-pattern` | `Unsupported element: TypePatternExpr` (record pattern via `instanceof`) |
41+
| `switch-enum-unqualified` | KeY cannot resolve the **unqualified** `case RED` label (the enum is declared in the file, so this is the label form, not a missing type) |
42+
| `module-declaration` | `module-info` currently triggers an internal error (`IndexOutOfBoundsException`) |
43+
| `var-reference-type` | a `var` with a `String` initializer currently triggers an internal `NullPointerException` |
44+
45+
Because the host construct is otherwise supported (a `switch` statement,
46+
`instanceof`, a normal class), the failure lands on the tested feature, not on
47+
its surroundings.
48+
49+
## Notes
50+
51+
- `switch-enum-qualified` vs `switch-enum-unqualified` capture a real quirk: a
52+
`switch` over an `enum` only loads with *qualified* case labels
53+
(`case E.C:`); the standard unqualified `case C:` fails to resolve.
54+
- Pattern matching for `switch` is split by pattern kind:
55+
`switch-pattern-type` and `switch-pattern-record` are rejected, but a bare
56+
`case null` label (`switch-null-label`) actually parses, so it lives in
57+
`supported/`. The examples use `switch` *statements*, not `switch`
58+
*expressions*, so the failure is attributed to the pattern and not to the
59+
(also unsupported) switch-expression form.
60+
- `var-primitive` / `var-in-for` load, but `var-reference-type` does not.
61+
- Generics come in five variants (`plain`, `bounded`, `wildcard`, `method`,
62+
`diamond`), all rejected.
63+
- "Parses" is not "soundly reasoned about": `autoboxing` and
64+
`try-with-resources` sit in `supported/` because they load, but KeY does not
65+
model them soundly (see the documentation's *Comments* column).
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public class AnnDecl {
2+
@interface Marker { int value() default 0; }
3+
@Marker(3) int x;
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
\javaSource ".";
2+
3+
\problem { true }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
public class GenBounded {
2+
interface S { }
3+
static class Box<T extends S> { T v; }
4+
static class Impl implements S { }
5+
Box<Impl> b = new Box<Impl>();
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
\javaSource ".";
2+
3+
\problem { true }
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public class GenDiamond {
2+
static class Box<T> { T v; }
3+
Box<String> b = new Box<>();
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
\javaSource ".";
2+
3+
\problem { true }
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public class GenMethod {
2+
<T> T id(T x) { return x; }
3+
String f() { return id("hi"); }
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
\javaSource ".";
2+
3+
\problem { true }

0 commit comments

Comments
 (0)