| // 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 |
| } |