Merge branch 'master' of ..\Extensions\ into johluo/remove-extensions-deps

This commit is contained in:
John Luo 2020-04-03 11:40:13 -07:00
commit 52d0f31c3f
14 changed files with 1300 additions and 0 deletions

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core internal use analyzers.</Description>
<TargetFramework>netstandard1.3</TargetFramework>
<PackageTags>$(PackageTags);analyzers</PackageTags>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<BuildOutputTargetFolder>analyzers/dotnet/cs/</BuildOutputTargetFolder>
<!--
Except for analyzer unit tests, analyzers should not be referenced by other projects in this repo. Analyzers cannot be used as
a project reference. The SDK currently only supports using analyzers as a PackageReference. This flag prevents this project from
being used as a `<Reference>`, which indicates that a reference is interchangeable between ProjectRef and PackageRef.
-->
<IsProjectReferenceProvider>false</IsProjectReferenceProvider>
<UseLatestPackageReferences>true</UseLatestPackageReferences>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,279 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Internal.AspNetCore.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class PubternalityAnalyzer : DiagnosticAnalyzer
{
public PubternalityAnalyzer()
{
SupportedDiagnostics = ImmutableArray.Create(new[]
{
PubturnalityDescriptors.PUB0001,
PubturnalityDescriptors.PUB0002
});
}
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(analysisContext =>
{
analysisContext.RegisterSymbolAction(symbolAnalysisContext => AnalyzeTypeUsage(symbolAnalysisContext), SymbolKind.Namespace);
analysisContext.RegisterSyntaxNodeAction(syntaxContext => AnalyzeTypeUsage(syntaxContext), SyntaxKind.IdentifierName);
});
}
private void AnalyzeTypeUsage(SymbolAnalysisContext context)
{
var ns = (INamespaceSymbol)context.Symbol;
if (IsInternal(ns))
{
return;
}
foreach (var namespaceOrTypeSymbol in ns.GetMembers())
{
if (namespaceOrTypeSymbol.IsType)
{
CheckType((ITypeSymbol)namespaceOrTypeSymbol, context);
}
}
}
private void CheckType(ITypeSymbol typeSymbol, SymbolAnalysisContext context)
{
if (IsPrivate(typeSymbol) || IsPrivate(typeSymbol.ContainingType))
{
return;
}
if (typeSymbol.BaseType != null)
{
CheckType(context, typeSymbol.BaseType, typeSymbol.DeclaringSyntaxReferences);
}
foreach (var member in typeSymbol.GetMembers())
{
CheckMember(context, member);
}
foreach (var innerType in typeSymbol.GetTypeMembers())
{
CheckType(innerType, context);
}
if (typeSymbol is INamedTypeSymbol namedTypeSymbol)
{
// Check delegate signatures
if (namedTypeSymbol.DelegateInvokeMethod != null)
{
CheckMethod(context, namedTypeSymbol.DelegateInvokeMethod);
}
}
}
private void CheckMember(SymbolAnalysisContext context, ISymbol symbol)
{
if (IsPrivate(symbol))
{
return;
}
switch (symbol)
{
case IFieldSymbol fieldSymbol:
{
CheckType(context, fieldSymbol.Type, fieldSymbol.DeclaringSyntaxReferences);
break;
}
case IPropertySymbol propertySymbol:
{
CheckType(context, propertySymbol.Type, propertySymbol.DeclaringSyntaxReferences);
break;
}
case IMethodSymbol methodSymbol:
{
// Skip compiler generated members that we already explicitly check
switch (methodSymbol.MethodKind)
{
case MethodKind.EventAdd:
case MethodKind.EventRaise:
case MethodKind.EventRemove:
case MethodKind.PropertyGet:
case MethodKind.PropertySet:
case MethodKind.DelegateInvoke:
case MethodKind.Ordinary when methodSymbol.ContainingType.TypeKind == TypeKind.Delegate:
return;
}
CheckMethod(context, methodSymbol);
break;
}
case IEventSymbol eventSymbol:
CheckType(context, eventSymbol.Type, eventSymbol.DeclaringSyntaxReferences);
break;
}
}
private void CheckMethod(SymbolAnalysisContext context, IMethodSymbol methodSymbol)
{
if (IsPrivate(methodSymbol))
{
return;
}
foreach (var parameter in methodSymbol.Parameters)
{
CheckType(context, parameter.Type, parameter.DeclaringSyntaxReferences);
}
CheckType(context, methodSymbol.ReturnType, methodSymbol.DeclaringSyntaxReferences);
}
private static bool IsPrivate(ISymbol symbol)
{
return symbol != null &&
(symbol.DeclaredAccessibility == Accessibility.Private ||
symbol.DeclaredAccessibility == Accessibility.Internal ||
IsInternal(symbol.ContainingNamespace));
}
private void CheckAttributes(SymbolAnalysisContext context, ImmutableArray<AttributeData> attributes)
{
foreach (var attributeData in attributes)
{
CheckType(context, attributeData.AttributeClass, attributeData.ApplicationSyntaxReference);
}
}
private void CheckType(SymbolAnalysisContext context, ITypeSymbol symbol, SyntaxReference syntax)
{
var pubternalType = GetPubternalType(symbol);
if (pubternalType != null)
{
ReportPUB0001(context, pubternalType, syntax);
}
}
private void CheckType(SymbolAnalysisContext context, ITypeSymbol symbol, ImmutableArray<SyntaxReference> syntaxReferences)
{
var pubternalType = GetPubternalType(symbol);
if (pubternalType != null)
{
foreach (var syntaxReference in syntaxReferences)
{
ReportPUB0001(context, pubternalType, syntaxReference);
}
}
}
private static void ReportPUB0001(SymbolAnalysisContext context, ITypeSymbol pubternalType, SyntaxReference syntax)
{
var syntaxNode = syntax.GetSyntax();
var location = syntaxNode.GetLocation();
if (syntaxNode is BaseTypeDeclarationSyntax baseTypeDeclarationSyntax)
{
location = baseTypeDeclarationSyntax.Identifier.GetLocation();
}
if (syntaxNode is DelegateDeclarationSyntax delegateDeclarationSyntax)
{
location = delegateDeclarationSyntax.ReturnType.GetLocation();
}
if (syntaxNode is BasePropertyDeclarationSyntax propertyDeclaration)
{
location = propertyDeclaration.Type.GetLocation();
}
if (syntaxNode is MethodDeclarationSyntax method)
{
location = method.ReturnType.GetLocation();
}
if (syntaxNode is VariableDeclaratorSyntax variableDeclarator)
{
if (variableDeclarator.Parent is VariableDeclarationSyntax fieldDeclaration)
{
location = fieldDeclaration.Type.GetLocation();
}
}
context.ReportDiagnostic(Diagnostic.Create(PubturnalityDescriptors.PUB0001, location, pubternalType.ToDisplayString()));
}
private ITypeSymbol GetPubternalType(ITypeSymbol symbol)
{
if (IsInternal(symbol.ContainingNamespace))
{
return symbol;
}
else
{
if (symbol is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType)
{
foreach (var argument in namedTypeSymbol.TypeArguments)
{
var argumentSymbol = GetPubternalType(argument);
if (argumentSymbol != null)
{
return argumentSymbol;
}
}
}
}
return null;
}
private void AnalyzeTypeUsage(SyntaxNodeAnalysisContext syntaxContext)
{
var identifier = (IdentifierNameSyntax)syntaxContext.Node;
var symbolInfo = ModelExtensions.GetTypeInfo(syntaxContext.SemanticModel, identifier, syntaxContext.CancellationToken);
if (symbolInfo.Type == null)
{
return;
}
var type = symbolInfo.Type;
if (!IsInternal(type.ContainingNamespace))
{
// don't care about non-pubternal type references
return;
}
if (!syntaxContext.ContainingSymbol.ContainingAssembly.Equals(type.ContainingAssembly))
{
syntaxContext.ReportDiagnostic(Diagnostic.Create(PubturnalityDescriptors.PUB0002, identifier.GetLocation(), type.ToDisplayString()));
}
}
private static bool IsInternal(INamespaceSymbol ns)
{
while (ns != null)
{
if (ns.Name == "Internal")
{
return true;
}
ns = ns.ContainingNamespace;
}
return false;
}
}
}

