|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.LocalAnalyzers.Resource.Test;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Test;
public class ApiLifecycleAnalyzerTest
{
[Theory]
[MemberData(nameof(CodeWithMissingApis))]
public async Task Analyzer_Reports_Diagnostics_When_Code_Was_Not_Annotated_Correctly(int expectedDiagnostics, string fileName,
string testAssemblyName, DiagnosticDescriptor descriptor, string source)
{
var options = AnalyzerOptionsFactory.WithFiles(fileName);
var diagnostics = await RoslynTestUtils.RunAnalyzer(
analyzer: new ApiLifecycleAnalyzer(),
references: References,
sources: new[]
{
"[assembly: System.Runtime.Versioning.TargetFramework(\".NETCoreApp,Version=v6.0\")]",
source
},
options: options,
testAssemblyName: testAssemblyName)
;
Assert.Equal(expectedDiagnostics, diagnostics.Count);
for (int i = 0; i < diagnostics.Count; i++)
{
source.AssertDiagnostic(i, descriptor, diagnostics[i]);
}
}
[Theory]
[MemberData(nameof(CodeWithMissingMembers))]
public async Task Analyzer_Reports_Diagnostics_When_StableCode_Was_Not_Found_In_The_Compilation(int expectedDiagnostics, string fileName,
string testAssemblyName, string[] ids, string source)
{
var options = AnalyzerOptionsFactory.WithFiles(fileName);
var diagnostics = await RoslynTestUtils.RunAnalyzer(
analyzer: new ApiLifecycleAnalyzer(),
references: References,
sources: new[]
{
"[assembly: System.Runtime.Versioning.TargetFramework(\".NETCoreApp,Version=v6.0\")]",
source
},
options: options,
testAssemblyName: testAssemblyName)
;
Assert.Equal(expectedDiagnostics, diagnostics.Count);
for (int i = 0; i < diagnostics.Count; i++)
{
var actual = diagnostics[i];
Assert.Contains(actual.Id, ids);
}
}
public static IEnumerable<System.Reflection.Assembly> References => new[]
{
Assembly.GetAssembly(typeof(ObsoleteAttribute))!,
Assembly.GetAssembly(typeof(EditorBrowsableAttribute))!,
Assembly.GetAssembly(typeof(Debugger))!,
Assembly.GetAssembly(typeof(IReadOnlyList<>))!,
Assembly.GetAssembly(typeof(ArgumentOutOfRangeException))!,
Assembly.GetAssembly(typeof(IServiceProvider))!,
Assembly.GetAssembly(typeof(RequiredAttribute))!,
Assembly.GetAssembly(typeof(OptionsBuilder<>))!,
Assembly.GetAssembly(typeof(IConfigurationSection))!,
Assembly.GetAssembly(typeof(HttpRequestMessage))!,
Assembly.GetAssembly(typeof(IDistributedCache))!,
Assembly.GetAssembly(typeof(Microsoft.Extensions.ObjectPool.ObjectPool))!,
Assembly.GetAssembly(typeof(IBufferWriter<>))!,
Assembly.GetAssembly(typeof(ITagCollector))!,
Assembly.GetAssembly(typeof(Microsoft.Extensions.Logging.ILogger))!,
};
public static IEnumerable<object[]> CodeWithMissingMembers => new List<object[]>
{
new object[]
{
0,
"ApiLifecycle/Data/SimpleTaxonomy.json",
"SimpleTaxonomy",
new [] { DiagDescriptors.NewSymbolsMustBeMarkedExperimental.Id },
@"
using System;
namespace Microsoft.Extensions.Compliance.Testing;
[Flags]
public enum SimpleTaxonomy : ulong
{
None = 0,
PublicData = 1 << 0,
PrivateData = 1 << 1,
Unknown = 1 << 2,
}
"
},
new object[]
{
1,
"ApiLifecycle/Data/CompletelyEmpty.json",
"CompletelyEmpty",
new [] { DiagDescriptors.NewSymbolsMustBeMarkedExperimental.Id },
@"
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.Data.Classification;
[Obsolete(""Deleted at 1.18.0. Use AccessControlData<string> instead."", true)]
[EditorBrowsable]
[ExcludeFromCodeCoverage]
public readonly struct AccessControlDataString2
{
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Delegates.json",
"Delegates",
Array.Empty<string>(),
@"
using System.Net.Http;
namespace Microsoft.Extensions.HttpClient.Resilience
{
public delegate string PipelineKeySelector(HttpRequestMessage requestMessage);
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/StringSplit.json",
"StringSplit",
Array.Empty<string>(),
@"
namespace System.Text;
public static partial class StringSplitExtensions
{
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/HttpClientResiliencePolly.json",
"HttpClientResiliencePolly",
Array.Empty<string>(),
@"
using System;
using System.Collections.Generic;
using System.Net.Http;
namespace Microsoft.Extensions.HttpClient.Resilience;
public static class HttpClientResilienceGenerators2
{
public static readonly Func<List<HttpResponseMessage>, TimeSpan> HandleRetryAfterHeader = null!;
}
"
},
new object[]
{
1,
"ApiLifecycle/Data/ICounterT.json",
"ICounterT",
new[] { DiagDescriptors.PublishedSymbolsCantChange.Id },
@"
namespace Microsoft.Extensions.Metrics;
using System.Collections.Generic;
public interface ICounter2<in T>
{
void Add(T value, params string[] dimensionValues);
void Add(T value, IDictionary<string, string> dimensions);
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Protected.json",
"Protected",
Array.Empty<string>(),
@"
namespace Microsoft.Extensions.Security.Identity;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
[Experimental(diagnosticId: ""TBD"", UrlFormat = ""TBD"")]
public class AdditionalContext2
{
protected IReadOnlyDictionary<string, object> Features { get; } = new Dictionary<string, object>();
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/ICounterT.json",
"ICounterT",
Array.Empty<string>(),
@"
namespace Microsoft.Extensions.Metrics;
using System.Collections.Generic;
public interface ICounter2<in T>
where T : struct
{
void Add(T value, params string[] dimensionValues);
void Add(T value, IDictionary<string, string> dimensions);
}
"
},
new object[]
{
1,
"ApiLifecycle/Data/Experimental.json",
"Experimental",
new[] { DiagDescriptors.PublishedSymbolsCantChange.Id },
@"
namespace Microsoft.Extensions.Diagnostics;
public sealed class ExperimentalAttribute2
{
public string? Message { get; }
public ExperimentalAttribute2(string? message = null)
{
if (!string.IsNullOrWhiteSpace(message))
{
Message = message;
}
}
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Histogram.json",
"Histogram",
Array.Empty<string>(),
@"
namespace Microsoft.Extensions.Metrics;
using System.Collections.Generic;
public interface IHistogram2<in T> where T : struct
{
void Record(T value, params string[] dimensionValues);
void Record(T value, IDictionary<string, string> dimensions);
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Time.json",
"Time",
Array.Empty<string>(),
@"
namespace Microsoft.Extensions.Time;
public sealed class SystemClock2
{
public static readonly SystemClock2 Instance = new();
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Indexer.json",
"Indexer",
Array.Empty<string>(),
@"
namespace Microsoft.Extensions.Collections.Frozen;
public readonly struct FrozenOrdinalStringDictionary<TValue>
{
public TValue this[string key] => default!;
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/BufferWriter2.json",
"BufferWriter2",
Array.Empty<string>(),
@"
using System;
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.Buffers;
public sealed class BufferWriter2<T> : IBufferWriter<T>
{
internal const int MaxArrayLength = 0X7FEF_FFFF; // Copy of the internal Array.MaxArrayLength const
private const int DefaultCapacity = 256;
private T[] _buffer = Array.Empty<T>();
[Experimental(diagnosticId: ""TBD"", UrlFormat = ""TBD"")]
public BufferWriter2() { }
public ReadOnlyMemory<T> WrittenMemory => _buffer.AsMemory(0, WrittenCount);
public ReadOnlySpan<T> WrittenSpan => _buffer.AsSpan(0, WrittenCount);
/// <summary>
/// Gets the amount of data written to the underlying buffer so far.
/// </summary>
public int WrittenCount { get; private set; }
public int Capacity
{
get => _buffer.Length;
set
{
_ = value;
}
}
public void Reset()
{
}
public void Advance(int count)
{
}
public Memory<T> GetMemory(int sizeHint = 0)
{
return new Memory<T>(Array.Empty<T>());
}
public Span<T> GetSpan(int sizeHint = 0)
{
return new Span<T>(Array.Empty<T>());
}
private void EnsureCapacity(int sizeHint)
{
}
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/ImplicitOperator.json",
"ImplicitOperator",
Array.Empty<string>(),
@"
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.Data.Classification;
[Obsolete(""Deleted at 1.18.0. Use CustomerContent<string> instead."", true)]
[EditorBrowsable(EditorBrowsableState.Never)]
[ExcludeFromCodeCoverage]
public readonly struct CustomerContentString2
{
internal CustomerContentString2(int c)
{
// Intentionally left empty.
}
[Obsolete(""Deleted at 1.18.0. Use CustomerContent<string> instead."", true)]
[EditorBrowsable(EditorBrowsableState.Never)]
[ExcludeFromCodeCoverage]
public static implicit operator string(CustomerContentString2 value)
{
return string.Empty;
}
[Obsolete(""Deleted at 1.18.0. Use CustomerContent<string> instead."", true)]
[EditorBrowsable(EditorBrowsableState.Never)]
[ExcludeFromCodeCoverage]
public static implicit operator CustomerContentString2(string value)
{
return new(0);
}
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/AsyncState.json",
"AsyncState",
Array.Empty<string>(),
@"
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.Extensions.AsyncState;
[Obsolete(""Deprecated since 1.17.0 and will be removed in 1.20.0. Use IAsyncContext<T> instead."")]
public interface IAsyncContext
{
[SuppressMessage(""Minor Code Smell"", ""S4049:Properties should be preferred"", Justification = ""Not suitable"")]
T? GetAsyncState<T>()
where T : notnull;
void SetAsyncState<T>(T? instance) where T : notnull;
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Data.Classification2.json",
"Data.Classification2",
Array.Empty<string>(),
@"
namespace Microsoft.Extensions.Data.Classification2;
using System.Diagnostics.CodeAnalysis;
public interface IClassifiedData
{
public DataClass DataClass { get; }
}
[Experimental(diagnosticId: ""TBD"", UrlFormat = ""TBD"")]
public enum DataClass
{
}
"
},
new object[]
{
1,
"ApiLifecycle/Data/CompletelyEmpty.json",
"Something",
new[] { DiagDescriptors.NewSymbolsMustBeMarkedExperimental.Id },
@"
namespace Example;
/// <summary>
/// Some text for test.
/// </summary>
public static class TestClass
{
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Pools.json",
"Pools",
Array.Empty<string>(),
@"
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.Extensions.Pools;
public sealed class ScaledObjectPool<T> : ObjectPool<T>
where T : class
{
public override T Get()
{
return default!;
}
public override void Return(T obj)
{
// Intentionally left empty.
}
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Data.Classification.json",
"Microsoft.Extensions.Data.Classification",
Array.Empty<string>(),
@"
namespace Microsoft.Extensions.Data.Classification;
public enum DataClass
{
Unknown,
AccessControlData,
CustomerContent,
EUII,
SupportData,
FeedbackData,
AccountData,
PublicPersonalData,
EUPI,
OII,
SystemMetadata,
PublicNonPersonalData,
}
"
},
new object[]
{
2,
"ApiLifecycle/Data/SomePackage.json",
"SomePackage",
new[] { DiagDescriptors.PublishedSymbolsCantBeMarkedExperimental.Id },
@"
namespace SomePackage;
using System.Diagnostics.CodeAnalysis;
[Experimental(diagnosticId: ""TBD"", UrlFormat = ""TBD"")]
public static class Test
{
[Experimental(diagnosticId: ""TBD"", UrlFormat = ""TBD"")]
public static void Load()
{
// Intentionally left empty.
}
}
"
},
new object[]
{
1,
"ApiLifecycle/Data/Caching.Abstractions.json",
"Microsoft.Extensions.Caching.Abstractions",
new[] { DiagDescriptors.PublishedSymbolsCantBeDeleted.Id },
@"
using System;
using Microsoft.Extensions.Caching.Distributed;
namespace Microsoft.Extensions.Caching;
public interface ICachePipelineBuilder
{
ICachePipelineBuilder SetCache(Func<IDistributedCache> createCache);
ICachePipelineBuilder SetCache(IDistributedCache cache);
ICachePipelineBuilder SetCache(Func<IServiceProvider, IDistributedCache> createCache, IServiceProvider serviceProvider);
ICachePipelineBuilder AddCacheWrapper(Func<IDistributedCache, IDistributedCache> createCacheWrapper);
ICachePipelineBuilder AddCacheWrapper(Func<IServiceProvider, IDistributedCache, IDistributedCache> createCacheWrapper, IServiceProvider serviceProvider);
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Metrics.Abstractions.json",
"Microsoft.Extensions.Metrics.Abstractions",
new[] { DiagDescriptors.PublishedSymbolsCantBeDeleted.Id },
@"
using System;
using System.Diagnostics;
namespace Microsoft.Extensions.Metrics;
[AttributeUsage(AttributeTargets.Method)]
[Conditional(""CODE_GENERATION_ATTRIBUTES"")]
public sealed class CounterAttribute : Attribute
{
public CounterAttribute(params string[] dimensions)
{
Dimensions = dimensions;
}
public CounterAttribute(Type type)
{
Type = type;
}
public string? Name { get; set; }
public string[]? Dimensions { get; }
public Type? Type { get; }
}
"
},
new object[]
{
2,
"ApiLifecycle/Data/Essentials.json",
"Microsoft.Extensions.Essentials",
new[] { DiagDescriptors.PublishedSymbolsCantBeDeleted.Id },
@"
using System;
using System.Collections;
using System.Collections.Generic;
namespace Microsoft.Extensions.Collections;
public static class Empty
{
public static IReadOnlyCollection<T> ReadOnlyCollection<T>() => EmptyReadOnlyList<T>.Instance;
public static IEnumerable<T> Enumerable<T>() => EmptyReadOnlyList<T>.Instance;
public static IReadOnlyList<T> ReadOnlyList<T>() => EmptyReadOnlyList<T>.Instance;
}
internal sealed class EmptyReadOnlyList<T> : IReadOnlyList<T>
{
public static readonly EmptyReadOnlyList<T> Instance = new();
private readonly Enumerator _enumerator = new();
internal sealed class Enumerator : IEnumerator<T>
{
public void Dispose()
{
// nop
}
public void Reset()
{
// nop
}
public bool MoveNext() => false;
public T Current => throw new InvalidOperationException();
object IEnumerator.Current => throw new InvalidOperationException();
}
public IEnumerator<T> GetEnumerator() => _enumerator;
IEnumerator IEnumerable.GetEnumerator() => _enumerator;
public int Count => 0;
public T this[int index] => throw new ArgumentOutOfRangeException(nameof(index));
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/ExperimentalDeletedNoErorr.json",
"Microsoft.Extensions.ExperimentalNoErrors",
Array.Empty<string>(),
""
},
new object[]
{
0,
"ApiLifecycle/Data/WindowsCountersOptions.json",
"WindowsCountersOptions",
Array.Empty<string>(),
@"
namespace Microsoft.Extensions.Diagnostics;
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
[Experimental(diagnosticId: ""TBD"", UrlFormat = ""TBD"")]
public class WindowsCountersOptions2
{
[Required]
public ISet<string> InstanceIpAddresses { get; set; } = new HashSet<string>();
public TimeSpan CachingInterval { get; set; } = TimeSpan.FromSeconds(2);
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Analyzers.json",
"Microsoft.Extensions.Analyzers",
Array.Empty<string>(),
@"
namespace Test;
using System.Diagnostics.CodeAnalysis;
[Experimental(diagnosticId: ""TBD"", UrlFormat = ""TBD"")]
public sealed class BufferWriter<T>
{
internal const int MaxArrayLength = 0X7FEF_FFFF; // Copy of the internal Array.MaxArrayLength const
private const int DefaultCapacity = 256;
private T[] _buffer = System.Array.Empty<T>();
public int WrittenMemory => 5;
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Empty.json",
"Empty",
Array.Empty<string>(),
@"
namespace Inheritance;
using System.Diagnostics.CodeAnalysis;
[Experimental(diagnosticId: ""TBD"", UrlFormat = ""TBD"")]
public class BaseType
{
public virtual int P => 1;
}
public class InheritedType : BaseType
{
public int ReadValue(string s) => P;
}
"
},
new object[]
{
0,
"ApiLifecycle/Data/Empty.json",
"Empty",
Array.Empty<string>(),
@"
namespace Nested;
using System.Diagnostics.CodeAnalysis;
[Experimental(diagnosticId: ""TBD"", UrlFormat = ""TBD"")]
public class OuterType
{
public int ReadValue(string s) => new InnerType().P;
public class InnerType
{
public int P => 2;
}
}
"
}
};
public static IEnumerable<object[]> CodeWithMissingApis => new List<object[]>
{
new object[]
{
0,
"ApiLifecycle/Data/Microsoft.Extensions.TimeProvider.Testing.json",
"Microsoft.Extensions.TimeProvider.Testing",
DiagDescriptors.NewSymbolsMustBeMarkedExperimental,
@"
using System;
using System.Threading;
namespace Microsoft.Extensions.Time.Testing;
public class FakeTimeProvider : TimeProvider
{
public FakeTimeProvider() { }
public FakeTimeProvider(DateTimeOffset startDateTime) { }
public DateTimeOffset Start { get; }
public TimeSpan AutoAdvanceAmount { get; set; }
public override DateTimeOffset GetUtcNow() => default;
public void SetUtcNow(DateTimeOffset value) { }
public void Advance(TimeSpan delta) { }
public override long GetTimestamp() => 0;
public override TimeZoneInfo LocalTimeZone => null!;
public void SetLocalTimeZone(TimeZoneInfo localTimeZone) { }
public override long TimestampFrequency => 0;
public override string ToString() => string.Empty;
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) => null!;
}
"
},
new object[]
{
1,
"ApiLifecycle/Data/Analyzers.json",
"Microsoft.Extensions.Analyzers",
DiagDescriptors.NewSymbolsMustBeMarkedExperimental,
@"
namespace Test;
public sealed class /*0+*/BufferWriter/*-0*/
{
public BufferWriter()
{
}
public int WrittenMemory => 5;
}
"
},
new object[]
{
2,
"ApiLifecycle/Data/Analyzers.json",
"Microsoft.Extensions.Analyzers",
DiagDescriptors.NewSymbolsMustBeMarkedExperimental,
@"
namespace Microsoft.Extensions.Diagnostics;
using System.ComponentModel;
public static class /*0+*/DebuggerState/*-0*/
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static IDebuggerState System => SystemDebugger.Instance;
[EditorBrowsable(EditorBrowsableState.Always)]
public static IDebuggerState Attached => AttachedDebugger.Instance;
public static IDebuggerState Detached => DetachedDebugger.Instance;
}
internal sealed class AttachedDebugger : IDebuggerState
{
private AttachedDebugger()
{
// Intentionally left empty.
}
public static AttachedDebugger Instance { get; } = new();
public bool IsAttached => true;
}
internal sealed class DetachedDebugger : IDebuggerState
{
private DetachedDebugger()
{
// Intentionally left empty.
}
public static DetachedDebugger Instance { get; } = new();
public bool IsAttached => false;
}
internal sealed class SystemDebugger : IDebuggerState
{
private SystemDebugger()
{
// Intentionally left empty.
}
public static SystemDebugger Instance { get; } = new();
public bool IsAttached => System.Diagnostics.Debugger.IsAttached;
}
public interface /*1+*/IDebuggerState/*-1*/
{
bool IsAttached { get; }
}
"
},
new object[]
{
1,
"ApiLifecycle/Data/Analyzers.json",
"Microsoft.Extensions.Analyzers",
DiagDescriptors.NewSymbolsMustBeMarkedExperimental,
@"
namespace Test
{
public static class /*0+*/Test/*-0*/
{
public static string Something()
{
return ""Hello"";
}
}
}
"
}
};
}
|