Skip to content
This repository was archived by the owner on Jun 30, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
973fd6d
Initial support for a legacy API shim
kzu Jul 23, 2019
b83445a
Add codegen support for new Mock<T>
kzu Jul 23, 2019
a58ad2d
Setup should happen as soon as possible
kzu Jul 23, 2019
72a1a28
Make sure we use the test calling assembly
adalon Jul 24, 2019
df46b70
Added SkipBehavior support for behavior pipeline
adalon Jul 24, 2019
5aaca90
Add new behavior for setup scope runs
kzu Jul 24, 2019
0c2ff79
Add setup overloads to avoid the mock T argument
kzu Jul 24, 2019
60237fe
Added AsMoq extension method
adalon Jul 24, 2019
eda348b
Simplify Behavior implementation by using SkipBehaviors
kzu Jul 24, 2019
edc7b68
Revert "Add setup overloads to avoid the mock T argument"
kzu Jul 24, 2019
efeefc3
Fix failing tests
kzu Jul 24, 2019
4e9d640
Move setup scope to Moq
kzu Jul 24, 2019
004e96e
Fix visiblity of various Sdk-like classes in the Moq main assembly
kzu Jul 24, 2019
91b4853
Added CallBase support
adalon Jul 24, 2019
58a67fe
Don't run FixupImports twice
kzu Jul 25, 2019
3f191e7
Ensure both analyzers and codefixers have NuGetPackageId metadata
kzu Jul 25, 2019
4c5db10
Optimize codegen performance for real world solutions
kzu Jul 25, 2019
e159b48
Do not clean unused namespaces, since it is costly for little benefit
kzu Jul 25, 2019
814afdc
Bump to latest Roslyn for VS2017 and updated supported code fix names
kzu Jul 25, 2019
708db5b
Ensure a clean restore is performed always, add CI feed
kzu Jul 25, 2019
20b9fd2
Set proper names for CallBase tests
adalon Jul 25, 2019
6143a8a
Bump TFV to the 16.0+ official one supporting NS2
kzu Jul 29, 2019
e5b1c4e
Properly generate code for generic mocks
kzu Aug 3, 2019
d9bbc2e
Add support for mocking generic types
kzu Aug 6, 2019
34a6c49
Move OverrideAllMembersCodeFix to CodeFix assembly to avoid csc error
kzu Aug 6, 2019
1e97239
Don't assume mocked types will be public
kzu Aug 6, 2019
e30d526
Cleanup and encapsulate the batch code fixer and avoid state capturing
kzu Aug 6, 2019
f4b4be4
Delete OverrideAllMembersCodeFix class that moved to CodeFix
kzu Aug 6, 2019
d5e5fd7
Re-enable end to end tests for VB since they work now
kzu Aug 6, 2019
cfddaf2
Fix roslyn internals tests from moved RoslynInternals.cs file
kzu Aug 8, 2019
2e4f6e8
Fix minor style issues flagged by codefactor.io
kzu Aug 8, 2019
462cf05
Unify naming conventions for runtime lookup
kzu Aug 8, 2019
83a22b0
Minor docs tweaks to CallBase
kzu Aug 8, 2019
db87731
Drastically simplify As<T> support by adding new Mock<T...Tn>
kzu Aug 8, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add support for mocking generic types
We previously used the very rudimentary `MetadataName` property to determine the type
to generate a stunt for. This was error prone since it does not contain the full metadata
for generic types (or nullable types) and basically you cannot resolve an `INamedTypeSymbol`
from it.

Since we need to support a more reliable and robust way to communicate back from an
analyzer to its code fix what is the target type(s) to generate stunts for, we implement our
own formatting (leveraging the `SymbolDisplayFormat` and then implementing a resolver
that parses the C#-like generic expression (i.e. `IEnumerable<KeyValuePair<string, int?>>`)
and resolves it back to a fully constructed generic `INamedTypeSymbol`.

We leverage the Superpower library for the parsing which is fast, provides great error reporting
and is a breeze to use and understand.
  • Loading branch information
kzu committed Aug 6, 2019
commit d9bbc2ea8d1677a3a0fbf79dbb1a08b41a07ac0f
4 changes: 2 additions & 2 deletions src/Moq/Moq.CodeAnalysis/RecursiveMockAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ static void ReportDiagnostics(SyntaxNodeAnalysisContext context, INamedTypeSymbo
new Dictionary<string, string>
{
{ "TargetFullName", name },
{ "Symbols", type.ToFullMetadataName() },
{ "Symbols", type.ToFullName() },
{ "RecursiveSymbols", "" },
}.ToImmutableDictionary(),
name));
Expand All @@ -136,7 +136,7 @@ static void ReportDiagnostics(SyntaxNodeAnalysisContext context, INamedTypeSymbo
new Dictionary<string, string>
{
{ "TargetFullName", name },
{ "Symbols", type.ToFullMetadataName() },
{ "Symbols", type.ToFullName() },
{ "RecursiveSymbols", "" },
}.ToImmutableDictionary(),
name));
Expand Down
1 change: 1 addition & 0 deletions src/Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<PackageReference Update="AutoCodeFix" Version="2.10.0-alpha*" />
<PackageReference Update="StreamJsonRpc" Version="1.3.23" />
<PackageReference Update="Ben.Demystifier" Version="0.1.4" />
<PackageReference Update="Superpower" Version="2.3.0" />

