blob: 5c252ab1fdedb0793b0f51feafb40eba478074af [file] [log] [blame]
// 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;