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, string replacementForMissingPropertyAndItem)
44 ReplacementForMissingPropertyAndItem = replacementForMissingPropertyAndItem;
47 GetItems = (name) => project.GetItems (name).Select (i => new KeyValuePair<string,string> (i.ItemType, i.EvaluatedInclude));
48 GetProperty = (name) => {
49 var prop = project.GetProperty (name);
50 return new KeyValuePair<string,string> (prop != null ? prop.Name : null, prop != null ? prop.EvaluatedValue : null);
55 public ExpressionEvaluator (ProjectInstance project, string replacementForMissingPropertyAndItem)
57 ReplacementForMissingPropertyAndItem = replacementForMissingPropertyAndItem;
58 ProjectInstance = project;
60 GetItems = (name) => project.GetItems (name).Select (i => new KeyValuePair<string,string> (i.ItemType, i.EvaluatedInclude));
61 GetProperty = (name) => {
62 var prop = project.GetProperty (name);
63 return new KeyValuePair<string,string> (prop != null ? prop.Name : null, prop != null ? prop.EvaluatedValue : null);
68 EvaluationContext CreateContext (string source)
70 return new EvaluationContext (source, this);
73 public Project Project { get; private set; }
74 public ProjectInstance ProjectInstance { get; set; }
75 //public Func<string,IEnumerable<KeyValuePair<string,string>>> GetItems { get; private set; }
76 //public Func<string,KeyValuePair<string,string>> GetProperty { get; private set; }
78 public string ReplacementForMissingPropertyAndItem { get; set; }
80 // it is to prevent sequential property value expansion in boolean expression
81 public string Wrapper {
82 get { return ReplacementForMissingPropertyAndItem != null ? "'" : null; }
85 public string Evaluate (string source)
87 return Evaluate (source, new ExpressionParserManual (source ?? string.Empty, ExpressionValidationType.LaxString).Parse ());
90 string Evaluate (string source, ExpressionList exprList)
93 throw new ArgumentNullException ("exprList");
94 return string.Concat (exprList.Select (e => e.EvaluateAsString (CreateContext (source))));
97 public bool EvaluateAsBoolean (string source)
100 var el = new ExpressionParser ().Parse (source, ExpressionValidationType.StrictBoolean);
101 if (el.Count () != 1)
102 throw new InvalidProjectFileException ("Unexpected number of tokens: " + el.Count ());
103 return el.First ().EvaluateAsBoolean (CreateContext (source));
104 } catch (yyParser.yyException ex) {
105 throw new InvalidProjectFileException (string.Format ("failed to evaluate expression as boolean: '{0}': {1}", source, ex.Message), ex);
110 class EvaluationContext
112 public EvaluationContext (string source, ExpressionEvaluator evaluator)
115 Evaluator = evaluator;
118 public string Source { get; private set; }
120 public ExpressionEvaluator Evaluator { get; private set; }
121 public object ContextItem { get; set; }
123 Stack<object> evaluating_items = new Stack<object> ();
124 Stack<object> evaluating_props = new Stack<object> ();
126 public IEnumerable<object> GetItems (string name)
128 if (Evaluator.Project != null)
129 return Evaluator.Project.GetItems (name);
131 return Evaluator.ProjectInstance.GetItems (name);
134 public IEnumerable<object> GetAllItems ()
136 if (Evaluator.Project != null)
137 return Evaluator.Project.AllEvaluatedItems;
139 return Evaluator.ProjectInstance.AllEvaluatedItems;
142 public string EvaluateItem (string itemType, object item)
144 if (evaluating_items.Contains (item))
145 throw new InvalidProjectFileException (string.Format ("Recursive reference to item '{0}' was found", itemType));
147 evaluating_items.Push (item);
148 var eval = item as ProjectItem;
150 return Evaluator.Evaluate (eval.EvaluatedInclude);
152 return Evaluator.Evaluate (((ProjectItemInstance) item).EvaluatedInclude);
154 evaluating_items.Pop ();
158 public string EvaluateProperty (string name)
160 if (Evaluator.Project != null) {
161 var prop = Evaluator.Project.GetProperty (name);
164 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
166 var prop = Evaluator.ProjectInstance.GetProperty (name);
169 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
173 public string EvaluateProperty (object prop, string name, string value)
175 if (evaluating_props.Contains (prop))
176 throw new InvalidProjectFileException (string.Format ("Recursive reference to property '{0}' was found", name));
178 evaluating_props.Push (prop);
179 // FIXME: needs verification on whether string evaluation is appropriate or not.
180 return Evaluator.Evaluate (value);
182 evaluating_props.Pop ();
187 abstract partial class Expression
189 public abstract string EvaluateAsString (EvaluationContext context);
190 public abstract bool EvaluateAsBoolean (EvaluationContext context);
191 public abstract object EvaluateAsObject (EvaluationContext context);
193 public bool EvaluateStringAsBoolean (EvaluationContext context, string ret)
196 if (ret.Equals ("TRUE", StringComparison.InvariantCultureIgnoreCase))
198 else if (ret.Equals ("FALSE", StringComparison.InvariantCultureIgnoreCase))
201 throw new InvalidProjectFileException (this.Location, string.Format ("Condition '{0}' is evaluated as '{1}' and cannot be converted to boolean", context.Source, ret));
205 partial class BinaryExpression : Expression
207 public override bool EvaluateAsBoolean (EvaluationContext context)
211 return string.Equals (Left.EvaluateAsString (context), Right.EvaluateAsString (context), StringComparison.OrdinalIgnoreCase);
213 return !string.Equals (Left.EvaluateAsString (context), Right.EvaluateAsString (context), StringComparison.OrdinalIgnoreCase);
216 // evaluate first, to detect possible syntax error on right expr.
217 var lb = Left.EvaluateAsBoolean (context);
218 var rb = Right.EvaluateAsBoolean (context);
219 return Operator == Operator.And ? (lb && rb) : (lb || rb);
221 // comparison expressions - evaluate comparable first, then compare values.
222 var left = Left.EvaluateAsObject (context);
223 var right = Right.EvaluateAsObject (context);
224 if (!(left is IComparable && right is IComparable))
225 throw new InvalidProjectFileException ("expression cannot be evaluated as boolean");
226 var result = ((IComparable) left).CompareTo (right);
237 throw new InvalidOperationException ();
240 public override object EvaluateAsObject (EvaluationContext context)
242 throw new NotImplementedException ();
245 static readonly Dictionary<Operator,string> strings = new Dictionary<Operator, string> () {
246 {Operator.EQ, " == "},
247 {Operator.NE, " != "},
248 {Operator.LT, " < "},
249 {Operator.LE, " <= "},
250 {Operator.GT, " > "},
251 {Operator.GE, " >= "},
252 {Operator.And, " And "},
253 {Operator.Or, " Or "},
256 public override string EvaluateAsString (EvaluationContext context)
258 return Left.EvaluateAsString (context) + strings [Operator] + Right.EvaluateAsString (context);
262 partial class BooleanLiteral : Expression
264 public override string EvaluateAsString (EvaluationContext context)
266 return Value ? "True" : "False";
269 public override bool EvaluateAsBoolean (EvaluationContext context)
274 public override object EvaluateAsObject (EvaluationContext context)
280 partial class NotExpression : Expression
282 public override string EvaluateAsString (EvaluationContext context)
284 // no negation for string
285 return "!" + Negated.EvaluateAsString (context);
288 public override bool EvaluateAsBoolean (EvaluationContext context)
290 return !Negated.EvaluateAsBoolean (context);
293 public override object EvaluateAsObject (EvaluationContext context)
295 return EvaluateAsString (context);
299 partial class PropertyAccessExpression : Expression
301 public override bool EvaluateAsBoolean (EvaluationContext context)
303 var ret = EvaluateAsString (context);
304 return EvaluateStringAsBoolean (context, ret);
307 public override string EvaluateAsString (EvaluationContext context)
309 var ret = EvaluateAsObject (context);
310 // FIXME: this "wrapper" is kind of hack, to prevent sequential property references such as $(X)$(Y).
311 return ret == null ? context.Evaluator.ReplacementForMissingPropertyAndItem : context.Evaluator.Wrapper + ret.ToString () + context.Evaluator.Wrapper;
314 public override object EvaluateAsObject (EvaluationContext context)
317 return DoEvaluateAsObject (context);
318 } catch (TargetInvocationException ex) {
319 throw new InvalidProjectFileException ("Access to property caused an error", ex);
323 object DoEvaluateAsObject (EvaluationContext context)
325 if (Access.Target == null) {
326 return context.EvaluateProperty (Access.Name.Name);
328 if (this.Access.TargetType == PropertyTargetType.Object) {
329 var obj = Access.Target.EvaluateAsObject (context);
332 if (Access.Arguments != null) {
333 var args = Access.Arguments.Select (e => e.EvaluateAsObject (context)).ToArray ();
334 var method = FindMethod (obj.GetType (), Access.Name.Name, args);
336 throw new InvalidProjectFileException (Location, string.Format ("access to undefined method '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
337 return method.Invoke (obj, AdjustArgsForCall (method, args));
339 var prop = obj.GetType ().GetProperty (Access.Name.Name);
341 throw new InvalidProjectFileException (Location, string.Format ("access to undefined property '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
342 return prop.GetValue (obj, null);
345 var type = Type.GetType (Access.Target.EvaluateAsString (context));
347 throw new InvalidProjectFileException (Location, string.Format ("specified type '{0}' was not found", Access.Target.EvaluateAsString (context)));
348 if (Access.Arguments != null) {
349 var args = Access.Arguments.Select (e => e.EvaluateAsObject (context)).ToArray ();
350 var method = FindMethod (type, Access.Name.Name, args);
352 throw new InvalidProjectFileException (Location, string.Format ("access to undefined static method '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
353 return method.Invoke (null, AdjustArgsForCall (method, args));
355 var prop = type.GetProperty (Access.Name.Name);
357 throw new InvalidProjectFileException (Location, string.Format ("access to undefined static property '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
358 return prop.GetValue (null, null);
364 MethodInfo FindMethod (Type type, string name, object [] args)
366 var methods = type.GetMethods ().Where (m => {
369 var pl = m.GetParameters ();
370 if (pl.Length == args.Length)
372 // calling String.Format() with either set of arguments is valid:
373 // - three strings (two for varargs)
374 // - two strings (happen to be exact match)
375 // - one string (no varargs)
376 if (pl.Length > 0 && pl.Length - 1 <= args.Length &&
377 pl.Last ().GetCustomAttributesData ().Any (a => a.Constructor.DeclaringType == typeof (ParamArrayAttribute)))
381 if (methods.Count () == 1)
382 return methods.First ();
383 return args.Any (a => a == null) ?
384 type.GetMethod (name) :
385 type.GetMethod (name, args.Select (o => o.GetType ()).ToArray ());
388 object [] AdjustArgsForCall (MethodInfo m, object[] args)
390 if (m.GetParameters ().Length == args.Length + 1)
391 return args.Concat (new object[] {Array.CreateInstance (m.GetParameters ().Last ().ParameterType.GetElementType (), 0)}).ToArray ();
397 partial class ItemAccessExpression : Expression
399 public override bool EvaluateAsBoolean (EvaluationContext context)
401 return EvaluateStringAsBoolean (context, EvaluateAsString (context));
404 public override string EvaluateAsString (EvaluationContext context)
406 string itemType = Application.Name.Name;
407 var items = context.GetItems (itemType);
409 return context.Evaluator.ReplacementForMissingPropertyAndItem;
410 if (Application.Expressions == null)
411 return string.Join (";", items.Select (item => context.EvaluateItem (itemType, item)));
413 return string.Join (";", items.Select (item => {
414 context.ContextItem = item;
415 var ret = string.Concat (Application.Expressions.Select (e => e.EvaluateAsString (context)));
416 context.ContextItem = null;
421 public override object EvaluateAsObject (EvaluationContext context)
423 return EvaluateAsString (context);
427 partial class MetadataAccessExpression : Expression
429 public override bool EvaluateAsBoolean (EvaluationContext context)
431 return EvaluateStringAsBoolean (context, EvaluateAsString (context));
434 public override string EvaluateAsString (EvaluationContext context)
436 string itemType = this.Access.ItemType != null ? this.Access.ItemType.Name : null;
437 string metadataName = Access.Metadata.Name;
438 IEnumerable<object> items;
439 if (this.Access.ItemType != null)
440 items = context.GetItems (itemType);
441 else if (context.ContextItem != null)
442 items = new Object [] { context.ContextItem };
444 items = context.GetAllItems ();
446 var values = items.Select (i => (i is ProjectItem) ? ((ProjectItem) i).GetMetadataValue (metadataName) : ((ProjectItemInstance) i).GetMetadataValue (metadataName)).Where (s => !string.IsNullOrEmpty (s));
447 return string.Join (";", values);
450 public override object EvaluateAsObject (EvaluationContext context)
452 return EvaluateAsString (context);
455 partial class StringLiteral : Expression
457 public override bool EvaluateAsBoolean (EvaluationContext context)
459 var ret = EvaluateAsString (context);
460 return EvaluateStringAsBoolean (context, ret);
463 public override string EvaluateAsString (EvaluationContext context)
465 return context.Evaluator.Evaluate (this.Value.Name);
468 public override object EvaluateAsObject (EvaluationContext context)
470 return EvaluateAsString (context);
473 partial class RawStringLiteral : Expression
475 public override string EvaluateAsString (EvaluationContext context)
480 public override bool EvaluateAsBoolean (EvaluationContext context)
482 throw new InvalidProjectFileException ("raw string literal cannot be evaluated as boolean");
485 public override object EvaluateAsObject (EvaluationContext context)
487 return EvaluateAsString (context);
491 partial class FunctionCallExpression : Expression
493 public override string EvaluateAsString (EvaluationContext context)
495 throw new NotImplementedException ();
498 public override bool EvaluateAsBoolean (EvaluationContext context)
500 if (string.Equals (Name.Name, "Exists", StringComparison.OrdinalIgnoreCase)) {
501 if (Arguments.Count != 1)
502 throw new InvalidProjectFileException (Location, "Function 'Exists' expects 1 argument");
503 string val = Arguments.First ().EvaluateAsString (context);
504 val = WindowsCompatibilityExtensions.NormalizeFilePath (val);
505 return Directory.Exists (val) || System.IO.File.Exists (val);
507 if (string.Equals (Name.Name, "HasTrailingSlash", StringComparison.OrdinalIgnoreCase)) {
508 if (Arguments.Count != 1)
509 throw new InvalidProjectFileException (Location, "Function 'HasTrailingSlash' expects 1 argument");
510 string val = Arguments.First ().EvaluateAsString (context);
511 return val.LastOrDefault () == '\\' || val.LastOrDefault () == '/';
513 throw new InvalidProjectFileException (Location, string.Format ("Unsupported function '{0}'", Name));
516 public override object EvaluateAsObject (EvaluationContext context)
518 throw new NotImplementedException ();