Split functions relating to type-nullability out of pointer_nullability.h

The former has been kind of an "everything that needs to be shared" grab-bag,
and it's growing...
The functionality here today splits neatly into two groups, one that interacts
with the Value world and one that interacts with the Type world, with analysis
depending on both.

The exception is getNullabilityForChild, which fits naturally into analysis.
It was moved here with the plan of being shared with diagnosis. but hasn't been.
It doesn't seem suitable to sharein its current form - it causes other layering
problems too, and its preconditions/API are defined in terms of analysis.
So I've moved this back into analysis for now.

Wrote a few words about the type-nullability model.
I'd like to follow up by renaming a few functions, adding a TypeNullability
alias, adding more tests, but trying to keep the scope for discussion smaller.

PiperOrigin-RevId: 528755134
diff --git a/nullability/BUILD b/nullability/BUILD
index 7272c47..0fffa9b 100644
--- a/nullability/BUILD
+++ b/nullability/BUILD
@@ -31,8 +31,8 @@
         ":pointer_nullability",
         ":pointer_nullability_lattice",
         ":pointer_nullability_matchers",
+        ":type_nullability",
         "@absl//absl/log:check",
-        "@absl//absl/strings",
         "@llvm-project//clang:analysis",
         "@llvm-project//clang:ast",
         "@llvm-project//clang:ast_matchers",
@@ -49,20 +49,17 @@
         ":pointer_nullability",
         ":pointer_nullability_lattice",
         ":pointer_nullability_matchers",
+        ":type_nullability",
         "@llvm-project//clang:analysis",
         "@llvm-project//clang:ast",
         "@llvm-project//clang:ast_matchers",
         "@llvm-project//clang:basic",
-        "@llvm-project//llvm:Support",
     ],
 )
 
 cc_library(
     name = "pointer_nullability",
-    srcs = [
-        "pointer_nullability.cc",
-        "type_nullability.cc",
-    ],
+    srcs = ["pointer_nullability.cc"],
     hdrs = ["pointer_nullability.h"],
     deps = [
         ":pointer_nullability_lattice",
@@ -86,3 +83,29 @@
         "@llvm-project//third-party/unittest:gtest_main",
     ],
 )
