blob: 3063f59b53f7b4f0aa2b41d8e05e3be38d4c6770 [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"
"io/ioutil"
"strings"
fidlcommon "fidl-lsp/third_party/common"
"fidl-lsp/state"
)
// DefinitionOfSymbol returns the location (or locations) where the given symbol
// is defined, as long as that location is available to the Analyzer (i.e. in
// a.libs somewhere).
//
// For a library name, e.g. `fuchsia.foo`, DefinitionOfSymbol returns the
// locations of the `library` declarations for each file in that library.
//
// DefinitionOfSymbol only works for library names and top-level names: enums,
// bits, structs, protocols, etc. -- not for member names. For example, it can
// find the definition of `Foo` in the following example:
//
// protocol P {
// Method(Foo foo);
// };
//
// But it can't find the definition of `B.FOO`:
//
// bits B {
// FOO = 0;
// }
// const ZERO = B.FOO;
func (a *Analyzer) DefinitionOfSymbol(fs *state.FileSystem, sym state.Symbol) ([]state.Location, error) {
// If `sym` is a library name, return locations pointing at all the files in
// the library (specifically, pointing at their `library` declarations).
libName, err := fidlcommon.ReadLibraryName(sym.Name)
if err == nil {
if _, isLib := a.libs[libName]; isLib {
return a.declarationsOfLibrary(fs, libName), nil
}
}
// Otherwise, we assume it is a local or fully-qualified name
name, err := a.symbolToFullyQualifiedName(fs, sym)
if err != nil {
return nil, fmt.Errorf(
"could not convert symbol `%s` to fully-qualified name: %s",
sym.Name,
err,
)
}
lib, ok := a.libs[name.LibraryName()]
if !ok {
return nil, fmt.Errorf("unknown library `%s`", name.LibraryName().FullyQualifiedName())
}
if lib.ir == nil {
if err := a.importLibrary(lib.json); err != nil {
return nil, fmt.Errorf(
"error importing library `%s`: %s",
name.LibraryName().FullyQualifiedName(),
err,
)
}
}
// Check that we have a symbolMap for this library, and look up this
// symbol's definition location in that symbolMap.
if lib.symbols == nil {
return nil, fmt.Errorf(
"no symbol map for library `%s`",
name.LibraryName().FullyQualifiedName(),
)
}
symInfo, ok := lib.symbols[name.FullyQualifiedName()]
if !ok {
return nil, fmt.Errorf("could not find definition of symbol `%s`", name.FullyQualifiedName())
}
return []state.Location{symInfo.definition}, nil
}
func (a *Analyzer) declarationsOfLibrary(fs *state.FileSystem, lib fidlcommon.LibraryName) []state.Location {
files := []state.FileID{}
for file := range a.libs[lib].files {
// These files are all absolute paths, but some are to temporary input
// files used by the Analyzer. For these, we want to convert the path
// to the document URI the editor knows about.
if fileID, err := a.inputFileToFileID(file); err != nil {
files = append(files, state.FileID(file))
} else {
files = append(files, fileID)
}
}
locs := []state.Location{}
// Find the `library` declaration in each file
for _, fileID := range files {
file, err := fs.File(fileID)
if err != nil {
// If we don't have the file in memory, read it in
bytes, err := ioutil.ReadFile(string(fileID))
if err != nil {
continue
}
file = string(bytes)
}
if libraryMatch, ok := state.ParseLibraryMatch(file); ok {
locs = append(locs, state.Location{
FileID: fileID,
Range: libraryMatch.Range,
})
}
}
return locs
}
func (a *Analyzer) symbolToFullyQualifiedName(fs *state.FileSystem, sym state.Symbol) (fidlcommon.Name, error) {
// If `sym` is a local name (not fully-qualified), we create a FQN by
// attaching its library name.
var fqn string
if !strings.Contains(sym.Name, ".") {
file, err := fs.File(sym.Location.FileID)
if err != nil {
return fidlcommon.Name{}, fmt.Errorf("could not open file `%s`", sym.Location.FileID)
}
libName, err := state.LibraryOfFile(file)
if err != nil {
return fidlcommon.Name{}, fmt.Errorf(
"could not find library of symbol `%s` in file `%s`",
sym.Name,
sym.Location.FileID,
)
}
fqn = libName.FullyQualifiedName() + "/" + sym.Name
} else {
// If the symbol contains '.', we assume it is a fully-qualified name.
i := strings.LastIndex(sym.Name, ".")
fqn = sym.Name[:i] + "/" + sym.Name[i+1:]
}
// Convert `fqn` to a fidlcommon.Name.
name, err := fidlcommon.ReadName(fqn)
if err != nil {
return fidlcommon.Name{}, fmt.Errorf("could not read fully-qualified name `%s`: %s", fqn, err)
}
return name, nil
}