-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unify code paths that update ordinary/additional/analyzerconfig documents #74441
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -129,6 +129,14 @@ public ProjectState(LanguageServices languageServices, ProjectInfo projectInfo, | |
_lazyContentHashToDocumentId = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeContentHashToDocumentIdAsync(cancellationToken), arg: this); | ||
} | ||
|
||
public TextDocumentStates<TDocumentState> GetDocumentStates<TDocumentState>() | ||
where TDocumentState : TextDocumentState | ||
=> (TextDocumentStates<TDocumentState>)(object)( | ||
typeof(TDocumentState) == typeof(DocumentState) ? DocumentStates : | ||
typeof(TDocumentState) == typeof(AdditionalDocumentState) ? AdditionalDocumentStates : | ||
typeof(TDocumentState) == typeof(AnalyzerConfigDocumentState) ? AnalyzerConfigDocumentStates : | ||
throw ExceptionUtilities.UnexpectedValue(typeof(TDocumentState))); | ||
|
||
private async Task<Dictionary<ImmutableArray<byte>, DocumentId>> ComputeContentHashToDocumentIdAsync(CancellationToken cancellationToken) | ||
{ | ||
var result = new Dictionary<ImmutableArray<byte>, DocumentId>(ImmutableArrayComparer<byte>.Instance); | ||
|
@@ -276,6 +284,14 @@ internal DocumentState CreateDocument(DocumentInfo documentInfo, ParseOptions? p | |
return doc; | ||
} | ||
|
||
internal TDocumentState CreateDocument<TDocumentState>(DocumentInfo documentInfo) | ||
where TDocumentState : TextDocumentState | ||
=> (TDocumentState)(object)( | ||
typeof(TDocumentState) == typeof(DocumentState) ? CreateDocument(documentInfo, ParseOptions, new LoadTextOptions(ChecksumAlgorithm)) : | ||
typeof(TDocumentState) == typeof(AdditionalDocumentState) ? new AdditionalDocumentState(LanguageServices.SolutionServices, documentInfo, new LoadTextOptions(ChecksumAlgorithm)) : | ||
typeof(TDocumentState) == typeof(AnalyzerConfigDocumentState) ? new AnalyzerConfigDocumentState(LanguageServices.SolutionServices, documentInfo, new LoadTextOptions(ChecksumAlgorithm)) : | ||
throw ExceptionUtilities.UnexpectedValue(typeof(TDocumentState))); | ||
|
||
public AnalyzerOptions AnalyzerOptions | ||
=> _lazyAnalyzerOptions ??= new AnalyzerOptions( | ||
additionalFiles: AdditionalDocumentStates.SelectAsArray(static documentState => documentState.AdditionalText), | ||
|
@@ -828,26 +844,25 @@ public ProjectState RemoveAllNormalDocuments() | |
} | ||
|
||
public ProjectState UpdateDocument(DocumentState newDocument, bool contentChanged) | ||
=> UpdateDocuments([newDocument], contentChanged); | ||
=> UpdateDocuments([DocumentStates.GetRequiredState(newDocument.Id)], [newDocument], contentChanged); | ||
|
||
public ProjectState UpdateDocuments(ImmutableArray<DocumentState> newDocuments, bool contentChanged) | ||
public ProjectState UpdateDocuments(ImmutableArray<DocumentState> oldDocuments, ImmutableArray<DocumentState> newDocuments, bool contentChanged) | ||
{ | ||
var oldDocuments = newDocuments.SelectAsArray(d => DocumentStates.GetRequiredState(d.Id)); | ||
if (oldDocuments.SequenceEqual(newDocuments)) | ||
return this; | ||
|
||
// Must not be empty as we would have otherwise bailed out in the check above. | ||
Contract.ThrowIfTrue(newDocuments.IsEmpty); | ||
Contract.ThrowIfTrue(oldDocuments.IsEmpty); | ||
Contract.ThrowIfFalse(oldDocuments.Length == newDocuments.Length); | ||
Debug.Assert(!oldDocuments.SequenceEqual(newDocuments)); | ||
|
||
var newDocumentStates = DocumentStates.SetStates(newDocuments); | ||
|
||
// When computing the latest dependent version, we just need to know how | ||
GetLatestDependentVersions( | ||
newDocumentStates, AdditionalDocumentStates, | ||
newDocumentStates, | ||
AdditionalDocumentStates, | ||
oldDocuments.CastArray<TextDocumentState>(), | ||
newDocuments.CastArray<TextDocumentState>(), | ||
contentChanged, | ||
out var dependentDocumentVersion, out var dependentSemanticVersion); | ||
out var dependentDocumentVersion, | ||
out var dependentSemanticVersion); | ||
|
||
return With( | ||
documentStates: newDocumentStates, | ||
|
@@ -856,34 +871,38 @@ public ProjectState UpdateDocuments(ImmutableArray<DocumentState> newDocuments, | |
} | ||
|
||
public ProjectState UpdateAdditionalDocument(AdditionalDocumentState newDocument, bool contentChanged) | ||
=> UpdateAdditionalDocuments([AdditionalDocumentStates.GetRequiredState(newDocument.Id)], [newDocument], contentChanged); | ||
|
||
public ProjectState UpdateAdditionalDocuments(ImmutableArray<AdditionalDocumentState> oldDocuments, ImmutableArray<AdditionalDocumentState> newDocuments, bool contentChanged) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Follow the same pattern as |
||
{ | ||
var oldDocument = AdditionalDocumentStates.GetRequiredState(newDocument.Id); | ||
if (oldDocument == newDocument) | ||
{ | ||
return this; | ||
} | ||
Contract.ThrowIfTrue(oldDocuments.IsEmpty); | ||
Contract.ThrowIfFalse(oldDocuments.Length == newDocuments.Length); | ||
Debug.Assert(!oldDocuments.SequenceEqual(newDocuments)); | ||
|
||
var newDocumentStates = AdditionalDocumentStates.SetStates(newDocuments); | ||
|
||
var newDocumentStates = AdditionalDocumentStates.SetState(newDocument); | ||
GetLatestDependentVersions( | ||
DocumentStates, newDocumentStates, [oldDocument], [newDocument], contentChanged, | ||
out var dependentDocumentVersion, out var dependentSemanticVersion); | ||
DocumentStates, | ||
newDocumentStates, | ||
oldDocuments.CastArray<TextDocumentState>(), | ||
newDocuments.CastArray<TextDocumentState>(), | ||
contentChanged, | ||
out var dependentDocumentVersion, | ||
out var dependentSemanticVersion); | ||
|
||
return this.With( | ||
return With( | ||
additionalDocumentStates: newDocumentStates, | ||
latestDocumentVersion: dependentDocumentVersion, | ||
latestDocumentTopLevelChangeVersion: dependentSemanticVersion); | ||
} | ||
|
||
public ProjectState UpdateAnalyzerConfigDocument(AnalyzerConfigDocumentState newDocument) | ||
{ | ||
var oldDocument = AnalyzerConfigDocumentStates.GetRequiredState(newDocument.Id); | ||
if (oldDocument == newDocument) | ||
{ | ||
return this; | ||
} | ||
|
||
var newDocumentStates = AnalyzerConfigDocumentStates.SetState(newDocument); | ||
=> UpdateAnalyzerConfigDocuments([AnalyzerConfigDocumentStates.GetRequiredState(newDocument.Id)], [newDocument]); | ||
|
||
public ProjectState UpdateAnalyzerConfigDocuments(ImmutableArray<AnalyzerConfigDocumentState> oldDocuments, ImmutableArray<AnalyzerConfigDocumentState> newDocuments) | ||
{ | ||
Debug.Assert(!oldDocuments.SequenceEqual(newDocuments)); | ||
tmat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var newDocumentStates = AnalyzerConfigDocumentStates.SetStates(newDocuments); | ||
return CreateNewStateForChangedAnalyzerConfig(newDocumentStates, _analyzerConfigOptionsCache.FallbackOptions); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,15 +20,16 @@ private abstract partial class TranslationAction | |
internal sealed class TouchDocumentsAction( | ||
ProjectState oldProjectState, | ||
ProjectState newProjectState, | ||
ImmutableArray<DocumentState> oldStates, | ||
ImmutableArray<DocumentState> newStates) : TranslationAction(oldProjectState, newProjectState) | ||
{ | ||
private readonly ImmutableArray<DocumentState> _oldStates = newStates.SelectAsArray(s => oldProjectState.DocumentStates.GetRequiredState(s.Id)); | ||
private readonly ImmutableArray<DocumentState> _oldStates = oldStates; | ||
private readonly ImmutableArray<DocumentState> _newStates = newStates; | ||
|
||
public override async Task<Compilation> TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken) | ||
{ | ||
var finalCompilation = oldCompilation; | ||
for (int i = 0, n = _newStates.Length; i < n; i++) | ||
for (var i = 0; i < _newStates.Length; i++) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
var newState = _newStates[i]; | ||
|
@@ -57,22 +58,22 @@ public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generat | |
{ | ||
// As we're merging ourselves with the prior touch action, we want to keep the old project state | ||
// that we are translating from. | ||
return new TouchDocumentsAction(priorAction.OldProjectState, this.NewProjectState, _newStates); | ||
return new TouchDocumentsAction(priorAction.OldProjectState, NewProjectState, priorTouchAction._oldStates, _newStates); | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
|
||
internal sealed class TouchAdditionalDocumentAction( | ||
internal sealed class TouchAdditionalDocumentsAction( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allow multiple documents to be changed at the same time, like TouchDocumentsAction. |
||
ProjectState oldProjectState, | ||
ProjectState newProjectState, | ||
AdditionalDocumentState oldState, | ||
AdditionalDocumentState newState) | ||
ImmutableArray<AdditionalDocumentState> oldStates, | ||
ImmutableArray<AdditionalDocumentState> newStates) | ||
: TranslationAction(oldProjectState, newProjectState) | ||
{ | ||
private readonly AdditionalDocumentState _oldState = oldState; | ||
private readonly AdditionalDocumentState _newState = newState; | ||
private readonly ImmutableArray<AdditionalDocumentState> _oldStates = oldStates; | ||
private readonly ImmutableArray<AdditionalDocumentState> _newStates = newStates; | ||
|
||
// Changing an additional document doesn't change the compilation directly, so we can "apply" the | ||
// translation (which is a no-op). Since we use a 'false' here to mean that it's not worth keeping the | ||
|
@@ -84,24 +85,48 @@ public override Task<Compilation> TransformCompilationAsync(Compilation oldCompi | |
|
||
public override TranslationAction? TryMergeWithPrior(TranslationAction priorAction) | ||
{ | ||
if (priorAction is TouchAdditionalDocumentAction priorTouchAction && | ||
priorTouchAction._newState == _oldState) | ||
if (priorAction is TouchAdditionalDocumentsAction priorTouchAction && | ||
priorTouchAction._newStates.SequenceEqual(_oldStates)) | ||
{ | ||
// As we're merging ourselves with the prior touch action, we want to keep the old project state | ||
// that we are translating from. | ||
return new TouchAdditionalDocumentAction(priorAction.OldProjectState, this.NewProjectState, priorTouchAction._oldState, _newState); | ||
return new TouchAdditionalDocumentsAction(priorAction.OldProjectState, NewProjectState, priorTouchAction._oldStates, _newStates); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generatorDriver) | ||
{ | ||
var oldText = _oldState.AdditionalText; | ||
var newText = _newState.AdditionalText; | ||
for (var i = 0; i < _newStates.Length; i++) | ||
{ | ||
generatorDriver = generatorDriver.ReplaceAdditionalText(_oldStates[i].AdditionalText, _newStates[i].AdditionalText); | ||
} | ||
|
||
return generatorDriver; | ||
} | ||
} | ||
|
||
return generatorDriver.ReplaceAdditionalText(oldText, newText); | ||
internal sealed class TouchAnalyzerConfigDocumentsAction( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Separate this action from |
||
ProjectState oldProjectState, | ||
ProjectState newProjectState) | ||
: TranslationAction(oldProjectState, newProjectState) | ||
{ | ||
/// <summary> | ||
/// Updating editorconfig document updates <see cref="CompilationOptions.SyntaxTreeOptionsProvider"/>. | ||
/// </summary> | ||
public override Task<Compilation> TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken) | ||
{ | ||
RoslynDebug.AssertNotNull(this.NewProjectState.CompilationOptions); | ||
return Task.FromResult(oldCompilation.WithOptions(this.NewProjectState.CompilationOptions)); | ||
} | ||
|
||
// Updating the analyzer config optons doesn't require us to reparse trees, so we can use this to update | ||
// compilations with stale generated trees. | ||
public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput => true; | ||
|
||
public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generatorDriver) | ||
=> generatorDriver.WithUpdatedAnalyzerConfigOptions(NewProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider); | ||
} | ||
|
||
internal sealed class RemoveDocumentsAction( | ||
|
@@ -210,8 +235,7 @@ public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generat | |
|
||
internal sealed class ProjectCompilationOptionsAction( | ||
ProjectState oldProjectState, | ||
ProjectState newProjectState, | ||
bool isAnalyzerConfigChange) | ||
ProjectState newProjectState) | ||
: TranslationAction(oldProjectState, newProjectState) | ||
{ | ||
public override Task<Compilation> TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken) | ||
|
@@ -226,16 +250,9 @@ public override Task<Compilation> TransformCompilationAsync(Compilation oldCompi | |
|
||
public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generatorDriver) | ||
{ | ||
if (isAnalyzerConfigChange) | ||
{ | ||
return generatorDriver.WithUpdatedAnalyzerConfigOptions(this.NewProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider); | ||
} | ||
else | ||
{ | ||
// Changing any other option is fine and the driver can be reused. The driver | ||
// will get the new compilation once we pass it to it. | ||
return generatorDriver; | ||
} | ||
// Changing any other option is fine and the driver can be reused. The driver | ||
// will get the new compilation once we pass it to it. | ||
return generatorDriver; | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wanted to make sure this is a new requirement of the callers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I follow. The contract has been there before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it would have returned before hitting the contract failure previously, right?