+
+cc_library(
+    name = "type_nullability",
+    srcs = ["type_nullability.cc"],
+    hdrs = ["type_nullability.h"],
+    deps = [
+        "@absl//absl/log:check",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:basic",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_test(
+    name = "type_nullability_test",
+    srcs = ["type_nullability_test.cc"],
+    deps = [
+        ":type_nullability",
+        "@absl//absl/log:check",
+        "@llvm-project//clang:testing",
+        "@llvm-project//llvm:Support",
+        "@llvm-project//third-party/unittest:gmock",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
diff --git a/nullability/pointer_nullability.cc b/nullability/pointer_nullability.cc
index 34e16ff..ae3e024 100644
--- a/nullability/pointer_nullability.cc
+++ b/nullability/pointer_nullability.cc
@@ -4,15 +4,11 @@
 
 #include "nullability/pointer_nullability.h"
 
-#include "absl/log/check.h"
 #include "nullability/pointer_nullability_lattice.h"
 #include "clang/AST/ASTDumper.h"
-#include "clang/AST/TypeVisitor.h"
-#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
 #include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
 #include "clang/Analysis/FlowSensitive/Value.h"
 #include "llvm/ADT/StringRef.h"
-#include "llvm/Support/SaveAndRestore.h"
 
 namespace clang {
 namespace tidy {
@@ -70,247 +66,6 @@
   return !Env.flowConditionImplies(PointerNotKnownNull);
 }
 
-std::string nullabilityToString(ArrayRef<NullabilityKind> Nullability) {
-  std::string Result = "[";
-  llvm::interleave(
-      Nullability,
-      [&](const NullabilityKind n) {
-        Result += getNullabilitySpelling(n).str();
-      },
-      [&] { Result += ", "; });
-  Result += "]";
-  return Result;
-}
-
-namespace {
-// Traverses a Type to find the points where it might be nullable.
-// This will visit the contained PointerType in the correct order to produce
-// the TypeNullability vector.
-//
-// Subclasses must provide `void report(const PointerType*, NullabilityKind)`,
-// and may override TypeVisitor Visit*Type methods to customize the traversal.
-//
-// Canonically-equivalent Types produce equivalent sequences of report() calls:
-//  - corresponding PointerTypes are canonically-equivalent
-//  - the NullabilityKind may be different, as it derives from type sugar
-template <class Impl>
-class NullabilityWalker : public TypeVisitor<Impl> {
-  using Base = TypeVisitor<Impl>;
-  Impl& derived() { return *static_cast<Impl*>(this); }
-
-  // A nullability attribute we've seen, waiting to attach to a pointer type.
-  // There may be sugar in between: Attributed -> Typedef -> Typedef -> Pointer.
-  // All non-sugar types must consume nullability, most will ignore it.
-  std::optional<NullabilityKind> PendingNullability;
-
-  void ignoreUnexpectedNullability() {
-    // TODO: Can we upgrade this to an assert?
-    // clang is pretty thorough about ensuring we can't put _Nullable on
-    // non-pointers, even failing template instantiation on this basis.
-    PendingNullability.reset();
-  }
-
-  // While walking the underlying type of alias TemplateSpecializationTypes,
-  // we see SubstTemplateTypeParmTypes where type parameters were referenced.
-  // The directly-available underlying types lack sugar, but we can retrieve the
-  // sugar from the arguments of the original TemplateSpecializationType.
-  //
-  // It is only possible to reference params of the immediately enclosing alias,
-  // so we keep details of the alias specialization we're currently processing.
-  struct AliasArgs {
-    const Decl* AssociatedDecl;
-    ArrayRef<TemplateArgument> Args;
-    // The alias context in which the alias specialization itself appeared.
-    // (The alias's args may reference params from this context.)
-    const AliasArgs* Parent;
-  };
-  const AliasArgs* CurrentAliasTemplate = nullptr;
-
- public:
-  void Visit(QualType T) { Base::Visit(T.getTypePtr()); }
-  void Visit(const TemplateArgument& TA) {
-    if (TA.getKind() == TemplateArgument::Type) Visit(TA.getAsType());
-    if (TA.getKind() == TemplateArgument::Pack)
-      for (const auto& PackElt : TA.getPackAsArray()) Visit(PackElt);
-  }
-  void Visit(const DeclContext* DC) {
-    // For now, only consider enclosing classes.
-    // TODO: The nullability of template functions can affect local classes too,
-    // this can be relevant e.g. when instantiating templates with such types.
-    if (auto* CRD = llvm::dyn_cast<CXXRecordDecl>(DC))
-      Visit(DC->getParentASTContext().getRecordType(CRD));
-  }
-
-  void VisitType(const Type* T) {
-    // For sugar not explicitly handled below, desugar and continue.
-    // (We need to walk the full structure of the canonical type.)
-    if (auto* Desugar =
-            T->getLocallyUnqualifiedSingleStepDesugaredType().getTypePtr();
-        Desugar != T)
-      return Base::Visit(Desugar);
-
-    // We don't expect to see any nullable non-sugar types except PointerType.
-    ignoreUnexpectedNullability();
-    Base::VisitType(T);
-  }
-
-  void VisitFunctionProtoType(const FunctionProtoType* FPT) {
-    ignoreUnexpectedNullability();
-    Visit(FPT->getReturnType());
-    for (auto ParamType : FPT->getParamTypes()) Visit(ParamType);
-  }
-
-  void VisitTemplateSpecializationType(const TemplateSpecializationType* TST) {
-    if (TST->isTypeAlias()) {
-      // Aliases are sugar, visit the underlying type.
-      // Record template args so we can resugar substituted params.
-      const AliasArgs Args{TST->getTemplateName().getAsTemplateDecl(),
-                           TST->template_arguments(), CurrentAliasTemplate};
-      llvm::SaveAndRestore UseAlias(CurrentAliasTemplate, &Args);
-      VisitType(TST);
-      return;
-    }
-
-    auto* CRD = TST->getAsCXXRecordDecl();
-    CHECK(CRD) << "Expected an alias or class specialization in concrete code";
-    ignoreUnexpectedNullability();
-    Visit(CRD->getDeclContext());
-    for (auto TA : TST->template_arguments()) Visit(TA);
-  }
-
-  void VisitSubstTemplateTypeParmType(const SubstTemplateTypeParmType* T) {
-    if (isa<TypeAliasTemplateDecl>(T->getAssociatedDecl())) {
-      if (CurrentAliasTemplate != nullptr) {
-        CHECK(T->getAssociatedDecl() == CurrentAliasTemplate->AssociatedDecl);
-        unsigned Index = T->getIndex();
-        // Valid because pack must be the last param in alias templates.
-        if (auto PackIndex = T->getPackIndex())
-          Index = CurrentAliasTemplate->Args.size() - 1 - *PackIndex;
-        const TemplateArgument& Arg = CurrentAliasTemplate->Args[Index];
-
-        llvm::SaveAndRestore OriginalContext(CurrentAliasTemplate,
-                                             CurrentAliasTemplate->Parent);
-        return Visit(Arg);
-      } else {
-        // Our top-level type references an unbound type alias param.
-        // Presumably our original input was the underlying type of an alias
-        // instantiation, we now lack the context needed to resugar it.
-        // TODO: maybe this could be an assert? We would need to trust all
-        // callers are obtaining types appropriately, and that clang never
-        // partially-desugars in a problematic way.
-      }
-    }
-    VisitType(T);
-  }
-
-  void VisitRecordType(const RecordType* RT) {
-    ignoreUnexpectedNullability();
-    Visit(RT->getDecl()->getDeclContext());
-    if (auto* CTSD = dyn_cast<ClassTemplateSpecializationDecl>(RT->getDecl())) {
-      // TODO: if this is an instantiation, these args lack sugar.
-      // We can try to retrieve it from the current template context.
-      for (auto& TA : CTSD->getTemplateArgs().asArray()) Visit(TA);
-    }
-  }
-
-  void VisitAttributedType(const AttributedType* AT) {
-    if (auto NK = AT->getImmediateNullability()) {
-      // If we see nullability applied twice, the outer one wins.
-      if (!PendingNullability.has_value()) PendingNullability = *NK;
-    }
-    Visit(AT->getModifiedType());
-    CHECK(!PendingNullability.has_value())
-        << "Should have been consumed by modified type! "
-        << AT->getModifiedType().getAsString();
-  }
-
-  void VisitPointerType(const PointerType* PT) {
-    derived().report(PT,
-                     PendingNullability.value_or(NullabilityKind::Unspecified));
-    PendingNullability.reset();
-    Visit(PT->getPointeeType());
-  }
-};
-
-template <typename T>
-unsigned countPointers(const T& Object) {
-  struct Walker : public NullabilityWalker<Walker> {
-    unsigned Count = 0;
-    void report(const PointerType*, NullabilityKind) { ++Count; }
-  } PointerCountWalker;
-  PointerCountWalker.Visit(Object);
-  return PointerCountWalker.Count;
-}
-
-}  // namespace
-
-unsigned countPointersInType(QualType T) { return countPointers(T); }
-
-unsigned countPointersInType(const DeclContext* DC) {
-  return countPointers(DC);
-}
-unsigned countPointersInType(TemplateArgument TA) { return countPointers(TA); }
-
-QualType exprType(const Expr* E) {
-  if (E->hasPlaceholderType(BuiltinType::BoundMember))
-    return Expr::findBoundMemberType(E);
-  return E->getType();
-}
-
-unsigned countPointersInType(const Expr* E) {
-  return countPointersInType(exprType(E));
-}
-
-std::vector<NullabilityKind> getNullabilityAnnotationsFromType(
-    QualType T,
-    llvm::function_ref<GetTypeParamNullability> SubstituteTypeParam) {
-  struct Walker : NullabilityWalker<Walker> {
-    std::vector<NullabilityKind> Annotations;
-    llvm::function_ref<GetTypeParamNullability> SubstituteTypeParam;
-
-    void report(const PointerType*, NullabilityKind NK) {
-      Annotations.push_back(NK);
-    }
-
-    void VisitSubstTemplateTypeParmType(const SubstTemplateTypeParmType* ST) {
-      if (SubstituteTypeParam) {
-        if (auto Subst = SubstituteTypeParam(ST)) {
-          DCHECK_EQ(Subst->size(),
-                    countPointersInType(ST->getCanonicalTypeInternal()))
-              << "Substituted nullability has the wrong structure: "
-              << QualType(ST, 0).getAsString();
-          llvm::append_range(Annotations, *Subst);
-          return;
-        }
-      }
-      NullabilityWalker::VisitSubstTemplateTypeParmType(ST);
-    }
-  } AnnotationVisitor;
-  AnnotationVisitor.SubstituteTypeParam = SubstituteTypeParam;
-  AnnotationVisitor.Visit(T);
-  return std::move(AnnotationVisitor.Annotations);
-}
-
-std::vector<NullabilityKind> unspecifiedNullability(const Expr* E) {
-  return std::vector<NullabilityKind>(countPointersInType(E),
-                                      NullabilityKind::Unspecified);
-}
-
-ArrayRef<NullabilityKind> getNullabilityForChild(
-    const Expr* E, TransferState<PointerNullabilityLattice>& State) {
-  return State.Lattice.insertExprNullabilityIfAbsent(E, [&] {
-    // Since we process child nodes before parents, we should already have
-    // computed the child nullability. However, this is not true in all test
-    // cases. So, we return unspecified nullability annotations.
-    // TODO: fix this issue, and CHECK() instead.
-    llvm::dbgs() << "=== Missing child nullability: ===\n";
-    dump(E, llvm::dbgs());
-    llvm::dbgs() << "==================================\n";
-
-    return unspecifiedNullability(E);
-  });
-}
-
 }  // namespace nullability
 }  // namespace tidy
 }  // namespace clang
diff --git a/nullability/pointer_nullability.h b/nullability/pointer_nullability.h
index b88a889..0ec84f2 100644
--- a/nullability/pointer_nullability.h
+++ b/nullability/pointer_nullability.h
@@ -22,10 +22,6 @@
 
 using dataflow::TransferState;
 
-/// Returns the `NullabilityKind` corresponding to the nullability annotation on
-/// `Type` if present. Otherwise, returns `NullabilityKind::Unspecified`.
-NullabilityKind getNullabilityKind(QualType Type, ASTContext& Ctx);
-
 /// Returns the `PointerValue` allocated to `PointerExpr` if available.
 /// Otherwise, returns nullptr.
 dataflow::PointerValue* getPointerValueFromExpr(
@@ -95,43 +91,6 @@
 bool isNullable(const dataflow::PointerValue& PointerVal,
                 const dataflow::Environment& Env);
 
-/// Returns a human-readable debug representation of a nullability vector.
-std::string nullabilityToString(ArrayRef<NullabilityKind> Nullability);
-
-/// A function that may provide enhanced nullability information for a
-/// substituted template parameter (which has no sugar of its own).
-using GetTypeParamNullability = std::optional<std::vector<NullabilityKind>>(
-    const SubstTemplateTypeParmType* ST);
-/// Traverse over a type to get its nullability. For example, if T is the type
-/// Struct3Arg<int * _Nonnull, int, pair<int * _Nullable, int *>> * _Nonnull,
-/// the resulting nullability annotations will be {_Nonnull, _Nonnull,
-/// _Nullable, _Unknown}. Note that non-pointer elements (e.g., the second
-/// argument of Struct3Arg) do not get a nullability annotation.
-std::vector<NullabilityKind> getNullabilityAnnotationsFromType(
-    QualType T,
-    llvm::function_ref<GetTypeParamNullability> SubstituteTypeParam = nullptr);
-
-/// Prints QualType's underlying canonical type, annotated with nullability.
-/// See rebuildWithNullability().
-std::string printWithNullability(QualType, ArrayRef<NullabilityKind>,
-                                 ASTContext&);
-/// Returns an equivalent type annotated with the provided nullability.
-/// Any existing sugar (including nullability) is discarded.
-/// rebuildWithNullability(int *, {Nullable}) ==> int * _Nullable.
-QualType rebuildWithNullability(QualType, ArrayRef<NullabilityKind>,
-                                ASTContext&);
-
-/// Computes the number of pointer slots within a type.
-/// Each of these could conceptually be nullable, so this is the length of
-/// the nullability vector computed by getNullabilityAnnotationsFromType().
-unsigned countPointersInType(QualType T);
-unsigned countPointersInType(const Expr* E);
-unsigned countPointersInType(TemplateArgument TA);
-unsigned countPointersInType(const DeclContext* DC);
-
-QualType exprType(const Expr* E);
-
-std::vector<NullabilityKind> unspecifiedNullability(const Expr* E);
 
 // Work around the lack of Expr.dump() etc with an ostream but no ASTContext.
 template <typename T>
@@ -139,11 +98,6 @@
   clang::ASTDumper(OS, /*ShowColors=*/false).Visit(Node);
 }
 
-// Returns the computed nullability for a subexpr of the current expression.
-// This is always available as we compute bottom-up.
-ArrayRef<NullabilityKind> getNullabilityForChild(
-    const Expr* E, TransferState<PointerNullabilityLattice>& State);
-
 }  // namespace nullability
 }  // namespace tidy
 }  // namespace clang
