[fidl][lsp] Use new fidl_project.json format

Changes the Analyzer to use to new fidl_project.json format, which,
instead of being a map of library name --> library info, is a list of
libraries. This supports non-unique FIDL library names.

This essentially involves replacing indexes into Analyzer.libs with
calls to either Analyzer.getLibrary (which just takes a library name,
and uses a "heuristic" to find the library -- assumes library names are
unique) or, better, Analyzer.getLibraryWithFile, which takes a library
name and a file, thereby supporting lookup to libraries with non-unique
names.

This CL also fixes a flaky test, references_test.

Test: go test -p 1 ./...
Change-Id: I66e9e6be010ca7cab02ec751ecc6410ee3df20f8
Reviewed-on: https://fuchsia-review.googlesource.com/c/fidl-misc/+/399954
Reviewed-by: Pascal Perez <pascallouis@google.com>
Reviewed-by: Benjamin Prosnitz <bprosnitz@google.com>
diff --git a/fidl-lsp/analysis/analyzer.go b/fidl-lsp/analysis/analyzer.go
index 45fa471..1d8729e 100644
--- a/fidl-lsp/analysis/analyzer.go
+++ b/fidl-lsp/analysis/analyzer.go
@@ -33,6 +33,7 @@
 // exists (`json`), its deserialized JSON IR (`ir`), and any diagnostics on it.
 // `deps` and `files` are treated as sets.
 type Library struct {
+	name fidlcommon.LibraryName
 	deps map[fidlcommon.LibraryName]bool
 	// This library's constituent files in the form of absolute paths
 	files   map[string]bool
@@ -56,7 +57,7 @@
 	// A map from library name (string), like "zx", to a set of data about that
 	// library: the FIDL files it includes, its JSON IR, its dependencies, its
 	// JSON IR, and diagnostics returned on it from fidlc and fidl-lint.
-	libs map[fidlcommon.LibraryName]*Library
+	libs []*Library
 
 	// A map from open files to their corresponding tmp file used by the
 	// Analyzer as an input file for fidlc, fidl-lint, fidl-format, etc.
@@ -73,7 +74,7 @@
 func NewAnalyzer(cfg Config, compiledLibraries CompiledLibraries) *Analyzer {
 	a := &Analyzer{
 		cfg:            cfg,
-		libs:           make(map[fidlcommon.LibraryName]*Library),
+		libs:           []*Library{},
 		inputFilesFIDL: make(map[state.FileID]string),
 		inputFilesJSON: make(map[fidlcommon.LibraryName]string),
 	}
@@ -84,19 +85,25 @@
 // ImportCompiledLibraries adds the info for the compiled FIDL libraries in
 // `compiledLibraries` to the Analyzer's set of libraries.
 func (a *Analyzer) ImportCompiledLibraries(compiledLibraries CompiledLibraries) {
-	for name, lib := range compiledLibraries {
-		libName := fidlcommon.MustReadLibraryName(name)
-		a.libs[libName] = &Library{
-			deps:  make(map[fidlcommon.LibraryName]bool, len(lib.Deps)),
-			files: make(map[string]bool, len(lib.Files)),
-			json:  lib.JSON,
+	for _, compiledLib := range compiledLibraries {
+		name, err := fidlcommon.ReadLibraryName(compiledLib.Name)
+		if err != nil {
+			log.Printf("could not import compiled library `%s`\n", compiledLib.Name)
+			continue
 		}
-		for _, dep := range lib.Deps {
-			a.libs[libName].deps[fidlcommon.MustReadLibraryName(dep)] = true
+		lib := &Library{
+			name:  name,
+			deps:  make(map[fidlcommon.LibraryName]bool, len(compiledLib.Deps)),
+			files: make(map[string]bool, len(compiledLib.Files)),
+			json:  compiledLib.JSON,
 		}
-		for _, file := range lib.Files {
-			a.libs[libName].files[file] = true
+		for _, dep := range compiledLib.Deps {
+			lib.deps[fidlcommon.MustReadLibraryName(dep)] = true
 		}
+		for _, file := range compiledLib.Files {
+			lib.files[file] = true
+		}
+		a.libs = append(a.libs, lib)
 	}
 }
 
@@ -120,6 +127,60 @@
 	}
 }
 
+// getLibrary looks up the library with `name` in a.libs. It does this with an
+// imperfect "heuristic" -- return the first library with a matching name --
+// imperfect because FIDL library names are not required to be unique. This
+// method essentially assumes that they are unique.
+//
+// Ideally, everywhere getLibrary is called should instead use the
+// (*Analyzer).getLibraryWithFile method, since this is guaranteed to be unique.
+// Calls to getLibrary should include a comment explaining why it's necessary.
+func (a *Analyzer) getLibrary(name fidlcommon.LibraryName) (*Library, bool) {
+	for _, lib := range a.libs {
+		if lib.name == name {
+			return lib, true
+		}
+	}
+	return nil, false
+}
+
+// getLibraryWithFile looks for a library in a.libs whose name matches `name`
+// and which contains a file with the path specified.
+//
+// This allows a more precise search than getLibrary, since library name + file
+// path together is guaranteed to be unique (a file only belongs to one FIDL
+// library).
+func (a *Analyzer) getLibraryWithFile(name fidlcommon.LibraryName, path state.FileID) (*Library, bool) {
+	// If there is a corresponding temporary input file used by the Analyzer,
+	// use that to lookup the library in a.libs.
+	inputFilePath, hadTmpFile := a.inputFilesFIDL[path]
+
+	// Also, try to convert the file to an absolute filepath.
+	// The Analyzer stores filepaths for the last analysis of the library, so if
+	// files have been changed and are now stored as temporary input file, there
+	// could be a mix of tmp files and absolute filepaths.
+	// We check for both.
+	absPath, err := state.FileIDToPath(path)
+
+	// If we can't find either a temporary file path to lookup, or the absolute
+	// file path, we fall back to using the getLibrary method, which doesn't
+	// parameterize on a file path.
+	if err != nil && !hadTmpFile {
+		return a.getLibrary(name)
+	}
+
+	for _, lib := range a.libs {
+		if lib.name == name {
+			for libFile := range lib.files {
+				if libFile == inputFilePath || libFile == absPath {
+					return lib, true
+				}
+			}
+		}
+	}
+	return nil, false
+}
+
 // Analyze compiles and generates an analysis for the file at `path` in the
 // FileSystem. This currently includes compiling the library the file belongs to
 // and obtaining diagnostics on it.
@@ -137,15 +198,26 @@
 		return fmt.Errorf("could not find file `%s`", path)
 	}
 	if libName, err := state.LibraryOfFile(file); err == nil {
-		if _, ok := a.libs[libName]; !ok {
-			a.libs[libName] = &Library{
-				deps:  make(map[fidlcommon.LibraryName]bool),
-				files: make(map[string]bool),
-				diags: make(map[state.FileID][]Diagnostic),
+		// If this file is not already part of a library, either add it to its
+		// existing library or, if its library does not exist yet, create a new
+		// one.
+		var lib *Library
+		lib, ok := a.getLibraryWithFile(libName, path)
+		if !ok {
+			// We don't use getLibraryWithFile here because we have not yet added `file`
+			// to its library.
+			lib, ok = a.getLibrary(libName)
+			if !ok {
+				lib = &Library{
+					name:  libName,
+					deps:  make(map[fidlcommon.LibraryName]bool),
+					files: make(map[string]bool),
+					diags: make(map[state.FileID][]Diagnostic),
+				}
+				a.libs = append(a.libs, lib)
 			}
 		}
 
-		lib := a.libs[libName]
 		// If the file currently being edited was already part of lib.files,
 		// delete that and replace it with the temporary/edited version.
 		if absPath, err := state.FileIDToPath(path); err != nil {
@@ -169,7 +241,7 @@
 		return fmt.Errorf("error on analysis: %s", err)
 	}
 
-	a.libs[compileResult.lib].diags = compileResult.diags
+	compileResult.lib.diags = compileResult.diags
 	return nil
 }
 
@@ -179,13 +251,13 @@
 	for _, path := range a.inputFilesFIDL {
 		err := os.Remove(path)
 		if err != nil {
-			fmt.Fprintln(os.Stderr, err)
+			log.Print(err)
 		}
 	}
 	for _, path := range a.inputFilesJSON {
 		err := os.Remove(path)
 		if err != nil {
-			fmt.Fprintln(os.Stderr, err)
+			log.Print(err)
 		}
 	}
 }
diff --git a/fidl-lsp/analysis/compile.go b/fidl-lsp/analysis/compile.go
index 9336999..78d1744 100644
--- a/fidl-lsp/analysis/compile.go
+++ b/fidl-lsp/analysis/compile.go
@@ -9,6 +9,7 @@
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
+	"log"
 	"os/exec"
 	"strconv"
 	"strings"
@@ -19,7 +20,7 @@
 )
 
 type compileResult struct {
-	lib   fidlcommon.LibraryName
+	lib   *Library
 	diags map[state.FileID][]Diagnostic
 }
 
@@ -52,7 +53,7 @@
 	cmd.Stderr = &stderr
 	if err := cmd.Run(); err == nil {
 		// If fidlc compiled successfully, update a.libs with the compiled JSON IR
-		if err := a.importLibrary(jsonPath); err != nil {
+		if err := a.importLibraryWithFile(jsonPath, path); err != nil {
 			return compileResult{}, fmt.Errorf("error on adding compiled JSON: %s", err)
 		}
 	} else {
@@ -65,10 +66,15 @@
 	}
 
 	diags := make(map[state.FileID][]Diagnostic)
-	for fileName := range a.libs[libraryName].files {
-		fileID, err := a.inputFileToFileID(fileName)
-		if err == nil {
-			diags[fileID] = []Diagnostic{}
+	lib, ok := a.getLibraryWithFile(libraryName, path)
+	if !ok {
+		log.Printf("could not find library `%s` with file `%s`\n", libraryName.FullyQualifiedName(), path)
+	} else {
+		for fileName := range lib.files {
+			fileID, err := a.inputFileToFileID(fileName)
+			if err == nil {
+				diags[fileID] = []Diagnostic{}
+			}
 		}
 	}
 	if errorsAndWarnings, err := a.fidlcDiagsFromStderr(stderr.Bytes()); err == nil {
@@ -89,35 +95,61 @@
 	}
 
 	return compileResult{
-		lib:   libraryName,
+		lib:   lib,
 		diags: diags,
 	}, nil
 }
 
-// Read in the JSON IR at absolute filepath `jsonPath`.
-func (a *Analyzer) importLibrary(jsonPath string) error {
+// compileLibrary essentially does the same thing as importLibrary but for a
+// library that is already stored in a.libs, but the JSON IR for which has not
+// been imported.
+func (a *Analyzer) compileLibrary(lib *Library) error {
+	data, err := ioutil.ReadFile(lib.json)
+	if err != nil {
+		return err
+	}
+	var jsonIR FidlLibrary
+	if err := json.Unmarshal(data, &jsonIR); err != nil {
+		return err
+	}
+	lib.ir = &jsonIR
+
+	// Construct symbol map from JSON IR for the file's library
+	symbols, err := a.genSymbolMap(jsonIR)
+	if err != nil {
+		return fmt.Errorf("unable to generate symbol map from library %s: %s", jsonIR.Name, err)
+	}
+	lib.symbols = symbols
+
+	return nil
+}
+
+// importLibrary reads in the JSON IR at absolute filepath `jsonPath`.
+func (a *Analyzer) importLibraryWithFile(jsonPath string, path state.FileID) error {
 	data, err := ioutil.ReadFile(jsonPath)
 	if err != nil {
 		return err
 	}
-	var lib FidlLibrary
-	if err := json.Unmarshal(data, &lib); err != nil {
+	var jsonIR FidlLibrary
+	if err := json.Unmarshal(data, &jsonIR); err != nil {
 		return err
 	}
 	// TODO: import more things? files + deps?
-	libName := fidlcommon.MustReadLibraryName(lib.Name)
-	if _, ok := a.libs[libName]; !ok {
-		a.libs[libName] = &Library{}
+	libName := fidlcommon.MustReadLibraryName(jsonIR.Name)
+	lib, ok := a.getLibraryWithFile(libName, path)
+	if !ok {
+		lib = &Library{}
+		a.libs = append(a.libs, lib)
 	}
-	a.libs[libName].ir = &lib
-	a.libs[libName].json = jsonPath
+	lib.ir = &jsonIR
+	lib.json = jsonPath
 
 	// Construct symbol map from JSON IR for the file's library
-	symbols, err := a.genSymbolMap(lib)
+	symbols, err := a.genSymbolMap(jsonIR)
 	if err != nil {
-		return fmt.Errorf("unable to generate symbol map from library %s: %s", lib.Name, err)
+		return fmt.Errorf("unable to generate symbol map from library %s: %s", jsonIR.Name, err)
 	}
-	a.libs[libName].symbols = symbols
+	lib.symbols = symbols
 
 	return nil
 }
diff --git a/fidl-lsp/analysis/definition.go b/fidl-lsp/analysis/definition.go
index 566b3be..e9ccc30 100644
--- a/fidl-lsp/analysis/definition.go
+++ b/fidl-lsp/analysis/definition.go
@@ -41,8 +41,12 @@
 	// the library (specifically, pointing at their `library` declarations).
 	libName, err := fidlcommon.ReadLibraryName(sym.Name)
 	if err == nil {
-		if _, isLib := a.libs[libName]; isLib {
-			return a.declarationsOfLibrary(fs, libName), nil
+		// TODO: We could use getLibraryWithFile here, but it would be more
+		// complicated: if this library name is a declaration, we should pass it
+		// the file the symbol is in; but if it's a library import, we should
+		// pass it a file of that imported library.
+		if lib, isLib := a.getLibrary(libName); isLib {
+			return a.declarationsOfLibrary(fs, lib), nil
 		}
 	}
 
@@ -56,17 +60,33 @@
 		)
 	}
 
-	symInfo, err := a.lookupSymbolInfo(name)
-	if err != nil {
-		return nil, fmt.Errorf("could not find definition of symbol `%s`: %s", name.FullyQualifiedName(), err)
+	var symInfo *symbolInfo
+	if name.lib != nil {
+		symInfo, err = a.lookupSymbolInfoInLibrary(name.name, name.lib)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"could not find definition of symbol `%s`: %s",
+				name.name.FullyQualifiedName(),
+				err,
+			)
+		}
+	} else {
+		symInfo, err = a.lookupSymbolInfo(name.name)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"could not find definition of symbol `%s`: %s",
+				name.name.FullyQualifiedName(),
+				err,
+			)
+		}
 	}
 
 	return []state.Location{symInfo.definition}, nil
 }
 
-func (a *Analyzer) declarationsOfLibrary(fs *state.FileSystem, lib fidlcommon.LibraryName) []state.Location {
+func (a *Analyzer) declarationsOfLibrary(fs *state.FileSystem, lib *Library) []state.Location {
 	files := []state.FileID{}
-	for file := range a.libs[lib].files {
+	for file := range lib.files {
 		// These files are all absolute paths, but some are to temporary input
 		// files used by the Analyzer. For these, we want to convert the path
 		// to the document URI the editor knows about.
diff --git a/fidl-lsp/analysis/definition_test.go b/fidl-lsp/analysis/definition_test.go
index 3bd4c2e..b42a652 100644
--- a/fidl-lsp/analysis/definition_test.go
+++ b/fidl-lsp/analysis/definition_test.go
@@ -31,6 +31,7 @@
 
 func TestDefinitionOfLibrary(t *testing.T) {
 	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	defer analyzer.Cleanup()
 	fs := state.NewFileSystem()
 	fs.NewFile(
 		"file.fidl",
@@ -71,6 +72,7 @@
 
 func TestDefinitionOfLibraryMultipleFiles(t *testing.T) {
 	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	defer analyzer.Cleanup()
 	fs := state.NewFileSystem()
 	fs.NewFile(
 		"file1.fidl",
@@ -136,6 +138,7 @@
 
 func TestDefinitionOfLibraryImport(t *testing.T) {
 	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	defer analyzer.Cleanup()
 	fs := state.NewFileSystem()
 	fs.NewFile(
 		"import.fidl",
@@ -192,6 +195,7 @@
 
 func TestDefinitionOfFullyQualifiedSymbol(t *testing.T) {
 	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	defer analyzer.Cleanup()
 	fs := state.NewFileSystem()
 	fs.NewFile(
 		"import.fidl",
@@ -250,6 +254,7 @@
 
 func TestDefinitionOfLocalSymbol(t *testing.T) {
 	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	defer analyzer.Cleanup()
 	fs := state.NewFileSystem()
 	fs.NewFile(
 		"test.fidl",
@@ -298,6 +303,7 @@
 
 func TestDefinitionOfSymbolInSameLibraryDifferentFile(t *testing.T) {
 	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	defer analyzer.Cleanup()
 	fs := state.NewFileSystem()
 	fs.NewFile(
 		"test1.fidl",
@@ -353,8 +359,85 @@
 	}
 }
 
+func TestTwoLibrariesWithSameName(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(
+		defaultConfig(),
+		// Two distinct libraries with the same name
+		analysis.CompiledLibraries{
+			{
+				Name:  "fuchsia.test",
+				Files: []string{"test1.fidl"},
+			},
+			{
+				Name:  "fuchsia.test",
+				Files: []string{"test2.fidl"},
+			},
+		},
+	)
+	defer analyzer.Cleanup()
+
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"test1.fidl",
+		`
+library fuchsia.test;
+
+struct Foo {};
+union Bar {
+    1: Foo foo;
+};
+`,
+	)
+	fs.NewFile(
+		"test2.fidl",
+		`
+library fuchsia.test;
+
+struct Foo {};
+union Bar {
+    1: Foo foo;
+};
+`,
+	)
+	if err := analyzer.Analyze(fs, "test1.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+	if err := analyzer.Analyze(fs, "test2.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+
+	locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+		Name: "Foo",
+		Location: state.Location{
+			FileID: state.FileID("test1.fidl"),
+			Range: state.Range{
+				Start: state.Position{Line: 5, Character: 7},
+				End:   state.Position{Line: 5, Character: 10},
+			},
+		},
+	})
+	if err != nil {
+		t.Fatalf("error getting definition of symbol: %s", err)
+	}
+
+	expLoc := state.Location{
+		FileID: state.FileID("test1.fidl"),
+		Range: state.Range{
+			Start: state.Position{Line: 3, Character: 7},
+			End:   state.Position{Line: 3, Character: 10},
+		},
+	}
+	if len(locs) != 1 {
+		t.Fatalf("expected 1 definition location")
+	}
+	if locs[0] != expLoc {
+		t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+	}
+}
+
 func TestDefinitionOfReservedKeyword(t *testing.T) {
 	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	defer analyzer.Cleanup()
 	fs := state.NewFileSystem()
 	fs.NewFile(
 		"test.fidl",
diff --git a/fidl-lsp/analysis/deps.go b/fidl-lsp/analysis/deps.go
index 506ecfa..24d6b00 100644
--- a/fidl-lsp/analysis/deps.go
+++ b/fidl-lsp/analysis/deps.go
@@ -19,12 +19,13 @@
 // This is a file that declares all the FIDL libraries the language server
 // should be aware of, and includes information on how to build them and find
 // their compiled JSON IR.
-type CompiledLibraries map[string]CompiledLibrary
+type CompiledLibraries []CompiledLibrary
 
 // CompiledLibrary represents the build information for a single FIDL library,
-// including its constituent files, its dependencies, and the absolute filepath
-// to its compiled JSON IR.
+// including its name, its constituent files, its dependencies, and the absolute
+// filepath to its compiled JSON IR.
 type CompiledLibrary struct {
+	Name  string
 	Files []string
 	Deps  []string
 	JSON  string
@@ -92,7 +93,11 @@
 
 		// Find the library's files and dependencies.
 		var info libraryInfo
-		if lib, ok := a.libs[libName]; ok {
+		// We don't call getLibraryWithFile here because these libraries are
+		// found either by searching for regex patterns of library imports, or
+		// by looking in a library's dependencies, which are only stored as bare
+		// library names.
+		if lib, ok := a.getLibrary(libName); ok {
 			// Add the library's files to info.files
 			for file := range lib.files {
 				// Unless that file is a duplicate of one that is open
diff --git a/fidl-lsp/analysis/deps_test.go b/fidl-lsp/analysis/deps_test.go
index 04e7140..000e9b0 100644
--- a/fidl-lsp/analysis/deps_test.go
+++ b/fidl-lsp/analysis/deps_test.go
@@ -22,6 +22,7 @@
 
 func TestFindDepsNoDeps(t *testing.T) {
 	analyzer := analysis.NewAnalyzer(analysis.Config{}, analysis.CompiledLibraries{})
+	defer analyzer.Cleanup()
 	fs := state.NewFileSystem()
 	fs.NewFile(
 		"file.fidl",
@@ -42,14 +43,17 @@
 	analyzer := analysis.NewAnalyzer(
 		analysis.Config{},
 		analysis.CompiledLibraries{
-			"fuchsia.import1": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.import1",
 				Files: []string{"import1.fidl"},
 			},
-			"fuchsia.import2": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.import2",
 				Files: []string{"import2.fidl"},
 			},
 		},
 	)
+	defer analyzer.Cleanup()
 
 	fs := state.NewFileSystem()
 	fs.NewFile(
@@ -88,22 +92,27 @@
 	analyzer := analysis.NewAnalyzer(
 		analysis.Config{},
 		analysis.CompiledLibraries{
-			"fuchsia.import1": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.import1",
 				Files: []string{"import1.fidl"},
 				Deps:  []string{"fuchsia.import2", "fuchsia.import3"},
 			},
-			"fuchsia.import2": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.import2",
 				Files: []string{"import2.fidl"},
 				Deps:  []string{"fuchsia.import4"},
 			},
-			"fuchsia.import3": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.import3",
 				Files: []string{"import3.fidl"},
 			},
-			"fuchsia.import4": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.import4",
 				Files: []string{"import4.fidl"},
 			},
 		},
 	)
+	defer analyzer.Cleanup()
 
 	fs := state.NewFileSystem()
 	fs.NewFile(
@@ -146,22 +155,27 @@
 	analyzer := analysis.NewAnalyzer(
 		analysis.Config{},
 		analysis.CompiledLibraries{
-			"fuchsia.import1": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.import1",
 				Files: []string{"import1_1.fidl", "import1_2.fidl"},
 				Deps:  []string{"fuchsia.import3", "fuchsia.import4"},
 			},
-			"fuchsia.import2": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.import2",
 				Files: []string{"import2_1.fidl", "import2_2.fidl"},
 				Deps:  []string{"fuchsia.import4"},
 			},
-			"fuchsia.import3": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.import3",
 				Files: []string{"import3.fidl"},
 			},
-			"fuchsia.import4": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.import4",
 				Files: []string{"import4_1.fidl", "import4_2.fidl", "import4_3.fidl"},
 			},
 		},
 	)
+	defer analyzer.Cleanup()
 
 	fs := state.NewFileSystem()
 	fs.NewFile(
@@ -206,6 +220,7 @@
 
 func TestFindDepsNoLibraryInFile(t *testing.T) {
 	analyzer := analysis.NewAnalyzer(analysis.Config{}, analysis.CompiledLibraries{})
+	defer analyzer.Cleanup()
 	fs := state.NewFileSystem()
 	fs.NewFile(
 		"test1.fidl",
@@ -220,6 +235,7 @@
 
 func TestFindDepsImportNotInFidlProject(t *testing.T) {
 	analyzer := analysis.NewAnalyzer(analysis.Config{}, analysis.CompiledLibraries{})
+	defer analyzer.Cleanup()
 	fs := state.NewFileSystem()
 	fs.NewFile(
 		"foo.fidl",
@@ -239,16 +255,19 @@
 	analyzer := analysis.NewAnalyzer(
 		analysis.Config{},
 		analysis.CompiledLibraries{
-			"fuchsia.foo": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.foo",
 				Files: []string{"foo.fidl"},
 				Deps:  []string{"fuchsia.bar"},
 			},
-			"fuchsia.bar": analysis.CompiledLibrary{
+			{
+				Name:  "fuchsia.bar",
 				Files: []string{"bar.fidl"},
 				Deps:  []string{"fuchsia.foo"},
 			},
 		},
 	)
+	defer analyzer.Cleanup()
 
 	fs := state.NewFileSystem()
 	fs.NewFile(
diff --git a/fidl-lsp/analysis/diagnostics.go b/fidl-lsp/analysis/diagnostics.go
index 05a42b6..c8f7e4a 100644
--- a/fidl-lsp/analysis/diagnostics.go
+++ b/fidl-lsp/analysis/diagnostics.go
@@ -14,12 +14,14 @@
 	"fidl-lsp/state"
 )
 
-// DiagnosticsOnLibrary retrieves the cached diagnostics for `lib`.
-func (a *Analyzer) DiagnosticsOnLibrary(lib fidlcommon.LibraryName) (map[state.FileID][]Diagnostic, error) {
-	if _, ok := a.libs[lib]; !ok {
-		return nil, fmt.Errorf("could not find library `%s`", lib)
+// DiagnosticsOnLibrary retrieves the cached diagnostics for the library with
+// name `libName` including the file `path`.
+func (a *Analyzer) DiagnosticsOnLibrary(libName fidlcommon.LibraryName, path state.FileID) (map[state.FileID][]Diagnostic, error) {
+	lib, ok := a.getLibraryWithFile(libName, path)
+	if !ok {
+		return nil, fmt.Errorf("could not find library `%s`", libName)
 	}
-	return a.libs[lib].diags, nil
+	return lib.diags, nil
 }
 
 type fidlLintSuggestion struct {
diff --git a/fidl-lsp/analysis/references.go b/fidl-lsp/analysis/references.go
index 346f9c9..95727a1 100644
--- a/fidl-lsp/analysis/references.go
+++ b/fidl-lsp/analysis/references.go
@@ -27,8 +27,12 @@
 	// declarations).
 	libName, err := fidlcommon.ReadLibraryName(sym.Name)
 	if err == nil {
-		if _, isLib := a.libs[libName]; isLib {
-			return a.importsOfLibrary(fs, libName), nil
+		// TODO: We could use getLibraryWithFile here, but it would be more
+		// complicated: if this library name is a declaration, we should pass it
+		// the file the symbol is in; but if it's a library import, we should
+		// pass it a file of that imported library.
+		if lib, isLib := a.getLibrary(libName); isLib {
+			return a.importsOfLibrary(fs, lib.name), nil
 		}
 	}
 
@@ -44,23 +48,27 @@
 
 	// Search for references to `name` in all the libraries that import `name`'s
 	// library, as well as its own library.
-	libs := a.librariesThatImport(name.LibraryName())
-	libs = append(libs, name.LibraryName())
+	libs := a.librariesThatImport(name.name.LibraryName())
+	selfLibrary, ok := a.getLibraryWithFile(name.name.LibraryName(), sym.Location.FileID)
+	if !ok {
+		return nil, fmt.Errorf("could not find library of symbol `%s`", sym.Name)
+	}
+	libs = append(libs, selfLibrary)
 	refs := []state.Location{}
 
 	for _, lib := range libs {
-		if a.libs[lib].ir == nil {
-			if err := a.importLibrary(a.libs[lib].json); err != nil {
+		if lib.ir == nil {
+			if err := a.compileLibrary(lib); err != nil {
 				return nil, fmt.Errorf(
 					"error importing library `%s`: %s",
-					name.LibraryName().FullyQualifiedName(),
+					name.name.LibraryName().FullyQualifiedName(),
 					err,
 				)
 			}
 		}
 
-		for _, si := range a.libs[lib].symbols {
-			if a.typeReferencesName(si.typeInfo, name) {
+		for _, si := range lib.symbols {
+			if a.typeReferencesName(si.typeInfo, name.name) {
 				refs = append(refs, si.definition)
 			}
 		}
@@ -78,12 +86,12 @@
 	return locs
 }
 
-func (a *Analyzer) librariesThatImport(library fidlcommon.LibraryName) []fidlcommon.LibraryName {
-	libs := []fidlcommon.LibraryName{}
-	for name, lib := range a.libs {
+func (a *Analyzer) librariesThatImport(name fidlcommon.LibraryName) []*Library {
+	libs := []*Library{}
+	for _, lib := range a.libs {
 		for dep := range lib.deps {
-			if dep == library {
-				libs = append(libs, name)
+			if dep == name {
+				libs = append(libs, lib)
 				break
 			}
 		}
@@ -91,9 +99,9 @@
 	return libs
 }
 
-func (a *Analyzer) importsOfLibraryInLibrary(fs *state.FileSystem, importedLib fidlcommon.LibraryName, library fidlcommon.LibraryName) []state.Location {
+func (a *Analyzer) importsOfLibraryInLibrary(fs *state.FileSystem, importedLib fidlcommon.LibraryName, library *Library) []state.Location {
 	files := []state.FileID{}
-	for file := range a.libs[library].files {
+	for file := range library.files {
 		// These files are all absolute paths, but some are to temporary input
 		// files used by the Analyzer. For these, we want to convert the path
 		// to the document URI the editor knows about.
diff --git a/fidl-lsp/analysis/references_test.go b/fidl-lsp/analysis/references_test.go
index 608477a..5e021b4 100644
--- a/fidl-lsp/analysis/references_test.go
+++ b/fidl-lsp/analysis/references_test.go
@@ -11,20 +11,31 @@
 	"fidl-lsp/state"
 )
 
+type TestFile struct {
+	name state.FileID
+	text string
+}
+
 func TestReferencesToSymbol(t *testing.T) {
 	cases := []struct {
 		name    string
-		files   map[state.FileID]string
+		files   []TestFile
 		symbol  state.Symbol
 		expRefs []state.Location
 	}{
 		{
 			name: "Reference to library declaration",
-			files: map[state.FileID]string{
-				"test.fidl": `library fuchsia.test;`,
-				"other.fidl": `
+			files: []TestFile{
+				{
+					"test.fidl",
+					`library fuchsia.test;`,
+				},
+				{
+					"other.fidl",
+					`
 library other;
 using fuchsia.test;`,
+				},
 			},
 			symbol: state.Symbol{
 				Name: "fuchsia.test",
@@ -48,11 +59,17 @@
 		},
 		{
 			name: "Reference to library import",
-			files: map[state.FileID]string{
-				"test.fidl": `library fuchsia.test;`,
-				"other.fidl": `
+			files: []TestFile{
+				{
+					"test.fidl",
+					`library fuchsia.test;`,
+				},
+				{
+					"other.fidl",
+					`
 library other;
 using fuchsia.test;`,
+				},
 			},
 			symbol: state.Symbol{
 				Name: "fuchsia.test",
@@ -76,8 +93,9 @@
 		},
 		{
 			name: "Reference to symbol in same file",
-			files: map[state.FileID]string{
-				"test.fidl": `
+			files: []TestFile{{
+				"test.fidl",
+				`
 library test;
 
 struct Foo {};
@@ -87,7 +105,7 @@
 //             ~~~
 };
 `,
-			},
+			}},
 			symbol: state.Symbol{
 				Name: "Foo",
 				Location: state.Location{
@@ -110,13 +128,18 @@
 		},
 		{
 			name: "Reference to symbol in other file in same library",
-			files: map[state.FileID]string{
-				"test1.fidl": `
+			files: []TestFile{
+				{
+					"test1.fidl",
+					`
 library test;
 
 struct Foo {};
 `,
-				"test2.fidl": `
+				},
+				{
+					"test2.fidl",
+					`
 library test;
 
 protocol Bar {
@@ -124,6 +147,7 @@
 //             ~~~
 };
 `,
+				},
 			},
 			symbol: state.Symbol{
 				Name: "Foo",
@@ -147,13 +171,18 @@
 		},
 		{
 			name: "References to symbol in other library",
-			files: map[state.FileID]string{
-				"imported.fidl": `
+			files: []TestFile{
+				{
+					"imported.fidl",
+					`
 library fuchsia.imported;
 
 struct Foo {};
 `,
-				"test.fidl": `
+				},
+				{
+					"test.fidl",
+					`
 library test;
 using fuchsia.imported;
 
@@ -162,6 +191,7 @@
 //                              ~~~
 };
 `,
+				},
 			},
 			symbol: state.Symbol{
 				Name: "Foo",
@@ -185,13 +215,14 @@
 		},
 		{
 			name: "No references to symbol",
-			files: map[state.FileID]string{
-				"test.fidl": `
+			files: []TestFile{{
+				"test.fidl",
+				`
 library test;
 
 struct Foo {};
 `,
-			},
+			}},
 			symbol: state.Symbol{
 				Name: "Foo",
 				Location: state.Location{
@@ -206,13 +237,19 @@
 		},
 		{
 			name: "References to type alias",
-			files: map[state.FileID]string{
-				"zx.fidl": `
+			files: []TestFile{
+				{
+					"zx.fidl",
+					`
 library zx;
 
 using status = int32;
 `,
-				"test.fidl": `
+				},
+				{
+
+					"test.fidl",
+					`
 library test;
 using zx;
 
@@ -221,6 +258,7 @@
 //                         ~~~~~~
 };
 `,
+				},
 			},
 			symbol: state.Symbol{
 				Name: "status",
@@ -244,8 +282,9 @@
 		},
 		{
 			name: "References in type parameters",
-			files: map[state.FileID]string{
-				"test.fidl": `
+			files: []TestFile{{
+				"test.fidl",
+				`
 library test;
 
 table Foo {};
@@ -255,7 +294,7 @@
 //                     ~~~~~
 };
 `,
-			},
+			}},
 			symbol: state.Symbol{
 				Name: "Foo",
 				Location: state.Location{
@@ -280,12 +319,13 @@
 
 	for _, ex := range cases {
 		analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+		defer analyzer.Cleanup()
 		fs := state.NewFileSystem()
-		for file, text := range ex.files {
-			fs.NewFile(file, text)
+		for _, file := range ex.files {
+			fs.NewFile(file.name, file.text)
 		}
-		for file := range ex.files {
-			if err := analyzer.Analyze(fs, file); err != nil {
+		for _, file := range ex.files {
+			if err := analyzer.Analyze(fs, file.name); err != nil {
 				t.Fatalf("case `%s`: failed to analyze file `%s`: %s", ex.name, file, err)
 			}
 		}
diff --git a/fidl-lsp/analysis/symbol.go b/fidl-lsp/analysis/symbol.go
index c6e1554..a76e227 100644
--- a/fidl-lsp/analysis/symbol.go
+++ b/fidl-lsp/analysis/symbol.go
@@ -186,7 +186,12 @@
 	Type Type
 }
 
-func (a *Analyzer) symbolToFullyQualifiedName(fs *state.FileSystem, sym state.Symbol) (fidlcommon.Name, error) {
+type nameInLibrary struct {
+	lib  *Library // Left nil if uncertain
+	name fidlcommon.Name
+}
+
+func (a *Analyzer) symbolToFullyQualifiedName(fs *state.FileSystem, sym state.Symbol) (nameInLibrary, error) {
 	// 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
@@ -205,20 +210,25 @@
 	// If `sym` is a local name (not fully-qualified), we create a FQN by
 	// attaching its library name.
 	var fqn string
+	var lib *Library
 	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)
+			return nameInLibrary{}, fmt.Errorf("could not open file `%s`", sym.Location.FileID)
 		}
 		libName, err := state.LibraryOfFile(file)
 		if err != nil {
-			return fidlcommon.Name{}, fmt.Errorf(
+			return nameInLibrary{}, fmt.Errorf(
 				"could not find library of symbol `%s` in file `%s`",
 				sym.Name,
 				sym.Location.FileID,
 			)
 		}
 		fqn = libName.FullyQualifiedName() + "/" + sym.Name
+		// We know this symbol's library, so we can return it
+		if library, ok := a.getLibraryWithFile(libName, sym.Location.FileID); ok {
+			lib = library
+		}
 	} else {
 		// If the symbol contains '.', we assume it is a fully-qualified name.
 		i := strings.LastIndex(sym.Name, ".")
@@ -228,18 +238,27 @@
 	// 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 nameInLibrary{}, fmt.Errorf("could not read fully-qualified name `%s`: %s", fqn, err)
 	}
-	return name, nil
+	return nameInLibrary{lib: lib, name: name}, nil
 }
 
 func (a *Analyzer) lookupSymbolInfo(name fidlcommon.Name) (*symbolInfo, error) {
-	lib, ok := a.libs[name.LibraryName()]
+	// We don't call getLibraryWithFile here because we don't know for sure
+	// which library this symbol is part of. Unless the library is declared as a
+	// dependency of the library that includes this symbol, it couldn't be
+	// resolved, and we want to be able to resolve the symbol even if a
+	// dependency isn't declared.
+	lib, ok := a.getLibrary(name.LibraryName())
 	if !ok {
 		return nil, fmt.Errorf("unknown library `%s`", name.LibraryName().FullyQualifiedName())
 	}
+	return a.lookupSymbolInfoInLibrary(name, lib)
+}
+
+func (a *Analyzer) lookupSymbolInfoInLibrary(name fidlcommon.Name, lib *Library) (*symbolInfo, error) {
 	if lib.ir == nil {
-		if err := a.importLibrary(lib.json); err != nil {
+		if err := a.compileLibrary(lib); err != nil {
 			return nil, fmt.Errorf(
 				"error importing library `%s`: %s",
 				name.LibraryName().FullyQualifiedName(),
diff --git a/fidl-lsp/analysis/type.go b/fidl-lsp/analysis/type.go
index fcec389..041c4ec 100644
--- a/fidl-lsp/analysis/type.go
+++ b/fidl-lsp/analysis/type.go
@@ -20,7 +20,11 @@
 	// 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 {
+		// TODO: We could use getLibraryWithFile here, but it would be more
+		// complicated: if this library name is a declaration, we should pass it
+		// the file the symbol is in; but if it's a library import, we should
+		// pass it a file of that imported library.
+		if _, isLib := a.getLibrary(libName); isLib {
 			return Type{
 				IsLib: true,
 			}, nil
@@ -37,9 +41,25 @@
 		)
 	}
 
-	symInfo, err := a.lookupSymbolInfo(name)
-	if err != nil {
-		return Type{}, fmt.Errorf("could not find type of symbol `%s`: %s", name.FullyQualifiedName(), err)
+	var symInfo *symbolInfo
+	if name.lib != nil {
+		symInfo, err = a.lookupSymbolInfoInLibrary(name.name, name.lib)
+		if err != nil {
+			return Type{}, fmt.Errorf(
+				"could not find type of symbol `%s`: %s",
+				name.name.FullyQualifiedName(),
+				err,
+			)
+		}
+	} else {
+		symInfo, err = a.lookupSymbolInfo(name.name)
+		if err != nil {
+			return Type{}, fmt.Errorf(
+				"could not find type of symbol `%s`: %s",
+				name.name.FullyQualifiedName(),
+				err,
+			)
+		}
 	}
 
 	// Resolve identifier type, if necessary.
@@ -60,7 +80,7 @@
 			return Type{}, fmt.Errorf(
 				"could not find identifier type `%s` of symbol `%s`: %s",
 				typeName.FullyQualifiedName(),
-				name.FullyQualifiedName(),
+				name.name.FullyQualifiedName(),
 				err,
 			)
 		}
@@ -91,7 +111,7 @@
 			return Type{}, fmt.Errorf(
 				"could not find identifier type `%s` of symbol `%s`: %s",
 				typeName.FullyQualifiedName(),
-				name.FullyQualifiedName(),
+				name.name.FullyQualifiedName(),
 				err,
 			)
 		}
diff --git a/fidl-lsp/langserver/config.go b/fidl-lsp/langserver/config.go
index 1924550..56662fd 100644
--- a/fidl-lsp/langserver/config.go
+++ b/fidl-lsp/langserver/config.go
@@ -67,7 +67,7 @@
 	if cfg.FidlProject != "" {
 		fidlProject, err := analysis.LoadFidlProject(cfg.FidlProject)
 		if err != nil {
-			h.log.Printf("Failed to parse fidl_project.json at `%s`: %s\n", cfg.FidlProject, err)
+			h.log.Printf("failed to parse fidl_project.json at `%s`: %s\n", cfg.FidlProject, err)
 		}
 		// TODO: remove the currently compiled libraries?
 		h.analyzer.ImportCompiledLibraries(fidlProject)
diff --git a/fidl-lsp/langserver/diagnostics.go b/fidl-lsp/langserver/diagnostics.go
index 01d1212..f8aef73 100644
--- a/fidl-lsp/langserver/diagnostics.go
+++ b/fidl-lsp/langserver/diagnostics.go
@@ -42,7 +42,7 @@
 		return nil, fmt.Errorf("could not find library name of file `%s`: %s", uri, err)
 	}
 
-	diags, err := h.analyzer.DiagnosticsOnLibrary(fidlcommon.LibraryName(libraryName))
+	diags, err := h.analyzer.DiagnosticsOnLibrary(fidlcommon.LibraryName(libraryName), state.FileID(uri))
 	if err != nil {
 		return nil, fmt.Errorf("error getting diagnostics: %s", err)
 	}