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)
46 GetItems = (name) => project.GetItems (name).Select (i => new KeyValuePair<string,string> (i.ItemType, i.EvaluatedInclude));
47 GetProperty = (name) => {
48 var prop = project.GetProperty (name);
49 return new KeyValuePair<string,string> (prop != null ? prop.Name : null, prop != null ? prop.EvaluatedValue : null);
54 public ExpressionEvaluator (ProjectInstance project)
56 ProjectInstance = project;
58 GetItems = (name) => project.GetItems (name).Select (i => new KeyValuePair<string,string> (i.ItemType, i.EvaluatedInclude));
59 GetProperty = (name) => {
60 var prop = project.GetProperty (name);
61 return new KeyValuePair<string,string> (prop != null ? prop.Name : null, prop != null ? prop.EvaluatedValue : null);
66 EvaluationContext CreateContext (string source)
68 return new EvaluationContext (source, this);
71 public Project Project { get; private set; }
72 public ProjectInstance ProjectInstance { get; set; }
73 //public Func<string,IEnumerable<KeyValuePair<string,string>>> GetItems { get; private set; }
74 //public Func<string,KeyValuePair<string,string>> GetProperty { get; private set; }
76 List<ITaskItem> evaluated_task_items = new List<ITaskItem> ();
78 public IList<ITaskItem> EvaluatedTaskItems {
79 get { return evaluated_task_items; }
82 public string Evaluate (string source)
84 return Evaluate (source, new ExpressionParserManual (source ?? string.Empty, ExpressionValidationType.LaxString).Parse ());
87 string Evaluate (string source, ExpressionList exprList)
90 throw new ArgumentNullException ("exprList");
91 return string.Concat (exprList.Select (e => e.EvaluateAsString (CreateContext (source))));
94 public bool EvaluateAsBoolean (string source)
97 var el = new ExpressionParser ().Parse (source, ExpressionValidationType.StrictBoolean);
99 throw new InvalidProjectFileException ("Unexpected number of tokens: " + el.Count ());
100 return el.First ().EvaluateAsBoolean (CreateContext (source));
101 } catch (yyParser.yyException ex) {
102 throw new InvalidProjectFileException (string.Format ("failed to evaluate expression as boolean: '{0}': {1}", source, ex.Message), ex);
107 class EvaluationContext
109 public EvaluationContext (string source, ExpressionEvaluator evaluator)
112 Evaluator = evaluator;
115 public string Source { get; private set; }
117 public ExpressionEvaluator Evaluator { get; private set; }
118 public object ContextItem { get; set; }
120 Stack<object> evaluating_items = new Stack<object> ();
121 Stack<object> evaluating_props = new Stack<object> ();
123 public IEnumerable<object> GetItems (string name)
125 if (Evaluator.Project != null)
126 return Evaluator.Project.GetItems (name);
128 return Evaluator.ProjectInstance.GetItems (name);
131 public IEnumerable<object> GetAllItems ()
133 if (Evaluator.Project != null)
134 return Evaluator.Project.AllEvaluatedItems;
136 return Evaluator.ProjectInstance.AllEvaluatedItems;
139 public string EvaluateItem (string itemType, object item)
141 if (evaluating_items.Contains (item))
142 throw new InvalidProjectFileException (string.Format ("Recursive reference to item '{0}' was found", itemType));
144 evaluating_items.Push (item);
145 var eval = item as ProjectItem;
147 return Evaluator.Evaluate (eval.EvaluatedInclude);
149 var inst = (ProjectItemInstance) item;
150 if (!Evaluator.EvaluatedTaskItems.Contains (inst))
151 Evaluator.EvaluatedTaskItems.Add (inst);
152 return Evaluator.Evaluate (inst.EvaluatedInclude);
155 evaluating_items.Pop ();
159 public string EvaluateProperty (string name)
161 if (Evaluator.Project != null) {
162 var prop = Evaluator.Project.GetProperty (name);
165 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
167 var prop = Evaluator.ProjectInstance.GetProperty (name);
170 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
174 public string EvaluateProperty (object prop, string name, string value)
176 if (evaluating_props.Contains (prop))
177 throw new InvalidProjectFileException (string.Format ("Recursive reference to property '{0}' was found", name));
179 evaluating_props.Push (prop);
180 // FIXME: needs verification on whether string evaluation is appropriate or not.
181 return Evaluator.Evaluate (value);
183 evaluating_props.Pop ();
188 abstract partial class Expression
190 public abstract string EvaluateAsString (EvaluationContext context);
191 public abstract bool EvaluateAsBoolean (EvaluationContext context);
192 public abstract object EvaluateAsObject (EvaluationContext context);
194 public bool EvaluateStringAsBoolean (EvaluationContext context, string ret)
197 if (ret.Equals ("TRUE", StringComparison.InvariantCultureIgnoreCase))
199 else if (ret.Equals ("FALSE", StringComparison.InvariantCultureIgnoreCase))
202 throw new InvalidProjectFileException (this.Location, string.Format ("Condition '{0}' is evaluated as '{1}' and cannot be converted to boolean", context.Source, ret));
206 partial class BinaryExpression : Expression
208 public override bool EvaluateAsBoolean (EvaluationContext context)
212 return string.Equals (StripStringWrap (Left.EvaluateAsString (context)), StripStringWrap (Right.EvaluateAsString (context)), StringComparison.OrdinalIgnoreCase);
214 return !string.Equals (StripStringWrap (Left.EvaluateAsString (context)), StripStringWrap (Right.EvaluateAsString (context)), StringComparison.OrdinalIgnoreCase);
217 // evaluate first, to detect possible syntax error on right expr.
218 var lb = Left.EvaluateAsBoolean (context);
219 var rb = Right.EvaluateAsBoolean (context);
220 return Operator == Operator.And ? (lb && rb) : (lb || rb);
222 // comparison expressions - evaluate comparable first, then compare values.
223 var left = Left.EvaluateAsObject (context);
224 var right = Right.EvaluateAsObject (context);
225 if (!(left is IComparable && right is IComparable))
226 throw new InvalidProjectFileException ("expression cannot be evaluated as boolean");
227 var result = ((IComparable) left).CompareTo (right);
238 throw new InvalidOperationException ();
241 string StripStringWrap (string s)
246 if (s.Length > 1 && s [0] == '"' && s [s.Length - 1] == '"')
247 return s.Substring (1, s.Length - 2);
248 else if (s.Length > 1 && s [0] == '\'' && s [s.Length - 1] == '\'')
249 return s.Substring (1, s.Length - 2);
253 public override object EvaluateAsObject (EvaluationContext context)
255 throw new NotImplementedException ();
258 static readonly Dictionary<Operator,string> strings = new Dictionary<Operator, string> () {
259 {Operator.EQ, " == "},
260 {Operator.NE, " != "},
261 {Operator.LT, " < "},
262 {Operator.LE, " <= "},
263 {Operator.GT, " > "},
264 {Operator.GE, " >= "},
265 {Operator.And, " And "},
266 {Operator.Or, " Or "},
269 public override string EvaluateAsString (EvaluationContext context)
271 return Left.EvaluateAsString (context) + strings [Operator] + Right.EvaluateAsString (context);
275 partial class BooleanLiteral : Expression
277 public override string EvaluateAsString (EvaluationContext context)
279 return Value ? "True" : "False";
282 public override bool EvaluateAsBoolean (EvaluationContext context)
287 public override object EvaluateAsObject (EvaluationContext context)
293 partial class NotExpression : Expression
295 public override string EvaluateAsString (EvaluationContext context)
297 // no negation for string
298 return "!" + Negated.EvaluateAsString (context);
301 public override bool EvaluateAsBoolean (EvaluationContext context)
303 return !Negated.EvaluateAsBoolean (context);
306 public override object EvaluateAsObject (EvaluationContext context)
308 return EvaluateAsString (context);
312 partial class PropertyAccessExpression : Expression
314 public override bool EvaluateAsBoolean (EvaluationContext context)
316 var ret = EvaluateAsString (context);
317 return EvaluateStringAsBoolean (context, ret);
320 public override string EvaluateAsString (EvaluationContext context)
322 var ret = EvaluateAsObject (context);
323 return ret == null ? null : ret.ToString ();
326 public override object EvaluateAsObject (EvaluationContext context)
329 return DoEvaluateAsObject (context);
330 } catch (TargetInvocationException ex) {
331 throw new InvalidProjectFileException ("Access to property caused an error", ex);
335 object DoEvaluateAsObject (EvaluationContext context)
337 if (Access.Target == null) {
338 return context.EvaluateProperty (Access.Name.Name);
340 if (this.Access.TargetType == PropertyTargetType.Object) {
341 var obj = Access.Target.EvaluateAsObject (context);
344 if (Access.Arguments != null) {
345 var args = Access.Arguments.Select (e => e.EvaluateAsObject (context)).ToArray ();
346 var method = FindMethod (obj.GetType (), Access.Name.Name, args);
348 throw new InvalidProjectFileException (Location, string.Format ("access to undefined method '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
349 return method.Invoke (obj, AdjustArgsForCall (method, args));
351 var prop = obj.GetType ().GetProperty (Access.Name.Name);
353 throw new InvalidProjectFileException (Location, string.Format ("access to undefined property '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
354 return prop.GetValue (obj, null);
357 var type = Type.GetType (Access.Target.EvaluateAsString (context));
359 throw new InvalidProjectFileException (Location, string.Format ("specified type '{0}' was not found", Access.Target.EvaluateAsString (context)));
360 if (Access.Arguments != null) {
361 var args = Access.Arguments.Select (e => e.EvaluateAsObject (context)).ToArray ();
362 var method = FindMethod (type, Access.Name.Name, args);
364 throw new InvalidProjectFileException (Location, string.Format ("access to undefined static method '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
365 return method.Invoke (null, AdjustArgsForCall (method, args));
367 var prop = type.GetProperty (Access.Name.Name);
369 throw new InvalidProjectFileException (Location, string.Format ("access to undefined static property '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
370 return prop.GetValue (null, null);
376 MethodInfo FindMethod (Type type, string name, object [] args)
378 var methods = type.GetMethods ().Where (m => {
381 var pl = m.GetParameters ();
382 if (pl.Length == args.Length)
384 // calling String.Format() with either set of arguments is valid:
385 // - three strings (two for varargs)
386 // - two strings (happen to be exact match)
387 // - one string (no varargs)
388 if (pl.Length > 0 && pl.Length - 1 <= args.Length &&
389 pl.Last ().GetCustomAttributesData ().Any (a => a.Constructor.DeclaringType == typeof (ParamArrayAttribute)))
393 if (methods.Count () == 1)
394 return methods.First ();
395 return args.Any (a => a == null) ?
396 type.GetMethod (name) :
397 type.GetMethod (name, args.Select (o => o.GetType ()).ToArray ());
400 object [] AdjustArgsForCall (MethodInfo m, object[] args)
402 if (m.GetParameters ().Length == args.Length + 1)
403 return args.Concat (new object[] {Array.CreateInstance (m.GetParameters ().Last ().ParameterType.GetElementType (), 0)}).ToArray ();
409 partial class ItemAccessExpression : Expression
411 public override bool EvaluateAsBoolean (EvaluationContext context)
413 return EvaluateStringAsBoolean (context, EvaluateAsString (context));
416 public override string EvaluateAsString (EvaluationContext context)
418 string itemType = Application.Name.Name;
419 var items = context.GetItems (itemType);
422 if (Application.Expressions == null)
423 return string.Join (";", items.Select (item => Unwrap (context.EvaluateItem (itemType, item))));
425 return string.Join (";", items.Select (item => {
426 context.ContextItem = item;
427 var ret = Unwrap (string.Concat (Application.Expressions.Select (e => e.EvaluateAsString (context))));
428 context.ContextItem = null;
433 static string Unwrap (string ret)
435 if (ret.Length < 2 || ret [0] != ret [ret.Length - 1] || ret [0] != '"' && ret [0] != '\'')
437 return ret.Substring (1, ret.Length - 2);
440 public override object EvaluateAsObject (EvaluationContext context)
442 return EvaluateAsString (context);
446 partial class MetadataAccessExpression : Expression
448 public override bool EvaluateAsBoolean (EvaluationContext context)
450 return EvaluateStringAsBoolean (context, EvaluateAsString (context));
453 public override string EvaluateAsString (EvaluationContext context)
455 string itemType = this.Access.ItemType != null ? this.Access.ItemType.Name : null;
456 string metadataName = Access.Metadata.Name;
457 IEnumerable<object> items;
458 if (this.Access.ItemType != null)
459 items = context.GetItems (itemType);
460 else if (context.ContextItem != null)
461 items = new Object [] { context.ContextItem };
463 items = context.GetAllItems ();
465 var values = items.Select (i => (i is ProjectItem) ? ((ProjectItem) i).GetMetadataValue (metadataName) : ((ProjectItemInstance) i).GetMetadataValue (metadataName)).Where (s => !string.IsNullOrEmpty (s));
466 return string.Join (";", values);
469 public override object EvaluateAsObject (EvaluationContext context)
471 return EvaluateAsString (context);
474 partial class StringLiteral : Expression
476 public override bool EvaluateAsBoolean (EvaluationContext context)
478 var ret = EvaluateAsString (context);
479 return EvaluateStringAsBoolean (context, ret);
482 public override string EvaluateAsString (EvaluationContext context)
484 return context.Evaluator.Evaluate (this.Value.Name);
487 public override object EvaluateAsObject (EvaluationContext context)
489 return EvaluateAsString (context);
492 partial class RawStringLiteral : Expression
494 public override string EvaluateAsString (EvaluationContext context)
499 public override bool EvaluateAsBoolean (EvaluationContext context)
501 throw new InvalidProjectFileException ("raw string literal cannot be evaluated as boolean");
504 public override object EvaluateAsObject (EvaluationContext context)
506 return EvaluateAsString (context);
510 partial class FunctionCallExpression : Expression
512 public override string EvaluateAsString (EvaluationContext context)
514 throw new NotImplementedException ();
517 public override bool EvaluateAsBoolean (EvaluationContext context)
519 if (string.Equals (Name.Name, "Exists", StringComparison.OrdinalIgnoreCase)) {
520 if (Arguments.Count != 1)
521 throw new InvalidProjectFileException (Location, "Function 'Exists' expects 1 argument");
522 string val = Arguments.First ().EvaluateAsString (context);
523 val = WindowsCompatibilityExtensions.FindMatchingPath (val);
524 return Directory.Exists (val) || System.IO.File.Exists (val);
526 if (string.Equals (Name.Name, "HasTrailingSlash", StringComparison.OrdinalIgnoreCase)) {
527 if (Arguments.Count != 1)
528 throw new InvalidProjectFileException (Location, "Function 'HasTrailingSlash' expects 1 argument");
529 string val = Arguments.First ().EvaluateAsString (context);
530 return val.LastOrDefault () == '\\' || val.LastOrDefault () == '/';
532 throw new InvalidProjectFileException (Location, string.Format ("Unsupported function '{0}'", Name));
535 public override object EvaluateAsObject (EvaluationContext context)
537 throw new NotImplementedException ();