diff --git a/OpenRA.Game/Support/VariableExpression.cs b/OpenRA.Game/Support/VariableExpression.cs index b43dd8dac7..adb36fc2c4 100644 --- a/OpenRA.Game/Support/VariableExpression.cs +++ b/OpenRA.Game/Support/VariableExpression.cs @@ -160,6 +160,7 @@ namespace OpenRA.Support public readonly string Symbol; public readonly Precedence Precedence; public readonly Sides OperandSides; + public readonly Sides WhitespaceSides; public readonly Associativity Associativity; public readonly Grouping Opens; public readonly Grouping Closes; @@ -171,6 +172,21 @@ namespace OpenRA.Support Symbol = symbol; Precedence = precedence; OperandSides = operandSides; + WhitespaceSides = Sides.None; + Associativity = associativity; + Opens = opens; + Closes = closes; + } + + public TokenTypeInfo(string symbol, Precedence precedence, Sides operandSides, + Sides whitespaceSides, + Associativity associativity = Associativity.Left, + Grouping opens = Grouping.None, Grouping closes = Grouping.None) + { + Symbol = symbol; + Precedence = precedence; + OperandSides = operandSides; + WhitespaceSides = whitespaceSides; Associativity = associativity; Opens = opens; Closes = closes; @@ -181,6 +197,7 @@ namespace OpenRA.Support { Symbol = symbol; Precedence = precedence; + WhitespaceSides = Sides.None; OperandSides = opens == Grouping.None ? (closes == Grouping.None ? Sides.None : Sides.Left) : @@ -228,43 +245,43 @@ namespace OpenRA.Support yield return new TokenTypeInfo("-", Precedence.Unary, Sides.Right, Associativity.Right); continue; case TokenType.And: - yield return new TokenTypeInfo("&&", Precedence.And, Sides.Both); + yield return new TokenTypeInfo("&&", Precedence.And, Sides.Both, Sides.Both); continue; case TokenType.Or: - yield return new TokenTypeInfo("||", Precedence.Or, Sides.Both); + yield return new TokenTypeInfo("||", Precedence.Or, Sides.Both, Sides.Both); continue; case TokenType.Equals: - yield return new TokenTypeInfo("==", Precedence.Equality, Sides.Both); + yield return new TokenTypeInfo("==", Precedence.Equality, Sides.Both, Sides.Both); continue; case TokenType.NotEquals: - yield return new TokenTypeInfo("!=", Precedence.Equality, Sides.Both); + yield return new TokenTypeInfo("!=", Precedence.Equality, Sides.Both, Sides.Both); continue; case TokenType.LessThan: - yield return new TokenTypeInfo("<", Precedence.Relation, Sides.Both); + yield return new TokenTypeInfo("<", Precedence.Relation, Sides.Both, Sides.Both); continue; case TokenType.LessThanOrEqual: - yield return new TokenTypeInfo("<=", Precedence.Relation, Sides.Both); + yield return new TokenTypeInfo("<=", Precedence.Relation, Sides.Both, Sides.Both); continue; case TokenType.GreaterThan: - yield return new TokenTypeInfo(">", Precedence.Relation, Sides.Both); + yield return new TokenTypeInfo(">", Precedence.Relation, Sides.Both, Sides.Both); continue; case TokenType.GreaterThanOrEqual: - yield return new TokenTypeInfo(">=", Precedence.Relation, Sides.Both); + yield return new TokenTypeInfo(">=", Precedence.Relation, Sides.Both, Sides.Both); continue; case TokenType.Add: - yield return new TokenTypeInfo("+", Precedence.Addition, Sides.Both); + yield return new TokenTypeInfo("+", Precedence.Addition, Sides.Both, Sides.Both); continue; case TokenType.Subtract: - yield return new TokenTypeInfo("-", Precedence.Addition, Sides.Both); + yield return new TokenTypeInfo("-", Precedence.Addition, Sides.Both, Sides.Both); continue; case TokenType.Multiply: - yield return new TokenTypeInfo("*", Precedence.Multiplication, Sides.Both); + yield return new TokenTypeInfo("*", Precedence.Multiplication, Sides.Both, Sides.Both); continue; case TokenType.Divide: - yield return new TokenTypeInfo("/", Precedence.Multiplication, Sides.Both); + yield return new TokenTypeInfo("/", Precedence.Multiplication, Sides.Both, Sides.Both); continue; case TokenType.Modulo: - yield return new TokenTypeInfo("%", Precedence.Multiplication, Sides.Both); + yield return new TokenTypeInfo("%", Precedence.Multiplication, Sides.Both, Sides.Both); continue; } @@ -285,6 +302,21 @@ namespace OpenRA.Support return type == TokenType.Invalid || HasRightOperand(type); } + static bool RequiresWhitespaceAfter(TokenType type) + { + return ((int)TokenTypeInfos[(int)type].WhitespaceSides & (int)Sides.Right) != 0; + } + + static bool RequiresWhitespaceBefore(TokenType type) + { + return ((int)TokenTypeInfos[(int)type].WhitespaceSides & (int)Sides.Left) != 0; + } + + static string GetTokenSymbol(TokenType type) + { + return TokenTypeInfos[(int)type].Symbol; + } + class Token { public readonly TokenType Type; @@ -484,16 +516,30 @@ namespace OpenRA.Support if (i == expression.Length) return null; - // Ignore whitespace - while (CharClassOf(expression[i]) == CharClass.Whitespace) + // Check and eat whitespace + var whitespaceBefore = false; + if (CharClassOf(expression[i]) == CharClass.Whitespace) { - if (++i == expression.Length) - return null; + whitespaceBefore = true; + while (CharClassOf(expression[i]) == CharClass.Whitespace) + { + if (++i == expression.Length) + return null; + } } + else if (lastType == TokenType.Invalid) + whitespaceBefore = true; + else if (RequiresWhitespaceAfter(lastType)) + throw new InvalidDataException("Missing whitespace at index {0}, after `{1}` operator." + .F(i, GetTokenSymbol(lastType))); var start = i; var type = GetNextType(expression, ref i, lastType); + if (!whitespaceBefore && RequiresWhitespaceBefore(type)) + throw new InvalidDataException("Missing whitespace at index {0}, before `{1}` operator." + .F(i, GetTokenSymbol(type))); + switch (type) { case TokenType.Number: diff --git a/OpenRA.Test/OpenRA.Game/VariableExpressionTest.cs b/OpenRA.Test/OpenRA.Game/VariableExpressionTest.cs index 0b9da2243a..55343ad8b8 100644 --- a/OpenRA.Test/OpenRA.Game/VariableExpressionTest.cs +++ b/OpenRA.Test/OpenRA.Game/VariableExpressionTest.cs @@ -304,7 +304,6 @@ namespace OpenRA.Test AssertValue("-t-1", -7); AssertValue("t - 1", 4); AssertValue("-1", -1); - AssertValue("6- 3", 3); } [TestCase(TestName = "Parenthesis and mixed operations")] @@ -353,7 +352,8 @@ namespace OpenRA.Test AssertParseFailure("-", "Missing value or sub-expression at end for `-` operator"); AssertParseFailure("-1-1", "Missing binary operation before `-1` at index 2"); AssertParseFailure("5-1", "Missing binary operation before `-1` at index 1"); - AssertParseFailure("6 -3", "Missing binary operation before `-3` at index 2"); + AssertParseFailure("6 -1", "Missing binary operation before `-1` at index 2"); + AssertParseFailure("t -1", "Missing binary operation before `-1` at index 2"); } [TestCase(TestName = "Test mixed charaters at end of identifier parser errors")] @@ -369,6 +369,98 @@ namespace OpenRA.Test AssertParseFailure("t$", "Invalid identifier end character at index 1 for `t$`"); } + [TestCase(TestName = "Test binary operator whitespace parser errors")] + public void TestParseSpacedBinaryOperatorErrors() + { + // `t-1` is valid variable name and `t- 1` starts with an invalid variable name. + // `6 -1`, `6-1`, `t -1` contain `-1` and are missing a binary operator. + AssertParseFailure("6- 1", "Missing whitespace at index 2, before `-` operator."); + + AssertParseFailure("6+ 1", "Missing whitespace at index 2, before `+` operator."); + AssertParseFailure("t+ 1", "Missing whitespace at index 2, before `+` operator."); + AssertParseFailure("6 +1", "Missing whitespace at index 3, after `+` operator."); + AssertParseFailure("t +1", "Missing whitespace at index 3, after `+` operator."); + AssertParseFailure("6+1", "Missing whitespace at index 2, before `+` operator."); + AssertParseFailure("t+1", "Missing whitespace at index 2, before `+` operator."); + + AssertParseFailure("6* 1", "Missing whitespace at index 2, before `*` operator."); + AssertParseFailure("t* 1", "Missing whitespace at index 2, before `*` operator."); + AssertParseFailure("6 *1", "Missing whitespace at index 3, after `*` operator."); + AssertParseFailure("t *1", "Missing whitespace at index 3, after `*` operator."); + AssertParseFailure("6*1", "Missing whitespace at index 2, before `*` operator."); + AssertParseFailure("t*1", "Missing whitespace at index 2, before `*` operator."); + + AssertParseFailure("6/ 1", "Missing whitespace at index 2, before `/` operator."); + AssertParseFailure("t/ 1", "Missing whitespace at index 2, before `/` operator."); + AssertParseFailure("6 /1", "Missing whitespace at index 3, after `/` operator."); + AssertParseFailure("t /1", "Missing whitespace at index 3, after `/` operator."); + AssertParseFailure("6/1", "Missing whitespace at index 2, before `/` operator."); + AssertParseFailure("t/1", "Missing whitespace at index 2, before `/` operator."); + + AssertParseFailure("6% 1", "Missing whitespace at index 2, before `%` operator."); + AssertParseFailure("t% 1", "Missing whitespace at index 2, before `%` operator."); + AssertParseFailure("6 %1", "Missing whitespace at index 3, after `%` operator."); + AssertParseFailure("t %1", "Missing whitespace at index 3, after `%` operator."); + AssertParseFailure("6%1", "Missing whitespace at index 2, before `%` operator."); + AssertParseFailure("t%1", "Missing whitespace at index 2, before `%` operator."); + + AssertParseFailure("6< 1", "Missing whitespace at index 2, before `<` operator."); + AssertParseFailure("t< 1", "Missing whitespace at index 2, before `<` operator."); + AssertParseFailure("6 <1", "Missing whitespace at index 3, after `<` operator."); + AssertParseFailure("t <1", "Missing whitespace at index 3, after `<` operator."); + AssertParseFailure("6<1", "Missing whitespace at index 2, before `<` operator."); + AssertParseFailure("t<1", "Missing whitespace at index 2, before `<` operator."); + + AssertParseFailure("6> 1", "Missing whitespace at index 2, before `>` operator."); + AssertParseFailure("t> 1", "Missing whitespace at index 2, before `>` operator."); + AssertParseFailure("6 >1", "Missing whitespace at index 3, after `>` operator."); + AssertParseFailure("t >1", "Missing whitespace at index 3, after `>` operator."); + AssertParseFailure("6>1", "Missing whitespace at index 2, before `>` operator."); + AssertParseFailure("t>1", "Missing whitespace at index 2, before `>` operator."); + + AssertParseFailure("6&& 1", "Missing whitespace at index 3, before `&&` operator."); + AssertParseFailure("t&& 1", "Missing whitespace at index 3, before `&&` operator."); + AssertParseFailure("6 &&1", "Missing whitespace at index 4, after `&&` operator."); + AssertParseFailure("t &&1", "Missing whitespace at index 4, after `&&` operator."); + AssertParseFailure("6&&1", "Missing whitespace at index 3, before `&&` operator."); + AssertParseFailure("t&&1", "Missing whitespace at index 3, before `&&` operator."); + + AssertParseFailure("6|| 1", "Missing whitespace at index 3, before `||` operator."); + AssertParseFailure("t|| 1", "Missing whitespace at index 3, before `||` operator."); + AssertParseFailure("6 ||1", "Missing whitespace at index 4, after `||` operator."); + AssertParseFailure("t ||1", "Missing whitespace at index 4, after `||` operator."); + AssertParseFailure("6||1", "Missing whitespace at index 3, before `||` operator."); + AssertParseFailure("t||1", "Missing whitespace at index 3, before `||` operator."); + + AssertParseFailure("6== 1", "Missing whitespace at index 3, before `==` operator."); + AssertParseFailure("t== 1", "Missing whitespace at index 3, before `==` operator."); + AssertParseFailure("6 ==1", "Missing whitespace at index 4, after `==` operator."); + AssertParseFailure("t ==1", "Missing whitespace at index 4, after `==` operator."); + AssertParseFailure("6==1", "Missing whitespace at index 3, before `==` operator."); + AssertParseFailure("t==1", "Missing whitespace at index 3, before `==` operator."); + + AssertParseFailure("6!= 1", "Missing whitespace at index 3, before `!=` operator."); + AssertParseFailure("t!= 1", "Missing whitespace at index 3, before `!=` operator."); + AssertParseFailure("6 !=1", "Missing whitespace at index 4, after `!=` operator."); + AssertParseFailure("t !=1", "Missing whitespace at index 4, after `!=` operator."); + AssertParseFailure("6!=1", "Missing whitespace at index 3, before `!=` operator."); + AssertParseFailure("t!=1", "Missing whitespace at index 3, before `!=` operator."); + + AssertParseFailure("6<= 1", "Missing whitespace at index 3, before `<=` operator."); + AssertParseFailure("t<= 1", "Missing whitespace at index 3, before `<=` operator."); + AssertParseFailure("6 <=1", "Missing whitespace at index 4, after `<=` operator."); + AssertParseFailure("t <=1", "Missing whitespace at index 4, after `<=` operator."); + AssertParseFailure("6<=1", "Missing whitespace at index 3, before `<=` operator."); + AssertParseFailure("t<=1", "Missing whitespace at index 3, before `<=` operator."); + + AssertParseFailure("6>= 1", "Missing whitespace at index 3, before `>=` operator."); + AssertParseFailure("t>= 1", "Missing whitespace at index 3, before `>=` operator."); + AssertParseFailure("6 >=1", "Missing whitespace at index 4, after `>=` operator."); + AssertParseFailure("t >=1", "Missing whitespace at index 4, after `>=` operator."); + AssertParseFailure("6>=1", "Missing whitespace at index 3, before `>=` operator."); + AssertParseFailure("t>=1", "Missing whitespace at index 3, before `>=` operator."); + } + [TestCase(TestName = "Undefined symbols are treated as `false` (0) values")] public void TestUndefinedSymbols() {