Merge remote-tracking branch 'aosp/upstream-master' into turbine

* aosp/upstream-master:
  Don't hang if we run out of input while seeking for matching `)` or `}`
  Improve diagnostics for unresolvable fields inside constant expressions
  Don't crash on multiple visibility modifiers
  Fix a crash reporting missing symbols in constant expressions
  Don't report an error if a static type import cannot be resolved
  Make missing symbol diagnostics more consistent
  Migrate off jsr305 annotations
  Improve diagnostics for missing enclosing classes
  Remove unused options

Bug: 110098902
Test: m checkbuild
Test: turbine framework.jar with syntax error introduced
Change-Id: I865e2e2ca466e635fa7281c25b311d3da904646c
diff --git a/java/com/google/turbine/binder/Binder.java b/java/com/google/turbine/binder/Binder.java
index cbafdef..d4b151f 100644
--- a/java/com/google/turbine/binder/Binder.java
+++ b/java/com/google/turbine/binder/Binder.java
@@ -31,6 +31,7 @@
 import com.google.turbine.binder.bound.PackageSourceBoundModule;
 import com.google.turbine.binder.bound.SourceBoundClass;
 import com.google.turbine.binder.bound.SourceHeaderBoundClass;
+import com.google.turbine.binder.bound.SourceModuleInfo;
 import com.google.turbine.binder.bound.SourceTypeBoundClass;
 import com.google.turbine.binder.bound.TypeBoundClass;
 import com.google.turbine.binder.bound.TypeBoundClass.FieldInfo;
@@ -112,7 +113,7 @@
         canonicalizeTypes(
             syms, tenv, CompoundEnv.<ClassSymbol, TypeBoundClass>of(classPathEnv).append(tenv));
 
-    ImmutableList<ModuleInfo> boundModules =
+    ImmutableList<SourceModuleInfo> boundModules =
         bindModules(
             modules,
             CompoundEnv.<ClassSymbol, TypeBoundClass>of(classPathEnv).append(tenv),
@@ -244,7 +245,7 @@
     return builder.build();
   }
 
-  private static ImmutableList<ModuleInfo> bindModules(
+  private static ImmutableList<SourceModuleInfo> bindModules(
       SimpleEnv<ModuleSymbol, PackageSourceBoundModule> modules,
       CompoundEnv<ClassSymbol, TypeBoundClass> env,
       CompoundEnv<ModuleSymbol, ModuleInfo> moduleEnv,
@@ -274,7 +275,7 @@
                 return null;
               }
             });
-    ImmutableList.Builder<ModuleInfo> bound = ImmutableList.builder();
+    ImmutableList.Builder<SourceModuleInfo> bound = ImmutableList.builder();
     for (PackageSourceBoundModule module : modules.asMap().values()) {
       bound.add(ModuleBinder.bind(module, env, moduleEnv, moduleVersion));
     }
@@ -378,12 +379,12 @@
   /** The result of binding: bound nodes for sources in the compilation, and the classpath. */
   public static class BindingResult {
     private final ImmutableMap<ClassSymbol, SourceTypeBoundClass> units;
-    private final ImmutableList<ModuleInfo> modules;
+    private final ImmutableList<SourceModuleInfo> modules;
     private final CompoundEnv<ClassSymbol, BytecodeBoundClass> classPathEnv;
 
     public BindingResult(
         ImmutableMap<ClassSymbol, SourceTypeBoundClass> units,
-        ImmutableList<ModuleInfo> modules,
+        ImmutableList<SourceModuleInfo> modules,
         CompoundEnv<ClassSymbol, BytecodeBoundClass> classPathEnv) {
       this.units = units;
       this.modules = modules;
@@ -395,7 +396,7 @@
       return units;
     }
 
-    public ImmutableList<ModuleInfo> modules() {
+    public ImmutableList<SourceModuleInfo> modules() {
       return modules;
     }
 
diff --git a/java/com/google/turbine/binder/CanonicalTypeBinder.java b/java/com/google/turbine/binder/CanonicalTypeBinder.java
index 2ff56a3..20db0d7 100644
--- a/java/com/google/turbine/binder/CanonicalTypeBinder.java
+++ b/java/com/google/turbine/binder/CanonicalTypeBinder.java
@@ -27,6 +27,7 @@
 import com.google.turbine.binder.env.Env;
 import com.google.turbine.binder.sym.ClassSymbol;
 import com.google.turbine.binder.sym.TyVarSymbol;
+import com.google.turbine.diag.SourceFile;
 import com.google.turbine.type.Type;
 import com.google.turbine.type.Type.ClassTy;
 import com.google.turbine.types.Canonicalize;
@@ -41,16 +42,17 @@
       ClassSymbol sym, SourceTypeBoundClass base, Env<ClassSymbol, TypeBoundClass> env) {
     ClassTy superClassType = null;
     if (base.superClassType() != null) {
-      superClassType = Canonicalize.canonicalizeClassTy(env, base.owner(), base.superClassType());
+      superClassType =
+          Canonicalize.canonicalizeClassTy(base.source(), env, base.owner(), base.superClassType());
     }
     ImmutableList.Builder<ClassTy> interfaceTypes = ImmutableList.builder();
     for (ClassTy i : base.interfaceTypes()) {
-      interfaceTypes.add(Canonicalize.canonicalizeClassTy(env, base.owner(), i));
+      interfaceTypes.add(Canonicalize.canonicalizeClassTy(base.source(), env, base.owner(), i));
     }
     ImmutableMap<TyVarSymbol, TyVarInfo> typParamTypes =
-        typeParameters(env, sym, base.typeParameterTypes());
-    ImmutableList<MethodInfo> methods = methods(env, sym, base.methods());
-    ImmutableList<FieldInfo> fields = fields(env, sym, base.fields());
+        typeParameters(base.source(), env, sym, base.typeParameterTypes());
+    ImmutableList<MethodInfo> methods = methods(base.source(), env, sym, base.methods());
+    ImmutableList<FieldInfo> fields = fields(base.source(), env, sym, base.fields());
     return new SourceTypeBoundClass(
         interfaceTypes.build(),
         superClassType,
@@ -71,13 +73,16 @@
   }
 
   private static ImmutableList<FieldInfo> fields(
-      Env<ClassSymbol, TypeBoundClass> env, ClassSymbol sym, ImmutableList<FieldInfo> fields) {
+      SourceFile source,
+      Env<ClassSymbol, TypeBoundClass> env,
+      ClassSymbol sym,
+      ImmutableList<FieldInfo> fields) {
     ImmutableList.Builder<FieldInfo> result = ImmutableList.builder();
     for (FieldInfo base : fields) {
       result.add(
           new FieldInfo(
               base.sym(),
-              Canonicalize.canonicalize(env, sym, base.type()),
+              Canonicalize.canonicalize(source, env, sym, base.type()),
               base.access(),
               base.annotations(),
               base.decl(),
@@ -87,16 +92,19 @@
   }
 
   private static ImmutableList<MethodInfo> methods(
-      Env<ClassSymbol, TypeBoundClass> env, ClassSymbol sym, ImmutableList<MethodInfo> methods) {
+      SourceFile source,
+      Env<ClassSymbol, TypeBoundClass> env,
+      ClassSymbol sym,
+      ImmutableList<MethodInfo> methods) {
     ImmutableList.Builder<MethodInfo> result = ImmutableList.builder();
     for (MethodInfo base : methods) {
-      ImmutableMap<TyVarSymbol, TyVarInfo> tps = typeParameters(env, sym, base.tyParams());
-      Type ret = Canonicalize.canonicalize(env, sym, base.returnType());
+      ImmutableMap<TyVarSymbol, TyVarInfo> tps = typeParameters(source, env, sym, base.tyParams());
+      Type ret = Canonicalize.canonicalize(source, env, sym, base.returnType());
       ImmutableList.Builder<ParamInfo> parameters = ImmutableList.builder();
       for (ParamInfo parameter : base.parameters()) {
-        parameters.add(param(env, sym, parameter));
+        parameters.add(param(source, env, sym, parameter));
       }
-      ImmutableList<Type> exceptions = canonicalizeList(env, sym, base.exceptions());
+      ImmutableList<Type> exceptions = canonicalizeList(source, env, sym, base.exceptions());
       result.add(
           new MethodInfo(
               base.sym(),
@@ -108,40 +116,47 @@
               base.defaultValue(),
               base.decl(),
               base.annotations(),
-              base.receiver() != null ? param(env, sym, base.receiver()) : null));
+              base.receiver() != null ? param(source, env, sym, base.receiver()) : null));
     }
     return result.build();
   }
 
   private static ParamInfo param(
-      Env<ClassSymbol, TypeBoundClass> env, ClassSymbol sym, ParamInfo base) {
+      SourceFile source, Env<ClassSymbol, TypeBoundClass> env, ClassSymbol sym, ParamInfo base) {
     return new ParamInfo(
-        Canonicalize.canonicalize(env, sym, base.type()),
+        Canonicalize.canonicalize(source, env, sym, base.type()),
         base.name(),
         base.annotations(),
         base.access());
   }
 
   private static ImmutableMap<TyVarSymbol, TyVarInfo> typeParameters(
-      Env<ClassSymbol, TypeBoundClass> env, ClassSymbol sym, Map<TyVarSymbol, TyVarInfo> tps) {
+      SourceFile source,
+      Env<ClassSymbol, TypeBoundClass> env,
+      ClassSymbol sym,
+      Map<TyVarSymbol, TyVarInfo> tps) {
     ImmutableMap.Builder<TyVarSymbol, TyVarInfo> result = ImmutableMap.builder();
     for (Map.Entry<TyVarSymbol, TyVarInfo> e : tps.entrySet()) {
       TyVarInfo info = e.getValue();
       Type superClassBound = null;
       if (info.superClassBound() != null) {
-        superClassBound = Canonicalize.canonicalize(env, sym, info.superClassBound());
+        superClassBound = Canonicalize.canonicalize(source, env, sym, info.superClassBound());
       }
-      ImmutableList<Type> interfaceBounds = canonicalizeList(env, sym, info.interfaceBounds());
+      ImmutableList<Type> interfaceBounds =
+          canonicalizeList(source, env, sym, info.interfaceBounds());
       result.put(e.getKey(), new TyVarInfo(superClassBound, interfaceBounds, info.annotations()));
     }
     return result.build();
   }
 
   private static ImmutableList<Type> canonicalizeList(
-      Env<ClassSymbol, TypeBoundClass> env, ClassSymbol sym, ImmutableList<Type> types) {
+      SourceFile source,
+      Env<ClassSymbol, TypeBoundClass> env,
+      ClassSymbol sym,
+      ImmutableList<Type> types) {
     ImmutableList.Builder<Type> result = ImmutableList.builder();
     for (Type type : types) {
-      result.add(Canonicalize.canonicalize(env, sym, type));
+      result.add(Canonicalize.canonicalize(source, env, sym, type));
     }
     return result.build();
   }
diff --git a/java/com/google/turbine/binder/ConstBinder.java b/java/com/google/turbine/binder/ConstBinder.java
index 4cb40a2..4b48dd3 100644
--- a/java/com/google/turbine/binder/ConstBinder.java
+++ b/java/com/google/turbine/binder/ConstBinder.java
@@ -182,7 +182,7 @@
       return null;
     }
     EnumConstantValue enumValue = (EnumConstantValue) value;
-    if (!enumValue.sym().owner().toString().equals("java/lang/annotation/RetentionPolicy")) {
+    if (!enumValue.sym().owner().binaryName().equals("java/lang/annotation/RetentionPolicy")) {
       return null;
     }
     return RetentionPolicy.valueOf(enumValue.sym().name());
diff --git a/java/com/google/turbine/binder/ConstEvaluator.java b/java/com/google/turbine/binder/ConstEvaluator.java
index d661fb9..7e79919 100644
--- a/java/com/google/turbine/binder/ConstEvaluator.java
+++ b/java/com/google/turbine/binder/ConstEvaluator.java
@@ -199,18 +199,24 @@
     }
     LookupResult result = scope.lookup(new LookupKey(flat));
     if (result == null) {
-      throw error(classTy.position(), ErrorKind.SYMBOL_NOT_FOUND, flat.peekFirst());
+      throw error(classTy.position(), ErrorKind.CANNOT_RESOLVE, flat.peekFirst());
     }
     ClassSymbol classSym = (ClassSymbol) result.sym();
     for (String bit : result.remaining()) {
-      classSym = Resolve.resolve(env, origin, classSym, bit);
-      if (classSym == null) {
-        throw error(classTy.position(), ErrorKind.SYMBOL_NOT_FOUND, bit);
-      }
+      classSym = resolveNext(classTy.position(), classSym, bit);
     }
     return classSym;
   }
 
+  private ClassSymbol resolveNext(int position, ClassSymbol sym, String bit) {
+    ClassSymbol next = Resolve.resolve(env, origin, sym, bit);
+    if (next == null) {
+      throw error(
+          position, ErrorKind.SYMBOL_NOT_FOUND, new ClassSymbol(sym.binaryName() + '$' + bit));
+    }
+    return next;
+  }
+
   /** Evaluates a reference to another constant variable. */
   Const evalConstVar(ConstVarName t) {
     FieldInfo field = resolveField(t);
@@ -256,7 +262,10 @@
       }
       return field;
     }
