-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathProjectItem.cs
More file actions
1136 lines (984 loc) · 46.8 KB
/
ProjectItem.cs
File metadata and controls
1136 lines (984 loc) · 46.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.Framework;
using Microsoft.Build.ObjectModelRemoting;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
#nullable disable
namespace Microsoft.Build.Evaluation
{
/// <summary>
/// An evaluated design-time item
/// </summary>
/// <remarks>
/// Edits to this object will indirectly dirty the containing project because they will modify the backing XML.
/// </remarks>
/// <comment>
/// We cannot use a copy-on-write table for the metadata, as ProjectMetadata objects are mutable. However,
/// we do use it for build-time items.
/// </comment>
[DebuggerDisplay("{ItemType}={EvaluatedInclude} [{UnevaluatedInclude}] #DirectMetadata={DirectMetadataCount}")]
public class ProjectItem : IItem<ProjectMetadata>, IProjectMetadataParent, IItemData
{
/// <summary>
/// Project that this item lives in.
/// ProjectItems always live in a project.
/// Used to get item definitions and project directory.
/// </summary>
private readonly Project _project;
/// <summary>
/// Fragment of the original include that led to this item,
/// with properties expanded but not wildcards. Escaped as necessary
/// </summary>
/// <remarks>
/// This is ONLY used to figure out %(RecursiveDir) when it is requested.
/// It's likely too expensive to figure that out if it isn't needed, so we store
/// the necessary material here.
/// </remarks>
private readonly string _evaluatedIncludeBeforeWildcardExpansionEscaped;
/// <summary>
/// Item definitions are stored in one single table shared by all items of a particular item type.
///
/// When an item is created from another item, such as by using an expression like Include="@(x)",
/// any item definition metadata those source items have must override any item definition metadata
/// associated with the new item type.
///
/// Copying all those item definition metadata into real metadata on this item would be very inefficient, because
/// it would turn a single shared table into a separate table for every item.
///
/// Instead, we get a reference to the item definition of the source items, and consult
/// that table before we consult our own item type's item definition. Since item definitions can't change at this point,
/// it's safe to reference their original table.
///
/// If our item gets copied again, we need a reference to the inherited item definition and we need the real item
/// definition of the source items. Thus a list is created. On copying, a list is created, beginning with a clone
/// of any list the source item had, and ending with the item definition list of the source item type.
///
/// When we look up a metadata value we look at
/// (1) directly associated metadata and built-in metadata
/// (2) the inherited item definition list, starting from the top
/// (3) the item definition associated with our item type
/// </summary>
private readonly List<ProjectItemDefinition> _inheritedItemDefinitions;
/// <summary>
/// Backing XML item.
/// Can never be null
/// </summary>
private ProjectItemElement _xml;
/// <summary>
/// Evaluated include.
/// The original XML may have evaluated to several of these items,
/// each with a different include.
/// May be empty, for example from expanding an empty list or from a transform with undefined metadata.
/// Escaped as necessary
/// </summary>
private string _evaluatedIncludeEscaped;
/// <summary>
/// Collection of metadata that link the XML metadata and evaluated metadata.
/// Since evaluation has occurred, this is an unordered collection.
/// May be null.
/// </summary>
/// <remarks>
/// Lazily created, as there are lots of items
/// that have no metadata at all.
/// </remarks>
private PropertyDictionary<ProjectMetadata> _directMetadata;
/// <summary>
/// Cached values of derivable item-spec modifiers. All time-based metadata are computed on demand.
/// </summary>
private ItemSpecModifiers.Cache _cachedModifiers;
/// <summary>
/// External projects support
/// </summary>
internal ProjectItem(ProjectItemElement xml, Project project)
{
this._project = project;
this._xml = xml;
}
/// <summary>
/// Called by the Evaluator during project evaluation.
/// Direct metadata may be null, indicating no metadata. It is assumed to have already been cloned.
/// Inherited item definition metadata may be null. It is assumed that its list has already been cloned.
/// ProjectMetadata objects may be shared with other items.
/// </summary>
internal ProjectItem(
Project project,
ProjectItemElement xml,
string evaluatedIncludeEscaped,
string evaluatedIncludeBeforeWildcardExpansionEscaped,
PropertyDictionary<ProjectMetadata> directMetadataCloned,
List<ProjectItemDefinition> inheritedItemDefinitionsCloned)
{
ErrorUtilities.VerifyThrowInternalNull(project);
ErrorUtilities.VerifyThrowArgumentNull(xml);
// Orcas accidentally allowed empty includes if they resulted from expansion: we preserve that bug
ErrorUtilities.VerifyThrowArgumentNull(evaluatedIncludeEscaped);
ErrorUtilities.VerifyThrowArgumentNull(evaluatedIncludeBeforeWildcardExpansionEscaped);
_xml = xml;
_project = project;
_evaluatedIncludeEscaped = evaluatedIncludeEscaped;
_evaluatedIncludeBeforeWildcardExpansionEscaped = evaluatedIncludeBeforeWildcardExpansionEscaped;
_directMetadata = directMetadataCloned;
_inheritedItemDefinitions = inheritedItemDefinitionsCloned;
}
internal virtual ProjectItemLink Link => null;
/// <inheritdoc cref="IItemData.EnumerateMetadata"/>
IEnumerable<KeyValuePair<string, string>> IItemData.EnumerateMetadata() => Metadata.Select(m => new KeyValuePair<string, string>(m.Name, m.EvaluatedValue));
/// <summary>
/// Backing XML item.
/// Can never be null.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public ProjectItemElement Xml
{
[DebuggerStepThrough]
get
{ return _xml; }
}
/// <summary>
/// Gets or sets the type of this item.
/// </summary>
public string ItemType
{
[DebuggerStepThrough]
get
{ return _xml.ItemType; }
set
{
if (Link != null)
{
Link.ChangeItemType(value);
}
else
{
ChangeItemType(value);
}
}
}
/// <summary>
/// Gets or sets the unevaluated value of the Include.
/// </summary>
public string UnevaluatedInclude
{
[DebuggerStepThrough]
get
{
return _xml.Include;
}
set
{
Rename(value);
}
}
/// <inheritdoc cref="IItemData.EvaluatedInclude"/>
/// <remarks>
/// Gets the evaluated value of the include, unescaped.
/// </remarks>
public string EvaluatedInclude
{
[DebuggerStepThrough]
get
{ return Link != null ? Link.EvaluatedInclude : EscapingUtilities.UnescapeAll(_evaluatedIncludeEscaped); }
}
/// <summary>
/// Gets the evaluated value of the include, escaped as necessary.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
string IItem.EvaluatedIncludeEscaped
{
[DebuggerStepThrough]
get
{ return _evaluatedIncludeEscaped; }
}
/// <summary>
/// The directory of the project being built
/// Never null: If there is no project filename yet, it will use the current directory
/// </summary>
string IItem.ProjectDirectory
{
get { return _project.DirectoryPath; }
}
/// <summary>
/// Project that this item lives in.
/// ProjectItems always live in a project.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public Project Project
{
[DebuggerStepThrough]
get
{ return _project; }
}
/// <summary>
/// If the item originated in an imported file, returns true.
/// Otherwise returns false.
/// </summary>
public bool IsImported
{
get
{
bool isImported = !Object.ReferenceEquals(_xml.ContainingProject, _project.Xml);
return isImported;
}
}
/// <summary>
/// Metadata directly on the item, if any.
/// Does not include metadata from item definitions.
/// Does not include built-in metadata.
/// Never returns null.
/// </summary>
public IEnumerable<ProjectMetadata> DirectMetadata
{
get { return Link != null ? Link.DirectMetadata : (IEnumerable<ProjectMetadata>)_directMetadata ?? (IEnumerable<ProjectMetadata>)ReadOnlyEmptyCollection<ProjectMetadata>.Instance; }
}
/// <summary>
/// Count of direct metadata on this item, if any.
/// Does NOT count any metadata inherited from item definitions.
/// Does not count built-in metadata, such as "FullPath".
/// </summary>
public int DirectMetadataCount
{
[DebuggerStepThrough]
get
{ return Link != null ? Link.DirectMetadata.Count : _directMetadata != null ? _directMetadata.Count : 0; }
}
/// <summary>
/// Metadata on the item, if any. Includes metadata specified by the definition, if any.
/// If there is no metadata, returns an empty collection.
/// Does not include built-in metadata, such as "FullPath".
/// Get the values of built-in metadata using <see cref="GetMetadataValue(string)"/>.
/// This is a read-only collection. To modify the metadata, use <see cref="SetMetadataValue(string, string)"/>.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")]
public ICollection<ProjectMetadata> Metadata
{
[DebuggerStepThrough]
get
{ return Link != null ? Link.MetadataCollection : MetadataCollection; }
}
IEnumerable<ProjectMetadata> IItem<ProjectMetadata>.Metadata => Metadata;
/// <summary>
/// Count of metadata on this item, if any.
/// Includes any metadata inherited from item definitions.
/// Includes both custom and built-in metadata.
/// </summary>
public int MetadataCount => Metadata.Count + ItemSpecModifiers.All.Length;
/// <summary>
/// Implementation of IKeyed exposing the item type, so items
/// can be put in a dictionary conveniently.
/// </summary>
string IKeyed.Key
{
[DebuggerStepThrough]
get
{ return ItemType; }
}
/// <summary>
/// Internal version of <see cref="Metadata">Metadata</see> that returns
/// a full ICollection.
/// Unordered collection of evaluated metadata on the item.
/// If there is no metadata, returns an empty collection.
/// Does not include built-in metadata.
/// Includes any from item definitions not masked by directly set metadata.
/// This is a read-only collection. To modify the metadata, use <see cref="SetMetadataValue(string, string)"/>.
/// </summary>
internal ICollection<ProjectMetadata> MetadataCollection
{
get
{
RetrievableEntryHashSet<ProjectMetadata> allMetadata = new RetrievableEntryHashSet<ProjectMetadata>(MSBuildNameIgnoreCaseComparer.Default);
// Lowest priority: regular item definitions
ProjectItemDefinition itemDefinition;
if (_project.ItemDefinitions.TryGetValue(ItemType, out itemDefinition))
{
foreach (ProjectMetadata metadataFromDefinition in itemDefinition.Metadata)
{
allMetadata[metadataFromDefinition.Name] = metadataFromDefinition;
}
}
// Next, any inherited item definitions. Front of the list is highest priority,
// so walk backwards.
if (_inheritedItemDefinitions != null)
{
for (int i = _inheritedItemDefinitions.Count - 1; i >= 0; i--)
{
foreach (ProjectMetadata metadatum in _inheritedItemDefinitions[i].Metadata)
{
allMetadata[metadatum.Name] = metadatum;
}
}
}
// Finally any direct metadata win.
if (_directMetadata != null)
{
foreach (ProjectMetadata metadatum in _directMetadata)
{
allMetadata[metadatum.Name] = metadatum;
}
}
return allMetadata.Values;
}
}
/// <summary>
/// Accesses the unescaped evaluated include prior to wildcard expansion
/// </summary>
internal string EvaluatedIncludeBeforeWildcardExpansion
{
[DebuggerStepThrough]
get
{ return EscapingUtilities.UnescapeAll(_evaluatedIncludeBeforeWildcardExpansionEscaped); }
}
/// <summary>
/// Accesses the evaluated include prior to wildcard expansion
/// </summary>
internal string EvaluatedIncludeBeforeWildcardExpansionEscaped
{
[DebuggerStepThrough]
get
{ return _evaluatedIncludeBeforeWildcardExpansionEscaped; }
}
/// <summary>
/// Accesses the inherited item definitions, if any.
/// Used ONLY by the ProjectInstance, when cloning a ProjectItem.
/// </summary>
internal List<ProjectItemDefinition> InheritedItemDefinitions
{
[DebuggerStepThrough]
get
{ return _inheritedItemDefinitions; }
}
/// <summary>
/// Gets an evaluated metadata on this item.
/// Potentially includes a metadata from an item definition.
/// Does not return built-in metadata, such as "FullPath".
/// Returns null if not found.
/// </summary>
public ProjectMetadata GetMetadata(string name)
{
if (Link != null)
{
return Link.GetMetadata(name);
}
ErrorUtilities.VerifyThrowArgumentLength(name);
ProjectMetadata result = null;
if (_directMetadata != null)
{
result = _directMetadata[name];
}
if (result == null)
{
result = GetItemDefinitionMetadata(name);
}
return result;
}
/// <summary>
/// Get the evaluated value of a metadata on this item, possibly from an item definition.
/// Returns empty string if it does not exist.
/// To determine whether a piece of metadata does not exist vs. simply has no value, use <see cref="HasMetadata(string)">HasMetadata</see>.
/// May be used to access the value of built-in metadata, such as "FullPath".
/// Attempting to get built-in metadata on a value that is not a valid path throws InvalidOperationException.
/// </summary>
public string GetMetadataValue(string name)
{
return Link != null ? Link.GetMetadataValue(name) : EscapingUtilities.UnescapeAll(((IItem)this).GetMetadataValueEscaped(name));
}
/// <summary>
/// Returns true if a particular piece of metadata is defined on this item,
/// otherwise false.
/// Includes built-in metadata and metadata inherited from item definitions.
/// </summary>
public bool HasMetadata(string name)
{
if (Link != null)
{
return Link.HasMetadata(name);
}
if (_directMetadata?.Contains(name) == true)
{
return true;
}
if (ItemSpecModifiers.IsItemSpecModifier(name))
{
return true;
}
ProjectMetadata metadatum = GetItemDefinitionMetadata(name);
if (metadatum != null)
{
return true;
}
return false;
}
/// <summary>
/// See <see cref="GetMetadataValue(string)">GetMetadataValue</see> for a more detailed explanation.
/// Returns the escaped value of the metadatum requested.
/// </summary>
string IItem.GetMetadataValueEscaped(string name)
{
ErrorUtilities.VerifyThrowArgumentLength(name);
string value = null;
if (_directMetadata != null)
{
ProjectMetadata metadatum = _directMetadata[name];
if (metadatum != null)
{
value = metadatum.EvaluatedValueEscaped;
}
}
if (value == null)
{
value = GetBuiltInMetadataEscaped(name);
}
if (value == null)
{
ProjectMetadata metadatum = GetItemDefinitionMetadata(name);
if (metadatum != null && Expander<ProjectProperty, ProjectItem>.ExpressionMayContainExpandableExpressions(metadatum.EvaluatedValueEscaped))
{
Expander<ProjectProperty, ProjectItem> expander = new Expander<ProjectProperty, ProjectItem>(null, null, new BuiltInMetadataTable(this), FileSystems.Default);
value = expander.ExpandIntoStringLeaveEscaped(metadatum.EvaluatedValueEscaped, ExpanderOptions.ExpandBuiltInMetadata, metadatum.Location);
}
else if (metadatum != null)
{
return metadatum.EvaluatedValueEscaped;
}
}
return value ?? String.Empty;
}
/// <summary>
/// Gets any existing ProjectMetadata on the item, or
/// else any on an applicable item definition.
/// This is ONLY called during evaluation.
/// Does not return built-in metadata, such as "FullPath".
/// Returns null if not found.
/// </summary>
ProjectMetadata IItem<ProjectMetadata>.GetMetadata(string name)
{
return GetMetadata(name);
}
/// <summary>
/// Adds a ProjectMetadata to the item.
/// This is ONLY called during evaluation and does not affect the XML.
/// </summary>
ProjectMetadata IItem<ProjectMetadata>.SetMetadata(ProjectMetadataElement metadataElement, string evaluatedInclude)
{
_directMetadata ??= new PropertyDictionary<ProjectMetadata>();
ProjectMetadata predecessor = GetMetadata(metadataElement.Name);
ProjectMetadata metadatum = new ProjectMetadata(this, metadataElement, evaluatedInclude, predecessor);
_directMetadata.Set(metadatum);
return metadatum;
}
/// <summary>
/// Adds metadata with the specified name and value to the item.
/// Updates an existing metadata if one already exists with the same name on the item directly, as opposed to inherited from an item definition.
/// Updates the evaluated project, but does not affect anything else in the project until reevaluation. For example,
/// if a piece of metadata named "m" is added on item of type "i", it does not affect "j" which is evaluated from "@(j->'%(m)')" until reevaluation.
/// Also if the unevaluated value of "m" is set to something that is modified by evaluation, such as "$(p)", the evaluated value will be set to literally "$(p)" until reevaluation.
/// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state without a reevaluation.
/// Returns the new or existing metadatum.
/// </summary>
/// <remarks>Unevaluated value is assumed to be escaped as necessary</remarks>
public ProjectMetadata SetMetadataValue(string name, string unevaluatedValue)
{
return Link != null ? Link.SetMetadataValue(name, unevaluatedValue, false) :
SetMetadataOperation(name, unevaluatedValue, propagateMetadataToSiblingItems: false);
}
/// <summary>
/// Overload of <see cref="SetMetadataValue(string,string)"/>. Adds the option of not splitting the item element and thus affecting all sibling items.
/// Sibling items are defined as all ProjectItem instances that were created from the same item element.
///
/// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state without a reevaluation
/// </summary>
/// /// <param name="name">Metadata name</param>
/// <param name="unevaluatedValue">Metadata value</param>
/// <param name="propagateMetadataToSiblingItems">
/// If true, adds direct metadata to the <see cref="ProjectItemElement"/> from which this <see cref="ProjectItem"/> originated. The intent is to affect all other sibling items.
/// </param>
/// <returns>Returns the new or existing metadatum.</returns>
public ProjectMetadata SetMetadataValue(string name, string unevaluatedValue, bool propagateMetadataToSiblingItems)
{
return Link != null ? Link.SetMetadataValue(name, unevaluatedValue, propagateMetadataToSiblingItems) :
SetMetadataOperation(name, unevaluatedValue, propagateMetadataToSiblingItems: propagateMetadataToSiblingItems);
}
private ProjectMetadata SetMetadataOperation(string name, string unevaluatedValue, bool propagateMetadataToSiblingItems)
{
Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject);
XmlUtilities.VerifyThrowArgumentValidElementName(name);
ErrorUtilities.VerifyThrowArgument(!ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name);
ErrorUtilities.VerifyThrowInvalidOperation(!XMakeElements.ReservedItemNames.Contains(name), "CannotModifyReservedItemMetadata", name);
ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent != null, "OM_ObjectIsNoLongerActive");
if (!propagateMetadataToSiblingItems)
{
_project.SplitItemElementIfNecessary(_xml);
}
ProjectMetadata metadatum;
if (_directMetadata?.Contains(name) == true)
{
metadatum = _directMetadata[name];
metadatum.UnevaluatedValue = unevaluatedValue;
}
else
{
ProjectMetadataElement metadatumXml = _xml.AddMetadata(name, unevaluatedValue);
string evaluatedValueEscaped = _project.ExpandMetadataValueBestEffortLeaveEscaped(this, unevaluatedValue, metadatumXml.Location);
metadatum = new ProjectMetadata(this, metadatumXml, evaluatedValueEscaped, null /* predecessor unknown */);
}
if (!propagateMetadataToSiblingItems)
{
_directMetadata ??= new PropertyDictionary<ProjectMetadata>();
_directMetadata.Set(metadatum);
}
else
{
var siblingItems = _project.Items.Where(i => i._xml == _xml);
foreach (var siblingItem in siblingItems)
{
siblingItem._directMetadata ??= new PropertyDictionary<ProjectMetadata>();
siblingItem._directMetadata.Set(metadatum.DeepClone());
}
}
return metadatum;
}
/// <summary>
/// Removes any metadata with the specified name.
/// Returns true if the evaluated metadata existed, otherwise false.
/// If the metadata name is one of the built-in metadata, like "FullPath", throws InvalidArgumentException.
/// If the metadata originates in an item definition, and was not overridden, throws InvalidOperationException.
/// </summary>
public bool RemoveMetadata(string name)
{
if (Link != null)
{
return Link.RemoveMetadata(name);
}
ErrorUtilities.VerifyThrowArgumentLength(name);
ErrorUtilities.VerifyThrowArgument(!ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name);
Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject);
ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent != null, "OM_ObjectIsNoLongerActive");
ProjectMetadata metadatum = _directMetadata?[name];
if (metadatum == null)
{
ProjectMetadata itemDefinitionMetadata = GetItemDefinitionMetadata(name);
ErrorUtilities.VerifyThrowInvalidOperation(itemDefinitionMetadata == null, "OM_CannotRemoveMetadataOriginatingFromItemDefinition", name);
return false;
}
_project.SplitItemElementIfNecessary(_xml);
// New metadata objects may have been created
metadatum = _directMetadata[name];
_xml.RemoveChild(metadatum.Xml);
_directMetadata.Remove(name);
return true;
}
/// <summary>
/// Renames the item.
/// Equivalent to setting the <see cref="UnevaluatedInclude"/> value.
/// Generally, no expansion occurs. This is because it would potentially result in several items,
/// which is not meaningful semantics when renaming a single item.
/// However if the item does not need to be split (which would invalidate its ProjectItemElement),
/// and the new value expands to exactly one item, then its evaluated include is updated
/// with the expanded value, rather than the unexpanded value.
/// </summary>
/// <remarks>
/// Even if the new value expands to zero items, we do not expand it.
/// The common case we are interested in for expansion here is setting something
/// like "$(sourcesroot)\foo.cs" and expanding that to a single item.
/// If say "@(foo)" is set as the new name, and it expands to blank, that might
/// be surprising to the host and maybe even unhandled, if on full reevaluation
/// it wouldn’t expand to blank. That’s why we're being cautious and supporting
/// the most common scenario only.
/// Many hosts will do a ReevaluateIfNecessary before reading anyway.
/// </remarks>
public void Rename(string name)
{
if (Link != null)
{
Link.Rename(name);
return;
}
Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject);
ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent != null, "OM_ObjectIsNoLongerActive");
if (String.Equals(UnevaluatedInclude, name, StringComparison.Ordinal))
{
return;
}
_cachedModifiers.Clear(); // Clear cached values
if (_xml.Count == 0 /* no metadata */ && _project.IsSuitableExistingItemXml(_xml, name, null /* no metadata */) && !FileMatcher.HasWildcardsSemicolonItemOrPropertyReferences(name))
{
_evaluatedIncludeEscaped = name;
// Fast item lookup tables are invalid now.
// Make sure that when the caller invokes ReevaluateIfNecessary() that they'll be refreshed.
Project.MarkDirty();
return;
}
bool splitOccurred = _project.SplitItemElementIfNecessary(_xml);
_xml.Include = name;
if (splitOccurred)
{
_evaluatedIncludeEscaped = name;
}
else
{
_evaluatedIncludeEscaped = _project.ExpandItemIncludeBestEffortLeaveEscaped(_xml);
}
}
#region IMetadataTable Members
/// <summary>
/// Retrieves any value we have in our metadata table for the metadata name specified.
/// If no value is available, returns empty string.
/// Value, if escaped, remains escaped.
/// </summary>
string IMetadataTable.GetEscapedValue(string name)
{
string value = ((IMetadataTable)this).GetEscapedValue(null, name);
return value;
}
/// <summary>
/// Retrieves any value we have in our metadata table for the metadata name and item type specified.
/// If no value is available, returns empty string.
/// If item type is null, it is ignored, otherwise it must match.
/// Value, if escaped, remains escaped.
/// </summary>
string IMetadataTable.GetEscapedValue(string itemType, string name)
{
string value = ((IMetadataTable)this).GetEscapedValueIfPresent(itemType, name);
return value ?? String.Empty;
}
/// <summary>
/// Returns the value if it exists.
/// If no value is available, returns null.
/// If item type is null, it is ignored, otherwise it must match.
/// Value, if escaped, remains escaped.
/// </summary>
string IMetadataTable.GetEscapedValueIfPresent(string itemType, string name)
{
if (itemType == null || MSBuildNameIgnoreCaseComparer.Default.Equals(ItemType, itemType))
{
string value = ((IItem)this).GetMetadataValueEscaped(name);
if (value.Length > 0 || HasMetadata(name))
{
return value;
}
}
return null;
}
#endregion
/// <summary>
/// Changes the item type of this item.
/// Until reevaluation puts it in the correct place, it will be placed at
/// the end of the list of items of its new type.
/// </summary>
/// <remarks>
/// This is a little involved, as it requires replacing
/// the XmlElement, and updating the project's datastructures.
/// </remarks>
internal void ChangeItemType(string newItemType)
{
ErrorUtilities.VerifyThrowArgumentLength(newItemType, "ItemType");
Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject);
ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent != null, "OM_ObjectIsNoLongerActive");
if (String.Equals(ItemType, newItemType, StringComparison.Ordinal))
{
return;
}
_project.SplitItemElementIfNecessary(_xml);
_project.RemoveItemBeforeItemTypeChange(this);
// xml.ChangeItemType will throw if new item type is invalid. Make sure we re-add the item anyway
try
{
_xml.ChangeItemType(newItemType);
}
finally
{
_project.ReAddExistingItemAfterItemTypeChange(this);
}
}
/// <summary>
/// Creates new xml objects for itself, disconnecting from the old xml objects.
/// Called ONLY by <see cref="Microsoft.Build.Evaluation.Project.SplitItemElementIfNecessary(ProjectItemElement)"/>
/// </summary>
/// <remarks>
/// Called when breaking up a single ProjectItemElement that evaluates into several ProjectItems.
/// </remarks>
internal void SplitOwnItemElement()
{
ProjectItemElement oldXml = _xml;
_xml = _xml.ContainingProject.CreateItemElement(ItemType, ((IItem)this).EvaluatedIncludeEscaped);
oldXml.Parent.InsertBeforeChild(_xml, oldXml);
if (_directMetadata == null)
{
return;
}
// ProjectMetadata objects may be being shared with other ProjectItem objects,
// or originate from item definitions, so it is necessary to replace ours with
// new ones.
List<ProjectMetadata> temporary = new List<ProjectMetadata>(_directMetadata.Count);
foreach (ProjectMetadata metadatum in _directMetadata)
{
temporary.Add(metadatum);
}
_directMetadata = new PropertyDictionary<ProjectMetadata>(_directMetadata.Count);
foreach (ProjectMetadata metadatum in temporary)
{
SetMetadataValue(metadatum.Name, metadatum.EvaluatedValueEscaped);
}
}
/// <summary>
/// Helper to get the value of a built-in metadatum with
/// the specified name, if any.
/// </summary>
private string GetBuiltInMetadataEscaped(string name)
=> ItemSpecModifiers.TryGetModifierKind(name, out ItemSpecModifierKind modifierKind)
? BuiltInMetadata.GetMetadataValueEscaped(_project.DirectoryPath, _evaluatedIncludeBeforeWildcardExpansionEscaped, _evaluatedIncludeEscaped, Xml.ContainingProject.FullPath, modifierKind, ref _cachedModifiers)
: null;
/// <summary>
/// Retrieves the named metadata from the item definition, if any.
/// If it is not present, returns null
/// </summary>
/// <param name="name">The metadata name.</param>
/// <returns>The value if it exists, null otherwise.</returns>
private ProjectMetadata GetItemDefinitionMetadata(string name)
{
ProjectMetadata metadataFromDefinition = null;
// Check any inherited item definition metadata first. It's more like
// direct metadata, but we didn't want to copy the tables.
if (_inheritedItemDefinitions != null)
{
foreach (ProjectItemDefinition inheritedItemDefinition in _inheritedItemDefinitions)
{
metadataFromDefinition = inheritedItemDefinition.GetMetadata(name);
if (metadataFromDefinition != null)
{
return metadataFromDefinition;
}
}
}
// Now try regular item definition metadata for this item type.
ProjectItemDefinition itemDefinition;
if (_project.ItemDefinitions.TryGetValue(ItemType, out itemDefinition))
{
metadataFromDefinition = itemDefinition.GetMetadata(name);
}
return metadataFromDefinition;
}
/// <summary>
/// A class factory for ProjectItems.
/// </summary>
internal class ProjectItemFactory : IItemFactory<ProjectItem, ProjectItem>
{
/// <summary>
/// The Project with which each item should be associated.
/// </summary>
private readonly Project _project;
/// <summary>
/// The project item's XML
/// </summary>
private ProjectItemElement _xml;
/// <summary>
/// Creates an item factory which does not specify an item xml. The item xml must
/// be specified later.
/// </summary>
/// <param name="project">The project for items generated.</param>
internal ProjectItemFactory(Project project)
{
_project = project;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="project">The project for items generated.</param>
/// <param name="xml">The xml for items generated.</param>
internal ProjectItemFactory(Project project, ProjectItemElement xml)
{
_project = project;
_xml = xml;
}
/// <summary>
/// Item type that items created by this factory will have.
/// </summary>
public string ItemType
{
get { return _xml.ItemType; }
set { ErrorUtilities.ThrowInternalError("Cannot change the item type on ProjectItem.ProjectItemFactory"); }
}
/// <summary>
/// Set the item xml from which items will be created.
/// Used by the evaluator only.
/// </summary>
public ProjectItemElement ItemElement
{
set { _xml = value; }
}
/// <summary>
/// Creates an item with the specified type and evaluated include.
/// Used for making items from "just strings" and from expressions like "@(Compile, ';')"
/// </summary>
/// <param name="include">The include.</param>
/// <param name="definingProject">The path to the project that defined the item.</param>
/// <returns>A new project item.</returns>
/// <comments>
/// NOTE: defining project is ignored because we already know the ItemElement associated with
/// this item, and use that for where it is defined.
/// </comments>
public ProjectItem CreateItem(string include, string definingProject)
{
return CreateItem(include, include, definingProject);
}
/// <summary>
/// Creates an item based on the provided item, but with
/// the project and xml of this factory. Metadata is cloned,
/// but continues to point to the original ProjectMetadataElement objects.
/// This is to support the scenario Include="@(i)" where we are copying
/// metadata, and are happy to see changes in the original metadata, but
/// setting metadata should create new XML.
/// </summary>
/// <comments>
/// NOTE: defining project is ignored because we already know the ItemElement associated with
/// this item, and use that for where it is defined.
/// </comments>
public ProjectItem CreateItem(ProjectItem source, string definingProject)
{
return CreateItem(source._evaluatedIncludeEscaped, source._evaluatedIncludeBeforeWildcardExpansionEscaped, source);
}
/// <summary>
/// Creates an item based on the provided item, but with
/// the project and xml of this factory and the specified include. Metadata is cloned,
/// but continues to point to the original ProjectMetadataElement objects.
/// This is to support this scenario: Include="@(i->'xxx')"
/// </summary>
/// <remarks>
/// If the item type of the source is the same as the item type of the destination,
/// then it's not necessary to copy metadata originating in an item definition.
/// If it's not, we have to clone that too.
/// </remarks>
/// <comments>
/// NOTE: defining project is ignored because we already know the ItemElement associated with
/// this item, and use that for where it is defined.
/// </comments>
public ProjectItem CreateItem(string evaluatedIncludeEscaped, ProjectItem source, string definingProject)
{
return CreateItem(evaluatedIncludeEscaped, evaluatedIncludeEscaped, source);
}
/// <summary>
/// Creates an item with the specified include and include before wildcard expansion.
/// This is to support creating items from an include that may have a wildcard expression in it.