Merge history from vscode-language-fidl
Change-Id: I7fc7e4e9a46f2e084e3d3efc9530da18165a4907
diff --git a/.gitignore b/.gitignore
index 21d8cfd..0785e8e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+*.pyc
+__pycache__
+
node_modules/
out/
/language-fidl-*.vsix
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..c2a4eac
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,8 @@
+# This is the list of Fuchsia Authors.
+
+# Names should be added to this file as one of
+# Organization's name
+# Individual's name <submission email address>
+# Individual's name <submission email address> <email2> <emailN>
+
+Google Inc.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..81e2938
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,9 @@
+This repository accepts contributions using Gerrit.
+
+Instructions for using Gerrit:
+
+ * https://gerrit-review.googlesource.com/Documentation/
+
+Before we can land your change, you need to sign the Google CLA:
+
+ * https://cla.developers.google.com/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6d6ddbc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/PATENTS b/PATENTS
new file mode 100644
index 0000000..2746e78
--- /dev/null
+++ b/PATENTS
@@ -0,0 +1,22 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Fuchsia project.
+
+Google hereby grants to you a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this
+section) patent license to make, have made, use, offer to sell, sell,
+import, transfer, and otherwise run, modify and propagate the contents
+of this implementation of Fuchsia, where such license applies only to
+those patent claims, both currently owned by Google and acquired in
+the future, licensable by Google that are necessarily infringed by
+this implementation. This grant does not include claims that would be
+infringed only as a consequence of further modification of this
+implementation. If you or your agent or exclusive licensee institute
+or order or agree to the institution of patent litigation or any other
+patent enforcement activity against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that this
+implementation of Fuchsia constitutes direct or contributory patent
+infringement, or inducement of patent infringement, then any patent
+rights granted to you under this License for this implementation of
+Fuchsia shall terminate as of the date such litigation is filed.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8b90b74
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+Fuchsia Open Source Template Repository
+=======================================
+
+This repository is a template that we will use when creating new open source
+repositories for Fuchsia.
diff --git a/fidl-lsp/README.md b/fidl-lsp/README.md
new file mode 100644
index 0000000..7b051a3
--- /dev/null
+++ b/fidl-lsp/README.md
@@ -0,0 +1,101 @@
+# fidl-lsp
+
+`fidl-lsp` is an [LSP](https://microsoft.github.io/language-server-protocol/)
+Language Server for [FIDL](https://fuchsia.dev/fuchsia-src/development/languages/fidl).
+
+## Architecture
+
+The general flow of control in the language server goes like this:
+* A request/notification is received in `langserver/handler.go`
+* If it is a notification (changing state of the open files):
+ * The `LangHandler` tells the `FileSystem` to make the change
+ * The `LangHandler` triggers a re-analysis by the `Analyzer`
+* If it is a request for some language feature:
+ * The `LangHandler` dispatches the request to the `Analyzer`
+ * The `Analyzer` extracts the needed information and returns it
+ * The `LangHandler` sends the response to the client
+
+### `langserver` package
+
+This package includes the core logic of the language server. It deals with LSP specific boilerplate, JSON-RPC messages, etc. It includes the `LangHandler`
+type, which handles LSP requests, sends reponses and notifications to the
+client, and dispatches changes to the state manager or requests for analyses to the `Analyzer`.
+
+### `state` package
+
+The language server's state management is in the `state` package. Currently this is just an in memory file system (mapping of editor file names to file text).
+This could be wrapped in e.g. an `RWLock` to enable concurrent handling of LSP requests and notifications.
+
+### `analysis` package
+
+The "backend" of the server, which doesn't know about LSP but knows how
+to compile FIDL and analyze the JSON IR.
+
+Includes the `Analyzer` type, which maintains a set of compiled FIDL `Library`s
+and their constituent files, dependencies, and build artifacts. Main entry point
+is `Analyzer.Analyze()`, which is called every time a file is changed on the client. `Analyze` recompiles the relevant FIDL library for that file and imports the JSON IR.
+
+The `Analyzer` compiles FIDL libraries by invoking `fidlc` in a separate
+process, gets diagnostics from `fidlc` and `fidl-lint`, and uses `fidl-format`
+for formatting. It locates dependencies using a `fidl_project.json` file, the
+path to which will be configurable in the LSP client extension. `fidl_project.json` declares all FIDL libraries the language server should be aware
+of, the paths to their constituent files, the path to their JSON IR,
+and their dependencies (by library name).
+
+## Supported LSP features
+
+This list documents LSP features supported by fidl-lsp.
+
+### General
+- [x] [initialize](https://microsoft.github.io/language-server-protocol/specification#initialize)
+- [x] [initialized](https://microsoft.github.io/language-server-protocol/specification#initialized)
+- [x] [shutdown](https://microsoft.github.io/language-server-protocol/specification#shutdown)
+- [ ] [exit](https://microsoft.github.io/language-server-protocol/specification#exit)
+- [ ] [$/cancelRequest](https://microsoft.github.io/language-server-protocol/specification#cancelRequest)
+
+### Workspace
+- [ ] [workspace/workspaceFolders](https://microsoft.github.io/language-server-protocol/specification#workspace_workspaceFolders)
+- [ ] [workspace/didChangeWorkspaceFolders](https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeWorkspaceFolders)
+- [ ] [workspace/didChangeConfiguration](https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeConfiguration)
+- [ ] [workspace/configuration](https://microsoft.github.io/language-server-protocol/specification#workspace_configuration)
+- [ ] [workspace/didChangeWatchedFiles](https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeWatchedFiles)
+- [ ] [workspace/symbol](https://microsoft.github.io/language-server-protocol/specification#workspace_symbol)
+- [ ] [workspace/applyEdit](https://microsoft.github.io/language-server-protocol/specification#workspace_applyEdit)
+
+### Text Synchronization
+- [x] [textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specification#textDocument_didOpen)
+- [x] [textDocument/didChange](https://microsoft.github.io/language-server-protocol/specification#textDocument_didChange)
+- [ ] [textDocument/willSave](https://microsoft.github.io/language-server-protocol/specification#textDocument_willSave)
+- [ ] [textDocument/willSaveWaitUntil](https://microsoft.github.io/language-server-protocol/specification#textDocument_willSaveWaitUntil)
+- [ ] [textDocument/didSave](https://microsoft.github.io/language-server-protocol/specification#textDocument_didSave)
+- [ ] [textDocument/didClose](https://microsoft.github.io/language-server-protocol/specification#textDocument_didClose)
+
+### Diagnostics
+- [x] [textDocument/publishDiagnostics](https://microsoft.github.io/language-server-protocol/specification#textDocument_publishDiagnostics)
+
+### Language Features
+- [ ] [textDocument/completion](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion)
+- [ ] [completionItem/resolve](https://microsoft.github.io/language-server-protocol/specification#completionItem_resolve)
+- [x] [textDocument/hover](https://microsoft.github.io/language-server-protocol/specification#textDocument_hover)
+- [ ] [textDocument/signatureHelp](https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp)
+- [ ] [textDocument/declaration](https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration)
+- [x] [textDocument/definition](https://microsoft.github.io/language-server-protocol/specification#textDocument_definition)
+- [ ] [textDocument/typeDefinition](https://microsoft.github.io/language-server-protocol/specification#textDocument_typeDefinition)
+- [ ] [textDocument/implementation](https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation)
+- [x] [textDocument/references](https://microsoft.github.io/language-server-protocol/specification#textDocument_references)
+- [ ] [textDocument/documentHighlight](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentHighlight)
+- [ ] [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol)
+- [ ] [textDocument/codeAction](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction)
+- [ ] [textDocument/selectionRange](https://github.com/Microsoft/language-server-protocol/issues/613)
+- [ ] [textDocument/codeLens](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens)
+- [ ] [codeLens/resolve](https://microsoft.github.io/language-server-protocol/specification#codeLens_resolve)
+- [x] [textDocument/documentLink](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentLink)
+- [ ] [documentLink/resolve](https://microsoft.github.io/language-server-protocol/specification#documentLink_resolve)
+- [ ] [textDocument/documentColor](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentColor)
+- [ ] [textDocument/colorPresentation](https://microsoft.github.io/language-server-protocol/specification#textDocument_colorPresentation)
+- [x] [textDocument/formatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting)
+- [ ] [textDocument/rangeFormatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_rangeFormatting)
+- [ ] [textDocument/onTypeFormatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_onTypeFormatting)
+- [ ] [textDocument/rename](https://microsoft.github.io/language-server-protocol/specification#textDocument_rename)
+- [ ] [textDocument/prepareRename](https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename)
+- [ ] [textDocument/foldingRange](https://microsoft.github.io/language-server-protocol/specification#textDocument_foldingRange)
diff --git a/fidl-lsp/analysis/analyzer.go b/fidl-lsp/analysis/analyzer.go
new file mode 100644
index 0000000..8bbb4c7
--- /dev/null
+++ b/fidl-lsp/analysis/analyzer.go
@@ -0,0 +1,212 @@
+// 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"
+ "hash/fnv"
+ "io/ioutil"
+ "log"
+ "os"
+
+ fidlcommon "fidl-lsp/third_party/common"
+
+ "fidl-lsp/state"
+)
+
+// Config keeps track of where the Analyzer can find FIDL tools, as well as the
+// root build directory, to which all locations in CompiledLibraries are
+// relative.
+// This config will be exposed where possible as settings in FIDL LSP client IDE
+// extensions.
+type Config struct {
+ BuildRootDir string
+ FidlcPath string
+ FidlLintPath string
+ FidlFormatPath string
+}
+
+// A Library is a set of information about a FIDL library, including build
+// information (files and dependencies), the location of its JSON IR if it
+// exists (`json`), its deserialized JSON IR (`ir`), and any diagnostics on it.
+// `deps` and `files` are treated as sets.
+type Library struct {
+ deps map[fidlcommon.LibraryName]bool
+ // This library's constituent files in the form of absolute paths
+ files map[string]bool
+ json string
+ ir *FidlLibrary // nil if not yet imported
+ diags map[state.FileID][]Diagnostic
+ symbols symbolMap
+}
+
+// Analyzer compiles FIDL libraries and extracts analyses from them, including
+// diagnostics.
+// * inputFilesFIDL is a map from documents open in the editor to the absolute
+// filepath of the corresponding temporary file that Analyzer uses as input
+// for fidlc, etc.
+// * inputFileJSON is a map from FIDL library name to an absolute filepath to
+// its JSON IR.
+type Analyzer struct {
+ // Where to find the fuchsia directory and FIDL tools.
+ cfg Config
+
+ // A map from library name (string), like "zx", to a set of data about that
+ // library: the FIDL files it includes, its JSON IR, its dependencies, its
+ // JSON IR, and diagnostics returned on it from fidlc and fidl-lint.
+ libs map[fidlcommon.LibraryName]*Library
+
+ // A map from open files to their corresponding tmp file used by the
+ // Analyzer as an input file for fidlc, fidl-lint, fidl-format, etc.
+ // Key is an lsp.DocumentURI, value is an absolute filepath.
+ inputFilesFIDL map[state.FileID]string
+
+ // A map from library name to the corresponding tmp file used for their JSON
+ // IR.
+ inputFilesJSON map[fidlcommon.LibraryName]string
+}
+
+// NewAnalyzer returns an Analyzer initialized with the set of CompiledLibraries
+// passed in.
+func NewAnalyzer(cfg Config, compiledLibraries CompiledLibraries) *Analyzer {
+ a := &Analyzer{
+ cfg: cfg,
+ libs: make(map[fidlcommon.LibraryName]*Library),
+ inputFilesFIDL: make(map[state.FileID]string),
+ inputFilesJSON: make(map[fidlcommon.LibraryName]string),
+ }
+ for name, lib := range compiledLibraries {
+ libName := fidlcommon.MustReadLibraryName(name)
+ a.libs[libName] = &Library{
+ deps: make(map[fidlcommon.LibraryName]bool, len(lib.Deps)),
+ files: make(map[string]bool, len(lib.Files)),
+ json: lib.JSON,
+ }
+ for _, dep := range lib.Deps {
+ a.libs[libName].deps[fidlcommon.MustReadLibraryName(dep)] = true
+ }
+ for _, file := range lib.Files {
+ a.libs[libName].files[file] = true
+ }
+ }
+ return a
+}
+
+// Analyze compiles and generates an analysis for the file at `path` in the
+// FileSystem. This currently includes compiling the library the file belongs to
+// and obtaining diagnostics on it.
+// Analyze only uses read access to the FileSystem.
+func (a *Analyzer) Analyze(fs *state.FileSystem, path state.FileID) error {
+ inputFilePath, err := a.writeFileToTmp(fs, path)
+ if err != nil {
+ return fmt.Errorf("failed to write file `%s` to tmp: %s", path, err)
+ }
+
+ // Add this file and its deps to its Library
+ // TODO: do this on setup with all files in workspace
+ file, err := fs.File(path)
+ if err != nil {
+ return fmt.Errorf("could not find file `%s`", path)
+ }
+ if libName, err := state.LibraryOfFile(file); err == nil {
+ if _, ok := a.libs[libName]; !ok {
+ a.libs[libName] = &Library{
+ deps: make(map[fidlcommon.LibraryName]bool),
+ files: make(map[string]bool),
+ diags: make(map[state.FileID][]Diagnostic),
+ }
+ }
+
+ lib := a.libs[libName]
+ // If the file currently being edited was already part of lib.files,
+ // delete that and replace it with the temporary/edited version.
+ if absPath, err := state.FileIDToPath(path); err != nil {
+ log.Printf("error parsing: %s\n", err)
+ } else {
+ if _, ok := lib.files[absPath]; ok {
+ delete(lib.files, absPath)
+ }
+ }
+
+ lib.files[inputFilePath] = true
+ imports := state.ParsePlatformImportsMatch(file)
+ for _, dep := range imports {
+ lib.deps[fidlcommon.LibraryName(dep.Lib)] = true
+ }
+ }
+
+ // Compile the file's library
+ compileResult, err := a.compile(fs, path)
+ if err != nil {
+ return fmt.Errorf("error on analysis: %s", err)
+ }
+
+ a.libs[compileResult.lib].diags = compileResult.diags
+ return nil
+}
+
+// Cleanup deletes all temporary files Analyzer created during its lifetime to
+// compile FIDL libraries.
+func (a *Analyzer) Cleanup() {
+ for _, path := range a.inputFilesFIDL {
+ err := os.Remove(path)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ }
+ for _, path := range a.inputFilesJSON {
+ err := os.Remove(path)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ }
+}
+
+// writeFileToTmp writes the in-memory file indexed by `path` to a temporary
+// file on disk, in order to enable invoking some command on it in a separate
+// process, e.g. fidlc, fidl-format, etc.
+func (a *Analyzer) writeFileToTmp(fs *state.FileSystem, path state.FileID) (string, error) {
+ if _, ok := a.inputFilesFIDL[path]; !ok {
+ a.inputFilesFIDL[path] = fmt.Sprintf("/tmp/%d.fidl", hash(string(path)))
+ }
+
+ file, err := fs.File(path)
+ if err != nil {
+ return "", fmt.Errorf("could not find file `%s`", path)
+ }
+
+ // TODO: Can we do 0644 with os.FileMode(os.O_FLAGS)?
+ if err := ioutil.WriteFile(a.inputFilesFIDL[path], []byte(file), 0644); err != nil {
+ return "", fmt.Errorf("error writing file `%s` to tmp directory: %s", path, err)
+ }
+ return a.inputFilesFIDL[path], nil
+}
+
+func (a *Analyzer) inputFileToFileID(inputFilePath string) (state.FileID, error) {
+ for fileID, inputFile := range a.inputFilesFIDL {
+ if inputFile == inputFilePath {
+ return fileID, nil
+ }
+ }
+ return "", fmt.Errorf("could not find input file `%s`", inputFilePath)
+}
+
+// pathToJSON returns the path to the tmp file where the Analyzer saved the JSON
+// IR for the library `lib`, or, if there is not a saved JSON file, generates
+// a path. It is used for fidlc invocations and for importing libraries' JSON
+// IR.
+func (a *Analyzer) pathToJSON(lib fidlcommon.LibraryName) string {
+ if path, ok := a.inputFilesJSON[lib]; ok {
+ return path
+ }
+ a.inputFilesJSON[lib] = fmt.Sprintf("/tmp/%d.json", hash(lib.FullyQualifiedName()))
+ return a.inputFilesJSON[lib]
+}
+
+func hash(s string) uint32 {
+ h := fnv.New32a()
+ h.Write([]byte(s))
+ return h.Sum32()
+}
diff --git a/fidl-lsp/analysis/compile.go b/fidl-lsp/analysis/compile.go
new file mode 100644
index 0000000..9336999
--- /dev/null
+++ b/fidl-lsp/analysis/compile.go
@@ -0,0 +1,497 @@
+// 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"
+ "strconv"
+ "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 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
+}
diff --git a/fidl-lsp/analysis/definition.go b/fidl-lsp/analysis/definition.go
new file mode 100644
index 0000000..566b3be
--- /dev/null
+++ b/fidl-lsp/analysis/definition.go
@@ -0,0 +1,102 @@
+// 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"
+ "log"
+
+ 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,
+ )
+ }
+
+ symInfo, err := a.lookupSymbolInfo(name)
+ if err != nil {
+ return nil, fmt.Errorf("could not find definition of symbol `%s`: %s", name.FullyQualifiedName(), err)
+ }
+
+ 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 {
+ log.Printf("could not read in file `%s`: %s", fileID, err)
+ continue
+ }
+ file = string(bytes)
+ }
+
+ if libraryMatch, ok := state.ParseLibraryMatch(file); ok {
+ locs = append(locs, state.Location{
+ FileID: fileID,
+ Range: libraryMatch.Range,
+ })
+ }
+ }
+ return locs
+}
diff --git a/fidl-lsp/analysis/definition_test.go b/fidl-lsp/analysis/definition_test.go
new file mode 100644
index 0000000..3bd4c2e
--- /dev/null
+++ b/fidl-lsp/analysis/definition_test.go
@@ -0,0 +1,383 @@
+// 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_test
+
+import (
+ "os"
+ "testing"
+
+ "fidl-lsp/analysis"
+ "fidl-lsp/state"
+)
+
+var (
+ fuchsiaDir = os.Getenv("FUCHSIA_DIR")
+ toolsPath = fuchsiaDir + "/out/default.zircon/tools/"
+ fidlcPath = toolsPath + "fidlc"
+ fidlLintPath = toolsPath + "fidl-lint"
+ fidlFormatPath = toolsPath + "fidl-format"
+)
+
+func defaultConfig() analysis.Config {
+ return analysis.Config{
+ BuildRootDir: fuchsiaDir,
+ FidlcPath: fidlcPath,
+ FidlLintPath: fidlLintPath,
+ FidlFormatPath: fidlFormatPath,
+ }
+}
+
+func TestDefinitionOfLibrary(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "file.fidl",
+ `library test;`,
+ )
+ if err := analyzer.Analyze(fs, "file.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "test",
+ Location: state.Location{
+ FileID: state.FileID("file.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 8},
+ End: state.Position{Line: 0, Character: 12},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLoc := state.Location{
+ FileID: state.FileID("file.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 8},
+ End: state.Position{Line: 0, Character: 12},
+ },
+ }
+ if len(locs) != 1 {
+ t.Fatalf("expected 1 definition location")
+ }
+ if locs[0] != expLoc {
+ t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+ }
+}
+
+func TestDefinitionOfLibraryMultipleFiles(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "file1.fidl",
+ `library test;`,
+ )
+ fs.NewFile(
+ "file2.fidl",
+ `library test;`,
+ )
+ if err := analyzer.Analyze(fs, "file1.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+ if err := analyzer.Analyze(fs, "file2.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "test",
+ Location: state.Location{
+ FileID: state.FileID("file1.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 8},
+ End: state.Position{Line: 0, Character: 12},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLocs := []state.Location{
+ {
+ FileID: state.FileID("file1.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 8},
+ End: state.Position{Line: 0, Character: 12},
+ },
+ },
+ {
+ FileID: state.FileID("file2.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 8},
+ End: state.Position{Line: 0, Character: 12},
+ },
+ },
+ }
+ if len(locs) != len(expLocs) {
+ t.Fatalf("expected %d definition locations", len(expLocs))
+ }
+ for _, expLoc := range expLocs {
+ found := false
+ for _, loc := range locs {
+ if expLoc == loc {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("expected but did not find definition of symbol: %v", expLoc)
+ }
+ }
+}
+
+func TestDefinitionOfLibraryImport(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "import.fidl",
+ `
+library fuchsia.import;
+
+const uint8 FOO = 0;
+`,
+ )
+ fs.NewFile(
+ "test.fidl",
+ `
+library test;
+using fuchsia.import;
+
+const uint8 BAR = fuchsia.import.FOO;
+`,
+ )
+ if err := analyzer.Analyze(fs, "import.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+ if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "fuchsia.import",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 6},
+ End: state.Position{Line: 3, Character: 20},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLoc := state.Location{
+ FileID: state.FileID("import.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 8},
+ End: state.Position{Line: 1, Character: 22},
+ },
+ }
+ if len(locs) != 1 {
+ t.Fatalf("expected 1 definition location")
+ }
+ if locs[0] != expLoc {
+ t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+ }
+}
+
+func TestDefinitionOfFullyQualifiedSymbol(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "import.fidl",
+ `
+library fuchsia.import;
+
+struct Foo {};
+`,
+ )
+ fs.NewFile(
+ "test.fidl",
+ `
+library test;
+using fuchsia.import;
+
+protocol Baz {
+ Method(fuchsia.import.Foo f);
+};
+`,
+ )
+ if err := analyzer.Analyze(fs, "import.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+ if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "fuchsia.import.Foo",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 5, Character: 11},
+ End: state.Position{Line: 5, Character: 29},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLoc := state.Location{
+ FileID: state.FileID("import.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 7},
+ End: state.Position{Line: 3, Character: 10},
+ },
+ }
+ if len(locs) != 1 {
+ t.Fatalf("expected 1 definition location")
+ }
+ if locs[0] != expLoc {
+ t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+ }
+}
+
+func TestDefinitionOfLocalSymbol(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "test.fidl",
+ `
+library test;
+
+struct Foo {};
+
+protocol Baz {
+ Method(Foo f);
+};
+`,
+ )
+ if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "Foo",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 6, Character: 11},
+ End: state.Position{Line: 6, Character: 14},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLoc := state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 7},
+ End: state.Position{Line: 3, Character: 10},
+ },
+ }
+ if len(locs) != 1 {
+ t.Fatalf("expected 1 definition location")
+ }
+ if locs[0] != expLoc {
+ t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+ }
+}
+
+func TestDefinitionOfSymbolInSameLibraryDifferentFile(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "test1.fidl",
+ `
+library test;
+
+struct Foo {};
+`,
+ )
+ fs.NewFile(
+ "test2.fidl",
+ `
+library test;
+
+protocol Baz {
+ Method(Foo f);
+};
+`,
+ )
+ if err := analyzer.Analyze(fs, "test1.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+ if err := analyzer.Analyze(fs, "test2.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "Foo",
+ Location: state.Location{
+ FileID: state.FileID("test2.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 5, Character: 11},
+ End: state.Position{Line: 5, Character: 14},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLoc := state.Location{
+ FileID: state.FileID("test1.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 7},
+ End: state.Position{Line: 3, Character: 10},
+ },
+ }
+ if len(locs) != 1 {
+ t.Fatalf("expected 1 definition location")
+ }
+ if locs[0] != expLoc {
+ t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+ }
+}
+
+func TestDefinitionOfReservedKeyword(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "test.fidl",
+ `
+library test;
+
+struct Foo {};
+`,
+ )
+ if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ if _, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "struct",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 0},
+ End: state.Position{Line: 3, Character: 6},
+ },
+ },
+ }); err == nil {
+ t.Fatalf("expect error on definition of reserved keyword")
+ }
+}
diff --git a/fidl-lsp/analysis/deps.go b/fidl-lsp/analysis/deps.go
new file mode 100644
index 0000000..506ecfa
--- /dev/null
+++ b/fidl-lsp/analysis/deps.go
@@ -0,0 +1,190 @@
+// 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"
+
+ fidlcommon "fidl-lsp/third_party/common"
+
+ "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 map[string]CompiledLibrary
+
+// CompiledLibrary represents the build information for a single FIDL library,
+// including its constituent files, its dependencies, and the absolute filepath
+// to its compiled JSON IR.
+type CompiledLibrary struct {
+ 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[fidlcommon.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 []fidlcommon.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 []fidlcommon.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, fidlcommon.LibraryName(rootLibraryMatch.Lib))
+ rootImports := state.ParsePlatformImportsMatch(file)
+ for _, m := range rootImports {
+ stack = append(stack, fidlcommon.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 fidlcommon.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
+ if lib, ok := a.libs[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() ([]fidlcommon.LibraryName, error) {
+ var sorted []fidlcommon.LibraryName
+ incoming := map[fidlcommon.LibraryName]int{}
+ for u := range m {
+ incoming[u] = 0
+ }
+ for _, info := range m {
+ for _, v := range info.deps {
+ incoming[v]++
+ }
+ }
+ var src []fidlcommon.LibraryName
+ for u, deg := range incoming {
+ if deg == 0 {
+ src = append(src, u)
+ }
+ }
+ for len(src) != 0 {
+ var u fidlcommon.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
+}
diff --git a/fidl-lsp/analysis/deps_test.go b/fidl-lsp/analysis/deps_test.go
new file mode 100644
index 0000000..04e7140
--- /dev/null
+++ b/fidl-lsp/analysis/deps_test.go
@@ -0,0 +1,274 @@
+// 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_test
+
+import (
+ "testing"
+
+ "fidl-lsp/analysis"
+ "fidl-lsp/state"
+)
+
+func indexOf(slice []string, elem string) int {
+ for i, v := range slice {
+ if v == elem {
+ return i
+ }
+ }
+ return -1
+}
+
+func TestFindDepsNoDeps(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(analysis.Config{}, analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "file.fidl",
+ `library test;`,
+ )
+
+ analyzer.Analyze(fs, "file.fidl")
+ fidlcArgs, err := analyzer.FindDeps(fs, "file.fidl")
+ if err != nil {
+ t.Error(err)
+ }
+ if len(fidlcArgs) != 2 {
+ t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), 2)
+ }
+}
+
+func TestFindDepsDirectImports(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(
+ analysis.Config{},
+ analysis.CompiledLibraries{
+ "fuchsia.import1": analysis.CompiledLibrary{
+ Files: []string{"import1.fidl"},
+ },
+ "fuchsia.import2": analysis.CompiledLibrary{
+ Files: []string{"import2.fidl"},
+ },
+ },
+ )
+
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "test.fidl",
+ `
+library test;
+using fuchsia.import1;
+using fuchsia.import2;
+`,
+ )
+
+ analyzer.Analyze(fs, "test.fidl")
+ fidlcArgs, err := analyzer.FindDeps(fs, "test.fidl")
+ if err != nil {
+ t.Error(err)
+ }
+ expArgs := []string{
+ "--files", "import1.fidl",
+ "--files", "import2.fidl",
+ "--files",
+ }
+ // FindDeps returns the required --files args to fidlc, including the library
+ // itself, which we don't check. So there should be (# of dependencies + 1)
+ // total --files args.
+ if len(fidlcArgs) != len(expArgs)+1 {
+ t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+fs.NFiles())
+ }
+ for _, expArg := range expArgs {
+ if indexOf(fidlcArgs, expArg) == -1 {
+ t.Errorf("missing expected fidlc --files arg %s", expArg)
+ }
+ }
+}
+
+func TestFindDepsIndirectImports(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(
+ analysis.Config{},
+ analysis.CompiledLibraries{
+ "fuchsia.import1": analysis.CompiledLibrary{
+ Files: []string{"import1.fidl"},
+ Deps: []string{"fuchsia.import2", "fuchsia.import3"},
+ },
+ "fuchsia.import2": analysis.CompiledLibrary{
+ Files: []string{"import2.fidl"},
+ Deps: []string{"fuchsia.import4"},
+ },
+ "fuchsia.import3": analysis.CompiledLibrary{
+ Files: []string{"import3.fidl"},
+ },
+ "fuchsia.import4": analysis.CompiledLibrary{
+ Files: []string{"import4.fidl"},
+ },
+ },
+ )
+
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "test.fidl",
+ `
+library test;
+using fuchsia.import1; // depends on import2, import3
+using fuchsia.import2; // depends on import 4
+`,
+ )
+
+ analyzer.Analyze(fs, "test.fidl")
+ fidlcArgs, err := analyzer.FindDeps(fs, "test.fidl")
+ if err != nil {
+ t.Error(err)
+ }
+ expArgs := []string{
+ "--files", "import4.fidl",
+ "--files", "import3.fidl",
+ "--files", "import2.fidl",
+ "--files", "import1.fidl",
+ "--files",
+ }
+ if len(fidlcArgs) != len(expArgs)+1 {
+ t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+fs.NFiles())
+ }
+ for _, expArg := range expArgs {
+ if indexOf(fidlcArgs, expArg) == -1 {
+ t.Errorf("missing expected fidlc --files arg %s", expArg)
+ }
+ }
+ if indexOf(fidlcArgs, "import4.fidl") > indexOf(fidlcArgs, "import2.fidl") ||
+ indexOf(fidlcArgs, "import3.fidl") > indexOf(fidlcArgs, "import1.fidl") ||
+ indexOf(fidlcArgs, "import2.fidl") > indexOf(fidlcArgs, "import1.fidl") {
+ t.Errorf("fidlc --files are not in correct dependency order")
+ }
+}
+
+func TestFindDepsMultiFileLibraries(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(
+ analysis.Config{},
+ analysis.CompiledLibraries{
+ "fuchsia.import1": analysis.CompiledLibrary{
+ Files: []string{"import1_1.fidl", "import1_2.fidl"},
+ Deps: []string{"fuchsia.import3", "fuchsia.import4"},
+ },
+ "fuchsia.import2": analysis.CompiledLibrary{
+ Files: []string{"import2_1.fidl", "import2_2.fidl"},
+ Deps: []string{"fuchsia.import4"},
+ },
+ "fuchsia.import3": analysis.CompiledLibrary{
+ Files: []string{"import3.fidl"},
+ },
+ "fuchsia.import4": analysis.CompiledLibrary{
+ Files: []string{"import4_1.fidl", "import4_2.fidl", "import4_3.fidl"},
+ },
+ },
+ )
+
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "test1.fidl",
+ `
+library test;
+using fuchsia.import1; // depends on import2, import3
+`,
+ )
+ fs.NewFile(
+ "test2.fidl",
+ `
+library test;
+using fuchsia.import2; // depends on import 4
+`,
+ )
+
+ analyzer.Analyze(fs, "test1.fidl")
+ analyzer.Analyze(fs, "test2.fidl")
+ fidlcArgs, err := analyzer.FindDeps(fs, "test1.fidl")
+ if err != nil {
+ t.Error(err)
+ }
+ expArgs := []string{
+ "--files", "import4_1.fidl", "import4_2.fidl", "import4_3.fidl",
+ "--files", "import3.fidl",
+ "--files", "import2_1.fidl", "import2_2.fidl",
+ "--files", "import1_1.fidl", "import1_2.fidl",
+ "--files",
+ }
+ if len(fidlcArgs) != len(expArgs)+2 {
+ t.Errorf("args: %v\n", fidlcArgs)
+ t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+fs.NFiles())
+ }
+ // We don't test the topological sort, just that all the required files are present.
+ for _, expArg := range expArgs {
+ if indexOf(fidlcArgs, expArg) == -1 {
+ t.Errorf("missing expected fidlc --files arg %s", expArg)
+ }
+ }
+}
+
+func TestFindDepsNoLibraryInFile(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(analysis.Config{}, analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "test1.fidl",
+ `const FOO = 0;`,
+ )
+
+ analyzer.Analyze(fs, "test1.fidl")
+ if _, err := analyzer.FindDeps(fs, "test1.fidl"); err == nil {
+ t.Errorf("expect error on file with no library")
+ }
+}
+
+func TestFindDepsImportNotInFidlProject(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(analysis.Config{}, analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "foo.fidl",
+ `
+library foo;
+using fuchsia.bar;
+`,
+ )
+
+ analyzer.Analyze(fs, "foo.fidl")
+ if _, err := analyzer.FindDeps(fs, "foo.fidl"); err == nil {
+ t.Errorf("expect error on unknown import")
+ }
+}
+
+func TestFindDepsImportCycle(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(
+ analysis.Config{},
+ analysis.CompiledLibraries{
+ "fuchsia.foo": analysis.CompiledLibrary{
+ Files: []string{"foo.fidl"},
+ Deps: []string{"fuchsia.bar"},
+ },
+ "fuchsia.bar": analysis.CompiledLibrary{
+ Files: []string{"bar.fidl"},
+ Deps: []string{"fuchsia.foo"},
+ },
+ },
+ )
+
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "foo.fidl",
+ `
+library fuchsia.foo;
+using fuchsia.bar;
+`,
+ )
+ fs.NewFile(
+ "bar.fidl",
+ `
+library fuchsia.bar;
+using fuchsia.foo;
+`,
+ )
+
+ analyzer.Analyze(fs, "foo.fidl")
+ analyzer.Analyze(fs, "bar.fidl")
+ if _, err := analyzer.FindDeps(fs, "foo.fidl"); err == nil {
+ t.Errorf("expect error on import cycle")
+ }
+}
diff --git a/fidl-lsp/analysis/diagnostics.go b/fidl-lsp/analysis/diagnostics.go
new file mode 100644
index 0000000..05a42b6
--- /dev/null
+++ b/fidl-lsp/analysis/diagnostics.go
@@ -0,0 +1,95 @@
+// 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"
+ "fmt"
+ "os/exec"
+
+ fidlcommon "fidl-lsp/third_party/common"
+
+ "fidl-lsp/state"
+)
+
+// DiagnosticsOnLibrary retrieves the cached diagnostics for `lib`.
+func (a *Analyzer) DiagnosticsOnLibrary(lib fidlcommon.LibraryName) (map[state.FileID][]Diagnostic, error) {
+ if _, ok := a.libs[lib]; !ok {
+ return nil, fmt.Errorf("could not find library `%s`", lib)
+ }
+ return a.libs[lib].diags, nil
+}
+
+type fidlLintSuggestion struct {
+ Description string
+}
+
+type FidlcTool string
+
+const (
+ Fidlc FidlcTool = "fidlc"
+ FidlLint = "fidl-lint"
+)
+
+// Diagnostic represents an {error, warning, lint} deserialized from the JSON
+// output of fidlc or fidl-lint.
+type Diagnostic struct {
+ Source FidlcTool
+ Category string
+ Message string
+ Path string
+ StartLine int `json:"start_line"`
+ StartChar int `json:"start_char"`
+ EndLine int `json:"end_line"`
+ EndChar int `json:"end_char"`
+ Suggestions []fidlLintSuggestion
+}
+
+func (a *Analyzer) fidlcDiagsFromStderr(stderr []byte) (map[state.FileID][]Diagnostic, error) {
+ // Deserialize stderr into []Diagnostic
+ var diags []Diagnostic
+ if err := json.Unmarshal(stderr, &diags); err != nil {
+ return nil, fmt.Errorf("could not deserialize fidlc diagnostics: `%s`", err)
+ }
+ fileToDiags := make(map[state.FileID][]Diagnostic)
+ for i := range diags {
+ fileID, err := a.inputFileToFileID(diags[i].Path)
+ if err != nil {
+ continue
+ }
+ if _, ok := fileToDiags[fileID]; !ok {
+ fileToDiags[fileID] = []Diagnostic{}
+ }
+ diags[i].Source = Fidlc
+ fileToDiags[fileID] = append(fileToDiags[fileID], diags[i])
+ }
+ return fileToDiags, nil
+}
+
+func (a *Analyzer) runFidlLint(path string) (map[state.FileID][]Diagnostic, error) {
+ // TODO: return early with an error if we got status code != 0 or 1?
+ // Otherwise, ignore err: we expect this to fail with status code 1 if there
+ // are any lints from fidl-lint.
+ out, _ := exec.Command(a.cfg.FidlLintPath, "--format=json", path).Output()
+
+ // Deserialize stdout into []fidlLint
+ var diags []Diagnostic
+ if err := json.Unmarshal(out, &diags); err != nil {
+ return nil, fmt.Errorf("error deserializing fidl-lint output: %s", err)
+ }
+ fileToDiags := make(map[state.FileID][]Diagnostic)
+ for i := range diags {
+ fileID, err := a.inputFileToFileID(diags[i].Path)
+ if err != nil {
+ continue
+ }
+ if _, ok := fileToDiags[fileID]; !ok {
+ fileToDiags[fileID] = []Diagnostic{}
+ }
+ diags[i].Source = FidlLint
+ fileToDiags[fileID] = append(fileToDiags[fileID], diags[i])
+ }
+ return fileToDiags, nil
+}
diff --git a/fidl-lsp/analysis/format.go b/fidl-lsp/analysis/format.go
new file mode 100644
index 0000000..8d09d0a
--- /dev/null
+++ b/fidl-lsp/analysis/format.go
@@ -0,0 +1,28 @@
+// 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"
+ "os/exec"
+
+ "fidl-lsp/state"
+)
+
+// FormatFile runs fidl-format on the file at `path` in the FileSystem, and
+// returns the formatted document, or an error.
+func (a *Analyzer) FormatFile(fs *state.FileSystem, path state.FileID) (string, error) {
+ // TODO: write tmp file on all changes in langserver?
+ analysisInputFile, err := a.writeFileToTmp(fs, path)
+ if err != nil {
+ return "", fmt.Errorf("error writing to tmp file: %s", err)
+ }
+
+ out, err := exec.Command(a.cfg.FidlFormatPath, analysisInputFile).Output()
+ if err != nil {
+ return "", fmt.Errorf("error running fidl-format: %s", err)
+ }
+ return string(out), nil
+}
diff --git a/fidl-lsp/analysis/library.go b/fidl-lsp/analysis/library.go
new file mode 100644
index 0000000..1efa0f8
--- /dev/null
+++ b/fidl-lsp/analysis/library.go
@@ -0,0 +1,173 @@
+// 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
+
+// Map from fully qualified name to type.kind, e.g.
+// "fuchsia.hardware.camera/Device": "interface"
+type decls map[string]string
+
+type dependency struct {
+ Name string
+ Declarations decls
+}
+
+type location struct {
+ Filename string
+ Line, Column, Length int
+}
+
+type Attribute struct {
+ Name string
+ Value string
+}
+
+type TypeKind string
+
+const (
+ ArrayType TypeKind = "array"
+ VectorType = "vector"
+ StringType = "string"
+ HandleType = "handle"
+ RequestType = "request"
+ PrimitiveType = "primitive"
+ IdentifierType = "identifier"
+)
+
+type typeCtor struct {
+ Name string
+ Args []typeCtor
+ Nullable bool
+ Size *constValue `json:"maybe_size,omitempty"`
+ HandleSubtype *HandleSubtype `json:"maybe_handle_subtype,omitempty"`
+}
+
+type constValue struct {
+ Value string
+}
+
+type declType struct {
+ Kind TypeKind
+
+ // Array
+ ElementType *declType `json:"element_type,omitempty"`
+ ElementCount uint `json:"element_count,omitempty"`
+
+ // Vector
+ MaybeElementCount *uint `json:"maybe_element_count,omitempty"`
+ Nullable bool `json:"nullable,omitempty"`
+
+ // Handle
+ Subtype string `json:"subtype,omitempty"`
+ Rights uint `json:"rights,omitempty"`
+
+ Identifier string `json:"identifier,omitempty"`
+}
+
+type member struct {
+ Name string
+ Loc location `json:"location"`
+ Type declType `json:"type,omitempty"`
+ FromTypeAlias typeCtor `json:"experimental_maybe_from_type_alias,omitempty"`
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type method struct {
+ Name string
+ Loc location `json:"location"`
+ MaybeRequest []member `json:"maybe_request,omitempty"`
+ MaybeResponse []member `json:"maybe_response,omitempty"`
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type bitsDecl struct {
+ Name string
+ Loc location `json:"location"`
+ Type declType
+ FromTypeAlias typeCtor `json:"experimental_maybe_from_type_alias,omitempty"`
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+ Members []member
+}
+
+type enumDecl struct {
+ Name string
+ Loc location `json:"location"`
+ // For some reason, Bits have a full type in the JSON IR, while enums have
+ // a string
+ Type string
+ FromTypeAlias typeCtor `json:"experimental_maybe_from_type_alias,omitempty"`
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+ Members []member
+}
+
+type constDecl struct {
+ Name string
+ Loc location `json:"location"`
+ Type declType
+ Value constValue `json:"value"`
+ FromTypeAlias typeCtor `json:"experimental_maybe_from_type_alias,omitempty"`
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type protocolDecl struct {
+ Name string
+ Loc location `json:"location"`
+ Methods []method
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type serviceDecl struct {
+ Name string
+ Loc location `json:"location"`
+ Members []member
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type structDecl struct {
+ Name string
+ Loc location `json:"location"`
+ Members []member
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+ Anonymous bool
+}
+
+type tableDecl struct {
+ Name string
+ Loc location `json:"location"`
+ Members []member
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type unionDecl struct {
+ Name string
+ Loc location `json:"location"`
+ Members []member
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type typeAliasDecl struct {
+ Name string
+ Loc location `json:"location"`
+ TypeCtor typeCtor `json:"partial_type_ctor"`
+ Attrs []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+// TODO: rename to FidlJSONIR, or JSONLibrary?
+
+// FidlLibrary is the type of the deserialized JSON IR of a FIDL library.
+type FidlLibrary struct {
+ Version string
+ Name string
+ Deps []dependency `json:"library_dependencies"`
+ BitsDecls []bitsDecl `json:"bits_declarations"`
+ ConstDecls []constDecl `json:"const_declarations"`
+ EnumDecls []enumDecl `json:"enum_declarations"`
+ ProtocolDecls []protocolDecl `json:"interface_declarations"`
+ ServiceDecls []serviceDecl `json:"service_declarations"`
+ StructDecls []structDecl `json:"struct_declarations"`
+ TableDecls []tableDecl `json:"table_declarations"`
+ UnionDecls []unionDecl `json:"union_declarations"`
+ TypeAliasDecls []typeAliasDecl `json:"type_alias_declarations"`
+ Decls decls `json:"declarations"`
+}
diff --git a/fidl-lsp/analysis/references.go b/fidl-lsp/analysis/references.go
new file mode 100644
index 0000000..346f9c9
--- /dev/null
+++ b/fidl-lsp/analysis/references.go
@@ -0,0 +1,173 @@
+// 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"
+ "log"
+
+ fidlcommon "fidl-lsp/third_party/common"
+
+ "fidl-lsp/state"
+)
+
+// ReferencesToSymbol returns the locations in `fs` of symbols that reference
+// `sym`, as long as those locations are available to the Analyzer (i.e. in
+// a.libs somewhere).
+//
+// For a library name, e.g. `fuchsia.foo`, ReferencesToSymbol returns the
+// locations of the `using` import declarations for each file that imports that
+// library.
+func (a *Analyzer) ReferencesToSymbol(fs *state.FileSystem, sym state.Symbol) ([]state.Location, error) {
+ // If `sym` is a library name, return locations pointing at all the files
+ // that import that library (specifically, pointing at their `using`
+ // declarations).
+ libName, err := fidlcommon.ReadLibraryName(sym.Name)
+ if err == nil {
+ if _, isLib := a.libs[libName]; isLib {
+ return a.importsOfLibrary(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,
+ )
+ }
+
+ // Search for references to `name` in all the libraries that import `name`'s
+ // library, as well as its own library.
+ libs := a.librariesThatImport(name.LibraryName())
+ libs = append(libs, name.LibraryName())
+ refs := []state.Location{}
+
+ for _, lib := range libs {
+ if a.libs[lib].ir == nil {
+ if err := a.importLibrary(a.libs[lib].json); err != nil {
+ return nil, fmt.Errorf(
+ "error importing library `%s`: %s",
+ name.LibraryName().FullyQualifiedName(),
+ err,
+ )
+ }
+ }
+
+ for _, si := range a.libs[lib].symbols {
+ if a.typeReferencesName(si.typeInfo, name) {
+ refs = append(refs, si.definition)
+ }
+ }
+ }
+
+ return refs, nil
+}
+
+func (a *Analyzer) importsOfLibrary(fs *state.FileSystem, importedLib fidlcommon.LibraryName) []state.Location {
+ libs := a.librariesThatImport(importedLib)
+ locs := []state.Location{}
+ for _, lib := range libs {
+ locs = append(locs, a.importsOfLibraryInLibrary(fs, importedLib, lib)...)
+ }
+ return locs
+}
+
+func (a *Analyzer) librariesThatImport(library fidlcommon.LibraryName) []fidlcommon.LibraryName {
+ libs := []fidlcommon.LibraryName{}
+ for name, lib := range a.libs {
+ for dep := range lib.deps {
+ if dep == library {
+ libs = append(libs, name)
+ break
+ }
+ }
+ }
+ return libs
+}
+
+func (a *Analyzer) importsOfLibraryInLibrary(fs *state.FileSystem, importedLib fidlcommon.LibraryName, library fidlcommon.LibraryName) []state.Location {
+ files := []state.FileID{}
+ for file := range a.libs[library].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, fileID)
+ } else {
+ files = append(files, state.FileID(file))
+ }
+ }
+
+ locs := []state.Location{}
+ // Find the `using` declaration for `importedLib` 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 {
+ log.Printf("could not read in file `%s`: %s", fileID, err)
+ continue
+ }
+ file = string(bytes)
+ }
+
+ imports := state.ParsePlatformImportsMatch(file)
+ for _, libraryMatch := range imports {
+ if libraryMatch.Lib == importedLib {
+ locs = append(locs, state.Location{
+ FileID: fileID,
+ Range: libraryMatch.Range,
+ })
+ }
+ }
+ }
+ return locs
+}
+
+func (a *Analyzer) typeReferencesName(t Type, name fidlcommon.Name) bool {
+ if t.FromTypeAlias != nil {
+ if *t.FromTypeAlias == name.FullyQualifiedName() {
+ return true
+ }
+ return false
+ }
+
+ switch t.Kind {
+ default:
+ return false
+
+ case ArrayType:
+ return a.typeReferencesName(t.Array.ElementType, name)
+
+ case VectorType:
+ return a.typeReferencesName(t.Vector.ElementType, name)
+
+ case StringType:
+ return false
+
+ case HandleType:
+ return false
+
+ case RequestType:
+ return t.Request.Subtype == name.FullyQualifiedName()
+
+ case PrimitiveType:
+ return false
+
+ case IdentifierType:
+ if !t.Identifier.IsDecl {
+ return t.Identifier.Identifier == name.FullyQualifiedName()
+ }
+ if t.Identifier.Kind != TypeAliasKind {
+ return false
+ }
+ return a.typeReferencesName(t.Identifier.TypeAlias.Type, name)
+ }
+}
diff --git a/fidl-lsp/analysis/references_test.go b/fidl-lsp/analysis/references_test.go
new file mode 100644
index 0000000..608477a
--- /dev/null
+++ b/fidl-lsp/analysis/references_test.go
@@ -0,0 +1,335 @@
+// 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_test
+
+import (
+ "testing"
+
+ "fidl-lsp/analysis"
+ "fidl-lsp/state"
+)
+
+func TestReferencesToSymbol(t *testing.T) {
+ cases := []struct {
+ name string
+ files map[state.FileID]string
+ symbol state.Symbol
+ expRefs []state.Location
+ }{
+ {
+ name: "Reference to library declaration",
+ files: map[state.FileID]string{
+ "test.fidl": `library fuchsia.test;`,
+ "other.fidl": `
+library other;
+using fuchsia.test;`,
+ },
+ symbol: state.Symbol{
+ Name: "fuchsia.test",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 6},
+ End: state.Position{Line: 0, Character: 10},
+ },
+ },
+ },
+ expRefs: []state.Location{
+ {
+ FileID: state.FileID("other.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 2, Character: 6},
+ End: state.Position{Line: 2, Character: 18},
+ },
+ },
+ },
+ },
+ {
+ name: "Reference to library import",
+ files: map[state.FileID]string{
+ "test.fidl": `library fuchsia.test;`,
+ "other.fidl": `
+library other;
+using fuchsia.test;`,
+ },
+ symbol: state.Symbol{
+ Name: "fuchsia.test",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 2, Character: 6},
+ End: state.Position{Line: 2, Character: 18},
+ },
+ },
+ },
+ expRefs: []state.Location{
+ {
+ FileID: state.FileID("other.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 2, Character: 6},
+ End: state.Position{Line: 2, Character: 18},
+ },
+ },
+ },
+ },
+ {
+ name: "Reference to symbol in same file",
+ files: map[state.FileID]string{
+ "test.fidl": `
+library test;
+
+struct Foo {};
+
+protocol Bar {
+ Method(Foo foo);
+// ~~~
+};
+`,
+ },
+ symbol: state.Symbol{
+ Name: "Foo",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 7},
+ End: state.Position{Line: 3, Character: 10},
+ },
+ },
+ },
+ expRefs: []state.Location{
+ {
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 6, Character: 12},
+ End: state.Position{Line: 6, Character: 15},
+ },
+ },
+ },
+ },
+ {
+ name: "Reference to symbol in other file in same library",
+ files: map[state.FileID]string{
+ "test1.fidl": `
+library test;
+
+struct Foo {};
+`,
+ "test2.fidl": `
+library test;
+
+protocol Bar {
+ Method(Foo foo);
+// ~~~
+};
+`,
+ },
+ symbol: state.Symbol{
+ Name: "Foo",
+ Location: state.Location{
+ FileID: state.FileID("test1.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 7},
+ End: state.Position{Line: 3, Character: 10},
+ },
+ },
+ },
+ expRefs: []state.Location{
+ {
+ FileID: state.FileID("test2.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 4, Character: 12},
+ End: state.Position{Line: 4, Character: 15},
+ },
+ },
+ },
+ },
+ {
+ name: "References to symbol in other library",
+ files: map[state.FileID]string{
+ "imported.fidl": `
+library fuchsia.imported;
+
+struct Foo {};
+`,
+ "test.fidl": `
+library test;
+using fuchsia.imported;
+
+protocol Bar {
+ Method(fuchsia.imported.Foo foo);
+// ~~~
+};
+`,
+ },
+ symbol: state.Symbol{
+ Name: "Foo",
+ Location: state.Location{
+ FileID: state.FileID("imported.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 7},
+ End: state.Position{Line: 3, Character: 10},
+ },
+ },
+ },
+ expRefs: []state.Location{
+ {
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 5, Character: 29},
+ End: state.Position{Line: 5, Character: 32},
+ },
+ },
+ },
+ },
+ {
+ name: "No references to symbol",
+ files: map[state.FileID]string{
+ "test.fidl": `
+library test;
+
+struct Foo {};
+`,
+ },
+ symbol: state.Symbol{
+ Name: "Foo",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 7},
+ End: state.Position{Line: 3, Character: 10},
+ },
+ },
+ },
+ expRefs: []state.Location{},
+ },
+ {
+ name: "References to type alias",
+ files: map[state.FileID]string{
+ "zx.fidl": `
+library zx;
+
+using status = int32;
+`,
+ "test.fidl": `
+library test;
+using zx;
+
+protocol Foo {
+ Method() -> (zx.status status);
+// ~~~~~~
+};
+`,
+ },
+ symbol: state.Symbol{
+ Name: "status",
+ Location: state.Location{
+ FileID: state.FileID("zx.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 7},
+ End: state.Position{Line: 3, Character: 10},
+ },
+ },
+ },
+ expRefs: []state.Location{
+ {
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 5, Character: 24},
+ End: state.Position{Line: 5, Character: 30},
+ },
+ },
+ },
+ },
+ {
+ name: "References in type parameters",
+ files: map[state.FileID]string{
+ "test.fidl": `
+library test;
+
+table Foo {};
+
+protocol Bar {
+ Method(vector<Foo> items);
+// ~~~~~
+};
+`,
+ },
+ symbol: state.Symbol{
+ Name: "Foo",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 6},
+ End: state.Position{Line: 3, Character: 9},
+ },
+ },
+ },
+ expRefs: []state.Location{
+ {
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 6, Character: 20},
+ End: state.Position{Line: 6, Character: 25},
+ },
+ },
+ },
+ },
+ }
+
+ for _, ex := range cases {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ for file, text := range ex.files {
+ fs.NewFile(file, text)
+ }
+ for file := range ex.files {
+ if err := analyzer.Analyze(fs, file); err != nil {
+ t.Fatalf("case `%s`: failed to analyze file `%s`: %s", ex.name, file, err)
+ }
+ }
+
+ refs, err := analyzer.ReferencesToSymbol(fs, ex.symbol)
+ if err != nil {
+ t.Fatalf("case `%s`: error getting references to symbol: %s", ex.name, err)
+ }
+ if len(refs) != len(ex.expRefs) {
+ t.Fatalf("case `%s`: expected %d references, got %d", ex.name, len(ex.expRefs), len(refs))
+ }
+ for _, ref := range refs {
+ found := false
+ for _, expRef := range ex.expRefs {
+ if ref == expRef {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf(
+ "case `%s`: got unexpected reference %v to symbol %s",
+ ex.name,
+ ref,
+ ex.symbol.Name,
+ )
+ }
+ }
+ for _, expRef := range ex.expRefs {
+ found := false
+ for _, ref := range refs {
+ if ref == expRef {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf(
+ "case `%s`: expected but did not find reference %v to symbol %s",
+ ex.name,
+ expRef,
+ ex.symbol.Name,
+ )
+ }
+ }
+ }
+}
diff --git a/fidl-lsp/analysis/symbol.go b/fidl-lsp/analysis/symbol.go
new file mode 100644
index 0000000..c6e1554
--- /dev/null
+++ b/fidl-lsp/analysis/symbol.go
@@ -0,0 +1,265 @@
+// 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"
+ "strings"
+
+ fidlcommon "fidl-lsp/third_party/common"
+
+ "fidl-lsp/state"
+)
+
+type symbolInfo struct {
+ name string
+ definition state.Location
+ typeInfo Type
+}
+
+type Type struct {
+ // If IsLib is true, other fields are empty.
+ // TODO: is there a better way to represent this?
+ IsLib bool
+ // If IsMethod is true, only Attrs is set.
+ IsMethod bool
+
+ // Set if this type is actually a resolved alias
+ FromTypeAlias *string
+ Attrs []Attribute
+
+ Kind TypeKind
+
+ Array *ArrayTypeInfo
+ Vector *VectorTypeInfo
+ String *StringTypeInfo
+ Handle *HandleTypeInfo
+ Request *RequestTypeInfo
+ Primitive *PrimitiveTypeInfo
+ Identifier *IdentifierTypeInfo
+}
+
+type ArrayTypeInfo struct {
+ ElementType Type
+ ElementCount uint
+}
+
+type VectorTypeInfo struct {
+ ElementType Type
+ ElementCount *uint
+ Nullable bool
+}
+
+type StringTypeInfo struct {
+ ElementCount *uint
+ Nullable bool
+}
+
+type HandleTypeInfo struct {
+ Subtype HandleSubtype
+ Rights uint
+ Nullable bool
+}
+
+type HandleSubtype string
+
+const (
+ Handle HandleSubtype = "handle"
+ Bti = "bti"
+ Channel = "channel"
+ Clock = "clock"
+ DebugLog = "debuglog"
+ Event = "event"
+ Eventpair = "eventpair"
+ Exception = "exception"
+ Fifo = "fifo"
+ Guest = "guest"
+ Interrupt = "interrupt"
+ Iommu = "iommu"
+ Job = "job"
+ Pager = "pager"
+ PciDevice = "pcidevice"
+ Pmt = "pmt"
+ Port = "port"
+ Process = "process"
+ Profile = "profile"
+ Resource = "resource"
+ Socket = "socket"
+ Stream = "stream"
+ SuspendToken = "suspendtoken"
+ Thread = "thread"
+ Time = "timer"
+ Vcpu = "vcpu"
+ Vmar = "vmar"
+ Vmo = "vmo"
+)
+
+type RequestTypeInfo struct {
+ Subtype string
+ Nullable bool
+}
+
+type PrimitiveTypeInfo struct {
+ Subtype PrimitiveSubtype
+}
+
+type PrimitiveSubtype string
+
+const (
+ Bool PrimitiveSubtype = "bool"
+ Int8 = "int8"
+ Int16 = "int16"
+ Int32 = "int32"
+ Int64 = "int64"
+ Uint8 = "uint8"
+ Uint16 = "uint16"
+ Uint32 = "uint32"
+ Uint64 = "uint64"
+ Float32 = "float32"
+ Float64 = "float64"
+)
+
+// IdentifierTypeInfo is not a perfect analog for `identifier-type` from the
+// JSON IR, because it serves as the type for both declarations of identifier
+// types and values of identifier types.
+//
+// For example, if you have declared a struct Foo, and a protocol method that
+// takes a Foo, IdentifierTypeInfo will hold the same type information for each
+// of the following symbols:
+//
+// struct Foo {};
+// ~~~
+// protocol P {
+// Method(Foo foo);
+// ~~~ ~~~
+// };
+//
+// So it is both a kind of "declaration type" as well as an "identifier type".
+// Iff `IsDecl` == true, it is a "declaration type"; otherwise, it is an
+// "identifier type", and `Identifier` is the name of a declaration type that
+// can be looked up in the symbol map.
+type IdentifierTypeInfo struct {
+ // If IsDecl is true, all the type information is contained in the tagged
+ // type info objects -- Kind and one of {Bits, Const, etc.}.
+ // If IsDecl is false, Identifier and Nullable are set, and Identifier is
+ // a key to the declaration of the identifier type, in the symbolMap.
+ IsDecl bool
+ Identifier string
+ Nullable bool
+
+ Kind IdentifierKind
+ Name string
+
+ Bits *BitsTypeInfo
+ Const *ConstTypeInfo
+ Enum *EnumTypeInfo
+ TypeAlias *TypeAliasTypeInfo
+}
+
+type IdentifierKind string
+
+const (
+ BitsKind IdentifierKind = "bits"
+ ConstKind = "const"
+ EnumKind = "enum"
+ ProtocolKind = "protocol"
+ ServiceKind = "service"
+ StructKind = "struct"
+ TableKind = "table"
+ UnionKind = "union"
+ TypeAliasKind = "typeAlias"
+)
+
+type BitsTypeInfo struct {
+ Type Type
+}
+type ConstTypeInfo struct {
+ Type Type
+ Value string
+}
+type EnumTypeInfo struct {
+ Type PrimitiveSubtype
+}
+type TypeAliasTypeInfo struct {
+ Type Type
+}
+
+func (a *Analyzer) symbolToFullyQualifiedName(fs *state.FileSystem, sym state.Symbol) (fidlcommon.Name, error) {
+ // TODO: if we want to support hovering over members (e.g. struct fields,
+ // method parameters, bits members) or protocol methods, we need to check
+ // here whether the symbol is namespaced -- whether it is inside a
+ // declaration.
+ // If it is, prepend it with that declaration's name. For example, this
+ // struct field:
+ //
+ // struct Foo {
+ // MyType my_field;
+ // ~~~~~~~~
+ // }
+ //
+ // Would become "library.name/Foo.my_field", as this is how it's stored in
+ // the symbol map.
+
+ // 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
+}
+
+func (a *Analyzer) lookupSymbolInfo(name fidlcommon.Name) (*symbolInfo, error) {
+ 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 symbol `%s`", name.FullyQualifiedName())
+ }
+
+ return symInfo, nil
+}
diff --git a/fidl-lsp/analysis/type.go b/fidl-lsp/analysis/type.go
new file mode 100644
index 0000000..fcec389
--- /dev/null
+++ b/fidl-lsp/analysis/type.go
@@ -0,0 +1,105 @@
+// 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"
+
+ fidlcommon "fidl-lsp/third_party/common"
+
+ "fidl-lsp/state"
+)
+
+// TypeOfSymbol returns the type information of `sym`.
+//
+// analysis.Type contains all the information needed to create a human-readable
+// description of the type.
+func (a *Analyzer) TypeOfSymbol(fs *state.FileSystem, sym state.Symbol) (Type, error) {
+ // If `sym` is a library name, return a `library`-kinded TypeInfo.
+ libName, err := fidlcommon.ReadLibraryName(sym.Name)
+ if err == nil {
+ if _, isLib := a.libs[libName]; isLib {
+ return Type{
+ IsLib: true,
+ }, nil
+ }
+ }
+
+ // Otherwise, we assume it is a local or fully-qualified name
+ name, err := a.symbolToFullyQualifiedName(fs, sym)
+ if err != nil {
+ return Type{}, fmt.Errorf(
+ "could not convert symbol `%s` to fully-qualified name: %s",
+ sym.Name,
+ err,
+ )
+ }
+
+ symInfo, err := a.lookupSymbolInfo(name)
+ if err != nil {
+ return Type{}, fmt.Errorf("could not find type of symbol `%s`: %s", name.FullyQualifiedName(), err)
+ }
+
+ // Resolve identifier type, if necessary.
+ if symInfo.typeInfo.Kind == IdentifierType && !symInfo.typeInfo.Identifier.IsDecl {
+ // This means that rather than being a declaration, symInfo is a value
+ // of an identifier type, so we lookup that type's info based on the
+ // type name.
+ typeName, err := fidlcommon.ReadName(symInfo.typeInfo.Identifier.Identifier)
+ if err != nil {
+ return Type{}, fmt.Errorf(
+ "invalid identifier type `%s`: %s",
+ symInfo.typeInfo.Identifier.Identifier,
+ err,
+ )
+ }
+ t, err := a.lookupSymbolInfo(typeName)
+ if err != nil {
+ return Type{}, fmt.Errorf(
+ "could not find identifier type `%s` of symbol `%s`: %s",
+ typeName.FullyQualifiedName(),
+ name.FullyQualifiedName(),
+ err,
+ )
+ }
+ identifierType := t.typeInfo
+ identifierType.Identifier.IsDecl = false
+ identifierType.Identifier.Nullable = symInfo.typeInfo.Identifier.Nullable
+ identifierType.Identifier.Identifier = symInfo.typeInfo.Identifier.Identifier
+ return identifierType, nil
+ }
+
+ // Resolve aliased identifier type, if necessary.
+ if symInfo.typeInfo.Kind == IdentifierType &&
+ symInfo.typeInfo.Identifier.Kind == TypeAliasKind &&
+ symInfo.typeInfo.Identifier.TypeAlias.Type.Kind == IdentifierType {
+ // This means that `sym` is a type alias to an identifier type, so we
+ // need to lookup that identifier type's info based on the type name.
+ aliasedType := symInfo.typeInfo.Identifier.TypeAlias.Type
+ typeName, err := fidlcommon.ReadName(aliasedType.Identifier.Identifier)
+ if err != nil {
+ return Type{}, fmt.Errorf(
+ "invalid identifier type `%s`: %s",
+ aliasedType.Identifier.Identifier,
+ err,
+ )
+ }
+ t, err := a.lookupSymbolInfo(typeName)
+ if err != nil {
+ return Type{}, fmt.Errorf(
+ "could not find identifier type `%s` of symbol `%s`: %s",
+ typeName.FullyQualifiedName(),
+ name.FullyQualifiedName(),
+ err,
+ )
+ }
+ identifierType := t.typeInfo
+ identifierType.Identifier.Nullable = aliasedType.Identifier.Nullable
+ identifierType.Identifier.Identifier = aliasedType.Identifier.Identifier
+ symInfo.typeInfo.Identifier.TypeAlias.Type = identifierType
+ }
+
+ return symInfo.typeInfo, nil
+}
diff --git a/fidl-lsp/go.mod b/fidl-lsp/go.mod
new file mode 100644
index 0000000..9ab5fda
--- /dev/null
+++ b/fidl-lsp/go.mod
@@ -0,0 +1,8 @@
+module fidl-lsp
+
+go 1.14
+
+require (
+ github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d
+ github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37
+)
diff --git a/fidl-lsp/langserver/definition.go b/fidl-lsp/langserver/definition.go
new file mode 100644
index 0000000..d0468ea
--- /dev/null
+++ b/fidl-lsp/langserver/definition.go
@@ -0,0 +1,45 @@
+// 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 langserver
+
+import (
+ "github.com/sourcegraph/go-lsp"
+
+ "fidl-lsp/state"
+)
+
+func (h *LangHandler) handleDefinition(params lsp.TextDocumentPositionParams) ([]lsp.Location, error) {
+ sym, err := h.fs.SymbolAtPos(
+ state.FileID(params.TextDocument.URI),
+ state.Position{Line: params.Position.Line, Character: params.Position.Character},
+ )
+ if err != nil {
+ h.log.Printf(
+ "could not find symbol at position `%#v` in document `%s`\n",
+ params.Position,
+ params.TextDocument.URI,
+ )
+ return nil, err
+ }
+
+ locs, err := h.analyzer.DefinitionOfSymbol(h.fs, sym)
+ if err != nil {
+ h.log.Printf("error on definition: %s\n", err)
+ return nil, err
+ }
+
+ // Convert state.Locations --> lsp.Locations
+ lspLocs := make([]lsp.Location, len(locs))
+ for i, loc := range locs {
+ lspLocs[i] = lsp.Location{
+ URI: lsp.DocumentURI(loc.FileID),
+ Range: lsp.Range{
+ Start: lsp.Position{Line: loc.Range.Start.Line, Character: loc.Range.Start.Character},
+ End: lsp.Position{Line: loc.Range.End.Line, Character: loc.Range.End.Character},
+ },
+ }
+ }
+ return lspLocs, nil
+}
diff --git a/fidl-lsp/langserver/diagnostics.go b/fidl-lsp/langserver/diagnostics.go
new file mode 100644
index 0000000..01d1212
--- /dev/null
+++ b/fidl-lsp/langserver/diagnostics.go
@@ -0,0 +1,87 @@
+// 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 langserver
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ fidlcommon "fidl-lsp/third_party/common"
+ "github.com/sourcegraph/go-lsp"
+ "github.com/sourcegraph/jsonrpc2"
+
+ "fidl-lsp/analysis"
+ "fidl-lsp/state"
+)
+
+func (h *LangHandler) publishDiagnostics(ctx context.Context, conn *jsonrpc2.Conn, uri lsp.DocumentURI) {
+ diags, err := h.getDiagnosticsForLibraryWithFile(uri)
+ if err != nil {
+ h.log.Printf("error publishing diagnostics: %s\n", err)
+ return
+ }
+
+ for fileURI, fileDiags := range diags {
+ h.Notify(ctx, conn, "textDocument/publishDiagnostics", lsp.PublishDiagnosticsParams{
+ URI: fileURI,
+ Diagnostics: fileDiags,
+ })
+ }
+}
+
+func (h *LangHandler) getDiagnosticsForLibraryWithFile(uri lsp.DocumentURI) (map[lsp.DocumentURI][]lsp.Diagnostic, error) {
+ file, err := h.fs.File(state.FileID(uri))
+ if err != nil {
+ return nil, fmt.Errorf("could not find file `%s`: %s", uri, err)
+ }
+ libraryName, err := state.LibraryOfFile(file)
+ if err != nil {
+ return nil, fmt.Errorf("could not find library name of file `%s`: %s", uri, err)
+ }
+
+ diags, err := h.analyzer.DiagnosticsOnLibrary(fidlcommon.LibraryName(libraryName))
+ if err != nil {
+ return nil, fmt.Errorf("error getting diagnostics: %s", err)
+ }
+
+ // Convert analysis.Diagnostics --> lsp.Diagnostics
+ lspDiags := make(map[lsp.DocumentURI][]lsp.Diagnostic)
+ for fileID, fileDiags := range diags {
+ fileURI := lsp.DocumentURI(fileID)
+ lspDiags[fileURI] = []lsp.Diagnostic{}
+
+ for _, diag := range fileDiags {
+ var severity lsp.DiagnosticSeverity
+ message := diag.Message
+
+ switch diag.Source {
+ case analysis.Fidlc:
+ diagType := strings.Split(diag.Category, "/")[1]
+ if diagType == "error" {
+ severity = lsp.Error
+ } else {
+ severity = lsp.Warning
+ }
+ case analysis.FidlLint:
+ severity = lsp.Warning
+ if len(diag.Suggestions) > 0 {
+ message = message + "; " + diag.Suggestions[0].Description
+ }
+ }
+
+ lspDiags[fileURI] = append(lspDiags[fileURI], lsp.Diagnostic{
+ Range: lsp.Range{
+ Start: lsp.Position{Line: diag.StartLine - 1, Character: diag.StartChar},
+ End: lsp.Position{Line: diag.EndLine - 1, Character: diag.EndChar},
+ },
+ Severity: severity,
+ Source: string(diag.Source),
+ Message: message,
+ })
+ }
+ }
+ return lspDiags, nil
+}
diff --git a/fidl-lsp/langserver/diagnostics_test.go b/fidl-lsp/langserver/diagnostics_test.go
new file mode 100644
index 0000000..efacd87
--- /dev/null
+++ b/fidl-lsp/langserver/diagnostics_test.go
@@ -0,0 +1,171 @@
+// 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 langserver
+
+import (
+ "testing"
+
+ "github.com/sourcegraph/go-lsp"
+)
+
+func TestFileWithNoDiagnostics(t *testing.T) {
+ handler := NewHandlerWithFiles(TestFile{
+ Name: "test.fidl",
+ Text: `// 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.
+
+library test;
+const uint8 VALID_NAME = 1;
+
+struct Foo {
+ uint8 field_name;
+};
+
+protocol Bar {
+ Baz(string:256 message);
+};
+`,
+ })
+
+ diags, err := handler.getDiagnosticsForLibraryWithFile(lsp.DocumentURI("test.fidl"))
+ if err != nil {
+ t.Fatalf("failed to publish diagnostics: %s", err)
+ }
+ if len(diags["test.fidl"]) > 0 {
+ t.Fatalf("incorrect number of diagnostics; expected 0, actual %d", len(diags))
+ }
+}
+
+func TestDiagnosticsFidlcErrors(t *testing.T) {
+ handler := NewHandlerWithFiles(TestFile{
+ Name: "test.fidl",
+ Text: `// 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.
+
+library test;
+const uint8 DUPLICATE_NAME = 0;
+const uint8 DUPLICATE_NAME = 1;
+// ~~~~~~~~~~~~~~ expected range of diagnostic
+`,
+ })
+
+ allDiags, err := handler.getDiagnosticsForLibraryWithFile(lsp.DocumentURI("test.fidl"))
+ if err != nil {
+ t.Fatalf("failed to publish diagnostics: %s", err)
+ }
+ diags := allDiags["test.fidl"]
+ if len(diags) != 1 {
+ t.Fatalf("incorrect number of diagnostics %d; expected 1", len(diags))
+ }
+ if diags[0].Source != "fidlc" {
+ t.Errorf("unexpected source of diagnostic %s; expected `fidlc`", diags[0].Source)
+ }
+ expRange := lsp.Range{
+ Start: lsp.Position{Line: 6, Character: 12},
+ End: lsp.Position{Line: 6, Character: 26},
+ }
+ if diags[0].Range != expRange {
+ t.Errorf("unexpected range of diagnostic %v; expected %v", diags[0].Range, expRange)
+ }
+ if diags[0].Severity != lsp.Error {
+ t.Errorf("unexpected severity of diagnostic %v; expected %v", diags[0].Severity, lsp.Error)
+ }
+}
+
+func TestDiagnosticsFidlcWarnings(t *testing.T) {
+ handler := NewHandlerWithFiles(TestFile{
+ Name: "test.fidl",
+ Text: `// 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.
+
+library test;
+
+[layout = "Simple"]
+//~~~~~~~~~~~~~~~~ expected range of diagnostic
+struct S {
+ uint8 foo;
+};
+`,
+ })
+
+ allDiags, err := handler.getDiagnosticsForLibraryWithFile(lsp.DocumentURI("test.fidl"))
+ if err != nil {
+ t.Fatalf("failed to publish diagnostics: %s", err)
+ }
+ diags := allDiags["test.fidl"]
+ if len(diags) != 1 {
+ t.Fatalf("incorrect number of diagnostics %d; expected 1", len(diags))
+ }
+ if diags[0].Source != "fidlc" {
+ t.Errorf("unexpected source of diagnostic %s; expected `fidlc`", diags[0].Source)
+ }
+ expRange := lsp.Range{
+ Start: lsp.Position{Line: 6, Character: 1},
+ End: lsp.Position{Line: 6, Character: 18},
+ }
+ if diags[0].Range != expRange {
+ t.Errorf("unexpected range of diagnostic %v; expected %v", diags[0].Range, expRange)
+ }
+ if diags[0].Severity != lsp.Warning {
+ t.Errorf("unexpected severity of diagnostic %v; expected %v", diags[0].Severity, lsp.Warning)
+ }
+}
+
+func TestDiagnosticsFidlLint(t *testing.T) {
+ handler := NewHandlerWithFiles(TestFile{
+ Name: "test.fidl",
+ Text: `
+library x;
+
+enum my_enum {
+ IncorrectCamelCase = 1;
+};
+`,
+ })
+
+ allDiags, err := handler.getDiagnosticsForLibraryWithFile(lsp.DocumentURI("test.fidl"))
+ if err != nil {
+ t.Fatalf("failed to publish diagnostics: %s", err)
+ }
+ diags := allDiags["test.fidl"]
+ if len(diags) != 4 {
+ t.Fatalf("incorrect number of diagnostics %d; expected 4", len(diags))
+ }
+ for _, diag := range diags {
+ if diag.Source != "fidl-lint" {
+ t.Errorf("unexpected source of diagnostic %s; expected `fidl-lint`", diag.Source)
+ }
+ if diag.Severity != lsp.Warning {
+ t.Errorf("unexpected severity of diagnostic %v; expected %v", diag.Severity, lsp.Warning)
+ }
+ }
+}
+
+func TestErrorAndLint(t *testing.T) {
+ handler := NewHandlerWithFiles(TestFile{
+ Name: "test.fidl",
+ Text: `
+// expected lint: need Fuchsia copyright notice
+library test;
+
+protocol P {
+ DuplicateMethodName();
+ DuplicateMethodName();
+// expected error: duplicate method name
+};
+`,
+ })
+
+ diags, err := handler.getDiagnosticsForLibraryWithFile(lsp.DocumentURI("test.fidl"))
+ if err != nil {
+ t.Fatalf("failed to publish diagnostics: %s", err)
+ }
+ if len(diags["test.fidl"]) != 2 {
+ t.Fatalf("incorrect number of diagnostics %d; expected 3", len(diags["test.fidl"]))
+ }
+}
diff --git a/fidl-lsp/langserver/files.go b/fidl-lsp/langserver/files.go
new file mode 100644
index 0000000..c480a0c
--- /dev/null
+++ b/fidl-lsp/langserver/files.go
@@ -0,0 +1,43 @@
+// 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 langserver
+
+import (
+ "github.com/sourcegraph/go-lsp"
+
+ "fidl-lsp/state"
+)
+
+func (h *LangHandler) handleOpenFile(params lsp.DidOpenTextDocumentParams) {
+ h.fs.NewFile(state.FileID(params.TextDocument.URI), params.TextDocument.Text)
+}
+
+func (h *LangHandler) handleCloseFile(params lsp.DidCloseTextDocumentParams) {
+ h.fs.DeleteFile(state.FileID(params.TextDocument.URI))
+}
+
+func (h *LangHandler) handleDidChange(params lsp.DidChangeTextDocumentParams) {
+ // Convert from []lsp.TextDocumentContentChangeEvent --> []state.Change
+ changes := make([]state.Change, len(params.ContentChanges))
+ for i, lspChange := range params.ContentChanges {
+ changes[i] = state.Change{
+ Range: state.Range{
+ Start: state.Position{
+ Line: lspChange.Range.Start.Line,
+ Character: lspChange.Range.Start.Character,
+ },
+ End: state.Position{
+ Line: lspChange.Range.End.Line,
+ Character: lspChange.Range.End.Character,
+ },
+ },
+ NewContent: lspChange.Text,
+ }
+ }
+
+ if err := h.fs.ApplyChanges(state.FileID(params.TextDocument.URI), changes); err != nil {
+ h.log.Printf("error on ApplyChanges: %s", err)
+ }
+}
diff --git a/fidl-lsp/langserver/format.go b/fidl-lsp/langserver/format.go
new file mode 100644
index 0000000..a7b86c3
--- /dev/null
+++ b/fidl-lsp/langserver/format.go
@@ -0,0 +1,38 @@
+// 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 langserver
+
+import (
+ "strings"
+
+ "github.com/sourcegraph/go-lsp"
+
+ "fidl-lsp/state"
+)
+
+// handleFormat asks the Analyzer for the formatted version of the document
+// specified in `params` and returns any resulting TextEdits, or an error.
+//
+// In practice, since fidl-format only runs on an entire document, when
+// successful this will always return a single TextEdit spanning (and replacing)
+// the entire document with its formatted version.
+func (h *LangHandler) handleFormat(params lsp.DocumentFormattingParams) ([]lsp.TextEdit, error) {
+ formatted, err := h.analyzer.FormatFile(h.fs, state.FileID(params.TextDocument.URI))
+ if err != nil {
+ return nil, err
+ }
+
+ lines := strings.Split(formatted, "\n")
+ lastLine := lines[len(lines)-1]
+ lastChar := len([]rune(lastLine))
+
+ return []lsp.TextEdit{{
+ Range: lsp.Range{
+ Start: lsp.Position{},
+ End: lsp.Position{Line: len(lines), Character: lastChar},
+ },
+ NewText: formatted,
+ }}, nil
+}
diff --git a/fidl-lsp/langserver/format_test.go b/fidl-lsp/langserver/format_test.go
new file mode 100644
index 0000000..6dbcb04
--- /dev/null
+++ b/fidl-lsp/langserver/format_test.go
@@ -0,0 +1,99 @@
+// 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 langserver
+
+import (
+ "testing"
+
+ "github.com/sourcegraph/go-lsp"
+)
+
+func TestFormat(t *testing.T) {
+ handler := NewHandlerWithFiles(TestFile{
+ Name: "test.fidl",
+ Text: `
+library example;
+struct S { int32 field; };
+protocol P {
+UnindentedMethod ( uint8 param1 , string param2 ) ;
+};
+table T { 1 :uint u;
+};
+`,
+ })
+
+ edits, err := handler.handleFormat(lsp.DocumentFormattingParams{
+ TextDocument: lsp.TextDocumentIdentifier{
+ URI: lsp.DocumentURI("test.fidl"),
+ },
+ })
+ if err != nil {
+ t.Fatalf("failed to format file: %s", err)
+ }
+
+ if len(edits) != 1 {
+ t.Errorf("incorrect number of formatting edits: expected 1, got %d", len(edits))
+ }
+ expRange := lsp.Range{
+ Start: lsp.Position{Line: 0, Character: 0},
+ End: lsp.Position{Line: 14, Character: 0},
+ }
+ if edits[0].Range != expRange {
+ t.Errorf(
+ "formatted range does not span entire file: expected %v, got %v",
+ expRange,
+ edits[0].Range,
+ )
+ }
+
+ expText := `library example;
+
+struct S {
+ int32 field;
+};
+
+protocol P {
+ UnindentedMethod(uint8 param1, string param2);
+};
+
+table T {
+ 1: uint u;
+};
+`
+ if edits[0].NewText != expText {
+ t.Errorf("formatted text is incorrect: expected %s, got %s", expText, edits[0].NewText)
+ }
+}
+
+func TestFormatFileWithErrors(t *testing.T) {
+ handler := NewHandlerWithFiles(TestFile{
+ Name: "test.fidl",
+ Text: `library example`,
+ })
+
+ _, err := handler.handleFormat(lsp.DocumentFormattingParams{
+ TextDocument: lsp.TextDocumentIdentifier{
+ URI: lsp.DocumentURI("test.fidl"),
+ },
+ })
+
+ if err == nil {
+ t.Errorf("formatting file with errors did not fail")
+ }
+}
+
+func TestFormatNonexistentFile(t *testing.T) {
+ handler := NewHandlerWithFiles()
+
+ _, err := handler.handleFormat(lsp.DocumentFormattingParams{
+ TextDocument: lsp.TextDocumentIdentifier{
+ URI: lsp.DocumentURI("test.fidl"),
+ },
+ })
+
+ if err == nil {
+ t.Errorf("formatting nonexistent file did not fail")
+ }
+}
diff --git a/fidl-lsp/langserver/handler.go b/fidl-lsp/langserver/handler.go
new file mode 100644
index 0000000..686053c
--- /dev/null
+++ b/fidl-lsp/langserver/handler.go
@@ -0,0 +1,293 @@
+// 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 langserver
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+
+ fidlcommon "fidl-lsp/third_party/common"
+ "github.com/sourcegraph/go-lsp"
+ "github.com/sourcegraph/jsonrpc2"
+
+ "fidl-lsp/analysis"
+ "fidl-lsp/state"
+)
+
+// LspServer is a wrapper around a LangHandler.
+// It is the entry point for all LSP communication with the client.
+type LspServer struct {
+ jsonrpc2.Handler
+}
+
+// LangHandler implements jsonrpc2.Handler, an interface with one Handle method.
+// It fields all LSP requests from the client, and sends back responses and
+// notifications.
+//
+// LangHandler has a *state.FileSystem, which keeps track of the the language
+// editor's open files (in the future, to support concurrency, this state can be
+// wrapped in e.g. an RWLock).
+//
+// LangHandler also has an *analysis.Analyzer,
+// which is used to compile and extract insights from files being edited.
+type LangHandler struct {
+ cfg Config
+ init *lsp.InitializeParams
+ shutdown bool
+
+ log *log.Logger
+
+ fs *state.FileSystem
+
+ analyzer *analysis.Analyzer
+}
+
+// Assert that LangHandler implements the jsonrpc2.Handler interface
+var _ jsonrpc2.Handler = (*LangHandler)(nil)
+
+type Config struct {
+ serverCapabilities serverCapabilities
+
+ // The base URI for reference links. Currently we linkify Fuchsia platform
+ // library imports, and the links point to the SDK docs.
+ baseRefURI string
+}
+
+func (c *Config) FormatRefURI(libraryName fidlcommon.LibraryName) string {
+ return c.baseRefURI + libraryName.FullyQualifiedName()
+}
+
+// NewDefaultConfig returns a default configuration for the language server.
+func NewDefaultConfig() Config {
+ return Config{
+ serverCapabilities: serverCapabilities{
+ DefinitionProvider: true,
+ DocumentFormattingProvider: true,
+ DocumentLinkProvider: &documentLinkOptions{},
+ HoverProvider: true,
+ ReferencesProvider: true,
+ TextDocumentSync: &lsp.TextDocumentSyncOptionsOrKind{
+ Options: &lsp.TextDocumentSyncOptions{
+ OpenClose: true,
+ Change: lsp.TDSKIncremental,
+ },
+ },
+ },
+ baseRefURI: "https://fuchsia.dev/reference/fidl/",
+ }
+}
+
+// NewLangHandler returns a LangHandler with the given configuration, logger,
+// and set of CompiledLibraries.
+func NewLangHandler(cfg Config, trace *log.Logger, analyzer *analysis.Analyzer) *LangHandler {
+ return &LangHandler{
+ log: trace,
+ cfg: cfg,
+ fs: state.NewFileSystem(),
+ analyzer: analyzer,
+ }
+}
+
+// Handle fields an LSP request or notification.
+// If there is an internal server error, it will be logged and an empty response
+// will be sent to the client. If there is an error in the request, an error
+// reponse will be sent.
+func (h *LangHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {
+ if resp, handleErr := h.handle(ctx, conn, req); handleErr == nil {
+ if resp != nil {
+ if err := conn.Reply(ctx, req.ID, resp); err != nil {
+ h.log.Printf("error on conn.Reply: %s\n", err)
+ }
+ }
+ } else {
+ switch handleErr.(type) {
+ case *jsonrpc2.Error:
+ h.log.Printf("got a request we couldn't handle: %s\n", req.Method)
+ if err := conn.ReplyWithError(ctx, req.ID, handleErr.(*jsonrpc2.Error)); err != nil {
+ h.log.Printf("error on conn.ReplyWithError: %s\n", err)
+ }
+ default:
+ // If the error is not a jsonrpc2.Error, this means that we had an
+ // internal server error and were unable to meet the request. Rather
+ // than pass that error on to the client, we should log the error
+ // and reply with an empty response so the client is not waiting for
+ // a response.
+ h.log.Printf("err on handling `%s`: %s\n", req.Method, handleErr)
+ if err := conn.Reply(ctx, req.ID, resp); err != nil {
+ h.log.Printf("error on conn.Reply: %s\n", err)
+ }
+ }
+ }
+}
+
+// Notify sends an LSP notification.
+func (h *LangHandler) Notify(ctx context.Context, conn *jsonrpc2.Conn, method string, notif interface{}) {
+ if err := conn.Notify(ctx, method, notif); err != nil {
+ h.log.Printf("error on conn.Reply: %s\n", err)
+ }
+}
+
+// The general pattern of the `handle` callback is as follows:
+//
+// * receive a notification from the client indicating a change of state -->
+// pass the change to the FileSystem -->
+// trigger a re-analysis of the file that was changed -->
+// publish diagnostics on that file.
+//
+// * receive a request from the client -->
+// request the relevant analysis from the Analyzer, passing it a snapshot of
+// the state of the FileSystem -->
+// wrap the analysis in relevant LSP type/object and send to the client.
+func (h *LangHandler) handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) {
+ // TODO: locking over `h`
+ switch req.Method {
+ case "initialize":
+ // TODO: if server receives any request before `initialize`, it should
+ // respond with error code: -32002
+ if h.init != nil {
+ return nil, &jsonrpc2.Error{
+ Code: jsonrpc2.CodeInvalidRequest,
+ Message: "language server is already initialized",
+ }
+ }
+ if req.Params == nil {
+ return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+ }
+ var params lsp.InitializeParams
+ if err := json.Unmarshal(*req.Params, ¶ms); err != nil {
+ return nil, err
+ }
+ h.init = ¶ms
+ return initializeResult{Capabilities: h.cfg.serverCapabilities}, nil
+
+ case "initialized":
+ // A notification that the client is ready to receive requests. Ignore
+ return nil, nil
+
+ case "shutdown":
+ // TODO: if server receives any request after `shutdown`, it should
+ // respond with an error: InvalidRequest.
+ h.shutdown = true
+ h.analyzer.Cleanup()
+ return nil, nil
+
+ case "exit":
+ if h.shutdown {
+ os.Exit(0)
+ } else {
+ os.Exit(1)
+ }
+ return nil, nil
+
+ case "textDocument/didOpen":
+ if req.Params == nil {
+ return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+ }
+ var params lsp.DidOpenTextDocumentParams
+ if err := json.Unmarshal(*req.Params, ¶ms); err != nil {
+ return nil, err
+ }
+
+ h.handleOpenFile(params)
+ if err := h.analyzer.Analyze(h.fs, state.FileID(params.TextDocument.URI)); err != nil {
+ h.log.Println(err)
+ }
+ h.publishDiagnostics(ctx, conn, params.TextDocument.URI)
+ return nil, nil
+
+ case "textDocument/didClose":
+ if req.Params == nil {
+ return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+ }
+ var params lsp.DidCloseTextDocumentParams
+ if err := json.Unmarshal(*req.Params, ¶ms); err != nil {
+ return nil, err
+ }
+
+ h.handleCloseFile(params)
+ // TODO: remove diagnostics associated with this file?
+ return nil, nil
+
+ case "textDocument/didChange":
+ if req.Params == nil {
+ return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+ }
+ var params lsp.DidChangeTextDocumentParams
+ if err := json.Unmarshal(*req.Params, ¶ms); err != nil {
+ return nil, err
+ }
+
+ h.handleDidChange(params)
+ if err := h.analyzer.Analyze(h.fs, state.FileID(params.TextDocument.URI)); err != nil {
+ h.log.Println(err)
+ }
+ h.publishDiagnostics(ctx, conn, params.TextDocument.URI)
+ return nil, nil
+
+ case "textDocument/definition":
+ if req.Params == nil {
+ return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+ }
+ var params lsp.TextDocumentPositionParams
+ if err := json.Unmarshal(*req.Params, ¶ms); err != nil {
+ return nil, err
+ }
+
+ return h.handleDefinition(params)
+
+ case "textDocument/documentLink":
+ if req.Params == nil {
+ return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+ }
+ var params documentLinkParams
+ if err := json.Unmarshal(*req.Params, ¶ms); err != nil {
+ return nil, err
+ }
+
+ return h.handleDocumentLinks(params)
+
+ case "textDocument/formatting":
+ if req.Params == nil {
+ return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+ }
+ var params lsp.DocumentFormattingParams
+ if err := json.Unmarshal(*req.Params, ¶ms); err != nil {
+ return nil, err
+ }
+
+ return h.handleFormat(params)
+
+ case "textDocument/hover":
+ if req.Params == nil {
+ return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+ }
+ var params lsp.TextDocumentPositionParams
+ if err := json.Unmarshal(*req.Params, ¶ms); err != nil {
+ return nil, err
+ }
+
+ return h.handleHover(params)
+
+ case "textDocument/references":
+ if req.Params == nil {
+ return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+ }
+ var params lsp.TextDocumentPositionParams
+ if err := json.Unmarshal(*req.Params, ¶ms); err != nil {
+ return nil, err
+ }
+
+ return h.handleReferences(params)
+
+ default:
+ return nil, &jsonrpc2.Error{
+ Code: jsonrpc2.CodeMethodNotFound,
+ Message: fmt.Sprintf("method not supported: %s", req.Method),
+ }
+ }
+}
diff --git a/fidl-lsp/langserver/hover.go b/fidl-lsp/langserver/hover.go
new file mode 100644
index 0000000..5977b4c
--- /dev/null
+++ b/fidl-lsp/langserver/hover.go
@@ -0,0 +1,318 @@
+// 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 langserver
+
+import (
+ "fmt"
+
+ "github.com/sourcegraph/go-lsp"
+
+ "fidl-lsp/analysis"
+ "fidl-lsp/state"
+)
+
+// handleHover asks the FileSystem for the symbol at the location specified in
+// `params`, and then asks the Analyzer for the type of that symbol.
+//
+// It then converts that type to a human-readable string representation that
+// will be displayed as a tooltip, including doc comments, other attributes on
+// the type, and the type itself. For example, given this FIDL:
+//
+// library example;
+//
+// /// Foo is a struct.
+// [Attribute = "Value"]
+// struct Foo {};
+//
+// protocol Bar {
+// Method(Foo foo);
+// ~~~
+// }
+//
+// Hovering over the `~~~` would give you this tooltip:
+//
+// Foo is a struct. // In plain text
+// [Attribute = "Value"] // In monospace font
+// struct example/Foo
+//
+func (h *LangHandler) handleHover(params lsp.TextDocumentPositionParams) (lsp.Hover, error) {
+ sym, err := h.fs.SymbolAtPos(
+ state.FileID(params.TextDocument.URI),
+ state.Position{Line: params.Position.Line, Character: params.Position.Character},
+ )
+ if err != nil {
+ h.log.Printf(
+ "could not find symbol at position `%#v` in document `%s`\n",
+ params.Position,
+ params.TextDocument.URI,
+ )
+ return lsp.Hover{}, err
+ }
+
+ symType, err := h.analyzer.TypeOfSymbol(h.fs, sym)
+ if err != nil {
+ h.log.Printf("could not get type of symbol `%s`: %s", sym.Name, err)
+ return lsp.Hover{}, err
+ }
+
+ // Convert SymbolType --> []lsp.MarkedString
+ typeDescription, err := symbolTypeToMarkedStrings(sym.Name, symType)
+ if err != nil {
+ h.log.Printf("could not get string representation of symbol `%s`'s type: %s", sym.Name, err)
+ return lsp.Hover{}, err
+ }
+
+ return lsp.Hover{
+ Contents: typeDescription,
+ Range: &lsp.Range{
+ Start: lsp.Position{
+ Line: sym.Location.Range.Start.Line,
+ Character: sym.Location.Range.Start.Character,
+ },
+ End: lsp.Position{
+ Line: sym.Location.Range.End.Line,
+ Character: sym.Location.Range.End.Character,
+ },
+ },
+ }, nil
+}
+
+func newFIDLString(s string) lsp.MarkedString {
+ return lsp.MarkedString{
+ Language: "fidl",
+ Value: s,
+ }
+}
+
+func symbolTypeToMarkedStrings(name string, symType analysis.Type) ([]lsp.MarkedString, error) {
+ if symType.IsLib {
+ return []lsp.MarkedString{{
+ Language: "fidl",
+ Value: fmt.Sprintf("library %s", name),
+ }}, nil
+ }
+
+ res := []lsp.MarkedString{}
+
+ var docStr *lsp.MarkedString
+ attrList := "["
+ firstAttr := true
+ // Add attributes as a marked string
+ for _, attr := range symType.Attrs {
+ if attr.Name == "Doc" {
+ rawMarkedStr := lsp.RawMarkedString(attr.Value)
+ docStr = &rawMarkedStr
+ } else {
+ if !firstAttr {
+ attrList += ", "
+ }
+ firstAttr = false
+ if attr.Value != "" {
+ attrList += fmt.Sprintf("%s = \"%s\"", attr.Name, attr.Value)
+ } else {
+ attrList += attr.Name
+ }
+ }
+ }
+ attrList += "]"
+ // Prepend doc string if there is one
+ if docStr != nil {
+ res = append([]lsp.MarkedString{*docStr}, res...)
+ }
+ if !firstAttr {
+ res = append(res, lsp.MarkedString{
+ Language: "fidl",
+ Value: attrList,
+ })
+ }
+
+ switch symType.Kind {
+ default:
+ return nil, fmt.Errorf("unknown kind of symbol: %s", symType.Kind)
+
+ case analysis.ArrayType:
+ if symType.Array == nil {
+ goto TypeInfoNotSet
+ }
+ res = append(res, newFIDLString(shortName(symType)))
+
+ case analysis.VectorType:
+ if symType.Vector == nil {
+ goto TypeInfoNotSet
+ }
+ res = append(res, newFIDLString(shortName(symType)))
+
+ case analysis.StringType:
+ if symType.String == nil {
+ goto TypeInfoNotSet
+ }
+ res = append(res, newFIDLString(shortName(symType)))
+
+ case analysis.HandleType:
+ if symType.Handle == nil {
+ goto TypeInfoNotSet
+ }
+ res = append(res, newFIDLString(shortName(symType)))
+
+ case analysis.RequestType:
+ if symType.Request == nil {
+ goto TypeInfoNotSet
+ }
+ res = append(res, newFIDLString(shortName(symType)))
+
+ case analysis.PrimitiveType:
+ if symType.Primitive == nil {
+ goto TypeInfoNotSet
+ }
+ res = append(res, newFIDLString(shortName(symType)))
+
+ case analysis.IdentifierType:
+ if symType.Identifier == nil {
+ goto TypeInfoNotSet
+ }
+
+ if !symType.Identifier.IsDecl {
+ res = append(res, newFIDLString(shortName(symType)))
+ break
+ }
+
+ switch symType.Identifier.Kind {
+ default:
+ return nil, fmt.Errorf("unknown identifier kind: %s", symType.Identifier.Kind)
+
+ case analysis.BitsKind:
+ if symType.Identifier.Bits == nil {
+ goto IdentifierTypeInfoNotSet
+ }
+ res = append(res, newFIDLString(
+ fmt.Sprintf(
+ "bits %s: %s",
+ symType.Identifier.Name,
+ shortName(symType.Identifier.Bits.Type),
+ ),
+ ))
+
+ case analysis.EnumKind:
+ if symType.Identifier.Enum == nil {
+ goto IdentifierTypeInfoNotSet
+ }
+ res = append(res, newFIDLString(
+ fmt.Sprintf(
+ "enum %s: %s",
+ symType.Identifier.Name,
+ symType.Identifier.Enum.Type,
+ ),
+ ))
+
+ case analysis.ConstKind:
+ if symType.Identifier.Const == nil {
+ goto IdentifierTypeInfoNotSet
+ }
+ res = append(res, newFIDLString(
+ fmt.Sprintf(
+ "const %s %s = `%s`",
+ shortName(symType.Identifier.Const.Type),
+ symType.Identifier.Name,
+ symType.Identifier.Const.Value,
+ ),
+ ))
+
+ case analysis.TypeAliasKind:
+ if symType.Identifier.TypeAlias == nil {
+ goto IdentifierTypeInfoNotSet
+ }
+ res = append(res, newFIDLString(
+ fmt.Sprintf(
+ "%s: alias to %s",
+ symType.Identifier.Name,
+ shortName(symType.Identifier.TypeAlias.Type),
+ ),
+ ))
+
+ case analysis.ProtocolKind,
+ analysis.ServiceKind,
+ analysis.StructKind,
+ analysis.TableKind,
+ analysis.UnionKind:
+ res = append(res, newFIDLString(
+ fmt.Sprintf("%s %s", symType.Identifier.Kind, symType.Identifier.Name),
+ ))
+ }
+ }
+
+ return res, nil
+
+TypeInfoNotSet:
+ return nil, fmt.Errorf(
+ "symbol is tagged as a `%s` but `%s` type info not set",
+ symType.Kind,
+ symType.Kind,
+ )
+IdentifierTypeInfoNotSet:
+ return nil, fmt.Errorf(
+ "symbol is tagged as a `%s` but `%s` type info not set",
+ symType.Identifier.Kind,
+ symType.Identifier.Kind,
+ )
+}
+
+func shortName(t analysis.Type) string {
+ switch t.Kind {
+ default:
+ return "unknown type"
+
+ case analysis.ArrayType:
+ return fmt.Sprintf("array<%s>:%d", shortName(t.Array.ElementType), t.Array.ElementCount)
+
+ case analysis.VectorType:
+ str := fmt.Sprintf("vector<%s>", shortName(t.Vector.ElementType))
+ if t.Vector.ElementCount != nil {
+ str += fmt.Sprintf(":%d", *t.Vector.ElementCount)
+ }
+ if t.Vector.Nullable {
+ str += "?"
+ }
+ return str
+
+ case analysis.StringType:
+ str := "string"
+ if t.String.ElementCount != nil {
+ str += fmt.Sprintf(":%d", *t.String.ElementCount)
+ }
+ if t.String.Nullable {
+ str += "?"
+ }
+ return str
+
+ case analysis.RequestType:
+ str := fmt.Sprintf("request<%s>", t.Request.Subtype)
+ if t.Request.Nullable {
+ str += "?"
+ }
+ return str
+
+ case analysis.HandleType:
+ str := fmt.Sprintf("handle<%s>", t.Handle.Subtype)
+ if t.Handle.Nullable {
+ str += "?"
+ }
+ return str
+
+ case analysis.PrimitiveType:
+ return string(t.Primitive.Subtype)
+
+ case analysis.IdentifierType:
+ var str string
+ if t.Identifier.IsDecl {
+ str = t.Identifier.Name
+ } else {
+ str = t.Identifier.Identifier
+ }
+ if t.Identifier.Nullable {
+ str += "?"
+ }
+ return str
+ }
+}
diff --git a/fidl-lsp/langserver/hover_test.go b/fidl-lsp/langserver/hover_test.go
new file mode 100644
index 0000000..7b0bf0a
--- /dev/null
+++ b/fidl-lsp/langserver/hover_test.go
@@ -0,0 +1,366 @@
+// 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 langserver
+
+import (
+ "testing"
+
+ "github.com/sourcegraph/go-lsp"
+
+ "fidl-lsp/analysis"
+)
+
+func TestHover(t *testing.T) {
+ var elementCount uint = 10
+
+ cases := []struct {
+ symName string
+ symType analysis.Type
+ hoverText []lsp.MarkedString
+ }{
+ {
+ symName: "fuchsia.test",
+ symType: analysis.Type{
+ IsLib: true,
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("library fuchsia.test")},
+ },
+ {
+ symName: "array_value",
+ symType: analysis.Type{
+ Kind: analysis.ArrayType,
+ Array: &analysis.ArrayTypeInfo{
+ ElementType: analysis.Type{
+ Kind: analysis.PrimitiveType,
+ Primitive: &analysis.PrimitiveTypeInfo{
+ Subtype: analysis.Uint32,
+ },
+ },
+ ElementCount: 10,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("array<uint32>:10")},
+ },
+ {
+ symName: "vector_without_size_constraint",
+ symType: analysis.Type{
+ Kind: analysis.VectorType,
+ Vector: &analysis.VectorTypeInfo{
+ ElementType: analysis.Type{
+ Kind: analysis.PrimitiveType,
+ Primitive: &analysis.PrimitiveTypeInfo{
+ Subtype: analysis.Uint32,
+ },
+ },
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("vector<uint32>")},
+ },
+ {
+ symName: "nullable_vector_with_size_constraint",
+ symType: analysis.Type{
+ Kind: analysis.VectorType,
+ Vector: &analysis.VectorTypeInfo{
+ ElementType: analysis.Type{
+ Kind: analysis.PrimitiveType,
+ Primitive: &analysis.PrimitiveTypeInfo{
+ Subtype: analysis.Uint32,
+ },
+ },
+ ElementCount: &elementCount,
+ Nullable: true,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("vector<uint32>:10?")},
+ },
+ {
+ symName: "nullable_string_without_size_constraint",
+ symType: analysis.Type{
+ Kind: analysis.StringType,
+ String: &analysis.StringTypeInfo{
+ Nullable: true,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("string?")},
+ },
+ {
+ symName: "string_with_size_constraint",
+ symType: analysis.Type{
+ Kind: analysis.StringType,
+ String: &analysis.StringTypeInfo{
+ ElementCount: &elementCount,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("string:10")},
+ },
+ {
+ symName: "handle",
+ symType: analysis.Type{
+ Kind: analysis.HandleType,
+ Handle: &analysis.HandleTypeInfo{
+ Subtype: analysis.Vmo,
+ Nullable: true,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("handle<vmo>?")},
+ },
+ {
+ symName: "request",
+ symType: analysis.Type{
+ Kind: analysis.RequestType,
+ Request: &analysis.RequestTypeInfo{
+ Subtype: "File",
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("request<File>")},
+ },
+ {
+ symName: "primitive_bool",
+ symType: analysis.Type{
+ Kind: analysis.PrimitiveType,
+ Primitive: &analysis.PrimitiveTypeInfo{
+ Subtype: analysis.Bool,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("bool")},
+ },
+ {
+ symName: "primitive_uint64",
+ symType: analysis.Type{
+ Kind: analysis.PrimitiveType,
+ Primitive: &analysis.PrimitiveTypeInfo{
+ Subtype: analysis.Uint64,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("uint64")},
+ },
+ {
+ symName: "bits_decl",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: true,
+ Name: "fuchsia.test/MyBits",
+ Kind: analysis.BitsKind,
+ Bits: &analysis.BitsTypeInfo{
+ Type: analysis.Type{
+ Kind: analysis.PrimitiveType,
+ Primitive: &analysis.PrimitiveTypeInfo{
+ Subtype: analysis.Uint32,
+ },
+ },
+ },
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("bits fuchsia.test/MyBits: uint32")},
+ },
+ {
+ symName: "enum_decl",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: true,
+ Name: "fuchsia.test/MyEnum",
+ Kind: analysis.EnumKind,
+ Enum: &analysis.EnumTypeInfo{
+ Type: "string",
+ },
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("enum fuchsia.test/MyEnum: string")},
+ },
+ {
+ symName: "const_decl",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: true,
+ Name: "fuchsia.test/MyConst",
+ Kind: analysis.ConstKind,
+ Const: &analysis.ConstTypeInfo{
+ Type: analysis.Type{
+ Kind: analysis.StringType,
+ String: &analysis.StringTypeInfo{},
+ },
+ Value: "hello, world!",
+ },
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("const string fuchsia.test/MyConst = `hello, world!`")},
+ },
+ {
+ symName: "protocol_decl",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: true,
+ Name: "fuchsia.test/MyProtocol",
+ Kind: analysis.ProtocolKind,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("protocol fuchsia.test/MyProtocol")},
+ },
+ {
+ symName: "service_decl",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: true,
+ Name: "fuchsia.test/MyService",
+ Kind: analysis.ServiceKind,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("service fuchsia.test/MyService")},
+ },
+ {
+ symName: "struct_decl",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: true,
+ Name: "fuchsia.test/MyStruct",
+ Kind: analysis.StructKind,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("struct fuchsia.test/MyStruct")},
+ },
+ {
+ symName: "table_decl",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: true,
+ Name: "fuchsia.test/MyTable",
+ Kind: analysis.TableKind,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("table fuchsia.test/MyTable")},
+ },
+ {
+ symName: "union_decl",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: true,
+ Name: "fuchsia.test/MyUnion",
+ Kind: analysis.UnionKind,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("union fuchsia.test/MyUnion")},
+ },
+ {
+ symName: "type_alias_decl",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: true,
+ Name: "fuchsia.test/AliasToVectorOfBools",
+ Kind: analysis.TypeAliasKind,
+ TypeAlias: &analysis.TypeAliasTypeInfo{
+ Type: analysis.Type{
+ Kind: analysis.VectorType,
+ Vector: &analysis.VectorTypeInfo{
+ ElementType: analysis.Type{
+ Kind: analysis.PrimitiveType,
+ Primitive: &analysis.PrimitiveTypeInfo{
+ Subtype: analysis.Bool,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("fuchsia.test/AliasToVectorOfBools: alias to vector<bool>")},
+ },
+ {
+ symName: "nullable_struct_param",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: false,
+ Identifier: "fuchsia.test/MyStruct",
+ Nullable: true,
+ },
+ },
+ hoverText: []lsp.MarkedString{newFIDLString("fuchsia.test/MyStruct?")},
+ },
+ {
+ symName: "type_with_attributes",
+ symType: analysis.Type{
+ Kind: analysis.IdentifierType,
+ Identifier: &analysis.IdentifierTypeInfo{
+ IsDecl: true,
+ Name: "fuchsia.test/MyProtocol",
+ Kind: analysis.ProtocolKind,
+ },
+ Attrs: []analysis.Attribute{
+ {
+ Name: "Doc",
+ Value: "Example doc comments on MyProtocol",
+ },
+ {Name: "Transitional"},
+ {
+ Name: "OtherAttribute",
+ Value: "AttributeValue",
+ },
+ },
+ },
+ hoverText: []lsp.MarkedString{
+ lsp.RawMarkedString(`Example doc comments on MyProtocol`),
+ newFIDLString(`[Transitional, OtherAttribute = "AttributeValue"]`),
+ newFIDLString(`protocol fuchsia.test/MyProtocol`),
+ },
+ },
+ }
+
+ for _, ex := range cases {
+ hoverText, err := symbolTypeToMarkedStrings(ex.symName, ex.symType)
+ if err != nil {
+ t.Errorf("could not get hover text for symbol `%s`: %s", ex.symName, err)
+ continue
+ }
+ if len(hoverText) != len(ex.hoverText) {
+ t.Errorf(
+ "incorrect number of marked strings for symbol `%s`: expected %d, got %d",
+ ex.symName,
+ len(ex.hoverText),
+ len(hoverText),
+ )
+ continue
+ }
+ for i, expMarkedString := range ex.hoverText {
+ if hoverText[i] != expMarkedString {
+ t.Errorf(
+ "incorrect hoverText for symbol `%s`: expected %v, got %v",
+ ex.symName,
+ expMarkedString,
+ hoverText[i],
+ )
+ }
+ }
+ }
+}
+
+func TestHoverInvalidType(t *testing.T) {
+ _, err := symbolTypeToMarkedStrings(
+ "invalid_type",
+ analysis.Type{
+ Kind: analysis.PrimitiveType,
+ Array: &analysis.ArrayTypeInfo{
+ ElementType: analysis.Type{
+ Kind: analysis.PrimitiveType,
+ Primitive: &analysis.PrimitiveTypeInfo{
+ Subtype: analysis.Uint64,
+ },
+ },
+ ElementCount: 255,
+ },
+ },
+ )
+ if err == nil {
+ t.Errorf("expect error for hover text on invalid type")
+ }
+}
diff --git a/fidl-lsp/langserver/links.go b/fidl-lsp/langserver/links.go
new file mode 100644
index 0000000..378bff8
--- /dev/null
+++ b/fidl-lsp/langserver/links.go
@@ -0,0 +1,42 @@
+// 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 langserver
+
+import (
+ "fmt"
+
+ "github.com/sourcegraph/go-lsp"
+
+ "fidl-lsp/state"
+)
+
+func (h *LangHandler) handleDocumentLinks(params documentLinkParams) ([]documentLink, error) {
+ file, err := h.fs.File(state.FileID(params.TextDocument.URI))
+ if err != nil {
+ return nil, fmt.Errorf("could not find file `%s`", params.TextDocument.URI)
+ }
+ imports := state.ParsePlatformImportsMatch(file)
+ links := []documentLink{}
+ for _, m := range imports {
+ links = append(links, documentLink{
+ Range: lsp.Range{
+ Start: lsp.Position{
+ Line: m.Range.Start.Line,
+ Character: m.Range.Start.Character,
+ },
+ End: lsp.Position{
+ Line: m.Range.End.Line,
+ Character: m.Range.End.Character,
+ },
+ },
+ Target: lsp.DocumentURI(h.cfg.FormatRefURI(m.Lib)),
+ Tooltip: fmt.Sprintf("Fuchsia SDK Docs for library %s", m.Lib.FullyQualifiedName()),
+ })
+ }
+
+ return links, nil
+}
+
+// TODO: fxr/ --> fxrev.dev/, fxb/ --> fxbug.dev/?
diff --git a/fidl-lsp/langserver/links_test.go b/fidl-lsp/langserver/links_test.go
new file mode 100644
index 0000000..2c65e7a
--- /dev/null
+++ b/fidl-lsp/langserver/links_test.go
@@ -0,0 +1,72 @@
+// 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 langserver
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/sourcegraph/go-lsp"
+
+ "fidl-lsp/state"
+)
+
+func TestLinks(t *testing.T) {
+ fileText := `
+library test;
+
+using fuchsia.io;
+using non.platform.import;
+`
+ lines := strings.SplitAfter(fileText, "\n")
+
+ handler := NewHandlerWithFiles(TestFile{
+ Name: "test.fidl",
+ Text: fileText,
+ })
+
+ links, err := handler.handleDocumentLinks(documentLinkParams{
+ TextDocument: lsp.TextDocumentIdentifier{
+ URI: lsp.DocumentURI("test.fidl"),
+ },
+ })
+ if err != nil {
+ t.Fatalf("failed to get document links: %s", err)
+ }
+ expLinks := map[string]lsp.DocumentURI{
+ "fuchsia.io": lsp.DocumentURI("https://fuchsia.dev/reference/fidl/fuchsia.io"),
+ }
+ if len(links) != len(expLinks) {
+ t.Fatalf("incorrect number of links; expected 1, actual %d", len(links))
+ }
+ for expSpan, expLink := range expLinks {
+ found := false
+ for _, link := range links {
+ start, err := state.OffsetInFile(
+ lines,
+ state.Position{Line: link.Range.Start.Line, Character: link.Range.Start.Character},
+ )
+ if err != nil {
+ t.Fatalf("could not get offset of link in file: %s", err)
+ }
+ end, err := state.OffsetInFile(
+ lines,
+ state.Position{Line: link.Range.End.Line, Character: link.Range.End.Character},
+ )
+ if err != nil {
+ t.Fatalf("could not get offset of link in file: %s", err)
+ }
+ span := fileText[start:end]
+ if span == expSpan && link.Target == expLink {
+ found = true
+ break
+ }
+ }
+ // Check if it's in links
+ if !found {
+ t.Fatalf("missing expected link `%s` on span `%s`", expLink, expSpan)
+ }
+ }
+}
diff --git a/fidl-lsp/langserver/lsp.go b/fidl-lsp/langserver/lsp.go
new file mode 100644
index 0000000..07e0f55
--- /dev/null
+++ b/fidl-lsp/langserver/lsp.go
@@ -0,0 +1,67 @@
+// 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 langserver
+
+import "github.com/sourcegraph/go-lsp"
+
+// `go-lsp` does not include all the necessary types for an LSP server. For
+// example, the types associated with the "textDocument/documentLink" language
+// feature are missing. This file includes those missing definitions.
+
+type serverCapabilities struct {
+ TextDocumentSync *lsp.TextDocumentSyncOptionsOrKind `json:"textDocumentSync,omitempty"`
+ HoverProvider bool `json:"hoverProvider,omitempty"`
+ CompletionProvider *lsp.CompletionOptions `json:"completionProvider,omitempty"`
+ SignatureHelpProvider *lsp.SignatureHelpOptions `json:"signatureHelpProvider,omitempty"`
+ DefinitionProvider bool `json:"definitionProvider,omitempty"`
+ TypeDefinitionProvider bool `json:"typeDefinitionProvider,omitempty"`
+ ReferencesProvider bool `json:"referencesProvider,omitempty"`
+ DocumentHighlightProvider bool `json:"documentHighlightProvider,omitempty"`
+ DocumentSymbolProvider bool `json:"documentSymbolProvider,omitempty"`
+ WorkspaceSymbolProvider bool `json:"workspaceSymbolProvider,omitempty"`
+ ImplementationProvider bool `json:"implementationProvider,omitempty"`
+ CodeActionProvider bool `json:"codeActionProvider,omitempty"`
+ CodeLensProvider *lsp.CodeLensOptions `json:"codeLensProvider,omitempty"`
+ DocumentFormattingProvider bool `json:"documentFormattingProvider,omitempty"`
+ DocumentRangeFormattingProvider bool `json:"documentRangeFormattingProvider,omitempty"`
+ DocumentOnTypeFormattingProvider *lsp.DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"`
+ DocumentLinkProvider *documentLinkOptions `json:"documentLinkProvider,omitempty"`
+ RenameProvider bool `json:"renameProvider,omitempty"`
+ ExecuteCommandProvider *lsp.ExecuteCommandOptions `json:"executeCommandProvider,omitempty"`
+ SemanticHighlighting *lsp.SemanticHighlightingOptions `json:"semanticHighlighting,omitempty"`
+
+ // XWorkspaceReferencesProvider indicates the server provides support for
+ // xworkspace/references. This is a Sourcegraph extension.
+ XWorkspaceReferencesProvider bool `json:"xworkspaceReferencesProvider,omitempty"`
+
+ // XDefinitionProvider indicates the server provides support for
+ // textDocument/xdefinition. This is a Sourcegraph extension.
+ XDefinitionProvider bool `json:"xdefinitionProvider,omitempty"`
+
+ // XWorkspaceSymbolByProperties indicates the server provides support for
+ // querying symbols by properties with WorkspaceSymbolParams.symbol. This
+ // is a Sourcegraph extension.
+ XWorkspaceSymbolByProperties bool `json:"xworkspaceSymbolByProperties,omitempty"`
+
+ Experimental interface{} `json:"experimental,omitempty"`
+}
+
+type initializeResult struct {
+ Capabilities serverCapabilities `json:"capabilities,omitempty"`
+}
+
+type documentLinkOptions struct {
+ ResolveProvider bool `json:"resolveProvider,omitempty"`
+}
+
+type documentLinkParams struct {
+ TextDocument lsp.TextDocumentIdentifier `json:"textDocument"`
+}
+
+type documentLink struct {
+ Range lsp.Range `json:"range"`
+ Target lsp.DocumentURI `json:"target"`
+ Tooltip string `json:"tooltip"`
+}
diff --git a/fidl-lsp/langserver/references.go b/fidl-lsp/langserver/references.go
new file mode 100644
index 0000000..26e41a2
--- /dev/null
+++ b/fidl-lsp/langserver/references.go
@@ -0,0 +1,45 @@
+// 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 langserver
+
+import (
+ "github.com/sourcegraph/go-lsp"
+
+ "fidl-lsp/state"
+)
+
+func (h *LangHandler) handleReferences(params lsp.TextDocumentPositionParams) ([]lsp.Location, error) {
+ sym, err := h.fs.SymbolAtPos(
+ state.FileID(params.TextDocument.URI),
+ state.Position{Line: params.Position.Line, Character: params.Position.Character},
+ )
+ if err != nil {
+ h.log.Printf(
+ "could not find symbol at position `%#v` in document `%s`\n",
+ params.Position,
+ params.TextDocument.URI,
+ )
+ return []lsp.Location{}, nil
+ }
+
+ locs, err := h.analyzer.ReferencesToSymbol(h.fs, sym)
+ if err != nil {
+ h.log.Printf("error on definition: %s\n", err)
+ return []lsp.Location{}, nil
+ }
+
+ // Convert state.Locations --> lsp.Locations
+ lspLocs := make([]lsp.Location, len(locs))
+ for i, loc := range locs {
+ lspLocs[i] = lsp.Location{
+ URI: lsp.DocumentURI(loc.FileID),
+ Range: lsp.Range{
+ Start: lsp.Position{Line: loc.Range.Start.Line, Character: loc.Range.Start.Character},
+ End: lsp.Position{Line: loc.Range.End.Line, Character: loc.Range.End.Character},
+ },
+ }
+ }
+ return lspLocs, nil
+}
diff --git a/fidl-lsp/langserver/testutils.go b/fidl-lsp/langserver/testutils.go
new file mode 100644
index 0000000..43ce4c5
--- /dev/null
+++ b/fidl-lsp/langserver/testutils.go
@@ -0,0 +1,49 @@
+// 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 langserver
+
+import (
+ "log"
+ "os"
+
+ "fidl-lsp/analysis"
+ "fidl-lsp/state"
+)
+
+var (
+ fuchsiaDir = os.Getenv("FUCHSIA_DIR")
+ toolsPath = fuchsiaDir + "/out/default.zircon/tools/"
+ fidlcPath = toolsPath + "fidlc"
+ fidlLintPath = toolsPath + "fidl-lint"
+ fidlFormatPath = toolsPath + "fidl-format"
+)
+
+type TestFile struct {
+ Name string
+ Text string
+}
+
+func NewHandlerWithFiles(files ...TestFile) *LangHandler {
+ handler := NewLangHandler(
+ NewDefaultConfig(),
+ log.New(os.Stderr, "[LSP Test] ", log.Lshortfile),
+ analysis.NewAnalyzer(
+ analysis.Config{
+ BuildRootDir: fuchsiaDir,
+ FidlcPath: fidlcPath,
+ FidlLintPath: fidlLintPath,
+ FidlFormatPath: fidlFormatPath,
+ },
+ analysis.CompiledLibraries{},
+ ),
+ )
+
+ for _, file := range files {
+ handler.fs.NewFile(state.FileID(file.Name), file.Text)
+ handler.analyzer.Analyze(handler.fs, state.FileID(file.Name))
+ }
+
+ return handler
+}
diff --git a/fidl-lsp/main.go b/fidl-lsp/main.go
new file mode 100644
index 0000000..8a387bd
--- /dev/null
+++ b/fidl-lsp/main.go
@@ -0,0 +1,82 @@
+// 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 (
+ "context"
+ "log"
+ "os"
+
+ "github.com/sourcegraph/jsonrpc2"
+
+ "fidl-lsp/analysis"
+ "fidl-lsp/langserver"
+)
+
+var (
+ fuchsiaDir = os.Getenv("FUCHSIA_DIR")
+ outDir = fuchsiaDir + "/out/default/"
+ fidlProjectPath = outDir + "fidl_project.json"
+ toolsPath = fuchsiaDir + "/out/default.zircon/tools/"
+ fidlcPath = toolsPath + "fidlc"
+ fidlLintPath = toolsPath + "fidl-lint"
+ fidlFormatPath = toolsPath + "fidl-format"
+)
+
+type stdrwc struct{}
+
+func (stdrwc) Read(p []byte) (int, error) {
+ return os.Stdin.Read(p)
+}
+
+func (stdrwc) Write(p []byte) (int, error) {
+ return os.Stdout.Write(p)
+}
+
+func (stdrwc) Close() error {
+ if err := os.Stdin.Close(); err != nil {
+ return err
+ }
+ return os.Stdout.Close()
+}
+
+func main() {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ trace := log.New(os.Stderr, "[LSP] ", log.Lshortfile)
+ logOpt := jsonrpc2.LogMessages(trace)
+
+ // fidl_project.json is generated by running
+ // `python3 scripts/gen_fidl_project.py path/to/fidl_project.json`
+ fidlProject, err := analysis.LoadFidlProject(fidlProjectPath)
+ if err != nil {
+ trace.Fatalf("Failed to parse fidl_project.json at `%s`: %s\n", fidlProjectPath, err)
+ }
+ analyzer := analysis.NewAnalyzer(
+ analysis.Config{
+ BuildRootDir: fuchsiaDir,
+ FidlcPath: fidlcPath,
+ FidlLintPath: fidlLintPath,
+ FidlFormatPath: fidlFormatPath,
+ },
+ fidlProject,
+ )
+
+ handler := langserver.NewLangHandler(langserver.NewDefaultConfig(), trace, analyzer)
+ server := langserver.LspServer{handler}
+
+ // Setup a new Conn over stdio
+ trace.Println("setting up a new connection")
+ conn := jsonrpc2.NewConn(
+ ctx,
+ jsonrpc2.NewBufferedStream(stdrwc{}, jsonrpc2.VSCodeObjectCodec{}),
+ server,
+ logOpt,
+ )
+ defer conn.Close()
+
+ <-conn.DisconnectNotify()
+}
diff --git a/fidl-lsp/scripts/copy_fidlgen_common.py b/fidl-lsp/scripts/copy_fidlgen_common.py
new file mode 100644
index 0000000..b59abfa
--- /dev/null
+++ b/fidl-lsp/scripts/copy_fidlgen_common.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+# 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.
+"""
+copy fidlgen's fidl/compiler/backend/common/identifiers.go from fuchsia.git
+
+This script copies the `identifiers.go` file from the fildgen `common` package
+in fuchsia.git. This is used to share common FIDL library identifier code with
+the fidlgens. It should be run on build to maintain freshness.
+
+TODO: it would be better if we could just directly import the `common` package.
+
+The script assumes that it is run from the fidl-lsp root
+directory.
+"""
+import os
+from pathlib import Path
+from shutil import copyfile
+
+FUCHSIA_DIR = os.getenv('FUCHSIA_DIR')
+FIDLGEN_COMMON_PATH = 'garnet/go/src/fidl/compiler/backend/common/identifiers.go'
+
+
+def copy_fidlgen_common():
+ project_root = path = Path.cwd()
+ assert project_root.name == 'fidl-lsp', 'Script should be run from fidl-lsp root directory'
+ Path(f'{project_root}/third_party/common').mkdir(exist_ok=True)
+ copyfile(f'{FUCHSIA_DIR}/{FIDLGEN_COMMON_PATH}', f'{project_root}/third_party/common/identifiers.go')
+
+
+if __name__ == '__main__':
+ copy_fidlgen_common()
diff --git a/fidl-lsp/scripts/gen_fidl_project.py b/fidl-lsp/scripts/gen_fidl_project.py
new file mode 100644
index 0000000..ebd2bd3
--- /dev/null
+++ b/fidl-lsp/scripts/gen_fidl_project.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# 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.
+"""
+generate fidl_project.json file declaring FIDL libraries
+
+This script reads the all_fidl_generated.json file which contains all fidlgen- &
+fidlc-generated output, and generates a fidl_project.json file which declares
+all FIDL libraries along with their constituent files, dependencies, and build
+artifacts (JSON IR and bindings). This is for use in the FIDL Language Server,
+which uses fidl_project to do dependency resolution.
+
+This script makes the assumption that all FIDL library names are unique, which
+is currently not the case. However, the exceptions are mainly test libraries, so
+for now this is left unaddressed.
+"""
+import glob
+import json
+import os
+import re
+import sys
+from pathlib import Path
+
+FUCHSIA_DIR = os.getenv('FUCHSIA_DIR')
+ALL_FIDL_GENERATED_PATH = 'out/default/all_fidl_generated.json'
+
+# schema:
+# map<string, Library>
+# where string is library name e.g. 'fuchsia.mem'
+# and Library is
+# {
+# "files": []string,
+# "json": string,
+# "deps": []string,
+# "bindings": {
+# "hlcpp": {},
+# "llcpp": {},
+# "rust": {},
+# "go": {},
+# "dart": {},
+# ...
+# }
+# }
+
+
+def find_files(artifact):
+ library_name = artifact['library']
+ pattern = f'^fidling\/gen\/([\w\.\/-]+)\/[\w\-. ]+\.fidl\.json$'
+ result = re.search(pattern, artifact['files'][0])
+ if not result or not result.group(1):
+ return []
+
+ fidl_dir = Path(f'{FUCHSIA_DIR}/{result.group(1)}')
+ globs = [
+ fidl_dir.glob('*.fidl'),
+ fidl_dir.parent.glob('*.fidl'),
+ ]
+
+ files = []
+ for glob in globs:
+ for file in glob:
+ # TODO: read in file
+ # parse `library` decl
+ # check that it matches library name
+ files.append(str(file))
+ return files
+
+
+def find_deps(artifact):
+ library_json = artifact['files'][0]
+ library_json_path = Path(f'{FUCHSIA_DIR}/out/default/{library_json}')
+ with open(library_json_path, 'r') as f:
+ library = json.load(f)
+ deps = library['library_dependencies']
+ deps = [dep['name'] for dep in deps]
+ return deps
+
+
+def gen_fidl_project(fidl_project_path):
+ result = {}
+
+ all_fidl_path = FUCHSIA_DIR + '/' + ALL_FIDL_GENERATED_PATH
+ with open(all_fidl_path, 'r') as f:
+ artifacts = json.load(f)
+
+ print('have read in all_fidl_generated')
+ print(len(artifacts))
+
+ for artifact in artifacts:
+ if artifact['type'] == 'json':
+ result[artifact['library']] = {
+ 'json': f"{FUCHSIA_DIR}/out/default/{artifact['files'][0]}",
+ 'files': find_files(artifact),
+ 'deps': find_deps(artifact),
+ 'bindings': {}, # TODO
+ }
+
+ print('writing to {}', fidl_project_path)
+ with open(fidl_project_path, 'w') as f:
+ json.dump(result, f, indent=4, sort_keys=True)
+
+
+if __name__ == '__main__':
+ gen_fidl_project(sys.argv[1])
diff --git a/fidl-lsp/state/fs.go b/fidl-lsp/state/fs.go
new file mode 100644
index 0000000..b6eb790
--- /dev/null
+++ b/fidl-lsp/state/fs.go
@@ -0,0 +1,154 @@
+// 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 state
+
+import (
+ "fmt"
+ "net/url"
+ "strings"
+)
+
+// A FileID is a path to a file open in the client editor.
+//
+// We never assume the "saved-ness" of a file, and maintain all files that are
+// open in the editor in memory. Therefore this path does not necessarily
+// correspond to an absolute filepath; it's just used as an ID to access the
+// in-memory file.
+type FileID string
+
+// FileSystem manages file synchronization between client and server.
+// `files` is an in-memory mapping of URI to file contents of the currently open
+// files on the client.
+type FileSystem struct {
+ // TODO: should this be a map[FileID][]byte instead?
+
+ // `files` is stored as a map of FileIDs to files that are split by line
+ // (but retaining their '\n' characters).
+ // This speeds up the common operation of searching for a symbol at a given
+ // (line, column) in the file as the file is indexed by line.
+ files map[FileID][]string
+
+ // TODO: RWLock for safe concurrency
+}
+
+// Range represents a span of text in a source file.
+type Range struct {
+ Start Position
+ End Position
+}
+
+// Position represents the location of one character in a source file.
+// Both are zero-indexed, i.e. lines start at 0 and the first character of a
+// line is at index 0.
+type Position struct {
+ Line int
+ Character int
+}
+
+// Location represents a range in a particular file.
+type Location struct {
+ FileID FileID
+ Range Range
+}
+
+// Change represents a single diff in a source file.
+// Range is the range that is being changed. RangeLength is the length of the
+// replacement text (0 if the range is being deleted). Text is the text to
+// insert in the source at that range.
+type Change struct {
+ Range Range
+ NewContent string
+}
+
+// NewFileSystem returns an initialized empty FileSystem.
+func NewFileSystem() *FileSystem {
+ return &FileSystem{
+ files: make(map[FileID][]string),
+ }
+}
+
+// File is a read-only accessor for the in-memory file with ID `path`.
+func (fs *FileSystem) File(path FileID) (string, error) {
+ if lines, ok := fs.files[path]; ok {
+ return strings.Join(lines, ""), nil
+ }
+ return "", fmt.Errorf("file `%s` not in memory", path)
+}
+
+// NFiles returns the number of in-memory files.
+func (fs *FileSystem) NFiles() int {
+ return len(fs.files)
+}
+
+// NewFile copies `text` into the FileSystem's in-memory file system ("opening"
+// the file), indexed by the FileID `path`.
+func (fs *FileSystem) NewFile(path FileID, text string) {
+ fs.files[path] = strings.SplitAfter(text, "\n")
+}
+
+// DeleteFile deletes the FileSystem's in-memory representation of the file
+// indexed by `path`.
+func (fs *FileSystem) DeleteFile(path FileID) {
+ delete(fs.files, path)
+}
+
+// ApplyChanges applies the specified changes to the relevant in-memory file.
+func (fs *FileSystem) ApplyChanges(path FileID, changes []Change) error {
+ for _, change := range changes {
+ start, err := OffsetInFile(fs.files[path], change.Range.Start)
+ if err != nil {
+ return err
+ }
+ end, err := OffsetInFile(fs.files[path], change.Range.End)
+ if err != nil {
+ return err
+ }
+
+ file := strings.Join(fs.files[path], "")
+
+ // Copy file over to new buffer with inserted text change
+ var newFile strings.Builder
+ newFile.WriteString(file[:start])
+ newFile.WriteString(change.NewContent)
+ newFile.WriteString(file[end:])
+ fs.files[path] = strings.SplitAfter(newFile.String(), "\n")
+ }
+ return nil
+}
+
+// OffsetInFile converts a Position (line, character) to a rune offset in
+// `contents`.
+func OffsetInFile(lines []string, pos Position) (uint, error) {
+ var offset uint
+ for line, l := range lines {
+ for col := range l {
+ if line == pos.Line && col == pos.Character {
+ return offset, nil
+ }
+ offset++
+ }
+ }
+ // Check if pos is pointing at the end of the file
+ lastLine := len(lines) - 1
+ if pos.Line == lastLine && pos.Character == len(lines[lastLine]) {
+ return offset, nil
+ }
+ return 0, fmt.Errorf(
+ "position %#v was out of bounds in file with %d lines, %d total chars",
+ pos,
+ len(lines),
+ offset,
+ )
+}
+
+// FileIDToPath converts a FileID, which is in the form of a file schema URI
+// ("file:///"), to an absolute path.
+func FileIDToPath(fileID FileID) (string, error) {
+ fileURI, err := url.Parse(string(fileID))
+ if err != nil {
+ return "", fmt.Errorf("could not parse FileID: %s", err)
+ }
+ return fileURI.Path, nil
+}
diff --git a/fidl-lsp/state/fs_test.go b/fidl-lsp/state/fs_test.go
new file mode 100644
index 0000000..39d3654
--- /dev/null
+++ b/fidl-lsp/state/fs_test.go
@@ -0,0 +1,293 @@
+// 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 state_test
+
+import (
+ "testing"
+
+ "fidl-lsp/state"
+)
+
+func TestFsApplyChangesInsertions(t *testing.T) {
+ fs := state.NewFileSystem()
+
+ fs.NewFile("test.fidl", `
+library example;
+
+struct Foo {
+ uint8 foo;
+};
+`)
+
+ err := fs.ApplyChanges("test.fidl",
+ []state.Change{
+ {
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 10},
+ End: state.Position{Line: 1, Character: 10},
+ },
+ NewContent: "tended.library.ex",
+ },
+ {
+ Range: state.Range{
+ Start: state.Position{Line: 2, Character: 0},
+ End: state.Position{Line: 2, Character: 0},
+ },
+ NewContent: "\nconst uint8 INSERTED_CONST = 0;\n",
+ },
+ {
+ Range: state.Range{
+ Start: state.Position{Line: 5, Character: 0},
+ End: state.Position{Line: 5, Character: 0},
+ },
+ NewContent: "/// Inserted doc comment\n",
+ },
+ {
+ Range: state.Range{
+ Start: state.Position{Line: 7, Character: 14},
+ End: state.Position{Line: 7, Character: 14},
+ },
+ NewContent: "\n string inserted_struct_member;",
+ },
+ },
+ )
+ if err != nil {
+ t.Errorf("error on applying changes: %s", err)
+ }
+
+ expFile := `
+library extended.library.example;
+
+const uint8 INSERTED_CONST = 0;
+
+/// Inserted doc comment
+struct Foo {
+ uint8 foo;
+ string inserted_struct_member;
+};
+`
+ file, err := fs.File("test.fidl")
+ if err != nil {
+ t.Error(err)
+ }
+ if file != expFile {
+ t.Errorf(
+ "file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+ expFile,
+ file,
+ )
+ }
+}
+
+func TestFsApplyChangesDeletions(t *testing.T) {
+ fs := state.NewFileSystem()
+
+ fs.NewFile("test.fidl", `
+library example;
+
+struct ToBeDeleted {
+ uint8 foo;
+};
+`)
+
+ err := fs.ApplyChanges("test.fidl",
+ []state.Change{
+ {
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 0},
+ End: state.Position{Line: 6, Character: 0},
+ },
+ NewContent: "",
+ },
+ },
+ )
+ if err != nil {
+ t.Errorf("error on applying changes: %s", err)
+ }
+
+ expFile := `
+library example;
+
+`
+ file, err := fs.File("test.fidl")
+ if err != nil {
+ t.Error(err)
+ }
+ if file != expFile {
+ t.Errorf(
+ "file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+ expFile,
+ file,
+ )
+ }
+}
+
+func TestFsApplyChangesToBeginningOfFile(t *testing.T) {
+ fs := state.NewFileSystem()
+
+ fs.NewFile(
+ "test.fidl",
+ `library example;`,
+ )
+
+ err := fs.ApplyChanges("test.fidl",
+ []state.Change{
+ {
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 0},
+ End: state.Position{Line: 0, Character: 0},
+ },
+ NewContent: "// Prepended comment\n",
+ },
+ },
+ )
+ if err != nil {
+ t.Errorf("error on applying changes: %s", err)
+ }
+
+ expFile := `// Prepended comment
+library example;`
+ file, err := fs.File("test.fidl")
+ if err != nil {
+ t.Error(err)
+ }
+ if file != expFile {
+ t.Errorf(
+ "file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+ expFile,
+ file,
+ )
+ }
+
+ err = fs.ApplyChanges("test.fidl",
+ []state.Change{
+ {
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 0},
+ End: state.Position{Line: 1, Character: 0},
+ },
+ NewContent: "",
+ },
+ },
+ )
+ if err != nil {
+ t.Errorf("error on applying changes: %s", err)
+ }
+
+ expFile = `library example;`
+ file, err = fs.File("test.fidl")
+ if err != nil {
+ t.Error(err)
+ }
+ if file != expFile {
+ t.Errorf(
+ "file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+ expFile,
+ file,
+ )
+ }
+}
+
+func TestFsApplyChangesToEndOfFile(t *testing.T) {
+ fs := state.NewFileSystem()
+
+ fs.NewFile("test.fidl", `
+library example;
+`)
+
+ err := fs.ApplyChanges("test.fidl",
+ []state.Change{
+ {
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 16},
+ End: state.Position{Line: 1, Character: 16},
+ },
+ NewContent: "\n// Appended comment",
+ },
+ },
+ )
+ if err != nil {
+ t.Errorf("error on applying changes: %s", err)
+ }
+
+ expFile := `
+library example;
+// Appended comment
+`
+ file, err := fs.File("test.fidl")
+ if err != nil {
+ t.Error(err)
+ }
+ if file != expFile {
+ t.Errorf(
+ "file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+ expFile,
+ file,
+ )
+ }
+
+ fs.ApplyChanges("test.fidl",
+ []state.Change{
+ {
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 16},
+ End: state.Position{Line: 2, Character: 19},
+ },
+ NewContent: "",
+ },
+ },
+ )
+
+ expFile = `
+library example;
+`
+ file, err = fs.File("test.fidl")
+ if err != nil {
+ t.Error(err)
+ }
+ if file != expFile {
+ t.Errorf(
+ "file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+ expFile,
+ file,
+ )
+ }
+}
+
+func TestFsApplyChangesOutsideFileRange(t *testing.T) {
+ fs := state.NewFileSystem()
+
+ fs.NewFile("test.fidl", `library example;`)
+
+ err := fs.ApplyChanges("test.fidl",
+ []state.Change{
+ {
+ Range: state.Range{
+ Start: state.Position{Line: -1, Character: -1},
+ End: state.Position{Line: 0, Character: 0},
+ },
+ NewContent: "inserted text",
+ },
+ },
+ )
+ if err == nil {
+ t.Errorf("expect error on applying change out of file bounds")
+ }
+
+ err = fs.ApplyChanges("test.fidl",
+ []state.Change{
+ {
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 0},
+ End: state.Position{Line: 1, Character: 0},
+ },
+ NewContent: "inserted text",
+ },
+ },
+ )
+ if err == nil {
+ t.Errorf("expect error on applying change out of file bounds")
+ }
+}
diff --git a/fidl-lsp/state/parse.go b/fidl-lsp/state/parse.go
new file mode 100644
index 0000000..f91f9b3
--- /dev/null
+++ b/fidl-lsp/state/parse.go
@@ -0,0 +1,169 @@
+// 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 state
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ fidlcommon "fidl-lsp/third_party/common"
+)
+
+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*;`)
+
+ // TODO: make this agnostic to platform
+
+ // 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*;`)
+)
+
+// LibraryMatch is the result of ParseLibraryMatch and ParsePlatformImportsMatch
+// and represents the location and value of a `library` or `using` declaration
+// in a FIDL file.
+type LibraryMatch struct {
+ Lib fidlcommon.LibraryName
+ Range Range
+}
+
+// TODO: we should keep track of URI <-> fidlLibrary mapping, so we don't have
+// to parse for `library` name each time
+
+// LibraryOfFile extracts the name of the FIDL library of the provided FIDL
+// file, by extracting its `library` declaration.
+func LibraryOfFile(file string) (fidlcommon.LibraryName, error) {
+ fidlLib, ok := ParseLibraryMatch(file)
+ if !ok {
+ return fidlcommon.LibraryName{}, fmt.Errorf("Could not find library declaration in file")
+ }
+ return fidlLib.Lib, nil
+}
+
+// ParseLibraryMatch extracts with regex the `library __;` declaration from the
+// FIDL file passed in, if there is one, or returns false if not.
+func ParseLibraryMatch(fidl string) (LibraryMatch, bool) {
+ m := libraryRegexp.FindStringSubmatchIndex(fidl)
+ if m == nil {
+ return LibraryMatch{}, false
+ }
+ return toLibraryMatch(fidl, m[2], m[3]), true
+}
+
+// ParsePlatformImportsMatch extracts with regex the `using __;` import
+// declarations in the FIDL file passed in.
+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: fidlcommon.MustReadLibraryName(fidl[start:end]),
+ Range: Range{
+ Start: Position{
+ Line: strings.Count(fidl[:start], "\n"),
+ Character: start - strings.LastIndexByte(fidl[:start], '\n') - 1,
+ },
+ End: Position{
+ Line: strings.Count(fidl[:end], "\n"),
+ Character: end - strings.LastIndexByte(fidl[:end], '\n') - 1,
+ },
+ },
+ }
+}
+
+// https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/language.md#identifiers
+//
+// This also includes a '.' as long as it is not initial or trailing, so we can
+// capture fully-qualified names and library names.
+var identRegexp = regexp.MustCompile(`\b[a-zA-Z](?:[a-zA-Z0-9_.]*[a-zA-Z0-9])?\b`)
+
+// Symbol represents a FIDL identifier.
+// It could be a fully-qualified name, a local declaration name, a library name,
+// etc.
+type Symbol struct {
+ Name string
+ Location Location
+}
+
+// SymbolAtPos returns the FIDL identifier that is at `pos` in `file`.
+func (fs *FileSystem) SymbolAtPos(path FileID, pos Position) (Symbol, error) {
+ lines, ok := fs.files[path]
+ if !ok {
+ return Symbol{}, fmt.Errorf("could not find file `%s`", path)
+ }
+
+ if pos.Line >= len(lines) {
+ return Symbol{}, fmt.Errorf("position %#v in file `%s` is out of bounds", pos, path)
+ }
+ line := lines[pos.Line]
+ tokens := identRegexp.FindAllStringIndex(line, -1)
+ if len(tokens) == 0 {
+ return Symbol{}, fmt.Errorf("could not find any symbols in line %d in file `%s`", pos.Line, path)
+ }
+
+ var name string
+ r := Range{
+ Start: Position{Line: pos.Line},
+ End: Position{Line: pos.Line},
+ }
+ for _, token := range tokens {
+ if pos.Character >= token[0] &&
+ pos.Character <= token[1] {
+ name = line[token[0]:token[1]]
+ r.Start.Character = token[0]
+ r.End.Character = token[1]
+ break
+ }
+ }
+ if name == "" {
+ return Symbol{}, fmt.Errorf("could not find symbol at pos %#v in file `%s`", pos, path)
+ }
+
+ // TODO: could exclude all FIDL keywords, to save work
+
+ // TODO: check that the symbol is not part of a comment: if the line
+ // contains "//" and the symbol is after the "//", then don't return the
+ // symbol. "//" could also be part of a string const, but that also wouldn't
+ // be an identifer.
+
+ return Symbol{
+ Name: name,
+ Location: Location{
+ FileID: path,
+ Range: r,
+ },
+ }, nil
+}
diff --git a/fidl-lsp/state/parse_test.go b/fidl-lsp/state/parse_test.go
new file mode 100644
index 0000000..6e32a0d
--- /dev/null
+++ b/fidl-lsp/state/parse_test.go
@@ -0,0 +1,287 @@
+// 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 state_test
+
+import (
+ "testing"
+
+ fidlcommon "fidl-lsp/third_party/common"
+
+ "fidl-lsp/state"
+)
+
+func TestLibraryOfFile(t *testing.T) {
+ library, err := state.LibraryOfFile(`library example;`)
+ if err != nil {
+ t.Errorf("error getting library of file: %s", err)
+ }
+ expLibrary := "example"
+ if library.FullyQualifiedName() != expLibrary {
+ t.Errorf("expected library `%s`, got `%s`", expLibrary, library)
+ }
+
+ library, err = state.LibraryOfFile(`
+/// Doc comments
+library fuchsia.foo.bar;
+struct S {};
+`)
+ if err != nil {
+ t.Errorf("error getting library of file: %s", err)
+ }
+ expLibrary = "fuchsia.foo.bar"
+ if library.FullyQualifiedName() != expLibrary {
+ t.Errorf("expected library `%s`, got `%s`", expLibrary, library)
+ }
+
+ library, err = state.LibraryOfFile(`library .invalid.name;`)
+ if err == nil {
+ t.Error("expect error getting library of file")
+ }
+
+ library, err = state.LibraryOfFile(`library exampl // Missing semicolon`)
+ if err == nil {
+ t.Error("expect error getting library of file")
+ }
+}
+
+func TestParseLibraryMatch(t *testing.T) {
+ library, ok := state.ParseLibraryMatch(`
+library example;
+// ~~~~~~~ expected range
+`)
+ if !ok {
+ t.Errorf("error getting library of file")
+ }
+ expLibrary := state.LibraryMatch{
+ Lib: fidlcommon.MustReadLibraryName("example"),
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 8},
+ End: state.Position{Line: 1, Character: 15},
+ },
+ }
+ if library != expLibrary {
+ t.Errorf("expected library `%v`, got `%v`", expLibrary, library)
+ }
+
+ library, ok = state.ParseLibraryMatch(`
+/// Doc comments
+library fuchsia.foo.bar;
+// ~~~~~~~~~~~~~~~ expected range
+struct S {};
+`)
+ if !ok {
+ t.Errorf("error getting library of file")
+ }
+ expLibrary = state.LibraryMatch{
+ Lib: fidlcommon.MustReadLibraryName("fuchsia.foo.bar"),
+ Range: state.Range{
+ Start: state.Position{Line: 2, Character: 8},
+ End: state.Position{Line: 2, Character: 23},
+ },
+ }
+ if library != expLibrary {
+ t.Errorf("expected library `%v`, got `%v`", expLibrary, library)
+ }
+
+ library, ok = state.ParseLibraryMatch(`library .invalid.name;`)
+ if ok {
+ t.Error("expect error getting library of file")
+ }
+
+ library, ok = state.ParseLibraryMatch(`library exampl // Missing semicolon`)
+ if ok {
+ t.Error("expect error getting library of file")
+ }
+}
+
+func TestParsePlatformImportsMatch(t *testing.T) {
+ imports := state.ParsePlatformImportsMatch(`
+library example;
+using fuchsia.foo.bar;
+using fuchsia.baz.qux;
+using non.platform.import;
+using TypeAlias = uint8;
+`)
+
+ expImports := []state.LibraryMatch{
+ {
+ Lib: fidlcommon.MustReadLibraryName("fuchsia.foo.bar"),
+ Range: state.Range{
+ Start: state.Position{Line: 2, Character: 6},
+ End: state.Position{Line: 2, Character: 21},
+ },
+ },
+ {
+ Lib: fidlcommon.MustReadLibraryName("fuchsia.baz.qux"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 6},
+ End: state.Position{Line: 3, Character: 21},
+ },
+ },
+ }
+ if len(imports) != len(expImports) {
+ t.Errorf("expected %d imports, found %d", len(expImports), len(imports))
+ }
+ for i, expImport := range expImports {
+ if imports[i] != expImport {
+ t.Errorf("unexpected import `%v`, expected `%v`", imports[i], expImport)
+ }
+ }
+}
+
+func TestSymbolAtPos(t *testing.T) {
+ fs := state.NewFileSystem()
+ fs.NewFile("test.fidl", `
+library example;
+using test.import;
+
+struct Foo {
+ uint8 bar_baz;
+};
+`)
+
+ cases := []struct {
+ pos state.Position
+ sym state.Symbol
+ }{
+ {
+ pos: state.Position{Line: 1, Character: 0},
+ sym: state.Symbol{
+ Name: "library",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 0},
+ End: state.Position{Line: 1, Character: 7},
+ },
+ },
+ },
+ },
+ {
+ pos: state.Position{Line: 1, Character: 15},
+ sym: state.Symbol{
+ Name: "example",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 8},
+ End: state.Position{Line: 1, Character: 15},
+ },
+ },
+ },
+ },
+ {
+ pos: state.Position{Line: 2, Character: 11},
+ sym: state.Symbol{
+ Name: "test.import",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 2, Character: 6},
+ End: state.Position{Line: 2, Character: 17},
+ },
+ },
+ },
+ },
+ {
+ pos: state.Position{Line: 4, Character: 10},
+ sym: state.Symbol{
+ Name: "Foo",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 4, Character: 7},
+ End: state.Position{Line: 4, Character: 10},
+ },
+ },
+ },
+ },
+ {
+ pos: state.Position{Line: 5, Character: 13},
+ sym: state.Symbol{
+ Name: "bar_baz",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 5, Character: 10},
+ End: state.Position{Line: 5, Character: 17},
+ },
+ },
+ },
+ },
+ }
+
+ for _, ex := range cases {
+ sym, err := fs.SymbolAtPos(
+ state.FileID("test.fidl"),
+ ex.pos,
+ )
+ if err != nil {
+ t.Fatalf("error finding symbol: %s", err)
+ }
+ if sym.Name != ex.sym.Name {
+ t.Fatalf("did not find correct symbol; expected %s, found %s", ex.sym.Name, sym.Name)
+ }
+ if sym.Location.FileID != ex.sym.Location.FileID {
+ t.Fatalf(
+ "incorrect file; expected %s, found %s",
+ ex.sym.Location.FileID,
+ sym.Location.FileID,
+ )
+ }
+ if sym.Location.Range != ex.sym.Location.Range {
+ t.Fatalf(
+ "incorrect range; expected %#v, found %#v",
+ ex.sym.Location.Range,
+ sym.Location.Range,
+ )
+ }
+ }
+}
+
+func TestNoSymbolAtPos(t *testing.T) {
+ fs := state.NewFileSystem()
+ fs.NewFile("test.fidl", `
+// ^ should not be a symbol
+library example;
+// ^ should not be a symbol
+
+struct Foo {
+// ^ should not be a symbol
+ uint8 foo;
+//^ should not be a symbol
+};
+
+.prepended.dot
+^ should not be a symbol ('prepended.dot' is a valid symbol)
+trailing.dot.
+ ^ should not be a symbol ('trailing.dot' is a valid symbol)
+0starts_with_a_number
+ ^ should not be a symbol
+ends_with_a_semicolon_
+ ^ should not be a symbol
+`)
+
+ cases := []state.Position{
+ {Line: 0, Character: 0},
+ {Line: 2, Character: 16},
+ {Line: 5, Character: 11},
+ {Line: 7, Character: 2},
+ {Line: 11, Character: 0},
+ {Line: 13, Character: 13},
+ {Line: 15, Character: 3},
+ {Line: 17, Character: 3},
+ }
+
+ for _, ex := range cases {
+ sym, err := fs.SymbolAtPos(
+ state.FileID("test.fidl"),
+ ex,
+ )
+ if err == nil {
+ t.Errorf("expected not to find symbol at position %v, but found %s", ex, sym.Name)
+ }
+ }
+}
diff --git a/fidl-lsp/third_party/common/copy_test.go b/fidl-lsp/third_party/common/copy_test.go
new file mode 100644
index 0000000..199e066
--- /dev/null
+++ b/fidl-lsp/third_party/common/copy_test.go
@@ -0,0 +1,32 @@
+// 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 common
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "testing"
+)
+
+func TestCodegenImplDotGo(t *testing.T) {
+ path, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("error getting cwd: %s", err)
+ }
+ identifiersDotGo, err := ioutil.ReadFile(fmt.Sprintf("%s/identifiers.go", path))
+ if err != nil {
+ t.Fatalf("error reading identifiers.go in fidl-lsp: %s", err)
+ }
+ fidlgenCommonDir := fmt.Sprintf("%s/garnet/go/src/fidl/compiler/backend/common", os.Getenv("FUCHSIA_DIR"))
+ actualIdentifiersDotGo, err := ioutil.ReadFile(fmt.Sprintf("%s/identifiers.go", fidlgenCommonDir))
+ if err != nil {
+ t.Fatalf("error reading identifiers.go in fuchsia.git: %s", err)
+ }
+ if bytes.Compare(identifiersDotGo, actualIdentifiersDotGo) != 0 {
+ t.Fatalf("common/identifers.go is out of date from fuchsia.git, please run copy_fidlgen_common.py")
+ }
+}
diff --git a/fidl-lsp/third_party/common/identifiers.go b/fidl-lsp/third_party/common/identifiers.go
new file mode 100644
index 0000000..85d3b9d
--- /dev/null
+++ b/fidl-lsp/third_party/common/identifiers.go
@@ -0,0 +1,109 @@
+// 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 common
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+)
+
+// LibraryName represents a FIDL library name, such as `fuchsia.mem` or
+// `fuchsia.ui.scenic`.
+type LibraryName struct {
+ fqn string
+}
+
+// Parts returns the library name in parts, e.g. `fuchsia`, `mem` or
+// `fuchsia`, `ui`, `scenic`.
+func (name LibraryName) Parts() []string {
+ return strings.Split(name.fqn, ".")
+}
+
+// FullyQualifiedName returns the fully qualified name, e.g. `fuchsia.mem` or
+// `fuchsia.ui.scenic`.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func (name LibraryName) FullyQualifiedName() string {
+ return name.fqn
+}
+
+// Name represents a FIDL declaration name, consisting of a FIDL library, and
+// a FIDL declration such as `fuchsia.mem` and `Buffer`.
+type Name struct {
+ libraryName LibraryName
+ declName string
+}
+
+// LibraryName returns the library name, e.g. `fuchsia.mem`.
+func (name Name) LibraryName() LibraryName {
+ return name.libraryName
+}
+
+// DeclarationName returns the declaration name, e.g. `Buffer`.
+func (name Name) DeclarationName() string {
+ return name.declName
+}
+
+// FullyQualifiedName returns the fully qualified name, e.g. `fuchsia.mem/Buffer`.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func (name Name) FullyQualifiedName() string {
+ return fmt.Sprintf("%s/%s", name.libraryName.fqn, name.declName)
+}
+
+var checkLibraryName = regexp.MustCompile("^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)*$")
+
+// ReadLibraryName reads a library name from a fully qualified name.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func ReadLibraryName(fullyQualifiedName string) (LibraryName, error) {
+ if !checkLibraryName.MatchString(fullyQualifiedName) {
+ return LibraryName{}, fmt.Errorf("invalid library name: %s", fullyQualifiedName)
+ }
+ return LibraryName{fullyQualifiedName}, nil
+}
+
+// MustReadLibraryName reads a library name from a fully qualified name, and
+// panics in case of error.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func MustReadLibraryName(fullyQualifiedName string) LibraryName {
+ name, err := ReadLibraryName(fullyQualifiedName)
+ if err != nil {
+ panic(err.Error())
+ }
+ return name
+}
+
+// ReadName reads a name from a fully qualified name.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func ReadName(fullyQualifiedName string) (Name, error) {
+ parts := strings.Split(fullyQualifiedName, "/")
+ if len(parts) != 2 {
+ return Name{}, fmt.Errorf("expected a fully qualified name library.name/DeclarationName, found %s", fullyQualifiedName)
+ }
+ libraryName, err := ReadLibraryName(parts[0])
+ if err != nil {
+ return Name{}, err
+ }
+ return Name{
+ libraryName: libraryName,
+ declName: parts[1],
+ }, nil
+}
+
+// MustReadName reads a name from a fully qualified name, and panics in case of
+// error.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func MustReadName(fullyQualifiedName string) Name {
+ name, err := ReadName(fullyQualifiedName)
+ if err != nil {
+ panic(err.Error())
+ }
+ return name
+}
diff --git a/fidldev/OWNERS b/fidldev/OWNERS
new file mode 100644
index 0000000..aefdd75
--- /dev/null
+++ b/fidldev/OWNERS
@@ -0,0 +1,11 @@
+apang@google.com
+bprosnitz@google.com
+fcz@google.com
+godtamit@google.com
+ianloic@google.com
+mkember@google.com
+pascallouis@google.com
+peterjohnston@google.com
+yifeit@google.com
+
+# COMPONENT: FIDL
diff --git a/fidldev/README.md b/fidldev/README.md
new file mode 100644
index 0000000..03e6bcf
--- /dev/null
+++ b/fidldev/README.md
@@ -0,0 +1,25 @@
+# fidldev
+
+`fidldev` is a FIDL development workflow tool. Its goal is to automate
+repetitive processes while working on FIDL code, like running tests based on
+changed files, and regenerating golden files. It is also meant to be the
+source of truth for FIDL code locations and tests/regen commands.
+
+## Running fidldev:
+
+ $FIDLMISC_DIR/fidldev/fidldev.py --help
+
+This can be aliased for convenienced:
+
+ alias fidldev=$FIDLMISC_DIR/fidldev/fidldev.py
+
+Note that `fidldev` should be run from a valid repo (e.g. fuchsia.git, topaz,
+or third_party/go) in order to correctly detect changes, similar to
+`fx format-code`.
+
+## Testing fidldev:
+
+ python3 $FIDLMISC_DIR/fidldev/fidldev_test.py -b
+
+The `-b` flag will separate stdout output when printing test results. It can
+be removed when using debugging print statements in the test itself.
diff --git a/fidldev/env.py b/fidldev/env.py
new file mode 100644
index 0000000..f34ecfc
--- /dev/null
+++ b/fidldev/env.py
@@ -0,0 +1,24 @@
+"""
+Constants that come from the environment. These are in a separate module so
+that they can be mocked by tests if necessary.
+"""
+import os
+from pathlib import Path
+import sys
+
+FUCHSIA_DIR = Path(os.environ["FUCHSIA_DIR"])
+assert FUCHSIA_DIR.exists()
+
+with open(FUCHSIA_DIR / ".fx-build-dir") as f:
+ BUILD_DIR = f.read().strip()
+
+if sys.platform.startswith('linux'):
+ PLATFORM = 'linux'
+elif sys.platform == 'darwin':
+ PLATFORM = 'mac'
+else:
+ print("Unsupported platform: " + sys.platform)
+ sys.exit(1)
+
+with open(FUCHSIA_DIR / (BUILD_DIR + '.zircon') / 'args.gn') as f:
+ MODE = 'asan' if 'asan' in f.read() else 'clang'
diff --git a/fidldev/fidldev.py b/fidldev/fidldev.py
new file mode 100755
index 0000000..3bd6c06
--- /dev/null
+++ b/fidldev/fidldev.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+
+import argparse
+import collections
+import enum
+import subprocess
+import sys
+
+import regen
+import test_
+import util
+
+EXAMPLES = """
+Examples:
+
+Regen goldens and checked in bindings based on changed files in the current
+repo
+
+ fidldev regen
+
+Explicitly specify regen scripts:
+
+ fidldev regen fidlc fidlgen_dart
+ fidldev regen all
+
+Check which regen commands should be run:
+
+ fidldev regen --dry-run --no-build
+
+Run tests based on changed files in the current repo:
+
+ fidldev test
+
+Explicitly specify tests:
+
+ fidldev test fidlc hlcpp llcpp c
+
+Interactively filter test targets:
+
+ fidldev test --interactive
+
+Check which tests should be run:
+
+ fidldev test --dry-run --no-build --no-regen
+
+Pass flags to invocations of fx test:
+
+ fidldev test --fx-test-args "-v -o --dry"
+
+"""
+
+
+def test(args):
+ success = True
+ if args.targets:
+ if not args.no_regen:
+ util.print_warning(
+ 'explicit test targets provided, skipping regen...')
+ success = test_.test_explicit(
+ args.targets, not args.no_build, args.dry_run, args.interactive,
+ args.fx_test_args)
+ else:
+ changed_files = util.get_changed_files()
+ if not args.no_regen:
+ regen.regen_changed(changed_files, not args.no_build, args.dry_run)
+ changed_files = util.get_changed_files()
+ success = test_.test_changed(
+ changed_files, not args.no_build, args.dry_run, args.interactive,
+ args.fx_test_args)
+ if args.dry_run:
+ print_dryrun_warning()
+ if not success:
+ sys.exit(1)
+
+
+def regen_cmd(args):
+ if args.targets:
+ regen.regen_explicit(args.targets, not args.no_build, args.dry_run)
+ else:
+ changed_files = util.get_changed_files()
+ regen.regen_changed(changed_files, not args.no_build, args.dry_run)
+ if args.dry_run:
+ print_dryrun_warning()
+
+
+def print_dryrun_warning():
+ print(
+ 'NOTE: dry run is conservative and assumes that regen will '
+ 'always change files. If goldens do not change during an actual '
+ 'run, fewer tests/regen scripts may be run.')
+
+
+parser = argparse.ArgumentParser(
+ description="FIDL development workflow tool",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=EXAMPLES)
+subparsers = parser.add_subparsers()
+
+test_parser = subparsers.add_parser("test", help="Test your FIDL changes")
+test_parser.set_defaults(func=test)
+test_targets = [name for (name, _) in test_.TEST_GROUPS] + ['all']
+test_parser.add_argument(
+ 'targets',
+ metavar='target',
+ nargs='*',
+ help=
+ "Manually specify targets to regen, where a target is one of {}. Omit positional arguments to test based on changed files"
+ .format(test_targets))
+test_parser.add_argument(
+ "--dry-run",
+ "-n",
+ help="Print out test commands without running",
+ action="store_true",
+)
+test_parser.add_argument(
+ "--no-build",
+ "-b",
+ help="Don't rebuild targets used for testing",
+ action="store_true",
+)
+test_parser.add_argument(
+ "--no-regen",
+ "-r",
+ help="Don't regen goldens before running tests",
+ action="store_true",
+)
+test_parser.add_argument(
+ "--interactive",
+ "-i",
+ help="Interactively filter tests to be run",
+ action="store_true",
+)
+test_parser.add_argument(
+ "--fx-test-args",
+ "-t",
+ help=
+ "Extra flags and arguments to pass to any invocations of fx test. The flag value is passed verbatim. By default, only '-v' is used.",
+ default='-v',
+)
+
+regen_parser = subparsers.add_parser("regen", help="Run regen commands")
+regen_parser.set_defaults(func=regen_cmd)
+regen_targets = [name for (name, _) in regen.REGEN_TARGETS] + ['all']
+regen_parser.add_argument(
+ 'targets',
+ metavar='target',
+ nargs='*',
+ help=
+ "Manually specify targets to regen, where a target is one of {}. Omit positional arguments to regen based on changed files"
+ .format(regen_targets))
+regen_parser.add_argument(
+ "--dry-run",
+ "-n",
+ help="Print out commands without running them",
+ action="store_true",
+)
+regen_parser.add_argument(
+ "--no-build",
+ "-b",
+ help="Don't rebuild targets used for regen",
+ action="store_true",
+)
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+ args.func(args)
diff --git a/fidldev/fidldev_test.py b/fidldev/fidldev_test.py
new file mode 100644
index 0000000..0dd557a
--- /dev/null
+++ b/fidldev/fidldev_test.py
@@ -0,0 +1,414 @@
+import itertools
+import os
+import unittest
+
+from test_util import get_commands, MOCK_FUCHSIA_DIR, MOCK_BUILD_DIR
+import regen
+import util
+
+
+# Many mocks contain 3 return values, because the regen script can call
+# get_changed_files up to 3 times:
+# 1. get the initial set of changed files
+# 2. get changed files after fidlc regen, to check if fidlgen needs regen
+# 3. get changed files after fidlgen regen, to check if go needs regen
+class TestFidlDevRegen(unittest.TestCase):
+
+ def test_basic_regen(self):
+ mocks = {
+ 'get_changed_files':
+ itertools.repeat(['zircon/tools/fidl/lib/flat_ast.cc']),
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_ir_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ ['zircon/tools/fidl/lib/parser.cc'],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden'
+ ],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden'
+ ],
+ ]
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN), util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ util.BUILD_FIDLGEN_DART,
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN)
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_tables_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ ['zircon/tools/fidl/lib/parser.cc'],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.tables.c.golden'
+ ],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.tables.c.golden'
+ ],
+ ]
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_go_goldens_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ ['zircon/tools/fidl/lib/parser.cc'],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden'
+ ],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden',
+ 'garnet/go/src/fidl/compiler/backend/goldens/union.test.json.go.golden'
+ ],
+ ]
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ util.BUILD_FIDLGEN_DART,
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+ regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_rust_goldens_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ ['zircon/tools/fidl/lib/parser.cc'],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden'
+ ],
+ [
+ 'zircon/tools/fidl/lib/parser.cc',
+ 'zircon/tools/fidl/goldens/bits.test.json.golden',
+ 'garnet/go/src/fidl/compiler/backend/goldens/union.test.json.rs.golden'
+ ],
+ ]
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ util.BUILD_FIDLGEN_DART,
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_fidlgen_go_changed(self):
+ mocks = {
+ 'get_changed_files':
+ itertools.repeat(['tools/fidl/fidlgen_go/ir/ir.go'])
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_fidlgen_changed(self):
+ mocks = {
+ 'get_changed_files':
+ itertools.repeat(
+ ['tools/fidl/fidlgen_syzkaller/templates/struct.tmpl.go'])
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_fidlgen_dart_changed(self):
+ mocks = {
+ 'get_changed_files':
+ itertools.repeat(['topaz/bin/fidlgen_dart/fidlgen_dart.go'])
+ }
+ command = ['regen']
+ expected = [
+ util.BUILD_FIDLGEN_DART,
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+ ]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_regen_all(self):
+ command = ['regen', 'all']
+ expected = [
+ util.BUILD_FIDLC,
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ util.BUILD_FIDLGEN_DART,
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+ regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+ ]
+ self.assertListEqual(get_commands({}, command), expected)
+
+ def test_regen_no_build(self):
+ command = ['regen', 'all', '--no-build']
+ expected = [
+ regen.path_to_regen_command(regen.FIDLC_REGEN),
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+ regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+ ]
+ self.assertListEqual(get_commands({}, command), expected)
+
+ def test_regen_fidlgen(self):
+ command = ['regen', 'fidlgen']
+ expected = [
+ util.BUILD_FIDLGEN,
+ regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+ ]
+ self.assertListEqual(get_commands({}, command), expected)
+
+
+class TestFidlDevTest(unittest.TestCase):
+
+ def test_no_changes(self):
+ mocks = {'get_changed_files': [[]]}
+ command = ['test', '--no-regen']
+ self.assertListEqual(get_commands(mocks, command), [])
+
+ def test_fidlc_changed(self):
+ mocks = {'get_changed_files': [['zircon/tools/fidl/lib/parser.cc']]}
+ command = ['test', '--no-regen']
+ expected = [util.BUILD_FIDLC_TESTS, util.TEST_FIDLC]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_ir_changed_zircon(self):
+ mocks = {
+ 'get_changed_files':
+ [['zircon/tools/fidl/goldens/bits.test.json.golden']]
+ }
+ command = ['test', '--no-regen']
+ expected = [util.BUILD_FIDLC_TESTS, util.TEST_FIDLC]
+ self.assertListEqual(get_commands(mocks, command), expected)
+
+ def test_ir_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ [
+ 'garnet/go/src/fidl/compiler/backend/goldens/struct.test.json',
+ ]
+ ]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = set(util.FIDLGEN_TEST_TARGETS)
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_fidlgen_util_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [[
+ 'garnet/go/src/fidl/compiler/backend/types/types.go',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = set(util.FIDLGEN_TEST_TARGETS)
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_fidlgen_backend_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [[
+ 'tools/fidl/fidlgen_rust/templates/enum.tmpl.go',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = set(util.FIDLGEN_TEST_TARGETS)
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_fidlgen_golden_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ [
+ 'garnet/go/src/fidl/compiler/backend/goldens/union.test.json.cc.golden',
+ ]
+ ]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = set(util.FIDLGEN_TEST_TARGETS) | {util.HLCPP_TEST_TARGET}
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_fidlgen_dart_changed(self):
+ mocks = {
+ 'get_changed_files': [[
+ 'topaz/bin/fidlgen_dart/backend/ir/ir.go',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {util.FIDLGEN_DART_TEST_TARGET}
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN_DART)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_fidlgen_dart_golden_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ [
+ 'topaz/bin/fidlgen_dart/goldens/handles.test.json_async.dart.golden',
+ ]
+ ]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {
+ util.DART_TEST_TARGET,
+ util.FIDLGEN_DART_TEST_TARGET,
+ }
+ self.assertEqual(len(actual), 2)
+ self.assertEqual(actual[0], util.BUILD_FIDLGEN_DART)
+ self.assertTestsRun(actual[1], expected)
+
+ def test_c_runtime_changed(self):
+ mocks = {
+ 'get_changed_files': [[
+ 'zircon/system/ulib/fidl/txn_header.c',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {
+ util.HLCPP_TEST_TARGET,
+ util.LLCPP_TEST_TARGET,
+ util.C_TEST_TARGET,
+ }
+ self.assertEqual(len(actual), 1)
+ self.assertTestsRun(actual[0], expected)
+
+ def test_coding_tables_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [[
+ 'zircon/tools/fidl/goldens/union.test.tables.c.golden',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {
+ util.HLCPP_TEST_TARGET,
+ util.LLCPP_TEST_TARGET,
+ util.C_TEST_TARGET,
+ }
+ self.assertEqual(actual[1], util.TEST_FIDLC)
+ self.assertTestsRun(actual[2], expected)
+
+ def test_go_runtime_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [[
+ 'third_party/go/src/syscall/zx/fidl/encoding_new.go',
+ ]]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {
+ util.GO_CONFORMANCE_TEST_TARGET,
+ util.GO_TEST_TARGET,
+ }
+ self.assertEqual(len(actual), 1)
+ self.assertTestsRun(actual[0], expected)
+
+ def test_dart_runtime_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ [
+ 'topaz/public/dart/fidl/lib/src/types.dart',
+ 'topaz/public/dart/fidl/lib/src/message.dart',
+ ]
+ ]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {util.DART_TEST_TARGET}
+ self.assertEqual(len(actual), 1)
+ self.assertTestsRun(actual[0], expected)
+
+ def test_gidl_changed(self):
+ mocks = {
+ 'get_changed_files':
+ [
+ [
+ 'tools/fidl/gidl/main.go ',
+ 'tools/fidl/gidl/rust/benchmarks.go',
+ 'tools/fidl/gidl/rust/conformance.go',
+ ]
+ ]
+ }
+ command = ['test', '--no-regen']
+ actual = get_commands(mocks, command)
+ expected = {
+ util.GIDL_TEST_TARGET,
+ util.GO_CONFORMANCE_TEST_TARGET,
+ util.HLCPP_CONFORMANCE_TEST_TARGET,
+ util.HLCPP_HOST_CONFORMANCE_TEST_TARGET,
+ util.LLCPP_CONFORMANCE_TEST_TARGET,
+ util.RUST_CONFORMANCE_TEST_TARGET,
+ util.DART_TEST_TARGET,
+ }
+ self.assertEqual(len(actual), 1)
+ self.assertTestsRun(actual[0], expected)
+
+ def assertTestsRun(self, raw_command, expected):
+ self.assertEqual(raw_command[0], 'fx')
+ self.assertEqual(raw_command[1], 'test')
+ self.assertEqual(raw_command[2], '-v')
+ tests = set(raw_command[3:])
+ self.assertSetEqual(tests, expected)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/fidldev/regen.py b/fidldev/regen.py
new file mode 100644
index 0000000..55dc63d
--- /dev/null
+++ b/fidldev/regen.py
@@ -0,0 +1,106 @@
+import os
+
+import util
+
+FIDLC_REGEN = 'zircon/tools/fidl/testdata/regen.sh'
+FIDLGEN_REGEN = 'garnet/go/src/fidl/compiler/backend/typestest/regen.sh'
+FIDLGEN_DART_REGEN = 'topaz/bin/fidlgen_dart/regen.sh'
+GO_BINDINGS_REGEN = 'third_party/go/regen-fidl'
+
+
+def regen_fidlc_goldens(build_first, dry_run):
+ if build_first:
+ util.run(util.BUILD_FIDLC, dry_run, exit_on_failure=True)
+ util.run(path_to_regen_command(FIDLC_REGEN), dry_run, exit_on_failure=True)
+
+
+def regen_fidlgen_goldens(build_first, dry_run):
+ if build_first:
+ util.run(util.BUILD_FIDLGEN, dry_run, exit_on_failure=True)
+ util.run(
+ path_to_regen_command(FIDLGEN_REGEN), dry_run, exit_on_failure=True)
+
+
+def regen_fidlgendart_goldens(build_first, dry_run):
+ if build_first:
+ util.run(util.BUILD_FIDLGEN_DART, dry_run, exit_on_failure=True)
+ util.run(
+ path_to_regen_command(FIDLGEN_DART_REGEN),
+ dry_run,
+ exit_on_failure=True)
+
+
+def regen_go_bindings(build_first, dry_run, exit_on_failure=True):
+ util.run(
+ path_to_regen_command(GO_BINDINGS_REGEN), dry_run, exit_on_failure=True)
+
+
+def path_to_regen_command(path):
+ return ['fx', 'exec', os.path.join(util.FUCHSIA_DIR, path)]
+
+
+def is_ir_changed():
+ for path in util.get_changed_files():
+ if path.startswith(
+ util.FIDLC_DIR) and path.endswith('.test.json.golden'):
+ return True
+ return False
+
+
+def is_go_bindings_changed():
+ for path in util.get_changed_files():
+ if path.startswith(
+ util.FIDLGEN_DIR) and path.endswith('.test.json.go.golden'):
+ return True
+ return False
+
+
+REGEN_TARGETS = [
+ ('fidlc', regen_fidlc_goldens),
+ ('fidlgen', regen_fidlgen_goldens),
+ ('fidlgen_dart', regen_fidlgendart_goldens),
+ ('go', regen_go_bindings),
+]
+
+
+def regen_explicit(targets, build_first, dry_run):
+ for target, func in REGEN_TARGETS:
+ if target in targets or 'all' in targets:
+ func(build_first, dry_run)
+
+
+def regen_changed(changed_files, build_first, dry_run):
+ regen_fidlc = False
+ regen_fidlgen = False
+ regen_fidlgendart = False
+ regen_go = False
+ for file_ in changed_files:
+ if file_.startswith(util.FIDLC_DIR):
+ regen_fidlc = True
+ if file_.startswith(util.FIDLGEN_DIR) or is_in_fidlgen_backend(file_):
+ regen_fidlgen = True
+ if file_.startswith(util.FIDLGEN_DART_DIR):
+ regen_fidlgendart = True
+ if file_.startswith(util.FIDLGEN_GO_DIR):
+ regen_go = True
+
+ if regen_fidlc:
+ regen_fidlc_goldens(build_first, dry_run)
+ if dry_run or is_ir_changed():
+ regen_fidlgen = True
+ regen_fidlgendart = True
+
+ if regen_fidlgen:
+ regen_fidlgen_goldens(build_first, dry_run)
+ if dry_run or is_go_bindings_changed():
+ regen_go = True
+
+ if regen_fidlgendart:
+ regen_fidlgendart_goldens(build_first, dry_run)
+
+ if regen_go:
+ regen_go_bindings(build_first, dry_run)
+
+
+def is_in_fidlgen_backend(path):
+ return any(path.startswith(b) for b in util.FIDLGEN_BACKEND_DIRS)
diff --git a/fidldev/test_.py b/fidldev/test_.py
new file mode 100644
index 0000000..ba438b7
--- /dev/null
+++ b/fidldev/test_.py
@@ -0,0 +1,201 @@
+import functools
+import pprint
+import util
+import regen
+
+
+def startswith(prefix):
+ return lambda s: s.startswith(prefix)
+
+
+def endswith(suffix):
+ return lambda s: s.endswith(suffix)
+
+
+# List of test groups. Each test group is of the following structure:
+# (NAME, (PREDICATES, TARGETS, BUILD COMMAND))
+# where:
+# - NAME is a name for the group of tests. This name is used to explicitly
+# invoke this test group on the command line (e.g. fidldev test foo would call
+# fx test on the TARGETS for group foo)
+# - PREDICATES is a list of predicates P such that P(file) returns true if the
+# test group should run when |file| is changed
+# - TARGETS is a list of test names as supported by `fx test`, e.g.
+# fully-formed Fuchsia Package URLs, package names, or directories
+# - BUILD COMMAND is any command that should be run prior to running the test
+# group. It can be None if no build step is required, and is skipped if the
+# --no-build flag is passed in. It currently needs to be a List - run_tests
+# needs to be updated to support strings
+TEST_GROUPS = [
+ (
+ 'fidlc', (
+ [startswith('zircon/tools/fidl')], [util.TEST_FIDLC],
+ util.BUILD_FIDLC_TESTS)),
+
+ # it's possible to be more selective on which changes map to which tests,
+ # but since fidlgen tests are fast to build and run, just do a blanket
+ # check.
+ (
+ 'fidlgen', (
+ [startswith(util.FIDLGEN_DIR)] +
+ [startswith(p) for p in util.FIDLGEN_BACKEND_DIRS],
+ util.FIDLGEN_TEST_TARGETS, util.BUILD_FIDLGEN)),
+ (
+ 'fidlgen_dart', (
+ [startswith(util.FIDLGEN_DART_DIR)],
+ [util.FIDLGEN_DART_TEST_TARGET], util.BUILD_FIDLGEN_DART)),
+ (
+ 'hlcpp', (
+ [
+ endswith('test.json.cc.golden'),
+ endswith('test.json.h.golden'),
+ endswith('test.tables.c.golden'),
+ startswith(util.HLCPP_RUNTIME),
+ startswith(util.C_RUNTIME),
+ ], [util.HLCPP_TEST_TARGET], None)),
+ (
+ 'llcpp', (
+ [
+ endswith('test.json.llcpp.cc.golden'),
+ endswith('test.json.llcpp.h.golden'),
+ endswith('test.tables.c.golden'),
+ startswith(util.LLCPP_RUNTIME),
+ startswith(util.C_RUNTIME)
+ ], [util.LLCPP_TEST_TARGET], None)),
+ (
+ 'c',
+ (
+ # normally, changes to the generated bindings are detected by looking at the
+ # goldens. Since we can't do this for C, we look at the coding table goldens
+ # and the c_generator instead.
+ [
+ endswith('test.tables.c.golden'),
+ startswith('zircon/tools/fidl/include/fidl/c_generator.h'),
+ startswith('zircon/tools/fidl/lib/c_generator.cc'),
+ startswith(util.C_RUNTIME),
+ ],
+ # NOTE: fidl-test should also run, but this script only supports component
+ # tests
+ [util.C_TEST_TARGET],
+ None)),
+ (
+ 'go', (
+ [endswith('test.json.go.golden'),
+ startswith(util.GO_RUNTIME)],
+ [util.GO_TEST_TARGET, util.GO_CONFORMANCE_TEST_TARGET], None)),
+ (
+ 'rust', (
+ [endswith('test.json.rs.golden'),
+ startswith(util.RUST_RUNTIME)], [util.RUST_TEST_TARGET], None)),
+ (
+ 'dart', (
+ [
+ endswith('test.json_async.dart.golden'),
+ endswith('test.json_test.dart.golden'),
+ startswith(util.DART_RUNTIME)
+ ], [util.DART_TEST_TARGET], None)),
+ (
+ 'gidl',
+ (
+ [startswith('tools/fidl/gidl')],
+ [
+ util.GIDL_TEST_TARGET,
+ util.GO_CONFORMANCE_TEST_TARGET,
+ util.HLCPP_CONFORMANCE_TEST_TARGET,
+ util.HLCPP_HOST_CONFORMANCE_TEST_TARGET,
+ util.LLCPP_CONFORMANCE_TEST_TARGET,
+ util.RUST_CONFORMANCE_TEST_TARGET,
+ # dart conformance is bundled into the rest of the tests
+ util.DART_TEST_TARGET
+ ],
+ None)),
+]
+
+
+def test_explicit(targets, build_first, dry_run, interactive, fx_test_args):
+ """ Test an explicit set of test groups """
+ tests = []
+ for name, test in TEST_GROUPS:
+ if name in targets or 'all' in targets:
+ tests.append(test)
+ return run_tests(tests, build_first, dry_run, interactive, fx_test_args)
+
+
+def test_changed(
+ changed_files, build_first, dry_run, interactive, fx_test_args):
+ """ Test relevant test groups given a set of changed files """
+ tests = []
+ for _, test in TEST_GROUPS:
+ (predicates, _, _) = test
+ for file_ in changed_files:
+ if any(p(file_) for p in predicates):
+ tests.append(test)
+ return run_tests(tests, build_first, dry_run, interactive, fx_test_args)
+
+
+def run_tests(tests, build_first, dry_run, interactive, fx_test_args):
+ already_built = set()
+ test_targets = set()
+ manual_tests = set()
+ for name, targets, build in tests:
+ if build_first and build is not None and tuple(
+ build) not in already_built:
+ already_built.add(tuple(build))
+ util.run(build, dry_run, exit_on_failure=True)
+
+ for target in targets:
+ if is_manual_test(target):
+ manual_tests.add(target)
+ else:
+ test_targets.add(target)
+
+ manual_tests = list(manual_tests)
+ test_targets = list(test_targets)
+ if interactive:
+ print('all tests: ')
+ pprint.pprint(manual_tests + test_targets)
+ manual_tests = interactive_filter(manual_tests)
+ test_targets = interactive_filter(test_targets)
+
+ success = True
+ for cmd in manual_tests:
+ success = success and util.run(cmd, dry_run)
+ # print test line that can be copied into a commit message
+ # the absolute FUCHSIA_DIR paths are stripped for readability and
+ # because they are user specific
+ print('Test: ' + cmd.replace(str(util.FUCHSIA_DIR) + '/', ''))
+
+ if test_targets:
+ cmd = ['fx', 'test'] + fx_test_args.split()
+ if not build_first:
+ cmd.append('--no-build')
+ # group all tests into a single `fx test` invocation so that the summary
+ # prints all results
+ cmd.extend(test_targets)
+ success = success and util.run(cmd, dry_run)
+ print('Test: ' + ' '.join(cmd))
+
+ return success
+
+
+def interactive_filter(test_targets):
+ if not test_targets:
+ return []
+ filtered = []
+ for test in test_targets:
+ if input('run {}? (Y/n) '.format(test)) == 'n':
+ continue
+ filtered.append(test)
+ return filtered
+
+
+def is_manual_test(test):
+ """
+ Return whether this is meant to be called with fx test or used as a
+ standalone test command.
+ """
+ # currently fidlc is the only test that doesn't use fx test, since it
+ # uses some fidlc/fidl-compiler-test binary that is not built with the
+ # usual build commands (like fx build zircon/tools, fx ninja -C out/default
+ # host_x64/fidlc)
+ return test == util.TEST_FIDLC
diff --git a/fidldev/test_util.py b/fidldev/test_util.py
new file mode 100644
index 0000000..c70ca7c
--- /dev/null
+++ b/fidldev/test_util.py
@@ -0,0 +1,62 @@
+import argparse
+import contextlib
+import pprint
+
+import env
+
+# mock out these environment consts before importing anything else
+MOCK_FUCHSIA_DIR = 'fuchsia_dir'
+MOCK_BUILD_DIR = 'out/default'
+env.FUCHSIA_DIR = MOCK_FUCHSIA_DIR
+env.BUILD_DIR = MOCK_BUILD_DIR
+env.PLATFORM = 'linux'
+env.MODE = 'clang'
+
+import fidldev
+import util
+
+
+@contextlib.contextmanager
+def mocked_func(func_name, mocked_func):
+ """ Patch util.[func_name] with mocked_func within the specified context. """
+ original = getattr(util, func_name)
+ try:
+ setattr(util, func_name, mocked_func)
+ yield
+ finally:
+ setattr(util, func_name, original)
+
+
+def create_fixed_func(return_values):
+ """ Returns a function that successively returns each of the provided |return_values|. """
+ return_values = iter(return_values)
+
+ def mocked(*args, **kwargs):
+ return next(return_values)
+
+ return mocked
+
+
+def get_commands(mocks, test_cmd):
+ """ Run |test_cmd| with the provided |mocks|, and return the commands that fidldev would have run. """
+ mocked_funcs = [
+ mocked_func(name, create_fixed_func(values))
+ for name, values in mocks.items()
+ ]
+
+ commands = []
+
+ # The arguments and return value of this function need to be kept up to date with util.run
+ def mocked_run(command, dry_run, exit_on_failure=False):
+ commands.append(command)
+ return True
+
+ mocked_funcs.append(mocked_func('run', mocked_run))
+
+ with contextlib.ExitStack() as stack:
+ for func in mocked_funcs:
+ stack.enter_context(func)
+ args = fidldev.parser.parse_args(test_cmd)
+ args.func(args)
+
+ return commands
diff --git a/fidldev/util.py b/fidldev/util.py
new file mode 100644
index 0000000..0482dd0
--- /dev/null
+++ b/fidldev/util.py
@@ -0,0 +1,117 @@
+import os
+import subprocess
+
+from env import FUCHSIA_DIR, BUILD_DIR, MODE, PLATFORM
+
+TOPAZ_DIR = os.path.join(FUCHSIA_DIR, 'topaz')
+GO_DIR = os.path.join(FUCHSIA_DIR, 'third_party/go')
+
+FIDLC_DIR = 'zircon/tools/fidl'
+FIDLGEN_DIR = 'garnet/go/src/fidl/compiler'
+FIDLGEN_DART_DIR = 'topaz/bin/fidlgen_dart'
+FIDLGEN_GO_DIR = 'tools/fidl/fidlgen_go'
+FIDLGEN_BACKEND_DIRS = [
+ 'garnet/go/src/fidl/compiler/llcpp_backend',
+ FIDLGEN_GO_DIR,
+ 'tools/fidl/fidlgen_hlcpp',
+ 'tools/fidl/fidlgen_libfuzzer',
+ 'tools/fidl/fidlgen_rust',
+ 'tools/fidl/fidlgen_syzkaller',
+]
+
+TEST_FIDLC = os.path.join(FUCHSIA_DIR, BUILD_DIR, 'host_x64/fidl-compiler')
+FIDLGEN_TEST_TARGETS = [
+ '//garnet/go/src/fidl',
+ '//tools/fidl/fidlgen_hlcpp',
+ '//tools/fidl/fidlgen_go',
+ '//tools/fidl/fidlgen_libfuzzer',
+ '//tools/fidl/fidlgen_rust',
+ '//tools/fidl/fidlgen_syzkaller',
+]
+FIDLGEN_DART_TEST_TARGET = '//topaz/bin/fidlgen_dart'
+HLCPP_TEST_TARGET = '//sdk/lib/fidl'
+LLCPP_TEST_TARGET = '//src/lib/fidl/llcpp'
+C_TEST_TARGET = '//src/lib/fidl/c'
+GO_TEST_TARGET = 'go_fidl_test'
+RUST_TEST_TARGET = '//src/lib/fidl/rust'
+DART_TEST_TARGET = 'fidl_bindings_test'
+GIDL_TEST_TARGET = '//tools/fidl/gidl'
+
+HLCPP_CONFORMANCE_TEST_TARGET = 'conformance_test'
+HLCPP_HOST_CONFORMANCE_TEST_TARGET = 'fidl_cpp_host_conformance_test'
+LLCPP_CONFORMANCE_TEST_TARGET = 'fidl_llcpp_conformance_test'
+GO_CONFORMANCE_TEST_TARGET = 'fidl_go_conformance'
+RUST_CONFORMANCE_TEST_TARGET = 'fidl_rust_conformance_tests'
+
+HLCPP_RUNTIME = 'sdk/lib/fidl'
+LLCPP_RUNTIME = 'src/lib/fidl/llcpp'
+C_RUNTIME = 'zircon/system/ulib/fidl'
+GO_RUNTIME = 'third_party/go/src/syscall/zx/fidl'
+RUST_RUNTIME = 'src/lib/fidl/rust'
+DART_RUNTIME = 'topaz/public/dart/fidl/lib'
+
+BUILD_FIDLC = ['fx', 'build', 'zircon/tools']
+BUILD_FIDLC_TESTS = ['fx', 'ninja', '-C', BUILD_DIR, 'host_x64/fidl-compiler']
+BUILD_FIDLGEN = ['fx', 'build', 'garnet/go/src/fidl']
+BUILD_FIDLGEN_DART = ['fx', 'ninja', '-C', BUILD_DIR, 'host_x64/fidlgen_dart']
+
+
+def run(command, dry_run, exit_on_failure=False):
+ """
+ Run the given command, returning True if it completed successfuly. If
+ dry_run is true, just prints rather than running. If exit_on_failure is
+ true, exits instead of returning False.
+ """
+ if dry_run:
+ print('would run: {}'.format(command))
+ return True
+ retcode = subprocess.call(command)
+ success = retcode == 0
+ if exit_on_failure and not success:
+ print_err(
+ 'Error: command failed with status {}! {}'.format(retcode, command))
+ exit(1)
+ return success
+
+
+def get_changed_files():
+ """
+ Return a List of paths relative to FUCHSIA_DIR of changed files relative to
+ the parent. This uses the same logic as fx format-code.
+ """
+ upstream = "origin/master"
+ local_commit = subprocess.check_output(
+ "git rev-list HEAD ^{} -- 2>/dev/null | tail -1".format(upstream),
+ shell=True).strip().decode()
+ diff_base = subprocess.check_output(
+ ['git', 'rev-parse', local_commit +
+ '^']).strip().decode() if local_commit else "HEAD"
+ files = subprocess.check_output(['git', 'diff', '--name-only',
+ diff_base]).strip().decode().split('\n')
+
+ repo = subprocess.check_output(['git', 'rev-parse',
+ '--show-toplevel']).strip().decode()
+ # add prefixes so that all and targets can be specified relative to FUCHSIA_DIR
+ if repo.endswith('topaz'):
+ files = [os.path.join('topaz', p) for p in files]
+ elif repo.endswith('third_party/go'):
+ files = [os.path.join('third_party/go', p) for p in files]
+
+ return files
+
+
+RED = '\033[1;31m'
+YELLOW = '\033[1;33m'
+NC = '\033[0m'
+
+
+def print_err(s):
+ print_color(s, RED)
+
+
+def print_warning(s):
+ print_color(s, YELLOW)
+
+
+def print_color(s, color):
+ print('{}{}{}'.format(color, s, NC))
diff --git a/.vscode/launch.json b/vscode-language-fidl/.vscode/launch.json
similarity index 100%
rename from .vscode/launch.json
rename to vscode-language-fidl/.vscode/launch.json