| // Copyright 2021 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 ( |
| "regexp" |
| "strconv" |
| "strings" |
| |
| "github.com/alecthomas/participle/v2" |
| "github.com/alecthomas/participle/v2/lexer" |
| ) |
| |
| // This file is based on the official FIDL grammar: |
| // https://fuchsia.dev/fuchsia-src/reference/fidl/language/grammar |
| // |
| // It uses the participle parser library: |
| // https://github.com/alecthomas/participle |
| // |
| // A quick primer on participle: |
| // |
| // - It has separate lexing and parsing stages. |
| // - Lexer tokens are defined by regexes. |
| // - Productions are defined by structs with tags on each field. |
| // - The tags get concatenated (e.g. parens can be unbalanced within a tag). |
| // - Tags have the format `<exp1> <exp2> ... <expN>`. |
| // - Expression `"foo"` matches the literal "foo". |
| // - Expression `Foo` matches a Foo token. |
| // - Expression `@<exp>` captures <exp> into the field. |
| // - Expression `@@` recursively captures based on the field's type. |
| // - Tags for slice types can have multiple captures. |
| // - Boolean fields become true if a capture occurred. |
| // - Parentheses, `?`, `+`, `*`, `|` behave as expected. |
| |
| var ( |
| fidlLexer = lexer.MustSimple([]lexer.Rule{ |
| {Name: "Whitespace", Pattern: `[ \n\r\t]`}, |
| {Name: "Comment", Pattern: `//[^\n]*`}, |
| {Name: "Punctuation", Pattern: "[.,;@=()]"}, |
| {Name: "Keyword", Pattern: `library|using|as`}, |
| {Name: "Bool", Pattern: `true|false`}, |
| {Name: "String", Pattern: `"(\\.|[^"\\])*"`}, |
| // This pattern matches the FIDL lexer, which defers more precise |
| // numeric parsing to the flat AST stage. |
| {Name: "Number", Pattern: `[0-9][0-9a-fA-FxX._\-]*`}, |
| // https://fuchsia.dev/fuchsia-src/reference/fidl/language/language?hl=en#identifiers |
| {Name: "Identifier", Pattern: `[a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])?`}, |
| // Define a fallback token so that the lexer never fails and we can |
| // ignore the rest of the file (see the Rest field in fidlTopLevel). |
| {Name: "Other", Pattern: `.*`}, |
| }) |
| |
| fidlParser = participle.MustBuild( |
| &fidlTopLevel{}, |
| participle.Lexer(fidlLexer), |
| participle.Elide("Whitespace", "Comment"), |
| ) |
| ) |
| |
| type fidlTopLevel struct { |
| Header fidlLibraryHeader `@@` |
| Using []fidlUsing `@@*` |
| // Ignore the rest of the file by matching 0+ tokens of any kind. Rather |
| // than list every token type, we say ~"" (anything not equal to ""). |
| Rest bool `(@~"")*` |
| } |
| |
| type fidlLibraryHeader struct { |
| Attributes []fidlAttribute `@@*` |
| Library fidlCompoundIdentifier `"library" @@ ";"` |
| } |
| |
| type fidlUsing struct { |
| Library fidlCompoundIdentifier `"using" @@` |
| Alias bool `("as" @Identifier)? ";"` |
| } |
| |
| type fidlAttribute struct { |
| Name bool `"@" @Identifier` |
| Args []fidlAttributeArg `("(" @@ ("," @@)* ")")?` |
| } |
| |
| type fidlAttributeArg struct { |
| Name bool `(@Identifier "=")?` |
| Value fidlConstant `@@` |
| } |
| |
| type fidlConstant struct { |
| Identifier *fidlCompoundIdentifier `@@` |
| Literal bool `| @String | @Number | @Bool` |
| } |
| |
| type fidlCompoundIdentifier struct { |
| // Note: The field must be named "Pos" for participle to recognize it. |
| Pos lexer.Position |
| Parts []string `@Identifier ("." @Identifier)*` |
| } |
| |
| // Note: fields exported for cmp.Diff. |
| type parsedFidl struct { |
| Decl parsedLibrary |
| Using []parsedLibrary |
| } |
| |
| // Note: fields exported for cmp.Diff. |
| type parsedLibrary struct { |
| Library library |
| Line, Column int |
| } |
| |
| func parseFidl(input string) (parsedFidl, error) { |
| var ( |
| top fidlTopLevel |
| parsed parsedFidl |
| filename = "" |
| ) |
| err := fidlParser.ParseString(filename, input, &top) |
| if err != nil { |
| return parsed, err |
| } |
| parsed.Decl = toParsedLibrary(top.Header.Library) |
| for _, using := range top.Using { |
| parsed.Using = append(parsed.Using, toParsedLibrary(using.Library)) |
| } |
| return parsed, nil |
| } |
| |
| func toParsedLibrary(ident fidlCompoundIdentifier) parsedLibrary { |
| return parsedLibrary{ |
| Library: library(strings.Join(ident.Parts, ".")), |
| Line: ident.Pos.Line, |
| Column: ident.Pos.Column, |
| } |
| } |
| |
| func (m *parsedLibrary) annotation(kind annotationKind, text string) annotation { |
| return annotation{ |
| Line: m.Line, |
| Column: m.Column, |
| Kind: kind, |
| Text: text, |
| } |
| } |
| |
| var fidlAnnotationRegexp = regexp.MustCompile(`` + |
| // Multi-line mode: ^ and $ match begin/end of line OR text. |
| `(?m)` + |
| `^/.*/fidlbolt\.fidl:(\d+):(\d+).*?` + |
| `(` + |
| string(Info) + `|` + |
| string(Warning) + `|` + |
| string(Error) + `)` + |
| `:\s*(.*)$`) |
| |
| // parseFidlAnnotations parses a list of annotations from error output. It |
| // assumes that the output refers to a file called "fidlbolt.fidl". |
| // TODO: Use fidlc --format=json instead of regex parsing. |
| func parseFidlAnnotations(output string) []annotation { |
| matches := fidlAnnotationRegexp.FindAllStringSubmatch(output, -1) |
| var anns []annotation |
| for _, m := range matches { |
| line, err := strconv.Atoi(m[1]) |
| if err != nil { |
| continue |
| } |
| column, err := strconv.Atoi(m[2]) |
| if err != nil { |
| continue |
| } |
| anns = append(anns, annotation{ |
| Line: line, |
| Column: column, |
| Kind: annotationKind(m[3]), |
| Text: m[4], |
| }) |
| } |
| return anns |
| } |