<PackageReference Update="xunit" Version="2.4.1" />
<PackageReference Update="xunit.runner.visualstudio" Version="2.4.1" />
Expand Down
8 changes: 4 additions & 4 deletions src/Stunts/Stunts.CodeAnalysis/StuntGeneratorAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
ImmutableArray<ITypeSymbol> typeArguments = default;
if (!method.GetAttributes().Any(x => x.AttributeClass == generator))
return;

if (method.MethodKind == MethodKind.Constructor)
{
if (method.ReceiverType is INamedTypeSymbol owner &&
Expand Down Expand Up @@ -128,7 +128,7 @@ private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
var candidate = context.Compilation.GetTypeByMetadataName(naming.GetFullName(new[] { x }));
return candidate == null || candidate.HasDiagnostic(compilationErrors.Value);
})
.Select(x => x.ToFullMetadataName()));
.Select(x => x.ToFullName()));
}
else
{
Expand All @@ -146,7 +146,7 @@ private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
{
{ "TargetFullName", name },
{ "Symbols", string.Join("|", typeArguments
.OfType<INamedTypeSymbol>().Select(x => x.ToFullMetadataName())) },
.OfType<INamedTypeSymbol>().Select(x => x.ToFullName())) },
// By passing the detected recursive symbols to update/generate,
// we avoid doing all the work we already did during analysis.
// The code action can therefore simply act on them, without
Expand Down Expand Up @@ -198,7 +198,7 @@ private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
{ "TargetFullName", name },
{ "Location", location },
{ "Symbols", string.Join("|", typeArguments
.OfType<INamedTypeSymbol>().Select(x => x.ToFullMetadataName())) },
.OfType<INamedTypeSymbol>().Select(x => x.ToFullName())) },
// We pass the same recursive symbols in either case. The
// Different diagnostics exist only to customize the message
// displayed to the user.
Expand Down
5 changes: 5 additions & 0 deletions src/Stunts/Stunts.CodeAnalysis/Stunts.CodeAnalysis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<RootNamespace>Stunts</RootNamespace>
<IncludeRoslyn>true</IncludeRoslyn>
<PrimaryOutputKind Condition="'$(PrimaryOutputKind)' == ''">Analyzers</PrimaryOutputKind>
<EnforceCopyLocalAssets>true</EnforceCopyLocalAssets>
</PropertyGroup>

<ItemDefinitionGroup>
Expand All @@ -21,6 +22,10 @@
</ProjectReference>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Superpower" Pack="false" CopyToOutput="runtime" CopyLocal="true" />
</ItemGroup>

<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
Expand Down
19 changes: 19 additions & 0 deletions src/Stunts/Stunts.CodeAnalysis/Stunts.CodeAnalysis.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project>

<Target Name="IncludeCopyToOutputFromPackage"
Inputs="@(PackageReference -> WithMetadataValue('CopyToOutput', 'runtime'))"
Outputs="%(PackageReference.Identity)"
DependsOnTargets="ResolvePackageAssets"
AfterTargets="ResolveReferences">
<PropertyGroup>
<CopyToOutputPackageId>%(PackageReference.Identity)</CopyToOutputPackageId>
</PropertyGroup>
<ItemGroup>
<CopyLocalPackageFile Include="@(RuntimeCopyLocalItems)"
Condition="'%(RuntimeCopyLocalItems.NuGetPackageId)' == '$(CopyToOutputPackageId)'" />
<ReferenceCopyLocalPaths Include="@(CopyLocalPackageFile)" />
<PackageFile Include="@(CopyLocalPackageFile)" Kind="$(PrimaryOutputKind) " />
</ItemGroup>
</Target>

</Project>
19 changes: 7 additions & 12 deletions src/Stunts/Stunts.CodeAnalysis/SymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Xml;
using Microsoft.CodeAnalysis;
using Stunts.Properties;

