├── .editorconfig
├── .github
└── workflows
│ └── build-and-publish.yml
├── .gitignore
├── CHANGELOG.md
├── Directory.Build.props
├── Generators.Tests
├── Expected
│ ├── BlobContainerClientMock.cs
│ └── LoggerMock.cs
├── Generators.Tests.csproj
└── MockGeneratorTests.cs
├── Generators
├── Generators.csproj
├── Internal
│ ├── CodeWriter.cs
│ ├── DefaultConstraintCandidateCollector.cs
│ ├── Exceptions.cs
│ ├── GeneratorCache.cs
│ ├── GeneratorLog.cs
│ ├── Indents.cs
│ ├── KnownTypes.cs
│ ├── MockClassGenerator.cs
│ ├── MockMemberGenerator.cs
│ ├── MockTargetModelFactory.cs
│ ├── Models
│ │ ├── MockTarget.cs
│ │ ├── MockTargetMember.cs
│ │ ├── MockTargetMethodRunDelegateType.cs
│ │ └── MockTargetParameter.cs
│ └── NamedTypeSymbolCacheKeyEqualityComparer.cs
└── MockGenerator.cs
├── LICENSE.txt
├── README.md
├── SourceMock.sln
├── Tests.Interfaces
├── AbstractClass.cs
├── Disposable.cs
├── IEmptyInterface.cs
├── IExcludedInterface.cs
├── IInheritedInteface.cs
├── IInternalInterface.cs
├── IMockable.cs
├── IMockable2.cs
├── INeedsCollectionDefaults.cs
├── INeedsGenericConstraints.cs
├── INeedsGenerics.cs
├── INeedsOtherDefaults.cs
├── INeedsParameterModifiers.cs
└── Tests.Interfaces.csproj
├── Tests
├── ArgumentTests.cs
├── DefaultValueTests.cs
├── ExceptionSetupTests.cs
├── Generated
│ └── SourceMock.Generators
│ │ └── SourceMock.Generators.MockGenerator
│ │ ├── global__Azure_Storage_Blobs_BlobContainerClient.cs
│ │ ├── global__SourceMock_Tests_Interfaces_AbstractClass.cs
│ │ ├── global__SourceMock_Tests_Interfaces_Disposable.cs
│ │ ├── global__SourceMock_Tests_Interfaces_IEmptyInterface.cs
│ │ ├── global__SourceMock_Tests_Interfaces_IInheritedInteface.cs
│ │ ├── global__SourceMock_Tests_Interfaces_IInternalInterface.cs
│ │ ├── global__SourceMock_Tests_Interfaces_IMockable.cs
│ │ ├── global__SourceMock_Tests_Interfaces_IMockable2.cs
│ │ ├── global__SourceMock_Tests_Interfaces_INeedsCollectionDefaults.cs
│ │ ├── global__SourceMock_Tests_Interfaces_INeedsGenericConstraints.cs
│ │ ├── global__SourceMock_Tests_Interfaces_INeedsGenericConstraints_TNotNull__TClass__TNullableClass__TStruct__TUnmanaged_.cs
│ │ ├── global__SourceMock_Tests_Interfaces_INeedsGenerics.cs
│ │ ├── global__SourceMock_Tests_Interfaces_INeedsGenerics_U_.cs
│ │ ├── global__SourceMock_Tests_Interfaces_INeedsOtherDefaults.cs
│ │ ├── global__SourceMock_Tests_Interfaces_INeedsParameterModifiers.cs
│ │ └── global__System_Net_WebSockets_WebSocket.cs
├── MockManifest.cs
├── ReturnValueTests.cs
├── RunsCallbackTests.cs
├── Tests.csproj
└── VerificationTests.cs
└── [Main]
├── GenerateMocksForAssemblyOfAttribute.cs
├── GenerateMocksForTypesAttribute.cs
├── IMock.cs
├── Interfaces
├── IMockMethodSetup.cs
├── IMockPropertyCalls.cs
├── IMockPropertySetup.cs
├── IMockSettablePropertyCalls.cs
├── IMockSettablePropertySetup.cs
├── IMockSetupReturns.cs
├── IMockSetupRuns.cs
└── IMockSetupThrows.cs
├── Internal
├── DefaultValue.cs
├── IMockArgumentMatcher.cs
├── IMockMethodSetupInternal.cs
├── MockArgumentMatcher.cs
├── MockCall.cs
├── MockMethodHandler.cs
├── MockMethodSetup.cs
├── MockPropertyCalls.cs
├── MockPropertyHandler.cs
├── MockPropertySetup.cs
└── VoidReturn.cs
├── MockExtensions.cs
├── NoArguments.cs
└── [Main].csproj
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 4
7 | end_of_line = crlf
8 | trim_trailing_whitespace = true
9 | insert_final_newline = false
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
14 | [*.{html,config,csproj,targets,props,yml}]
15 | indent_size = 2
16 |
17 | [{package,appsettings*}.json]
18 | indent_size = 2
19 |
20 | [*.cs]
21 | csharp_new_line_before_open_brace = none
22 | csharp_style_var_for_built_in_types = true:warning
23 | csharp_style_var_when_type_is_apparent = true:warning
24 | csharp_style_var_elsewhere = true:warning
25 |
26 | # IDE0044: Add readonly modifier
27 | dotnet_style_readonly_field = true:warning
28 |
29 | # IDE0005: Using directive is unnecessary.
30 | dotnet_diagnostic.IDE0005.severity = warning
31 |
32 | # IDE0090: 'new' expression can be simplified.
33 | dotnet_diagnostic.IDE0090.severity = warning
34 |
35 | # Performance Sensitive Analyzers
36 | dotnet_diagnostic.HAA0501.severity = warning
37 | dotnet_diagnostic.HAA0502.severity = warning
38 | dotnet_diagnostic.HAA0503.severity = warning
--------------------------------------------------------------------------------
/.github/workflows/build-and-publish.yml:
--------------------------------------------------------------------------------
1 | name: 'Build and Publish'
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | build-and-publish:
6 | name: 'Build and Publish'
7 | runs-on: windows-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: actions/setup-dotnet@v1
11 | with:
12 | dotnet-version: 5.0.x
13 |
14 | - run: dotnet build --configuration Release
15 | - run: dotnet test --no-build --configuration Release
16 | - run: dotnet pack --no-build --output . --configuration Release
17 |
18 | - run: dotnet nuget push SourceMock.*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{secrets.NUGET_API_KEY}} --skip-duplicate
19 | if: github.ref == 'refs/heads/main'
20 |
21 | - uses: actions/upload-artifact@v2
22 | with:
23 | name: Package
24 | path: SourceMock.*.nupkg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs/
2 | bin/
3 | obj/
4 | /Generators/Logs/
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [0.12.0] - 2021-11-13
4 |
5 | ### Fixed
6 | - Fix class mock generation failure when base class has any fields
7 | - Fix class mock generation failure when base class has internal members but no InternalsVisibleTo
8 |
9 | ## [0.11.0] - 2021-10-21
10 |
11 | ### Fixed
12 | - Fix mock generation bug in generic types referenced through GenerateMocksForTypes
13 |
14 | ## [0.10.0] - 2021-06-21
15 |
16 | ### Changed
17 | - All generated mocks are now internal
18 |
19 | ## [0.9.1] - 2021-06-21
20 |
21 | ### Fixed
22 | - Fix class mocks with implicit interface implementations
23 | - Fix class mocks with non-virtual properties
24 |
25 | ## [0.9.0] - 2021-05-03
26 |
27 | ### Added
28 | - Initial version
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | 9.0
4 | enable
5 | true
6 | CS1591
7 |
8 |
--------------------------------------------------------------------------------
/Generators.Tests/Expected/LoggerMock.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | namespace Microsoft.Extensions.Logging.Mocks {
3 | internal class LoggerMock : global::Microsoft.Extensions.Logging.ILogger, ILoggerSetup, ILoggerCalls, SourceMock.IMock> {
4 | public ILoggerSetup Setup => this;
5 | public ILoggerCalls Calls => this;
6 |
7 | private readonly SourceMock.Internal.MockMethodHandler _logHandler = new();
8 | SourceMock.Interfaces.IMockMethodSetup.LogAction> ILoggerSetup.Log(SourceMock.Internal.MockArgumentMatcher logLevel, SourceMock.Internal.MockArgumentMatcher eventId, SourceMock.Internal.MockArgumentMatcher state, SourceMock.Internal.MockArgumentMatcher exception, SourceMock.Internal.MockArgumentMatcher> formatter) => _logHandler.Setup.LogAction, SourceMock.Internal.VoidReturn>(new[] { typeof(TState) }, new SourceMock.Internal.IMockArgumentMatcher[] { logLevel, eventId, state, exception, formatter });
9 | public void Log(global::Microsoft.Extensions.Logging.LogLevel logLevel, global::Microsoft.Extensions.Logging.EventId eventId, TState state, global::System.Exception exception, global::System.Func formatter) => _logHandler.Call.LogAction, SourceMock.Internal.VoidReturn>(new[] { typeof(TState) }, new object?[] { logLevel, eventId, state, exception, formatter });
10 | System.Collections.Generic.IReadOnlyList<(global::Microsoft.Extensions.Logging.LogLevel logLevel, global::Microsoft.Extensions.Logging.EventId eventId, TState state, global::System.Exception exception, global::System.Func formatter)> ILoggerCalls.Log(SourceMock.Internal.MockArgumentMatcher logLevel, SourceMock.Internal.MockArgumentMatcher eventId, SourceMock.Internal.MockArgumentMatcher state, SourceMock.Internal.MockArgumentMatcher exception, SourceMock.Internal.MockArgumentMatcher> formatter) => _logHandler.Calls(new[] { typeof(TState) }, new SourceMock.Internal.IMockArgumentMatcher[] { logLevel, eventId, state, exception, formatter }, args => ((global::Microsoft.Extensions.Logging.LogLevel)args[0]!, (global::Microsoft.Extensions.Logging.EventId)args[1]!, (TState)args[2]!, (global::System.Exception)args[3]!, (global::System.Func)args[4]!));
11 |
12 | private readonly SourceMock.Internal.MockMethodHandler _isEnabledHandler = new();
13 | SourceMock.Interfaces.IMockMethodSetup, bool> ILoggerSetup.IsEnabled(SourceMock.Internal.MockArgumentMatcher logLevel) => _isEnabledHandler.Setup, bool>(null, new SourceMock.Internal.IMockArgumentMatcher[] { logLevel });
14 | public bool IsEnabled(global::Microsoft.Extensions.Logging.LogLevel logLevel) => _isEnabledHandler.Call, bool>(null, new object?[] { logLevel });
15 | System.Collections.Generic.IReadOnlyList ILoggerCalls.IsEnabled(SourceMock.Internal.MockArgumentMatcher logLevel) => _isEnabledHandler.Calls(null, new SourceMock.Internal.IMockArgumentMatcher[] { logLevel }, args => ((global::Microsoft.Extensions.Logging.LogLevel)args[0]!));
16 |
17 | private readonly SourceMock.Internal.MockMethodHandler _beginScopeHandler = new();
18 | SourceMock.Interfaces.IMockMethodSetup.BeginScopeFunc, global::System.IDisposable> ILoggerSetup.BeginScope(SourceMock.Internal.MockArgumentMatcher state) => _beginScopeHandler.Setup.BeginScopeFunc, global::System.IDisposable>(new[] { typeof(TState) }, new SourceMock.Internal.IMockArgumentMatcher[] { state });
19 | public global::System.IDisposable BeginScope(TState state) => _beginScopeHandler.Call.BeginScopeFunc, global::System.IDisposable>(new[] { typeof(TState) }, new object?[] { state });
20 | System.Collections.Generic.IReadOnlyList ILoggerCalls.BeginScope(SourceMock.Internal.MockArgumentMatcher state) => _beginScopeHandler.Calls(new[] { typeof(TState) }, new SourceMock.Internal.IMockArgumentMatcher[] { state }, args => ((TState)args[0]!));
21 | }
22 |
23 | internal static class LoggerDelegates {
24 | public delegate void LogAction(global::Microsoft.Extensions.Logging.LogLevel logLevel, global::Microsoft.Extensions.Logging.EventId eventId, TState state, global::System.Exception exception, global::System.Func formatter);
25 | public delegate global::System.IDisposable BeginScopeFunc(TState state);
26 | }
27 |
28 | internal interface ILoggerSetup {
29 | SourceMock.Interfaces.IMockMethodSetup.LogAction> Log(SourceMock.Internal.MockArgumentMatcher logLevel = default, SourceMock.Internal.MockArgumentMatcher eventId = default, SourceMock.Internal.MockArgumentMatcher state = default, SourceMock.Internal.MockArgumentMatcher exception = default, SourceMock.Internal.MockArgumentMatcher> formatter = default);
30 | SourceMock.Interfaces.IMockMethodSetup, bool> IsEnabled(SourceMock.Internal.MockArgumentMatcher logLevel = default);
31 | SourceMock.Interfaces.IMockMethodSetup.BeginScopeFunc, global::System.IDisposable> BeginScope(SourceMock.Internal.MockArgumentMatcher state = default);
32 | }
33 |
34 | internal interface ILoggerCalls {
35 | System.Collections.Generic.IReadOnlyList<(global::Microsoft.Extensions.Logging.LogLevel logLevel, global::Microsoft.Extensions.Logging.EventId eventId, TState state, global::System.Exception exception, global::System.Func formatter)> Log(SourceMock.Internal.MockArgumentMatcher logLevel = default, SourceMock.Internal.MockArgumentMatcher eventId = default, SourceMock.Internal.MockArgumentMatcher state = default, SourceMock.Internal.MockArgumentMatcher exception = default, SourceMock.Internal.MockArgumentMatcher> formatter = default);
36 | System.Collections.Generic.IReadOnlyList IsEnabled(SourceMock.Internal.MockArgumentMatcher logLevel = default);
37 | System.Collections.Generic.IReadOnlyList BeginScope(SourceMock.Internal.MockArgumentMatcher state = default);
38 | }
39 | }
--------------------------------------------------------------------------------
/Generators.Tests/Generators.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net5.0
4 | false
5 | SourceMock.Generators.Tests
6 | SourceMock.Generators.Tests
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | PreserveNewest
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Generators.Tests/MockGeneratorTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using Azure.Storage.Blobs;
7 | using Microsoft.CodeAnalysis;
8 | using Microsoft.CodeAnalysis.CSharp;
9 | using Microsoft.Extensions.Logging;
10 | using Xunit;
11 | using Xunit.Abstractions;
12 |
13 | namespace SourceMock.Generators.Tests {
14 | public class MockGeneratorTests {
15 | private readonly ITestOutputHelper _output;
16 |
17 | public MockGeneratorTests(ITestOutputHelper output) {
18 | _output = output;
19 | }
20 |
21 | [Theory]
22 | [InlineData(typeof(ILogger<>), "LoggerMock.cs")]
23 | [InlineData(typeof(BlobContainerClient), "BlobContainerClientMock.cs")]
24 | public void Generator_GeneratesExpectedMock(Type targetType, string expectedTextFileName) {
25 | // Arrange
26 | var targetTypeFullName = targetType.IsGenericType
27 | // Remove `1 at the end. Would not work with more than 9 generic arguments.
28 | ? targetType.FullName!.Remove(targetType.FullName.Length - 2) + "<>"
29 | : targetType.FullName!;
30 | var originalCompilation = CreateCompilationFromText(
31 | $@"[assembly: SourceMock.GenerateMocksForTypes(new[] {{
32 | typeof({targetTypeFullName})
33 | }})]",
34 | targetType.Assembly
35 | );
36 |
37 | var generator = new MockGenerator();
38 | var driver = (GeneratorDriver)CSharpGeneratorDriver.Create(generator);
39 |
40 | // Act
41 | driver = driver.RunGeneratorsAndUpdateCompilation(originalCompilation, out _, out var diagnostics);
42 |
43 | // Assert
44 | Assert.Empty(diagnostics);
45 | var tree = Assert.Single(driver.GetRunResult().GeneratedTrees);
46 | var text = tree.ToString();
47 | _output.WriteLine(text);
48 | Assert.Equal(GetExpectedText(expectedTextFileName), text);
49 | }
50 |
51 | private CSharpCompilation CreateCompilationFromText(string source, Assembly referenceAssembly) {
52 | var initialReferenceAssemblies = new[] {
53 | typeof(GenerateMocksForTypesAttribute).Assembly,
54 | referenceAssembly
55 | };
56 | var referenceAssemblies = initialReferenceAssemblies.Concat(
57 | initialReferenceAssemblies.SelectMany(GetReferencesRecursive)
58 | ).Distinct();
59 |
60 | var compilation = CSharpCompilation.Create("_",
61 | new[] { CSharpSyntaxTree.ParseText(source) },
62 | referenceAssemblies.Select(a => MetadataReference.CreateFromFile(a.Location)),
63 | new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
64 | );
65 |
66 | Assert.Empty(compilation.GetDiagnostics());
67 |
68 | return compilation;
69 | }
70 |
71 | private IEnumerable GetReferencesRecursive(Assembly assembly) {
72 | foreach (var reference in assembly.GetReferencedAssemblies()) {
73 | var referencedAssembly = Assembly.Load(reference);
74 | yield return referencedAssembly;
75 | foreach (var nestedReference in GetReferencesRecursive(referencedAssembly)) {
76 | yield return nestedReference;
77 | }
78 | }
79 | }
80 |
81 | private string GetExpectedText(string name) {
82 | return File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Expected", name));
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Generators/Generators.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | false
6 | SourceMock.Generators
7 | SourceMock.Generators
8 | true
9 |
10 |
11 |
12 | Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | all
23 | runtime; build; native; contentfiles; analyzers
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Generators/Internal/CodeWriter.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Roslyn.Utilities;
3 |
4 | namespace SourceMock.Generators.Internal {
5 | internal class CodeWriter {
6 | private readonly StringBuilder _builder;
7 |
8 | [PerformanceSensitive("")]
9 | public CodeWriter() {
10 | #pragma warning disable HAA0502 // Explicit allocation -- unavoidable for now, can be pooled later
11 | _builder = new StringBuilder();
12 | #pragma warning restore HAA0502
13 | }
14 |
15 | [PerformanceSensitive("")]
16 | public CodeWriter Write(string value) {
17 | _builder.Append(value);
18 | return this;
19 | }
20 |
21 | [PerformanceSensitive("")]
22 | public CodeWriter Write(string part1, string part2) {
23 | _builder
24 | .Append(part1)
25 | .Append(part2);
26 | return this;
27 | }
28 |
29 | [PerformanceSensitive("")]
30 | public CodeWriter Write(string part1, string part2, string part3) {
31 | _builder
32 | .Append(part1)
33 | .Append(part2)
34 | .Append(part3);
35 | return this;
36 | }
37 |
38 | [PerformanceSensitive("")]
39 | public CodeWriter Write(string part1, string part2, string part3, string part4) {
40 | _builder
41 | .Append(part1)
42 | .Append(part2)
43 | .Append(part3)
44 | .Append(part4);
45 | return this;
46 | }
47 |
48 | [PerformanceSensitive("")]
49 | internal CodeWriter Write(string part1, string part2, string part3, string part4, string part5) {
50 | _builder
51 | .Append(part1)
52 | .Append(part2)
53 | .Append(part3)
54 | .Append(part4)
55 | .Append(part5);
56 | return this;
57 | }
58 |
59 | [PerformanceSensitive("")]
60 | public CodeWriter Write(string part1, string part2, string part3, string part4, string part5, string part6) {
61 | _builder
62 | .Append(part1)
63 | .Append(part2)
64 | .Append(part3)
65 | .Append(part4)
66 | .Append(part5)
67 | .Append(part6);
68 | return this;
69 | }
70 |
71 | [PerformanceSensitive("")]
72 | public CodeWriter Write(string part1, string part2, string part3, string part4, string part5, string part6, string part7) {
73 | _builder
74 | .Append(part1)
75 | .Append(part2)
76 | .Append(part3)
77 | .Append(part4)
78 | .Append(part5)
79 | .Append(part6)
80 | .Append(part7);
81 | return this;
82 | }
83 |
84 | [PerformanceSensitive("")]
85 | public CodeWriter Write((string part1, string part2, string part3, string part4) parts) {
86 | _builder
87 | .Append(parts.part1)
88 | .Append(parts.part2)
89 | .Append(parts.part3)
90 | .Append(parts.part4);
91 | return this;
92 | }
93 |
94 | [PerformanceSensitive("")]
95 | public CodeWriter WriteLine() {
96 | _builder.AppendLine();
97 | return this;
98 | }
99 |
100 | [PerformanceSensitive("")]
101 | public CodeWriter WriteLine(string line) {
102 | _builder.AppendLine(line);
103 | return this;
104 | }
105 |
106 | [PerformanceSensitive("")]
107 | public CodeWriter WriteLine(string part1, string part2) {
108 | _builder
109 | .Append(part1)
110 | .AppendLine(part2);
111 | return this;
112 | }
113 |
114 | [PerformanceSensitive("")]
115 | public CodeWriter WriteLine(string part1, string part2, string part3) {
116 | _builder
117 | .Append(part1)
118 | .Append(part2)
119 | .AppendLine(part3);
120 | return this;
121 | }
122 |
123 | [PerformanceSensitive("")]
124 | public CodeWriter WriteLine(string part1, string part2, string part3, string part4) {
125 | _builder
126 | .Append(part1)
127 | .Append(part2)
128 | .Append(part3)
129 | .AppendLine(part4);
130 | return this;
131 | }
132 |
133 | [PerformanceSensitive("")]
134 | public CodeWriter WriteLine(string part1, string part2, string part3, string part4, string part5) {
135 | _builder
136 | .Append(part1)
137 | .Append(part2)
138 | .Append(part3)
139 | .Append(part4)
140 | .AppendLine(part5);
141 | return this;
142 | }
143 |
144 | [PerformanceSensitive("")]
145 | public CodeWriter WriteLine(string part1, string part2, string part3, string part4, string part5, string part6) {
146 | _builder
147 | .Append(part1)
148 | .Append(part2)
149 | .Append(part3)
150 | .Append(part4)
151 | .Append(part5)
152 | .AppendLine(part6);
153 | return this;
154 | }
155 |
156 | [PerformanceSensitive("")]
157 | public CodeWriter WriteLine(string part1, string part2, string part3, string part4, string part5, string part6, string part7) {
158 | _builder
159 | .Append(part1)
160 | .Append(part2)
161 | .Append(part3)
162 | .Append(part4)
163 | .Append(part5)
164 | .Append(part6)
165 | .AppendLine(part7);
166 | return this;
167 | }
168 |
169 | [PerformanceSensitive("")]
170 | public CodeWriter WriteLine(string part1, string part2, string part3, string part4, string part5, string part6, string part7, string part8) {
171 | _builder
172 | .Append(part1)
173 | .Append(part2)
174 | .Append(part3)
175 | .Append(part4)
176 | .Append(part5)
177 | .Append(part6)
178 | .Append(part7)
179 | .AppendLine(part8);
180 | return this;
181 | }
182 |
183 | [PerformanceSensitive("")]
184 | public CodeWriter WriteGeneric(string genericTypeName, string genericArgumentName) {
185 | _builder
186 | .Append(genericTypeName)
187 | .Append("<")
188 | .Append(genericArgumentName)
189 | .Append(">");
190 |
191 | return this;
192 | }
193 |
194 | [PerformanceSensitive("")]
195 | public CodeWriter WriteGeneric(string genericTypeName, string genericArgumentName1, string genericArgumentName2) {
196 | _builder
197 | .Append(genericTypeName)
198 | .Append("<")
199 | .Append(genericArgumentName1)
200 | .Append(", ")
201 | .Append(genericArgumentName2)
202 | .Append(">");
203 |
204 | return this;
205 | }
206 |
207 | public CodeWriter WriteIfNotNull(string? value) {
208 | _builder.Append(value);
209 | return this;
210 | }
211 |
212 | public CodeWriter WriteIfNotNull(string? value, (string part1, string part2) ifNotNull, string ifNull) {
213 | if (value != null) {
214 | _builder
215 | .Append(value)
216 | .Append(ifNotNull.part1)
217 | .Append(ifNotNull.part2);
218 | }
219 | else {
220 | _builder.Append(ifNull);
221 | }
222 |
223 | return this;
224 | }
225 |
226 | public CodeWriter WriteIfNotNull(string? value, (string part1, string part2, string part3) ifNotNull, string ifNull) {
227 | if (value != null) {
228 | _builder
229 | .Append(value)
230 | .Append(ifNotNull.part1)
231 | .Append(ifNotNull.part2)
232 | .Append(ifNotNull.part3);
233 | }
234 | else {
235 | _builder.Append(ifNull);
236 | }
237 |
238 | return this;
239 | }
240 |
241 | [PerformanceSensitive("")]
242 | public CodeWriter Append(CodeWriter other) {
243 | _builder.Append(other._builder);
244 | return this;
245 | }
246 |
247 | public int CurrentLength => _builder.Length;
248 |
249 | public override string ToString() => _builder.ToString();
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/Generators/Internal/DefaultConstraintCandidateCollector.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Microsoft.CodeAnalysis;
3 | using Roslyn.Utilities;
4 |
5 | namespace SourceMock.Generators.Internal {
6 | // https://github.com/dotnet/csharplang/discussions/4638#discussioncomment-594388
7 | public class DefaultConstraintCandidateCollector : SymbolVisitor {
8 | private NullableAnnotation? _lastAnnotation;
9 | private readonly HashSet _results = new();
10 |
11 | public ISet Collect(ISymbol symbol) {
12 | _results.Clear();
13 | Visit(symbol);
14 | return _results;
15 | }
16 |
17 | public override void VisitMethod(IMethodSymbol symbol) {
18 | _lastAnnotation = symbol.ReturnNullableAnnotation;
19 | Visit(symbol.ReturnType);
20 |
21 | foreach (var parameter in symbol.Parameters) {
22 | _lastAnnotation = parameter.NullableAnnotation;
23 | Visit(parameter.Type);
24 | }
25 | }
26 |
27 | [PerformanceSensitive("")]
28 | public override void VisitArrayType(IArrayTypeSymbol symbol) {
29 | _lastAnnotation = symbol.ElementNullableAnnotation;
30 | Visit(symbol.ElementType);
31 | }
32 |
33 | [PerformanceSensitive("")]
34 | public override void VisitNamedType(INamedTypeSymbol symbol) {
35 | if (!symbol.IsGenericType)
36 | return;
37 |
38 | var arguments = symbol.TypeArguments;
39 | for (var i = 0; i < arguments.Length; i++) {
40 | _lastAnnotation = symbol.TypeArgumentNullableAnnotations[0];
41 | Visit(arguments[i]);
42 | }
43 | }
44 |
45 | [PerformanceSensitive("")]
46 | public override void VisitTypeParameter(ITypeParameterSymbol symbol) {
47 | if (_lastAnnotation != NullableAnnotation.Annotated)
48 | return;
49 |
50 | if (symbol.HasValueTypeConstraint || symbol.HasReferenceTypeConstraint)
51 | return; // we'll copy existing constraints anyways
52 |
53 | _results.Add(symbol);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Generators/Internal/Exceptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.CodeAnalysis;
3 |
4 | namespace SourceMock.Generators.Internal {
5 | internal static class Exceptions {
6 |
7 | // Having this as a separate method removes need to suppress allocation warnings each time in exceptional situations
8 | public static NotSupportedException NotSupported(string message) => new(message);
9 |
10 | public static NotSupportedException MemberNotSupported(ISymbol symbol) => Exceptions.NotSupported(
11 | $"{symbol.Name} has an unsupported member symbol type ({symbol.GetType()})"
12 | );
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Generators/Internal/GeneratorCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace SourceMock.Generators.Internal {
8 | // Note: SourceGenerator documentation explicitly does NOT recommend caching.
9 | // https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#participate-in-the-ide-experience
10 | // However I can't in good conscience release something that regenerates tons of files on each cursor move.
11 | internal class GeneratorCache: IDisposable {
12 | private static readonly TimeSpan Expiry = TimeSpan.FromMinutes(10);
13 |
14 | private readonly string _name;
15 | private readonly ConcurrentDictionary _dictionary;
16 | private readonly CancellationTokenSource _cleanupCancellationSource = new();
17 | #pragma warning disable IDE0052 // Remove unread private members
18 | private readonly Task _cleanupTask;
19 | #pragma warning restore IDE0052 // Remove unread private members
20 |
21 | public GeneratorCache(string name, IEqualityComparer? comparer = null) {
22 | _dictionary = new(comparer ?? EqualityComparer.Default);
23 | _cleanupTask = Task.Run(CleanupAsync);
24 | _name = name;
25 | }
26 |
27 | public bool TryGetValue(TKey key, out TValue? value) {
28 | var found = _dictionary.TryGetValue(key, out var entry);
29 | if (!found) {
30 | value = default;
31 | return false;
32 | }
33 |
34 | value = entry.Value;
35 | entry.LastAccessUtc = DateTime.UtcNow;
36 | return found;
37 | }
38 |
39 | public void TryAdd(TKey key, TValue value) {
40 | _dictionary.TryAdd(key, new Entry(value, DateTime.UtcNow));
41 | }
42 |
43 | private async Task CleanupAsync() {
44 | while (!_cleanupCancellationSource.IsCancellationRequested) {
45 | await Task.Delay(Expiry, _cleanupCancellationSource.Token).ConfigureAwait(false);
46 |
47 | GeneratorLog.Log($"Starting cache cleanup for {_name}. Size: {_dictionary.Count}");
48 | var cutoff = DateTime.UtcNow - Expiry;
49 | var keysToRemove = new List();
50 | foreach (var pair in _dictionary) {
51 | if (pair.Value.LastAccessUtc < cutoff)
52 | keysToRemove.Add(pair.Key);
53 | }
54 |
55 | foreach (var key in keysToRemove) {
56 | _dictionary.TryRemove(key, out _);
57 | }
58 | GeneratorLog.Log($"Finished cache cleanup for {_name}. Size: {_dictionary.Count}");
59 | }
60 | }
61 |
62 | private class Entry {
63 | public Entry(TValue value, DateTime lastAccessUtc) {
64 | Value = value;
65 | LastAccessUtc = lastAccessUtc;
66 | }
67 |
68 | public TValue Value { get; }
69 | // Thread safety does not matter for this one --
70 | // if we set it from multiple threads, the times
71 | // will be close enough for it not to matter who
72 | // wins.
73 | public DateTime LastAccessUtc { get; set; }
74 | }
75 |
76 | public void Dispose() {
77 | _cleanupCancellationSource.Cancel();
78 | _cleanupCancellationSource.Dispose();
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Generators/Internal/GeneratorLog.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Roslyn.Utilities;
3 | using System.Threading;
4 | using System.Runtime.CompilerServices;
5 | #if DEBUG
6 | using System;
7 | using System.Collections.Concurrent;
8 | using System.IO;
9 | using System.Threading.Tasks;
10 | #endif
11 |
12 | namespace SourceMock.Generators.Internal {
13 | internal static class GeneratorLog {
14 | #if DEBUG
15 | private static readonly DateTime _start;
16 | private static readonly Stopwatch _stopwatch;
17 |
18 | private static readonly CancellationTokenSource _logTaskCancellationSource;
19 | private static readonly Task? _logTask;
20 | private static readonly ConcurrentQueue<(DateTime date, string message)> _entries = new();
21 |
22 | static GeneratorLog() {
23 | static string GetLogPath([CallerFilePath] string path = "") {
24 | using var process = Process.GetCurrentProcess();
25 | var now = DateTime.Now;
26 | var fileName = $"{Path.GetFileName(process.MainModule.FileName)} ({process.Id}) {now:HH_mm_ss.ffffff}.log";
27 | return Path.Combine(Path.GetDirectoryName(path), "..", "Logs", $"{now:MMM dd}", fileName);
28 | }
29 |
30 | // Note that the time will drift over time (https://nima-ara-blog.azurewebsites.net/high-resolution-clock-in-net/),
31 | // but for debug scenarios that's totally fine
32 | _start = DateTime.Now;
33 | _stopwatch = Stopwatch.StartNew();
34 |
35 | var logPath = GetLogPath();
36 | Directory.CreateDirectory(Path.GetDirectoryName(logPath));
37 |
38 | _logTaskCancellationSource = new CancellationTokenSource();
39 | _logTask = Task.Run(async () => {
40 | while (!_logTaskCancellationSource.IsCancellationRequested) {
41 | if (!_entries.IsEmpty) {
42 | using var streamWriter = new StreamWriter(logPath, append: true);
43 | while (_entries.TryDequeue(out var entry)) {
44 | streamWriter.WriteLine($"[{entry.date:HH.mm.ss.fff}] {entry.message}");
45 | }
46 | }
47 | await Task.Delay(100, _logTaskCancellationSource.Token).ConfigureAwait(false);
48 | }
49 | });
50 |
51 | Log($"Starting log from {typeof(GeneratorLog).Assembly.Location}");
52 | AppDomain.CurrentDomain.ProcessExit += (_, _) => {
53 | Log("Stopping log on process exit");
54 | SpinWait.SpinUntil(() => _entries.IsEmpty, 500);
55 | _logTaskCancellationSource.Cancel();
56 | SpinWait.SpinUntil(() => _logTask.IsCompleted, 500);
57 | };
58 | }
59 | #endif
60 |
61 | [Conditional("DEBUG")]
62 | [PerformanceSensitive("")]
63 | public static void Log(string message) {
64 | #if DEBUG
65 | _entries.Enqueue((_start.AddTicks(_stopwatch.ElapsedTicks), message));
66 | #endif
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Generators/Internal/Indents.cs:
--------------------------------------------------------------------------------
1 | namespace SourceMock.Generators.Internal {
2 | internal static class Indents {
3 | public const string Type = " ";
4 | public const string Member = Type + " ";
5 | public const string MemberBody = Member + " ";
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Generators/Internal/KnownTypes.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace SourceMock.Generators.Internal {
4 | internal static class KnownTypes {
5 | public static class GenerateMocksForAssemblyOfAttribute {
6 | public const string Name = "GenerateMocksForAssemblyOfAttribute";
7 | public const string NameWithoutAttribute = "GenerateMocksForAssemblyOf";
8 |
9 | public static bool NamespaceMatches(INamespaceSymbol? @namespace) => @namespace is {
10 | Name: "SourceMock", ContainingNamespace: { IsGlobalNamespace: true }
11 | };
12 |
13 | public static class NamedParameters {
14 | public const string ExcludeRegex = "ExcludeRegex";
15 | }
16 | }
17 |
18 | public static class GenerateMocksForTypesAttribute {
19 | public const string Name = "GenerateMocksForTypesAttribute";
20 | public const string NameWithoutAttribute = "GenerateMocksForTypes";
21 |
22 | public static bool NamespaceMatches(INamespaceSymbol? @namespace) => @namespace is {
23 | Name: "SourceMock", ContainingNamespace: { IsGlobalNamespace: true }
24 | };
25 | }
26 |
27 | public static class IMock {
28 | public const string FullName = "SourceMock.IMock";
29 | }
30 |
31 | public static class IMockMethodSetup {
32 | public const string FullName = "SourceMock.Interfaces.IMockMethodSetup";
33 | }
34 |
35 | public static class IMockArgumentMatcher {
36 | public const string FullName = "SourceMock.Internal.IMockArgumentMatcher";
37 | }
38 |
39 | public static class MockMethodHandler {
40 | public const string FullName = "SourceMock.Internal.MockMethodHandler";
41 | }
42 |
43 | public static class IMockPropertySetup {
44 | public const string FullName = "SourceMock.Interfaces.IMockPropertySetup";
45 | }
46 |
47 | public static class IMockSettablePropertySetup {
48 | public const string FullName = "SourceMock.Interfaces.IMockSettablePropertySetup";
49 | }
50 |
51 | public static class IMockPropertyCalls {
52 | public const string FullName = "SourceMock.Interfaces.IMockPropertyCalls";
53 | }
54 |
55 | public static class IMockSettablePropertyCalls {
56 | public const string FullName = "SourceMock.Interfaces.IMockSettablePropertyCalls";
57 | }
58 |
59 | public static class MockPropertyHandler {
60 | public const string FullName = "SourceMock.Internal.MockPropertyHandler";
61 | }
62 |
63 | public static class MockArgumentMatcher {
64 | public const string FullName = "SourceMock.Internal.MockArgumentMatcher";
65 | }
66 |
67 | public static class NoArguments {
68 | public const string FullName = "SourceMock.NoArguments";
69 | }
70 |
71 | public static class VoidReturn {
72 | public const string FullName = "SourceMock.Internal.VoidReturn";
73 | }
74 |
75 | public static class IReadOnlyList {
76 | public const string FullName = "System.Collections.Generic.IReadOnlyList";
77 | }
78 |
79 | public static class Func {
80 | public const string FullName = "System.Func";
81 | }
82 |
83 | public static class Action {
84 | public const string FullName = "System.Action";
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Generators/Internal/MockClassGenerator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.CodeAnalysis;
3 | using Roslyn.Utilities;
4 | using SourceMock.Generators.Internal.Models;
5 |
6 | namespace SourceMock.Generators.Internal {
7 | internal class MockClassGenerator {
8 | private static readonly SymbolDisplayFormat TargetTypeNamespaceDisplayFormat = SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(
9 | SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining
10 | );
11 |
12 | private readonly MockTargetModelFactory _modelFactory;
13 | private readonly MockMemberGenerator _mockMemberGenerator = new();
14 |
15 | public MockClassGenerator(MockTargetModelFactory modelFactory) {
16 | _modelFactory = modelFactory;
17 | }
18 |
19 | [PerformanceSensitive("")]
20 | public string Generate(MockTarget target, IAssemblySymbol currentAssembly) {
21 | var targetTypeNamespace = target.Type.ContainingNamespace.ToDisplayString(TargetTypeNamespaceDisplayFormat);
22 | var mockBaseName = GenerateMockBaseName(target.Type.Name);
23 | var typeParameters = GenerateTypeParametersAsString(target);
24 | var mockClassName = mockBaseName + "Mock" + typeParameters;
25 | var setupInterfaceName = "I" + mockBaseName + "Setup" + typeParameters;
26 | var callsInterfaceName = "I" + mockBaseName + "Calls" + typeParameters;
27 | var customDelegatesClassName = mockBaseName + "Delegates" + typeParameters;
28 |
29 | #pragma warning disable HAA0502 // Explicit allocation -- unavoidable for now, can be pooled later
30 | var mainWriter = new CodeWriter()
31 | #pragma warning restore HAA0502
32 | .WriteLine("#nullable enable")
33 | .WriteLine("namespace ", targetTypeNamespace, ".Mocks {");
34 |
35 | mainWriter
36 | .Write(Indents.Type, "internal class ", mockClassName, " : ")
37 | .Write(target.FullTypeName, ", ", setupInterfaceName, ", ", callsInterfaceName, ", ")
38 | .WriteGeneric(KnownTypes.IMock.FullName, target.FullTypeName)
39 | .WriteIfNotNull(target.GenericParameterConstraints, (Environment.NewLine, Indents.Type, "{"), " {")
40 | .WriteLine()
41 | .WriteLine(Indents.Member, "public ", setupInterfaceName, " Setup => this;")
42 | .WriteLine(Indents.Member, "public ", callsInterfaceName, " Calls => this;");
43 |
44 | #pragma warning disable HAA0502 // Explicit allocation -- unavoidable for now, can be pooled later
45 | var customDelegatesClassWriter = new CodeWriter()
46 | #pragma warning restore HAA0502
47 | .WriteLine(Indents.Type, "internal static class ", customDelegatesClassName, " {");
48 | var customDelegatesEmptyLength = customDelegatesClassWriter.CurrentLength;
49 |
50 | #pragma warning disable HAA0502 // Explicit allocation -- unavoidable for now, can be pooled later
51 | var setupInterfaceWriter = new CodeWriter()
52 | #pragma warning restore HAA0502
53 | .WriteLine(Indents.Type, "internal interface ", setupInterfaceName, " {");
54 |
55 | #pragma warning disable HAA0502 // Explicit allocation -- unavoidable for now, can be pooled later
56 | var callsInterfaceWriter = new CodeWriter()
57 | #pragma warning restore HAA0502
58 | .WriteLine(Indents.Type, "internal interface ", callsInterfaceName, " {");
59 |
60 | #pragma warning disable HAA0401 // Possible allocation of reference type enumerator - TODO
61 | foreach (var member in _modelFactory.GetMockTargetMembers(target, customDelegatesClassName, currentAssembly)) {
62 | #pragma warning restore HAA0401
63 | mainWriter.WriteLine();
64 | _mockMemberGenerator.WriteMemberMocks(
65 | mainWriter,
66 | customDelegatesClassWriter,
67 | setupInterfaceWriter,
68 | setupInterfaceName,
69 | callsInterfaceWriter,
70 | callsInterfaceName,
71 | member,
72 | currentAssembly
73 | ).WriteLine();
74 | }
75 |
76 | mainWriter.WriteLine(Indents.Type, "}");
77 |
78 | if (customDelegatesClassWriter.CurrentLength != customDelegatesEmptyLength) {
79 | customDelegatesClassWriter.Write(Indents.Type, "}");
80 | mainWriter
81 | .WriteLine()
82 | .Append(customDelegatesClassWriter)
83 | .WriteLine();
84 | }
85 |
86 | setupInterfaceWriter.Write(Indents.Type, "}");
87 | mainWriter
88 | .WriteLine()
89 | .Append(setupInterfaceWriter)
90 | .WriteLine();
91 |
92 | callsInterfaceWriter.Write(Indents.Type, "}");
93 | mainWriter
94 | .WriteLine()
95 | .Append(callsInterfaceWriter)
96 | .WriteLine();
97 |
98 | mainWriter.Write("}");
99 | return mainWriter.ToString();
100 | }
101 |
102 | [PerformanceSensitive("")]
103 | private string GenerateMockBaseName(string targetName) {
104 | if (targetName.Length < 3)
105 | return targetName;
106 |
107 | var canRemoveI = targetName[0] == 'I'
108 | && char.IsUpper(targetName[1])
109 | && char.IsLower(targetName[2]);
110 |
111 | return canRemoveI ? targetName.Substring(1) : targetName;
112 | }
113 |
114 | [PerformanceSensitive("")]
115 | private string GenerateTypeParametersAsString(MockTarget target) {
116 | var parameters = target.Type.TypeParameters;
117 | if (parameters.IsEmpty)
118 | return "";
119 |
120 | #pragma warning disable HAA0502 // Explicit allocation -- unavoidable for now, can be pooled later
121 | var writer = new CodeWriter();
122 | #pragma warning restore HAA0502
123 | writer.Write("<");
124 | var index = 0;
125 | foreach (var parameter in parameters) {
126 | if (index > 0)
127 | writer.Write(", ");
128 | writer.Write(parameter.Name);
129 | index += 1;
130 | }
131 | writer.Write(">");
132 | return writer.ToString();
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/Generators/Internal/MockMemberGenerator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Immutable;
3 | using Microsoft.CodeAnalysis;
4 | using Roslyn.Utilities;
5 | using SourceMock.Generators.Internal.Models;
6 |
7 | namespace SourceMock.Generators.Internal {
8 | internal class MockMemberGenerator {
9 | private readonly DefaultConstraintCandidateCollector _defaultConstraintCollector = new();
10 |
11 | [PerformanceSensitive("")]
12 | public CodeWriter WriteMemberMocks(
13 | CodeWriter mockWriter,
14 | CodeWriter customDelegatesClassWriter,
15 | CodeWriter setupInterfaceWriter,
16 | string setupInterfaceName,
17 | CodeWriter callsInterfaceWriter,
18 | string callsInterfaceName,
19 | in MockTargetMember member,
20 | IAssemblySymbol currentAssembly
21 | ) {
22 | WriteHandlerField(mockWriter, member).WriteLine();
23 | if (member.MethodRunDelegateType is { IsCustom: true } runDelegate)
24 | WriteCustomRunDelegate(customDelegatesClassWriter, member, runDelegate).WriteLine();
25 | WriteSetupInterfaceMember(setupInterfaceWriter, member).WriteLine();
26 | WriteSetupMemberImplementation(mockWriter, setupInterfaceName, member).WriteLine();
27 | WriteMemberImplementation(mockWriter, member, currentAssembly).WriteLine();
28 | WriteCallsInterfaceMember(callsInterfaceWriter, member).WriteLine();
29 | WriteCallsMemberImplementation(mockWriter, callsInterfaceName, member);
30 | return mockWriter;
31 | }
32 |
33 | [PerformanceSensitive("")]
34 | private CodeWriter WriteHandlerField(CodeWriter writer, in MockTargetMember member) {
35 | writer.Write(Indents.Member, "private readonly ");
36 | switch (member.Symbol) {
37 | case IMethodSymbol:
38 | writer.Write(KnownTypes.MockMethodHandler.FullName, " ", member.HandlerFieldName, " = new()");
39 | break;
40 |
41 | case IPropertySymbol property:
42 | writer
43 | .WriteGeneric(KnownTypes.MockPropertyHandler.FullName, member.HandlerGenericParameterFullName)
44 | .Write(" ", member.HandlerFieldName)
45 | .Write(" = new(", property.SetMethod != null ? "true" : "false", ")");
46 | break;
47 |
48 | default:
49 | throw Exceptions.MemberNotSupported(member.Symbol);
50 | }
51 | return writer.Write(";");
52 | }
53 |
54 | [PerformanceSensitive("")]
55 | private CodeWriter WriteCustomRunDelegate(CodeWriter writer, in MockTargetMember member, MockTargetMethodRunDelegateType runDelegate) {
56 | writer.Write(Indents.Member, "public delegate ", member.TypeFullName, " ", runDelegate.CustomNameWithGenericParameters!, "(");
57 | WriteMethodImplementationParametersIfAny(writer, member, out _);
58 | return writer.Write(");");
59 | }
60 |
61 | [PerformanceSensitive("")]
62 | private CodeWriter WriteSetupInterfaceMember(CodeWriter writer, in MockTargetMember member) {
63 | writer.Write(Indents.Member);
64 | WriteSetupMemberType(writer, member);
65 | writer.Write(" ");
66 | WriteSetupOrCallsInterfaceMemberNameAndParameters(writer, member);
67 | return writer;
68 | }
69 |
70 | [PerformanceSensitive("")]
71 | private CodeWriter WriteSetupMemberImplementation(CodeWriter writer, string setupInterfaceName, in MockTargetMember member) {
72 | writer.Write(Indents.Member);
73 |
74 | WriteSetupMemberType(writer, member);
75 | writer.Write(" ", setupInterfaceName, ".", member.Name);
76 | if (member.Symbol is IMethodSymbol) {
77 | WriteMethodGenericParametersIfAny(writer, member);
78 | writer.Write("(");
79 | WriteSetupOrCallsMemberParameters(writer, member, appendDefaultValue: false);
80 | writer.Write(")");
81 | WriteExplicitImplementationDefaultConstraintsIfAny(writer, member);
82 | }
83 |
84 | writer.Write(" => ");
85 |
86 | writer.Write(member.HandlerFieldName, ".Setup");
87 | if (member.Symbol is IMethodSymbol) {
88 | writer.Write("<", member.MethodRunDelegateType!.Value.FullName, ", ", member.HandlerGenericParameterFullName, ">(");
89 | WriteCommonMethodHandlerArguments(writer, member, KnownTypes.IMockArgumentMatcher.FullName);
90 | writer.Write(")");
91 | }
92 | else {
93 | writer.Write("()");
94 | }
95 |
96 | return writer.Write(";");
97 | }
98 |
99 | [PerformanceSensitive("")]
100 | private CodeWriter WriteSetupMemberType(CodeWriter writer, in MockTargetMember member) {
101 | var setupTypeFullName = member.Symbol switch {
102 | IMethodSymbol => KnownTypes.IMockMethodSetup.FullName,
103 | IPropertySymbol property => property.SetMethod != null
104 | ? KnownTypes.IMockSettablePropertySetup.FullName
105 | : KnownTypes.IMockPropertySetup.FullName,
106 | var s => throw Exceptions.MemberNotSupported(s)
107 | };
108 |
109 | if (member.Symbol is IPropertySymbol)
110 | return writer.WriteGeneric(setupTypeFullName, member.TypeFullName);
111 |
112 | if (member.IsVoidMethod)
113 | return writer.WriteGeneric(setupTypeFullName, member.MethodRunDelegateType!.Value.FullName);
114 |
115 | return writer.WriteGeneric(setupTypeFullName, member.MethodRunDelegateType!.Value.FullName, member.TypeFullName);
116 | }
117 |
118 | [PerformanceSensitive("")]
119 | private CodeWriter WriteMemberImplementation(CodeWriter writer, in MockTargetMember member, IAssemblySymbol currentAssembly) {
120 | writer.Write(Indents.Member);
121 | if (member.Symbol.ContainingType.TypeKind == TypeKind.Interface) {
122 | writer.Write("public ");
123 | }
124 | else {
125 | var currentAssemblyHasInternalAccess = member.Symbol.ContainingAssembly.GivesAccessTo(currentAssembly);
126 | writer.Write(GetAccessibility(member.Symbol.DeclaredAccessibility, currentAssemblyHasInternalAccess), " override ");
127 | }
128 | writer.Write(member.TypeFullName, " ", member.Name);
129 |
130 | switch (member.Symbol) {
131 | case IMethodSymbol:
132 | WriteMethodGenericParametersIfAny(writer, member);
133 | writer.Write("(");
134 | WriteMethodImplementationParametersIfAny(writer, member, out var hasOutParameters);
135 | writer.Write(")");
136 | writer.WriteIfNotNull(member.GenericParameterConstraints, (Environment.NewLine, Indents.Member), " ");
137 | WriteMethodImplementationBody(writer, hasOutParameters, member);
138 | break;
139 |
140 | case IPropertySymbol property:
141 | if (property.SetMethod != null) {
142 | writer.WriteLine(" {");
143 | writer.Write(Indents.MemberBody, "get => ", member.HandlerFieldName, ".GetterHandler");
144 | WriteMemberImplementationHandlerCall(
145 | writer, member,
146 | (KnownTypes.Func.FullName, "<", member.TypeFullName, ">"),
147 | parametersOverride: ImmutableArray.Empty
148 | );
149 | writer.WriteLine(";");
150 | writer.Write(Indents.MemberBody, "set => ", member.HandlerFieldName, ".SetterHandler");
151 | WriteMemberImplementationHandlerCall(
152 | writer, member,
153 | (KnownTypes.Action.FullName, "<", member.TypeFullName, ">")
154 | );
155 | writer.WriteLine(";");
156 | writer.Write(Indents.Member, "}");
157 | }
158 | else {
159 | writer.Write(" => ", member.HandlerFieldName, ".GetterHandler");
160 | WriteMemberImplementationHandlerCall(
161 | writer, member,
162 | (KnownTypes.Func.FullName, "<", member.TypeFullName, ">")
163 | );
164 | writer.Write(";");
165 | }
166 | break;
167 |
168 | default:
169 | throw Exceptions.MemberNotSupported(member.Symbol);
170 | }
171 |
172 | return writer;
173 | }
174 |
175 | [PerformanceSensitive("")]
176 | private string GetAccessibility(Accessibility accessibility, bool currentAssemblylHasInternalAccess) => accessibility switch {
177 | Accessibility.Public => "public",
178 | Accessibility.Protected => "protected",
179 | Accessibility.ProtectedOrInternal => currentAssemblylHasInternalAccess ? "protected internal" : "protected",
180 | Accessibility.ProtectedAndInternal => "private protected",
181 | #pragma warning disable HAA0601 // Boxing -- OK in exceptional case
182 | _ => throw Exceptions.NotSupported($"Unexpected accessibility: {accessibility}")
183 | #pragma warning restore HAA0601
184 | };
185 |
186 | [PerformanceSensitive("")]
187 | private CodeWriter WriteMethodImplementationParametersIfAny(CodeWriter writer, MockTargetMember member, out bool hasOutParameters) {
188 | hasOutParameters = false;
189 | foreach (var parameter in member.Parameters) {
190 | if (parameter.Index > 0)
191 | writer.Write(", ");
192 | if (GetRefModifier(parameter.RefKind) is {} modifier)
193 | writer.Write(modifier, " ");
194 | writer.Write(parameter.FullTypeName, " ", parameter.Name);
195 | hasOutParameters = hasOutParameters || (parameter.RefKind == RefKind.Out);
196 | }
197 | return writer;
198 | }
199 |
200 | [PerformanceSensitive("")]
201 | private string? GetRefModifier(RefKind refKind) => refKind switch {
202 | RefKind.None => null,
203 | RefKind.Ref => "ref",
204 | RefKind.In => "in",
205 | RefKind.Out => "out",
206 | #pragma warning disable HAA0601 // Boxing -- OK in exceptional case
207 | _ => throw Exceptions.NotSupported($"Unsupported parameter ref kind: {refKind}")
208 | #pragma warning restore HAA0601
209 | };
210 |
211 | [PerformanceSensitive("")]
212 | private CodeWriter WriteMethodImplementationBody(CodeWriter writer, bool hasOutParameters, in MockTargetMember member) {
213 | if (hasOutParameters) {
214 | writer
215 | .WriteLine("{")
216 | .Write(Indents.MemberBody, "var arguments = ");
217 | WriteCommonArgumentsArray(writer, "object?", member.Parameters).WriteLine(";");
218 | writer
219 | .Write(Indents.MemberBody, "var result = ", member.HandlerFieldName);
220 | WriteMemberImplementationHandlerCall(writer, member, argumentsArrayOverride: "arguments")
221 | .WriteLine(";");
222 | foreach (var parameter in member.Parameters) {
223 | if (parameter.RefKind is not RefKind.Out or RefKind.Ref)
224 | continue;
225 | writer.Write(Indents.MemberBody, parameter.Name, " = (", parameter.FullTypeName, ")arguments[", parameter.Index.ToString(), "]");
226 | if (!parameter.FullTypeName.EndsWith("?"))
227 | writer.Write("!");
228 | writer.WriteLine(";");
229 | }
230 | writer.WriteLine(Indents.MemberBody, "return result;");
231 | return writer.Write(Indents.Member, "}");
232 | }
233 |
234 | writer.Write("=> ", member.HandlerFieldName);
235 | WriteMemberImplementationHandlerCall(writer, member);
236 | return writer.Write(";");
237 | }
238 |
239 | [PerformanceSensitive("")]
240 | private CodeWriter WriteMemberImplementationHandlerCall(
241 | CodeWriter writer,
242 | in MockTargetMember member,
243 | (string part1, string part2, string part3, string part4)? runDelegateFullNameOverride = null,
244 | string? argumentsArrayOverride = null,
245 | ImmutableArray? parametersOverride = null
246 | ) {
247 | writer.Write(".Call<");
248 | if (runDelegateFullNameOverride != null) {
249 | writer.Write(runDelegateFullNameOverride.Value);
250 | }
251 | else {
252 | writer.Write(member.MethodRunDelegateType!.Value.FullName);
253 | }
254 | writer.Write(", ", member.HandlerGenericParameterFullName, ">(");
255 | WriteCommonMethodHandlerArguments(writer, member, "object?", argumentsArrayOverride, parametersOverride ?? member.Parameters);
256 | return writer.Write(")");
257 | }
258 |
259 | [PerformanceSensitive("")]
260 | private CodeWriter WriteCallsInterfaceMember(CodeWriter writer, in MockTargetMember member) {
261 | writer.Write(Indents.Member);
262 | WriteCallsMemberType(writer, member);
263 | writer.Write(" ");
264 | return WriteSetupOrCallsInterfaceMemberNameAndParameters(writer, member);
265 | }
266 |
267 | [PerformanceSensitive("")]
268 | private CodeWriter WriteCallsMemberImplementation(CodeWriter writer, string callsInterfaceName, in MockTargetMember member) {
269 | writer.Write(Indents.Member);
270 | WriteCallsMemberType(writer, member);
271 | writer.Write(" ", callsInterfaceName, ".", member.Name);
272 | if (member.Symbol is IMethodSymbol) {
273 | WriteMethodGenericParametersIfAny(writer, member);
274 | writer.Write("(");
275 | WriteSetupOrCallsMemberParameters(writer, member, appendDefaultValue: false);
276 | writer.Write(")");
277 | WriteExplicitImplementationDefaultConstraintsIfAny(writer, member);
278 | }
279 | writer.Write(" => ", member.HandlerFieldName, ".Calls(");
280 | if (member.Symbol is IMethodSymbol) {
281 | WriteCommonMethodHandlerArguments(writer, member, KnownTypes.IMockArgumentMatcher.FullName).Write(", ");
282 | var parameters = member.Parameters;
283 | if (!parameters.IsEmpty) {
284 | writer.Write("args => (");
285 | foreach (var parameter in parameters) {
286 | if (parameter.Index > 0)
287 | writer.Write(", ");
288 | writer.Write("(", parameter.FullTypeName, ")args[", parameter.Index.ToString(), "]");
289 | if (!parameter.FullTypeName.EndsWith("?"))
290 | writer.Write("!");
291 | }
292 | writer.Write(")");
293 | }
294 | else {
295 | writer.Write("_ => ", KnownTypes.NoArguments.FullName, ".Value");
296 | }
297 | }
298 | return writer.Write(");");
299 | }
300 |
301 | [PerformanceSensitive("")]
302 | private CodeWriter WriteCallsMemberType(CodeWriter writer, in MockTargetMember member) {
303 | if (member.Symbol is IPropertySymbol property) {
304 | var callsTypeFullName = property.SetMethod != null
305 | ? KnownTypes.IMockSettablePropertyCalls.FullName
306 | : KnownTypes.IMockPropertyCalls.FullName;
307 | return writer.WriteGeneric(callsTypeFullName, member.TypeFullName);
308 | }
309 |
310 | writer.Write(KnownTypes.IReadOnlyList.FullName, "<");
311 |
312 | var parameters = member.Parameters;
313 | if (parameters.Length > 1) {
314 | writer.Write("(");
315 | foreach (var parameter in parameters) {
316 | if (parameter.Index > 0)
317 | writer.Write(", ");
318 | writer.Write(parameter.FullTypeName, " ", parameter.Name);
319 | }
320 | writer.Write(")");
321 | }
322 | else if (parameters.Length == 1) {
323 | writer.Write(parameters[0].FullTypeName);
324 | }
325 | else {
326 | writer.Write(KnownTypes.NoArguments.FullName);
327 | }
328 |
329 | return writer.Write(">");
330 | }
331 |
332 | [PerformanceSensitive("")]
333 | private CodeWriter WriteSetupOrCallsInterfaceMemberNameAndParameters(CodeWriter writer, in MockTargetMember member) {
334 | writer.Write(member.Name);
335 | switch (member.Symbol) {
336 | case IMethodSymbol:
337 | WriteMethodGenericParametersIfAny(writer, member);
338 | writer.Write("(");
339 | WriteSetupOrCallsMemberParameters(writer, member, appendDefaultValue: true);
340 | writer.Write(")");
341 | writer.WriteIfNotNull(member.GenericParameterConstraints);
342 | writer.Write(";");
343 | break;
344 | case IPropertySymbol:
345 | writer.Write(" { get; }");
346 | break;
347 | default:
348 | throw Exceptions.MemberNotSupported(member.Symbol);
349 | }
350 | return writer;
351 | }
352 |
353 | [PerformanceSensitive("")]
354 | private CodeWriter WriteSetupOrCallsMemberParameters(CodeWriter writer, in MockTargetMember member, bool appendDefaultValue) {
355 | foreach (var parameter in member.Parameters) {
356 | if (parameter.Index > 0)
357 | writer.Write(", ");
358 | writer
359 | .WriteGeneric(KnownTypes.MockArgumentMatcher.FullName, parameter.FullTypeName)
360 | .Write(" ", parameter.Name);
361 | if (appendDefaultValue)
362 | writer.Write(" = default");
363 | }
364 | return writer;
365 | }
366 |
367 | [PerformanceSensitive("")]
368 | private CodeWriter WriteCommonMethodHandlerArguments(
369 | CodeWriter writer,
370 | in MockTargetMember member,
371 | string argumentTypeFullName,
372 | string? argumentsArrayOverride = null,
373 | ImmutableArray? parametersOverride = null
374 | ) {
375 | var parameters = parametersOverride ?? member.Parameters;
376 |
377 | var genericParameters = member.GenericParameters;
378 | if (!genericParameters.IsEmpty) {
379 | writer.Write("new[] { ");
380 | for (var i = 0; i < genericParameters.Length; i++) {
381 | if (i > 0)
382 | writer.Write(", ");
383 | writer.Write("typeof(", genericParameters[i].Name, ")");
384 | }
385 | writer.Write(" }");
386 | }
387 | else {
388 | writer.Write("null");
389 | }
390 | writer.Write(", ");
391 |
392 | if (argumentsArrayOverride != null)
393 | return writer.Write(argumentsArrayOverride);
394 |
395 | return WriteCommonArgumentsArray(writer, argumentTypeFullName, parameters);
396 | }
397 |
398 | [PerformanceSensitive("")]
399 | private CodeWriter WriteCommonArgumentsArray(CodeWriter writer, string argumentTypeFullName, ImmutableArray parameters) {
400 | if (parameters.IsEmpty)
401 | return writer.Write("null");
402 |
403 | writer.Write("new ", argumentTypeFullName, "[] { ");
404 | foreach (var parameter in parameters) {
405 | if (parameter.Index > 0)
406 | writer.Write(", ");
407 |
408 | if (parameter.RefKind == RefKind.Out && argumentTypeFullName == "object?") {
409 | writer.Write("default(", parameter.FullTypeName, ")");
410 | continue;
411 | }
412 |
413 | writer.Write(parameter.Name);
414 | }
415 | return writer.Write(" }");
416 | }
417 |
418 | [PerformanceSensitive("")]
419 | private CodeWriter WriteMethodGenericParametersIfAny(CodeWriter writer, in MockTargetMember member) {
420 | var genericParameters = member.GenericParameters;
421 | if (genericParameters.IsEmpty)
422 | return writer;
423 | writer.Write("<");
424 | for (var i = 0; i < genericParameters.Length; i++) {
425 | if (i > 0)
426 | writer.Write(", ");
427 | writer.Write(genericParameters[i].Name);
428 | }
429 | return writer.Write(">");
430 | }
431 |
432 | [PerformanceSensitive("")]
433 | private CodeWriter WriteExplicitImplementationDefaultConstraintsIfAny(CodeWriter writer, in MockTargetMember member) {
434 | if (member.GenericParameters.Length == 0)
435 | return writer;
436 |
437 | var parametersNeedingDefault = _defaultConstraintCollector.Collect(member.Symbol);
438 | if (parametersNeedingDefault.Count == 0)
439 | return writer;
440 |
441 | #pragma warning disable HAA0401 // Possible allocation of reference type enumerator - TODO
442 | foreach (var parameter in parametersNeedingDefault) {
443 | #pragma warning restore HAA0401
444 | writer.Write(" where ", parameter.Name, ": default");
445 | }
446 | return writer;
447 | }
448 | }
449 | }
450 |
--------------------------------------------------------------------------------
/Generators/Internal/MockTargetModelFactory.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Collections.Immutable;
3 | using System.Linq;
4 | using Microsoft.CodeAnalysis;
5 | using Roslyn.Utilities;
6 | using SourceMock.Generators.Internal.Models;
7 |
8 | namespace SourceMock.Generators.Internal {
9 | internal class MockTargetModelFactory {
10 | private static readonly SymbolDisplayFormat TargetTypeDisplayFormat = SymbolDisplayFormat.FullyQualifiedFormat
11 | .WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
12 |
13 | public MockTarget GetMockTarget(INamedTypeSymbol type) {
14 | var fullName = GetFullTypeName(type, NullableAnnotation.None);
15 | return new MockTarget(type, fullName, GetGenericConstraintsAsCode(Indents.Member, type.TypeParameters));
16 | }
17 |
18 | [PerformanceSensitive("")]
19 | public IEnumerable GetMockTargetMembers(MockTarget target, string customDelegatesClassName, IAssemblySymbol currentAssembly) {
20 | #pragma warning disable HAA0502 // Explicit allocation -- unavoidable for now, can be pooled later (or removed if we handle them differently)
21 | var lastOverloadIds = new Dictionary();
22 | #pragma warning restore HAA0502
23 |
24 | foreach (var member in target.Type.GetMembers()) {
25 | if (!lastOverloadIds.TryGetValue(member.Name, out var lastOverloadId))
26 | lastOverloadId = 0;
27 |
28 | var overloadId = lastOverloadId + 1;
29 | if (GetMockTargetMember(member, overloadId, customDelegatesClassName, currentAssembly) is not {} discovered)
30 | continue;
31 |
32 | lastOverloadIds[member.Name] = overloadId;
33 | yield return discovered;
34 | }
35 |
36 | if (target.Type.TypeKind != TypeKind.Interface)
37 | yield break;
38 |
39 | foreach (var @interface in target.Type.AllInterfaces) {
40 | foreach (var member in @interface.GetMembers()) {
41 | if (lastOverloadIds.ContainsKey(member.Name))
42 | throw Exceptions.NotSupported($"Type member {@interface.Name}.{member.Name} is hidden or overloaded by another type member. This is not yet supported.");
43 |
44 | if (GetMockTargetMember(member, 1, customDelegatesClassName, currentAssembly) is not {} discovered)
45 | continue;
46 |
47 | lastOverloadIds[member.Name] = 1;
48 | yield return discovered;
49 | }
50 | }
51 | }
52 |
53 | [PerformanceSensitive("")]
54 | private MockTargetMember? GetMockTargetMember(ISymbol member, int overloadId, string customDelegatesClassName, IAssemblySymbol currentAssembly) => member switch {
55 | IMethodSymbol method => GetMockTargetMethod(method, overloadId, customDelegatesClassName, currentAssembly),
56 | IPropertySymbol property => GetMockTargetProperty(property, overloadId, currentAssembly),
57 | IFieldSymbol => null,
58 | ITypeSymbol => null,
59 | _ => throw Exceptions.MemberNotSupported(member)
60 | };
61 |
62 | [PerformanceSensitive("")]
63 | private MockTargetMember? GetMockTargetMethod(IMethodSymbol method, int overloadId, string customDelegatesClassName, IAssemblySymbol currentAssembly) {
64 | if (method.MethodKind != MethodKind.Ordinary)
65 | return null;
66 |
67 | if (!IsVirtual(method))
68 | return null;
69 |
70 | if (!IsVisible(method, currentAssembly))
71 | return null;
72 |
73 | var parameters = ConvertParametersFromSymbols(method.Parameters);
74 | var returnTypeFullName = GetFullTypeName(method.ReturnType, method.ReturnNullableAnnotation);
75 |
76 | return new(
77 | method, method.Name, method.ReturnType,
78 | returnTypeFullName,
79 | method.TypeParameters,
80 | GetGenericConstraintsAsCode(Indents.MemberBody, method.TypeParameters),
81 | parameters,
82 | GetHandlerFieldName(method.Name, overloadId),
83 | GetRunDelegateType(method, parameters, returnTypeFullName, overloadId, customDelegatesClassName)
84 | );
85 | }
86 |
87 | [PerformanceSensitive("")]
88 | private MockTargetMember? GetMockTargetProperty(IPropertySymbol property, int overloadId, IAssemblySymbol currentAssembly) {
89 | if (!IsVirtual(property))
90 | return null;
91 |
92 | if (!IsVisible(property, currentAssembly))
93 | return null;
94 |
95 | return new(
96 | property, property.Name, property.Type,
97 | GetFullTypeName(property.Type, property.NullableAnnotation),
98 | ImmutableArray.Empty,
99 | genericParameterConstraints: null,
100 | property.SetMethod == null
101 | ? ImmutableArray.Empty
102 | : ConvertParametersFromSymbols(property.SetMethod.Parameters),
103 | GetHandlerFieldName(property.Name, overloadId),
104 | methodRunDelegateType: null
105 | );
106 | }
107 |
108 | [PerformanceSensitive("")]
109 | private bool IsVirtual(ISymbol symbol)
110 | => symbol.ContainingType.TypeKind == TypeKind.Interface
111 | || symbol.IsAbstract
112 | || symbol.IsVirtual;
113 |
114 | [PerformanceSensitive("")]
115 | private bool IsVisible(ISymbol symbol, IAssemblySymbol currentAssembly)
116 | => symbol.ContainingType.TypeKind == TypeKind.Interface
117 | || symbol.DeclaredAccessibility is Accessibility.Public or Accessibility.ProtectedOrInternal
118 | || symbol.ContainingAssembly.GivesAccessTo(currentAssembly) && (symbol.DeclaredAccessibility is Accessibility.Internal or Accessibility.ProtectedAndInternal);
119 |
120 | [PerformanceSensitive("")]
121 | private string GetFullTypeName(ITypeSymbol type, NullableAnnotation nullableAnnotation) {
122 | var nullableFlowState = nullableAnnotation switch {
123 | NullableAnnotation.Annotated => NullableFlowState.MaybeNull,
124 | NullableAnnotation.NotAnnotated => NullableFlowState.NotNull,
125 | _ => NullableFlowState.None
126 | };
127 | return type.ToDisplayString(nullableFlowState, TargetTypeDisplayFormat);
128 | }
129 |
130 | [PerformanceSensitive("")]
131 | private ImmutableArray ConvertParametersFromSymbols(ImmutableArray parameters) {
132 | if (parameters.Length == 0)
133 | return ImmutableArray.Empty;
134 |
135 | var builder = ImmutableArray.CreateBuilder(parameters.Length);
136 | for (var i = 0; i < parameters.Length; i++) {
137 | var parameter = parameters[i];
138 | builder.Add(new MockTargetParameter(
139 | parameter.Name, GetFullTypeName(parameter.Type, parameter.NullableAnnotation), parameter.RefKind, i
140 | ));
141 | }
142 | return builder.MoveToImmutable();
143 | }
144 |
145 | [PerformanceSensitive("")]
146 | private string GetHandlerFieldName(string memberName, int overloadId) {
147 | #pragma warning disable HAA0601 // Boxing - unavoidable for now, will revisit later
148 | return $"_{char.ToLowerInvariant(memberName[0])}{memberName.Substring(1)}{(overloadId > 1 ? overloadId.ToString() : "")}Handler";
149 | #pragma warning restore HAA0601
150 | }
151 |
152 | [PerformanceSensitive("")]
153 | private MockTargetMethodRunDelegateType GetRunDelegateType(
154 | IMethodSymbol method,
155 | ImmutableArray parameters,
156 | string returnTypeFullName,
157 | int overloadId,
158 | string customDelegatesClassName
159 | ) {
160 | var needsCustomDelegate = method.IsGenericMethod
161 | || parameters.Length > 16
162 | || parameters.Any(static p => p.RefKind is not RefKind.None or RefKind.In);
163 | if (needsCustomDelegate) {
164 | var suffix = method.ReturnsVoid ? "Action" : "Func";
165 | var nameWithGenericParameters = method.Name + (overloadId > 1 ? overloadId.ToString() : "") + suffix;
166 | if (method.IsGenericMethod)
167 | nameWithGenericParameters += "<" + string.Join(", ", method.TypeParameters.Select(static t => t.Name)) + ">";
168 |
169 | return MockTargetMethodRunDelegateType.Custom(nameWithGenericParameters, customDelegatesClassName + "." + nameWithGenericParameters);
170 | }
171 |
172 | if (method.ReturnsVoid) {
173 | if (!parameters.IsEmpty)
174 | return new($"{KnownTypes.Action.FullName}<{string.Join(",", parameters.Select(static x => x.FullTypeName))}>");
175 |
176 | return new(KnownTypes.Action.FullName);
177 | }
178 |
179 | if (!parameters.IsEmpty)
180 | return new($"{KnownTypes.Func.FullName}<{string.Join(",", parameters.Select(static x => x.FullTypeName))}, {returnTypeFullName}>");
181 |
182 | return new($"{KnownTypes.Func.FullName}<{returnTypeFullName}>");
183 | }
184 |
185 |
186 | [PerformanceSensitive("")]
187 | private string? GetGenericConstraintsAsCode(string indent, ImmutableArray parameters) {
188 | var writer = (CodeWriter?)null;
189 | foreach (var parameter in parameters) {
190 | writer = WriteGenericConstraints(writer, indent, parameter);
191 | }
192 | return writer?.ToString();
193 | }
194 |
195 | [PerformanceSensitive("")]
196 | private CodeWriter? WriteGenericConstraints(CodeWriter? writer, string indent, ITypeParameterSymbol parameter) {
197 | var parameterStarted = false;
198 | void WriteConstraint(string constraint) {
199 | if (!parameterStarted) {
200 | #pragma warning disable HAA0502 // Explicit new reference type allocation - currently unavoidable
201 | writer ??= new CodeWriter();
202 | #pragma warning restore HAA0502 // Explicit new reference type allocation
203 | writer
204 | .WriteLine()
205 | .Write(indent, "where ", parameter.Name, ": ");
206 | parameterStarted = true;
207 | }
208 | else {
209 | writer!.Write(", ");
210 | }
211 | writer.Write(constraint);
212 | }
213 |
214 | if (parameter.HasReferenceTypeConstraint) {
215 | WriteConstraint("class");
216 | if (parameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated)
217 | writer!.Write("?");
218 | }
219 | // seems like unmanaged adds implict struct constraint
220 | if (parameter.HasValueTypeConstraint && !parameter.HasUnmanagedTypeConstraint)
221 | WriteConstraint("struct");
222 | if (parameter.HasNotNullConstraint)
223 | WriteConstraint("notnull");
224 | if (parameter.HasUnmanagedTypeConstraint)
225 | WriteConstraint("unmanaged");
226 |
227 | for (var i = 0; i < parameter.ConstraintTypes.Length; i++) {
228 | var fullTypeName = GetFullTypeName(parameter.ConstraintTypes[i], parameter.ConstraintNullableAnnotations[i]);
229 | WriteConstraint(fullTypeName);
230 | }
231 |
232 | if (parameter.HasConstructorConstraint)
233 | WriteConstraint("new()");
234 |
235 | return writer;
236 | }
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/Generators/Internal/Models/MockTarget.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Roslyn.Utilities;
3 |
4 | namespace SourceMock.Generators.Internal.Models {
5 | internal readonly struct MockTarget {
6 | [PerformanceSensitive("")]
7 | public MockTarget(
8 | INamedTypeSymbol targetType,
9 | string targetTypeQualifiedName,
10 | string? genericParameterConstraints
11 | )
12 | {
13 | Type = targetType;
14 | FullTypeName = targetTypeQualifiedName;
15 | GenericParameterConstraints = genericParameterConstraints;
16 | }
17 |
18 | public INamedTypeSymbol Type { get; }
19 | public string FullTypeName { get; }
20 | public string? GenericParameterConstraints { get; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Generators/Internal/Models/MockTargetMember.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using Microsoft.CodeAnalysis;
3 | using Roslyn.Utilities;
4 |
5 | namespace SourceMock.Generators.Internal.Models {
6 | internal readonly struct MockTargetMember {
7 | [PerformanceSensitive("")]
8 | public MockTargetMember(
9 | ISymbol symbol,
10 | string name,
11 | ITypeSymbol type,
12 | string typeFullName,
13 | ImmutableArray genericParameters,
14 | string? genericParameterConstraints,
15 | ImmutableArray parameters,
16 | string handlerFieldName,
17 | MockTargetMethodRunDelegateType? methodRunDelegateType
18 | ) {
19 | Symbol = symbol;
20 | Name = name;
21 | Type = type;
22 | TypeFullName = typeFullName;
23 | GenericParameters = genericParameters;
24 | GenericParameterConstraints = genericParameterConstraints;
25 | Parameters = parameters;
26 | HandlerFieldName = handlerFieldName;
27 | MethodRunDelegateType = methodRunDelegateType;
28 | }
29 |
30 | public ISymbol Symbol { get; }
31 | public string Name { get; }
32 | public ITypeSymbol Type { get; }
33 | public string TypeFullName { get; }
34 | public ImmutableArray GenericParameters { get; }
35 | public string? GenericParameterConstraints { get; }
36 | public ImmutableArray Parameters { get; }
37 | public string HandlerFieldName { get; }
38 | public MockTargetMethodRunDelegateType? MethodRunDelegateType { get; }
39 |
40 | [PerformanceSensitive("")]
41 | public bool IsVoidMethod => Symbol is IMethodSymbol && Type.SpecialType == SpecialType.System_Void;
42 | [PerformanceSensitive("")]
43 | public string HandlerGenericParameterFullName => !IsVoidMethod ? TypeFullName : KnownTypes.VoidReturn.FullName;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Generators/Internal/Models/MockTargetMethodRunDelegateType.cs:
--------------------------------------------------------------------------------
1 | namespace SourceMock.Generators.Internal.Models {
2 | internal readonly struct MockTargetMethodRunDelegateType {
3 | public MockTargetMethodRunDelegateType(string fullName) {
4 | FullName = fullName;
5 | CustomNameWithGenericParameters = null;
6 | }
7 |
8 | private MockTargetMethodRunDelegateType(string fullName, string customNameWithGenericParameters) {
9 | FullName = fullName;
10 | CustomNameWithGenericParameters = customNameWithGenericParameters;
11 | }
12 |
13 | public static MockTargetMethodRunDelegateType Custom(string customNameWithGenericParameters, string fullName) {
14 | return new(fullName, customNameWithGenericParameters);
15 | }
16 |
17 | public string FullName { get; }
18 | public string? CustomNameWithGenericParameters { get; }
19 | public bool IsCustom => CustomNameWithGenericParameters != null;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Generators/Internal/Models/MockTargetParameter.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace SourceMock.Generators.Internal.Models {
4 | internal readonly struct MockTargetParameter {
5 | public MockTargetParameter(string name, string typeFullName, RefKind refKind, int index) {
6 | Name = name;
7 | FullTypeName = typeFullName;
8 | RefKind = refKind;
9 | Index = index;
10 | }
11 |
12 | public string Name { get; }
13 | public string FullTypeName { get; }
14 | public RefKind RefKind { get; }
15 | public int Index { get; }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Generators/Internal/NamedTypeSymbolCacheKeyEqualityComparer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Microsoft.CodeAnalysis;
4 |
5 | namespace SourceMock.Generators.Internal {
6 | internal class NamedTypeSymbolCacheKeyEqualityComparer : IEqualityComparer {
7 | public static NamedTypeSymbolCacheKeyEqualityComparer Default { get; } = new NamedTypeSymbolCacheKeyEqualityComparer();
8 |
9 | public bool Equals(INamedTypeSymbol x, INamedTypeSymbol y) {
10 | if (ReferenceEquals(x, y))
11 | return true;
12 | if (SymbolEqualityComparer.Default.Equals(x, y))
13 | return true;
14 | if (x.DeclaringSyntaxReferences.Length == 0)
15 | return false; // non-syntax types will only be cached on assembly level
16 |
17 | return x.DeclaringSyntaxReferences.SequenceEqual(
18 | y.DeclaringSyntaxReferences, static (x, y) => x == y
19 | );
20 | }
21 |
22 | public int GetHashCode(INamedTypeSymbol obj) {
23 | if (obj.DeclaringSyntaxReferences.Length == 0)
24 | return obj.GetHashCode();
25 |
26 | var hashCode = 0;
27 | foreach (var reference in obj.DeclaringSyntaxReferences) {
28 | hashCode ^= reference.GetHashCode();
29 | }
30 | return hashCode;
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/Generators/MockGenerator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Immutable;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Text.RegularExpressions;
6 | using Microsoft.CodeAnalysis;
7 | using Microsoft.CodeAnalysis.Text;
8 | using Roslyn.Utilities;
9 | using SourceMock.Generators.Internal;
10 | using SourceMock.Generators.Internal.Models;
11 |
12 | namespace SourceMock.Generators {
13 | [Generator]
14 | internal class MockGenerator : ISourceGenerator, IDisposable {
15 | private static class DiagnosticDescriptors {
16 | #pragma warning disable RS2008 // Enable analyzer release tracking
17 | public static readonly DiagnosticDescriptor SingleMockFailedToGenerate = new(
18 | "SourceMock001", "Failed to generate a single mock", "Failed to generate mock for {0}: {1}",
19 | "Generation", DiagnosticSeverity.Warning, isEnabledByDefault: true
20 | );
21 | public static readonly DiagnosticDescriptor RegexPatternFailedToParse = new(
22 | "SourceMock002", "Regex pattern is not a valid regex", "Regex pattern \"{0}\" cannot be parsed: {1}",
23 | "Generation", DiagnosticSeverity.Error, isEnabledByDefault: true
24 | );
25 | #pragma warning restore RS2008 // Enable analyzer release tracking
26 | }
27 |
28 | private readonly GeneratorCache<(IAssemblySymbol assembly, string? excludePattern), ImmutableArray<(string name, SourceText source)>> _mockedAssemblyCache = new("MockedAssemblyCache");
29 | private readonly GeneratorCache _mockedTypeCache = new("MockedTypeCache", NamedTypeSymbolCacheKeyEqualityComparer.Default);
30 | private readonly MockTargetModelFactory _modelFactory = new();
31 | private readonly MockClassGenerator _classGenerator;
32 |
33 | public MockGenerator() {
34 | GeneratorLog.Log("MockGenerator constructor");
35 | _classGenerator = new(_modelFactory);
36 | }
37 |
38 | public void Initialize(GeneratorInitializationContext context) {
39 | }
40 |
41 | [PerformanceSensitive("")]
42 | public void Execute(GeneratorExecutionContext context) {
43 | GeneratorLog.Log("MockGenerator.Execute started");
44 | try {
45 | GeneratorLog.Log("Get attributes started");
46 | var attributes = context.Compilation.Assembly.GetAttributes();
47 | GeneratorLog.Log("Get attributes finished");
48 | foreach (var attribute in attributes) {
49 | ProcessAssemblyAttribute(attribute, context);
50 | }
51 | }
52 | catch (Exception ex) {
53 | GeneratorLog.Log("MockGenerator.Execute failed: " + ex);
54 | throw;
55 | }
56 | GeneratorLog.Log("MockGenerator.Execute finished");
57 | }
58 |
59 | private void ProcessAssemblyAttribute(AttributeData attribute, in GeneratorExecutionContext context) {
60 | if (attribute.AttributeClass is not {} attributeClass)
61 | return;
62 |
63 | switch (attributeClass.Name) {
64 | case KnownTypes.GenerateMocksForAssemblyOfAttribute.Name:
65 | if (!KnownTypes.GenerateMocksForAssemblyOfAttribute.NamespaceMatches(attributeClass.ContainingNamespace))
66 | return;
67 | ProcessGenerateMocksForAssemblyAttribute(attribute, context);
68 | break;
69 |
70 | case KnownTypes.GenerateMocksForTypesAttribute.Name:
71 | if (!KnownTypes.GenerateMocksForTypesAttribute.NamespaceMatches(attributeClass.ContainingNamespace))
72 | return;
73 | ProcessGenerateMocksForTypesAttribute(attribute, context);
74 | break;
75 | }
76 | }
77 |
78 | [PerformanceSensitive("")]
79 | private void ProcessGenerateMocksForAssemblyAttribute(AttributeData attribute, GeneratorExecutionContext context) {
80 | // intermediate code state? just in case
81 | if (attribute.ConstructorArguments.ElementAtOrDefault(0).Value is not INamedTypeSymbol anyTypeInAssembly)
82 | return;
83 |
84 | string? excludePattern = null;
85 | foreach (var named in attribute.NamedArguments) {
86 | if (named.Key == KnownTypes.GenerateMocksForAssemblyOfAttribute.NamedParameters.ExcludeRegex)
87 | excludePattern = named.Value.Value as string;
88 | }
89 |
90 | GenerateMocksForAssembly(anyTypeInAssembly.ContainingAssembly, excludePattern, attribute.ApplicationSyntaxReference, context);
91 | }
92 |
93 | [PerformanceSensitive("")]
94 | private void ProcessGenerateMocksForTypesAttribute(AttributeData attribute, GeneratorExecutionContext context) {
95 | if (attribute.ConstructorArguments.ElementAtOrDefault(0) is not { Kind: TypedConstantKind.Array, Values: var typeConstants })
96 | return;
97 |
98 | foreach (var typeConstant in typeConstants) {
99 | if (typeConstant is not { Value: INamedTypeSymbol type })
100 | continue;
101 | if (type.IsUnboundGenericType)
102 | type = type.ConstructedFrom;
103 |
104 | GenerateMockForType(_modelFactory.GetMockTarget(type), assemblyCacheBuilder: null, context);
105 | }
106 | }
107 |
108 | [PerformanceSensitive("")]
109 | private void GenerateMocksForAssembly(IAssemblySymbol assembly, string? excludePattern, SyntaxReference? errorSyntaxReference, in GeneratorExecutionContext context) {
110 | if (_mockedAssemblyCache.TryGetValue((assembly, excludePattern), out var sources)) {
111 | GeneratorLog.Log("Using cached mocks for assembly " + assembly.Name);
112 | foreach (var (name, source) in sources) {
113 | context.AddSource(name, source);
114 | }
115 | return;
116 | }
117 |
118 | GeneratorLog.Log("Generating mocks for assembly " + assembly.Name);
119 |
120 | Regex? excludeRegex;
121 | try {
122 | #pragma warning disable HAA0502 // Explicit new reference type allocation -- No alternative at the moment (cache/pool later?)
123 | excludeRegex = excludePattern != null ? new Regex(excludePattern) : null;
124 | #pragma warning restore HAA0502
125 | }
126 | catch (ArgumentException ex) {
127 | #pragma warning disable HAA0101 // Array allocation for params parameter -- Exceptional case: OK to allocate
128 | context.ReportDiagnostic(Diagnostic.Create(
129 | DiagnosticDescriptors.RegexPatternFailedToParse,
130 | errorSyntaxReference?.SyntaxTree.GetLocation(errorSyntaxReference.Span),
131 | excludePattern, ex.Message
132 | ));
133 | #pragma warning restore HAA0101 // Array allocation for params parameter
134 | return;
135 | }
136 |
137 | var assemblyCacheBuilder = ImmutableArray.CreateBuilder<(string, SourceText)>();
138 | GenerateMocksForNamespace(assembly.GlobalNamespace, excludeRegex, assemblyCacheBuilder, context);
139 | _mockedAssemblyCache.TryAdd((assembly, excludePattern), assemblyCacheBuilder.ToImmutable());
140 | }
141 |
142 | [PerformanceSensitive("")]
143 | private void GenerateMocksForNamespace(
144 | INamespaceSymbol parent,
145 | Regex? excludeRegex,
146 | ImmutableArray<(string, SourceText)>.Builder assemblyCacheBuilder,
147 | in GeneratorExecutionContext context
148 | ) {
149 | #pragma warning disable HAA0401 // Possible allocation of reference type enumerator -- TODO to revisit later
150 | foreach (var member in parent.GetMembers()) {
151 | #pragma warning restore HAA0401
152 | switch (member) {
153 | case INamedTypeSymbol type:
154 | var target = _modelFactory.GetMockTarget(type);
155 | if (!ShouldIncludeInMocksForAssembly(target, excludeRegex, context))
156 | continue;
157 | GenerateMockForType(target, assemblyCacheBuilder, context);
158 | break;
159 |
160 | case INamespaceSymbol nested:
161 | GenerateMocksForNamespace(nested, excludeRegex, assemblyCacheBuilder, context);
162 | break;
163 | }
164 | }
165 | }
166 |
167 | [PerformanceSensitive("")]
168 | private bool ShouldIncludeInMocksForAssembly(MockTarget target, Regex? excludeRegex, in GeneratorExecutionContext context) {
169 | var type = target.Type;
170 | if (type.TypeKind != TypeKind.Interface)
171 | return false;
172 |
173 | if (type.DeclaredAccessibility != Accessibility.Public) {
174 | var isVisibleInternal = type.DeclaredAccessibility == Accessibility.Internal
175 | && type.ContainingAssembly.GivesAccessTo(context.Compilation.Assembly);
176 | if (!isVisibleInternal)
177 | return false;
178 | }
179 |
180 | if (excludeRegex != null && excludeRegex.IsMatch(target.FullTypeName))
181 | return false;
182 |
183 | return true;
184 | }
185 |
186 | [PerformanceSensitive("")]
187 | private void GenerateMockForType(
188 | MockTarget target,
189 | ImmutableArray<(string, SourceText)>.Builder? assemblyCacheBuilder,
190 | in GeneratorExecutionContext context
191 | ) {
192 | if (_mockedTypeCache.TryGetValue(target.Type, out var cached)) {
193 | GeneratorLog.Log("Using cached mock for type " + target.FullTypeName);
194 | context.AddSource(cached.name, cached.source);
195 | assemblyCacheBuilder?.Add(cached);
196 | return;
197 | }
198 |
199 | GeneratorLog.Log("Generating mock for type " + target.FullTypeName);
200 | string mockContent;
201 | try {
202 | mockContent = _classGenerator.Generate(target, context.Compilation.Assembly);
203 | }
204 | catch (Exception ex) {
205 | #pragma warning disable HAA0101 // Array allocation for params parameter
206 | // Exceptional case: OK to allocate
207 | context.ReportDiagnostic(Diagnostic.Create(
208 | DiagnosticDescriptors.SingleMockFailedToGenerate, location: null, target.FullTypeName, ex.ToString()
209 | ));
210 | #pragma warning restore HAA0101 // Array allocation for params parameter
211 | return;
212 | }
213 |
214 | var mockFileName = Regex.Replace(target.FullTypeName, @"[^\w\d]", "_") + ".cs";
215 | var mockSource = SourceText.From(mockContent, Encoding.UTF8);
216 | context.AddSource(mockFileName, mockSource);
217 | _mockedTypeCache.TryAdd(target.Type, (mockFileName, mockSource));
218 | assemblyCacheBuilder?.Add((mockFileName, mockSource));
219 | }
220 |
221 | public void Dispose() {
222 | _mockedAssemblyCache.Dispose();
223 | _mockedTypeCache.Dispose();
224 | }
225 | }
226 | }
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021-2022, Andrey Shchekin
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | 
4 |
5 | SourceMock is a C# mocking framework based on source generators.
6 |
7 | This allows for a nicer interface than reflection based frameworks, for example:
8 | ```csharp
9 | mock.Setup.Parse().Returns(3);
10 | ```
11 | instead of
12 | ```csharp
13 | mock.Setup(x => x.Parse(It.IsAny())).Return(3)
14 | ```
15 |
16 | # Cookbook
17 |
18 | All examples assume the following interface:
19 | ```csharp
20 | namespace Parsing {
21 | interface IParser { int Parse(string value); }
22 | }
23 | ```
24 |
25 | ## Set up a simple mock
26 |
27 | ```csharp
28 | [assembly: GenerateMocksForAssemblyOf(typeof(IParser))]
29 |
30 | using Parsing.Mocks;
31 |
32 | var parser = new ParserMock();
33 |
34 | parser.Setup.Parse().Returns(1);
35 |
36 | Assert.Equal(1, parser.Parse());
37 | ```
38 |
39 | ## Verify calls
40 |
41 | ```csharp
42 | [assembly: GenerateMocksForAssemblyOf(typeof(IParser))]
43 |
44 | using Parsing.Mocks;
45 |
46 | var parser = new ParserMock();
47 |
48 | parser.Parse("1");
49 | parser.Parse("2");
50 |
51 | Assert.Equal(new[] { "1", "2" }, parser.Calls.Parse());
52 | ```
53 |
54 | ## Set up a callback
55 |
56 | ```csharp
57 | [assembly: GenerateMocksForAssemblyOf(typeof(IParser))]
58 |
59 | using Parsing.Mocks;
60 |
61 | var parser = new ParserMock();
62 |
63 | parser.Setup.Parse().Runs(s => int.Parse(s));
64 |
65 | Assert.Equal(1, parser.Parse("1"));
66 | ```
67 |
68 | # Limitations
69 |
70 | ## By Design
71 |
72 | ### Strict Mocks
73 |
74 | SourceMock does not provide strict mocks — this is by design.
75 | I believe that verifying setups blurs the line between Arrange and Assert and decreases test readability.
76 |
77 | Instead, assert `.Calls` at the end of the test to confirm the expected calls.
78 |
79 | ## Not Yet
80 |
81 | These are not _intentionally_ excluded, just not yet supported:
82 | 1. Class mocks: custom constructors, calling base methods, mocking protected members
83 | 2. Custom default values
84 | 3. Custom parameter matchers
85 | 4. Setting up output values for ref and out parameters
86 | 5. Chained setups
87 | 6. Anything more advanced than the above
88 |
89 | # Kudos
90 |
91 | This framework borrows a lot of its design from [Moq](https://github.com/moq), which is one of the best .NET libraries overall.
92 |
--------------------------------------------------------------------------------
/SourceMock.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30804.86
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "[Main]", "[Main]\[Main].csproj", "{FFE646A4-7C02-4BF9-A5E4-BD331D02A73C}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{E69446D0-2931-4428-AB16-4CF904A61134}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{18512360-554B-48A7-94AD-2CA72C06EFB8}"
11 | ProjectSection(SolutionItems) = preProject
12 | .editorconfig = .editorconfig
13 | CHANGELOG.md = CHANGELOG.md
14 | Directory.Build.props = Directory.Build.props
15 | README.md = README.md
16 | EndProjectSection
17 | EndProject
18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators", "Generators\Generators.csproj", "{46146400-5A8E-4019-8BB4-B1BF9CA63C3A}"
19 | EndProject
20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Interfaces", "Tests.Interfaces\Tests.Interfaces.csproj", "{2ADB146B-C267-4772-A324-D2E3E22A4CDD}"
21 | EndProject
22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators.Tests", "Generators.Tests\Generators.Tests.csproj", "{E26138CB-E9BC-4E23-926E-C7FAA8BDDE8F}"
23 | EndProject
24 | Global
25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
26 | Debug|Any CPU = Debug|Any CPU
27 | Release|Any CPU = Release|Any CPU
28 | EndGlobalSection
29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
30 | {FFE646A4-7C02-4BF9-A5E4-BD331D02A73C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {FFE646A4-7C02-4BF9-A5E4-BD331D02A73C}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {FFE646A4-7C02-4BF9-A5E4-BD331D02A73C}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {FFE646A4-7C02-4BF9-A5E4-BD331D02A73C}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {E69446D0-2931-4428-AB16-4CF904A61134}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {E69446D0-2931-4428-AB16-4CF904A61134}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {E69446D0-2931-4428-AB16-4CF904A61134}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {E69446D0-2931-4428-AB16-4CF904A61134}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {46146400-5A8E-4019-8BB4-B1BF9CA63C3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {46146400-5A8E-4019-8BB4-B1BF9CA63C3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {46146400-5A8E-4019-8BB4-B1BF9CA63C3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {46146400-5A8E-4019-8BB4-B1BF9CA63C3A}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {2ADB146B-C267-4772-A324-D2E3E22A4CDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43 | {2ADB146B-C267-4772-A324-D2E3E22A4CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
44 | {2ADB146B-C267-4772-A324-D2E3E22A4CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
45 | {2ADB146B-C267-4772-A324-D2E3E22A4CDD}.Release|Any CPU.Build.0 = Release|Any CPU
46 | {E26138CB-E9BC-4E23-926E-C7FAA8BDDE8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47 | {E26138CB-E9BC-4E23-926E-C7FAA8BDDE8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
48 | {E26138CB-E9BC-4E23-926E-C7FAA8BDDE8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
49 | {E26138CB-E9BC-4E23-926E-C7FAA8BDDE8F}.Release|Any CPU.Build.0 = Release|Any CPU
50 | EndGlobalSection
51 | GlobalSection(SolutionProperties) = preSolution
52 | HideSolutionNode = FALSE
53 | EndGlobalSection
54 | GlobalSection(ExtensibilityGlobals) = postSolution
55 | SolutionGuid = {1116CF99-62CD-4830-A075-B997B3A6B6C1}
56 | EndGlobalSection
57 | EndGlobal
58 |
--------------------------------------------------------------------------------
/Tests.Interfaces/AbstractClass.cs:
--------------------------------------------------------------------------------
1 | namespace SourceMock.Tests.Interfaces {
2 | public abstract class AbstractClass {
3 | private readonly string? _getStringDefault;
4 |
5 | public AbstractClass(string getStringDefault) {
6 | _getStringDefault = getStringDefault;
7 | }
8 |
9 | protected AbstractClass() {
10 | }
11 |
12 | public void NonVirtual() {}
13 | private void Private() {}
14 | protected virtual void Protected() {}
15 | protected internal virtual void ProtectedInternal() {}
16 | private protected virtual void PrivateProtected() {}
17 |
18 | public abstract int Get();
19 | public virtual string? GetString() => _getStringDefault;
20 |
21 | public virtual int VirtualProperty => 1;
22 | public int NonVirtualProperty => 1;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Tests.Interfaces/Disposable.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace SourceMock.Tests.Interfaces {
4 | public class Disposable : IDisposable {
5 | public virtual void Dispose() {
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Tests.Interfaces/IEmptyInterface.cs:
--------------------------------------------------------------------------------
1 | namespace SourceMock.Tests.Interfaces {
2 | public interface IEmptyInterface {
3 | }
4 | }
--------------------------------------------------------------------------------
/Tests.Interfaces/IExcludedInterface.cs:
--------------------------------------------------------------------------------
1 | namespace SourceMock.Tests.Interfaces {
2 | public interface IExcludedInterface {
3 | }
4 | }
5 |
--------------------------------------------------------------------------------
/Tests.Interfaces/IInheritedInteface.cs:
--------------------------------------------------------------------------------
1 | namespace SourceMock.Tests.Interfaces {
2 | interface IInheritedInteface : IMockable2, IEmptyInterface {
3 | void Method();
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/Tests.Interfaces/IInternalInterface.cs:
--------------------------------------------------------------------------------
1 | namespace SourceMock.Tests.Interfaces {
2 | internal interface IInternalInterface {
3 | }
4 | }
5 |
--------------------------------------------------------------------------------
/Tests.Interfaces/IMockable.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace SourceMock.Tests.Interfaces {
4 | public interface IMockable {
5 | int GetInt32();
6 | int? GetInt32Nullable();
7 | string GetString();
8 | string? GetStringNullable();
9 | IMockable2 GetMockable2();
10 | Task GetStringAsync();
11 |
12 | int ParseToInt32(string? value);
13 | bool TestInterface(IEmptyInterface value);
14 |
15 | double Divide(double value1, double value2);
16 |
17 | int Sum(int value1, int value2);
18 | int Sum(int value1, int value2, int value3);
19 |
20 | void Execute();
21 | void Execute(IEmptyInterface value);
22 |
23 | int Count { get; }
24 | string Name { get; set; }
25 | }
26 | }
--------------------------------------------------------------------------------
/Tests.Interfaces/IMockable2.cs:
--------------------------------------------------------------------------------
1 | namespace SourceMock.Tests.Interfaces {
2 | public interface IMockable2 {
3 | string GetString();
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/Tests.Interfaces/INeedsCollectionDefaults.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Collections.Immutable;
3 |
4 | namespace SourceMock.Tests.Interfaces {
5 | public interface INeedsCollectionDefaults {
6 | int[] GetArray();
7 | List GetList();
8 | ImmutableList GetImmutableList();
9 | ImmutableArray GetImmutableArray();
10 | IEnumerable GetIEnumerable();
11 | IList GetIList();
12 | ICollection GetICollection();
13 | IReadOnlyCollection GetIReadOnlyCollection();
14 | IReadOnlyList GetIReadOnlyList();
15 | IImmutableList GetIImmutableList();
16 |
17 | Dictionary GetDictionary();
18 | ImmutableDictionary GetImmutableDictionary();
19 | IDictionary GetIDictionary();
20 | IReadOnlyDictionary GetIReadOnlyDictionary();
21 | IImmutableDictionary GetIImmutableDictionary();
22 |
23 | HashSet GetHashSet();
24 | ImmutableHashSet GetImmutableHashSet();
25 | ISet GetISet();
26 | IReadOnlySet GetIReadOnlySet();
27 | IImmutableSet GetIImmutableSet();
28 | }
29 | }
--------------------------------------------------------------------------------
/Tests.Interfaces/INeedsGenericConstraints.cs:
--------------------------------------------------------------------------------
1 | namespace SourceMock.Tests.Interfaces {
2 | public interface INeedsGenericConstraints {
3 | void Method()
4 | where TNotNull : notnull, IMockable, new()
5 | where TClassNotNull : class, IMockable, new()
6 | where TNullableClass : class?, IMockable, new()
7 | where TStruct : struct, IMockable
8 | where TUnmanaged : unmanaged, IMockable;
9 | }
10 |
11 | public interface INeedsGenericConstraints
12 | where TNotNull : notnull, IMockable, new()
13 | where TClass : class, IMockable, new()
14 | where TNullableClass : class?, IMockable, new()
15 | where TStruct : struct, IMockable
16 | where TUnmanaged : unmanaged, IMockable
17 | {
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Tests.Interfaces/INeedsGenerics.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace SourceMock.Tests.Interfaces {
4 | public interface INeedsGenerics {
5 | T Parse(string value);
6 | T Get();
7 | void SetOptional(T? value);
8 | IEnumerable GetAll();
9 | }
10 |
11 | public interface INeedsGenerics {
12 | U Get();
13 | T Convert(U value);
14 | T Cast(U value)
15 | where T: U;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests.Interfaces/INeedsOtherDefaults.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 |
4 | namespace SourceMock.Tests.Interfaces {
5 | public interface INeedsOtherDefaults {
6 | Task ExecuteAsync();
7 | Task