4
4
5
5
#if ROSLYN_4_12_0_OR_GREATER
6
6
7
+ using System ;
7
8
using System . Collections . Immutable ;
9
+ using System . Diagnostics . CodeAnalysis ;
8
10
using System . Linq ;
9
11
using System . Threading ;
10
12
using CommunityToolkit . Mvvm . SourceGenerators . Extensions ;
@@ -68,11 +70,11 @@ public override void Initialize(AnalysisContext context)
68
70
fieldSymbol . ContainingType ,
69
71
fieldSymbol . Name ) ) ;
70
72
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 ) ;
76
78
}
77
79
} , SymbolKind . Field ) ;
78
80
@@ -95,6 +97,59 @@ public override void Initialize(AnalysisContext context)
95
97
} ) ;
96
98
} ) ;
97
99
}
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
98
153
}
99
154
100
155
#endif
0 commit comments