-    return null;
+    throw error(
+        t.position(),
+        ErrorKind.CANNOT_RESOLVE,
+        String.format("field %s", Iterables.getLast(t.name())));
   }
 
   private FieldInfo resolveQualifiedField(ConstVarName t) {
@@ -267,6 +276,10 @@
     if (result == null) {
       return null;
     }
+    if (result.remaining().isEmpty()) {
+      // unexpectedly resolved qualified name to a type
+      return null;
+    }
     ClassSymbol sym = (ClassSymbol) result.sym();
     for (int i = 0; i < result.remaining().size() - 1; i++) {
       sym = Resolve.resolve(env, sym, sym, result.remaining().get(i));
@@ -984,7 +997,12 @@
   }
 
   public Const.Value evalFieldInitializer(Expression expression, Type type) {
-    Const value = eval(expression);
+    Const value;
+    try {
+      value = eval(expression);
+    } catch (TurbineError error) {
+      return null;
+    }
     if (value == null || value.kind() != Const.Kind.PRIMITIVE) {
       return null;
     }
diff --git a/java/com/google/turbine/binder/CtSymClassBinder.java b/java/com/google/turbine/binder/CtSymClassBinder.java
index 0d71b8d..5988ac5 100644
--- a/java/com/google/turbine/binder/CtSymClassBinder.java
+++ b/java/com/google/turbine/binder/CtSymClassBinder.java
@@ -34,7 +34,7 @@
 import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.Map;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** Constructs a platform {@link ClassPath} from the current JDK's ct.sym file. */
 public class CtSymClassBinder {
diff --git a/java/com/google/turbine/binder/HierarchyBinder.java b/java/com/google/turbine/binder/HierarchyBinder.java
index c3e82ba..2545c17 100644
--- a/java/com/google/turbine/binder/HierarchyBinder.java
+++ b/java/com/google/turbine/binder/HierarchyBinder.java
@@ -31,6 +31,7 @@
 import com.google.turbine.diag.TurbineError.ErrorKind;
 import com.google.turbine.model.TurbineTyKind;
 import com.google.turbine.tree.Tree;
+import com.google.turbine.tree.Tree.ClassTy;
 import java.util.ArrayDeque;
 
 /** Type hierarchy binding. */
@@ -116,24 +117,31 @@
     // Resolve the base symbol in the qualified name.
     LookupResult result = lookup(ty, new LookupKey(flat));
     if (result == null) {
-      throw TurbineError.format(base.source(), ty.position(), ErrorKind.SYMBOL_NOT_FOUND, ty);
+      throw TurbineError.format(base.source(), ty.position(), ErrorKind.CANNOT_RESOLVE, ty);
     }
     // Resolve pieces in the qualified name referring to member types.
     // This needs to consider member type declarations inherited from supertypes and interfaces.
     ClassSymbol sym = (ClassSymbol) result.sym();
     for (String bit : result.remaining()) {
-      try {
-        sym = Resolve.resolve(env, origin, sym, bit);
-      } catch (LazyBindingError e) {
-        throw error(ty.position(), ErrorKind.CYCLIC_HIERARCHY, e.getMessage());
-      }
-      if (sym == null) {
-        throw error(ty.position(), ErrorKind.SYMBOL_NOT_FOUND, bit);
-      }
+      sym = resolveNext(ty, sym, bit);
     }
     return sym;
   }
 
+  private ClassSymbol resolveNext(ClassTy ty, ClassSymbol sym, String bit) {
+    ClassSymbol next;
+    try {
+      next = Resolve.resolve(env, origin, sym, bit);
+    } catch (LazyBindingError e) {
+      throw error(ty.position(), ErrorKind.CYCLIC_HIERARCHY, e.getMessage());
+    }
+    if (next == null) {
+      throw error(
+          ty.position(), ErrorKind.SYMBOL_NOT_FOUND, new ClassSymbol(sym.binaryName() + '$' + bit));
+    }
+    return next;
+  }
+
   /** Resolve a qualified type name to a symbol. */
   private LookupResult lookup(Tree tree, LookupKey lookup) {
     // Handle any lexically enclosing class declarations (if we're binding a member class).
diff --git a/java/com/google/turbine/binder/JimageClassBinder.java b/java/com/google/turbine/binder/JimageClassBinder.java
index 40be3a3..5afc5d8 100644
--- a/java/com/google/turbine/binder/JimageClassBinder.java
+++ b/java/com/google/turbine/binder/JimageClassBinder.java
@@ -48,7 +48,7 @@
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** Constructs a platform {@link ClassPath} from the current JDK's jimage file using jrtfs. */
 public class JimageClassBinder {
diff --git a/java/com/google/turbine/binder/ModuleBinder.java b/java/com/google/turbine/binder/ModuleBinder.java
index 23c9624..312ec45 100644
--- a/java/com/google/turbine/binder/ModuleBinder.java
+++ b/java/com/google/turbine/binder/ModuleBinder.java
@@ -29,6 +29,7 @@
 import com.google.turbine.binder.bound.ModuleInfo.RequireInfo;
 import com.google.turbine.binder.bound.ModuleInfo.UseInfo;
 import com.google.turbine.binder.bound.PackageSourceBoundModule;
+import com.google.turbine.binder.bound.SourceModuleInfo;
 import com.google.turbine.binder.bound.TypeBoundClass;
 import com.google.turbine.binder.env.CompoundEnv;
 import com.google.turbine.binder.env.Env;
@@ -54,7 +55,7 @@
 /** Binding pass for modules. */
 public class ModuleBinder {
 
-  public static ModuleInfo bind(
+  public static SourceModuleInfo bind(
       PackageSourceBoundModule module,
       CompoundEnv<ClassSymbol, TypeBoundClass> env,
       Env<ModuleSymbol, ModuleInfo> moduleEnv,
@@ -80,7 +81,7 @@
     this.scope = module.scope().toScope(Resolve.resolveFunction(env, /* origin= */ null));
   }
 
-  private ModuleInfo bind() {
+  private SourceModuleInfo bind() {
     // bind annotations; constant fields are already bound
     ConstEvaluator constEvaluator =
         new ConstEvaluator(
@@ -146,7 +147,7 @@
               .addAll(requires.build());
     }
 
-    return new ModuleInfo(
+    return new SourceModuleInfo(
         module.module().moduleName(),
         moduleVersion.orNull(),
         flags,
@@ -155,7 +156,8 @@
         exports.build(),
         opens.build(),
         uses.build(),
-        provides.build());
+        provides.build(),
+        module.source());
   }
 
   private RequireInfo bindRequires(ModRequires directive) {
@@ -204,13 +206,15 @@
     LookupKey key = new LookupKey(simpleNames);
     LookupResult result = scope.lookup(key);
     if (result == null) {
-      throw error(ErrorKind.SYMBOL_NOT_FOUND, pos, Joiner.on('.').join(simpleNames));
+      throw error(
+          ErrorKind.SYMBOL_NOT_FOUND, pos, new ClassSymbol(Joiner.on('/').join(simpleNames)));
     }
     ClassSymbol sym = (ClassSymbol) result.sym();
     for (String name : result.remaining()) {
       sym = Resolve.resolve(env, /* origin= */ null, sym, name);
       if (sym == null) {
-        throw error(ErrorKind.SYMBOL_NOT_FOUND, pos, name);
+        throw error(
+            ErrorKind.SYMBOL_NOT_FOUND, pos, new ClassSymbol(sym.binaryName() + '$' + name));
       }
     }
     return sym;
diff --git a/java/com/google/turbine/binder/Resolve.java b/java/com/google/turbine/binder/Resolve.java
index d06dbc1..d722373 100644
--- a/java/com/google/turbine/binder/Resolve.java
+++ b/java/com/google/turbine/binder/Resolve.java
@@ -27,11 +27,7 @@
 import com.google.turbine.binder.env.LazyEnv.LazyBindingError;
 import com.google.turbine.binder.lookup.CanonicalSymbolResolver;
 import com.google.turbine.binder.lookup.ImportScope.ResolveFunction;
-import com.google.turbine.binder.lookup.LookupResult;
 import com.google.turbine.binder.sym.ClassSymbol;
-import com.google.turbine.diag.SourceFile;
-import com.google.turbine.diag.TurbineError;
-import com.google.turbine.diag.TurbineError.ErrorKind;
 import com.google.turbine.model.TurbineVisibility;
 import java.util.Objects;
 
@@ -104,18 +100,6 @@
     }
 
     @Override
-    public ClassSymbol resolve(SourceFile source, int position, LookupResult result) {
-      ClassSymbol sym = (ClassSymbol) result.sym();
-      for (String bit : result.remaining()) {
-        sym = resolveOne(sym, bit);
-        if (sym == null) {
-          throw TurbineError.format(source, position, ErrorKind.SYMBOL_NOT_FOUND, bit);
-        }
-      }
-      return sym;
-    }
-
-    @Override
     public ClassSymbol resolveOne(ClassSymbol sym, String bit) {
       BoundClass ci = env.get(sym);
       if (ci == null) {
diff --git a/java/com/google/turbine/binder/TypeBinder.java b/java/com/google/turbine/binder/TypeBinder.java
index 964958b..e0546ac 100644
--- a/java/com/google/turbine/binder/TypeBinder.java
+++ b/java/com/google/turbine/binder/TypeBinder.java
@@ -560,14 +560,11 @@
     for (Tree.Anno tree : trees) {
       LookupResult lookupResult = scope.lookup(new LookupKey(tree.name()));
       if (lookupResult == null) {
-        throw error(tree.position(), ErrorKind.SYMBOL_NOT_FOUND, Joiner.on('.').join(tree.name()));
+        throw error(tree.position(), ErrorKind.CANNOT_RESOLVE, Joiner.on('.').join(tree.name()));
       }
       ClassSymbol sym = (ClassSymbol) lookupResult.sym();
       for (String name : lookupResult.remaining()) {
-        sym = Resolve.resolve(env, owner, sym, name);
-        if (sym == null) {
-          throw error(tree.position(), ErrorKind.SYMBOL_NOT_FOUND, name);
-        }
+        sym = resolveNext(tree.position(), sym, name);
       }
       if (env.get(sym).kind() != TurbineTyKind.ANNOTATION) {
         throw error(tree.position(), ErrorKind.NOT_AN_ANNOTATION, sym);
@@ -577,6 +574,15 @@
     return result.build();
   }
 
+  private ClassSymbol resolveNext(int position, ClassSymbol sym, String bit) {
+    ClassSymbol next = Resolve.resolve(env, owner, sym, bit);
+    if (next == null) {
+      throw error(
+          position, ErrorKind.SYMBOL_NOT_FOUND, new ClassSymbol(sym.binaryName() + '$' + bit));
+    }
+    return next;
+  }
+
   private ImmutableList<Type> bindTyArgs(CompoundScope scope, ImmutableList<Tree.Type> targs) {
     ImmutableList.Builder<Type> result = ImmutableList.builder();
     for (Tree.Type ty : targs) {
@@ -627,7 +633,7 @@
     // resolve the prefix to a symbol
     LookupResult result = scope.lookup(new LookupKey(names));
     if (result == null || result.sym() == null) {
-      throw error(t.position(), ErrorKind.SYMBOL_NOT_FOUND, t);
+      throw error(t.position(), ErrorKind.CANNOT_RESOLVE, t);
     }
     Symbol sym = result.sym();
     int annoIdx = flat.size() - result.remaining().size() - 1;
@@ -660,10 +666,8 @@
             sym, bindTyArgs(scope, flat.get(idx++).tyargs()), annotations));
     for (; idx < flat.size(); idx++) {
       Tree.ClassTy curr = flat.get(idx);
-      sym = Resolve.resolve(env, owner, sym, curr.name());
-      if (sym == null) {
-        throw error(curr.position(), ErrorKind.CANNOT_RESOLVE, curr.name());
-      }
+      sym = resolveNext(curr.position(), sym, curr.name());
+
       annotations = bindAnnotations(scope, curr.annos());
       classes.add(
           new Type.ClassTy.SimpleClassTy(sym, bindTyArgs(scope, curr.tyargs()), annotations));
diff --git a/java/com/google/turbine/binder/bound/BoundClass.java b/java/com/google/turbine/binder/bound/BoundClass.java
index ad26af7..61dee0f 100644
--- a/java/com/google/turbine/binder/bound/BoundClass.java
+++ b/java/com/google/turbine/binder/bound/BoundClass.java
@@ -19,7 +19,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.turbine.binder.sym.ClassSymbol;
 import com.google.turbine.model.TurbineTyKind;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * The initial bound tree representation.
diff --git a/java/com/google/turbine/binder/bound/ModuleInfo.java b/java/com/google/turbine/binder/bound/ModuleInfo.java
index 9afd474..f21213b 100644
--- a/java/com/google/turbine/binder/bound/ModuleInfo.java
+++ b/java/com/google/turbine/binder/bound/ModuleInfo.java
@@ -19,7 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.turbine.binder.sym.ClassSymbol;
 import com.google.turbine.type.AnnoInfo;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A bound module declaration (see JLS §7.7). */
 public class ModuleInfo {
diff --git a/java/com/google/turbine/binder/bound/SourceModuleInfo.java b/java/com/google/turbine/binder/bound/SourceModuleInfo.java
new file mode 100644
index 0000000..1163e9f
--- /dev/null
+++ b/java/com/google/turbine/binder/bound/SourceModuleInfo.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.turbine.binder.bound;
+
+import com.google.common.collect.ImmutableList;
+import com.google.turbine.binder.bound.ModuleInfo.ExportInfo;
+import com.google.turbine.binder.bound.ModuleInfo.OpenInfo;
+import com.google.turbine.binder.bound.ModuleInfo.ProvideInfo;
+import com.google.turbine.binder.bound.ModuleInfo.RequireInfo;
+import com.google.turbine.binder.bound.ModuleInfo.UseInfo;
+import com.google.turbine.diag.SourceFile;
+import com.google.turbine.type.AnnoInfo;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A {@link ModuleInfo} that corresponds to a source file being compiled. */
+public class SourceModuleInfo extends ModuleInfo {
+
+  private final SourceFile source;
+
+  public SourceModuleInfo(
+      String name,
+      @Nullable String version,
+      int flags,
+      ImmutableList<AnnoInfo> annos,
+      ImmutableList<RequireInfo> requires,
+      ImmutableList<ExportInfo> exports,
+      ImmutableList<OpenInfo> opens,
+      ImmutableList<UseInfo> uses,
+      ImmutableList<ProvideInfo> provides,
+      SourceFile source) {
+    super(name, version, flags, annos, requires, exports, opens, uses, provides);
+    this.source = source;
+  }
+
+  public SourceFile source() {
+    return source;
+  }
+}
diff --git a/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java b/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
index 73f2832..b44614c 100644
--- a/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
+++ b/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
@@ -29,7 +29,7 @@
 import com.google.turbine.type.AnnoInfo;
 import com.google.turbine.type.Type;
 import com.google.turbine.type.Type.ClassTy;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A HeaderBoundClass for classes compiled from source. */
 public class SourceTypeBoundClass implements TypeBoundClass {
diff --git a/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java b/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
index 76a06e5..cfab044 100644
--- a/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
+++ b/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
@@ -53,7 +53,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.Map;
 import java.util.function.Function;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * A bound class backed by a class file.
@@ -83,7 +83,7 @@
             new Supplier<ClassFile>() {
               @Override
               public ClassFile get() {
-                ClassFile cf = ClassReader.read(jarFile + "!" + sym, bytes.get());
+                ClassFile cf = ClassReader.read(jarFile + "!" + sym.binaryName(), bytes.get());
                 verify(
                     cf.name().equals(sym.binaryName()),
                     "expected class data for %s, saw %s instead",
diff --git a/java/com/google/turbine/binder/env/CompoundEnv.java b/java/com/google/turbine/binder/env/CompoundEnv.java
index 43ce768..9b216e3 100644
--- a/java/com/google/turbine/binder/env/CompoundEnv.java
+++ b/java/com/google/turbine/binder/env/CompoundEnv.java
@@ -19,7 +19,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.turbine.binder.sym.Symbol;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** An {@link Env} that chains two existing envs together. */
 public class CompoundEnv<S extends Symbol, V> implements Env<S, V> {
diff --git a/java/com/google/turbine/binder/lookup/CanonicalSymbolResolver.java b/java/com/google/turbine/binder/lookup/CanonicalSymbolResolver.java
index 64d7c10..1e6eee1 100644
--- a/java/com/google/turbine/binder/lookup/CanonicalSymbolResolver.java
+++ b/java/com/google/turbine/binder/lookup/CanonicalSymbolResolver.java
@@ -17,17 +17,9 @@
 package com.google.turbine.binder.lookup;
 
 import com.google.turbine.binder.sym.ClassSymbol;
-import com.google.turbine.diag.SourceFile;
 
 /** Canonical type resolution. Breaks a circular dependency between binding and import handling. */
 public interface CanonicalSymbolResolver extends ImportScope.ResolveFunction {
-  /**
-   * Resolves a type by canonical name (member types must be qualified by the type that declares
-   * them, not by types that are inherited into).
-   */
-  @Override
-  ClassSymbol resolve(SourceFile source, int position, LookupResult result);
-
   /** Resolves a single member type of the given symbol by canonical name. */
   @Override
   ClassSymbol resolveOne(ClassSymbol sym, String bit);
diff --git a/java/com/google/turbine/binder/lookup/CompoundScope.java b/java/com/google/turbine/binder/lookup/CompoundScope.java
index a14eb05..11309bf 100644
--- a/java/com/google/turbine/binder/lookup/CompoundScope.java
+++ b/java/com/google/turbine/binder/lookup/CompoundScope.java
@@ -18,7 +18,7 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A {@link Scope} that chains other scopes together. */
 public class CompoundScope implements Scope {
diff --git a/java/com/google/turbine/binder/lookup/CompoundTopLevelIndex.java b/java/com/google/turbine/binder/lookup/CompoundTopLevelIndex.java
index 526e493..de50a2e 100644
--- a/java/com/google/turbine/binder/lookup/CompoundTopLevelIndex.java
+++ b/java/com/google/turbine/binder/lookup/CompoundTopLevelIndex.java
@@ -19,7 +19,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.collect.ImmutableList;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A {@link TopLevelIndex} that aggregates multiple indices into one. */
 // Note: this implementation doesn't detect if the indices contain incompatible information,
diff --git a/java/com/google/turbine/binder/lookup/ImportIndex.java b/java/com/google/turbine/binder/lookup/ImportIndex.java
index 305bbfd..eb3ac8f 100644
--- a/java/com/google/turbine/binder/lookup/ImportIndex.java
+++ b/java/com/google/turbine/binder/lookup/ImportIndex.java
@@ -90,7 +90,7 @@
               new Supplier<ImportScope>() {
                 @Override
                 public ImportScope get() {
-                  return staticNamedImport(source, cpi, i);
+                  return staticNamedImport(cpi, i);
                 }
               }));
     }
@@ -103,17 +103,41 @@
     LookupResult result = cpi.scope().lookup(new LookupKey(i.type()));
     if (result == null) {
       throw TurbineError.format(
-          source, i.position(), ErrorKind.SYMBOL_NOT_FOUND, Joiner.on('.').join(i.type()));
+          source,
+          i.position(),
+          ErrorKind.SYMBOL_NOT_FOUND,
+          new ClassSymbol(Joiner.on('/').join(i.type())));
     }
-    ClassSymbol sym = resolve.resolve(source, i.position(), result);
+    ClassSymbol sym = (ClassSymbol) result.sym();
+    for (String bit : result.remaining()) {
+      sym = resolveNext(source, i.position(), resolve, sym, bit);
+    }
+    ClassSymbol resolved = sym;
     return new ImportScope() {
       @Override
       public LookupResult lookup(LookupKey lookupKey, ResolveFunction unused) {
-        return new LookupResult(sym, lookupKey);
+        return new LookupResult(resolved, lookupKey);
       }
     };
   }
 
+  private static ClassSymbol resolveNext(
+      SourceFile source,
+      int position,
+      CanonicalSymbolResolver resolve,
+      ClassSymbol sym,
+      String bit) {
+    ClassSymbol next = resolve.resolveOne(sym, bit);
+    if (next == null) {
+      throw TurbineError.format(
+          source,
+          position,
+          ErrorKind.SYMBOL_NOT_FOUND,
+          new ClassSymbol(sym.binaryName() + '$' + bit));
+    }
+    return next;
+  }
+
   /**
    * Resolve the base class symbol of a possibly non-canonical static named import. For example,
    * {@code import static java.util.HashMap.Entry;} is a non-canonical import for {@code
@@ -121,7 +145,7 @@
    * hierarchy analysis is complete, so for now we resolve the base {@code java.util.HashMap} and
    * defer the rest.
    */
-  private static ImportScope staticNamedImport(SourceFile source, TopLevelIndex cpi, ImportDecl i) {
+  private static ImportScope staticNamedImport(TopLevelIndex cpi, ImportDecl i) {
     LookupResult base = cpi.scope().lookup(new LookupKey(i.type()));
     if (base == null) {
       return null;
@@ -129,8 +153,16 @@
     return new ImportScope() {
       @Override
       public LookupResult lookup(LookupKey lookupKey, ResolveFunction resolve) {
-        ClassSymbol result = resolve.resolve(source, i.position(), base);
-        return new LookupResult(result, lookupKey);
+        ClassSymbol sym = (ClassSymbol) base.sym();
+        for (String bit : base.remaining()) {
+          sym = resolve.resolveOne(sym, bit);
+          if (sym == null) {
+            // Assume that static imports that don't resolve to types are non-type member imports,
+            // even if the simple name matched what we're looking for.
+            return null;
+          }
+        }
+        return new LookupResult(sym, lookupKey);
       }
     };
   }
diff --git a/java/com/google/turbine/binder/lookup/ImportScope.java b/java/com/google/turbine/binder/lookup/ImportScope.java
index 69ab78d..8b28cb3 100644
--- a/java/com/google/turbine/binder/lookup/ImportScope.java
+++ b/java/com/google/turbine/binder/lookup/ImportScope.java
@@ -17,9 +17,6 @@
 package com.google.turbine.binder.lookup;
 
 import com.google.turbine.binder.sym.ClassSymbol;
-import com.google.turbine.diag.SourceFile;
-import com.google.turbine.diag.TurbineError;
-import com.google.turbine.diag.TurbineError.ErrorKind;
 
 /**
  * A scope for imports. Non-canonical imports depend on hierarchy analysis, so to break the cycle we
@@ -34,19 +31,7 @@
    */
   @FunctionalInterface
   interface ResolveFunction {
-
     ClassSymbol resolveOne(ClassSymbol base, String name);
-
-    default ClassSymbol resolve(SourceFile source, int position, LookupResult result) {
-      ClassSymbol sym = (ClassSymbol) result.sym();
-      for (String bit : result.remaining()) {
-        sym = resolveOne(sym, bit);
-        if (sym == null) {
-          throw TurbineError.format(source, position, ErrorKind.SYMBOL_NOT_FOUND, bit);
-        }
-      }
-      return sym;
-    }
   }
 
   /** See {@link Scope#lookup(LookupKey)}. */
diff --git a/java/com/google/turbine/binder/lookup/MemberImportIndex.java b/java/com/google/turbine/binder/lookup/MemberImportIndex.java
index e2ce5cb..45eefb5 100644
--- a/java/com/google/turbine/binder/lookup/MemberImportIndex.java
+++ b/java/com/google/turbine/binder/lookup/MemberImportIndex.java
@@ -23,6 +23,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.turbine.binder.sym.ClassSymbol;
 import com.google.turbine.diag.SourceFile;
+import com.google.turbine.diag.TurbineError;
+import com.google.turbine.diag.TurbineError.ErrorKind;
 import com.google.turbine.tree.Tree.ImportDecl;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -53,7 +55,14 @@
                   @Override
                   public ClassSymbol get() {
                     LookupResult result = tli.scope().lookup(new LookupKey(i.type()));
-                    return result != null ? resolve.resolve(source, i.position(), result) : null;
+                    if (result == null) {
+                      return null;
+                    }
+                    ClassSymbol sym = (ClassSymbol) result.sym();
+                    for (String bit : result.remaining()) {
+                      sym = resolveNext(resolve, source, i.position(), sym, bit);
+                    }
+                    return sym;
                   }
                 }));
       } else {
@@ -79,6 +88,23 @@
     this.classes = packageScopes.build();
   }
 
+  private static ClassSymbol resolveNext(
+      CanonicalSymbolResolver resolve,
+      SourceFile source,
+      int position,
+      ClassSymbol sym,
+      String bit) {
+    ClassSymbol next = resolve.resolveOne(sym, bit);
+    if (next == null) {
+      throw TurbineError.format(
+          source,
+          position,
+          ErrorKind.SYMBOL_NOT_FOUND,
+          new ClassSymbol(sym.binaryName() + '$' + bit));
+    }
+    return next;
+  }
+
   /** Resolves the owner of a single-member static import of the given simple name. */
   public ClassSymbol singleMemberImport(String simpleName) {
     Supplier<ClassSymbol> cachedResult = cache.get(simpleName);
diff --git a/java/com/google/turbine/binder/lookup/Scope.java b/java/com/google/turbine/binder/lookup/Scope.java
index 682a62a..12466f4 100644
--- a/java/com/google/turbine/binder/lookup/Scope.java
+++ b/java/com/google/turbine/binder/lookup/Scope.java
@@ -16,7 +16,7 @@
 
 package com.google.turbine.binder.lookup;
 
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A scope that defines types, and supports qualified name resolution. */
 public interface Scope {
diff --git a/java/com/google/turbine/binder/lookup/SimpleTopLevelIndex.java b/java/com/google/turbine/binder/lookup/SimpleTopLevelIndex.java
index 403c53f..7fc7913 100644
--- a/java/com/google/turbine/binder/lookup/SimpleTopLevelIndex.java
+++ b/java/com/google/turbine/binder/lookup/SimpleTopLevelIndex.java
@@ -23,7 +23,7 @@
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Objects;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * An index of canonical type names where all members are known statically.
@@ -85,7 +85,7 @@
 
     /** Inserts a {@link ClassSymbol} into the index, creating any needed packages. */
     public boolean insert(ClassSymbol sym) {
-      Iterator<String> it = Splitter.on('/').split(sym.toString()).iterator();
+      Iterator<String> it = Splitter.on('/').split(sym.binaryName()).iterator();
       Node curr = root;
       while (it.hasNext()) {
         String simpleName = it.next();
diff --git a/java/com/google/turbine/binder/sym/ClassSymbol.java b/java/com/google/turbine/binder/sym/ClassSymbol.java
index 63dcfb1..2adf15f 100644
--- a/java/com/google/turbine/binder/sym/ClassSymbol.java
+++ b/java/com/google/turbine/binder/sym/ClassSymbol.java
@@ -47,7 +47,7 @@
 
   @Override
   public String toString() {
-    return className;
+    return className.replace('/', '.');
   }
 
   @Override
diff --git a/java/com/google/turbine/bytecode/ClassFile.java b/java/com/google/turbine/bytecode/ClassFile.java
index 502e295..b418d70 100644
--- a/java/com/google/turbine/bytecode/ClassFile.java
+++ b/java/com/google/turbine/bytecode/ClassFile.java
@@ -26,7 +26,7 @@
 import java.util.Deque;
 import java.util.List;
 import java.util.Map;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A JVMS §4.1 ClassFile. */
 public class ClassFile {
@@ -131,7 +131,7 @@
     private final String name;
     private final String descriptor;
     @Nullable private final String signature;
-    @Nullable private final Const.Value value;
+    private final Const.@Nullable Value value;
     private final List<AnnotationInfo> annotations;
     private final ImmutableList<TypeAnnotationInfo> typeAnnotations;
 
@@ -174,8 +174,7 @@
     }
 
     /** The compile-time constant value. */
-    @Nullable
-    public Const.Value value() {
+    public Const.@Nullable Value value() {
       return value;
     }
 
@@ -234,7 +233,7 @@
     private final String descriptor;
     @Nullable private final String signature;
     private final List<String> exceptions;
-    @Nullable private final AnnotationInfo.ElementValue defaultValue;
+    private final AnnotationInfo.@Nullable ElementValue defaultValue;
     private final List<AnnotationInfo> annotations;
     private final ImmutableList<ImmutableList<AnnotationInfo>> parameterAnnotations;
     private final ImmutableList<TypeAnnotationInfo> typeAnnotations;
@@ -290,8 +289,7 @@
     }
 
     /** The value of the AnnotationDefault attribute. */
-    @Nullable
-    public AnnotationInfo.ElementValue defaultValue() {
+    public AnnotationInfo.@Nullable ElementValue defaultValue() {
       return defaultValue;
     }
 
diff --git a/java/com/google/turbine/bytecode/ClassReader.java b/java/com/google/turbine/bytecode/ClassReader.java
index c8b4734..0032e63 100644
--- a/java/com/google/turbine/bytecode/ClassReader.java
+++ b/java/com/google/turbine/bytecode/ClassReader.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CheckReturnValue;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.ConstClassValue;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.EnumConstValue;
@@ -32,8 +33,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import javax.annotation.CheckReturnValue;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A JVMS §4 class file reader. */
 public class ClassReader {
diff --git a/java/com/google/turbine/bytecode/sig/Sig.java b/java/com/google/turbine/bytecode/sig/Sig.java
index 8a326ec..e85740f 100644
--- a/java/com/google/turbine/bytecode/sig/Sig.java
+++ b/java/com/google/turbine/bytecode/sig/Sig.java
@@ -18,7 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.turbine.model.TurbineConstantTypeKind;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** JVMS 4.7.9.1 signatures. */
 public class Sig {
diff --git a/java/com/google/turbine/diag/TurbineError.java b/java/com/google/turbine/diag/TurbineError.java
index b8d6b65..cd7d9e1 100644
--- a/java/com/google/turbine/diag/TurbineError.java
+++ b/java/com/google/turbine/diag/TurbineError.java
@@ -17,9 +17,13 @@
 package com.google.turbine.diag;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Iterables.getOnlyElement;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.turbine.binder.sym.ClassSymbol;
 
 /** A compilation error. */
 public class TurbineError extends Error {
@@ -33,10 +37,11 @@
     INVALID_LITERAL("invalid literal: %s"),
     UNEXPECTED_TYPE_PARAMETER("unexpected type parameter %s"),
     SYMBOL_NOT_FOUND("symbol not found %s"),
+    CLASS_FILE_NOT_FOUND("could not locate class file for %s"),
     TYPE_PARAMETER_QUALIFIER("type parameter used as type qualifier"),
     UNEXPECTED_TOKEN("unexpected token: %s"),
     INVALID_ANNOTATION_ARGUMENT("invalid annotation argument"),
-    CANNOT_RESOLVE("cannot resolve %s"),
+    CANNOT_RESOLVE("could not resolve %s"),
     EXPRESSION_ERROR("could not evaluate constant expression"),
     CYCLIC_HIERARCHY("cycle in class hierarchy: %s"),
     NOT_AN_ANNOTATION("%s is not an annotation"),
@@ -58,7 +63,20 @@
   /**
    * Formats a diagnostic.
    *
-   * @param source the source file
+   * @param source the current source file
+   * @param kind the error kind
+   * @param args format args
+   */
+  public static TurbineError format(SourceFile source, ErrorKind kind, Object... args) {
+    String path = firstNonNull(source.path(), "<>");
+    String message = kind.format(args);
+    String diagnostic = path + ": error: " + message.trim() + System.lineSeparator();
+    return new TurbineError(kind, diagnostic, ImmutableList.copyOf(args));
+  }
+
+  /**
+   * Formats a diagnostic.
+   *
    * @param position the diagnostic position
    * @param kind the error kind
    * @param args format args
@@ -78,18 +96,37 @@
         .append(System.lineSeparator());
     sb.append(Strings.repeat(" ", column)).append('^');
     String diagnostic = sb.toString();
-    return new TurbineError(kind, diagnostic);
+    return new TurbineError(kind, diagnostic, ImmutableList.copyOf(args));
   }
 
-  final ErrorKind kind;
+  private final ErrorKind kind;
+  private final ImmutableList<Object> args;
 
-  private TurbineError(ErrorKind kind, String diagnostic) {
+  private TurbineError(ErrorKind kind, String diagnostic, ImmutableList<Object> args) {
     super(diagnostic);
+    switch (kind) {
+      case SYMBOL_NOT_FOUND:
+        {
+          checkArgument(
+              args.size() == 1 && getOnlyElement(args) instanceof ClassSymbol,
+              "diagnostic (%s) has invalid argument args %s",
+              diagnostic,
+              args);
+          break;
+        }
+      default: // fall out
+    }
     this.kind = kind;
+    this.args = args;
   }
 
   /** The diagnostic kind. */
   public ErrorKind kind() {
     return kind;
   }
+
+  /** The diagnostic arguments. */
+  public ImmutableList<Object> args() {
+    return args;
+  }
 }
diff --git a/java/com/google/turbine/lower/Lower.java b/java/com/google/turbine/lower/Lower.java
index d8b464b..29fca00 100644
--- a/java/com/google/turbine/lower/Lower.java
+++ b/java/com/google/turbine/lower/Lower.java
@@ -27,12 +27,12 @@
 import com.google.turbine.binder.bound.AnnotationValue;
 import com.google.turbine.binder.bound.ClassValue;
 import com.google.turbine.binder.bound.EnumConstantValue;
-import com.google.turbine.binder.bound.ModuleInfo;
 import com.google.turbine.binder.bound.ModuleInfo.ExportInfo;
 import com.google.turbine.binder.bound.ModuleInfo.OpenInfo;
 import com.google.turbine.binder.bound.ModuleInfo.ProvideInfo;
 import com.google.turbine.binder.bound.ModuleInfo.RequireInfo;
 import com.google.turbine.binder.bound.ModuleInfo.UseInfo;
+import com.google.turbine.binder.bound.SourceModuleInfo;
 import com.google.turbine.binder.bound.SourceTypeBoundClass;
 import com.google.turbine.binder.bound.TypeBoundClass;
 import com.google.turbine.binder.bound.TypeBoundClass.FieldInfo;
@@ -60,6 +60,9 @@
 import com.google.turbine.bytecode.sig.Sig.MethodSig;
 import com.google.turbine.bytecode.sig.Sig.TySig;
 import com.google.turbine.bytecode.sig.SigWriter;
+import com.google.turbine.diag.SourceFile;
+import com.google.turbine.diag.TurbineError;
+import com.google.turbine.diag.TurbineError.ErrorKind;
 import com.google.turbine.model.Const;
 import com.google.turbine.model.TurbineFlag;
 import com.google.turbine.model.TurbineVisibility;
@@ -80,7 +83,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** Lowering from bound classes to bytecode. */
 public class Lower {
@@ -109,7 +112,7 @@
   /** Lowers all given classes to bytecode. */
   public static Lowered lowerAll(
       ImmutableMap<ClassSymbol, SourceTypeBoundClass> units,
-      ImmutableList<ModuleInfo> modules,
+      ImmutableList<SourceModuleInfo> modules,
       Env<ClassSymbol, BytecodeBoundClass> classpath) {
     CompoundEnv<ClassSymbol, TypeBoundClass> env =
         CompoundEnv.<ClassSymbol, TypeBoundClass>of(classpath).append(new SimpleEnv<>(units));
@@ -124,7 +127,7 @@
     } else {
       // multi-module mode: the output module-info.class are in a directory corresponding to their
       // package
-      for (ModuleInfo module : modules) {
+      for (SourceModuleInfo module : modules) {
         result.put(module.name().replace('.', '/') + "/module-info", lower(module, env, symbols));
       }
     }
@@ -141,7 +144,9 @@
   }
 
   private static byte[] lower(
-      ModuleInfo module, CompoundEnv<ClassSymbol, TypeBoundClass> env, Set<ClassSymbol> symbols) {
+      SourceModuleInfo module,
+      CompoundEnv<ClassSymbol, TypeBoundClass> env,
+      Set<ClassSymbol> symbols) {
     return new Lower(env).lower(module, symbols);
   }
 
@@ -152,7 +157,7 @@
     this.env = env;
   }
 
-  private byte[] lower(ModuleInfo module, Set<ClassSymbol> symbols) {
+  private byte[] lower(SourceModuleInfo module, Set<ClassSymbol> symbols) {
     String name = "module-info";
     ImmutableList<AnnotationInfo> annotations = lowerAnnotations(module.annos());
     ClassFile.ModuleInfo moduleInfo = lowerModule(module);
@@ -161,7 +166,7 @@
     {
       Set<ClassSymbol> all = new LinkedHashSet<>();
       for (ClassSymbol sym : sig.classes) {
-        addEnclosing(env, all, sym);
+        addEnclosing(module.source(), env, all, sym);
       }
       for (ClassSymbol innerSym : all) {
         innerClasses.add(innerClass(env, innerSym));
@@ -185,7 +190,7 @@
     return ClassWriter.writeClass(classfile);
   }
 
-  private ClassFile.ModuleInfo lowerModule(ModuleInfo module) {
+  private ClassFile.ModuleInfo lowerModule(SourceModuleInfo module) {
     ImmutableList.Builder<ClassFile.ModuleInfo.RequireInfo> requires = ImmutableList.builder();
     for (RequireInfo require : module.requires()) {
       requires.add(
@@ -258,7 +263,7 @@
 
     ImmutableList<AnnotationInfo> annotations = lowerAnnotations(info.annotations());
 
-    ImmutableList<ClassFile.InnerClass> inners = collectInnerClasses(sym, info);
+    ImmutableList<ClassFile.InnerClass> inners = collectInnerClasses(info.source(), sym, info);
 
     ImmutableList<TypeAnnotationInfo> typeAnnotations = classTypeAnnotations(info);
 
@@ -382,14 +387,14 @@
 
   /** Creates inner class attributes for all referenced inner classes. */
   private ImmutableList<ClassFile.InnerClass> collectInnerClasses(
-      ClassSymbol origin, SourceTypeBoundClass info) {
+      SourceFile source, ClassSymbol origin, SourceTypeBoundClass info) {
     Set<ClassSymbol> all = new LinkedHashSet<>();
-    addEnclosing(env, all, origin);
+    addEnclosing(source, env, all, origin);
     for (ClassSymbol sym : info.children().values()) {
-      addEnclosing(env, all, sym);
+      addEnclosing(source, env, all, sym);
     }
     for (ClassSymbol sym : sig.classes) {
-      addEnclosing(env, all, sym);
+      addEnclosing(source, env, all, sym);
     }
     ImmutableList.Builder<ClassFile.InnerClass> inners = ImmutableList.builder();
     for (ClassSymbol innerSym : all) {
@@ -406,14 +411,17 @@
    * classes' entries.
    */
   private void addEnclosing(
-      Env<ClassSymbol, TypeBoundClass> env, Set<ClassSymbol> all, ClassSymbol sym) {
+      SourceFile source,
+      Env<ClassSymbol, TypeBoundClass> env,
+      Set<ClassSymbol> all,
+      ClassSymbol sym) {
     TypeBoundClass info = env.get(sym);
     if (info == null) {
-      throw new AssertionError(sym);
+      throw TurbineError.format(source, ErrorKind.CLASS_FILE_NOT_FOUND, sym);
     }
     ClassSymbol owner = env.get(sym).owner();
     if (owner != null) {
-      addEnclosing(env, all, owner);
+      addEnclosing(source, env, all, owner);
       all.add(sym);
     }
   }
diff --git a/java/com/google/turbine/model/TurbineVisibility.java b/java/com/google/turbine/model/TurbineVisibility.java
index 4f250c7..a30af4c 100644
--- a/java/com/google/turbine/model/TurbineVisibility.java
+++ b/java/com/google/turbine/model/TurbineVisibility.java
@@ -42,19 +42,27 @@
   public static final int VISIBILITY_MASK =
       TurbineFlag.ACC_PUBLIC | TurbineFlag.ACC_PRIVATE | TurbineFlag.ACC_PROTECTED;
 
+  /**
+   * Returns the {@link TurbineVisibility} corresponding to the given access bits.
+   *
+   * <p>If the input is ill-formed and corresponds to multiple visibilities, {@code PUBLIC}, {@code
+   * PROTECTED}, {@code PRIVATE}, and {@code PACKAGE}, are returned in that order. This means that
+   * turbine will occasionally produce valid output for invalid input. In general turbine performs
+   * the minimum possible error-checking, and the expectation is that it is run in parallel with
+   * javac or another non-header compiler as part of a build, and it defers well-formedness checking
+   * to the other tool.
+   */
   public static TurbineVisibility fromAccess(int access) {
-    switch (access & VISIBILITY_MASK) {
-      case TurbineFlag.ACC_PUBLIC:
-        return PUBLIC;
-      case TurbineFlag.ACC_PRIVATE:
-        return PRIVATE;
-      case TurbineFlag.ACC_PROTECTED:
-        return PROTECTED;
-      case 0:
-        return PACKAGE;
-      default:
-        throw new AssertionError(String.format("0x%x", access));
+    if ((access & TurbineFlag.ACC_PUBLIC) == TurbineFlag.ACC_PUBLIC) {
+      return PUBLIC;
     }
+    if ((access & TurbineFlag.ACC_PROTECTED) == TurbineFlag.ACC_PROTECTED) {
+      return PROTECTED;
+    }
+    if ((access & TurbineFlag.ACC_PRIVATE) == TurbineFlag.ACC_PRIVATE) {
+      return PRIVATE;
+    }
+    return PACKAGE;
   }
 
   public int setAccess(int access) {
diff --git a/java/com/google/turbine/options/TurbineOptions.java b/java/com/google/turbine/options/TurbineOptions.java
index 20d81fe..7dd118e 100644
--- a/java/com/google/turbine/options/TurbineOptions.java
+++ b/java/com/google/turbine/options/TurbineOptions.java
@@ -21,7 +21,7 @@
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** Header compilation options. */
 public class TurbineOptions {
@@ -271,12 +271,6 @@
       return this;
     }
 
-    // TODO(b/72379900): Remove this
-    public Builder addDirectJarToTarget(String jar) {
-      directJars.add(jar);
-      return this;
-    }
-
     public Builder setTargetLabel(String targetLabel) {
       this.targetLabel = targetLabel;
       return this;
diff --git a/java/com/google/turbine/options/TurbineOptionsParser.java b/java/com/google/turbine/options/TurbineOptionsParser.java
index 419a04e..40c9ecc 100644
--- a/java/com/google/turbine/options/TurbineOptionsParser.java
+++ b/java/com/google/turbine/options/TurbineOptionsParser.java
@@ -28,7 +28,7 @@
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.Iterator;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A command line options parser for {@link TurbineOptions}. */
 public class TurbineOptionsParser {
@@ -73,7 +73,6 @@
         case "--processorpath":
           builder.addProcessorPathEntries(readList(argumentDeque));
           break;
-          // TODO(b/72379900): Remove this
         case "--classpath":
           builder.addClassPathEntries(readList(argumentDeque));
           break;
@@ -102,27 +101,6 @@
         case "--direct_dependencies":
           builder.addDirectJars(readList(argumentDeque));
           break;
-        case "--direct_dependency":
-          {
-            // TODO(b/72379900): Remove this
-            String jar = readOne(argumentDeque);
-            readOne(argumentDeque);
-            builder.addDirectJarToTarget(jar);
-            if (!argumentDeque.isEmpty() && !argumentDeque.peekFirst().startsWith("--")) {
-              argumentDeque.removeFirst(); // the aspect that created the dependency
-            }
-            break;
-          }
-        case "--indirect_dependency":
-          {
-            // TODO(b/72379900): Remove this
-            readOne(argumentDeque);
-            readOne(argumentDeque);
-            if (!argumentDeque.isEmpty() && !argumentDeque.peekFirst().startsWith("--")) {
-              argumentDeque.removeFirst(); // the aspect that created the dependency
-            }
-            break;
-          }
         case "--deps_artifacts":
           builder.addAllDepsArtifacts(readList(argumentDeque));
           break;
diff --git a/java/com/google/turbine/parse/ConstExpressionParser.java b/java/com/google/turbine/parse/ConstExpressionParser.java
index e6a7f97..51f3128 100644
--- a/java/com/google/turbine/parse/ConstExpressionParser.java
+++ b/java/com/google/turbine/parse/ConstExpressionParser.java
@@ -20,6 +20,7 @@
 
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CheckReturnValue;
 import com.google.turbine.diag.TurbineError;
 import com.google.turbine.diag.TurbineError.ErrorKind;
 import com.google.turbine.model.Const;
@@ -29,8 +30,7 @@
 import com.google.turbine.tree.Tree.ClassTy;
 import com.google.turbine.tree.Tree.Expression;
 import com.google.turbine.tree.TurbineOperatorKind;
-import javax.annotation.CheckReturnValue;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A parser for compile-time constant expressions. */
 public class ConstExpressionParser {
@@ -220,7 +220,8 @@
         case NOT:
         case TILDE:
         case IDENT:
-          return new Tree.TypeCast(position, asClassTy(cvar.name()), primary(false));
+          return new Tree.TypeCast(
+              position, asClassTy(cvar.position(), cvar.name()), primary(false));
         default:
           return expr;
       }
@@ -229,12 +230,11 @@
     }
   }
 
-  private ClassTy asClassTy(ImmutableList<String> names) {
+  private static ClassTy asClassTy(int pos, ImmutableList<String> names) {
     ClassTy cty = null;
     for (String bit : names) {
       cty =
-          new ClassTy(
-              position, Optional.fromNullable(cty), bit, ImmutableList.of(), ImmutableList.of());
+          new ClassTy(pos, Optional.fromNullable(cty), bit, ImmutableList.of(), ImmutableList.of());
     }
     return cty;
   }
@@ -419,14 +419,13 @@
     return new Tree.Unary(position, expr, op);
   }
 
-  @Nullable
-  private Tree.Expression qualIdent() {
+  private Tree.@Nullable Expression qualIdent() {
     int pos = position;
     ImmutableList.Builder<String> bits = ImmutableList.builder();
     bits.add(lexer.stringValue());
     eat();
     if (token == Token.LBRACK) {
-      return finishClassLiteral(pos, asClassTy(bits.build()));
+      return finishClassLiteral(pos, asClassTy(pos, bits.build()));
     }
     while (token == Token.DOT) {
       eat();
@@ -437,7 +436,7 @@
         case CLASS:
           // TODO(cushon): only allow in annotations?
           eat();
-          return new Tree.ClassLiteral(pos, asClassTy(bits.build()));
+          return new Tree.ClassLiteral(pos, asClassTy(pos, bits.build()));
         default:
           return null;
       }
diff --git a/java/com/google/turbine/parse/Parser.java b/java/com/google/turbine/parse/Parser.java
index 7b3a92e..752b2a5 100644
--- a/java/com/google/turbine/parse/Parser.java
+++ b/java/com/google/turbine/parse/Parser.java
@@ -29,6 +29,7 @@
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CheckReturnValue;
 import com.google.turbine.diag.SourceFile;
 import com.google.turbine.diag.TurbineError;
 import com.google.turbine.diag.TurbineError.ErrorKind;
@@ -62,8 +63,7 @@
 import java.util.Deque;
 import java.util.EnumSet;
 import java.util.List;
-import javax.annotation.CheckReturnValue;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * A parser for the subset of Java required for header compilation.
@@ -981,6 +981,8 @@
         case LPAREN:
           depth++;
           break;
+        case EOF:
+          throw error(ErrorKind.UNEXPECTED_EOF);
         default:
           break;
       }
@@ -999,6 +1001,8 @@
         case LBRACE:
           depth++;
           break;
+        case EOF:
+          throw error(ErrorKind.UNEXPECTED_EOF);
         default:
           break;
       }
diff --git a/java/com/google/turbine/parse/VariableInitializerParser.java b/java/com/google/turbine/parse/VariableInitializerParser.java
index 6226e3a..a39e9e8 100644
--- a/java/com/google/turbine/parse/VariableInitializerParser.java
+++ b/java/com/google/turbine/parse/VariableInitializerParser.java
@@ -17,6 +17,9 @@
 package com.google.turbine.parse;
 
 import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CheckReturnValue;
+import com.google.turbine.diag.TurbineError;
+import com.google.turbine.diag.TurbineError.ErrorKind;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.List;
@@ -232,6 +235,8 @@
           next();
           depth--;
           break;
+        case EOF:
+          throw error(ErrorKind.UNEXPECTED_EOF);
         default:
           save();
           next();
@@ -254,6 +259,8 @@
           next();
           depth--;
           break;
+        case EOF:
+          throw error(ErrorKind.UNEXPECTED_EOF);
         default:
           save();
           next();
@@ -330,4 +337,13 @@
       }
     }
   }
+
+  @CheckReturnValue
+  private TurbineError error(ErrorKind kind, Object... args) {
+    return TurbineError.format(
+        lexer.source(),
+        Math.min(lexer.position(), lexer.source().source().length() - 1),
+        kind,
+        args);
+  }
 }
diff --git a/java/com/google/turbine/type/Type.java b/java/com/google/turbine/type/Type.java
index cdd8605..61a5bbe 100644
--- a/java/com/google/turbine/type/Type.java
+++ b/java/com/google/turbine/type/Type.java
@@ -106,9 +106,9 @@
       for (SimpleClassTy c : classes) {
         if (!first) {
           sb.append('.');
-          sb.append(c.sym.toString().substring(c.sym.toString().lastIndexOf('$') + 1));
+          sb.append(c.sym.binaryName().substring(c.sym.binaryName().lastIndexOf('$') + 1));
         } else {
-          sb.append(c.sym);
+          sb.append(c.sym.binaryName());
         }
         if (!c.targs.isEmpty()) {
           sb.append('<');
diff --git a/java/com/google/turbine/types/Canonicalize.java b/java/com/google/turbine/types/Canonicalize.java
index fc5d907..b7a9048 100644
--- a/java/com/google/turbine/types/Canonicalize.java
+++ b/java/com/google/turbine/types/Canonicalize.java
@@ -22,6 +22,9 @@
 import com.google.turbine.binder.env.Env;
 import com.google.turbine.binder.sym.ClassSymbol;
 import com.google.turbine.binder.sym.TyVarSymbol;
+import com.google.turbine.diag.SourceFile;
+import com.google.turbine.diag.TurbineError;
+import com.google.turbine.diag.TurbineError.ErrorKind;
 import com.google.turbine.model.TurbineFlag;
 import com.google.turbine.type.Type;
 import com.google.turbine.type.Type.ArrayTy;
@@ -40,7 +43,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import javax.annotation.Nullable;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * Canonicalizes qualified type names so qualifiers are always the declaring class of the qualified
@@ -63,35 +66,38 @@
 
   /** Canonicalizes the given type. */
   public static Type canonicalize(
-      Env<ClassSymbol, TypeBoundClass> env, ClassSymbol base, Type type) {
+      SourceFile source, Env<ClassSymbol, TypeBoundClass> env, ClassSymbol base, Type type) {
     switch (type.tyKind()) {
       case PRIM_TY:
       case VOID_TY:
       case TY_VAR:
         return type;
       case WILD_TY:
-        return canonicalizeWildTy(env, base, (WildTy) type);
+        return canonicalizeWildTy(source, env, base, (WildTy) type);
       case ARRAY_TY:
         {
           Type.ArrayTy arrayTy = (Type.ArrayTy) type;
-          return new Type.ArrayTy(canonicalize(env, base, arrayTy.elementType()), arrayTy.annos());
+          return new Type.ArrayTy(
+              canonicalize(source, env, base, arrayTy.elementType()), arrayTy.annos());
         }
       case CLASS_TY:
-        return canonicalizeClassTy(env, base, (ClassTy) type);
+        return canonicalizeClassTy(source, env, base, (ClassTy) type);
       default:
         throw new AssertionError(type.tyKind());
     }
   }
 
   /** Canonicalize a qualified class type, excluding type arguments. */
-  private static ClassTy canon(Env<ClassSymbol, TypeBoundClass> env, ClassSymbol base, ClassTy ty) {
+  private static ClassTy canon(
+      SourceFile source, Env<ClassSymbol, TypeBoundClass> env, ClassSymbol base, ClassTy ty) {
     if (isRaw(env, ty)) {
       return Erasure.eraseClassTy(ty);
     }
     // if the first name is a simple name resolved inside a nested class, add explicit qualifiers
     // for the enclosing declarations
     Iterator<ClassTy.SimpleClassTy> it = ty.classes.iterator();
-    Collection<ClassTy.SimpleClassTy> lexicalBase = lexicalBase(env, ty.classes.get(0).sym(), base);
+    Collection<ClassTy.SimpleClassTy> lexicalBase =
+        lexicalBase(source, env, ty.classes.get(0).sym(), base);
     ClassTy canon =
         !lexicalBase.isEmpty()
             ? new ClassTy(lexicalBase)
@@ -99,7 +105,7 @@
 
     // canonicalize each additional simple name that appeared in source
     while (it.hasNext()) {
-      canon = canonOne(env, canon, it.next());
+      canon = canonOne(source, env, canon, it.next());
     }
     return canon;
   }
@@ -122,7 +128,10 @@
 
   /** Given a base symbol to canonicalize, find any implicit enclosing instances. */
   private static Collection<ClassTy.SimpleClassTy> lexicalBase(
-      Env<ClassSymbol, TypeBoundClass> env, ClassSymbol first, ClassSymbol owner) {
+      SourceFile source,
+      Env<ClassSymbol, TypeBoundClass> env,
+      ClassSymbol first,
+      ClassSymbol owner) {
     if ((env.get(first).access() & TurbineFlag.ACC_STATIC) == TurbineFlag.ACC_STATIC) {
       return Collections.emptyList();
     }
@@ -137,7 +146,11 @@
       if ((env.get(owner).access() & TurbineFlag.ACC_STATIC) == TurbineFlag.ACC_STATIC) {
         break;
       }
-      canonOwner = env.get(canonOwner).owner();
+      TypeBoundClass info = env.get(canonOwner);
+      if (info == null) {
+        throw TurbineError.format(source, ErrorKind.CLASS_FILE_NOT_FOUND, canonOwner);
+      }
+      canonOwner = info.owner();
     }
     return result;
   }
@@ -167,7 +180,7 @@
    * result.
    */
   private static ClassTy canonOne(
-      Env<ClassSymbol, TypeBoundClass> env, ClassTy base, ClassTy.SimpleClassTy ty) {
+      SourceFile source, Env<ClassSymbol, TypeBoundClass> env, ClassTy base, SimpleClassTy ty) {
     // if the class is static, it has a trivial canonical qualifier with no type arguments
     if ((env.get(ty.sym()).access() & TurbineFlag.ACC_STATIC) == TurbineFlag.ACC_STATIC) {
       return new ClassTy(Collections.singletonList(ty));
@@ -196,7 +209,7 @@
         break;
       }
       TypeBoundClass info = env.get(curr.sym());
-      curr = canon(env, info.owner(), info.superClassType());
+      curr = canon(source, env, info.owner(), info.superClassType());
     }
     simples.add(ty);
     return new ClassTy(simples.build());
@@ -315,36 +328,41 @@
   }
 
   public static ClassTy canonicalizeClassTy(
-      Env<ClassSymbol, TypeBoundClass> env, ClassSymbol base, ClassTy ty) {
+      SourceFile source, Env<ClassSymbol, TypeBoundClass> env, ClassSymbol base, ClassTy ty) {
     // canonicalize type arguments first
     ImmutableList.Builder<ClassTy.SimpleClassTy> args = ImmutableList.builder();
     for (ClassTy.SimpleClassTy s : ty.classes) {
-      args.add(new ClassTy.SimpleClassTy(s.sym(), canonicalize(s.targs(), base, env), s.annos()));
+      args.add(
+          new ClassTy.SimpleClassTy(
+              s.sym(), canonicalize(source, s.targs(), base, env), s.annos()));
     }
     ty = new ClassTy(args.build());
-    return canon(env, base, ty);
+    return canon(source, env, base, ty);
   }
 
   private static ImmutableList<Type> canonicalize(
-      ImmutableList<Type> targs, ClassSymbol base, Env<ClassSymbol, TypeBoundClass> env) {
+      SourceFile source,
+      ImmutableList<Type> targs,
+      ClassSymbol base,
+      Env<ClassSymbol, TypeBoundClass> env) {
     ImmutableList.Builder<Type> result = ImmutableList.builder();
     for (Type a : targs) {
-      result.add(canonicalize(env, base, a));
+      result.add(canonicalize(source, env, base, a));
     }
     return result.build();
   }
 
   private static Type canonicalizeWildTy(
-      Env<ClassSymbol, TypeBoundClass> env, ClassSymbol base, WildTy type) {
+      SourceFile source, Env<ClassSymbol, TypeBoundClass> env, ClassSymbol base, WildTy type) {
     switch (type.boundKind()) {
       case NONE:
         return type;
       case LOWER:
         return new Type.WildLowerBoundedTy(
-            canonicalize(env, base, type.bound()), type.annotations());
+            canonicalize(source, env, base, type.bound()), type.annotations());
       case UPPER:
         return new Type.WildUpperBoundedTy(
-            canonicalize(env, base, type.bound()), type.annotations());
+            canonicalize(source, env, base, type.bound()), type.annotations());
       default:
         throw new AssertionError(type.boundKind());
     }
diff --git a/javatests/com/google/turbine/binder/BinderErrorTest.java b/javatests/com/google/turbine/binder/BinderErrorTest.java
index a4c2e25..0f37537 100644
--- a/javatests/com/google/turbine/binder/BinderErrorTest.java
+++ b/javatests/com/google/turbine/binder/BinderErrorTest.java
@@ -46,7 +46,7 @@
           "}",
         },
         {
-          "<>:2: error: symbol not found NoSuch",
+          "<>:2: error: could not resolve NoSuch",
           "public class A extends NoSuch {",
           "                       ^",
         }
@@ -61,7 +61,7 @@
         },
         // TODO(cushon): we'd prefer the caret at NoSuch instead of A
         {
-          "<>:4: error: symbol not found NoSuch", //
+          "<>:4: error: symbol not found a.A$NoSuch", //
           "class B extends A.NoSuch {",
           "                ^",
         }
@@ -73,7 +73,7 @@
           "class B extends A<NoSuch> {}",
         },
         {
-          "<>:3: error: symbol not found NoSuch",
+          "<>:3: error: could not resolve NoSuch",
           "class B extends A<NoSuch> {}",
           "                  ^",
         }
@@ -84,7 +84,7 @@
           "@Anno(foo=100, bar=200) class Test {}",
         },
         {
-          "<>:2: error: cannot resolve foo", //
+          "<>:2: error: could not resolve foo", //
           "@Anno(foo=100, bar=200) class Test {}",
           "      ^",
         },
@@ -95,7 +95,7 @@
           "@Anno(foo=100, bar=200) class Test {}",
         },
         {
-          "<>:2: error: cannot resolve bar", //
+          "<>:2: error: could not resolve bar", //
           "@Anno(foo=100, bar=200) class Test {}",
           "               ^",
         },
@@ -146,8 +146,8 @@
           "}",
         },
         {
-          "<>:4: error: cycle in class hierarchy: p/OuterExtendsInner$Inner"
-              + " -> p/OuterExtendsInner$Inner",
+          "<>:4: error: cycle in class hierarchy: p.OuterExtendsInner$Inner"
+              + " -> p.OuterExtendsInner$Inner",
           "  public static class Inner extends Foo {}",
           "                                    ^"
         },
@@ -173,7 +173,7 @@
           "}",
         },
         {
-          "<>:2: error: symbol not found NoSuch", //
+          "<>:2: error: symbol not found java.util.List$NoSuch", //
           "import java.util.List.NoSuch;",
           "       ^"
         },
@@ -186,9 +186,9 @@
           "}",
         },
         {
-          "<>:2: error: symbol not found NoSuch", //
-          "import static java.util.List.NoSuch;",
-          "              ^"
+          "<>:3: error: could not resolve NoSuch", //
+          "public class Test extends NoSuch {",
+          "                          ^"
         },
       },
       {
@@ -199,7 +199,7 @@
           "}",
         },
         {
-          "<>:3: error: symbol not found NoSuchOther",
+          "<>:3: error: could not resolve NoSuchOther",
           "public class Test extends NoSuchOther {",
           "                          ^",
         },
@@ -212,7 +212,7 @@
           "}",
         },
         {
-          "<>:3: error: symbol not found NoSuchOther",
+          "<>:3: error: could not resolve NoSuchOther",
           "public class Test extends NoSuchOther {",
           "                          ^",
         },
@@ -225,7 +225,7 @@
           "}",
         },
         {
-          "<>:3: error: symbol not found NoSuchOther",
+          "<>:3: error: could not resolve NoSuchOther",
           "public class Test extends NoSuchOther {",
           "                          ^",
         },
@@ -238,7 +238,7 @@
           "}",
         },
         {
-          "<>:3: error: symbol not found NoSuchOther",
+          "<>:3: error: could not resolve NoSuchOther",
           "public class Test extends NoSuchOther {",
           "                          ^",
         },
@@ -250,7 +250,7 @@
           "}",
         },
         {
-          "<>:2: error: java/lang/Object is not an annotation", //
+          "<>:2: error: java.lang.Object is not an annotation", //
           "  @Object int x;",
           "   ^",
         },
@@ -262,7 +262,7 @@
           "}",
         },
         {
-          "<>:2: error: java/lang/Deprecated is not @Repeatable", //
+          "<>:2: error: java.lang.Deprecated is not @Repeatable", //
           "  @Deprecated @Deprecated int x;",
           "   ^",
         },
@@ -274,7 +274,7 @@
           "}",
         },
         {
-          "<>:2: error: symbol not found NoSuch.NoSuch", //
+          "<>:2: error: could not resolve NoSuch.NoSuch", //
           "  @NoSuch.NoSuch int x;",
           "   ^",
         },
@@ -286,7 +286,7 @@
           "}",
         },
         {
-          "<>:2: error: symbol not found NoSuch", //
+          "<>:2: error: symbol not found java.lang.Deprecated$NoSuch", //
           "  @Deprecated.NoSuch int x;",
           "   ^",
         },
@@ -301,7 +301,7 @@
           "}",
         },
         {
-          "<>:5: error: could not evaluate constant expression", //
+          "<>:5: error: could not resolve field NO_SUCH", //
           "  @Anno(value=Test.NO_SUCH) int x;",
           "              ^",
         },
@@ -360,6 +360,49 @@
           "               ^",
         },
       },
+      {
+        {
+          "import java.util.List;", //
+          "@interface Anno { Class<?> value() default Object.class; }",
+          "@Anno(List.NoSuch.class)",
+          "public class Test {}",
+        },
+        {
+          "<>:3: error: symbol not found java.util.List$NoSuch", //
+          "@Anno(List.NoSuch.class)",
+          "      ^",
+        },
+      },
+      {
+        {
+          "public class Test {", //
+          "  @interface Anno {",
+          "    Class<?>[] value() default Object.class;",
+          "  }",
+          "  @Anno(value={java.util.Map.Entry}) int x;",
+          "}",
+        },
+        {
+          "<>:5: error: could not resolve field Entry", //
+          "  @Anno(value={java.util.Map.Entry}) int x;",
+          "               ^",
+        },
+      },
+      {
+        {
+          "public class Test {", //
+          "  @interface Anno {",
+          "    Class<?>[] value() default Object.class;",
+          "  }",
+          "  @Anno(value={java.lang.Object}) int x;",
+          "}",
+        },
+        {
+          "<>:5: error: could not resolve field Object", //
+          "  @Anno(value={java.lang.Object}) int x;",
+          "               ^",
+        },
+      },
     };
     return Arrays.asList((Object[][]) testCases);
   }
diff --git a/javatests/com/google/turbine/binder/BinderTest.java b/javatests/com/google/turbine/binder/BinderTest.java
index 9ba5705..700baf0 100644
--- a/javatests/com/google/turbine/binder/BinderTest.java
+++ b/javatests/com/google/turbine/binder/BinderTest.java
@@ -195,7 +195,7 @@
           /* moduleVersion=*/ Optional.absent());
       fail();
     } catch (TurbineError e) {
-      assertThat(e.getMessage()).contains("cycle in class hierarchy: a/A -> b/B -> a/A");
+      assertThat(e.getMessage()).contains("cycle in class hierarchy: a.A -> b.B -> a.A");
     }
   }
 
diff --git a/javatests/com/google/turbine/lower/LowerIntegrationTest.java b/javatests/com/google/turbine/lower/LowerIntegrationTest.java
index 698627c..6496756 100644
--- a/javatests/com/google/turbine/lower/LowerIntegrationTest.java
+++ b/javatests/com/google/turbine/lower/LowerIntegrationTest.java
@@ -302,6 +302,7 @@
       "B70953542.test",
       // TODO(cushon): support for source level 9 in integration tests
       // "B74332665.test",
+      "memberimport.test",
     };
     List<Object[]> tests =
         ImmutableList.copyOf(testCases).stream().map(x -> new Object[] {x}).collect(toList());
diff --git a/javatests/com/google/turbine/lower/LowerTest.java b/javatests/com/google/turbine/lower/LowerTest.java
index 6409d4d..d97f46a 100644
--- a/javatests/com/google/turbine/lower/LowerTest.java
+++ b/javatests/com/google/turbine/lower/LowerTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.turbine.testing.TestClassPaths.TURBINE_BOOTCLASSPATH;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Optional;
@@ -36,6 +37,7 @@
 import com.google.turbine.binder.sym.TyVarSymbol;
 import com.google.turbine.bytecode.ByteReader;
 import com.google.turbine.bytecode.ConstantPoolReader;
+import com.google.turbine.diag.TurbineError;
 import com.google.turbine.model.TurbineConstantTypeKind;
 import com.google.turbine.model.TurbineFlag;
 import com.google.turbine.model.TurbineTyKind;
@@ -480,6 +482,151 @@
         .isEqualTo(IntegrationTestSupport.dump(IntegrationTestSupport.canonicalize(expected)));
   }
 
