[lsp] textDocument.hover

This adds the "textDocument/hover" capability to the language server.

The functionality is split into three main steps:
* in compile.go, when a library is imported, Analyzer.genSymbolMap
  builds a map of fully-qualified symbol names to their type
  information (types defined in symbol.go).
* when the server receives a `hover` request, it calls
  Analyzer.TypeOfSymbol (in type.go), which looks up the symbol's type
  information in the symbol map for its library.
* the LangHandler converts that type information to a human-readable
  tooltip in symbolTypeToMarkedStrings (hover.go).

Test: go test ./...
Change-Id: I8619d125383688f4438ccce06b83414480d4874f
Reviewed-on: https://fuchsia-review.googlesource.com/c/fidl-misc/+/395996
Reviewed-by: Benjamin Prosnitz <bprosnitz@google.com>
Reviewed-by: Pascal Perez <pascallouis@google.com>
diff --git a/fidl-lsp/analysis/compile.go b/fidl-lsp/analysis/compile.go
index 4084400..885c9a5 100644
--- a/fidl-lsp/analysis/compile.go
+++ b/fidl-lsp/analysis/compile.go
@@ -10,6 +10,7 @@
 	"fmt"
 	"io/ioutil"
 	"os/exec"
+	"strconv"
 	"strings"
 
 	fidlcommon "fidl-lsp/third_party/common"
@@ -121,168 +122,200 @@
 	return nil
 }
 
-type symbolKind string
-
-const (
-	bitsKind      symbolKind = "bits"
-	constKind                = "const"
-	enumKind                 = "enum"
-	protocolKind             = "protocol"
-	serviceKind              = "service"
-	structKind               = "struct"
-	tableKind                = "table"
-	unionKind                = "union"
-	typeAliasKind            = "typeAlias"
-)
-
-type symbolInfo struct {
-	lib                string
-	name               string
-	definition         state.Location
-	attrs              []attribute
-	kind               symbolKind
-	isMember           bool
-	isMethod           bool
-	typeInfo           interface{}
-	maybeFromTypeAlias typeCtor
-}
-
 type symbolMap map[string]*symbolInfo
 
 func (a *Analyzer) genSymbolMap(l FidlLibrary) (symbolMap, error) {
-	// TODO: skip SomeLongAnonymousPrefix* structs? (are we double counting method request/response params?)
-	// TODO: how to handle const literals?
-
 	sm := make(symbolMap)
 
 	for _, d := range l.BitsDecls {
 		sm[d.Name] = &symbolInfo{
-			lib:                l.Name,
-			name:               d.Name,
-			definition:         a.fidlLocToStateLoc(d.Loc),
-			kind:               bitsKind,
-			typeInfo:           d.Type,
-			maybeFromTypeAlias: d.FromTypeAlias,
-			attrs:              d.Attrs,
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   BitsKind,
+					Bits: &BitsTypeInfo{
+						Type: d.Type.Type(),
+					},
+				},
+				Attrs: d.Attrs,
+			},
 		}
-		a.addMembersToSymbolMap(sm, l.Name, d.Name, bitsKind, d.Members)
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
 	}
 	for _, d := range l.ConstDecls {
 		sm[d.Name] = &symbolInfo{
-			lib:                l.Name,
-			name:               d.Name,
-			definition:         a.fidlLocToStateLoc(d.Loc),
-			kind:               constKind,
-			typeInfo:           d.Type,
-			maybeFromTypeAlias: d.FromTypeAlias,
-			attrs:              d.Attrs,
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   ConstKind,
+					Const: &ConstTypeInfo{
+						Type:  d.Type.Type(),
+						Value: d.Value.Value,
+					},
+				},
+				Attrs: d.Attrs,
+			},
 		}
 	}
 	for _, d := range l.EnumDecls {
 		sm[d.Name] = &symbolInfo{
-			lib:                l.Name,
-			name:               d.Name,
-			definition:         a.fidlLocToStateLoc(d.Loc),
-			kind:               enumKind,
-			typeInfo:           d.Type,
-			maybeFromTypeAlias: d.FromTypeAlias,
-			attrs:              d.Attrs,
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   EnumKind,
+					Enum: &EnumTypeInfo{
+						Type: PrimitiveSubtype(d.Type),
+					},
+				},
+				Attrs: d.Attrs,
+			},
 		}
-		a.addMembersToSymbolMap(sm, l.Name, d.Name, enumKind, d.Members)
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
 	}
 	for _, d := range l.ProtocolDecls {
 		sm[d.Name] = &symbolInfo{
-			lib:        l.Name,
 			name:       d.Name,
 			definition: a.fidlLocToStateLoc(d.Loc),
-			kind:       protocolKind,
-			attrs:      d.Attrs,
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   ProtocolKind,
+				},
+				Attrs: d.Attrs,
+			},
 		}
 		for _, m := range d.Methods {
 			methodName := fmt.Sprintf("%s.%s", d.Name, m.Name)
 			sm[methodName] = &symbolInfo{
-				lib:        l.Name,
-				name:       methodName,
+				name:       m.Name,
 				definition: a.fidlLocToStateLoc(m.Loc),
-				kind:       protocolKind,
-				isMethod:   true,
-				attrs:      m.Attrs,
+				typeInfo: Type{
+					IsMethod: true,
+					Attrs:    m.Attrs,
+				},
 			}
 			if len(m.MaybeRequest) > 0 {
-				a.addMembersToSymbolMap(sm, l.Name, methodName, protocolKind, m.MaybeRequest)
+				a.addMembersToSymbolMap(sm, methodName, m.MaybeRequest)
 			}
 			if len(m.MaybeResponse) > 0 {
-				a.addMembersToSymbolMap(sm, l.Name, methodName, protocolKind, m.MaybeResponse)
+				a.addMembersToSymbolMap(sm, methodName, m.MaybeResponse)
 			}
 		}
 	}
 	for _, d := range l.ServiceDecls {
 		sm[d.Name] = &symbolInfo{
-			lib:        l.Name,
 			name:       d.Name,
 			definition: a.fidlLocToStateLoc(d.Loc),
-			kind:       serviceKind,
-			attrs:      d.Attrs,
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   ServiceKind,
+				},
+				Attrs: d.Attrs,
+			},
 		}
-		a.addMembersToSymbolMap(sm, l.Name, d.Name, serviceKind, d.Members)
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
 	}
 	for _, d := range l.StructDecls {
+		// Skip anonymous structs, since we add them to the symbol map as method
+		// parameter structs
+		if d.Anonymous {
+			continue
+		}
 		sm[d.Name] = &symbolInfo{
-			lib:        l.Name,
 			name:       d.Name,
 			definition: a.fidlLocToStateLoc(d.Loc),
-			kind:       structKind,
-			attrs:      d.Attrs,
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   StructKind,
+				},
+				Attrs: d.Attrs,
+			},
 		}
