Skip to content

Commit

Permalink
Merge pull request #5422 from twsouthwick/merge-multiple-public-apis
Browse files Browse the repository at this point in the history
Thanks for all your hard work @twsouthwick!
  • Loading branch information
jmarolf authored Nov 2, 2021
2 parents 0141deb + 6b51a0d commit 3bf2b00
Show file tree
Hide file tree
Showing 9 changed files with 335 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,12 @@ private void OnSymbolActionCore(ISymbol symbol, Action<Diagnostic> reportDiagnos
}
else
{
reportAnnotateApi(symbol, isImplicitlyDeclaredConstructor, publicApiName, foundApiLine.IsShippedApi);
reportAnnotateApi(symbol, isImplicitlyDeclaredConstructor, publicApiName, foundApiLine.IsShippedApi, foundApiLine.Path);
}
}
else if (hasPublicApiEntryWithNullability && symbolUsesOblivious)
{
reportAnnotateApi(symbol, isImplicitlyDeclaredConstructor, publicApiName, foundApiLine.IsShippedApi);
reportAnnotateApi(symbol, isImplicitlyDeclaredConstructor, publicApiName, foundApiLine.IsShippedApi, foundApiLine.Path);
}
}
else
Expand Down Expand Up @@ -395,15 +395,16 @@ void reportDeclareNewApi(ISymbol symbol, bool isImplicitlyDeclaredConstructor, s
reportDiagnosticAtLocations(DeclareNewApiRule, propertyBag, errorMessageName);
}

void reportAnnotateApi(ISymbol symbol, bool isImplicitlyDeclaredConstructor, ApiName publicApiName, bool isShipped)
void reportAnnotateApi(ISymbol symbol, bool isImplicitlyDeclaredConstructor, ApiName publicApiName, bool isShipped, string filename)
{
// Public API missing annotations in public API file - report diagnostic.
string errorMessageName = GetErrorMessageName(symbol, isImplicitlyDeclaredConstructor);
ImmutableDictionary<string, string> propertyBag = ImmutableDictionary<string, string>.Empty
.Add(PublicApiNamePropertyBagKey, publicApiName.Name)
.Add(PublicApiNameWithNullabilityPropertyBagKey, withObliviousIfNeeded(publicApiName.NameWithNullability))
.Add(MinimalNamePropertyBagKey, errorMessageName)
.Add(PublicApiIsShippedPropertyBagKey, isShipped ? "true" : "false");
.Add(PublicApiIsShippedPropertyBagKey, isShipped ? "true" : "false")
.Add(FileName, filename);

reportDiagnosticAtLocations(AnnotateApiRule, propertyBag, errorMessageName);
}
Expand Down
94 changes: 53 additions & 41 deletions src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
Expand All @@ -21,8 +20,11 @@ namespace Microsoft.CodeAnalysis.PublicApiAnalyzers
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed partial class DeclarePublicApiAnalyzer : DiagnosticAnalyzer
{
internal const string ShippedFileNamePrefix = "PublicAPI.Shipped";
internal const string ShippedFileName = "PublicAPI.Shipped.txt";
internal const string UnshippedFileName = "PublicAPI.Unshipped.txt";
internal const string UnshippedFileNamePrefix = "PublicAPI.Unshipped";
internal const string Extension = ".txt";
internal const string UnshippedFileName = UnshippedFileNamePrefix + Extension;
internal const string PublicApiNamePropertyBagKey = "PublicAPIName";
internal const string PublicApiNameWithNullabilityPropertyBagKey = "PublicAPINameWithNullability";
internal const string MinimalNamePropertyBagKey = "MinimalName";
Expand All @@ -33,6 +35,7 @@ public sealed partial class DeclarePublicApiAnalyzer : DiagnosticAnalyzer
internal const string InvalidReasonShippedCantHaveRemoved = "The shipped API file can't have removed members";
internal const string InvalidReasonMisplacedNullableEnable = "The '#nullable enable' marker can only appear as the first line in the shipped API file";
internal const string PublicApiIsShippedPropertyBagKey = "PublicAPIIsShipped";
internal const string FileName = "FileName";

private const char ObliviousMarker = '~';

Expand Down Expand Up @@ -258,38 +261,42 @@ private void OnCompilationStart(CompilationStartAnalysisContext compilationConte
compilationContext.RegisterCompilationEndAction(impl.OnCompilationEnd);
}

private static ApiData ReadApiData(string path, SourceText sourceText, bool isShippedApi)
private static ApiData ReadApiData(List<(string path, SourceText sourceText)> data, bool isShippedApi)
{
var apiBuilder = ImmutableArray.CreateBuilder<ApiLine>();
var removedBuilder = ImmutableArray.CreateBuilder<RemovedApiLine>();
var maxNullableRank = -1;

int rank = -1;
foreach (TextLine line in sourceText.Lines)
foreach (var (path, sourceText) in data)
{
string text = line.ToString();
if (string.IsNullOrWhiteSpace(text))
int rank = -1;

foreach (TextLine line in sourceText.Lines)
{
continue;
}
string text = line.ToString();
if (string.IsNullOrWhiteSpace(text))
{
continue;
}

rank++;
rank++;

if (text == NullableEnable)
{
maxNullableRank = rank;
continue;
}
if (text == NullableEnable)
{
maxNullableRank = Math.Max(rank, maxNullableRank);
continue;
}

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);
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);
}
}
}