+  @Test
+  public void missingOuter() throws Exception {
+
+    Map<String, byte[]> lib =
+        IntegrationTestSupport.runJavac(
+            ImmutableMap.of(
+                "A.java",
+                    lines(
+                        "interface A {", //
+                        "  interface M {",
+                        "    interface I {}",
+                        "  } ",
+                        "}"),
+                "B.java",
+                    lines(
+                        "interface B extends A {",
+                        "  interface BM extends M {",
+                        "    interface BI extends I {}",
+                        "  }",
+                        "}")),
+            ImmutableList.of());
+
+    Path libJar = temporaryFolder.newFile("lib.jar").toPath();
+    try (OutputStream os = Files.newOutputStream(libJar);
+        JarOutputStream jos = new JarOutputStream(os)) {
+      jos.putNextEntry(new JarEntry("A$M.class"));
+      jos.write(lib.get("A$M"));
+      jos.putNextEntry(new JarEntry("A$M$I.class"));
+      jos.write(lib.get("A$M$I"));
+      jos.putNextEntry(new JarEntry("B.class"));
+      jos.write(lib.get("B"));
+      jos.putNextEntry(new JarEntry("B$BM.class"));
+      jos.write(lib.get("B$BM"));
+      jos.putNextEntry(new JarEntry("B$BM$BI.class"));
+      jos.write(lib.get("B$BM$BI"));
+    }
+
+    ImmutableMap<String, String> sources =
+        ImmutableMap.<String, String>builder()
+            .put(
+                "Test.java",
+                lines(
+                    "public class Test extends B.BM {", //
+                    "  I i;",
+                    "}"))
+            .build();
+
+    try {
+      IntegrationTestSupport.runTurbine(sources, ImmutableList.of(libJar));
+      fail();
+    } catch (TurbineError error) {
+      assertThat(error)
+          .hasMessageThat()
+          .contains("Test.java: error: could not locate class file for A");
+    }
+  }
+
+  @Test
+  public void missingOuter2() throws Exception {
+
+    Map<String, byte[]> lib =
+        IntegrationTestSupport.runJavac(
+            ImmutableMap.of(
+                "A.java",
+                lines(
+                    "class A {", //
+                    "  class M { ",
+                    "    class I {} ",
+                    "  } ",
+                    "}"),
+                "B.java",
+                lines(
+                    "class B extends A { ",
+                    "  class BM extends M { ",
+                    "    class BI extends I {} ",
+                    "  } ",
+                    "}")),
+            ImmutableList.of());
+
+    Path libJar = temporaryFolder.newFile("lib.jar").toPath();
+    try (OutputStream os = Files.newOutputStream(libJar);
+        JarOutputStream jos = new JarOutputStream(os)) {
+      jos.putNextEntry(new JarEntry("A$M.class"));
+      jos.write(lib.get("A$M"));
+      jos.putNextEntry(new JarEntry("A$M$I.class"));
+      jos.write(lib.get("A$M$I"));
+      jos.putNextEntry(new JarEntry("B.class"));
+      jos.write(lib.get("B"));
+      jos.putNextEntry(new JarEntry("B$BM.class"));
+      jos.write(lib.get("B$BM"));
+      jos.putNextEntry(new JarEntry("B$BM$BI.class"));
+      jos.write(lib.get("B$BM$BI"));
+    }
+
+    ImmutableMap<String, String> sources =
+        ImmutableMap.<String, String>builder()
+            .put(
+                "Test.java",
+                lines(
+                    "public class Test extends B {", //
+                    "  class M extends BM {",
+                    "     I i;",
+                    "  }",
+                    "}"))
+            .build();
+
+    try {
+      IntegrationTestSupport.runTurbine(sources, ImmutableList.of(libJar));
+      fail();
+    } catch (TurbineError error) {
+      assertThat(error)
+          .hasMessageThat()
+          .contains("Test.java: error: could not locate class file for A");
+    }
+  }
+
+  // If an element incorrectly has multiple visibility modifiers, pick one, and rely on javac to
+  // report a diagnostic.
+  @Test
+  public void multipleVisibilities() throws Exception {
+    ImmutableMap<String, String> sources =
+        ImmutableMap.of("Test.java", "public protected class Test {}");
+
+    Map<String, byte[]> lowered =
+        IntegrationTestSupport.runTurbine(sources, /* classpath= */ ImmutableList.of());
+    int[] testAccess = {0};
+    new ClassReader(lowered.get("Test"))
+        .accept(
+            new ClassVisitor(Opcodes.ASM6) {
+              @Override
+              public void visit(
+                  int version,
+                  int access,
+                  String name,
+                  String signature,
+                  String superName,
+                  String[] interfaces) {
+                testAccess[0] = access;
+              }
+            },
+            0);
+    assertThat((testAccess[0] & TurbineFlag.ACC_PUBLIC) == TurbineFlag.ACC_PUBLIC).isTrue();
+    assertThat((testAccess[0] & TurbineFlag.ACC_PROTECTED) == TurbineFlag.ACC_PROTECTED).isFalse();
+  }
+
   static String lines(String... lines) {
     return Joiner.on("\n").join(lines);
   }
