| // 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. |
| |
| // 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() { |
| // TODO(fxbug.dev/119360): reenable this |
| // 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) { |
| // TODO(fxbug.dev/119360): reenable this |
| // 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}.fuchsia-log`, |
| 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}.log`, |
| 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 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 non-capturing NamedPattern group that matches the input pattern zero or more times. |
| * @param pattern |
| * @returns NamedPattern |
| */ |
| zeroOrMore: 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)]); |
| }, |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////////////////////////// |
| // Fuchsia Log language description. |
| /////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| const TIMESTAMP_TAG = patterns.named('\\[[0-9]+[.][0-9]+\\]', 'constant.numeric'); |
| const USER_TAGS = patterns.named(patterns.zeroOrMore('\\[([^\\]]*)\\]'), 'entity.name.type'); |
| |
| const TAGS = new NamedPattern(`^${TIMESTAMP_TAG}${USER_TAGS}`, getPatternNames([TIMESTAMP_TAG, USER_TAGS])); |
| |
| const logLevels = [ |
| ' TRACE: ', |
| ' DEBUG: ', |
| ' INFO: ', |
| patterns.named(' WARN: ', 'string.quoted'), |
| patterns.named(' ERROR: ', 'string.quoted'), |
| patterns.named(' FATAL: ', 'string.quoted'), |
| ]; |
| |
| const SEVERITY = patterns.oneFrom(...logLevels); |
| |
| TIMESTAMP_TAG.assert('[00047.419145]') |
| |
| USER_TAGS.assert('[pkg-resolver]'); |
| USER_TAGS.assert('[agents:agent-b05def7e]'); |
| USER_TAGS.assert('[pkg-resolver][agents:agent-b05def7e]'); |
| |
| TAGS.assert('[00792.409891][agents:agent-b05def7e]'); |
| TAGS.assert('[03723.724755][netstack][DHCP]'); |
| |
| SEVERITY.assert('WARN'); |
| |
| const tmLanguage: TmLanguage = { |
| $schema: tmSchema, |
| name: 'Fuchsia Log', |
| scopeName: 'source.fuchsia-log', |
| patterns: [ |
| match('meta.tags', TAGS), |
| match('meta.severity', SEVERITY), |
| ], |
| repository: {}, |
| } |
| |
| export default tmLanguage; |