diff --git a/fluent-syntax/src/errors.js b/fluent-syntax/src/errors.js index 028ee6a4a..ee9a1dd00 100644 --- a/fluent-syntax/src/errors.js +++ b/fluent-syntax/src/errors.js @@ -74,6 +74,8 @@ function getErrorMessage(code, args) { } case "E0027": return "Unbalanced closing brace in TextElement."; + case "E0028": + return "Expected an inline expression"; default: return code; } diff --git a/fluent-syntax/src/parser.js b/fluent-syntax/src/parser.js index 2fcbd5b29..0443969e2 100644 --- a/fluent-syntax/src/parser.js +++ b/fluent-syntax/src/parser.js @@ -17,7 +17,7 @@ function withSpan(fn) { const start = ps.index; const node = fn.call(this, ps, ...args); - // Don't re-add the span if the node already has it. This may happen when + // Don't re-add the span if the node already has it. This may happen when // one decorated function calls another decorated function. if (node.span) { return node; @@ -41,7 +41,8 @@ export default class FluentParser { "getComment", "getMessage", "getTerm", "getAttribute", "getIdentifier", "getVariant", "getNumber", "getPattern", "getVariantList", "getTextElement", "getPlaceable", "getExpression", - "getSelectorExpression", "getCallArg", "getString", "getLiteral" + "getInlineExpression", "getCallArgument", "getString", + "getSimpleExpression", "getLiteral" ]; for (const name of methodNames) { this[name] = withSpan(this[name]); @@ -639,11 +640,10 @@ export default class FluentParser { } getExpression(ps) { - const selector = this.getSelectorExpression(ps); + const selector = this.getInlineExpression(ps); ps.skipBlank(); if (ps.currentChar === "-") { - if (ps.peek() !== ">") { ps.resetPeek(); return selector; @@ -691,73 +691,95 @@ export default class FluentParser { return selector; } - getSelectorExpression(ps) { + getInlineExpression(ps) { if (ps.currentChar === "{") { return this.getPlaceable(ps); } - let selector = this.getLiteral(ps); - switch (selector.type) { - case "StringLiteral": + let expr = this.getSimpleExpression(ps); + switch (expr.type) { case "NumberLiteral": + case "StringLiteral": case "VariableReference": - return selector; - } + return expr; + case "MessageReference": { + if (ps.currentChar === ".") { + ps.next(); + const attr = this.getIdentifier(ps); + return new AST.AttributeExpression(expr, attr); + } - if (ps.currentChar === "[") { - ps.next(); + if (ps.currentChar === "(") { + // It's a Function. Ensure it's all upper-case. + if (!/^[A-Z][A-Z_?-]*$/.test(expr.id.name)) { + throw new ParseError("E0008"); + } - if (selector.type === "MessageReference") { - throw new ParseError("E0024"); + const func = new AST.FunctionReference(expr.id); + if (this.withSpans) { + func.addSpan(expr.span.start, expr.span.end); + } + return new AST.CallExpression(func, ...this.getCallArguments(ps)); + } + + return expr; } + case "TermReference": { + if (ps.currentChar === "[") { + ps.next(); + const key = this.getVariantKey(ps); + ps.expectChar("]"); + return new AST.VariantExpression(expr, key); + } - const key = this.getVariantKey(ps); - ps.expectChar("]"); - return new AST.VariantExpression(selector, key); - } + if (ps.currentChar === ".") { + ps.next(); + const attr = this.getIdentifier(ps); + expr = new AST.AttributeExpression(expr, attr); + } - if (ps.currentChar === ".") { - ps.next(); - const attr = this.getIdentifier(ps); - selector = new AST.AttributeExpression(selector, attr); + if (ps.currentChar === "(") { + return new AST.CallExpression(expr, ...this.getCallArguments(ps)); + } + + return expr; + } + default: + throw new ParseError("E0028"); } + } - if (ps.currentChar === "(") { - ps.next(); + getSimpleExpression(ps) { + if (ps.isNumberStart()) { + return this.getNumber(ps); + } - if (selector.type === "MessageReference") { - if (/^[A-Z][A-Z_?-]*$/.test(selector.id.name)) { - // The callee is a Function. - var func = new AST.FunctionReference(selector.id); - if (this.withSpans) { - func.addSpan(selector.span.start, selector.span.end); - } - } else { - // Messages can't be callees. - throw new ParseError("E0008"); - } - } + if (ps.currentChar === '"') { + return this.getString(ps); + } - if (selector.type === "AttributeExpression" - && selector.ref.type === "MessageReference") { - throw new ParseError("E0008"); - } + if (ps.currentChar === "$") { + ps.next(); + const id = this.getIdentifier(ps); + return new AST.VariableReference(id); + } - const args = this.getCallArgs(ps); - ps.expectChar(")"); + if (ps.currentChar === "-") { + ps.next(); + const id = this.getIdentifier(ps); + return new AST.TermReference(id); + } - return new AST.CallExpression( - func || selector, - args.positional, - args.named, - ); + if (ps.isIdentifierStart()) { + const id = this.getIdentifier(ps); + return new AST.MessageReference(id); } - return selector; + throw new ParseError("E0028"); } - getCallArg(ps) { - const exp = this.getSelectorExpression(ps); + getCallArgument(ps) { + const exp = this.getInlineExpression(ps); ps.skipBlank(); @@ -772,16 +794,16 @@ export default class FluentParser { ps.next(); ps.skipBlank(); - const val = this.getArgVal(ps); - - return new AST.NamedArgument(exp.id, val); + const value = this.getLiteral(ps); + return new AST.NamedArgument(exp.id, value); } - getCallArgs(ps) { + getCallArguments(ps) { const positional = []; const named = []; const argumentNames = new Set(); + ps.expectChar("("); ps.skipBlank(); while (true) { @@ -789,7 +811,7 @@ export default class FluentParser { break; } - const arg = this.getCallArg(ps); + const arg = this.getCallArgument(ps); if (arg.type === "NamedArgument") { if (argumentNames.has(arg.name.name)) { throw new ParseError("E0022"); @@ -808,23 +830,13 @@ export default class FluentParser { ps.next(); ps.skipBlank(); continue; - } else { - break; } - } - return { - positional, - named - }; - } - getArgVal(ps) { - if (ps.isNumberStart()) { - return this.getNumber(ps); - } else if (ps.currentChar === '"') { - return this.getString(ps); + break; } - throw new ParseError("E0012"); + + ps.expectChar(")"); + return [positional, named]; } getString(ps) { @@ -855,34 +867,11 @@ export default class FluentParser { } getLiteral(ps) { - const ch = ps.currentChar; - - if (ch === EOF) { - throw new ParseError("E0014"); - } - - if (ch === "$") { - ps.next(); - const id = this.getIdentifier(ps); - return new AST.VariableReference(id); - } - - if (ps.isIdentifierStart()) { - const id = this.getIdentifier(ps); - return new AST.MessageReference(id); - } - if (ps.isNumberStart()) { return this.getNumber(ps); } - if (ch === "-") { - ps.next(); - const id = this.getIdentifier(ps); - return new AST.TermReference(id); - } - - if (ch === '"') { + if (ps.currentChar === '"') { return this.getString(ps); } diff --git a/fluent-syntax/test/fixtures_behavior/call_expression_with_bad_id.ftl b/fluent-syntax/test/fixtures_behavior/call_expression_with_bad_id.ftl index e82146965..9bc2bbbc1 100644 --- a/fluent-syntax/test/fixtures_behavior/call_expression_with_bad_id.ftl +++ b/fluent-syntax/test/fixtures_behavior/call_expression_with_bad_id.ftl @@ -1,3 +1,3 @@ key = { no-caps-name() } -# ~ERROR E0008, pos 21 +# ~ERROR E0008, pos 20 diff --git a/fluent-syntax/test/fixtures_behavior/call_expression_with_wrong_value_type.ftl b/fluent-syntax/test/fixtures_behavior/call_expression_with_wrong_value_type.ftl index 4a8f9604c..b0be5cdeb 100644 --- a/fluent-syntax/test/fixtures_behavior/call_expression_with_wrong_value_type.ftl +++ b/fluent-syntax/test/fixtures_behavior/call_expression_with_wrong_value_type.ftl @@ -1,2 +1,2 @@ key = { BUILTIN(key: foo) } -# ~ERROR E0012, pos 21 +# ~ERROR E0014, pos 21 diff --git a/fluent-syntax/test/fixtures_behavior/unclosed_empty_placeable_error.ftl b/fluent-syntax/test/fixtures_behavior/unclosed_empty_placeable_error.ftl index 5f812a949..e29ced2b5 100644 --- a/fluent-syntax/test/fixtures_behavior/unclosed_empty_placeable_error.ftl +++ b/fluent-syntax/test/fixtures_behavior/unclosed_empty_placeable_error.ftl @@ -1,5 +1,5 @@ # ~ERROR E0003, pos 8, args "}" foo = { bar = Bar -# ~ERROR E0014, pos 26 +# ~ERROR E0028, pos 26 baz = { diff --git a/fluent-syntax/test/fixtures_behavior/variant_expression_as_placeable.ftl b/fluent-syntax/test/fixtures_behavior/variant_expression_as_placeable.ftl index 1866ef64b..6b2796ffa 100644 --- a/fluent-syntax/test/fixtures_behavior/variant_expression_as_placeable.ftl +++ b/fluent-syntax/test/fixtures_behavior/variant_expression_as_placeable.ftl @@ -1,3 +1,3 @@ -# ~ERROR E0024, pos 18 +# ~ERROR E0003, pos 17, args "}" key01 = { message[variant] } key02 = { -term[variant] } diff --git a/fluent-syntax/test/fixtures_behavior/variant_expression_as_selector.ftl b/fluent-syntax/test/fixtures_behavior/variant_expression_as_selector.ftl index 88d6b5a2b..946c1c831 100644 --- a/fluent-syntax/test/fixtures_behavior/variant_expression_as_selector.ftl +++ b/fluent-syntax/test/fixtures_behavior/variant_expression_as_selector.ftl @@ -3,7 +3,7 @@ err1 = *[1] One [2] Two } -# ~ERROR E0024, pos 17 +# ~ERROR E0003, pos 16, args "}" err2 = { -foo[bar] -> diff --git a/fluent-syntax/test/fixtures_behavior/variant_lists.ftl b/fluent-syntax/test/fixtures_behavior/variant_lists.ftl index 21ffb8f1d..681751de1 100644 --- a/fluent-syntax/test/fixtures_behavior/variant_lists.ftl +++ b/fluent-syntax/test/fixtures_behavior/variant_lists.ftl @@ -1,10 +1,10 @@ -# ~ERROR E0014, pos 25 +# ~ERROR E0028, pos 25 message1 = { *[one] One } -# ~ERROR E0014, pos 97 +# ~ERROR E0028, pos 97 message2 = { $sel -> *[one] { @@ -17,7 +17,7 @@ message2 = *[one] One } -# ~ERROR E0014, pos 211 +# ~ERROR E0028, pos 211 -term2 = { *[one] { @@ -25,7 +25,7 @@ message2 = } } -# ~ERROR E0014, pos 292 +# ~ERROR E0028, pos 292 -term3 = { $sel -> *[one] { diff --git a/fluent-syntax/test/fixtures_structure/junk.json b/fluent-syntax/test/fixtures_structure/junk.json index 47d5eb153..df1f862df 100644 --- a/fluent-syntax/test/fixtures_structure/junk.json +++ b/fluent-syntax/test/fixtures_structure/junk.json @@ -212,9 +212,9 @@ "annotations": [ { "type": "Annotation", - "code": "E0014", + "code": "E0028", "args": [], - "message": "Expected literal", + "message": "Expected an inline expression", "span": { "type": "Span", "start": 153,