blob: f91f9b3168e4b9c7ae938d0d38f7419f638f5cdd [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package 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
}