diff --git a/nullability/pointer_nullability_analysis.cc b/nullability/pointer_nullability_analysis.cc
index 7ab5c82..bacba41 100644
--- a/nullability/pointer_nullability_analysis.cc
+++ b/nullability/pointer_nullability_analysis.cc
@@ -12,6 +12,7 @@
 #include "nullability/pointer_nullability.h"
 #include "nullability/pointer_nullability_lattice.h"
 #include "nullability/pointer_nullability_matchers.h"
+#include "nullability/type_nullability.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/ASTDumper.h"
 #include "clang/AST/Expr.h"
@@ -76,6 +77,23 @@
   });
 }
 
+// Returns the computed nullability for a subexpr of the current expression.
+// This is always available as we compute bottom-up.
+ArrayRef<NullabilityKind> getNullabilityForChild(
+    const Expr* E, TransferState<PointerNullabilityLattice>& State) {
+  return State.Lattice.insertExprNullabilityIfAbsent(E, [&] {
+    // Since we process child nodes before parents, we should already have
+    // computed the child nullability. However, this is not true in all test
+    // cases. So, we return unspecified nullability annotations.
+    // TODO: fix this issue, and CHECK() instead.
+    llvm::dbgs() << "=== Missing child nullability: ===\n";
+    dump(E, llvm::dbgs());
+    llvm::dbgs() << "==================================\n";
+
+    return unspecifiedNullability(E);
+  });
+}
+
 /// Compute the nullability annotation of type `T`, which contains types
 /// originally written as a class template type parameter.
 ///
@@ -692,6 +710,7 @@
 
   return true;
 }
+
 }  // namespace nullability
 }  // namespace tidy
 }  // namespace clang
diff --git a/nullability/pointer_nullability_diagnosis.cc b/nullability/pointer_nullability_diagnosis.cc
index 8289142..afd2e23 100644
--- a/nullability/pointer_nullability_diagnosis.cc
+++ b/nullability/pointer_nullability_diagnosis.cc
@@ -9,6 +9,7 @@
 
 #include "nullability/pointer_nullability.h"
 #include "nullability/pointer_nullability_matchers.h"
+#include "nullability/type_nullability.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/DeclCXX.h"
 #include "clang/AST/Expr.h"
