Merge pull request #1074 from esdrubal/bug18421
[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)
43                 {
44                         Project = project;
45                 }
46                 
47                 public ExpressionEvaluator (ProjectInstance project)
48                 {
49                         ProjectInstance = project;
50                 }
51                 
52                 EvaluationContext CreateContext (string source)
53                 {
54                         return new EvaluationContext (source, this);
55                 }
56                 
57                 public Project Project { get; private set; }
58                 public ProjectInstance ProjectInstance { get; set; }
59
60                 List<ITaskItem> evaluated_task_items = new List<ITaskItem> ();
61
62                 public IList<ITaskItem> EvaluatedTaskItems {
63                         get { return evaluated_task_items; }
64                 }
65
66                 public string Evaluate (string source)
67                 {
68                         return Evaluate (source, new ExpressionParserManual (source ?? string.Empty, ExpressionValidationType.LaxString).Parse ());
69                 }
70                 
71                 string Evaluate (string source, ExpressionList exprList)
72                 {
73                         if (exprList == null)
74                                 throw new ArgumentNullException ("exprList");
75                         return string.Concat (exprList.Select (e => e.EvaluateAsString (CreateContext (source))));
76                 }
77                 
78                 public bool EvaluateAsBoolean (string source)
79                 {
80                         try {
81                                 var el = new ExpressionParser ().Parse (source, ExpressionValidationType.StrictBoolean);
82                                 if (el.Count () != 1)
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);
87                         }
88                 }
89         }
90         
91         class EvaluationContext
92         {
93                 public EvaluationContext (string source, ExpressionEvaluator evaluator)
94                 {
95                         Source = source;
96                         Evaluator = evaluator;
97                 }
98
99                 public string Source { get; private set; }
100                 
101                 public ExpressionEvaluator Evaluator { get; private set; }
102                 public object ContextItem { get; set; }
103                 
104                 Stack<object> evaluating_items = new Stack<object> ();
105                 Stack<object> evaluating_props = new Stack<object> ();
106
107                 public IEnumerable<object> GetItems (string name)
108                 {
109                         if (Evaluator.Project != null)
110                                 return Evaluator.Project.GetItems (name);
111                         else
112                                 return Evaluator.ProjectInstance.GetItems (name);
113                 }
114
115                 public IEnumerable<object> GetAllItems ()
116                 {
117                         if (Evaluator.Project != null)
118                                 return Evaluator.Project.AllEvaluatedItems;
119                         else
120                                 return Evaluator.ProjectInstance.AllEvaluatedItems;
121                 }
122                 
123                 public string EvaluateItem (string itemType, object item)
124                 {
125                         if (evaluating_items.Contains (item))
126                                 throw new InvalidProjectFileException (string.Format ("Recursive reference to item '{0}' was found", itemType));
127                         try {
128                                 evaluating_items.Push (item);
129                                 var eval = item as ProjectItem;
130                                 if (eval != null)
131                                         return Evaluator.Evaluate (eval.EvaluatedInclude);
132                                 else {
133                                         var inst = (ProjectItemInstance) item;
134                                         if (!Evaluator.EvaluatedTaskItems.Contains (inst))
135                                                 Evaluator.EvaluatedTaskItems.Add (inst);
136                                         return Evaluator.Evaluate (inst.EvaluatedInclude);
137                                 }
138                         } finally {
139                                 evaluating_items.Pop ();
140                         }
141                 }
142                                 
143                 public string EvaluateProperty (string name)
144                 {
145                         if (Evaluator.Project != null) {
146                                 var prop = Evaluator.Project.GetProperty (name);
147                                 if (prop == null)
148                                         return null;
149                                 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
150                         } else {
151                                 var prop = Evaluator.ProjectInstance.GetProperty (name);
152                                 if (prop == null)
153                                         return null;
154                                 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
155                         }
156                 }
157                 
158                 public string EvaluateProperty (object prop, string name, string value)
159                 {
160                         if (evaluating_props.Contains (prop))
161                                 throw new InvalidProjectFileException (string.Format ("Recursive reference to property '{0}' was found", name));
162                         try {
163                                 evaluating_props.Push (prop);
164                                 // FIXME: needs verification on whether string evaluation is appropriate or not.
165                                 return Evaluator.Evaluate (value);
166                         } finally {
167                                 evaluating_props.Pop ();
168                         }
169                 }
170         }
171         
172         abstract partial class Expression
173         {
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);
178
179                 public bool EvaluateStringAsBoolean (EvaluationContext context, string ret)
180                 {
181                         if (ret != null) {
182                                 if (ret.Equals ("TRUE", StringComparison.InvariantCultureIgnoreCase))
183                                         return true;
184                                 else if (ret.Equals ("FALSE", StringComparison.InvariantCultureIgnoreCase))
185                                         return false;
186                         }
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));
188                 }
189         }
190         
191         partial class BinaryExpression : Expression
192         {
193                 public override bool EvaluateAsBoolean (EvaluationContext context)
194                 {
195                         switch (Operator) {
196                         case Operator.EQ:
197                                 return string.Equals (StripStringWrap (Left.EvaluateAsString (context)), StripStringWrap (Right.EvaluateAsString (context)), StringComparison.OrdinalIgnoreCase);
198                         case Operator.NE:
199                                 return !string.Equals (StripStringWrap (Left.EvaluateAsString (context)), StripStringWrap (Right.EvaluateAsString (context)), StringComparison.OrdinalIgnoreCase);
200                         case Operator.And:
201                         case Operator.Or:
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);
206                         }
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);
213                         switch (Operator) {
214                         case Operator.GE:
215                                 return result >= 0;
216                         case Operator.GT:
217                                 return result > 0;
218                         case Operator.LE:
219                                 return result <= 0;
220                         case Operator.LT:
221                                 return result < 0;
222                         }
223                         throw new InvalidOperationException ();
224                 }
225                 
226                 string StripStringWrap (string s)
227                 {
228                         if (s == null)
229                                 return string.Empty;
230                         s = s.Trim ();
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);
235                         return s;
236                 }
237                 
238                 public override object EvaluateAsObject (EvaluationContext context)
239                 {
240                         throw new NotImplementedException ();
241                 }
242                 
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 "},
252                 };
253                 
254                 public override string EvaluateAsString (EvaluationContext context)
255                 {
256                         return Left.EvaluateAsString (context) + strings [Operator] + Right.EvaluateAsString (context);
257                 }
258         }
259         
260         partial class BooleanLiteral : Expression
261         {
262                 public override string EvaluateAsString (EvaluationContext context)
263                 {
264                         return Value ? "True" : "False";
265                 }
266                 
267                 public override bool EvaluateAsBoolean (EvaluationContext context)
268                 {
269                         return Value;
270                 }
271                 
272                 public override object EvaluateAsObject (EvaluationContext context)
273                 {
274                         return Value;
275                 }
276         }
277
278         partial class NotExpression : Expression
279         {
280                 public override string EvaluateAsString (EvaluationContext context)
281                 {
282                         // no negation for string
283                         return "!" + Negated.EvaluateAsString (context);
284                 }
285                 
286                 public override bool EvaluateAsBoolean (EvaluationContext context)
287                 {
288                         return !Negated.EvaluateAsBoolean (context);
289                 }
290                 
291                 public override object EvaluateAsObject (EvaluationContext context)
292                 {
293                         return EvaluateAsString (context);
294                 }
295         }
296
297         partial class PropertyAccessExpression : Expression
298         {
299                 public override bool EvaluateAsBoolean (EvaluationContext context)
300                 {
301                         var ret = EvaluateAsString (context);
302                         return EvaluateStringAsBoolean (context, ret);
303                 }
304                 
305                 public override string EvaluateAsString (EvaluationContext context)
306                 {
307                         var ret = EvaluateAsObject (context);
308                         return ret == null ? null : ret.ToString ();
309                 }
310                 
311                 public override object EvaluateAsObject (EvaluationContext context)
312                 {
313                         try {
314                                 return DoEvaluateAsObject (context);
315                         } catch (TargetInvocationException ex) {
316                                 throw new InvalidProjectFileException ("Access to property caused an error", ex);
317                         }
318                 }
319                 
320                 object DoEvaluateAsObject (EvaluationContext context)
321                 {
322                         if (Access.Target == null) {
323                                 return context.EvaluateProperty (Access.Name.Name);
324                         } else {
325                                 if (this.Access.TargetType == PropertyTargetType.Object) {
326                                         var obj = Access.Target.EvaluateAsObject (context);
327                                         if (obj == null)
328                                                 return null;
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);
332                                                 if (method == null)
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));
335                                         } else {
336                                                 var prop = obj.GetType ().GetProperty (Access.Name.Name);
337                                                 if (prop == null)
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);
340                                         }
341                                 } else {
342                                         var type = Type.GetType (Access.Target.EvaluateAsString (context));
343                                         if (type == null)
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);
348                                                 if (method == null)
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));
351                                         } else {
352                                                 var prop = type.GetProperty (Access.Name.Name);
353                                                 if (prop == null)
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);
356                                         }
357                                 }
358                         }
359                 }
360         
361                 MethodInfo FindMethod (Type type, string name, object [] args)
362                 {
363                         var methods = type.GetMethods ().Where (m => {
364                                 if (m.Name != name)
365                                         return false;
366                                 var pl = m.GetParameters ();
367                                 if (pl.Length == args.Length)
368                                         return true;
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)))
375                                         return true;
376                                 return false;
377                                 });
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 ());
383                 }
384                 
385                 object [] AdjustArgsForCall (MethodInfo m, object[] args)
386                 {
387                         if (m.GetParameters ().Length == args.Length + 1)
388                                 return args.Concat (new object[] {Array.CreateInstance (m.GetParameters ().Last ().ParameterType.GetElementType (), 0)}).ToArray ();
389                         else
390                                 return args;
391                 }
392         }
393
394         partial class ItemAccessExpression : Expression
395         {
396                 public override bool EvaluateAsBoolean (EvaluationContext context)
397                 {
398                         return EvaluateStringAsBoolean (context, EvaluateAsString (context));
399                 }
400                 
401                 public override string EvaluateAsString (EvaluationContext context)
402                 {
403                         string itemType = Application.Name.Name;
404                         var items = context.GetItems (itemType);
405                         if (!items.Any ())
406                                 return null;
407                         if (Application.Expressions == null)
408                                 return string.Join (";", items.Select (item => Unwrap (context.EvaluateItem (itemType, item))));
409                         else
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;
414                                         return ret;
415                                 }));
416                 }
417
418                 static string Unwrap (string ret)
419                 {
420                         if (ret.Length < 2 || ret [0] != ret [ret.Length - 1] || ret [0] != '"' && ret [0] != '\'')
421                                 return ret;
422                         return ret.Substring (1, ret.Length - 2);
423                 }
424
425                 public override object EvaluateAsObject (EvaluationContext context)
426                 {
427                         return EvaluateAsString (context);
428                 }
429         }
430
431         partial class MetadataAccessExpression : Expression
432         {
433                 public override bool EvaluateAsBoolean (EvaluationContext context)
434                 {
435                         return EvaluateStringAsBoolean (context, EvaluateAsString (context));
436                 }
437                 
438                 public override string EvaluateAsString (EvaluationContext context)
439                 {
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 };
447                         else
448                                 items = context.GetAllItems ();
449                         
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);
452                 }
453
454                 public override object EvaluateAsObject (EvaluationContext context)
455                 {
456                         return EvaluateAsString (context);
457                 }
458         }
459         partial class StringLiteral : Expression
460         {
461                 public override bool EvaluateAsBoolean (EvaluationContext context)
462                 {
463                         var ret = EvaluateAsString (context);
464                         return EvaluateStringAsBoolean (context, ret);
465                 }
466                 
467                 public override string EvaluateAsString (EvaluationContext context)
468                 {
469                         return context.Evaluator.Evaluate (this.Value.Name);
470                 }
471                 
472                 public override object EvaluateAsObject (EvaluationContext context)
473                 {
474                         return EvaluateAsString (context);
475                 }
476         }
477         partial class RawStringLiteral : Expression
478         {
479                 public override string EvaluateAsString (EvaluationContext context)
480                 {
481                         return Value.Name;
482                 }
483                 
484                 public override bool EvaluateAsBoolean (EvaluationContext context)
485                 {
486                         throw new InvalidProjectFileException ("raw string literal cannot be evaluated as boolean");
487                 }
488                 
489                 public override object EvaluateAsObject (EvaluationContext context)
490                 {
491                         return EvaluateAsString (context);
492                 }
493         }
494
495         partial class QuotedExpression : Expression
496         {
497                 public override string EvaluateAsString (EvaluationContext context)
498                 {
499                         return QuoteChar + EvaluateAsStringWithoutQuote (context) + QuoteChar;
500                 }
501
502                 public string EvaluateAsStringWithoutQuote (EvaluationContext context)
503                 {
504                         return string.Concat (Contents.Select (e => e.EvaluateAsString (context)));
505                 }
506
507                 public override bool EvaluateAsBoolean (EvaluationContext context)
508                 {
509                         var ret = EvaluateAsStringWithoutQuote (context);
510                         return EvaluateStringAsBoolean (context, ret);
511                 }
512
513                 public override object EvaluateAsObject (EvaluationContext context)
514                 {
515                         return EvaluateAsStringWithoutQuote (context);
516                 }
517         }
518         
519         partial class FunctionCallExpression : Expression
520         {
521                 public override string EvaluateAsString (EvaluationContext context)
522                 {
523                         throw new NotImplementedException ();
524                 }
525                 
526                 public override bool EvaluateAsBoolean (EvaluationContext context)
527                 {
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);
534                         }
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 () == '/';
540                         }
541                         throw new InvalidProjectFileException (Location, string.Format ("Unsupported function '{0}'", Name));
542                 }
543                 
544                 public override object EvaluateAsObject (EvaluationContext context)
545                 {
546                         throw new NotImplementedException ();
547                 }
548         }
549 }
550