44
55#if ROSLYN_4_12_0_OR_GREATER
66
7+ using System ;
78using System . Collections . Immutable ;
9+ using System . Diagnostics . CodeAnalysis ;
810using System . Linq ;
911using System . Threading ;
1012using CommunityToolkit . Mvvm . SourceGenerators . Extensions ;
@@ -68,11 +70,11 @@ public override void Initialize(AnalysisContext context)
6870 fieldSymbol . ContainingType ,
6971 fieldSymbol . Name ) ) ;
7072
71- // Notify that we did produce at least one diagnostic. Note: callbacks can run in parallel, so the order
72- // is not guaranteed. As such, there's no point in using an interlocked compare exchange operation here,
73- // since we couldn't rely on the value being written actually being the "first" occurrence anyway.
74- // So we can just do a normal volatile read for better performance .
75- Volatile . Write ( ref firstObservablePropertyAttribute , observablePropertyAttribute ) ;
73+ // Track the attribute data to use as target for the diagnostic. This method takes care of effectively
74+ // sorting all incoming values, so that the final one is deterministic across runs. This ensures that
75+ // the actual location will be the same across recompilations, instead of jumping around all over the
76+ // place. This also makes it possible to more easily suppress it, since its location would not change .
77+ SetOrUpdateAttributeDataBySourceLocation ( ref firstObservablePropertyAttribute , observablePropertyAttribute ) ;
7678 }
7779 } , SymbolKind . Field ) ;
7880
@@ -95,6 +97,59 @@ public override void Initialize(AnalysisContext context)
9597 } ) ;
9698 } ) ;
9799 }
100+
101+ /// <summary>
102+ /// Sets or updates the <see cref="AttributeData"/> instance to use for compilation diagnostics, sorting by source location.
103+ /// </summary>
104+ /// <param name="oldAttributeDataLocation">The location of the previous value to potentially overwrite.</param>
105+ /// <param name="newAttributeData">Thew new <see cref="AttributeData"/> instance.</param>
106+ private static void SetOrUpdateAttributeDataBySourceLocation ( [ NotNull ] ref AttributeData ? oldAttributeDataLocation , AttributeData newAttributeData )
107+ {
108+ bool hasReplacedOriginalValue ;
109+
110+ do
111+ {
112+ AttributeData ? oldAttributeData = Volatile . Read ( ref oldAttributeDataLocation ) ;
113+
114+ // If the old attribute data is null, it means this is the first time we called this method with a new
115+ // attribute candidate. In that case, there is nothing to check: we should always store the new instance.
116+ if ( oldAttributeData is not null )
117+ {
118+ // Sort by file paths, alphabetically
119+ int filePathRelativeSortIndex = string . Compare (
120+ newAttributeData . ApplicationSyntaxReference ? . SyntaxTree . FilePath ,
121+ oldAttributeData . ApplicationSyntaxReference ? . SyntaxTree . FilePath ,
122+ StringComparison . OrdinalIgnoreCase ) ;
123+
124+ // Also sort by location (this is a tie-breaker if two values are from the same file)
125+ bool isTextSpanLowerSorted =
126+ ( newAttributeData . ApplicationSyntaxReference ? . Span . Start ?? 0 ) <
127+ ( oldAttributeData . ApplicationSyntaxReference ? . Span . Start ?? 0 ) ;
128+
129+ // The new candidate can be dropped if it's from a file that's alphabetically sorted after
130+ // the old value, or whether the location is after the previous one, within the same file.
131+ if ( filePathRelativeSortIndex == 1 || ( filePathRelativeSortIndex == 0 && ! isTextSpanLowerSorted ) )
132+ {
133+ break ;
134+ }
135+ }
136+
137+ // Attempt to actually replace the old value, without taking locks
138+ AttributeData ? originalValue = Interlocked . CompareExchange (
139+ location1 : ref oldAttributeDataLocation ,
140+ value : newAttributeData ,
141+ comparand : oldAttributeData ) ;
142+
143+ // This call might have raced against other threads, since all symbol actions can run in parallel.
144+ // If the original value is the old value we read at the start of the method, it means no other
145+ // thread raced against this one, so we are done. If it's different, then we failed to write the
146+ // new candidate. We can discard the work done in this iteration and simply try again.
147+ hasReplacedOriginalValue = oldAttributeData == originalValue ;
148+ }
149+ while ( ! hasReplacedOriginalValue ) ;
150+ #pragma warning disable CS8777 // The loop always ensures that 'oldAttributeDataLocation' is not null on exit
151+ }
152+ #pragma warning restore CS8777
98153}
99154
100155#endif
0 commit comments