-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathLookup.cs
1477 lines (1312 loc) · 62.8 KB
/
Lookup.cs
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;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Build.Collections;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Shared;
using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames;
#nullable disable
namespace Microsoft.Build.BackEnd
{
using ItemsMetadataUpdateDictionary = System.Collections.Generic.Dictionary<Microsoft.Build.Execution.ProjectItemInstance, Microsoft.Build.BackEnd.Lookup.MetadataModifications>;
using ItemTypeToItemsMetadataUpdateDictionary = System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<Microsoft.Build.Execution.ProjectItemInstance, Microsoft.Build.BackEnd.Lookup.MetadataModifications>>;
/// <summary>
/// Contains a list of item and property collections, optimized to allow
/// - very fast "cloning"
/// - quick lookups
/// - scoping down of item subsets in nested scopes (useful for batches)
/// - isolation of adds, removes, modifies, and property sets inside nested scopes
///
/// When retrieving the item group for an item type, each table is consulted in turn,
/// starting with the primary table (the "top" or "innermost" table), until a table is found that has an entry for that type.
/// When an entry is found, it is returned without looking deeper.
/// This makes it possible to let callers see only a subset of items without affecting or cloning the original item groups,
/// by populating a scope with item groups that are empty or contain subsets of items in lower scopes.
///
/// Instances of this class can be cloned with Clone() to share between batches.
///
/// When EnterScope() is called, a fresh primary table is inserted, and all adds and removes will be invisible to
/// any clones made before the scope was entered and anyone who has access to item groups in lower tables.
///
/// When LeaveScope() is called, the primary tables are merged into the secondary tables, and the primary tables are discarded.
/// This makes the adds and removes in the primary tables visible to clones made during the previous scope.
///
/// Scopes can be populated (before Adds, Removes, and Lookups) using PopulateWithItem(). This reduces the set of items of a particular
/// type that are visible in a scope, because lookups of items of this type will stop at this level and see the subset, rather than the
/// larger set in a scope below.
///
/// Items can be added or removed by calling AddNewItem() and RemoveItem(). Only the primary level is modified.
/// When items are added or removed they enter into a primary table exclusively for adds or removes, instead of the main primary table.
/// This allows the adds and removes to be applied to the scope below on LeaveScope(). Even when LeaveScope() is called, the adds and removes
/// stay in their separate add and remove tables: if they were applied to a main table, they could truncate the downward traversal performed by lookups
/// and hide items in a lower main table. Only on the final call of LeaveScope() can all adds and removes be applied to the outermost table, i.e., the project.
///
/// Much the same applies to properties.
///
/// For sensible semantics, only the current primary scope can be modified at any point.
/// </summary>
internal class Lookup : IPropertyProvider<ProjectPropertyInstance>, IItemProvider<ProjectItemInstance>
{
#region Fields
/// <summary>
/// Ordered list of scope used for lookup.
/// Each scope contains multiple tables:
/// - the main item table (populated with subsets of lists, in order to create batches)
/// - the add table (items that have been added during execution)
/// - the remove table (items that have been removed during execution)
/// - the modify table (item metadata modifications)
/// - the main property table (populated with properties that are visible in this scope)
/// - the property set table (changes made to properties)
/// All have to be consulted to find the items and properties available in the current scope.
/// We have to keep them separate, because the adds and removes etc need to be applied to the table
/// below when we leave a scope.
/// </summary>
private LinkedList<Lookup.Scope> _lookupScopes = new LinkedList<Lookup.Scope>();
/// <summary>
/// When we are asked for all the items of a certain type using the GetItems() method, we may have to handle items
/// that have been modified earlier with ModifyItems(). These pending modifications can't be applied immediately to
/// the item because that would affect other batches. Instead we clone the item, apply the modification, and hand that over.
/// The problem is that later we might get asked to remove or modify that item. We want to make sure that we record that as
/// a remove or modify of the real item, not the clone we handed over. So we keep a lookup of (clone, original) to consult.
/// </summary>
private Dictionary<ProjectItemInstance, ProjectItemInstance> _cloneTable;
#endregion
#region Constructors
/// <summary>
/// Construct a lookup over specified items and properties.
/// </summary>
internal Lookup(IItemDictionary<ProjectItemInstance> projectItems, PropertyDictionary<ProjectPropertyInstance> properties)
{
ErrorUtilities.VerifyThrowInternalNull(projectItems);
ErrorUtilities.VerifyThrowInternalNull(properties);
Lookup.Scope scope = new Lookup.Scope(this, "Lookup()", projectItems, properties);
_lookupScopes.AddFirst(scope);
}
/// <summary>
/// Copy constructor (called via Clone() - clearer semantics)
/// </summary>
private Lookup(Lookup that)
{
// Add the same tables from the original
foreach (Lookup.Scope scope in that._lookupScopes)
{
_lookupScopes.AddLast(scope);
}
// Clones need to share an (item)clone table; the batching engine asks for items from the lookup,
// then populates buckets with them, which have clone lookups.
_cloneTable = that._cloneTable;
}
#endregion
#region Properties
// Convenience private properties
// "Primary" is the "top" or "innermost" scope
// "Secondary" is the next from the top.
private IItemDictionary<ProjectItemInstance> PrimaryTable
{
get { return _lookupScopes.First.Value.Items; }
set { _lookupScopes.First.Value.Items = value; }
}
private ItemDictionary<ProjectItemInstance> PrimaryAddTable
{
get { return _lookupScopes.First.Value.Adds; }
set { _lookupScopes.First.Value.Adds = value; }
}
private ItemDictionary<ProjectItemInstance> PrimaryRemoveTable
{
get { return _lookupScopes.First.Value.Removes; }
set { _lookupScopes.First.Value.Removes = value; }
}
private ItemTypeToItemsMetadataUpdateDictionary PrimaryModifyTable
{
get { return _lookupScopes.First.Value.Modifies; }
set { _lookupScopes.First.Value.Modifies = value; }
}
private PropertyDictionary<ProjectPropertyInstance> PrimaryPropertySets
{
get { return _lookupScopes.First.Value.PropertySets; }
set { _lookupScopes.First.Value.PropertySets = value; }
}
private IItemDictionary<ProjectItemInstance> SecondaryTable
{
get { return _lookupScopes.First.Next.Value.Items; }
set { _lookupScopes.First.Next.Value.Items = value; }
}
private ItemDictionary<ProjectItemInstance> SecondaryAddTable
{
get { return _lookupScopes.First.Next.Value.Adds; }
set { _lookupScopes.First.Next.Value.Adds = value; }
}
private ItemDictionary<ProjectItemInstance> SecondaryRemoveTable
{
get { return _lookupScopes.First.Next.Value.Removes; }
set { _lookupScopes.First.Next.Value.Removes = value; }
}
private ItemTypeToItemsMetadataUpdateDictionary SecondaryModifyTable
{
get { return _lookupScopes.First.Next.Value.Modifies; }
set { _lookupScopes.First.Next.Value.Modifies = value; }
}
private PropertyDictionary<ProjectPropertyInstance> SecondaryProperties
{
get { return _lookupScopes.First.Next.Value.Properties; }
set { _lookupScopes.First.Next.Value.Properties = value; }
}
private PropertyDictionary<ProjectPropertyInstance> SecondaryPropertySets
{
get { return _lookupScopes.First.Next.Value.PropertySets; }
set { _lookupScopes.First.Next.Value.PropertySets = value; }
}
#endregion
#region Internal Methods
/// <summary>
/// Compares the primary property sets of the passed in lookups to determine if there are properties which are shared between
/// the lookups. We find these shared property names because this indicates that the current Lookup is overriding the property value of another Lookup
/// When an override is detected a messages is generated to inform the users that the property is being changed between batches
/// </summary>
/// <returns>array or error messages to log </returns>
internal List<string> GetPropertyOverrideMessages(Dictionary<string, string> lookupHash)
{
List<string> errorMessages = null;
// For each batch lookup list we need to compare the property items to see if they have already been set
if (PrimaryPropertySets != null)
{
foreach (ProjectPropertyInstance property in PrimaryPropertySets)
{
if (String.Equals(property.Name, ReservedPropertyNames.lastTaskResult, StringComparison.OrdinalIgnoreCase))
{
continue;
}
string propertyName = property.Name;
// If the hash contains the property name, output a messages that displays the previous property value and the new property value
if (lookupHash.TryGetValue(propertyName, out string propertyValue))
{
if (errorMessages == null)
{
errorMessages = new List<string>();
}
errorMessages.Add(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("PropertyOutputOverridden", propertyName, EscapingUtilities.UnescapeAll(propertyValue), property.EvaluatedValue));
}
// Set the value of the hash to the new property value
// PERF: we store the EvaluatedValueEscaped here to avoid unnecessary unescaping (the value is stored
// escaped in the property)
lookupHash[propertyName] = ((IProperty)property).EvaluatedValueEscaped;
}
}
return errorMessages;
}
/// <summary>
/// Clones this object, to create another one with its own list, but the same contents.
/// Then the clone can enter scope and have its own fresh primary list without affecting the other object.
/// </summary>
internal Lookup Clone()
{
return new Lookup(this);
}
/// <summary>
/// Enters the scope using the specified description.
/// Callers keep the scope in order to pass it to <see cref="LeaveScope">LeaveScope</see>.
/// </summary>
internal Lookup.Scope EnterScope(string description)
{
// We don't create the tables unless we need them
Scope scope = new Scope(this, description, null, null);
_lookupScopes.AddFirst(scope);
return scope;
}
/// <summary>
/// Leaves the specified scope, which must be the active one.
/// Moves all tables up one: the tertiary table becomes the secondary table, and so on. The primary
/// and secondary table are merged. This has the effect of "applying" the adds applied to the primary
/// table into the secondary table.
/// </summary>
private void LeaveScope(Lookup.Scope scopeToLeave)
{
ErrorUtilities.VerifyThrow(_lookupScopes.Count >= 2, "Too many calls to Leave().");
ErrorUtilities.VerifyThrow(Object.ReferenceEquals(scopeToLeave, _lookupScopes.First.Value), "Attempting to leave with scope '{0}' but scope '{1}' is on top of the stack.", scopeToLeave.Description, _lookupScopes.First.Value.Description);
// Our lookup works by stopping the first time it finds an item group of the appropriate type.
// So we can't apply an add directly into the table below because that could create a new group
// of that type, which would cause the next lookup to stop there and miss any existing items in a table below.
// Instead we keep adds stored separately until we're leaving the very last scope. Until then
// we only move adds down into the next add table below, and when we lookup we consider both tables.
// Same applies to removes.
if (_lookupScopes.Count == 2)
{
MergeScopeIntoLastScope();
}
else
{
MergeScopeIntoNotLastScope();
}
// Let go of our pointer into the clone table; we assume we won't need it after leaving scope and want to save memory.
// This is an assumption on IntrinsicTask, that it won't ask to remove or modify a clone in a higher scope than it was handed out in.
// We mustn't call cloneTable.Clear() because other clones of this lookup may still be using it. When the last lookup clone leaves scope,
// the table will be collected.
_cloneTable = null;
// Move all tables up one, discarding the primary tables
_lookupScopes.RemoveFirst();
}
/// <summary>
/// Leaving an arbitrary scope, just merging all the adds, removes, modifies, and sets into the scope below.
/// </summary>
private void MergeScopeIntoNotLastScope()
{
// Move all the adds down
if (PrimaryAddTable != null)
{
if (SecondaryAddTable == null)
{
SecondaryAddTable = PrimaryAddTable;
}
else
{
SecondaryAddTable.ImportItems(PrimaryAddTable);
}
}
// Move all the removes down
if (PrimaryRemoveTable != null)
{
if (SecondaryRemoveTable == null)
{
SecondaryRemoveTable = PrimaryRemoveTable;
}
else
{
// When merging remove lists from two or more batches both tables (primary and secondary) may contain identical items. The reason is when removing the items we get the original items rather than a clone
// so the same item may have already been added to the secondary table. If we then try and add the same item from the primary table we will get a duplicate key exception from the
// dictionary. Therefore we must not merge in an item if it already is in the secondary remove table.
foreach (ProjectItemInstance item in PrimaryRemoveTable)
{
if (!SecondaryRemoveTable.Contains(item))
{
SecondaryRemoveTable.Add(item);
}
}
}
}
// Move all the modifies down
if (PrimaryModifyTable != null)
{
if (SecondaryModifyTable == null)
{
SecondaryModifyTable = PrimaryModifyTable;
}
else
{
foreach (KeyValuePair<string, Dictionary<ProjectItemInstance, MetadataModifications>> entry in PrimaryModifyTable)
{
Dictionary<ProjectItemInstance, MetadataModifications> modifiesOfType;
if (SecondaryModifyTable.TryGetValue(entry.Key, out modifiesOfType))
{
// There are already modifies of this type: add to the existing table
foreach (KeyValuePair<ProjectItemInstance, MetadataModifications> modify in entry.Value)
{
MergeModificationsIntoModificationTable(modifiesOfType, modify, ModifyMergeType.SecondWins);
}
}
else
{
SecondaryModifyTable.Add(entry.Key, entry.Value);
}
}
}
}
// Move all the sets down
if (PrimaryPropertySets != null)
{
if (SecondaryPropertySets == null)
{
SecondaryPropertySets = PrimaryPropertySets;
}
else
{
SecondaryPropertySets.ImportProperties(PrimaryPropertySets);
}
}
}
/// <summary>
/// Merge the current scope down into the base scope. This means applying the adds, removes, modifies, and sets
/// directly into the item and property tables in this scope.
/// </summary>
private void MergeScopeIntoLastScope()
{
// End of the line for this object: we are done with add tables, and we want to expose our
// adds to the world
if (PrimaryAddTable != null)
{
SecondaryTable ??= new ItemDictionary<ProjectItemInstance>();
SecondaryTable.ImportItems(PrimaryAddTable);
}
if (PrimaryRemoveTable != null)
{
SecondaryTable ??= new ItemDictionary<ProjectItemInstance>();
SecondaryTable.RemoveItems(PrimaryRemoveTable);
}
if (PrimaryModifyTable != null)
{
foreach (KeyValuePair<string, Dictionary<ProjectItemInstance, MetadataModifications>> entry in PrimaryModifyTable)
{
SecondaryTable ??= new ItemDictionary<ProjectItemInstance>();
ApplyModificationsToTable(SecondaryTable, entry.Key, entry.Value);
}
}
if (PrimaryPropertySets != null)
{
SecondaryProperties ??= new PropertyDictionary<ProjectPropertyInstance>(PrimaryPropertySets.Count);
SecondaryProperties.ImportProperties(PrimaryPropertySets);
}
}
/// <summary>
/// Gets the effective property for the current scope.
/// taking the name from the provided string within the specified start and end indexes.
/// If no match is found, returns null.
/// Caller must not modify the property returned.
/// </summary>
public ProjectPropertyInstance GetProperty(string name, int startIndex, int endIndex)
{
// Walk down the tables and stop when the first
// property with this name is found
foreach (Scope scope in _lookupScopes)
{
if (scope.PropertySets != null)
{
ProjectPropertyInstance property = scope.PropertySets.GetProperty(name, startIndex, endIndex);
if (property != null)
{
return property;
}
}
if (scope.Properties != null)
{
ProjectPropertyInstance property = scope.Properties.GetProperty(name, startIndex, endIndex);
if (property != null)
{
return property;
}
}
if (scope.TruncateLookupsAtThisScope)
{
break;
}
}
return null;
}
/// <summary>
/// Gets the effective property for the current scope.
/// If no match is found, returns null.
/// Caller must not modify the property returned.
/// </summary>
public ProjectPropertyInstance GetProperty(string name)
{
ErrorUtilities.VerifyThrowInternalLength(name, nameof(name));
return GetProperty(name, 0, name.Length - 1);
}
/// <summary>
/// Gets the items of the specified type that are visible in the current scope.
/// If no match is found, returns an empty list.
/// Caller must not modify the group returned.
/// </summary>
public ICollection<ProjectItemInstance> GetItems(string itemType)
{
// The visible items consist of the adds (accumulated as we go down)
// plus the first set of regular items we encounter
// minus any removes
List<ProjectItemInstance> allAdds = null;
List<ProjectItemInstance> allRemoves = null;
Dictionary<ProjectItemInstance, MetadataModifications> allModifies = null;
ICollection<ProjectItemInstance> groupFound = null;
foreach (Scope scope in _lookupScopes)
{
// Accumulate adds while we look downwards
if (scope.Adds != null)
{
ICollection<ProjectItemInstance> adds = scope.Adds[itemType];
if (adds.Count != 0)
{
if (allAdds == null)
{
allAdds = new List<ProjectItemInstance>(adds);
}
else
{
allAdds.AddRange(adds);
}
}
}
// Accumulate removes while we look downwards
if (scope.Removes != null)
{
ICollection<ProjectItemInstance> removes = scope.Removes[itemType];
if (removes.Count != 0)
{
if (allRemoves == null)
{
allRemoves = new List<ProjectItemInstance>(removes);
}
else
{
allRemoves.AddRange(removes);
}
}
}
// Accumulate modifications as we look downwards
if (scope.Modifies != null)
{
Dictionary<ProjectItemInstance, MetadataModifications> modifies;
if (scope.Modifies.TryGetValue(itemType, out modifies))
{
if (modifies.Count != 0)
{
allModifies ??= new Dictionary<ProjectItemInstance, MetadataModifications>(modifies.Count);
// We already have some modifies for this type
foreach (KeyValuePair<ProjectItemInstance, MetadataModifications> modify in modifies)
{
// If earlier scopes modify the same metadata on the same item,
// they have priority
MergeModificationsIntoModificationTable(allModifies, modify, ModifyMergeType.FirstWins);
}
}
}
}
if (scope.Items != null)
{
groupFound = scope.Items[itemType];
if (groupFound.Count != 0 || scope.Items.HasEmptyMarker(itemType))
{
// Found a group: we go no further
break;
}
}
if (scope.TruncateLookupsAtThisScope)
{
break;
}
}
if ((allAdds == null) &&
(allRemoves == null) &&
(allModifies == null))
{
// We can just hand out this group verbatim -
// that avoids any importing
groupFound ??= Array.Empty<ProjectItemInstance>();
return groupFound;
}
// Set the initial sizes to avoid resizing during import
int itemsTypesCount = 1; // We're only ever importing a single item type
int itemsCount = groupFound?.Count ?? 0; // Start with initial set
itemsCount += allAdds?.Count ?? 0; // Add all the additions
itemsCount -= allRemoves?.Count ?? 0; // Remove the removals
if (itemsCount < 0)
{
itemsCount = 0;
}
// We have adds and/or removes and/or modifies to incorporate.
// We can't modify the group, because that might
// be visible to other batches; we have to create
// a new one.
ItemDictionary<ProjectItemInstance> result = new ItemDictionary<ProjectItemInstance>(itemsTypesCount, itemsCount);
if (groupFound != null)
{
result.ImportItemsOfType(itemType, groupFound);
}
// Removes are processed after adds; this means when we remove there's no need to concern ourselves
// with the case where the item being removed is in an add table somewhere. The converse case is not possible
// using a project file: a project file cannot create an item that was already removed, it can only create
// a unique new item.
if (allAdds != null)
{
result.ImportItemsOfType(itemType, allAdds);
}
if (allRemoves != null)
{
result.RemoveItems(allRemoves);
}
// Modifies can be processed last; if a modified item was removed, the modify will be ignored
if (allModifies != null)
{
ApplyModifies(result, allModifies);
}
return result[itemType];
}
/// <summary>
/// Populates with an item group. This is done before the item lookup is used in this scope.
/// Assumes all the items in the group have the same, provided, type.
/// Assumes there is no item group of this type in the primary table already.
/// Should be used only by batching buckets, and if no items are passed,
/// explicitly stores a marker for this item type indicating this.
/// </summary>
internal void PopulateWithItems(string itemType, ICollection<ProjectItemInstance> group)
{
PrimaryTable ??= new ItemDictionary<ProjectItemInstance>();
ICollection<ProjectItemInstance> existing = PrimaryTable[itemType];
ErrorUtilities.VerifyThrow(existing.Count == 0, "Cannot add an itemgroup of this type.");
if (group.Count > 0)
{
PrimaryTable.ImportItemsOfType(itemType, group);
}
else
{
PrimaryTable.AddEmptyMarker(itemType);
}
}
/// <summary>
/// Populates with an item. This is done before the item lookup is used in this scope.
/// There may or may not already be a group for it.
/// </summary>
internal void PopulateWithItem(ProjectItemInstance item)
{
PrimaryTable ??= new ItemDictionary<ProjectItemInstance>();
PrimaryTable.Add(item);
}
/// <summary>
/// Apply a property to this scope.
/// </summary>
internal void SetProperty(ProjectPropertyInstance property)
{
// Setting in outer scope could be easily implemented, but our code does not do it at present
MustNotBeOuterScope();
// Put in the set table
PrimaryPropertySets ??= new PropertyDictionary<ProjectPropertyInstance>();
PrimaryPropertySets.Set(property);
}
/// <summary>
/// Implements a true add, an item that has been created in a batch.
/// </summary>
internal void AddNewItemsOfItemType(string itemType, ICollection<ProjectItemInstance> group, bool doNotAddDuplicates = false, Action<IList> logFunction = null)
{
// Adding to outer scope could be easily implemented, but our code does not do it at present
MustNotBeOuterScope();
#if DEBUG
foreach (ProjectItemInstance item in group)
{
MustNotBeInAnyTables(item);
}
#endif
if (group.Count == 0)
{
return;
}
// Put them in the add table
PrimaryAddTable ??= new ItemDictionary<ProjectItemInstance>();
IEnumerable<ProjectItemInstance> itemsToAdd = group;
if (doNotAddDuplicates)
{
// Ensure we don't also add any that already exist.
var existingItems = GetItems(itemType);
var existingItemsHashSet = existingItems.ToHashSet(ProjectItemInstance.EqualityComparer);
var deduplicatedItemsToAdd = new List<ProjectItemInstance>();
foreach (var item in itemsToAdd)
{
if (existingItemsHashSet.Add(item))
{
deduplicatedItemsToAdd.Add(item);
}
}
itemsToAdd = deduplicatedItemsToAdd;
}
if (logFunction != null)
{
if (doNotAddDuplicates)
{
// itemsToAdd is guaranteed to be a List if we're doing the doNotAddDuplicates part.
logFunction.Invoke(itemsToAdd as List<ProjectItemInstance>);
}
else
{
var groupAsList = group as List<ProjectItemInstance>;
logFunction.Invoke(groupAsList ?? group.ToList());
}
}
PrimaryAddTable.ImportItemsOfType(itemType, itemsToAdd);
}
/// <summary>
/// Implements a true add, an item that has been created in a batch.
/// </summary>
internal void AddNewItem(ProjectItemInstance item)
{
// Adding to outer scope could be easily implemented, but our code does not do it at present
MustNotBeOuterScope();
#if DEBUG
// This item must not be in any table already; a project cannot create an item
// that already exists
MustNotBeInAnyTables(item);
#endif
// Put in the add table
PrimaryAddTable ??= new ItemDictionary<ProjectItemInstance>();
PrimaryAddTable.Add(item);
}
/// <summary>
/// Remove a bunch of items from this scope
/// </summary>
internal void RemoveItems(IEnumerable<ProjectItemInstance> items)
{
foreach (ProjectItemInstance item in items.GetStructEnumerable())
{
RemoveItem(item);
}
}
/// <summary>
/// Remove an item from this scope
/// </summary>
internal void RemoveItem(ProjectItemInstance item)
{
// Removing from outer scope could be easily implemented, but our code does not do it at present
MustNotBeOuterScope();
item = RetrieveOriginalFromCloneTable(item);
// Put in the remove table
PrimaryRemoveTable ??= new ItemDictionary<ProjectItemInstance>();
PrimaryRemoveTable.Add(item);
// No need to remove this item from the primary add table if it's
// already there -- we always apply removes after adds, so that add
// will be reversed anyway.
}
/// <summary>
/// Modifies items in this scope with the same set of metadata modifications.
/// Assumes all the items in the group have the same, provided, type.
/// </summary>
internal void ModifyItems(string itemType, ICollection<ProjectItemInstance> group, MetadataModifications metadataChanges)
{
// Modifying in outer scope could be easily implemented, but our code does not do it at present
MustNotBeOuterScope();
#if DEBUG
// This item should not already be in any remove table; there is no way a project can
// modify items that were already removed
// Obviously, do this only in debug, as it's a slow check for bugs.
LinkedListNode<Scope> node = _lookupScopes.First;
while (node != null)
{
Scope scope = node.Value;
foreach (ProjectItemInstance item in group)
{
ProjectItemInstance actualItem = RetrieveOriginalFromCloneTable(item);
MustNotBeInTable(scope.Removes, actualItem);
}
node = node.Next;
}
#endif
if (!metadataChanges.HasChanges)
{
return;
}
// Put in the modify table
// We don't need to check whether the item is in the add table vs. the main table; either
// way the modification will be applied.
PrimaryModifyTable ??= new ItemTypeToItemsMetadataUpdateDictionary(MSBuildNameIgnoreCaseComparer.Default);
Dictionary<ProjectItemInstance, MetadataModifications> modifiesOfType;
if (!PrimaryModifyTable.TryGetValue(itemType, out modifiesOfType))
{
modifiesOfType = new Dictionary<ProjectItemInstance, MetadataModifications>();
PrimaryModifyTable[itemType] = modifiesOfType;
}
foreach (ProjectItemInstance item in group)
{
// Each item needs its own collection for metadata changes, even if this particular change is the same
// for more than one item, subsequent changes might not be.
var metadataChangeCopy = metadataChanges.Clone();
// If we're asked to modify a clone we handed out, record it as a modify of the original
// instead
ProjectItemInstance actualItem = RetrieveOriginalFromCloneTable(item);
var modify = new KeyValuePair<ProjectItemInstance, MetadataModifications>(actualItem, metadataChangeCopy);
MergeModificationsIntoModificationTable(modifiesOfType, modify, ModifyMergeType.SecondWins);
}
}
#endregion
#region Private Methods
/// <summary>
/// Apply modifies to a temporary result group.
/// Items to be modified are virtual-cloned so the original isn't changed.
/// </summary>
private void ApplyModifies(ItemDictionary<ProjectItemInstance> result, Dictionary<ProjectItemInstance, MetadataModifications> allModifies)
{
// Clone, because we're modifying actual items, and this would otherwise be visible to other batches,
// and would be "published" even if a target fails.
// FUTURE - don't need to clone here for non intrinsic tasks, but at present, they don't do modifies
// Store the clone, in case we're asked to modify or remove it later (we will record it against the real item)
_cloneTable ??= new Dictionary<ProjectItemInstance, ProjectItemInstance>();
foreach (var modify in allModifies)
{
ProjectItemInstance originalItem = modify.Key;
if (result.Contains(originalItem))
{
var modificationsToApply = modify.Value;
// Modify the cloned item and replace the original with it.
ProjectItemInstance cloneItem = modify.Key.DeepClone();
ApplyMetadataModificationsToItem(modificationsToApply, cloneItem);
result.Replace(originalItem, cloneItem);
// This will be null if the item wasn't in the result group, ie, it had been removed after being modified
ErrorUtilities.VerifyThrow(!_cloneTable.ContainsKey(cloneItem), "Should be new, not already in table!");
_cloneTable[cloneItem] = originalItem;
}
}
}
/// <summary>
/// Applies the specified modifications to the supplied item.
/// </summary>
private static void ApplyMetadataModificationsToItem(MetadataModifications modificationsToApply, ProjectItemInstance itemToModify)
{
// Remove any metadata from the item which is slated for removal. The indexer in the modifications table will
// return a modification with Remove == true either if there is an explicit entry for that name in the modifications
// or if keepOnlySpecified == true and there is no entry for that name.
if (modificationsToApply.KeepOnlySpecified)
{
List<string> metadataToRemove = new List<string>(itemToModify.Metadata.Where(m => modificationsToApply[m.Name].Remove).Select(m => m.Name));
foreach (var metadataName in metadataToRemove)
{
itemToModify.RemoveMetadata(metadataName);
}
}
// Now make any additions or modifications
foreach (var modificationPair in modificationsToApply.ExplicitModifications)
{
if (modificationPair.Value.Remove)
{
itemToModify.RemoveMetadata(modificationPair.Key);
}
else if (modificationPair.Value.NewValue != null)
{
itemToModify.SetMetadata(modificationPair.Key, modificationPair.Value.NewValue);
}
}
}
/// <summary>
/// Look up the "real" item by using its clone, and return the real item.
/// See <see cref="_cloneTable"/> for explanation of the clone table.
/// </summary>
private ProjectItemInstance RetrieveOriginalFromCloneTable(ProjectItemInstance item)
{
ProjectItemInstance original;
if (_cloneTable != null)
{
if (_cloneTable.TryGetValue(item, out original))
{
item = original;
}
}
return item;
}
/// <summary>
/// Applies a list of modifications to the appropriate <see cref="ItemDictionary{ProjectItemInstance}" /> in a main table.
/// If any modifications conflict, these modifications win.
/// </summary>
private void ApplyModificationsToTable(IItemDictionary<ProjectItemInstance> table, string itemType, ItemsMetadataUpdateDictionary modify)
{
ICollection<ProjectItemInstance> existing = table[itemType];
if (existing != null)
{
foreach (var kvPair in modify)
{
if (table.Contains(kvPair.Key))
{
var itemToModify = kvPair.Key;
var modificationsToApply = kvPair.Value;
ApplyMetadataModificationsToItem(modificationsToApply, itemToModify);
}
}
}
}
/// <summary>
/// Applies a modification to an item in a table of modifications.
/// If the item already exists in the table, merges in the modifications; if there is a conflict
/// the mergeType indicates which should win.
/// </summary>
private void MergeModificationsIntoModificationTable(Dictionary<ProjectItemInstance, MetadataModifications> modifiesOfType,
KeyValuePair<ProjectItemInstance, MetadataModifications> modify,
ModifyMergeType mergeType)
{
MetadataModifications existingMetadataChanges;
if (modifiesOfType.TryGetValue(modify.Key, out existingMetadataChanges))
{
// There's already modifications for this item; merge with those
if (mergeType == ModifyMergeType.SecondWins)
{
// Merge the new modifications on top of the existing modifications.
existingMetadataChanges.ApplyModifications(modify.Value);
}
else
{
// Only apply explicit modifications.
foreach (var metadataChange in modify.Value.ExplicitModifications)
{
// If the existing metadata change list has an entry for this metadata, ignore this change.
// We continue to allow changes made when KeepOnlySpecified is set because it is assumed that explicit metadata changes
// always trump implicit ones.
if (!existingMetadataChanges.ContainsExplicitModification(metadataChange.Key))
{
existingMetadataChanges[metadataChange.Key] = metadataChange.Value;
}
}
}
}
else
{
modifiesOfType.Add(modify.Key, modify.Value);
}
}
#if DEBUG
/// <summary>
/// Verify item is not in the table
/// </summary>
private void MustNotBeInTable(ItemDictionary<ProjectItemInstance> table, ProjectItemInstance item)
{
if (table?.ItemTypes.Contains(item.ItemType) == true)
{
ICollection<ProjectItemInstance> tableOfItemsOfSameType = table[item.ItemType];
if (tableOfItemsOfSameType != null)
{
ErrorUtilities.VerifyThrow(!tableOfItemsOfSameType.Contains(item), "Item should not be in table");
}
}
}
/// <summary>
/// Verify item is not in the modify table
/// </summary>
private void MustNotBeInTable(ItemTypeToItemsMetadataUpdateDictionary table, ProjectItemInstance item)
{
ItemsMetadataUpdateDictionary tableOfItemsOfSameType = null;
if (table?.TryGetValue(item.ItemType, out tableOfItemsOfSameType) == true)
{
if (tableOfItemsOfSameType is not null)
{
ErrorUtilities.VerifyThrow(!tableOfItemsOfSameType.ContainsKey(item), "Item should not be in table");
}
}
}
/// <summary>
/// Verify item is not in any table in any scope
/// </summary>
private void MustNotBeInAnyTables(ProjectItemInstance item)
{
// This item should not already be in any table; there is no way a project can
// create items that already existed
// Obviously, do this only in debug, as it's a slow check for bugs.
LinkedListNode<Scope> node = _lookupScopes.First;