Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions fluent-syntax/src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
175 changes: 82 additions & 93 deletions fluent-syntax/src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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]);
Expand Down Expand Up @@ -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() !== ">") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be more strict in checking the -> syntax here. I'll fix this as part of a future refactor to reduce the use of peek() and next().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I filed #320 for this.

ps.resetPeek();
return selector;
Expand Down Expand Up @@ -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();

Expand All @@ -772,24 +794,24 @@ 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) {
if (ps.currentChar === ")") {
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");
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
key = { no-caps-name() }

# ~ERROR E0008, pos 21
# ~ERROR E0008, pos 20
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
key = { BUILTIN(key: foo) }
# ~ERROR E0012, pos 21
# ~ERROR E0014, pos 21
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ~ERROR E0003, pos 8, args "}"
foo = {
bar = Bar
# ~ERROR E0014, pos 26
# ~ERROR E0028, pos 26
baz = {
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# ~ERROR E0024, pos 18
# ~ERROR E0003, pos 17, args "}"
key01 = { message[variant] }
key02 = { -term[variant] }
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ err1 =
*[1] One
[2] Two
}
# ~ERROR E0024, pos 17
# ~ERROR E0003, pos 16, args "}"

err2 =
{ -foo[bar] ->
Expand Down
8 changes: 4 additions & 4 deletions fluent-syntax/test/fixtures_behavior/variant_lists.ftl
Original file line number Diff line number Diff line change
@@ -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] {
Expand All @@ -17,15 +17,15 @@ message2 =
*[one] One
}

# ~ERROR E0014, pos 211
# ~ERROR E0028, pos 211
-term2 =
{
*[one] {
*[two] Two
}
}

# ~ERROR E0014, pos 292
# ~ERROR E0028, pos 292
-term3 =
{ $sel ->
*[one] {
Expand Down
4 changes: 2 additions & 2 deletions fluent-syntax/test/fixtures_structure/junk.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down