-		a.addMembersToSymbolMap(sm, l.Name, d.Name, structKind, d.Members)
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
 	}
 	for _, d := range l.TableDecls {
 		sm[d.Name] = &symbolInfo{
-			lib:        l.Name,
 			name:       d.Name,
 			definition: a.fidlLocToStateLoc(d.Loc),
-			kind:       tableKind,
-			attrs:      d.Attrs,
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   TableKind,
+				},
+				Attrs: d.Attrs,
+			},
 		}
-		a.addMembersToSymbolMap(sm, l.Name, d.Name, tableKind, d.Members)
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
 	}
 	for _, d := range l.UnionDecls {
 		sm[d.Name] = &symbolInfo{
-			lib:        l.Name,
 			name:       d.Name,
 			definition: a.fidlLocToStateLoc(d.Loc),
-			kind:       unionKind,
-			attrs:      d.Attrs,
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   UnionKind,
+				},
+				Attrs: d.Attrs,
+			},
 		}
-		a.addMembersToSymbolMap(sm, l.Name, d.Name, unionKind, d.Members)
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
 	}
 	for _, d := range l.TypeAliasDecls {
 		sm[d.Name] = &symbolInfo{
-			lib:        l.Name,
 			name:       d.Name,
 			definition: a.fidlLocToStateLoc(d.Loc),
-			kind:       typeAliasKind,
-			typeInfo:   d.TypeCtor,
-			attrs:      d.Attrs,
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   TypeAliasKind,
+					TypeAlias: &TypeAliasTypeInfo{
+						Type: d.TypeCtor.Type(),
+					},
+				},
+				Attrs: d.Attrs,
+			},
 		}
 	}
 
 	return sm, nil
 }
 
-func (a *Analyzer) addMembersToSymbolMap(sm symbolMap, libName string, declName string, kind symbolKind, members []member) {
+func (a *Analyzer) addMembersToSymbolMap(sm symbolMap, declName string, members []member) {
 	for _, m := range members {
 		memberName := fmt.Sprintf("%s.%s", declName, m.Name)
 		sm[memberName] = &symbolInfo{
-			lib:                libName,
-			name:               memberName,
-			definition:         a.fidlLocToStateLoc(m.Loc),
-			kind:               kind,
-			isMember:           true,
-			typeInfo:           m.Type,
-			maybeFromTypeAlias: m.FromTypeAlias,
-			attrs:              m.Attrs,
+			name:       m.Name,
+			definition: a.fidlLocToStateLoc(m.Loc),
+			typeInfo:   m.Type.Type(),
 		}
+		sm[memberName].typeInfo.Attrs = m.Attrs
 	}
 }
 
@@ -297,7 +330,7 @@
 		//
 		// The `Filename` in a decl's location is recorded relative to where
 		// fidlc was invoked. All the locations in CompiledLibraries are
-		// prepended with "../../" which can be replaced by $FUCHSIA_DIR/.
+		// prepended with "../../" which can be replaced by the `BuildRootDir`.
 		fileID = state.FileID(strings.Replace(loc.Filename, "../..", a.cfg.BuildRootDir, 1))
 	}
 	return state.Location{
@@ -308,3 +341,138 @@
 		},
 	}
 }
+
+func (d declType) Type() Type {
+	t := Type{Kind: d.Kind}
+
+	switch d.Kind {
+	default:
+		// Sometimes this will be called with a zeroed declType, e.g. for bits
+		// and enum members, which don't have types.
+		return t
+	case ArrayType:
+		t.Array = &ArrayTypeInfo{
+			ElementType:  d.ElementType.Type(),
+			ElementCount: d.ElementCount,
+		}
+	case VectorType:
+		t.Vector = &VectorTypeInfo{
+			ElementType:  d.ElementType.Type(),
+			ElementCount: d.MaybeElementCount,
+			Nullable:     d.Nullable,
+		}
+	case StringType:
+		t.String = &StringTypeInfo{
+			ElementCount: d.MaybeElementCount,
+			Nullable:     d.Nullable,
+		}
+	case HandleType:
+		t.Handle = &HandleTypeInfo{
+			Subtype:  HandleSubtype(d.Subtype),
+			Rights:   d.Rights,
+			Nullable: d.Nullable,
+		}
+	case RequestType:
+		t.Request = &RequestTypeInfo{
+			Subtype:  d.Subtype,
+			Nullable: d.Nullable,
+		}
+	case PrimitiveType:
+		t.Primitive = &PrimitiveTypeInfo{
+			Subtype: PrimitiveSubtype(d.Subtype),
+		}
+	case IdentifierType:
+		t.Identifier = &IdentifierTypeInfo{
+			Identifier: d.Identifier,
+			Nullable:   d.Nullable,
+		}
+	}
+
+	return t
+}
+
+func (t typeCtor) Type() Type {
+	switch t.Name {
+	case string(ArrayType):
+		ty := Type{
+			Kind: ArrayType,
+			Array: &ArrayTypeInfo{
+				ElementType: t.Args[0].Type(),
+			},
+		}
+		if count, err := strconv.ParseUint(t.Size.Value, 10, 32); err == nil {
+			ty.Array.ElementCount = uint(count)
+		}
+		return ty
+
+	case string(VectorType):
+		ty := Type{
+			Kind: VectorType,
+			Vector: &VectorTypeInfo{
+				ElementType: t.Args[0].Type(),
+				Nullable:    t.Nullable,
+			},
+		}
+		if count, err := strconv.ParseUint(t.Size.Value, 10, 32); err == nil {
+			countUint := uint(count)
+			ty.Vector.ElementCount = &countUint
+		}
+		return ty
+
+	case string(StringType):
+		ty := Type{
+			Kind: StringType,
+			String: &StringTypeInfo{
+				Nullable: t.Nullable,
+			},
+		}
+		if count, err := strconv.ParseUint(t.Size.Value, 10, 32); err == nil {
+			countUint := uint(count)
+			ty.String.ElementCount = &countUint
+		}
+		return ty
+
+	case string(HandleType):
+		ty := Type{
+			Kind: HandleType,
+			Handle: &HandleTypeInfo{
+				Nullable: t.Nullable,
+			},
+		}
+		if t.HandleSubtype != nil {
+			ty.Handle.Subtype = *t.HandleSubtype
+		}
+		return ty
+
+	case string(RequestType):
+		return Type{
+			Kind: RequestType,
+			Request: &RequestTypeInfo{
+				Subtype:  t.Args[0].Name,
+				Nullable: t.Nullable,
+			},
+		}
+
+	case string(Bool), Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64, Float32, Float64:
+		return Type{
+			Kind: PrimitiveType,
+			Primitive: &PrimitiveTypeInfo{
+				Subtype: PrimitiveSubtype(t.Name),
+			},
+		}
+
+	default:
+		// We assume it is the name of an identifier type.
+		return Type{
+			Kind: IdentifierType,
+			Identifier: &IdentifierTypeInfo{
+				Identifier: t.Name,
+				Nullable:   t.Nullable,
+			},
+		}
+		// Nullable
+		// Args
+		// Size
+		// HandleSubtype
+	}
+}
diff --git a/fidl-lsp/analysis/definition.go b/fidl-lsp/analysis/definition.go
index 3063f59..1b386b8 100644
--- a/fidl-lsp/analysis/definition.go
+++ b/fidl-lsp/analysis/definition.go
@@ -7,7 +7,6 @@
 import (
 	"fmt"
 	"io/ioutil"
-	"strings"
 
 	fidlcommon "fidl-lsp/third_party/common"
 
@@ -55,31 +54,9 @@
 		)
 	}
 
