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