diff --git a/nullability/pointer_nullability_test.cc b/nullability/pointer_nullability_test.cc
index 89a6bbc..e44ae74 100644
--- a/nullability/pointer_nullability_test.cc
+++ b/nullability/pointer_nullability_test.cc
@@ -13,276 +13,5 @@
 namespace {
 using testing::ElementsAre;
 
-class GetNullabilityAnnotationsFromTypeTest : public ::testing::Test {
- protected:
-  // C++ declarations prepended before parsing type in nullVec().
-  std::string Preamble;
-
-  // Parses `Type` and returns getNullabilityAnnotationsFromType().
-  std::vector<NullabilityKind> nullVec(llvm::StringRef Type) {
-    clang::TestAST AST((Preamble + "\nusing Target = " + Type + ";").str());
-    auto Target = AST.context().getTranslationUnitDecl()->lookup(
-        &AST.context().Idents.get("Target"));
-    CHECK(Target.isSingleResult());
-    QualType TargetType =
-        AST.context().getTypedefType(Target.find_first<TypeAliasDecl>());
-    return getNullabilityAnnotationsFromType(TargetType);
-  }
-};
-
-TEST_F(GetNullabilityAnnotationsFromTypeTest, Pointers) {
-  EXPECT_THAT(nullVec("int"), ElementsAre());
-  EXPECT_THAT(nullVec("int *"), ElementsAre(NullabilityKind::Unspecified));
-  EXPECT_THAT(nullVec("int **"), ElementsAre(NullabilityKind::Unspecified,
-                                             NullabilityKind::Unspecified));
-  EXPECT_THAT(nullVec("int *_Nullable*_Nonnull"),
-              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable));
-}
-
-TEST_F(GetNullabilityAnnotationsFromTypeTest, Sugar) {
-  Preamble = "using X = int* _Nonnull;";
-
-  EXPECT_THAT(nullVec("X"), ElementsAre(NullabilityKind::NonNull));
-  EXPECT_THAT(nullVec("X*"), ElementsAre(NullabilityKind::Unspecified,
-                                         NullabilityKind::NonNull));
-
-  EXPECT_THAT(nullVec("X(*)"), ElementsAre(NullabilityKind::Unspecified,
-                                           NullabilityKind::NonNull));
-}
-
-TEST_F(GetNullabilityAnnotationsFromTypeTest, AliasTemplates) {
-  Preamble = R"cpp(
-    template <typename T>
-    using Nullable = T _Nullable;
-    template <typename T>
-    using Nonnull = T _Nonnull;
-  )cpp";
-  EXPECT_THAT(nullVec("Nullable<int*>"),
-              ElementsAre(NullabilityKind::Nullable));
-
-  EXPECT_THAT(
-      nullVec("Nullable<Nullable<int*>*>"),
-      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Nullable));
-
-  EXPECT_THAT(nullVec("Nullable<Nullable<Nonnull<int*>*>*>"),
-              ElementsAre(NullabilityKind::Nullable, NullabilityKind::Nullable,
-                          NullabilityKind::NonNull));
-
-  Preamble = R"cpp(
-    template <typename T, typename U>
-    struct Pair;
-    template <typename T>
-    using Two = Pair<T, T>;
-  )cpp";
-  EXPECT_THAT(
-      nullVec("Two<int* _Nullable>"),
-      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Nullable));
-
-  Preamble = R"cpp(
-    template <typename T1>
-    using A = T1* _Nullable;
-    template <typename T2>
-    using B = A<T2>* _Nonnull;
-  )cpp";
-  EXPECT_THAT(nullVec("B<int>"),
-              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable));
-
-  Preamble = R"cpp(
-    template <typename T, typename U, typename V>
-    struct Triple;
-    template <typename A, typename... Rest>
-    using TripleAlias = Triple<A _Nonnull, Rest...>;
-  )cpp";
-  EXPECT_THAT(nullVec("TripleAlias<int *, int *_Nullable, int*>"),
-              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable,
-                          NullabilityKind::Unspecified));
-}
-
-TEST_F(GetNullabilityAnnotationsFromTypeTest, DependentAlias) {
-  // Simple dependent type-aliases.
-  Preamble = R"cpp(
-    template <class T>
-    struct Nullable {
-      using type = T _Nullable;
-    };
-  )cpp";
-  // TODO: should be [Nullable, Nonnull]
-  EXPECT_THAT(
-      nullVec("Nullable<int* _Nonnull *>::type"),
-      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Unspecified));
-}
-
-TEST_F(GetNullabilityAnnotationsFromTypeTest, NestedClassTemplate) {
-  // Simple struct inside template.
-  Preamble = R"cpp(
-    template <class T>
-    struct Outer {
-      struct Inner;
-    };
-    using OuterNullableInner = Outer<int* _Nonnull>::Inner;
-  )cpp";
-  // TODO: should be [NonNull]
-  EXPECT_THAT(nullVec("Outer<int* _Nonnull>::Inner"),
-              ElementsAre(NullabilityKind::Unspecified));
-}
-
-TEST_F(GetNullabilityAnnotationsFromTypeTest, ReferenceOuterTemplateParam) {
-  // Referencing type-params from indirectly-enclosing template.
-  Preamble = R"cpp(
-    template <class A, class B>
-    struct Pair;
-
-    template <class T>
-    struct Outer {
-      template <class U>
-      struct Inner {
-        using type = Pair<U, T>;
-      };
-    };
-  )cpp";
-  // TODO: should be [Nonnull, Nullable]
-  EXPECT_THAT(
-      nullVec("Outer<int *_Nullable>::Inner<int *_Nonnull>::type"),
-      ElementsAre(NullabilityKind::Unspecified, NullabilityKind::Unspecified));
-}
-
-TEST_F(GetNullabilityAnnotationsFromTypeTest, DependentlyNamedTemplate) {
-  // Instantiation of dependent-named template
-  Preamble = R"cpp(
-    struct Wrapper {
-      template <class T>
-      using Nullable = T _Nullable;
-    };
-
-    template <class U, class WrapT>
-    struct S {
-      using type = typename WrapT::template Nullable<U>* _Nonnull;
-    };
-  )cpp";
-  EXPECT_THAT(nullVec("S<int *, Wrapper>::type"),
-              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable));
-}
-
-TEST_F(GetNullabilityAnnotationsFromTypeTest, TemplateTemplateParams) {
-  // Template template params
-  Preamble = R"cpp(
-    template <class X>
-    struct Nullable {
-      using type = X _Nullable;
-    };
-    template <class X>
-    struct Nonnull {
-      using type = X _Nonnull;
-    };
-
-    template <template <class> class Nullability, class T>
-    struct Pointer {
-      using type = typename Nullability<T*>::type;
-    };
-  )cpp";
-  EXPECT_THAT(nullVec("Pointer<Nullable, int>::type"),
-              ElementsAre(NullabilityKind::Nullable));
-  // TODO: should be [Nullable, Nonnull]
-  EXPECT_THAT(
-      nullVec("Pointer<Nullable, Pointer<Nonnull, int>::type>::type"),
-      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Unspecified));
-  // Same thing, but with alias templates.
-  Preamble = R"cpp(
-    template <class X>
-    using Nullable = X _Nullable;
-    template <class X>
-    using Nonnull = X _Nonnull;
-
-    template <template <class> class Nullability, class T>
-    struct Pointer {
-      using type = Nullability<T*>;
-    };
-  )cpp";
-  EXPECT_THAT(nullVec("Pointer<Nullable, int>::type"),
-              ElementsAre(NullabilityKind::Nullable));
-  // TODO: should be [Nullable, Nonnull]
-  EXPECT_THAT(
-      nullVec("Pointer<Nullable, Pointer<Nonnull, int>::type>::type"),
-      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Unspecified));
-}
-
-TEST_F(GetNullabilityAnnotationsFromTypeTest, ClassTemplateParamPack) {
-  // Parameter packs
-  Preamble = R"cpp(
-    template <class... X>
-    struct TupleWrapper {
-      class Tuple;
-    };
-
-    template <class... X>
-    struct NullableTuple {
-      using type = TupleWrapper<X _Nullable...>::Tuple;
-    };
-  )cpp";
-  // TODO: should be [Unspecified, Nonnull]
-  EXPECT_THAT(
-      nullVec("TupleWrapper<int*, int* _Nonnull>::Tuple"),
-      ElementsAre(NullabilityKind::Unspecified, NullabilityKind::Unspecified));
-  // TODO: should be [Nullable, Nullable]
-  EXPECT_THAT(
-      nullVec("NullableTuple<int*, int* _Nonnull>::type"),
-      ElementsAre(NullabilityKind::Unspecified, NullabilityKind::Unspecified));
-}
-
-class PrintWithNullabilityTest : public ::testing::Test {
- protected:
-  // C++ declarations prepended before parsing type in nullVec().
-  std::string Preamble;
-
-  // Parses `Type`, augments it with Nulls, and prints the result.
-  std::string print(llvm::StringRef Type, ArrayRef<NullabilityKind> Nulls) {
-    clang::TestAST AST((Preamble + "\n using Target = " + Type + ";").str());
-    auto Target = AST.context().getTranslationUnitDecl()->lookup(
-        &AST.context().Idents.get("Target"));
-    CHECK(Target.isSingleResult());
-    QualType TargetType =
-        AST.context().getTypedefType(Target.find_first<TypeAliasDecl>());
-    return printWithNullability(TargetType, Nulls, AST.context());
-  }
-};
-
-TEST_F(PrintWithNullabilityTest, Pointers) {
-  EXPECT_EQ(print("int*", {NullabilityKind::Nullable}), "int * _Nullable");
-  EXPECT_EQ(
-      print("int***", {NullabilityKind::Nullable, NullabilityKind::NonNull,
-                       NullabilityKind::Unspecified}),
-      "int ** _Nonnull * _Nullable");
-}
-
-TEST_F(PrintWithNullabilityTest, Sugar) {
-  Preamble = R"cpp(
-    template <class T>
-    using Ptr = T*;
-    using Int = int;
-    using IntPtr = Ptr<Int>;
-  )cpp";
-  EXPECT_EQ(print("IntPtr", {NullabilityKind::Nullable}), "int * _Nullable");
-}
-
-TEST_F(PrintWithNullabilityTest, Templates) {
-  Preamble = R"cpp(
-    template <class>
-    struct vector;
-    template <class, class>
-    struct pair;
-  )cpp";
-  EXPECT_EQ(print("vector<pair<int*, int*>*>",
-                  {NullabilityKind::Nullable, NullabilityKind::NonNull,
-                   NullabilityKind::Unspecified}),
-            "vector<pair<int * _Nonnull, int *> * _Nullable>");
-}
-
-TEST_F(PrintWithNullabilityTest, Functions) {
-  EXPECT_EQ(print("float*(*)(double*, double*)",
-                  {NullabilityKind::Nullable, NullabilityKind::NonNull,
-                   NullabilityKind::NonNull, NullabilityKind::Unspecified}),
-            "float * _Nonnull (* _Nullable)(double * _Nonnull, double *)");
-}
-
 }  // namespace
 }  // namespace clang::tidy::nullability
