mirror of https://github.com/grafana/grafana
Prometheus: Query parsing for visual editor (#44824)
* Add parsing from string to visual query * Add comments * Parse expr when changing to builder * Support template variables * Report parsing errors * More error handling * Add ts-ignore for debug func * Fix commentspull/45171/head
parent
914966a347
commit
071ff0b399
@ -0,0 +1,312 @@ |
||||
import { buildVisualQueryFromString } from './parsing'; |
||||
import { PromVisualQuery } from './types'; |
||||
|
||||
describe('buildVisualQueryFromString', () => { |
||||
it('parses simple query', () => { |
||||
expect(buildVisualQueryFromString('counters_logins{app="frontend"}')).toEqual( |
||||
noErrors({ |
||||
metric: 'counters_logins', |
||||
labels: [ |
||||
{ |
||||
op: '=', |
||||
value: 'frontend', |
||||
label: 'app', |
||||
}, |
||||
], |
||||
operations: [], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('parses query with rate and interval', () => { |
||||
expect(buildVisualQueryFromString('rate(counters_logins{app="frontend"}[5m])')).toEqual( |
||||
noErrors({ |
||||
metric: 'counters_logins', |
||||
labels: [ |
||||
{ |
||||
op: '=', |
||||
value: 'frontend', |
||||
label: 'app', |
||||
}, |
||||
], |
||||
operations: [ |
||||
{ |
||||
id: 'rate', |
||||
params: ['5m'], |
||||
}, |
||||
], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('parses query with nested query and interval variable', () => { |
||||
expect( |
||||
buildVisualQueryFromString( |
||||
'avg(rate(access_evaluation_duration_count{instance="host.docker.internal:3000"}[$__rate_interval]))' |
||||
) |
||||
).toEqual( |
||||
noErrors({ |
||||
metric: 'access_evaluation_duration_count', |
||||
labels: [ |
||||
{ |
||||
op: '=', |
||||
value: 'host.docker.internal:3000', |
||||
label: 'instance', |
||||
}, |
||||
], |
||||
operations: [ |
||||
{ |
||||
id: 'rate', |
||||
params: ['$__rate_interval'], |
||||
}, |
||||
{ |
||||
id: 'avg', |
||||
params: [], |
||||
}, |
||||
], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('parses query with aggregation by labels', () => { |
||||
const visQuery = { |
||||
metric: 'metric_name', |
||||
labels: [ |
||||
{ |
||||
label: 'instance', |
||||
op: '=', |
||||
value: 'internal:3000', |
||||
}, |
||||
], |
||||
operations: [ |
||||
{ |
||||
id: '__sum_by', |
||||
params: ['app', 'version'], |
||||
}, |
||||
], |
||||
}; |
||||
expect(buildVisualQueryFromString('sum(metric_name{instance="internal:3000"}) by (app, version)')).toEqual( |
||||
noErrors(visQuery) |
||||
); |
||||
expect(buildVisualQueryFromString('sum by (app, version)(metric_name{instance="internal:3000"})')).toEqual( |
||||
noErrors(visQuery) |
||||
); |
||||
}); |
||||
|
||||
it('parses aggregation with params', () => { |
||||
expect(buildVisualQueryFromString('topk(5, http_requests_total)')).toEqual( |
||||
noErrors({ |
||||
metric: 'http_requests_total', |
||||
labels: [], |
||||
operations: [ |
||||
{ |
||||
id: 'topk', |
||||
params: [5], |
||||
}, |
||||
], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('parses aggregation with params and labels', () => { |
||||
expect(buildVisualQueryFromString('topk by(instance, job) (5, http_requests_total)')).toEqual( |
||||
noErrors({ |
||||
metric: 'http_requests_total', |
||||
labels: [], |
||||
operations: [ |
||||
{ |
||||
id: '__topk_by', |
||||
params: [5, 'instance', 'job'], |
||||
}, |
||||
], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('parses function with multiple arguments', () => { |
||||
expect( |
||||
buildVisualQueryFromString( |
||||
'label_replace(avg_over_time(http_requests_total{instance="foo"}[$__interval]), "instance", "$1", "", "(.*)")' |
||||
) |
||||
).toEqual( |
||||
noErrors({ |
||||
metric: 'http_requests_total', |
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }], |
||||
operations: [ |
||||
{ |
||||
id: 'avg_over_time', |
||||
params: ['$__interval'], |
||||
}, |
||||
{ |
||||
id: 'label_replace', |
||||
params: ['instance', '$1', '', '(.*)'], |
||||
}, |
||||
], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('parses binary operation with scalar', () => { |
||||
expect(buildVisualQueryFromString('avg_over_time(http_requests_total{instance="foo"}[$__interval]) / 2')).toEqual( |
||||
noErrors({ |
||||
metric: 'http_requests_total', |
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }], |
||||
operations: [ |
||||
{ |
||||
id: 'avg_over_time', |
||||
params: ['$__interval'], |
||||
}, |
||||
{ |
||||
id: '__divide_by', |
||||
params: [2], |
||||
}, |
||||
], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('parses binary operation with 2 queries', () => { |
||||
expect( |
||||
buildVisualQueryFromString('avg_over_time(http_requests_total{instance="foo"}[$__interval]) / sum(logins_count)') |
||||
).toEqual( |
||||
noErrors({ |
||||
metric: 'http_requests_total', |
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }], |
||||
operations: [{ id: 'avg_over_time', params: ['$__interval'] }], |
||||
binaryQueries: [ |
||||
{ |
||||
operator: '/', |
||||
query: { |
||||
metric: 'logins_count', |
||||
labels: [], |
||||
operations: [{ id: 'sum', params: [] }], |
||||
}, |
||||
}, |
||||
], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('parses template variables in strings', () => { |
||||
expect(buildVisualQueryFromString('http_requests_total{instance="$label_variable"}')).toEqual( |
||||
noErrors({ |
||||
metric: 'http_requests_total', |
||||
labels: [{ label: 'instance', op: '=', value: '$label_variable' }], |
||||
operations: [], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('parses template variables for metric', () => { |
||||
expect(buildVisualQueryFromString('$metric_variable{instance="foo"}')).toEqual( |
||||
noErrors({ |
||||
metric: '$metric_variable', |
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }], |
||||
operations: [], |
||||
}) |
||||
); |
||||
|
||||
expect(buildVisualQueryFromString('${metric_variable:fmt}{instance="foo"}')).toEqual( |
||||
noErrors({ |
||||
metric: '${metric_variable:fmt}', |
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }], |
||||
operations: [], |
||||
}) |
||||
); |
||||
|
||||
expect(buildVisualQueryFromString('[[metric_variable:fmt]]{instance="foo"}')).toEqual( |
||||
noErrors({ |
||||
metric: '[[metric_variable:fmt]]', |
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }], |
||||
operations: [], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('parses template variables in label name', () => { |
||||
expect(buildVisualQueryFromString('metric{${variable_label}="foo"}')).toEqual( |
||||
noErrors({ |
||||
metric: 'metric', |
||||
labels: [{ label: '${variable_label}', op: '=', value: 'foo' }], |
||||
operations: [], |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('fails to parse variable for function', () => { |
||||
expect(buildVisualQueryFromString('${func_var}(metric{bar="foo"})')).toEqual({ |
||||
errors: [ |
||||
{ |
||||
text: '(', |
||||
from: 20, |
||||
to: 21, |
||||
parentType: 'VectorSelector', |
||||
}, |
||||
{ |
||||
text: 'metric', |
||||
from: 21, |
||||
to: 27, |
||||
parentType: 'VectorSelector', |
||||
}, |
||||
], |
||||
query: { |
||||
metric: '${func_var}', |
||||
labels: [{ label: 'bar', op: '=', value: 'foo' }], |
||||
operations: [], |
||||
}, |
||||
}); |
||||
}); |
||||
|
||||
it('fails to parse malformed query', () => { |
||||
expect(buildVisualQueryFromString('asdf-metric{bar="})')).toEqual({ |
||||
errors: [ |
||||
{ |
||||
text: '', |
||||
from: 19, |
||||
to: 19, |
||||
parentType: 'LabelMatchers', |
||||
}, |
||||
], |
||||
query: { |
||||
metric: 'asdf', |
||||
labels: [], |
||||
operations: [], |
||||
binaryQueries: [ |
||||
{ |
||||
operator: '-', |
||||
query: { |
||||
metric: 'metric', |
||||
labels: [{ label: 'bar', op: '=', value: '})' }], |
||||
operations: [], |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
}); |
||||
}); |
||||
|
||||
it('fails to parse malformed query 2', () => { |
||||
expect(buildVisualQueryFromString('ewafweaf{afea=afe}')).toEqual({ |
||||
errors: [ |
||||
{ |
||||
text: 'afe}', |
||||
from: 14, |
||||
to: 18, |
||||
parentType: 'LabelMatcher', |
||||
}, |
||||
], |
||||
query: { |
||||
metric: 'ewafweaf', |
||||
labels: [{ label: 'afea', op: '=', value: '' }], |
||||
operations: [], |
||||
}, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
function noErrors(query: PromVisualQuery) { |
||||
return { |
||||
errors: [], |
||||
query, |
||||
}; |
||||
} |
@ -0,0 +1,417 @@ |
||||
import { parser } from 'lezer-promql'; |
||||
import { SyntaxNode } from 'lezer-tree'; |
||||
import { QueryBuilderLabelFilter, QueryBuilderOperation } from './shared/types'; |
||||
import { PromVisualQuery } from './types'; |
||||
|
||||
// Taken from template_srv, but copied so to not mess with the regex.index which is manipulated in the service
|
||||
/* |
||||
* This regex matches 3 types of variable reference with an optional format specifier |
||||
* \$(\w+) $var1 |
||||
* \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] |
||||
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3} |
||||
*/ |
||||
const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g; |
||||
|
||||
/** |
||||
* As variables with $ are creating parsing errors, we first replace them with magic string that is parseable and at |
||||
* the same time we can get the variable and it's format back from it. |
||||
* @param expr |
||||
*/ |
||||
function replaceVariables(expr: string) { |
||||
return expr.replace(variableRegex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => { |
||||
const fmt = fmt2 || fmt3; |
||||
let variable = var1; |
||||
let varType = '0'; |
||||
|
||||
if (var2) { |
||||
variable = var2; |
||||
varType = '1'; |
||||
} |
||||
|
||||
if (var3) { |
||||
variable = var3; |
||||
varType = '2'; |
||||
} |
||||
|
||||
return `__V_${varType}__` + variable + '__V__' + (fmt ? '__F__' + fmt + '__F__' : ''); |
||||
}); |
||||
} |
||||
|
||||
const varTypeFunc = [ |
||||
(v: string, f?: string) => `\$${v}`, |
||||
(v: string, f?: string) => `[[${v}${f ? `:${f}` : ''}]]`, |
||||
(v: string, f?: string) => `\$\{${v}${f ? `:${f}` : ''}\}`, |
||||
]; |
||||
|
||||
/** |
||||
* Get beck the text with variables in their original format. |
||||
* @param expr |
||||
*/ |
||||
function returnVariables(expr: string) { |
||||
return expr.replace(/__V_(\d)__(.+)__V__(?:__F__(\w+)__F__)?/g, (match, type, v, f) => { |
||||
return varTypeFunc[parseInt(type, 10)](v, f); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Parses a PromQL query into a visual query model. |
||||
* |
||||
* It traverses the tree and uses sort of state machine to update update the query model. The query model is modified |
||||
* during the traversal and sent to each handler as context. |
||||
* |
||||
* @param expr |
||||
*/ |
||||
export function buildVisualQueryFromString(expr: string): Context { |
||||
const replacedExpr = replaceVariables(expr); |
||||
const tree = parser.parse(replacedExpr); |
||||
const node = tree.topNode; |
||||
|
||||
// This will be modified in the handlers.
|
||||
const visQuery: PromVisualQuery = { |
||||
metric: '', |
||||
labels: [], |
||||
operations: [], |
||||
}; |
||||
const context = { |
||||
query: visQuery, |
||||
errors: [], |
||||
}; |
||||
|
||||
handleExpression(replacedExpr, node, context); |
||||
return context; |
||||
} |
||||
|
||||
interface ParsingError { |
||||
text: string; |
||||
from: number; |
||||
to: number; |
||||
parentType?: string; |
||||
} |
||||
|
||||
interface Context { |
||||
query: PromVisualQuery; |
||||
errors: ParsingError[]; |
||||
} |
||||
|
||||
// This is used for error type for some reason
|
||||
const ErrorName = '⚠'; |
||||
|
||||
/** |
||||
* Handler for default state. It will traverse the tree and call the appropriate handler for each node. The node |
||||
* handled here does not necessarily needs to be of type == Expr. |
||||
* @param expr |
||||
* @param node |
||||
* @param context |
||||
*/ |
||||
export function handleExpression(expr: string, node: SyntaxNode, context: Context) { |
||||
const visQuery = context.query; |
||||
switch (node.name) { |
||||
case 'MetricIdentifier': { |
||||
// Expectation is that there is only one of those per query.
|
||||
visQuery.metric = getString(expr, node); |
||||
break; |
||||
} |
||||
|
||||
case 'LabelMatcher': { |
||||
// Same as MetricIdentifier should be just one per query.
|
||||
visQuery.labels.push(getLabel(expr, node)); |
||||
const err = node.getChild(ErrorName); |
||||
if (err) { |
||||
context.errors.push(makeError(expr, err)); |
||||
} |
||||
break; |
||||
} |
||||
|
||||
case 'FunctionCall': { |
||||
handleFunction(expr, node, context); |
||||
break; |
||||
} |
||||
|
||||
case 'AggregateExpr': { |
||||
handleAggregation(expr, node, context); |
||||
break; |
||||
} |
||||
|
||||
case 'BinaryExpr': { |
||||
handleBinary(expr, node, context); |
||||
break; |
||||
} |
||||
|
||||
case ErrorName: { |
||||
if (isIntervalVariableError(node)) { |
||||
break; |
||||
} |
||||
context.errors.push(makeError(expr, node)); |
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
// Any other nodes we just ignore and go to it's children. This should be fine as there are lot's of wrapper
|
||||
// nodes that can be skipped.
|
||||
// TODO: there are probably cases where we will just skip nodes we don't support and we should be able to
|
||||
// detect those and report back.
|
||||
let child = node.firstChild; |
||||
while (child) { |
||||
handleExpression(expr, child, context); |
||||
child = child.nextSibling; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
function makeError(expr: string, node: SyntaxNode) { |
||||
return { |
||||
text: getString(expr, node), |
||||
// TODO: this are positions in the string with the replaced variables. Means it cannot be used to show exact
|
||||
// placement of the error for the user. We need some translation table to positions before the variable
|
||||
// replace.
|
||||
from: node.from, |
||||
to: node.to, |
||||
parentType: node.parent?.name, |
||||
}; |
||||
} |
||||
|
||||
function isIntervalVariableError(node: SyntaxNode) { |
||||
return node.prevSibling?.name === 'Expr' && node.prevSibling?.firstChild?.name === 'VectorSelector'; |
||||
} |
||||
|
||||
function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter { |
||||
const label = getString(expr, node.getChild('LabelName')); |
||||
const op = getString(expr, node.getChild('MatchOp')); |
||||
const value = getString(expr, node.getChild('StringLiteral')).replace(/"/g, ''); |
||||
return { |
||||
label, |
||||
op, |
||||
value, |
||||
}; |
||||
} |
||||
|
||||
const rangeFunctions = ['changes', 'rate', 'irate', 'increase', 'delta']; |
||||
/** |
||||
* Handle function call which is usually and identifier and its body > arguments. |
||||
* @param expr |
||||
* @param node |
||||
* @param context |
||||
*/ |
||||
function handleFunction(expr: string, node: SyntaxNode, context: Context) { |
||||
const visQuery = context.query; |
||||
const nameNode = node.getChild('FunctionIdentifier'); |
||||
const funcName = getString(expr, nameNode); |
||||
|
||||
const body = node.getChild('FunctionCallBody'); |
||||
const callArgs = body!.getChild('FunctionCallArgs'); |
||||
const params = []; |
||||
|
||||
// This is a bit of a shortcut to get the interval argument. Reasons are
|
||||
// - interval is not part of the function args per promQL grammar but we model it as argument for the function in
|
||||
// the query model.
|
||||
// - it is easier to handle template variables this way as template variable is an error for the parser
|
||||
if (rangeFunctions.includes(funcName) || funcName.endsWith('_over_time')) { |
||||
let match = getString(expr, node).match(/\[(.+)\]/); |
||||
if (match?.[1]) { |
||||
params.push(returnVariables(match[1])); |
||||
} |
||||
} |
||||
|
||||
const op = { id: funcName, params }; |
||||
// We unshift operations to keep the more natural order that we want to have in the visual query editor.
|
||||
visQuery.operations.unshift(op); |
||||
updateFunctionArgs(expr, callArgs!, context, op); |
||||
} |
||||
|
||||
/** |
||||
* Handle aggregation as they are distinct type from other functions. |
||||
* @param expr |
||||
* @param node |
||||
* @param context |
||||
*/ |
||||
function handleAggregation(expr: string, node: SyntaxNode, context: Context) { |
||||
const visQuery = context.query; |
||||
const nameNode = node.getChild('AggregateOp'); |
||||
let funcName = getString(expr, nameNode); |
||||
|
||||
const modifier = node.getChild('AggregateModifier'); |
||||
const labels = []; |
||||
|
||||
// TODO: support also Without modifier (but we don't support it in visual query yet)
|
||||
if (modifier) { |
||||
const byModifier = modifier.getChild(`By`); |
||||
if (byModifier && funcName) { |
||||
funcName = `__${funcName}_by`; |
||||
} |
||||
labels.push(...getAllByType(expr, modifier, 'GroupingLabel')); |
||||
} |
||||
|
||||
const body = node.getChild('FunctionCallBody'); |
||||
const callArgs = body!.getChild('FunctionCallArgs'); |
||||
|
||||
const op: QueryBuilderOperation = { id: funcName, params: [] }; |
||||
visQuery.operations.unshift(op); |
||||
updateFunctionArgs(expr, callArgs!, context, op); |
||||
// We add labels after params in the visual query editor.
|
||||
op.params.push(...labels); |
||||
} |
||||
|
||||
/** |
||||
* Handle (probably) all types of arguments that function or aggregation can have. |
||||
* |
||||
* FunctionCallArgs are nested bit weirdly basically its [firstArg, ...rest] where rest is again FunctionCallArgs so |
||||
* we cannot just get all the children and iterate them as arguments we have to again recursively traverse through |
||||
* them. |
||||
* |
||||
* @param expr |
||||
* @param node |
||||
* @param context |
||||
* @param op - We need the operation to add the params to as an additional context. |
||||
*/ |
||||
function updateFunctionArgs(expr: string, node: SyntaxNode, context: Context, op: QueryBuilderOperation) { |
||||
switch (node.name) { |
||||
// In case we have an expression we don't know what kind so we have to look at the child as it can be anything.
|
||||
case 'Expr': |
||||
// FunctionCallArgs are nested bit weirdly as mentioned so we have to go one deeper in this case.
|
||||
case 'FunctionCallArgs': { |
||||
let child = node.firstChild; |
||||
while (child) { |
||||
updateFunctionArgs(expr, child, context, op); |
||||
child = child.nextSibling; |
||||
} |
||||
break; |
||||
} |
||||
|
||||
case 'NumberLiteral': { |
||||
op.params.push(parseInt(getString(expr, node), 10)); |
||||
break; |
||||
} |
||||
|
||||
case 'StringLiteral': { |
||||
op.params.push(getString(expr, node).replace(/"/g, '')); |
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
// Means we get to something that does not seem like simple function arg and is probably nested query so jump
|
||||
// back to main context
|
||||
handleExpression(expr, node, context); |
||||
} |
||||
} |
||||
} |
||||
|
||||
const operatorToOpName: Record<string, string> = { |
||||
'/': '__divide_by', |
||||
'*': '__multiply_by', |
||||
}; |
||||
|
||||
/** |
||||
* Right now binary expressions can be represented in 2 way in visual query. As additional operation in case it is |
||||
* just operation with scalar or it creates a binaryQuery when it's 2 queries. |
||||
* @param expr |
||||
* @param node |
||||
* @param context |
||||
*/ |
||||
function handleBinary(expr: string, node: SyntaxNode, context: Context) { |
||||
const visQuery = context.query; |
||||
const left = node.firstChild!; |
||||
const op = getString(expr, left.nextSibling); |
||||
const right = node.lastChild!; |
||||
|
||||
const opName = operatorToOpName[op]; |
||||
|
||||
const leftNumber = left.getChild('NumberLiteral'); |
||||
const rightNumber = right.getChild('NumberLiteral'); |
||||
|
||||
if (leftNumber || rightNumber) { |
||||
// Scalar case, just add operation.
|
||||
const [num, query] = leftNumber ? [leftNumber, right] : [rightNumber, left]; |
||||
visQuery.operations.push({ id: opName, params: [parseInt(getString(expr, num), 10)] }); |
||||
handleExpression(expr, query, context); |
||||
} else { |
||||
// Two queries case so we create a binary query.
|
||||
visQuery.binaryQueries = visQuery.binaryQueries || []; |
||||
const binQuery = { |
||||
operator: op, |
||||
query: { |
||||
metric: '', |
||||
labels: [], |
||||
operations: [], |
||||
}, |
||||
}; |
||||
visQuery.binaryQueries.push(binQuery); |
||||
// One query is the main query, second is wrapped in the binaryQuery wrapper.
|
||||
handleExpression(expr, left, context); |
||||
handleExpression(expr, right, { |
||||
query: binQuery.query, |
||||
errors: context.errors, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Get the actual string of the expression. That is not stored in the tree so we have to get the indexes from the node |
||||
* and then based on that get it from the expression. |
||||
* @param expr |
||||
* @param node |
||||
*/ |
||||
function getString(expr: string, node: SyntaxNode | null) { |
||||
if (!node) { |
||||
return ''; |
||||
} |
||||
return returnVariables(expr.substring(node.from, node.to)); |
||||
} |
||||
|
||||
/** |
||||
* Get all nodes with type in the tree. This traverses the tree so it is safe only when you know there shouldn't be |
||||
* too much nesting but you just want to skip some of the wrappers. For example getting function args this way would |
||||
* not be safe is it would also find arguments of nested functions. |
||||
* @param expr |
||||
* @param cur |
||||
* @param type |
||||
*/ |
||||
function getAllByType(expr: string, cur: SyntaxNode, type: string): string[] { |
||||
if (cur.name === type) { |
||||
return [getString(expr, cur)]; |
||||
} |
||||
const values: string[] = []; |
||||
let pos = 0; |
||||
let child = cur.childAfter(pos); |
||||
while (child) { |
||||
values.push(...getAllByType(expr, child, type)); |
||||
pos = child.to; |
||||
child = cur.childAfter(pos); |
||||
} |
||||
return values; |
||||
} |
||||
|
||||
// Debugging function for convenience.
|
||||
// @ts-ignore
|
||||
function log(expr: string, cur?: SyntaxNode) { |
||||
const json = toJson(expr, cur); |
||||
if (!json) { |
||||
console.log('<empty>'); |
||||
return; |
||||
} |
||||
console.log(JSON.stringify(json, undefined, 2)); |
||||
} |
||||
|
||||
function toJson(expr: string, cur?: SyntaxNode) { |
||||
if (!cur) { |
||||
return undefined; |
||||
} |
||||
const treeJson: any = {}; |
||||
const name = nodeToString(expr, cur); |
||||
const children = []; |
||||
|
||||
let pos = 0; |
||||
let child = cur.childAfter(pos); |
||||
while (child) { |
||||
children.push(toJson(expr, child)); |
||||
pos = child.to; |
||||
child = cur.childAfter(pos); |
||||
} |
||||
|
||||
treeJson[name] = children; |
||||
return treeJson; |
||||
} |
||||
|
||||
function nodeToString(expr: string, node: SyntaxNode) { |
||||
return node.name + ':' + getString(expr, node); |
||||
} |
Loading…
Reference in new issue