Expand Down Expand Up @@ -107,6 +110,8 @@ public static bool TryValidateGeneratorTypes(this IEnumerable<ITypeSymbol> types
if (symbols.Length == 0)
return false;

Debug.Assert(!symbols.Any(x => x.TypeKind == TypeKind.Error), "Symbol(s) contain errors.");

var baseType = default(INamedTypeSymbol);
var additionalInterfaces = default(IEnumerable<INamedTypeSymbol>);
if (symbols[0].TypeKind == TypeKind.Class)
Expand Down Expand Up @@ -136,24 +141,14 @@ public static bool TryValidateGeneratorTypes(this IEnumerable<ITypeSymbol> types
}

/// <summary>
/// Whether the given type symbol is either an interface or a non-sealed class.
/// Whether the given type symbol is either an interface or a non-sealed class, and not a generic task.
/// </summary>
public static bool CanBeIntercepted(this ITypeSymbol symbol)
=> symbol != null &&
symbol.CanBeReferencedByName &&
!symbol.IsValueType &&
!symbol.ToString().StartsWith(TaskFullName, StringComparison.Ordinal) &&
!symbol.MetadataName.StartsWith(TaskFullName, StringComparison.Ordinal) &&
(symbol.TypeKind == TypeKind.Interface ||
(symbol.TypeKind == TypeKind.Class && symbol.IsSealed == false));

/// <summary>
/// Gets the full metadata name of the given symbol.
/// </summary>
public static string ToFullMetadataName(this INamedTypeSymbol symbol)
=> (symbol.ContainingNamespace == null || symbol.ContainingNamespace.IsGlobalNamespace ?
"" : symbol.ContainingNamespace + ".") +
(symbol.ContainingType != null ?
symbol.ContainingType.MetadataName + "+" : "") +
symbol.MetadataName;
}
}
109 changes: 109 additions & 0 deletions src/Stunts/Stunts.CodeAnalysis/SymbolFullNameExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Superpower;
using Superpower.Parsers;

namespace Stunts
{
/// <summary>
/// Provides uniform rendering and resolving of symbols from a full name.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public static class SymbolFullNameExtensions
{
static readonly SymbolDisplayFormat fullNameFormat = new SymbolDisplayFormat(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.ExpandNullable);

/// <summary>
/// Gets the full name for the symbol, which then be used with
/// <see cref="GetTypeByFullName(Compilation, string)"/> to resolve it
/// back to the original symbol.
/// </summary>
public static string ToFullName(this ITypeSymbol symbol) => symbol.ToDisplayString(fullNameFormat);

/// <summary>
/// Resolves a symbol given its full name, as returned by <see cref="ToFullName(ITypeSymbol)"/>.
/// </summary>
public static ITypeSymbol GetTypeByFullName(this Compilation compilation, string symbolFullName)
=> new SymbolResolver(compilation).Resolve(symbolFullName ?? throw new ArgumentNullException(nameof(symbolFullName)));

class SymbolResolver
{
TextParser<ITypeSymbol[]> symbolArguments;
TextParser<ITypeSymbol> typeSymbol;
Compilation compilation;

public SymbolResolver(Compilation compilation) => this.compilation = compilation;

static TextParser<string> Identifier { get; } =
from first in Character.Letter
from rest in Character.LetterOrDigit.Or(Character.EqualTo('_')).Many()
select first + new string(rest);

static TextParser<int> ArrayRank { get; } =
from open in Character.EqualTo('[')
from dimensions in Character.EqualTo(',').Many()
from close in Character.EqualTo(']')
select dimensions.Length + 1;

static TextParser<string> FullName { get; } =
from identifiers in Identifier.ManyDelimitedBy(Character.EqualTo('.').Or(Character.EqualTo('+')))
select string.Join(".", identifiers);

TextParser<ITypeSymbol[]> SymbolArguments => LazyInitializer.EnsureInitialized(ref symbolArguments, () =>
from open in Character.EqualTo('<')
from arguments in TypeSymbol.ManyDelimitedBy(Character.EqualTo(',').IgnoreThen(Character.WhiteSpace))
from close in Character.EqualTo('>')
select arguments);

TextParser<ITypeSymbol> TypeSymbol => LazyInitializer.EnsureInitialized(ref typeSymbol, () =>
from name in FullName
from dimensions in ArrayRank.OptionalOrDefault()
from arguments in SymbolArguments.OptionalOrDefault(Array.Empty<ITypeSymbol>())
select ResolveSymbol(name, dimensions, arguments));

public ITypeSymbol Resolve(string typeName) => TypeSymbol.Parse(typeName);

ITypeSymbol ResolveSymbol(string fullName, int arrayRank, ITypeSymbol[] typeArguments)
{
var metadataName = fullName;
if (typeArguments.Length > 0)
metadataName += "`" + typeArguments.Length;

var symbol = compilation.GetTypeByMetadataName(metadataName);
if (symbol == null)
{
var nameBuilder = new StringBuilder(metadataName);
// Start replacing . with + to catch nested types, from
// last to first
while (symbol == null)
{
var indexOfDot = nameBuilder.ToString().LastIndexOf('.');
if (indexOfDot == -1)
break;

nameBuilder[indexOfDot] = '+';
symbol = compilation.GetTypeByMetadataName(nameBuilder.ToString());
}
}

if (symbol == null)
return null;

if (typeArguments.Length > 0)
symbol = symbol.Construct(typeArguments);

if (arrayRank > 0 && symbol != null)
return compilation.CreateArrayTypeSymbol(symbol, arrayRank);

return symbol;
}
}
}
}
4 changes: 2 additions & 2 deletions src/Stunts/Stunts.CodeFix/CustomStuntCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ bool isGenerated(SyntaxNode n)
return
symbol.Symbol != null &&
symbol.Symbol.GetAttributes().Any(attr =>
attr.AttributeClass.ToFullMetadataName() == typeof(GeneratedCodeAttribute).FullName ||
attr.AttributeClass.ToFullMetadataName() == typeof(CompilerGeneratedAttribute).FullName);
attr.AttributeClass.ToFullName() == typeof(GeneratedCodeAttribute).FullName ||
attr.AttributeClass.ToFullName() == typeof(CompilerGeneratedAttribute).FullName);
}

