From 2a29930c8677b75b9c99a9cd5339f0b8009e72f4 Mon Sep 17 00:00:00 2001 From: George Fraser Date: Wed, 9 Jan 2019 00:07:25 -0800 Subject: Reader access flags directly from class file --- src/main/java/org/javacs/ClassHeader.java | 140 +++++++++++++++++++++ src/main/java/org/javacs/ClassSource.java | 35 ------ src/main/java/org/javacs/Classes.java | 50 +------- src/main/java/org/javacs/CompileFile.java | 4 +- src/main/java/org/javacs/CompileFocus.java | 45 ++++++- src/main/java/org/javacs/JavaCompilerService.java | 2 +- src/test/java/org/javacs/ClassesTest.java | 11 +- .../java/org/javacs/SourceFileManagerTest.java | 13 ++ 8 files changed, 202 insertions(+), 98 deletions(-) create mode 100644 src/main/java/org/javacs/ClassHeader.java delete mode 100644 src/main/java/org/javacs/ClassSource.java diff --git a/src/main/java/org/javacs/ClassHeader.java b/src/main/java/org/javacs/ClassHeader.java new file mode 100644 index 0000000..5f5ad0d --- /dev/null +++ b/src/main/java/org/javacs/ClassHeader.java @@ -0,0 +1,140 @@ +package org.javacs; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; + +// Read the classfile format defined in https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html +class ClassHeader { + + final boolean isPublic, isFinal, isInterface, isAbstract, isAnnotation, isEnum, isModule; + + static ClassHeader of(InputStream in) { + return new ClassHeader(new DataInputStream(in)); + } + + private ClassHeader(DataInputStream in) { + try { + // u4 magic; + // u2 minor_version; + // u2 major_version; + // u2 constant_pool_count; + // cp_info constant_pool[constant_pool_count-1]; + // u2 access_flags; + var magic = in.readNBytes(4); + var minorVersion = in.readUnsignedShort(); + var majorVersion = in.readUnsignedShort(); + var constantPoolCount = in.readUnsignedShort(); + var constants = new Constant[constantPoolCount]; + var i = 0; + while (i < constantPoolCount - 1) { + constants[i] = readConstant(in); + i += slots(constants[i]); + } + var accessFlags = in.readUnsignedShort(); + + this.isPublic = (accessFlags & ACC_PUBLIC) != 0; + this.isFinal = (accessFlags & ACC_FINAL) != 0; + this.isInterface = (accessFlags & ACC_INTERFACE) != 0; + this.isAbstract = (accessFlags & ACC_ABSTRACT) != 0; + this.isAnnotation = (accessFlags & ACC_ANNOTATION) != 0; + this.isEnum = (accessFlags & ACC_ENUM) != 0; + this.isModule = (accessFlags & ACC_MODULE) != 0; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static final int ACC_PUBLIC = 0x0001; // Declared public; may be accessed from outside its package. + private static final int ACC_FINAL = 0x0010; // Declared final; no subclasses allowed. + private static final int ACC_SUPER = + 0x0020; // Treat superclass methods specially when invoked by the invokespecial instruction. + private static final int ACC_INTERFACE = 0x0200; // Is an interface, not a class. + private static final int ACC_ABSTRACT = 0x0400; // Declared abstract; must not be instantiated. + private static final int ACC_SYNTHETIC = 0x1000; // Declared synthetic; not present in the source code. + private static final int ACC_ANNOTATION = 0x2000; // Declared as an annotation type. + private static final int ACC_ENUM = 0x4000; // Declared as an enum type. + private static final int ACC_MODULE = 0x8000; // Is a module, not a class or interface. + + private static final int CONSTANT_Class = 7; + private static final int CONSTANT_Fieldref = 9; + private static final int CONSTANT_Methodref = 10; + private static final int CONSTANT_InterfaceMethodref = 11; + private static final int CONSTANT_String = 8; + private static final int CONSTANT_Integer = 3; + private static final int CONSTANT_Float = 4; + private static final int CONSTANT_Long = 5; + private static final int CONSTANT_Double = 6; + private static final int CONSTANT_NameAndType = 12; + private static final int CONSTANT_Utf8 = 1; + private static final int CONSTANT_MethodHandle = 15; + private static final int CONSTANT_MethodType = 16; + private static final int CONSTANT_InvokeDynamic = 18; + private static final int CONSTANT_Module = 19; + private static final int CONSTANT_Package = 20; + + private Constant readConstant(DataInputStream in) throws IOException { + var tag = (short) in.readByte(); + switch (tag) { + case CONSTANT_Class: + case CONSTANT_String: + case CONSTANT_MethodType: + { + var info = in.readNBytes(2); + return new Constant(tag, info); + } + case CONSTANT_Fieldref: + case CONSTANT_Methodref: + case CONSTANT_InterfaceMethodref: + case CONSTANT_Integer: + case CONSTANT_Float: + case CONSTANT_NameAndType: + case CONSTANT_InvokeDynamic: + { + var info = in.readNBytes(4); + return new Constant(tag, info); + } + case CONSTANT_Long: + case CONSTANT_Double: + { + var info = in.readNBytes(8); + return new Constant(tag, info); + } + case CONSTANT_Utf8: + { + var length = in.readUnsignedShort(); + var string = in.readNBytes(length); + return new Constant(tag, string); + } + case CONSTANT_MethodHandle: + case CONSTANT_Module: + case CONSTANT_Package: + { + var info = in.readNBytes(3); + return new Constant(tag, info); + } + default: + throw new RuntimeException("Don't know what to do with " + tag); + } + } + + private int slots(Constant c) { + switch (c.tag) { + case CONSTANT_Long: + case CONSTANT_Double: + return 2; + default: + return 1; + } + } + + private static class Constant { + final short tag; + final byte[] info; + + Constant(short tag, byte[] info) { + this.tag = tag; + this.info = info; + } + } +} diff --git a/src/main/java/org/javacs/ClassSource.java b/src/main/java/org/javacs/ClassSource.java deleted file mode 100644 index cbf6bbd..0000000 --- a/src/main/java/org/javacs/ClassSource.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.javacs; - -import java.lang.reflect.Modifier; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; -import java.util.logging.Logger; - -interface ClassSource { - Set classes(); - - Optional> load(String className); - - static final Logger LOG = Logger.getLogger("main"); - static final Set failedToLoad = new HashSet<>(); - - // TODO figure this out by directly reading the class name - // https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html - // https://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/jdk.jdeps/share/classes/com/sun/tools/classfile/ClassFile.java - default boolean isPublic(String className) { - if (failedToLoad.contains(className)) return false; - try { - return load(className).map(c -> Modifier.isPublic(c.getModifiers())).orElse(false); - } catch (Exception e) { - LOG.warning(String.format("Failed to load %s: %s", className, e.getMessage())); - failedToLoad.add(className); - return false; - } - } - - default boolean isAccessibleFromPackage(String className, String fromPackage) { - var packageName = Parser.mostName(className); - return packageName.equals(fromPackage) || isPublic(className); - } -} diff --git a/src/main/java/org/javacs/Classes.java b/src/main/java/org/javacs/Classes.java index bc6b321..a935d33 100644 --- a/src/main/java/org/javacs/Classes.java +++ b/src/main/java/org/javacs/Classes.java @@ -9,9 +9,7 @@ import java.net.URLClassLoader; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.logging.Level; @@ -115,9 +113,7 @@ class Classes { "jdk.zipfs", }; - private static Set loadError = new HashSet(); - - static ClassSource jdkTopLevelClasses() { + static Set jdkTopLevelClasses() { LOG.info("Searching for top-level classes in the JDK"); var classes = new HashSet(); @@ -142,29 +138,10 @@ class Classes { LOG.info(String.format("Found %d classes in the java platform", classes.size())); - class PlatformClassSource implements ClassSource { - @Override - public Set classes() { - return Collections.unmodifiableSet(classes); - } - - @Override - public Optional> load(String className) { - if (loadError.contains(className)) return Optional.empty(); - - try { - return Optional.of(ClassLoader.getPlatformClassLoader().loadClass(className)); - } catch (ClassNotFoundException | NoClassDefFoundError | ClassFormatError e) { - LOG.log(Level.WARNING, "Could not load " + className + ": " + e.getMessage()); - loadError.add(className); - return Optional.empty(); - } - } - } - return new PlatformClassSource(); + return classes; } - static ClassSource classPathTopLevelClasses(Set classPath) { + static Set classPathTopLevelClasses(Set classPath) { LOG.info(String.format("Searching for top-level classes in %d classpath locations", classPath.size())); Function toUrl = @@ -187,26 +164,7 @@ class Classes { LOG.info(String.format("Found %d classes in classpath", classes.size())); - class ClassPathClassSource implements ClassSource { - @Override - public Set classes() { - return Collections.unmodifiableSet(classes); - } - - @Override - public Optional> load(String className) { - if (loadError.contains(className)) return Optional.empty(); - - try { - return Optional.of(classLoader.loadClass(className)); - } catch (ClassNotFoundException | NoClassDefFoundError | ClassFormatError e) { - LOG.log(Level.WARNING, "Could not load " + className + ": " + e.getMessage()); - loadError.add(className); - return Optional.empty(); - } - } - } - return new ClassPathClassSource(); + return classes; } private static final Logger LOG = Logger.getLogger("main"); diff --git a/src/main/java/org/javacs/CompileFile.java b/src/main/java/org/javacs/CompileFile.java index aa38001..f0d2f86 100644 --- a/src/main/java/org/javacs/CompileFile.java +++ b/src/main/java/org/javacs/CompileFile.java @@ -220,8 +220,8 @@ public class CompileFile { // TODO cache parsed imports on a per-file basis var sourcePathImports = Parser.existingImports(FileStore.all()); var classes = new HashSet(); - classes.addAll(parent.jdkClasses.classes()); - classes.addAll(parent.classPathClasses.classes()); + classes.addAll(parent.jdkClasses); + classes.addAll(parent.classPathClasses); var fixes = Parser.resolveSymbols(unresolved, sourcePathImports, classes); // Figure out which existing imports are actually used var trees = Trees.instance(task); diff --git a/src/main/java/org/javacs/CompileFocus.java b/src/main/java/org/javacs/CompileFocus.java index d7634e8..38a71d4 100644 --- a/src/main/java/org/javacs/CompileFocus.java +++ b/src/main/java/org/javacs/CompileFocus.java @@ -15,6 +15,8 @@ import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; public class CompileFocus { public static final int MAX_COMPLETION_ITEMS = 50; @@ -625,8 +627,8 @@ public class CompileFocus { result.add(prefix); } }; - for (var name : parent.jdkClasses.classes()) checkClassName.accept(name); - for (var name : parent.classPathClasses.classes()) checkClassName.accept(name); + for (var name : parent.jdkClasses) checkClassName.accept(name); + for (var name : parent.classPathClasses) checkClassName.accept(name); return result; } @@ -665,9 +667,10 @@ public class CompileFocus { // Check JDK LOG.info("...checking JDK"); - for (var c : parent.jdkClasses.classes()) { + for (var c : parent.jdkClasses) { if (tooManyItems(result.size())) return; - if (matchesPartialName.test(c) && parent.jdkClasses.isAccessibleFromPackage(c, packageName)) { + if (!matchesPartialName.test(c)) continue; + if (isSamePackage(c, packageName) || isPublicClassFile(c)) { result.add(Completion.ofClassName(c, isImported(c))); } } @@ -675,9 +678,10 @@ public class CompileFocus { // Check classpath LOG.info("...checking classpath"); var classPathNames = new HashSet(); - for (var c : parent.classPathClasses.classes()) { + for (var c : parent.classPathClasses) { if (tooManyItems(result.size())) return; - if (matchesPartialName.test(c) && parent.classPathClasses.isAccessibleFromPackage(c, packageName)) { + if (!matchesPartialName.test(c)) continue; + if (isSamePackage(c, packageName) || isPublicClassFile(c)) { result.add(Completion.ofClassName(c, isImported(c))); classPathNames.add(c); } @@ -699,6 +703,35 @@ public class CompileFocus { } } + private boolean isSamePackage(String className, String fromPackage) { + return Parser.mostName(className).equals(fromPackage); + } + + private boolean isPublicClassFile(String className) { + try { + var platform = + parent.fileManager.getJavaFileForInput( + StandardLocation.PLATFORM_CLASS_PATH, className, JavaFileObject.Kind.CLASS); + if (platform != null) return isPublic(platform); + var classpath = + parent.fileManager.getJavaFileForInput( + StandardLocation.CLASS_PATH, className, JavaFileObject.Kind.CLASS); + if (classpath != null) return isPublic(classpath); + return false; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private boolean isPublic(JavaFileObject classFile) { + try (var in = classFile.openInputStream()) { + var header = ClassHeader.of(in); + return header.isPublic; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private List accessibleClasses(Path file, String partialName, String fromPackage, Set skip) { var parse = Parser.parse(file); var toPackage = Objects.toString(parse.getPackageName(), ""); diff --git a/src/main/java/org/javacs/JavaCompilerService.java b/src/main/java/org/javacs/JavaCompilerService.java index 80fbd2b..8dfeaf8 100644 --- a/src/main/java/org/javacs/JavaCompilerService.java +++ b/src/main/java/org/javacs/JavaCompilerService.java @@ -18,7 +18,7 @@ public class JavaCompilerService { final Set classPath, docPath; final JavaCompiler compiler = ServiceLoader.load(JavaCompiler.class).iterator().next(); final Docs docs; - final ClassSource jdkClasses = Classes.jdkTopLevelClasses(), classPathClasses; + final Set jdkClasses = Classes.jdkTopLevelClasses(), classPathClasses; // Diagnostics from the last compilation task final List> diags = new ArrayList<>(); // Use the same file manager for multiple tasks, so we don't repeatedly re-compile the same files diff --git a/src/test/java/org/javacs/ClassesTest.java b/src/test/java/org/javacs/ClassesTest.java index 95212fe..c5e2a7e 100644 --- a/src/test/java/org/javacs/ClassesTest.java +++ b/src/test/java/org/javacs/ClassesTest.java @@ -16,18 +16,16 @@ public class ClassesTest { @Test public void list() { var jdk = Classes.jdkTopLevelClasses(); - assertThat(jdk.classes(), hasItem("java.util.List")); - assertThat(jdk.load("java.util.List"), not(nullValue())); + assertThat(jdk, hasItem("java.util.List")); var empty = Classes.classPathTopLevelClasses(Collections.emptySet()); - assertThat(empty.classes(), not(hasItem("java.util.List"))); + assertThat(empty, not(hasItem("java.util.List"))); } @Test public void arrayList() { var jdk = Classes.jdkTopLevelClasses(); - assertThat(jdk.classes(), hasItem("java.util.ArrayList")); - assertThat(jdk.load("java.util.ArrayList"), not(nullValue())); + assertThat(jdk, hasItem("java.util.ArrayList")); } @Test @@ -46,9 +44,6 @@ public class ClassesTest { var main = found.stream().filter(c -> c.getName().equals("org.javacs.Main")).findFirst(); assertTrue(main.isPresent()); - - var load = main.get().load(); - assertNotNull(load); } void ancestors(ClassLoader classLoader) { diff --git a/src/test/java/org/javacs/SourceFileManagerTest.java b/src/test/java/org/javacs/SourceFileManagerTest.java index 64a35ac..3b50327 100644 --- a/src/test/java/org/javacs/SourceFileManagerTest.java +++ b/src/test/java/org/javacs/SourceFileManagerTest.java @@ -71,5 +71,18 @@ public class SourceFileManagerTest { assertThat(sourceClassName, equalTo(standardJavaName)); } + @Test + public void javaUtilList() throws IOException { + var file = + sourceFileManager.getJavaFileForInput( + StandardLocation.PLATFORM_CLASS_PATH, "java.util.List", JavaFileObject.Kind.CLASS); + assertThat("Found java.util.List in platform classpath", file, notNullValue()); + + var header = ClassHeader.of(file.openInputStream()); + assertTrue(header.isInterface); + assertTrue(header.isAbstract); + assertTrue(header.isPublic); + } + private static final Logger LOG = Logger.getLogger("main"); } -- cgit v1.2.3