blob: 1d0ec9b5b5d4a1309483ae9936551afd2bd21e71 [file] [log] [blame]
// 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"
"hash/fnv"
"io/ioutil"
"log"
"os"
"fidl-lsp/third_party/fidlgen"
"fidl-lsp/state"
)
// Config keeps track of where the Analyzer can find FIDL tools, as well as the
// root build directory, to which all locations in CompiledLibraries are
// relative.
// This config will be exposed where possible as settings in FIDL LSP client IDE
// extensions.
type Config struct {
BuildRootDir string
FidlcPath string
FidlLintPath string
FidlFormatPath string
FidlExperiments []string
}
// A Library is a set of information about a FIDL library, including build
// information (files and dependencies), the location of its JSON IR if it
// exists (`json`), its deserialized JSON IR (`ir`), and any diagnostics on it.
// `deps` and `files` are treated as sets.
type Library struct {
name fidlgen.LibraryName
deps map[fidlgen.LibraryName]bool
// This library's constituent files in the form of absolute paths
files map[string]bool
json string
ir *FidlLibrary // nil if not yet imported
diags map[state.FileID][]Diagnostic
symbols symbolMap
}
// Analyzer compiles FIDL libraries and extracts analyses from them, including
// diagnostics.
// - inputFilesFIDL is a map from documents open in the editor to the absolute
// filepath of the corresponding temporary file that Analyzer uses as input
// for fidlc, etc.
// - inputFileJSON is a map from FIDL library name to an absolute filepath to
// its JSON IR.
type Analyzer struct {
// Where to find the fuchsia directory and FIDL tools.
cfg Config
// 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 []*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.
// Key is an lsp.DocumentURI, value is an absolute filepath.
inputFilesFIDL map[state.FileID]string
// A map from library name to the corresponding tmp file used for their JSON
// IR.
inputFilesJSON map[fidlgen.LibraryName]string
}
// NewAnalyzer returns an Analyzer initialized with the set of CompiledLibraries
// passed in.
func NewAnalyzer(cfg Config) *Analyzer {
a := &Analyzer{
cfg: cfg,
libs: []*Library{},
inputFilesFIDL: make(map[state.FileID]string),
inputFilesJSON: make(map[fidlgen.LibraryName]string),
}
return a
}
// ImportCompiledLibraries adds the info for the compiled FIDL libraries in
// `compiledLibraries` to the Analyzer's set of libraries.
func (a *Analyzer) ImportCompiledLibraries(compiledLibraries CompiledLibraries) {
// TODO(fxbug.dev/55060): currently this tries to reconcile the new set of
// compiled libraries with the libraries already cached in a.libs, by
// merging files and deps for libraries that are the same.
// However, it might be a cleaner approach just to clear a.libs before
// importing a new set.
for _, compiledLib := range compiledLibraries {
name, err := fidlgen.ReadLibraryName(compiledLib.Name)
if err != nil {
log.Printf("could not import compiled library `%s`: %s\n", compiledLib.Name, err)
continue
}
var lib *Library
var alreadyImported bool
// Check if we already have this library imported.
if len(compiledLib.Files) > 0 {
for _, file := range compiledLib.Files {
lib, alreadyImported = a.getLibraryWithFile(name, state.PathToFileID(file))
if alreadyImported {
break
}
}
}
if !alreadyImported {
lib = &Library{
name: name,
deps: make(map[fidlgen.LibraryName]bool, len(compiledLib.Deps)),
files: make(map[string]bool, len(compiledLib.Files)),
json: compiledLib.JSON,
}
a.libs = append(a.libs, lib)
}
// Whether or not the library was previously imported, we add all the
// deps and files from the CompiledLibrary.
if lib.json == "" {
lib.json = compiledLib.JSON
}
for _, dep := range compiledLib.Deps {
lib.deps[fidlgen.MustReadLibraryName(dep)] = true
}
for _, file := range compiledLib.Files {
// If the file is already included as a replaced tmp input file,
// don't also include the duplicate.
if _, ok := a.inputFilesFIDL[state.PathToFileID(file)]; ok {
continue
}
lib.files[file] = true
}
}
}
// SetConfig updates the Analyzer's configuration settings.
// Allows the LSP server to update the Analyzer's configuration settings when
// the client updates their settings.
func (a *Analyzer) SetConfig(cfg Config) {
// The client sends over all settings at once every time but we want to
// ignore ones that are "unset" -- i.e. equal to the empty string.
if cfg.BuildRootDir != "" {
a.cfg.BuildRootDir = cfg.BuildRootDir
}
if cfg.FidlcPath != "" {
a.cfg.FidlcPath = cfg.FidlcPath
}
if cfg.FidlLintPath != "" {
a.cfg.FidlLintPath = cfg.FidlLintPath
}
if cfg.FidlFormatPath != "" {
a.cfg.FidlFormatPath = cfg.FidlFormatPath
}
if len(cfg.FidlExperiments) != 0 {
a.cfg.FidlExperiments = cfg.FidlExperiments
}
}
// 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 fidlgen.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 fidlgen.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.
// Analyze only uses read access to the FileSystem.
func (a *Analyzer) Analyze(fs *state.FileSystem, path state.FileID) error {
inputFilePath, err := a.writeFileToTmp(fs, path)
if err != nil {
return fmt.Errorf("failed to write file `%s` to tmp: %s", path, err)
}
// Add this file and its deps to its Library
// TODO: do this on setup with all files in workspace
file, err := fs.File(path)
if err != nil {
return fmt.Errorf("could not find file `%s`", path)
}
if libName, err := state.LibraryOfFile(file); err == nil {
// 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[fidlgen.LibraryName]bool),
files: make(map[string]bool),
diags: make(map[state.FileID][]Diagnostic),
}
a.libs = append(a.libs, lib)
}
}
// 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 {
log.Printf("error parsing: %s\n", err)
} else {
if _, ok := lib.files[absPath]; ok {
delete(lib.files, absPath)
}
}
lib.files[inputFilePath] = true
imports := state.ParsePlatformImportsMatch(file)
for _, dep := range imports {
lib.deps[fidlgen.LibraryName(dep.Lib)] = true
}
}
// Compile the file's library
compileResult, err := a.compile(fs, path)
if err != nil {
return fmt.Errorf("error on analysis: %s", err)
}
compileResult.lib.diags = compileResult.diags
return nil
}
// Cleanup deletes all temporary files Analyzer created during its lifetime to
// compile FIDL libraries.
func (a *Analyzer) Cleanup() {
for _, path := range a.inputFilesFIDL {
err := os.Remove(path)
if err != nil {
log.Print(err)
}
}
for _, path := range a.inputFilesJSON {
err := os.Remove(path)
if err != nil {
log.Print(err)
}
}
}
// writeFileToTmp writes the in-memory file indexed by `path` to a temporary
// file on disk, in order to enable invoking some command on it in a separate
// process, e.g. fidlc, fidl-format, etc.
func (a *Analyzer) writeFileToTmp(fs *state.FileSystem, path state.FileID) (string, error) {
if _, ok := a.inputFilesFIDL[path]; !ok {
a.inputFilesFIDL[path] = fmt.Sprintf("/tmp/%d.fidl", hash(string(path)))
}
file, err := fs.File(path)
if err != nil {
return "", fmt.Errorf("could not find file `%s`", path)
}
// TODO: Can we do 0644 with os.FileMode(os.O_FLAGS)?
if err := ioutil.WriteFile(a.inputFilesFIDL[path], []byte(file), 0644); err != nil {
return "", fmt.Errorf("error writing file `%s` to tmp directory: %s", path, err)
}
return a.inputFilesFIDL[path], nil
}
func (a *Analyzer) inputFileToFileID(inputFilePath string) (state.FileID, error) {
for fileID, inputFile := range a.inputFilesFIDL {
if inputFile == inputFilePath {
return fileID, nil
}
}
return "", fmt.Errorf("could not find input file `%s`", inputFilePath)
}
// pathToJSON returns the path to the tmp file where the Analyzer saved the JSON
// IR for the library `lib`, or, if there is not a saved JSON file, generates
// a path. It is used for fidlc invocations and for importing libraries' JSON
// IR.
func (a *Analyzer) pathToJSON(lib fidlgen.LibraryName) string {
if path, ok := a.inputFilesJSON[lib]; ok {
return path
}
a.inputFilesJSON[lib] = fmt.Sprintf("/tmp/%d.json", hash(lib.FullyQualifiedName()))
return a.inputFilesJSON[lib]
}
func hash(s string) uint32 {
h := fnv.New32a()
h.Write([]byte(s))
return h.Sum32()
}