Expand Down Expand Up @@ -322,8 +329,8 @@ private static bool TryGetApiData(AnalyzerOptions analyzerOptions, Compilation c
return false;
}

shippedData = ReadApiData(shippedText.Value.path, shippedText.Value.text, isShippedApi: true);
unshippedData = ReadApiData(unshippedText.Value.path, unshippedText.Value.text, isShippedApi: false);
shippedData = ReadApiData(shippedText, isShippedApi: true);
unshippedData = ReadApiData(unshippedText, isShippedApi: false);
return true;
}

Expand Down Expand Up @@ -380,42 +387,47 @@ private static bool TryGetEditorConfigOptionForMissingFiles(AnalyzerOptions anal
private static bool TryGetApiText(
ImmutableArray<AdditionalText> additionalTexts,
CancellationToken cancellationToken,
[NotNullWhen(returnValue: true)] out (string path, SourceText text)? shippedText,
[NotNullWhen(returnValue: true)] out (string path, SourceText text)? unshippedText)
[NotNullWhen(returnValue: true)] out List<(string path, SourceText text)>? shippedText,
[NotNullWhen(returnValue: true)] out List<(string path, SourceText text)>? unshippedText)
{
shippedText = null;
unshippedText = null;

StringComparer comparer = StringComparer.Ordinal;
foreach (AdditionalText additionalText in additionalTexts)
{
cancellationToken.ThrowIfCancellationRequested();

string fileName = Path.GetFileName(additionalText.Path);
var file = new PublicApiFile(additionalText.Path);

bool isShippedFile = comparer.Equals(fileName, ShippedFileName);
bool isUnshippedFile = comparer.Equals(fileName, UnshippedFileName);

if (isShippedFile || isUnshippedFile)
if (file.IsApiFile)
{
SourceText text = additionalText.GetText(cancellationToken);

if (text == null)
if (text is null)
{
continue;
}

var data = (additionalText.Path, text);
if (isShippedFile)

if (file.IsShipping)
{
shippedText = data;
}
if (shippedText is null)
{
shippedText = new();
}

if (isUnshippedFile)
shippedText.Add(data);
}
else
{
unshippedText = data;
if (unshippedText is null)
{
unshippedText = new();
}

unshippedText.Add(data);
}
continue;
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/PublicApiAnalyzers/Core/Analyzers/PublicApiFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.IO;

namespace Microsoft.CodeAnalysis.PublicApiAnalyzers
{
public readonly struct PublicApiFile
{
public PublicApiFile(string path)
{
var fileName = Path.GetFileName(path);

IsShipping = IsFile(fileName, DeclarePublicApiAnalyzer.ShippedFileNamePrefix);
var isUnshippedFile = IsFile(fileName, DeclarePublicApiAnalyzer.UnshippedFileNamePrefix);

IsApiFile = IsShipping || isUnshippedFile;
}

public bool IsShipping { get; }

public bool IsApiFile { get; }

private static bool IsFile(string path, string prefix)
=> path.StartsWith(prefix, StringComparison.Ordinal) && path.EndsWith(DeclarePublicApiAnalyzer.Extension, StringComparison.Ordinal);
}
}
79 changes: 28 additions & 51 deletions src/PublicApiAnalyzers/Core/CodeFixes/AnnotatePublicApiFix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Text;

using DiagnosticIds = Roslyn.Diagnostics.Analyzers.RoslynDiagnosticIds;

#nullable enable
Expand All @@ -38,15 +38,16 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
string minimalSymbolName = diagnostic.Properties[DeclarePublicApiAnalyzer.MinimalNamePropertyBagKey];
string publicSymbolName = diagnostic.Properties[DeclarePublicApiAnalyzer.PublicApiNamePropertyBagKey];
string publicSymbolNameWithNullability = diagnostic.Properties[DeclarePublicApiAnalyzer.PublicApiNameWithNullabilityPropertyBagKey];
bool isShippedDocument = diagnostic.Properties[DeclarePublicApiAnalyzer.PublicApiIsShippedPropertyBagKey] == "true";
string fileName = diagnostic.Properties[DeclarePublicApiAnalyzer.FileName];

TextDocument? document = isShippedDocument ? PublicApiFixHelpers.GetShippedDocument(project) : PublicApiFixHelpers.GetUnshippedDocument(project);
TextDocument? document = project.GetPublicApiDocument(fileName);

if (document != null)
{
context.RegisterCodeFix(
new DeclarePublicApiFix.AdditionalDocumentChangeAction(
$"Annotate {minimalSymbolName} in public API",
document.Id,
c => GetFix(document, publicSymbolName, publicSymbolNameWithNullability, c)),
diagnostic);
}
Expand Down Expand Up @@ -81,8 +82,8 @@ private static SourceText AnnotateSymbolNamesInSourceText(SourceText sourceText,
}
}

var endOfLine = PublicApiFixHelpers.GetEndOfLine(sourceText);
SourceText newSourceText = sourceText.Replace(new TextSpan(0, sourceText.Length), string.Join(endOfLine, lines) + PublicApiFixHelpers.GetEndOfFileText(sourceText, endOfLine));
var endOfLine = sourceText.GetEndOfLine();
SourceText newSourceText = sourceText.Replace(new TextSpan(0, sourceText.Length), string.Join(endOfLine, lines) + sourceText.GetEndOfFileText(endOfLine));
return newSourceText;
}

