Jo Shields fe777c5c82 Imported Upstream version 3.8.0
Former-commit-id: 6a76a29bd07d86e57c6c8da45c65ed5447d38a61
2014-09-04 09:07:35 +01:00

551 lines
18 KiB
C#

//
// ExpressionEvaluator.cs
//
// Author:
// Atsushi Enomoto (atsushi@xamarin.com)
//
// Copyright (C) 2013 Xamarin Inc. (http://www.xamarin.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using System;
using System.Linq;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Exceptions;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using System.IO;
namespace Microsoft.Build.Internal.Expressions
{
class ExpressionEvaluator
{
public ExpressionEvaluator (Project project)
{
Project = project;
}
public ExpressionEvaluator (ProjectInstance project)
{
ProjectInstance = project;
}
EvaluationContext CreateContext (string source)
{
return new EvaluationContext (source, this);
}
public Project Project { get; private set; }
public ProjectInstance ProjectInstance { get; set; }
List<ITaskItem> evaluated_task_items = new List<ITaskItem> ();
public IList<ITaskItem> EvaluatedTaskItems {
get { return evaluated_task_items; }
}
public string Evaluate (string source)
{
return Evaluate (source, new ExpressionParserManual (source ?? string.Empty, ExpressionValidationType.LaxString).Parse ());
}
string Evaluate (string source, ExpressionList exprList)
{
if (exprList == null)
throw new ArgumentNullException ("exprList");
return string.Concat (exprList.Select (e => e.EvaluateAsString (CreateContext (source))));
}
public bool EvaluateAsBoolean (string source)
{
try {
var el = new ExpressionParser ().Parse (source, ExpressionValidationType.StrictBoolean);
if (el.Count () != 1)
throw new InvalidProjectFileException ("Unexpected number of tokens: " + el.Count ());
return el.First ().EvaluateAsBoolean (CreateContext (source));
} catch (yyParser.yyException ex) {
throw new InvalidProjectFileException (string.Format ("failed to evaluate expression as boolean: '{0}': {1}", source, ex.Message), ex);
}
}
}
class EvaluationContext
{
public EvaluationContext (string source, ExpressionEvaluator evaluator)
{
Source = source;
Evaluator = evaluator;
}
public string Source { get; private set; }
public ExpressionEvaluator Evaluator { get; private set; }
public object ContextItem { get; set; }
Stack<object> evaluating_items = new Stack<object> ();
Stack<object> evaluating_props = new Stack<object> ();
public IEnumerable<object> GetItems (string name)
{
if (Evaluator.Project != null)
return Evaluator.Project.GetItems (name);
else
return Evaluator.ProjectInstance.GetItems (name);
}
public IEnumerable<object> GetAllItems ()
{
if (Evaluator.Project != null)
return Evaluator.Project.AllEvaluatedItems;
else
return Evaluator.ProjectInstance.AllEvaluatedItems;
}
public string EvaluateItem (string itemType, object item)
{
if (evaluating_items.Contains (item))
throw new InvalidProjectFileException (string.Format ("Recursive reference to item '{0}' was found", itemType));
try {
evaluating_items.Push (item);
var eval = item as ProjectItem;
if (eval != null)
return Evaluator.Evaluate (eval.EvaluatedInclude);
else {
var inst = (ProjectItemInstance) item;
if (!Evaluator.EvaluatedTaskItems.Contains (inst))
Evaluator.EvaluatedTaskItems.Add (inst);
return Evaluator.Evaluate (inst.EvaluatedInclude);
}
} finally {
evaluating_items.Pop ();
}
}
public string EvaluateProperty (string name)
{
if (Evaluator.Project != null) {
var prop = Evaluator.Project.GetProperty (name);
if (prop == null)
return null;
return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
} else {
var prop = Evaluator.ProjectInstance.GetProperty (name);
if (prop == null)
return null;
return EvaluateProperty (prop, prop.Name, prop.EvaluatedValue);
}
}
public string EvaluateProperty (object prop, string name, string value)
{
if (evaluating_props.Contains (prop))
throw new InvalidProjectFileException (string.Format ("Recursive reference to property '{0}' was found", name));
try {
evaluating_props.Push (prop);
// FIXME: needs verification on whether string evaluation is appropriate or not.
return Evaluator.Evaluate (value);
} finally {
evaluating_props.Pop ();
}
}
}
abstract partial class Expression
{
public abstract string ExpressionString { get; }
public abstract string EvaluateAsString (EvaluationContext context);
public abstract bool EvaluateAsBoolean (EvaluationContext context);
public abstract object EvaluateAsObject (EvaluationContext context);
public bool EvaluateStringAsBoolean (EvaluationContext context, string ret)
{
if (ret != null) {
if (ret.Equals ("TRUE", StringComparison.InvariantCultureIgnoreCase))
return true;
else if (ret.Equals ("FALSE", StringComparison.InvariantCultureIgnoreCase))
return false;
}
throw new InvalidProjectFileException (this.Location, string.Format ("Part of condition '{0}' is evaluated as '{1}' and cannot be converted to boolean", context.Source, ret));
}
}
partial class BinaryExpression : Expression
{
public override bool EvaluateAsBoolean (EvaluationContext context)
{
switch (Operator) {
case Operator.EQ:
return string.Equals (StripStringWrap (Left.EvaluateAsString (context)), StripStringWrap (Right.EvaluateAsString (context)), StringComparison.OrdinalIgnoreCase);
case Operator.NE:
return !string.Equals (StripStringWrap (Left.EvaluateAsString (context)), StripStringWrap (Right.EvaluateAsString (context)), StringComparison.OrdinalIgnoreCase);
case Operator.And:
case Operator.Or:
// evaluate first, to detect possible syntax error on right expr.
var lb = Left.EvaluateAsBoolean (context);
var rb = Right.EvaluateAsBoolean (context);
return Operator == Operator.And ? (lb && rb) : (lb || rb);
}
// comparison expressions - evaluate comparable first, then compare values.
var left = Left.EvaluateAsObject (context);
var right = Right.EvaluateAsObject (context);
if (!(left is IComparable && right is IComparable))
throw new InvalidProjectFileException ("expression cannot be evaluated as boolean");
var result = ((IComparable) left).CompareTo (right);
switch (Operator) {
case Operator.GE:
return result >= 0;
case Operator.GT:
return result > 0;
case Operator.LE:
return result <= 0;
case Operator.LT:
return result < 0;
}
throw new InvalidOperationException ();
}
string StripStringWrap (string s)
{
if (s == null)
return string.Empty;
s = s.Trim ();
if (s.Length > 1 && s [0] == '"' && s [s.Length - 1] == '"')
return s.Substring (1, s.Length - 2);
else if (s.Length > 1 && s [0] == '\'' && s [s.Length - 1] == '\'')
return s.Substring (1, s.Length - 2);
return s;
}
public override object EvaluateAsObject (EvaluationContext context)
{
throw new NotImplementedException ();
}
static readonly Dictionary<Operator,string> strings = new Dictionary<Operator, string> () {
{Operator.EQ, " == "},
{Operator.NE, " != "},
{Operator.LT, " < "},
{Operator.LE, " <= "},
{Operator.GT, " > "},
{Operator.GE, " >= "},
{Operator.And, " And "},
{Operator.Or, " Or "},
};
public override string EvaluateAsString (EvaluationContext context)
{
return Left.EvaluateAsString (context) + strings [Operator] + Right.EvaluateAsString (context);
}
}
partial class BooleanLiteral : Expression
{
public override string EvaluateAsString (EvaluationContext context)
{
return Value ? "True" : "False";
}
public override bool EvaluateAsBoolean (EvaluationContext context)
{
return Value;
}
public override object EvaluateAsObject (EvaluationContext context)
{
return Value;
}
}
partial class NotExpression : Expression
{
public override string EvaluateAsString (EvaluationContext context)
{
// no negation for string
return "!" + Negated.EvaluateAsString (context);
}
public override bool EvaluateAsBoolean (EvaluationContext context)
{
return !Negated.EvaluateAsBoolean (context);
}
public override object EvaluateAsObject (EvaluationContext context)
{
return EvaluateAsString (context);
}
}
partial class PropertyAccessExpression : Expression
{
public override bool EvaluateAsBoolean (EvaluationContext context)
{
var ret = EvaluateAsString (context);
return EvaluateStringAsBoolean (context, ret);
}
public override string EvaluateAsString (EvaluationContext context)
{
var ret = EvaluateAsObject (context);
return ret == null ? null : ret.ToString ();
}
public override object EvaluateAsObject (EvaluationContext context)
{
try {
return DoEvaluateAsObject (context);
} catch (TargetInvocationException ex) {
throw new InvalidProjectFileException ("Access to property caused an error", ex);
}
}
object DoEvaluateAsObject (EvaluationContext context)
{
if (Access.Target == null) {
return context.EvaluateProperty (Access.Name.Name);
} else {
if (this.Access.TargetType == PropertyTargetType.Object) {
var obj = Access.Target.EvaluateAsObject (context);
if (obj == null)
return null;
if (Access.Arguments != null) {
var args = Access.Arguments.Select (e => e.EvaluateAsObject (context)).ToArray ();
var method = FindMethod (obj.GetType (), Access.Name.Name, args);
if (method == null)
throw new InvalidProjectFileException (Location, string.Format ("access to undefined method '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
return method.Invoke (obj, AdjustArgsForCall (method, args));
} else {
var prop = obj.GetType ().GetProperty (Access.Name.Name);
if (prop == null)
throw new InvalidProjectFileException (Location, string.Format ("access to undefined property '{0}' of '{1}' at {2}", Access.Name.Name, Access.Target.EvaluateAsString (context), Location));
return prop.GetValue (obj, null);
}
} else {
var type = Type.GetType (Access.Target.EvaluateAsString (context));
if (type == null)
throw new InvalidProjectFileException (Location, string.Format ("specified type '{0}' was not found", Access.Target.EvaluateAsString (context)));
if (Access.Arguments != null) {
var args = Access.Arguments.Select (e => e.EvaluateAsObject (context)).ToArray ();
var method = FindMethod (type, Access.Name.Name, args);
if (method == null)
throw new InvalidProjectFileException (Location, string.Format ("access to undefined static method '{0}' of '{1}' at {2}", Access.Name.Name, type, Location));
return method.Invoke (null, AdjustArgsForCall (method, args));
} else {
var prop = type.GetProperty (Access.Name.Name);
if (prop == null)
throw new InvalidProjectFileException (Location, string.Format ("access to undefined static property '{0}' of '{1}' at {2}", Access.Name.Name, type, Location));
return prop.GetValue (null, null);
}
}
}
}
MethodInfo FindMethod (Type type, string name, object [] args)
{
var methods = type.GetMethods ().Where (m => {
if (m.Name != name)
return false;
var pl = m.GetParameters ();
if (pl.Length == args.Length)
return true;
// calling String.Format() with either set of arguments is valid:
// - three strings (two for varargs)
// - two strings (happen to be exact match)
// - one string (no varargs)
if (pl.Length > 0 && pl.Length - 1 <= args.Length &&
pl.Last ().GetCustomAttributesData ().Any (a => a.Constructor.DeclaringType == typeof (ParamArrayAttribute)))
return true;
return false;
});
if (methods.Count () == 1)
return methods.First ();
return args.Any (a => a == null) ?
type.GetMethod (name) :
type.GetMethod (name, args.Select (o => o.GetType ()).ToArray ());
}
object [] AdjustArgsForCall (MethodInfo m, object[] args)
{
if (m.GetParameters ().Length == args.Length + 1)
return args.Concat (new object[] {Array.CreateInstance (m.GetParameters ().Last ().ParameterType.GetElementType (), 0)}).ToArray ();
else
return args;
}
}
partial class ItemAccessExpression : Expression
{
public override bool EvaluateAsBoolean (EvaluationContext context)
{
return EvaluateStringAsBoolean (context, EvaluateAsString (context));
}
public override string EvaluateAsString (EvaluationContext context)
{
string itemType = Application.Name.Name;
var items = context.GetItems (itemType);
if (!items.Any ())
return null;
if (Application.Expressions == null)
return string.Join (";", items.Select (item => Unwrap (context.EvaluateItem (itemType, item))));
else
return string.Join (";", items.Select (item => {
context.ContextItem = item;
var ret = Unwrap (string.Concat (Application.Expressions.Select (e => e.EvaluateAsString (context))));
context.ContextItem = null;
return ret;
}));
}
static string Unwrap (string ret)
{
if (ret.Length < 2 || ret [0] != ret [ret.Length - 1] || ret [0] != '"' && ret [0] != '\'')
return ret;
return ret.Substring (1, ret.Length - 2);
}
public override object EvaluateAsObject (EvaluationContext context)
{
return EvaluateAsString (context);
}
}
partial class MetadataAccessExpression : Expression
{
public override bool EvaluateAsBoolean (EvaluationContext context)
{
return EvaluateStringAsBoolean (context, EvaluateAsString (context));
}
public override string EvaluateAsString (EvaluationContext context)
{
string itemType = this.Access.ItemType != null ? this.Access.ItemType.Name : null;
string metadataName = Access.Metadata.Name;
IEnumerable<object> items;
if (this.Access.ItemType != null)
items = context.GetItems (itemType);
else if (context.ContextItem != null)
items = new Object [] { context.ContextItem };
else
items = context.GetAllItems ();
var values = items.Select (i => (i is ProjectItem) ? ((ProjectItem) i).GetMetadataValue (metadataName) : ((ProjectItemInstance) i).GetMetadataValue (metadataName)).Where (s => !string.IsNullOrEmpty (s));
return string.Join (";", values);
}
public override object EvaluateAsObject (EvaluationContext context)
{
return EvaluateAsString (context);
}
}
partial class StringLiteral : Expression
{
public override bool EvaluateAsBoolean (EvaluationContext context)
{
var ret = EvaluateAsString (context);
return EvaluateStringAsBoolean (context, ret);
}
public override string EvaluateAsString (EvaluationContext context)
{
return context.Evaluator.Evaluate (this.Value.Name);
}
public override object EvaluateAsObject (EvaluationContext context)
{
return EvaluateAsString (context);
}
}
partial class RawStringLiteral : Expression
{
public override string EvaluateAsString (EvaluationContext context)
{
return Value.Name;
}
public override bool EvaluateAsBoolean (EvaluationContext context)
{
throw new InvalidProjectFileException ("raw string literal cannot be evaluated as boolean");
}
public override object EvaluateAsObject (EvaluationContext context)
{
return EvaluateAsString (context);
}
}
partial class QuotedExpression : Expression
{
public override string EvaluateAsString (EvaluationContext context)
{
return QuoteChar + EvaluateAsStringWithoutQuote (context) + QuoteChar;
}
public string EvaluateAsStringWithoutQuote (EvaluationContext context)
{
return string.Concat (Contents.Select (e => e.EvaluateAsString (context)));
}
public override bool EvaluateAsBoolean (EvaluationContext context)
{
var ret = EvaluateAsStringWithoutQuote (context);
return EvaluateStringAsBoolean (context, ret);
}
public override object EvaluateAsObject (EvaluationContext context)
{
return EvaluateAsStringWithoutQuote (context);
}
}
partial class FunctionCallExpression : Expression
{
public override string EvaluateAsString (EvaluationContext context)
{
throw new NotImplementedException ();
}
public override bool EvaluateAsBoolean (EvaluationContext context)
{
if (string.Equals (Name.Name, "Exists", StringComparison.OrdinalIgnoreCase)) {
if (Arguments.Count != 1)
throw new InvalidProjectFileException (Location, "Function 'Exists' expects 1 argument");
string val = Arguments.First ().EvaluateAsString (context);
val = WindowsCompatibilityExtensions.FindMatchingPath (val);
return Directory.Exists (val) || System.IO.File.Exists (val);
}
if (string.Equals (Name.Name, "HasTrailingSlash", StringComparison.OrdinalIgnoreCase)) {
if (Arguments.Count != 1)
throw new InvalidProjectFileException (Location, "Function 'HasTrailingSlash' expects 1 argument");
string val = Arguments.First ().EvaluateAsString (context);
return val.LastOrDefault () == '\\' || val.LastOrDefault () == '/';
}
throw new InvalidProjectFileException (Location, string.Format ("Unsupported function '{0}'", Name));
}
public override object EvaluateAsObject (EvaluationContext context)
{
throw new NotImplementedException ();
}
}
}