blob: 1d93e5686dd8d88ee84a220b457680956805d79b [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"
"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()
}