diff --git a/NuGet.Config b/NuGet.Config index a703d7f8..1436d285 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,6 +1,9 @@  - + + + + diff --git a/src/Moq/Moq.CodeAnalysis/MockNamingConvention.cs b/src/Moq/Moq.CodeAnalysis/MockNamingConvention.cs index 7b94d1ce..d248dddf 100644 --- a/src/Moq/Moq.CodeAnalysis/MockNamingConvention.cs +++ b/src/Moq/Moq.CodeAnalysis/MockNamingConvention.cs @@ -10,13 +10,13 @@ namespace Moq public class MockNamingConvention : NamingConvention { /// - /// Gets the generated code namespace, which is . + /// Gets the generated code namespace, which is . /// - public override string Namespace => MockNaming.Namespace; + public override string Namespace => MockNaming.DefaultNamespace; /// - /// Gets the generated type names suffix, which is . + /// Gets the generated type names suffix, which is . /// - public override string NameSuffix => MockNaming.NameSuffix; + public override string NameSuffix => MockNaming.DefaultSuffix; } } diff --git a/src/Moq/Moq.CodeAnalysis/RecursiveMockAnalyzer.cs b/src/Moq/Moq.CodeAnalysis/RecursiveMockAnalyzer.cs index 8ff96f1c..090fbf3f 100644 --- a/src/Moq/Moq.CodeAnalysis/RecursiveMockAnalyzer.cs +++ b/src/Moq/Moq.CodeAnalysis/RecursiveMockAnalyzer.cs @@ -119,7 +119,7 @@ static void ReportDiagnostics(SyntaxNodeAnalysisContext context, INamedTypeSymbo new Dictionary { { "TargetFullName", name }, - { "Symbols", type.ToFullMetadataName() }, + { "Symbols", type.ToFullName() }, { "RecursiveSymbols", "" }, }.ToImmutableDictionary(), name)); @@ -136,7 +136,7 @@ static void ReportDiagnostics(SyntaxNodeAnalysisContext context, INamedTypeSymbo new Dictionary { { "TargetFullName", name }, - { "Symbols", type.ToFullMetadataName() }, + { "Symbols", type.ToFullName() }, { "RecursiveSymbols", "" }, }.ToImmutableDictionary(), name)); diff --git a/src/Moq/Moq.CodeFix/MockGenerator.cs b/src/Moq/Moq.CodeFix/MockGenerator.cs index 44c32d74..c26db199 100644 --- a/src/Moq/Moq.CodeFix/MockGenerator.cs +++ b/src/Moq/Moq.CodeFix/MockGenerator.cs @@ -20,7 +20,7 @@ public MockGenerator(NamingConvention naming) { new DefaultImports(typeof(LazyInitializer).Namespace, typeof(IMocked).Namespace), } - .Concat(GetDefaultProcessors()) + .Concat(GetDefaultProcessors().Where(p => !(p is FixupImports))) .Concat(new IDocumentProcessor[] { new CSharpMocked(), diff --git a/src/Moq/Moq.CodeFix/Moq.props b/src/Moq/Moq.CodeFix/Moq.props index 247880b1..7d8e7109 100644 --- a/src/Moq/Moq.CodeFix/Moq.props +++ b/src/Moq/Moq.CodeFix/Moq.props @@ -10,8 +10,8 @@ - - + + diff --git a/src/Moq/Moq.Sdk.Tests/Fakes.cs b/src/Moq/Moq.Sdk.Tests/Fakes.cs index f0481445..26e74c38 100644 --- a/src/Moq/Moq.Sdk.Tests/Fakes.cs +++ b/src/Moq/Moq.Sdk.Tests/Fakes.cs @@ -42,7 +42,7 @@ public class FakeSetup : IMockSetup public class FakeInvocation : IMethodInvocation { public FakeInvocation() => Target = new Mocked(); - + public IArgumentCollection Arguments { get; set; } public IDictionary Context { get; set; } @@ -51,6 +51,8 @@ public class FakeInvocation : IMethodInvocation public object Target { get; set; } + public HashSet SkipBehaviors { get; } = new HashSet(); + public IMethodReturn CreateExceptionReturn(Exception exception) => new FakeReturn { Exception = exception }; public IMethodReturn CreateValueReturn(object returnValue, params object[] allArguments) => new FakeReturn { ReturnValue = returnValue }; diff --git a/src/Moq/Moq.Sdk.Tests/Moq.Sdk.Tests.csproj b/src/Moq/Moq.Sdk.Tests/Moq.Sdk.Tests.csproj index 43f4f862..fd13465e 100644 --- a/src/Moq/Moq.Sdk.Tests/Moq.Sdk.Tests.csproj +++ b/src/Moq/Moq.Sdk.Tests/Moq.Sdk.Tests.csproj @@ -2,7 +2,7 @@ - net471 + net472 true true @@ -16,6 +16,7 @@ + diff --git a/src/Moq/Moq.Sdk.Tests/StrictMockBehaviorTests.cs b/src/Moq/Moq.Sdk.Tests/StrictMockBehaviorTests.cs index 3406a993..156fb534 100644 --- a/src/Moq/Moq.Sdk.Tests/StrictMockBehaviorTests.cs +++ b/src/Moq/Moq.Sdk.Tests/StrictMockBehaviorTests.cs @@ -13,19 +13,5 @@ public void AppliesToAllInvocations() public void ThrowsStrictMockException() => Assert.Throws(() => new StrictMockBehavior().Execute(new FakeInvocation(), () => throw new NotImplementedException())); - - [Fact] - public void ThrowsIfNullInvocation() - => Assert.Throws(() => - new StrictMockBehavior().Execute(null, () => throw new NotImplementedException())); - - [Fact] - public void DoesNotThrowIfSetupScopeActive() - { - using (new SetupScope()) - { - new StrictMockBehavior().Execute(new FakeInvocation(), () => (m, n) => m.CreateValueReturn(null)); - } - } } } diff --git a/src/Moq/Moq.Sdk/IMock`1.cs b/src/Moq/Moq.Sdk/IMock`1.cs index 42aa83ef..5e872ae3 100644 --- a/src/Moq/Moq.Sdk/IMock`1.cs +++ b/src/Moq/Moq.Sdk/IMock`1.cs @@ -3,7 +3,7 @@ /// /// Provides introspection information about a mock. /// - public interface IMock : IMock, IFluentInterface + public interface IMock : IMock, IFluentInterface where T : class { /// /// The mock object this introspection data belongs to. diff --git a/src/Moq/Moq.Sdk/MockDecorator`1.cs b/src/Moq/Moq.Sdk/MockDecorator`1.cs index 9668e051..664e8853 100644 --- a/src/Moq/Moq.Sdk/MockDecorator`1.cs +++ b/src/Moq/Moq.Sdk/MockDecorator`1.cs @@ -1,13 +1,9 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using Stunts; - -namespace Moq.Sdk +namespace Moq.Sdk { /// /// Decorator implementation over an . /// - public abstract class MockDecorator : MockDecorator, IMock + public abstract class MockDecorator : MockDecorator, IMock where T : class { readonly IMock mock; diff --git a/src/Moq/Moq.Sdk/MockExtensions.cs b/src/Moq/Moq.Sdk/MockExtensions.cs index 3ae45d36..c60a6126 100644 --- a/src/Moq/Moq.Sdk/MockExtensions.cs +++ b/src/Moq/Moq.Sdk/MockExtensions.cs @@ -18,14 +18,14 @@ public static class MockExtensions /// /// Gets the introspection information for a mocked object instance. /// - public static IMock AsMock(this T instance) + public static IMock AsMock(this T instance) where T : class => (instance as IMocked)?.Mock.As(instance) ?? throw new ArgumentException(Strings.TargetNotMock, nameof(instance)); /// /// Clones a mock by creating a new instance of the /// from and copying its behaviors, invocations and state. /// - public static IMock Clone(this IMock mock) + public static IMock Clone(this IMock mock) where T : class { if (!mock.State.TryGetValue(".ctor", out var ctor)) throw new ArgumentException("No constructor state found for cloning."); @@ -52,7 +52,7 @@ public static IMock Clone(this IMock mock) /// Gets the invocations performed on the mock so far that match the given /// setup lambda. /// - public static IEnumerable InvocationsFor(this IMock mock, Action action) + public static IEnumerable InvocationsFor(this IMock mock, Action action) where T : class { using (new SetupScope()) { @@ -66,7 +66,7 @@ public static IEnumerable InvocationsFor(this IMock moc /// Gets the invocations performed on the mock so far that match the given /// setup lambda. /// - public static IEnumerable InvocationsFor(this IMock mock, Func function) + public static IEnumerable InvocationsFor(this IMock mock, Func function) where T : class { using (new SetupScope()) { @@ -76,9 +76,9 @@ public static IEnumerable InvocationsFor(this IMo } } - static IMock As(this IMock mock, T target) => mock == null ? null : new Mock(mock, target); + static IMock As(this IMock mock, T target) where T : class => mock == null ? null : new Mock(mock, target); - class Mock : IMock + class Mock : IMock where T : class { IMock mock; diff --git a/src/Moq/Moq.Sdk/MockGeneratorAttribute.cs b/src/Moq/Moq.Sdk/MockGeneratorAttribute.cs index f9acec5b..ae8b5a59 100644 --- a/src/Moq/Moq.Sdk/MockGeneratorAttribute.cs +++ b/src/Moq/Moq.Sdk/MockGeneratorAttribute.cs @@ -6,7 +6,7 @@ namespace Moq /// Annotates a method that is a factory for mocks, so that a /// compile-time or design-time generator can generate them ahead of time. /// - [AttributeUsage(AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor)] public class MockGeneratorAttribute : Attribute { } diff --git a/src/Moq/Moq.Sdk/MockNaming.cs b/src/Moq/Moq.Sdk/MockNaming.cs index d6d8192a..baec0740 100644 --- a/src/Moq/Moq.Sdk/MockNaming.cs +++ b/src/Moq/Moq.Sdk/MockNaming.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; +using Stunts; namespace Moq.Sdk { @@ -10,29 +9,46 @@ namespace Moq.Sdk public static class MockNaming { /// - /// The namespace where generated mocks are declared. + /// The default namespace where generated mocks are declared. /// - public const string Namespace = "Mocks"; + public const string DefaultNamespace = "Mocks"; /// - /// The suffix added to mock type names. + /// The default suffix added to mock type names. /// - public const string NameSuffix = "Mock"; + public const string DefaultSuffix = "Mock"; /// - /// Gets the runtime mock name from its base type and implemented interfaces. + /// Gets the runtime mock name from its base type and optional additional + /// interfaces, using the . /// - public static string GetName(Type baseType, Type[] implementedInterfaces) - { - Array.Sort(implementedInterfaces, Comparer.Create((x, y) => x.Name.CompareTo(y.Name))); + public static string GetName(Type baseType, Type[] additionalInterfaces) + => GetName(DefaultSuffix, baseType, additionalInterfaces); - return baseType.Name + string.Join("", implementedInterfaces.Select(x => x.Name)) + NameSuffix; - } + /// + /// Gets the runtime mock name from its base type and optional additional interfaces + /// and the given appended to the type name. + /// + public static string GetName(string suffix, Type baseType, Type[] additionalInterfaces) + => StuntNaming.GetName(suffix, baseType, additionalInterfaces); + + /// + /// Gets the runtime mock full name from its base type and optional additional interfaces, + /// using the and . + /// + public static string GetFullName(Type baseType, params Type[] additionalInterfaces) + => GetFullName(DefaultNamespace, DefaultSuffix, baseType, additionalInterfaces); + + /// + /// Gets the runtime mock full name from its base type and implemented interfaces. + /// + public static string GetFullName(string @namespace, Type baseType, params Type[] additionalInterfaces) + => GetFullName(@namespace, DefaultSuffix, baseType, additionalInterfaces); /// /// Gets the runtime mock full name from its base type and implemented interfaces. /// - public static string GetFullName(Type baseType, Type[] implementedInterfaces) - => Namespace + "." + GetName(baseType, implementedInterfaces); + public static string GetFullName(string @namespace, string suffix, Type baseType, params Type[] additionalInterfaces) + => StuntNaming.GetFullName(@namespace, suffix, baseType, additionalInterfaces); } } \ No newline at end of file diff --git a/src/Moq/Moq.Sdk/StrictMockBehavior.cs b/src/Moq/Moq.Sdk/StrictMockBehavior.cs index c0a0c5e7..25641033 100644 --- a/src/Moq/Moq.Sdk/StrictMockBehavior.cs +++ b/src/Moq/Moq.Sdk/StrictMockBehavior.cs @@ -10,8 +10,6 @@ namespace Moq.Sdk /// public class StrictMockBehavior : IStuntBehavior { - IStuntBehavior fallback = new DefaultValueBehavior(); - /// /// Always returns /// @@ -20,16 +18,6 @@ public class StrictMockBehavior : IStuntBehavior /// /// Throws . /// - public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) - { - if (invocation == null) throw new ArgumentNullException(nameof(invocation)); - - if (!SetupScope.IsActive) - throw new StrictMockException(); - - // Otherwise, fallback to returning default values so that - // the fluent setup API can do its work. - return fallback.Execute(invocation, next); - } + public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) => throw new StrictMockException(); } -} +} \ No newline at end of file diff --git a/src/Moq/Moq.Tests/CallBaseTests.cs b/src/Moq/Moq.Tests/CallBaseTests.cs new file mode 100644 index 00000000..dfd1b6a9 --- /dev/null +++ b/src/Moq/Moq.Tests/CallBaseTests.cs @@ -0,0 +1,82 @@ +using Xunit; +using Moq.Sdk; +using Sample; + +namespace Moq.Tests +{ + public class CallBaseTests + { + [Fact] + public void CallBaseNotCalled() + { + var mock = Mock.Of(); + + mock.TurnOn(); + + Assert.False(mock.TurnOnCalled); + } + + [Fact] + public void CallBaseCalledForMockConfig() + { + var mock = Mock.Of().CallBase(); + + mock.TurnOn(); + + Assert.True(mock.TurnOnCalled); + } + + [Fact] + public void CallBaseCalledForInvocationConfig() + { + var mock = Mock.Of(); + + mock.Setup(x => x.TurnOn()).CallBase(); + + mock.TurnOn(); + + Assert.True(mock.TurnOnCalled); + } + + [Fact] + public void ThrowsForStrictMockAndMissingSetup() + { + // Configure CallBase at the Mock level + var mock = Mock.Of(MockBehavior.Strict).CallBase(); + + Assert.Throws(() => mock.TurnOn()); + } + + [Fact] + public void CallBaseCalledForStrictMockAndMockConfig() + { + // Configure CallBase at the Mock level + var mock = Mock.Of(MockBehavior.Strict).CallBase(); + + mock.Setup(x => x.TurnOn()); + + mock.TurnOn(); + + Assert.True(mock.TurnOnCalled); + + // And we make sure we throw for other missing setups + Assert.Throws(() => mock.Recall("")); + } + + [Fact] + public void CallBaseCalledForStrickMockAndInvocationConfig() + { + var mock = Mock.Of(MockBehavior.Strict); + + // Configure CallBase at the invocation level + mock.Setup(x => x.TurnOn()).CallBase(); + + mock.TurnOn(); + + Assert.True(mock.TurnOnCalled); + + // And we make sure we throw for other missing setups + Assert.Throws(() => mock.Recall("")); + } + } +} \ No newline at end of file diff --git a/src/Moq/Moq.Tests/DynamicMock.cs b/src/Moq/Moq.Tests/DynamicMock.cs index b204c201..2574544f 100644 --- a/src/Moq/Moq.Tests/DynamicMock.cs +++ b/src/Moq/Moq.Tests/DynamicMock.cs @@ -11,7 +11,6 @@ namespace Moq { - // TODO: can't get Roslyn to compile generated code while running the tests :( class DynamicMock { MockGenerator generator = new MockGenerator(); @@ -34,12 +33,13 @@ async Task GenerateAsync(params Type[] types) { var project = await GetProjectAsync(); var compilation = await project.GetCompilationAsync(); - var symbols = types.Select(t => compilation.GetTypeByMetadataName(t.FullName)).ToArray(); + var symbols = types.Select(t => GetSymbolFromType(compilation, t)).ToArray(); + var document = await generator.GenerateDocumentAsync(project, symbols, TimeoutToken(5)); var syntax = await document.GetSyntaxRootAsync(); - document = project.AddDocument(MockNaming.GetName(types[0], types.Skip(1).ToArray()) + (language == LanguageNames.CSharp ? ".cs" : ".vb"), - syntax, + document = project.AddDocument(MockNaming.GetName(types[0], types.Skip(1).ToArray()) + (language == LanguageNames.CSharp ? ".cs" : ".vb"), + syntax, filePath: document.FilePath); await AssertCode.NoErrorsAsync(document); @@ -61,5 +61,15 @@ async Task GetProjectAsync() return project; } + + static ITypeSymbol GetSymbolFromType(Compilation compilation, Type type) + { + if (!type.IsConstructedGenericType) + return compilation.GetTypeByMetadataName(type.FullName); + + return compilation + .GetTypeByMetadataName(type.GetGenericTypeDefinition().FullName) + .Construct(type.GenericTypeArguments.Select(t => GetSymbolFromType(compilation, t)).ToArray()); + } } } diff --git a/src/Moq/Moq.Tests/DynamicMockTests.cs b/src/Moq/Moq.Tests/DynamicMockTests.cs index 5cc9bef6..6050423e 100644 --- a/src/Moq/Moq.Tests/DynamicMockTests.cs +++ b/src/Moq/Moq.Tests/DynamicMockTests.cs @@ -1,4 +1,7 @@ -using System.Threading.Tasks; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Sample; using Stunts; @@ -24,5 +27,30 @@ public async Task WhenAddingMockBehavior_ThenCanInterceptSelectively() Assert.Equal(CalculatorMode.Scientific, mode); Assert.Equal(0, add); } + + [Fact] + public async Task WhenGeneratingGenericMock_ThenImplementsGenericType() + { + var calculator = await new DynamicMock(LanguageNames.CSharp).CreateAsync>(); + + Assert.IsAssignableFrom(calculator); + Assert.IsAssignableFrom>(calculator); + } + + [Fact] + public async Task WhenGeneratingGenericMock_ThenImplementsMultipleGenericType() + { + var calculator = await new DynamicMock(LanguageNames.CSharp).CreateAsync>>>(); + + Assert.IsAssignableFrom(calculator); + Assert.IsAssignableFrom>>>(calculator); + } + } + + public interface IRepository + { + int Add(T value); + bool Delete(int id); + T Get(int id); } } \ No newline at end of file diff --git a/src/Moq/Moq.Tests/LegacyTests.cs b/src/Moq/Moq.Tests/LegacyTests.cs new file mode 100644 index 00000000..0773264c --- /dev/null +++ b/src/Moq/Moq.Tests/LegacyTests.cs @@ -0,0 +1,28 @@ +using System; +using Xunit; + +namespace Moq.Tests +{ + public class LegacyTests + { + [Fact] + public void AsInterface() + { + var sp = new Mock(); + + sp.Setup(x => x.GetService(typeof(IDisposable))).Returns(Mock.Of()); + + sp.As() + .Setup(x => x.Dispose()) + .Callback(() => Assert.False(true, "Callback")); + + Assert.NotNull(sp.Object.GetService(typeof(IDisposable))); + + var disposable = sp.Object as IDisposable; + + Assert.NotNull(disposable); + + disposable.Dispose(); + } + } +} diff --git a/src/Moq/Moq.Tests/Mocks/CalculatorMock.cs b/src/Moq/Moq.Tests/Mocks/CalculatorMock.cs new file mode 100644 index 00000000..61ac5237 --- /dev/null +++ b/src/Moq/Moq.Tests/Mocks/CalculatorMock.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using Moq.Sdk; +using Sample; +using Stunts; + +namespace Mocks +{ + public partial class CalculatorMock : Calculator, IMocked, IStunt + { + BehaviorPipeline pipeline = new BehaviorPipeline(); + + public ObservableCollection Behaviors => pipeline.Behaviors; + + public override event EventHandler TurnedOn + { + add => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), value), (m, n) => { base.TurnedOn += value; return m.CreateValueReturn(null, value); }); + remove => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), value), (m, n) => { base.TurnedOn -= value; return m.CreateValueReturn(null, value); }); + } + + public override CalculatorMode Mode + { + get => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod()), (m, n) => m.CreateValueReturn(base.Mode)); + set => pipeline.Invoke(new MethodInvocation(this, MethodBase.GetCurrentMethod(), value), (m, n) => { base.Mode = value; return m.CreateValueReturn(null, value); }); + } + + public override int? this[string name] + { + get => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod()), (m, n) => m.CreateValueReturn(base[name])); + set => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), name, value), (m, n) => { base[name] = value; return m.CreateValueReturn(null, value); }); + } + + public override bool IsOn => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod()), (m, n) => m.CreateValueReturn(base.IsOn)); + + public override int Add(int x, int y) => + pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), x, y), (m, n) => m.CreateValueReturn(base.Add(x, y), x, y)); + + public override int Add(int x, int y, int z) => + pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), x, y, z), (m, n) => m.CreateValueReturn(base.Add(x, y, z), x, y, z)); + + public override bool TryAdd(ref int x, ref int y, out int z) + { + z = default(int); + var local_x = x; + var local_y = y; + var local_z = z; + + var result = pipeline.Invoke(new MethodInvocation(this, MethodBase.GetCurrentMethod(), x, y, z), + (m, n) => m.CreateValueReturn(base.TryAdd(ref local_x, ref local_y, out local_z), local_x, local_y, local_z), true); + + x = (int)result.Outputs["x"]; + y = (int)result.Outputs["y"]; + z = (int)result.Outputs["z"]; + + return (bool)result.ReturnValue; + } + + public override void TurnOn() => + pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod()), (m, n) => { base.TurnOn(); return m.CreateValueReturn(null); }); + + public override void Store(string name, int value) => + pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), name, value), (m, n) => { base.Store(name, value); return m.CreateValueReturn(null, name, value); }); + + public override int? Recall(string name) => + pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), name), (m, n) => m.CreateValueReturn(base.Recall(name), name)); + + public override void Clear(string name) => + pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), name), (m, n) => { base.Clear(name); return m.CreateValueReturn(null, name); }); + + public override ICalculatorMemory Memory + { + get => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod()), (m, n) => m.CreateValueReturn(base.Memory)); + } + + #region IMocked + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + IMock mock; + + [DebuggerDisplay("Invocations = {Invocations.Count}", Name = nameof(IMocked.Mock))] + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + [CompilerGenerated] + IMock IMocked.Mock => LazyInitializer.EnsureInitialized(ref mock, () => new DefaultMock(this)); + #endregion + } +} diff --git a/src/Moq/Moq.Tests/Moq.Tests.csproj b/src/Moq/Moq.Tests/Moq.Tests.csproj index b403c7ba..2626132a 100644 --- a/src/Moq/Moq.Tests/Moq.Tests.csproj +++ b/src/Moq/Moq.Tests/Moq.Tests.csproj @@ -1,7 +1,8 @@  + - net471 + net472 true true @@ -9,9 +10,11 @@ false + + @@ -23,11 +26,21 @@ + + + + + + + + + + @@ -37,5 +50,10 @@ + + + + + \ No newline at end of file diff --git a/src/Moq/Moq.Tests/MoqTests.cs b/src/Moq/Moq.Tests/MoqTests.cs index 4e94c9e5..5f5a6185 100644 --- a/src/Moq/Moq.Tests/MoqTests.cs +++ b/src/Moq/Moq.Tests/MoqTests.cs @@ -5,7 +5,6 @@ using static Moq.Syntax; using Stunts; using Sample; -using System.Linq; using Xunit.Abstractions; namespace Moq.Tests @@ -16,6 +15,16 @@ public class MoqTests public MoqTests(ITestOutputHelper output) => this.output = output; + [Fact] + public void SetupDoesNotRecordCalls() + { + var calculator = Mock.Of(); + + calculator.Setup(c => c.TurnOn()); + + Assert.Empty(calculator.AsMock().Invocations); + } + [Fact] public void CanRaiseEvents() { @@ -264,7 +273,7 @@ public void CanSetupPropertyWithValueForStrictMock() { var calculator = Mock.Of(MockBehavior.Strict); - calculator.Setup(c => c.Mode = CalculatorMode.Scientific); + calculator.Setup(c => c.Mode).Returns(CalculatorMode.Scientific); var mode = calculator.Mode; diff --git a/src/Moq/Moq.Tests/RefOutTests.cs b/src/Moq/Moq.Tests/RefOutTests.cs index 2433bd52..0a11b83a 100644 --- a/src/Moq/Moq.Tests/RefOutTests.cs +++ b/src/Moq/Moq.Tests/RefOutTests.cs @@ -718,7 +718,9 @@ async Task ApplyCodeFixAsync(string language, string initial, string expected, b var doc = project.AddDocument("code.cs", SourceText.From(code)); var compilation = await doc.Project.GetCompilationAsync(TimeoutToken(4)); var diagnostic = compilation.GetDiagnostics(TimeoutToken(5)) - .First(d => provider.FixableDiagnosticIds.Any(fixable => d.Id == fixable)); + .FirstOrDefault(d => provider.FixableDiagnosticIds.Any(fixable => d.Id == fixable)); + + Assert.True(diagnostic != null, $"Could not find any diagnostics with IDs {string.Join(", ", provider.FixableDiagnosticIds)}"); var actions = new List(); var context = new CodeFixContext(doc, diagnostic, (a, d) => actions.Add(a), TimeoutToken(5)); diff --git a/src/Moq/Moq.Tests/VerificationTests.cs b/src/Moq/Moq.Tests/VerificationTests.cs index 44bb632d..5934a2d9 100644 --- a/src/Moq/Moq.Tests/VerificationTests.cs +++ b/src/Moq/Moq.Tests/VerificationTests.cs @@ -222,8 +222,6 @@ public void VerifyExtensionAction() Assert.Throws(() => calculator.Verify(x => x.TurnOn(), "Should have been called!")); // Once with message Assert.Throws(() => calculator.Verify(x => x.TurnOn(), 1, "Should have been called!")); - // Times.Once with message - Assert.Throws(() => calculator.Verify(x => x.TurnOn(), Times.Once, "Should have been called!")); calculator.TurnOn(); @@ -235,8 +233,6 @@ public void VerifyExtensionAction() calculator.Verify(x => x.TurnOn(), "Should have been called!"); // Once with message calculator.Verify(x => x.TurnOn(), 1, "Should have been called!"); - // Times.Once with message - calculator.Verify(x => x.TurnOn(), Times.Once, "Should have been called!"); } [Fact] @@ -253,7 +249,7 @@ public void VerifyExtensionFunction() // Once with message Assert.Throws(() => calculator.Verify(x => x.Add(2, 3), 1, "Should have been called!")); // Times.Once with message - Assert.Throws(() => calculator.Verify(x => x.Add(2, 3), Times.Once, "Should have been called!")); + Assert.Throws(() => calculator.Verify(x => x.Add(2, 3).Once(), "Should have been called!")); calculator.Add(2, 3); @@ -266,7 +262,7 @@ public void VerifyExtensionFunction() // Once with message calculator.Verify(x => x.Add(2, 3), 1, "Should have been called!"); // Times.Once with message - calculator.Verify(x => x.Add(2, 3), Times.Once, "Should have been called!"); + calculator.Verify(x => x.Add(2, 3).Once(), "Should have been called!"); } //[Fact] diff --git a/src/Moq/Moq/CallBaseBehavior.cs b/src/Moq/Moq/CallBaseBehavior.cs new file mode 100644 index 00000000..6c503f25 --- /dev/null +++ b/src/Moq/Moq/CallBaseBehavior.cs @@ -0,0 +1,38 @@ +using Stunts; +using Moq.Sdk; +using System.Linq; +using System.ComponentModel; + +namespace Moq +{ + /// + /// A custom behavior to enable calls to the base member virtual implementation. + /// See method calls. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class CallBaseBehavior : IStuntBehavior + { + /// + public bool AppliesTo(IMethodInvocation invocation) => true; + + /// + public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) + { + // Check if CallBase is configured at the Mock or Invocation level + var shouldCallBase = invocation.Target.AsMoq().CallBase || invocation.Context.ContainsKey(nameof(IMoq.CallBase)); + + if (shouldCallBase) + { + // Skip the default value to force the base target member is executed + invocation.SkipBehaviors.Add(typeof(DefaultValueBehavior)); + + // If there is a matching setup for the current invocation, skip the strict + // behavior because CallBase should be called instead + if (invocation.Target.AsMock().Behaviors.OfType().Any(x => x.AppliesTo(invocation))) + invocation.SkipBehaviors.Add(typeof(StrictMockBehavior)); + } + + return next().Invoke(invocation, next); + } + } +} \ No newline at end of file diff --git a/src/Moq/Moq/CallBaseExtension.cs b/src/Moq/Moq/CallBaseExtension.cs new file mode 100644 index 00000000..bf1c8141 --- /dev/null +++ b/src/Moq/Moq/CallBaseExtension.cs @@ -0,0 +1,39 @@ +using Moq.Sdk; +using System.ComponentModel; + +namespace Moq +{ + /// + /// Extensions for calling base member virtual implementation. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class CallBaseExtension + { + /// + /// Specifies to call the base member virtual implementations by default. + /// + public static T CallBase(this T target) + { + if (target is IMocked mocked && mocked != null) + { + // Configure CallBase at the Mock level + mocked.AsMoq().CallBase = true; + } + else if (MockContext.CurrentInvocation != null) + { + // Configure CallBase at the invocation level + MockContext.CurrentInvocation.Target.AsMock().GetPipeline(MockContext.CurrentSetup).Behaviors.Add(new DelegateMockBehavior( + (m, i, next) => + { + // set CallBase + i.Context[nameof(IMoq.CallBase)] = true; + return next().Invoke(i.Target.AsMock(), i, next); + }, + nameof(IMoq.CallBase))); + } + // TODO: else throw? + + return target; + } + } +} \ No newline at end of file diff --git a/src/Moq/Moq/ConfigurePipelineBehavior.cs b/src/Moq/Moq/ConfigurePipelineBehavior.cs new file mode 100644 index 00000000..e7784786 --- /dev/null +++ b/src/Moq/Moq/ConfigurePipelineBehavior.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; +using Moq.Sdk; +using Stunts; + +namespace Moq +{ + /// + /// Configures the current invocation depending on the + /// specified for the mock. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal class ConfigurePipelineBehavior : IStuntBehavior + { + /// + /// Always applies to all invocations. + /// + public bool AppliesTo(IMethodInvocation invocation) => true; + + /// + /// Configures the current invocation depending on the + /// specified for the mock. + /// + public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) + { + var moq = invocation.Target.AsMoq(); + if (moq.Behavior != MockBehavior.Strict) + { + invocation.SkipBehaviors.Add(typeof(StrictMockBehavior)); + } + + // Ensure the Mock pipeline is always created for the matching setup + // We need this to skip the StrictBehavior in the CallBaseBehavior + if (SetupScope.IsActive) + invocation.Target.AsMock().GetPipeline(MockContext.CurrentSetup); + + return next().Invoke(invocation, next); + } + } +} diff --git a/src/Moq/Moq/Extensions.cs b/src/Moq/Moq/Extensions.cs index 433feac3..1e56cdf5 100644 --- a/src/Moq/Moq/Extensions.cs +++ b/src/Moq/Moq/Extensions.cs @@ -1,6 +1,7 @@ using Stunts; using System.Reflection; using System; +using Moq.Sdk; namespace Moq { @@ -22,5 +23,7 @@ public static bool CanBeIntercepted(this Type type) !type.FullName.StartsWith(TaskFullName, StringComparison.Ordinal) && (type.GetTypeInfo().IsInterface || (type.GetTypeInfo().IsClass && !type.GetTypeInfo().IsSealed)); + + public static IMoq AsMoq(this T instance) where T : class => new Moq(instance.AsMock()); } } diff --git a/src/Moq/Moq/IMoq.cs b/src/Moq/Moq/IMoq.cs index e5133de2..2ae3bf34 100644 --- a/src/Moq/Moq/IMoq.cs +++ b/src/Moq/Moq/IMoq.cs @@ -17,5 +17,11 @@ public interface IMoq : IMock /// Only available for mocks. /// DefaultValueProvider DefaultValue { get; set; } + + /// + /// Whether the base member virtual implementation will be called for mocked classes if no setup is matched. + /// Defaults to . + /// + bool CallBase { get; set; } } -} +} \ No newline at end of file diff --git a/src/Moq/Moq/IMoq`1.cs b/src/Moq/Moq/IMoq`1.cs index 3d0870cc..2cedd58f 100644 --- a/src/Moq/Moq/IMoq`1.cs +++ b/src/Moq/Moq/IMoq`1.cs @@ -5,7 +5,7 @@ namespace Moq /// /// Provides configuration and introspection information for a mock. /// - public interface IMoq : IMoq, IMock, IFluentInterface + public interface IMoq : IMoq, IMock, IFluentInterface where T : class { } } diff --git a/src/Moq/Moq/Legacy/It.cs b/src/Moq/Moq/Legacy/It.cs new file mode 100644 index 00000000..49600e38 --- /dev/null +++ b/src/Moq/Moq/Legacy/It.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +using System.ComponentModel; +using static Moq.Syntax; + +namespace Moq +{ + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static class It + { + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static T IsAny() => Any(); + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/src/Moq/Moq/Legacy/MockSetup`1.cs b/src/Moq/Moq/Legacy/MockSetup`1.cs new file mode 100644 index 00000000..8c86e81a --- /dev/null +++ b/src/Moq/Moq/Legacy/MockSetup`1.cs @@ -0,0 +1,179 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +using System; +using System.ComponentModel; +using Moq.Sdk; + +namespace Moq +{ + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class MockSetup where TTarget : class + { + readonly TTarget target; + readonly IMock mock; + readonly Action action; + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup(TTarget target, IMock mock, Action action) + { + this.target = target; + this.mock = mock; + this.action = action; + target.Setup(action); + } + + #region Callback + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + #endregion + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public void Verifiable() => target.Setup(action).Verifiable(); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Throws(Exception exception) + { + target.Setup(action).Throws(exception); + return this; + } + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/src/Moq/Moq/Legacy/MockSetup`2.cs b/src/Moq/Moq/Legacy/MockSetup`2.cs new file mode 100644 index 00000000..2f201bee --- /dev/null +++ b/src/Moq/Moq/Legacy/MockSetup`2.cs @@ -0,0 +1,327 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +using System; +using System.ComponentModel; +using Moq.Sdk; + +namespace Moq +{ + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class MockSetup where TTarget : class + { + readonly TTarget target; + readonly IMock mock; + readonly Func function; + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup(TTarget target, IMock mock, Func function) + { + this.target = target; + this.mock = mock; + this.function = function; + target.Setup(function); + } + + #region Callback + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Callback(Action action) + { + target.Setup(action).Callback(action); + return this; + } + + #endregion + + #region Returns + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(Func valueFunction) + { + target.Setup(function).Returns(valueFunction); + return this; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Returns(TResult value) + { + target.Setup(function).Returns(value); + return this; + } + + #endregion + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public void Verifiable() => target.Setup(function).Verifiable(); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Throws(Exception exception) + { + target.Setup(function).Throws(exception); + return this; + } + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/src/Moq/Moq/Legacy/Mock`1.Overloads.cs b/src/Moq/Moq/Legacy/Mock`1.Overloads.cs new file mode 100644 index 00000000..b0f6a105 --- /dev/null +++ b/src/Moq/Moq/Legacy/Mock`1.Overloads.cs @@ -0,0 +1,167 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +using System.ComponentModel; +using System.Reflection; + +namespace Moq +{ + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class Mock : Mock where T : class + { + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock() : base(Assembly.GetCallingAssembly(), MockBehavior.Loose) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(params object[] args) : base(Assembly.GetCallingAssembly(), MockBehavior.Loose, args) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(MockBehavior behavior, params object[] args) : base(Assembly.GetCallingAssembly(), behavior, args) { } + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class Mock : Mock where T : class + { + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock() : base(Assembly.GetCallingAssembly(), MockBehavior.Loose) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(params object[] args) : base(Assembly.GetCallingAssembly(), MockBehavior.Loose, args) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(MockBehavior behavior, params object[] args) : base(Assembly.GetCallingAssembly(), behavior, args) { } + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class Mock : Mock where T : class + { + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock() : base(Assembly.GetCallingAssembly(), MockBehavior.Loose) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(params object[] args) : base(Assembly.GetCallingAssembly(), MockBehavior.Loose, args) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(MockBehavior behavior, params object[] args) : base(Assembly.GetCallingAssembly(), behavior, args) { } + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class Mock : Mock where T : class + { + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock() : base(Assembly.GetCallingAssembly(), MockBehavior.Loose) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(params object[] args) : base(Assembly.GetCallingAssembly(), MockBehavior.Loose, args) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(MockBehavior behavior, params object[] args) : base(Assembly.GetCallingAssembly(), behavior, args) { } + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class Mock : Mock where T : class + { + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock() : base(Assembly.GetCallingAssembly(), MockBehavior.Loose) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(params object[] args) : base(Assembly.GetCallingAssembly(), MockBehavior.Loose, args) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(MockBehavior behavior, params object[] args) : base(Assembly.GetCallingAssembly(), behavior, args) { } + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class Mock : Mock where T : class + { + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock() : base(Assembly.GetCallingAssembly(), MockBehavior.Loose) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(params object[] args) : base(Assembly.GetCallingAssembly(), MockBehavior.Loose, args) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(MockBehavior behavior, params object[] args) : base(Assembly.GetCallingAssembly(), behavior, args) { } + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class Mock : Mock where T : class + { + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock() : base(Assembly.GetCallingAssembly(), MockBehavior.Loose) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(params object[] args) : base(Assembly.GetCallingAssembly(), MockBehavior.Loose, args) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(MockBehavior behavior, params object[] args) : base(Assembly.GetCallingAssembly(), behavior, args) { } + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class Mock : Mock where T : class + { + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock() : base(Assembly.GetCallingAssembly(), MockBehavior.Loose) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(params object[] args) : base(Assembly.GetCallingAssembly(), MockBehavior.Loose, args) { } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(MockBehavior behavior, params object[] args) : base(Assembly.GetCallingAssembly(), behavior, args) { } + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/src/Moq/Moq/Legacy/Mock`1.cs b/src/Moq/Moq/Legacy/Mock`1.cs new file mode 100644 index 00000000..887baf00 --- /dev/null +++ b/src/Moq/Moq/Legacy/Mock`1.cs @@ -0,0 +1,110 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using Moq.Sdk; + +namespace Moq +{ + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class Mock where T : class + { + T target; + IMock mock; + readonly MockBehavior behavior; + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock() : this(Assembly.GetCallingAssembly(), MockBehavior.Loose) + { + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(params object[] args) : this(Assembly.GetCallingAssembly(), MockBehavior.Loose, args) + { + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + [MockGenerator] + public Mock(MockBehavior behavior, params object[] args) : this(Assembly.GetCallingAssembly(), behavior, args) + { + } + + protected Mock(Assembly mocksAssembly, MockBehavior behavior, params object[] args) + { + var mocked = (IMocked)MockFactory.Default.CreateMock(mocksAssembly, typeof(T), new Type[0], args); + mocked.Initialize(behavior); + + target = (T)mocked; + mock = target.AsMock(); + } + + Mock(T target, IMock mock, MockBehavior behavior) + { + this.target = target; + this.mock = mock; + this.behavior = behavior; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public T Object => target; + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public bool CallBase + { + get => mock.AsMoq().CallBase; + set => mock.AsMoq().CallBase = value; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public Mock As() where TInterface : class + => new Mock(target as TInterface, (target as TInterface)?.AsMock(), behavior); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public void SetupAllProperties() { } // TODO + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Setup(Expression> expression) + => new MockSetup(target, mock, expression.Compile()); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup SetupGet(Expression> expression) + => new MockSetup(target, mock, expression.Compile()); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public MockSetup Setup(Expression> expression) + => new MockSetup(target, mock, expression.Compile()); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public void VerifyAll() => throw new NotImplementedException(); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public void Verify() => Moq.Verify.Called(target); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public void Verify(Expression> expression) + => Moq.Verify.Called(() => expression.Compile().Invoke(target)); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public void Verify(Expression> expression, Times times) + => Moq.Verify.CalledImpl(() => expression.Compile().Invoke(target), times.ToSdk()); + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/src/Moq/Moq/Legacy/Range.cs b/src/Moq/Moq/Legacy/Range.cs new file mode 100644 index 00000000..04536a22 --- /dev/null +++ b/src/Moq/Moq/Legacy/Range.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +using System.ComponentModel; + +namespace Moq +{ + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public enum Range + { + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + Inclusive, + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + Exclusive + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/src/Moq/Moq/Legacy/Times.cs b/src/Moq/Moq/Legacy/Times.cs new file mode 100644 index 00000000..d1b7e7ab --- /dev/null +++ b/src/Moq/Moq/Legacy/Times.cs @@ -0,0 +1,162 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +using System; +using System.ComponentModel; +using System.Diagnostics; + +namespace Moq +{ + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly struct Times : IEquatable + { + readonly int from; + readonly int to; + readonly Kind kind; + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public Sdk.Times ToSdk() + { + switch (kind) + { + case Kind.AtLeastOnce: + return Sdk.Times.AtLeastOnce; + case Kind.AtMostOnce: + return Sdk.Times.AtMostOnce; + case Kind.Once: + return Sdk.Times.Once; + case Kind.Never: + return Sdk.Times.Once; + case Kind.AtLeast: + case Kind.AtMost: + case Kind.BetweenExclusive: + case Kind.BetweenInclusive: + case Kind.Exactly: + return new Sdk.Times(from, to); + default: + return default; + } + } + + Times(Kind kind, int from, int to) + { + this.from = from; + this.to = to; + this.kind = kind; + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public void Deconstruct(out int from, out int to) + { + if (this.kind == default) + { + // This branch makes `default(Times)` equivalent to `Times.AtLeastOnce()`, + // which is the implicit default across Moq's API for overloads that don't + // accept a `Times` instance. While user code shouldn't use `default(Times)` + // (but instead either specify `Times` explicitly or not at all), it is + // easy enough to correct: + + Debug.Assert(this.kind == Kind.AtLeastOnce); + + from = 1; + to = int.MaxValue; + } + else + { + from = this.from; + to = this.to; + } + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Times AtLeast(int callCount) + { + if (callCount < 1) + throw new ArgumentOutOfRangeException(nameof(callCount)); + + return new Times(Kind.AtLeast, callCount, int.MaxValue); + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Times AtLeastOnce() => new Times(Kind.AtLeastOnce, 1, int.MaxValue); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Times AtMost(int callCount) + { + if (callCount < 0) + throw new ArgumentOutOfRangeException(nameof(callCount)); + + return new Times(Kind.AtMost, 0, callCount); + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Times AtMostOnce() => new Times(Kind.AtMostOnce, 0, 1); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Times Between(int callCountFrom, int callCountTo, Range rangeKind) + { + if (rangeKind == Range.Exclusive) + { + if (callCountFrom <= 0 || callCountTo <= callCountFrom) + throw new ArgumentOutOfRangeException(nameof(callCountFrom)); + + if (callCountTo - callCountFrom == 1) + throw new ArgumentOutOfRangeException(nameof(callCountTo)); + + return new Times(Kind.BetweenExclusive, callCountFrom + 1, callCountTo - 1); + } + + if (callCountFrom < 0 || callCountTo < callCountFrom) + throw new ArgumentOutOfRangeException(nameof(callCountFrom)); + + return new Times(Kind.BetweenInclusive, callCountFrom, callCountTo); + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Times Exactly(int callCount) + { + if (callCount < 0) + throw new ArgumentOutOfRangeException(nameof(callCount)); + + return new Times(Kind.Exactly, callCount, callCount); + } + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Times Never() => new Times(Kind.Never, 0, 0); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Times Once() => new Times(Kind.Once, 1, 1); + + /// Supports the legacy API. + [EditorBrowsable(EditorBrowsableState.Never)] + public bool Equals(Times other) + { + var (from, to) = this; + var (otherFrom, otherTo) = other; + return from == otherFrom && to == otherTo; + } + + enum Kind + { + AtLeastOnce, + AtLeast, + AtMost, + AtMostOnce, + BetweenExclusive, + BetweenInclusive, + Exactly, + Once, + Never, + } + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/src/Moq/Moq/MockBehavior.cs b/src/Moq/Moq/MockBehavior.cs index 045bff0b..7f13d2f8 100644 --- a/src/Moq/Moq/MockBehavior.cs +++ b/src/Moq/Moq/MockBehavior.cs @@ -9,18 +9,18 @@ public enum MockBehavior { /// Obsolete [EditorBrowsable(EditorBrowsableState.Never)] - Default = 1, + Default = 0, /// /// Will never throw exceptions, returning default /// values when necessary (null for reference types, /// zero for value types and empty enumerables and arrays). /// - Loose = 1, + Loose = 0, /// /// Causes the mock to always throw /// an exception for invocations that don't have a /// corresponding setup. /// - Strict = 0, + Strict = 1, } } diff --git a/src/Moq/Moq/MockInitializer.cs b/src/Moq/Moq/MockInitializer.cs index 182ce8f6..062ffccf 100644 --- a/src/Moq/Moq/MockInitializer.cs +++ b/src/Moq/Moq/MockInitializer.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Linq; +using System.Runtime.InteropServices; using Moq.Sdk; using Stunts; @@ -22,31 +23,28 @@ public static class MockInitializer /// This method can be used by custom mocks to ensure they have the /// same default behaviors as a mock created using Mock.Of{T}. /// - public static void Initialize(this IMocked mocked, MockBehavior behavior) + public static void Initialize(this IMocked mocked, MockBehavior behavior = MockBehavior.Loose) { mocked.Mock.Behaviors.Clear(); + mocked.AsMoq().Behavior = behavior; + + mocked.Mock.Behaviors.Add(new SetupScopeBehavior()); mocked.Mock.Behaviors.Add(new MockContextBehavior()); + mocked.Mock.Behaviors.Add(new ConfigurePipelineBehavior()); mocked.Mock.Behaviors.Add(new MockRecordingBehavior()); mocked.Mock.Behaviors.Add(new EventBehavior()); mocked.Mock.Behaviors.Add(new PropertyBehavior { SetterRequiresSetup = behavior == MockBehavior.Strict }); mocked.Mock.Behaviors.Add(new DefaultEqualityBehavior()); mocked.Mock.Behaviors.Add(new RecursiveMockBehavior()); + mocked.Mock.Behaviors.Add(new CallBaseBehavior()); - if (behavior == MockBehavior.Strict) - { - mocked.Mock.Behaviors.Add(new StrictMockBehavior()); - } - else - { - var defaultValue = mocked.Mock.State.GetOrAdd(() => new DefaultValueProvider()); - mocked.Mock.Behaviors.Add(new DefaultValueBehavior(defaultValue)); - mocked.Mock.State.Set(defaultValue); - } + // Dynamically configured by the ConfigurePipelineBehavior. + mocked.Mock.Behaviors.Add(new StrictMockBehavior()); - mocked.Mock.State.Set(behavior); - // Saves the initial set of behaviors, which allows resetting the mock. - mocked.Mock.State.Set(mocked.Mock.Behaviors.ToArray()); + var defaultValue = mocked.Mock.State.GetOrAdd(() => new DefaultValueProvider()); + mocked.Mock.Behaviors.Add(new DefaultValueBehavior(defaultValue)); + mocked.Mock.State.Set(defaultValue); } } } diff --git a/src/Moq/Moq/Moq.csproj b/src/Moq/Moq/Moq.csproj index 7905add8..b804b18c 100644 --- a/src/Moq/Moq/Moq.csproj +++ b/src/Moq/Moq/Moq.csproj @@ -20,14 +20,6 @@ - - - - - - - - True diff --git a/src/Moq/Moq/Moq`1.cs b/src/Moq/Moq/Moq`1.cs index 4a11d727..790f10a6 100644 --- a/src/Moq/Moq/Moq`1.cs +++ b/src/Moq/Moq/Moq`1.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel; using System.Linq; -using Moq.Properties; using Moq.Sdk; namespace Moq @@ -11,7 +10,7 @@ namespace Moq /// the Moq API provides beyond the SDK. /// [EditorBrowsable(EditorBrowsableState.Never)] - public class Moq : MockDecorator, IMoq + public class Moq : MockDecorator, IMoq where T : class { /// /// Decorates the given with Moq specific @@ -20,55 +19,13 @@ public class Moq : MockDecorator, IMoq public Moq(IMock mock) : base(mock) { } /// - /// Gets or sets the for the - /// mock. + /// Gets or sets the for the mock. /// // NOTE: the setter is somewhat duplicating behavior in Initialize... public MockBehavior Behavior { - get => Behaviors.Any(x => x is StrictMockBehavior) ? MockBehavior.Strict : - Behaviors.Any(x => x is DefaultValueBehavior) ? MockBehavior.Loose : - throw new NotSupportedException(Strings.TargetNotLooseOrStrict(nameof(StrictMockBehavior), nameof(DefaultValueBehavior))); - set - { - if (value == MockBehavior.Loose) - { - var defaultValue = State.GetOrAdd(() => new DefaultValueProvider()); - var strict = Behaviors.FirstOrDefault(x => x is StrictMockBehavior); - if (strict != null) - { - var index = Behaviors.IndexOf(strict); - Behaviors.Remove(strict); - Behaviors.Insert(index, new DefaultValueBehavior(defaultValue)); - } - else if (!Behaviors.Any(x => x is DefaultValueBehavior)) - { - Behaviors.Add(new DefaultValueBehavior(defaultValue)); - } - - var propertyBehavior = Behaviors.OfType().FirstOrDefault(); - if (propertyBehavior != null) - propertyBehavior.SetterRequiresSetup = false; - } - else if (value == MockBehavior.Strict) - { - var loose = Behaviors.FirstOrDefault(x => x is DefaultValueBehavior); - if (loose != null) - { - var index = Behaviors.IndexOf(loose); - Behaviors.Remove(loose); - Behaviors.Insert(index, new StrictMockBehavior()); - } - else if (!Behaviors.Any(x => x is StrictMockBehavior)) - { - Behaviors.Add(new StrictMockBehavior()); - } - - var propertyBehavior = Behaviors.OfType().FirstOrDefault(); - if (propertyBehavior != null) - propertyBehavior.SetterRequiresSetup = true; - } - } + get => State.GetOrAdd(nameof(IMoq) + "." + nameof(Behavior), () => MockBehavior.Default); + set => State.Set(nameof(IMoq) + "." + nameof(Behavior), value); } /// @@ -94,5 +51,12 @@ public DefaultValueProvider DefaultValue State.Set(value); } } + + /// + public bool CallBase + { + get => State.GetOrAdd(nameof(IMoq) + "." + nameof(CallBase), () => false); + set => State.Set(nameof(IMoq) + "." + nameof(CallBase), value); + } } -} +} \ No newline at end of file diff --git a/src/Moq/Moq/NamedExtension.cs b/src/Moq/Moq/NamedExtension.cs index 1b43c04a..10c1eead 100644 --- a/src/Moq/Moq/NamedExtension.cs +++ b/src/Moq/Moq/NamedExtension.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Text; -using Moq.Properties; +using System.ComponentModel; using Moq.Sdk; namespace Moq @@ -16,7 +12,7 @@ public static class NamedExtension /// /// Names the mock. /// - public static T Named(this T target, string name) + public static T Named(this T target, string name)where T : class { target.AsMock().State.Set("Name", name); return target; diff --git a/src/Moq/Moq/OccurrenceExtension.cs b/src/Moq/Moq/OccurrenceExtension.cs index e0c7ec31..da9f5b8a 100644 --- a/src/Moq/Moq/OccurrenceExtension.cs +++ b/src/Moq/Moq/OccurrenceExtension.cs @@ -26,7 +26,7 @@ public static TResult AtLeastOnce(this TResult target) var setup = MockContext.CurrentSetup; if (setup != null) { - setup.Occurrence = Times.AtLeastOnce; + setup.Occurrence = Sdk.Times.AtLeastOnce; var mock = setup.Invocation.Target.AsMock(); if (Verify.IsVerifying(mock)) { @@ -52,7 +52,7 @@ public static TResult Once(this TResult target) var setup = MockContext.CurrentSetup; if (setup != null) { - setup.Occurrence = Times.Once; + setup.Occurrence = Sdk.Times.Once; var mock = setup.Invocation.Target.AsMock(); if (Verify.IsVerifying(mock)) { @@ -78,7 +78,7 @@ public static TResult Never(this TResult target) var setup = MockContext.CurrentSetup; if (setup != null) { - setup.Occurrence = Times.Never; + setup.Occurrence = Sdk.Times.Never; var mock = setup.Invocation.Target.AsMock(); if (Verify.IsVerifying(mock)) { @@ -103,7 +103,7 @@ public static TResult Exactly(this TResult target, int callCount) var setup = MockContext.CurrentSetup; if (setup != null) { - setup.Occurrence = Times.Exactly(callCount); + setup.Occurrence = Sdk.Times.Exactly(callCount); var mock = setup.Invocation.Target.AsMock(); if (Verify.IsVerifying(mock)) { diff --git a/src/Moq/Moq/RecursiveMockBehavior.cs b/src/Moq/Moq/RecursiveMockBehavior.cs index 5b0773d0..f4aa1e7e 100644 --- a/src/Moq/Moq/RecursiveMockBehavior.cs +++ b/src/Moq/Moq/RecursiveMockBehavior.cs @@ -1,15 +1,18 @@ using System; +using System.ComponentModel; using System.Linq; using System.Reflection; +using Moq.Sdk; using Stunts; -namespace Moq.Sdk +namespace Moq { /// /// Adds support for recursive mocks invoked during a setup, /// so that types that can be intercepted (see ) /// are turned into mocks automatically. /// + [EditorBrowsable(EditorBrowsableState.Never)] public class RecursiveMockBehavior : IStuntBehavior { /// diff --git a/src/Moq/Moq/ReturnsBehavior.cs b/src/Moq/Moq/ReturnsBehavior.cs index bfa670ca..38f3ff69 100644 --- a/src/Moq/Moq/ReturnsBehavior.cs +++ b/src/Moq/Moq/ReturnsBehavior.cs @@ -3,6 +3,7 @@ using Moq.Sdk; using System.Diagnostics; using System.Linq; +using System.ComponentModel; namespace Moq { @@ -12,7 +13,8 @@ namespace Moq /// method calls. /// [DebuggerDisplay("{DebuggerValue}", Name = "Returns", Type = nameof(ReturnsBehavior))] - class ReturnsBehavior : IMockBehavior + [EditorBrowsable(EditorBrowsableState.Never)] + internal class ReturnsBehavior : IMockBehavior { [DebuggerBrowsable(DebuggerBrowsableState.Never)] Func getter; diff --git a/src/Moq/Moq/ReturnsDelegateBehavior.cs b/src/Moq/Moq/ReturnsDelegateBehavior.cs index 76d2a069..0558eaf5 100644 --- a/src/Moq/Moq/ReturnsDelegateBehavior.cs +++ b/src/Moq/Moq/ReturnsDelegateBehavior.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Diagnostics; using System.Linq; using Moq.Sdk; @@ -7,7 +8,8 @@ namespace Moq { [DebuggerDisplay("{@delegate}", Name = "Returns", Type = nameof(ReturnsDelegateBehavior))] - class ReturnsDelegateBehavior : IMockBehavior + [EditorBrowsable(EditorBrowsableState.Never)] + internal class ReturnsDelegateBehavior : IMockBehavior { [DebuggerDisplay("")] Delegate @delegate; diff --git a/src/Moq/Moq/ReturnsExtension.Overloads.cs b/src/Moq/Moq/ReturnsExtension.Overloads.cs index cc48fa5f..1de52d22 100644 --- a/src/Moq/Moq/ReturnsExtension.Overloads.cs +++ b/src/Moq/Moq/ReturnsExtension.Overloads.cs @@ -50,6 +50,15 @@ public static TResult Returns(this TResult target, => Returns(value, (m, i, next) => i.CreateValueReturn(value((T1)i.Arguments[0], (T2)i.Arguments[1], (T3)i.Arguments[2], (T4)i.Arguments[3], (T5)i.Arguments[4]), i.Arguments.ToArray())); + /// + /// Sets the return value for a property or non-void method to + /// be evaluated dynamically using the given function on every + /// call. + /// + public static TResult Returns(this TResult target, Func value) + => Returns(value, (m, i, next) + => i.CreateValueReturn(value((T1)i.Arguments[0], (T2)i.Arguments[1], (T3)i.Arguments[2], (T4)i.Arguments[3], (T5)i.Arguments[4], (T6)i.Arguments[5]), i.Arguments.ToArray())); + /// /// Sets the return value for a property or non-void method to /// be evaluated dynamically using the given function on every diff --git a/src/Moq/Moq/SetupScopeBehavior.cs b/src/Moq/Moq/SetupScopeBehavior.cs new file mode 100644 index 00000000..cb1fbca8 --- /dev/null +++ b/src/Moq/Moq/SetupScopeBehavior.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Moq.Sdk; +using Stunts; + +namespace Moq +{ + /// + /// A behavior that skips all behaviors that do not apply during a setup scope. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal class SetupScopeBehavior : IStuntBehavior + { + static readonly HashSet setupScopeBehaviors = new HashSet + { + typeof(DefaultValueBehavior), + typeof(MockContextBehavior), + typeof(RecursiveMockBehavior), + typeof(ConfigurePipelineBehavior) + }; + + /// + /// Applies only if is . + /// + public bool AppliesTo(IMethodInvocation invocation) => SetupScope.IsActive; + + /// + /// Skips all non-setup behaviors from execution. + /// + public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) + { + foreach (var behavior in invocation.Target.AsMock().Behaviors.Where(x => !setupScopeBehaviors.Contains(x.GetType()))) + { + invocation.SkipBehaviors.Add(behavior.GetType()); + } + + return next().Invoke(invocation, next); + } + } +} diff --git a/src/Moq/Moq/Syntax.cs b/src/Moq/Moq/Syntax.cs index 6c0d1f87..93fd50f5 100644 --- a/src/Moq/Moq/Syntax.cs +++ b/src/Moq/Moq/Syntax.cs @@ -14,7 +14,7 @@ public static class Syntax /// Verifies all occurrence constraints on the given mock' setups, and /// allows further verification on the returned instance. /// - public static T Verify(T mock) => Moq.Verify.Called(mock); + public static T Verify(T mock) where T : class => Moq.Verify.Called(mock); /// /// Matches any value of the given type. diff --git a/src/Moq/Moq/Verify.cs b/src/Moq/Moq/Verify.cs index 8f791b0e..e03d9888 100644 --- a/src/Moq/Moq/Verify.cs +++ b/src/Moq/Moq/Verify.cs @@ -23,7 +23,7 @@ internal static bool IsVerifying(IMock mock) /// object too. /// /// An object that can be used to perform additional call verifications. - public static T Called(T target) => Calls(target); + public static T Called(T target) where T : class => Calls(target); /// /// Verifies a method invocation matching the was executed @@ -31,7 +31,7 @@ internal static bool IsVerifying(IMock mock) /// /// The method invocation to match against actual calls. /// Number of times the method should have been called. - public static void Called(Func function, int times) => Called(function, (Times)times); + public static void Called(Func function, int times) => CalledImpl(function, times); /// /// Verifies a method invocation matching the was called at @@ -39,7 +39,7 @@ internal static bool IsVerifying(IMock mock) /// /// The method invocation to match against actual calls. /// User message to show. - public static void Called(Func function, string message) => Called(function, default, message: message); + public static void Called(Func function, string message) => CalledImpl(function, default, message: message); /// /// Verifies a method invocation matching the was executed at @@ -49,7 +49,17 @@ internal static bool IsVerifying(IMock mock) /// Optional number of times the method should have been called. Defaults to . /// An integer value can also be specificed since there is built-in conversion support from integer to . /// Optional user message to show. - public static void Called(Func function, Times times = default, string message = null) + public static void Called(Func function, int times = -1, string message = null) => CalledImpl(function, times, message); + + /// + /// Verifies a method invocation matching the was executed at + /// least once. If is provided, the number of calls is verified too. + /// + /// The method invocation to match against actual calls. + /// Optional number of times the method should have been called. Defaults to . + /// An integer value can also be specificed since there is built-in conversion support from integer to . + /// Optional user message to show. + internal static void CalledImpl(Func function, Sdk.Times times = default, string message = null) { using (new SetupScope()) { @@ -68,7 +78,7 @@ public static void Called(Func function, Times times = default, string mes /// /// The method invocation to match against actual calls. /// Number of times the method should have been called. - public static void Called(Action action, int times) => Called(action, (Times)times); + public static void Called(Action action, int times) => CalledImpl(action, times); /// /// Verifies a method invocation matching the was executed at @@ -76,7 +86,17 @@ public static void Called(Func function, Times times = default, string mes /// /// The method invocation to match against actual calls. /// Optional user message to show. - public static void Called(Action action, string message) => Called(action, default, message: message); + public static void Called(Action action, string message) => CalledImpl(action, default, message: message); + + /// + /// Verifies a method invocation matching the was executed at + /// least once. If is provided, the number of calls is verified too. + /// + /// The method invocation to match against actual calls. + /// Optional number of times the method should have been called. Defaults to . + /// An integer value can also be specificed since there is built-in conversion support from integer to . + /// Optional user message to show. + public static void Called(Action action, int times = -1, string message = null) => CalledImpl(action, times, message); /// /// Verifies a method invocation matching the was executed at @@ -86,7 +106,7 @@ public static void Called(Func function, Times times = default, string mes /// Optional number of times the method should have been called. Defaults to . /// An integer value can also be specificed since there is built-in conversion support from integer to . /// Optional user message to show. - public static void Called(Action action, Times times = default, string message = null) + internal static void CalledImpl(Action action, Sdk.Times times = default, string message = null) { using (new SetupScope()) { @@ -105,21 +125,21 @@ public static void Called(Action action, Times times = default, string message = /// object too. /// /// An object that can be used to perform additional call verifications. - public static T NotCalled(T target) => GetVerifier(GetVerified(target), true); + public static T NotCalled(T target) where T : class => GetVerifier(GetVerified(target), true); /// /// Verifies a method invocation matching the was never called. /// /// The method invocation to match against actual calls. /// Optional user message to show. - public static void NotCalled(Func function, string message = null) => Called(function, Times.Never, message); + public static void NotCalled(Func function, string message = null) => CalledImpl(function, Sdk.Times.Never, message); /// /// Verifies a method invocation matching the was never called. /// /// The method invocation to match against actual calls. /// Optional user message to show. - public static void NotCalled(Action action, string message = null) => Called(action, Times.Never, message); + public static void NotCalled(Action action, string message = null) => CalledImpl(action, Sdk.Times.Never, message); /// /// Verifies all setups that had an occurrence constraint applied, @@ -127,7 +147,7 @@ public static void Called(Action action, Times times = default, string message = /// object too. /// /// An object that can be used to perform additional call verifications. - public static T Calls(T target) => GetVerifier(GetVerified(target)); + public static T Calls(T target) where T : class => GetVerifier(GetVerified(target)); /// /// Allows performing custom verification against all actual calls that match the @@ -167,7 +187,7 @@ public static void Calls(Action action, Action> c /// Gets the mock after verifying that all setups that specified occurrence /// constraints have succeeded. /// - static IMock GetVerified(T target) + static IMock GetVerified(T target) where T : class { var mock = target.AsMock(); var failures = (from pipeline in mock.Setups @@ -191,7 +211,7 @@ select pipeline.Setup /// Whether to add a behavior that verifies the invocations performed on /// the clone were never performed on the original mock. /// - static T GetVerifier(IMock mock, bool notCalled = false) + static T GetVerifier(IMock mock, bool notCalled = false) where T : class { // If the mock is already being verified, we don't need to clone again. if (mock.State.TryGetValue(typeof(Verify), out var verifying) && verifying) @@ -252,8 +272,6 @@ class TargetReplacerBehavior : IStuntBehavior public bool AppliesTo(IMethodInvocation invocation) => true; public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) => next().Invoke(invocation, next); - //public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) - // => next().Invoke(new MethodInvocation(target, invocation.MethodBase, invocation.Arguments.ToArray()), next); } } } diff --git a/src/Moq/Moq/VerifyExtension.cs b/src/Moq/Moq/VerifyExtension.cs index c4463a51..1b9aa0f9 100644 --- a/src/Moq/Moq/VerifyExtension.cs +++ b/src/Moq/Moq/VerifyExtension.cs @@ -39,8 +39,8 @@ public static void Verify(this T target, Action action, string message) /// Optional number of times the method should have been called. Defaults to . /// An integer value can also be specificed since there is built-in conversion support from integer to . /// Optional user message to show. - public static void Verify(this T target, Action action, Times times = default, string message = null) - => Moq.Verify.Called(() => action(target), times, message); + public static void Verify(this T target, Action action, int times = -1, string message = null) + => Moq.Verify.CalledImpl(() => action(target), (Sdk.Times)times, message); /// /// Verifies a method invocation matching the was executed the @@ -72,6 +72,6 @@ public static void Verify(this T target, Func function, /// An integer value can also be specificed since there is built-in conversion support from integer to . /// Optional user message to show. public static void Verify(this T target, Func function, int times = -1, string message = null) - => Moq.Verify.Called(() => function(target), times, message); + => Moq.Verify.CalledImpl(() => function(target), (Sdk.Times)times, message); } } diff --git a/src/Moq/Moq/contentFiles/cs/netstandard2.0/Mocks/Mock.Overloads.cs b/src/Moq/Moq/contentFiles/cs/netstandard2.0/Mocks/Mock.Overloads.cs index 064f2886..0e13ba51 100644 --- a/src/Moq/Moq/contentFiles/cs/netstandard2.0/Mocks/Mock.Overloads.cs +++ b/src/Moq/Moq/contentFiles/cs/netstandard2.0/Mocks/Mock.Overloads.cs @@ -14,47 +14,47 @@ partial class Mock [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(params object[] constructorArgs) => Create(MockBehavior.Loose, constructorArgs); + public static T Of(params object[] constructorArgs) where T : class => Create(MockBehavior.Loose, constructorArgs); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(params object[] constructorArgs) => Create(MockBehavior.Loose, constructorArgs, typeof(T1)); + public static T Of(params object[] constructorArgs) where T : class => Create(MockBehavior.Loose, constructorArgs, typeof(T1)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(params object[] constructorArgs) => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2)); + public static T Of(params object[] constructorArgs) where T : class => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(params object[] constructorArgs) => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3)); + public static T Of(params object[] constructorArgs) where T : class => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(params object[] constructorArgs) => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + public static T Of(params object[] constructorArgs) where T : class => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(params object[] constructorArgs) => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); + public static T Of(params object[] constructorArgs) where T : class => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(params object[] constructorArgs) => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); + public static T Of(params object[] constructorArgs) where T : class => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(params object[] constructorArgs) => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7)); + public static T Of(params object[] constructorArgs) where T : class => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(params object[] constructorArgs) => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8)); + public static T Of(params object[] constructorArgs) where T : class => Create(MockBehavior.Loose, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8)); /// /// Creates a mock that inherits or implements the type . @@ -62,46 +62,46 @@ partial class Mock [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(MockBehavior behavior, params object[] constructorArgs) => Create(behavior, constructorArgs); + public static T Of(MockBehavior behavior, params object[] constructorArgs) where T : class => Create(behavior, constructorArgs); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(MockBehavior behavior, params object[] constructorArgs) => Create(behavior, constructorArgs, typeof(T1)); + public static T Of(MockBehavior behavior, params object[] constructorArgs) where T : class => Create(behavior, constructorArgs, typeof(T1)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(MockBehavior behavior, params object[] constructorArgs) => Create(behavior, constructorArgs, typeof(T1), typeof(T2)); + public static T Of(MockBehavior behavior, params object[] constructorArgs) where T : class => Create(behavior, constructorArgs, typeof(T1), typeof(T2)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(MockBehavior behavior, params object[] constructorArgs) => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3)); + public static T Of(MockBehavior behavior, params object[] constructorArgs) where T : class => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(MockBehavior behavior, params object[] constructorArgs) => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + public static T Of(MockBehavior behavior, params object[] constructorArgs) where T : class => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(MockBehavior behavior, params object[] constructorArgs) => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); + public static T Of(MockBehavior behavior, params object[] constructorArgs) where T : class => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(MockBehavior behavior, params object[] constructorArgs) => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); + public static T Of(MockBehavior behavior, params object[] constructorArgs) where T : class => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(MockBehavior behavior, params object[] constructorArgs) => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7)); + public static T Of(MockBehavior behavior, params object[] constructorArgs) where T : class => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7)); [MockGenerator] [GeneratedCode("Moq", "5.0")] [CompilerGenerated] - public static T Of(MockBehavior behavior, params object[] constructorArgs) => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8)); + public static T Of(MockBehavior behavior, params object[] constructorArgs) where T : class => Create(behavior, constructorArgs, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8)); } } \ No newline at end of file diff --git a/src/Moq/Moq/contentFiles/cs/netstandard2.0/Mocks/Mock.cs b/src/Moq/Moq/contentFiles/cs/netstandard2.0/Mocks/Mock.cs index 2cefa4ea..62e2bd23 100644 --- a/src/Moq/Moq/contentFiles/cs/netstandard2.0/Mocks/Mock.cs +++ b/src/Moq/Moq/contentFiles/cs/netstandard2.0/Mocks/Mock.cs @@ -16,13 +16,13 @@ partial class Mock /// /// Gets the configuration and introspection for the given mocked instance. /// - public static IMoq Get(T instance) => new Moq(instance.AsMock()); + public static IMoq Get(T instance) where T : class => new Moq(instance.AsMock()); /// /// Creates the mock instance by using the specified types to /// lookup the mock type in the assembly defining this class. /// - private static T Create(MockBehavior behavior, object[] constructorArgs, params Type[] interfaces) + private static T Create(MockBehavior behavior, object[] constructorArgs, params Type[] interfaces) where T : class { var mocked = (IMocked)MockFactory.Default.CreateMock(typeof(Mock).GetTypeInfo().Assembly, typeof(T), interfaces, constructorArgs); diff --git a/src/Moq/Moq/contentFiles/vb/netstandard2.0/Mocks/Mock.Overloads.vb b/src/Moq/Moq/contentFiles/vb/netstandard2.0/Mocks/Mock.Overloads.vb index 8723e566..ffaff399 100644 --- a/src/Moq/Moq/contentFiles/vb/netstandard2.0/Mocks/Mock.Overloads.vb +++ b/src/Moq/Moq/contentFiles/vb/netstandard2.0/Mocks/Mock.Overloads.vb @@ -11,126 +11,126 @@ Namespace Global.Moq - Public Shared Function [Of](Of T)(ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(MockBehavior.Loose, constructorArgs) End Function - Public Shared Function [Of](Of T, T1)(ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(MockBehavior.Loose, constructorArgs, GetType(T1)) End Function - Public Shared Function [Of](Of T, T1, T2)(ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(MockBehavior.Loose, constructorArgs, GetType(T1), GetType(T2)) End Function - Public Shared Function [Of](Of T, T1, T2, T3)(ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(MockBehavior.Loose, constructorArgs, GetType(T1), GetType(T2), GetType(T3)) End Function - Public Shared Function [Of](Of T, T1, T2, T3, T4)(ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3, T4)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(MockBehavior.Loose, constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4)) End Function - Public Shared Function [Of](Of T, T1, T2, T3, T4, T5)(ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3, T4, T5)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(MockBehavior.Loose, constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5)) End Function - Public Shared Function [Of](Of T, T1, T2, T3, T4, T5, T6)(ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3, T4, T5, T6)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(MockBehavior.Loose, constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6)) End Function - Public Shared Function [Of](Of T, T1, T2, T3, T4, T5, T6, T7)(ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3, T4, T5, T6, T7)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(MockBehavior.Loose, constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6), GetType(T7)) End Function - Public Shared Function [Of](Of T, T1, T2, T3, T4, T5, T6, T7, T8)(ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3, T4, T5, T6, T7, T8)(ParamArray constructorArgs As Object()) As T Return Create(Of T)(MockBehavior.Loose, constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6), GetType(T7), GetType(T8)) End Function - Public Shared Function [Of](Of T)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T Return Create(Of T)(behavior, constructorArgs) End Function - Public Shared Function [Of](Of T, T1)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T Return Create(Of T)(behavior, constructorArgs, GetType(T1)) End Function - Public Shared Function [Of](Of T, T1, T2)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T Return Create(Of T)(behavior, constructorArgs, GetType(T1), GetType(T2)) End Function - Public Shared Function [Of](Of T, T1, T2, T3)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T Return Create(Of T)(behavior, constructorArgs, GetType(T1), GetType(T2), GetType(T3)) End Function - Public Shared Function [Of](Of T, T1, T2, T3, T4)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3, T4)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T Return Create(Of T)(behavior, constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4)) End Function - Public Shared Function [Of](Of T, T1, T2, T3, T4, T5)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3, T4, T5)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T Return Create(Of T)(MockBehavior.Loose, constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5)) End Function - Public Shared Function [Of](Of T, T1, T2, T3, T4, T5, T6)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3, T4, T5, T6)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T Return Create(Of T)(behavior, constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6)) End Function - Public Shared Function [Of](Of T, T1, T2, T3, T4, T5, T6, T7)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3, T4, T5, T6, T7)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T Return Create(Of T)(behavior, constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6), GetType(T7)) End Function - Public Shared Function [Of](Of T, T1, T2, T3, T4, T5, T6, T7, T8)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T + Public Shared Function [Of](Of T As Class, T1, T2, T3, T4, T5, T6, T7, T8)(ByVal behavior As MockBehavior, ParamArray constructorArgs As Object()) As T Return Create(Of T)(behavior, constructorArgs, GetType(T1), GetType(T2), GetType(T3), GetType(T4), GetType(T5), GetType(T6), GetType(T7), GetType(T8)) End Function diff --git a/src/Moq/Moq/contentFiles/vb/netstandard2.0/Mocks/Mock.vb b/src/Moq/Moq/contentFiles/vb/netstandard2.0/Mocks/Mock.vb index 4114e3fc..e175d4d5 100644 --- a/src/Moq/Moq/contentFiles/vb/netstandard2.0/Mocks/Mock.vb +++ b/src/Moq/Moq/contentFiles/vb/netstandard2.0/Mocks/Mock.vb @@ -12,7 +12,7 @@ Namespace Global.Moq Partial Friend Class Mock - Private Shared Function Create(Of T)(ByVal behavior As MockBehavior, ByVal constructorArgs As Object(), ParamArray interfaces As Type()) As T + Private Shared Function Create(Of T As Class)(ByVal behavior As MockBehavior, ByVal constructorArgs As Object(), ParamArray interfaces As Type()) As T Dim mocked = DirectCast(MockFactory.[Default].CreateMock(GetType(Mock).GetTypeInfo().Assembly, GetType(T), interfaces, constructorArgs), IMocked) mocked.Initialize(behavior) diff --git a/src/Packages.props b/src/Packages.props index b18df949..d12f80fb 100644 --- a/src/Packages.props +++ b/src/Packages.props @@ -20,13 +20,14 @@ - - + + - + + diff --git a/src/Samples/Sample/Calculator.cs b/src/Samples/Sample/Calculator.cs index 81ce8bd5..a4c5d073 100644 --- a/src/Samples/Sample/Calculator.cs +++ b/src/Samples/Sample/Calculator.cs @@ -10,6 +10,8 @@ public class Calculator : ICalculator, IDisposable public virtual event EventHandler TurnedOn; + public bool TurnOnCalled { get; set; } + public virtual bool IsOn { get; private set; } public virtual CalculatorMode Mode { get; set; } @@ -22,6 +24,7 @@ public virtual void TurnOn() { TurnedOn?.Invoke(this, EventArgs.Empty); IsOn = true; + TurnOnCalled = true; } public virtual int? this[string name] diff --git a/src/Samples/Sample/CalculatorClassStunt.cs b/src/Samples/Sample/CalculatorClassStunt.cs index 7b2557ef..09c39f3f 100644 --- a/src/Samples/Sample/CalculatorClassStunt.cs +++ b/src/Samples/Sample/CalculatorClassStunt.cs @@ -29,6 +29,8 @@ public override int? this[string name] set => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), name, value), (m, n) => { base[name] = value; return m.CreateValueReturn(null, value); }); } + public override bool IsOn => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod()), (m, n) => m.CreateValueReturn(base.IsOn)); + public override int Add(int x, int y) => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), x, y), (m, n) => m.CreateValueReturn(base.Add(x, y), x, y)); @@ -61,11 +63,9 @@ public override void Store(string name, int value) => public override int? Recall(string name) => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), name), (m, n) => m.CreateValueReturn(base.Recall(name), name)); - public override void Clear(string name) => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod(), name), (m, n) => { base.Clear(name); return m.CreateValueReturn(null, name); }); - public override ICalculatorMemory Memory { get => pipeline.Execute(new MethodInvocation(this, MethodBase.GetCurrentMethod()), (m, n) => m.CreateValueReturn(base.Memory)); diff --git a/src/Stunts/Stunts.CodeAnalysis/NamingConvention.cs b/src/Stunts/Stunts.CodeAnalysis/NamingConvention.cs index 55bf90d0..880d5346 100644 --- a/src/Stunts/Stunts.CodeAnalysis/NamingConvention.cs +++ b/src/Stunts/Stunts.CodeAnalysis/NamingConvention.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text; using Microsoft.CodeAnalysis; namespace Stunts @@ -12,28 +13,38 @@ public class NamingConvention /// /// The namespace of the generated code. /// - public virtual string Namespace => StuntNaming.Namespace; + public virtual string Namespace => StuntNaming.DefaultNamespace; /// /// Suffix appended to the type name, i.e. IFooStunt. /// - public virtual string NameSuffix => StuntNaming.NameSuffix; + public virtual string NameSuffix => StuntNaming.DefaultSuffix; /// /// The type name to generate for the given (optional) base type and implemented interfaces. /// - public string GetName(IEnumerable symbols) - // NOTE: we sort the names the same way the StuntGenerator.ValidateTypes does. - // There should be another analyzer that forces the first T to be the one with - // a class type, if any. - => string.Join("", symbols - .Where(x => x?.TypeKind == TypeKind.Class) - .Concat(symbols - .Where(x => x?.TypeKind == TypeKind.Interface) - .OrderBy(x => x.Name)) - .Select(x => x?.Name) - .Where(x => x != null)) + - NameSuffix; + public string GetName(IEnumerable symbols) + { + var builder = new StringBuilder(); + // First add the base class + AddNames(builder, symbols.Where(x => x.TypeKind == TypeKind.Class)); + // Then the interfaces + AddNames(builder, symbols.Where(x => x.TypeKind == TypeKind.Interface).OrderBy(x => x.Name)); + return builder.Append(NameSuffix).ToString(); + } + + static void AddNames(StringBuilder builder, IEnumerable symbols) + { + foreach (var symbol in symbols) + { + builder.Append(symbol.Name); + if (symbol is INamedTypeSymbol named && named.IsGenericType) + { + builder.Append("Of"); + AddNames(builder, named.TypeArguments); + } + } + } /// /// The full type name for the given (optional) base type and implemented interfaces. diff --git a/src/Stunts/Stunts.CodeAnalysis/StuntGeneratorAnalyzer.cs b/src/Stunts/Stunts.CodeAnalysis/StuntGeneratorAnalyzer.cs index 34b109fb..b6b59d3f 100644 --- a/src/Stunts/Stunts.CodeAnalysis/StuntGeneratorAnalyzer.cs +++ b/src/Stunts/Stunts.CodeAnalysis/StuntGeneratorAnalyzer.cs @@ -65,6 +65,7 @@ public sealed override ImmutableArray SupportedDiagnostics public override void Initialize(AnalysisContext context) { context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, Microsoft.CodeAnalysis.CSharp.SyntaxKind.InvocationExpression); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, Microsoft.CodeAnalysis.CSharp.SyntaxKind.ObjectCreationExpression); context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.SimpleMemberAccessExpression); } @@ -87,25 +88,46 @@ private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) } var method = (IMethodSymbol)symbol.Symbol; + ImmutableArray typeArguments = default; + if (!method.GetAttributes().Any(x => x.AttributeClass == generator)) + return; + + if (method.MethodKind == MethodKind.Constructor) + { + if (method.ReceiverType is INamedTypeSymbol owner && + owner.IsGenericType) + { + typeArguments = owner.TypeArguments; + } + else + { + return; + } + } + else + { + typeArguments = method.TypeArguments; + } + if (method.GetAttributes().Any(x => x.AttributeClass == generator) && // We don't generate anything if generator is applied to a non-generic method. - !method.TypeArguments.IsDefaultOrEmpty && - method.TypeArguments.TryValidateGeneratorTypes(out _)) + !typeArguments.IsDefaultOrEmpty && + typeArguments.TryValidateGeneratorTypes(out _)) { - var name = naming.GetFullName(method.TypeArguments.OfType()); + var name = naming.GetFullName(typeArguments.OfType()); var compilationErrors = new Lazy(() => context.Compilation.GetCompilationErrors()); HashSet recursiveSymbols; if (recursive) { // Collect recursive symbols to generate/update as needed. - recursiveSymbols = new HashSet(method.TypeArguments.OfType().InterceptableRecursively() + recursiveSymbols = new HashSet(typeArguments.OfType().InterceptableRecursively() .Where(x => { 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 { @@ -122,8 +144,8 @@ private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) new Dictionary { { "TargetFullName", name }, - { "Symbols", string.Join("|", method.TypeArguments - .OfType().Select(x => x.ToFullMetadataName())) }, + { "Symbols", string.Join("|", typeArguments + .OfType().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 @@ -174,8 +196,8 @@ private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) { { "TargetFullName", name }, { "Location", location }, - { "Symbols", string.Join("|", method.TypeArguments - .OfType().Select(x => x.ToFullMetadataName())) }, + { "Symbols", string.Join("|", typeArguments + .OfType().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. diff --git a/src/Stunts/Stunts.CodeAnalysis/Stunts.CodeAnalysis.csproj b/src/Stunts/Stunts.CodeAnalysis/Stunts.CodeAnalysis.csproj index 96a12122..c83b0e71 100644 --- a/src/Stunts/Stunts.CodeAnalysis/Stunts.CodeAnalysis.csproj +++ b/src/Stunts/Stunts.CodeAnalysis/Stunts.CodeAnalysis.csproj @@ -6,6 +6,7 @@ Stunts true Analyzers + true @@ -21,6 +22,10 @@ + + + + True diff --git a/src/Stunts/Stunts.CodeAnalysis/Stunts.CodeAnalysis.targets b/src/Stunts/Stunts.CodeAnalysis/Stunts.CodeAnalysis.targets new file mode 100644 index 00000000..edfe013e --- /dev/null +++ b/src/Stunts/Stunts.CodeAnalysis/Stunts.CodeAnalysis.targets @@ -0,0 +1,19 @@ + + + + + %(PackageReference.Identity) + + + + + + + + + \ No newline at end of file diff --git a/src/Stunts/Stunts.CodeAnalysis/SymbolExtensions.cs b/src/Stunts/Stunts.CodeAnalysis/SymbolExtensions.cs index b0a32e2d..0a1b62ae 100644 --- a/src/Stunts/Stunts.CodeAnalysis/SymbolExtensions.cs +++ b/src/Stunts/Stunts.CodeAnalysis/SymbolExtensions.cs @@ -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; @@ -107,6 +110,8 @@ public static bool TryValidateGeneratorTypes(this IEnumerable 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); if (symbols[0].TypeKind == TypeKind.Class) @@ -136,24 +141,14 @@ public static bool TryValidateGeneratorTypes(this IEnumerable types } /// - /// 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. /// 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)); - - /// - /// Gets the full metadata name of the given symbol. - /// - public static string ToFullMetadataName(this INamedTypeSymbol symbol) - => (symbol.ContainingNamespace == null || symbol.ContainingNamespace.IsGlobalNamespace ? - "" : symbol.ContainingNamespace + ".") + - (symbol.ContainingType != null ? - symbol.ContainingType.MetadataName + "+" : "") + - symbol.MetadataName; } } diff --git a/src/Stunts/Stunts.CodeAnalysis/SymbolFullNameExtensions.cs b/src/Stunts/Stunts.CodeAnalysis/SymbolFullNameExtensions.cs new file mode 100644 index 00000000..a351137e --- /dev/null +++ b/src/Stunts/Stunts.CodeAnalysis/SymbolFullNameExtensions.cs @@ -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 +{ + /// + /// Provides uniform rendering and resolving of symbols from a full name. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class SymbolFullNameExtensions + { + static readonly SymbolDisplayFormat fullNameFormat = new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.ExpandNullable); + + /// + /// Gets the full name for the symbol, which then be used with + /// to resolve it + /// back to the original symbol. + /// + public static string ToFullName(this ITypeSymbol symbol) => symbol.ToDisplayString(fullNameFormat); + + /// + /// Resolves a symbol given its full name, as returned by . + /// + public static ITypeSymbol GetTypeByFullName(this Compilation compilation, string symbolFullName) + => new SymbolResolver(compilation).Resolve(symbolFullName ?? throw new ArgumentNullException(nameof(symbolFullName))); + + class SymbolResolver + { + TextParser symbolArguments; + TextParser typeSymbol; + Compilation compilation; + + public SymbolResolver(Compilation compilation) => this.compilation = compilation; + + static TextParser Identifier { get; } = + from first in Character.Letter + from rest in Character.LetterOrDigit.Or(Character.EqualTo('_')).Many() + select first + new string(rest); + + static TextParser ArrayRank { get; } = + from open in Character.EqualTo('[') + from dimensions in Character.EqualTo(',').Many() + from close in Character.EqualTo(']') + select dimensions.Length + 1; + + static TextParser FullName { get; } = + from identifiers in Identifier.ManyDelimitedBy(Character.EqualTo('.').Or(Character.EqualTo('+'))) + select string.Join(".", identifiers); + + TextParser 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 TypeSymbol => LazyInitializer.EnsureInitialized(ref typeSymbol, () => + from name in FullName + from dimensions in ArrayRank.OptionalOrDefault() + from arguments in SymbolArguments.OptionalOrDefault(Array.Empty()) + 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; + } + } + } +} diff --git a/src/Stunts/Stunts.CodeFix/CustomStuntCodeFixProvider.cs b/src/Stunts/Stunts.CodeFix/CustomStuntCodeFixProvider.cs index 954fcb9c..769bb5e1 100644 --- a/src/Stunts/Stunts.CodeFix/CustomStuntCodeFixProvider.cs +++ b/src/Stunts/Stunts.CodeFix/CustomStuntCodeFixProvider.cs @@ -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. diff --git a/src/Stunts/Stunts.Sdk/OverrideAllMembersCodeFix.cs b/src/Stunts/Stunts.CodeFix/OverrideAllMembersCodeFix.cs similarity index 96% rename from src/Stunts/Stunts.Sdk/OverrideAllMembersCodeFix.cs rename to src/Stunts/Stunts.CodeFix/OverrideAllMembersCodeFix.cs index 833cc0be..d6fd00cb 100644 --- a/src/Stunts/Stunts.Sdk/OverrideAllMembersCodeFix.cs +++ b/src/Stunts/Stunts.CodeFix/OverrideAllMembersCodeFix.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -14,6 +13,7 @@ namespace Stunts /// Implements the codefix for overriding all members in a class /// using . /// + [ExportCodeFixProvider(LanguageNames.CSharp, new[] { LanguageNames.VisualBasic }, Name = nameof(OverrideAllMembersCodeFix))] class OverrideAllMembersCodeFix : CodeFixProvider { public sealed override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(OverridableMembersAnalyzer.DiagnosticId); diff --git a/src/Stunts/Stunts.CodeFix/Properties/Resources.Designer.cs b/src/Stunts/Stunts.CodeFix/Properties/Resources.Designer.cs index ce5f9198..97504875 100644 --- a/src/Stunts/Stunts.CodeFix/Properties/Resources.Designer.cs +++ b/src/Stunts/Stunts.CodeFix/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Stunts.Properties { // 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.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -132,15 +132,6 @@ internal static string DuplicateBaseType_Title { } } - /// - /// Looks up a localized string similar to Generate all stunts in {0} {1}. - /// - internal static string GenerateStuntCodeFix_FixAllTitle { - get { - return ResourceManager.GetString("GenerateStuntCodeFix_FixAllTitle", resourceCulture); - } - } - /// /// Looks up a localized string similar to Generate Stunt. /// @@ -258,6 +249,24 @@ internal static string SealedBaseType_Title { } } + /// + /// Looks up a localized string similar to Fix {0}. + /// + internal static string StuntCodeAction_Title { + get { + return ResourceManager.GetString("StuntCodeAction_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fix all {0} in {1}. + /// + internal static string StuntFixAllProvider_Title { + get { + return ResourceManager.GetString("StuntFixAllProvider_Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to Generating code for nested types is not supported yet.. /// @@ -285,15 +294,6 @@ internal static string UnsupportedNestedTypeAnalyzer_Title { } } - /// - /// Looks up a localized string similar to Update all stunts in {0} {1}. - /// - internal static string UpdateStuntCodeFix_FixAllTitle { - get { - return ResourceManager.GetString("UpdateStuntCodeFix_FixAllTitle", resourceCulture); - } - } - /// /// Looks up a localized string similar to Update Stunt. /// diff --git a/src/Stunts/Stunts.CodeFix/Properties/Resources.resx b/src/Stunts/Stunts.CodeFix/Properties/Resources.resx index 9b85d860..880840fd 100644 --- a/src/Stunts/Stunts.CodeFix/Properties/Resources.resx +++ b/src/Stunts/Stunts.CodeFix/Properties/Resources.resx @@ -141,9 +141,6 @@ Multiple base types specified - - Generate all stunts in {0} {1} - Generate Stunt @@ -183,6 +180,12 @@ Invalid sealed base type + + Fix {0} + + + Fix all {0} in {1} + Generating code for nested types is not supported yet. @@ -192,9 +195,6 @@ Unsupported nested type - - Update all stunts in {0} {1} - Update Stunt diff --git a/src/Stunts/Stunts.CodeFix/StuntCodeAction.cs b/src/Stunts/Stunts.CodeFix/StuntCodeAction.cs index 82675dea..a1979593 100644 --- a/src/Stunts/Stunts.CodeFix/StuntCodeAction.cs +++ b/src/Stunts/Stunts.CodeFix/StuntCodeAction.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Simplification; +using Stunts.Properties; namespace Stunts { @@ -22,6 +23,14 @@ public class StuntCodeAction : CodeAction readonly Diagnostic diagnostic; readonly NamingConvention naming; + /// + /// Initializes the action. + /// + public StuntCodeAction(Document document, Diagnostic diagnostic, NamingConvention naming) + : this(Resources.StuntCodeAction_Title, document, diagnostic, naming) + { + } + /// /// Initializes the action. /// @@ -34,7 +43,9 @@ public StuntCodeAction(string title, Document document, Diagnostic diagnostic, N } /// - public override string EquivalenceKey => diagnostic.Id + ":" + diagnostic.Properties["TargetFullName"]; + //public override string EquivalenceKey => diagnostic.Id + ":" + diagnostic.Properties["TargetFullName"]; + + public override string EquivalenceKey => diagnostic.Id; /// public override string Title => title; @@ -58,10 +69,16 @@ protected override async Task 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(); + var name = naming.GetFullName(symbols); + // Check if the compilation already contains the type, which will be + // the case in batch fixing + if (compilation.GetTypeByMetadataName(name) != null) + return document; + var generator = SyntaxGenerator.GetGenerator(document.Project); var stunts = CreateGenerator(naming); diff --git a/src/Stunts/Stunts.CodeFix/StuntCodeFixProvider.cs b/src/Stunts/Stunts.CodeFix/StuntCodeFixProvider.cs index e0e31bdd..d90baf00 100644 --- a/src/Stunts/Stunts.CodeFix/StuntCodeFixProvider.cs +++ b/src/Stunts/Stunts.CodeFix/StuntCodeFixProvider.cs @@ -78,6 +78,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) /// // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers //public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - public override FixAllProvider GetFixAllProvider() => null; + public override FixAllProvider GetFixAllProvider() + => new StuntFixAllProvider(FixableDiagnosticIds.First(), CreateCodeAction); } } \ No newline at end of file diff --git a/src/Stunts/Stunts.CodeFix/StuntFixAllProvider.cs b/src/Stunts/Stunts.CodeFix/StuntFixAllProvider.cs index 5b3a9e72..df574894 100644 --- a/src/Stunts/Stunts.CodeFix/StuntFixAllProvider.cs +++ b/src/Stunts/Stunts.CodeFix/StuntFixAllProvider.cs @@ -1,23 +1,28 @@ -using System.Collections.Generic; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; +using Stunts.Properties; namespace Stunts { class StuntFixAllProvider : FixAllProvider { - string titleFormat; - string diagnosticId; + readonly string diagnosticId; + readonly Func codeActionFactory; - public StuntFixAllProvider(string titleFormat, string diagnosticId) + public StuntFixAllProvider(string diagnosticId, Func codeActionFactory) { - this.titleFormat = titleFormat; this.diagnosticId = diagnosticId; + this.codeActionFactory = codeActionFactory; } public override async Task GetFixAsync(FixAllContext fixAllContext) @@ -34,7 +39,7 @@ public override async Task GetFixAsync(FixAllContext fixAllContext) if (diagnostics.Length > 0) diagnosticsToFix.Add(new KeyValuePair>(fixAllContext.Project, diagnostics)); - title = string.Format(titleFormat, "document", fixAllContext.Document.Name); + title = string.Format(Resources.StuntFixAllProvider_Title, diagnosticId, fixAllContext.Document.Name); break; } @@ -45,7 +50,7 @@ public override async Task GetFixAsync(FixAllContext fixAllContext) if (diagnostics.Length > 0) diagnosticsToFix.Add(new KeyValuePair>(project, diagnostics)); - title = string.Format(titleFormat, "project", fixAllContext.Project.Name); + title = string.Format(Resources.StuntFixAllProvider_Title, diagnosticId, fixAllContext.Project.Name); break; } @@ -58,7 +63,10 @@ public override async Task GetFixAsync(FixAllContext fixAllContext) diagnosticsToFix.Add(new KeyValuePair>(project, diagnostics)); } - title = string.Format(titleFormat, "solution", ""); + title = string.Format(Resources.StuntFixAllProvider_Title, diagnosticId, + fixAllContext.Solution.FilePath == null ? + nameof(FixAllScope.Solution) : + Path.GetFileName(fixAllContext.Solution.FilePath)); break; } @@ -68,48 +76,122 @@ public override async Task GetFixAsync(FixAllContext fixAllContext) break; } - return new FixAllProxiesCodeAction(title, fixAllContext.Solution, diagnosticsToFix); + return new FixAllProxiesCodeAction(title, fixAllContext.Solution, diagnosticsToFix, codeActionFactory); } class FixAllProxiesCodeAction : CodeAction { - string title; - Solution solution; - List>> diagnosticsToFix; - - public FixAllProxiesCodeAction(string title, Solution solution, List>> diagnosticsToFix) + readonly string title; + readonly Solution solution; + readonly List>> diagnosticsToFix; + readonly Func codeActionFactory; + + public FixAllProxiesCodeAction(string title, + Solution solution, + List>> diagnosticsToFix, + Func codeActionFactory) { this.title = title; this.solution = solution; this.diagnosticsToFix = diagnosticsToFix; + this.codeActionFactory = codeActionFactory; } public override string Title => title; protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken) { - var changedSolution = solution; + var currentSolution = solution; + var fixerTasks = new List(); + var addedDocs = new ConcurrentBag(); + var updatedDocs = new ConcurrentBag(); + foreach (var pair in diagnosticsToFix) { - var project = changedSolution.GetProject(pair.Key.Id); + var project = solution.GetProject(pair.Key.Id); + Debug.Assert(project != null, "Failed to get project from solution."); var diagnostics = pair.Value; - var group = diagnostics.GroupBy(d => d.Properties["Name"]); + var group = diagnostics.GroupBy(d => d.Properties["TargetFullName"]); foreach (var diag in group) { var diagnostic = diag.First(); - var document = project.GetDocument(diagnostic.Location.SourceTree); - var codeAction = new StuntCodeAction(Title, document, diagnostic, new NamingConvention()); - - var operations = await codeAction.GetOperationsAsync(cancellationToken); - ApplyChangesOperation operation; - if ((operation = operations.OfType().FirstOrDefault()) != null) - changedSolution = operation.ChangedSolution; + var document = diagnostic.Location.IsInSource ? + project.Documents.FirstOrDefault(doc => doc.FilePath == diagnostic.Location.SourceTree.FilePath) : + project.GetDocument(diagnostic.Location.SourceTree); + + Debug.Assert(document != null, "Failed to locate document from diagnostic."); + + fixerTasks.Add(new CodeFixer(document, diagnostic, addedDocs, updatedDocs, codeActionFactory) + .RunAsync(cancellationToken)); } } - return changedSolution; + await Task.WhenAll(fixerTasks).ConfigureAwait(false); + + foreach (var addedDoc in addedDocs) + { + var addedText = await addedDoc.GetTextAsync(cancellationToken).ConfigureAwait(false); + var addedVersion = await addedDoc.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); + currentSolution = currentSolution.AddDocument(DocumentInfo.Create( + addedDoc.Id, addedDoc.Name, addedDoc.Folders, + addedDoc.SourceCodeKind, + TextLoader.From(TextAndVersion.Create(addedText, addedVersion, addedDoc.FilePath)), + addedDoc.FilePath)); + } + + foreach (var updatedDoc in updatedDocs) + { + var updatedText = await updatedDoc.GetTextAsync(cancellationToken).ConfigureAwait(false); + currentSolution = currentSolution.WithDocumentText(updatedDoc.Id, updatedText); + } + + return currentSolution; + } + + class CodeFixer + { + private readonly Document document; + private readonly Diagnostic diagnostic; + private readonly ConcurrentBag addedDocs; + private readonly ConcurrentBag updatedDocs; + private readonly Func codeActionFactory; + + public CodeFixer(Document document, Diagnostic diagnostic, ConcurrentBag addedDocs, ConcurrentBag updatedDocs, Func codeActionFactory) + { + this.document = document; + this.diagnostic = diagnostic; + this.addedDocs = addedDocs; + this.updatedDocs = updatedDocs; + this.codeActionFactory = codeActionFactory; + } + + public async Task RunAsync(CancellationToken cancellationToken) + { + // NOTE: stunts don't need to update the source document where the diagnostic + // was reported, so we don't need any of the document updating stuff that + // we need when applying code fixers in the moq/stunt codegen itself on its own + // document. So we just apply the workspace changes and that's it. + var codeAction = codeActionFactory(document, diagnostic); + var operations = await codeAction.GetOperationsAsync(cancellationToken).ConfigureAwait(false); + var applyChanges = operations.OfType().FirstOrDefault(); + if (applyChanges != null) + { + var changes = applyChanges.ChangedSolution.GetChanges(document.Project.Solution); + foreach (var change in changes.GetProjectChanges()) + { + foreach (var addedId in change.GetAddedDocuments()) + { + addedDocs.Add(applyChanges.ChangedSolution.GetDocument(addedId)); + } + foreach (var changedId in change.GetChangedDocuments(true)) + { + updatedDocs.Add(applyChanges.ChangedSolution.GetDocument(changedId)); + } + } + } + } } } } -} +} \ No newline at end of file diff --git a/src/Stunts/Stunts.CodeFix/Stunts.CodeFix.csproj b/src/Stunts/Stunts.CodeFix/Stunts.CodeFix.csproj index fb7a238a..fae31ce7 100644 --- a/src/Stunts/Stunts.CodeFix/Stunts.CodeFix.csproj +++ b/src/Stunts/Stunts.CodeFix/Stunts.CodeFix.csproj @@ -15,6 +15,10 @@ PrimaryOutputKind=$(PrimaryOutputKind);IncludeApi=false + + + + diff --git a/src/Stunts/Stunts.CodeFix/Stunts.props b/src/Stunts/Stunts.CodeFix/Stunts.props index 55314d80..f739678a 100644 --- a/src/Stunts/Stunts.CodeFix/Stunts.props +++ b/src/Stunts/Stunts.CodeFix/Stunts.props @@ -10,8 +10,8 @@ - - + + @@ -23,5 +23,4 @@ - \ No newline at end of file diff --git a/src/Stunts/Stunts.Sdk/DocumentExtensions.cs b/src/Stunts/Stunts.Sdk/DocumentExtensions.cs index 889b8424..57b2bbf2 100644 --- a/src/Stunts/Stunts.Sdk/DocumentExtensions.cs +++ b/src/Stunts/Stunts.Sdk/DocumentExtensions.cs @@ -93,8 +93,24 @@ static async Task> GetCodeFixes( if (analyzers.IsDefaultOrEmpty) analyzers = builtInAnalyzers.Value; - var analyerCompilation = compilation.WithAnalyzers(analyzers, cancellationToken: cancellationToken); - var allDiagnostics = await analyerCompilation.GetAllDiagnosticsAsync(cancellationToken); + var supportedAnalyers = analyzers + .Where(a => a.SupportedDiagnostics.Any(d => provider.FixableDiagnosticIds.Contains(d.Id))) + .ToImmutableArray(); + + var allDiagnostics = default(ImmutableArray); + + // This may be a compiler warning/error, not an analyzer-produced one, such as + // the missing abstract method implementations. + if (supportedAnalyers.IsEmpty) + { + allDiagnostics = compilation.GetDiagnostics(cancellationToken); + } + else + { + var analyerCompilation = compilation.WithAnalyzers(supportedAnalyers, cancellationToken: cancellationToken); + allDiagnostics = await analyerCompilation.GetAllDiagnosticsAsync(cancellationToken); + } + var diagnostics = allDiagnostics .Where(x => provider.FixableDiagnosticIds.Contains(x.Id)) // Only consider the diagnostics raised by the target document. @@ -111,12 +127,23 @@ await provider.RegisterCodeFixesAsync( cancellationToken)); } - return codeFixes.ToImmutableArray(); + var finalFixes = new List(); + + // All code actions without equivalence keys must be applied individually. + finalFixes.AddRange(codeFixes.Where(x => x.Action.EquivalenceKey == null)); + // All code actions with the same equivalence key should be applied only once. + finalFixes.AddRange(codeFixes + .Where(x => x.Action.EquivalenceKey != null) + .GroupBy(x => x.Action.EquivalenceKey) + .Select(x => x.First())); + + return finalFixes.ToImmutableArray(); } + // Debug view of all available providers and their metadata + // document.Project.Solution.Workspace.Services.HostServices.GetExports>().OrderBy(x => x.Metadata["Name"]?.ToString()).Select(x => $"{x.Metadata["Name"]}: {string.Join(", ", (string[])x.Metadata["Languages"])}" ).ToList() static CodeFixProvider GetCodeFixProvider(Document document, string codeFixName) - => codeFixName == nameof(OverrideAllMembersCodeFix) ? new OverrideAllMembersCodeFix() : - document.Project.Solution.Workspace.Services.HostServices + => document.Project.Solution.Workspace.Services.HostServices .GetExports>() .Where(x => x.Metadata.ContainsKey("Languages") && x.Metadata.ContainsKey("Name") && diff --git a/src/Stunts/Stunts.Sdk/OverridableMembersAnalyzer.cs b/src/Stunts/Stunts.Sdk/OverridableMembersAnalyzer.cs index 1d28bb8a..f65c570d 100644 --- a/src/Stunts/Stunts.Sdk/OverridableMembersAnalyzer.cs +++ b/src/Stunts/Stunts.Sdk/OverridableMembersAnalyzer.cs @@ -1,10 +1,8 @@ using System.Collections.Immutable; using System.ComponentModel; using System.Linq; -using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Shared.Extensions; namespace Stunts { diff --git a/src/Stunts/Stunts.Sdk/Processors/CodeFixNames.g.cs b/src/Stunts/Stunts.Sdk/Processors/CodeFixNames.g.cs index fca45675..3ed258a5 100644 --- a/src/Stunts/Stunts.Sdk/Processors/CodeFixNames.g.cs +++ b/src/Stunts/Stunts.Sdk/Processors/CodeFixNames.g.cs @@ -21,12 +21,18 @@ public static partial class All public const string ConvertToAsync = "ConvertToAsync"; public const string ConvertToIterator = "ConvertToIterator"; public const string CorrectNextControlVariable = "CorrectNextControlVariable"; + public const string CSharpAddObsoleteAttributeCodeFixProvider = "CSharpAddObsoleteAttributeCodeFixProvider"; + public const string CSharpConvertAnonymousTypeToTupleCodeFixProvider = "CSharpConvertAnonymousTypeToTupleCodeFixProvider"; public const string CSharpUseAutoPropertyCodeFixProvider = "CSharpUseAutoPropertyCodeFixProvider"; + public const string DeclareAsNullable = "DeclareAsNullable"; + public const string FixFormatting = "FixFormatting"; public const string FixIncorrectExitContinue = "FixIncorrectExitContinue"; public const string FixIncorrectFunctionReturnType = "FixIncorrectFunctionReturnType"; + public const string FixReturnType = "FixReturnType"; public const string FullyQualify = "FullyQualify"; public const string GenerateConstructor = "GenerateConstructor"; public const string GenerateConversion = "GenerateConversion"; + public const string GenerateDeconstructMethod = "GenerateDeconstructMethod"; public const string GenerateEndConstruct = "GenerateEndConstruct"; public const string GenerateEnumMember = "GenerateEnumMember"; public const string GenerateEvent = "GenerateEvent"; @@ -38,7 +44,10 @@ public static partial class All public const string InsertMissingCast = "InsertMissingCast"; public const string InvokeDelegateWithConditionalAccessCodeFixProvider = "InvokeDelegateWithConditionalAccessCodeFixProvider"; public const string MakeFieldReadonly = "MakeFieldReadonly"; + public const string MakeLocalFunctionStaticCodeFixProvider = "MakeLocalFunctionStaticCodeFixProvider"; public const string MakeMethodSynchronous = "MakeMethodSynchronous"; + public const string MakeStatementAsynchronous = "MakeStatementAsynchronous"; + public const string MakeStructFieldsWritable = "MakeStructFieldsWritable"; public const string MoveToTopOfFile = "MoveToTopOfFile"; public const string PopulateSwitch = "PopulateSwitch"; public const string PreferFrameworkType = "PreferFrameworkType"; @@ -48,15 +57,23 @@ public static partial class All public const string RemoveUnnecessaryImports = "RemoveUnnecessaryImports"; public const string RemoveUnreachableCode = "RemoveUnreachableCode"; public const string RemoveUnusedLocalFunction = "RemoveUnusedLocalFunction"; + public const string RemoveUnusedMembers = "RemoveUnusedMembers"; + public const string RemoveUnusedValues = "RemoveUnusedValues"; public const string RemoveUnusedVariable = "RemoveUnusedVariable"; + public const string ReplaceDefaultLiteral = "ReplaceDefaultLiteral"; public const string SimplifyNames = "SimplifyNames"; public const string SimplifyThisOrMe = "SimplifyThisOrMe"; public const string SpellCheck = "SpellCheck"; + public const string UnsealClass = "UnsealClass"; public const string UseCollectionInitializer = "UseCollectionInitializer"; public const string UseExplicitType = "UseExplicitType"; + public const string UseExplicitTypeForConst = "UseExplicitTypeForConst"; public const string UseImplicitType = "UseImplicitType"; public const string UseObjectInitializer = "UseObjectInitializer"; + public const string UseSimpleUsingStatementCodeFixProvider = "UseSimpleUsingStatementCodeFixProvider"; public const string UseThrowExpression = "UseThrowExpression"; + public const string VisualBasicAddObsoleteAttributeCodeFixProvider = "VisualBasicAddObsoleteAttributeCodeFixProvider"; + public const string VisualBasicConvertAnonymousTypeToTupleCodeFixProvider = "VisualBasicConvertAnonymousTypeToTupleCodeFixProvider"; public const string VisualBasicUseAutoPropertyCodeFixProvider = "VisualBasicUseAutoPropertyCodeFixProvider"; } @@ -75,10 +92,16 @@ public static partial class CSharp public const string ChangeReturnType = "ChangeReturnType"; public const string ChangeToYield = "ChangeToYield"; public const string ConvertToAsync = "ConvertToAsync"; + public const string CSharpAddObsoleteAttributeCodeFixProvider = "CSharpAddObsoleteAttributeCodeFixProvider"; + public const string CSharpConvertAnonymousTypeToTupleCodeFixProvider = "CSharpConvertAnonymousTypeToTupleCodeFixProvider"; public const string CSharpUseAutoPropertyCodeFixProvider = "CSharpUseAutoPropertyCodeFixProvider"; + public const string DeclareAsNullable = "DeclareAsNullable"; + public const string FixFormatting = "FixFormatting"; + public const string FixReturnType = "FixReturnType"; public const string FullyQualify = "FullyQualify"; public const string GenerateConstructor = "GenerateConstructor"; public const string GenerateConversion = "GenerateConversion"; + public const string GenerateDeconstructMethod = "GenerateDeconstructMethod"; public const string GenerateEnumMember = "GenerateEnumMember"; public const string GenerateMethod = "GenerateMethod"; public const string GenerateType = "GenerateType"; @@ -86,7 +109,10 @@ public static partial class CSharp public const string ImplementAbstractClass = "ImplementAbstractClass"; public const string ImplementInterface = "ImplementInterface"; public const string InvokeDelegateWithConditionalAccessCodeFixProvider = "InvokeDelegateWithConditionalAccessCodeFixProvider"; + public const string MakeLocalFunctionStaticCodeFixProvider = "MakeLocalFunctionStaticCodeFixProvider"; public const string MakeMethodSynchronous = "MakeMethodSynchronous"; + public const string MakeStatementAsynchronous = "MakeStatementAsynchronous"; + public const string MakeStructFieldsWritable = "MakeStructFieldsWritable"; public const string PopulateSwitch = "PopulateSwitch"; public const string PreferFrameworkType = "PreferFrameworkType"; public const string QualifyMemberAccess = "QualifyMemberAccess"; @@ -95,14 +121,20 @@ public static partial class CSharp public const string RemoveUnnecessaryImports = "RemoveUnnecessaryImports"; public const string RemoveUnreachableCode = "RemoveUnreachableCode"; public const string RemoveUnusedLocalFunction = "RemoveUnusedLocalFunction"; + public const string RemoveUnusedMembers = "RemoveUnusedMembers"; + public const string RemoveUnusedValues = "RemoveUnusedValues"; public const string RemoveUnusedVariable = "RemoveUnusedVariable"; + public const string ReplaceDefaultLiteral = "ReplaceDefaultLiteral"; public const string SimplifyNames = "SimplifyNames"; public const string SimplifyThisOrMe = "SimplifyThisOrMe"; public const string SpellCheck = "SpellCheck"; + public const string UnsealClass = "UnsealClass"; public const string UseCollectionInitializer = "UseCollectionInitializer"; public const string UseExplicitType = "UseExplicitType"; + public const string UseExplicitTypeForConst = "UseExplicitTypeForConst"; public const string UseImplicitType = "UseImplicitType"; public const string UseObjectInitializer = "UseObjectInitializer"; + public const string UseSimpleUsingStatementCodeFixProvider = "UseSimpleUsingStatementCodeFixProvider"; public const string UseThrowExpression = "UseThrowExpression"; } @@ -119,6 +151,7 @@ public static partial class VisualBasic public const string ConvertToAsync = "ConvertToAsync"; public const string ConvertToIterator = "ConvertToIterator"; public const string CorrectNextControlVariable = "CorrectNextControlVariable"; + public const string FixFormatting = "FixFormatting"; public const string FixIncorrectExitContinue = "FixIncorrectExitContinue"; public const string FixIncorrectFunctionReturnType = "FixIncorrectFunctionReturnType"; public const string FullyQualify = "FullyQualify"; @@ -142,12 +175,17 @@ public static partial class VisualBasic public const string RemoveDocCommentNode = "RemoveDocCommentNode"; public const string RemoveUnnecessaryCast = "RemoveUnnecessaryCast"; public const string RemoveUnnecessaryImports = "RemoveUnnecessaryImports"; + public const string RemoveUnusedMembers = "RemoveUnusedMembers"; + public const string RemoveUnusedValues = "RemoveUnusedValues"; public const string RemoveUnusedVariable = "RemoveUnusedVariable"; public const string SimplifyNames = "SimplifyNames"; public const string SimplifyThisOrMe = "SimplifyThisOrMe"; public const string SpellCheck = "SpellCheck"; + public const string UnsealClass = "UnsealClass"; public const string UseCollectionInitializer = "UseCollectionInitializer"; public const string UseObjectInitializer = "UseObjectInitializer"; + public const string VisualBasicAddObsoleteAttributeCodeFixProvider = "VisualBasicAddObsoleteAttributeCodeFixProvider"; + public const string VisualBasicConvertAnonymousTypeToTupleCodeFixProvider = "VisualBasicConvertAnonymousTypeToTupleCodeFixProvider"; public const string VisualBasicUseAutoPropertyCodeFixProvider = "VisualBasicUseAutoPropertyCodeFixProvider"; } } diff --git a/src/Stunts/Stunts.Sdk/Processors/FixupImports.cs b/src/Stunts/Stunts.Sdk/Processors/FixupImports.cs index a7893ef2..0c5207b3 100644 --- a/src/Stunts/Stunts.Sdk/Processors/FixupImports.cs +++ b/src/Stunts/Stunts.Sdk/Processors/FixupImports.cs @@ -32,7 +32,7 @@ public class FixupImports : IDocumentProcessor public async Task ProcessAsync(Document document, CancellationToken cancellationToken = default) { // This codefix is available for both C# and VB - document = await document.ApplyCodeFixAsync(CodeFixNames.All.RemoveUnnecessaryImports); + //document = await document.ApplyCodeFixAsync(CodeFixNames.All.RemoveUnnecessaryImports); var generator = SyntaxGenerator.GetGenerator(document); var syntax = await document.GetSyntaxRootAsync(); diff --git a/src/Stunts/Stunts.Sdk/StuntGenerator.cs b/src/Stunts/Stunts.Sdk/StuntGenerator.cs index ee45b956..c693a16b 100644 --- a/src/Stunts/Stunts.Sdk/StuntGenerator.cs +++ b/src/Stunts/Stunts.Sdk/StuntGenerator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Dynamic; using System.IO; using System.Linq; using System.Threading; @@ -8,6 +9,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; using Stunts.Processors; namespace Stunts @@ -108,6 +110,7 @@ public async Task GenerateDocumentAsync(Project project, ITypeSymbol[] // In debug builds, we persist the file so we can inspect the generated code // for troubleshooting. File.WriteAllText(filePath, code); + Debug.WriteLine(filePath); #endif Document document; @@ -137,6 +140,8 @@ public async Task GenerateDocumentAsync(Project project, ITypeSymbol[] #if DEBUG // Update the persisted temp file in debug builds. File.WriteAllText(filePath, code); + if (Debugger.IsAttached) + Process.Start(filePath); #endif return document; @@ -154,12 +159,10 @@ public async Task GenerateDocumentAsync(Project project, ITypeSymbol[] var imports = new HashSet(); var (baseType, implementedInterfaces) = symbols.ValidateGeneratorTypes(); - if (baseType != null && baseType.ContainingNamespace != null && baseType.ContainingNamespace.CanBeReferencedByName) - imports.Add(baseType.ContainingNamespace.ToDisplayString()); - - foreach (var iface in implementedInterfaces.Where(i => i.ContainingNamespace != null && i.ContainingNamespace.CanBeReferencedByName)) + AddImports(imports, baseType); + foreach (var iface in implementedInterfaces) { - imports.Add(iface.ContainingNamespace.ToDisplayString()); + AddImports(imports, iface); } var syntax = generator.CompilationUnit(imports @@ -169,13 +172,10 @@ public async Task GenerateDocumentAsync(Project project, ITypeSymbol[] generator.NamespaceDeclaration(naming.Namespace, generator.AddAttributes( generator.ClassDeclaration(name, - accessibility: Accessibility.Public, modifiers: DeclarationModifiers.Partial, - baseType: baseType == null ? null : generator.IdentifierName(baseType.Name), + baseType: baseType == null ? null : AsSyntaxNode(generator, baseType), interfaceTypes: implementedInterfaces - .Select(x => generator.IdentifierName(x.ContainingType != null - ? x.ContainingType.Name + "." + x.Name - : x.Name)) + .Select(x => AsSyntaxNode(generator, x)) ) ) ) @@ -184,6 +184,29 @@ public async Task GenerateDocumentAsync(Project project, ITypeSymbol[] return (name, syntax); } + void AddImports(HashSet imports, ITypeSymbol symbol) + { + if (symbol != null && symbol.ContainingNamespace != null && symbol.ContainingNamespace.CanBeReferencedByName) + imports.Add(symbol.ContainingNamespace.ToDisplayString()); + + if (symbol is INamedTypeSymbol named && named.IsGenericType) + { + foreach (var typeArgument in named.TypeArguments) + { + AddImports(imports, typeArgument); + } + } + } + + SyntaxNode AsSyntaxNode(SyntaxGenerator generator, ITypeSymbol symbol) + { + var prefix = symbol.ContainingType == null ? "" : symbol.ContainingType.Name + "."; + if (symbol is INamedTypeSymbol named && named.IsGenericType) + return generator.GenericName(prefix + symbol.Name, named.TypeArguments.Select(arg => AsSyntaxNode(generator, arg))); + + return generator.IdentifierName(prefix = symbol.Name); + } + /// /// Applies all received s received in the generator constructor. /// diff --git a/src/Stunts/Stunts.Tests/BehaviorPipelineTests.cs b/src/Stunts/Stunts.Tests/BehaviorPipelineTests.cs index 572ca0de..839a86da 100644 --- a/src/Stunts/Stunts.Tests/BehaviorPipelineTests.cs +++ b/src/Stunts/Stunts.Tests/BehaviorPipelineTests.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using Moq.Sdk; using Xunit; namespace Stunts.Tests @@ -320,6 +321,20 @@ public void WhenExecutingPipelineResultWithNoTarget_ThenThrowsIfNoResult() => pipeline.Execute(new MethodInvocation(this, f.GetMethodInfo()))); } + [Fact] + public void WhenSkippingBehavior_ThenBehaviorIsNotExecuted() + { + var pipeline = new BehaviorPipeline(); + + pipeline.Behaviors.Add(new DefaultValueBehavior()); + + var invocation = new MethodInvocation(new object(), typeof(object).GetMethod("ToString")); + invocation.SkipBehaviors.Add(typeof(DefaultValueBehavior)); + + Assert.Throws(() + => pipeline.Execute(invocation)); + } + delegate object NonVoidMethodWithArgRefDelegate(object arg1, ref object arg2); delegate object NonVoidMethodWithArgOutDelegate(object arg1, out object arg2); delegate object NonVoidMethodWithArgRefOutDelegate(object arg1, ref object arg2, out object arg3); diff --git a/src/Stunts/Stunts.Tests/CodeFixNamesGenerator.cs b/src/Stunts/Stunts.Tests/CodeFixNamesGenerator.cs index 94e2e9da..5a35595a 100644 --- a/src/Stunts/Stunts.Tests/CodeFixNamesGenerator.cs +++ b/src/Stunts/Stunts.Tests/CodeFixNamesGenerator.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Composition.Hosting; using System.IO; using System.Linq; @@ -18,13 +17,8 @@ class CodeFixNamesGenerator // Re-run this method with TD.NET AdHoc runner to regenerate CodeFixNames.g.cs as needed. public void GenerateCodeFixNames() { - var composition = new ContainerConfiguration() - .WithAssemblies(MefHostServices - .DefaultAssemblies - .Add(typeof(CodeFixNamesGenerator).Assembly)) - .CreateContainer(); - - var providers = composition.GetExports>>(); + var host = MefHostServices.Create(MefHostServices.DefaultAssemblies.Concat(new[] { typeof(CodeFixNamesGenerator).Assembly })); + var providers = host.GetExports>(); var allFixes = new HashSet(); var codeFixes = new Dictionary> diff --git a/src/Stunts/Stunts.Tests/DynamicStunt.cs b/src/Stunts/Stunts.Tests/DynamicStunt.cs index fd08d362..26048eff 100644 --- a/src/Stunts/Stunts.Tests/DynamicStunt.cs +++ b/src/Stunts/Stunts.Tests/DynamicStunt.cs @@ -9,9 +9,10 @@ namespace Stunts.Tests { - class DynamicStunt + class DynamicStunt : IDisposable { StuntGenerator generator = new StuntGenerator(); + Workspace workspace; Project project; readonly string language; @@ -36,7 +37,7 @@ async Task GenerateAsync(params Type[] types) .Select(d => d.ToString()) .ToArray(); - var symbols = types.Select(t => compilation.GetTypeByMetadataName(t.FullName)).ToArray(); + var symbols = types.Select(t => GetSymbolFromType(compilation, t)).ToArray(); var document = await generator.GenerateDocumentAsync(project, symbols, TimeoutToken(5)); var syntax = await document.GetSyntaxRootAsync(); @@ -53,8 +54,8 @@ async Task GetProjectAsync() { if (project == null) { - var (workspace, project) = CreateWorkspaceAndProject(language); - this.project = project.AddAnalyzerReference(new AnalyzerImageReference(new DiagnosticAnalyzer[] { new OverridableMembersAnalyzer() }.ToImmutableArray())); + (workspace, project) = CreateWorkspaceAndProject(language); + project = project.AddAnalyzerReference(new AnalyzerImageReference(new DiagnosticAnalyzer[] { new OverridableMembersAnalyzer() }.ToImmutableArray())); var compilation = await project.GetCompilationAsync(TimeoutToken(5)); Assert.False(compilation.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error), @@ -63,5 +64,17 @@ async Task GetProjectAsync() return project; } + + static ITypeSymbol GetSymbolFromType(Compilation compilation, Type type) + { + if (!type.IsConstructedGenericType) + return compilation.GetTypeByMetadataName(type.FullName); + + return compilation + .GetTypeByMetadataName(type.GetGenericTypeDefinition().FullName) + .Construct(type.GenericTypeArguments.Select(t => GetSymbolFromType(compilation, t)).ToArray()); + } + + public void Dispose() => workspace.Dispose(); } } diff --git a/src/Stunts/Stunts.Tests/InternalTests.cs b/src/Stunts/Stunts.Tests/InternalTests.cs index a9ea4e24..be664ac2 100644 --- a/src/Stunts/Stunts.Tests/InternalTests.cs +++ b/src/Stunts/Stunts.Tests/InternalTests.cs @@ -30,7 +30,7 @@ public void CanAccessRequiredInternalsViaReflection(PackageIdentity package) $@" Exe - net461 + net472 false bin\{package.Version} @@ -93,6 +93,9 @@ public void CanAccessRequiredInternalsViaReflection(PackageIdentity package) { "Configuration", "Debug" } }, null, new[] { "Build" }, null)); + if (result.OverallResult != BuildResultCode.Success) + Process.Start(Path.ChangeExtension(projectFile, "-build.binlog")); + Assert.Equal(BuildResultCode.Success, result.OverallResult); var info = new ProcessStartInfo(Path.Combine( diff --git a/src/Stunts/Stunts.Tests/StuntFactoryTests.cs b/src/Stunts/Stunts.Tests/StuntFactoryTests.cs index a5585aa6..5e6d650a 100644 --- a/src/Stunts/Stunts.Tests/StuntFactoryTests.cs +++ b/src/Stunts/Stunts.Tests/StuntFactoryTests.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using Xunit; namespace Stunts.Tests @@ -19,27 +15,26 @@ public void CreateStuntFromCallingAssemblyWithNamingConvention() new[] { typeof(IDisposable) }, Array.Empty()); - Assert.IsType(stunt); + Assert.IsType(stunt); } [Fact] public void CanReplaceDefaultFactory() { var existing = StuntFactory.Default; - var factory = new IStuntFactoryIDisposableStunt(); + var factory = new IDisposableIStuntFactoryStunt(); StuntFactory.Default = factory; Assert.Same(factory, StuntFactory.Default); StuntFactory.Default = existing; } - } } namespace Stunts { - public class IStuntFactoryIDisposableStunt : IStuntFactory, IDisposable + public class IDisposableIStuntFactoryStunt : IStuntFactory, IDisposable { public object CreateStunt(Assembly stuntsAssembly, Type baseType, Type[] implementedInterfaces, object[] construtorArguments) => throw new NotImplementedException(); diff --git a/src/Stunts/Stunts.Tests/StuntGeneratorEndToEnd.cs b/src/Stunts/Stunts.Tests/StuntGeneratorEndToEnd.cs index c63e0d9c..697a2404 100644 --- a/src/Stunts/Stunts.Tests/StuntGeneratorEndToEnd.cs +++ b/src/Stunts/Stunts.Tests/StuntGeneratorEndToEnd.cs @@ -18,171 +18,175 @@ public class StuntGeneratorEndToEnd [Theory] public async Task WhenTypeIsInterface(string language) { - var dynamic = new DynamicStunt(language); - var target = await dynamic.CreateAsync(); - var intercepted = false; - - target.AddBehavior((method, next) => { intercepted = true; return next()(method, next); }); - target.AddBehavior(new DefaultValueBehavior()); - - target.Add(1, 1); - Assert.True(intercepted); - Assert.True(intercepted, "Failed to intercept regular method"); - - intercepted = false; - target.Clear("foo"); - Assert.True(intercepted); - Assert.True(intercepted, "Failed to intercept regular method"); - - intercepted = false; - Assert.False(target.IsOn); - Assert.True(intercepted, "Failed to intercept property getter"); - - intercepted = false; - Assert.Equal(default, target.Mode); - Assert.True(intercepted, "Failed to intercept property getter"); - - intercepted = false; - target.Mode = CalculatorMode.Scientific; - Assert.True(intercepted, "Failed to intercept property setter"); - - intercepted = false; - target.Recall("foo"); - Assert.True(intercepted); - Assert.True(intercepted, "Failed to intercept regular method"); - - intercepted = false; - target.Store("foo", 1); - Assert.True(intercepted); - Assert.True(intercepted, "Failed to intercept regular method"); - - var x = 0; - var y = 0; - var z = 0; - - intercepted = false; - target.TryAdd(ref x, ref y, out z); - Assert.True(intercepted); - Assert.True(intercepted, "Failed to intercept ref/out method"); - - intercepted = false; - target.TurnedOn += (s, e) => { }; - Assert.True(intercepted, "Failed to intercept event add"); - - intercepted = false; - target.TurnedOn -= (s, e) => { }; - Assert.True(intercepted, "Failed to intercept event remove"); - - intercepted = false; - target.TurnOn(); - Assert.True(intercepted, "Failed to intercept regular method"); + using (var dynamic = new DynamicStunt(language)) + { + var target = await dynamic.CreateAsync(); + var intercepted = false; + + target.AddBehavior((method, next) => { intercepted = true; return next()(method, next); }); + target.AddBehavior(new DefaultValueBehavior()); + + target.Add(1, 1); + Assert.True(intercepted); + Assert.True(intercepted, "Failed to intercept regular method"); + + intercepted = false; + target.Clear("foo"); + Assert.True(intercepted); + Assert.True(intercepted, "Failed to intercept regular method"); + + intercepted = false; + Assert.False(target.IsOn); + Assert.True(intercepted, "Failed to intercept property getter"); + + intercepted = false; + Assert.Equal(default, target.Mode); + Assert.True(intercepted, "Failed to intercept property getter"); + + intercepted = false; + target.Mode = CalculatorMode.Scientific; + Assert.True(intercepted, "Failed to intercept property setter"); + + intercepted = false; + target.Recall("foo"); + Assert.True(intercepted); + Assert.True(intercepted, "Failed to intercept regular method"); + + intercepted = false; + target.Store("foo", 1); + Assert.True(intercepted); + Assert.True(intercepted, "Failed to intercept regular method"); + + var x = 0; + var y = 0; + var z = 0; + + intercepted = false; + target.TryAdd(ref x, ref y, out z); + Assert.True(intercepted); + Assert.True(intercepted, "Failed to intercept ref/out method"); + + intercepted = false; + target.TurnedOn += (s, e) => { }; + Assert.True(intercepted, "Failed to intercept event add"); + + intercepted = false; + target.TurnedOn -= (s, e) => { }; + Assert.True(intercepted, "Failed to intercept event remove"); + + intercepted = false; + target.TurnOn(); + Assert.True(intercepted, "Failed to intercept regular method"); + } } - // TODO: why does this fail for VB? - //[InlineData(LanguageNames.VisualBasic)] [InlineData(LanguageNames.CSharp)] + [InlineData(LanguageNames.VisualBasic)] [Theory] public async Task WhenTypeIsAbstract(string language) { - var dynamic = new DynamicStunt(language); - var target = await dynamic.CreateAsync(); - var intercepted = false; - - target.AddBehavior((method, next) => { intercepted = true; return next()(method, next); }); - target.AddBehavior(new DefaultValueBehavior()); - - intercepted = false; - Assert.False(target.IsOn); - Assert.True(intercepted); - - intercepted = false; - target.TurnOn(); - Assert.True(intercepted); - - intercepted = false; - target.TurnedOn += (s, e) => { }; - if (language == LanguageNames.VisualBasic) - // Intercepting events doesn't work in VB - Assert.False(intercepted, "Visual Basic can't intercept virtual events"); - else + using (var dynamic = new DynamicStunt(language)) + { + var target = await dynamic.CreateAsync(); + var intercepted = false; + + target.AddBehavior((method, next) => { intercepted = true; return next()(method, next); }); + target.AddBehavior(new DefaultValueBehavior()); + + intercepted = false; + Assert.False(target.IsOn); Assert.True(intercepted); - intercepted = false; - target.TurnedOn -= (s, e) => { }; - if (language == LanguageNames.VisualBasic) - // Intercepting events doesn't work in VB - Assert.False(intercepted, "Visual Basic can't intercept virtual events"); - else + intercepted = false; + target.TurnOn(); Assert.True(intercepted); + + intercepted = false; + target.TurnedOn += (s, e) => { }; + if (language == LanguageNames.VisualBasic) + // Intercepting events doesn't work in VB + Assert.False(intercepted, "Visual Basic can't intercept virtual events"); + else + Assert.True(intercepted); + + intercepted = false; + target.TurnedOn -= (s, e) => { }; + if (language == LanguageNames.VisualBasic) + // Intercepting events doesn't work in VB + Assert.False(intercepted, "Visual Basic can't intercept virtual events"); + else + Assert.True(intercepted); + } } - // TODO: why does this fail for VB? - // [InlineData(LanguageNames.VisualBasic)] + [InlineData(LanguageNames.VisualBasic)] [InlineData(LanguageNames.CSharp)] [Theory] public async Task WhenTypeHasVirtualMembers(string language) { - var dynamic = new DynamicStunt(language); - var target = await dynamic.CreateAsync(); - var intercepted = false; - - target.AddBehavior((method, next) => { intercepted = true; return next()(method, next); }); - target.AddBehavior(new DefaultValueBehavior()); - - target.Add(1, 1); - Assert.True(intercepted, "Failed to intercept regular method"); - - intercepted = false; - target.Clear("foo"); - Assert.True(intercepted, "Failed to intercept regular method"); - - intercepted = false; - Assert.False(target.IsOn); - Assert.True(intercepted, "Failed to intercept property getter"); - - intercepted = false; - target.Mode = CalculatorMode.Scientific; - Assert.True(intercepted, "Failed to intercept property setter"); - - intercepted = false; - Assert.Equal(default, target.Mode); - Assert.True(intercepted, "Failed to intercept property getter"); - - intercepted = false; - target.Recall("foo"); - Assert.True(intercepted, "Failed to intercept regular method"); - - intercepted = false; - target.Store("foo", 1); - Assert.True(intercepted, "Failed to intercept regular method"); - - var x = 0; - var y = 0; - var z = 0; - - intercepted = false; - target.TryAdd(ref x, ref y, out z); - Assert.True(intercepted, "Failed to intercept ref/out method"); - - intercepted = false; - target.TurnedOn += (s, e) => { }; - if (language == LanguageNames.VisualBasic) - // Intercepting events doesn't work in VB - Assert.False(intercepted, "Visual Basic can't intercept virtual events"); - else - Assert.True(intercepted, "Failed to intercept event add"); - - intercepted = false; - target.TurnedOn -= (s, e) => { }; - if (language == LanguageNames.VisualBasic) - // Intercepting events doesn't work in VB - Assert.False(intercepted, "Visual Basic can't intercept virtual events"); - else - Assert.True(intercepted, "Failed to intercept event remove"); - - intercepted = false; - target.TurnOn(); - Assert.True(intercepted, "Failed to intercept regular method"); + using (var dynamic = new DynamicStunt(language)) + { + var target = await dynamic.CreateAsync(); + var intercepted = false; + + target.AddBehavior((method, next) => { intercepted = true; return next()(method, next); }); + target.AddBehavior(new DefaultValueBehavior()); + + target.Add(1, 1); + Assert.True(intercepted, "Failed to intercept regular method"); + + intercepted = false; + target.Clear("foo"); + Assert.True(intercepted, "Failed to intercept regular method"); + + intercepted = false; + Assert.False(target.IsOn); + Assert.True(intercepted, "Failed to intercept property getter"); + + intercepted = false; + target.Mode = CalculatorMode.Scientific; + Assert.True(intercepted, "Failed to intercept property setter"); + + intercepted = false; + Assert.Equal(default, target.Mode); + Assert.True(intercepted, "Failed to intercept property getter"); + + intercepted = false; + target.Recall("foo"); + Assert.True(intercepted, "Failed to intercept regular method"); + + intercepted = false; + target.Store("foo", 1); + Assert.True(intercepted, "Failed to intercept regular method"); + + var x = 0; + var y = 0; + var z = 0; + + intercepted = false; + target.TryAdd(ref x, ref y, out z); + Assert.True(intercepted, "Failed to intercept ref/out method"); + + intercepted = false; + target.TurnedOn += (s, e) => { }; + if (language == LanguageNames.VisualBasic) + // Intercepting events doesn't work in VB + Assert.False(intercepted, "Visual Basic can't intercept virtual events"); + else + Assert.True(intercepted, "Failed to intercept event add"); + + intercepted = false; + target.TurnedOn -= (s, e) => { }; + if (language == LanguageNames.VisualBasic) + // Intercepting events doesn't work in VB + Assert.False(intercepted, "Visual Basic can't intercept virtual events"); + else + Assert.True(intercepted, "Failed to intercept event remove"); + + intercepted = false; + target.TurnOn(); + Assert.True(intercepted, "Failed to intercept regular method"); + } } } } diff --git a/src/Stunts/Stunts.Tests/StuntGeneratorTests.cs b/src/Stunts/Stunts.Tests/StuntGeneratorTests.cs index 7d987158..7b0cd2d9 100644 --- a/src/Stunts/Stunts.Tests/StuntGeneratorTests.cs +++ b/src/Stunts/Stunts.Tests/StuntGeneratorTests.cs @@ -35,7 +35,7 @@ public async Task CanGenerateStuntForInterface(string language, bool trace = fal var generator = new StuntGenerator(); var compilation = await CreateStunt(generator, language, typeof(IFoo), trace); var assembly = compilation.Emit(); - var type = assembly.GetExportedTypes().FirstOrDefault(); + var type = assembly.GetType(StuntNaming.GetFullName(typeof(IFoo)), true); Assert.NotNull(type); @@ -63,9 +63,7 @@ public async Task CanGenerateStuntForClass(string language, bool trace = false) var generator = new StuntGenerator(); var compilation = await CreateStunt(generator, language, typeof(Foo), trace); var assembly = compilation.Emit(); - var type = assembly.GetExportedTypes().FirstOrDefault(); - - Assert.NotNull(type); + var type = assembly.GetType(StuntNaming.GetFullName(typeof(Foo)), true); var instance = Activator.CreateInstance(type); @@ -91,8 +89,8 @@ public async Task GeneratedNameContainsAdditionalInterfaceInName(string language { var compilation = await CreateStunt(new StuntGenerator(), language, new[] { typeof(INotifyPropertyChanged), typeof(IDisposable) }, trace); var assembly = compilation.Emit(); - var type = assembly.GetExportedTypes().FirstOrDefault(); - + var type = assembly.GetType(StuntNaming.GetFullName(typeof(INotifyPropertyChanged), typeof(IDisposable)), true); + Assert.NotNull(type); Assert.True(typeof(IDisposable).IsAssignableFrom(type)); Assert.True(type.FullName.Contains(nameof(IDisposable)), @@ -106,7 +104,7 @@ public async Task GeneratedInterfaceHasCompilerGeneratedAttribute(string languag { var compilation = await CreateStunt(new StuntGenerator(), language, typeof(ICalculator), trace); var assembly = compilation.Emit(); - var type = assembly.GetExportedTypes().FirstOrDefault(); + var type = assembly.GetType(StuntNaming.GetFullName(typeof(ICalculator)), true); Assert.NotNull(type); @@ -148,7 +146,7 @@ public async Task GeneratedTypeOverridesVirtualObjectMembers(string language, bo { var compilation = await CreateStunt(new StuntGenerator(), language, new[] { typeof(INotifyPropertyChanged), typeof(IDisposable) }, trace); var assembly = compilation.Emit(); - var type = assembly.GetExportedTypes().FirstOrDefault(); + var type = assembly.GetType(StuntNaming.GetFullName(typeof(INotifyPropertyChanged), typeof(IDisposable)), true); Assert.NotNull(type); diff --git a/src/Stunts/Stunts.Tests/StuntNamingTests.cs b/src/Stunts/Stunts.Tests/StuntNamingTests.cs index 61ec1b1e..2f092dde 100644 --- a/src/Stunts/Stunts.Tests/StuntNamingTests.cs +++ b/src/Stunts/Stunts.Tests/StuntNamingTests.cs @@ -6,12 +6,12 @@ namespace Stunts.Tests public class StuntNamingTests { [Theory] - [InlineData("StuntFactoryIDisposableIServiceProvider" + StuntNaming.NameSuffix, typeof(StuntFactory), typeof(IServiceProvider), typeof(IDisposable))] + [InlineData("StuntFactoryIDisposableIServiceProvider" + StuntNaming.DefaultSuffix, typeof(StuntFactory), typeof(IServiceProvider), typeof(IDisposable))] public void GetNameOrdersTypes(string expectedName, Type baseType, params Type[] implementedInterfaces) => Assert.Equal(expectedName, StuntNaming.GetName(baseType, implementedInterfaces)); [Theory] - [InlineData(StuntNaming.Namespace + ".StuntFactoryIDisposableIServiceProvider" + StuntNaming.NameSuffix, typeof(StuntFactory), typeof(IServiceProvider), typeof(IDisposable))] + [InlineData(StuntNaming.DefaultNamespace + ".StuntFactoryIDisposableIServiceProvider" + StuntNaming.DefaultSuffix, typeof(StuntFactory), typeof(IServiceProvider), typeof(IDisposable))] public void GetFullNameOrdersTypes(string expectedName, Type baseType, params Type[] implementedInterfaces) => Assert.Equal(expectedName, StuntNaming.GetFullName(baseType, implementedInterfaces)); } diff --git a/src/Stunts/Stunts.Tests/Stunts.Tests.csproj b/src/Stunts/Stunts.Tests/Stunts.Tests.csproj index 4b531071..3772470a 100644 --- a/src/Stunts/Stunts.Tests/Stunts.Tests.csproj +++ b/src/Stunts/Stunts.Tests/Stunts.Tests.csproj @@ -44,6 +44,7 @@ + @@ -59,6 +60,7 @@ + diff --git a/src/Stunts/Stunts.Tests/SymbolExtensionsTests.cs b/src/Stunts/Stunts.Tests/SymbolExtensionsTests.cs deleted file mode 100644 index 2121d393..00000000 --- a/src/Stunts/Stunts.Tests/SymbolExtensionsTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Xunit; -using static TestHelpers; - -namespace Stunts.Tests -{ - class SymbolExtensionsTests - { - [InlineData("System.IDisposable")] - [InlineData("System.Threading.Tasks.Task`1")] - [InlineData("System.Environment+SpecialFolder")] - [Theory] - public async Task ToFullMetadataRoundTrip(string metadataName) - { - var (workspace, project) = CreateWorkspaceAndProject(LanguageNames.CSharp); - var compilation = await project.GetCompilationAsync(); - var symbol = compilation.GetTypeByMetadataName(metadataName); - - Assert.NotNull(symbol); - - var fullMetadata = symbol.ToFullMetadataName(); - - var symbol2 = compilation.GetTypeByMetadataName(fullMetadata); - - Assert.NotNull(symbol2); - - Assert.Same(symbol, symbol2); - } - - [Fact] - public async Task ToFullMetadata() - { - var (workspace, project) = CreateWorkspaceAndProject(LanguageNames.CSharp); - var code = @"public class Foo { }"; - var doc = project.AddDocument("code.cs", SourceText.From(code)); - var compilation = await doc.Project.GetCompilationAsync(); - var symbol = compilation.GetTypeByMetadataName("Foo"); - - Assert.NotNull(symbol); - - var fullMetadata = symbol.ToFullMetadataName(); - - var symbol2 = compilation.GetTypeByMetadataName(fullMetadata); - - Assert.NotNull(symbol2); - - Assert.Same(symbol, symbol2); - } - } -} diff --git a/src/Stunts/Stunts.Tests/SymbolFullNameTests.cs b/src/Stunts/Stunts.Tests/SymbolFullNameTests.cs new file mode 100644 index 00000000..acdb101b --- /dev/null +++ b/src/Stunts/Stunts.Tests/SymbolFullNameTests.cs @@ -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")] + [InlineData("System.Collections.Generic.IEnumerable")] + [InlineData("System.Collections.Generic.IDictionary>>")] + [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); + } + } +} diff --git a/src/Stunts/Stunts/BehaviorPipeline.cs b/src/Stunts/Stunts/BehaviorPipeline.cs index 0323c3e9..25d181e1 100644 --- a/src/Stunts/Stunts/BehaviorPipeline.cs +++ b/src/Stunts/Stunts/BehaviorPipeline.cs @@ -78,7 +78,7 @@ public IMethodReturn Invoke(IMethodInvocation invocation, ExecuteDelegate target var index = -1; for (var i = 0; i < behaviors.Length; i++) { - if (behaviors[i].AppliesTo(invocation)) + if (!invocation.SkipBehaviors.Contains(behaviors[i].GetType()) && behaviors[i].AppliesTo(invocation)) { index = i; break; @@ -92,7 +92,7 @@ public IMethodReturn Invoke(IMethodInvocation invocation, ExecuteDelegate target { for (index++; index < behaviors.Length; index++) { - if (behaviors[index].AppliesTo(invocation)) + if (!invocation.SkipBehaviors.Contains(behaviors[index].GetType()) && behaviors[index].AppliesTo(invocation)) break; } diff --git a/src/Stunts/Stunts/IMethodInvocation.cs b/src/Stunts/Stunts/IMethodInvocation.cs index e0fa64fb..06065f88 100644 --- a/src/Stunts/Stunts/IMethodInvocation.cs +++ b/src/Stunts/Stunts/IMethodInvocation.cs @@ -30,6 +30,11 @@ public interface IMethodInvocation : IEquatable, IFluentInter /// object Target { get; } + /// + /// Behaviors in the pipeline that should be skipped during this invocation. + /// + HashSet SkipBehaviors { get; } + /// /// Creates the method invocation return that ends the /// current invocation. @@ -37,7 +42,7 @@ public interface IMethodInvocation : IEquatable, IFluentInter /// Optional return value from the method invocation. for methods. /// Ordered list of all arguments to the method invocation, including ref/out arguments. /// The for the current invocation. - IMethodReturn CreateValueReturn(object returnValue, params object[] allArguments); + IMethodReturn CreateValueReturn(object returnValue, params object[] allArguments); /// /// Creates a method invocation return that represents diff --git a/src/Stunts/Stunts/MethodInvocation.cs b/src/Stunts/Stunts/MethodInvocation.cs index c4f275df..422c75af 100644 --- a/src/Stunts/Stunts/MethodInvocation.cs +++ b/src/Stunts/Stunts/MethodInvocation.cs @@ -29,43 +29,26 @@ public MethodInvocation(object target, MethodBase method, params object[] argume Context = new Dictionary(); } - /// - /// The arguments of the method invocation. - /// + /// public IArgumentCollection Arguments { get; } - /// - /// An arbitrary property bag used during the invocation. - /// + /// public IDictionary Context { get; } - /// - /// The runtime method being invoked. - /// + /// public MethodBase MethodBase { get; } - /// - /// The ultimate target of the method invocation, typically - /// a stunt object. - /// + /// public object Target { get; } - /// - /// Creates the method invocation return that ends the - /// current invocation with an exception. - /// - /// The exception to throw from the method invocation. - /// The for the current invocation. + /// + public HashSet SkipBehaviors { get; } = new HashSet(); + + /// public IMethodReturn CreateExceptionReturn(Exception exception) => new MethodReturn(this, exception); - /// - /// Creates a method invocation return that represents - /// a thrown exception. - /// - /// Optional return value from the method invocation. for methods. - /// Ordered list of all arguments to the method invocation, including ref/out arguments. - /// The for the current invocation. + /// public IMethodReturn CreateValueReturn(object returnValue, params object[] allArguments) => new MethodReturn(this, returnValue, allArguments); diff --git a/src/Stunts/Stunts/StuntNaming.cs b/src/Stunts/Stunts/StuntNaming.cs index 71d0f8c4..3ef0b791 100644 --- a/src/Stunts/Stunts/StuntNaming.cs +++ b/src/Stunts/Stunts/StuntNaming.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; namespace Stunts { @@ -10,29 +11,83 @@ namespace Stunts public static class StuntNaming { /// - /// The namespace where generated stunts are declared. + /// The default namespace where generated stunts are declared. /// - public const string Namespace = "Stunts"; + public const string DefaultNamespace = "Stunts"; /// - /// The suffix added to stunt type names. + /// The default suffix added to stunt type names. /// - public const string NameSuffix = "Stunt"; + public const string DefaultSuffix = "Stunt"; /// - /// Gets the runtime stunt name from its base type and implemented interfaces. + /// Gets the runtime stunt name from its base type and optional additional + /// interfaces, using the . /// - public static string GetName(Type baseType, Type[] implementedInterfaces) - { - Array.Sort(implementedInterfaces, Comparer.Create((x, y) => x.Name.CompareTo(y.Name))); + public static string GetName(Type baseType, params Type[] additionalInterfaces) + => GetName(DefaultSuffix, baseType, additionalInterfaces); - return baseType.Name + string.Join("", implementedInterfaces.Select(x => x.Name)) + NameSuffix; + /// + /// Gets the runtime stunt name from its base type and optional additional interfaces + /// and the given appended to the type name. + /// + public static string GetName(string suffix, Type baseType, params Type[] additionalInterfaces) + { + if (baseType.IsClass) + { + return new StringBuilder() + .AddName(baseType) + .AddNames(additionalInterfaces.OrderBy(x => x.Name, StringComparer.Ordinal)) + .Append(suffix) + .ToString(); + } + else + { + return new StringBuilder() + .AddNames(new[] { baseType } + .Concat(additionalInterfaces) + .OrderBy(x => x.Name, StringComparer.Ordinal)) + .Append(suffix) + .ToString(); + } } + /// + /// Gets the runtime stunt full name from its base type and optional additional interfaces, + /// using the and . + /// + public static string GetFullName(Type baseType, params Type[] additionalInterfaces) + => GetFullName(DefaultNamespace, DefaultSuffix, baseType, additionalInterfaces); + /// /// Gets the runtime stunt full name from its base type and implemented interfaces. /// - public static string GetFullName(Type baseType, Type[] implementedInterfaces) - => Namespace + "." + GetName(baseType, implementedInterfaces); + public static string GetFullName(string @namespace, Type baseType, params Type[] additionalInterfaces) + => GetFullName(@namespace, DefaultSuffix, baseType, additionalInterfaces); + + /// + /// Gets the runtime stunt full name from its base type and implemented interfaces. + /// + public static string GetFullName(string @namespace, string suffix, Type baseType, params Type[] additionalInterfaces) + => @namespace + "." + GetName(suffix, baseType, additionalInterfaces); + } + + internal static class StringBuilderExtensions + { + public static StringBuilder AddNames(this StringBuilder builder, IEnumerable types) + { + foreach (var type in types) + { + builder.AddName(type); + if (type.IsConstructedGenericType) + { + builder.Append("Of").AddNames(type.GenericTypeArguments); + } + } + return builder; + } + + public static StringBuilder AddName(this StringBuilder builder, Type type) + => type.IsGenericType ? builder.Append(type.Name.Substring(0, type.Name.IndexOf('`'))) : builder.Append(type.Name); } } diff --git a/src/Stunts/Stunts/Stunts.csproj b/src/Stunts/Stunts/Stunts.csproj index 0f2de1e3..569e9e5c 100644 --- a/src/Stunts/Stunts/Stunts.csproj +++ b/src/Stunts/Stunts/Stunts.csproj @@ -2,7 +2,7 @@ - netstandard1.3 + netstandard2.0 Stunts.Core diff --git a/src/Testing/TestHelpers.cs b/src/Testing/TestHelpers.cs index 495f0d8a..d25a654c 100644 --- a/src/Testing/TestHelpers.cs +++ b/src/Testing/TestHelpers.cs @@ -14,15 +14,17 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.VisualBasic; +using Stunts; using Xunit; static partial class TestHelpers { public static (AdhocWorkspace workspace, Project project) CreateWorkspaceAndProject(string language, string assemblyName = "Code", bool includeStuntApi = true, bool includeMockApi = false) { - var workspace = new AdhocWorkspace(); + var workspace = new AdhocWorkspace(MefHostServices.Create(MefHostServices.DefaultAssemblies.Concat(new[] { typeof(StuntCodeFixProvider).Assembly }))); var projectInfo = CreateProjectInfo(language, assemblyName, includeStuntApi, includeMockApi); var project = workspace.AddProject(projectInfo); diff --git a/src/build/Settings.props b/src/build/Settings.props index 899a0b8f..f7d12d2d 100644 --- a/src/build/Settings.props +++ b/src/build/Settings.props @@ -35,6 +35,7 @@ false + $(AllowedReferenceRelatedFileExtensions);*.pdb;*.xml false $(DefaultExcludeItems);*.binlog @@ -55,6 +56,12 @@ + + + true + + +