diff --git a/fluent.runtime/tests/format/test_functions.py b/fluent.runtime/tests/format/test_functions.py index 2135b3f4..2b16cc19 100644 --- a/fluent.runtime/tests/format/test_functions.py +++ b/fluent.runtime/tests/format/test_functions.py @@ -143,6 +143,7 @@ def my_function(arg, kwarg1=None, kwarg2="default"): pass-kwarg1 = { MYFUNC("a", kwarg1: 1) } pass-kwarg2 = { MYFUNC("a", kwarg2: "other") } pass-kwargs = { MYFUNC("a", kwarg1: 1, kwarg2: "other") } + pass-user-kwarg = { MYFUNC("a", kwarg1: $foo) } pass-user-arg = { MYFUNC($arg) } """ ) @@ -170,6 +171,20 @@ def test_pass_kwargs(self, args_passed, bundle): assert args_passed == [("a", 1, "other")] assert len(errs) == 0 + def test_pass_user_kwarg(self, args_passed, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("pass-user-kwarg").value, {"foo": 42} + ) + assert args_passed == [("a", 42, "default")] + assert len(errs) == 0 + + def test_missing_kwarg(self, args_passed, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("pass-user-kwarg").value, {} + ) + assert args_passed == [("a", FluentNone("foo"), "default")] + assert len(errs) == 1 + def test_missing_arg(self, args_passed, bundle): val, errs = bundle.format_pattern(bundle.get_message("pass-user-arg").value, {}) assert args_passed == [(FluentNone("arg"), None, "default")] diff --git a/fluent.runtime/tests/format/test_parameterized_terms.py b/fluent.runtime/tests/format/test_parameterized_terms.py index 5f7c5d96..64b53695 100644 --- a/fluent.runtime/tests/format/test_parameterized_terms.py +++ b/fluent.runtime/tests/format/test_parameterized_terms.py @@ -23,6 +23,7 @@ def bundle(self): thing-with-arg = { -thing(article: "indefinite") } thing-positional-arg = { -thing("foo") } thing-fallback = { -thing(article: "somethingelse") } + thing-variable-arg = { -thing(article: $art) } bad-term = { -missing() } """ ) @@ -65,6 +66,13 @@ def test_fallback(self, bundle): assert val == "the thing" assert errs == [] + def test_variable_named_arg(self, bundle): + val, errs = bundle.format_pattern( + bundle.get_message("thing-variable-arg").value, {"art": "indefinite"} + ) + assert val == "a thing" + assert errs == [] + def test_no_implicit_access_to_external_args(self, bundle): # The '-thing' term should not get passed article="indefinite" val, errs = bundle.format_pattern( diff --git a/fluent.syntax/fluent/syntax/ast.py b/fluent.syntax/fluent/syntax/ast.py index c13ee1c7..fde5fcae 100644 --- a/fluent.syntax/fluent/syntax/ast.py +++ b/fluent.syntax/fluent/syntax/ast.py @@ -327,7 +327,7 @@ class NamedArgument(SyntaxNode): def __init__( self, name: "Identifier", - value: Union[NumberLiteral, StringLiteral], + value: Union[NumberLiteral, StringLiteral, VariableReference], **kwargs: Any ): super().__init__(**kwargs) diff --git a/fluent.syntax/fluent/syntax/errors.py b/fluent.syntax/fluent/syntax/errors.py index baa98113..ce9cf505 100644 --- a/fluent.syntax/fluent/syntax/errors.py +++ b/fluent.syntax/fluent/syntax/errors.py @@ -38,7 +38,7 @@ def get_error_message(code: str, args: tuple[Union[str, None], ...]) -> str: if code == "E0013": return "Expected variant key" if code == "E0014": - return "Expected literal" + return "Expected literal or variable reference" if code == "E0015": return "Only one variant can be marked as default (*)" if code == "E0016": diff --git a/fluent.syntax/fluent/syntax/parser.py b/fluent.syntax/fluent/syntax/parser.py index 5888c448..0eb81c43 100644 --- a/fluent.syntax/fluent/syntax/parser.py +++ b/fluent.syntax/fluent/syntax/parser.py @@ -653,7 +653,7 @@ def get_call_argument( ps.next() ps.skip_blank() - value = self.get_literal(ps) + value = self.get_named_argument_value(ps) return ast.NamedArgument(exp.id, value) raise ParseError("E0009") @@ -717,11 +717,15 @@ def get_string(self, ps: FluentParserStream) -> ast.StringLiteral: return ast.StringLiteral(value) @with_span - def get_literal( + def get_named_argument_value( self, ps: FluentParserStream - ) -> Union[ast.NumberLiteral, ast.StringLiteral]: + ) -> Union[ast.NumberLiteral, ast.StringLiteral, ast.VariableReference]: if ps.is_number_start(): return self.get_number(ps) if ps.current_char == '"': return self.get_string(ps) + if ps.current_char == "$": + ps.next() + id = self.get_identifier(ps) + return ast.VariableReference(id) raise ParseError("E0014") diff --git a/fluent.syntax/tests/syntax/fixtures_reference/call_expressions.ftl b/fluent.syntax/tests/syntax/fixtures_reference/call_expressions.ftl index 77c2188a..e9dfe710 100644 --- a/fluent.syntax/tests/syntax/fixtures_reference/call_expressions.ftl +++ b/fluent.syntax/tests/syntax/fixtures_reference/call_expressions.ftl @@ -19,6 +19,7 @@ positional-args = {FUN(1, "a", msg)} named-args = {FUN(x: 1, y: "Y")} dense-named-args = {FUN(x:1, y:"Y")} mixed-args = {FUN(1, "a", msg, x: 1, y: "Y")} +variable-args = {FUN($foo, arg: $bar)} # ERROR Positional arg must not follow keyword args shuffled-args = {FUN(1, x: 1, "a", y: "Y", msg)} @@ -80,11 +81,11 @@ unindented-closing-paren = {FUN( one-argument = {FUN(1,)} many-arguments = {FUN(1, 2, 3,)} inline-sparse-args = {FUN( 1, 2, 3, )} -mulitline-args = {FUN( +multiline-args = {FUN( 1, 2, )} -mulitline-sparse-args = {FUN( +multiline-sparse-args = {FUN( 1 , diff --git a/fluent.syntax/tests/syntax/fixtures_reference/call_expressions.json b/fluent.syntax/tests/syntax/fixtures_reference/call_expressions.json index 62671172..25e566e7 100644 --- a/fluent.syntax/tests/syntax/fixtures_reference/call_expressions.json +++ b/fluent.syntax/tests/syntax/fixtures_reference/call_expressions.json @@ -351,6 +351,58 @@ "attributes": [], "comment": null }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "variable-args" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "FunctionReference", + "id": { + "type": "Identifier", + "name": "FUN" + }, + "arguments": { + "type": "CallArguments", + "positional": [ + { + "type": "VariableReference", + "id": { + "type": "Identifier", + "name": "foo" + } + } + ], + "named": [ + { + "type": "NamedArgument", + "name": { + "type": "Identifier", + "name": "arg" + }, + "value": { + "type": "VariableReference", + "id": { + "type": "Identifier", + "name": "bar" + } + } + } + ] + } + } + } + ] + }, + "attributes": [], + "comment": null + }, { "type": "Comment", "content": "ERROR Positional arg must not follow keyword args" @@ -1022,7 +1074,7 @@ "type": "Message", "id": { "type": "Identifier", - "name": "mulitline-args" + "name": "multiline-args" }, "value": { "type": "Pattern", @@ -1060,7 +1112,7 @@ "type": "Message", "id": { "type": "Identifier", - "name": "mulitline-sparse-args" + "name": "multiline-sparse-args" }, "value": { "type": "Pattern", diff --git a/fluent.syntax/tests/syntax/fixtures_reference/term_parameters.ftl b/fluent.syntax/tests/syntax/fixtures_reference/term_parameters.ftl index 600c12c9..02aff9a5 100644 --- a/fluent.syntax/tests/syntax/fixtures_reference/term_parameters.ftl +++ b/fluent.syntax/tests/syntax/fixtures_reference/term_parameters.ftl @@ -6,3 +6,4 @@ key01 = { -term } key02 = { -term () } key03 = { -term(arg: 1) } key04 = { -term("positional", narg1: 1, narg2: 2) } +key05 = { -term(arg: $foo) } diff --git a/fluent.syntax/tests/syntax/fixtures_reference/term_parameters.json b/fluent.syntax/tests/syntax/fixtures_reference/term_parameters.json index 18d97cd1..59bbce03 100644 --- a/fluent.syntax/tests/syntax/fixtures_reference/term_parameters.json +++ b/fluent.syntax/tests/syntax/fixtures_reference/term_parameters.json @@ -202,6 +202,51 @@ }, "attributes": [], "comment": null + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "key05" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "TermReference", + "id": { + "type": "Identifier", + "name": "term" + }, + "attribute": null, + "arguments": { + "type": "CallArguments", + "positional": [], + "named": [ + { + "type": "NamedArgument", + "name": { + "type": "Identifier", + "name": "arg" + }, + "value": { + "type": "VariableReference", + "id": { + "type": "Identifier", + "name": "foo" + } + } + } + ] + } + } + } + ] + }, + "attributes": [], + "comment": null } ] } diff --git a/fluent.syntax/tests/syntax/fixtures_structure/call_expression_errors.json b/fluent.syntax/tests/syntax/fixtures_structure/call_expression_errors.json index 3710025d..91898b17 100644 --- a/fluent.syntax/tests/syntax/fixtures_structure/call_expression_errors.json +++ b/fluent.syntax/tests/syntax/fixtures_structure/call_expression_errors.json @@ -52,7 +52,7 @@ "type": "Annotation", "code": "E0014", "arguments": [], - "message": "Expected literal", + "message": "Expected literal or variable reference", "span": { "type": "Span", "start": 80,