2 // ExpressionEvaluator.cs
5 // Atsushi Enomoto (atsushi@xamarin.com)
7 // Copyright (C) 2013 Xamarin Inc. (http://www.xamarin.com)
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:
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
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.
30 using Microsoft.Build.Evaluation;
31 using Microsoft.Build.Exceptions;
32 using System.Collections.Generic;
33 using System.Reflection;
34 using Microsoft.Build.Execution;
35 using Microsoft.Build.Framework;
38 namespace Microsoft.Build.Internal.Expressions
40 class ExpressionEvaluator
42 public ExpressionEvaluator (Project project)
47 public ExpressionEvaluator (ProjectInstance project)
49 ProjectInstance = project;
52 EvaluationContext CreateContext (string source)
54 return new EvaluationContext (source, this);
57 public Project Project { get; private set; }
58 public ProjectInstance ProjectInstance { get; set; }
60 List<ITaskItem> evaluated_task_items = new List<ITaskItem> ();
62 public IList<ITaskItem> EvaluatedTaskItems {
63 get { return evaluated_task_items; }
66 public string Evaluate (string source)
68 return Evaluate (source, new ExpressionParserManual (source ?? string.Empty, ExpressionValidationType.LaxString).Parse ());
71 string Evaluate (string source, ExpressionList exprList)
74 throw new ArgumentNullException ("exprList");
75 return string.Concat (exprList.Select (e => e.EvaluateAsString (CreateContext (source))));
78 public bool EvaluateAsBoolean (string source)
81 var el = new ExpressionParser ().Parse (source, ExpressionValidationType.StrictBoolean);
83 throw new InvalidProjectFileException ("Unexpected number of tokens: " + el.Count ());
84 return el.First ().EvaluateAsBoolean (CreateContext (source));
85 } catch (yyParser.yyException ex) {
86 throw new InvalidProjectFileException (string.Format ("failed to evaluate expression as boolean: '{0}': {1}", source, ex.Message), ex);
91 class EvaluationContext
93 public EvaluationContext (string source, ExpressionEvaluator evaluator)
96 Evaluator = evaluator;
99 public string Source { get; private set; }
101 public ExpressionEvaluator Evaluator { get; private set; }
102 public object ContextItem { get; set; }
104 Stack<object> evaluating_items = new Stack<object> ();
105 Stack<object> evaluating_props = new Stack<object> ();
107 public IEnumerable<object> GetItems (string name)
109 if (Evaluator.Project != null)
110 return Evaluator.Project.GetItems (name);
112 return Evaluator.ProjectInstance.GetItems (name);
115 public IEnumerable<object> GetAllItems ()
117 if (Evaluator.Project != null)
118 return Evaluator.Project.AllEvaluatedItems;
120 return Evaluator.ProjectInstance.AllEvaluatedItems;
123 public string EvaluateItem (string itemType, object item)
125 if (evaluating_items.Contains (item))
126 throw new InvalidProjectFileException (string.Format ("Recursive reference to item '{0}' was found", itemType));
128 evaluating_items.Push (item);
129 var eval = item as ProjectItem;
131 return Evaluator.Evaluate (eval.EvaluatedInclude);
133 var inst = (ProjectItemInstance) item;
134 if (!Evaluator.EvaluatedTaskItems.Contains (inst))
135 Evaluator.EvaluatedTaskItems.Add (inst);
136 return Evaluator.Evaluate (inst.EvaluatedInclude);
139 evaluating_items.Pop ();
143 public string EvaluateProperty (string name)
145 if (Evaluator.Project != null) {
146 var prop = Evaluator.Project.GetProperty (name);
149 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
151 var prop = Evaluator.ProjectInstance.GetProperty (name);
154 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
158 public string EvaluateProperty (object prop, string name, string value)
160 if (evaluating_props.Contains (prop))
161 throw new InvalidProjectFileException (string.Format ("Recursive reference to property '{0}' was found", name));
163 evaluating_props.Push (prop);
164 // FIXME: needs verification on whether string evaluation is appropriate or not.
165 return Evaluator.Evaluate (value);
167 evaluating_props.Pop ();
172 abstract partial class Expression
174 public abstract string ExpressionString { get; }
175 public abstract string EvaluateAsString (EvaluationContext context);
176 public abstract bool EvaluateAsBoolean (EvaluationContext context);
177 public abstract object EvaluateAsObject (EvaluationContext context);
179 public bool EvaluateStringAsBoolean (EvaluationContext context, string ret)
182 if (ret.Equals ("TRUE", StringComparison.InvariantCultureIgnoreCase))
184 else if (ret.Equals ("FALSE", StringComparison.InvariantCultureIgnoreCase))
187 throw new InvalidProjectFileException (this.Location, string.Format ("Part of condition '{0}' is evaluated as '{1}' and cannot be converted to boolean", context.Source, ret));
191 partial class BinaryExpression : Expression
193 public override bool EvaluateAsBoolean (EvaluationContext context)
197 return string.Equals (StripStringWrap (Left.EvaluateAsString (context)), StripStringWrap (Right.EvaluateAsString (context)), StringComparison.OrdinalIgnoreCase);
199 return !string.Equals (StripStringWrap (Left.EvaluateAsString (context)), StripStringWrap (Right.EvaluateAsString (context)), StringComparison.OrdinalIgnoreCase);
202 // evaluate first, to detect possible syntax error on right expr.
203 var lb = Left.EvaluateAsBoolean (context);
204 var rb = Right.EvaluateAsBoolean (context);
205 return Operator == Operator.And ? (lb && rb) : (lb || rb);
207 // comparison expressions - evaluate comparable first, then compare values.
208 var left = Left.EvaluateAsObject (context);
209 var right = Right.EvaluateAsObject (context);
210 if (!(left is IComparable && right is IComparable))
211 throw new InvalidProjectFileException ("expression cannot be evaluated as boolean");
212 var result = ((IComparable) left).CompareTo (right);
223 throw new InvalidOperationException ();
226 string StripStringWrap (string s)
231 if (s.Length > 1 && s [0] == '"' && s [s.Length - 1] == '"')
232 return s.Substring (1, s.Length - 2);
233 else if (s.Length > 1 && s [0] == '\'' && s [s.Length - 1] == '\'')
234 return s.Substring (1, s.Length - 2);
238 public override object EvaluateAsObject (EvaluationContext context)
240 throw new NotImplementedException ();
243 static readonly Dictionary<Operator,string> strings = new Dictionary<Operator, string> () {
244 {Operator.EQ, " == "},
245 {Operator.NE, " != "},
246 {Operator.LT, " < "},
247 {Operator.LE, " <= "},
248 {Operator.GT, " > "},
249 {Operator.GE, " >= "},
250 {Operator.And, " And "},
251 {Operator.Or, " Or "},
254 public override string EvaluateAsString (EvaluationContext context)
256 return Left.EvaluateAsString (context) + strings [Operator] + Right.EvaluateAsString (context);
260 partial class BooleanLiteral : Expression
262 public override string EvaluateAsString (EvaluationContext context)
264 return Value ? "True" : "False";
267 public override bool EvaluateAsBoolean (EvaluationContext context)
272 public override object EvaluateAsObject (EvaluationContext context)
278 partial class NotExpression : Expression
280 public override string EvaluateAsString (EvaluationContext context)
282 // no negation for string
283 return "!" + Negated.EvaluateAsString (context);
286 public override bool EvaluateAsBoolean (EvaluationContext context)
288 return !Negated.EvaluateAsBoolean (context);
291 public override object EvaluateAsObject (EvaluationContext context)
293 return EvaluateAsString (context);
297 partial class PropertyAccessExpression : Expression
299 public override bool EvaluateAsBoolean (EvaluationContext context)
301 var ret = EvaluateAsString (context);
302 return EvaluateStringAsBoolean (context, ret);
305 public override string EvaluateAsString (EvaluationContext context)
307 var ret = EvaluateAsObject (context);
308 return ret == null ? null : ret.ToString ();
311 public override object EvaluateAsObject (EvaluationContext context)
314 return DoEvaluateAsObject (context);
315 } catch (TargetInvocationException ex) {
316 throw new InvalidProjectFileException ("Access to property caused an error", ex);
320 object DoEvaluateAsObject (EvaluationContext context)
322 if (Access.Target == null) {
323 return context.EvaluateProperty (Access.Name.Name);
325 if (this.Access.TargetType == PropertyTargetType.Object) {
326 var obj = Access.Target.EvaluateAsObject (context);
329 if (Access.Arguments != null) {
330 var args = Access.Arguments.Select (e => e.EvaluateAsObject (context)).ToArray ();
331 var method = FindMethod (obj.GetType (), Access.Name.Name, args);
333 throw new InvalidProjectFileException (Location, string.Format ("access to undefined method '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
334 return method.Invoke (obj, AdjustArgsForCall (method, args));
336 var prop = obj.GetType ().GetProperty (Access.Name.Name);
338 throw new InvalidProjectFileException (Location, string.Format ("access to undefined property '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
339 return prop.GetValue (obj, null);
342 var type = Type.GetType (Access.Target.EvaluateAsString (context));
344 throw new InvalidProjectFileException (Location, string.Format ("specified type '{0}' was not found", Access.Target.EvaluateAsString (context)));
345 if (Access.Arguments != null) {
346 var args = Access.Arguments.Select (e => e.EvaluateAsObject (context)).ToArray ();
347 var method = FindMethod (type, Access.Name.Name, args);
349 throw new InvalidProjectFileException (Location, string.Format ("access to undefined static method '{0}' of '{1}' at {2}", Access.Name.Name, type, Location));
350 return method.Invoke (null, AdjustArgsForCall (method, args));
352 var prop = type.GetProperty (Access.Name.Name);
354 throw new InvalidProjectFileException (Location, string.Format ("access to undefined static property '{0}' of '{1}' at {2}", Access.Name.Name, type, Location));
355 return prop.GetValue (null, null);
361 MethodInfo FindMethod (Type type, string name, object [] args)
363 var methods = type.GetMethods ().Where (m => {
366 var pl = m.GetParameters ();
367 if (pl.Length == args.Length)
369 // calling String.Format() with either set of arguments is valid:
370 // - three strings (two for varargs)
371 // - two strings (happen to be exact match)
372 // - one string (no varargs)
373 if (pl.Length > 0 && pl.Length - 1 <= args.Length &&
374 pl.Last ().GetCustomAttributesData ().Any (a => a.Constructor.DeclaringType == typeof (ParamArrayAttribute)))
378 if (methods.Count () == 1)
379 return methods.First ();
380 return args.Any (a => a == null) ?
381 type.GetMethod (name) :
382 type.GetMethod (name, args.Select (o => o.GetType ()).ToArray ());
385 object [] AdjustArgsForCall (MethodInfo m, object[] args)
387 if (m.GetParameters ().Length == args.Length + 1)
388 return args.Concat (new object[] {Array.CreateInstance (m.GetParameters ().Last ().ParameterType.GetElementType (), 0)}).ToArray ();
394 partial class ItemAccessExpression : Expression
396 public override bool EvaluateAsBoolean (EvaluationContext context)
398 return EvaluateStringAsBoolean (context, EvaluateAsString (context));
401 public override string EvaluateAsString (EvaluationContext context)
403 string itemType = Application.Name.Name;
404 var items = context.GetItems (itemType);
407 if (Application.Expressions == null)
408 return string.Join (";", items.Select (item => Unwrap (context.EvaluateItem (itemType, item))));
410 return string.Join (";", items.Select (item => {
411 context.ContextItem = item;
412 var ret = Unwrap (string.Concat (Application.Expressions.Select (e => e.EvaluateAsString (context))));
413 context.ContextItem = null;
418 static string Unwrap (string ret)
420 if (ret.Length < 2 || ret [0] != ret [ret.Length - 1] || ret [0] != '"' && ret [0] != '\'')
422 return ret.Substring (1, ret.Length - 2);
425 public override object EvaluateAsObject (EvaluationContext context)
427 return EvaluateAsString (context);
431 partial class MetadataAccessExpression : Expression
433 public override bool EvaluateAsBoolean (EvaluationContext context)
435 return EvaluateStringAsBoolean (context, EvaluateAsString (context));
438 public override string EvaluateAsString (EvaluationContext context)
440 string itemType = this.Access.ItemType != null ? this.Access.ItemType.Name : null;
441 string metadataName = Access.Metadata.Name;
442 IEnumerable<object> items;
443 if (this.Access.ItemType != null)
444 items = context.GetItems (itemType);
445 else if (context.ContextItem != null)
446 items = new Object [] { context.ContextItem };
448 items = context.GetAllItems ();
450 var values = items.Select (i => (i is ProjectItem) ? ((ProjectItem) i).GetMetadataValue (metadataName) : ((ProjectItemInstance) i).GetMetadataValue (metadataName)).Where (s => !string.IsNullOrEmpty (s));
451 return string.Join (";", values);
454 public override object EvaluateAsObject (EvaluationContext context)
456 return EvaluateAsString (context);
459 partial class StringLiteral : Expression
461 public override bool EvaluateAsBoolean (EvaluationContext context)
463 var ret = EvaluateAsString (context);
464 return EvaluateStringAsBoolean (context, ret);
467 public override string EvaluateAsString (EvaluationContext context)
469 return context.Evaluator.Evaluate (this.Value.Name);
472 public override object EvaluateAsObject (EvaluationContext context)
474 return EvaluateAsString (context);
477 partial class RawStringLiteral : Expression
479 public override string EvaluateAsString (EvaluationContext context)
484 public override bool EvaluateAsBoolean (EvaluationContext context)
486 throw new InvalidProjectFileException ("raw string literal cannot be evaluated as boolean");
489 public override object EvaluateAsObject (EvaluationContext context)
491 return EvaluateAsString (context);
495 partial class QuotedExpression : Expression
497 public override string EvaluateAsString (EvaluationContext context)
499 return QuoteChar + EvaluateAsStringWithoutQuote (context) + QuoteChar;
502 public string EvaluateAsStringWithoutQuote (EvaluationContext context)
504 return string.Concat (Contents.Select (e => e.EvaluateAsString (context)));
507 public override bool EvaluateAsBoolean (EvaluationContext context)
509 var ret = EvaluateAsStringWithoutQuote (context);
510 return EvaluateStringAsBoolean (context, ret);
513 public override object EvaluateAsObject (EvaluationContext context)
515 return EvaluateAsStringWithoutQuote (context);
519 partial class FunctionCallExpression : Expression
521 public override string EvaluateAsString (EvaluationContext context)
523 throw new NotImplementedException ();
526 public override bool EvaluateAsBoolean (EvaluationContext context)
528 if (string.Equals (Name.Name, "Exists", StringComparison.OrdinalIgnoreCase)) {
529 if (Arguments.Count != 1)
530 throw new InvalidProjectFileException (Location, "Function 'Exists' expects 1 argument");
531 string val = Arguments.First ().EvaluateAsString (context);
532 val = WindowsCompatibilityExtensions.FindMatchingPath (val);
533 return Directory.Exists (val) || System.IO.File.Exists (val);
535 if (string.Equals (Name.Name, "HasTrailingSlash", StringComparison.OrdinalIgnoreCase)) {
536 if (Arguments.Count != 1)
537 throw new InvalidProjectFileException (Location, "Function 'HasTrailingSlash' expects 1 argument");
538 string val = Arguments.First ().EvaluateAsString (context);
539 return val.LastOrDefault () == '\\' || val.LastOrDefault () == '/';
541 throw new InvalidProjectFileException (Location, string.Format ("Unsupported function '{0}'", Name));
544 public override object EvaluateAsObject (EvaluationContext context)
546 throw new NotImplementedException ();