/* * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.io.File; import java.io.InputStream; import java.io.Writer; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.classfile.ClassFile; import java.lang.classfile.ClassModel; import java.lang.classfile.attribute.ModuleAttribute; import java.lang.classfile.attribute.ModulePackagesAttribute; import java.lang.constant.PackageDesc; import java.lang.reflect.Method; import java.util.Arrays; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Collectors; import java.util.stream.Stream; import toolbox.JavacTask; import toolbox.Task; import toolbox.Task.Expect; import toolbox.ToolBox; import build.tools.symbolgenerator.CreateSymbols; import build.tools.symbolgenerator.CreateSymbols.ClassDescription; import build.tools.symbolgenerator.CreateSymbols.ClassList; import build.tools.symbolgenerator.CreateSymbols.ExcludeIncludeList; import build.tools.symbolgenerator.CreateSymbols.VersionDescription; import java.io.UncheckedIOException; import java.lang.classfile.attribute.ModuleMainClassAttribute; import java.lang.constant.ClassDesc; import java.util.Objects; import java.util.function.Consumer; public class CreateSymbolsTestImpl { static final String CREATE_SYMBOLS_NAME = "symbolgenerator.CreateSymbols"; public static void main(String... args) throws Exception { new CreateSymbolsTestImpl().doTest(); } void doTest() throws Exception { boolean testRun = false; for (Method m : CreateSymbolsTestImpl.class.getDeclaredMethods()) { if (m.isAnnotationPresent(Test.class)) { m.invoke(this); testRun = true; } } if (!testRun) { throw new IllegalStateException("No tests found."); } } @Test void testMethodRemoved() throws Exception { doTest("package t; public class T { public void m() { } }", "package t; public class T { }", "package t; public class Test { { T t = null; t.m(); } }", Expect.SUCCESS, Expect.FAIL); doTest("package t; public class T { public void b() { } public void m() { } public void a() { } }", "package t; public class T { public void b() { } public void a() { } }", "package t; public class Test { { T t = null; t.b(); t.a(); } }", Expect.SUCCESS, Expect.SUCCESS); //with additional attribute (need to properly skip the member): doTest("package t; public class T { public void m() throws IllegalStateException { } public void a() { } }", "package t; public class T { public void a() { } }", "package t; public class Test { { T t = null; t.a(); } }", Expect.SUCCESS, Expect.SUCCESS); } @Test void testMethodAdded() throws Exception { doTest("package t; public class T { }", "package t; public class T { public void m() { } }", "package t; public class Test { { T t = null; t.m(); } }", Expect.FAIL, Expect.SUCCESS); doTest("package t; public class T { public void b() { } public void a() { } }", "package t; public class T { public void b() { } public void m() { } public void a() { } }", "package t; public class Test { { T t = null; t.b(); t.a(); } }", Expect.SUCCESS, Expect.SUCCESS); } //verify fields added/modified/removed @Test void testClassAdded() throws Exception { doTest("class Dummy {}", "package t; public class T { }", "package t; public class Test { { T t = new T(); } }", Expect.FAIL, Expect.SUCCESS); } @Test void testClassModified() throws Exception { doTest("package t; public class T { public void m() { } }", "package t; public class T implements java.io.Serializable { public void m() { } }", "package t; public class Test { { java.io.Serializable t = new T(); } }", Expect.FAIL, Expect.SUCCESS); } @Test void testClassRemoved() throws Exception { doTest("package t; public class T { }", "class Dummy {}", "package t; public class Test { { T t = new T(); } }", Expect.SUCCESS, Expect.FAIL); } @Test void testInnerClassAttributes() throws Exception { doTest("package t; public class T { public static class Inner { } }", "package t; public class T { public static class Inner { } public void extra() {} }", "package t; import t.T.Inner; public class Test { Inner i; }", Expect.SUCCESS, Expect.SUCCESS); } @Test void testConstantAdded() throws Exception { doTest("package t; public class T { }", "package t; public class T { public static final int A = 0; }", "package t; public class Test { void t(int i) { switch (i) { case T.A: break;} } }", Expect.FAIL, Expect.SUCCESS); } @Test void testAnnotationAttributeDefaultvalue() throws Exception { //TODO: this only verifies that there is *some* value, but we should also verify there is a specific value: doTest("package t; public @interface T { }", "package t;\n" + "public @interface T {\n" + " public boolean booleanValue() default true;\n" + " public byte byteValue() default 1;\n" + " public char charValue() default 2;\n" + " public short shortValue() default 3;\n" + " public int intValue() default 4;\n" + " public long longValue() default 5;\n" + " public float floatValue() default 6;\n" + " public double doubleValue() default 7;\n" + " public String stringValue() default \"8\";\n" + " public java.lang.annotation.RetentionPolicy enumValue() default java.lang.annotation.RetentionPolicy.RUNTIME;\n" + " public Class classValue() default Number.class;\n" + " public int[] arrayValue() default {1, 2};\n" + " public SuppressWarnings annotationValue() default @SuppressWarnings(\"cast\");\n" + "}\n", "package t; public @T class Test { }", Expect.SUCCESS, Expect.SUCCESS); } @Test void testConstantTest() throws Exception { //XXX: other constant types (String in particular) - see testStringConstant doPrintElementTest("package t; public class T { public static final int A = 1; }", "package t; public class T { public static final int A = 2; }", "t.T", "package t;\n\n" + "public class T {\n" + " public static final int A = 1;\n\n" + " public T();\n" + "}\n", "t.T", "package t;\n\n" + "public class T {\n" + " public static final int A = 2;\n\n" + " public T();\n" + "}\n"); doPrintElementTest("package t; public class T { public static final boolean A = false; }", "package t; public class T { public static final boolean A = true; }", "t.T", "package t;\n\n" + "public class T {\n" + " public static final boolean A = false;\n\n" + " public T();\n" + "}\n", "t.T", "package t;\n\n" + "public class T {\n" + " public static final boolean A = true;\n\n" + " public T();\n" + "}\n"); } @Test void testAnnotations() throws Exception { Set extraAnnotations = Set.of("Ljava/lang/annotation/Retention;"); CreateSymbols.HARDCODED_ANNOTATIONS.addAll(extraAnnotations); try { doPrintElementTest("package t;" + "import java.lang.annotation.*;" + "public @Visible @Invisible class T { public void extra() { } }" + "@Retention(RetentionPolicy.RUNTIME) @interface Visible { }" + "@Retention(RetentionPolicy.CLASS) @interface Invisible { }", "package t;" + "import java.lang.annotation.*;" + "public @Visible @Invisible class T { }" + "@Retention(RetentionPolicy.RUNTIME) @interface Visible { }" + "@Retention(RetentionPolicy.CLASS) @interface Invisible { }", "t.T", "package t;\n\n" + "@t.Invisible\n" + "@t.Visible\n" + "public class T {\n\n" + " public T();\n\n" + " public void extra();\n" + "}\n", "t.Visible", "package t;\n\n" + "@java.lang.annotation.Retention(RUNTIME)\n" + "@interface Visible {\n" + "}\n"); doPrintElementTest("package t;" + "import java.lang.annotation.*;" + "import java.util.*;" + "public class T {" + " public void test(int h, @Invisible int i, @Visible List j, int k) { }" + "}" + "@Retention(RetentionPolicy.RUNTIME) @interface Visible { }" + "@Retention(RetentionPolicy.CLASS) @interface Invisible { }", "package t;" + "import java.lang.annotation.*;" + "import java.util.*;" + "public class T {" + " public void test(int h, @Invisible int i, @Visible List j, int k) { }" + " public void extra() { }" + "}" + "@Retention(RetentionPolicy.RUNTIME) @interface Visible { }" + "@Retention(RetentionPolicy.CLASS) @interface Invisible { }", "t.T", "package t;\n\n" + "public class T {\n\n" + " public T();\n\n" + " public void test(int arg0,\n" + " @t.Invisible int arg1,\n" + " @t.Visible java.util.List arg2,\n" + " int arg3);\n" + "}\n", "t.Visible", "package t;\n\n" + "@java.lang.annotation.Retention(RUNTIME)\n" + "@interface Visible {\n" + "}\n"); doPrintElementTest("package t;" + "import java.lang.annotation.*;" + "public class T {" + " public void test(@Ann(v=\"url\", dv=\"\\\"\\\"\") String str) { }" + "}" + "@Retention(RetentionPolicy.RUNTIME) @interface Ann {" + " public String v();" + " public String dv();" + "}", "package t;" + "public class T { }", "t.T", "package t;\n\n" + "public class T {\n\n" + " public T();\n\n" + " public void test(@t.Ann(dv=\"\\\"\\\"\", v=\"url\") java.lang.String arg0);\n" + "}\n", "t.T", "package t;\n\n" + "public class T {\n\n" + " public T();\n" + "}\n"); } finally { CreateSymbols.HARDCODED_ANNOTATIONS.removeAll(extraAnnotations); } } @Test void testStringConstant() throws Exception { doTest("package t; public class T { public static final String C = \"\"; }", "package t; public class T { public static final String C = \"\"; public void extra() { } }", "package t; public class Test { { System.err.println(T.C); } }", Expect.SUCCESS, Expect.SUCCESS); } @Test void testCopyProfileAnnotation() throws Exception { String oldProfileAnnotation = CreateSymbols.PROFILE_ANNOTATION; try { CreateSymbols.PROFILE_ANNOTATION = "Lt/Ann;"; doTestEquivalence("package t; public @Ann class T { public void t() {} } @interface Ann { }", "package t; public class T { public void t() {} }", "t.T"); } finally { CreateSymbols.PROFILE_ANNOTATION = oldProfileAnnotation; } } @Test void testParseAnnotation() throws Exception { CreateSymbols.parseAnnotations("@Lsun/Proprietary+Annotation;@Ljdk/Profile+Annotation;(value=I1)", new int[1]); CreateSymbols.parseAnnotations("@Ltest;(value={\"\"})", new int[1]); CreateSymbols.parseAnnotations("@Ljava/beans/ConstructorProperties;(value={\"path\"})", new int[1]); CreateSymbols.parseAnnotations("@Ljava/beans/ConstructorProperties;(value=I-2)", new int[1]); } @Test void testStringCharLiterals() throws Exception { doPrintElementTest("package t;" + "public class T {" + " public static final String STR = \"\\u0000\\u0001\\uffff\";" + " public static final String EMPTY = \"\";" + " public static final String AMP = \"&&<<>>''\";" + "}", "package t;" + " public class T {" + " public static final char c = '\\uffff';" + "}", "t.T", "package t;\n\n" + "public class T {\n" + " public static final java.lang.String STR = \"\\u0000\\u0001\\uffff\";\n" + " public static final java.lang.String EMPTY = \"\";\n" + " public static final java.lang.String AMP = \"&&<<>>''\";\n\n" + " public T();\n" + "}\n", "t.T", "package t;\n\n" + "public class T {\n" + " public static final char c = '\\uffff';\n\n" + " public T();\n" + "}\n"); } @Test void testGenerification() throws Exception { doTest("package t; public class T { public class TT { public Object t() { return null; } } }", "package t; public class T { public class TT { public E t() { return null; } } }", "package t; public class Test { { T.TT tt = null; tt.t(); } }", Expect.SUCCESS, Expect.SUCCESS); } @Test void testClearMissingAnnotations() throws Exception { doPrintElementTest(new String[] { """ package t; import t.impl.HC; import t.impl.HR; @HC @HR public class T { @HC @HR public static final int i = 0; @HC @HR public void t() {} } """, """ package t.impl; import java.lang.annotation.*; @Retention(RetentionPolicy.CLASS) public @interface HC { } """, """ package t.impl; import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) public @interface HR { } """ }, new String[] { """ package t; public class T { public static final int i = 0; } """ }, "t.T", """ package t; public class T { public static final int i = 0; public T(); public void t(); } """, "t.T", """ package t; public class T { public static final int i = 0; public T(); } """); } int i = 0; void doTest(String code7, String code8, String testCode, Expect result7, Expect result8) throws Exception { ToolBox tb = new ToolBox(); Path classes = prepareVersionedCTSym(new String[] {code7}, new String[] {code8}); Path output = classes.getParent(); Path scratch = output.resolve("scratch"); Files.createDirectories(scratch); new JavacTask(tb) .sources(testCode) .options("-d", scratch.toAbsolutePath().toString(), "-classpath", computeClassPath(classes, "7")) .run(result7) .writeAll(); new JavacTask(tb) .sources(testCode) .options("-d", scratch.toAbsolutePath().toString(), "-classpath", computeClassPath(classes, "8")) .run(result8) .writeAll(); } private static String computeClassPath(Path classes, String version) throws IOException { try (Stream elements = Files.list(classes)) { return elements.filter(el -> el.getFileName().toString().contains(version)) .map(el -> el.resolve("java.base")) .map(el -> el.toAbsolutePath().toString()) .collect(Collectors.joining(File.pathSeparator)); } } void doPrintElementTest(String code7, String code8, String className7, String printed7, String className8, String printed8) throws Exception { doPrintElementTest(new String[] {code7}, new String[] {code8}, className7, printed7, className8, printed8); } void doPrintElementTest(String[] code7, String[] code8, String className7, String printed7, String className8, String printed8) throws Exception { ToolBox tb = new ToolBox(); Path classes = prepareVersionedCTSym(code7, code8); Path output = classes.getParent(); Path scratch = output.resolve("scratch"); Files.createDirectories(scratch); String out; out = new JavacTask(tb, Task.Mode.CMDLINE) .options("-d", scratch.toAbsolutePath().toString(), "-classpath", computeClassPath(classes, "7"), "-Xprint", className7) .run(Expect.SUCCESS) .getOutput(Task.OutputKind.STDOUT) .replaceAll("\\R", "\n"); if (!out.equals(printed7)) { throw new AssertionError("out=" + out + "; printed7=" + printed7); } out = new JavacTask(tb, Task.Mode.CMDLINE) .options("-d", scratch.toAbsolutePath().toString(), "-classpath", computeClassPath(classes, "8"), "-Xprint", className8) .run(Expect.SUCCESS) .getOutput(Task.OutputKind.STDOUT) .replaceAll("\\R", "\n"); if (!out.equals(printed8)) { throw new AssertionError("out=" + out + "; printed8=" + printed8); } } void doTestEquivalence(String code7, String code8, String testClass) throws Exception { Path classes = prepareVersionedCTSym(new String[] {code7}, new String[] {code8}); Path classfile = classes.resolve("78").resolve("java.base").resolve(testClass.replace('.', '/') + ".class"); if (!Files.isReadable(classfile)) { throw new AssertionError("Cannot find expected class."); } } @Test void testIncluded() throws Exception { doTestIncluded("package t;\n" + "public class Test extends PP1 implements PP3, PP5 {\n" + " public PP7 m1(PP8 p) { return null;}\n" + " public PP9 m2(PPB p) { return null;}\n" + " public PPD f1;\n" + " public PPE f2;\n" + " public Test2 aux;\n" + "}\n" + "class Test2 extends PPG implements PPH, PPI {\n" + "}\n" + "class PP1 {}\n" + "class PP2 {}\n" + "interface PP3 {}\n" + "class PP4 {}\n" + "interface PP5 {}\n" + "class PP6 {}\n" + "class PP7 {}\n" + "class PP8 {}\n" + "class PP9 {}\n" + "class PPA {}\n" + "class PPB {}\n" + "class PPC {}\n" + "class PPD {}\n" + "class PPE {}\n" + "class PPF {}\n" + "class PPG {}\n" + "interface PPH {}\n" + "interface PPI {}\n", "t.Test", "t.Test2", "t.PP1", "t.PP2", "t.PP3", "t.PP4", "t.PP5", "t.PP6", "t.PP7", "t.PP8", "t.PP9", "t.PPA", "t.PPB", "t.PPC", "t.PPD", "t.PPE", "t.PPF", "t.PPG", "t.PPH", "t.PPI"); } @Test void testRecords() throws Exception { doPrintElementTest("package t;" + "public class T {" + " public record R(int i, java.util.List l) { }" + "}", "package t;" + "public class T {" + " public record R(@Ann int i, long j, java.util.List l) { }" + " public @interface Ann {} " + "}", "t.T$R", """ public static record R(int i, java.util.List l) { public R(int i, java.util.List l); public final java.lang.String toString(); public final int hashCode(); public final boolean equals(java.lang.Object arg0); public int i(); public java.util.List l(); } """, "t.T$R", """ public static record R(@t.T.Ann int i, long j, java.util.List l) { public final java.lang.String toString(); public final int hashCode(); public final boolean equals(java.lang.Object arg0); public java.util.List l(); public R(@t.T.Ann int i, long j, java.util.List l); @t.T.Ann public int i(); public long j(); } """); doPrintElementTest("package t;" + "public record R() {" + "}", "package t;" + "public record R(int i) {" + "}", "t.R", """ package t; \n\ public record R() { \n\ public R(); \n\ public final java.lang.String toString(); \n\ public final int hashCode(); \n\ public final boolean equals(java.lang.Object arg0); } """, "t.R", """ package t; \n\ public record R(int i) { \n\ public final java.lang.String toString(); \n\ public final int hashCode(); \n\ public final boolean equals(java.lang.Object arg0); \n\ public R(int i); \n\ public int i(); } """); } @Test void testNonExportedSuperclass() throws Exception { doTestComplex("api.Api", """ package api; public class Api extends nonapi.Impl.Nested.Exp { public Api(); } """, """ import api.Api; public class Test { private void t(Api api) { api.run(); } } """, """ import api.Api; public class Test { private void t(Api api) { fail } } """, """ module m { exports api; } """, """ package api; import nonapi.Impl; public class Api extends Impl.Nested.Exp { } """, """ package api; public @interface Ann { } """, """ package nonapi; import api.Ann; public class Impl { public static final String C = ""; public void test() {} @Ann public static class Nested { public static class Exp extends Nested implements Runnable { public void run() {} public OtherNested get() { return null; } } } public static class OtherNested {} } """); } void doTestComplex(String printClass, String expected, String depSuccess, String depFailure, String... code) throws Exception { ToolBox tb = new ToolBox(); String testClasses = System.getProperty("test.classes"); Path output = Paths.get(testClasses, "test-data" + i++); deleteRecursively(output); Files.createDirectories(output); Path ver9Jar = output.resolve("9.jar"); compileAndPack(output, ver9Jar, code); Path ctSym = output.resolve("ct.sym"); deleteRecursively(ctSym); CreateSymbols.ALLOW_NON_EXISTING_CLASSES = true; CreateSymbols.EXTENSION = ".class"; deleteRecursively(ctSym); List versions = Arrays.asList(new VersionDescription(ver9Jar.toAbsolutePath().toString(), "9", null)); ExcludeIncludeList acceptAll = new ExcludeIncludeList(null, null) { @Override public boolean accepts(String className, boolean includePrivateClasses) { return true; } }; new CreateSymbols().createBaseLine(versions, acceptAll, ctSym, new String[0]); Path symbolsDesc = ctSym.resolve("symbols"); Path modules = ctSym.resolve("modules"); Path modulesList = ctSym.resolve("modules-list"); Files.createDirectories(modules); try (Writer w = Files.newBufferedWriter(modulesList)) {} Path classesZip = output.resolve("classes.zip"); Path classesDir = output.resolve("classes"); new CreateSymbols().createSymbols(null, symbolsDesc.toAbsolutePath().toString(), classesZip.toAbsolutePath().toString(), 0, "9", "", modules.toString(), modulesList.toString()); try (JarFile jf = new JarFile(classesZip.toFile())) { Enumeration en = jf.entries(); while (en.hasMoreElements()) { JarEntry je = en.nextElement(); if (je.isDirectory()) continue; Path target = classesDir.resolve(je.getName()); Files.createDirectories(target.getParent()); Files.copy(jf.getInputStream(je), target); } } Path classes = classesDir; Path scratch = output.resolve("scratch"); Files.createDirectories(scratch); String modulePath; try (Stream elements = Files.list(classes)) { modulePath = elements.filter(el -> el.getFileName().toString().contains("9")) .map(el -> el.resolve("m")) .map(el -> el.toAbsolutePath().toString()) .collect(Collectors.joining(File.pathSeparator)); } { String out = new JavacTask(tb, Task.Mode.CMDLINE) .options("-d", scratch.toAbsolutePath().toString(), "--module-path", modulePath, "--add-modules", "m", "-Xprint", "api.Api") .run(Expect.SUCCESS) .getOutput(Task.OutputKind.STDOUT) .replaceAll("\\R", "\n"); if (!out.equals(expected)) { throw new AssertionError("out=" + out + "; expected=" + expected); } } { new JavacTask(tb) .options("-d", scratch.toAbsolutePath().toString(), "--module-path", modulePath, "--add-modules", "m") .sources(depSuccess) .run(Expect.SUCCESS) .writeAll(); } { String expectedFailure = new JavacTask(tb) .options("-d", scratch.toAbsolutePath().toString(), "--module-path", output.resolve("temp").toString(), "--add-modules", "m", "-XDrawDiagnostics") .sources(depFailure) .run(Expect.FAIL) .getOutput(Task.OutputKind.DIRECT) .replaceAll("\\R", "\n"); String out = new JavacTask(tb) .options("-d", scratch.toAbsolutePath().toString(), "--module-path", modulePath, "--add-modules", "m", "-XDrawDiagnostics") .sources(depFailure) .run(Expect.FAIL) .getOutput(Task.OutputKind.DIRECT) .replaceAll("\\R", "\n"); if (!out.equals(expectedFailure)) { throw new AssertionError("out=" + out + "; expected=" + expectedFailure); } } } @Test void testExtendsInternalData1() throws Exception { doTestData(""" module name m header exports api extraModulePackages nonapi requires name\\u0020;java.base\\u0020;flags\\u0020;8000\\u0020;version\\u0020;0 flags 8000 class name api/Ann header extends java/lang/Object implements java/lang/annotation/Annotation flags 2601 class name api/Api header extends nonapi/Impl$Nested$Exp flags 21 innerclass innerClass nonapi/Impl$Nested outerClass nonapi/Impl innerClassName Nested flags 9 innerclass innerClass nonapi/Impl$Nested$Exp outerClass nonapi/Impl$Nested innerClassName Exp flags 9 method name descriptor ()V flags 1 class name nonapi/Impl header extends java/lang/Object nestMembers nonapi/Impl$Nested,nonapi/Impl$Nested$Exp flags 21 innerclass innerClass nonapi/Impl$Nested outerClass nonapi/Impl innerClassName Nested flags 9 innerclass innerClass nonapi/Impl$Nested$Exp outerClass nonapi/Impl$Nested innerClassName Exp flags 9 field name C descriptor Ljava/lang/String; constantValue flags 19 method name descriptor ()V flags 1 method name test descriptor ()V flags 1 class name nonapi/Impl$Nested header extends java/lang/Object nestHost nonapi/Impl flags 21 classAnnotations @Lapi/Ann; innerclass innerClass nonapi/Impl$Nested outerClass nonapi/Impl innerClassName Nested flags 9 innerclass innerClass nonapi/Impl$Nested$Exp outerClass nonapi/Impl$Nested innerClassName Exp flags 9 method name descriptor ()V flags 1 class name nonapi/Impl$Nested$Exp header extends nonapi/Impl$Nested implements java/lang/Runnable nestHost nonapi/Impl flags 21 innerclass innerClass nonapi/Impl$Nested outerClass nonapi/Impl innerClassName Nested flags 9 innerclass innerClass nonapi/Impl$Nested$Exp outerClass nonapi/Impl$Nested innerClassName Exp flags 9 method name descriptor ()V flags 1 method name run descriptor ()V flags 1 method name get descriptor ()Lnonapi/Impl$OtherNested; flags 1 """, """ module m { exports api; exports nonapi to java.base; } """, """ package api; import nonapi.Impl; public class Api extends Impl.Nested.Exp { } """, """ package api; public @interface Ann { } """, """ package nonapi; import api.Ann; public class Impl { public static final String C = ""; public void test() {} @Ann public static class Nested { public static class Exp extends Nested implements Runnable { public void run() {} public OtherNested get() { return null; } } } public static class OtherNested {} } """); } void doTestData(String data, String... code) throws Exception { String testClasses = System.getProperty("test.classes"); Path output = Paths.get(testClasses, "test-data" + i++); deleteRecursively(output); Files.createDirectories(output); Path ver9Jar = output.resolve("9.jar"); compileAndPack(output, ver9Jar, code); Path ctSym = output.resolve("ct.sym"); deleteRecursively(ctSym); CreateSymbols.ALLOW_NON_EXISTING_CLASSES = true; CreateSymbols.DO_NOT_MODIFY = ""; CreateSymbols.EXTENSION = ".class"; CreateSymbols.INJECTED_VERSION = "0"; deleteRecursively(ctSym); List versions = Arrays.asList(new VersionDescription(ver9Jar.toAbsolutePath().toString(), "9", null)); ExcludeIncludeList acceptAll = new ExcludeIncludeList(null, null) { @Override public boolean accepts(String className, boolean includePrivateClasses) { return true; } }; new CreateSymbols().createBaseLine(versions, acceptAll, ctSym, new String[0]); Path symFile = null; try (DirectoryStream ds = Files.newDirectoryStream(ctSym)) { for (Path p : ds) { if (p.toString().endsWith(".sym.txt")) { if (symFile != null) { throw new IllegalStateException("Multiple sym files!"); } else { symFile = p; } } } } String acutalContent = new String(Files.readAllBytes(symFile), StandardCharsets.UTF_8); if (!acutalContent.equals(data)) { throw new AssertionError("out=" + acutalContent + "; expected=" + data); } } void doTestIncluded(String code, String... includedClasses) throws Exception { boolean oldIncludeAll = includeAll; try { includeAll = false; Path classes = prepareVersionedCTSym(new String[] {code}, new String[] {"package other; public class Other {}"}); Path root = classes.resolve("7").resolve("java.base"); try (Stream classFiles = Files.walk(root)) { Set names = classFiles.map(p -> root.relativize(p)) .map(p -> p.toString()) .map(n -> {System.err.println("n= " + n); return n;}) .filter(n -> n.endsWith(".class")) .map(n -> n.substring(0, n.lastIndexOf('.'))) .map(n -> n.replace(File.separator, ".")) .collect(Collectors.toSet()); if (!names.equals(new HashSet<>(Arrays.asList(includedClasses)))) throw new AssertionError("Expected classes not included: " + names); } } finally { includeAll = oldIncludeAll; } } Path prepareVersionedCTSym(String[] code7, String[] code8) throws Exception { return prepareVersionedCTSym(code7, code8, _ -> {}); } Path prepareVersionedCTSym(String[] code7, String[] code8, Consumer adjustClassFiles) throws Exception { String testClasses = System.getProperty("test.classes"); Path output = Paths.get(testClasses, "test-data" + i++); deleteRecursively(output); Files.createDirectories(output); Path ver7Jar = output.resolve("7.jar"); compileAndPack(output, ver7Jar, adjustClassFiles, code7); Path ver8Jar = output.resolve("8.jar"); compileAndPack(output, ver8Jar, adjustClassFiles, code8); Path classes = output.resolve("classes.zip"); Path ctSym = output.resolve("ct.sym"); deleteRecursively(ctSym); CreateSymbols.ALLOW_NON_EXISTING_CLASSES = true; CreateSymbols.EXTENSION = ".class"; testGenerate(ver7Jar, ver8Jar, ctSym, "8", classes.toAbsolutePath().toString()); Path classesDir = output.resolve("classes"); try (JarFile jf = new JarFile(classes.toFile())) { Enumeration en = jf.entries(); while (en.hasMoreElements()) { JarEntry je = en.nextElement(); if (je.isDirectory()) continue; Path target = classesDir.resolve(je.getName()); Files.createDirectories(target.getParent()); Files.copy(jf.getInputStream(je), target); } } return classesDir; } boolean includeAll = true; void testGenerate(Path jar7, Path jar8, Path descDest, String version, String classDest) throws IOException { deleteRecursively(descDest); List versions = Arrays.asList(new VersionDescription(jar7.toAbsolutePath().toString(), "7", null), new VersionDescription(jar8.toAbsolutePath().toString(), "8", "7")); ExcludeIncludeList acceptAll = new ExcludeIncludeList(null, null) { @Override public boolean accepts(String className, boolean includePrivateClasses) { return true; } }; new CreateSymbols() { @Override protected boolean includeEffectiveAccess(ClassList classes, ClassDescription clazz) { return includeAll ? true : super.includeEffectiveAccess(classes, clazz); } }.createBaseLine(versions, acceptAll, descDest, new String[0]); Path symbolsDesc = descDest.resolve("symbols"); Path modules = descDest.resolve("modules"); Path modulesList = descDest.resolve("modules-list"); Files.createDirectories(modules); try (Writer w = Files.newBufferedWriter(modulesList)) {} new CreateSymbols().createSymbols(null, symbolsDesc.toAbsolutePath().toString(), classDest, 0, "8", "", modules.toString(), modulesList.toString()); } @Test void testModuleMainClass() throws Exception { ClassFile cf = ClassFile.of(); ToolBox tb = new ToolBox(); String testClasses = System.getProperty("test.classes"); Path output = Paths.get(testClasses, "test-data" + i++); deleteRecursively(output); Files.createDirectories(output); Path ver9Jar = output.resolve("9.jar"); compileAndPack(output, ver9Jar, classesDir -> { try { Path moduleInfo = classesDir.resolve("module-info.class"); byte[] newClassData = cf.transformClass(cf.parse(moduleInfo), (builder, element) -> { builder.with(element); if (element instanceof ModuleAttribute) { builder.with(ModuleMainClassAttribute.of(ClassDesc.of("main.Main"))); } }); try (OutputStream out = Files.newOutputStream(moduleInfo)) { out.write(newClassData); } } catch (IOException ex) { throw new UncheckedIOException(ex); } }, """ module m { } """, """ package main; public class Main {} """); Path ctSym = output.resolve("ct.sym"); deleteRecursively(ctSym); CreateSymbols.ALLOW_NON_EXISTING_CLASSES = true; CreateSymbols.EXTENSION = ".class"; List versions = Arrays.asList(new VersionDescription(ver9Jar.toAbsolutePath().toString(), "9", null)); ExcludeIncludeList acceptAll = new ExcludeIncludeList(null, null) { @Override public boolean accepts(String className, boolean includePrivateClasses) { return true; } }; new CreateSymbols().createBaseLine(versions, acceptAll, ctSym, new String[0]); Path symbolsDesc = ctSym.resolve("symbols"); Path modules = ctSym.resolve("modules"); Path modulesList = ctSym.resolve("modules-list"); Files.createDirectories(modules); try (Writer w = Files.newBufferedWriter(modulesList)) {} Path classesZip = output.resolve("classes.zip"); Path classesDir = output.resolve("classes"); new CreateSymbols().createSymbols(null, symbolsDesc.toAbsolutePath().toString(), classesZip.toAbsolutePath().toString(), 0, "9", "", modules.toString(), modulesList.toString()); try (JarFile jf = new JarFile(classesZip.toFile())) { Enumeration en = jf.entries(); while (en.hasMoreElements()) { JarEntry je = en.nextElement(); if (je.isDirectory()) continue; Path target = classesDir.resolve(je.getName()); Files.createDirectories(target.getParent()); Files.copy(jf.getInputStream(je), target); } } Path moduleInfo = classesDir.resolve("9") .resolve("m") .resolve("module-info.class"); cf.parse(moduleInfo) .attributes() .stream() .filter(attr -> attr instanceof ModuleMainClassAttribute) .forEach(attr -> { String expectedMain = "Lmain/Main;"; String mainClass = ((ModuleMainClassAttribute) attr).mainClass() .asSymbol() .descriptorString(); if (!Objects.equals(expectedMain, mainClass)) { throw new AssertionError("Expected " + expectedMain + " as a main class, " + "but got: " + mainClass); } }); } void compileAndPack(Path output, Path outputFile, String... code) throws Exception { compileAndPack(output, outputFile, _ -> {}, code); } void compileAndPack(Path output, Path outputFile, Consumer adjustClassFiles, String... code) throws Exception { ToolBox tb = new ToolBox(); Path scratch = output.resolve("temp"); deleteRecursively(scratch); Files.createDirectories(scratch); System.err.println(Arrays.asList(code)); new JavacTask(tb).sources(code).options("-d", scratch.toAbsolutePath().toString()).run(Expect.SUCCESS); List classFiles = collectClassFile(scratch); Path moduleInfo = scratch.resolve("module-info.class"); if (Files.exists(moduleInfo)) { Set packages = new HashSet<>(); for (String cf : classFiles) { int sep = cf.lastIndexOf(scratch.getFileSystem().getSeparator()); if (sep != (-1)) { packages.add(cf.substring(0, sep)); } } ClassFile cf = ClassFile.of(); ClassModel cm = cf.parse(moduleInfo); byte[] newData = cf.transformClass(cm, (builder, element) -> { builder.with(element); if (element instanceof ModuleAttribute) { builder.with(ModulePackagesAttribute.ofNames(packages.stream() .map(pack -> PackageDesc.of(pack)) .toList())); } }); try (OutputStream out = Files.newOutputStream(moduleInfo)) { out.write(newData); } } adjustClassFiles.accept(scratch); try (Writer out = Files.newBufferedWriter(outputFile)) { for (String classFile : classFiles) { try (InputStream in = Files.newInputStream(scratch.resolve(classFile))) { int read; while ((read = in.read()) != (-1)) { out.write(String.format("%02x", read)); } out.write("\n"); } } } } List collectClassFile(Path root) throws IOException { try (Stream files = Files.walk(root)) { return files.filter(p -> Files.isRegularFile(p)) .filter(p -> p.getFileName().toString().endsWith(".class")) .map(p -> root.relativize(p).toString()) .filter(p -> !p.contains("impl")) .collect(Collectors.toList()); } } void deleteRecursively(Path dir) throws IOException { Files.walkFileTree(dir, new FileVisitor() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); } @Retention(RetentionPolicy.RUNTIME) @interface Test { } }