| // 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 |
| } |
| |
| // 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 |
| } |
| } |
| |
| // 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() |
| } |