diff --git a/appveyor.yml b/appveyor.yml index 7e1f1874..b777f4ed 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -os: Visual Studio 2017 +image: Visual Studio 2017 Preview init: - git config --global core.autocrlf input diff --git a/build.proj b/build.proj index 1d3863c9..77e0f891 100644 --- a/build.proj +++ b/build.proj @@ -4,13 +4,13 @@ Debug $(RestoreOutputPath)\ - Configuration=$(Configuration) + Configuration=$(Configuration);Dev=15.0 out - + diff --git a/src/Analyzer.Vsix/BindingRedirects.targets b/src/Analyzer.Vsix/BindingRedirects.targets new file mode 100644 index 00000000..392a54c7 --- /dev/null +++ b/src/Analyzer.Vsix/BindingRedirects.targets @@ -0,0 +1,120 @@ + + + + + BindingRedirects.pkgdef + + ^Microsoft.CodeAnalysis|^System.Composition + + true + + + + + + CollectBindingRedirected; + CleanBindingRedirectsPackage; + GenerateBindingRedirectsPackage + + + BindingRedirects; + $(GetCopyToOutputDirectoryItemsDependsOn) + + + $(ResolveReferencesDependsOn); + BindingRedirects + + + $(BuildDependsOn); + ReportBindingRedirects + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.9.9.9999 + + + + + + + + + + + + <_FusionName>%(BindingRedirected.FusionName) + <_IsFullName Condition=" $(_FusionName.IndexOf(',')) != '-1' ">true + + + <_Name>$(_FusionName.Substring(0, $(_FusionName.IndexOf(',')))) + <_IndexOfToken>$(_FusionName.IndexOf('PublicKeyToken=')) + <_IndexOfToken>$([MSBuild]::Add($(_IndexOfToken), 15)) + <_Token>$(_FusionName.Substring($(_IndexOfToken))) + + + + + $([System.Guid]::NewGuid().ToString().ToUpper()) + $(_Name) + $(_Token) + 0.0.0.0 + 99.9.9.9 + %(BindingRedirected.Version) + + + + + + + + + true + PreserveNewest + $(BindingRedirects) + + + + + diff --git a/src/Analyzer.Vsix/Moq.Analyzer.Vsix.csproj b/src/Analyzer.Vsix/Moq.Analyzer.Vsix.csproj new file mode 100644 index 00000000..5c8d9218 --- /dev/null +++ b/src/Analyzer.Vsix/Moq.Analyzer.Vsix.csproj @@ -0,0 +1,43 @@ + + + + net461;net462 + net461 + + false + false + true + false + false + false + false + Moq + + Moq.vsix + None + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Analyzer.Vsix/Moq.Analyzer.Vsix.targets b/src/Analyzer.Vsix/Moq.Analyzer.Vsix.targets new file mode 100644 index 00000000..d97127c1 --- /dev/null +++ b/src/Analyzer.Vsix/Moq.Analyzer.Vsix.targets @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Analyzer.Vsix/Properties/launchSettings.json b/src/Analyzer.Vsix/Properties/launchSettings.json new file mode 100644 index 00000000..f9ac517d --- /dev/null +++ b/src/Analyzer.Vsix/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "Moq.Analyzer.Vsix": { + "commandName": "Executable", + "executablePath": "$(VsInstallRoot)\\Common7\\IDE\\devenv.exe", + "commandLineArgs": "/rootSuffix $(VSSDKTargetPlatformRegRootSuffix)" + } + } +} \ No newline at end of file diff --git a/src/Analyzer.Vsix/source.extension.vsixmanifest b/src/Analyzer.Vsix/source.extension.vsixmanifest new file mode 100644 index 00000000..a70bd64c --- /dev/null +++ b/src/Analyzer.Vsix/source.extension.vsixmanifest @@ -0,0 +1,23 @@ + + + + + Moq + Provides analyzers and code fixes for Moq. + + + + + + + + + + + + + + + + + diff --git a/src/Analyzer/GenerateProxyCodeFix.cs b/src/Analyzer/GenerateProxyCodeFix.cs new file mode 100644 index 00000000..306d90e3 --- /dev/null +++ b/src/Analyzer/GenerateProxyCodeFix.cs @@ -0,0 +1,103 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Host; +using System.IO; +using Microsoft.CodeAnalysis.Text; +using Moq.Analyzer.Properties; +using Moq.Proxy; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Formatting; + +namespace Moq.Analyzer +{ + [ExportCodeFixProvider(LanguageNames.CSharp, new [] { LanguageNames.VisualBasic }, Name = nameof(GenerateProxyCodeFix)), Shared] + public class GenerateProxyCodeFix : CodeFixProvider + { + ICodeAnalysisServices analysisServices; + + [ImportingConstructor] + public GenerateProxyCodeFix([Import(AllowDefault = true)] ICodeAnalysisServices analysisServices) => this.analysisServices = analysisServices; + + public sealed override ImmutableArray FixableDiagnosticIds + { + get => ImmutableArray.Create(MissingProxyAnalyzer.DiagnosticId, OutdatedProxyAnalyzer.DiagnosticId); + } + + public sealed override FixAllProvider GetFixAllProvider() + { + // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers + // TODO: implement + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + var diagnostic = context.Diagnostics.First(); + var sourceToken = root.FindToken(diagnostic.Location.SourceSpan.Start); + + // Find the invocation identified by the diagnostic. + var invocation = + (SyntaxNode)sourceToken.Parent.AncestorsAndSelf().OfType().FirstOrDefault() ?? + (SyntaxNode)sourceToken.Parent.AncestorsAndSelf().OfType().FirstOrDefault(); + + // Register a code action that will invoke the fix. + context.RegisterCodeFix( + CodeAction.Create( + title: Strings.MissingProxyCodeFix.Title, + createChangedSolution: c => GenerateProxyAsync(context.Document, invocation, c), + equivalenceKey: nameof(GenerateProxyCodeFix)), + diagnostic); + } + + async Task GenerateProxyAsync(Document document, SyntaxNode invocation, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken); + var symbol = semanticModel.GetSymbolInfo(invocation); + if (symbol.Symbol?.Kind == SymbolKind.Method) + { + var method = (IMethodSymbol)symbol.Symbol; + var generator = SyntaxGenerator.GetGenerator(document.Project); + var (name, syntax) = ProxyGenerator.CreateProxy(method.TypeArguments, generator); + + var extension = document.Project.Language == LanguageNames.CSharp ? ".cs" : ".vb"; + var file = Path.Combine(Path.GetDirectoryName(document.Project.FilePath), ProxyFactory.ProxyNamespace, name + extension); + + var proxyDoc = document.Project.Documents.FirstOrDefault(d => d.Name == Path.GetFileName(file) && d.Folders.SequenceEqual(new[] { "Proxies" })); + if (proxyDoc == null) + { + proxyDoc = document.Project.AddDocument(Path.GetFileName(file), + syntax, + new[] { "Proxies" }, + file); + } + else + { + proxyDoc = proxyDoc.WithSyntaxRoot(syntax); + } + + proxyDoc = await ProxyGenerator.ApplyVisitors(proxyDoc, analysisServices, cancellationToken); + // This is somewhat expensive, but since we're adding it to the user' solution, we might + // as well make it look great ;) + proxyDoc = await Simplifier.ReduceAsync(proxyDoc); + proxyDoc = await Formatter.FormatAsync(proxyDoc); + syntax = await proxyDoc.GetSyntaxRootAsync(); + //syntax = syntax.NormalizeWhitespace(); + + return proxyDoc.WithSyntaxRoot(syntax).Project.Solution; + } + else + { + return document.Project.Solution; + } + } + } +} diff --git a/src/Analyzer/MissingProxyAnalyzer.cs b/src/Analyzer/MissingProxyAnalyzer.cs new file mode 100644 index 00000000..4fe1fa02 --- /dev/null +++ b/src/Analyzer/MissingProxyAnalyzer.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Moq.Analyzer.Properties; +using Moq.Proxy; + +namespace Moq.Analyzer +{ + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public class MissingProxyAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "MOQ001"; + + static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.MissingProxyAnalyzer_Title), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.MissingProxyAnalyzer_Description), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.MissingProxyAnalyzer_Message), Resources.ResourceManager, typeof(Resources)); + + const string Category = "Build"; + + static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, Microsoft.CodeAnalysis.CSharp.SyntaxKind.InvocationExpression); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.InvocationExpression); + } + + static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var generator = context.Compilation.GetTypeByMetadataName(typeof(ProxyGeneratorAttribute).FullName); + if (generator == null) + return; + + var symbol = context.Compilation.GetSemanticModel(context.Node.SyntaxTree).GetSymbolInfo(context.Node); + if (symbol.Symbol?.Kind == SymbolKind.Method) + { + var method = (IMethodSymbol)symbol.Symbol; + if (method.GetAttributes().Any(x => x.AttributeClass == generator) && + // Skip generic method definitions since they are typically usability overloads + // like Mock.Of(...) + !method.TypeArguments.Any(x => x.Kind == SymbolKind.TypeParameter)) + { + var name = ProxyGenerator.GetProxyFullName(method.TypeArguments); + + // See if the proxy already exists + var proxy = context.Compilation.GetTypeByMetadataName(name); + if (proxy == null) + { + var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), + new [] { new KeyValuePair("Name", name) }.ToImmutableDictionary(), + name); + + context.ReportDiagnostic(diagnostic); + } + } + } + } + } +} diff --git a/src/Analyzer/Moq.Analyzer.csproj b/src/Analyzer/Moq.Analyzer.csproj new file mode 100644 index 00000000..4670cf91 --- /dev/null +++ b/src/Analyzer/Moq.Analyzer.csproj @@ -0,0 +1,37 @@ + + + + net461 + true + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + \ No newline at end of file diff --git a/src/Analyzer/OutdatedProxyAnalyzer.cs b/src/Analyzer/OutdatedProxyAnalyzer.cs new file mode 100644 index 00000000..c5c3ab5e --- /dev/null +++ b/src/Analyzer/OutdatedProxyAnalyzer.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Moq.Analyzer.Properties; +using Moq.Proxy; + +namespace Moq.Analyzer +{ + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public class OutdatedProxyAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "MOQ002"; + + static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.OutdatedProxyAnalyzer_Title), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.OutdatedProxyAnalyzer_Description), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.OutdatedProxyAnalyzer_Message), Resources.ResourceManager, typeof(Resources)); + + const string Category = "Build"; + + static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, Microsoft.CodeAnalysis.CSharp.SyntaxKind.InvocationExpression); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.InvocationExpression); + } + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var symbol = context.Compilation.GetSemanticModel(context.Node.SyntaxTree).GetSymbolInfo(context.Node); + if (symbol.Symbol?.Kind == SymbolKind.Method) + { + var generator = context.Compilation.GetTypeByMetadataName(typeof(ProxyGeneratorAttribute).FullName); + var method = (IMethodSymbol)symbol.Symbol; + if (method.GetAttributes().Any(x => x.AttributeClass == generator) && + // Skip generic method definitions since they are typically usability overloads + // like Mock.Of(...) + !method.TypeArguments.Any(x => x.Kind == SymbolKind.TypeParameter)) + { + var name = ProxyGenerator.GetProxyFullName(method.TypeArguments); + + // See if the proxy already exists + var proxy = context.Compilation.GetTypeByMetadataName(name); + if (proxy != null) + { + // See if the symbol has any diagnostics associated + var diag = context.Compilation.GetDiagnostics(); + var proxyPath = proxy.Locations[0].GetLineSpan().Path; + + Func isProxyLoc = (loc) => loc.IsInSource && loc.GetLineSpan().Path == proxyPath; + + var proxyDiag = diag + .Where(d => isProxyLoc(d.Location) || d.AdditionalLocations.Any(a => isProxyLoc(a))) + .Where(d => d.Id == "CS0535"); + + if (proxyDiag.Any()) + { + // If there are compilation errors, we should update the proxy. + var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), name); + + context.ReportDiagnostic(diagnostic); + } + } + } + } + } + } +} diff --git a/src/Analyzer/Properties/Resources.Designer.cs b/src/Analyzer/Properties/Resources.Designer.cs new file mode 100644 index 00000000..8c0ffc58 --- /dev/null +++ b/src/Analyzer/Properties/Resources.Designer.cs @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Moq.Analyzer.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Moq.Analyzer.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Only classes and interfaces can be used in proxies. Invalid set of symbols: {0}.. + /// + internal static string InvalidProxyTypes { + get { + return ResourceManager.GetString("InvalidProxyTypes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invoked method requires a proxy to be generated at design-time or compile-time.. + /// + internal static string MissingProxyAnalyzer_Description { + get { + return ResourceManager.GetString("MissingProxyAnalyzer_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A proxy named '{0}' was not found in the current compilation.. + /// + internal static string MissingProxyAnalyzer_Message { + get { + return ResourceManager.GetString("MissingProxyAnalyzer_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Proxy not found. + /// + internal static string MissingProxyAnalyzer_Title { + get { + return ResourceManager.GetString("MissingProxyAnalyzer_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate Proxy. + /// + internal static string MissingProxyCodeFix_Title { + get { + return ResourceManager.GetString("MissingProxyCodeFix_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Existing generated proxy is no longer up-to-date.. + /// + internal static string OutdatedProxyAnalyzer_Description { + get { + return ResourceManager.GetString("OutdatedProxyAnalyzer_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Proxy named '{0}' must be updated.. + /// + internal static string OutdatedProxyAnalyzer_Message { + get { + return ResourceManager.GetString("OutdatedProxyAnalyzer_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Proxy must be updated. + /// + internal static string OutdatedProxyAnalyzer_Title { + get { + return ResourceManager.GetString("OutdatedProxyAnalyzer_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one base type can be specified for a proxy to generate and it must be the first type in the list. Invalid set of symbols: {0}.. + /// + internal static string WrongProxyBaseType { + get { + return ResourceManager.GetString("WrongProxyBaseType", resourceCulture); + } + } + } +} diff --git a/src/Analyzer/Properties/Resources.resx b/src/Analyzer/Properties/Resources.resx new file mode 100644 index 00000000..2c2f4382 --- /dev/null +++ b/src/Analyzer/Properties/Resources.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Only classes and interfaces can be used in proxies. Invalid set of symbols: {0}. + + + Invoked method requires a proxy to be generated at design-time or compile-time. + + + A proxy named '{0}' was not found in the current compilation. + + + Proxy not found + + + Generate Proxy + + + Existing generated proxy is no longer up-to-date. + + + Proxy named '{0}' must be updated. + + + Proxy must be updated + + + Only one base type can be specified for a proxy to generate and it must be the first type in the list. Invalid set of symbols: {0}. + + \ No newline at end of file diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 39353f9a..d672aa30 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,7 +1,18 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - + + + + + $(ResolveAssemblyReferencesDependsOn); + ResolveProjectReferences + + + + + + \ No newline at end of file diff --git a/src/Moq.Sdk.sln b/src/Moq.Sdk.sln index c9b4f5aa..35222844 100644 --- a/src/Moq.Sdk.sln +++ b/src/Moq.Sdk.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.6 +VisualStudioVersion = 15.0.26430.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Sdk", "Sdk\Moq.Sdk.csproj", "{234A39D6-6265-462D-A482-6A22BD351718}" EndProject @@ -13,10 +13,32 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Moq.Testing", "..\test\Moq. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Proxy.Generator", "Proxy\Proxy.Generator\Moq.Proxy.Generator.csproj", "{9115EAB8-8897-4454-8270-0156EBA6A5DF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Sdk.Build", "Sdk.Build\Moq.Sdk.Build.csproj", "{CE7D2B30-CDFF-443E-9916-F6A072C823EB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Proxy.Generator.Build", "Proxy\Proxy.Generator.Build\Moq.Proxy.Generator.Build.csproj", "{35ED3B8A-E10A-42A6-BD81-DCE46480A6F2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManualProxies", "..\test\ManualProxies\ManualProxies.csproj", "{9952FC0D-48B0-4F60-81E3-C9ADE3BE6475}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Proxy.Tests", "..\test\Proxy.Tests\Moq.Proxy.Tests.csproj", "{377223FA-B07C-4E99-852A-0D4E5D0A4347}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzer", "Analyzer\Moq.Analyzer.csproj", "{D6C737CA-5E8C-49F2-96DA-4062264D6866}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzer.Vsix", "Analyzer.Vsix\Moq.Analyzer.Vsix.csproj", "{D54839E8-325E-4994-86BC-1079516CB245}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{072E955D-AE09-41B0-810C-7A005CF276CA}" + ProjectSection(SolutionItems) = preProject + ..\appveyor.yml = ..\appveyor.yml + ..\build.cmd = ..\build.cmd + ..\build.proj = ..\build.proj + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + Version.targets = Version.targets + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Proxy.Generator.Console", "Proxy\Proxy.Generator.Console\Moq.Proxy.Generator.Console.csproj", "{886905FA-99B0-48E8-BD3E-6C6ED96CE69F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Proxy.Sdk", "Proxy\Proxy.Sdk\Moq.Proxy.Sdk.csproj", "{8E379F7D-D919-4104-AB74-B5C071EBD0A4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Sdk.Generator", "Sdk.Generator\Moq.Sdk.Generator.csproj", "{00D2D86C-1B49-4E6A-A659-30DBC01F9761}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution ..\test\Moq.Testing\Moq.Testing.projitems*{1bd7dfde-2fc8-4899-9903-6ca5724cc185}*SharedItemsImports = 13 @@ -42,14 +64,38 @@ Global {9115EAB8-8897-4454-8270-0156EBA6A5DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {9115EAB8-8897-4454-8270-0156EBA6A5DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {9115EAB8-8897-4454-8270-0156EBA6A5DF}.Release|Any CPU.Build.0 = Release|Any CPU - {CE7D2B30-CDFF-443E-9916-F6A072C823EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE7D2B30-CDFF-443E-9916-F6A072C823EB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CE7D2B30-CDFF-443E-9916-F6A072C823EB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CE7D2B30-CDFF-443E-9916-F6A072C823EB}.Release|Any CPU.Build.0 = Release|Any CPU {35ED3B8A-E10A-42A6-BD81-DCE46480A6F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {35ED3B8A-E10A-42A6-BD81-DCE46480A6F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {35ED3B8A-E10A-42A6-BD81-DCE46480A6F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {35ED3B8A-E10A-42A6-BD81-DCE46480A6F2}.Release|Any CPU.Build.0 = Release|Any CPU + {9952FC0D-48B0-4F60-81E3-C9ADE3BE6475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9952FC0D-48B0-4F60-81E3-C9ADE3BE6475}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9952FC0D-48B0-4F60-81E3-C9ADE3BE6475}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9952FC0D-48B0-4F60-81E3-C9ADE3BE6475}.Release|Any CPU.Build.0 = Release|Any CPU + {377223FA-B07C-4E99-852A-0D4E5D0A4347}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {377223FA-B07C-4E99-852A-0D4E5D0A4347}.Debug|Any CPU.Build.0 = Debug|Any CPU + {377223FA-B07C-4E99-852A-0D4E5D0A4347}.Release|Any CPU.ActiveCfg = Release|Any CPU + {377223FA-B07C-4E99-852A-0D4E5D0A4347}.Release|Any CPU.Build.0 = Release|Any CPU + {D6C737CA-5E8C-49F2-96DA-4062264D6866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6C737CA-5E8C-49F2-96DA-4062264D6866}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6C737CA-5E8C-49F2-96DA-4062264D6866}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6C737CA-5E8C-49F2-96DA-4062264D6866}.Release|Any CPU.Build.0 = Release|Any CPU + {D54839E8-325E-4994-86BC-1079516CB245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D54839E8-325E-4994-86BC-1079516CB245}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D54839E8-325E-4994-86BC-1079516CB245}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D54839E8-325E-4994-86BC-1079516CB245}.Release|Any CPU.Build.0 = Release|Any CPU + {886905FA-99B0-48E8-BD3E-6C6ED96CE69F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {886905FA-99B0-48E8-BD3E-6C6ED96CE69F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {886905FA-99B0-48E8-BD3E-6C6ED96CE69F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {886905FA-99B0-48E8-BD3E-6C6ED96CE69F}.Release|Any CPU.Build.0 = Release|Any CPU + {8E379F7D-D919-4104-AB74-B5C071EBD0A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E379F7D-D919-4104-AB74-B5C071EBD0A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E379F7D-D919-4104-AB74-B5C071EBD0A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E379F7D-D919-4104-AB74-B5C071EBD0A4}.Release|Any CPU.Build.0 = Release|Any CPU + {00D2D86C-1B49-4E6A-A659-30DBC01F9761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00D2D86C-1B49-4E6A-A659-30DBC01F9761}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00D2D86C-1B49-4E6A-A659-30DBC01F9761}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00D2D86C-1B49-4E6A-A659-30DBC01F9761}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Moq.sln b/src/Moq.sln index d15ae0ce..0cb7c49a 100644 --- a/src/Moq.sln +++ b/src/Moq.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.6 +VisualStudioVersion = 15.0.26706.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Proxy", "Proxy\Proxy\Moq.Proxy.csproj", "{C6932108-488E-4138-B411-6C1F63A0024D}" EndProject @@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Proxy.Generator", "Prox EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Sdk.Tests", "..\test\Sdk.Tests\Moq.Sdk.Tests.csproj", "{EB66BC5E-4072-4F26-9772-979D8561883F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Proxy.Sdk", "Proxy\Proxy.Sdk\Moq.Proxy.Sdk.csproj", "{3402E15C-DB88-4DFA-906E-8B529AFA3AC3}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution ..\test\Moq.Testing\Moq.Testing.projitems*{1bd7dfde-2fc8-4899-9903-6ca5724cc185}*SharedItemsImports = 13 @@ -73,8 +75,15 @@ Global {EB66BC5E-4072-4F26-9772-979D8561883F}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB66BC5E-4072-4F26-9772-979D8561883F}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB66BC5E-4072-4F26-9772-979D8561883F}.Release|Any CPU.Build.0 = Release|Any CPU + {3402E15C-DB88-4DFA-906E-8B529AFA3AC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3402E15C-DB88-4DFA-906E-8B529AFA3AC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3402E15C-DB88-4DFA-906E-8B529AFA3AC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3402E15C-DB88-4DFA-906E-8B529AFA3AC3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DA4CFD03-827D-482B-9304-83456D2A8115} + EndGlobalSection EndGlobal diff --git a/src/Moq/MockExtensions.cs b/src/Moq/MockExtensions.cs index 7b13a544..4a221d18 100644 --- a/src/Moq/MockExtensions.cs +++ b/src/Moq/MockExtensions.cs @@ -21,14 +21,52 @@ public static TResult Callback(this TResult target, Action callback) mock.Invocations.Remove(setup.Invocation); var behavior = mock.BehaviorFor(setup); - behavior.Behaviors.Add(new InvocationBehavior( - (mi, next) => - { - callback(); - return next()(mi, next); - }, - "Callback") - ); + + // If there is already a behavior wrap it instead, + // so we can do a callback after even if it's a + // shortcircuiting one like Returns. + if (behavior.Behaviors.Count > 0) + { + var wrapped = behavior.Behaviors.Pop(); + behavior.Behaviors.Add(new InvocationBehavior( + (mi, next) => + { + // If the wrapped target does not invoke the next + // behavior (us), then we invoke the callback explicitly. + var called = false; + + // Note we're tweaking the GetNextBehavior to always + // call us, before invoking the actual next behavior. + var result = wrapped.Invoke(mi, () => (_, __) => + { + callback(); + called = true; + return next()(mi, next); + }); + + // The Returns behavior does not invoke the GetNextBehavior, + // and therefore we won't have been called in that case, + // so call the callback before returning. + if (!called) + callback(); + + return result; + } + , + "Callback") + ); + } + else + { + behavior.Behaviors.Add(new InvocationBehavior( + (mi, next) => + { + callback(); + return next()(mi, next); + }, + "Callback") + ); + } } return target; diff --git a/src/Proxy/Proxy.Generator.Build/Moq.Proxy.Generator.targets b/src/Proxy/Proxy.Generator.Build/Moq.Proxy.Generator.targets index da986ca7..11c7eddc 100644 --- a/src/Proxy/Proxy.Generator.Build/Moq.Proxy.Generator.targets +++ b/src/Proxy/Proxy.Generator.Build/Moq.Proxy.Generator.targets @@ -48,7 +48,7 @@ AdditionalProxies="@(AdditionalProxyForType)" AdditionalGenerators="@(AdditionalProxyGenerator)" ToolPath="$(PGenPath)" - Debug="$(DebugPGen)"/> + Debug="$(DebugPGen)"/> diff --git a/src/Proxy/Proxy.Generator.Console/Moq.Proxy.Generator.Console.csproj b/src/Proxy/Proxy.Generator.Console/Moq.Proxy.Generator.Console.csproj index edf2b779..37f0bd1e 100644 --- a/src/Proxy/Proxy.Generator.Console/Moq.Proxy.Generator.Console.csproj +++ b/src/Proxy/Proxy.Generator.Console/Moq.Proxy.Generator.Console.csproj @@ -17,6 +17,7 @@ true + true diff --git a/src/Proxy/Proxy.Generator/DocumentVisitorLayer.cs b/src/Proxy/Proxy.Generator/DocumentVisitorLayer.cs deleted file mode 100644 index 4b4dd191..00000000 --- a/src/Proxy/Proxy.Generator/DocumentVisitorLayer.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Moq.Proxy -{ - /// - /// The layer at which an acts. - /// - public static class DocumentVisitorLayer - { - /// - /// Initial proxy generation phase, where the members are laid out with - /// default implementations that basically throw . - /// - public const string Scaffold = nameof(Scaffold); - - /// - /// First phase after scaffold, which does the initial proxy implementation rewriting - /// to replace the methods that throw generated - /// during scaffold to invoke the instead. - /// - public const string Rewrite = nameof(Rewrite); - - /// - /// Final phase that allows generators to perform additional generation beyond scaffold - /// and inital proxy rewriting. Members generated in this phase are not rewritten at all - /// to use the . - /// - public const string Fixup = nameof(Fixup); - } -} diff --git a/src/Proxy/Proxy.Generator/Moq.Proxy.Generator.csproj b/src/Proxy/Proxy.Generator/Moq.Proxy.Generator.csproj index b5f227a7..b21ebf3d 100644 --- a/src/Proxy/Proxy.Generator/Moq.Proxy.Generator.csproj +++ b/src/Proxy/Proxy.Generator/Moq.Proxy.Generator.csproj @@ -10,11 +10,12 @@ - + + diff --git a/src/Proxy/Proxy.Generator/ProxyGenerator.cs b/src/Proxy/Proxy.Generator/ProxyGenerator.cs index b34bb547..0dea2c9d 100644 --- a/src/Proxy/Proxy.Generator/ProxyGenerator.cs +++ b/src/Proxy/Proxy.Generator/ProxyGenerator.cs @@ -36,6 +36,10 @@ public static HostServices CreateHost(ImmutableArray additionalGenerator // TODO: error handling => CreateHost(additionalGenerators.Select(x => Assembly.LoadFrom(x)).ToArray()); + public static string GetProxyName(ImmutableArray types) => string.Join("", types.Select(x => x.Name)) + "Proxy"; + + public static string GetProxyFullName(ImmutableArray types) => ProxyFactory.ProxyNamespace + "." + GetProxyName(types); + /// /// Generates proxies by discovering proxy factory method invocations in the given /// source documents. @@ -189,15 +193,35 @@ public async Task GenerateProxyAsync(AdhocWorkspace workspace, Project cancellationToken.ThrowIfCancellationRequested(); - var name = string.Join("", types.Select(x => x.Name)) + "Proxy"; - // NOTE: we append the additional interfaces *after* determining the proxy name, // to avoid including them there. types = types.Concat(additionalInterfaces).ToImmutableArray(); - - var (baseType, interfaceTypes) = ValidateTypes(types); var generator = SyntaxGenerator.GetGenerator(project); + var (name, syntax) = CreateProxy(types, generator); + + var services = workspace.Services.GetService(); + var code = syntax.NormalizeWhitespace().ToFullString(); + var filePath = Path.GetTempFileName(); +#if DEBUG + File.WriteAllText(filePath, code); +#endif + + var document = workspace.AddDocument(DocumentInfo.Create( + DocumentId.CreateNewId(project.Id), + name, + filePath: filePath, + loader: TextLoader.From(TextAndVersion.Create(SourceText.From(code), VersionStamp.Create())))); + + document = await ApplyVisitors(document, services, cancellationToken); + + return document; + } + + public static (string name, SyntaxNode syntax) CreateProxy(ImmutableArray types, SyntaxGenerator generator) + { + var name = GetProxyName(types); + var (baseType, interfaceTypes) = ValidateTypes(types); var syntax = generator.CompilationUnit(types .Where(x => x.ContainingNamespace != null && x.ContainingNamespace.CanBeReferencedByName) .Select(x => x.ContainingNamespace.ToDisplayString()) @@ -213,41 +237,48 @@ public async Task GenerateProxyAsync(AdhocWorkspace workspace, Project .Select(x => generator.NamespaceImportDeclaration(x)) .Concat(new[] { - generator.AddAttributes( - generator.ClassDeclaration(name, - accessibility: Accessibility.Public, - modifiers: DeclarationModifiers.Partial, - baseType: baseType == null ? null : generator.IdentifierName(baseType.Name), - interfaceTypes: interfaceTypes.Select(x => generator.IdentifierName(x.Name))), - generator.Attribute(nameof(CompilerGeneratedAttribute))) + generator.NamespaceDeclaration(ProxyFactory.ProxyNamespace, + generator.AddAttributes( + generator.ClassDeclaration(name, + accessibility: Accessibility.Public, + modifiers: DeclarationModifiers.Partial, + baseType: baseType == null ? null : generator.IdentifierName(baseType.Name), + interfaceTypes: interfaceTypes.Select(x => generator.IdentifierName(x.Name))), + generator.Attribute("CompilerGenerated") + ) + ) })); - var services = workspace.Services.GetService(); - var code = syntax.NormalizeWhitespace().ToFullString(); - var filePath = Path.GetTempFileName(); + return (name, syntax); + } + + public static async Task ApplyVisitors(Document document, ICodeAnalysisServices services, CancellationToken cancellationToken) + { #if DEBUG - File.WriteAllText(filePath, code); + if (Debugger.IsAttached) + cancellationToken = CancellationToken.None; #endif - var document = workspace.AddDocument(DocumentInfo.Create( - DocumentId.CreateNewId(project.Id), - name, - filePath: filePath, - loader: TextLoader.From(TextAndVersion.Create(SourceText.From(code), VersionStamp.Create())))); + var language = document.Project.Language; + var prepares = services.GetLanguageServices(language, DocumentVisitorLayer.Prepare).ToArray(); + foreach (var prepare in prepares) + { + document = await prepare.VisitAsync(document, cancellationToken); + } - var scaffolds = services.GetLanguageServices(project.Language, DocumentVisitorLayer.Scaffold).ToArray(); + var scaffolds = services.GetLanguageServices(language, DocumentVisitorLayer.Scaffold).ToArray(); foreach (var scaffold in scaffolds) { document = await scaffold.VisitAsync(document, cancellationToken); } - var rewriters = services.GetLanguageServices(project.Language, DocumentVisitorLayer.Rewrite).ToArray(); + var rewriters = services.GetLanguageServices(language, DocumentVisitorLayer.Rewrite).ToArray(); foreach (var rewriter in rewriters) { document = await rewriter.VisitAsync(document, cancellationToken); } - var fixups = services.GetLanguageServices(project.Language, DocumentVisitorLayer.Fixup).ToArray(); + var fixups = services.GetLanguageServices(language, DocumentVisitorLayer.Fixup).ToArray(); foreach (var fixup in fixups) { document = await fixup.VisitAsync(document, cancellationToken); @@ -256,7 +287,7 @@ public async Task GenerateProxyAsync(AdhocWorkspace workspace, Project return document; } - (ITypeSymbol baseType, ImmutableArray interfaceTypes) ValidateTypes(ImmutableArray types) + static (ITypeSymbol baseType, ImmutableArray interfaceTypes) ValidateTypes(ImmutableArray types) { var baseType = default(ITypeSymbol); var interfaceTypes = default(ImmutableArray); diff --git a/src/Proxy/Proxy.Generator/Rewrite/CSharpProxy.cs b/src/Proxy/Proxy.Generator/Rewrite/CSharpProxy.cs index a72e3c82..c981d9cb 100644 --- a/src/Proxy/Proxy.Generator/Rewrite/CSharpProxy.cs +++ b/src/Proxy/Proxy.Generator/Rewrite/CSharpProxy.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Composition; using System.Linq; using System.Threading; @@ -45,16 +46,55 @@ public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) .ToArray()); node = (ClassDeclarationSyntax)base.VisitClassDeclaration(node); - - // Add the IProxy implementation last so it's not visited. node = node.AddBaseListTypes(SimpleBaseType(IdentifierName(nameof(IProxy)))); - node = (ClassDeclarationSyntax)generator.InsertMembers(node, 0, - generator.FieldDeclaration( - "pipeline", - generator.IdentifierName(nameof(BehaviorPipeline)), - initializer: generator.ObjectCreationExpression(generator.IdentifierName(nameof(BehaviorPipeline))))); - node = node.AddMembers(PropertyDeclaration( + node = node.AddMembers( + FieldDeclaration( + VariableDeclaration( + IdentifierName(Identifier( + TriviaList( + CarriageReturnLineFeed, + Trivia(RegionDirectiveTrivia(true) + .WithRegionKeyword(Token( + TriviaList(), + SyntaxKind.RegionKeyword, + TriviaList(Space))) + .WithEndOfDirectiveToken(Token( + TriviaList(PreprocessingMessage(nameof(IProxy))), + SyntaxKind.EndOfDirectiveToken, + TriviaList(CarriageReturnLineFeed)) + ) + ) + ), + nameof(BehaviorPipeline), + TriviaList(Space) + ) + ) + ) + .WithVariables( + SingletonSeparatedList( + VariableDeclarator(Identifier( + TriviaList(), + "pipeline", + TriviaList(Space))) + .WithInitializer( + EqualsValueClause( + ObjectCreationExpression(IdentifierName("BehaviorPipeline")) + .WithNewKeyword(Token( + TriviaList(), + SyntaxKind.NewKeyword, + TriviaList(Space))) + .WithArgumentList(ArgumentList())) + .WithEqualsToken(Token( + TriviaList(), + SyntaxKind.EqualsToken, + TriviaList(Space))))))) + .WithSemicolonToken(Token( + TriviaList(), + SyntaxKind.SemicolonToken, + TriviaList(LineFeed)) + ), + PropertyDeclaration( GenericName(Identifier("IList"), TypeArgumentList(SingletonSeparatedList(IdentifierName(nameof(IProxyBehavior))))), Identifier(nameof(IProxy.Behaviors))) .WithExplicitInterfaceSpecifier(ExplicitInterfaceSpecifier(IdentifierName(nameof(IProxy)))) @@ -63,7 +103,15 @@ public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) SyntaxKind.SimpleMemberAccessExpression, IdentifierName("pipeline"), IdentifierName("Behaviors")))) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))); + .WithSemicolonToken(Token( + TriviaList(), + SyntaxKind.SemicolonToken, + TriviaList(CarriageReturnLineFeed))) + // #endregion + .WithTrailingTrivia(TriviaList( + CarriageReturnLineFeed, + Trivia(EndRegionDirectiveTrivia(true)), + CarriageReturnLineFeed))); return node; } diff --git a/src/Proxy/Proxy.Generator/Rewrite/VisualBasicProxy.cs b/src/Proxy/Proxy.Generator/Rewrite/VisualBasicProxy.cs index bcd419a2..4043069b 100644 --- a/src/Proxy/Proxy.Generator/Rewrite/VisualBasicProxy.cs +++ b/src/Proxy/Proxy.Generator/Rewrite/VisualBasicProxy.cs @@ -33,6 +33,11 @@ class VisualBasicProxy : VisualBasicSyntaxRewriter, IDocumentVisitor return document.WithSyntaxRoot(syntax); } + // The namespace for the proxy should be global, just like C# + public override SyntaxNode VisitNamespaceStatement(NamespaceStatementSyntax node) + => base.VisitNamespaceStatement(node.WithName(ParseName("Global." + ProxyFactory.ProxyNamespace))) + .WithTrailingTrivia(CarriageReturnLineFeed); + public override SyntaxNode VisitClassBlock(ClassBlockSyntax node) { // Turn event fields into event declarations. @@ -68,10 +73,9 @@ public override SyntaxNode VisitClassBlock(ClassBlockSyntax node) }); } - node = (ClassBlockSyntax)base.VisitClassBlock(node); - - // Add the IProxy implementation last so it's not visited. - node = node.AddImplements(ImplementsStatement(IdentifierName(nameof(IProxy)))); + node = (ClassBlockSyntax)generator.AddInterfaceType( + base.VisitClassBlock(node), + generator.IdentifierName(nameof(IProxy))); var field = generator.FieldDeclaration( "pipeline", @@ -94,7 +98,7 @@ public override SyntaxNode VisitClassBlock(ClassBlockSyntax node) property.PropertyStatement.WithImplementsClause( ImplementsClause(QualifiedName(IdentifierName(nameof(IProxy)), IdentifierName(nameof(IProxy.Behaviors)))))); - return generator.InsertMembers(node, 0, field, property); + return generator.AddMembers(node, field, property); } public override SyntaxNode VisitMethodBlock(MethodBlockSyntax node) diff --git a/src/Proxy/Proxy.Generator/Scaffold/CSharpCodeFixes.cs b/src/Proxy/Proxy.Generator/Scaffold/CSharpCodeFixes.cs index eca80446..366edf70 100644 --- a/src/Proxy/Proxy.Generator/Scaffold/CSharpCodeFixes.cs +++ b/src/Proxy/Proxy.Generator/Scaffold/CSharpCodeFixes.cs @@ -15,8 +15,9 @@ class CSharpCodeFixes : CodeFixDocumentVisitor public CSharpCodeFixes(ICodeAnalysisServices services) : base(services, CodeFixNames.CSharp.ImplementAbstractClass, - CodeFixNames.CSharp.ImplementInterface) - // NOTE: should we also add CodeFixNames.All.RemoveUnnecessaryImports? + CodeFixNames.CSharp.ImplementInterface, + CodeFixNames.All.SimplifyNames, + CodeFixNames.All.RemoveUnnecessaryImports) { } } diff --git a/src/Proxy/Proxy.Generator/Scaffold/VisualBasicCodeFixes.cs b/src/Proxy/Proxy.Generator/Scaffold/VisualBasicCodeFixes.cs index 0d2f269d..736c966c 100644 --- a/src/Proxy/Proxy.Generator/Scaffold/VisualBasicCodeFixes.cs +++ b/src/Proxy/Proxy.Generator/Scaffold/VisualBasicCodeFixes.cs @@ -16,8 +16,9 @@ public VisualBasicCodeFixes(ICodeAnalysisServices services) : base(services, CodeFixNames.VisualBasic.ImplementAbstractClass, CodeFixNames.VisualBasic.ImplementInterface, - CodeFixNames.VisualBasic.AddOverloads) - // NOTE: should we also add CodeFixNames.All.RemoveUnnecessaryImports? + CodeFixNames.VisualBasic.AddOverloads, + CodeFixNames.All.SimplifyNames, + CodeFixNames.All.RemoveUnnecessaryImports) { } } diff --git a/src/Proxy/Proxy.Generator/CodeFixDocumentVisitor.cs b/src/Proxy/Proxy.Sdk/CodeFixDocumentVisitor.cs similarity index 52% rename from src/Proxy/Proxy.Generator/CodeFixDocumentVisitor.cs rename to src/Proxy/Proxy.Sdk/CodeFixDocumentVisitor.cs index dda9f2d5..438627d9 100644 --- a/src/Proxy/Proxy.Generator/CodeFixDocumentVisitor.cs +++ b/src/Proxy/Proxy.Sdk/CodeFixDocumentVisitor.cs @@ -2,26 +2,25 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Host; namespace Moq.Proxy { - abstract class CodeFixDocumentVisitor : IDocumentVisitor + public abstract class CodeFixDocumentVisitor : IDocumentVisitor { ICodeFixService codeFixService; string[] codeFixNames; protected CodeFixDocumentVisitor(ICodeAnalysisServices services, params string[] codeFixes) { - this.codeFixService = services.GetWorkspaceService(); - this.codeFixNames = codeFixes; + codeFixService = services.GetWorkspaceService(); + codeFixNames = codeFixes; } public async Task VisitAsync(Document document, CancellationToken cancellationToken = default(CancellationToken)) { - var workspace = document.Project.Solution.Workspace; - foreach (var codeFixName in codeFixNames) { // If we request and process ALL codefixes at once, we'll get one for each @@ -32,23 +31,19 @@ protected CodeFixDocumentVisitor(ICodeAnalysisServices services, params string[] var codeFixes = await codeFixService.GetCodeFixes(document, codeFixName, cancellationToken); while (codeFixes.Length != 0) { - // We first try to apply all codefixes that don't involve our IProxy interface. - var codeFix = codeFixes.FirstOrDefault(x - => !x.Diagnostics.Any(d - => d.GetMessage().Contains(nameof(IProxy)))); - - if (codeFix == null) + var operations = await codeFixes[0].Action.GetOperationsAsync(cancellationToken); + ApplyChangesOperation operation; + if ((operation = operations.OfType().FirstOrDefault()) != null) { - // We have at least one codeFix for IProxy, pick last instance, which would be - // the explicit implementation one. - codeFix = codeFixes.Last(); + document = operation.ChangedSolution.GetDocument(document.Id); + // Retrieve the codefixes for the updated doc again. + codeFixes = await codeFixService.GetCodeFixes(document, codeFixName, cancellationToken); + } + else + { + // If we got no applicable code fixes, exit the loop and move on to the next codefix. + break; } - - await codeFix.ApplyAsync(workspace, cancellationToken); - // Retrieve the updated document for the next pass. - document = workspace.CurrentSolution.GetDocument(document.Id); - // Retrieve the codefixes for the updated doc again. - codeFixes = await codeFixService.GetCodeFixes(document, codeFixName, cancellationToken); } } diff --git a/src/Proxy/Proxy.Sdk/DocumentVisitorLayer.cs b/src/Proxy/Proxy.Sdk/DocumentVisitorLayer.cs new file mode 100644 index 00000000..43f295dd --- /dev/null +++ b/src/Proxy/Proxy.Sdk/DocumentVisitorLayer.cs @@ -0,0 +1,43 @@ +namespace Moq.Proxy +{ + /// + /// The layer at which an acts. + /// + public static class DocumentVisitorLayer + { + /// + /// Initial proxy generation phase where the basic imports, namespace and + /// blank proxy class declaration and initial base type and implemented interfaces + /// are laid out in the doc. This is typically the phase were additional interfaces + /// can be injected to participate in the subsequent phases automatically. + /// + /// + /// Moq's IMocked interface is registered in this phase, for example. + /// + public const string Prepare = nameof(Prepare); + + /// + /// Phase that generates the basic boilerplate for all abstract and interface members, + /// that typically have default implementations that basically throw . + /// For C# and VB, this is achieved by executing the built-in code fixes for abstract + /// class and interface default implementations. + /// + public const string Scaffold = nameof(Scaffold); + + /// + /// Executed right after scaffold, this phase performs the initial proxy implementation rewriting + /// by replacing methods that throw generated during scaffold + /// and invokes the instead for each of them. + /// + public const string Rewrite = nameof(Rewrite); + + /// + /// Final phase that allows generators to perform additional generation beyond scaffold + /// and inital proxy rewriting. Members generated in this phase are not rewritten at all + /// to use the and can consist of language-specific fixups + /// or cleanups to make the generated code more idiomatic than the default code fixes may + /// provide. + /// + public const string Fixup = nameof(Fixup); + } +} diff --git a/src/Proxy/Proxy.Generator/IDocumentVisitor.cs b/src/Proxy/Proxy.Sdk/IDocumentVisitor.cs similarity index 100% rename from src/Proxy/Proxy.Generator/IDocumentVisitor.cs rename to src/Proxy/Proxy.Sdk/IDocumentVisitor.cs diff --git a/src/Proxy/Proxy.Sdk/Moq.Proxy.Sdk.csproj b/src/Proxy/Proxy.Sdk/Moq.Proxy.Sdk.csproj new file mode 100644 index 00000000..512cb702 --- /dev/null +++ b/src/Proxy/Proxy.Sdk/Moq.Proxy.Sdk.csproj @@ -0,0 +1,17 @@ + + + + net461 + Moq.Proxy + true + + + + + + + + + + + \ No newline at end of file diff --git a/src/Proxy/Proxy/ProxyFactory.cs b/src/Proxy/Proxy/ProxyFactory.cs index 4bf90351..b4b3b396 100644 --- a/src/Proxy/Proxy/ProxyFactory.cs +++ b/src/Proxy/Proxy/ProxyFactory.cs @@ -11,6 +11,11 @@ namespace Moq.Proxy /// public class ProxyFactory : IProxyFactory { + /// + /// The namespace where generated proxies are declared. + /// + public const string ProxyNamespace = "Proxies"; + /// /// Gets or sets the default to use /// to create proxies. @@ -24,7 +29,7 @@ private ProxyFactory() { } /// public object CreateProxy(Assembly proxiesAssembly, Type baseType, IEnumerable implementedInterfaces, object[] construtorArguments) { - var name = baseType.Name + string.Join("", implementedInterfaces.Select(x => x.Name)) + "Proxy"; + var name = ProxyNamespace + "." + baseType.Name + string.Join("", implementedInterfaces.Select(x => x.Name)) + "Proxy"; var type = proxiesAssembly.GetType(name, true, false); return Activator.CreateInstance(type, construtorArguments); diff --git a/src/Sdk.Build/CSharpMockGenerator.cs b/src/Sdk.Build/CSharpMockGenerator.cs deleted file mode 100644 index 886064c6..00000000 --- a/src/Sdk.Build/CSharpMockGenerator.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; -using Microsoft.CodeAnalysis.Host.Mef; -using Moq.Proxy; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace Moq.Sdk -{ - [ExportLanguageService(typeof(IDocumentVisitor), LanguageNames.CSharp, DocumentVisitorLayer.Fixup)] - public class CSharpMockGenerator : CSharpSyntaxRewriter, IDocumentVisitor - { - SyntaxGenerator generator; - - public async Task VisitAsync(Document document, CancellationToken cancellationToken = default(CancellationToken)) - { - generator = SyntaxGenerator.GetGenerator(document); - var syntax = await document.GetSyntaxRootAsync(cancellationToken); - syntax = Visit(syntax); - - return document.WithSyntaxRoot(syntax); - } - - public override SyntaxNode VisitCompilationUnit(CompilationUnitSyntax node) - => base.VisitCompilationUnit(node.AddUsings(UsingDirective(IdentifierName(typeof(LazyInitializer).Namespace)))); - - public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) - => generator.AddMembers( - base.VisitClassDeclaration(node), - generator.FieldDeclaration( - "mock", - ParseTypeName(nameof(IMock)) - )); - - public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node) - { - if (generator.GetName(node) == nameof(IMocked.Mock) && - generator.GetType(node).ToString() == nameof(IMock)) - return base.VisitPropertyDeclaration(node - // Make IMocked properties explicit. - .WithExplicitInterfaceSpecifier( - ExplicitInterfaceSpecifier( - IdentifierName(nameof(IMocked)))) - .WithModifiers(TokenList()) - // => LazyInitializer.EnsureInitialized(ref mock, () => new MockInfo(pipeline.Behaviors)); - .WithExpressionBody(ArrowExpressionClause( - InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName(nameof(LazyInitializer)), - IdentifierName(nameof(LazyInitializer.EnsureInitialized))), - ArgumentList(SeparatedList(new ArgumentSyntax[] - { - Argument(RefExpression(IdentifierName("mock"))), - Argument(ParenthesizedLambdaExpression( - ObjectCreationExpression( - IdentifierName(nameof(MockInfo))) - .WithArgumentList(ArgumentList(SingletonSeparatedList(Argument( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("pipeline"), - IdentifierName(nameof(BehaviorPipeline.Behaviors)) - ) - )))) - )) - })) - ) - )) - ); - - return base.VisitPropertyDeclaration(node); - } - } -} \ No newline at end of file diff --git a/src/Sdk.Build/Moq.Sdk.Build.props b/src/Sdk.Build/Moq.Sdk.Build.props deleted file mode 100644 index 671a7d9e..00000000 --- a/src/Sdk.Build/Moq.Sdk.Build.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/Sdk.Generator/CSharpMocked.cs b/src/Sdk.Generator/CSharpMocked.cs new file mode 100644 index 00000000..33d067f8 --- /dev/null +++ b/src/Sdk.Generator/CSharpMocked.cs @@ -0,0 +1,100 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Host.Mef; +using Moq.Proxy; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Moq.Sdk +{ + [ExportLanguageService(typeof(IDocumentVisitor), LanguageNames.CSharp, DocumentVisitorLayer.Fixup)] + public class CSharpMocked : CSharpSyntaxRewriter, IDocumentVisitor + { + SyntaxGenerator generator; + + public async Task VisitAsync(Document document, CancellationToken cancellationToken = default(CancellationToken)) + { + generator = SyntaxGenerator.GetGenerator(document); + var syntax = await document.GetSyntaxRootAsync(cancellationToken); + syntax = Visit(syntax); + + return document.WithSyntaxRoot(syntax); + } + + public override SyntaxNode VisitCompilationUnit(CompilationUnitSyntax node) + => base.VisitCompilationUnit(node.AddUsings( + UsingDirective(IdentifierName(typeof(LazyInitializer).Namespace)), + UsingDirective(IdentifierName(typeof(IMocked).Namespace)))); + + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) + { + var result = generator.AddInterfaceType( + base.VisitClassDeclaration(node), + generator.IdentifierName(nameof(IMocked))); + + result = generator.AddMembers(result, + generator.FieldDeclaration("mock", ParseTypeName(nameof(IMock))) + // #region IMocked + .WithLeadingTrivia( + CarriageReturnLineFeed, + Trivia(RegionDirectiveTrivia(true) + .WithRegionKeyword(Token( + TriviaList(), + SyntaxKind.RegionKeyword, + TriviaList(Space))) + .WithEndOfDirectiveToken(Token( + TriviaList(PreprocessingMessage(nameof(IMocked))), + SyntaxKind.EndOfDirectiveToken, + TriviaList(CarriageReturnLineFeed)) + ) + ) + ) + ); + + var prop = PropertyDeclaration(IdentifierName(nameof(IMock)), nameof(IMocked.Mock)) + // Make IMocked properties explicit. + .WithExplicitInterfaceSpecifier( + ExplicitInterfaceSpecifier( + IdentifierName(nameof(IMocked)))) + .WithModifiers(TokenList()) + // => LazyInitializer.EnsureInitialized(ref mock, () => new MockInfo(pipeline.Behaviors)); + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(nameof(LazyInitializer)), + IdentifierName(nameof(LazyInitializer.EnsureInitialized))), + ArgumentList(SeparatedList(new ArgumentSyntax[] + { + Argument(RefExpression(IdentifierName("mock"))), + Argument(ParenthesizedLambdaExpression( + ObjectCreationExpression( + IdentifierName(nameof(MockInfo))) + .WithArgumentList(ArgumentList(SingletonSeparatedList(Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("pipeline"), + IdentifierName(nameof(BehaviorPipeline.Behaviors)) + ) + )))) + )) + })) + ) + )) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + // #endregion + .WithTrailingTrivia( + CarriageReturnLineFeed, + Trivia(EndRegionDirectiveTrivia(false)), + CarriageReturnLineFeed); + + result = generator.AddMembers(result, prop); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Sdk.Generator/Moq.Sdk.Build.props b/src/Sdk.Generator/Moq.Sdk.Build.props new file mode 100644 index 00000000..0dfb6a31 --- /dev/null +++ b/src/Sdk.Generator/Moq.Sdk.Build.props @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Sdk.Build/Moq.Sdk.Build.targets b/src/Sdk.Generator/Moq.Sdk.Build.targets similarity index 86% rename from src/Sdk.Build/Moq.Sdk.Build.targets rename to src/Sdk.Generator/Moq.Sdk.Build.targets index 20fcb318..4220c43f 100644 --- a/src/Sdk.Build/Moq.Sdk.Build.targets +++ b/src/Sdk.Generator/Moq.Sdk.Build.targets @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/src/Sdk.Build/Moq.Sdk.Build.csproj b/src/Sdk.Generator/Moq.Sdk.Generator.csproj similarity index 86% rename from src/Sdk.Build/Moq.Sdk.Build.csproj rename to src/Sdk.Generator/Moq.Sdk.Generator.csproj index d1a58de6..91b1b228 100644 --- a/src/Sdk.Build/Moq.Sdk.Build.csproj +++ b/src/Sdk.Generator/Moq.Sdk.Generator.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Sdk.Build/VisualBasicMockGenerator.cs b/src/Sdk.Generator/VisualBasicMocked.cs similarity index 64% rename from src/Sdk.Build/VisualBasicMockGenerator.cs rename to src/Sdk.Generator/VisualBasicMocked.cs index f1463e08..8f46e7c8 100644 --- a/src/Sdk.Build/VisualBasicMockGenerator.cs +++ b/src/Sdk.Generator/VisualBasicMocked.cs @@ -7,11 +7,12 @@ using Microsoft.CodeAnalysis.Host.Mef; using Moq.Proxy; using static Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory; +using System; namespace Moq.Sdk { [ExportLanguageService(typeof(IDocumentVisitor), LanguageNames.VisualBasic, DocumentVisitorLayer.Fixup)] - public class VisualBasicMockGenerator : VisualBasicSyntaxRewriter, IDocumentVisitor + public class VisualBasicMocked : VisualBasicSyntaxRewriter, IDocumentVisitor { SyntaxGenerator generator; @@ -26,26 +27,24 @@ public class VisualBasicMockGenerator : VisualBasicSyntaxRewriter, IDocumentVisi public override SyntaxNode VisitCompilationUnit(CompilationUnitSyntax node) => base.VisitCompilationUnit(node.AddImports( - ImportsStatement(SingletonSeparatedList( - SimpleImportsClause(IdentifierName(typeof(LazyInitializer).Namespace)))))); + ImportsStatement(SingletonSeparatedList(SimpleImportsClause(IdentifierName(typeof(LazyInitializer).Namespace)))), + ImportsStatement(SingletonSeparatedList(SimpleImportsClause(IdentifierName(typeof(IMocked).Namespace)))))); public override SyntaxNode VisitClassBlock(ClassBlockSyntax node) - => generator.AddMembers( + { + var result = generator.AddInterfaceType( base.VisitClassBlock(node), - generator.FieldDeclaration( - "_mock", - ParseTypeName(nameof(IMock)) - )); + generator.IdentifierName(nameof(IMocked))); - public override SyntaxNode VisitPropertyBlock(PropertyBlockSyntax node) - { - var type = (TypeSyntax)generator.GetType(node); - var name = generator.GetName(node); + result = generator.AddMembers(result, + generator.FieldDeclaration("_mock", ParseTypeName(nameof(IMock))) + .WithLeadingTrivia(Whitespace(Environment.NewLine))); - if (type.ToString() == nameof(IMock) && - name == nameof(IMocked.Mock)) - { - node = (PropertyBlockSyntax)generator.WithGetAccessorStatements(node, new[] + var property = (PropertyBlockSyntax)generator.PropertyDeclaration( + nameof(IMocked.Mock), + ParseTypeName(nameof(IMock)), + modifiers: DeclarationModifiers.ReadOnly, + getAccessorStatements: new[] { generator.ReturnStatement( generator.InvocationExpression( @@ -75,23 +74,15 @@ public override SyntaxNode VisitPropertyBlock(PropertyBlockSyntax node) ) ) ) - //generator.Argument( - // RefKind.None - // generator.ValueReturningLambdaExpression( - // new [] - // { - // generator.ObjectCreationExpression( - // generator.IdentifierName(nameof(MockInfo)), - // generator.MemberAccessExpression( - // generator.IdentifierName("pipeline"), - // nameof(BehaviorPipeline.Behaviors))) - // } - // ) - //) }); - } - return base.VisitPropertyBlock(node); + property = property.WithPropertyStatement( + property.PropertyStatement.WithImplementsClause( + ImplementsClause(QualifiedName(IdentifierName(nameof(IMocked)), IdentifierName(nameof(IMocked.Mock)))))); + + result = generator.AddMembers(result, property); + + return result; } } } \ No newline at end of file diff --git a/src/Sdk/InvocationBehavior.cs b/src/Sdk/InvocationBehavior.cs index 0009e9de..04bf9d17 100644 --- a/src/Sdk/InvocationBehavior.cs +++ b/src/Sdk/InvocationBehavior.cs @@ -16,4 +16,4 @@ public InvocationBehavior(InvokeBehavior invoke, string name = null) public override string ToString() => Name ?? ""; } -} +} \ No newline at end of file diff --git a/src/Version.targets b/src/Version.targets index 2e3f6c3c..83c429e6 100644 --- a/src/Version.targets +++ b/src/Version.targets @@ -1,5 +1,5 @@ - + diff --git a/test/Moq.Tests.Basic/Moq.Tests.Basic.vbproj b/test/Moq.Tests.Basic/Moq.Tests.Basic.vbproj index 10642dcf..f226e009 100644 --- a/test/Moq.Tests.Basic/Moq.Tests.Basic.vbproj +++ b/test/Moq.Tests.Basic/Moq.Tests.Basic.vbproj @@ -8,8 +8,8 @@ ..\..\src\Proxy\Proxy.Generator.Build\bin\$(Configuration)\Moq.Proxy.Generator.props ..\..\src\Proxy\Proxy.Generator.Build\bin\$(Configuration)\Moq.Proxy.Generator.targets - ..\..\src\Sdk.Build\bin\$(Configuration)\Moq.Sdk.Build.props - ..\..\src\Sdk.Build\bin\$(Configuration)\Moq.Sdk.Build.targets + ..\..\src\Sdk.Generator\bin\$(Configuration)\Moq.Sdk.Build.props + ..\..\src\Sdk.Generator\bin\$(Configuration)\Moq.Sdk.Build.targets ..\..\src\Proxy\Proxy.Generator.Console\bin\$(Configuration) diff --git a/test/Moq.Tests/Moq.Tests.csproj b/test/Moq.Tests/Moq.Tests.csproj index f1bc47ed..aa4d287b 100644 --- a/test/Moq.Tests/Moq.Tests.csproj +++ b/test/Moq.Tests/Moq.Tests.csproj @@ -2,18 +2,9 @@ net461 9a09225f-e0bc-4890-bed4-d9f6f5dac146 - true + true false - - ..\..\src\Proxy\Proxy.Generator.Build\bin\$(Configuration)\Moq.Proxy.Generator.props - ..\..\src\Proxy\Proxy.Generator.Build\bin\$(Configuration)\Moq.Proxy.Generator.targets - - ..\..\src\Sdk.Build\bin\$(Configuration)\Moq.Sdk.Build.props - ..\..\src\Sdk.Build\bin\$(Configuration)\Moq.Sdk.Build.targets - ..\..\src\Proxy\Proxy.Generator.Console\bin\$(Configuration) - - @@ -22,11 +13,5 @@ - - - - - - \ No newline at end of file diff --git a/test/Moq.Tests/MoqTests.cs b/test/Moq.Tests/MoqTests.cs index c3856c32..3a6babf3 100644 --- a/test/Moq.Tests/MoqTests.cs +++ b/test/Moq.Tests/MoqTests.cs @@ -148,7 +148,8 @@ public void CanInvokeTwoCallbacks() calculator.Add(Any(), Any()) .Callback(() => called1 = true) .Callback(() => called2 = true) - .Returns((int x, int y) => x + y); + .Returns((int x, int y) => x + y) + ; calculator.Add(2, 2); @@ -157,7 +158,7 @@ public void CanInvokeTwoCallbacks() } [Fact] - public void CannotInvokeCallbackAfterReturn() + public void CanInvokeCallbackAfterReturn() { var calculator = Mock.Of(); var called = false; @@ -171,7 +172,7 @@ public void CannotInvokeCallbackAfterReturn() calculator.Add(2, 2); - Assert.False(called); + Assert.True(called); } } } \ No newline at end of file diff --git a/test/Moq.Tests/Proxies/ICalculatorProxy.cs b/test/Moq.Tests/Proxies/ICalculatorProxy.cs new file mode 100644 index 00000000..59b6e949 --- /dev/null +++ b/test/Moq.Tests/Proxies/ICalculatorProxy.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Moq.Proxy; +using System.Runtime.CompilerServices; +using System.ComponentModel; +using System.Threading; +using Moq.Sdk; + +namespace Proxies +{ + [CompilerGenerated] + public partial class ICalculatorProxy : ICalculator, IProxy, IMocked + { + public string Mode + { + get => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod())); + set => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), value)); + } + + public int Add(int a, int b) => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), a, b)); + public void PowerUp() => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod())); + public bool TryAdd(ref int x, ref int y, out int z) + { + z = default (int); + IMethodReturn returns = pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), x, y, z)); + x = (int)returns.Outputs["x"]; + y = (int)returns.Outputs["y"]; + z = (int)returns.Outputs["z"]; + return (bool)returns.ReturnValue; + } + + public event PropertyChangedEventHandler PropertyChanged + { + add => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), value)); + remove => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), value)); + } + + public event EventHandler Progress + { + add => pipeline.Execute>(new MethodInvocation(this, MethodBase.GetCurrentMethod(), value)); + remove => pipeline.Execute>(new MethodInvocation(this, MethodBase.GetCurrentMethod(), value)); + } + + public event EventHandler PoweringUp + { + add => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), value)); + remove => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), value)); + } + +#region IProxy + BehaviorPipeline pipeline = new BehaviorPipeline(); + IList IProxy.Behaviors => pipeline.Behaviors; +#endregion +#region IMocked + IMock mock; + IMock IMocked.Mock => LazyInitializer.EnsureInitialized(ref mock, () => new MockInfo(pipeline.Behaviors)); +#endregion + } +} \ No newline at end of file diff --git a/test/Proxy.Tests/CompositionTests.cs b/test/Proxy.Tests/CompositionTests.cs index 51c19918..ffd4661a 100644 --- a/test/Proxy.Tests/CompositionTests.cs +++ b/test/Proxy.Tests/CompositionTests.cs @@ -43,7 +43,7 @@ public void CanGetProxyGenerationServices() var instances = services.GetLanguageServices(group.Key.Item1, group.Key.Item2, group.Key.Item3).ToArray(); Assert.Equal(group.Count(), instances.Length); - output.WriteLine(group.Key.Item1 + ":" + group.Key.Item2.Substring(0, group.Key.Item2.IndexOf(",")) + ":" + group.Key.Item3 + "=" + instances.Length); + //output.WriteLine(group.Key.Item1 + ":" + group.Key.Item2.Substring(0, group.Key.Item2.IndexOf(",")) + ":" + group.Key.Item3 + "=" + instances.Length); } } } diff --git a/test/Proxy.Tests/ProxyGeneratorTests.cs b/test/Proxy.Tests/ProxyGeneratorTests.cs index 74db7e36..9d10c90e 100644 --- a/test/Proxy.Tests/ProxyGeneratorTests.cs +++ b/test/Proxy.Tests/ProxyGeneratorTests.cs @@ -28,7 +28,7 @@ public class ProxyGeneratorTests [InlineData(LanguageNames.CSharp)] [InlineData(LanguageNames.VisualBasic)] [Theory] - public async Task GeneratedProxyDoesNotContainAdditionalInterfaceInName(string languageName) + public async Task GeneratedProxyNameContainsAdditionalInterfaceInName(string languageName) { var compilation = await CanGenerateProxy(languageName, typeof(INotifyPropertyChanged), typeof(IDisposable)); var assembly = compilation.Emit(); @@ -36,8 +36,8 @@ public async Task GeneratedProxyDoesNotContainAdditionalInterfaceInName(string l Assert.NotNull(proxyType); Assert.True(typeof(IDisposable).IsAssignableFrom(proxyType)); - Assert.False(proxyType.FullName.Contains(nameof(IDisposable)), - $"Generated proxy should not contain the additional type {nameof(IDisposable)} in its name."); + Assert.True(proxyType.FullName.Contains(nameof(IDisposable)), + $"Generated proxy should contain the additional type {nameof(IDisposable)} in its name."); } [InlineData(LanguageNames.CSharp)] diff --git a/test/Sdk.Tests/GeneratorTests.cs b/test/Sdk.Tests/GeneratorTests.cs index 8307cbbb..28942f8c 100644 --- a/test/Sdk.Tests/GeneratorTests.cs +++ b/test/Sdk.Tests/GeneratorTests.cs @@ -35,8 +35,7 @@ public async Task CanGenerateProxies(string languageName) References = ReferencePaths.Paths .Concat(new[] { typeof(GeneratorTests).Assembly.ManifestModule.FullyQualifiedName }) .Select(x => new MSBuild.TaskItem(x)).ToArray(), - AdditionalGenerators = new[] { new MSBuild.TaskItem(typeof(CSharpMockGenerator).Assembly.ManifestModule.FullyQualifiedName) }, - AdditionalInterfaces = new[] { new MSBuild.TaskItem(typeof(IMocked).FullName) }, + AdditionalGenerators = new[] { new MSBuild.TaskItem(typeof(CSharpMocked).Assembly.ManifestModule.FullyQualifiedName) }, AdditionalProxies = new[] { new MSBuild.TaskItem(typeof(ICalculator).FullName) }, }; diff --git a/test/Sdk.Tests/IntrospectionTests.cs b/test/Sdk.Tests/IntrospectionTests.cs new file mode 100644 index 00000000..c931105f --- /dev/null +++ b/test/Sdk.Tests/IntrospectionTests.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Proxies; + +namespace Tests +{ + public class IntrospectionTests + { + [Fact] + public void CanIntrospectMock() + { + var mock = new ICalculatorProxy(); + + var info = ((Moq.Sdk.IMocked)mock).Mock; + + //info.BehaviorFor() + + } + } +} diff --git a/test/Sdk.Tests/MockBehaviorTests.cs b/test/Sdk.Tests/MockBehaviorTests.cs index 41f01d85..e25460d6 100644 --- a/test/Sdk.Tests/MockBehaviorTests.cs +++ b/test/Sdk.Tests/MockBehaviorTests.cs @@ -3,6 +3,7 @@ using System.Threading; using Moq.Proxy; using Xunit; +using Proxies; namespace Moq.Sdk.Tests { diff --git a/test/Sdk.Tests/Moq.Sdk.Tests.csproj b/test/Sdk.Tests/Moq.Sdk.Tests.csproj index d500190b..8d63dabd 100644 --- a/test/Sdk.Tests/Moq.Sdk.Tests.csproj +++ b/test/Sdk.Tests/Moq.Sdk.Tests.csproj @@ -8,8 +8,8 @@ ..\..\src\Proxy\Proxy.Generator.Build\bin\$(Configuration)\Moq.Proxy.Generator.props ..\..\src\Proxy\Proxy.Generator.Build\bin\$(Configuration)\Moq.Proxy.Generator.targets - ..\..\src\Sdk.Build\bin\$(Configuration)\Moq.Sdk.Build.props - ..\..\src\Sdk.Build\bin\$(Configuration)\Moq.Sdk.Build.targets + ..\..\src\Sdk.Generator\bin\$(Configuration)\Moq.Sdk.Build.props + ..\..\src\Sdk.Generator\bin\$(Configuration)\Moq.Sdk.Build.targets ..\..\src\Proxy\Proxy.Generator.Console\bin\$(Configuration) @@ -21,7 +21,7 @@ - +