blob: b5165f580433d839840337905405a1274ea2383a [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 (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"fidl-lsp/third_party/fidlgen"
"fidl-lsp/state"
)
// CompiledLibraries represents a deserialized fidl_project.json.
// This is a file that declares all the FIDL libraries the language server
// should be aware of, and includes information on how to build them and find
// their compiled JSON IR.
type CompiledLibraries []CompiledLibrary
// CompiledLibrary represents the build information for a single FIDL library,
// including its name, its constituent files, its dependencies, and the absolute
// filepath to its compiled JSON IR.
type CompiledLibrary struct {
Name string
Files []string
Deps []string
JSON string
}
// LoadFidlProject builds a CompiledLibraries out of a fidl_project.json file.
func LoadFidlProject(fidlProjectPath string) (CompiledLibraries, error) {
data, err := ioutil.ReadFile(fidlProjectPath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %s", err)
}
var libraries CompiledLibraries
if err := json.Unmarshal(data, &libraries); err != nil {
return nil, fmt.Errorf("failed to deserialize compiled libraries: %s", err)
}
return libraries, nil
}
// A libraryMap stores information derived about a group of libraries.
type libraryMap map[fidlgen.LibraryName]libraryInfo
type libraryInfo struct {
// FIDL files that make up the library (relative to dir, or absolute).
files []string
// Direct dependencies (imported by "using" declarations in files).
deps []fidlgen.LibraryName
}
// empty returns true if i represents an empty library (has no files).
func (i *libraryInfo) empty() bool {
return len(i.files) == 0
}
// FindDeps analyzes the file at `path`, locates its dependencies, and returns
// the --files args for a fidlc invocation that will compile `file`'s library.
func (a *Analyzer) FindDeps(fs *state.FileSystem, path state.FileID) ([]string, error) {
result := make(libraryMap)
// Start with a stack containing the root library (if it's a platform source
// library, meaning we can expect to find it in the tree) and its imports.
var stack []fidlgen.LibraryName
file, err := fs.File(path)
if err != nil {
return nil, fmt.Errorf("could not find file `%s`", path)
}
rootLibraryMatch, rootLibraryOk := state.ParseLibraryMatch(file)
if !rootLibraryOk {
return nil, fmt.Errorf("no library found for file %s", file)
}
stack = append(stack, fidlgen.LibraryName(rootLibraryMatch.Lib))
rootImports := state.ParsePlatformImportsMatch(file)
for _, m := range rootImports {
stack = append(stack, fidlgen.LibraryName(m.Lib))
}
// Explore all dependencies via depth-first search.
for len(stack) != 0 {
// Pop a library and check if it needs processing.
var libName fidlgen.LibraryName
libName, stack = stack[len(stack)-1], stack[:len(stack)-1]
if _, ok := result[libName]; ok {
continue
}
// Find the library's files and dependencies.
var info libraryInfo
// We don't call getLibraryWithFile here because these libraries are
// found either by searching for regex patterns of library imports, or
// by looking in a library's dependencies, which are only stored as bare
// library names.
if lib, ok := a.getLibrary(libName); ok {
// Add the library's files to info.files
for file := range lib.files {
// Unless that file is a duplicate of one that is open
if file == string(path) {
continue
}
info.files = append(info.files, file)
}
// Add each of the library's dependencies to the stack
for dep := range lib.deps {
info.deps = append(info.deps, dep)
stack = append(stack, dep)
}
} else {
return nil, fmt.Errorf("could not find deps for library `%s`", libName)
}
if !info.empty() {
result[libName] = info
}
}
// `result` is now a libraryMap of the dependencies for the current file
// Now do a topological sort on libraryMap (this will give us a list of
// files in order of dependency for each library) and reverse order this
// to get the correct list of --files args to return
fileArgs, err := result.fidlcFileArgs()
if err != nil {
return nil, fmt.Errorf("error in getting fidlc --files args: %s", err)
}
return fileArgs, nil
}
// fidlcFileArgs returns a list of arguments to pass to fidlc to compile the
// libraries in m. This is a fairly expensive operation. It returns an error if
// there is an import cycle.
func (m libraryMap) fidlcFileArgs() ([]string, error) {
sorted, err := m.topologicalSort()
if err != nil {
return nil, err
}
// Iterate in reverse order so that dependencies come before dependents.
var args []string
for i := len(sorted) - 1; i >= 0; i-- {
lib := sorted[i]
args = append(args, "--files")
info := m[lib]
for _, file := range info.files {
args = append(args, file)
}
}
return args, nil
}
// topologicalSort runs topological sort on m, whose deps fields define a
// directed graph in adjacency list form. In the resulting list, libraries only
// depend on other libraries later in the list, not on earlier ones. Returns
// an error if there is a cycle.
func (m libraryMap) topologicalSort() ([]fidlgen.LibraryName, error) {
var sorted []fidlgen.LibraryName
incoming := map[fidlgen.LibraryName]int{}
for u := range m {
incoming[u] = 0
}
for _, info := range m {
for _, v := range info.deps {
incoming[v]++
}
}
var src []fidlgen.LibraryName
for u, deg := range incoming {
if deg == 0 {
src = append(src, u)
}
}
for len(src) != 0 {
var u fidlgen.LibraryName
u, src = src[len(src)-1], src[:len(src)-1]
sorted = append(sorted, u)
for _, v := range m[u].deps {
incoming[v]--
if incoming[v] == 0 {
src = append(src, v)
}
}
}
for _, deg := range incoming {
if deg != 0 {
return nil, errors.New("detected an import cycle")
}
}
return sorted, nil
}