diff --git a/src/PublicApiAnalyzers/Core/Analyzers/AnalyzerReleases.Unshipped.md b/src/PublicApiAnalyzers/Core/Analyzers/AnalyzerReleases.Unshipped.md index 5062e19588..c3721e8fbe 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/PublicApiAnalyzers/Core/Analyzers/AnalyzerReleases.Unshipped.md @@ -3,4 +3,5 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- RS0036 | ApiDesign | Warning | DeclarePublicApiAnalyzer, [Documentation](https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) RS0037 | ApiDesign | Warning | DeclarePublicApiAnalyzer, [Documentation](https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) -RS0041 | ApiDesign | Warning | DeclarePublicApiAnalyzer, [Documentation](https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) \ No newline at end of file +RS0041 | ApiDesign | Warning | DeclarePublicApiAnalyzer, [Documentation](https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) +RS0048 | ApiDesign | Warning | DeclarePublicApiAnalyzer, [Documentation](https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) \ No newline at end of file diff --git a/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs b/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs index e89a813c2f..64c717ab44 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs +++ b/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs @@ -69,6 +69,8 @@ public ApiName(string name, string nameWithNullability) private readonly struct ApiData #pragma warning restore CA1815 // Override equals and operator equals on value types { + public static readonly ApiData Empty = new ApiData(ImmutableArray.Empty, ImmutableArray.Empty, nullableRank: -1); + public ImmutableArray ApiList { get; } public ImmutableArray RemovedApiList { get; } // Number for the max line where #nullable enable was found (-1 otherwise) diff --git a/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.cs b/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.cs index d7d49d11a7..99e7e343bd 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.cs +++ b/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; @@ -93,6 +94,16 @@ public sealed partial class DeclarePublicApiAnalyzer : DiagnosticAnalyzer helpLinkUri: "https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md", customTags: WellKnownDiagnosticTags.Telemetry); + internal static readonly DiagnosticDescriptor PublicApiFileMissing = new DiagnosticDescriptor( + id: DiagnosticIds.PublicApiFileMissing, + title: PublicApiAnalyzerResources.PublicApiFileMissingTitle, + messageFormat: PublicApiAnalyzerResources.PublicApiFileMissingMessage, + category: "ApiDesign", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: "https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md", + customTags: WellKnownDiagnosticTags.Telemetry); + internal static readonly DiagnosticDescriptor DuplicateSymbolInApiFiles = new DiagnosticDescriptor( id: DiagnosticIds.DuplicatedSymbolInPublicApiFiles, title: PublicApiAnalyzerResources.DuplicateSymbolsInPublicApiFilesTitle, @@ -179,7 +190,7 @@ public sealed partial class DeclarePublicApiAnalyzer : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DeclareNewApiRule, AnnotateApiRule, ObliviousApiRule, RemoveDeletedApiRule, ExposedNoninstantiableType, - PublicApiFilesInvalid, DuplicateSymbolInApiFiles, AvoidMultipleOverloadsWithOptionalParameters, + PublicApiFilesInvalid, PublicApiFileMissing, DuplicateSymbolInApiFiles, AvoidMultipleOverloadsWithOptionalParameters, OverloadWithOptionalParametersShouldHaveMostParameters, ShouldAnnotateApiFilesRule); public override void Initialize(AnalysisContext context) @@ -195,13 +206,11 @@ public override void Initialize(AnalysisContext context) private void OnCompilationStart(CompilationStartAnalysisContext compilationContext) { var additionalFiles = compilationContext.Options.AdditionalFiles; + var errors = new List(); - if (!TryGetApiData(additionalFiles, compilationContext.CancellationToken, out ApiData shippedData, out ApiData unshippedData)) - { - return; - } - - if (!ValidateApiFiles(shippedData, unshippedData, out List errors)) + // Switch to "RegisterAdditionalFileAction" available in Microsoft.CodeAnalysis "3.8.x" to report additional file diagnostics: https://github.com/dotnet/roslyn-analyzers/issues/3918 + if (!TryGetApiData(additionalFiles, errors, compilationContext.CancellationToken, out ApiData shippedData, out ApiData unshippedData) || + !ValidateApiFiles(shippedData, unshippedData, errors)) { compilationContext.RegisterCompilationEndAction(context => { @@ -214,6 +223,8 @@ private void OnCompilationStart(CompilationStartAnalysisContext compilationConte return; } + Debug.Assert(errors.Count == 0); + var impl = new Impl(compilationContext.Compilation, shippedData, unshippedData); compilationContext.RegisterSymbolAction( impl.OnSymbolAction, @@ -265,10 +276,19 @@ private static ApiData ReadApiData(string path, SourceText sourceText, bool isSh return new ApiData(apiBuilder.ToImmutable(), removedBuilder.ToImmutable(), maxNullableRank); } - private static bool TryGetApiData(ImmutableArray additionalTexts, CancellationToken cancellationToken, out ApiData shippedData, out ApiData unshippedData) + private static bool TryGetApiData(ImmutableArray additionalTexts, List errors, CancellationToken cancellationToken, out ApiData shippedData, out ApiData unshippedData) { if (!TryGetApiText(additionalTexts, cancellationToken, out var shippedText, out var unshippedText)) { + if (shippedText == null && unshippedText == null) + { + // Bootstrapping public API files. + (shippedData, unshippedData) = (ApiData.Empty, ApiData.Empty); + return true; + } + + var missingFileName = shippedText == null ? ShippedFileName : UnshippedFileName; + errors.Add(Diagnostic.Create(PublicApiFileMissing, Location.None, missingFileName)); shippedData = default; unshippedData = default; return false; @@ -310,9 +330,8 @@ private static bool TryGetApiText( return shippedText != null && unshippedText != null; } - private static bool ValidateApiFiles(ApiData shippedData, ApiData unshippedData, out List errors) + private static bool ValidateApiFiles(ApiData shippedData, ApiData unshippedData, List errors) { - errors = new List(); if (!shippedData.RemovedApiList.IsEmpty) { errors.Add(Diagnostic.Create(PublicApiFilesInvalid, Location.None, InvalidReasonShippedCantHaveRemoved)); diff --git a/src/PublicApiAnalyzers/Core/Analyzers/PublicApiAnalyzerResources.resx b/src/PublicApiAnalyzers/Core/Analyzers/PublicApiAnalyzerResources.resx index 40f8b0212a..ec0d2b80d0 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/PublicApiAnalyzerResources.resx +++ b/src/PublicApiAnalyzers/Core/Analyzers/PublicApiAnalyzerResources.resx @@ -178,6 +178,12 @@ The contents of the public API files are invalid: {0} + + Missing shipped or unshipped public API file + + + Public API file '{0}' is missing or not marked as an additional analyzer file + Do not duplicate symbols in public API files diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.cs.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.cs.xlf index ef49eb4ee5..6ff2456a97 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.cs.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.cs.xlf @@ -127,6 +127,16 @@ Veřejné rozhraní API s nepovinnými parametry by mělo mít ze všech veřejných přetížení nejvíce parametrů + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} Obsah souborů veřejných rozhraní API není platný: {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.de.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.de.xlf index c2ef25518b..25a17bd404 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.de.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.de.xlf @@ -127,6 +127,16 @@ Öffentliche API mit optionalen Parametern muss die meisten Parameter bei den öffentlichen Überladungen aufweisen. + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} Die Inhalte der öffentlichen API-Dateien sind ungültig: {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.es.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.es.xlf index eed0e5d6d8..7f60dd7261 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.es.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.es.xlf @@ -127,6 +127,16 @@ La API pública con parámetros opcionales debe tener la mayoría de los parámetros entre sus sobrecargas públicas + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} El contenido de los archivos de la API pública no es válido: {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.fr.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.fr.xlf index e1eb6e6aaf..473e6e3377 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.fr.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.fr.xlf @@ -127,6 +127,16 @@ L'API publique avec un ou plusieurs paramètres optionnels doit avoir le plus de paramètres possibles parmi ses surcharges publiques. + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} Le contenu des fichiers de l'API publique est non valide : {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.it.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.it.xlf index 80abf08f68..4762c7a9f6 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.it.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.it.xlf @@ -127,6 +127,16 @@ L'API pubblica con parametri facoltativi deve includere la maggior parte dei parametri tra i relativi overload public. + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} Il contenuto dei file dell'API pubblica non è valido: {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ja.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ja.xlf index cc539deb8c..777bf68db7 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ja.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ja.xlf @@ -127,6 +127,16 @@ 省略可能なパラメーターを持つパブリック API は、そのパブリック オーバーロード内のほとんどのパラメーターを持つ必要があります。 + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} パブリック API ファイルのコンテンツが無効です: {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ko.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ko.xlf index f001121f55..14d9563646 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ko.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ko.xlf @@ -127,6 +127,16 @@ 선택적 매개 변수가 있는 공용 API에는 공용 오버로드 중 대부분의 매개 변수가 포함되어야 합니다. + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} 공용 API 파일의 콘텐츠가 유효하지 않습니다. {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.pl.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.pl.xlf index b1d15d46f4..ac796dbbce 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.pl.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.pl.xlf @@ -127,6 +127,16 @@ Publiczny interfejs API z opcjonalnymi parametrami powinien mieć najwięcej parametrów spośród przeciążeń publicznych. + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} Zawartość plików publicznego interfejsu API jest nieprawidłowa: {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.pt-BR.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.pt-BR.xlf index 76ec9cafab..21dd5ae0bf 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.pt-BR.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.pt-BR.xlf @@ -127,6 +127,16 @@ A API pública com parâmetros opcionais deve ter a maioria dos parâmetros entre suas sobrecargas públicas. + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} O conteúdo dos arquivos da API pública é inválido: {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ru.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ru.xlf index 2c5f336c23..6e751cae39 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ru.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.ru.xlf @@ -127,6 +127,16 @@ Открытый API с необязательными параметрами должен иметь основную часть параметров среди своих открытых перегрузок. + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} Недопустимое содержимое файлов открытого API: {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.tr.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.tr.xlf index 19b665789f..96ee170390 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.tr.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.tr.xlf @@ -127,6 +127,16 @@ İsteğe bağlı parametreleri olan genel API'nin, genel aşırı yüklemeleri arasında en çok parametreye sahip olması gerekir. + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} Genel API dosyalarının içerikleri geçersiz: {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.zh-Hans.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.zh-Hans.xlf index a3a77757ba..a3a0535abc 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.zh-Hans.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.zh-Hans.xlf @@ -127,6 +127,16 @@ 具有可选参数的公共 API 应在其公共重载中具有最多参数。 + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} 公共 API 文件的内容无效: {0} diff --git a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.zh-Hant.xlf b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.zh-Hant.xlf index 8c8c24c83c..a922305789 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.zh-Hant.xlf +++ b/src/PublicApiAnalyzers/Core/Analyzers/xlf/PublicApiAnalyzerResources.zh-Hant.xlf @@ -127,6 +127,16 @@ 具有選擇性參數的公用 API,大部分的參數應位於其公用多載之間。 + + Public API file '{0}' is missing or not marked as an additional analyzer file + Public API file '{0}' is missing or not marked as an additional analyzer file + + + + Missing shipped or unshipped public API file + Missing shipped or unshipped public API file + + The contents of the public API files are invalid: {0} 公用 API 檔案的內容無效: {0} diff --git a/src/PublicApiAnalyzers/Core/CodeFixes/DeclarePublicApiFix.cs b/src/PublicApiAnalyzers/Core/CodeFixes/DeclarePublicApiFix.cs index 042e697357..eb17154142 100644 --- a/src/PublicApiAnalyzers/Core/CodeFixes/DeclarePublicApiFix.cs +++ b/src/PublicApiAnalyzers/Core/CodeFixes/DeclarePublicApiFix.cs @@ -30,12 +30,8 @@ public sealed override FixAllProvider GetFixAllProvider() public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) { - Project project = context.Document.Project; - TextDocument publicSurfaceAreaDocument = GetUnshippedDocument(project); - if (publicSurfaceAreaDocument == null) - { - return Task.CompletedTask; - } + var project = context.Document.Project; + var publicSurfaceAreaDocument = GetUnshippedDocument(project); foreach (Diagnostic diagnostic in context.Diagnostics) { @@ -48,14 +44,14 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( new AdditionalDocumentChangeAction( $"Add {minimalSymbolName} to public API", - c => GetFix(publicSurfaceAreaDocument, publicSurfaceAreaSymbolName, siblingSymbolNamesToRemove, c)), + c => GetFixAsync(publicSurfaceAreaDocument, project, publicSurfaceAreaSymbolName, siblingSymbolNamesToRemove, c)), diagnostic); } return Task.CompletedTask; } - internal static TextDocument GetUnshippedDocument(Project project) + internal static TextDocument? GetUnshippedDocument(Project project) { return project.AdditionalDocuments.FirstOrDefault(doc => doc.Name.Equals(DeclarePublicApiAnalyzer.UnshippedFileName, StringComparison.Ordinal)); } @@ -65,16 +61,44 @@ internal static TextDocument GetUnshippedDocument(Project project) return project.AdditionalDocuments.FirstOrDefault(doc => doc.Name.Equals(DeclarePublicApiAnalyzer.ShippedFileName, StringComparison.Ordinal)); } - private static async Task GetFix(TextDocument publicSurfaceAreaDocument, string newSymbolName, ImmutableHashSet siblingSymbolNamesToRemove, CancellationToken cancellationToken) + private static async Task GetFixAsync(TextDocument? publicSurfaceAreaDocument, Project project, string newSymbolName, ImmutableHashSet siblingSymbolNamesToRemove, CancellationToken cancellationToken) { - SourceText sourceText = await publicSurfaceAreaDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); - SourceText newSourceText = AddSymbolNamesToSourceText(sourceText, new[] { newSymbolName }); - newSourceText = RemoveSymbolNamesFromSourceText(newSourceText, siblingSymbolNamesToRemove); + if (publicSurfaceAreaDocument == null) + { + var newSourceText = AddSymbolNamesToSourceText(sourceText: null, new[] { newSymbolName }); + return AddPublicApiFiles(project, newSourceText); + } + else + { + var sourceText = await publicSurfaceAreaDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); + var newSourceText = AddSymbolNamesToSourceText(sourceText, new[] { newSymbolName }); + newSourceText = RemoveSymbolNamesFromSourceText(newSourceText, siblingSymbolNamesToRemove); - return publicSurfaceAreaDocument.Project.Solution.WithAdditionalDocumentText(publicSurfaceAreaDocument.Id, newSourceText); + return publicSurfaceAreaDocument.Project.Solution.WithAdditionalDocumentText(publicSurfaceAreaDocument.Id, newSourceText); + } } - private static SourceText AddSymbolNamesToSourceText(SourceText sourceText, IEnumerable newSymbolNames) + private static Solution AddPublicApiFiles(Project project, SourceText unshippedText) + { + Debug.Assert(unshippedText.Length > 0); + project = AddAdditionalDocument(project, DeclarePublicApiAnalyzer.ShippedFileName, SourceText.From(string.Empty)); + project = AddAdditionalDocument(project, DeclarePublicApiAnalyzer.UnshippedFileName, unshippedText); + return project.Solution; + + // Local functions. + static Project AddAdditionalDocument(Project project, string name, SourceText text) + { + TextDocument? additionalDocument = project.AdditionalDocuments.FirstOrDefault(doc => string.Equals(doc.Name, name, StringComparison.OrdinalIgnoreCase)); + if (additionalDocument == null) + { + project = project.AddAdditionalDocument(name, text).Project; + } + + return project; + } + } + + private static SourceText AddSymbolNamesToSourceText(SourceText? sourceText, IEnumerable newSymbolNames) { List lines = GetLinesFromSourceText(sourceText); @@ -83,8 +107,8 @@ private static SourceText AddSymbolNamesToSourceText(SourceText sourceText, IEnu insertInList(lines, name); } - SourceText newSourceText = sourceText.Replace(new TextSpan(0, sourceText.Length), string.Join(Environment.NewLine, lines) + GetEndOfFileText(sourceText)); - return newSourceText; + var newText = string.Join(Environment.NewLine, lines) + GetEndOfFileText(sourceText); + return sourceText?.Replace(new TextSpan(0, sourceText.Length), newText) ?? SourceText.From(newText); // Insert name at the first suitable position static void insertInList(List list, string name) @@ -116,8 +140,13 @@ private static SourceText RemoveSymbolNamesFromSourceText(SourceText sourceText, return newSourceText; } - internal static List GetLinesFromSourceText(SourceText sourceText) + internal static List GetLinesFromSourceText(SourceText? sourceText) { + if (sourceText == null) + { + return new List(); + } + var lines = new List(); foreach (TextLine textLine in sourceText.Lines) @@ -138,9 +167,9 @@ internal static List GetLinesFromSourceText(SourceText sourceText) /// The source text. /// if ends with a trailing newline; /// otherwise, . - public static string GetEndOfFileText(SourceText sourceText) + public static string GetEndOfFileText(SourceText? sourceText) { - if (sourceText.Length == 0) + if (sourceText == null || sourceText.Length == 0) return string.Empty; var lastLine = sourceText.Lines[^1]; @@ -184,20 +213,18 @@ public FixAllAdditionalDocumentChangeAction(string title, Solution solution, Lis protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken) { var updatedPublicSurfaceAreaText = new List>(); + var addedPublicSurfaceAreaText = new List>(); foreach (KeyValuePair> pair in _diagnosticsToFix) { Project project = pair.Key; ImmutableArray diagnostics = pair.Value; - TextDocument publicSurfaceAreaAdditionalDocument = GetUnshippedDocument(project); - - if (publicSurfaceAreaAdditionalDocument == null) - { - continue; - } + var publicSurfaceAreaAdditionalDocument = GetUnshippedDocument(project); - SourceText sourceText = await publicSurfaceAreaAdditionalDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); + var sourceText = publicSurfaceAreaAdditionalDocument != null ? + await publicSurfaceAreaAdditionalDocument.GetTextAsync(cancellationToken).ConfigureAwait(false) : + null; IEnumerable> groupedDiagnostics = diagnostics @@ -251,7 +278,14 @@ protected override async Task GetChangedSolutionAsync(CancellationToke SourceText newSourceText = AddSymbolNamesToSourceText(sourceText, newSymbolNames); newSourceText = RemoveSymbolNamesFromSourceText(newSourceText, symbolNamesToRemove); - updatedPublicSurfaceAreaText.Add(new KeyValuePair(publicSurfaceAreaAdditionalDocument.Id, newSourceText)); + if (publicSurfaceAreaAdditionalDocument != null) + { + updatedPublicSurfaceAreaText.Add(new KeyValuePair(publicSurfaceAreaAdditionalDocument.Id, newSourceText)); + } + else if (newSourceText.Length > 0) + { + addedPublicSurfaceAreaText.Add(new KeyValuePair(project.Id, newSourceText)); + } } Solution newSolution = _solution; @@ -261,6 +295,12 @@ protected override async Task GetChangedSolutionAsync(CancellationToke newSolution = newSolution.WithAdditionalDocumentText(pair.Key, pair.Value); } + foreach (KeyValuePair pair in addedPublicSurfaceAreaText) + { + var project = newSolution.GetProject(pair.Key); + newSolution = AddPublicApiFiles(project, pair.Value); + } + return newSolution; } } diff --git a/src/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.md b/src/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.md index 82921cf15c..77a1e03bed 100644 --- a/src/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.md +++ b/src/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.md @@ -11,3 +11,4 @@ Rule ID | Title | Category | Enabled | Severity | CodeFix | Description | [RS0036](https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) | Annotate nullability of public types and members in the declared API | ApiDesign | True | Warning | True | All public types and members should be declared with nullability annotations in PublicAPI.txt. This draws attention to API nullability changes in the code reviews and source control history, and helps prevent breaking changes. | [RS0037](https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) | Enable tracking of nullability of reference types in the declared API | ApiDesign | True | Warning | True | PublicAPI.txt files should have `#nullable enable` to track nullability information, or this diagnostic should be suppressed. With nullability enabled, PublicAPI.txt records which types are nullable (suffix `?` on type) or non-nullable (suffix `!`). It also tracks any API that is still using an oblivious reference type (prefix `~` on line). | [RS0041](https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) | Public members should not use oblivious types | ApiDesign | True | Warning | False | All public members should use either nullable or non-nullable reference types, but no oblivious reference types. | +[RS0048](https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) | Missing shipped or unshipped public API file | ApiDesign | True | Warning | False | Public API file '{0}' is missing or not marked as an additional analyzer file | diff --git a/src/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.sarif b/src/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.sarif index 36a5175fbc..1e5d7a123c 100644 --- a/src/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.sarif +++ b/src/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.sarif @@ -198,6 +198,25 @@ "Telemetry" ] } + }, + "RS0048": { + "id": "RS0048", + "shortDescription": "Missing shipped or unshipped public API file", + "fullDescription": "Public API file '{0}' is missing or not marked as an additional analyzer file", + "defaultLevel": "warning", + "helpUri": "https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md", + "properties": { + "category": "ApiDesign", + "isEnabledByDefault": true, + "typeName": "DeclarePublicApiAnalyzer", + "languages": [ + "C#", + "Visual Basic" + ], + "tags": [ + "Telemetry" + ] + } } } }, diff --git a/src/PublicApiAnalyzers/UnitTests/DeclarePublicApiAnalyzerTests.cs b/src/PublicApiAnalyzers/UnitTests/DeclarePublicApiAnalyzerTests.cs index 18f5d32d47..a5ba256f37 100644 --- a/src/PublicApiAnalyzers/UnitTests/DeclarePublicApiAnalyzerTests.cs +++ b/src/PublicApiAnalyzers/UnitTests/DeclarePublicApiAnalyzerTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable #pragma warning disable CA1305 using System.Globalization; @@ -63,7 +64,7 @@ private async Task VerifyBasicAsync(string source, string shippedApiText, string await test.RunAsync(); } - private async Task VerifyCSharpAsync(string source, string shippedApiText, string unshippedApiText, params DiagnosticResult[] expected) + private async Task VerifyCSharpAsync(string source, string? shippedApiText, string? unshippedApiText, params DiagnosticResult[] expected) { var test = new CSharpCodeFixTest { @@ -113,22 +114,24 @@ private async Task VerifyCSharpAsync(string source, string shippedApiText, strin await test.RunAsync(); } - private async Task VerifyCSharpAdditionalFileFixAsync(string source, string shippedApiText, string oldUnshippedApiText, string newUnshippedApiText) + private async Task VerifyCSharpAdditionalFileFixAsync(string source, string? shippedApiText, string? oldUnshippedApiText, string newUnshippedApiText) { await VerifyAdditionalFileFixAsync(LanguageNames.CSharp, source, shippedApiText, oldUnshippedApiText, newUnshippedApiText); } - private async Task VerifyAdditionalFileFixAsync(string language, string source, string shippedApiText, string oldUnshippedApiText, string newUnshippedApiText) + private async Task VerifyAdditionalFileFixAsync(string language, string source, string? shippedApiText, string? oldUnshippedApiText, string newUnshippedApiText) { var test = language == LanguageNames.CSharp ? new CSharpCodeFixTest() : (CodeFixTest)new VisualBasicCodeFixTest(); test.TestState.Sources.Add(source); - test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.ShippedFileName, shippedApiText)); - test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.UnshippedFileName, oldUnshippedApiText)); + if (shippedApiText != null) + test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.ShippedFileName, shippedApiText)); + if (oldUnshippedApiText != null) + test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.UnshippedFileName, oldUnshippedApiText)); - test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.ShippedFileName, shippedApiText)); + test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.ShippedFileName, shippedApiText ?? string.Empty)); test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.UnshippedFileName, newUnshippedApiText)); await test.RunAsync(); @@ -137,7 +140,7 @@ private async Task VerifyAdditionalFileFixAsync(string language, string source, #region Diagnostic tests - [Fact(Skip = "https://github.com/dotnet/roslyn-analyzers/issues/2622")] + [Fact] [WorkItem(2622, "https://github.com/dotnet/roslyn-analyzers/issues/2622")] public async Task AnalyzerFileMissing_Shipped() { @@ -148,13 +151,15 @@ private C() { } } "; - string shippedText = null; - string unshippedText = @""; + string? shippedText = null; + string? unshippedText = @""; - await VerifyCSharpAsync(source, shippedText, unshippedText, GetCSharpResultAt(2, 14, DeclarePublicApiAnalyzer.DeclareNewApiRule, "C")); + var expected = new DiagnosticResult(DeclarePublicApiAnalyzer.PublicApiFileMissing) + .WithArguments(DeclarePublicApiAnalyzer.ShippedFileName); + await VerifyCSharpAsync(source, shippedText, unshippedText, expected); } - [Fact(Skip = "https://github.com/dotnet/roslyn-analyzers/issues/2622")] + [Fact] [WorkItem(2622, "https://github.com/dotnet/roslyn-analyzers/issues/2622")] public async Task AnalyzerFileMissing_Unshipped() { @@ -165,13 +170,15 @@ private C() { } } "; - string shippedText = @""; - string unshippedText = null; + string? shippedText = @""; + string? unshippedText = null; - await VerifyCSharpAsync(source, shippedText, unshippedText, GetCSharpResultAt(2, 14, DeclarePublicApiAnalyzer.DeclareNewApiRule, "C")); + var expected = new DiagnosticResult(DeclarePublicApiAnalyzer.PublicApiFileMissing) + .WithArguments(DeclarePublicApiAnalyzer.UnshippedFileName); + await VerifyCSharpAsync(source, shippedText, unshippedText, expected); } - [Fact(Skip = "https://github.com/dotnet/roslyn-analyzers/issues/2622")] + [Fact] [WorkItem(2622, "https://github.com/dotnet/roslyn-analyzers/issues/2622")] public async Task AnalyzerFileMissing_Both() { @@ -182,8 +189,8 @@ private C() { } } "; - string shippedText = null; - string unshippedText = null; + string? shippedText = null; + string? unshippedText = null; await VerifyCSharpAsync(source, shippedText, unshippedText, GetCSharpResultAt(2, 14, DeclarePublicApiAnalyzer.DeclareNewApiRule, "C")); } @@ -1249,6 +1256,24 @@ await VerifyCSharpAsync(source, shippedText, unshippedText, #region Fix tests + [Fact] + [WorkItem(2622, "https://github.com/dotnet/roslyn-analyzers/issues/2622")] + public async Task AnalyzerFileMissing_Both_Fix() + { + var source = @" +public class {|RS0016:C|} +{ + private C() { } +} +"; + + string? shippedText = null; + string? unshippedText = null; + var fixedUnshippedText = @"C"; + + await VerifyCSharpAdditionalFileFixAsync(source, shippedText, unshippedText, fixedUnshippedText); + } + [Fact] public async Task TestSimpleMissingMember_Fix() { diff --git a/src/Roslyn.Diagnostics.Analyzers/Core/RoslynDiagnosticIds.cs b/src/Roslyn.Diagnostics.Analyzers/Core/RoslynDiagnosticIds.cs index 79c54889f9..d051b48aa4 100644 --- a/src/Roslyn.Diagnostics.Analyzers/Core/RoslynDiagnosticIds.cs +++ b/src/Roslyn.Diagnostics.Analyzers/Core/RoslynDiagnosticIds.cs @@ -51,6 +51,7 @@ internal static class RoslynDiagnosticIds // public const string ExposeMemberForTestingRuleId = "RS0045"; // Now converted to a refactoring public const string AvoidOptSuffixForNullableEnableCodeRuleId = "RS0046"; public const string NamedTypeFullNameNotNullSuppressionRuleId = "RS0047"; + public const string PublicApiFileMissing = "RS0048"; public const string WrapStatementsRuleId = "RS0100"; public const string BlankLinesRuleId = "RS0101"; diff --git a/src/Tools/GenerateGlobalAnalyzerConfigs/Program.cs b/src/Tools/GenerateGlobalAnalyzerConfigs/Program.cs index fc6b8c4be2..0b06860a5d 100644 --- a/src/Tools/GenerateGlobalAnalyzerConfigs/Program.cs +++ b/src/Tools/GenerateGlobalAnalyzerConfigs/Program.cs @@ -379,6 +379,18 @@ static string GetPackageSpecificContents(string packageName) "; } + else if (packageName == "Microsoft.CodeAnalysis.PublicApiAnalyzers") + { + return @" + + + + + + + + "; + } return string.Empty; }