-	lib, ok := a.libs[name.LibraryName()]
-	if !ok {
-		return nil, fmt.Errorf("unknown library `%s`", name.LibraryName().FullyQualifiedName())
-	}
-	if lib.ir == nil {
-		if err := a.importLibrary(lib.json); err != nil {
-			return nil, fmt.Errorf(
-				"error importing library `%s`: %s",
-				name.LibraryName().FullyQualifiedName(),
-				err,
-			)
-		}
-	}
-
-	// Check that we have a symbolMap for this library, and look up this
-	// symbol's definition location in that symbolMap.
-	if lib.symbols == nil {
-		return nil, fmt.Errorf(
-			"no symbol map for library `%s`",
-			name.LibraryName().FullyQualifiedName(),
-		)
-	}
-	symInfo, ok := lib.symbols[name.FullyQualifiedName()]
-	if !ok {
-		return nil, fmt.Errorf("could not find definition of symbol `%s`", name.FullyQualifiedName())
+	symInfo, err := a.lookupSymbolInfo(name)
+	if err != nil {
+		return nil, fmt.Errorf("could not find definition of symbol `%s`: %s", name.FullyQualifiedName(), err)
 	}
 
 	return []state.Location{symInfo.definition}, nil
@@ -120,35 +97,3 @@
 	}
 	return locs
 }
-
-func (a *Analyzer) symbolToFullyQualifiedName(fs *state.FileSystem, sym state.Symbol) (fidlcommon.Name, error) {
-	// If `sym` is a local name (not fully-qualified), we create a FQN by
-	// attaching its library name.
-	var fqn string
-	if !strings.Contains(sym.Name, ".") {
-		file, err := fs.File(sym.Location.FileID)
-		if err != nil {
-			return fidlcommon.Name{}, fmt.Errorf("could not open file `%s`", sym.Location.FileID)
-		}
-		libName, err := state.LibraryOfFile(file)
-		if err != nil {
-			return fidlcommon.Name{}, fmt.Errorf(
-				"could not find library of symbol `%s` in file `%s`",
-				sym.Name,
-				sym.Location.FileID,
-			)
-		}
-		fqn = libName.FullyQualifiedName() + "/" + sym.Name
-	} else {
-		// If the symbol contains '.', we assume it is a fully-qualified name.
-		i := strings.LastIndex(sym.Name, ".")
-		fqn = sym.Name[:i] + "/" + sym.Name[i+1:]
-	}
-
-	// Convert `fqn` to a fidlcommon.Name.
-	name, err := fidlcommon.ReadName(fqn)
-	if err != nil {
-		return fidlcommon.Name{}, fmt.Errorf("could not read fully-qualified name `%s`: %s", fqn, err)
-	}
-	return name, nil
-}
diff --git a/fidl-lsp/analysis/library.go b/fidl-lsp/analysis/library.go
index fbde7ef..4fc9480 100644
--- a/fidl-lsp/analysis/library.go
+++ b/fidl-lsp/analysis/library.go
@@ -18,7 +18,7 @@
 	Line, Column, Length int
 }
 
-type attribute struct {
+type Attribute struct {
 	Name  string
 	Value string
 }
@@ -35,22 +35,34 @@
 	IdentifierType          = "identifier"
 )
 
