Skip to content

Commit 421ad36

Browse files
committed
Update analyzer to emit consistent locations
1 parent ef22336 commit 421ad36

File tree

2 files changed

+68
-9
lines changed

2 files changed

+68
-9
lines changed

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
#if ROSLYN_4_12_0_OR_GREATER
66

7+
using System;
78
using System.Collections.Immutable;
9+
using System.Diagnostics.CodeAnalysis;
810
using System.Linq;
911
using System.Threading;
1012
using 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

tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -526,10 +526,14 @@ public partial class YetAnotherViewModel : ObservableObject
526526
}
527527
""";
528528

529-
await CSharpAnalyzerWithLanguageVersionTest<WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer>.VerifyAnalyzerAsync(
530-
source,
531-
LanguageVersion.CSharp12,
532-
editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]);
529+
// This test is non deterministic, so run it 10 times to ensure the likelihood of it passing just by luck is almost 0
530+
for (int i = 0; i < 10; i++)
531+
{
532+
await CSharpAnalyzerWithLanguageVersionTest<WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer>.VerifyAnalyzerAsync(
533+
source,
534+
LanguageVersion.CSharp12,
535+
editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]);
536+
}
533537
}
534538

535539
[TestMethod]

0 commit comments

Comments
 (0)