From 5dda93a0b113afa94736910ad006a0b4d1100cf6 Mon Sep 17 00:00:00 2001 From: Tom Robinson <tlrobinson@gmail.com> Date: Fri, 9 Dec 2016 14:49:38 -0800 Subject: [PATCH] Don't allow field names outside of aggregations --- .../src/metabase/lib/expressions/parser.js | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/frontend/src/metabase/lib/expressions/parser.js b/frontend/src/metabase/lib/expressions/parser.js index f1c0b104711..bb08ebb44cc 100644 --- a/frontend/src/metabase/lib/expressions/parser.js +++ b/frontend/src/metabase/lib/expressions/parser.js @@ -64,8 +64,8 @@ class ExpressionsParser extends Parser { let $ = this; // an expression without aggregations in it - $.RULE("expression", function (allowAggregations = false) { - return $.SUBRULE($.additionExpression, [allowAggregations]) + $.RULE("expression", function (outsideAggregation = false) { + return $.SUBRULE($.additionExpression, [outsideAggregation]) }); // an expression with aggregations in it @@ -76,11 +76,11 @@ class ExpressionsParser extends Parser { // Lowest precedence thus it is first in the rule chain // The precedence of binary expressions is determined by // how far down the Parse Tree the binary expression appears. - $.RULE("additionExpression", (allowAggregations) => { - let value = $.SUBRULE($.multiplicationExpression, [allowAggregations]); + $.RULE("additionExpression", (outsideAggregation) => { + let value = $.SUBRULE($.multiplicationExpression, [outsideAggregation]); $.MANY(() => { const op = $.CONSUME(AdditiveOperator); - const rhsVal = $.SUBRULE2($.multiplicationExpression, [allowAggregations]); + const rhsVal = $.SUBRULE2($.multiplicationExpression, [outsideAggregation]); if (Array.isArray(value) && value[0] === op.image) { value.push(rhsVal); @@ -91,11 +91,11 @@ class ExpressionsParser extends Parser { return value }); - $.RULE("multiplicationExpression", (allowAggregations) => { - let value = $.SUBRULE($.atomicExpression, [allowAggregations]); + $.RULE("multiplicationExpression", (outsideAggregation) => { + let value = $.SUBRULE($.atomicExpression, [outsideAggregation]); $.MANY(() => { const op = $.CONSUME(MultiplicativeOperator); - const rhsVal = $.SUBRULE2($.atomicExpression, [allowAggregations]); + const rhsVal = $.SUBRULE2($.atomicExpression, [outsideAggregation]); if (Array.isArray(value) && value[0] === op.image) { value.push(rhsVal); @@ -106,7 +106,7 @@ class ExpressionsParser extends Parser { return value }); - $.RULE("aggregationExpression", (allowAggregations) => { + $.RULE("aggregationExpression", (outsideAggregation) => { const agg = $.CONSUME(Aggregation).image; let value = [aggregationsMap.get(agg)] $.CONSUME(LParen); @@ -130,20 +130,22 @@ class ExpressionsParser extends Parser { return ["field-id", this.getFieldIdForName(fieldName)]; }); - $.RULE("atomicExpression", (allowAggregations) => { + $.RULE("atomicExpression", (outsideAggregation) => { return $.OR([ - {GATE: () => allowAggregations, ALT: () => $.SUBRULE($.aggregationExpression, [false]) }, - {ALT: () => $.SUBRULE($.parenthesisExpression, [allowAggregations]) }, - {ALT: () => $.SUBRULE($.fieldExpression, [allowAggregations]) }, + // aggregations not allowed inside other aggregations + {GATE: () => outsideAggregation, ALT: () => $.SUBRULE($.aggregationExpression, [false]) }, + // fields not allowed outside aggregations + {GATE: () => !outsideAggregation, ALT: () => $.SUBRULE($.fieldExpression) }, + {ALT: () => $.SUBRULE($.parenthesisExpression, [outsideAggregation]) }, {ALT: () => parseFloat($.CONSUME(NumberLiteral).image) } ], "a number or field name"); }); - $.RULE("parenthesisExpression", (allowAggregations) => { + $.RULE("parenthesisExpression", (outsideAggregation) => { let expValue; $.CONSUME(LParen); - expValue = $.SUBRULE($.expression, [allowAggregations]); + expValue = $.SUBRULE($.expression, [outsideAggregation]); $.CONSUME(RParen); return expValue @@ -210,6 +212,10 @@ export function suggest(source, { index = source.length, fields } = {}) { for (const suggestion of syntacticSuggestions) { const { nextTokenType, ruleStack } = suggestion; + // no nesting of aggregations or field references outside of aggregations + // we have a predicate in the grammar to prevent nested aggregations but chevrotain + // doesn't support predicates in content-assist mode, so we need this extra check + const outsideAggregation = ruleStack.slice(0, -1).indexOf("aggregationExpression") < 0; if (nextTokenType === MultiplicativeOperator || nextTokenType === AdditiveOperator) { let tokens = getSubTokenTypes(nextTokenType); @@ -238,25 +244,23 @@ export function suggest(source, { index = source.length, fields } = {}) { postfixTrim: /^\s+/ }); } else if (nextTokenType === Identifier || nextTokenType === StringLiteral) { - finalSuggestions.push(...fields.map(field => ({ - type: "fields", - name: field.display_name, - text: /^\w+$/.test(field.display_name) ? - field.display_name + " " : // need a space to terminate identifier - JSON.stringify(field.display_name), - prefixTrim: /\w+$/, - postfixTrim: /^\w+\s*/ - }))) + if (!outsideAggregation) { + finalSuggestions.push(...fields.map(field => ({ + type: "fields", + name: field.display_name, + text: /^\w+$/.test(field.display_name) ? + field.display_name + " " : // need a space to terminate identifier + JSON.stringify(field.display_name), + prefixTrim: /\w+$/, + postfixTrim: /^\w+\s*/ + }))) + } } else if (nextTokenType === Aggregation) { - // no nesting of aggregations - // we have a predicate in the grammar to prevent nested aggregations but chevrotain - // doesn't support predicates in content-assist mode, so we need this extra check - if (ruleStack.slice(0, -1).indexOf("aggregationExpression") < 0) { + if (outsideAggregation) { let tokens = getSubTokenTypes(nextTokenType); finalSuggestions.push(...tokens.map(token => { let text = getTokenSource(token); let arity = AGGREGATION_ARITY.get(text); - console.log("arity", arity, text) return { type: "aggregations", name: text, -- GitLab