private ScriptExpression ParseVariable() { var currentToken = Current; var currentSpan = CurrentSpan; var endSpan = currentSpan; var text = GetAsText(currentToken); // Return ScriptLiteral for null, true, false // Return ScriptAnonymousFunction switch (text) { case "null": var nullValue = Open <ScriptLiteral>(); NextToken(); return(Close(nullValue)); case "true": var trueValue = Open <ScriptLiteral>(); trueValue.Value = true; NextToken(); return(Close(trueValue)); case "false": var falseValue = Open <ScriptLiteral>(); falseValue.Value = false; NextToken(); return(Close(falseValue)); case "do": var functionExp = Open <ScriptAnonymousFunction>(); functionExp.Function = ParseFunctionStatement(true); var func = Close(functionExp); return(func); case "this": if (!_isLiquid) { var thisExp = Open <ScriptThisExpression>(); NextToken(); return(Close(thisExp)); } break; } // Keeps trivia before this token List <ScriptTrivia> triviasBefore = null; if (_isKeepTrivia && _trivias.Count > 0) { triviasBefore = new List <ScriptTrivia>(); triviasBefore.AddRange(_trivias); _trivias.Clear(); } NextToken(); var scope = ScriptVariableScope.Global; if (text.StartsWith("$")) { scope = ScriptVariableScope.Local; text = text.Substring(1); // Convert $0, $1... $n variable into $[0] $[1]...$[n] variables int index; if (int.TryParse(text, out index)) { var indexerExpression = new ScriptIndexerExpression { Span = currentSpan, Target = new ScriptVariableLocal(ScriptVariable.Arguments.Name) { Span = currentSpan }, Index = new ScriptLiteral() { Span = currentSpan, Value = index } }; if (_isKeepTrivia) { if (triviasBefore != null) { indexerExpression.Target.AddTrivias(triviasBefore, true); } FlushTrivias(indexerExpression.Index, false); } return(indexerExpression); } } else if (text == "for" || text == "while" || text == "tablerow" || (_isLiquid && (text == "forloop" || text == "tablerowloop"))) { if (Current.Type == TokenType.Dot) { NextToken(); if (Current.Type == TokenType.Identifier) { endSpan = CurrentSpan; var loopVariableText = GetAsText(Current); NextToken(); scope = ScriptVariableScope.Loop; if (_isLiquid) { switch (loopVariableText) { case "first": text = ScriptVariable.LoopFirst.Name; break; case "last": text = ScriptVariable.LoopLast.Name; break; case "index0": text = ScriptVariable.LoopIndex.Name; break; case "rindex0": text = ScriptVariable.LoopRIndex.Name; break; case "rindex": case "index": // Because forloop.index is 1 based index, we need to create a binary expression // to support it here bool isrindex = loopVariableText == "rindex"; var nested = new ScriptNestedExpression() { Expression = new ScriptBinaryExpression() { Operator = ScriptBinaryOperator.Add, Left = new ScriptVariableLoop(isrindex ? ScriptVariable.LoopRIndex.Name : ScriptVariable.LoopIndex.Name) { Span = currentSpan }, Right = new ScriptLiteral(1) { Span = currentSpan }, Span = currentSpan }, Span = currentSpan }; if (_isKeepTrivia) { if (triviasBefore != null) { nested.AddTrivias(triviasBefore, true); } FlushTrivias(nested, false); } return(nested); case "length": text = ScriptVariable.LoopLength.Name; break; case "col": if (text != "tablerowloop") { // unit test: 108-variable-loop-error2.txt LogError(currentToken, $"The loop variable <{text}.col> is invalid"); } text = ScriptVariable.TableRowCol.Name; break; default: text = text + "." + loopVariableText; LogError(currentToken, $"The liquid loop variable <{text}> is not supported"); break; } } else { switch (loopVariableText) { case "first": text = ScriptVariable.LoopFirst.Name; break; case "last": if (text == "while") { // unit test: 108-variable-loop-error2.txt LogError(currentToken, "The loop variable <while.last> is invalid"); } text = ScriptVariable.LoopLast.Name; break; case "changed": if (text == "while") { // unit test: 108-variable-loop-error2.txt LogError(currentToken, "The loop variable <while.changed> is invalid"); } text = ScriptVariable.LoopChanged.Name; break; case "length": if (text == "while") { // unit test: 108-variable-loop-error2.txt LogError(currentToken, "The loop variable <while.length> is invalid"); } text = ScriptVariable.LoopLength.Name; break; case "even": text = ScriptVariable.LoopEven.Name; break; case "odd": text = ScriptVariable.LoopOdd.Name; break; case "index": text = ScriptVariable.LoopIndex.Name; break; case "rindex": if (text == "while") { // unit test: 108-variable-loop-error2.txt LogError(currentToken, "The loop variable <while.rindex> is invalid"); } text = ScriptVariable.LoopRIndex.Name; break; case "col": if (text != "tablerow") { // unit test: 108-variable-loop-error2.txt LogError(currentToken, $"The loop variable <{text}.col> is invalid"); } text = ScriptVariable.TableRowCol.Name; break; default: text = text + "." + loopVariableText; // unit test: 108-variable-loop-error1.txt LogError(currentToken, $"The loop variable <{text}> is not supported"); break; } } // We no longer checks at parse time usage of loop variables, as they can be used in a wrap context //if (!IsInLoop()) //{ // LogError(currentToken, $"Unexpected variable <{text}> outside of a loop"); //} } else { LogError(currentToken, $"Invalid token `{Current.Type}`. The loop variable <{text}> dot must be followed by an identifier"); } } } else if (_isLiquid && text == "continue") { scope = ScriptVariableScope.Local; } var result = ScriptVariable.Create(text, scope); result.Span = new SourceSpan { FileName = currentSpan.FileName, Start = currentSpan.Start, End = endSpan.End }; // A liquid variable can have `-` in its identifier // If this is the case, we need to translate it to `this["this"]` instead if (_isLiquid && text.IndexOf('-') >= 0) { var newExp = new ScriptIndexerExpression { Target = new ScriptThisExpression() { Span = result.Span }, Index = new ScriptLiteral(text) { Span = result.Span }, Span = result.Span }; // Flush any trivias after if (_isKeepTrivia) { if (triviasBefore != null) { newExp.Target.AddTrivias(triviasBefore, true); } FlushTrivias(newExp, false); } // Return the expression return(newExp); } if (_isKeepTrivia) { // Flush any trivias after if (triviasBefore != null) { result.AddTrivias(triviasBefore, true); } FlushTrivias(result, false); } return(result); }
private ScriptExpression ParseVariableOrLiteral() { var currentToken = Current; var currentSpan = CurrentSpan; var endSpan = currentSpan; var text = GetAsText(currentToken); // Return ScriptLiteral for null, true, false // Return ScriptAnonymousFunction switch (text) { case "null": var nullValue = Open <ScriptLiteral>(); NextToken(); return(Close(nullValue)); case "true": var trueValue = Open <ScriptLiteral>(); trueValue.Value = true; NextToken(); return(Close(trueValue)); case "false": var falseValue = Open <ScriptLiteral>(); falseValue.Value = false; NextToken(); return(Close(falseValue)); case "do": var functionExp = Open <ScriptAnonymousFunction>(); functionExp.Function = ParseFunctionStatement(true); return(Close(functionExp)); } NextToken(); var scope = ScriptVariableScope.Global; if (text.StartsWith("$")) { scope = ScriptVariableScope.Local; text = text.Substring(1); // Convert $0, $1... $n variable into $[0] $[1]...$[n] variables int index; if (int.TryParse(text, out index)) { var indexerExpression = new ScriptIndexerExpression { Span = currentSpan, Target = new ScriptVariable(ScriptVariable.Arguments.Name, ScriptVariableScope.Local) { Span = currentSpan }, Index = new ScriptLiteral() { Span = currentSpan, Value = index } }; return(indexerExpression); } } if (text == "for" || text == "while") { if (Current.Type == TokenType.Dot) { NextToken(); if (Current.Type == TokenType.Identifier) { endSpan = CurrentSpan; var loopVariableText = GetAsText(Current); scope = ScriptVariableScope.Loop; switch (loopVariableText) { case "first": text = "for.first"; break; case "last": if (text == "while") { // unit test: 108-variable-loop-error2.txt LogError(currentToken, "The loop variable <while.last> is invalid"); } text = "for.last"; break; case "even": text = "for.even"; break; case "odd": text = "for.odd"; break; case "index": text = "for.index"; break; default: text = text + "." + loopVariableText; // unit test: 108-variable-loop-error1.txt LogError(currentToken, $"The loop variable <{text}> is not supported"); break; } // We no longer checks at parse time usage of loop variables, as they can be used in a wrap context //if (!IsInLoop()) //{ // LogError(currentToken, $"Unexpected variable <{text}> outside of a loop"); //} NextToken(); } else { LogError(currentToken, $"Invalid token [{Current.Type}]. The loop variable <{text}> dot must be followed by an identifier"); } } else { LogError(currentToken, $"The reserved keyword <{text}> cannot be used as a variable"); } } else if (IsKeyword(text)) { // unit test: 108-variable-error1.txt LogError(currentToken, $"The reserved keyword <{text}> cannot be used as a variable"); } var result = new ScriptVariable(text, scope) { Span = { FileName = currentSpan.FileName, Start = currentSpan.Start, End = endSpan.End } }; return(result); }
private ScriptExpression ParseVariable() { var currentToken = Current; var currentSpan = CurrentSpan; var endSpan = currentSpan; var text = GetAsText(currentToken); // Return ScriptLiteral for null, true, false // Return ScriptAnonymousFunction switch (text) { case "null": var nullValue = Open <ScriptLiteral>(); NextToken(); return(Close(nullValue)); case "true": var trueValue = Open <ScriptLiteral>(); trueValue.Value = true; NextToken(); return(Close(trueValue)); case "false": var falseValue = Open <ScriptLiteral>(); falseValue.Value = false; NextToken(); return(Close(falseValue)); case "do": var functionExp = Open <ScriptAnonymousFunction>(); functionExp.Function = ParseFunctionStatement(true); var func = Close(functionExp); return(func); case "this": if (!_isLiquid) { var thisExp = Open <ScriptThisExpression>(); ExpectAndParseKeywordTo(thisExp.ThisKeyword); return(Close(thisExp)); } break; } // Keeps trivia before this token List <ScriptTrivia> triviasBefore = null; if (_isKeepTrivia && _trivias.Count > 0) { triviasBefore = new List <ScriptTrivia>(); triviasBefore.AddRange(_trivias); _trivias.Clear(); } NextToken(); var scope = ScriptVariableScope.Global; if (text.StartsWith("$")) { scope = ScriptVariableScope.Local; text = text.Substring(1); // Convert $0, $1... $n variable into $[0] $[1]...$[n] variables int index; if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out index)) { var target = new ScriptVariableLocal(ScriptVariable.Arguments.BaseName) { Span = currentSpan }; var indexLiteral = new ScriptLiteral() { Span = currentSpan, Value = index }; var indexerExpression = new ScriptIndexerExpression { Span = currentSpan, Target = target, Index = indexLiteral }; if (_isKeepTrivia) { if (triviasBefore != null) { target.AddTrivias(triviasBefore, true); } // Special case, we add the trivias to the index as the [] brackets // won't be output-ed FlushTrivias(indexLiteral, false); _lastTerminalWithTrivias = indexLiteral; } return(indexerExpression); } } else if (text == "for" || text == "while" || text == "tablerow" || (_isLiquid && (text == "forloop" || text == "tablerowloop"))) { if (Current.Type == TokenType.Dot) { scope = ScriptVariableScope.Loop; var token = PeekToken(); if (token.Type == TokenType.Identifier) { //endSpan = GetSpanForToken(token); var loopVariableText = GetAsText(token); if (_isLiquid) { switch (loopVariableText) { case "first": case "last": case "index0": case "rindex0": case "index": case "rindex": case "length": break; case "col": if (text != "tablerowloop") { // unit test: 108-variable-loop-error2.txt LogError(currentToken, $"The loop variable <{text}.col> is invalid"); } break; default: LogError(currentToken, $"The liquid loop variable <{text}.{loopVariableText}> is not supported"); break; } if (text == "forloop") { text = "for"; } else if (text == "tablerowloop") { text = "tablerow"; } } else { switch (loopVariableText) { // supported by both for and while case "first": case "even": case "odd": case "index": break; case "last": case "changed": case "length": case "rindex": if (text == "while") { // unit test: 108-variable-loop-error2.txt LogError(currentToken, $"The loop variable <while.{loopVariableText}> is invalid"); } break; case "col": if (text != "tablerow") { // unit test: 108-variable-loop-error2.txt LogError(currentToken, $"The loop variable <{text}.col> is invalid"); } break; default: // unit test: 108-variable-loop-error1.txt LogError(currentToken, $"The loop variable <{text}.{loopVariableText}> is not supported"); break; } } } else { LogError(currentToken, $"Invalid token `{GetAsText(Current)}`. The loop variable <{text}> dot must be followed by an identifier"); } } } else if (_isLiquid && text == "continue") { scope = ScriptVariableScope.Local; } var result = ScriptVariable.Create(text, scope); result.Span = new SourceSpan { FileName = currentSpan.FileName, Start = currentSpan.Start, End = endSpan.End }; // A liquid variable can have `-` in its identifier // If this is the case, we need to translate it to `this["this"]` instead if (_isLiquid && text.IndexOf('-') >= 0) { var target = new ScriptThisExpression() { Span = result.Span }; var newExp = new ScriptIndexerExpression { Target = target, Index = new ScriptLiteral(text) { Span = result.Span }, Span = result.Span }; // Flush any trivias after if (_isKeepTrivia) { if (triviasBefore != null) { target.ThisKeyword.AddTrivias(triviasBefore, true); } FlushTrivias(newExp.CloseBracket, false); _lastTerminalWithTrivias = newExp.CloseBracket; } // Return the expression return(newExp); } if (_isKeepTrivia) { // Flush any trivias after if (triviasBefore != null) { result.AddTrivias(triviasBefore, true); } FlushTrivias(result, false); _lastTerminalWithTrivias = result; } return(result); }
private ScriptExpression ParseExpression(ScriptNode parentNode, ref bool hasAnonymousFunction, ScriptExpression parentExpression = null, int precedence = 0, ParseExpressionMode mode = ParseExpressionMode.Default) { int expressionCount = 0; _expressionLevel++; int expressionDepthBeforeEntering = _expressionDepth; EnterExpression(); try { ScriptFunctionCall functionCall = null; parseExpression: expressionCount++; ScriptExpression leftOperand = null; switch (Current.Type) { case TokenType.Identifier: case TokenType.IdentifierSpecial: leftOperand = ParseVariable(); // In case of liquid template, we accept the syntax colon after a tag if (_isLiquid && parentNode is ScriptPipeCall && Current.Type == TokenType.Colon) { NextToken(); } // Special handle of the $$ block delegate variable if (ScriptVariable.BlockDelegate.Equals(leftOperand)) { if (expressionCount != 1 || _expressionLevel > 1) { LogError(RS.DelegateBlockInNestedExpr); } if (!(parentNode is ScriptExpressionStatement)) { LogError(parentNode, RS.DelegateBlockOutsideExpr); } return(leftOperand); } break; case TokenType.Integer: leftOperand = ParseInteger(); break; case TokenType.Float: leftOperand = ParseFloat(); break; case TokenType.String: leftOperand = ParseString(); break; case TokenType.ImplicitString: leftOperand = ParseImplicitString(); break; case TokenType.VerbatimString: leftOperand = ParseVerbatimString(); break; case TokenType.OpenParent: leftOperand = ParseParenthesis(ref hasAnonymousFunction); break; case TokenType.OpenBrace: leftOperand = ParseObjectInitializer(); break; case TokenType.OpenBracket: leftOperand = ParseArrayInitializer(); break; case TokenType.Not: case TokenType.Minus: case TokenType.Arroba: case TokenType.Plus: case TokenType.Caret: leftOperand = ParseUnaryExpression(ref hasAnonymousFunction); break; } // Should not happen but in case if (leftOperand == null) { if (functionCall != null) { LogError(string.Format(RS.UnexpectedTokenInFunc, GetAsText(Current), functionCall)); } else { LogError(string.Format(RS.UnexpectedTokenInExpr, GetAsText(Current))); } return(null); } if (leftOperand is ScriptAnonymousFunction) { hasAnonymousFunction = true; } while (!hasAnonymousFunction) { if (_isLiquid && Current.Type == TokenType.Comma && functionCall != null) { NextToken(); // Skip the comma for arguments in a function call } // Parse Member expression are expected to be followed only by an identifier if (Current.Type == TokenType.Dot) { Token nextToken = PeekToken(); if (nextToken.Type == TokenType.Identifier) { NextToken(); if (GetAsText(Current) == "empty" && PeekToken().Type == TokenType.Question) { ScriptIsEmptyExpression memberExpression = Open <ScriptIsEmptyExpression>(); NextToken(); // skip empty NextToken(); // skip ? memberExpression.Target = leftOperand; leftOperand = Close(memberExpression); } else { ScriptMemberExpression memberExpression = Open <ScriptMemberExpression>(); memberExpression.Target = leftOperand; ScriptExpression member = ParseVariable(); if (!(member is ScriptVariable)) { LogError(string.Format(RS.UnexpectedLiteralMember, member)); return(null); } memberExpression.Member = (ScriptVariable)member; leftOperand = Close(memberExpression); } } else { LogError(nextToken, string.Format(RS.InvalidTokenAfterDot, nextToken.Type)); return(null); } continue; } // If we have a bracket but left operand is a (variable || member || indexer), then we consider next as an indexer // unit test: 130-indexer-accessor-accept1.txt if (Current.Type == TokenType.OpenBracket && leftOperand is IScriptVariablePath && !IsPreviousCharWhitespace()) { NextToken(); ScriptIndexerExpression indexerExpression = Open <ScriptIndexerExpression>(); indexerExpression.Target = leftOperand; // unit test: 130-indexer-accessor-error5.txt indexerExpression.Index = ExpectAndParseExpression(indexerExpression, ref hasAnonymousFunction, functionCall, 0, string.Format(RS.ExpectToken, "index_expression", Current.Type)); if (Current.Type != TokenType.CloseBracket) { LogError(string.Format(RS.ExpectToken, "]", Current.Type)); } else { NextToken(); } leftOperand = Close(indexerExpression); continue; } if (mode == ParseExpressionMode.BasicExpression) { break; } if (Current.Type == TokenType.Equal) { ScriptAssignExpression assignExpression = Open <ScriptAssignExpression>(); if (_expressionLevel > 1) { // unit test: 101-assign-complex-error1.txt LogError(assignExpression, RS.ExprForTopLevelAssignmentOnly); } NextToken(); assignExpression.Target = TransformKeyword(leftOperand); // unit test: 105-assign-error3.txt assignExpression.Value = ExpectAndParseExpression(assignExpression, ref hasAnonymousFunction, parentExpression); leftOperand = Close(assignExpression); continue; } // Handle binary operators here ScriptBinaryOperator binaryOperatorType; if (BinaryOperators.TryGetValue(Current.Type, out binaryOperatorType) || (_isLiquid && TryLiquidBinaryOperator(out binaryOperatorType))) { int newPrecedence = GetOperatorPrecedence(binaryOperatorType); // Check precedence to see if we should "take" this operator here (Thanks TimJones for the tip code! ;) if (newPrecedence < precedence) { break; } // We fake entering an expression here to limit the number of expression EnterExpression(); ScriptBinaryExpression binaryExpression = Open <ScriptBinaryExpression>(); binaryExpression.Left = leftOperand; binaryExpression.Operator = binaryOperatorType; NextToken(); // skip the operator // unit test: 110-binary-simple-error1.txt binaryExpression.Right = ExpectAndParseExpression(binaryExpression, ref hasAnonymousFunction, functionCall ?? parentExpression, newPrecedence, string.Format(RS.ExpectTokenInRightOperator, "expression", Current.Type)); leftOperand = Close(binaryExpression); continue; } if (precedence > 0) { break; } if (StartAsExpression()) { // If we can parse a statement, we have a method call if (parentExpression != null) { break; } // Parse named parameters var paramContainer = parentNode as IScriptNamedArgumentContainer; if (Current.Type == TokenType.Identifier && (parentNode is IScriptNamedArgumentContainer || !_isLiquid && PeekToken().Type == TokenType.Colon)) { if (paramContainer == null) { if (functionCall == null) { functionCall = Open <ScriptFunctionCall>(); functionCall.Target = leftOperand; functionCall.Span.Start = leftOperand.Span.Start; } else { functionCall.Arguments.Add(leftOperand); } Close(leftOperand); } while (true) { if (Current.Type != TokenType.Identifier) { break; } ScriptNamedArgument parameter = Open <ScriptNamedArgument>(); string parameterName = GetAsText(Current); parameter.Name = parameterName; // Skip argument name NextToken(); if (paramContainer != null) { paramContainer.AddParameter(Close(parameter)); } else { functionCall.Arguments.Add(parameter); } // If we have a colon, we have a value // otherwise it is a boolean argument name if (Current.Type == TokenType.Colon) { NextToken(); parameter.Value = ExpectAndParseExpression(parentNode, mode: ParseExpressionMode.BasicExpression); parameter.Span.End = parameter.Value.Span.End; } if (functionCall != null) { functionCall.Span.End = parameter.Span.End; } } // As we have handled leftOperand here, we don't let the function out of this while to pick up the leftOperand if (functionCall != null) { leftOperand = functionCall; functionCall = null; } // We don't allow anything after named parameters break; } if (functionCall == null) { functionCall = Open <ScriptFunctionCall>(); functionCall.Target = leftOperand; // If we need to convert liquid to textscript functions: if (_isLiquid && Options.ConvertLiquidFunctions) { TransformFromLiquidFunctionCall(functionCall); } functionCall.Span.Start = leftOperand.Span.Start; } else { functionCall.Arguments.Add(leftOperand); } goto parseExpression; } if (Current.Type == TokenType.Pipe) { if (functionCall != null) { functionCall.Arguments.Add(leftOperand); leftOperand = functionCall; } ScriptPipeCall pipeCall = Open <ScriptPipeCall>(); pipeCall.From = leftOperand; NextToken(); // skip | // unit test: 310-func-pipe-error1.txt pipeCall.To = ExpectAndParseExpression(pipeCall, ref hasAnonymousFunction); return(Close(pipeCall)); } break; } if (functionCall != null) { functionCall.Arguments.Add(leftOperand); functionCall.Span.End = leftOperand.Span.End; return(functionCall); } return(Close(leftOperand)); } finally { LeaveExpression(); // Force to restore back to a level _expressionDepth = expressionDepthBeforeEntering; _expressionLevel--; } }