-type elementType struct {
-	Kind       string
-	Identifier string `json:"identifier,omitempty"`
-	Subtype    string `json:"subtype,omitempty"`
+type typeCtor struct {
+	Name          string
+	Args          []typeCtor
+	Nullable      bool
+	Size          *constValue    `json:"maybe_size,omitempty"`
+	HandleSubtype *HandleSubtype `json:"maybe_handle_subtype,omitempty"`
 }
 
-type typeCtor struct {
-	Name string
-	Args []typeCtor
+type constValue struct {
+	Value string
 }
 
 type declType struct {
-	Kind        TypeKind
-	ElementType elementType `json:"element_type,omitempty"`
-	Identifier  string      `json:"identifier,omitempty"`
-	Subtype     string      `json:"subtype,omitempty"`
+	Kind TypeKind
+
+	// Array
+	ElementType  *declType `json:"element_type,omitempty"`
+	ElementCount uint      `json:"element_count,omitempty"`
+
+	// Vector
+	MaybeElementCount *uint `json:"maybe_element_count,omitempty"`
+	Nullable          bool  `json:"nullable,omitempty"`
+
+	// Handle
+	Subtype string `json:"subtype,omitempty"`
+	Rights  uint   `json:"rights,omitempty"`
+
+	Identifier string `json:"identifier,omitempty"`
 }
 
 type member struct {
@@ -58,7 +70,7 @@
 	Loc           location    `json:"location"`
 	Type          declType    `json:"type,omitempty"`
 	FromTypeAlias typeCtor    `json:"experimental_maybe_from_type_alias,omitempty"`
-	Attrs         []attribute `json:"maybe_attributes,omitempty"`
+	Attrs         []Attribute `json:"maybe_attributes,omitempty"`
 }
 
 type method struct {
@@ -66,7 +78,7 @@
 	Loc           location `json:"location"`
 	MaybeRequest  []member
 	MaybeResponse []member
-	Attrs         []attribute `json:"maybe_attributes,omitempty"`
+	Attrs         []Attribute `json:"maybe_attributes,omitempty"`
 }
 
 type bitsDecl struct {
@@ -74,7 +86,7 @@
 	Loc           location `json:"location"`
 	Type          declType
 	FromTypeAlias typeCtor    `json:"experimental_maybe_from_type_alias,omitempty"`
-	Attrs         []attribute `json:"maybe_attributes,omitempty"`
+	Attrs         []Attribute `json:"maybe_attributes,omitempty"`
 	Members       []member
 }
 
@@ -85,7 +97,7 @@
 	// a string
 	Type          string
 	FromTypeAlias typeCtor    `json:"experimental_maybe_from_type_alias,omitempty"`
-	Attrs         []attribute `json:"maybe_attributes,omitempty"`
+	Attrs         []Attribute `json:"maybe_attributes,omitempty"`
 	Members       []member
 }
 
@@ -93,50 +105,52 @@
 	Name          string
 	Loc           location `json:"location"`
 	Type          declType
+	Value         constValue  `json:"value"`
 	FromTypeAlias typeCtor    `json:"experimental_maybe_from_type_alias,omitempty"`
-	Attrs         []attribute `json:"maybe_attributes,omitempty"`
+	Attrs         []Attribute `json:"maybe_attributes,omitempty"`
 }
 
 type protocolDecl struct {
 	Name    string
 	Loc     location `json:"location"`
 	Methods []method
-	Attrs   []attribute `json:"maybe_attributes,omitempty"`
+	Attrs   []Attribute `json:"maybe_attributes,omitempty"`
 }
 
 type serviceDecl struct {
 	Name    string
 	Loc     location `json:"location"`
 	Members []member
-	Attrs   []attribute `json:"maybe_attributes,omitempty"`
+	Attrs   []Attribute `json:"maybe_attributes,omitempty"`
 }
 
 type structDecl struct {
-	Name    string
-	Loc     location `json:"location"`
-	Members []member
-	Attrs   []attribute `json:"maybe_attributes,omitempty"`
+	Name      string
+	Loc       location `json:"location"`
+	Members   []member
+	Attrs     []Attribute `json:"maybe_attributes,omitempty"`
+	Anonymous bool
 }
 
 type tableDecl struct {
 	Name    string
 	Loc     location `json:"location"`
 	Members []member
-	Attrs   []attribute `json:"maybe_attributes,omitempty"`
+	Attrs   []Attribute `json:"maybe_attributes,omitempty"`
 }
 
 type unionDecl struct {
 	Name    string
 	Loc     location `json:"location"`
 	Members []member
-	Attrs   []attribute `json:"maybe_attributes,omitempty"`
+	Attrs   []Attribute `json:"maybe_attributes,omitempty"`
 }
 
 type typeAliasDecl struct {
 	Name     string
 	Loc      location    `json:"location"`
 	TypeCtor typeCtor    `json:"partial_type_ctor"`
-	Attrs    []attribute `json:"maybe_attributes,omitempty"`
+	Attrs    []Attribute `json:"maybe_attributes,omitempty"`
 }
 
 // TODO: rename to FidlJSONIR, or JSONLibrary?
