blob: efad874305d1fe5f87ddc9d97b0430ba6dd218ae [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 main
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
)
// A library is a FIDL library represented by its name (as spelled in its
// "library" declaration, not necessarily matching directory or file names).
type library string
// The zx library describes Zircon syscalls.
const zxLibrary library = "zx"
// A libraryMap stores information derived about a group of libraries.
type libraryMap map[library]libraryInfo
type libraryInfo struct {
// Directory containing the library.
dir string
// FIDL files that make up the library (relative to dir, or absolute).
files []string
// Direct dependencies (imported by "using" declarations in files).
deps []library
}
// A libraryCache is a thread-safe, append-only libraryMap.
type libraryCache struct {
sync.RWMutex
libs libraryMap
}
func (c *libraryCache) get(lib library) (libraryInfo, bool) {
c.RLock()
defer c.RUnlock()
info, ok := c.libs[lib]
return info, ok
}
func (c *libraryCache) storeNew(libs libraryMap) {
// Avoid write contention by skipping the operation if all the libraries in
// libs are already in the cache.
if !c.hasNew(libs) {
return
}
c.Lock()
defer c.Unlock()
for lib, info := range libs {
if _, ok := c.libs[lib]; !ok {
c.libs[lib] = info
}
}
}
func (c *libraryCache) hasNew(libs libraryMap) bool {
c.RLock()
defer c.RUnlock()
for lib := range libs {
if _, ok := c.libs[lib]; !ok {
return true
}
}
return false
}
// libraryAnalyzer analyzes FIDL files to infer library dependencies. It caches
// information to speed up future analyses.
type libraryAnalyzer struct {
// Paths in which to search for FIDL library files. Given library foo.bar,
// the analyzer will check P/foo.bar and P/foo-bar for each path P in paths.
// If one of these exists, it reads all descendent .fidl files.
paths []string
// Global cache of library information.
cache libraryCache
}
func newLibraryAnalyzer(paths []string) *libraryAnalyzer {
return &libraryAnalyzer{
paths: paths,
cache: libraryCache{
libs: make(libraryMap),
},
}
}
const existingLibraryNotice = "" +
"NOTE: Your file will be compiled with the others in this library. " +
"If you don't want this behavior, choose a different library name."
// analyze analyzes content, returning a libraryMap that includes content's
// library together with all its transitive dependencies, and annotations on the
// "library" and "using" declarations indicating their inferred location. It
// assumes that filename is a FIDL file containing content. If any library
// cannot be located in a.paths, it is silently ignored.
func (a *libraryAnalyzer) analyze(filename, content string) (libraryMap, []Annotation) {
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 []library
rootLibraryMatch, rootLibraryOk := parseLibraryMatch(content)
if rootLibraryOk && rootLibraryMatch.lib.isPlatform() {
stack = append(stack, rootLibraryMatch.lib)
}
rootImports := parsePlatformImportsMatch(content)
for _, m := range rootImports {
stack = append(stack, m.lib)
}
// Explore all dependencies via depth-first search.
for len(stack) != 0 {
// Pop a library and check if it needs processing.
var lib library
lib, stack = stack[len(stack)-1], stack[:len(stack)-1]
if _, ok := result[lib]; ok {
continue
}
// Check the analyzer's cache.
if info, ok := a.cache.get(lib); ok {
result[lib] = info
stack = append(stack, info.deps...)
continue
}
// Find the library's files and dependencies.
var info libraryInfo
for _, path := range a.paths {
for _, dir := range lib.possibleDirs() {
if !info.empty() {
break
}
info.dir = filepath.Join(path, dir)
// Keep track of already-seen imports to ensure that info.deps
// has no duplicates (there won't be duplicates in a valid FIDL
// file, but separate files might import the same library).
seen := map[library]struct{}{}
// Visit all .fidl files, including in subdirectories:
filepath.Walk(info.dir, func(file string, fi os.FileInfo, err error) error {
if err != nil || fi.IsDir() || filepath.Ext(file) != ".fidl" {
return err
}
rel, err := filepath.Rel(info.dir, file)
if err != nil {
return err
}
b, err := ioutil.ReadFile(file)
if err != nil {
return err
}
declaredLib, ok := parseLibrary(b)
if !ok || declaredLib != lib {
return nil
}
info.files = append(info.files, rel)
for _, dep := range parsePlatformImports(b) {
if _, ok := seen[dep]; ok {
continue
}
seen[dep] = struct{}{}
info.deps = append(info.deps, dep)
stack = append(stack, dep)
}
return nil
})
}
}
if !info.empty() {
result[lib] = info
}
}
// Cache reusable results first, before adding request-specific information.
a.cache.storeNew(result)
// The following handles two cases:
// 1. (More common) The user typed something like `library foo;`. This
// is a new library outside the source tree, consisting of one file.
// 2. The user typed something like `library fuchsia.io;`. This behaves as
// if they were adding a new file to the fuchsia.io library, alongside
// the other ones in the source tree.
rootLib := rootLibraryMatch.lib
if !rootLibraryOk {
// The name doesn't really matter. We just need an entry in the map.
rootLib = "fidlbolt"
}
var anns []Annotation
rootInfo := libraryInfo{files: []string{filename}}
for _, m := range rootImports {
rootInfo.deps = append(rootInfo.deps, m.lib)
if info, ok := result[m.lib]; ok {
text := fmt.Sprintf("Found %s in %s", m.lib, info.userVisibleDir())
anns = append(anns, m.annotation(Info, text))
}
}
if info, ok := result[rootLib]; ok {
rootInfo = mergeInfo(rootInfo, info)
if rootLibraryOk {
text := fmt.Sprintf("Found %s in %s\n\n%s",
rootLib, info.userVisibleDir(), existingLibraryNotice)
anns = append(anns, rootLibraryMatch.annotation(Info, text))
}
}
result[rootLib] = rootInfo
return result, anns
}
// 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 {
// When the user declares their file as belong to an existing
// library, their file will be an absolute path in a temp directory.
// Otherwise, all files are relative to info.dir.
if filepath.IsAbs(file) {
args = append(args, file)
} else {
args = append(args, filepath.Join(info.dir, file))
}
}
}
return args, nil
}
// dependencyGraph returns a map representing the the dependency graph of m.
// Each key is a library name, and each value is another map whose keys are its
// direct dependencies.
func (m libraryMap) dependencyGraph() (map[string]interface{}, error) {
// Make sure there are no cycles, otherwise this will never terminate.
if _, err := m.topologicalSort(); err != nil {
return nil, err
}
sources := map[library]struct{}{}
for lib := range m {
sources[lib] = struct{}{}
}
for _, info := range m {
for _, dep := range info.deps {
delete(sources, dep)
}
}
var start []library
for lib := range sources {
start = append(start, lib)
}
var gen func(libs []library) map[string]interface{}
gen = func(libs []library) map[string]interface{} {
g := map[string]interface{}{}
for _, lib := range libs {
g[string(lib)] = gen(m[library(lib)].deps)
}
return g
}
return gen(start), 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() ([]library, error) {
var sorted []library
incoming := map[library]int{}
for u := range m {
incoming[u] = 0
}
for _, info := range m {
for _, v := range info.deps {
incoming[v]++
}
}
var src []library
for u, deg := range incoming {
if deg == 0 {
src = append(src, u)
}
}
for len(src) != 0 {
var u library
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
}
// possibleDirs returns directory names that lib's files might be found in.
func (lib library) possibleDirs() []string {
if lib == zxLibrary {
// The zx library is a special case: its .fidl files are in a
// directory named "vsdo" (fuchsia/zircon/vdso), not "zx".
return []string{"vdso"}
}
return []string{string(lib), strings.ReplaceAll(string(lib), ".", "-")}
}
// isPlatform returns true if lib adheres to the naming scheme for platform
// source libraries (enforced by the FIDL linter).
func (lib library) isPlatform() bool {
s := string(lib)
return strings.HasPrefix(s, "fidl.") ||
strings.HasPrefix(s, "fuchsia.") ||
strings.HasPrefix(s, "test.")
}
// empty returns true if i represents an empty library (has no files).
func (i *libraryInfo) empty() bool {
return len(i.files) == 0
}
// userVisibleDir returns a representation of i.dir to show to the user.
func (i *libraryInfo) userVisibleDir() string {
dir := filepath.ToSlash(i.dir)
if i := strings.Index(dir, "/fuchsia/"); i != -1 {
return dir[i+1:]
}
return dir
}
// mergeInfo returns a combination of the information in info and other. It
// assumes that only one of them has dir set.
func mergeInfo(info, other libraryInfo) libraryInfo {
if info.dir != "" && other.dir != "" {
panic("mergeInfo called with incompatible dirs")
}
if info.dir == "" {
info.dir = other.dir
}
info.files = append(info.files, other.files...)
deps := map[library]struct{}{}
for _, lib := range info.deps {
deps[lib] = struct{}{}
}
for _, lib := range other.deps {
if _, ok := deps[lib]; !ok {
info.deps = append(info.deps, lib)
}
}
return info
}
var (
// https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/language.md#identifiers
fidlIdentifierPattern = `[a-zA-Z](?:[a-zA-Z0-9_]*[a-zA-Z0-9])?`
// Although "library" can be used anywhere (e.g. as a type name), this regex
// is robust because the the library declaration must appear at the top of
// the file (only comments and whitespace can precede it).
libraryRegexp = regexp.MustCompile(`` +
`^(?:\s*//[^\n]*\n)*\s*` +
`library\s+(` +
fidlIdentifierPattern +
`(?:\.` + fidlIdentifierPattern + `)*` +
`)\s*;`)
// Although "using" can be used anywhere (e.g. as a type name), this regex
// is robust because it only matches imports of platform libraries (ones
// that start with fidl, fuchsia, or test) and the special library zx. The
// only problem is it can match commented imports, so we have to check for
// this after matching.
platformImportRegexp = regexp.MustCompile(`` +
`\busing\s+(` +
`zx|` +
`(?:fidl|fuchsia|test)` +
`(?:\.` + fidlIdentifierPattern + `)+` +
`)` +
`(?:\s+as\s+` + fidlIdentifierPattern + `)?` +
`\s*;`)
)
type libraryMatch struct {
lib library
line, column int
}
func parseLibrary(fidl []byte) (library, bool) {
m := libraryRegexp.FindSubmatch(fidl)
if m == nil {
return "", false
}
return library(m[1]), true
}
func parseLibraryMatch(fidl string) (libraryMatch, bool) {
m := libraryRegexp.FindStringSubmatchIndex(fidl)
if m == nil {
return libraryMatch{}, false
}
return toLibraryMatch(fidl, m[2], m[3]), true
}
func parsePlatformImports(fidl []byte) []library {
var libs []library
for _, m := range platformImportRegexp.FindAllSubmatchIndex(fidl, -1) {
// Make sure the line isn't commented. Technically this breaks if the
// line contains a string const with "//" in it. This isn't a big deal
// because imports are supposed to come before all other declarations.
i := bytes.LastIndexByte(fidl[:m[2]], '\n')
if i == -1 || !bytes.Contains(fidl[i:m[2]], []byte("//")) {
libs = append(libs, library(fidl[m[2]:m[3]]))
}
}
return libs
}
func parsePlatformImportsMatch(fidl string) []libraryMatch {
var libs []libraryMatch
for _, m := range platformImportRegexp.FindAllStringSubmatchIndex(fidl, -1) {
// See the comment in parsePlatformImports.
i := strings.LastIndexByte(fidl[:m[2]], '\n')
if i == -1 || !strings.Contains(fidl[i:m[2]], "//") {
libs = append(libs, toLibraryMatch(fidl, m[2], m[3]))
}
}
return libs
}
func toLibraryMatch(fidl string, start, end int) libraryMatch {
return libraryMatch{
lib: library(fidl[start:end]),
line: strings.Count(fidl[:start], "\n") + 1,
column: start - strings.LastIndexByte(fidl[:start], '\n'),
}
}
func (m *libraryMatch) annotation(kind AnnotationKind, text string) Annotation {
return Annotation{
Line: m.line,
Column: m.column,
Kind: kind,
Text: text,
}
}