diff --git a/javatests/com/google/turbine/lower/testdata/memberimport.test b/javatests/com/google/turbine/lower/testdata/memberimport.test
new file mode 100644
index 0000000..c32220a
--- /dev/null
+++ b/javatests/com/google/turbine/lower/testdata/memberimport.test
@@ -0,0 +1,18 @@
+=== A.java ===
+package a;
+public class A {
+  //                   this is not a field
+  //                    |  this is a field
+  //                    |     |
+  //                    v     v
+  public static final Object Object = new Object() {};
+}
+=== B.java ===
+package b;
+import static a.A.Object; // <-- this is a field
+class B {
+  public static final Object x = null;
+  //                    ^
+  //                    |
+  //                   this is not a field
+}
diff --git a/javatests/com/google/turbine/options/TurbineOptionsTest.java b/javatests/com/google/turbine/options/TurbineOptionsTest.java
index 8342ccc..36f9e20 100644
--- a/javatests/com/google/turbine/options/TurbineOptionsTest.java
+++ b/javatests/com/google/turbine/options/TurbineOptionsTest.java
@@ -120,37 +120,6 @@
     assertThat(options.depsArtifacts()).containsExactly("foo.jdeps", "bar.jdeps");
   }
 
-  /** Makes sure turbine accepts old-style arguments. */
-  // TODO(b/72379900): Remove this.
-  @Test
-  public void testLegacyStrictJavaDepsArgs() throws Exception {
-    String[] lines = {
-      "--direct_dependency",
-      "blaze-out/foo/libbar.jar",
-      "//foo/bar",
-      "--indirect_dependency",
-      "blaze-out/foo/libbaz1.jar",
-      "//foo/baz1",
-      "--indirect_dependency",
-      "blaze-out/foo/libbaz2.jar",
-      "//foo/baz2",
-      "--indirect_dependency",
-      "blaze-out/proto/libproto.jar",
-      "//proto",
-      "java_proto_library",
-      "--deps_artifacts",
-      "foo.jdeps",
-      "bar.jdeps",
-      "",
-    };
-
-    TurbineOptions options =
-        TurbineOptionsParser.parse(Iterables.concat(BASE_ARGS, Arrays.asList(lines)));
-
-    assertThat(options.targetLabel()).hasValue("//java/com/google/test");
-    assertThat(options.depsArtifacts()).containsExactly("foo.jdeps", "bar.jdeps");
-  }
-
   @Test
   public void classpathArgs() throws Exception {
     String[] lines = {
diff --git a/javatests/com/google/turbine/parse/ParseErrorTest.java b/javatests/com/google/turbine/parse/ParseErrorTest.java
index 0ec6208..c728c8a 100644
--- a/javatests/com/google/turbine/parse/ParseErrorTest.java
+++ b/javatests/com/google/turbine/parse/ParseErrorTest.java
@@ -122,6 +122,38 @@
     }
   }
 
