-
Notifications
You must be signed in to change notification settings - Fork 484
Cache the ApiData values we produce for an AdditionalText file. #6556
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
Merged
Merged
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
42d760e
In progress
CyrusNajmabadi 1bb8e66
in progrss
CyrusNajmabadi 4412ee7
Finish
CyrusNajmabadi 830e5de
Pooling
CyrusNajmabadi 92d39d2
Simplify
CyrusNajmabadi 6cae209
Share data
CyrusNajmabadi 7a29344
Make into struct
CyrusNajmabadi 254e7f1
Records
CyrusNajmabadi 55f177c
inline
CyrusNajmabadi 7e20263
REstore
CyrusNajmabadi ff71b5f
REstore
CyrusNajmabadi 87dfd85
REstore
CyrusNajmabadi 914c18b
REstore
CyrusNajmabadi 7704c79
Simplify
CyrusNajmabadi c3b9928
Simplify
CyrusNajmabadi 7882c3d
Simplufy
CyrusNajmabadi a86ecec
doc
CyrusNajmabadi 0aa693f
Switch approach
CyrusNajmabadi b9eaa3f
Make non static
CyrusNajmabadi ba8f956
Update src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.cs
CyrusNajmabadi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
using System.Threading; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using Microsoft.CodeAnalysis.Text; | ||
|
@@ -16,6 +17,12 @@ namespace Microsoft.CodeAnalysis.PublicApiAnalyzers | |
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] | ||
public sealed partial class DeclarePublicApiAnalyzer : DiagnosticAnalyzer | ||
{ | ||
/// <summary> | ||
/// Cache from additional text instance to the api data we have read out for that specific file. We only store | ||
/// data for additional texts that explicitly match the public/internal api file names we expect. | ||
/// </summary> | ||
private static readonly ConditionalWeakTable<AdditionalText, ApiData> s_additionalTextToApiData = new(); | ||
|
||
internal const string Extension = ".txt"; | ||
internal const string PublicShippedFileNamePrefix = "PublicAPI.Shipped"; | ||
internal const string PublicShippedFileName = PublicShippedFileNamePrefix + Extension; | ||
|
@@ -108,7 +115,14 @@ void CheckAndRegisterImplementation(bool isPublic) | |
{ | ||
var errors = new List<Diagnostic>(); | ||
// Switch to "RegisterAdditionalFileAction" available in Microsoft.CodeAnalysis "3.8.x" to report additional file diagnostics: https://github.com/dotnet/roslyn-analyzers/issues/3918 | ||
if (!TryGetAndValidateApiFiles(compilationContext.Options, compilationContext.Compilation, isPublic, compilationContext.CancellationToken, errors, out ApiData shippedData, out ApiData unshippedData)) | ||
if (!TryGetAndValidateApiFiles( | ||
compilationContext.Options, | ||
compilationContext.Compilation, | ||
isPublic, | ||
compilationContext.CancellationToken, | ||
errors, | ||
out var shippedData, | ||
out var unshippedData)) | ||
{ | ||
compilationContext.RegisterCompilationEndAction(context => | ||
{ | ||
|
@@ -124,8 +138,16 @@ void CheckAndRegisterImplementation(bool isPublic) | |
Debug.Assert(errors.Count == 0); | ||
|
||
RegisterImplActions(compilationContext, new Impl(compilationContext.Compilation, shippedData, unshippedData, isPublic, compilationContext.Options)); | ||
|
||
static bool TryGetAndValidateApiFiles(AnalyzerOptions options, Compilation compilation, bool isPublic, CancellationToken cancellationToken, List<Diagnostic> errors, out ApiData shippedData, out ApiData unshippedData) | ||
return; | ||
|
||
static bool TryGetAndValidateApiFiles( | ||
AnalyzerOptions options, | ||
Compilation compilation, | ||
bool isPublic, | ||
CancellationToken cancellationToken, | ||
List<Diagnostic> errors, | ||
[NotNullWhen(true)] out ApiData? shippedData, | ||
[NotNullWhen(true)] out ApiData? unshippedData) | ||
{ | ||
return TryGetApiData(options, compilation, isPublic, errors, cancellationToken, out shippedData, out unshippedData) | ||
&& ValidateApiFiles(shippedData, unshippedData, isPublic, errors); | ||
|
@@ -147,83 +169,110 @@ static void RegisterImplActions(CompilationStartAnalysisContext compilationConte | |
} | ||
} | ||
|
||
private static ApiData ReadApiData(List<(string path, SourceText sourceText)> data, bool isShippedApi) | ||
private static ApiData ReadApiData(string path, SourceText sourceText, bool isShippedApi) | ||
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. this now computes teh ApiData for a single file. handling of multiple files is taken care of by the caller. |
||
{ | ||
var apiBuilder = ImmutableArray.CreateBuilder<ApiLine>(); | ||
var removedBuilder = ImmutableArray.CreateBuilder<RemovedApiLine>(); | ||
var maxNullableRank = -1; | ||
var rank = -1; | ||
|
||
foreach (var (path, sourceText) in data) | ||
foreach (var line in sourceText.Lines) | ||
{ | ||
int rank = -1; | ||
string text = line.ToString(); | ||
if (string.IsNullOrWhiteSpace(text)) | ||
continue; | ||
|
||
foreach (TextLine line in sourceText.Lines) | ||
rank++; | ||
|
||
if (text == NullableEnable) | ||
{ | ||
string text = line.ToString(); | ||
if (string.IsNullOrWhiteSpace(text)) | ||
{ | ||
continue; | ||
} | ||
maxNullableRank = rank; | ||
continue; | ||
} | ||
|
||
rank++; | ||
var apiLine = new ApiLine(text, line.Span, sourceText, path, isShippedApi); | ||
if (text.StartsWith(RemovedApiPrefix, StringComparison.Ordinal)) | ||
{ | ||
string removedText = text[RemovedApiPrefix.Length..]; | ||
removedBuilder.Add(new RemovedApiLine(removedText, apiLine)); | ||
} | ||
else | ||
{ | ||
apiBuilder.Add(apiLine); | ||
} | ||
} | ||
|
||
if (text == NullableEnable) | ||
{ | ||
maxNullableRank = Math.Max(rank, maxNullableRank); | ||
continue; | ||
} | ||
return new ApiData(apiBuilder.ToImmutable(), removedBuilder.ToImmutable(), maxNullableRank); | ||
} | ||
|
||
var apiLine = new ApiLine(text, line.Span, sourceText, path, isShippedApi); | ||
if (text.StartsWith(RemovedApiPrefix, StringComparison.Ordinal)) | ||
{ | ||
string removedtext = text[RemovedApiPrefix.Length..]; | ||
removedBuilder.Add(new RemovedApiLine(removedtext, apiLine)); | ||
} | ||
else | ||
{ | ||
apiBuilder.Add(apiLine); | ||
} | ||
} | ||
private static ApiData Flatten(List<ApiData> allData) | ||
{ | ||
Debug.Assert(allData.Count > 0); | ||
if (allData.Count == 1) | ||
return allData[0]; | ||
|
||
var apiBuilder = ImmutableArray.CreateBuilder<ApiLine>(); | ||
var removedBuilder = ImmutableArray.CreateBuilder<RemovedApiLine>(); | ||
var maxNullableRank = -1; | ||
|
||
foreach (var data in allData) | ||
{ | ||
apiBuilder.AddRange(data.ApiList); | ||
removedBuilder.AddRange(data.RemovedApiList); | ||
maxNullableRank = Math.Max(maxNullableRank, data.NullableRank); | ||
} | ||
|
||
return new ApiData(apiBuilder.ToImmutable(), removedBuilder.ToImmutable(), maxNullableRank); | ||
} | ||
|
||
private static bool TryGetApiData(AnalyzerOptions analyzerOptions, Compilation compilation, bool isPublic, List<Diagnostic> errors, CancellationToken cancellationToken, out ApiData shippedData, out ApiData unshippedData) | ||
private static bool TryGetApiData( | ||
AnalyzerOptions analyzerOptions, | ||
Compilation compilation, | ||
bool isPublic, | ||
List<Diagnostic> errors, | ||
CancellationToken cancellationToken, | ||
[NotNullWhen(true)] out ApiData? shippedData, | ||
[NotNullWhen(true)] out ApiData? unshippedData) | ||
{ | ||
if (!TryGetApiText(analyzerOptions.AdditionalFiles, isPublic, cancellationToken, out var shippedText, out var unshippedText)) | ||
var allShippedData = new List<ApiData>(); | ||
var allUnshippedData = new List<ApiData>(); | ||
AddApiTexts( | ||
analyzerOptions.AdditionalFiles, isPublic, allShippedData, allUnshippedData, cancellationToken); | ||
|
||
if (allShippedData.Count == 0 && allUnshippedData.Count == 0) | ||
{ | ||
if (shippedText == null && unshippedText == null) | ||
if (TryGetEditorConfigOptionForMissingFiles(analyzerOptions, compilation, out var silentlyBailOutOnMissingApiFiles) && | ||
silentlyBailOutOnMissingApiFiles) | ||
{ | ||
if (TryGetEditorConfigOptionForMissingFiles(analyzerOptions, compilation, out var silentlyBailOutOnMissingApiFiles) && | ||
silentlyBailOutOnMissingApiFiles) | ||
{ | ||
shippedData = default; | ||
unshippedData = default; | ||
return false; | ||
} | ||
|
||
// Bootstrapping public API files. | ||
(shippedData, unshippedData) = (ApiData.Empty, ApiData.Empty); | ||
return true; | ||
shippedData = null; | ||
unshippedData = null; | ||
return false; | ||
} | ||
|
||
var missingFileName = (shippedText, isPublic) switch | ||
{ | ||
(shippedText: null, isPublic: true) => PublicShippedFileName, | ||
(shippedText: null, isPublic: false) => InternalShippedFileName, | ||
(shippedText: not null, isPublic: true) => PublicUnshippedFileName, | ||
(shippedText: not null, isPublic: false) => InternalUnshippedFileName | ||
}; | ||
errors.Add(Diagnostic.Create(isPublic ? PublicApiFileMissing : InternalApiFileMissing, Location.None, missingFileName)); | ||
shippedData = default; | ||
unshippedData = default; | ||
return false; | ||
// Bootstrapping public API files. | ||
(shippedData, unshippedData) = (ApiData.Empty, ApiData.Empty); | ||
return true; | ||
} | ||
|
||
shippedData = ReadApiData(shippedText, isShippedApi: true); | ||
unshippedData = ReadApiData(unshippedText, isShippedApi: false); | ||
return true; | ||
if (allShippedData.Count > 0 && allUnshippedData.Count > 0) | ||
{ | ||
shippedData = Flatten(allShippedData); | ||
unshippedData = Flatten(allUnshippedData); | ||
return true; | ||
} | ||
|
||
var missingFileName = (allShippedData.Count == 0, isPublic) switch | ||
{ | ||
(true, isPublic: true) => PublicShippedFileName, | ||
(true, isPublic: false) => InternalShippedFileName, | ||
(false, isPublic: true) => PublicUnshippedFileName, | ||
(false, isPublic: false) => InternalUnshippedFileName | ||
}; | ||
|
||
errors.Add(Diagnostic.Create(isPublic ? PublicApiFileMissing : InternalApiFileMissing, Location.None, missingFileName)); | ||
shippedData = null; | ||
unshippedData = null; | ||
return false; | ||
} | ||
|
||
private static bool TryGetEditorConfigOption(AnalyzerOptions analyzerOptions, SyntaxTree tree, string optionName, out string optionValue) | ||
|
@@ -302,49 +351,32 @@ private static bool TryGetEditorConfigOptionForSkippedNamespaces(AnalyzerOptions | |
return true; | ||
} | ||
|
||
private static bool TryGetApiText( | ||
private static void AddApiTexts( | ||
ImmutableArray<AdditionalText> additionalTexts, | ||
bool isPublic, | ||
CancellationToken cancellationToken, | ||
[NotNullWhen(returnValue: true)] out List<(string path, SourceText text)>? shippedText, | ||
[NotNullWhen(returnValue: true)] out List<(string path, SourceText text)>? unshippedText) | ||
List<ApiData> allShippedData, | ||
List<ApiData> allUnshippedData, | ||
CancellationToken cancellationToken) | ||
{ | ||
shippedText = null; | ||
unshippedText = null; | ||
|
||
foreach (AdditionalText additionalText in additionalTexts) | ||
foreach (var additionalText in additionalTexts) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
|
||
var file = new PublicApiFile(additionalText.Path, isPublic); | ||
|
||
if (file.IsApiFile) | ||
{ | ||
SourceText text = additionalText.GetText(cancellationToken); | ||
// if it's not an api file (quick filename check), we can just immediately ignore. | ||
if (!file.IsApiFile) | ||
continue; | ||
|
||
if (text is null) | ||
{ | ||
continue; | ||
} | ||
|
||
var data = (additionalText.Path, text); | ||
|
||
if (file.IsShipping) | ||
{ | ||
shippedText ??= new(); | ||
|
||
shippedText.Add(data); | ||
} | ||
else | ||
{ | ||
unshippedText ??= new(); | ||
|
||
unshippedText.Add(data); | ||
} | ||
if (!s_additionalTextToApiData.TryGetValue(additionalText, out var apiData)) | ||
{ | ||
apiData = ReadApiData(additionalText.Path, additionalText.GetText(cancellationToken), file.IsShipping); | ||
apiData = s_additionalTextToApiData.GetValue(additionalText, _ => apiData); | ||
} | ||
} | ||
|
||
return shippedText != null && unshippedText != null; | ||
var resultList = file.IsShipping ? allShippedData : allUnshippedData; | ||
resultList.Add(apiData); | ||
} | ||
} | ||
|
||
private static bool ValidateApiFiles(ApiData shippedData, ApiData unshippedData, bool isPublic, List<Diagnostic> errors) | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Please don’t use a CWT here. We already have an API on AnalysisContext to allow caching per-file data across compilations: https://github.com/dotnet/roslyn/blob/3a7a7407ea3c831630ecf2754092c33df3a6e452/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticAnalysisContext.cs#L234
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.
Internally, we also use a CWT, so the API already provides a simple way to achieve caching:
Note
Example test analyzer for this API: https://github.com/dotnet/roslyn/blob/3a7a7407ea3c831630ecf2754092c33df3a6e452/src/Compilers/Test/Core/Diagnostics/CommonDiagnosticAnalyzers.cs#L1605
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.
Sure. I can do this.
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.
So i don't seem to be able to use that helper. it is keyed off of SourceText. But avoiding the source-text is the point here. I need to keep off of AdditionalFile. Thoughts @mavasani ?
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.
I am not sure I understand. SourceText is already strongly held by the AdditionalText: https://github.com/dotnet/roslyn/blob/3a7a7407ea3c831630ecf2754092c33df3a6e452/src/Compilers/Core/Portable/AdditionalTextFile.cs#L49-L53
Isn't the key point here avoiding repeated compute of the data associated with each
AdditionalText
file?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.
Ok. Went deep down the rabit hole of trying to make this keyed off sourcetext. It's painful. Effectively, the data we compute off the source-text holds onto data outside of the source text (specifically, things like the path and other information about the original additionaltext). I went down the path of trying to extract this out (e.g. RawApiData, ApiData, RawApiLine, ApiLine) and have the map point from the SourceText to the raw data. But then there was so much wrapping of this i needed to do to make working with things palatable (and not have additional copies/allocatins happening).
If we make it so that hte context objects allow for storing arbitrary K/V and not just Text/Value or Tree/Value, then this will be trivial to do.
I discussed this with manish, and we decided that would take too much time. So we're going to accept that this is a CWT. However, i'm making it non-static so it has the same lifetime as the analyzer (And whatever owns it).