diff --git a/fidl-lsp/analysis/symbol.go b/fidl-lsp/analysis/symbol.go
new file mode 100644
index 0000000..eefcf29
--- /dev/null
+++ b/fidl-lsp/analysis/symbol.go
@@ -0,0 +1,248 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"fmt"
+	"strings"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+type symbolInfo struct {
+	name       string
+	definition state.Location
+	typeInfo   Type
+}
+
+type Type struct {
+	// If IsLib is true, other fields are empty.
+	// TODO: is there a better way to represent this?
+	IsLib bool
+	// If IsMethod is true, only Attrs is set.
+	IsMethod bool
+
+	Attrs []Attribute
+
+	Kind TypeKind
+
+	Array      *ArrayTypeInfo
+	Vector     *VectorTypeInfo
+	String     *StringTypeInfo
+	Handle     *HandleTypeInfo
+	Request    *RequestTypeInfo
+	Primitive  *PrimitiveTypeInfo
+	Identifier *IdentifierTypeInfo
+}
+
+type ArrayTypeInfo struct {
+	ElementType  Type
+	ElementCount uint
+}
+
+type VectorTypeInfo struct {
+	ElementType  Type
+	ElementCount *uint
+	Nullable     bool
+}
+
+type StringTypeInfo struct {
+	ElementCount *uint
+	Nullable     bool
+}
+
+type HandleTypeInfo struct {
+	Subtype  HandleSubtype
+	Rights   uint
+	Nullable bool
+}
+
+type HandleSubtype string
+
+const (
+	Handle       HandleSubtype = "handle"
+	Bti                        = "bti"
+	Channel                    = "channel"
+	Clock                      = "clock"
+	DebugLog                   = "debuglog"
+	Event                      = "event"
+	Eventpair                  = "eventpair"
+	Exception                  = "exception"
+	Fifo                       = "fifo"
+	Guest                      = "guest"
+	Interrupt                  = "interrupt"
+	Iommu                      = "iommu"
+	Job                        = "job"
+	Pager                      = "pager"
+	PciDevice                  = "pcidevice"
+	Pmt                        = "pmt"
+	Port                       = "port"
+	Process                    = "process"
+	Profile                    = "profile"
+	Resource                   = "resource"
+	Socket                     = "socket"
+	Stream                     = "stream"
+	SuspendToken               = "suspendtoken"
+	Thread                     = "thread"
+	Time                       = "timer"
+	Vcpu                       = "vcpu"
+	Vmar                       = "vmar"
+	Vmo                        = "vmo"
+)
+
+type RequestTypeInfo struct {
+	Subtype  string
+	Nullable bool
+}
+
+type PrimitiveTypeInfo struct {
+	Subtype PrimitiveSubtype
+}
+
+type PrimitiveSubtype string
+
+const (
+	Bool    PrimitiveSubtype = "bool"
+	Int8                     = "int8"
+	Int16                    = "int16"
+	Int32                    = "int32"
+	Int64                    = "int64"
+	Uint8                    = "uint8"
+	Uint16                   = "uint16"
+	Uint32                   = "uint32"
+	Uint64                   = "uint64"
+	Float32                  = "float32"
+	Float64                  = "float64"
+)
+
+// IdentifierTypeInfo is not a perfect analog for `identifier-type` from the
+// JSON IR, because it serves as the type for both declarations of identifier
+// types and values of identifier types.
+//
+// For example, if you have declared a struct Foo, and a protocol method that
+// takes a Foo, IdentifierTypeInfo will hold the same type information for each
+// of the following symbols:
+//
+//     struct Foo {};
+//            ~~~
+//     protocol P {
+//         Method(Foo foo);
+//                ~~~ ~~~
+//     };
+//
+// So it is both a kind of "declaration type" as well as an "identifier type".
+// Iff `IsDecl` == true, it is a "declaration type"; otherwise, it is an
+// "identifier type", and `Identifier` is the name of a declaration type that
+// can be looked up in the symbol map.
+type IdentifierTypeInfo struct {
+	// If IsDecl is true, all the type information is contained in the tagged
+	// type info objects -- Kind and one of {Bits, Const, etc.}.
+	// If IsDecl is false, Identifier and Nullable are set, and Identifier is
+	// a key to the declaration of the identifier type, in the symbolMap.
+	IsDecl     bool
+	Identifier string
+	Nullable   bool
+
+	Kind IdentifierKind
+	Name string
+
+	Bits      *BitsTypeInfo
+	Const     *ConstTypeInfo
+	Enum      *EnumTypeInfo
+	TypeAlias *TypeAliasTypeInfo
+}
+
+type IdentifierKind string
+
+const (
+	BitsKind      IdentifierKind = "bits"
+	ConstKind                    = "const"
+	EnumKind                     = "enum"
+	ProtocolKind                 = "protocol"
+	ServiceKind                  = "service"
+	StructKind                   = "struct"
+	TableKind                    = "table"
+	UnionKind                    = "union"
+	TypeAliasKind                = "typeAlias"
+)
+
+type BitsTypeInfo struct {
+	Type Type
+}
+type ConstTypeInfo struct {
+	Type  Type
+	Value string
+}
+type EnumTypeInfo struct {
+	Type PrimitiveSubtype
+}
+type TypeAliasTypeInfo struct {
+	Type Type
+}
+
+func (a *Analyzer) symbolToFullyQualifiedName(fs *state.FileSystem, sym state.Symbol) (fidlcommon.Name, error) {
+	// If `sym` is a local name (not fully-qualified), we create a FQN by
+	// attaching its library name.
+	var fqn string
+	if !strings.Contains(sym.Name, ".") {
+		file, err := fs.File(sym.Location.FileID)
+		if err != nil {
+			return fidlcommon.Name{}, fmt.Errorf("could not open file `%s`", sym.Location.FileID)
+		}
+		libName, err := state.LibraryOfFile(file)
+		if err != nil {
+			return fidlcommon.Name{}, fmt.Errorf(
+				"could not find library of symbol `%s` in file `%s`",
+				sym.Name,
+				sym.Location.FileID,
+			)
+		}
+		fqn = libName.FullyQualifiedName() + "/" + sym.Name
+	} else {
+		// If the symbol contains '.', we assume it is a fully-qualified name.
+		i := strings.LastIndex(sym.Name, ".")
+		fqn = sym.Name[:i] + "/" + sym.Name[i+1:]
+	}
+
+	// Convert `fqn` to a fidlcommon.Name.
+	name, err := fidlcommon.ReadName(fqn)
+	if err != nil {
+		return fidlcommon.Name{}, fmt.Errorf("could not read fully-qualified name `%s`: %s", fqn, err)
+	}
+	return name, nil
+}
+
+func (a *Analyzer) lookupSymbolInfo(name fidlcommon.Name) (*symbolInfo, error) {
+	lib, ok := a.libs[name.LibraryName()]
+	if !ok {
+		return nil, fmt.Errorf("unknown library `%s`", name.LibraryName().FullyQualifiedName())
+	}
+	if lib.ir == nil {
+		if err := a.importLibrary(lib.json); err != nil {
+			return nil, fmt.Errorf(
+				"error importing library `%s`: %s",
+				name.LibraryName().FullyQualifiedName(),
+				err,
+			)
+		}
+	}
+
+	// Check that we have a symbolMap for this library, and look up this
+	// symbol's definition location in that symbolMap.
+	if lib.symbols == nil {
+		return nil, fmt.Errorf(
+			"no symbol map for library `%s`",
+			name.LibraryName().FullyQualifiedName(),
+		)
+	}
+	symInfo, ok := lib.symbols[name.FullyQualifiedName()]
+	if !ok {
+		return nil, fmt.Errorf("could not find symbol `%s`", name.FullyQualifiedName())
+	}
+
+	return symInfo, nil
+}
diff --git a/fidl-lsp/analysis/type.go b/fidl-lsp/analysis/type.go
new file mode 100644
index 0000000..9ae2465
--- /dev/null
+++ b/fidl-lsp/analysis/type.go
@@ -0,0 +1,120 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"fmt"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+// TypeOfSymbol returns the type information of `sym`.
+//
+// analysis.Type contains all the information needed to create a human-readable
+// description of the type.
+func (a *Analyzer) TypeOfSymbol(fs *state.FileSystem, sym state.Symbol) (Type, error) {
+	// If `sym` is a library name, return a `library`-kinded TypeInfo.
+	libName, err := fidlcommon.ReadLibraryName(sym.Name)
+	if err == nil {
+		if _, isLib := a.libs[libName]; isLib {
+			return Type{
+				IsLib: true,
+			}, nil
+		}
+	}
+
+	// TODO: if we want to support hovering over members (e.g. struct fields,
+	// method parameters, bits members) or protocol methods, we need to check
+	// here whether the symbol is namespaced -- whether it is inside a
+	// declaration.
+	// If it is, prepend it with that declaration's name. For example, this
+	// struct field:
+	//
+	//     struct Foo {
+	//         MyType my_field;
+	//                ~~~~~~~~
+	//     }
+	//
+	// Would become "library.name/Foo.my_field", as this is how it's stored in
+	// the symbol map.
+
+	// Otherwise, we assume it is a local or fully-qualified name
+	name, err := a.symbolToFullyQualifiedName(fs, sym)
+	if err != nil {
+		return Type{}, fmt.Errorf(
+			"could not convert symbol `%s` to fully-qualified name: %s",
+			sym.Name,
+			err,
+		)
+	}
+
+	symInfo, err := a.lookupSymbolInfo(name)
+	if err != nil {
+		return Type{}, fmt.Errorf("could not find type of symbol `%s`: %s", name.FullyQualifiedName(), err)
+	}
+
+	// Resolve identifier type, if necessary.
+	if symInfo.typeInfo.Kind == IdentifierType && !symInfo.typeInfo.Identifier.IsDecl {
+		// This means that rather than being a declaration, symInfo is a value
+		// of an identifier type, so we lookup that type's info based on the
+		// type name.
+		typeName, err := fidlcommon.ReadName(symInfo.typeInfo.Identifier.Identifier)
+		if err != nil {
+			return Type{}, fmt.Errorf(
+				"invalid identifier type `%s`: %s",
+				symInfo.typeInfo.Identifier.Identifier,
+				err,
+			)
+		}
+		t, err := a.lookupSymbolInfo(typeName)
+		if err != nil {
+			return Type{}, fmt.Errorf(
+				"could not find identifier type `%s` of symbol `%s`: %s",
+				typeName.FullyQualifiedName(),
+				name.FullyQualifiedName(),
+				err,
+			)
+		}
+		identifierType := t.typeInfo
+		identifierType.Identifier.IsDecl = false
+		identifierType.Identifier.Nullable = symInfo.typeInfo.Identifier.Nullable
+		identifierType.Identifier.Identifier = symInfo.typeInfo.Identifier.Identifier
+		return identifierType, nil
+	}
+
+	// Resolve aliased identifier type, if necessary.
+	if symInfo.typeInfo.Kind == IdentifierType &&
+		symInfo.typeInfo.Identifier.Kind == TypeAliasKind &&
+		symInfo.typeInfo.Identifier.TypeAlias.Type.Kind == IdentifierType {
+		// This means that `sym` is a type alias to an identifier type, so we
+		// need to lookup that identifier type's info based on the type name.
+		aliasedType := symInfo.typeInfo.Identifier.TypeAlias.Type
+		typeName, err := fidlcommon.ReadName(aliasedType.Identifier.Identifier)
+		if err != nil {
+			return Type{}, fmt.Errorf(
+				"invalid identifier type `%s`: %s",
+				aliasedType.Identifier.Identifier,
+				err,
+			)
+		}
+		t, err := a.lookupSymbolInfo(typeName)
+		if err != nil {
+			return Type{}, fmt.Errorf(
+				"could not find identifier type `%s` of symbol `%s`: %s",
+				typeName.FullyQualifiedName(),
+				name.FullyQualifiedName(),
+				err,
+			)
+		}
+		identifierType := t.typeInfo
+		identifierType.Identifier.Nullable = aliasedType.Identifier.Nullable
+		identifierType.Identifier.Identifier = aliasedType.Identifier.Identifier
+		symInfo.typeInfo.Identifier.TypeAlias.Type = identifierType
+	}
+
+	return symInfo.typeInfo, nil
+}
diff --git a/fidl-lsp/langserver/handler.go b/fidl-lsp/langserver/handler.go
index f688369..18a6d4b 100644
--- a/fidl-lsp/langserver/handler.go
+++ b/fidl-lsp/langserver/handler.go
@@ -69,6 +69,7 @@
 			DefinitionProvider:         true,
 			DocumentFormattingProvider: true,
 			DocumentLinkProvider:       &documentLinkOptions{},