+  @Test
+  public void dropParens() {
+    String input = "enum E { ONE(";
+    try {
+      Parser.parse(input);
+      fail("expected parsing to fail");
+    } catch (TurbineError e) {
+      assertThat(e.getMessage())
+          .isEqualTo(
+              lines(
+                  "<>:1: error: unexpected end of input", //
+                  "enum E { ONE(",
+                  "            ^"));
+    }
+  }
+
+  @Test
+  public void dropBlocks() {
+    String input = "class T { Object f = new Object() {";
+    try {
+      Parser.parse(input);
+      fail("expected parsing to fail");
+    } catch (TurbineError e) {
+      assertThat(e.getMessage())
+          .isEqualTo(
+              lines(
+                  "<>:1: error: unexpected end of input", //
+                  "class T { Object f = new Object() {",
+                  "                                  ^"));
+    }
+  }
+
   private static String lines(String... lines) {
     return Joiner.on(System.lineSeparator()).join(lines);
   }
diff --git a/pom.xml b/pom.xml
index 58bc5f4..f9316f2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,17 +23,18 @@
     <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
-      <version>22.0</version>
-    </dependency>
-    <dependency>
-      <groupId>com.google.code.findbugs</groupId>
-      <artifactId>jsr305</artifactId>
-      <version>2.0.1</version>
+      <version>23.6-jre</version>
     </dependency>
     <dependency>
       <groupId>com.google.errorprone</groupId>
       <artifactId>error_prone_annotations</artifactId>
-      <version>2.0.12</version>
+      <version>2.2.0</version>
+    </dependency>
+    <dependency>
+      <groupId>org.checkerframework</groupId>
+      <artifactId>checker-qual</artifactId>
+      <version>2.0.0</version>
+      <optional>true</optional>
     </dependency>
     <dependency>
       <groupId>com.google.protobuf</groupId>