Skip to content

Commit c510ecf

Browse files
authored
Fix issue with source generator diagnostics not refreshing on save in… (#79507)
… balanced mode Resolves #75586 Support for this was originally added in #77271 However it seems like `Project.GetDependentVersionAsync` changed to no longer increment when the sg execution version changed, and due to a test issue, the problem was not caught. Now: ![sg_refresh](https://github.com/user-attachments/assets/2fa0a697-2ee8-48a9-b72c-66e9512bcd39) clean val - https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/656041
2 parents 3cefbb3 + e58e14f commit c510ecf

File tree

6 files changed

+33
-62
lines changed

6 files changed

+33
-62
lines changed

src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticsPullCache.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,16 @@ internal abstract partial class AbstractPullDiagnosticHandler<TDiagnosticsParams
2929
/// the version stamp but not the content (for example, forking LSP text).
3030
/// </summary>
3131
private sealed class DiagnosticsPullCache(IGlobalOptionService globalOptions, string uniqueKey)
32-
: VersionedPullCache<(int globalStateVersion, VersionStamp? dependentVersion), (int globalStateVersion, Checksum dependentChecksum), DiagnosticsRequestState, ImmutableArray<DiagnosticData>>(uniqueKey)
32+
: VersionedPullCache<(int globalStateVersion, Checksum dependentChecksum), DiagnosticsRequestState, ImmutableArray<DiagnosticData>>(uniqueKey)
3333
{
3434
private readonly IGlobalOptionService _globalOptions = globalOptions;
3535

36-
public override async Task<(int globalStateVersion, VersionStamp? dependentVersion)> ComputeCheapVersionAsync(DiagnosticsRequestState state, CancellationToken cancellationToken)
37-
{
38-
return (state.GlobalStateVersion, await state.Project.GetDependentVersionAsync(cancellationToken).ConfigureAwait(false));
39-
}
40-
41-
public override async Task<(int globalStateVersion, Checksum dependentChecksum)> ComputeExpensiveVersionAsync(DiagnosticsRequestState state, CancellationToken cancellationToken)
36+
public override async Task<(int globalStateVersion, Checksum dependentChecksum)> ComputeVersionAsync(DiagnosticsRequestState state, CancellationToken cancellationToken)
4237
{
4338
return (state.GlobalStateVersion, await state.Project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false));
4439
}
4540

46-
/// <inheritdoc cref="VersionedPullCache{TCheapVersion, TExpensiveVersion, TState, TComputedData}.ComputeDataAsync(TState, CancellationToken)"/>
41+
/// <inheritdoc cref="VersionedPullCache{TVersion, TState, TComputedData}.ComputeDataAsync(TState, CancellationToken)"/>
4742
public override async Task<ImmutableArray<DiagnosticData>> ComputeDataAsync(DiagnosticsRequestState state, CancellationToken cancellationToken)
4843
{
4944
var diagnostics = await state.DiagnosticSource.GetDiagnosticsAsync(state.Context, cancellationToken).ConfigureAwait(false);

src/LanguageServer/Protocol/Handler/PullHandlers/VersionedPullCache.CacheItem.cs

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99

1010
namespace Microsoft.CodeAnalysis.LanguageServer.Handler;
1111

12-
internal abstract partial class VersionedPullCache<TCheapVersion, TExpensiveVersion, TState, TComputedData>
12+
internal abstract partial class VersionedPullCache<TVersion, TState, TComputedData>
1313
{
1414
/// <summary>
15-
/// Internal cache item that updates state for a particular <see cref="Workspace"/> and <see cref="ProjectOrDocumentId"/> in <see cref="VersionedPullCache{TCheapVersion, TExpensiveVersion, TState, TComputedData}"/>
15+
/// Internal cache item that updates state for a particular <see cref="Workspace"/> and <see cref="ProjectOrDocumentId"/> in <see cref="VersionedPullCache{TVersion, TState, TComputedData}"/>
1616
/// This type ensures that the state for a particular key is never updated concurrently for the same key (but different key states can be concurrent).
1717
/// </summary>
1818
private sealed class CacheItem(string uniqueKey)
@@ -44,15 +44,15 @@ private sealed class CacheItem(string uniqueKey)
4444
/// </list>
4545
///
4646
/// </summary>
47-
private (string resultId, TCheapVersion cheapVersion, TExpensiveVersion expensiveVersion, Checksum dataChecksum)? _lastResult;
47+
private (string resultId, TVersion version, Checksum dataChecksum)? _lastResult;
4848

4949
/// <summary>
5050
/// Updates the values for this cache entry. Guarded by <see cref="_gate"/>
5151
///
5252
/// Returns <see langword="null"/> if the previousPullResult can be re-used, otherwise returns a new resultId and the new data associated with it.
5353
/// </summary>
5454
public async Task<(string, TComputedData)?> UpdateCacheItemAsync(
55-
VersionedPullCache<TCheapVersion, TExpensiveVersion, TState, TComputedData> cache,
55+
VersionedPullCache<TVersion, TState, TComputedData> cache,
5656
PreviousPullResult? previousPullResult,
5757
bool isFullyLoaded,
5858
TState state,
@@ -63,38 +63,27 @@ private sealed class CacheItem(string uniqueKey)
6363
// This means that the computation of new data for this item only occurs sequentially.
6464
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
6565
{
66-
TCheapVersion cheapVersion;
67-
TExpensiveVersion expensiveVersion;
66+
TVersion version;
6867

6968
// Check if the version we have in the cache matches the request version. If so we can re-use the resultId.
7069
if (isFullyLoaded &&
7170
_lastResult is not null &&
7271
_lastResult.Value.resultId == previousPullResult?.PreviousResultId)
7372
{
74-
cheapVersion = await cache.ComputeCheapVersionAsync(state, cancellationToken).ConfigureAwait(false);
75-
if (cheapVersion != null && cheapVersion.Equals(_lastResult.Value.cheapVersion))
76-
{
77-
// The client's resultId matches our cached resultId and the cheap version is an
78-
// exact match for our current cheap version. We return early here to avoid calculating
79-
// expensive versions as we know nothing is changed.
80-
return null;
81-
}
82-
8373
// The current cheap version does not match the last reported. This may be because we've forked
8474
// or reloaded a project, so fall back to calculating the full expensive version to determine if
8575
// anything is actually changed.
86-
expensiveVersion = await cache.ComputeExpensiveVersionAsync(state, cancellationToken).ConfigureAwait(false);
87-
if (expensiveVersion != null && expensiveVersion.Equals(_lastResult.Value.expensiveVersion))
76+
version = await cache.ComputeVersionAsync(state, cancellationToken).ConfigureAwait(false);
77+
if (version != null && version.Equals(_lastResult.Value.version))
8878
{
8979
return null;
9080
}
9181
}
9282
else
9383
{
94-
// The versions we have in our cache (if any) do not match the ones provided by the client (if any).
84+
// The version we have in our cache does not match the one provided by the client (if any).
9585
// We need to calculate new results.
96-
cheapVersion = await cache.ComputeCheapVersionAsync(state, cancellationToken).ConfigureAwait(false);
97-
expensiveVersion = await cache.ComputeExpensiveVersionAsync(state, cancellationToken).ConfigureAwait(false);
86+
version = await cache.ComputeVersionAsync(state, cancellationToken).ConfigureAwait(false);
9887
}
9988

10089
// Compute the new result for the request.
@@ -111,7 +100,7 @@ _lastResult is not null &&
111100
// subsequent requests will always fail the version comparison check (the resultId is still associated with the older version even
112101
// though we reused it here for a newer version) and will trigger re-computation.
113102
// By storing the updated version with the resultId we can short circuit earlier in the version checks.
114-
_lastResult = (_lastResult.Value.resultId, cheapVersion, expensiveVersion, dataChecksum);
103+
_lastResult = (_lastResult.Value.resultId, version, dataChecksum);
115104
return null;
116105
}
117106
else
@@ -127,7 +116,7 @@ _lastResult is not null &&
127116
// Note that we can safely update the map before computation as any cancellation or exception
128117
// during computation means that the client will never recieve this resultId and so cannot ask us for it.
129118
newResultId = $"{uniqueKey}:{cache.GetNextResultId()}";
130-
_lastResult = (newResultId, cheapVersion, expensiveVersion, dataChecksum);
119+
_lastResult = (newResultId, version, dataChecksum);
131120
return (newResultId, data);
132121
}
133122
}

src/LanguageServer/Protocol/Handler/PullHandlers/VersionedPullCache.cs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler;
1717
/// that existing results can be reused, or if new results need to be computed. Multiple keys can be used,
1818
/// with different computation costs to determine if the previous cached data is still valid.
1919
/// </summary>
20-
internal abstract partial class VersionedPullCache<TCheapVersion, TExpensiveVersion, TState, TComputedData>(string uniqueKey)
20+
internal abstract partial class VersionedPullCache<TVersion, TState, TComputedData>(string uniqueKey)
2121
{
2222
/// <summary>
2323
/// Map of workspace and diagnostic source to the data used to make the last pull report.
@@ -37,19 +37,12 @@ internal abstract partial class VersionedPullCache<TCheapVersion, TExpensiveVers
3737
private long _nextDocumentResultId;
3838

3939
/// <summary>
40-
/// Computes a cheap version of the current state. This is compared to the cached version we calculated for the client's previous resultId.
40+
/// Computes the version of the current state. We compare the version of the current state against the
41+
/// version we have cached for the client's previous resultId.
4142
///
4243
/// Note - this will run under the semaphore in <see cref="CacheItem._gate"/>.
4344
/// </summary>
44-
public abstract Task<TCheapVersion> ComputeCheapVersionAsync(TState state, CancellationToken cancellationToken);
45-
46-
/// <summary>
47-
/// Computes a more expensive version of the current state. If the cheap versions are mismatched, we then compare the expensive version of the current state against the
48-
/// expensive version we have cached for the client's previous resultId.
49-
///
50-
/// Note - this will run under the semaphore in <see cref="CacheItem._gate"/>.
51-
/// </summary>
52-
public abstract Task<TExpensiveVersion> ComputeExpensiveVersionAsync(TState state, CancellationToken cancellationToken);
45+
public abstract Task<TVersion> ComputeVersionAsync(TState state, CancellationToken cancellationToken);
5346

5447
/// <summary>
5548
/// Computes new data for this request. This data must be hashable as it we store the hash with the requestId to determine if

src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratedDocumentCache.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SourceGenerators;
1414

1515
internal record struct SourceGeneratedDocumentGetTextState(Document Document);
1616

17-
internal sealed class SourceGeneratedDocumentCache(string uniqueKey) : VersionedPullCache<(SourceGeneratorExecutionVersion, VersionStamp), object?, SourceGeneratedDocumentGetTextState, SourceText?>(uniqueKey), ILspService
17+
internal sealed class SourceGeneratedDocumentCache(string uniqueKey) : VersionedPullCache<(SourceGeneratorExecutionVersion, VersionStamp), SourceGeneratedDocumentGetTextState, SourceText?>(uniqueKey), ILspService
1818
{
19-
public override async Task<(SourceGeneratorExecutionVersion, VersionStamp)> ComputeCheapVersionAsync(SourceGeneratedDocumentGetTextState state, CancellationToken cancellationToken)
19+
public override async Task<(SourceGeneratorExecutionVersion, VersionStamp)> ComputeVersionAsync(SourceGeneratedDocumentGetTextState state, CancellationToken cancellationToken)
2020
{
2121
// The execution version and the dependent version must be considered as one version cached together -
2222
// it is not correct to say that if the execution version is the same then we can re-use results (as in automatic mode the execution version never changes).
@@ -25,11 +25,6 @@ internal sealed class SourceGeneratedDocumentCache(string uniqueKey) : Versioned
2525
return (executionVersion, dependentVersion);
2626
}
2727

28-
public override Task<object?> ComputeExpensiveVersionAsync(SourceGeneratedDocumentGetTextState state, CancellationToken cancellationToken)
29-
{
30-
return SpecializedTasks.Null<object>();
31-
}
32-
3328
public override Checksum ComputeChecksum(SourceText? data, string language)
3429
{
3530
return data is null ? Checksum.Null : Checksum.From(data.GetChecksum());

src/LanguageServer/Protocol/Handler/SpellCheck/SpellCheckPullCache.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SpellCheck;
1313
internal record struct SpellCheckState(ISpellCheckSpanService Service, Document Document);
1414

1515
/// <summary>
16-
/// Simplified version of <see cref="VersionedPullCache{TCheapVersion, TExpensiveVersion, TState, TComputedData}"/> that only uses a
16+
/// Simplified version of <see cref="VersionedPullCache{TVersion, TState, TComputedData}"/> that only uses a
1717
/// single cheap key to check results against.
1818
/// </summary>
19-
internal sealed class SpellCheckPullCache(string uniqueKey) : VersionedPullCache<(Checksum parseOptionsChecksum, Checksum textChecksum)?, object?, SpellCheckState, ImmutableArray<SpellCheckSpan>>(uniqueKey)
19+
internal sealed class SpellCheckPullCache(string uniqueKey) : VersionedPullCache<(Checksum parseOptionsChecksum, Checksum textChecksum), SpellCheckState, ImmutableArray<SpellCheckSpan>>(uniqueKey)
2020
{
21-
public override async Task<(Checksum parseOptionsChecksum, Checksum textChecksum)?> ComputeCheapVersionAsync(SpellCheckState state, CancellationToken cancellationToken)
21+
public override async Task<(Checksum parseOptionsChecksum, Checksum textChecksum)> ComputeVersionAsync(SpellCheckState state, CancellationToken cancellationToken)
2222
{
2323
var project = state.Document.Project;
2424
var parseOptionsChecksum = project.State.GetParseOptionsChecksum();
@@ -41,12 +41,6 @@ public override async Task<ImmutableArray<SpellCheckSpan>> ComputeDataAsync(Spel
4141
return spans;
4242
}
4343

44-
public override Task<object?> ComputeExpensiveVersionAsync(SpellCheckState state, CancellationToken cancellationToken)
45-
{
46-
// Spell check does not need an expensive version check - we return null to effectively skip this check.
47-
return SpecializedTasks.Null<object>();
48-
}
49-
5044
private void SerializeSpellCheckSpan(SpellCheckSpan span, ObjectWriter writer)
5145
{
5246
writer.WriteInt32(span.TextSpan.Start);

src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,7 @@ public partial class C
680680

681681
// First diagnostic request should report a diagnostic since the generator does not produce any source (text does not match).
682682
var results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics);
683+
var firstResultId = results.Single().ResultId;
683684
var diagnostic = AssertEx.Single(results.Single().Diagnostics);
684685
Assert.Equal("CS0103", diagnostic.Code);
685686

@@ -703,7 +704,8 @@ public partial class C
703704
}
704705

705706
await testLspServer.WaitForSourceGeneratorsAsync();
706-
results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics);
707+
results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics, previousResultId: firstResultId);
708+
var secondResultId = results.Single().ResultId;
707709

708710
if (executionPreference == SourceGeneratorExecutionPreference.Automatic)
709711
{
@@ -712,15 +714,18 @@ public partial class C
712714
}
713715
else
714716
{
715-
// In balanced mode, the diagnostic should remain until there is a manual source generator run that updates the sg text.
716-
diagnostic = AssertEx.Single(results.Single().Diagnostics);
717-
Assert.Equal("CS0103", diagnostic.Code);
717+
// In balanced mode, the diagnostic should be unchanged until there is a manual source generator run that updates the sg text.
718+
Assert.Null(results.Single().Diagnostics);
719+
Assert.Equal(firstResultId, secondResultId);
718720

719721
testLspServer.TestWorkspace.EnqueueUpdateSourceGeneratorVersion(document.Project.Id, forceRegeneration: false);
720722
await testLspServer.WaitForSourceGeneratorsAsync();
721723

722-
results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics);
724+
results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics, previousResultId: secondResultId);
725+
var thirdResultId = results.Single().ResultId;
726+
AssertEx.NotNull(results.Single().Diagnostics);
723727
Assert.Empty(results.Single().Diagnostics!);
728+
Assert.NotEqual(firstResultId, thirdResultId);
724729
}
725730
}
726731

0 commit comments

Comments
 (0)