| // Copyright 2022 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. |
| |
| // This generates a language definition file because writing it by hand is too hard. |
| |
| import { OnigRegExp } from 'oniguruma'; |
| |
| // Format of language definition JSON |
| const tmSchema = |
| 'https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json'; |
| type TmCaptures = { [k: string]: { name: string } }; |
| type TmIncludePattern = { include: string }; |
| type TmMatchPattern = { |
| name?: string; |
| match: string; |
| captures?: TmCaptures; |
| }; |
| type TmBlockPattern = { |
| name?: string; |
| begin: string; |
| beginCaptures?: TmCaptures; |
| end: string; |
| endCaptures?: TmCaptures; |
| patterns: TmPattern[]; |
| }; |
| type TmPattern = TmIncludePattern | TmMatchPattern | TmBlockPattern; |
| type TmLanguage = { |
| $schema: string; |
| name: string; |
| scopeName: string; |
| patterns: TmPattern[]; |
| repository: { |
| [key: string]: { patterns: TmPattern[] }; |
| }; |
| }; |
| |
| class NamedPattern { |
| readonly re: string; |
| readonly names: string[]; |
| constructor(re: string, names?: string[]) { |
| this.re = re; |
| this.names = names || []; |
| // Hack to only validate if there are no unresolved recursive groups |
| const recursiveRE = /\\g<(.*?)>/g; |
| let match; |
| while ((match = recursiveRE.exec(re)) !== null) { |
| if (!re.includes(`(?<${match[1]}>`)) { |
| // Group definition not found, can't validate yet. |
| return; |
| } |
| } |
| this.validate(); |
| } |
| |
| validate() { |
| const groupCount = new OnigRegExp(this.re + '|').searchSync('')!.length - 1; |
| if (groupCount !== this.names.length) { |
| throw new Error( |
| `Found ${groupCount} but expected ${this.names.length} groups in ${this.re}` |
| ); |
| } |
| } |
| |
| toString() { |
| return this.re; |
| } |
| |
| assert(s: string) { |
| const re = new OnigRegExp(this.re); |
| const m = re.searchSync(s); |
| if (!m) { |
| throw Error( |
| `${JSON.stringify(s)} did not match pattern ${JSON.stringify(this.re)}` |
| ); |
| } |
| const c = m[0]; |
| if (c.index !== 0 || c.start !== 0 || c.length !== s.length) { |
| throw Error( |
| `${JSON.stringify(s)} did not fully match pattern ${JSON.stringify( |
| this.re |
| )}, only matched ${JSON.stringify(m[0])}` |
| ); |
| } |
| } |
| } |
| |
| /** |
| * Create a dictionary of capture group names: <captureIdx, nameId> |
| * @param pattern input pattern |
| * @returns dictionary |
| */ |
| function getCaptures(pattern: Pattern): TmCaptures { |
| const captures: { [k: string]: { name: string } } = {}; |
| const names = getPatternNames(pattern); |
| for (let i = 0; i < names.length; i++) { |
| captures[`${i + 1}`] = { name: names[i] }; |
| } |
| return captures; |
| } |
| |
| /** |
| * Create TmIncludePattern from name identifier |
| * @param name identifier |
| * @returns TmIncludePattern |
| */ |
| function include(name: string): TmIncludePattern { |
| return { include: `#${name}` }; |
| } |
| |
| /** |
| * Create a match pattern for the output json syntax. |
| * @param name |
| * @param pattern |
| * @returns pattern struct for the output json. |
| */ |
| function match(name: string, pattern: Pattern): TmMatchPattern { |
| return { |
| name: `${name}.fidl`, |
| match: pattern.toString(), |
| captures: getCaptures(pattern), |
| }; |
| } |
| |
| /** |
| * Create a match pattern with no name for the output json syntax. |
| * @param pattern |
| * @returns pattern struct for the output json. |
| */ |
| function anonMatch(pattern: Pattern | TmPattern): TmPattern { |
| if (pattern instanceof String) { |
| return { |
| match: pattern.toString(), |
| }; |
| } |
| if (pattern instanceof NamedPattern) { |
| return { |
| match: pattern.toString(), |
| captures: getCaptures(pattern), |
| }; |
| } |
| return pattern; |
| } |
| |
| /** |
| * Create block pattern for the output json syntax. |
| * @param args |
| * @returns pattern struct for the output json. |
| */ |
| function block(args: { |
| name: string; |
| begin: Pattern; |
| end: Pattern; |
| patterns: Array<Pattern | TmPattern>; |
| }): TmBlockPattern { |
| const tmPatterns: TmPattern[] = args.patterns.map(anonMatch); |
| return { |
| name: `${args.name}.fidl`, |
| begin: args.begin.toString(), |
| beginCaptures: getCaptures(args.begin), |
| end: args.end.toString(), |
| endCaptures: getCaptures(args.end), |
| patterns: tmPatterns, |
| }; |
| } |
| |
| type Pattern = NamedPattern | String; |
| |
| /** Return the names of the groups in the pattern. */ |
| function getPatternNames(pat: Pattern | Pattern[]): string[] { |
| if (pat instanceof Array) { |
| return pat.map(getPatternNames).reduce((prev, names) => [...(prev || []), ...names]); |
| } |
| if (pat instanceof NamedPattern) { |
| return pat.names; |
| } else { |
| return []; |
| } |
| } |
| |
| /** |
| * Utility to create a NamedPattern by concatenating an input pattern array. Output = |
| * [prefix + (pat + separator?)* + suffix] |
| * @param pats input pattern array. |
| * @param prefix |
| * @param separator used to concatenate input patterns together. |
| * @param suffix |
| * @returns the resulting NamedPattern. |
| */ |
| function joinPattern( |
| pats: Pattern[], |
| prefix: string, |
| separator: string, |
| suffix: string |
| ): NamedPattern { |
| const re = pats.map((p) => p.toString()).join(separator); |
| return new NamedPattern(prefix + re + suffix, getPatternNames(pats)); |
| } |
| |
| // NamedPattern Factory. |
| var patterns = { |
| /** |
| * Create a new pattern consisting of adjacent input patterns optionally separated by whitespaces. |
| * @param pats Patterns |
| */ |
| seq: function (...pats: Pattern[]): NamedPattern { |
| return joinPattern(pats, '', '\\s*', ''); |
| }, |
| |
| /** |
| * Create a word NamedPattern match by searching word boundaries. |
| * @param word input pattern |
| * @returns NamedPattern. |
| */ |
| word: function (word: Pattern): NamedPattern { |
| return new NamedPattern(`\\b${word}\\b`, getPatternNames(word)); |
| }, |
| |
| /** |
| * Create a non-capturing NamedPattern group that matches one item from the pattern input vector. |
| * @param pats pattern input vector |
| * @returns NamedPattern. |
| */ |
| oneFrom: function (...pats: Pattern[]): NamedPattern { |
| return joinPattern(pats, '(?:', '|', ')'); |
| }, |
| |
| /** |
| * Create a NamedPattern that matches one word from the input vector. |
| * @param words vector string |
| * @returns NamedPattern |
| */ |
| oneWordFrom: function (...words: string[]) { |
| return patterns.word(patterns.oneFrom(...words)); |
| }, |
| |
| /** |
| * Create a non-capturing NamedPattern group that matches the input pattern zero or more times, |
| * and optionally separated by whitespaces. |
| * @param pattern |
| * @returns NamedPattern |
| */ |
| zeroOrMore: function (pattern: Pattern): NamedPattern { |
| return new NamedPattern(`(?:${pattern}\\s*)*`, getPatternNames(pattern)); |
| }, |
| |
| /** |
| * Create a non-capturing NamedPattern group that optionally matches a pattern. |
| * @param pattern |
| * @returns NamedPattern |
| */ |
| optional: function (pattern: Pattern): NamedPattern { |
| return new NamedPattern(`(?:${pattern})?`, getPatternNames(pattern)); |
| }, |
| |
| /** |
| * Create a NamedPattern. |
| * @param pattern |
| * @param name |
| * @returns NamedPattern |
| */ |
| named: function (pattern: Pattern, name: string) { |
| return new NamedPattern(`(${pattern})`, [name, ...getPatternNames(pattern)]); |
| }, |
| |
| /** |
| * Create a NamedPattern for separators. |
| * @param separator |
| * @returns |
| */ |
| separator: function (separator: string) { |
| return patterns.named(separator, 'punctuation.separator'); |
| }, |
| |
| /** |
| * Create a sequential NamedPattern = [`<` + contents + `>`] |
| * @param contents pattern |
| * @returns NamedPattern |
| */ |
| angleBrackets: function (contents: Pattern) { |
| const scope = 'punctuation.bracket.angle'; |
| return patterns.seq(patterns.named('<', scope), contents, patterns.named('>', scope)); |
| }, |
| |
| /** |
| * Create a word NamedPattern from a keyword string. |
| * @param keyword string |
| * @param name keyword identifier |
| * @returns NamedPattern |
| */ |
| keyword: function (keyword: string, name: string = 'keyword.control') { |
| return patterns.word(patterns.named(keyword, name)); |
| }, |
| }; |
| |
| const NUMERIC_LITERAL = patterns.named( |
| // eslint-disable-next-line max-len |
| '-?\\b(?:(?:0(?:x|X)[0-9a-fA-F]*)|(?:0(?:b|B)[01]*)|(?:(?:[0-9]+\\.?[0-9]*)|(?:\\.[0-9]+))(?:(?:e|E)(?:\\+|-)?[0-9]+)?)\\b', |
| 'constant.numeric' |
| ); |
| const BOOLEAN_LITERAL = patterns.named( |
| patterns.oneWordFrom('true', 'false'), |
| 'constant.language' |
| ); |
| |
| // eslint-disable-next-line quotes |
| const STRING_LITERAL = patterns.named('"(?:[^\\"]|\\.)*"', "string.quoted.double"); |
| const LITERAL = patterns.oneFrom(NUMERIC_LITERAL, BOOLEAN_LITERAL, STRING_LITERAL); |
| |
| const IDENTIFIER = '\\b[a-zA-Z_][0-9a-zA-Z_]*\\b'; |
| const COMPOUND_IDENTIFIER = new NamedPattern( |
| `${IDENTIFIER}(?:\\.${IDENTIFIER})*` |
| ); |
| |
| const CONSTANT = patterns.oneFrom(LITERAL, COMPOUND_IDENTIFIER); |
| const NUMERIC_CONSTANT = patterns.oneFrom(NUMERIC_LITERAL, COMPOUND_IDENTIFIER); |
| |
| const primitiveTypes = [ |
| 'bool', |
| 'float32', |
| 'float64', |
| 'int8', |
| 'int16', |
| 'int32', |
| 'int64', |
| 'uint8', |
| 'uint16', |
| 'uint32', |
| 'uint64', |
| ]; |
| |
| const PRIMITIVE_TYPE = patterns.named( |
| patterns.oneWordFrom(...primitiveTypes), |
| 'storage.type.basic' |
| ); |
| const EOL = new NamedPattern('(;)', ['punctuation.terminator']); |
| const LIBRARY_NAME = patterns.named(COMPOUND_IDENTIFIER, 'entity.name.type'); |
| const LOCAL_TYPE = patterns.named(IDENTIFIER, 'entity.name.type'); |
| const LAYOUT_REFERENCE = patterns.named(COMPOUND_IDENTIFIER, 'entity.name.type'); |
| const VARIABLE = patterns.named(IDENTIFIER, 'variable'); |
| |
| const ATTRIBUTE_TAG = patterns.named(patterns.seq( |
| '@', |
| IDENTIFIER |
| ), 'entity.other.attribute-name'); |
| |
| const MODIFIERS = patterns.named(patterns.zeroOrMore( |
| patterns.oneFrom( |
| patterns.word('strict'), |
| patterns.word('flexible'), |
| patterns.word('resource'), |
| ) |
| ), 'storage.type.modifier'); |
| |
| const ORDINAL = patterns.seq(NUMERIC_LITERAL, patterns.separator(':')); |
| |
| const LAYOUT_KIND = patterns.oneFrom( |
| patterns.keyword('union'), |
| patterns.keyword('struct'), |
| patterns.keyword('table'), |
| patterns.keyword('enum'), |
| patterns.keyword('bits') |
| ); |
| |
| const SUBTYPE = patterns.seq(patterns.separator(':'), LAYOUT_REFERENCE); |
| |
| // currently the only valid placement for a type layout parameter is the |
| // first parameter, so take advantage of this fact to simplify this rule |
| const TYPE_PARAMETERS = patterns.angleBrackets(patterns.seq(LAYOUT_REFERENCE, patterns.optional( |
| patterns.seq( |
| patterns.separator(','), |
| NUMERIC_CONSTANT, |
| ) |
| ))); |
| |
| // Checks |
| |
| const INLINE_LAYOUT_PREFIX = patterns.seq( |
| MODIFIERS, |
| LAYOUT_KIND, |
| patterns.optional(SUBTYPE), |
| '{' |
| ); |
| |
| COMPOUND_IDENTIFIER.assert('foo'); |
| COMPOUND_IDENTIFIER.assert('foo_bar'); |
| COMPOUND_IDENTIFIER.assert('foo.bar.baz'); |
| |
| NUMERIC_LITERAL.assert('-42'); |
| NUMERIC_LITERAL.assert('0x12345deadbeef'); |
| NUMERIC_LITERAL.assert('0b101110'); |
| |
| NUMERIC_CONSTANT.assert('0'); |
| NUMERIC_CONSTANT.assert('ABC'); |
| |
| MODIFIERS.assert('strict'); |
| MODIFIERS.assert('flexible'); |
| MODIFIERS.assert('resource'); |
| MODIFIERS.assert('strict resource'); |
| MODIFIERS.assert('flexible resource'); |
| MODIFIERS.assert('resource strict'); |
| MODIFIERS.assert('resource flexible'); |
| |
| TYPE_PARAMETERS.assert('<Foo, 3>'); |
| TYPE_PARAMETERS.assert('<Foo>'); |
| |
| INLINE_LAYOUT_PREFIX.assert('resource flexible struct {'); |
| INLINE_LAYOUT_PREFIX.assert('strict enum : int32 {'); |
| |
| ATTRIBUTE_TAG.assert('@foo'); |
| |
| const WITH_COMMENTS_AND_ATTRIBUTES = [ |
| include('comments'), |
| include('attributes'), |
| ]; |
| |
| const tmLanguage: TmLanguage = { |
| $schema: tmSchema, |
| name: 'FIDL', |
| scopeName: 'source.fidl', |
| patterns: [ |
| ...WITH_COMMENTS_AND_ATTRIBUTES, |
| |
| // Library declaration |
| match('meta.library', patterns.seq(patterns.keyword('library'), LIBRARY_NAME, EOL)), |
| |
| // Variations of using |
| match('meta.library', patterns.seq(patterns.keyword('using'), LIBRARY_NAME, EOL)), |
| match( |
| 'meta.library', |
| patterns.seq(patterns.keyword('using'), LIBRARY_NAME, patterns.keyword('as'), LOCAL_TYPE, EOL) |
| ), |
| |
| // Aliases: an aliased type can only be a layout reference, and cannot be re-parameterized |
| block({ |
| name: 'meta.type-alias', |
| begin: patterns.seq( |
| patterns.keyword('alias'), |
| patterns.named(IDENTIFIER, 'variable.alias'), |
| patterns.separator('='), |
| LAYOUT_REFERENCE, |
| ), |
| end: EOL, |
| patterns: [include('type-constructor')], |
| }), |
| |
| // Const declaration |
| block({ |
| name: 'meta.const', |
| begin: patterns.seq( |
| patterns.keyword('const'), |
| patterns.named(IDENTIFIER, 'variable.constant'), |
| patterns.named(patterns.oneFrom(COMPOUND_IDENTIFIER, PRIMITIVE_TYPE), 'storage.type'), |
| patterns.separator('='), |
| ), |
| end: EOL, |
| patterns: [include('const-value')], |
| }), |
| |
| // Protocols |
| block({ |
| name: 'meta.protocol-block', |
| begin: patterns.seq(patterns.keyword('protocol'), LOCAL_TYPE, '{'), |
| end: '}', |
| patterns: [ |
| ...WITH_COMMENTS_AND_ATTRIBUTES, |
| patterns.seq( |
| patterns.keyword('compose'), |
| patterns.named(COMPOUND_IDENTIFIER, 'entity.name.type')), |
| include('method'), |
| ], |
| }), |
| |
| // Type declaration |
| block({ |
| name: 'meta.type', |
| begin: patterns.seq( |
| patterns.keyword('type'), |
| LOCAL_TYPE, |
| patterns.separator('='), |
| ), |
| end: EOL, |
| patterns: [include('type-constructor')], |
| }), |
| ], |
| repository: { |
| comments: { |
| patterns: [ |
| match('invalid.illegal.stray-comment-end', '\\*/.*\\n'), |
| match('comment.line.documentation', '///.*\\n'), |
| match('comment.line.double-slash', '//.*\\n'), |
| ], |
| }, |
| 'attribute-with-args': { |
| patterns: [ |
| block({ |
| name: 'meta.attribute.with-args', |
| begin: patterns.seq(ATTRIBUTE_TAG, '\\('), |
| end: '\\)', |
| patterns: [ |
| patterns.seq( |
| patterns.optional(patterns.separator(',')), |
| IDENTIFIER, |
| patterns.separator('=')), |
| include('const-value'), |
| ], |
| }), |
| ], |
| }, |
| 'attribute-no-args': { |
| patterns: [ |
| match('meta.attribute.no-args', ATTRIBUTE_TAG), |
| ], |
| }, |
| attributes: { |
| patterns: [ |
| include('attribute-with-args'), |
| include('attribute-no-args'), |
| ], |
| }, |
| method: { |
| patterns: [ |
| block({ |
| name: 'meta.method', |
| begin: patterns.seq(patterns.named(IDENTIFIER, 'entity.name.function')), |
| end: EOL, |
| patterns: [ |
| include('method-argument'), |
| patterns.separator('->'), |
| patterns.keyword('error'), |
| ...WITH_COMMENTS_AND_ATTRIBUTES, |
| include('type-constructor'), |
| ], |
| }), |
| ], |
| }, |
| 'const-value': { |
| patterns: [ |
| match('meta.separator', patterns.separator('\\|')), |
| match('storage.type.operand', CONSTANT), |
| ], |
| }, |
| 'numeric-const-value': { |
| patterns: [ |
| match('meta.separator', patterns.separator('\\|')), |
| match('storage.type.operand', NUMERIC_CONSTANT), |
| ], |
| }, |
| 'default-value': { |
| patterns: [ |
| match('meta.separator', patterns.separator('=')), |
| include('const-value'), |
| ], |
| }, |
| 'method-argument': { |
| patterns: [ |
| block({ |
| name: 'meta.method.arguments', |
| begin: '\\(', |
| end: '\\)', |
| patterns: [ |
| ...WITH_COMMENTS_AND_ATTRIBUTES, include('type-constructor')], |
| }), |
| ], |
| }, |
| 'type-constructor': { |
| patterns: [ |
| include('type-constructor-inline'), |
| include('type-constructor-inline-arg'), |
| include('type-constructor-reference'), |
| // this must go last, so that we attempt to match on all unconstrained constructors first |
| include('type-constraints'), |
| ], |
| }, |
| // a type constructor that contains an inline layout, e.g. `foo struct { ... }; |
| 'type-constructor-inline': { |
| patterns: [ |
| block({ |
| name: 'meta.type-constructor-inline', |
| begin: patterns.seq(INLINE_LAYOUT_PREFIX), |
| end: patterns.seq( |
| '}', |
| ), |
| patterns: [...WITH_COMMENTS_AND_ATTRIBUTES, include('layout-member')], |
| }), |
| ], |
| }, |
| // a type constructor with a layout parameter that is an inline layout, e.g. |
| // `foo bar<struct {...}, ...>:<...>`; |
| 'type-constructor-inline-arg': { |
| patterns: [ |
| // currently the only valid placement for a type layout parameter is the |
| // first parameter, so take advantage of this fact to simplify this rule |
| block({ |
| name: 'meta.type-constructor-inline-arg', |
| begin: patterns.seq( |
| LAYOUT_REFERENCE, |
| '<', |
| INLINE_LAYOUT_PREFIX |
| ), |
| end: patterns.seq( |
| // the rest of the layout parameters (i.e. array size) |
| patterns.optional(patterns.seq(',', CONSTANT)), |
| '>', |
| ), |
| patterns: [...WITH_COMMENTS_AND_ATTRIBUTES, include('layout-member')], |
| }), |
| ], |
| }, |
| // a type constructor that references another type, e.g. `foo bar<...>:<...>;` |
| 'type-constructor-reference': { |
| patterns: [ |
| match( |
| 'meta.type-constructor-reference', |
| patterns.seq( |
| LAYOUT_REFERENCE, |
| patterns.optional(TYPE_PARAMETERS), |
| ) |
| ), |
| ], |
| }, |
| 'type-constraints': { |
| patterns: [ |
| include('type-constraints-list'), |
| include('type-constraints-singleton'), |
| ], |
| }, |
| 'type-constraints-list': { |
| patterns: [ |
| block({ |
| name: 'meta.type-constraints-list', |
| begin: ':<', |
| end: '>', |
| patterns: [include('const-value')], |
| }), |
| ], |
| }, |
| 'type-constraints-singleton': { |
| patterns: [ |
| match( |
| 'meta.type-constraints-singleton', |
| patterns.seq( |
| ':', |
| patterns.named(CONSTANT, 'storage.type.constraint'), |
| ) |
| ), |
| ], |
| }, |
| 'layout-member': { |
| // the ordering of the patterns below is important, since we want to attempt value before we |
| // attempt a default struct member |
| patterns: [ |
| include('layout-member-reserved'), |
| include('layout-member-value'), |
| include('layout-member-struct'), |
| include('layout-member-ordinaled'), |
| ], |
| }, |
| // the reserved member, e.g. `1: reserved;`, only applies to unions and tables |
| 'layout-member-reserved': { |
| patterns: [ |
| match( |
| 'meta.layout.reserved-member', |
| patterns.seq(ORDINAL, patterns.keyword('reserved'), EOL) |
| ), |
| ], |
| }, |
| // an ordinaled layout member, like `1: a bool;`, only applies to unions and tables |
| 'layout-member-ordinaled': { |
| patterns: [ |
| block({ |
| name: 'meta.layout.ordinaled-member', |
| begin: patterns.seq(ORDINAL, VARIABLE), |
| end: EOL, |
| patterns: [...WITH_COMMENTS_AND_ATTRIBUTES, include('type-constructor')], |
| }), |
| ], |
| }, |
| // a struct member; may have an optional default value, e.g. `foo uint8 = 3;` |
| 'layout-member-struct': { |
| patterns: [ |
| block({ |
| name: 'meta.layout.struct-struct-member', |
| begin: VARIABLE, |
| end: EOL, |
| patterns: [ |
| ...WITH_COMMENTS_AND_ATTRIBUTES, |
| include('type-constructor'), |
| include('default-value'), |
| ], |
| }), |
| ], |
| }, |
| // a layout member that specifies a value, e.g. `foo = 1;`, only applies to bits and enums |
| 'layout-member-value': { |
| patterns: [ |
| block({ |
| name: 'meta.layout.value-member', |
| begin: patterns.seq( |
| VARIABLE, |
| patterns.separator('='), |
| ), |
| end: EOL, |
| patterns: [include('numeric-const-value')], |
| }), |
| ], |
| }, |
| }, |
| }; |
| export default tmLanguage; |