1 // 2 // ExpressionParserManual.cs 3 // 4 // Author: 5 // Atsushi Enomoto (atsushi@xamarin.com) 6 // 7 // Copyright (C) 2013 Xamarin Inc. (http://www.xamarin.com) 8 // 9 // Permission is hereby granted, free of charge, to any person obtaining 10 // a copy of this software and associated documentation files (the 11 // "Software"), to deal in the Software without restriction, including 12 // without limitation the rights to use, copy, modify, merge, publish, 13 // distribute, sublicense, and/or sell copies of the Software, and to 14 // permit persons to whom the Software is furnished to do so, subject to 15 // the following conditions: 16 // 17 // The above copyright notice and this permission notice shall be 18 // included in all copies or substantial portions of the Software. 19 // 20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 // 28 using System; 29 using System.Collections.Generic; 30 using System.Linq; 31 using Microsoft.Build.Exceptions; 32 33 namespace Microsoft.Build.Internal.Expressions 34 { 35 class ExpressionParserManual 36 { 37 // FIXME: we are going to not need ExpressionValidationType for this; always LaxString. ExpressionParserManual(string source, ExpressionValidationType validationType)38 public ExpressionParserManual (string source, ExpressionValidationType validationType) 39 { 40 if (source == null) 41 throw new ArgumentNullException ("source"); 42 this.source = source; 43 validation_type = validationType; 44 } 45 46 string source; 47 ExpressionValidationType validation_type; 48 Parse()49 public ExpressionList Parse () 50 { 51 return Parse (0, source.Length); 52 } 53 Parse(int start, int end)54 ExpressionList Parse (int start, int end) 55 { 56 if (string.IsNullOrWhiteSpace (source)) 57 return new ExpressionList (); 58 59 var ret = new ExpressionList (); 60 while (start < end) { 61 int bak = start; 62 ret.Add (ParseSingle (ref start, end)); 63 if (bak == start) 64 throw new Exception ("Parser failed to progress token position: " + source); 65 } 66 return ret; 67 } 68 69 static readonly char [] token_starters = "$@%(),'\"".ToCharArray (); 70 ParseSingle(ref int start, int end)71 Expression ParseSingle (ref int start, int end) 72 { 73 char token = source [start]; 74 switch (token) { 75 case '$': 76 case '@': 77 case '%': 78 if (start == end || start + 1 == source.Length || source [start + 1] != '(') { 79 if (validation_type == ExpressionValidationType.StrictBoolean) 80 throw new InvalidProjectFileException (string.Format ("missing '(' after '{0}' at {1} in \"{2}\"", source [start], start, source)); 81 else 82 goto default; // treat as raw literal to the section end 83 } 84 start += 2; 85 int last = FindMatchingCloseParen (start, end); 86 if (last < 0) { 87 if (validation_type == ExpressionValidationType.StrictBoolean) 88 throw new InvalidProjectFileException (string.Format ("expression did not have matching ')' since index {0} in \"{1}\"", start, source)); 89 else { 90 start -= 2; 91 goto default; // treat as raw literal to the section end 92 } 93 } 94 Expression ret; 95 if (token == '$') 96 ret = EvaluatePropertyExpression (start, last); 97 else if (token == '%') 98 ret = EvaluateMetadataExpression (start, last); 99 else 100 ret = EvaluateItemExpression (start, last); 101 start = last + 1; 102 return ret; 103 104 case '\'': 105 case '"': 106 var quoteChar = source [start]; 107 start++; 108 last = FindMatchingCloseQuote (quoteChar, start, end); 109 if (last < 0) { 110 if (validation_type == ExpressionValidationType.StrictBoolean) 111 throw new InvalidProjectFileException (string.Format ("expression did not have matching ')' since index {0} in \"{1}\"", start, source)); 112 else { 113 start--; 114 goto default; // treat as raw literal to the section end 115 } 116 } 117 ret = new QuotedExpression () { QuoteChar = quoteChar, Contents = Parse (start, last) }; 118 start = last + 1; 119 return ret; 120 // Below (until default) are important only for Condition evaluation 121 case '(': 122 if (validation_type == ExpressionValidationType.LaxString) 123 goto default; 124 start++; 125 last = FindMatchingCloseParen (start, end); 126 if (last < 0) { 127 if (validation_type == ExpressionValidationType.StrictBoolean) 128 throw new InvalidProjectFileException (string.Format ("expression did not have matching ')' since index {0} in \"{1}\"", start, source)); 129 else { 130 start--; 131 goto default; // treat as raw literal to the section end 132 } 133 } 134 var contents = Parse (start, last).ToArray (); 135 if (contents.Length > 1) 136 throw new InvalidProjectFileException (string.Format ("unexpected continuous expression within (){0} in \"{1}\"", contents [1].Column > 0 ? " at " + contents [1].Column : null, source)); 137 return contents.First (); 138 139 default: 140 int idx = source.IndexOfAny (token_starters, start + 1); 141 string name = idx < 0 ? source.Substring (start, end - start) : source.Substring (start, idx - start); 142 var val = new NameToken () { Name = name }; 143 ret = new RawStringLiteral () { Value = val }; 144 if (idx >= 0) 145 start = idx; 146 else 147 start = end; 148 149 return ret; 150 } 151 } 152 FindMatchingCloseParen(int start, int end)153 int FindMatchingCloseParen (int start, int end) 154 { 155 int n = 0; 156 for (int i = start; i < end; i++) { 157 if (source [i] == '(') 158 n++; 159 else if (source [i] == ')') { 160 if (n-- == 0) 161 return i; 162 } 163 } 164 return -1; // invalid 165 } 166 FindMatchingCloseQuote(char quote, int start, int end)167 int FindMatchingCloseQuote (char quote, int start, int end) 168 { 169 int n = 0; 170 for (int i = start; i < end; i++) { 171 if (i < end + 1 && source [i] == '\\' && (source [i + 1] == quote || source [i + 1] == '\\')) 172 n += 2; 173 else if (source [i] == quote) { 174 if (n-- == 0) 175 return i; 176 } 177 } 178 return -1; // invalid 179 } 180 181 static readonly string spaces = " \t\r\n"; 182 SkipSpaces(ref int start)183 void SkipSpaces (ref int start) 184 { 185 while (start < source.Length && spaces.Contains (source [start])) 186 start++; 187 } 188 EvaluatePropertyExpression(int start, int end)189 PropertyAccessExpression EvaluatePropertyExpression (int start, int end) 190 { 191 // member access 192 int dotAt = source.LastIndexOf ('.', end, end - start); 193 int colonsAt = source.LastIndexOf ("::", end, end - start, StringComparison.Ordinal); 194 if (dotAt < 0 && colonsAt < 0) { 195 // property access without member specification 196 int parenAt = source.IndexOf ('(', start, end - start); 197 string name = parenAt < 0 ? source.Substring (start, end - start) : source.Substring (start, parenAt - start); 198 name = name.Trim (); 199 var access = new PropertyAccess () { 200 Name = new NameToken () { Name = name }, 201 TargetType = PropertyTargetType.Object 202 }; 203 if (parenAt > 0) { // method arguments 204 start = parenAt + 1; 205 access.Arguments = ParseFunctionArguments (ref start, end); 206 } 207 return new PropertyAccessExpression () { Access = access }; 208 } 209 if (colonsAt < 0 || colonsAt < dotAt) { 210 // property access with member specification 211 int mstart = dotAt + 1; 212 int parenAt = source.IndexOf ('(', mstart, end - mstart); 213 string name = parenAt < 0 ? source.Substring (mstart, end - mstart) : source.Substring (mstart, parenAt - mstart); 214 name = name.Trim (); 215 var access = new PropertyAccess () { 216 Name = new NameToken () { Name = name }, 217 TargetType = PropertyTargetType.Object, 218 Target = dotAt < 0 ? null : Parse (start, dotAt).FirstOrDefault () 219 }; 220 if (parenAt > 0) { // method arguments 221 start = parenAt + 1; 222 access.Arguments = ParseFunctionArguments (ref start, end); 223 } 224 return new PropertyAccessExpression () { Access = access }; 225 } else { 226 // static type access 227 string type = source.Substring (start, colonsAt - start); 228 if (type.Length < 2 || type [0] != '[' || type [type.Length - 1] != ']') 229 throw new InvalidProjectFileException (string.Format ("Static function call misses appropriate type name surrounded by '[' and ']' at {0} in \"{1}\"", start, source)); 230 type = type.Substring (1, type.Length - 2).Trim (); 231 start = colonsAt + 2; 232 int parenAt = source.IndexOf ('(', start, end - start); 233 string member = parenAt < 0 ? source.Substring (start, end - start) : source.Substring (start, parenAt - start); 234 member = member.Trim (); 235 if (member.Length == 0) 236 throw new InvalidProjectFileException ("Static member name is missing"); 237 var access = new PropertyAccess () { 238 Name = new NameToken () { Name = member }, 239 TargetType = PropertyTargetType.Type, 240 Target = new StringLiteral () { Value = new NameToken () { Name = type } } 241 }; 242 if (parenAt > 0) { // method arguments 243 start = parenAt + 1; 244 access.Arguments = ParseFunctionArguments (ref start, end); 245 } 246 return new PropertyAccessExpression () { Access = access }; 247 } 248 } 249 ParseFunctionArguments(ref int start, int end)250 ExpressionList ParseFunctionArguments (ref int start, int end) 251 { 252 var args = new ExpressionList (); 253 do { 254 SkipSpaces (ref start); 255 if (start == source.Length) 256 throw new InvalidProjectFileException ("unterminated function call arguments."); 257 if (source [start] == ')') 258 break; 259 else if (args.Any ()) { 260 if (source [start] != ',') 261 throw new InvalidProjectFileException (string.Format ("invalid function call arguments specification. ',' is expected, got '{0}'", source [start])); 262 start++; 263 SkipSpaces (ref start); 264 } 265 args.Add (ParseSingle (ref start, end)); 266 } while (true); 267 start++; 268 return args; 269 } 270 EvaluateItemExpression(int start, int end)271 ItemAccessExpression EvaluateItemExpression (int start, int end) 272 { 273 // using property as context and evaluate 274 int idx = source.IndexOf ("->", start, StringComparison.Ordinal); 275 if (idx > 0) { 276 string name = source.Substring (start, idx - start); 277 return new ItemAccessExpression () { 278 Application = new ItemApplication () { 279 Name = new NameToken () { Name = name }, 280 Expressions = Parse (idx + 2, end) 281 } 282 }; 283 } else { 284 string name = source.Substring (start, end - start); 285 return new ItemAccessExpression () { 286 Application = new ItemApplication () { Name = new NameToken () { Name = name } } 287 }; 288 } 289 } 290 EvaluateMetadataExpression(int start, int end)291 MetadataAccessExpression EvaluateMetadataExpression (int start, int end) 292 { 293 int idx = source.IndexOf ('.', start, end - start); 294 string item = idx < 0 ? null : source.Substring (start, idx - start); 295 string meta = idx < 0 ? source.Substring (start, end - start) : source.Substring (idx + 1, end - idx - 1); 296 var access = new MetadataAccess () { 297 ItemType = item == null ? null : new NameToken () { Column = start, Name = item }, 298 Metadata = new NameToken () { Column = idx < 0 ? start : idx + 1, Name = meta } 299 }; 300 return new MetadataAccessExpression () { Access = access }; 301 } 302 } 303 } 304 305