// If we find a symbol that happens to be IStunt, implement the core interface.
Expand Down
2 changes: 1 addition & 1 deletion src/Stunts/Stunts.CodeFix/StuntCodeAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ protected override async Task<Document> GetChangedDocumentAsync(CancellationToke
var compilation = await document.Project.GetCompilationAsync(cancellationToken);
var symbols = diagnostic.Properties["Symbols"]
.Split('|')
.Select(compilation.GetTypeByMetadataName)
.Select(x => (INamedTypeSymbol)compilation.GetTypeByFullName(x))
.Where(t => t != null)
.ToArray();

Expand Down
61 changes: 61 additions & 0 deletions src/Stunts/Stunts.Tests/SymbolFullNameTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Xunit;
using static TestHelpers;

namespace Stunts.Tests
{
public class SymbolFullNameTests
{
[InlineData("System.IDisposable")]
[InlineData("System.Threading.Tasks.Task<System.String>")]
[InlineData("System.Collections.Generic.IEnumerable<System.Environment+SpecialFolder>")]
[InlineData("System.Collections.Generic.IDictionary<System.Int32[], System.Collections.Generic.KeyValuePair<System.Environment+SpecialFolder, System.Nullable<System.Boolean>>>")]
[Theory]
public async Task GivenAFullName_ThenCanResolveSymbolAndRoundtrip(string fullName)
{
var (_, project) = CreateWorkspaceAndProject(LanguageNames.CSharp);
var compilation = await project.GetCompilationAsync();

var symbol = compilation.GetTypeByFullName(fullName);

Assert.NotNull(symbol);

var symbol2 = compilation.GetTypeByFullName(symbol.ToFullName());

Assert.NotNull(symbol2);

Assert.Equal(symbol, symbol2);
}

[Fact]
public async Task GivenASymbol_ThenCanRoundtripWithFullName()
{
var (workspace, project) = CreateWorkspaceAndProject(LanguageNames.CSharp);
var compilation = await project.GetCompilationAsync();

var dictionary = compilation.GetTypeByMetadataName(typeof(IDictionary<,>).FullName);
var list = compilation.GetTypeByMetadataName(typeof(IList<>).FullName);
var enumerable = compilation.GetTypeByMetadataName(typeof(IEnumerable<>).FullName);
var intsymbol = compilation.GetTypeByMetadataName(typeof(int).FullName);
var ints = compilation.CreateArrayTypeSymbol(intsymbol, 1);
var nullable = compilation.GetTypeByMetadataName(typeof(Nullable<>).FullName);
var special = compilation.GetTypeByMetadataName(typeof(Environment.SpecialFolder).FullName);
var pair = compilation.GetTypeByMetadataName(typeof(KeyValuePair<,>).FullName);

var pairof = pair.Construct(ints, nullable.Construct(special));
var enumpairs = enumerable.Construct(pairof);
var ints2 = compilation.CreateArrayTypeSymbol(intsymbol, 2);
var listof = list.Construct(ints2);
var dictof = dictionary.Construct(listof, enumpairs);

var display = dictof.ToFullName();

var resolved = compilation.GetTypeByFullName(display);

Assert.Equal(dictof, resolved);
}
}
}