[MS.Build] for item metadata access expression, remove surrounding ' and ".
[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                         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);
50                                 };
51                         */
52                 }
53                 
54                 public ExpressionEvaluator (ProjectInstance project)
55                 {
56                         ProjectInstance = project;
57                         /*
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);
62                                 };
63                         */
64                 }
65                 
66                 EvaluationContext CreateContext (string source)
67                 {
68                         return new EvaluationContext (source, this);
69                 }
70                 
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; }
75                 
76                 List<ITaskItem> evaluated_task_items = new List<ITaskItem> ();
77
78                 public IList<ITaskItem> EvaluatedTaskItems {
79                         get { return evaluated_task_items; }
80                 }
81
82                 public string Evaluate (string source)
83                 {
84                         return Evaluate (source, new ExpressionParserManual (source ?? string.Empty, ExpressionValidationType.LaxString).Parse ());
85                 }
86                 
87                 string Evaluate (string source, ExpressionList exprList)
88                 {
89                         if (exprList == null)
90                                 throw new ArgumentNullException ("exprList");
91                         return string.Concat (exprList.Select (e => e.EvaluateAsString (CreateContext (source))));
92                 }
93                 
94                 public bool EvaluateAsBoolean (string source)
95                 {
96                         try {
97                                 var el = new ExpressionParser ().Parse (source, ExpressionValidationType.StrictBoolean);
98                                 if (el.Count () != 1)
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);
103                         }
104                 }
105         }
106         
107         class EvaluationContext
108         {
109                 public EvaluationContext (string source, ExpressionEvaluator evaluator)
110                 {
111                         Source = source;
112                         Evaluator = evaluator;
113                 }
114
115                 public string Source { get; private set; }
116                 
117                 public ExpressionEvaluator Evaluator { get; private set; }
118                 public object ContextItem { get; set; }
119                 
120                 Stack<object> evaluating_items = new Stack<object> ();
121                 Stack<object> evaluating_props = new Stack<object> ();
122
123                 public IEnumerable<object> GetItems (string name)
124                 {
125                         if (Evaluator.Project != null)
126                                 return Evaluator.Project.GetItems (name);
127                         else
128                                 return Evaluator.ProjectInstance.GetItems (name);
129                 }
130
131                 public IEnumerable<object> GetAllItems ()
132                 {
133                         if (Evaluator.Project != null)
134                                 return Evaluator.Project.AllEvaluatedItems;
135                         else
136                                 return Evaluator.ProjectInstance.AllEvaluatedItems;
137                 }
138                 
139                 public string EvaluateItem (string itemType, object item)
140                 {
141                         if (evaluating_items.Contains (item))
142                                 throw new InvalidProjectFileException (string.Format ("Recursive reference to item '{0}' was found", itemType));
143                         try {
144                                 evaluating_items.Push (item);
145                                 var eval = item as ProjectItem;
146                                 if (eval != null)
147                                         return Evaluator.Evaluate (eval.EvaluatedInclude);
148                                 else {
149                                         var inst = (ProjectItemInstance) item;
150                                         if (!Evaluator.EvaluatedTaskItems.Contains (inst))
151                                                 Evaluator.EvaluatedTaskItems.Add (inst);
152                                         return Evaluator.Evaluate (inst.EvaluatedInclude);
153                                 }
154                         } finally {
155                                 evaluating_items.Pop ();
156                         }
157                 }
158                                 
159                 public string EvaluateProperty (string name)
160                 {
161                         if (Evaluator.Project != null) {
162                                 var prop = Evaluator.Project.GetProperty (name);
163                                 if (prop == null)
164                                         return null;
165                                 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
166                         } else {
167                                 var prop = Evaluator.ProjectInstance.GetProperty (name);
168                                 if (prop == null)
169                                         return null;
170                                 return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
171                         }
172                 }
173                 
174                 public string EvaluateProperty (object prop, string name, string value)
175                 {
176                         if (evaluating_props.Contains (prop))
177                                 throw new InvalidProjectFileException (string.Format ("Recursive reference to property '{0}' was found", name));
178                         try {
179                                 evaluating_props.Push (prop);
180                                 // FIXME: needs verification on whether string evaluation is appropriate or not.
181                                 return Evaluator.Evaluate (value);
182                         } finally {
183                                 evaluating_props.Pop ();
184                         }
185                 }
186         }
187         
188         abstract partial class Expression
189         {
190                 public abstract string EvaluateAsString (EvaluationContext context);
191                 public abstract bool EvaluateAsBoolean (EvaluationContext context);
192                 public abstract object EvaluateAsObject (EvaluationContext context);
193                 
194                 public bool EvaluateStringAsBoolean (EvaluationContext context, string ret)
195                 {
196                         if (ret != null) {
197                                 if (ret.Equals ("TRUE", StringComparison.InvariantCultureIgnoreCase))
198                                         return true;
199                                 else if (ret.Equals ("FALSE", StringComparison.InvariantCultureIgnoreCase))
200                                         return false;
201                         }
202                         throw new InvalidProjectFileException (this.Location, string.Format ("Condition '{0}' is evaluated as '{1}' and cannot be converted to boolean", context.Source, ret));
203                 }
204         }
205         
206         partial class BinaryExpression : Expression
207         {
208                 public override bool EvaluateAsBoolean (EvaluationContext context)
209                 {
210                         switch (Operator) {
211                         case Operator.EQ:
212                                 return string.Equals (StripStringWrap (Left.EvaluateAsString (context)), StripStringWrap (Right.EvaluateAsString (context)), StringComparison.OrdinalIgnoreCase);
213                         case Operator.NE:
214                                 return !string.Equals (StripStringWrap (Left.EvaluateAsString (context)), StripStringWrap (Right.EvaluateAsString (context)), StringComparison.OrdinalIgnoreCase);
215                         case Operator.And:
216                         case Operator.Or:
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);
221                         }
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);
228                         switch (Operator) {
229                         case Operator.GE:
230                                 return result >= 0;
231                         case Operator.GT:
232                                 return result > 0;
233                         case Operator.LE:
234                                 return result <= 0;
235                         case Operator.LT:
236                                 return result < 0;
237                         }
238                         throw new InvalidOperationException ();
239                 }
240                 
241                 string StripStringWrap (string s)
242                 {
243                         if (s == null)
244                                 return string.Empty;
245                         s = s.Trim ();
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);
250                         return s;
251                 }
252                 
253                 public override object EvaluateAsObject (EvaluationContext context)
254                 {
255                         throw new NotImplementedException ();
256                 }
257                 
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 "},
267                 };
268                 
269                 public override string EvaluateAsString (EvaluationContext context)
270                 {
271                         return Left.EvaluateAsString (context) + strings [Operator] + Right.EvaluateAsString (context);
272                 }
273         }
274         
275         partial class BooleanLiteral : Expression
276         {
277                 public override string EvaluateAsString (EvaluationContext context)
278                 {
279                         return Value ? "True" : "False";
280                 }
281                 
282                 public override bool EvaluateAsBoolean (EvaluationContext context)
283                 {
284                         return Value;
285                 }
286                 
287                 public override object EvaluateAsObject (EvaluationContext context)
288                 {
289                         return Value;
290                 }
291         }
292
293         partial class NotExpression : Expression
294         {
295                 public override string EvaluateAsString (EvaluationContext context)
296                 {
297                         // no negation for string
298                         return "!" + Negated.EvaluateAsString (context);
299                 }
300                 
301                 public override bool EvaluateAsBoolean (EvaluationContext context)
302                 {
303                         return !Negated.EvaluateAsBoolean (context);
304                 }
305                 
306                 public override object EvaluateAsObject (EvaluationContext context)
307                 {
308                         return EvaluateAsString (context);
309                 }
310         }
311
312         partial class PropertyAccessExpression : Expression
313         {
314                 public override bool EvaluateAsBoolean (EvaluationContext context)
315                 {
316                         var ret = EvaluateAsString (context);
317                         return EvaluateStringAsBoolean (context, ret);
318                 }
319                 
320                 public override string EvaluateAsString (EvaluationContext context)
321                 {
322                         var ret = EvaluateAsObject (context);
323                         return ret == null ? null : ret.ToString ();
324                 }
325                 
326                 public override object EvaluateAsObject (EvaluationContext context)
327                 {
328                         try {
329                                 return DoEvaluateAsObject (context);
330                         } catch (TargetInvocationException ex) {
331                                 throw new InvalidProjectFileException ("Access to property caused an error", ex);
332                         }
333                 }
334                 
335                 object DoEvaluateAsObject (EvaluationContext context)
336                 {
337                         if (Access.Target == null) {
338                                 return context.EvaluateProperty (Access.Name.Name);
339                         } else {
340                                 if (this.Access.TargetType == PropertyTargetType.Object) {
341                                         var obj = Access.Target.EvaluateAsObject (context);
342                                         if (obj == null)
343                                                 return null;
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);
347                                                 if (method == null)
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));
350                                         } else {
351                                                 var prop = obj.GetType ().GetProperty (Access.Name.Name);
352                                                 if (prop == null)
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);
355                                         }
356                                 } else {
357                                         var type = Type.GetType (Access.Target.EvaluateAsString (context));
358                                         if (type == null)
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);
363                                                 if (method == null)
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));
366                                         } else {
367                                                 var prop = type.GetProperty (Access.Name.Name);
368                                                 if (prop == null)
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);
371                                         }
372                                 }
373                         }
374                 }
375         
376                 MethodInfo FindMethod (Type type, string name, object [] args)
377                 {
378                         var methods = type.GetMethods ().Where (m => {
379                                 if (m.Name != name)
380                                         return false;
381                                 var pl = m.GetParameters ();
382                                 if (pl.Length == args.Length)
383                                         return true;
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)))
390                                         return true;
391                                 return false;
392                                 });
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 ());
398                 }
399                 
400                 object [] AdjustArgsForCall (MethodInfo m, object[] args)
401                 {
402                         if (m.GetParameters ().Length == args.Length + 1)
403                                 return args.Concat (new object[] {Array.CreateInstance (m.GetParameters ().Last ().ParameterType.GetElementType (), 0)}).ToArray ();
404                         else
405                                 return args;
406                 }
407         }
408
409         partial class ItemAccessExpression : Expression
410         {
411                 public override bool EvaluateAsBoolean (EvaluationContext context)
412                 {
413                         return EvaluateStringAsBoolean (context, EvaluateAsString (context));
414                 }
415                 
416                 public override string EvaluateAsString (EvaluationContext context)
417                 {
418                         string itemType = Application.Name.Name;
419                         var items = context.GetItems (itemType);
420                         if (!items.Any ())
421                                 return null;
422                         if (Application.Expressions == null)
423                                 return string.Join (";", items.Select (item => Unwrap (context.EvaluateItem (itemType, item))));
424                         else
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;
429                                         return ret;
430                                 }));
431                 }
432
433                 static string Unwrap (string ret)
434                 {
435                         if (ret.Length < 2 || ret [0] != ret [ret.Length - 1] || ret [0] != '"' && ret [0] != '\'')
436                                 return ret;
437                         return ret.Substring (1, ret.Length - 2);
438                 }
439
440                 public override object EvaluateAsObject (EvaluationContext context)
441                 {
442                         return EvaluateAsString (context);
443                 }
444         }
445
446         partial class MetadataAccessExpression : Expression
447         {
448                 public override bool EvaluateAsBoolean (EvaluationContext context)
449                 {
450                         return EvaluateStringAsBoolean (context, EvaluateAsString (context));
451                 }
452                 
453                 public override string EvaluateAsString (EvaluationContext context)
454                 {
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 };
462                         else
463                                 items = context.GetAllItems ();
464                         
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);
467                 }
468
469                 public override object EvaluateAsObject (EvaluationContext context)
470                 {
471                         return EvaluateAsString (context);
472                 }
473         }
474         partial class StringLiteral : Expression
475         {
476                 public override bool EvaluateAsBoolean (EvaluationContext context)
477                 {
478                         var ret = EvaluateAsString (context);
479                         return EvaluateStringAsBoolean (context, ret);
480                 }
481                 
482                 public override string EvaluateAsString (EvaluationContext context)
483                 {
484                         return context.Evaluator.Evaluate (this.Value.Name);
485                 }
486                 
487                 public override object EvaluateAsObject (EvaluationContext context)
488                 {
489                         return EvaluateAsString (context);
490                 }
491         }
492         partial class RawStringLiteral : Expression
493         {
494                 public override string EvaluateAsString (EvaluationContext context)
495                 {
496                         return Value.Name;
497                 }
498                 
499                 public override bool EvaluateAsBoolean (EvaluationContext context)
500                 {
501                         throw new InvalidProjectFileException ("raw string literal cannot be evaluated as boolean");
502                 }
503                 
504                 public override object EvaluateAsObject (EvaluationContext context)
505                 {
506                         return EvaluateAsString (context);
507                 }
508         }
509         
510         partial class FunctionCallExpression : Expression
511         {
512                 public override string EvaluateAsString (EvaluationContext context)
513                 {
514                         throw new NotImplementedException ();
515                 }
516                 
517                 public override bool EvaluateAsBoolean (EvaluationContext context)
518                 {
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);
525                         }
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 () == '/';
531                         }
532                         throw new InvalidProjectFileException (Location, string.Format ("Unsupported function '{0}'", Name));
533                 }
534                 
535                 public override object EvaluateAsObject (EvaluationContext context)
536                 {
537                         throw new NotImplementedException ();
538                 }
539         }
540 }
541