diff --git a/nullability/type_nullability.cc b/nullability/type_nullability.cc
index 307239d..5dd01c6 100644
--- a/nullability/type_nullability.cc
+++ b/nullability/type_nullability.cc
@@ -2,16 +2,246 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
+#include "nullability/type_nullability.h"
+
 #include "absl/log/check.h"
-#include "nullability/pointer_nullability.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/DeclTemplate.h"
 #include "clang/AST/Type.h"
 #include "clang/AST/TypeVisitor.h"
 #include "clang/Basic/LLVM.h"
 #include "clang/Basic/Specifiers.h"
+#include "llvm/Support/SaveAndRestore.h"
 
 namespace clang::tidy::nullability {
+
+std::string nullabilityToString(ArrayRef<NullabilityKind> Nullability) {
+  std::string Result = "[";
+  llvm::interleave(
+      Nullability,
+      [&](const NullabilityKind n) {
+        Result += getNullabilitySpelling(n).str();
+      },
+      [&] { Result += ", "; });
+  Result += "]";
+  return Result;
+}
+
+namespace {
+
+// Traverses a Type to find the points where it might be nullable.
+// This will visit the contained PointerType in the correct order to produce
+// the TypeNullability vector.
+//
+// Subclasses must provide `void report(const PointerType*, NullabilityKind)`,
+// and may override TypeVisitor Visit*Type methods to customize the traversal.
+//
+// Canonically-equivalent Types produce equivalent sequences of report() calls:
+//  - corresponding PointerTypes are canonically-equivalent
+//  - the NullabilityKind may be different, as it derives from type sugar
+template <class Impl>
+class NullabilityWalker : public TypeVisitor<Impl> {
+  using Base = TypeVisitor<Impl>;
+  Impl& derived() { return *static_cast<Impl*>(this); }
+
+  // A nullability attribute we've seen, waiting to attach to a pointer type.
+  // There may be sugar in between: Attributed -> Typedef -> Typedef -> Pointer.
+  // All non-sugar types must consume nullability, most will ignore it.
+  std::optional<NullabilityKind> PendingNullability;
+
+  void ignoreUnexpectedNullability() {
+    // TODO: Can we upgrade this to an assert?
+    // clang is pretty thorough about ensuring we can't put _Nullable on
+    // non-pointers, even failing template instantiation on this basis.
+    PendingNullability.reset();
+  }
+
+  // While walking the underlying type of alias TemplateSpecializationTypes,
+  // we see SubstTemplateTypeParmTypes where type parameters were referenced.
+  // The directly-available underlying types lack sugar, but we can retrieve the
+  // sugar from the arguments of the original TemplateSpecializationType.
+  //
+  // It is only possible to reference params of the immediately enclosing alias,
+  // so we keep details of the alias specialization we're currently processing.
+  struct AliasArgs {
+    const Decl* AssociatedDecl;
+    ArrayRef<TemplateArgument> Args;
+    // The alias context in which the alias specialization itself appeared.
+    // (The alias's args may reference params from this context.)
+    const AliasArgs* Parent;
+  };
+  const AliasArgs* CurrentAliasTemplate = nullptr;
+
+ public:
+  void Visit(QualType T) { Base::Visit(T.getTypePtr()); }
+  void Visit(const TemplateArgument& TA) {
+    if (TA.getKind() == TemplateArgument::Type) Visit(TA.getAsType());
+    if (TA.getKind() == TemplateArgument::Pack)
+      for (const auto& PackElt : TA.getPackAsArray()) Visit(PackElt);
+  }
+  void Visit(const DeclContext* DC) {
+    // For now, only consider enclosing classes.
+    // TODO: The nullability of template functions can affect local classes too,
+    // this can be relevant e.g. when instantiating templates with such types.
+    if (auto* CRD = llvm::dyn_cast<CXXRecordDecl>(DC))
+      Visit(DC->getParentASTContext().getRecordType(CRD));
+  }
+
+  void VisitType(const Type* T) {
+    // For sugar not explicitly handled below, desugar and continue.
+    // (We need to walk the full structure of the canonical type.)
+    if (auto* Desugar =
+            T->getLocallyUnqualifiedSingleStepDesugaredType().getTypePtr();
+        Desugar != T)
+      return Base::Visit(Desugar);
+
+    // We don't expect to see any nullable non-sugar types except PointerType.
+    ignoreUnexpectedNullability();
+    Base::VisitType(T);
+  }
+
+  void VisitFunctionProtoType(const FunctionProtoType* FPT) {
+    ignoreUnexpectedNullability();
+    Visit(FPT->getReturnType());
+    for (auto ParamType : FPT->getParamTypes()) Visit(ParamType);
+  }
+
+  void VisitTemplateSpecializationType(const TemplateSpecializationType* TST) {
+    if (TST->isTypeAlias()) {
+      // Aliases are sugar, visit the underlying type.
+      // Record template args so we can resugar substituted params.
+      const AliasArgs Args{TST->getTemplateName().getAsTemplateDecl(),
+                           TST->template_arguments(), CurrentAliasTemplate};
+      llvm::SaveAndRestore UseAlias(CurrentAliasTemplate, &Args);
+      VisitType(TST);
+      return;
+    }
+
+    auto* CRD = TST->getAsCXXRecordDecl();
+    CHECK(CRD) << "Expected an alias or class specialization in concrete code";
+    ignoreUnexpectedNullability();
+    Visit(CRD->getDeclContext());
+    for (auto TA : TST->template_arguments()) Visit(TA);
+  }
+
+  void VisitSubstTemplateTypeParmType(const SubstTemplateTypeParmType* T) {
+    if (isa<TypeAliasTemplateDecl>(T->getAssociatedDecl())) {
+      if (CurrentAliasTemplate != nullptr) {
+        CHECK(T->getAssociatedDecl() == CurrentAliasTemplate->AssociatedDecl);
+        unsigned Index = T->getIndex();
+        // Valid because pack must be the last param in alias templates.
+        if (auto PackIndex = T->getPackIndex())
+          Index = CurrentAliasTemplate->Args.size() - 1 - *PackIndex;
+        const TemplateArgument& Arg = CurrentAliasTemplate->Args[Index];
+
+        llvm::SaveAndRestore OriginalContext(CurrentAliasTemplate,
+                                             CurrentAliasTemplate->Parent);
+        return Visit(Arg);
+      } else {
+        // Our top-level type references an unbound type alias param.
+        // Presumably our original input was the underlying type of an alias
+        // instantiation, we now lack the context needed to resugar it.
+        // TODO: maybe this could be an assert? We would need to trust all
+        // callers are obtaining types appropriately, and that clang never
+        // partially-desugars in a problematic way.
+      }
+    }
+    VisitType(T);
+  }
+
+  void VisitRecordType(const RecordType* RT) {
+    ignoreUnexpectedNullability();
+    Visit(RT->getDecl()->getDeclContext());
+    if (auto* CTSD = dyn_cast<ClassTemplateSpecializationDecl>(RT->getDecl())) {
+      // TODO: if this is an instantiation, these args lack sugar.
+      // We can try to retrieve it from the current template context.
+      for (auto& TA : CTSD->getTemplateArgs().asArray()) Visit(TA);
+    }
+  }
+
+  void VisitAttributedType(const AttributedType* AT) {
+    if (auto NK = AT->getImmediateNullability()) {
+      // If we see nullability applied twice, the outer one wins.
+      if (!PendingNullability.has_value()) PendingNullability = *NK;
+    }
+    Visit(AT->getModifiedType());
+    CHECK(!PendingNullability.has_value())
+        << "Should have been consumed by modified type! "
+        << AT->getModifiedType().getAsString();
+  }
+
+  void VisitPointerType(const PointerType* PT) {
+    derived().report(PT,
+                     PendingNullability.value_or(NullabilityKind::Unspecified));
+    PendingNullability.reset();
+    Visit(PT->getPointeeType());
+  }
+};
+
+template <typename T>
+unsigned countPointers(const T& Object) {
+  struct Walker : public NullabilityWalker<Walker> {
+    unsigned Count = 0;
+    void report(const PointerType*, NullabilityKind) { ++Count; }
+  } PointerCountWalker;
+  PointerCountWalker.Visit(Object);
+  return PointerCountWalker.Count;
+}
+
+}  // namespace
+
+unsigned countPointersInType(QualType T) { return countPointers(T); }
+
+unsigned countPointersInType(const DeclContext* DC) {
+  return countPointers(DC);
+}
+unsigned countPointersInType(TemplateArgument TA) { return countPointers(TA); }
+
+QualType exprType(const Expr* E) {
+  if (E->hasPlaceholderType(BuiltinType::BoundMember))
+    return Expr::findBoundMemberType(E);
+  return E->getType();
+}
+
+unsigned countPointersInType(const Expr* E) {
+  return countPointersInType(exprType(E));
+}
+
+std::vector<NullabilityKind> getNullabilityAnnotationsFromType(
+    QualType T,
+    llvm::function_ref<GetTypeParamNullability> SubstituteTypeParam) {
+  struct Walker : NullabilityWalker<Walker> {
+    std::vector<NullabilityKind> Annotations;
+    llvm::function_ref<GetTypeParamNullability> SubstituteTypeParam;
+
+    void report(const PointerType*, NullabilityKind NK) {
+      Annotations.push_back(NK);
+    }
+
+    void VisitSubstTemplateTypeParmType(const SubstTemplateTypeParmType* ST) {
+      if (SubstituteTypeParam) {
+        if (auto Subst = SubstituteTypeParam(ST)) {
+          DCHECK_EQ(Subst->size(),
+                    countPointersInType(ST->getCanonicalTypeInternal()))
+              << "Substituted nullability has the wrong structure: "
+              << QualType(ST, 0).getAsString();
+          llvm::append_range(Annotations, *Subst);
+          return;
+        }
+      }
+      NullabilityWalker::VisitSubstTemplateTypeParmType(ST);
+    }
+  } AnnotationVisitor;
+  AnnotationVisitor.SubstituteTypeParam = SubstituteTypeParam;
+  AnnotationVisitor.Visit(T);
+  return std::move(AnnotationVisitor.Annotations);
+}
+
+std::vector<NullabilityKind> unspecifiedNullability(const Expr* E) {
+  return std::vector<NullabilityKind>(countPointersInType(E),
+                                      NullabilityKind::Unspecified);
+}
+
 namespace {
 
 // Visitor to rebuild a QualType with explicit nullability.
@@ -26,13 +256,13 @@
 // This needs to stay in sync with the other algorithms that manipulate
 // nullability data structures for particular types: the non-flow-sensitive
 // transfer and NullabilityWalker.
-struct Visitor : public TypeVisitor<Visitor, QualType> {
-  Visitor(ArrayRef<NullabilityKind> Nullability, ASTContext& Ctx)
+struct Rebuilder : public TypeVisitor<Rebuilder, QualType> {
+  Rebuilder(ArrayRef<NullabilityKind> Nullability, ASTContext& Ctx)
       : Nullability(Nullability), Ctx(Ctx) {}
 
   bool done() const { return Nullability.empty(); }
 
-  using Base = TypeVisitor<Visitor, QualType>;
+  using Base = TypeVisitor<Rebuilder, QualType>;
   using Base::Visit;
   QualType Visit(QualType T) {
     if (T.isNull()) return T;
@@ -89,7 +319,7 @@
 QualType rebuildWithNullability(QualType T,
                                 ArrayRef<NullabilityKind> Nullability,
                                 ASTContext& Ctx) {
-  Visitor V(Nullability, Ctx);
+  Rebuilder V(Nullability, Ctx);
   QualType Result = V.Visit(T.getCanonicalType());
   CHECK(V.done()) << "Nullability vector[" << Nullability.size()
                   << "] too long for " << T.getAsString();
diff --git a/nullability/type_nullability.h b/nullability/type_nullability.h
new file mode 100644
index 0000000..d9a3ba3
--- /dev/null
+++ b/nullability/type_nullability.h
@@ -0,0 +1,84 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// This file defines an extension of C++'s type system to cover nullability:
+// Each pointer "slot" within a compound type is marked with a nullability kind.
+//
+// e.g. vector<int *> *
+//         Nullable^  ^Nonnull
+// This type describes non-null pointers to vectors of possibly-null pointers.
+//
+// This model interacts with clang's nullability attributes: the type
+// above can be written `vector<int * _Nullable> _Nonnull`.
+// The two are not quite the same thing:
+//   - we may infer nullability or use defaults where no attributes are written
+//   - we generally pass nullability around as a separate data structure rather
+//     than materializing the sugared types
+//   - we do not use _Nullable_result
+//
+// This is separate from our model of pointer values as part of the Value graph
+// (see pointer_nullability.h). The analysis makes use of both: generally type
+// nullability is useful with compound types like templates and functions where
+// the concrete pointer values are not visible to analysis.
+
+#ifndef CRUBIT_NULLABILITY_TYPE_NULLABILITY_H_
+#define CRUBIT_NULLABILITY_TYPE_NULLABILITY_H_
+
+#include <string>
+#include <utility>
+
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Expr.h"
+#include "clang/Basic/LLVM.h"
+#include "clang/Basic/Specifiers.h"
+
+namespace clang::tidy::nullability {
+
+/// Returns the `NullabilityKind` corresponding to the nullability annotation on
+/// `Type` if present. Otherwise, returns `NullabilityKind::Unspecified`.
+NullabilityKind getNullabilityKind(QualType Type, ASTContext& Ctx);
+
+/// Returns a human-readable debug representation of a nullability vector.
+std::string nullabilityToString(ArrayRef<NullabilityKind> Nullability);
+
+/// A function that may provide enhanced nullability information for a
+/// substituted template parameter (which has no sugar of its own).
+using GetTypeParamNullability = std::optional<std::vector<NullabilityKind>>(
+    const SubstTemplateTypeParmType* ST);
+/// Traverse over a type to get its nullability. For example, if T is the type
+/// Struct3Arg<int * _Nonnull, int, pair<int * _Nullable, int *>> * _Nonnull,
+/// the resulting nullability annotations will be {_Nonnull, _Nonnull,
+/// _Nullable, _Unknown}. Note that non-pointer elements (e.g., the second
+/// argument of Struct3Arg) do not get a nullability annotation.
+std::vector<NullabilityKind> getNullabilityAnnotationsFromType(
+    QualType T,
+    llvm::function_ref<GetTypeParamNullability> SubstituteTypeParam = nullptr);
+
+/// Prints QualType's underlying canonical type, annotated with nullability.
+/// See rebuildWithNullability().
+std::string printWithNullability(QualType, ArrayRef<NullabilityKind>,
+                                 ASTContext&);
+/// Returns an equivalent type annotated with the provided nullability.
+/// Any existing sugar (including nullability) is discarded.
+/// rebuildWithNullability(int *, {Nullable}) ==> int * _Nullable.
+QualType rebuildWithNullability(QualType, ArrayRef<NullabilityKind>,
+                                ASTContext&);
+
+/// Computes the number of pointer slots within a type.
+/// Each of these could conceptually be nullable, so this is the length of
+/// the nullability vector computed by getNullabilityAnnotationsFromType().
+unsigned countPointersInType(QualType T);
+unsigned countPointersInType(const Expr* E);
+unsigned countPointersInType(TemplateArgument TA);
+unsigned countPointersInType(const DeclContext* DC);
+
+/// Returns the type of an expression for the purposes of nullability.
+/// This handles wrinkles in the type system like BoundMember.
+QualType exprType(const Expr* E);
+
+std::vector<NullabilityKind> unspecifiedNullability(const Expr* E);
+
+}  // namespace clang::tidy::nullability
+
+#endif
diff --git a/nullability/type_nullability_test.cc b/nullability/type_nullability_test.cc
new file mode 100644
index 0000000..b9ba969
--- /dev/null
+++ b/nullability/type_nullability_test.cc
@@ -0,0 +1,289 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "nullability/type_nullability.h"
+
+#include "absl/log/check.h"
+#include "clang/Testing/TestAST.h"
+#include "llvm/ADT/StringRef.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googlemock/include/gmock/gmock.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang::tidy::nullability {
+namespace {
+using testing::ElementsAre;
+
+class GetNullabilityAnnotationsFromTypeTest : public ::testing::Test {
+ protected:
+  // C++ declarations prepended before parsing type in nullVec().
+  std::string Preamble;
+
+  // Parses `Type` and returns getNullabilityAnnotationsFromType().
+  std::vector<NullabilityKind> nullVec(llvm::StringRef Type) {
+    clang::TestAST AST((Preamble + "\nusing Target = " + Type + ";").str());
+    auto Target = AST.context().getTranslationUnitDecl()->lookup(
+        &AST.context().Idents.get("Target"));
+    CHECK(Target.isSingleResult());
+    QualType TargetType =
+        AST.context().getTypedefType(Target.find_first<TypeAliasDecl>());
+    return getNullabilityAnnotationsFromType(TargetType);
+  }
+};
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, Pointers) {
+  EXPECT_THAT(nullVec("int"), ElementsAre());
+  EXPECT_THAT(nullVec("int *"), ElementsAre(NullabilityKind::Unspecified));
+  EXPECT_THAT(nullVec("int **"), ElementsAre(NullabilityKind::Unspecified,
+                                             NullabilityKind::Unspecified));
+  EXPECT_THAT(nullVec("int *_Nullable*_Nonnull"),
+              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, Sugar) {
+  Preamble = "using X = int* _Nonnull;";
+
+  EXPECT_THAT(nullVec("X"), ElementsAre(NullabilityKind::NonNull));
+  EXPECT_THAT(nullVec("X*"), ElementsAre(NullabilityKind::Unspecified,
+                                         NullabilityKind::NonNull));
+
+  EXPECT_THAT(nullVec("X(*)"), ElementsAre(NullabilityKind::Unspecified,
+                                           NullabilityKind::NonNull));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, AliasTemplates) {
+  Preamble = R"cpp(
+    template <typename T>
+    using Nullable = T _Nullable;
+    template <typename T>
+    using Nonnull = T _Nonnull;
+  )cpp";
+  EXPECT_THAT(nullVec("Nullable<int*>"),
+              ElementsAre(NullabilityKind::Nullable));
+
+  EXPECT_THAT(
+      nullVec("Nullable<Nullable<int*>*>"),
+      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Nullable));
+
+  EXPECT_THAT(nullVec("Nullable<Nullable<Nonnull<int*>*>*>"),
+              ElementsAre(NullabilityKind::Nullable, NullabilityKind::Nullable,
+                          NullabilityKind::NonNull));
+
+  Preamble = R"cpp(
+    template <typename T, typename U>
+    struct Pair;
+    template <typename T>
+    using Two = Pair<T, T>;
+  )cpp";
+  EXPECT_THAT(
+      nullVec("Two<int* _Nullable>"),
+      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Nullable));
+
+  Preamble = R"cpp(
+    template <typename T1>
+    using A = T1* _Nullable;
+    template <typename T2>
+    using B = A<T2>* _Nonnull;
+  )cpp";
+  EXPECT_THAT(nullVec("B<int>"),
+              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable));
+
+  Preamble = R"cpp(
+    template <typename T, typename U, typename V>
+    struct Triple;
+    template <typename A, typename... Rest>
+    using TripleAlias = Triple<A _Nonnull, Rest...>;
+  )cpp";
+  EXPECT_THAT(nullVec("TripleAlias<int *, int *_Nullable, int*>"),
+              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable,
+                          NullabilityKind::Unspecified));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, DependentAlias) {
+  // Simple dependent type-aliases.
+  Preamble = R"cpp(
+    template <class T>
+    struct Nullable {
+      using type = T _Nullable;
+    };
+  )cpp";
+  // TODO: should be [Nullable, Nonnull]
+  EXPECT_THAT(
+      nullVec("Nullable<int* _Nonnull *>::type"),
+      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Unspecified));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, NestedClassTemplate) {
+  // Simple struct inside template.
+  Preamble = R"cpp(
+    template <class T>
+    struct Outer {
+      struct Inner;
+    };
+    using OuterNullableInner = Outer<int* _Nonnull>::Inner;
+  )cpp";
+  // TODO: should be [NonNull]
+  EXPECT_THAT(nullVec("Outer<int* _Nonnull>::Inner"),
+              ElementsAre(NullabilityKind::Unspecified));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, ReferenceOuterTemplateParam) {
+  // Referencing type-params from indirectly-enclosing template.
+  Preamble = R"cpp(
+    template <class A, class B>
+    struct Pair;
+
+    template <class T>
+    struct Outer {
+      template <class U>
+      struct Inner {
+        using type = Pair<U, T>;
+      };
+    };
+  )cpp";
+  // TODO: should be [Nonnull, Nullable]
+  EXPECT_THAT(
+      nullVec("Outer<int *_Nullable>::Inner<int *_Nonnull>::type"),
+      ElementsAre(NullabilityKind::Unspecified, NullabilityKind::Unspecified));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, DependentlyNamedTemplate) {
+  // Instantiation of dependent-named template
+  Preamble = R"cpp(
+    struct Wrapper {
+      template <class T>
+      using Nullable = T _Nullable;
+    };
+
+    template <class U, class WrapT>
+    struct S {
+      using type = typename WrapT::template Nullable<U>* _Nonnull;
+    };
+  )cpp";
+  EXPECT_THAT(nullVec("S<int *, Wrapper>::type"),
+              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, TemplateTemplateParams) {
+  // Template template params
+  Preamble = R"cpp(
+    template <class X>
+    struct Nullable {
+      using type = X _Nullable;
+    };
+    template <class X>
+    struct Nonnull {
+      using type = X _Nonnull;
+    };
+
+    template <template <class> class Nullability, class T>
+    struct Pointer {
+      using type = typename Nullability<T*>::type;
+    };
+  )cpp";
+  EXPECT_THAT(nullVec("Pointer<Nullable, int>::type"),
+              ElementsAre(NullabilityKind::Nullable));
+  // TODO: should be [Nullable, Nonnull]
+  EXPECT_THAT(
+      nullVec("Pointer<Nullable, Pointer<Nonnull, int>::type>::type"),
+      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Unspecified));
+  // Same thing, but with alias templates.
+  Preamble = R"cpp(
+    template <class X>
+    using Nullable = X _Nullable;
+    template <class X>
+    using Nonnull = X _Nonnull;
+
+    template <template <class> class Nullability, class T>
+    struct Pointer {
+      using type = Nullability<T*>;
+    };
+  )cpp";
+  EXPECT_THAT(nullVec("Pointer<Nullable, int>::type"),
+              ElementsAre(NullabilityKind::Nullable));
+  // TODO: should be [Nullable, Nonnull]
+  EXPECT_THAT(
+      nullVec("Pointer<Nullable, Pointer<Nonnull, int>::type>::type"),
+      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Unspecified));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, ClassTemplateParamPack) {
+  // Parameter packs
+  Preamble = R"cpp(
+    template <class... X>
+    struct TupleWrapper {
+      class Tuple;
+    };
+
+    template <class... X>
+    struct NullableTuple {
+      using type = TupleWrapper<X _Nullable...>::Tuple;
+    };
+  )cpp";
+  // TODO: should be [Unspecified, Nonnull]
+  EXPECT_THAT(
+      nullVec("TupleWrapper<int*, int* _Nonnull>::Tuple"),
+      ElementsAre(NullabilityKind::Unspecified, NullabilityKind::Unspecified));
+  // TODO: should be [Nullable, Nullable]
+  EXPECT_THAT(
+      nullVec("NullableTuple<int*, int* _Nonnull>::type"),
+      ElementsAre(NullabilityKind::Unspecified, NullabilityKind::Unspecified));
+}
+
+class PrintWithNullabilityTest : public ::testing::Test {
+ protected:
+  // C++ declarations prepended before parsing type in nullVec().
+  std::string Preamble;
+
+  // Parses `Type`, augments it with Nulls, and prints the result.
+  std::string print(llvm::StringRef Type, ArrayRef<NullabilityKind> Nulls) {
+    clang::TestAST AST((Preamble + "\n using Target = " + Type + ";").str());
+    auto Target = AST.context().getTranslationUnitDecl()->lookup(
+        &AST.context().Idents.get("Target"));
+    CHECK(Target.isSingleResult());
+    QualType TargetType =
+        AST.context().getTypedefType(Target.find_first<TypeAliasDecl>());
+    return printWithNullability(TargetType, Nulls, AST.context());
+  }
+};
+
+TEST_F(PrintWithNullabilityTest, Pointers) {
+  EXPECT_EQ(print("int*", {NullabilityKind::Nullable}), "int * _Nullable");
+  EXPECT_EQ(
+      print("int***", {NullabilityKind::Nullable, NullabilityKind::NonNull,
+                       NullabilityKind::Unspecified}),
+      "int ** _Nonnull * _Nullable");
+}
+
+TEST_F(PrintWithNullabilityTest, Sugar) {
+  Preamble = R"cpp(
+    template <class T>
+    using Ptr = T*;
+    using Int = int;
+    using IntPtr = Ptr<Int>;
+  )cpp";
+  EXPECT_EQ(print("IntPtr", {NullabilityKind::Nullable}), "int * _Nullable");
+}
+
+TEST_F(PrintWithNullabilityTest, Templates) {
+  Preamble = R"cpp(
+    template <class>
+    struct vector;
+    template <class, class>
+    struct pair;
+  )cpp";
+  EXPECT_EQ(print("vector<pair<int*, int*>*>",
+                  {NullabilityKind::Nullable, NullabilityKind::NonNull,
+                   NullabilityKind::Unspecified}),
+            "vector<pair<int * _Nonnull, int *> * _Nullable>");
+}
+
+TEST_F(PrintWithNullabilityTest, Functions) {
+  EXPECT_EQ(print("float*(*)(double*, double*)",
+                  {NullabilityKind::Nullable, NullabilityKind::NonNull,
+                   NullabilityKind::NonNull, NullabilityKind::Unspecified}),
+            "float * _Nonnull (* _Nullable)(double * _Nonnull, double *)");
+}
+
+}  // namespace
+}  // namespace clang::tidy::nullability