| // 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" |
| "os" |
| |
| "fidl-lsp/state" |
| ) |
| |
| var ( |
| fuchsiaDir = os.Getenv("FUCHSIA_DIR") |
| outDir = fuchsiaDir + "/out/default/" |
| fidlProjectPath = outDir + "fidl_project.json" |
| toolsPath = fuchsiaDir + "/out/default.zircon/tools/" |
| fidlcPath = toolsPath + "fidlc" |
| fidlLintPath = toolsPath + "fidl-lint" |
| fidlFormatPath = toolsPath + "fidl-format" |
| ) |
| |
| // A LibraryName is a FIDL library represented by its name (as spelled in its |
| // `library` declaration, not necessarily matching directory or file names). |
| type LibraryName 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 { |
| deps map[LibraryName]bool |
| files map[string]bool |
| json string |
| ir *FidlLibrary // nil if not yet imported |
| diags map[state.EditorFile][]Diagnostic |
| } |
| |
| // 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 { |
| // 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[LibraryName]*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.EditorFile]string |
| |
| // A map from library name to the corresponding tmp file used for their JSON |
| // IR. |
| inputFilesJSON map[LibraryName]string |
| } |
| |
| // NewAnalyzer returns an Analyzer initialized with the set of CompiledLibraries |
| // passed in. |
| func NewAnalyzer(compiledLibraries CompiledLibraries) *Analyzer { |
| a := &Analyzer{ |
| libs: make(map[LibraryName]*Library), |
| inputFilesFIDL: make(map[state.EditorFile]string), |
| inputFilesJSON: make(map[LibraryName]string), |
| } |
| for libName, lib := range compiledLibraries { |
| a.libs[libName] = &Library{ |
| deps: make(map[LibraryName]bool, len(lib.Deps)), |
| files: make(map[string]bool, len(lib.Files)), |
| json: lib.JSON, |
| } |
| for _, dep := range lib.Deps { |
| a.libs[libName].deps[dep] = true |
| } |
| for _, file := range lib.Files { |
| a.libs[libName].files[file] = true |
| } |
| } |
| return a |
| } |
| |
| // 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.EditorFile) 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 _, ok := a.libs[LibraryName(libName)]; !ok { |
| a.libs[LibraryName(libName)] = &Library{ |
| deps: make(map[LibraryName]bool), |
| files: make(map[string]bool), |
| diags: make(map[state.EditorFile][]Diagnostic), |
| } |
| } |
| |
| lib := a.libs[LibraryName(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.EditorFileToPath(path); err == nil { |
| if _, ok := lib.files[absPath]; ok { |
| delete(lib.files, absPath) |
| } |
| } else { |
| fmt.Fprint(os.Stderr, "error parsing\n") |
| } |
| |
| lib.files[inputFilePath] = true |
| imports := state.ParsePlatformImportsMatch(file) |
| for _, dep := range imports { |
| lib.deps[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) |
| } |
| |
| a.libs[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 { |
| fmt.Fprintln(os.Stderr, err) |
| } |
| } |
| for _, path := range a.inputFilesJSON { |
| err := os.Remove(path) |
| if err != nil { |
| fmt.Fprintln(os.Stderr, 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.EditorFile) (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) inputFileToEditorFile(inputFilePath string) (state.EditorFile, error) { |
| for editorFile, inputFile := range a.inputFilesFIDL { |
| if inputFile == inputFilePath { |
| return editorFile, 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 LibraryName) string { |
| if path, ok := a.inputFilesJSON[lib]; ok { |
| return path |
| } |
| a.inputFilesJSON[lib] = fmt.Sprintf("/tmp/%d.json", hash(string(lib))) |
| return a.inputFilesJSON[lib] |
| } |
| |
| func hash(s string) uint32 { |
| h := fnv.New32a() |
| h.Write([]byte(s)) |
| return h.Sum32() |
| } |