2 // Expression.cs: Stores references to items or properties.
5 // Marek Sieradzki (marek.sieradzki@gmail.com)
6 // Marek Safar (marek.safar@gmail.com)
8 // (C) 2005 Marek Sieradzki
9 // Copyright (c) 2014 Xamarin Inc. (http://www.xamarin.com)
11 // Permission is hereby granted, free of charge, to any person obtaining
12 // a copy of this software and associated documentation files (the
13 // "Software"), to deal in the Software without restriction, including
14 // without limitation the rights to use, copy, modify, merge, publish,
15 // distribute, sublicense, and/or sell copies of the Software, and to
16 // permit persons to whom the Software is furnished to do so, subject to
17 // the following conditions:
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
22 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 using System.Collections;
33 using System.Collections.Generic;
35 using System.Text.RegularExpressions;
36 using Mono.XBuild.Utilities;
38 namespace Microsoft.Build.BuildEngine {
40 // Properties and items are processed in two ways
41 // 1. Evaluate, Project calls evaluate on all the item and property groups.
42 // At this time, the items are fully expanded, all item and property
43 // references are expanded to get the item's value.
44 // Properties on the other hand, expand property refs, but _not_
47 // 2. After the 'evaluation' phase, this could be when executing a target/task,
48 // - Items : no expansion required, as they are already at final value
49 // - Properties: Item references get expanded now, in the context of the
52 // The enum ExpressionOptions is for specifying this expansion of item references.
54 // GroupingCollection.Evaluate, evaluates all properties and then items
56 internal class Expression {
66 ExpressionCollection expressionCollection;
68 static Regex item_regex;
69 static Regex metadata_regex;
73 this.expressionCollection = new ExpressionCollection ();
76 public static T ParseAs<T> (string expression, ParseOptions options, Project project)
78 Expression expr = new Expression ();
79 expr.Parse (expression, options);
80 return (T)expr.ConvertTo (project, typeof (T));
83 public static T ParseAs<T> (string expression, ParseOptions options, Project project, ExpressionOptions exprOptions)
85 Expression expr = new Expression ();
86 expr.Parse (expression, options);
87 return (T)expr.ConvertTo (project, typeof (T), exprOptions);
90 // Split: Split on ';'
91 // Eg. Property values don't need to be split
93 // AllowItems: if false, item refs should not be treated as item refs!
94 // it converts them to strings in the final expressionCollection
96 // AllowMetadata: same as AllowItems, for metadata
97 public void Parse (string expression, ParseOptions options)
99 bool split = (options & ParseOptions.Split) == ParseOptions.Split;
100 bool allowItems = (options & ParseOptions.AllowItems) == ParseOptions.AllowItems;
101 bool allowMd = (options & ParseOptions.AllowMetadata) == ParseOptions.AllowMetadata;
103 expression = expression.Replace ('\\', Path.DirectorySeparatorChar);
107 parts = expression.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries);
109 parts = new string [] { expression };
111 // TODO: Too complicated, each part parses only its known part
112 // we should simply do it in one go and avoid all this parts code madness
114 List <ArrayList> p1 = new List <ArrayList> (parts.Length);
115 List <ArrayList> p2 = new List <ArrayList> (parts.Length);
116 List <ArrayList> p3 = new List <ArrayList> (parts.Length);
118 Prepare (p1, parts.Length);
119 Prepare (p2, parts.Length);
120 Prepare (p3, parts.Length);
122 for (int i = 0; i < parts.Length; i++)
123 p1 [i] = SplitItems (parts [i], allowItems);
125 for (int i = 0; i < parts.Length; i++) {
126 p2 [i] = new ArrayList ();
127 foreach (object o in p1 [i]) {
129 p2 [i].AddRange (ExtractProperties ((string) o));
135 for (int i = 0; i < parts.Length; i++) {
136 p3 [i] = new ArrayList ();
137 foreach (object o in p2 [i]) {
139 p3 [i].AddRange (SplitMetadata ((string) o));
145 CopyToExpressionCollection (p3, allowItems, allowMd);
148 void Prepare (List <ArrayList> l, int length)
150 for (int i = 0; i < length; i++)
154 void CopyToExpressionCollection (List <ArrayList> lists, bool allowItems, bool allowMd)
156 for (int i = 0; i < lists.Count; i++) {
157 foreach (object o in lists [i]) {
159 expressionCollection.Add (MSBuildUtils.Unescape ((string) o));
160 else if (!allowItems && o is ItemReference)
161 expressionCollection.Add (((ItemReference) o).OriginalString);
162 else if (!allowMd && o is MetadataReference) {
163 expressionCollection.Add (((MetadataReference) o).OriginalString);
165 else if (o is IReference)
166 expressionCollection.Add ((IReference) o);
168 if (i < lists.Count - 1)
169 expressionCollection.Add (";");
173 ArrayList SplitItems (string text, bool allowItems)
175 ArrayList phase1 = new ArrayList ();
177 m = ItemRegex.Match (text);
180 string name = null, transform = null, separator = null;
183 name = m.Groups [ItemRegex.GroupNumberFromName ("itemname")].Value;
185 if (m.Groups [ItemRegex.GroupNumberFromName ("has_transform")].Success)
186 transform = m.Groups [ItemRegex.GroupNumberFromName ("transform")].Value;
188 if (m.Groups [ItemRegex.GroupNumberFromName ("has_separator")].Success)
189 separator = m.Groups [ItemRegex.GroupNumberFromName ("separator")].Value;
191 ir = new ItemReference (text.Substring (m.Groups [0].Index, m.Groups [0].Length),
192 name, transform, separator, m.Groups [0].Index, m.Groups [0].Length);
197 ArrayList phase2 = new ArrayList ();
199 int end = text.Length - 1;
201 foreach (ItemReference ir in phase1) {
208 phase2.Add (text.Substring (a + 1, b - a - 1));
216 phase2.Add (text.Substring (last_end + 1, end - last_end));
222 // Parses property syntax
224 static List<object> ExtractProperties (string text)
226 var phase = new List<object> ();
228 var pos = text.IndexOf ("$(", StringComparison.Ordinal);
235 // Extract any whitespaces before property reference
236 phase.Add (text.Substring (0, pos));
239 while (pos < text.Length) {
243 bool requires_closing_parens = true;
246 if ((ch == 'r' || ch == 'R') && text.Substring (pos + 1).StartsWith ("egistry:", StringComparison.OrdinalIgnoreCase)) {
248 ParseRegistryFunction (text, pos);
250 while (char.IsWhiteSpace (ch))
254 phase.Add (ParsePropertyFunction (text, ref pos));
256 // TODO: There is something like char index syntax as well: $(aa [10])
257 // text.IndexOf ('[');
259 end = text.IndexOf (')', pos) + 1;
262 // Instance string method, $(foo.Substring (0, 3))
264 var dot = text.IndexOf ('.', pos, end - pos);
266 var name = text.Substring (start, dot - start);
268 var res = ParseInvocation (text, ref dot, null, new PropertyReference (name));
274 var name = text.Substring (start, end - start - 1);
277 // Check for wrong syntax e.g $(foo()
279 var open_parens = name.IndexOf ('(');
280 if (open_parens < 0) {
282 // Simple property reference $(Foo)
284 phase.Add (new PropertyReference (name));
285 requires_closing_parens = false;
295 phase.Add (text.Substring (start, end - start));
301 if (requires_closing_parens) {
302 end = text.IndexOf (')', pos);
310 end = text.IndexOf ("$(", pos, StringComparison.Ordinal);
315 phase.Add (text.Substring (pos, end - pos));
324 // Property function with syntax $([Class]::Method(Parameters))
326 static MemberInvocationReference ParsePropertyFunction (string text, ref int pos)
328 int p = text.IndexOf ("]::", pos, StringComparison.Ordinal);
330 throw new InvalidProjectFileException (string.Format ("Invalid static method invocation syntax '{0}'", text.Substring (pos)));
332 var type_name = text.Substring (pos + 1, p - pos - 1);
333 var type = GetTypeForStaticMethod (type_name);
335 if (type_name.Contains ("."))
336 throw new InvalidProjectFileException (string.Format ("Invalid type '{0}' used in static method invocation", type_name));
338 throw new InvalidProjectFileException (string.Format ("'{0}': Static method invocation requires full type name to be used", type_name));
342 return ParseInvocation (text, ref pos, type, null);
346 // Property function with syntax $(Registry:Call)
348 static void ParseRegistryFunction (string text, int pos)
350 throw new NotImplementedException ("Registry function");
353 static Type GetTypeForStaticMethod (string typeName)
356 // In static property functions, you can use any static method or property of these system classes:
358 switch (typeName.ToLowerInvariant ()) {
360 return typeof (byte);
362 return typeof (char);
363 case "system.convert":
364 return typeof (Convert);
365 case "system.datetime":
366 return typeof (DateTime);
367 case "system.decimal":
368 return typeof (decimal);
369 case "system.double":
370 return typeof (double);
372 return typeof (Enum);
374 return typeof (Guid);
376 return typeof (Int16);
378 return typeof (Int32);
380 return typeof (Int64);
381 case "system.io.path":
382 return typeof (System.IO.Path);
384 return typeof (Math);
385 case "system.uint16":
386 return typeof (UInt16);
387 case "system.uint32":
388 return typeof (UInt32);
389 case "system.uint64":
390 return typeof (UInt64);
392 return typeof (sbyte);
393 case "system.single":
394 return typeof (float);
395 case "system.string":
396 return typeof (string);
397 case "system.stringcomparer":
398 return typeof (StringComparer);
399 case "system.timespan":
400 return typeof (TimeSpan);
401 case "system.text.regularexpressions.regex":
402 return typeof (System.Text.RegularExpressions.Regex);
403 case "system.version":
404 return typeof (Version);
405 case "microsoft.build.utilities.toollocationhelper":
406 throw new NotImplementedException (typeName);
408 return typeof (PredefinedPropertyFunctions);
409 case "system.environment":
410 return typeof (System.Environment);
411 case "system.io.directory":
412 return typeof (System.IO.Directory);
413 case "system.io.file":
414 return typeof (System.IO.File);
420 static bool IsMethodAllowed (Type type, string name)
422 if (type == typeof (System.Environment)) {
423 switch (name.ToLowerInvariant ()) {
425 case "expandenvironmentvariables":
426 case "getenvironmentvariable":
427 case "getenvironmentvariables":
428 case "getfolderpath":
429 case "getlogicaldrives":
436 if (type == typeof (System.IO.Directory)) {
437 switch (name.ToLowerInvariant ()) {
438 case "getdirectories":
440 case "getlastaccesstime":
441 case "getlastwritetime":
448 if (type == typeof (System.IO.File)) {
449 switch (name.ToLowerInvariant ()) {
450 case "getcreationtime":
451 case "getattributes":
452 case "getlastaccesstime":
453 case "getlastwritetime":
462 static List<string> ParseArguments (string text, ref int pos)
464 List<string> args = new List<string> ();
466 bool backticks = false;
468 for (; pos < text.Length; ++pos) {
472 backticks = !backticks;
486 var arg = text.Substring (start, pos - start).Trim ();
502 args.Add (text.Substring (start, pos - start));
512 static MemberInvocationReference ParseInvocation (string text, ref int p, Type type, IReference instance)
515 MemberInvocationReference mir = null;
519 token = ScanName (text, ref p);
520 var name = text.Substring (prev, p - prev).TrimEnd ();
524 case TokenKind.OpenParens:
526 case TokenKind.CloseParens:
527 return new MemberInvocationReference (type, name) {
532 if (mir == null || name.Length != 0)
533 throw new InvalidProjectFileException (string.Format ("Invalid static method invocation syntax '{0}'", text.Substring (p)));
537 throw new NotImplementedException ();
540 instance = mir = new MemberInvocationReference (type, name) {
545 if (!IsMethodAllowed (type, name))
546 throw new InvalidProjectFileException (string.Format ("The function '{0}' on type '{1}' has not been enabled for execution", name, type.FullName));
551 if (token == TokenKind.OpenParens) {
553 mir.Arguments = ParseArguments (text, ref p);
556 if (p < text.Length && text [p] == '.') {
565 static TokenKind ScanName (string text, ref int p)
567 for (; p < text.Length; ++p) {
570 return TokenKind.OpenParens;
572 return TokenKind.Dot;
574 return TokenKind.CloseParens;
578 return TokenKind.End;
581 ArrayList SplitMetadata (string text)
583 ArrayList phase1 = new ArrayList ();
585 m = MetadataRegex.Match (text);
588 string name = null, meta = null;
589 MetadataReference mr;
591 if (m.Groups [MetadataRegex.GroupNumberFromName ("name")].Success)
592 name = m.Groups [MetadataRegex.GroupNumberFromName ("name")].Value;
594 meta = m.Groups [MetadataRegex.GroupNumberFromName ("meta")].Value;
596 mr = new MetadataReference (text.Substring (m.Groups [0].Index, m.Groups [0].Length),
597 name, meta, m.Groups [0].Index, m.Groups [0].Length);
602 ArrayList phase2 = new ArrayList ();
604 int end = text.Length - 1;
606 foreach (MetadataReference mr in phase1) {
613 phase2.Add (text.Substring (a + 1, b - a - 1));
621 phase2.Add (text.Substring (last_end + 1, end - last_end));
626 public object ConvertTo (Project project, Type type)
628 return ConvertTo (project, type, ExpressionOptions.ExpandItemRefs);
631 public object ConvertTo (Project project, Type type, ExpressionOptions options)
633 return expressionCollection.ConvertTo (project, type, options);
636 public ExpressionCollection Collection {
637 get { return expressionCollection; }
640 static Regex ItemRegex {
642 if (item_regex == null)
643 item_regex = new Regex (
645 + @"(?<itemname>[_A-Za-z][_\-0-9a-zA-Z]*)"
646 + @"(?<has_transform>\s*->\s*'(?<transform>[^']*)')?"
647 + @"(?<has_separator>\s*,\s*'(?<separator>[^']*)')?"
653 static Regex MetadataRegex {
655 if (metadata_regex == null)
656 metadata_regex = new Regex (
658 + @"((?<name>[_a-zA-Z][_\-0-9a-zA-Z]*)\.)?"
659 + @"(?<meta>[_a-zA-Z][_\-0-9a-zA-Z]*)"
661 return metadata_regex;
668 // absence of one of these flags, means
669 // false for that option
674 None = 0x8, // == no items, no metadata, and no split
676 // commonly used options
677 AllowItemsMetadataAndSplit = AllowItems | Split | AllowMetadata,
678 AllowItemsNoMetadataAndSplit = AllowItems | Split
681 enum ExpressionOptions {