Xamarin Public Jenkins f3e3aab35a Imported Upstream version 4.3.2.467
Former-commit-id: 9c2cb47f45fa221e661ab616387c9cda183f283d
2016-02-22 11:00:01 -05:00

698 lines
18 KiB
C#

//
// Expression.cs: Stores references to items or properties.
//
// Authors:
// Marek Sieradzki (marek.sieradzki@gmail.com)
// Marek Safar (marek.safar@gmail.com)
//
// (C) 2005 Marek Sieradzki
// Copyright (c) 2014 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.IO;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using Mono.XBuild.Utilities;
namespace Microsoft.Build.BuildEngine {
// Properties and items are processed in two ways
// 1. Evaluate, Project calls evaluate on all the item and property groups.
// At this time, the items are fully expanded, all item and property
// references are expanded to get the item's value.
// Properties on the other hand, expand property refs, but _not_
// item references.
//
// 2. After the 'evaluation' phase, this could be when executing a target/task,
// - Items : no expansion required, as they are already at final value
// - Properties: Item references get expanded now, in the context of the
// batching
//
// The enum ExpressionOptions is for specifying this expansion of item references.
//
// GroupingCollection.Evaluate, evaluates all properties and then items
internal class Expression {
enum TokenKind
{
OpenParens,
CloseParens,
Dot,
End
}
ExpressionCollection expressionCollection;
static Regex item_regex;
static Regex metadata_regex;
public Expression ()
{
this.expressionCollection = new ExpressionCollection ();
}
public static T ParseAs<T> (string expression, ParseOptions options, Project project)
{
Expression expr = new Expression ();
expr.Parse (expression, options);
return (T)expr.ConvertTo (project, typeof (T));
}
public static T ParseAs<T> (string expression, ParseOptions options, Project project, ExpressionOptions exprOptions)
{
Expression expr = new Expression ();
expr.Parse (expression, options);
return (T)expr.ConvertTo (project, typeof (T), exprOptions);
}
// Split: Split on ';'
// Eg. Property values don't need to be split
//
// AllowItems: if false, item refs should not be treated as item refs!
// it converts them to strings in the final expressionCollection
//
// AllowMetadata: same as AllowItems, for metadata
public void Parse (string expression, ParseOptions options)
{
bool split = (options & ParseOptions.Split) == ParseOptions.Split;
bool allowItems = (options & ParseOptions.AllowItems) == ParseOptions.AllowItems;
bool allowMd = (options & ParseOptions.AllowMetadata) == ParseOptions.AllowMetadata;
expression = expression.Replace ('\\', Path.DirectorySeparatorChar);
string [] parts;
if (split)
parts = expression.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries);
else
parts = new string [] { expression };
// TODO: Too complicated, each part parses only its known part
// we should simply do it in one go and avoid all this parts code madness
List <ArrayList> p1 = new List <ArrayList> (parts.Length);
List <ArrayList> p2 = new List <ArrayList> (parts.Length);
List <ArrayList> p3 = new List <ArrayList> (parts.Length);
Prepare (p1, parts.Length);
Prepare (p2, parts.Length);
Prepare (p3, parts.Length);
for (int i = 0; i < parts.Length; i++)
p1 [i] = SplitItems (parts [i], allowItems);
for (int i = 0; i < parts.Length; i++) {
p2 [i] = new ArrayList ();
foreach (object o in p1 [i]) {
if (o is string)
p2 [i].AddRange (ExtractProperties ((string) o));
else
p2 [i].Add (o);
}
}
for (int i = 0; i < parts.Length; i++) {
p3 [i] = new ArrayList ();
foreach (object o in p2 [i]) {
if (o is string)
p3 [i].AddRange (SplitMetadata ((string) o));
else
p3 [i].Add (o);
}
}
CopyToExpressionCollection (p3, allowItems, allowMd);
}
void Prepare (List <ArrayList> l, int length)
{
for (int i = 0; i < length; i++)
l.Add (null);
}
void CopyToExpressionCollection (List <ArrayList> lists, bool allowItems, bool allowMd)
{
for (int i = 0; i < lists.Count; i++) {
foreach (object o in lists [i]) {
if (o is string)
expressionCollection.Add (MSBuildUtils.Unescape ((string) o));
else if (!allowItems && o is ItemReference)
expressionCollection.Add (((ItemReference) o).OriginalString);
else if (!allowMd && o is MetadataReference) {
expressionCollection.Add (((MetadataReference) o).OriginalString);
}
else if (o is IReference)
expressionCollection.Add ((IReference) o);
}
if (i < lists.Count - 1)
expressionCollection.Add (";");
}
}
ArrayList SplitItems (string text, bool allowItems)
{
ArrayList phase1 = new ArrayList ();
Match m;
m = ItemRegex.Match (text);
while (m.Success) {
string name = null, transform = null, separator = null;
ItemReference ir;
name = m.Groups [ItemRegex.GroupNumberFromName ("itemname")].Value;
if (m.Groups [ItemRegex.GroupNumberFromName ("has_transform")].Success)
transform = m.Groups [ItemRegex.GroupNumberFromName ("transform")].Value;
if (m.Groups [ItemRegex.GroupNumberFromName ("has_separator")].Success)
separator = m.Groups [ItemRegex.GroupNumberFromName ("separator")].Value;
ir = new ItemReference (text.Substring (m.Groups [0].Index, m.Groups [0].Length),
name, transform, separator, m.Groups [0].Index, m.Groups [0].Length);
phase1.Add (ir);
m = m.NextMatch ();
}
ArrayList phase2 = new ArrayList ();
int last_end = -1;
int end = text.Length - 1;
foreach (ItemReference ir in phase1) {
int a,b;
a = last_end;
b = ir.Start;
if (b - a - 1 > 0) {
phase2.Add (text.Substring (a + 1, b - a - 1));
}
last_end = ir.End;
phase2.Add (ir);
}
if (last_end < end)
phase2.Add (text.Substring (last_end + 1, end - last_end));
return phase2;
}
//
// Parses property syntax
//
static List<object> ExtractProperties (string text)
{
var phase = new List<object> ();
var pos = text.IndexOf ("$(", StringComparison.Ordinal);
if (pos < 0) {
phase.Add (text);
return phase;
}
if (pos != 0) {
// Extract any whitespaces before property reference
phase.Add (text.Substring (0, pos));
}
while (pos < text.Length) {
pos += 2;
int start = pos;
int end = 0;
bool requires_closing_parens = true;
var ch = text [pos];
if ((ch == 'r' || ch == 'R') && text.Substring (pos + 1).StartsWith ("egistry:", StringComparison.OrdinalIgnoreCase)) {
pos += 9;
ParseRegistryFunction (text, pos);
} else {
while (char.IsWhiteSpace (ch))
ch = text [pos++];
if (ch == '[') {
phase.Add (ParsePropertyFunction (text, ref pos));
} else {
// TODO: There is something like char index syntax as well: $(aa [10])
// text.IndexOf ('[');
end = text.IndexOf (')', pos) + 1;
if (end > 0) {
//
// Instance string method, $(foo.Substring (0, 3))
//
var dot = text.IndexOf ('.', pos, end - pos);
if (dot > 0) {
var name = text.Substring (start, dot - start);
++dot;
var res = ParseInvocation (text, ref dot, null, new PropertyReference (name));
if (res != null) {
phase.Add (res);
end = dot;
}
} else {
var name = text.Substring (start, end - start - 1);
//
// Check for wrong syntax e.g $(foo()
//
var open_parens = name.IndexOf ('(');
if (open_parens < 0) {
//
// Simple property reference $(Foo)
//
phase.Add (new PropertyReference (name));
requires_closing_parens = false;
} else {
end = 0;
}
}
}
if (end == 0) {
end = text.Length;
start -= 2;
phase.Add (text.Substring (start, end - start));
}
pos = end;
}
if (requires_closing_parens) {
end = text.IndexOf (')', pos);
if (end < 0)
end = 0;
else
pos = end + 1;
}
}
end = text.IndexOf ("$(", pos, StringComparison.Ordinal);
if (end < 0)
end = text.Length;
if (end - pos > 0)
phase.Add (text.Substring (pos, end - pos));
pos = end;
}
return phase;
}
//
// Property function with syntax $([Class]::Method(Parameters))
//
static MemberInvocationReference ParsePropertyFunction (string text, ref int pos)
{
int p = text.IndexOf ("]::", pos, StringComparison.Ordinal);
if (p < 0)
throw new InvalidProjectFileException (string.Format ("Invalid static method invocation syntax '{0}'", text.Substring (pos)));
var type_name = text.Substring (pos + 1, p - pos - 1);
var type = GetTypeForStaticMethod (type_name);
if (type == null) {
if (type_name.Contains ("."))
throw new InvalidProjectFileException (string.Format ("Invalid type '{0}' used in static method invocation", type_name));
throw new InvalidProjectFileException (string.Format ("'{0}': Static method invocation requires full type name to be used", type_name));
}
pos = p + 3;
return ParseInvocation (text, ref pos, type, null);
}
//
// Property function with syntax $(Registry:Call)
//
static void ParseRegistryFunction (string text, int pos)
{
throw new NotImplementedException ("Registry function");
}
static Type GetTypeForStaticMethod (string typeName)
{
//
// In static property functions, you can use any static method or property of these system classes:
//
switch (typeName.ToLowerInvariant ()) {
case "system.byte":
return typeof (byte);
case "system.char":
return typeof (char);
case "system.convert":
return typeof (Convert);
case "system.datetime":
return typeof (DateTime);
case "system.decimal":
return typeof (decimal);
case "system.double":
return typeof (double);
case "system.enum":
return typeof (Enum);
case "system.guid":
return typeof (Guid);
case "system.int16":
return typeof (Int16);
case "system.int32":
return typeof (Int32);
case "system.int64":
return typeof (Int64);
case "system.io.path":
return typeof (System.IO.Path);
case "system.math":
return typeof (Math);
case "system.uint16":
return typeof (UInt16);
case "system.uint32":
return typeof (UInt32);
case "system.uint64":
return typeof (UInt64);
case "system.sbyte":
return typeof (sbyte);
case "system.single":
return typeof (float);
case "system.string":
return typeof (string);
case "system.stringcomparer":
return typeof (StringComparer);
case "system.timespan":
return typeof (TimeSpan);
case "system.text.regularexpressions.regex":
return typeof (System.Text.RegularExpressions.Regex);
case "system.version":
return typeof (Version);
case "microsoft.build.utilities.toollocationhelper":
throw new NotImplementedException (typeName);
case "msbuild":
return typeof (PredefinedPropertyFunctions);
case "system.environment":
return typeof (System.Environment);
case "system.io.directory":
return typeof (System.IO.Directory);
case "system.io.file":
return typeof (System.IO.File);
}
return null;
}
static bool IsMethodAllowed (Type type, string name)
{
if (type == typeof (System.Environment)) {
switch (name.ToLowerInvariant ()) {
case "commandline":
case "expandenvironmentvariables":
case "getenvironmentvariable":
case "getenvironmentvariables":
case "getfolderpath":
case "getlogicaldrives":
return true;
}
return false;
}
if (type == typeof (System.IO.Directory)) {
switch (name.ToLowerInvariant ()) {
case "getdirectories":
case "getfiles":
case "getlastaccesstime":
case "getlastwritetime":
return true;
}
return false;
}
if (type == typeof (System.IO.File)) {
switch (name.ToLowerInvariant ()) {
case "getcreationtime":
case "getattributes":
case "getlastaccesstime":
case "getlastwritetime":
case "readalltext":
return true;
}
}
return true;
}
static List<string> ParseArguments (string text, ref int pos)
{
List<string> args = new List<string> ();
int parens = 0;
bool backticks = false;
bool inDoubleQuotes = false;
bool inSingleQuotes = false;
int start = pos;
for (; pos < text.Length; ++pos) {
var ch = text [pos];
if (ch == '`') {
backticks = !backticks;
continue;
}
if (backticks)
continue;
if(ch == '\'' && !inDoubleQuotes) {
inSingleQuotes = !inSingleQuotes;
continue;
}
if (ch == '\"' && !inSingleQuotes) {
inDoubleQuotes = !inDoubleQuotes;
continue;
}
if (ch == '(') {
++parens;
continue;
}
if (ch == ')') {
if (parens == 0) {
var arg = text.Substring (start, pos - start).Trim ();
if (arg.Length > 0)
args.Add (arg);
++pos;
return args;
}
--parens;
continue;
}
if (parens != 0)
continue;
if (ch == ',' && !inDoubleQuotes && !inSingleQuotes) {
args.Add (text.Substring (start, pos - start));
start = pos + 1;
continue;
}
}
// Invalid syntax
return null;
}
static MemberInvocationReference ParseInvocation (string text, ref int p, Type type, IReference instance)
{
TokenKind token;
MemberInvocationReference mir = null;
while (true) {
int prev = p;
token = ScanName (text, ref p);
var name = text.Substring (prev, p - prev).TrimEnd ();
switch (token) {
case TokenKind.Dot:
case TokenKind.OpenParens:
break;
case TokenKind.CloseParens:
return new MemberInvocationReference (type, name) {
Instance = instance
};
case TokenKind.End:
if (mir == null || name.Length != 0)
throw new InvalidProjectFileException (string.Format ("Invalid static method invocation syntax '{0}'", text.Substring (p)));
return mir;
default:
throw new NotImplementedException ();
}
instance = mir = new MemberInvocationReference (type, name) {
Instance = instance
};
if (type != null) {
if (!IsMethodAllowed (type, name))
throw new InvalidProjectFileException (string.Format ("The function '{0}' on type '{1}' has not been enabled for execution", name, type.FullName));
type = null;
}
if (token == TokenKind.OpenParens) {
++p;
mir.Arguments = ParseArguments (text, ref p);
}
if (p < text.Length && text [p] == '.') {
++p;
continue;
}
return mir;
}
}
static TokenKind ScanName (string text, ref int p)
{
for (; p < text.Length; ++p) {
switch (text [p]) {
case '(':
return TokenKind.OpenParens;
case '.':
return TokenKind.Dot;
case ')':
return TokenKind.CloseParens;
}
}
return TokenKind.End;
}
ArrayList SplitMetadata (string text)
{
ArrayList phase1 = new ArrayList ();
Match m;
m = MetadataRegex.Match (text);
while (m.Success) {
string name = null, meta = null;
MetadataReference mr;
if (m.Groups [MetadataRegex.GroupNumberFromName ("name")].Success)
name = m.Groups [MetadataRegex.GroupNumberFromName ("name")].Value;
meta = m.Groups [MetadataRegex.GroupNumberFromName ("meta")].Value;
mr = new MetadataReference (text.Substring (m.Groups [0].Index, m.Groups [0].Length),
name, meta, m.Groups [0].Index, m.Groups [0].Length);
phase1.Add (mr);
m = m.NextMatch ();
}
ArrayList phase2 = new ArrayList ();
int last_end = -1;
int end = text.Length - 1;
foreach (MetadataReference mr in phase1) {
int a,b;
a = last_end;
b = mr.Start;
if (b - a - 1> 0) {
phase2.Add (text.Substring (a + 1, b - a - 1));
}
last_end = mr.End;
phase2.Add (mr);
}
if (last_end < end)
phase2.Add (text.Substring (last_end + 1, end - last_end));
return phase2;
}
public object ConvertTo (Project project, Type type)
{
return ConvertTo (project, type, ExpressionOptions.ExpandItemRefs);
}
public object ConvertTo (Project project, Type type, ExpressionOptions options)
{
return expressionCollection.ConvertTo (project, type, options);
}
public ExpressionCollection Collection {
get { return expressionCollection; }
}
static Regex ItemRegex {
get {
if (item_regex == null)
item_regex = new Regex (
@"@\(\s*"
+ @"(?<itemname>[_A-Za-z][_\-0-9a-zA-Z]*)"
+ @"(?<has_transform>\s*->\s*'(?<transform>[^']*)')?"
+ @"(?<has_separator>\s*,\s*'(?<separator>[^']*)')?"
+ @"\s*\)");
return item_regex;
}
}
static Regex MetadataRegex {
get {
if (metadata_regex == null)
metadata_regex = new Regex (
@"%\(\s*"
+ @"((?<name>[_a-zA-Z][_\-0-9a-zA-Z]*)\.)?"
+ @"(?<meta>[_a-zA-Z][_\-0-9a-zA-Z]*)"
+ @"\s*\)");
return metadata_regex;
}
}
}
[Flags]
enum ParseOptions {
// absence of one of these flags, means
// false for that option
AllowItems = 0x1,
Split = 0x2,
AllowMetadata = 0x4,
None = 0x8, // == no items, no metadata, and no split
// commonly used options
AllowItemsMetadataAndSplit = AllowItems | Split | AllowMetadata,
AllowItemsNoMetadataAndSplit = AllowItems | Split
}
enum ExpressionOptions {
ExpandItemRefs,
DoNotExpandItemRefs
}
}