The Labs \ Source Viewer \ SSCLI \ System.Xml.Xsl.XPath \ XPathParser

  1. //------------------------------------------------------------------------------
  2. // <copyright file="XPathParser.cs" company="Microsoft">
  3. //
  4. // Copyright (c) 2006 Microsoft Corporation. All rights reserved.
  5. //
  6. // The use and distribution terms for this software are contained in the file
  7. // named license.txt, which can be found in the root of this distribution.
  8. // By using this software in any fashion, you are agreeing to be bound by the
  9. // terms of this license.
  10. //
  11. // You must not remove this notice, or any other, from this software.
  12. //
  13. // </copyright>
  14. //------------------------------------------------------------------------------
  15. using System.Collections.Generic;
  16. using System.Diagnostics;
  17. namespace System.Xml.Xsl.XPath
  18. {
  19.     using Res = System.Xml.Utils.Res;
  20.     using XPathNodeType = System.Xml.XPath.XPathNodeType;
  21.    
  22.     internal class XPathParser<Node>
  23.     {
  24.         private XPathScanner scanner;
  25.         private IXPathBuilder<Node> builder;
  26.         private Stack<int> posInfo = new Stack<int>();
  27.        
  28.        
  29.         public Node Parse(XPathScanner scanner, IXPathBuilder<Node> builder, LexKind endLex)
  30.         {
  31.             Debug.Assert(this.scanner == null && this.builder == null);
  32.             Debug.Assert(scanner != null && builder != null);
  33.            
  34.             Node result = default(Node);
  35.             this.scanner = scanner;
  36.             this.builder = builder;
  37.             this.posInfo.Clear();
  38.            
  39.             try {
  40.                 builder.StartBuild();
  41.                 result = ParseExpr();
  42.                 scanner.CheckToken(endLex);
  43.             }
  44.             catch (XPathCompileException e) {
  45.                 if (e.queryString == null) {
  46.                     e.queryString = scanner.Source;
  47.                     PopPosInfo(out e.startChar, out e.endChar);
  48.                 }
  49.                 throw;
  50.             }
  51.             finally {
  52.                 result = builder.EndBuild(result);
  53.                 #if DEBUG
  54.                 this.builder = null;
  55.                 this.scanner = null;
  56.                 #endif
  57.             }
  58.             Debug.Assert(posInfo.Count == 0, "PushPosInfo() and PopPosInfo() calls have been unbalanced");
  59.             return result;
  60.         }
  61.        
  62.         #region Location paths and node tests
  63. /**************************************************************************************************/       
  64. /*  Location paths and node tests                                                                */       
  65. /**************************************************************************************************/       
  66.        
  67.         static internal bool IsStep(LexKind lexKind)
  68.         {
  69.                 // NodeTest is also Name
  70.             return (lexKind == LexKind.Dot || lexKind == LexKind.DotDot || lexKind == LexKind.At || lexKind == LexKind.Axis || lexKind == LexKind.Star || lexKind == LexKind.Name);
  71.         }
  72.        
  73. /*
  74.         *  LocationPath ::= RelativeLocationPath | '/' RelativeLocationPath? | '//' RelativeLocationPath
  75.         */       
  76.         private Node ParseLocationPath()
  77.         {
  78.             if (scanner.Kind == LexKind.Slash) {
  79.                 scanner.NextLex();
  80.                 Node opnd = builder.Axis(XPathAxis.Root, XPathNodeType.All, null, null);
  81.                
  82.                 if (IsStep(scanner.Kind)) {
  83.                     opnd = builder.JoinStep(opnd, ParseRelativeLocationPath());
  84.                 }
  85.                 return opnd;
  86.             }
  87.             else if (scanner.Kind == LexKind.SlashSlash) {
  88.                 scanner.NextLex();
  89.                 return builder.JoinStep(builder.Axis(XPathAxis.Root, XPathNodeType.All, null, null), builder.JoinStep(builder.Axis(XPathAxis.DescendantOrSelf, XPathNodeType.All, null, null), ParseRelativeLocationPath()));
  90.             }
  91.             else {
  92.                 return ParseRelativeLocationPath();
  93.             }
  94.         }
  95.        
  96. /*
  97.         *  RelativeLocationPath ::= Step (('/' | '//') Step)*
  98.         */       
  99.         private Node ParseRelativeLocationPath()
  100.         {
  101.             Node opnd = ParseStep();
  102.             if (scanner.Kind == LexKind.Slash) {
  103.                 scanner.NextLex();
  104.                 opnd = builder.JoinStep(opnd, ParseRelativeLocationPath());
  105.             }
  106.             else if (scanner.Kind == LexKind.SlashSlash) {
  107.                 scanner.NextLex();
  108.                 opnd = builder.JoinStep(opnd, builder.JoinStep(builder.Axis(XPathAxis.DescendantOrSelf, XPathNodeType.All, null, null), ParseRelativeLocationPath()));
  109.             }
  110.             return opnd;
  111.         }
  112.        
  113.         static internal XPathAxis GetAxis(string axisName, XPathScanner scanner)
  114.         {
  115.             switch (axisName) {
  116.                 case "ancestor":
  117.                     return XPathAxis.Ancestor;
  118.                 case "ancestor-or-self":
  119.                     return XPathAxis.AncestorOrSelf;
  120.                 case "attribute":
  121.                     return XPathAxis.Attribute;
  122.                 case "child":
  123.                     return XPathAxis.Child;
  124.                 case "descendant":
  125.                     return XPathAxis.Descendant;
  126.                 case "descendant-or-self":
  127.                     return XPathAxis.DescendantOrSelf;
  128.                 case "following":
  129.                     return XPathAxis.Following;
  130.                 case "following-sibling":
  131.                     return XPathAxis.FollowingSibling;
  132.                 case "namespace":
  133.                     return XPathAxis.Namespace;
  134.                 case "parent":
  135.                     return XPathAxis.Parent;
  136.                 case "preceding":
  137.                     return XPathAxis.Preceding;
  138.                 case "preceding-sibling":
  139.                     return XPathAxis.PrecedingSibling;
  140.                 case "self":
  141.                     return XPathAxis.Self;
  142.                 default:
  143.                     throw scanner.CreateException(Res.XPath_UnknownAxis, axisName);
  144.                     break;
  145.             }
  146.         }
  147.        
  148. /*
  149.         *  Step ::= '.' | '..' | (AxisName '::' | '@')? NodeTest Predicate*
  150.         */       
  151.         private Node ParseStep()
  152.         {
  153.             Node opnd;
  154.             if (LexKind.Dot == scanner.Kind) {
  155.                 //>> '.'
  156.                 scanner.NextLex();
  157.                 opnd = builder.Axis(XPathAxis.Self, XPathNodeType.All, null, null);
  158.                 if (LexKind.LBracket == scanner.Kind) {
  159.                     throw scanner.CreateException(Res.XPath_PredicateAfterDot);
  160.                 }
  161.             }
  162.             else if (LexKind.DotDot == scanner.Kind) {
  163.                 //>> '..'
  164.                 scanner.NextLex();
  165.                 opnd = builder.Axis(XPathAxis.Parent, XPathNodeType.All, null, null);
  166.                 if (LexKind.LBracket == scanner.Kind) {
  167.                     throw scanner.CreateException(Res.XPath_PredicateAfterDotDot);
  168.                 }
  169.             }
  170.             else {
  171.                 //>> (AxisName '::' | '@')? NodeTest Predicate*
  172.                 XPathAxis axis;
  173.                 switch (scanner.Kind) {
  174.                     case LexKind.Axis:
  175.                         //>> AxisName '::'
  176.                         axis = GetAxis(scanner.Name, scanner);
  177.                         scanner.NextLex();
  178.                         break;
  179.                     case LexKind.At:
  180.                         //>> '@'
  181.                         axis = XPathAxis.Attribute;
  182.                         scanner.NextLex();
  183.                         break;
  184.                     case LexKind.Name:
  185.                     case LexKind.Star:
  186.                         // NodeTest must start with Name or '*'
  187.                         axis = XPathAxis.Child;
  188.                         break;
  189.                     default:
  190.                         throw scanner.CreateException(Res.XPath_UnexpectedToken, scanner.RawValue);
  191.                         break;
  192.                 }
  193.                
  194.                 opnd = ParseNodeTest(axis);
  195.                
  196.                 while (LexKind.LBracket == scanner.Kind) {
  197.                     opnd = builder.Predicate(opnd, ParsePredicate(), IsReverseAxis(axis));
  198.                 }
  199.             }
  200.             return opnd;
  201.         }
  202.        
  203.         private static bool IsReverseAxis(XPathAxis axis)
  204.         {
  205.             return (axis == XPathAxis.Ancestor || axis == XPathAxis.Preceding || axis == XPathAxis.AncestorOrSelf || axis == XPathAxis.PrecedingSibling);
  206.         }
  207.        
  208. /*
  209.         *  NodeTest ::= NameTest | ('comment' | 'text' | 'node') '(' ')' | 'processing-instruction' '('  Literal? ')'
  210.         *  NameTest ::= '*' | NCName ':' '*' | QName
  211.         */       
  212.         private Node ParseNodeTest(XPathAxis axis)
  213.         {
  214.             XPathNodeType nodeType;
  215.             string nodePrefix;
  216.             string nodeName;
  217.            
  218.             int startChar = scanner.LexStart;
  219.             InternalParseNodeTest(scanner, axis, out nodeType, out nodePrefix, out nodeName);
  220.             PushPosInfo(startChar, scanner.PrevLexEnd);
  221.             Node result = builder.Axis(axis, nodeType, nodePrefix, nodeName);
  222.             PopPosInfo();
  223.             return result;
  224.         }
  225.        
  226.         private static bool IsNodeType(XPathScanner scanner)
  227.         {
  228.             return scanner.Prefix.Length == 0 && (scanner.Name == "node" || scanner.Name == "text" || scanner.Name == "processing-instruction" || scanner.Name == "comment");
  229.         }
  230.        
  231.         private static XPathNodeType PrincipalNodeType(XPathAxis axis)
  232.         {
  233.                 /*else*/            return (axis == XPathAxis.Attribute ? XPathNodeType.Attribute : axis == XPathAxis.Namespace ? XPathNodeType.Namespace : XPathNodeType.Element);
  234.         }
  235.        
  236.         static internal void InternalParseNodeTest(XPathScanner scanner, XPathAxis axis, out XPathNodeType nodeType, out string nodePrefix, out string nodeName)
  237.         {
  238.             switch (scanner.Kind) {
  239.                 case LexKind.Name:
  240.                     if (scanner.CanBeFunction && IsNodeType(scanner)) {
  241.                         nodePrefix = null;
  242.                         nodeName = null;
  243.                         switch (scanner.Name) {
  244.                             case "comment":
  245.                                 nodeType = XPathNodeType.Comment;
  246.                                 break;
  247.                             case "text":
  248.                                 nodeType = XPathNodeType.Text;
  249.                                 break;
  250.                             case "node":
  251.                                 nodeType = XPathNodeType.All;
  252.                                 break;
  253.                             default:
  254.                                 Debug.Assert(scanner.Name == "processing-instruction");
  255.                                 nodeType = XPathNodeType.ProcessingInstruction;
  256.                                 break;
  257.                         }
  258.                        
  259.                         scanner.NextLex();
  260.                         scanner.PassToken(LexKind.LParens);
  261.                        
  262.                         if (nodeType == XPathNodeType.ProcessingInstruction) {
  263.                             if (scanner.Kind != LexKind.RParens) {
  264.                                 //>> 'processing-instruction' '(' Literal ')'
  265.                                 scanner.CheckToken(LexKind.String);
  266.                                 // It is not needed to set nodePrefix here, but for our current implementation
  267.                                 // comparing whole QNames is faster than comparing just local names
  268.                                 nodePrefix = string.Empty;
  269.                                 nodeName = scanner.StringValue;
  270.                                 scanner.NextLex();
  271.                             }
  272.                         }
  273.                        
  274.                         scanner.PassToken(LexKind.RParens);
  275.                     }
  276.                     else {
  277.                         nodePrefix = scanner.Prefix;
  278.                         nodeName = scanner.Name;
  279.                         nodeType = PrincipalNodeType(axis);
  280.                         scanner.NextLex();
  281.                         if (nodeName == "*") {
  282.                             nodeName = null;
  283.                         }
  284.                     }
  285.                     break;
  286.                 case LexKind.Star:
  287.                     nodePrefix = null;
  288.                     nodeName = null;
  289.                     nodeType = PrincipalNodeType(axis);
  290.                     scanner.NextLex();
  291.                     break;
  292.                 default:
  293.                     throw scanner.CreateException(Res.XPath_NodeTestExpected, scanner.RawValue);
  294.                     break;
  295.             }
  296.         }
  297.        
  298. /*
  299.         *  Predicate ::= '[' Expr ']'
  300.         */       
  301.         private Node ParsePredicate()
  302.         {
  303.             scanner.PassToken(LexKind.LBracket);
  304.             Node opnd = ParseExpr();
  305.             scanner.PassToken(LexKind.RBracket);
  306.             return opnd;
  307.         }
  308.         #endregion
  309.        
  310.         #region Expressions
  311. /**************************************************************************************************/       
  312. /*  Expressions                                                                                  */       
  313. /**************************************************************************************************/       
  314.        
  315. /*
  316.         *  Expr  ::= OrExpr
  317.         *  OrExpr ::= AndExpr ('or' AndExpr)*
  318.         */       
  319.         private Node ParseExpr()
  320.         {
  321.             Node opnd = ParseAndExpr();
  322.            
  323.             while (scanner.IsKeyword("or")) {
  324.                 scanner.NextLex();
  325.                 opnd = builder.Operator(XPathOperator.Or, opnd, ParseAndExpr());
  326.             }
  327.             return opnd;
  328.         }
  329.        
  330. /*
  331.         *  AndExpr ::= EqualityExpr ('and' EqualityExpr)*
  332.         */       
  333.         private Node ParseAndExpr()
  334.         {
  335.             Node opnd = ParseEqualityExpr();
  336.            
  337.             while (scanner.IsKeyword("and")) {
  338.                 scanner.NextLex();
  339.                 opnd = builder.Operator(XPathOperator.And, opnd, ParseEqualityExpr());
  340.             }
  341.             return opnd;
  342.         }
  343.        
  344. /*
  345.         *  EqualityExpr ::= RelationalExpr (('=' | '!=') RelationalExpr)*
  346.         */       
  347.         private Node ParseEqualityExpr()
  348.         {
  349.             Node opnd = ParseRelationalExpr();
  350.             bool eq;
  351.            
  352.             while ((eq = scanner.Kind == LexKind.Eq) || scanner.Kind == LexKind.Ne) {
  353.                 XPathOperator op = eq ? XPathOperator.Eq : XPathOperator.Ne;
  354.                 scanner.NextLex();
  355.                 opnd = builder.Operator(op, opnd, ParseRelationalExpr());
  356.             }
  357.             return opnd;
  358.         }
  359.        
  360. /*
  361.         *  RelationalExpr ::= AdditiveExpr (('<' | '>' | '<=' | '>=') AdditiveExpr)*
  362.         */       
  363.         private Node ParseRelationalExpr()
  364.         {
  365.             Node opnd = ParseAdditiveExpr();
  366.            
  367.             while (true) {
  368.                 XPathOperator op;
  369.                 switch (scanner.Kind) {
  370.                     case LexKind.Lt:
  371.                         op = XPathOperator.Lt;
  372.                         break;
  373.                     case LexKind.Le:
  374.                         op = XPathOperator.Le;
  375.                         break;
  376.                     case LexKind.Gt:
  377.                         op = XPathOperator.Gt;
  378.                         break;
  379.                     case LexKind.Ge:
  380.                         op = XPathOperator.Ge;
  381.                         break;
  382.                     default:
  383.                         return opnd;
  384.                 }
  385.                 scanner.NextLex();
  386.                 opnd = builder.Operator(op, opnd, ParseAdditiveExpr());
  387.             }
  388.         }
  389.        
  390. /*
  391.         *  AdditiveExpr ::= MultiplicativeExpr (('+' | '-') MultiplicativeExpr)*
  392.         */       
  393.         private Node ParseAdditiveExpr()
  394.         {
  395.             Node opnd = ParseMultiplicativeExpr();
  396.             bool plus;
  397.            
  398.             while ((plus = scanner.Kind == LexKind.Plus) || scanner.Kind == LexKind.Minus) {
  399.                 XPathOperator op = plus ? XPathOperator.Plus : XPathOperator.Minus;
  400.                 scanner.NextLex();
  401.                 opnd = builder.Operator(op, opnd, ParseMultiplicativeExpr());
  402.             }
  403.             return opnd;
  404.         }
  405.        
  406. /*
  407.         *  MultiplicativeExpr ::= UnaryExpr (('*' | 'div' | 'mod') UnaryExpr)*
  408.         */       
  409.         private Node ParseMultiplicativeExpr()
  410.         {
  411.             Node opnd = ParseUnaryExpr();
  412.            
  413.             while (true) {
  414.                 XPathOperator op;
  415.                 if (scanner.Kind == LexKind.Star) {
  416.                     op = XPathOperator.Multiply;
  417.                 }
  418.                 else if (scanner.IsKeyword("div")) {
  419.                     op = XPathOperator.Divide;
  420.                 }
  421.                 else if (scanner.IsKeyword("mod")) {
  422.                     op = XPathOperator.Modulo;
  423.                 }
  424.                 else {
  425.                     return opnd;
  426.                 }
  427.                 scanner.NextLex();
  428.                 opnd = builder.Operator(op, opnd, ParseUnaryExpr());
  429.             }
  430.         }
  431.        
  432. /*
  433.         *  UnaryExpr ::= ('-')* UnionExpr
  434.         */       
  435.         private Node ParseUnaryExpr()
  436.         {
  437.             if (scanner.Kind == LexKind.Minus) {
  438.                 scanner.NextLex();
  439.                 return builder.Operator(XPathOperator.UnaryMinus, ParseUnaryExpr(), default(Node));
  440.             }
  441.             else {
  442.                 return ParseUnionExpr();
  443.             }
  444.         }
  445.        
  446. /*
  447.         *  UnionExpr ::= PathExpr ('|' PathExpr)*
  448.         */       
  449.         private Node ParseUnionExpr()
  450.         {
  451.             int startChar = scanner.LexStart;
  452.             Node opnd1 = ParsePathExpr();
  453.            
  454.             if (scanner.Kind == LexKind.Union) {
  455.                 PushPosInfo(startChar, scanner.PrevLexEnd);
  456.                 opnd1 = builder.Operator(XPathOperator.Union, default(Node), opnd1);
  457.                 PopPosInfo();
  458.                
  459.                 while (scanner.Kind == LexKind.Union) {
  460.                     scanner.NextLex();
  461.                     startChar = scanner.LexStart;
  462.                     Node opnd2 = ParsePathExpr();
  463.                     PushPosInfo(startChar, scanner.PrevLexEnd);
  464.                     opnd1 = builder.Operator(XPathOperator.Union, opnd1, opnd2);
  465.                     PopPosInfo();
  466.                 }
  467.             }
  468.             return opnd1;
  469.         }
  470.        
  471. /*
  472.         *  PathExpr ::= LocationPath | FilterExpr (('/' | '//') RelativeLocationPath )?
  473.         */       
  474.         private Node ParsePathExpr()
  475.         {
  476.             // Here we distinguish FilterExpr from LocationPath - the former starts with PrimaryExpr
  477.             if (IsPrimaryExpr()) {
  478.                 int startChar = scanner.LexStart;
  479.                 Node opnd = ParseFilterExpr();
  480.                 int endChar = scanner.PrevLexEnd;
  481.                
  482.                 if (scanner.Kind == LexKind.Slash) {
  483.                     scanner.NextLex();
  484.                     PushPosInfo(startChar, endChar);
  485.                     opnd = builder.JoinStep(opnd, ParseRelativeLocationPath());
  486.                     PopPosInfo();
  487.                 }
  488.                 else if (scanner.Kind == LexKind.SlashSlash) {
  489.                     scanner.NextLex();
  490.                     PushPosInfo(startChar, endChar);
  491.                     opnd = builder.JoinStep(opnd, builder.JoinStep(builder.Axis(XPathAxis.DescendantOrSelf, XPathNodeType.All, null, null), ParseRelativeLocationPath()));
  492.                     PopPosInfo();
  493.                 }
  494.                 return opnd;
  495.             }
  496.             else {
  497.                 return ParseLocationPath();
  498.             }
  499.         }
  500.        
  501. /*
  502.         *  FilterExpr ::= PrimaryExpr Predicate*
  503.         */       
  504.         private Node ParseFilterExpr()
  505.         {
  506.             int startChar = scanner.LexStart;
  507.             Node opnd = ParsePrimaryExpr();
  508.             int endChar = scanner.PrevLexEnd;
  509.            
  510.             while (scanner.Kind == LexKind.LBracket) {
  511.                 PushPosInfo(startChar, endChar);
  512.                     /*reverseStep:*/                opnd = builder.Predicate(opnd, ParsePredicate(), false);
  513.                 PopPosInfo();
  514.             }
  515.             return opnd;
  516.         }
  517.        
  518.         private bool IsPrimaryExpr()
  519.         {
  520.             return (scanner.Kind == LexKind.String || scanner.Kind == LexKind.Number || scanner.Kind == LexKind.Dollar || scanner.Kind == LexKind.LParens || scanner.Kind == LexKind.Name && scanner.CanBeFunction && !IsNodeType(scanner));
  521.         }
  522.        
  523. /*
  524.         *  PrimaryExpr ::= Literal | Number | VariableReference | '(' Expr ')' | FunctionCall
  525.         */       
  526.         private Node ParsePrimaryExpr()
  527.         {
  528.             Debug.Assert(IsPrimaryExpr());
  529.             Node opnd;
  530.             switch (scanner.Kind) {
  531.                 case LexKind.String:
  532.                     opnd = builder.String(scanner.StringValue);
  533.                     scanner.NextLex();
  534.                     break;
  535.                 case LexKind.Number:
  536.                     opnd = builder.Number(scanner.NumberValue);
  537.                     scanner.NextLex();
  538.                     break;
  539.                 case LexKind.Dollar:
  540.                     int startChar = scanner.LexStart;
  541.                     scanner.NextLex();
  542.                     scanner.CheckToken(LexKind.Name);
  543.                     PushPosInfo(startChar, scanner.LexStart + scanner.LexSize);
  544.                     opnd = builder.Variable(scanner.Prefix, scanner.Name);
  545.                     PopPosInfo();
  546.                     scanner.NextLex();
  547.                     break;
  548.                 case LexKind.LParens:
  549.                     scanner.NextLex();
  550.                     opnd = ParseExpr();
  551.                     scanner.PassToken(LexKind.RParens);
  552.                     break;
  553.                 default:
  554.                     Debug.Assert(scanner.Kind == LexKind.Name && scanner.CanBeFunction && !IsNodeType(scanner), "IsPrimaryExpr() returned true, but the lexeme is not recognized");
  555.                     opnd = ParseFunctionCall();
  556.                     break;
  557.             }
  558.             return opnd;
  559.         }
  560.        
  561. /*
  562.         *  FunctionCall ::= FunctionName '(' (Expr (',' Expr)* )? ')'
  563.         */       
  564.         private Node ParseFunctionCall()
  565.         {
  566.             List<Node> argList = new List<Node>();
  567.             string name = scanner.Name;
  568.             string prefix = scanner.Prefix;
  569.             int startChar = scanner.LexStart;
  570.            
  571.             scanner.PassToken(LexKind.Name);
  572.             scanner.PassToken(LexKind.LParens);
  573.            
  574.             if (scanner.Kind != LexKind.RParens) {
  575.                 while (true) {
  576.                     argList.Add(ParseExpr());
  577.                     if (scanner.Kind != LexKind.Comma) {
  578.                         scanner.CheckToken(LexKind.RParens);
  579.                         break;
  580.                     }
  581.                     scanner.NextLex();
  582.                     // move off the ','
  583.                 }
  584.             }
  585.            
  586.             scanner.NextLex();
  587.             // move off the ')'
  588.             PushPosInfo(startChar, scanner.PrevLexEnd);
  589.             Node result = builder.Function(prefix, name, argList);
  590.             PopPosInfo();
  591.             return result;
  592.         }
  593.         #endregion
  594.        
  595. /**************************************************************************************************/       
  596. /*  Helper methods                                                                                */       
  597. /**************************************************************************************************/       
  598.        
  599.         private void PushPosInfo(int startChar, int endChar)
  600.         {
  601.             posInfo.Push(startChar);
  602.             posInfo.Push(endChar);
  603.         }
  604.        
  605.         private void PopPosInfo()
  606.         {
  607.             posInfo.Pop();
  608.             posInfo.Pop();
  609.         }
  610.        
  611.         private void PopPosInfo(out int startChar, out int endChar)
  612.         {
  613.             endChar = posInfo.Pop();
  614.             startChar = posInfo.Pop();
  615.         }
  616.     }
  617. }

Developer Fusion