提交 fd04f516 编写于 作者: J Jared Parsons

Merge pull request #4476 from jaredpar/pub

Split the public API files
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
namespace Roslyn.Diagnostics.Analyzers.ApiDesign
{
public partial class DeclarePublicAPIAnalyzer : DiagnosticAnalyzer
{
private sealed class ApiLine
{
public readonly string Text;
public readonly TextSpan Span;
public readonly SourceText SourceText;
public readonly string Path;
internal ApiLine(string text, TextSpan span, SourceText sourceText, string path)
{
Text = text;
Span = span;
SourceText = sourceText;
Path = path;
}
}
private struct ApiData
{
public readonly ImmutableArray<ApiLine> ApiList;
public readonly ImmutableArray<ApiLine> RemovedApiList;
internal ApiData(ImmutableArray<ApiLine> apiList, ImmutableArray<ApiLine> removedApiList)
{
ApiList = apiList;
RemovedApiList = removedApiList;
}
}
private sealed class Impl
{
private static readonly HashSet<MethodKind> s_ignorableMethodKinds = new HashSet<MethodKind>
{
MethodKind.EventAdd,
MethodKind.EventRemove
};
private readonly ApiData _unshippedData;
private readonly Dictionary<ITypeSymbol, bool> _typeCanBeExtendedCache = new Dictionary<ITypeSymbol, bool>();
private readonly HashSet<string> _visitedApiList = new HashSet<string>(StringComparer.Ordinal);
private readonly Dictionary<string, ApiLine> _publicApiMap = new Dictionary<string, ApiLine>(StringComparer.Ordinal);
internal Impl(ApiData shippedData, ApiData unshippedData)
{
_unshippedData = unshippedData;
foreach (var cur in shippedData.ApiList)
{
_publicApiMap.Add(cur.Text, cur);
}
foreach (var cur in unshippedData.ApiList)
{
_publicApiMap.Add(cur.Text, cur);
}
}
internal void OnSymbolAction(SymbolAnalysisContext symbolContext)
{
var symbol = symbolContext.Symbol;
var methodSymbol = symbol as IMethodSymbol;
if (methodSymbol != null &&
s_ignorableMethodKinds.Contains(methodSymbol.MethodKind))
{
return;
}
if (!IsPublicApi(symbol))
{
return;
}
string publicApiName = GetPublicApiName(symbol);
_visitedApiList.Add(publicApiName);
if (!_publicApiMap.ContainsKey(publicApiName))
{
var errorMessageName = symbol.ToDisplayString(ShortSymbolNameFormat);
var propertyBag = ImmutableDictionary<string, string>.Empty
.Add(PublicApiNamePropertyBagKey, publicApiName)
.Add(MinimalNamePropertyBagKey, errorMessageName);
foreach (var sourceLocation in symbol.Locations.Where(loc => loc.IsInSource))
{
symbolContext.ReportDiagnostic(Diagnostic.Create(DeclareNewApiRule, sourceLocation, propertyBag, errorMessageName));
}
}
// Check if a public API is a constructor that makes this class instantiable, even though the base class
// is not instantiable. That API pattern is not allowed, because it causes protected members of
// the base class, which are not considered public APIs, to be exposed to subclasses of this class.
if ((symbol as IMethodSymbol)?.MethodKind == MethodKind.Constructor &&
symbol.ContainingType.TypeKind == TypeKind.Class &&
!symbol.ContainingType.IsSealed &&
symbol.ContainingType.BaseType != null &&
IsPublicApi(symbol.ContainingType.BaseType) &&
!CanTypeBeExtendedPublicly(symbol.ContainingType.BaseType))
{
var errorMessageName = symbol.ToDisplayString(ShortSymbolNameFormat);
var propertyBag = ImmutableDictionary<string, string>.Empty;
symbolContext.ReportDiagnostic(Diagnostic.Create(ExposedNoninstantiableType, symbol.Locations[0], propertyBag, errorMessageName));
}
}
internal void OnCompilationEnd(CompilationAnalysisContext context)
{
List<ApiLine> deletedApiList = GetDeletedApiList();
foreach (var cur in deletedApiList)
{
var linePositionSpan = cur.SourceText.Lines.GetLinePositionSpan(cur.Span);
var location = Location.Create(cur.Path, cur.Span, linePositionSpan);
var propertyBag = ImmutableDictionary<string, string>.Empty.Add(PublicApiNamePropertyBagKey, cur.Text);
context.ReportDiagnostic(Diagnostic.Create(RemoveDeletedApiRule, location, propertyBag, cur.Text));
}
}
/// <summary>
/// Calculated the set of APIs which have been deleted but not yet documented.
/// </summary>
/// <returns></returns>
internal List<ApiLine> GetDeletedApiList()
{
var list = new List<ApiLine>();
foreach (var pair in _publicApiMap)
{
if (_visitedApiList.Contains(pair.Key))
{
continue;
}
if (_unshippedData.RemovedApiList.Any(x => x.Text == pair.Key))
{
continue;
}
list.Add(pair.Value);
}
return list;
}
private bool IsPublicApi(ISymbol symbol)
{
switch (symbol.DeclaredAccessibility)
{
case Accessibility.Public:
return symbol.ContainingType == null || IsPublicApi(symbol.ContainingType);
case Accessibility.Protected:
case Accessibility.ProtectedOrInternal:
// Protected symbols must have parent types (that is, top-level protected
// symbols are not allowed.
return
symbol.ContainingType != null &&
IsPublicApi(symbol.ContainingType) &&
CanTypeBeExtendedPublicly(symbol.ContainingType);
default:
return false;
}
}
private bool CanTypeBeExtendedPublicly(ITypeSymbol type)
{
bool result;
if (_typeCanBeExtendedCache.TryGetValue(type, out result))
{
return result;
}
// a type can be extended publicly if (1) it isn't sealed, and (2) it has some constructor that is
// not internal, private or protected&internal
result = !type.IsSealed &&
type.GetMembers(WellKnownMemberNames.InstanceConstructorName).Any(
m => m.DeclaredAccessibility != Accessibility.Internal && m.DeclaredAccessibility != Accessibility.Private && m.DeclaredAccessibility != Accessibility.ProtectedAndInternal
);
_typeCanBeExtendedCache.Add(type, result);
return result;
}
}
}
}
......@@ -13,11 +13,14 @@
namespace Roslyn.Diagnostics.Analyzers.ApiDesign
{
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public class DeclarePublicAPIAnalyzer : DiagnosticAnalyzer
public sealed partial class DeclarePublicAPIAnalyzer : DiagnosticAnalyzer
{
internal const string PublicApiFileName = "PublicAPI.txt";
internal const string ShippedFileName = "PublicAPI.Shipped.txt";
internal const string UnshippedFileName = "PublicAPI.Unshipped.txt";
internal const string PublicApiNamePropertyBagKey = "PublicAPIName";
internal const string MinimalNamePropertyBagKey = "MinimalName";
internal const string RemovedApiPrefix = "*REMOVED*";
internal const string InvalidReasonShippedCantHaveRemoved = "The shipped API file can't have removed members";
internal static readonly DiagnosticDescriptor DeclareNewApiRule = new DiagnosticDescriptor(
id: RoslynDiagnosticIds.DeclarePublicApiRuleId,
......@@ -49,6 +52,16 @@ public class DeclarePublicAPIAnalyzer : DiagnosticAnalyzer
description: RoslynDiagnosticsResources.ExposedNoninstantiableTypeDescription,
customTags: WellKnownDiagnosticTags.Telemetry);
internal static readonly DiagnosticDescriptor PublicApiFilesInvalid = new DiagnosticDescriptor(
id: RoslynDiagnosticIds.PublicApiFilesInvalid,
title: RoslynDiagnosticsResources.PublicApiFilesInvalid,
messageFormat: RoslynDiagnosticsResources.PublicApiFilesInvalid,
category: "ApiDesign",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: RoslynDiagnosticsResources.PublicApiFilesInvalid,
customTags: WellKnownDiagnosticTags.Telemetry);
internal static readonly SymbolDisplayFormat ShortSymbolNameFormat =
new SymbolDisplayFormat(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining,
......@@ -83,132 +96,65 @@ public class DeclarePublicAPIAnalyzer : DiagnosticAnalyzer
miscellaneousOptions:
SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
private static readonly HashSet<MethodKind> s_ignorableMethodKinds = new HashSet<MethodKind>
{
MethodKind.EventAdd,
MethodKind.EventRemove
};
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DeclareNewApiRule, RemoveDeletedApiRule, ExposedNoninstantiableType);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DeclareNewApiRule, RemoveDeletedApiRule, ExposedNoninstantiableType, PublicApiFilesInvalid);
public override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction(compilationContext =>
{
AdditionalText publicApiAdditionalText = TryGetPublicApiSpec(compilationContext.Options.AdditionalFiles, compilationContext.CancellationToken);
private readonly ImmutableArray<AdditionalText> _extraAdditionalFiles;
if (publicApiAdditionalText == null)
/// <summary>
/// This API is used for testing to allow arguments to be passed to the analyzer.
/// </summary>
public DeclarePublicAPIAnalyzer(ImmutableArray<AdditionalText> extraAdditionalFiles)
{
return;
_extraAdditionalFiles = extraAdditionalFiles;
}
SourceText publicApiSourceText = publicApiAdditionalText.GetText(compilationContext.CancellationToken);
HashSet<string> declaredPublicSymbols = ReadPublicSymbols(publicApiSourceText, compilationContext.CancellationToken);
HashSet<string> examinedPublicTypes = new HashSet<string>();
object lockObj = new object();
Dictionary<ITypeSymbol, bool> typeCanBeExtendedPubliclyMap = new Dictionary<ITypeSymbol, bool>();
Func<ITypeSymbol, bool> typeCanBeExtendedPublicly = type =>
public DeclarePublicAPIAnalyzer()
{
bool result;
if (typeCanBeExtendedPubliclyMap.TryGetValue(type, out result)) return result;
// a type can be extended publicly if (1) it isn't sealed, and (2) it has some constructor that is
// not internal, private or protected&internal
result = !type.IsSealed &&
type.GetMembers(WellKnownMemberNames.InstanceConstructorName).Any(
m => m.DeclaredAccessibility != Accessibility.Internal && m.DeclaredAccessibility != Accessibility.Private && m.DeclaredAccessibility != Accessibility.ProtectedAndInternal
);
typeCanBeExtendedPubliclyMap.Add(type, result);
return result;
};
}
compilationContext.RegisterSymbolAction(symbolContext =>
public override void Initialize(AnalysisContext context)
{
var symbol = symbolContext.Symbol;
context.RegisterCompilationStartAction(OnCompilationStart);
}
var methodSymbol = symbol as IMethodSymbol;
if (methodSymbol != null &&
s_ignorableMethodKinds.Contains(methodSymbol.MethodKind))
private void OnCompilationStart(CompilationStartAnalysisContext compilationContext)
{
return;
var additionalFiles = compilationContext.Options.AdditionalFiles;
if (!_extraAdditionalFiles.IsDefaultOrEmpty)
{
additionalFiles = additionalFiles.AddRange(_extraAdditionalFiles);
}
lock (lockObj)
{
if (!IsPublicApi(symbol, typeCanBeExtendedPublicly))
ApiData shippedData;
ApiData unshippedData;
if (!TryGetApiData(additionalFiles, compilationContext.CancellationToken, out shippedData, out unshippedData))
{
return;
}
string publicApiName = GetPublicApiName(symbol);
examinedPublicTypes.Add(publicApiName);
if (!declaredPublicSymbols.Contains(publicApiName))
List<Diagnostic> errors;
if (!ValidateApiFiles(shippedData, unshippedData, out errors))
{
var errorMessageName = symbol.ToDisplayString(ShortSymbolNameFormat);
var propertyBag = ImmutableDictionary<string, string>.Empty
.Add(PublicApiNamePropertyBagKey, publicApiName)
.Add(MinimalNamePropertyBagKey, errorMessageName);
foreach (var sourceLocation in symbol.Locations.Where(loc => loc.IsInSource))
compilationContext.RegisterCompilationEndAction(context =>
{
symbolContext.ReportDiagnostic(Diagnostic.Create(DeclareNewApiRule, sourceLocation, propertyBag, errorMessageName));
}
}
// Check if a public API is a constructor that makes this class instantiable, even though the base class
// is not instantiable. That API pattern is not allowed, because it causes protected members of
// the base class, which are not considered public APIs, to be exposed to subclasses of this class.
if ((symbol as IMethodSymbol)?.MethodKind == MethodKind.Constructor &&
symbol.ContainingType.TypeKind == TypeKind.Class &&
!symbol.ContainingType.IsSealed &&
symbol.ContainingType.BaseType != null &&
IsPublicApi(symbol.ContainingType.BaseType, typeCanBeExtendedPublicly) &&
!typeCanBeExtendedPublicly(symbol.ContainingType.BaseType))
foreach (var cur in errors)
{
var errorMessageName = symbol.ToDisplayString(ShortSymbolNameFormat);
var propertyBag = ImmutableDictionary<string, string>.Empty;
symbolContext.ReportDiagnostic(Diagnostic.Create(ExposedNoninstantiableType, symbol.Locations[0], propertyBag, errorMessageName));
context.ReportDiagnostic(cur);
}
});
return;
}
},
var impl = new Impl(shippedData, unshippedData);
compilationContext.RegisterSymbolAction(
impl.OnSymbolAction,
SymbolKind.NamedType,
SymbolKind.Event,
SymbolKind.Field,
SymbolKind.Method);
compilationContext.RegisterCompilationEndAction(compilationEndContext =>
{
ImmutableArray<string> deletedSymbols;
lock (lockObj)
{
deletedSymbols = declaredPublicSymbols.Where(symbol => !examinedPublicTypes.Contains(symbol)).ToImmutableArray();
}
foreach (var symbol in deletedSymbols)
{
var span = FindString(publicApiSourceText, symbol);
Location location;
if (span.HasValue)
{
var linePositionSpan = publicApiSourceText.Lines.GetLinePositionSpan(span.Value);
location = Location.Create(publicApiAdditionalText.Path, span.Value, linePositionSpan);
}
else
{
location = Location.Create(publicApiAdditionalText.Path, default(TextSpan), default(LinePositionSpan));
}
var propertyBag = ImmutableDictionary<string, string>.Empty.Add(PublicApiNamePropertyBagKey, symbol);
compilationEndContext.ReportDiagnostic(Diagnostic.Create(RemoveDeletedApiRule, location, propertyBag, symbol));
}
});
});
compilationContext.RegisterCompilationEndAction(impl.OnCompilationEnd);
}
internal static string GetPublicApiName(ISymbol symbol)
......@@ -241,70 +187,85 @@ internal static string GetPublicApiName(ISymbol symbol)
return publicApiName;
}
private TextSpan? FindString(SourceText sourceText, string symbol)
private static ApiData ReadApiData(string path, SourceText sourceText)
{
var apiBuilder = ImmutableArray.CreateBuilder<ApiLine>();
var removedBuilder = ImmutableArray.CreateBuilder<ApiLine>();
foreach (var line in sourceText.Lines)
{
if (line.ToString() == symbol)
var text = line.ToString();
if (string.IsNullOrWhiteSpace(text))
{
return line.Span;
}
continue;
}
return null;
}
private static HashSet<string> ReadPublicSymbols(SourceText file, CancellationToken cancellationToken)
{
HashSet<string> publicSymbols = new HashSet<string>();
foreach (var line in file.Lines)
var apiLine = new ApiLine(text, line.Span, sourceText, path);
if (text.StartsWith(RemovedApiPrefix, StringComparison.Ordinal))
{
cancellationToken.ThrowIfCancellationRequested();
var text = line.ToString();
if (!string.IsNullOrWhiteSpace(text))
removedBuilder.Add(apiLine);
}
else
{
publicSymbols.Add(text);
apiBuilder.Add(apiLine);
}
}
return publicSymbols;
return new ApiData(apiBuilder.ToImmutable(), removedBuilder.ToImmutable());
}
private static bool IsPublicApi(ISymbol symbol, Func<ITypeSymbol, bool> typeCanBeExtendedPublicly)
private static bool TryGetApiData(ImmutableArray<AdditionalText> additionalTexts, CancellationToken cancellationToken, out ApiData shippedData, out ApiData unshippedData)
{
switch (symbol.DeclaredAccessibility)
AdditionalText shippedText;
AdditionalText unshippedText;
if (!TryGetApiText(additionalTexts, cancellationToken, out shippedText, out unshippedText))
{
case Accessibility.Public:
return symbol.ContainingType == null || IsPublicApi(symbol.ContainingType, typeCanBeExtendedPublicly);
case Accessibility.Protected:
case Accessibility.ProtectedOrInternal:
// Protected symbols must have parent types (that is, top-level protected
// symbols are not allowed.
return
symbol.ContainingType != null &&
IsPublicApi(symbol.ContainingType, typeCanBeExtendedPublicly) &&
typeCanBeExtendedPublicly(symbol.ContainingType);
default:
shippedData = default(ApiData);
unshippedData = default(ApiData);
return false;
}
shippedData = ReadApiData(ShippedFileName, shippedText.GetText(cancellationToken));
unshippedData = ReadApiData(UnshippedFileName, unshippedText.GetText(cancellationToken));
return true;
}
private static AdditionalText TryGetPublicApiSpec(ImmutableArray<AdditionalText> additionalTexts, CancellationToken cancellationToken)
private static bool TryGetApiText(ImmutableArray<AdditionalText> additionalTexts, CancellationToken cancellationToken, out AdditionalText shippedText, out AdditionalText unshippedText)
{
shippedText = null;
unshippedText = null;
var comparer = StringComparer.Ordinal;
foreach (var text in additionalTexts)
{
cancellationToken.ThrowIfCancellationRequested();
if (Path.GetFileName(text.Path).Equals(PublicApiFileName, StringComparison.OrdinalIgnoreCase))
var fileName = Path.GetFileName(text.Path);
if (comparer.Equals(fileName, ShippedFileName))
{
return text;
shippedText = text;
continue;
}
if (comparer.Equals(fileName, UnshippedFileName))
{
unshippedText = text;
continue;
}
}
return shippedText != null && unshippedText != null;
}
private bool ValidateApiFiles(ApiData shippedData, ApiData unshippedData, out List<Diagnostic> list)
{
list = new List<Diagnostic>();
if (shippedData.RemovedApiList.Length > 0)
{
list.Add(Diagnostic.Create(PublicApiFilesInvalid, Location.None, InvalidReasonShippedCantHaveRemoved));
}
return null;
return list.Count == 0;
}
}
}
......@@ -53,7 +53,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
private static TextDocument GetPublicSurfaceAreaDocument(Project project)
{
return project.AdditionalDocuments.FirstOrDefault(doc => doc.Name.Equals(DeclarePublicAPIAnalyzer.PublicApiFileName, StringComparison.OrdinalIgnoreCase));
return project.AdditionalDocuments.FirstOrDefault(doc => doc.Name.Equals(DeclarePublicAPIAnalyzer.UnshippedFileName, StringComparison.Ordinal));
}
private async Task<Solution> GetFix(TextDocument publicSurfaceAreaDocument, string newSymbolName, CancellationToken cancellationToken)
......
......@@ -70,6 +70,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ApiDesign\CancellationTokenMustBeLastAnalyzer.cs" />
<Compile Include="ApiDesign\DeclarePublicAPIAnalyzer.Impl.cs" />
<Compile Include="ApiDesign\DeclarePublicAPIAnalyzer.cs" />
<Compile Include="ApiDesign\DeclarePublicAPIFix.cs" />
<Compile Include="CodeFixProviderBase.cs" />
......
......@@ -27,5 +27,6 @@ internal static class RoslynDiagnosticIds
public const string DeadCodeTriggerRuleId = "RS0021";
public const string ExposedNoninstantiableTypeRuleId = "RS0022";
public const string MissingSharedAttributeRuleId = "RS0023";
public const string PublicApiFilesInvalid = "RS0024";
}
}
......@@ -349,6 +349,15 @@ internal class RoslynDiagnosticsResources {
}
}
/// <summary>
/// Looks up a localized string similar to The contents of the public API files are invalid: {0}.
/// </summary>
internal static string PublicApiFilesInvalid {
get {
return ResourceManager.GetString("PublicApiFilesInvalid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to When removing a public type or member the corresponding entry in PublicAPI.txt should also be removed. This draws attention to API changes in the code reviews and source control history, and helps prevent breaking changes..
/// </summary>
......
......@@ -276,4 +276,7 @@
<data name="ExposedNoninstantiableTypeTitle" xml:space="preserve">
<value>Constructor make noninheritable base class inheritable</value>
</data>
<data name="PublicApiFilesInvalid" xml:space="preserve">
<value>The contents of the public API files are invalid: {0}</value>
</data>
</root>
\ No newline at end of file
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Diagnostics.Analyzers;
using Roslyn.Diagnostics.Analyzers.ApiDesign;
using Roslyn.Diagnostics.Analyzers.CSharp.ApiDesign;
using Xunit;
namespace Microsoft.CodeAnalysis.UnitTests.ApiDesign
{
public class DeclarePublicAPIAnalyzerTests : CodeFixTestBase
{
private sealed class TestAdditionalText : AdditionalText
{
private readonly StringText _text;
public TestAdditionalText(string path, string text)
{
this.Path = path;
_text = new StringText(text, encodingOpt: null);
}
public override string Path { get; }
public override SourceText GetText(CancellationToken cancellationToken = default(CancellationToken)) => _text;
}
protected override CodeFixProvider GetCSharpCodeFixProvider()
{
return null;
}
protected override CodeFixProvider GetBasicCodeFixProvider()
{
return null;
}
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
return null;
}
protected override DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
{
return null;
}
private DeclarePublicAPIAnalyzer CreateAnalyzer(string shippedApiText = "", string unshippedApiText = "")
{
var shippedText = new TestAdditionalText(DeclarePublicAPIAnalyzer.ShippedFileName, shippedApiText);
var unshippedText = new TestAdditionalText(DeclarePublicAPIAnalyzer.UnshippedFileName, unshippedApiText);
var array = ImmutableArray.Create<AdditionalText>(shippedText, unshippedText);
return new DeclarePublicAPIAnalyzer(array);
}
private void VerifyCSharp(string source, string shippedApiText, string unshippedApiText, params DiagnosticResult[] expected)
{
var analyzer = CreateAnalyzer(shippedApiText, unshippedApiText);
Verify(source, LanguageNames.CSharp, analyzer, expected);
}
[Fact]
public void SimpleMissingType()
{
var source = @"
public class C
{
}
";
var shippedText = @"";
var unshippedText = @"";
VerifyCSharp(source, shippedText, unshippedText, GetCSharpResultAt(2, 14, DeclarePublicAPIAnalyzer.DeclareNewApiRule, "C"));
}
[Fact]
public void SimpleMissingMember()
{
var source = @"
public class C
{
public int Field;
public int Property { get; set; }
public void Method() { }
}
";
var shippedText = @"";
var unshippedText = @"";
VerifyCSharp(source, shippedText, unshippedText,
// Test0.cs(2,14): error RS0016: Symbol 'C' is not part of the declared API.
GetCSharpResultAt(2, 14, DeclarePublicAPIAnalyzer.DeclareNewApiRule, "C"),
// Test0.cs(4,16): error RS0016: Symbol 'Field' is not part of the declared API.
GetCSharpResultAt(4, 16, DeclarePublicAPIAnalyzer.DeclareNewApiRule, "Field"),
// Test0.cs(5,27): error RS0016: Symbol 'Property.get' is not part of the declared API.
GetCSharpResultAt(5, 27, DeclarePublicAPIAnalyzer.DeclareNewApiRule, "Property.get"),
// Test0.cs(5,32): error RS0016: Symbol 'Property.set' is not part of the declared API.
GetCSharpResultAt(5, 32, DeclarePublicAPIAnalyzer.DeclareNewApiRule, "Property.set"),
// Test0.cs(6,17): error RS0016: Symbol 'Method' is not part of the declared API.
GetCSharpResultAt(6, 17, DeclarePublicAPIAnalyzer.DeclareNewApiRule, "Method"));
}
[Fact]
public void SimpleMember()
{
var source = @"
public class C
{
public int Field;
public int Property { get; set; }
public void Method() { }
}
";
var shippedText = @"
C
C.Field -> int
C.Property.get -> int
C.Property.set -> void
C.Method() -> void
";
var unshippedText = @"";
VerifyCSharp(source, shippedText, unshippedText);
}
[Fact]
public void SplitBetweenShippedUnshipped()
{
var source = @"
public class C
{
public int Field;
public int Property { get; set; }
public void Method() { }
}
";
var shippedText = @"
C
C.Field -> int
C.Property.get -> int
C.Property.set -> void
";
var unshippedText = @"
C.Method() -> void
";
VerifyCSharp(source, shippedText, unshippedText);
}
[Fact]
public void EnumSplitBetweenFiles()
{
var source = @"
public enum E
{
V1 = 1,
V2 = 2,
V3 = 3,
}
";
var shippedText = @"
E
E.V1 = 1 -> E
E.V2 = 2 -> E
";
var unshippedText = @"
E.V3 = 3 -> E
";
VerifyCSharp(source, shippedText, unshippedText);
}
[Fact]
public void SimpleRemovedMember()
{
var source = @"
public class C
{
public int Field;
public int Property { get; set; }
}
";
var shippedText = @"
C
C.Field -> int
C.Property.get -> int
C.Property.set -> void
";
var unshippedText = $@"
{DeclarePublicAPIAnalyzer.RemovedApiPrefix}C.Method() -> void
";
VerifyCSharp(source, shippedText, unshippedText);
}
[Fact]
public void ApiFileShippedWithRemoved()
{
var source = @"
public class C
{
public int Field;
public int Property { get; set; }
}
";
var shippedText = $@"
C
C.Field -> int
C.Property.get -> int
C.Property.set -> void
{DeclarePublicAPIAnalyzer.RemovedApiPrefix}C.Method() -> void
";
var unshippedText = $@"";
VerifyCSharp(source, shippedText, unshippedText,
// error RS0024: The contents of the public API files are invalid: The shipped API file can't have removed members
GetGlobalResult(DeclarePublicAPIAnalyzer.PublicApiFilesInvalid, DeclarePublicAPIAnalyzer.InvalidReasonShippedCantHaveRemoved));
}
}
}
......@@ -79,11 +79,15 @@
<Reference Include="xunit">
<HintPath>..\..\..\..\packages\xunit.1.9.2\lib\net20\xunit.dll</HintPath>
</Reference>
<Reference Include="System.Collections.Immutable" >
<HintPath>..\..\..\..\packages\System.Collections.Immutable.$(SystemCollectionsImmutableVersion)\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<ItemGroup>
<Compile Include="ApiDesign\DeclarePublicAPIAnalyzerTests.cs" />
<Compile Include="ApiDesign\CancellationTokenMustBeLastTests.cs" />
<Compile Include="Performance\EmptyArrayDiagnosticAnalyzerTests.cs" />
<Compile Include="Performance\EquatableAnalyzerTests.cs" />
......
......@@ -53,7 +53,7 @@ protected static DiagnosticResult GetGlobalResult(DiagnosticDescriptor rule, par
{
Id = rule.Id,
Severity = rule.DefaultSeverity,
Message = rule.MessageFormat.ToString()
Message = string.Format(rule.MessageFormat.ToString(), messageArguments)
};
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册