Expand All @@ -102,53 +103,22 @@ public FixAllAdditionalDocumentChangeAction(string title, Solution solution, Lis

protected override async Task<Solution> GetChangedSolutionAsync(CancellationToken cancellationToken)
{
var updatedPublicSurfaceAreaText = new List<KeyValuePair<DocumentId, SourceText>>();

using var uniqueShippedDocuments = PooledHashSet<string>.GetInstance();
using var uniqueUnshippedDocuments = PooledHashSet<string>.GetInstance();
var updatedPublicSurfaceAreaText = new List<(DocumentId, SourceText)>();

foreach (KeyValuePair<Project, ImmutableArray<Diagnostic>> pair in _diagnosticsToFix)
foreach (var (project, diagnostics) in _diagnosticsToFix)
{
Project project = pair.Key;
ImmutableArray<Diagnostic> diagnostics = pair.Value;

TextDocument? unshippedDocument = PublicApiFixHelpers.GetUnshippedDocument(project);
if (unshippedDocument?.FilePath != null && !uniqueUnshippedDocuments.Add(unshippedDocument.FilePath))
{
// Skip past duplicate unshipped documents.
// Multi-tfm projects can likely share the same api files, and we want to avoid duplicate code fix application.
unshippedDocument = null;
}

TextDocument? shippedDocument = PublicApiFixHelpers.GetShippedDocument(project);
if (shippedDocument?.FilePath != null && !uniqueShippedDocuments.Add(shippedDocument.FilePath))
{
// Skip past duplicate shipped documents.
// Multi-tfm projects can likely share the same api files, and we want to avoid duplicate code fix application.
shippedDocument = null;
}

if (unshippedDocument == null && shippedDocument == null)
{
continue;
}

SourceText? unshippedSourceText = unshippedDocument is null ? null : await unshippedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
SourceText? shippedSourceText = shippedDocument is null ? null : await shippedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);

IEnumerable<IGrouping<SyntaxTree, Diagnostic>> groupedDiagnostics =
diagnostics
.Where(d => d.Location.IsInSource)
.GroupBy(d => d.Location.SourceTree);

var shippedChanges = new Dictionary<string, string>();
var unshippedChanges = new Dictionary<string, string>();
var allChanges = new Dictionary<string, Dictionary<string, string>>();

foreach (IGrouping<SyntaxTree, Diagnostic> grouping in groupedDiagnostics)
{
Document document = project.GetDocument(grouping.Key);

if (document == null)
if (document is null)
{
continue;
}
Expand All @@ -166,29 +136,36 @@ protected override async Task<Solution> GetChangedSolutionAsync(CancellationToke
string oldName = diagnostic.Properties[DeclarePublicApiAnalyzer.PublicApiNamePropertyBagKey];
string newName = diagnostic.Properties[DeclarePublicApiAnalyzer.PublicApiNameWithNullabilityPropertyBagKey];
bool isShipped = diagnostic.Properties[DeclarePublicApiAnalyzer.PublicApiIsShippedPropertyBagKey] == "true";
var mapToUpdate = isShipped ? shippedChanges : unshippedChanges;
string fileName = diagnostic.Properties[DeclarePublicApiAnalyzer.FileName];

if (!allChanges.TryGetValue(fileName, out var mapToUpdate))
{
mapToUpdate = new();
allChanges.Add(fileName, mapToUpdate);
}

mapToUpdate[oldName] = newName;
}
}

if (shippedSourceText is object)
foreach (var (path, changes) in allChanges)
{
SourceText newShippedSourceText = AnnotateSymbolNamesInSourceText(shippedSourceText, shippedChanges);
updatedPublicSurfaceAreaText.Add(new KeyValuePair<DocumentId, SourceText>(shippedDocument!.Id, newShippedSourceText));
}
var doc = project.GetPublicApiDocument(path);

if (unshippedSourceText is object)
{
SourceText newUnshippedSourceText = AnnotateSymbolNamesInSourceText(unshippedSourceText, unshippedChanges);
updatedPublicSurfaceAreaText.Add(new KeyValuePair<DocumentId, SourceText>(unshippedDocument!.Id, newUnshippedSourceText));
if (doc is not null)
{
var text = await doc.GetTextAsync(cancellationToken).ConfigureAwait(false);
SourceText newShippedSourceText = AnnotateSymbolNamesInSourceText(text, changes);
updatedPublicSurfaceAreaText.Add((doc.Id, newShippedSourceText));
}
}
}

Solution newSolution = _solution;

foreach (KeyValuePair<DocumentId, SourceText> pair in updatedPublicSurfaceAreaText)
foreach (var (docId, text) in updatedPublicSurfaceAreaText)
{
newSolution = newSolution.WithAdditionalDocumentText(pair.Key, pair.Value);
newSolution = newSolution.WithAdditionalDocumentText(docId, text);
}

return newSolution;
Expand Down
Loading

0 comments on commit 3bf2b00

Please sign in to comment.