Merge pull request #900 from Blewzman/FixAggregateExceptionGetBaseException
[mono.git] / mcs / class / Microsoft.Build / Microsoft.Build.Internal / ExpressionEvaluator.cs
1 //
2 // ExpressionEvaluator.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.Linq;
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;
36 using System.IO;
37
38 namespace Microsoft.Build.Internal.Expressions
39 {
40         class ExpressionEvaluator
41         {
42                 public ExpressionEvaluator (Project project, string replacementForMissingPropertyAndItem)
43                 {
44                         ReplacementForMissingPropertyAndItem = replacementForMissingPropertyAndItem;
45                         Project = project;
46                         /*
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);
51                                 };
52                         */
53                 }
54                 
55                 public ExpressionEvaluator (ProjectInstance project, string replacementForMissingPropertyAndItem)
56                 {
57                         ReplacementForMissingPropertyAndItem = replacementForMissingPropertyAndItem;
58                         ProjectInstance = project;
59                         /*
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);
64                                 };
65                         */
66                 }
67                 
68                 EvaluationContext CreateContext (string source)
69                 {
70                         return new EvaluationContext (source, this);
71                 }
72                 
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; }
77                 
78                 public string ReplacementForMissingPropertyAndItem { get; set; }
79                 
80                 // it is to prevent sequential property value expansion in boolean expression
81                 public string Wrapper {
82                         get { return ReplacementForMissingPropertyAndItem != null ? "'" : null; }
83                 }
84                 
85                 public string Evaluate (string source)
86                 {
87                         return Evaluate (source, new ExpressionParserManual (source ?? string.Empty, ExpressionValidationType.LaxString).Parse ());
88                 }
89                 
90                 string Evaluate (string source, ExpressionList exprList)
91                 {
92                         if (exprList == null)
93                                 throw new ArgumentNullException ("exprList");
94                         return string.Concat (exprList.Select (e => e.EvaluateAsString (CreateContext (source))));
95                 }
96                 
97                 public bool EvaluateAsBoolean (string source)
98                 {
99                         try {
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);
106                         }
107                 }
108         }
109         
110         class EvaluationContext
111         {
112                 public EvaluationContext (string source, ExpressionEvaluator evaluator)
113                 {
114                         Source = source;
115                         Evaluator = evaluator;
116                 }
117
118                 public string Source { get; private set; }
119                 
120                 public ExpressionEvaluator Evaluator { get; private set; }
121                 public object ContextItem { get; set; }
122                 
123                 Stack<object> evaluating_items = new Stack<object> ();
124                 Stack<object> evaluating_props = new Stack<object> ();
125                 
126                 public IEnumerable<object> GetItems (string name)
127                 {
128                         if (Evaluator.Project != null)
129                                 return Evaluator.Project.GetItems (name);
130                         else
131                                 return Evaluator.ProjectInstance.GetItems (name);
132                 }
133
134                 public IEnumerable<object> GetAllItems ()
135                 {
136                         if (Evaluator.Project != null)
137                                 return Evaluator.Project.AllEvaluatedItems;
138                         else
139                                 return Evaluator.ProjectInstance.AllEvaluatedItems;
140                 }
141                 
142                 public string EvaluateItem (string itemType, object item)
143                 {
144                         if (evaluating_items.Contains (item))
145                                 throw new InvalidProjectFileException (string.Format ("Recursive reference to item '{0}' was found", itemType));
146                         try {
147                                 evaluating_items.Push (item);
148                                 var eval = item as ProjectItem;
149                                 if (eval != null)
150                                         return Evaluator.Evaluate (eval.EvaluatedInclude);
151                                 else
152                                         return Evaluator.Evaluate (((ProjectItemInstance) item).EvaluatedInclude);
153                         } finally {
154                                 evaluating_items.Pop ();
155                         }
156                 }
157                                 
158                 public string EvaluateProperty (string name)
159                 {
160                         if (Evaluator.Project != null) {
161                                 var prop = Evaluator.Project.GetProperty (name);
162                                 if (prop == null)
163                                         return null;
164                                 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
165                         } else {
166                                 var prop = Evaluator.ProjectInstance.GetProperty (name);
167                                 if (prop == null)
168                                         return null;
169                                 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
170                         }
171                 }
172                 
173                 public string EvaluateProperty (object prop, string name, string value)
174                 {
175                         if (evaluating_props.Contains (prop))
176                                 throw new InvalidProjectFileException (string.Format ("Recursive reference to property '{0}' was found", name));
177                         try {
178                                 evaluating_props.Push (prop);
179                                 // FIXME: needs verification on whether string evaluation is appropriate or not.
180                                 return Evaluator.Evaluate (value);
181                         } finally {
182                                 evaluating_props.Pop ();
183                         }
184                 }
185         }
186         
187         abstract partial class Expression
188         {
189                 public abstract string EvaluateAsString (EvaluationContext context);
190                 public abstract bool EvaluateAsBoolean (EvaluationContext context);
191                 public abstract object EvaluateAsObject (EvaluationContext context);
192                 
193                 public bool EvaluateStringAsBoolean (EvaluationContext context, string ret)
194                 {
195                         if (ret != null) {
196                                 if (ret.Equals ("TRUE", StringComparison.InvariantCultureIgnoreCase))
197                                         return true;
198                                 else if (ret.Equals ("FALSE", StringComparison.InvariantCultureIgnoreCase))
199                                         return false;
200                         }
201                         throw new InvalidProjectFileException (this.Location, string.Format ("Condition '{0}' is evaluated as '{1}' and cannot be converted to boolean", context.Source, ret));
202                 }
203         }
204         
205         partial class BinaryExpression : Expression
206         {
207                 public override bool EvaluateAsBoolean (EvaluationContext context)
208                 {
209                         switch (Operator) {
210                         case Operator.EQ:
211                                 return string.Equals (Left.EvaluateAsString (context), Right.EvaluateAsString (context), StringComparison.OrdinalIgnoreCase);
212                         case Operator.NE:
213                                 return !string.Equals (Left.EvaluateAsString (context), Right.EvaluateAsString (context), StringComparison.OrdinalIgnoreCase);
214                         case Operator.And:
215                         case Operator.Or:
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);
220                         }
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);
227                         switch (Operator) {
228                         case Operator.GE:
229                                 return result >= 0;
230                         case Operator.GT:
231                                 return result > 0;
232                         case Operator.LE:
233                                 return result <= 0;
234                         case Operator.LT:
235                                 return result < 0;
236                         }
237                         throw new InvalidOperationException ();
238                 }
239                 
240                 public override object EvaluateAsObject (EvaluationContext context)
241                 {
242                         throw new NotImplementedException ();
243                 }
244                 
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 "},
254                 };
255                 
256                 public override string EvaluateAsString (EvaluationContext context)
257                 {
258                         return Left.EvaluateAsString (context) + strings [Operator] + Right.EvaluateAsString (context);
259                 }
260         }
261         
262         partial class BooleanLiteral : Expression
263         {
264                 public override string EvaluateAsString (EvaluationContext context)
265                 {
266                         return Value ? "True" : "False";
267                 }
268                 
269                 public override bool EvaluateAsBoolean (EvaluationContext context)
270                 {
271                         return Value;
272                 }
273                 
274                 public override object EvaluateAsObject (EvaluationContext context)
275                 {
276                         return Value;
277                 }
278         }
279
280         partial class NotExpression : Expression
281         {
282                 public override string EvaluateAsString (EvaluationContext context)
283                 {
284                         // no negation for string
285                         return "!" + Negated.EvaluateAsString (context);
286                 }
287                 
288                 public override bool EvaluateAsBoolean (EvaluationContext context)
289                 {
290                         return !Negated.EvaluateAsBoolean (context);
291                 }
292                 
293                 public override object EvaluateAsObject (EvaluationContext context)
294                 {
295                         return EvaluateAsString (context);
296                 }
297         }
298
299         partial class PropertyAccessExpression : Expression
300         {
301                 public override bool EvaluateAsBoolean (EvaluationContext context)
302                 {
303                         var ret = EvaluateAsString (context);
304                         return EvaluateStringAsBoolean (context, ret);
305                 }
306                 
307                 public override string EvaluateAsString (EvaluationContext context)
308                 {
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;
312                 }
313                 
314                 public override object EvaluateAsObject (EvaluationContext context)
315                 {
316                         try {
317                                 return DoEvaluateAsObject (context);
318                         } catch (TargetInvocationException ex) {
319                                 throw new InvalidProjectFileException ("Access to property caused an error", ex);
320                         }
321                 }
322                 
323                 object DoEvaluateAsObject (EvaluationContext context)
324                 {
325                         if (Access.Target == null) {
326                                 return context.EvaluateProperty (Access.Name.Name);
327                         } else {
328                                 if (this.Access.TargetType == PropertyTargetType.Object) {
329                                         var obj = Access.Target.EvaluateAsObject (context);
330                                         if (obj == null)
331                                                 return null;
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);
335                                                 if (method == null)
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));
338                                         } else {
339                                                 var prop = obj.GetType ().GetProperty (Access.Name.Name);
340                                                 if (prop == null)
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);
343                                         }
344                                 } else {
345                                         var type = Type.GetType (Access.Target.EvaluateAsString (context));
346                                         if (type == null)
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);
351                                                 if (method == null)
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));
354                                         } else {
355                                                 var prop = type.GetProperty (Access.Name.Name);
356                                                 if (prop == null)
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);
359                                         }
360                                 }
361                         }
362                 }
363         
364                 MethodInfo FindMethod (Type type, string name, object [] args)
365                 {
366                         var methods = type.GetMethods ().Where (m => {
367                                 if (m.Name != name)
368                                         return false;
369                                 var pl = m.GetParameters ();
370                                 if (pl.Length == args.Length)
371                                         return true;
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)))
378                                         return true;
379                                 return false;
380                                 });
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 ());
386                 }
387                 
388                 object [] AdjustArgsForCall (MethodInfo m, object[] args)
389                 {
390                         if (m.GetParameters ().Length == args.Length + 1)
391                                 return args.Concat (new object[] {Array.CreateInstance (m.GetParameters ().Last ().ParameterType.GetElementType (), 0)}).ToArray ();
392                         else
393                                 return args;
394                 }
395         }
396
397         partial class ItemAccessExpression : Expression
398         {
399                 public override bool EvaluateAsBoolean (EvaluationContext context)
400                 {
401                         return EvaluateStringAsBoolean (context, EvaluateAsString (context));
402                 }
403                 
404                 public override string EvaluateAsString (EvaluationContext context)
405                 {
406                         string itemType = Application.Name.Name;
407                         var items = context.GetItems (itemType);
408                         if (!items.Any ())
409                                 return context.Evaluator.ReplacementForMissingPropertyAndItem;
410                         if (Application.Expressions == null)
411                                 return string.Join (";", items.Select (item => context.EvaluateItem (itemType, item)));
412                         else
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;
417                                         return ret;
418                                         }));
419                 }
420                 
421                 public override object EvaluateAsObject (EvaluationContext context)
422                 {
423                         return EvaluateAsString (context);
424                 }
425         }
426
427         partial class MetadataAccessExpression : Expression
428         {
429                 public override bool EvaluateAsBoolean (EvaluationContext context)
430                 {
431                         return EvaluateStringAsBoolean (context, EvaluateAsString (context));
432                 }
433                 
434                 public override string EvaluateAsString (EvaluationContext context)
435                 {
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 };
443                         else
444                                 items = context.GetAllItems ();
445                         
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);
448                 }
449                 
450                 public override object EvaluateAsObject (EvaluationContext context)
451                 {
452                         return EvaluateAsString (context);
453                 }
454         }
455         partial class StringLiteral : Expression
456         {
457                 public override bool EvaluateAsBoolean (EvaluationContext context)
458                 {
459                         var ret = EvaluateAsString (context);
460                         return EvaluateStringAsBoolean (context, ret);
461                 }
462                 
463                 public override string EvaluateAsString (EvaluationContext context)
464                 {
465                         return context.Evaluator.Evaluate (this.Value.Name);
466                 }
467                 
468                 public override object EvaluateAsObject (EvaluationContext context)
469                 {
470                         return EvaluateAsString (context);
471                 }
472         }
473         partial class RawStringLiteral : Expression
474         {
475                 public override string EvaluateAsString (EvaluationContext context)
476                 {
477                         return Value.Name;
478                 }
479                 
480                 public override bool EvaluateAsBoolean (EvaluationContext context)
481                 {
482                         throw new InvalidProjectFileException ("raw string literal cannot be evaluated as boolean");
483                 }
484                 
485                 public override object EvaluateAsObject (EvaluationContext context)
486                 {
487                         return EvaluateAsString (context);
488                 }
489         }
490         
491         partial class FunctionCallExpression : Expression
492         {
493                 public override string EvaluateAsString (EvaluationContext context)
494                 {
495                         throw new NotImplementedException ();
496                 }
497                 
498                 public override bool EvaluateAsBoolean (EvaluationContext context)
499                 {
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);
506                         }
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 () == '/';
512                         }
513                         throw new InvalidProjectFileException (Location, string.Format ("Unsupported function '{0}'", Name));
514                 }
515                 
516                 public override object EvaluateAsObject (EvaluationContext context)
517                 {
518                         throw new NotImplementedException ();
519                 }
520         }
521 }
522