+			HoverProvider:              true,
 			TextDocumentSync: &lsp.TextDocumentSyncOptionsOrKind{
 				Options: &lsp.TextDocumentSyncOptions{
 					OpenClose: true,
@@ -260,6 +261,17 @@
 
 		return h.handleFormat(params)
 
+	case "textDocument/hover":
+		if req.Params == nil {
+			return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+		}
+		var params lsp.TextDocumentPositionParams
+		if err := json.Unmarshal(*req.Params, &params); err != nil {
+			return nil, err
+		}
+
+		return h.handleHover(params)
+
 	default:
 		return nil, &jsonrpc2.Error{
 			Code:    jsonrpc2.CodeMethodNotFound,
diff --git a/fidl-lsp/langserver/hover.go b/fidl-lsp/langserver/hover.go
new file mode 100644
index 0000000..5977b4c
--- /dev/null
+++ b/fidl-lsp/langserver/hover.go
@@ -0,0 +1,318 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"fmt"
+
+	"github.com/sourcegraph/go-lsp"
+
+	"fidl-lsp/analysis"
+	"fidl-lsp/state"
+)
+
+// handleHover asks the FileSystem for the symbol at the location specified in
+// `params`, and then asks the Analyzer for the type of that symbol.
+//
+// It then converts that type to a human-readable string representation that
+// will be displayed as a tooltip, including doc comments, other attributes on
+// the type, and the type itself. For example, given this FIDL:
+//
+//     library example;
+//
+//     /// Foo is a struct.
+//     [Attribute = "Value"]
+//     struct Foo {};
+//
+//     protocol Bar {
+//         Method(Foo foo);
+//                ~~~
+//     }
+//
+// Hovering over the `~~~` would give you this tooltip:
+//
+//     Foo is a struct.       // In plain text
+//     [Attribute = "Value"]  // In monospace font
+//     struct example/Foo
+//
+func (h *LangHandler) handleHover(params lsp.TextDocumentPositionParams) (lsp.Hover, error) {
+	sym, err := h.fs.SymbolAtPos(
+		state.FileID(params.TextDocument.URI),
+		state.Position{Line: params.Position.Line, Character: params.Position.Character},
+	)
+	if err != nil {
+		h.log.Printf(
+			"could not find symbol at position `%#v` in document `%s`\n",
+			params.Position,
+			params.TextDocument.URI,
+		)
+		return lsp.Hover{}, err
+	}
+
+	symType, err := h.analyzer.TypeOfSymbol(h.fs, sym)
+	if err != nil {
+		h.log.Printf("could not get type of symbol `%s`: %s", sym.Name, err)
+		return lsp.Hover{}, err
+	}
+
+	// Convert SymbolType --> []lsp.MarkedString
+	typeDescription, err := symbolTypeToMarkedStrings(sym.Name, symType)
+	if err != nil {
+		h.log.Printf("could not get string representation of symbol `%s`'s type: %s", sym.Name, err)
+		return lsp.Hover{}, err
+	}
+
+	return lsp.Hover{
+		Contents: typeDescription,
+		Range: &lsp.Range{
+			Start: lsp.Position{
+				Line:      sym.Location.Range.Start.Line,
+				Character: sym.Location.Range.Start.Character,
+			},
+			End: lsp.Position{
+				Line:      sym.Location.Range.End.Line,
+				Character: sym.Location.Range.End.Character,
+			},
+		},
+	}, nil
+}
+
+func newFIDLString(s string) lsp.MarkedString {
+	return lsp.MarkedString{
+		Language: "fidl",
+		Value:    s,
+	}
+}
+
+func symbolTypeToMarkedStrings(name string, symType analysis.Type) ([]lsp.MarkedString, error) {
+	if symType.IsLib {
+		return []lsp.MarkedString{{
+			Language: "fidl",
+			Value:    fmt.Sprintf("library %s", name),
+		}}, nil
+	}
+
+	res := []lsp.MarkedString{}
+
+	var docStr *lsp.MarkedString
+	attrList := "["
+	firstAttr := true
+	// Add attributes as a marked string
+	for _, attr := range symType.Attrs {
+		if attr.Name == "Doc" {
+			rawMarkedStr := lsp.RawMarkedString(attr.Value)
+			docStr = &rawMarkedStr
+		} else {
+			if !firstAttr {
+				attrList += ", "
+			}
+			firstAttr = false
+			if attr.Value != "" {
+				attrList += fmt.Sprintf("%s = \"%s\"", attr.Name, attr.Value)
+			} else {
+				attrList += attr.Name
+			}
+		}
+	}
+	attrList += "]"
+	// Prepend doc string if there is one
+	if docStr != nil {
+		res = append([]lsp.MarkedString{*docStr}, res...)
+	}
+	if !firstAttr {
+		res = append(res, lsp.MarkedString{
+			Language: "fidl",
+			Value:    attrList,
+		})
+	}
+
+	switch symType.Kind {
+	default:
+		return nil, fmt.Errorf("unknown kind of symbol: %s", symType.Kind)
+
+	case analysis.ArrayType:
+		if symType.Array == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.VectorType:
+		if symType.Vector == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.StringType:
+		if symType.String == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.HandleType:
+		if symType.Handle == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.RequestType:
+		if symType.Request == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.PrimitiveType:
+		if symType.Primitive == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.IdentifierType:
+		if symType.Identifier == nil {
+			goto TypeInfoNotSet
+		}
+
+		if !symType.Identifier.IsDecl {
+			res = append(res, newFIDLString(shortName(symType)))
+			break
+		}
+
+		switch symType.Identifier.Kind {
+		default:
+			return nil, fmt.Errorf("unknown identifier kind: %s", symType.Identifier.Kind)
+
+		case analysis.BitsKind:
+			if symType.Identifier.Bits == nil {
+				goto IdentifierTypeInfoNotSet
+			}
+			res = append(res, newFIDLString(
+				fmt.Sprintf(
+					"bits %s: %s",
+					symType.Identifier.Name,
+					shortName(symType.Identifier.Bits.Type),
+				),
+			))
+
+		case analysis.EnumKind:
+			if symType.Identifier.Enum == nil {
+				goto IdentifierTypeInfoNotSet
+			}
+			res = append(res, newFIDLString(
+				fmt.Sprintf(
+					"enum %s: %s",
+					symType.Identifier.Name,
+					symType.Identifier.Enum.Type,
+				),
+			))
+
+		case analysis.ConstKind:
+			if symType.Identifier.Const == nil {
+				goto IdentifierTypeInfoNotSet
+			}
+			res = append(res, newFIDLString(
+				fmt.Sprintf(
+					"const %s %s = `%s`",
+					shortName(symType.Identifier.Const.Type),
+					symType.Identifier.Name,
+					symType.Identifier.Const.Value,
+				),
+			))
+
+		case analysis.TypeAliasKind:
+			if symType.Identifier.TypeAlias == nil {
+				goto IdentifierTypeInfoNotSet
+			}
+			res = append(res, newFIDLString(
+				fmt.Sprintf(
+					"%s: alias to %s",
+					symType.Identifier.Name,
+					shortName(symType.Identifier.TypeAlias.Type),
+				),
+			))
+
+		case analysis.ProtocolKind,
+			analysis.ServiceKind,
+			analysis.StructKind,
+			analysis.TableKind,
+			analysis.UnionKind:
+			res = append(res, newFIDLString(
+				fmt.Sprintf("%s %s", symType.Identifier.Kind, symType.Identifier.Name),
+			))
+		}
+	}
+
+	return res, nil
+
+TypeInfoNotSet:
+	return nil, fmt.Errorf(
+		"symbol is tagged as a `%s` but `%s` type info not set",
+		symType.Kind,
+		symType.Kind,
+	)
+IdentifierTypeInfoNotSet:
+	return nil, fmt.Errorf(
+		"symbol is tagged as a `%s` but `%s` type info not set",
+		symType.Identifier.Kind,
+		symType.Identifier.Kind,
+	)
+}
+
+func shortName(t analysis.Type) string {
+	switch t.Kind {
+	default:
+		return "unknown type"
+
+	case analysis.ArrayType:
+		return fmt.Sprintf("array<%s>:%d", shortName(t.Array.ElementType), t.Array.ElementCount)
+
+	case analysis.VectorType:
+		str := fmt.Sprintf("vector<%s>", shortName(t.Vector.ElementType))
+		if t.Vector.ElementCount != nil {
+			str += fmt.Sprintf(":%d", *t.Vector.ElementCount)
+		}
+		if t.Vector.Nullable {
+			str += "?"
+		}
+		return str
+
+	case analysis.StringType:
+		str := "string"
+		if t.String.ElementCount != nil {
+			str += fmt.Sprintf(":%d", *t.String.ElementCount)
+		}
+		if t.String.Nullable {
+			str += "?"
+		}
+		return str
+
+	case analysis.RequestType:
+		str := fmt.Sprintf("request<%s>", t.Request.Subtype)
+		if t.Request.Nullable {
+			str += "?"
+		}
+		return str
+
+	case analysis.HandleType:
+		str := fmt.Sprintf("handle<%s>", t.Handle.Subtype)
+		if t.Handle.Nullable {
+			str += "?"
+		}
+		return str
+
+	case analysis.PrimitiveType:
+		return string(t.Primitive.Subtype)
+
+	case analysis.IdentifierType:
+		var str string
+		if t.Identifier.IsDecl {
+			str = t.Identifier.Name
+		} else {
+			str = t.Identifier.Identifier
+		}
+		if t.Identifier.Nullable {
+			str += "?"
+		}
+		return str
+	}
+}
diff --git a/fidl-lsp/langserver/hover_test.go b/fidl-lsp/langserver/hover_test.go
new file mode 100644
index 0000000..7b0bf0a
--- /dev/null
+++ b/fidl-lsp/langserver/hover_test.go
@@ -0,0 +1,366 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"testing"
+
+	"github.com/sourcegraph/go-lsp"
+
+	"fidl-lsp/analysis"
+)
+
+func TestHover(t *testing.T) {
+	var elementCount uint = 10
+
+	cases := []struct {
+		symName   string
+		symType   analysis.Type
+		hoverText []lsp.MarkedString
+	}{
+		{
+			symName: "fuchsia.test",
+			symType: analysis.Type{
+				IsLib: true,
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("library fuchsia.test")},
+		},
+		{
+			symName: "array_value",
+			symType: analysis.Type{
+				Kind: analysis.ArrayType,
+				Array: &analysis.ArrayTypeInfo{
+					ElementType: analysis.Type{
+						Kind: analysis.PrimitiveType,
+						Primitive: &analysis.PrimitiveTypeInfo{
+							Subtype: analysis.Uint32,
+						},
+					},
+					ElementCount: 10,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("array<uint32>:10")},
+		},
+		{
+			symName: "vector_without_size_constraint",
+			symType: analysis.Type{
+				Kind: analysis.VectorType,
+				Vector: &analysis.VectorTypeInfo{
+					ElementType: analysis.Type{
+						Kind: analysis.PrimitiveType,
+						Primitive: &analysis.PrimitiveTypeInfo{
+							Subtype: analysis.Uint32,
+						},
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("vector<uint32>")},
+		},
+		{
+			symName: "nullable_vector_with_size_constraint",
+			symType: analysis.Type{
+				Kind: analysis.VectorType,
+				Vector: &analysis.VectorTypeInfo{
+					ElementType: analysis.Type{
+						Kind: analysis.PrimitiveType,
+						Primitive: &analysis.PrimitiveTypeInfo{
+							Subtype: analysis.Uint32,
+						},
+					},
+					ElementCount: &elementCount,
+					Nullable:     true,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("vector<uint32>:10?")},
+		},
+		{
+			symName: "nullable_string_without_size_constraint",
+			symType: analysis.Type{
+				Kind: analysis.StringType,
+				String: &analysis.StringTypeInfo{
+					Nullable: true,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("string?")},
+		},
+		{
+			symName: "string_with_size_constraint",
+			symType: analysis.Type{
+				Kind: analysis.StringType,
+				String: &analysis.StringTypeInfo{
+					ElementCount: &elementCount,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("string:10")},
+		},
+		{
+			symName: "handle",
+			symType: analysis.Type{
+				Kind: analysis.HandleType,
+				Handle: &analysis.HandleTypeInfo{
+					Subtype:  analysis.Vmo,
+					Nullable: true,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("handle<vmo>?")},
+		},
+		{
+			symName: "request",
+			symType: analysis.Type{
+				Kind: analysis.RequestType,
+				Request: &analysis.RequestTypeInfo{
+					Subtype: "File",
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("request<File>")},
+		},
+		{
+			symName: "primitive_bool",
+			symType: analysis.Type{
+				Kind: analysis.PrimitiveType,
+				Primitive: &analysis.PrimitiveTypeInfo{
+					Subtype: analysis.Bool,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("bool")},
+		},
+		{
+			symName: "primitive_uint64",
+			symType: analysis.Type{
+				Kind: analysis.PrimitiveType,
+				Primitive: &analysis.PrimitiveTypeInfo{
+					Subtype: analysis.Uint64,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("uint64")},
+		},
+		{
+			symName: "bits_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyBits",
+					Kind:   analysis.BitsKind,
+					Bits: &analysis.BitsTypeInfo{
+						Type: analysis.Type{
+							Kind: analysis.PrimitiveType,
+							Primitive: &analysis.PrimitiveTypeInfo{
+								Subtype: analysis.Uint32,
+							},
+						},
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("bits fuchsia.test/MyBits: uint32")},
+		},
+		{
+			symName: "enum_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyEnum",
+					Kind:   analysis.EnumKind,
+					Enum: &analysis.EnumTypeInfo{
+						Type: "string",
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("enum fuchsia.test/MyEnum: string")},
+		},
+		{
+			symName: "const_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyConst",
+					Kind:   analysis.ConstKind,
+					Const: &analysis.ConstTypeInfo{
+						Type: analysis.Type{
+							Kind:   analysis.StringType,
+							String: &analysis.StringTypeInfo{},
+						},
+						Value: "hello, world!",
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("const string fuchsia.test/MyConst = `hello, world!`")},
+		},
+		{
+			symName: "protocol_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyProtocol",
+					Kind:   analysis.ProtocolKind,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("protocol fuchsia.test/MyProtocol")},
+		},
+		{
+			symName: "service_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyService",
+					Kind:   analysis.ServiceKind,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("service fuchsia.test/MyService")},
+		},
+		{
+			symName: "struct_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyStruct",
+					Kind:   analysis.StructKind,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("struct fuchsia.test/MyStruct")},
+		},
+		{
+			symName: "table_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyTable",
+					Kind:   analysis.TableKind,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("table fuchsia.test/MyTable")},
+		},
+		{
+			symName: "union_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyUnion",
+					Kind:   analysis.UnionKind,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("union fuchsia.test/MyUnion")},
+		},
+		{
+			symName: "type_alias_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/AliasToVectorOfBools",
+					Kind:   analysis.TypeAliasKind,
+					TypeAlias: &analysis.TypeAliasTypeInfo{
+						Type: analysis.Type{
+							Kind: analysis.VectorType,
+							Vector: &analysis.VectorTypeInfo{
+								ElementType: analysis.Type{
+									Kind: analysis.PrimitiveType,
+									Primitive: &analysis.PrimitiveTypeInfo{
+										Subtype: analysis.Bool,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("fuchsia.test/AliasToVectorOfBools: alias to vector<bool>")},
+		},
+		{
+			symName: "nullable_struct_param",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl:     false,
+					Identifier: "fuchsia.test/MyStruct",
+					Nullable:   true,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("fuchsia.test/MyStruct?")},
+		},
+		{
+			symName: "type_with_attributes",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyProtocol",
+					Kind:   analysis.ProtocolKind,
+				},
+				Attrs: []analysis.Attribute{
+					{
+						Name:  "Doc",
+						Value: "Example doc comments on MyProtocol",
+					},
+					{Name: "Transitional"},
+					{
+						Name:  "OtherAttribute",
+						Value: "AttributeValue",
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{
+				lsp.RawMarkedString(`Example doc comments on MyProtocol`),
+				newFIDLString(`[Transitional, OtherAttribute = "AttributeValue"]`),
+				newFIDLString(`protocol fuchsia.test/MyProtocol`),
+			},
+		},
+	}
+
+	for _, ex := range cases {
+		hoverText, err := symbolTypeToMarkedStrings(ex.symName, ex.symType)
+		if err != nil {
+			t.Errorf("could not get hover text for symbol `%s`: %s", ex.symName, err)
+			continue
+		}
+		if len(hoverText) != len(ex.hoverText) {
+			t.Errorf(
+				"incorrect number of marked strings for symbol `%s`: expected %d, got %d",
+				ex.symName,
+				len(ex.hoverText),
+				len(hoverText),
+			)
+			continue
+		}
+		for i, expMarkedString := range ex.hoverText {
+			if hoverText[i] != expMarkedString {
+				t.Errorf(
+					"incorrect hoverText for symbol `%s`: expected %v, got %v",
+					ex.symName,
+					expMarkedString,
+					hoverText[i],
+				)
+			}
+		}
+	}
+}
+
+func TestHoverInvalidType(t *testing.T) {
+	_, err := symbolTypeToMarkedStrings(
+		"invalid_type",
+		analysis.Type{
+			Kind: analysis.PrimitiveType,
+			Array: &analysis.ArrayTypeInfo{
+				ElementType: analysis.Type{
+					Kind: analysis.PrimitiveType,
+					Primitive: &analysis.PrimitiveTypeInfo{
+						Subtype: analysis.Uint64,
+					},
+				},
+				ElementCount: 255,
+			},
+		},
+	)
+	if err == nil {
+		t.Errorf("expect error for hover text on invalid type")
+	}
+}