Merge branch 'master' of ..\Extensions\ into johluo/remove-extensions-deps
This commit is contained in:
commit
52d0f31c3f
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in New Issue