blob: d4e2e525895332aa3bee1934d3764edb52fa09cc [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"
"log"
"os/exec"
"strconv"
"strings"
"fidl-lsp/third_party/fidlgen"
"fidl-lsp/state"
)
type compileResult struct {
lib *Library
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)
for _, experiment := range a.cfg.FidlExperiments {
args = append(args, "--experimental", experiment)
}
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.importLibraryWithFile(jsonPath, path); 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)
lib, ok := a.getLibraryWithFile(libraryName, path)
if !ok {
log.Printf("could not find library `%s` with file `%s`\n", libraryName.FullyQualifiedName(), path)
} else {
for fileName := range lib.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: lib,
diags: diags,
}, nil
}
// compileLibrary essentially does the same thing as importLibrary but for a
// library that is already stored in a.libs, but the JSON IR for which has not
// been imported.
func (a *Analyzer) compileLibrary(lib *Library) error {
data, err := ioutil.ReadFile(lib.json)
if err != nil {
return err
}
var jsonIR FidlLibrary
if err := json.Unmarshal(data, &jsonIR); err != nil {
return err
}
lib.ir = &jsonIR
// Construct symbol map from JSON IR for the file's library
symbols, err := a.genSymbolMap(jsonIR)
if err != nil {
return fmt.Errorf("unable to generate symbol map from library %s: %s", jsonIR.Name, err)
}
lib.symbols = symbols
return nil
}
// importLibraryWithFile reads in the JSON IR at absolute filepath `jsonPath`.
func (a *Analyzer) importLibraryWithFile(jsonPath string, path state.FileID) error {
data, err := ioutil.ReadFile(jsonPath)
if err != nil {
return err
}
var jsonIR FidlLibrary
if err := json.Unmarshal(data, &jsonIR); err != nil {
return err
}
// TODO: import more things? files + deps?
libName := fidlgen.MustReadLibraryName(jsonIR.Name)
lib, ok := a.getLibraryWithFile(libName, path)
if !ok {
lib = &Library{}
a.libs = append(a.libs, lib)
}
lib.ir = &jsonIR
lib.json = jsonPath
// Construct symbol map from JSON IR for the file's library
symbols, err := a.genSymbolMap(jsonIR)
if err != nil {
return fmt.Errorf("unable to generate symbol map from library %s: %s", jsonIR.Name, err)
}
lib.symbols = symbols
return nil
}
type symbolMap map[string]*symbolInfo
func (a *Analyzer) genSymbolMap(l FidlLibrary) (symbolMap, error) {
sm := make(symbolMap)
for _, d := range l.BitsDecls {
sm[d.Name] = &symbolInfo{
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
typeInfo: Type{
FromTypeAlias: d.FromTypeAlias.TypeAlias(),
Kind: IdentifierType,
Identifier: &IdentifierTypeInfo{
IsDecl: true,
Name: d.Name,
Kind: BitsKind,
Bits: &BitsTypeInfo{
Type: d.Type.Type(),
},
},
Attrs: d.Attrs,
},
}
a.addMembersToSymbolMap(sm, d.Name, d.Members)
}
for _, d := range l.ConstDecls {
sm[d.Name] = &symbolInfo{
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
typeInfo: Type{
FromTypeAlias: d.FromTypeAlias.TypeAlias(),
Kind: IdentifierType,
Identifier: &IdentifierTypeInfo{
IsDecl: true,
Name: d.Name,
Kind: ConstKind,
Const: &ConstTypeInfo{
Type: d.Type.Type(),
Value: d.Value.Value,
},
},
Attrs: d.Attrs,
},
}
}
for _, d := range l.EnumDecls {
sm[d.Name] = &symbolInfo{
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
typeInfo: Type{
FromTypeAlias: d.FromTypeAlias.TypeAlias(),
Kind: IdentifierType,
Identifier: &IdentifierTypeInfo{
IsDecl: true,
Name: d.Name,
Kind: EnumKind,
Enum: &EnumTypeInfo{
Type: PrimitiveSubtype(d.Type),
},
},
Attrs: d.Attrs,
},
}
a.addMembersToSymbolMap(sm, d.Name, d.Members)
}
for _, d := range l.ProtocolDecls {
sm[d.Name] = &symbolInfo{
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
typeInfo: Type{
Kind: IdentifierType,
Identifier: &IdentifierTypeInfo{
IsDecl: true,
Name: d.Name,
Kind: ProtocolKind,
},
Attrs: d.Attrs,
},
}
for _, m := range d.Methods {
methodName := fmt.Sprintf("%s.%s", d.Name, m.Name)
sm[methodName] = &symbolInfo{
name: m.Name,
definition: a.fidlLocToStateLoc(m.Loc),
typeInfo: Type{
IsMethod: true,
Attrs: m.Attrs,
},
}
if len(m.MaybeRequest) > 0 {
a.addMembersToSymbolMap(sm, methodName, m.MaybeRequest)
}
if len(m.MaybeResponse) > 0 {
a.addMembersToSymbolMap(sm, methodName, m.MaybeResponse)
}
}
}
for _, d := range l.ServiceDecls {
sm[d.Name] = &symbolInfo{
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
typeInfo: Type{
Kind: IdentifierType,
Identifier: &IdentifierTypeInfo{
IsDecl: true,
Name: d.Name,
Kind: ServiceKind,
},
Attrs: d.Attrs,
},
}
a.addMembersToSymbolMap(sm, d.Name, d.Members)
}
for _, d := range l.StructDecls {
// Skip anonymous structs, since we add them to the symbol map as method
// parameter structs
if d.Anonymous {
continue
}
sm[d.Name] = &symbolInfo{
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
typeInfo: Type{
Kind: IdentifierType,
Identifier: &IdentifierTypeInfo{
IsDecl: true,
Name: d.Name,
Kind: StructKind,
},
Attrs: d.Attrs,
},
}
a.addMembersToSymbolMap(sm, d.Name, d.Members)
}
for _, d := range l.TableDecls {
sm[d.Name] = &symbolInfo{
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
typeInfo: Type{
Kind: IdentifierType,
Identifier: &IdentifierTypeInfo{
IsDecl: true,
Name: d.Name,
Kind: TableKind,
},
Attrs: d.Attrs,
},
}
a.addMembersToSymbolMap(sm, d.Name, d.Members)
}
for _, d := range l.UnionDecls {
sm[d.Name] = &symbolInfo{
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
typeInfo: Type{
Kind: IdentifierType,
Identifier: &IdentifierTypeInfo{
IsDecl: true,
Name: d.Name,
Kind: UnionKind,
},
Attrs: d.Attrs,
},
}
a.addMembersToSymbolMap(sm, d.Name, d.Members)
}
for _, d := range l.TypeAliasDecls {
sm[d.Name] = &symbolInfo{
name: d.Name,
definition: a.fidlLocToStateLoc(d.Loc),
typeInfo: Type{
Kind: IdentifierType,
Identifier: &IdentifierTypeInfo{
IsDecl: true,
Name: d.Name,
Kind: TypeAliasKind,
TypeAlias: &TypeAliasTypeInfo{
Type: d.TypeCtor.Type(),
},
},
Attrs: d.Attrs,
},
}
}
return sm, nil
}
func (a *Analyzer) addMembersToSymbolMap(sm symbolMap, declName string, members []member) {
for _, m := range members {
memberName := fmt.Sprintf("%s.%s", declName, m.Name)
sm[memberName] = &symbolInfo{
name: m.Name,
definition: a.fidlLocToStateLoc(m.Loc),
typeInfo: m.Type.Type(),
}
sm[memberName].typeInfo.FromTypeAlias = m.FromTypeAlias.TypeAlias()
sm[memberName].typeInfo.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 the `BuildRootDir`.
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},
},
}
}
func (d declType) Type() Type {
t := Type{Kind: d.Kind}
switch d.Kind {
default:
// Sometimes this will be called with a zeroed declType, e.g. for bits
// and enum members, which don't have types.
return t
case ArrayType:
t.Array = &ArrayTypeInfo{
ElementType: d.ElementType.Type(),
ElementCount: d.ElementCount,
}
case VectorType:
t.Vector = &VectorTypeInfo{
ElementType: d.ElementType.Type(),
ElementCount: d.MaybeElementCount,
Nullable: d.Nullable,
}
case StringType:
t.String = &StringTypeInfo{
ElementCount: d.MaybeElementCount,
Nullable: d.Nullable,
}
case HandleType:
t.Handle = &HandleTypeInfo{
Subtype: HandleSubtype(d.Subtype),
Rights: d.Rights,
Nullable: d.Nullable,
}
case RequestType:
t.Request = &RequestTypeInfo{
Subtype: d.Subtype,
Nullable: d.Nullable,
}
case PrimitiveType:
t.Primitive = &PrimitiveTypeInfo{
Subtype: PrimitiveSubtype(d.Subtype),
}
case IdentifierType:
t.Identifier = &IdentifierTypeInfo{
Identifier: d.Identifier,
Nullable: d.Nullable,
}
}
return t
}
func (t typeCtor) Type() Type {
switch t.Name {
case string(ArrayType):
ty := Type{
Kind: ArrayType,
Array: &ArrayTypeInfo{},
}
if len(t.Args) > 0 {
ty.Array.ElementType = t.Args[0].Type()
}
if t.Size != nil {
if count, err := strconv.ParseUint(t.Size.Value, 10, 32); err == nil {
ty.Array.ElementCount = uint(count)
}
}
return ty
case string(VectorType):
ty := Type{
Kind: VectorType,
Vector: &VectorTypeInfo{
Nullable: t.Nullable,
},
}
if len(t.Args) > 0 {
ty.Vector.ElementType = t.Args[0].Type()
}
if t.Size != nil {
if count, err := strconv.ParseUint(t.Size.Value, 10, 32); err == nil {
countUint := uint(count)
ty.Vector.ElementCount = &countUint
}
}
return ty
case string(StringType):
ty := Type{
Kind: StringType,
String: &StringTypeInfo{
Nullable: t.Nullable,
},
}
if t.Size != nil {
if count, err := strconv.ParseUint(t.Size.Value, 10, 32); err == nil {
countUint := uint(count)
ty.String.ElementCount = &countUint
}
}
return ty
case string(HandleType):
ty := Type{
Kind: HandleType,
Handle: &HandleTypeInfo{
Nullable: t.Nullable,
},
}
if t.HandleSubtype != nil {
ty.Handle.Subtype = *t.HandleSubtype
}
return ty
case string(RequestType):
ty := Type{
Kind: RequestType,
Request: &RequestTypeInfo{
Nullable: t.Nullable,
},
}
if len(t.Args) > 0 {
ty.Request.Subtype = t.Args[0].Name
}
return ty
case string(Bool), Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64, Float32, Float64:
return Type{
Kind: PrimitiveType,
Primitive: &PrimitiveTypeInfo{
Subtype: PrimitiveSubtype(t.Name),
},
}
default:
// We assume it is the name of an identifier type.
return Type{
Kind: IdentifierType,
Identifier: &IdentifierTypeInfo{
Identifier: t.Name,
Nullable: t.Nullable,
},
}
}
}
func (t typeCtor) TypeAlias() *string {
if t.Name == "" {
return nil
}
return &t.Name
}