/// <summary> /// Parses a javascript expression. /// </summary> /// <param name="endToken"> A token that indicates the end of the expression. </param> /// <returns> An expression tree that represents the expression. </returns> private Expression ParseExpression(params Token[] endTokens) { // The root of the expression tree. Expression root = null; // The active operator, i.e. the one last encountered. OperatorExpression unboundOperator = null; while (this.nextToken != null) { if (this.nextToken is LiteralToken || this.nextToken is IdentifierToken || this.nextToken == KeywordToken.Function || this.nextToken == KeywordToken.This || this.nextToken == PunctuatorToken.LeftBrace || (this.nextToken == PunctuatorToken.LeftBracket && this.expressionState == ParserExpressionState.Literal) || (this.nextToken is KeywordToken && unboundOperator != null && unboundOperator.OperatorType == OperatorType.MemberAccess && this.expressionState == ParserExpressionState.Literal)) { // If a literal was found where an operator was expected, insert a semi-colon // automatically (if this would fix the error and a line terminator was // encountered) or throw an error. if (this.expressionState != ParserExpressionState.Literal) { // Check for automatic semi-colon insertion. if (Array.IndexOf(endTokens, PunctuatorToken.Semicolon) >= 0 && this.consumedLineTerminator == true) break; throw new JavaScriptException(this.engine, "SyntaxError", string.Format("Expected operator but found {0}", Token.ToText(this.nextToken)), this.LineNumber, this.SourcePath); } // New in ECMAScript 5 is the ability to use keywords as property names. if ((this.nextToken is KeywordToken || (this.nextToken is LiteralToken && ((LiteralToken)this.nextToken).IsKeyword == true)) && unboundOperator != null && unboundOperator.OperatorType == OperatorType.MemberAccess && this.expressionState == ParserExpressionState.Literal) { this.nextToken = new IdentifierToken(this.nextToken.Text); } Expression terminal; if (this.nextToken is LiteralToken) // If the token is a literal, convert it to a literal expression. terminal = new LiteralExpression(((LiteralToken)this.nextToken).Value); else if (this.nextToken is IdentifierToken) { // If the token is an identifier, convert it to a NameExpression. var identifierName = ((IdentifierToken)this.nextToken).Name; terminal = new NameExpression(this.currentScope, identifierName); // Record each occurance of a variable name. if (unboundOperator == null || unboundOperator.OperatorType != OperatorType.MemberAccess) this.methodOptimizationHints.EncounteredVariable(identifierName); } else if (this.nextToken == KeywordToken.This) { // Convert "this" to an expression. terminal = new ThisExpression(); // Add method optimization info. this.methodOptimizationHints.HasThis = true; } else if (this.nextToken == PunctuatorToken.LeftBracket) // Array literal. terminal = ParseArrayLiteral(); else if (this.nextToken == PunctuatorToken.LeftBrace) // Object literal. terminal = ParseObjectLiteral(); else if (this.nextToken == KeywordToken.Function) terminal = ParseFunctionExpression(); else throw new InvalidOperationException("Unsupported literal type."); // Push the literal to the most recent unbound operator, or, if there is none, to // the root of the tree. if (root == null) { // This is the first term in an expression. root = terminal; } else { Debug.Assert(unboundOperator != null && unboundOperator.AcceptingOperands == true); unboundOperator.Push(terminal); } } else if (this.nextToken is PunctuatorToken || this.nextToken is KeywordToken) { // The token is an operator (o1). Operator newOperator = OperatorFromToken(this.nextToken, postfixOrInfix: this.expressionState == ParserExpressionState.Operator); // Make sure the token is actually an operator and not just a random keyword. if (newOperator == null) { // Check if the token is an end token, for example a semi-colon. if (Array.IndexOf(endTokens, this.nextToken) >= 0) break; // Check for automatic semi-colon insertion. if (Array.IndexOf(endTokens, PunctuatorToken.Semicolon) >= 0 && (this.consumedLineTerminator == true || this.nextToken == PunctuatorToken.RightBrace)) break; throw new JavaScriptException(this.engine, "SyntaxError", string.Format("Unexpected token {0} in expression.", Token.ToText(this.nextToken)), this.LineNumber, this.SourcePath); } // Post-fix increment and decrement cannot have a line terminator in between // the operator and the operand. if (this.consumedLineTerminator == true && (newOperator == Operator.PostIncrement || newOperator == Operator.PostDecrement)) break; // There are four possibilities: // 1. The token is the second of a two-part operator (for example, the ':' in a // conditional operator. In this case, we need to go up the tree until we find // an instance of the operator and make that the active unbound operator. if (this.nextToken == newOperator.SecondaryToken) { // Traverse down the tree looking for the parent operator that corresponds to // this token. OperatorExpression parentExpression = null; var node = root as OperatorExpression; while (node != null) { if (node.Operator.Token == newOperator.Token && node.SecondTokenEncountered == false) parentExpression = node; if (node == unboundOperator) break; node = node.RightBranch; } // If the operator was not found, then this is a mismatched token, unless // it is the end token. For example, if an unbalanced right parenthesis is // found in an if statement then it is merely the end of the test expression. if (parentExpression == null) { // Check if the token is an end token, for example a right parenthesis. if (Array.IndexOf(endTokens, this.nextToken) >= 0) break; // Check for automatic semi-colon insertion. if (Array.IndexOf(endTokens, PunctuatorToken.Semicolon) >= 0 && this.consumedLineTerminator == true) break; throw new JavaScriptException(this.engine, "SyntaxError", "Mismatched closing token in expression.", this.LineNumber, this.SourcePath); } // Mark that we have seen the closing token. unboundOperator = parentExpression; unboundOperator.SecondTokenEncountered = true; } else { // Check if the token is an end token, for example the comma in a variable // declaration. if (Array.IndexOf(endTokens, this.nextToken) >= 0) { // But make sure the token isn't inside an operator. // For example, in the expression "var x = f(a, b)" the comma does not // indicate the start of a new variable clause because it is inside the // function call operator. bool insideOperator = false; var node = root as OperatorExpression; while (node != null) { if (node.Operator.SecondaryToken != null && node.SecondTokenEncountered == false) insideOperator = true; if (node == unboundOperator) break; node = node.RightBranch; } if (insideOperator == false) break; } // All the other situations involve the creation of a new operator. var newExpression = OperatorExpression.FromOperator(newOperator); // 2. The new operator is a prefix operator. The new operator becomes an operand // of the previous operator. if (newOperator.HasLHSOperand == false) { if (root == null) // "!" root = newExpression; else if (unboundOperator != null && unboundOperator.AcceptingOperands == true) { // "5 + !" unboundOperator.Push(newExpression); } else { // "5 !" or "5 + 5 !" // Check for automatic semi-colon insertion. if (Array.IndexOf(endTokens, PunctuatorToken.Semicolon) >= 0 && this.consumedLineTerminator == true) break; throw new JavaScriptException(this.engine, "SyntaxError", "Invalid use of prefix operator.", this.LineNumber, this.SourcePath); } } else { // Search up the tree for an operator that has a lower precedence. // Because we don't store the parent link, we have to traverse down the // tree and take the last one we find instead. OperatorExpression lowPrecedenceOperator = null; if (unboundOperator == null || (newOperator.Associativity == OperatorAssociativity.LeftToRight && unboundOperator.Precedence < newOperator.Precedence) || (newOperator.Associativity == OperatorAssociativity.RightToLeft && unboundOperator.Precedence <= newOperator.Precedence)) { // Performance optimization: look at the previous operator first. lowPrecedenceOperator = unboundOperator; } else { // Search for a lower precedence operator by traversing the tree. var node = root as OperatorExpression; while (node != null && node != unboundOperator) { if ((newOperator.Associativity == OperatorAssociativity.LeftToRight && node.Precedence < newOperator.Precedence) || (newOperator.Associativity == OperatorAssociativity.RightToLeft && node.Precedence <= newOperator.Precedence)) lowPrecedenceOperator = node; node = node.RightBranch; } } if (lowPrecedenceOperator == null) { // 3. The new operator has a lower precedence (or if the associativity is left to // right, a lower or equal precedence) than all the parent operators. The new // operator goes to the root of the tree and the previous operator becomes the // first operand for the new operator. if (root != null) newExpression.Push(root); root = newExpression; } else { // 4. Otherwise, the new operator can steal the last operand from the previous // operator and then put itself in the place of that last operand. if (lowPrecedenceOperator.OperandCount == 0) { // "! ++" // Check for automatic semi-colon insertion. if (Array.IndexOf(endTokens, PunctuatorToken.Semicolon) >= 0 && this.consumedLineTerminator == true) break; throw new JavaScriptException(this.engine, "SyntaxError", "Invalid use of prefix operator.", this.LineNumber, this.SourcePath); } newExpression.Push(lowPrecedenceOperator.Pop()); lowPrecedenceOperator.Push(newExpression); } } unboundOperator = newExpression; } } else { throw new JavaScriptException(this.engine, "SyntaxError", string.Format("Unexpected token {0} in expression", Token.ToText(this.nextToken)), this.LineNumber, this.SourcePath); } // Read the next token. this.Consume(root != null && (unboundOperator == null || unboundOperator.AcceptingOperands == false) ? ParserExpressionState.Operator : ParserExpressionState.Literal); } // Empty expressions are invalid. if (root == null) throw new JavaScriptException(this.engine, "SyntaxError", string.Format("Expected an expression but found {0} instead", Token.ToText(this.nextToken)), this.LineNumber, this.SourcePath); // Check the AST is valid. CheckASTValidity(root); // A literal is the next valid expression token. this.expressionState = ParserExpressionState.Literal; this.lexer.ParserExpressionState = expressionState; // Resolve all the unbound operators into real operators. return root; }
/// <summary> /// Reads a property name, used in object literals. /// </summary> /// <param name="nameType"> Identifies the particular way the property was specified. </param> /// <returns> An expression that evaluates to the property name. </returns> private Expression ReadPropertyNameExpression(out PropertyNameType nameType) { Expression result; if (this.nextToken is LiteralToken) { // The property name can be a string or a number or (in ES5) a keyword. if (((LiteralToken)this.nextToken).IsKeyword == true) { // false, true or null. result = new LiteralExpression(this.nextToken.Text); } else { object literalValue = ((LiteralToken)this.nextToken).Value; if (literalValue is string || literalValue is int) result = new LiteralExpression(((LiteralToken)this.nextToken).Value.ToString()); else if (literalValue is double) result = new LiteralExpression(((double)((LiteralToken)this.nextToken).Value).ToString(CultureInfo.InvariantCulture)); else throw new JavaScriptException(this.engine, ErrorType.SyntaxError, string.Format("Expected property name but found {0}", Token.ToText(this.nextToken)), this.LineNumber, this.SourcePath); } nameType = PropertyNameType.Name; } else if (this.nextToken is IdentifierToken) { // An identifier is also okay. var name = ((IdentifierToken)this.nextToken).Name; if (name == "get") nameType = PropertyNameType.Get; else if (name == "set") nameType = PropertyNameType.Set; else nameType = PropertyNameType.Name; result = new LiteralExpression(name); } else if (this.nextToken is KeywordToken) { // In ES5 a keyword is also okay. result = new LiteralExpression(((KeywordToken)this.nextToken).Name); nameType = PropertyNameType.Name; } else if (this.nextToken == PunctuatorToken.LeftBracket) { // ES6 computed property. this.Consume(); result = ParseExpression(PunctuatorToken.RightBracket); nameType = PropertyNameType.Expression; } else throw new JavaScriptException(this.engine, ErrorType.SyntaxError, string.Format("Expected property name but found {0}", Token.ToText(this.nextToken)), this.LineNumber, this.SourcePath); // Consume the token. this.Consume(); // Return the property name. return result; }