blob: 408440033f1fa8a8ccefae18fc5671064d581efe [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 (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os/exec"
"strings"
fidlcommon "fidl-lsp/third_party/common"
"fidl-lsp/state"
)
type compileResult struct {
lib fidlcommon.LibraryName
diags map[state.FileID][]Diagnostic
}
// Compile the FIDL library that includes the file at `path`.
// Returns either a compileResult containing the name of the library along with
// any diagnostics returned by fidlc and fidl-lint, or an error if compilation
// fails.
func (a *Analyzer) compile(fs *state.FileSystem, path state.FileID) (compileResult, error) {
var jsonPath string
file, err := fs.File(path)
if err != nil {
return compileResult{}, fmt.Errorf("could not find file `%s`", path)
}
libraryName, err := state.LibraryOfFile(file)
if err != nil {
return compileResult{}, fmt.Errorf("could not find library name of file `%s`: %s", path, err)
}
jsonPath = a.pathToJSON(libraryName)
args := append([]string{"--format=json"}, "--json", jsonPath)
files, err := a.FindDeps(fs, path)
if err != nil {
return compileResult{}, fmt.Errorf("could not find dependencies of file `%s`: %s", path, err)
}
args = append(args, files...)
cmd := exec.Command(a.cfg.FidlcPath, args...)
var stderr bytes.Buffer
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 {
return compileResult{}, fmt.Errorf("error on adding compiled JSON: %s", err)
}
} else {
// TODO: there is no platform-independent way in Go to check an error
// status code, but if we can, we should somehow determine if fidlc
// exited with status code 1 (failure due to FIDL errors) or something
// else (internal error or command otherwise failed).
// If it's status code 1, we should continue, but anything else, we
// should return an error from `compile` to signal failure.
}
diags := make(map[state.FileID][]Diagnostic)
for fileName := range a.libs[libraryName].files {
fileID, err := a.inputFileToFileID(fileName)
if err == nil {
diags[fileID] = []Diagnostic{}
}
}
if errorsAndWarnings, err := a.fidlcDiagsFromStderr(stderr.Bytes()); err == nil {
for fileID, fileDiags := range errorsAndWarnings {
if _, ok := diags[fileID]; !ok {
diags[fileID] = []Diagnostic{}
}
diags[fileID] = append(diags[fileID], fileDiags...)
}
}
if lints, err := a.runFidlLint(a.inputFilesFIDL[path]); err == nil {
for fileID, fileDiags := range lints {
if _, ok := diags[fileID]; !ok {
diags[fileID] = []Diagnostic{}
}
diags[fileID] = append(diags[fileID], fileDiags...)
}
}
return compileResult{
lib: libraryName,
diags: diags,
}, nil
}
// Read in the JSON IR at absolute filepath `jsonPath`.
func (a *Analyzer) importLibrary(jsonPath string) error {
data, err := ioutil.ReadFile(jsonPath)
if err != nil {
return err
}
var lib FidlLibrary
if err := json.Unmarshal(data, &lib); 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{}
}
a.libs[libName].ir = &lib
a.libs[libName].json = jsonPath
// Construct symbol map from JSON IR for the file's library
symbols, err := a.genSymbolMap(lib)
if err != nil {
return fmt.Errorf("unable to generate symbol map from library %s: %s", lib.Name, err)
}
a.libs[libName].symbols = symbols
return nil
}
type symbolKind string
const (
bitsKind symbolKind = "bits"
constKind = "const"
enumKind = "enum"
protocolKind = "protocol"
serviceKind = "service"
structKind = "struct"
tableKind = "table"
unionKind = "union"
typeAliasKind = "typeAlias"
)
type symbolInfo struct {
lib string
name string
definition state.Location
attrs []attribute
kind symbolKind
isMember bool
isMethod bool
typeInfo interface{}
maybeFromTypeAlias typeCtor
}
type symbolMap map[string]*symbolInfo
func (a *Analyzer) genSymbolMap(l FidlLibrary) (symbolMap, error) {
// TODO: skip SomeLongAnonymousPrefix* structs? (are we double counting method request/response params?)
// TODO: how to handle const literals?
sm := make(symbolMap)
for _, d := range l.BitsDecls {
sm[d.Name] = &symbolInfo{
lib: l.Name,
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
kind: bitsKind,
typeInfo: d.Type,
maybeFromTypeAlias: d.FromTypeAlias,
attrs: d.Attrs,
}
a.addMembersToSymbolMap(sm, l.Name, d.Name, bitsKind, d.Members)
}
for _, d := range l.ConstDecls {
sm[d.Name] = &symbolInfo{
lib: l.Name,
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
kind: constKind,
typeInfo: d.Type,
maybeFromTypeAlias: d.FromTypeAlias,
attrs: d.Attrs,
}
}
for _, d := range l.EnumDecls {
sm[d.Name] = &symbolInfo{
lib: l.Name,
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
kind: enumKind,
typeInfo: d.Type,
maybeFromTypeAlias: d.FromTypeAlias,
attrs: d.Attrs,
}
a.addMembersToSymbolMap(sm, l.Name, d.Name, enumKind, d.Members)
}
for _, d := range l.ProtocolDecls {
sm[d.Name] = &symbolInfo{
lib: l.Name,
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
kind: protocolKind,
attrs: d.Attrs,
}
for _, m := range d.Methods {
methodName := fmt.Sprintf("%s.%s", d.Name, m.Name)
sm[methodName] = &symbolInfo{
lib: l.Name,
name: methodName,
definition: a.fidlLocToStateLoc(m.Loc),
kind: protocolKind,
isMethod: true,
attrs: m.Attrs,
}
if len(m.MaybeRequest) > 0 {
a.addMembersToSymbolMap(sm, l.Name, methodName, protocolKind, m.MaybeRequest)
}
if len(m.MaybeResponse) > 0 {
a.addMembersToSymbolMap(sm, l.Name, methodName, protocolKind, m.MaybeResponse)
}
}
}
for _, d := range l.ServiceDecls {
sm[d.Name] = &symbolInfo{
lib: l.Name,
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
kind: serviceKind,
attrs: d.Attrs,
}
a.addMembersToSymbolMap(sm, l.Name, d.Name, serviceKind, d.Members)
}
for _, d := range l.StructDecls {
sm[d.Name] = &symbolInfo{
lib: l.Name,
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
kind: structKind,
attrs: d.Attrs,
}
a.addMembersToSymbolMap(sm, l.Name, d.Name, structKind, d.Members)
}
for _, d := range l.TableDecls {
sm[d.Name] = &symbolInfo{
lib: l.Name,
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
kind: tableKind,
attrs: d.Attrs,
}
a.addMembersToSymbolMap(sm, l.Name, d.Name, tableKind, d.Members)
}
for _, d := range l.UnionDecls {
sm[d.Name] = &symbolInfo{
lib: l.Name,
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
kind: unionKind,
attrs: d.Attrs,
}
a.addMembersToSymbolMap(sm, l.Name, d.Name, unionKind, d.Members)
}
for _, d := range l.TypeAliasDecls {
sm[d.Name] = &symbolInfo{
lib: l.Name,
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
kind: typeAliasKind,
typeInfo: d.TypeCtor,
attrs: d.Attrs,
}
}
return sm, nil
}
func (a *Analyzer) addMembersToSymbolMap(sm symbolMap, libName string, declName string, kind symbolKind, members []member) {
for _, m := range members {
memberName := fmt.Sprintf("%s.%s", declName, m.Name)
sm[memberName] = &symbolInfo{
lib: libName,
name: memberName,
definition: a.fidlLocToStateLoc(m.Loc),
kind: kind,
isMember: true,
typeInfo: m.Type,
maybeFromTypeAlias: m.FromTypeAlias,
attrs: m.Attrs,
}
}
}
func (a *Analyzer) fidlLocToStateLoc(loc location) state.Location {
// If this JSON IR was compiled by the language server, the `Filename`s for
// for all the symbols will be tmp input files from the Analyzer. We want
// to point at the corresponding editor files for these tmp files.
fileID, err := a.inputFileToFileID(loc.Filename)
if err != nil {
// We assume `Filename` is a filepath to a precompiled JSON IR, provided
// in the CompiledLibraries on startup.
//
// The `Filename` in a decl's location is recorded relative to where
// fidlc was invoked. All the locations in CompiledLibraries are
// prepended with "../../" which can be replaced by $FUCHSIA_DIR/.
fileID = state.FileID(strings.Replace(loc.Filename, "../..", a.cfg.BuildRootDir, 1))
}
return state.Location{
FileID: fileID,
Range: state.Range{
Start: state.Position{Line: loc.Line - 1, Character: loc.Column - 1},
End: state.Position{Line: loc.Line - 1, Character: loc.Column - 1 + loc.Length},
},
}
}