View File

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CodeAnalysis;
namespace Internal.AspNetCore.Analyzers
{
internal class PubturnalityDescriptors
{
public static DiagnosticDescriptor PUB0001 = new DiagnosticDescriptor(
"PUB0001",
"Pubternal type in public API",
"Pubternal type ('{0}') usage in public API",
"Usage",
DiagnosticSeverity.Warning, true);
public static DiagnosticDescriptor PUB0002 = new DiagnosticDescriptor(
"PUB0002",
"Cross assembly pubternal reference",
"Cross assembly pubternal type ('{0}') reference",
"Usage",
DiagnosticSeverity.Error, false);
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(DefaultNetCoreTargetFramework);net472</TargetFrameworks>
<PreserveCompilationContext>true</PreserveCompilationContext>
<!-- Analyzers tests can't find netfx ref assemblies from a package -->
<UsingToolNetFrameworkReferenceAssemblies Condition="'$(OS)' == 'Windows_NT'">false</UsingToolNetFrameworkReferenceAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\src\Internal.AspNetCore.Analyzers.csproj" />
<ProjectReference Include="..\..\Microsoft.AspNetCore.Analyzer.Testing\src\Microsoft.AspNetCore.Analyzer.Testing.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,260 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.AspNetCore.Analyzer.Testing;
using Xunit;
using Xunit.Abstractions;
namespace Internal.AspNetCore.Analyzers.Tests
{
public class PubternabilityAnalyzerTests : DiagnosticVerifier
{
private const string InternalDefinitions = @"
namespace A.Internal.Namespace
{
public class C {}
public delegate C CD ();
public class CAAttribute: System.Attribute {}
public class Program
{
public static void Main() {}
}
}";
public PubternabilityAnalyzerTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
{
}
[Theory]
[MemberData(nameof(PublicMemberDefinitions))]
public async Task PublicExposureOfPubternalTypeProducesPUB0001(string member)
{
var code = GetSourceFromNamespaceDeclaration($@"
namespace A
{{
public class T
{{
{member}
}}
}}");
var diagnostic = Assert.Single(await GetDiagnostics(code.Source));
Assert.Equal("PUB0001", diagnostic.Id);
AnalyzerAssert.DiagnosticLocation(code.DefaultMarkerLocation, diagnostic.Location);
}
[Theory]
[MemberData(nameof(PublicMemberWithAllowedDefinitions))]
public async Task PublicExposureOfPubternalMembersSometimesAllowed(string member)
{
var code = GetSourceFromNamespaceDeclaration($@"
namespace A
{{
public class T
{{
{member}
}}
}}");
Assert.Empty(await GetDiagnostics(code.Source));
}
[Theory]
[MemberData(nameof(PublicTypeDefinitions))]
public async Task PublicExposureOfPubternalTypeProducesInTypeDefinitionPUB0001(string member)
{
var code = GetSourceFromNamespaceDeclaration($@"
namespace A
{{
{member}
}}");
var diagnostic = Assert.Single(await GetDiagnostics(code.Source));
Assert.Equal("PUB0001", diagnostic.Id);
AnalyzerAssert.DiagnosticLocation(code.DefaultMarkerLocation, diagnostic.Location);
}
[Theory]
[MemberData(nameof(PublicMemberDefinitions))]
public async Task PrivateUsageOfPubternalTypeDoesNotProduce(string member)
{
var code = GetSourceFromNamespaceDeclaration($@"
namespace A
{{
internal class T
{{
{member}
}}
}}");
var diagnostics = await GetDiagnostics(code.Source);
Assert.Empty(diagnostics);
}
[Theory]
[MemberData(nameof(PrivateMemberDefinitions))]
public async Task PrivateUsageOfPubternalTypeDoesNotProduceInPublicClasses(string member)
{
var code = GetSourceFromNamespaceDeclaration($@"
namespace A
{{
public class T
{{
{member}
}}
}}");
var diagnostics = await GetDiagnostics(code.Source);
Assert.Empty(diagnostics);
}
[Theory]
[MemberData(nameof(PublicTypeWithAllowedDefinitions))]
public async Task PublicExposureOfPubternalTypeSometimesAllowed(string member)
{
var code = GetSourceFromNamespaceDeclaration($@"
namespace A
{{
{member}
}}");
var diagnostics = await GetDiagnostics(code.Source);
Assert.Empty(diagnostics);
}
[Theory]
[MemberData(nameof(PrivateMemberDefinitions))]
[MemberData(nameof(PublicMemberDefinitions))]
public async Task DefinitionOfPubternalCrossAssemblyProducesPUB0002(string member)
{
var code = TestSource.Read($@"
using A.Internal.Namespace;
namespace A
{{
internal class T
{{
{member}
}}
}}");
var diagnostic = Assert.Single(await GetDiagnosticWithProjectReference(code.Source));
Assert.Equal("PUB0002", diagnostic.Id);
AnalyzerAssert.DiagnosticLocation(code.DefaultMarkerLocation, diagnostic.Location);
}
[Theory]
[MemberData(nameof(TypeUsages))]
public async Task UsageOfPubternalCrossAssemblyProducesPUB0002(string usage)
{
var code = TestSource.Read($@"
using A.Internal.Namespace;
namespace A
{{
public class T
{{
private void M()
{{
{usage}
}}
}}
}}");
var diagnostic = Assert.Single(await GetDiagnosticWithProjectReference(code.Source));
Assert.Equal("PUB0002", diagnostic.Id);
AnalyzerAssert.DiagnosticLocation(code.DefaultMarkerLocation, diagnostic.Location);
}
public static IEnumerable<object[]> PublicMemberDefinitions =>
ApplyModifiers(MemberDefinitions, "public", "protected");
public static IEnumerable<object[]> PublicMemberWithAllowedDefinitions =>
ApplyModifiers(AllowedMemberDefinitions, "public");
public static IEnumerable<object[]> PublicTypeDefinitions =>
ApplyModifiers(TypeDefinitions, "public");
public static IEnumerable<object[]> PublicTypeWithAllowedDefinitions =>
ApplyModifiers(AllowedDefinitions, "public");
public static IEnumerable<object[]> PrivateMemberDefinitions =>
ApplyModifiers(MemberDefinitions, "private", "internal");
public static IEnumerable<object[]> TypeUsages =>
ApplyModifiers(TypeUsageStrings, string.Empty);
public static string[] MemberDefinitions => new []
{
"/*MM*/C c;",
"T(/*MM*/C c) {}",
"/*MM*/CD c { get; }",
"event /*MM*/CD c;",
"delegate /*MM*/C WOW();"
};
public static string[] TypeDefinitions => new []
{
"delegate /*MM*/C WOW();",
"class /*MM*/T: P<C> { } public class P<T> {}",
"class /*MM*/T: C {}",
"class T { public class /*MM*/T1: C { } }"
};
public static string[] AllowedMemberDefinitions => new []
{
"T([CA]int c) {}",
"[CA] MOD int f;",
"[CA] MOD int f { get; set; }",
"[CA] MOD class CC { }"
};
public static string[] AllowedDefinitions => new []
{
"class T: I<C> { } interface I<T> {}"
};
public static string[] TypeUsageStrings => new []
{
"/*MM*/var c = new C();",
"/*MM*/CD d = () => null;",
"var t = typeof(/*MM*/CAAttribute);"
};
private static IEnumerable<object[]> ApplyModifiers(string[] code, params string[] mods)
{
foreach (var mod in mods)
{
foreach (var s in code)
{
if (s.Contains("MOD"))
{
yield return new object[] { s.Replace("MOD", mod) };
}
else
{
yield return new object[] { mod + " " + s };
}
}
}
}
private TestSource GetSourceFromNamespaceDeclaration(string namespaceDefinition)
{
return TestSource.Read("using A.Internal.Namespace;" + InternalDefinitions + namespaceDefinition);
}
private Task<Diagnostic[]> GetDiagnosticWithProjectReference(string code)
{
var libraray = CreateProject(InternalDefinitions);
var mainProject = CreateProject(code).AddProjectReference(new ProjectReference(libraray.Id));
return GetDiagnosticsAsync(mainProject.Documents.ToArray(), new PubternalityAnalyzer(), new [] { "PUB0002" });
}
private Task<Diagnostic[]> GetDiagnostics(string code)
{
return GetDiagnosticsAsync(new[] { code }, new PubternalityAnalyzer(), new [] { "PUB0002" });
}
}
}

View File

@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CodeAnalysis;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Analyzer.Testing
{
public class AnalyzerAssert
{
public static void DiagnosticLocation(DiagnosticLocation expected, Location actual)
{
var actualSpan = actual.GetLineSpan();
var actualLinePosition = actualSpan.StartLinePosition;
// Only check line position if there is an actual line in the real diagnostic
if (actualLinePosition.Line > 0)
{
if (actualLinePosition.Line + 1 != expected.Line)
{
throw new DiagnosticLocationAssertException(
expected,
actual,
$"Expected diagnostic to be on line \"{expected.Line}\" was actually on line \"{actualLinePosition.Line + 1}\"");
}
}
// Only check column position if there is an actual column position in the real diagnostic
if (actualLinePosition.Character > 0)
{
if (actualLinePosition.Character + 1 != expected.Column)
{
throw new DiagnosticLocationAssertException(
expected,
actual,
$"Expected diagnostic to start at column \"{expected.Column}\" was actually on column \"{actualLinePosition.Character + 1}\"");
}
}
}
private class DiagnosticLocationAssertException : EqualException
{
public DiagnosticLocationAssertException(
DiagnosticLocation expected,
Location actual,
string message)
: base(expected, actual)
{
Message = message;
}
public override string Message { get; }
}
}
}

View File

@ -0,0 +1,72 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
namespace Microsoft.AspNetCore.Analyzer.Testing
{
public class CodeFixRunner
{
public static CodeFixRunner Default { get; } = new CodeFixRunner();
public async Task<string> ApplyCodeFixAsync(
CodeFixProvider codeFixProvider,
Document document,
Diagnostic analyzerDiagnostic,
int codeFixIndex = 0)
{
var actions = new List<CodeAction>();
var context = new CodeFixContext(document, analyzerDiagnostic, (a, d) => actions.Add(a), CancellationToken.None);
await codeFixProvider.RegisterCodeFixesAsync(context);
Assert.NotEmpty(actions);
var updatedSolution = await ApplyFixAsync(actions[codeFixIndex]);
var updatedProject = updatedSolution.GetProject(document.Project.Id);
await EnsureCompilable(updatedProject);
var updatedDocument = updatedSolution.GetDocument(document.Id);
var sourceText = await updatedDocument.GetTextAsync();
return sourceText.ToString();
}
private async Task EnsureCompilable(Project project)
{
var compilationOptions = ConfigureCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var compilation = await project
.WithCompilationOptions(compilationOptions)
.GetCompilationAsync();
var diagnostics = compilation.GetDiagnostics();
if (diagnostics.Length != 0)
{
var message = string.Join(
Environment.NewLine,
diagnostics.Select(d => CSharpDiagnosticFormatter.Instance.Format(d)));
throw new InvalidOperationException($"Compilation failed:{Environment.NewLine}{message}");
}
}
private static async Task<Solution> ApplyFixAsync(CodeAction codeAction)
{
var operations = await codeAction.GetOperationsAsync(CancellationToken.None);
return Assert.Single(operations.OfType<ApplyChangesOperation>()).ChangedSolution;
}
protected virtual CompilationOptions ConfigureCompilationOptions(CompilationOptions options)
{
return options.WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
}
}
}

View File

@ -0,0 +1,122 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace Microsoft.AspNetCore.Analyzer.Testing
{
/// <summary>
/// Base type for executing a <see cref="DiagnosticAnalyzer" />. Derived types implemented in the test assembly will
/// correctly resolve reference assemblies required for compilaiton.
/// </summary>
public abstract class DiagnosticAnalyzerRunner
{
/// <summary>
/// Given classes in the form of strings, and an DiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document.
/// </summary>
/// <param name="sources">Classes in the form of strings</param>
/// <param name="analyzer">The analyzer to be run on the sources</param>
/// <param name="additionalEnabledDiagnostics">Additional diagnostics to enable at Info level</param>
/// <param name="getAllDiagnostics">
/// When <c>true</c>, returns all diagnostics including compilation errors.
/// Otherwise; only returns analyzer diagnostics.
/// </param>
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
protected Task<Diagnostic[]> GetDiagnosticsAsync(
string[] sources,
DiagnosticAnalyzer analyzer,
string[] additionalEnabledDiagnostics,
bool getAllDiagnostics = true)
{
var project = DiagnosticProject.Create(GetType().Assembly, sources);
return GetDiagnosticsAsync(new[] { project }, analyzer, additionalEnabledDiagnostics);
}
/// <summary>
/// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it.
/// The returned diagnostics are then ordered by location in the source document.
/// </summary>
/// <param name="projects">Projects that the analyzer will be run on</param>
/// <param name="analyzer">The analyzer to run on the documents</param>
/// <param name="additionalEnabledDiagnostics">Additional diagnostics to enable at Info level</param>
/// <param name="getAllDiagnostics">
/// When <c>true</c>, returns all diagnostics including compilation errors.
/// Otherwise only returns analyzer diagnostics.
/// </param>
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
protected async Task<Diagnostic[]> GetDiagnosticsAsync(
IEnumerable<Project> projects,
DiagnosticAnalyzer analyzer,
string[] additionalEnabledDiagnostics,
bool getAllDiagnostics = true)
{
var diagnostics = new List<Diagnostic>();
foreach (var project in projects)
{
var compilation = await project.GetCompilationAsync();
// Enable any additional diagnostics
var options = ConfigureCompilationOptions(compilation.Options);
if (additionalEnabledDiagnostics.Length > 0)
{
options = compilation.Options
.WithSpecificDiagnosticOptions(
additionalEnabledDiagnostics.ToDictionary(s => s, s => ReportDiagnostic.Info));
}
var compilationWithAnalyzers = compilation
.WithOptions(options)
.WithAnalyzers(ImmutableArray.Create(analyzer));
if (getAllDiagnostics)
{
var diags = await compilationWithAnalyzers.GetAllDiagnosticsAsync();
Assert.DoesNotContain(diags, d => d.Id == "AD0001");
// Filter out non-error diagnostics not produced by our analyzer
// We want to KEEP errors because we might have written bad code. But sometimes we leave warnings in to make the
// test code more convenient
diags = diags.Where(d => d.Severity == DiagnosticSeverity.Error || analyzer.SupportedDiagnostics.Any(s => s.Id.Equals(d.Id))).ToImmutableArray();
foreach (var diag in diags)
{
if (diag.Location == Location.None || diag.Location.IsInMetadata)
{
diagnostics.Add(diag);
}
else
{
foreach (var document in projects.SelectMany(p => p.Documents))
{
var tree = await document.GetSyntaxTreeAsync();
if (tree == diag.Location.SourceTree)
{
diagnostics.Add(diag);
}
}
}
}
}
else
{
diagnostics.AddRange(await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync());
}
}
return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
}
protected virtual CompilationOptions ConfigureCompilationOptions(CompilationOptions options)
{
return options.WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
}
}
}

View File

@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.AspNetCore.Analyzer.Testing
{
/// <summary>
/// Location where the diagnostic appears, as determined by path, line number, and column number.
/// </summary>
public class DiagnosticLocation
{
public DiagnosticLocation(int line, int column)
: this($"{DiagnosticProject.DefaultFilePathPrefix}.cs", line, column)
{
}
public DiagnosticLocation(string path, int line, int column)
{
if (line < -1)
{
throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1");
}
if (column < -1)
{
throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1");
}
Path = path;
Line = line;
Column = column;
}
public string Path { get; }
public int Line { get; }
public int Column { get; }
}
}

View File

@ -0,0 +1,99 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.DependencyModel.Resolution;
namespace Microsoft.AspNetCore.Analyzer.Testing
{
public class DiagnosticProject
{
/// <summary>
/// File name prefix used to generate Documents instances from source.
/// </summary>
public static string DefaultFilePathPrefix = "Test";
/// <summary>
/// Project name.
/// </summary>
public static string TestProjectName = "TestProject";
private static readonly Dictionary<Assembly, Solution> _solutionCache = new Dictionary<Assembly, Solution>();
public static Project Create(Assembly testAssembly, string[] sources)
{
Solution solution;
lock (_solutionCache)
{
if (!_solutionCache.TryGetValue(testAssembly, out solution))
{
var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
solution = new AdhocWorkspace()
.CurrentSolution
.AddProject(projectId, TestProjectName, TestProjectName, LanguageNames.CSharp);
foreach (var defaultCompileLibrary in DependencyContext.Load(testAssembly).CompileLibraries)
{
foreach (var resolveReferencePath in defaultCompileLibrary.ResolveReferencePaths(new AppLocalResolver()))
{
solution = solution.AddMetadataReference(projectId, MetadataReference.CreateFromFile(resolveReferencePath));
}
}
_solutionCache.Add(testAssembly, solution);
}
}
var testProject = solution.ProjectIds.Single();
var fileNamePrefix = DefaultFilePathPrefix;
for (var i = 0; i < sources.Length; i++)
{
var newFileName = fileNamePrefix;
if (sources.Length > 1)
{
newFileName += i;
}
newFileName += ".cs";
var documentId = DocumentId.CreateNewId(testProject, debugName: newFileName);
solution = solution.AddDocument(documentId, newFileName, SourceText.From(sources[i]));
}
return solution.GetProject(testProject);
}
// Required to resolve compilation assemblies inside unit tests
private class AppLocalResolver : ICompilationAssemblyResolver
{
public bool TryResolveAssemblyPaths(CompilationLibrary library, List<string> assemblies)
{
foreach (var assembly in library.Assemblies)
{
var dll = Path.Combine(Directory.GetCurrentDirectory(), "refs", Path.GetFileName(assembly));
if (File.Exists(dll))
{
assemblies.Add(dll);
return true;
}
dll = Path.Combine(Directory.GetCurrentDirectory(), Path.GetFileName(assembly));
if (File.Exists(dll))
{
assemblies.Add(dll);
return true;
}
}
return false;
}
}
}
}

View File

@ -0,0 +1,213 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.DependencyModel.Resolution;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Analyzer.Testing
{
/// <summary>
/// Superclass of all Unit Tests for DiagnosticAnalyzers
/// </summary>
public abstract class DiagnosticVerifier
{
private readonly ITestOutputHelper _testOutputHelper;
/// <inheritdoc />
protected DiagnosticVerifier(): this(null)
{
}
/// <inheritdoc />
protected DiagnosticVerifier(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
/// <summary>
/// File name prefix used to generate Documents instances from source.
/// </summary>
protected static string DefaultFilePathPrefix = "Test";
/// <summary>
/// Project name of
/// </summary>
protected static string TestProjectName = "TestProject";
protected Solution Solution { get; set; }
/// <summary>
/// Given classes in the form of strings, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document.
/// </summary>
/// <param name="sources">Classes in the form of strings</param>
/// <param name="analyzer">The analyzer to be run on the sources</param>
/// <param name="additionalEnabledDiagnostics">Additional diagnostics to enable at Info level</param>
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
protected Task<Diagnostic[]> GetDiagnosticsAsync(string[] sources, DiagnosticAnalyzer analyzer, string[] additionalEnabledDiagnostics)
{
return GetDiagnosticsAsync(GetDocuments(sources), analyzer, additionalEnabledDiagnostics);
}
/// <summary>
/// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it.
/// The returned diagnostics are then ordered by location in the source document.
/// </summary>
/// <param name="documents">The Documents that the analyzer will be run on</param>
/// <param name="analyzer">The analyzer to run on the documents</param>
/// <param name="additionalEnabledDiagnostics">Additional diagnostics to enable at Info level</param>
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
protected async Task<Diagnostic[]> GetDiagnosticsAsync(Document[] documents, DiagnosticAnalyzer analyzer, string[] additionalEnabledDiagnostics)
{
var projects = new HashSet<Project>();
foreach (var document in documents)
{
projects.Add(document.Project);
}
var diagnostics = new List<Diagnostic>();
foreach (var project in projects)
{
var compilation = await project.GetCompilationAsync();
// Enable any additional diagnostics
var options = compilation.Options;
if (additionalEnabledDiagnostics.Length > 0)
{
options = compilation.Options
.WithSpecificDiagnosticOptions(
additionalEnabledDiagnostics.ToDictionary(s => s, s => ReportDiagnostic.Info));
}
var compilationWithAnalyzers = compilation
.WithOptions(options)
.WithAnalyzers(ImmutableArray.Create(analyzer));
var diags = await compilationWithAnalyzers.GetAllDiagnosticsAsync();
foreach (var diag in diags)
{
_testOutputHelper?.WriteLine("Diagnostics: " + diag);
}
Assert.DoesNotContain(diags, d => d.Id == "AD0001");
// Filter out non-error diagnostics not produced by our analyzer
// We want to KEEP errors because we might have written bad code. But sometimes we leave warnings in to make the
// test code more convenient
diags = diags.Where(d => d.Severity == DiagnosticSeverity.Error || analyzer.SupportedDiagnostics.Any(s => s.Id.Equals(d.Id))).ToImmutableArray();
foreach (var diag in diags)
{
if (diag.Location == Location.None || diag.Location.IsInMetadata)
{
diagnostics.Add(diag);
}
else
{
foreach (var document in documents)
{
var tree = await document.GetSyntaxTreeAsync();
if (tree == diag.Location.SourceTree)
{
diagnostics.Add(diag);
}
}
}
}
}
return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
}
/// <summary>
/// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it.
/// </summary>
/// <param name="sources">Classes in the form of strings</param>
/// <returns>An array of Documents produced from the sources.</returns>
private Document[] GetDocuments(string[] sources)
{
var project = CreateProject(sources);
var documents = project.Documents.ToArray();
Debug.Assert(sources.Length == documents.Length);
return documents;
}
/// <summary>
/// Create a project using the inputted strings as sources.
/// </summary>
/// <param name="sources">Classes in the form of strings</param>
/// <returns>A Project created out of the Documents created from the source strings</returns>
protected Project CreateProject(params string[] sources)
{
var fileNamePrefix = DefaultFilePathPrefix;
var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
Solution = Solution ?? new AdhocWorkspace().CurrentSolution;
Solution = Solution.AddProject(projectId, TestProjectName, TestProjectName, LanguageNames.CSharp)
.WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
foreach (var defaultCompileLibrary in DependencyContext.Load(GetType().Assembly).CompileLibraries)
{
foreach (var resolveReferencePath in defaultCompileLibrary.ResolveReferencePaths(new AppLocalResolver()))
{
Solution = Solution.AddMetadataReference(projectId, MetadataReference.CreateFromFile(resolveReferencePath));
}
}
var count = 0;
foreach (var source in sources)
{
var newFileName = fileNamePrefix + count;
_testOutputHelper?.WriteLine("Adding file: " + newFileName + Environment.NewLine + source);
var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
Solution = Solution.AddDocument(documentId, newFileName, SourceText.From(source));
count++;
}
return Solution.GetProject(projectId);
}
// Required to resolve compilation assemblies inside unit tests
private class AppLocalResolver : ICompilationAssemblyResolver
{
public bool TryResolveAssemblyPaths(CompilationLibrary library, List<string> assemblies)
{
foreach (var assembly in library.Assemblies)
{
var dll = Path.Combine(Directory.GetCurrentDirectory(), "refs", Path.GetFileName(assembly));
if (File.Exists(dll))
{
assemblies.Add(dll);
continue;
}
dll = Path.Combine(Directory.GetCurrentDirectory(), Path.GetFileName(assembly));
if (File.Exists(dll))
{
assemblies.Add(dll);
}
}
return assemblies.Count > 0;
}
}
}
}

View File

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Helpers for writing tests for Roslyn analyzers.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageTags>$(PackageTags);testing</PackageTags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- This is actually a library for test projects, not a test project. -->
<IsUnitTestProject>false</IsUnitTestProject>
<!-- This package is internal, so we don't generate a package baseline. Always build against the latest dependencies. -->
<UseLatestPackageReferences>true</UseLatestPackageReferences>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.Reflection.Metadata" />
<Reference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<Reference Include="Microsoft.Extensions.DependencyModel" />
<!--
This intentionally does not reference 'xunit', 'xunit.core', or any runner packages.
XUnit recommends only using xunit.extensibility.*, xunit.assert, and xunit.abstractions for packages which "extend" xunit.
This allows consumers to decide which type of xunit runner they want to use to run these tests,
and avoids problems with `dotnet pack`.
See https://xunit.github.io/docs/nuget-packages and the special note in https://xunit.github.io/releases/2.3.
-->
<Reference Include="xunit.assert" />
<Reference Include="xunit.abstractions" />
</ItemGroup>
<ItemGroup>
<Content Include="build\**\*" Pack="true" PackagePath="build" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
namespace Microsoft.AspNetCore.Analyzer.Testing
{
public class TestSource
{
private const string MarkerStart = "/*MM";
private const string MarkerEnd = "*/";
public IDictionary<string, DiagnosticLocation> MarkerLocations { get; }
= new Dictionary<string, DiagnosticLocation>(StringComparer.Ordinal);
public DiagnosticLocation DefaultMarkerLocation { get; private set; }
public string Source { get; private set; }
public static TestSource Read(string rawSource)
{
var testInput = new TestSource();
var lines = rawSource.Split(new[] { "\n", "\r\n" }, StringSplitOptions.None);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
var markerStartIndex = line.IndexOf(MarkerStart, StringComparison.Ordinal);
if (markerStartIndex != -1)
{
var markerEndIndex = line.IndexOf(MarkerEnd, markerStartIndex, StringComparison.Ordinal);
var markerName = line.Substring(markerStartIndex + 2, markerEndIndex - markerStartIndex - 2);
var markerLocation = new DiagnosticLocation(i + 1, markerStartIndex + 1);
if (testInput.DefaultMarkerLocation == null)
{
testInput.DefaultMarkerLocation = markerLocation;
}
testInput.MarkerLocations.Add(markerName, markerLocation);
line = line.Substring(0, markerStartIndex) + line.Substring(markerEndIndex + MarkerEnd.Length);
}
lines[i] = line;
}
testInput.Source = string.Join(Environment.NewLine, lines);
return testInput;
}
}
}

View File

@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
</Project>