From 79a2881a3fd722dd95f8aa589567003acb896678 Mon Sep 17 00:00:00 2001 From: Ian Johnson Date: Sun, 13 Sep 2026 12:45:10 -0400 Subject: [PATCH 1/3] Reformat every C# file with CSharpier The braces move from K&R to Allman, which is what the .NET ecosystem and every Microsoft template use, and is the one thing CSharpier does not offer a choice about. Taking its whole layout rather than a brace-only rewrite is what makes the style checkable instead of a review topic. The version is pinned in the tool manifest so a clone and CI agree on what formatted means. Project files are excluded. CSharpier reindents MSBuild XML to two spaces but leaves the interior of multi-line comments at its old indentation, and this repo's csproj comments are long enough that every one of them would come out hanging. No behaviour change: the solution builds and all 2258 tests pass before and after. Co-Authored-By: Claude Opus 5 (1M context) --- .config/dotnet-tools.json | 13 + .csharpierignore | 9 + .../DependencyModules.Benchmarks/Program.cs | 150 ++- integ-tests/ConsoleTestProject/Program.cs | 1 - integ-tests/ConsoleTestProject/TestExport.cs | 3 +- .../BetterDependencyOne.cs | 17 +- .../CircularReferenceModules/ModuleA.cs | 2 +- .../CircularReferenceModules/ModuleB.cs | 3 +- .../CircularReferenceModules/ServiceA.cs | 2 +- .../CircularReferenceModules/ServiceB.cs | 2 +- .../SecondarySutProject/PackagePolicies.cs | 12 +- .../ParameterizedModule.cs | 11 +- .../SecondaryParameterizedModule.cs | 2 +- .../SomeRuntimeDependency.cs | 9 +- .../SecondarySutProject/SecondarySutModule.cs | 2 +- .../SutProject.NUnitTests/DataRowTests.cs | 36 +- .../EnvironmentSeedingTests.cs | 13 +- .../FakeItEasy/FakeItEasyTests.cs | 11 +- .../IterationLifetimeTests.cs | 69 +- .../ModuleLoadingTests.cs | 31 +- .../SutProject.NUnitTests/Moq/MoqTests.cs | 12 +- .../NSubstitute/NSubstituteTests.cs | 22 +- .../ServiceProviderBuilderPrecedenceTests.cs | 26 +- .../SutProject.NUnitTests/TestExportTests.cs | 26 +- .../CircularModuleTests.cs | 17 +- .../ConventionTests/ConventionEdgeModules.cs | 140 ++- .../ConventionTests/ConventionEdgeTests.cs | 40 +- .../ConventionFeatureModules.cs | 157 ++- .../ConventionTests/ConventionFeatureTests.cs | 79 +- .../ConventionInteractionModules.cs | 122 +- .../ConventionInteractionTests.cs | 45 +- .../ConventionTests/ConventionModules.cs | 74 +- .../ConventionRegistrationTests.cs | 66 +- .../ConventionTests/NestedNamespaceTypes.cs | 3 +- .../CrossWire/CrossWireTests.cs | 55 +- .../Customization/CustomDependencyTestCase.cs | 8 +- .../CustomServiceProviderAttribute.cs | 9 +- .../Customization/CustomTestDependency.cs | 8 +- .../ServiceProviderBuilderPrecedenceTests.cs | 26 +- .../DataTests/InlineDataTests.cs | 14 +- .../DataTests/MemberDataTests.cs | 31 +- .../DuplicateNames/FirstDuplicateNameTests.cs | 6 +- .../SecondDuplicateNameTests.cs | 6 +- .../EnvironmentConfigurationTests.cs | 78 +- .../EnvironmentSeedingTests.cs | 29 +- .../FactoryTests/SimpleFactoryTests.cs | 27 +- .../FakeItEasy/FakeItEasyAttributeTests.cs | 15 +- .../Features/FeatureModules.cs | 33 +- .../SutProject.Tests/Features/FeatureTests.cs | 10 +- .../Features/OrderFeatureTests.cs | 10 +- .../Features/OrderedFeatureModules.cs | 30 +- .../ModuleAttributeTests.cs | 22 +- .../ModuleWithAttribute.cs | 4 +- .../ModuleWithoutAttribute.cs | 4 +- .../GenerateFactoryModule.cs | 38 +- .../GenerateFactories/GenerateFactoryTests.cs | 9 +- .../GenericTests/ResolveGenericTypeTests.cs | 22 +- .../KeyedTests/KeyedModule.cs | 4 +- .../KeyedTests/KeyedRegistrationModels.cs | 39 +- .../KeyedTests/KeyedRegistrationTests.cs | 34 +- .../SutProject.Tests/Moq/MoqAttributeTests.cs | 70 +- .../Moq/MoqSetupOrderingTests.cs | 12 +- .../NSubstitute/NSubstituteAttributeTests.cs | 16 +- .../ArrayParameterModule.cs | 10 +- .../DefaultedParameterModule.cs | 9 +- .../DefaultedParameterModuleTests.cs | 13 +- .../LocalParameterizedModule.cs | 9 +- .../LocalParameterizedModuleTests.cs | 8 +- .../RealmTests/FirstRealmModule.cs | 2 +- .../RealmTests/RealmDependencyOne.cs | 22 +- .../RealmTests/RealmIsolationTests.cs | 8 +- .../RegistrationTypeTests/ReplaceTests.cs | 27 +- .../TryRegistrationTests.cs | 29 +- .../StandardTests/AttributeTest.cs | 26 +- .../StandardTests/AutoRegistrationTest.cs | 20 +- .../StandardTests/DuplicateModules.cs | 2 +- .../StandardTests/LifetimeSemanticsTests.cs | 34 +- .../StandardTests/ModuleDuplicationTests.cs | 34 +- .../StandardTests/MultipleModulesTests.cs | 38 +- .../StandardTests/NoNamespaceTests.cs | 29 +- .../StandardTests/RecordModuleModels.cs | 6 +- .../StandardTests/RecordTests.cs | 15 +- .../StandardTests/SecondarySutProjectTests.cs | 8 +- .../StandardTests/SerializerTests.cs | 18 +- .../StandardTests/SimpleResolveTests.cs | 14 +- .../StandardTests/SingletonTests.cs | 11 +- .../TestFramework/ContainerLifetimeTests.cs | 33 +- .../TestFramework/InjectValueTests.cs | 9 +- .../TestFramework/ModuleTestCaseTests.cs | 20 +- .../TestFramework/TestCaseModels.cs | 13 +- .../TestFramework/TestModules.cs | 7 +- .../UseMethod/UseMethodTests.cs | 15 +- integ-tests/SutProject/DependencyOne.cs | 12 +- integ-tests/SutProject/GenericClass.cs | 16 +- integ-tests/SutProject/KeyedService.cs | 4 +- .../SutProject/RealmRegistrationService.cs | 2 +- integ-tests/SutProject/RecordService.cs | 9 +- integ-tests/SutProject/ScopedService.cs | 2 +- integ-tests/SutProject/SerializerClasses.cs | 8 +- integ-tests/SutProject/SingletonService.cs | 12 +- .../SutProject/SubDirectory/Constants.cs | 5 +- integ-tests/SutProject/SutModule.cs | 9 +- integ-tests/SutProject/SutRealmModule.cs | 3 +- integ-tests/SutProject/TestRealmModule.cs | 6 +- integ-tests/web/WebApiApp.Tests/Bootstrap.cs | 2 +- .../web/WebApiApp.Tests/WeatherTests.cs | 21 +- .../web/WebApiApp/AiSummaryProvider.cs | 28 +- integ-tests/web/WebApiApp/Program.cs | 1 - integ-tests/web/WebApiApp/SummaryProvider.cs | 12 +- .../web/WebApiApp/TemperatureProvider.cs | 14 +- integ-tests/web/WebApiApp/Weather.cs | 25 +- integ-tests/web/WebApiApp/WeatherForecast.cs | 5 +- .../FakeItEasySupportAttribute.cs | 7 +- .../MoqSupportAttribute.cs | 51 +- .../NSubstituteSupportAttribute.cs | 7 +- .../Attributes/ModuleTestAttribute.cs | 68 +- .../Attributes/ModuleTestCaseAttribute.cs | 19 +- .../Impl/ModuleTestCommand.cs | 121 +- .../Impl/NUnitTestMethodContext.cs | 21 +- .../Impl/TestCaseInfo.cs | 35 +- .../Attributes/BaseServiceAttribute.cs | 40 +- .../Attributes/CrossWireServiceAttribute.cs | 37 +- .../Attributes/DecorateAttribute.cs | 3 +- .../Attributes/DecoratorAttribute.cs | 3 +- .../Attributes/DependencyModuleAttribute.cs | 11 +- .../Attributes/IfEnvironmentAttribute.cs | 6 +- .../Attributes/IfEnvironmentValueAttribute.cs | 9 +- .../Attributes/IfNotEnvironmentAttribute.cs | 6 +- .../IfNotEnvironmentValueAttribute.cs | 9 +- .../Attributes/InterceptAttribute.cs | 3 +- .../Attributes/InterceptedMembers.cs | 5 +- .../Attributes/ScopedServiceAttribute.cs | 12 +- .../Attributes/SingletonServiceAttribute.cs | 11 +- .../Attributes/TransientServiceAttribute.cs | 12 +- .../Conventions/ConventionContracts.cs | 19 +- .../Features/FeatureApplicator.cs | 12 +- .../IDependencyModuleApplicatorProvider.cs | 9 +- .../Features/IDependencyModuleFeature.cs | 5 +- .../Features/IFeatureApplicator.cs | 5 +- .../Helpers/DecoratorHelper.cs | 234 ++-- .../Helpers/DecoratorRegistration.cs | 3 +- .../Helpers/DependencyRegistry.cs | 317 +++-- .../Helpers/EnvironmentConditions.cs | 22 +- .../Interception/CallerInfo.cs | 3 +- .../Interception/IArguments.cs | 3 +- .../Interception/IInterceptor.cs | 9 +- .../Interception/InvocationContext.cs | 18 +- .../Interception/InvocationState.cs | 12 +- .../Interception/NoResult.cs | 3 +- .../Interfaces/IDependencyModule.cs | 22 +- .../Interfaces/IDependencyModuleProvider.cs | 5 +- .../Interfaces/IModuleEnvironment.cs | 3 +- .../Interfaces/IModuleEnvironmentProvider.cs | 4 +- .../IServiceCollectionConfiguration.cs | 8 +- .../ModuleEnvironment.cs | 77 +- .../ServiceCollectionExtensions.cs | 32 +- .../BaseAttributeSourceGenerator.cs | 154 ++- .../BaseAttributeWithConfigSourceGenerator.cs | 63 +- .../BaseSourceGenerator.cs | 432 ++++--- .../DecoratorFileWriter.cs | 126 +- .../DependencyFileWriter.cs | 601 ++++++---- .../DependencyModuleDiagnostics.cs | 189 +-- .../DependencyModuleWriter.cs | 423 ++++--- .../InterceptorFileWriter.cs | 441 ++++--- .../InterceptorRegistrationWriter.cs | 122 +- .../KnownTypes.cs | 288 +++-- .../Models/AttributeModel.cs | 133 ++- .../Models/ConstructorInfoModel.cs | 6 +- .../Models/DecoratorModel.cs | 60 +- .../DependencyModuleConfigurationModel.cs | 49 +- .../Models/DiagnosticReporter.cs | 34 +- .../Models/EnvironmentConditionModel.cs | 24 +- .../Models/IClassModel.cs | 7 +- .../Models/InterceptedMemberKinds.cs | 5 +- .../Models/InterceptorModel.cs | 197 ++-- .../Models/LocationModel.cs | 26 +- .../Models/MethodInfoModel.cs | 6 +- .../Models/ModelEquality.cs | 74 +- .../Models/ModuleEntryPointModel.cs | 82 +- .../Models/ParameterInfoModel.cs | 23 +- .../Models/PropertyInfoModel.cs | 5 +- .../Models/ServiceModel.cs | 176 +-- .../Models/SyntaxTreeLookup.cs | 21 +- .../ModuleAttributeWriter.cs | 39 +- .../ModuleEntryPointDiagnostics.cs | 64 +- .../Utilities/AttributeModelCollector.cs | 53 +- .../Utilities/AttributeModelHelper.cs | 230 ++-- .../Utilities/AttributeTypeMatcher.cs | 41 +- .../Utilities/BaseAttributeWriter.cs | 104 +- .../Utilities/BaseMethodHelper.cs | 42 +- .../Utilities/CollectionSyntaxDeclaration.cs | 49 +- .../Utilities/CompilerService.cs | 2 +- .../Utilities/ConstructorArgumentWriter.cs | 53 +- .../Utilities/DecoratorConstraintChecker.cs | 120 +- .../Utilities/DecoratorExpansion.cs | 87 +- .../Utilities/DecoratorModelUtility.cs | 230 ++-- .../Utilities/DecoratorTypeUtility.cs | 80 +- .../Utilities/EntryModelUtil.cs | 168 ++- .../Utilities/EnvironmentConditionUtility.cs | 83 +- .../Utilities/EnvironmentConditionWriter.cs | 23 +- .../Utilities/FileLogger.cs | 69 +- .../Utilities/ITypeDefinitionExtensions.cs | 28 +- .../Utilities/InterceptedMemberReader.cs | 317 +++-- .../Utilities/InterceptorModelUtility.cs | 366 ++++-- .../Utilities/ModuleDecoratorResolver.cs | 67 +- .../Utilities/ServiceModelUtility.cs | 559 ++++++--- .../Utilities/SymbolConstructorReader.cs | 76 +- .../Utilities/SyntaxNodeExtensions.cs | 119 +- .../Utilities/SyntaxSelector.cs | 143 ++- .../Utilities/SyntaxTransformContext.cs | 12 +- .../Utilities/TypeParameterReader.cs | 36 +- .../Utilities/TypeSyntaxExtensions.cs | 140 ++- .../Utilities/UsageAttributeComponent.cs | 4 +- .../AssemblyModuleAttributeDiagnostics.cs | 156 ++- .../Conventions/ConventionContractSource.cs | 5 +- .../Conventions/ConventionGenerator.cs | 541 ++++++--- .../Models/ConventionCandidateModel.cs | 58 +- .../Conventions/Models/ConventionModel.cs | 140 ++- .../Conventions/Models/EquatableList.cs | 6 +- .../Utilities/ConventionCandidateCache.cs | 21 +- .../Utilities/ConventionCandidateUtility.cs | 239 ++-- .../Utilities/ConventionMatcher.cs | 511 ++++++--- .../Utilities/ConventionModelUtility.cs | 466 +++++--- .../Conventions/Utilities/DeclarationStamp.cs | 91 +- .../Utilities/MetadataCandidateUtility.cs | 120 +- .../InterceptorSourceGenerator.cs | 232 ++-- .../ReferencedModuleLookup.cs | 69 +- .../ServiceSourceGenerator.cs | 268 +++-- .../SourceGenerator.cs | 25 +- .../TestAttributeDiagnostics.cs | 118 +- .../Attributes/InjectValuesAttribute.cs | 11 +- .../Interfaces/IInjectValueAttribute.cs | 5 +- .../Interfaces/IMockSupportAttribute.cs | 5 +- .../Interfaces/IModuleTestAttribute.cs | 8 +- .../Interfaces/IOrderedAttribute.cs | 5 +- .../IServiceProviderBuilderAttribute.cs | 9 +- .../Interfaces/ISharedTestRegistration.cs | 5 +- .../Interfaces/ITestContainerSource.cs | 4 +- .../Interfaces/ITestMethodContext.cs | 12 +- .../Interfaces/ITestParameterValueProvider.cs | 14 +- .../Interfaces/ITestServiceSetupAttribute.cs | 9 +- .../Interfaces/ITestStartupAttribute.cs | 4 +- .../Attributes/MockAttribute.cs | 50 +- .../Attributes/TestExportAttribute.cs | 44 +- .../Impl/AttributeUtility.cs | 145 ++- .../Impl/SharedRegistrations.cs | 43 +- .../Impl/TestContainerSource.cs | 72 +- .../Impl/TestParameterResolver.cs | 97 +- .../Attributes/ModuleTestAttribute.cs | 25 +- .../Impl/ModuleTestCase.cs | 244 ++-- .../Impl/ModuleTestDiscoverer.cs | 24 +- .../Impl/TestCaseInfo.cs | 37 +- .../Impl/TraitDictionaryExtensions.cs | 18 +- .../Impl/XunitTestMethodContext.cs | 21 +- .../ApiTests/PublicApiTests.cs | 53 +- .../ApplicationModuleCollisionTests.cs | 104 +- ...AssemblyModuleAttributeDiagnosticsTests.cs | 102 +- .../AttributeModelOutputTests.cs | 157 ++- .../AutoModuleDelegationTests.cs | 251 ++-- .../GeneratorTests/ConfigurationTests.cs | 142 ++- .../GeneratorTests/ConventionContractTests.cs | 79 +- .../ConventionDecoratorTests.cs | 50 +- .../ConventionRegistrationTests.cs | 647 ++++++----- .../DecoratorGenerationTests.cs | 1018 +++++++++++------ .../DecoratorImplementationTests.cs | 99 +- .../DiagnosticSuppressionTests.cs | 638 ++++++----- .../GeneratorTests/DiagnosticsTests.cs | 228 ++-- .../GeneratorTests/EntryModelUtilTests.cs | 150 ++- .../EnvironmentConditionTests.cs | 211 ++-- .../GeneratorTests/ExtensionSeamTests.cs | 104 +- .../GeneratorTests/FileLoggerTests.cs | 91 +- .../GenerateFactoriesInterceptionTests.cs | 115 +- .../GeneratorTests/GeneratedBehaviourTests.cs | 318 +++-- .../GeneratedCodeRobustnessTests.cs | 141 ++- .../GeneratorTests/GeneratorFeatureTests.cs | 128 ++- .../IncrementalGenerationTests.cs | 127 +- .../InterceptAttributeSpellingTests.cs | 65 +- .../GeneratorTests/InterceptedMembersTests.cs | 86 +- .../GeneratorTests/InterceptionRealmTests.cs | 196 ++-- .../GeneratorTests/InterceptionScopeTests.cs | 68 +- .../InterceptorGenerationTests.cs | 878 ++++++++------ .../InterceptorLifetimeTests.cs | 56 +- .../MockTestExportDiagnosticTests.cs | 83 +- .../GeneratorTests/ModelComparerTests.cs | 342 ++++-- .../GeneratorTests/ModelEqualityTests.cs | 124 +- .../ModuleEqualityDiagnosticTests.cs | 46 +- .../ModuleGenerationSnapshotTests.cs | 58 +- .../ModuleParameterAccessibilityTests.cs | 31 +- .../ReferencedAssemblyModuleAttributeTests.cs | 95 +- .../ReferencedAssemblyScanTests.cs | 106 +- .../GeneratorTests/RobustnessTests.cs | 749 +++++++----- .../GeneratorTests/ServiceOrderTests.cs | 63 +- .../ServiceRegistrationTests.cs | 238 ++-- .../GeneratorTests/TypeShapeTests.cs | 146 ++- .../Infrastructure/GeneratedAssembly.cs | 81 +- .../Infrastructure/GeneratorTestHarness.cs | 310 +++-- .../Infrastructure/ModelFactory.cs | 16 +- .../Infrastructure/Snapshot.cs | 99 +- .../NUnitTests/ModuleTestAttributeTests.cs | 56 +- .../RuntimeTests/AttributeTests.cs | 53 +- .../RuntimeTests/DecoratorHelperTests.cs | 124 +- .../RuntimeTests/DependencyRegistryTests.cs | 223 ++-- .../RuntimeTests/FeatureApplicatorTests.cs | 43 +- .../RuntimeTests/InvocationPipelineTests.cs | 331 ++++-- .../RuntimeTests/ModuleEnvironmentTests.cs | 247 ++-- .../RuntimeTests/ModuleLoadingTests.cs | 150 ++- .../ServiceCollectionExtensionsTests.cs | 49 +- .../TestingTests/SharedRegistrationsTests.cs | 63 +- .../TestingTests/TestContainerSourceTests.cs | 88 +- .../TestContainerSourceUnitTests.cs | 83 +- .../TestingTests/TestExportAttributeTests.cs | 51 +- .../TestParameterResolverTests.cs | 168 ++- .../xUnitTests/AttributeUtilityTests.cs | 77 +- .../xUnitTests/ModuleTestCaseDataTests.cs | 76 +- .../xUnitTests/ModuleTestDiscovererTests.cs | 110 +- 315 files changed, 16675 insertions(+), 9256 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 .csharpierignore diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..6e2e244 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.3.0", + "commands": [ + "csharpier" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.csharpierignore b/.csharpierignore new file mode 100644 index 0000000..8da2be7 --- /dev/null +++ b/.csharpierignore @@ -0,0 +1,9 @@ +# CSharpier reindents MSBuild XML but leaves the interior of multi-line comments at its old +# indentation, so every commented csproj in this repo comes out with hanging comment bodies. +# The C# formatting is what is wanted here; the project files stay hand-maintained. +*.csproj +*.props +*.targets + +# Build output. Nothing here is a source file. +artifacts/ diff --git a/benchmarks/DependencyModules.Benchmarks/Program.cs b/benchmarks/DependencyModules.Benchmarks/Program.cs index 9c2b6bb..537c8c6 100644 --- a/benchmarks/DependencyModules.Benchmarks/Program.cs +++ b/benchmarks/DependencyModules.Benchmarks/Program.cs @@ -44,15 +44,18 @@ namespace DependencyModules.Benchmarks; /// /// /// -public static class Program { - +public static class Program +{ private const int Runs = 15; - public static void Main() { + public static void Main() + { Console.WriteLine( - $"TieredCompilation={Environment.GetEnvironmentVariable("DOTNET_TieredCompilation") ?? "(default, results will drift)"}"); + $"TieredCompilation={Environment.GetEnvironmentVariable("DOTNET_TieredCompilation") ?? "(default, results will drift)"}" + ); - for (var i = 0; i < 20; i++) { + for (var i = 0; i < 20; i++) + { ColdRun(BuildSources(400, 100)); } @@ -60,12 +63,16 @@ public static void Main() { Console.WriteLine("classes implementing cold ms after one edit ms"); Console.WriteLine("---------------------------------------------------"); - foreach (var total in new[] { 500, 2000 }) { - foreach (var implementing in new[] { 0, total / 4, total }) { + foreach (var total in new[] { 500, 2000 }) + { + foreach (var implementing in new[] { 0, total / 4, total }) + { var cold = Median(() => ColdRun(BuildSources(total, implementing))); var incremental = Median(() => IncrementalRun(total, implementing)); - Console.WriteLine($"{total,7} {implementing,12} {cold,8:F1} {incremental,18:F1}"); + Console.WriteLine( + $"{total, 7} {implementing, 12} {cold, 8:F1} {incremental, 18:F1}" + ); } } @@ -74,10 +81,12 @@ public static void Main() { FrameworkStack(2000); } - private static double Median(Func measure) { + private static double Median(Func measure) + { var timings = new List(Runs); - for (var run = 0; run < Runs; run++) { + for (var run = 0; run < Runs; run++) + { timings.Add(measure()); } @@ -95,19 +104,21 @@ private static double Median(Func measure) { /// what a consuming project loads is this one plus one per framework. Measured rather than /// assumed, because it is the cost every such framework imposes on every build. /// - private class ExtensionShapedGenerator(string attributeName) : BaseSourceGenerator { - + private class ExtensionShapedGenerator(string attributeName) : BaseSourceGenerator + { protected override ITypeDefinition[] ModuleAttributeTypes() => new[] { TypeDefinition.Get("Bench.Framework", attributeName) }; - protected override IEnumerable AttributeSourceGenerators() { + protected override IEnumerable AttributeSourceGenerators() + { yield return new FrameworkAttributeGenerator(); } } - private class FrameworkAttributeGenerator : IDependencyModuleSourceGenerator { - - private static readonly ITypeDefinition[] _attributes = { + private class FrameworkAttributeGenerator : IDependencyModuleSourceGenerator + { + private static readonly ITypeDefinition[] _attributes = + { TypeDefinition.Get("Bench.Framework", "EndpointAttribute"), TypeDefinition.Get("Bench.Framework", "HandlerAttribute"), TypeDefinition.Get("Bench.Framework", "JobAttribute"), @@ -115,55 +126,71 @@ private class FrameworkAttributeGenerator : IDependencyModuleSourceGenerator { public void SetupGenerator( IncrementalGeneratorInitializationContext context, - IncrementalValuesProvider<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> incrementalValueProvider) { - + IncrementalValuesProvider<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> incrementalValueProvider + ) + { var models = AttributeModelCollector.Collect( context, _attributes, static (syntaxContext, cancellation) => - ServiceModelUtility.GetServiceModel(syntaxContext, cancellation) ?? ServiceModel.Ignore, + ServiceModelUtility.GetServiceModel(syntaxContext, cancellation) + ?? ServiceModel.Ignore, new ServiceModelComparer(), - ServiceModel.Ignore); + ServiceModel.Ignore + ); context.RegisterSourceOutput( incrementalValueProvider.Collect().Combine(models), - static (productionContext, data) => { }); + static (productionContext, data) => { } + ); } } - private static void FrameworkStack(int total) { + private static void FrameworkStack(int total) + { Console.WriteLine(); Console.WriteLine("analyzers loaded cold ms after one edit ms"); Console.WriteLine("--------------------------------------------------------------------"); - for (var frameworks = 0; frameworks <= 3; frameworks++) { + for (var frameworks = 0; frameworks <= 3; frameworks++) + { var count = frameworks; var cold = Median(() => Run(BuildSources(total, total / 4), count, cold: true)); var incremental = Median(() => Run(BuildSources(total, total / 4), count, cold: false)); - var label = count == 0 - ? "DependencyModules only" - : $"DependencyModules + {count} framework generator(s)"; + var label = + count == 0 + ? "DependencyModules only" + : $"DependencyModules + {count} framework generator(s)"; - Console.WriteLine($"{label,-40} {cold,8:F1} {incremental,18:F1}"); + Console.WriteLine($"{label, -40} {cold, 8:F1} {incremental, 18:F1}"); } } - private static double Run(string[] sources, int frameworks, bool cold) { + private static double Run(string[] sources, int frameworks, bool cold) + { var compilation = Compile(sources); - var generators = new List { - new SourceGenerator.SourceGenerator().AsSourceGenerator() + var generators = new List + { + new SourceGenerator.SourceGenerator().AsSourceGenerator(), }; - for (var i = 0; i < frameworks; i++) { - generators.Add(new ExtensionShapedGenerator($"Framework{i}Attribute").AsSourceGenerator()); + for (var i = 0; i < frameworks; i++) + { + generators.Add( + new ExtensionShapedGenerator($"Framework{i}Attribute").AsSourceGenerator() + ); } GeneratorDriver driver = CSharpGeneratorDriver.Create(generators); - if (cold) { + if (cold) + { return Time(() => driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _)); } @@ -173,17 +200,20 @@ private static double Run(string[] sources, int frameworks, bool cold) { var edited = compilation.ReplaceSyntaxTree( compilation.SyntaxTrees.ElementAt(1), - CSharpSyntaxTree.ParseText(sources[1].Replace("_seed = 0", "_seed = 42"), options)); + CSharpSyntaxTree.ParseText(sources[1].Replace("_seed = 0", "_seed = 42"), options) + ); return Time(() => driver.RunGeneratorsAndUpdateCompilation(edited, out _, out _)); } - private static double ColdRun(string[] sources) { + private static double ColdRun(string[] sources) + { var compilation = Compile(sources); // A fresh driver each run: reusing one would measure the incremental cache instead. var driver = CSharpGeneratorDriver.Create( - new SourceGenerator.SourceGenerator().AsSourceGenerator()); + new SourceGenerator.SourceGenerator().AsSourceGenerator() + ); return Time(() => driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _)); } @@ -191,25 +221,29 @@ private static double ColdRun(string[] sources) { /// /// The second run of one driver, after editing a method body in a single file. /// - private static double IncrementalRun(int total, int implementing) { + private static double IncrementalRun(int total, int implementing) + { var sources = BuildSources(total, implementing); var options = new CSharpParseOptions(LanguageVersion.Latest); var compilation = Compile(sources); GeneratorDriver driver = CSharpGeneratorDriver.Create( - new SourceGenerator.SourceGenerator().AsSourceGenerator()); + new SourceGenerator.SourceGenerator().AsSourceGenerator() + ); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _); // Only the edited tree is replaced; the other N-1 keep their identity and should stay cached. var edited = compilation.ReplaceSyntaxTree( compilation.SyntaxTrees.ElementAt(1), - CSharpSyntaxTree.ParseText(sources[1].Replace("_seed = 0", "_seed = 42"), options)); + CSharpSyntaxTree.ParseText(sources[1].Replace("_seed = 0", "_seed = 42"), options) + ); return Time(() => driver.RunGeneratorsAndUpdateCompilation(edited, out _, out _)); } - private static double Time(Action action) { + private static double Time(Action action) + { GC.Collect(); GC.WaitForPendingFinalizers(); @@ -220,25 +254,30 @@ private static double Time(Action action) { return (Stopwatch.GetTimestamp() - start) * 1000.0 / Stopwatch.Frequency; } - private static CSharpCompilation Compile(string[] sources) { + private static CSharpCompilation Compile(string[] sources) + { var options = new CSharpParseOptions(LanguageVersion.Latest); return CSharpCompilation.Create( "BenchAssembly", sources.Select(source => CSharpSyntaxTree.ParseText(source, options)), References, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + ); } - private static string[] BuildSources(int total, int implementing) { + private static string[] BuildSources(int total, int implementing) + { var trees = new string[total + 2]; trees[0] = "namespace BenchNamespace; public interface IMarker { }"; - for (var i = 0; i < total; i++) { - trees[i + 1] = i < implementing - ? $"namespace BenchNamespace; public class Implementing{i} : IMarker {{{Body(i)}}}" - : $"namespace BenchNamespace; public class Plain{i} {{{Body(i)}}}"; + for (var i = 0; i < total; i++) + { + trees[i + 1] = + i < implementing + ? $"namespace BenchNamespace; public class Implementing{i} : IMarker {{{Body(i)}}}" + : $"namespace BenchNamespace; public class Plain{i} {{{Body(i)}}}"; } var builder = new StringBuilder(); @@ -248,7 +287,9 @@ private static string[] BuildSources(int total, int implementing) { builder.AppendLine("namespace BenchNamespace;"); builder.AppendLine("[DependencyModule]"); builder.AppendLine("public partial class BenchModule : IConventionModule {"); - builder.AppendLine(" void IConventionModule.Conventions(IConventionDefinitions conventions) {"); + builder.AppendLine( + " void IConventionModule.Conventions(IConventionDefinitions conventions) {" + ); builder.AppendLine(" conventions.RegisterAll().AsScoped();"); builder.AppendLine(" }"); builder.AppendLine("}"); @@ -258,7 +299,8 @@ private static string[] BuildSources(int total, int implementing) { return trees; } - private static string Body(int i) => $$""" + private static string Body(int i) => + $$""" private readonly int _seed = {{i}}; public int Value => _seed; @@ -270,14 +312,18 @@ private static string Body(int i) => $$""" private static readonly MetadataReference[] References = BuildReferences(); - private static MetadataReference[] BuildReferences() { + private static MetadataReference[] BuildReferences() + { // Touched so the runtime and DI assemblies are loaded before the sweep below. _ = typeof(IServiceCollection); _ = typeof(Runtime.ModuleEnvironment); - return AppDomain.CurrentDomain.GetAssemblies() + return AppDomain + .CurrentDomain.GetAssemblies() .Where(assembly => !assembly.IsDynamic && !string.IsNullOrEmpty(assembly.Location)) - .Select(assembly => (MetadataReference)MetadataReference.CreateFromFile(assembly.Location)) + .Select(assembly => + (MetadataReference)MetadataReference.CreateFromFile(assembly.Location) + ) .ToArray(); } } diff --git a/integ-tests/ConsoleTestProject/Program.cs b/integ-tests/ConsoleTestProject/Program.cs index 6aec88c..5e020c6 100644 --- a/integ-tests/ConsoleTestProject/Program.cs +++ b/integ-tests/ConsoleTestProject/Program.cs @@ -16,4 +16,3 @@ container.GetRequiredService(); SutProject.SutModule.Run(); - diff --git a/integ-tests/ConsoleTestProject/TestExport.cs b/integ-tests/ConsoleTestProject/TestExport.cs index 5ae08a8..e1c78f2 100644 --- a/integ-tests/ConsoleTestProject/TestExport.cs +++ b/integ-tests/ConsoleTestProject/TestExport.cs @@ -3,5 +3,4 @@ namespace ConsoleTestProject; [SingletonService] -public class TestExport { -} \ No newline at end of file +public class TestExport { } diff --git a/integ-tests/SecondarySutProject/BetterDependencyOne.cs b/integ-tests/SecondarySutProject/BetterDependencyOne.cs index d5c4d9e..9892a18 100644 --- a/integ-tests/SecondarySutProject/BetterDependencyOne.cs +++ b/integ-tests/SecondarySutProject/BetterDependencyOne.cs @@ -4,18 +4,15 @@ namespace SecondarySutProject; [TransientService] -public class BetterDependencyOne : IDependencyOne { - - public BetterDependencyOne(ISingletonService singletonService, IScopedService scopedService) { +public class BetterDependencyOne : IDependencyOne +{ + public BetterDependencyOne(ISingletonService singletonService, IScopedService scopedService) + { SingletonService = singletonService; ScopedService = scopedService; } - public ISingletonService SingletonService { - get; - } + public ISingletonService SingletonService { get; } - public IScopedService ScopedService { - get; - } -} \ No newline at end of file + public IScopedService ScopedService { get; } +} diff --git a/integ-tests/SecondarySutProject/CircularReferenceModules/ModuleA.cs b/integ-tests/SecondarySutProject/CircularReferenceModules/ModuleA.cs index 9e9b2ee..90f4b9e 100644 --- a/integ-tests/SecondarySutProject/CircularReferenceModules/ModuleA.cs +++ b/integ-tests/SecondarySutProject/CircularReferenceModules/ModuleA.cs @@ -4,4 +4,4 @@ namespace SecondarySutProject.CircularReferenceModules; [DependencyModule] [ModuleB] -public partial class ModuleA { } \ No newline at end of file +public partial class ModuleA { } diff --git a/integ-tests/SecondarySutProject/CircularReferenceModules/ModuleB.cs b/integ-tests/SecondarySutProject/CircularReferenceModules/ModuleB.cs index 73a4651..e98ce0d 100644 --- a/integ-tests/SecondarySutProject/CircularReferenceModules/ModuleB.cs +++ b/integ-tests/SecondarySutProject/CircularReferenceModules/ModuleB.cs @@ -4,5 +4,4 @@ namespace SecondarySutProject.CircularReferenceModules; [DependencyModule] [ModuleA] -public partial class ModuleB { -} \ No newline at end of file +public partial class ModuleB { } diff --git a/integ-tests/SecondarySutProject/CircularReferenceModules/ServiceA.cs b/integ-tests/SecondarySutProject/CircularReferenceModules/ServiceA.cs index 53454f5..451eb8a 100644 --- a/integ-tests/SecondarySutProject/CircularReferenceModules/ServiceA.cs +++ b/integ-tests/SecondarySutProject/CircularReferenceModules/ServiceA.cs @@ -3,4 +3,4 @@ namespace SecondarySutProject.CircularReferenceModules; [TransientService(Realm = typeof(ModuleA))] -public class ServiceA { } \ No newline at end of file +public class ServiceA { } diff --git a/integ-tests/SecondarySutProject/CircularReferenceModules/ServiceB.cs b/integ-tests/SecondarySutProject/CircularReferenceModules/ServiceB.cs index 75633ad..b115a7a 100644 --- a/integ-tests/SecondarySutProject/CircularReferenceModules/ServiceB.cs +++ b/integ-tests/SecondarySutProject/CircularReferenceModules/ServiceB.cs @@ -3,4 +3,4 @@ namespace SecondarySutProject.CircularReferenceModules; [TransientService(Realm = typeof(ModuleB))] -public class ServiceB { } \ No newline at end of file +public class ServiceB { } diff --git a/integ-tests/SecondarySutProject/PackagePolicies.cs b/integ-tests/SecondarySutProject/PackagePolicies.cs index b466c7a..607df53 100644 --- a/integ-tests/SecondarySutProject/PackagePolicies.cs +++ b/integ-tests/SecondarySutProject/PackagePolicies.cs @@ -6,25 +6,29 @@ namespace SecondarySutProject; // This project does not reference the conventions analyzer, so nothing here registers itself. /// A policy contract a consumer might scan for. -public interface IPackagePolicy { +public interface IPackagePolicy +{ /// Identifies the policy in assertions. string Name { get; } } /// A public policy, visible across the assembly boundary. -public class FirstPackagePolicy : IPackagePolicy { +public class FirstPackagePolicy : IPackagePolicy +{ /// public string Name => "first"; } /// A second public policy, so the scan has more than one match. -public class SecondPackagePolicy : IPackagePolicy { +public class SecondPackagePolicy : IPackagePolicy +{ /// public string Name => "second"; } // Internal, so it is invisible across the boundary — the difference between scanning metadata and // scanning the compilation being built. -internal class HiddenPackagePolicy : IPackagePolicy { +internal class HiddenPackagePolicy : IPackagePolicy +{ public string Name => "hidden"; } diff --git a/integ-tests/SecondarySutProject/ParameterizedModules/ParameterizedModule.cs b/integ-tests/SecondarySutProject/ParameterizedModules/ParameterizedModule.cs index 3cd4932..6bebcd2 100644 --- a/integ-tests/SecondarySutProject/ParameterizedModules/ParameterizedModule.cs +++ b/integ-tests/SecondarySutProject/ParameterizedModules/ParameterizedModule.cs @@ -6,18 +6,21 @@ namespace SecondarySutProject.ParameterizedModules; [DependencyModule(OnlyRealm = true)] -public partial class ParameterizedModule : IServiceCollectionConfiguration { +public partial class ParameterizedModule : IServiceCollectionConfiguration +{ private readonly string _a; private readonly int _b; - public ParameterizedModule(string a, int b) { + public ParameterizedModule(string a, int b) + { _a = a; _b = b; } public string? C { get; set; } - public void ConfigureServices(IServiceCollection services) { + public void ConfigureServices(IServiceCollection services) + { services.AddTransient(_ => new SomeRuntimeDependency(_a, _b, C!)); } @@ -27,4 +30,4 @@ public override bool Equals(object? obj) => obj is ParameterizedModule other && other._a == _a && other._b == _b && other.C == C; public override int GetHashCode() => HashCode.Combine(_a, _b, C); -} \ No newline at end of file +} diff --git a/integ-tests/SecondarySutProject/ParameterizedModules/SecondaryParameterizedModule.cs b/integ-tests/SecondarySutProject/ParameterizedModules/SecondaryParameterizedModule.cs index dd7c803..6aff520 100644 --- a/integ-tests/SecondarySutProject/ParameterizedModules/SecondaryParameterizedModule.cs +++ b/integ-tests/SecondarySutProject/ParameterizedModules/SecondaryParameterizedModule.cs @@ -4,4 +4,4 @@ namespace SecondarySutProject.ParameterizedModules; [DependencyModule(OnlyRealm = true)] [ParameterizedModule("test-string", 10)] -public partial class SecondaryParameterizedModule { } \ No newline at end of file +public partial class SecondaryParameterizedModule { } diff --git a/integ-tests/SecondarySutProject/ParameterizedModules/SomeRuntimeDependency.cs b/integ-tests/SecondarySutProject/ParameterizedModules/SomeRuntimeDependency.cs index 27eaaa9..955605c 100644 --- a/integ-tests/SecondarySutProject/ParameterizedModules/SomeRuntimeDependency.cs +++ b/integ-tests/SecondarySutProject/ParameterizedModules/SomeRuntimeDependency.cs @@ -1,8 +1,9 @@ namespace SecondarySutProject.ParameterizedModules; -public class SomeRuntimeDependency { - - public SomeRuntimeDependency(string someDependency, int intDependency, string cValue) { +public class SomeRuntimeDependency +{ + public SomeRuntimeDependency(string someDependency, int intDependency, string cValue) + { SomeDependency = someDependency; IntDependency = intDependency; CValue = cValue; @@ -13,4 +14,4 @@ public SomeRuntimeDependency(string someDependency, int intDependency, string cV public int IntDependency { get; } public string? CValue { get; } -} \ No newline at end of file +} diff --git a/integ-tests/SecondarySutProject/SecondarySutModule.cs b/integ-tests/SecondarySutProject/SecondarySutModule.cs index f99aa0e..8c86b04 100644 --- a/integ-tests/SecondarySutProject/SecondarySutModule.cs +++ b/integ-tests/SecondarySutProject/SecondarySutModule.cs @@ -5,4 +5,4 @@ namespace SecondarySutProject; [DependencyModule] [SutModule] -public partial class SecondarySutModule { } \ No newline at end of file +public partial class SecondarySutModule { } diff --git a/integ-tests/SutProject.NUnitTests/DataRowTests.cs b/integ-tests/SutProject.NUnitTests/DataRowTests.cs index aed1dea..1610ab5 100644 --- a/integ-tests/SutProject.NUnitTests/DataRowTests.cs +++ b/integ-tests/SutProject.NUnitTests/DataRowTests.cs @@ -12,14 +12,17 @@ namespace SutProject.NUnitTests; /// cannot express a row that covers the first parameters while the container covers the rest, which /// is what a module test with data is. [ModuleTestCase] is the same idea without that rule. /// -public class DataRowTests { - +public class DataRowTests +{ [ModuleTest(typeof(SutModule))] [ModuleTestCase(1)] [ModuleTestCase(2)] [ModuleTestCase(3)] public void RowSuppliesTheLeadingParameterAndTheContainerTheRest( - int number, ISingletonService singletonService) { + int number, + ISingletonService singletonService + ) + { Assert.That(number, Is.InRange(1, 3)); Assert.That(singletonService, Is.Not.Null, "resolved from the container, not from the row"); @@ -32,7 +35,11 @@ public void RowSuppliesTheLeadingParameterAndTheContainerTheRest( [ModuleTestCase("first", 1)] [ModuleTestCase("second", 2)] public void SeveralLeadingParametersComeFromTheRow( - string word, int number, ISingletonService singletonService) { + string word, + int number, + ISingletonService singletonService + ) + { Assert.That(word, Is.AnyOf("first", "second")); Assert.That(number, Is.AnyOf(1, 2)); Assert.That(singletonService, Is.Not.Null); @@ -41,7 +48,8 @@ public void SeveralLeadingParametersComeFromTheRow( /// A row can fill every parameter, leaving nothing for the container. [ModuleTest(typeof(SutModule))] [ModuleTestCase(4, 5)] - public void ARowMayCoverEveryParameter(int first, int second) { + public void ARowMayCoverEveryParameter(int first, int second) + { Assert.That(first + second, Is.EqualTo(9)); } @@ -50,7 +58,10 @@ public void ARowMayCoverEveryParameter(int first, int second) { [ModuleTestCase(10)] [ModuleTestCase(20)] public void RowsComposeWithInjectedValues( - int number, [InjectValues("supplied")] NeedsAValue needsAValue) { + int number, + [InjectValues("supplied")] NeedsAValue needsAValue + ) + { Assert.That(number, Is.AnyOf(10, 20)); Assert.That(needsAValue.Text, Is.EqualTo("supplied")); Assert.That(needsAValue.SingletonService, Is.Not.Null); @@ -58,11 +69,13 @@ public void RowsComposeWithInjectedValues( [ModuleTest(typeof(SutModule))] [ModuleTestCase(1, TestName = "a row can name itself")] - public void NamedRow(int number) { + public void NamedRow(int number) + { Assert.That(number, Is.EqualTo(1)); } - public class NeedsAValue(ISingletonService singletonService, string text) { + public class NeedsAValue(ISingletonService singletonService, string text) + { public ISingletonService SingletonService { get; } = singletonService; public string Text { get; } = text; @@ -72,10 +85,11 @@ public class NeedsAValue(ISingletonService singletonService, string text) { /// /// Each row is its own test case, so each gets its own container — the same rule repetitions follow. /// -public class DDataRowReport { - +public class DDataRowReport +{ [Test] - public void EveryRowRanExactlyOnce() { + public void EveryRowRanExactlyOnce() + { Assert.That(DataRowTests.SeenNumbers, Is.EquivalentTo(new[] { 1, 2, 3 })); } } diff --git a/integ-tests/SutProject.NUnitTests/EnvironmentSeedingTests.cs b/integ-tests/SutProject.NUnitTests/EnvironmentSeedingTests.cs index 80a8cb3..f555c4e 100644 --- a/integ-tests/SutProject.NUnitTests/EnvironmentSeedingTests.cs +++ b/integ-tests/SutProject.NUnitTests/EnvironmentSeedingTests.cs @@ -13,7 +13,8 @@ namespace SutProject.NUnitTests; /// conditions through the real runner, which applies modules before the service-setup pass. /// [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] -public class SeededEnvironmentAttribute(string name) : Attribute, IModuleEnvironmentProvider { +public class SeededEnvironmentAttribute(string name) : Attribute, IModuleEnvironmentProvider +{ public IModuleEnvironment? ProvideEnvironment(MethodInfo testMethod) => new ModuleEnvironment(false, name); } @@ -27,11 +28,12 @@ public class GatedByEnvironment : IGatedByEnvironment { } [DependencyModule(OnlyRealm = true)] public partial class SeededEnvironmentModule { } -public class EnvironmentSeedingTests { - +public class EnvironmentSeedingTests +{ [ModuleTest(typeof(SeededEnvironmentModule))] [SeededEnvironment("seeded-environment")] - public void AGatedRegistrationAppliesUnderTheSeededEnvironment(IServiceProvider provider) { + public void AGatedRegistrationAppliesUnderTheSeededEnvironment(IServiceProvider provider) + { Assert.That(provider.GetService(), Is.Not.Null); } @@ -40,7 +42,8 @@ public void AGatedRegistrationAppliesUnderTheSeededEnvironment(IServiceProvider /// condition was never compiled in. /// [ModuleTest(typeof(SeededEnvironmentModule))] - public void TheSameRegistrationIsAbsentWithoutASeed(IServiceProvider provider) { + public void TheSameRegistrationIsAbsentWithoutASeed(IServiceProvider provider) + { Assert.That(provider.GetService(), Is.Null); } } diff --git a/integ-tests/SutProject.NUnitTests/FakeItEasy/FakeItEasyTests.cs b/integ-tests/SutProject.NUnitTests/FakeItEasy/FakeItEasyTests.cs index dcaffc7..92f090f 100644 --- a/integ-tests/SutProject.NUnitTests/FakeItEasy/FakeItEasyTests.cs +++ b/integ-tests/SutProject.NUnitTests/FakeItEasy/FakeItEasyTests.cs @@ -10,14 +10,16 @@ namespace SutProject.NUnitTests.FakeItEasy; /// The FakeItEasy package, unchanged, against NUnit. /// [FakeItEasySupport] -public class FakeItEasyTests { - +public class FakeItEasyTests +{ [ModuleTest] [SutModule] public void MockTest( [Mock] IDependencyOne dependencyOne, [Mock] IScopedService scopedService, - ISingletonService singletonService) { + ISingletonService singletonService + ) + { A.CallTo(() => dependencyOne.SingletonService).Returns(singletonService); A.CallTo(() => dependencyOne.ScopedService).Returns(scopedService); @@ -28,7 +30,8 @@ public void MockTest( /// The injected fake is the thing you configure, unlike Moq — no unwrapping step. [ModuleTest] [SutModule] - public void TheInjectedInstanceIsTheFake([Mock] IDependencyOne dependencyOne) { + public void TheInjectedInstanceIsTheFake([Mock] IDependencyOne dependencyOne) + { Assert.That(Fake.GetFakeManager(dependencyOne), Is.Not.Null); } } diff --git a/integ-tests/SutProject.NUnitTests/IterationLifetimeTests.cs b/integ-tests/SutProject.NUnitTests/IterationLifetimeTests.cs index 88d2a94..a4f666a 100644 --- a/integ-tests/SutProject.NUnitTests/IterationLifetimeTests.cs +++ b/integ-tests/SutProject.NUnitTests/IterationLifetimeTests.cs @@ -12,23 +12,22 @@ public partial class LifetimeModule { } /// than inferred. /// [ScopedService(Realm = typeof(LifetimeModule))] -public class TrackedService : IDisposable { - +public class TrackedService : IDisposable +{ private static int _next; public static readonly List Constructed = []; public static readonly List Disposed = []; - public TrackedService() { + public TrackedService() + { Id = Interlocked.Increment(ref _next); Constructed.Add(Id); } - public int Id { - get; - } + public int Id { get; } public void Dispose() => Disposed.Add(Id); } @@ -43,8 +42,8 @@ public int Id { /// because the report at the end reads what they recorded; NUnit runs fixtures within an assembly /// in alphabetical order. /// -public class ARepeatedModuleTests { - +public class ARepeatedModuleTests +{ public static readonly List Log = []; public static readonly List ServiceIds = []; @@ -57,42 +56,54 @@ public class ARepeatedModuleTests { [ModuleTest(typeof(LifetimeModule))] [Repeat(3)] - public void EachRepetitionGetsItsOwnContainer(TrackedService trackedService) { + public void EachRepetitionGetsItsOwnContainer(TrackedService trackedService) + { Log.Add($"test:{trackedService.Id}"); ServiceIds.Add(trackedService.Id); } } -public class BRetriedModuleTests { - +public class BRetriedModuleTests +{ private static int _attempts; public static readonly List ServiceIds = []; [ModuleTest(typeof(LifetimeModule))] [Retry(3)] - public void EachRetryAttemptGetsItsOwnContainer(TrackedService trackedService) { + public void EachRetryAttemptGetsItsOwnContainer(TrackedService trackedService) + { ServiceIds.Add(trackedService.Id); _attempts++; - Assert.That(_attempts, Is.EqualTo(3), "fails the first two attempts on purpose, passes the third"); + Assert.That( + _attempts, + Is.EqualTo(3), + "fails the first two attempts on purpose, passes the third" + ); } } -public class CLifetimeReport { - +public class CLifetimeReport +{ /// /// The container has to outlive setup and teardown, not sit between them. Wrapping only the test /// method would order this setup, open, test, close, teardown — leaving [SetUp] running /// before the container exists and [TearDown] after it is gone. /// [Test] - public void SetUpAndTearDownRunInsideTheContainersLifetime() { - Assert.That(ARepeatedModuleTests.Log, Has.Count.EqualTo(9), "three iterations of setup, test, teardown"); - - for (var i = 0; i < 3; i++) { + public void SetUpAndTearDownRunInsideTheContainersLifetime() + { + Assert.That( + ARepeatedModuleTests.Log, + Has.Count.EqualTo(9), + "three iterations of setup, test, teardown" + ); + + for (var i = 0; i < 3; i++) + { Assert.That(ARepeatedModuleTests.Log[i * 3], Is.EqualTo("setup")); Assert.That(ARepeatedModuleTests.Log[i * 3 + 1], Does.StartWith("test:")); Assert.That(ARepeatedModuleTests.Log[i * 3 + 2], Is.EqualTo("teardown")); @@ -100,22 +111,30 @@ public void SetUpAndTearDownRunInsideTheContainersLifetime() { } [Test] - public void NoServiceInstanceIsSharedBetweenIterations() { + public void NoServiceInstanceIsSharedBetweenIterations() + { var repeated = ARepeatedModuleTests.ServiceIds; var retried = BRetriedModuleTests.ServiceIds; Assert.That(repeated, Has.Count.EqualTo(3)); Assert.That(retried, Has.Count.EqualTo(3)); - Assert.That(repeated.Concat(retried).Distinct().Count(), Is.EqualTo(6), - "three repetitions and three retry attempts, six containers, six instances"); + Assert.That( + repeated.Concat(retried).Distinct().Count(), + Is.EqualTo(6), + "three repetitions and three retry attempts, six containers, six instances" + ); } [Test] - public void EveryIterationsServicesWereDisposedWithItsContainer() { + public void EveryIterationsServicesWereDisposedWithItsContainer() + { var iterationIds = ARepeatedModuleTests.ServiceIds.Concat(BRetriedModuleTests.ServiceIds); - Assert.That(TrackedService.Disposed, Is.SupersetOf(iterationIds), - "the container is torn down at the end of the iteration, not left to the fixture"); + Assert.That( + TrackedService.Disposed, + Is.SupersetOf(iterationIds), + "the container is torn down at the end of the iteration, not left to the fixture" + ); } } diff --git a/integ-tests/SutProject.NUnitTests/ModuleLoadingTests.cs b/integ-tests/SutProject.NUnitTests/ModuleLoadingTests.cs index 83f4559..c4ca047 100644 --- a/integ-tests/SutProject.NUnitTests/ModuleLoadingTests.cs +++ b/integ-tests/SutProject.NUnitTests/ModuleLoadingTests.cs @@ -20,11 +20,12 @@ public class ExtraService { } /// [Test] does, so a module test fixture needs no class-level attribute — the same as the /// xUnit integration. /// -public class ModuleLoadingTests { - +public class ModuleLoadingTests +{ /// Modules named on the attribute itself. [ModuleTest(typeof(SutModule))] - public void LoadsAModuleNamedByType(ISingletonService singletonService) { + public void LoadsAModuleNamedByType(ISingletonService singletonService) + { Assert.That(singletonService, Is.Not.Null); Assert.That(singletonService.GetName(), Is.EqualTo(nameof(SingletonService))); } @@ -32,25 +33,29 @@ public void LoadsAModuleNamedByType(ISingletonService singletonService) { /// The generated module attribute, which reaches the same loading by another route. [ModuleTest] [SutModule] - public void LoadsAModuleNamedByItsGeneratedAttribute(IDependencyOne dependencyOne) { + public void LoadsAModuleNamedByItsGeneratedAttribute(IDependencyOne dependencyOne) + { Assert.That(dependencyOne.SingletonService, Is.Not.Null); Assert.That(dependencyOne.ScopedService, Is.Not.Null); } [ModuleTest(typeof(SutModule), typeof(ExtraModule))] - public void LoadsSeveralModules(ISingletonService singletonService, ExtraService extraService) { + public void LoadsSeveralModules(ISingletonService singletonService, ExtraService extraService) + { Assert.That(singletonService, Is.Not.Null); Assert.That(extraService, Is.Not.Null); } [ModuleTest] - public void TakesNoModulesAtAll() { + public void TakesNoModulesAtAll() + { Assert.Pass("a module test need not name a module"); } /// The container itself, which cannot be resolved from itself. [ModuleTest(typeof(SutModule))] - public void InjectsTheServiceProvider(IServiceProvider serviceProvider) { + public void InjectsTheServiceProvider(IServiceProvider serviceProvider) + { Assert.That(serviceProvider.GetService(), Is.Not.Null); } @@ -59,18 +64,24 @@ public void InjectsTheServiceProvider(IServiceProvider serviceProvider) { /// class under test without registering it. /// [ModuleTest(typeof(SutModule))] - public void ConstructsAnUnregisteredConcreteType(NeedsASingleton needsASingleton) { + public void ConstructsAnUnregisteredConcreteType(NeedsASingleton needsASingleton) + { Assert.That(needsASingleton.SingletonService, Is.Not.Null); } [ModuleTest(typeof(SutModule))] - public void PublishesTheTestCaseInfo(ITestCaseInfo testCaseInfo, ISingletonService singletonService) { + public void PublishesTheTestCaseInfo( + ITestCaseInfo testCaseInfo, + ISingletonService singletonService + ) + { Assert.That(testCaseInfo.TestMethod.Name, Is.EqualTo(nameof(PublishesTheTestCaseInfo))); Assert.That(testCaseInfo.TestMethodArguments, Has.Count.EqualTo(2)); Assert.That(testCaseInfo.TestMethodArguments[1], Is.SameAs(singletonService)); } - public class NeedsASingleton(ISingletonService singletonService) { + public class NeedsASingleton(ISingletonService singletonService) + { public ISingletonService SingletonService { get; } = singletonService; } } diff --git a/integ-tests/SutProject.NUnitTests/Moq/MoqTests.cs b/integ-tests/SutProject.NUnitTests/Moq/MoqTests.cs index e5ff3b3..a6fb00d 100644 --- a/integ-tests/SutProject.NUnitTests/Moq/MoqTests.cs +++ b/integ-tests/SutProject.NUnitTests/Moq/MoqTests.cs @@ -10,12 +10,15 @@ namespace SutProject.NUnitTests.Moq; /// The Moq package, unchanged, against NUnit. /// [MoqSupport] -public class MoqTests { - +public class MoqTests +{ [ModuleTest] [SutModule] public void MockTest( - [Mock] Mock dependencyOne, ISingletonService singletonService) { + [Mock] Mock dependencyOne, + ISingletonService singletonService + ) + { dependencyOne.Setup(mock => mock.SingletonService).Returns(singletonService); Assert.That(dependencyOne.Object.SingletonService, Is.SameAs(singletonService)); @@ -30,7 +33,8 @@ public void MockTest( [ModuleTest] [SutModule] [TestExport(typeof(ISingletonService), Implementation = typeof(ExportedSingletonService))] - public void TestExportBeatsAMockOfTheSameService(ISingletonService singletonService) { + public void TestExportBeatsAMockOfTheSameService(ISingletonService singletonService) + { Assert.That(singletonService, Is.TypeOf()); } } diff --git a/integ-tests/SutProject.NUnitTests/NSubstitute/NSubstituteTests.cs b/integ-tests/SutProject.NUnitTests/NSubstitute/NSubstituteTests.cs index c70916b..42c4af9 100644 --- a/integ-tests/SutProject.NUnitTests/NSubstitute/NSubstituteTests.cs +++ b/integ-tests/SutProject.NUnitTests/NSubstitute/NSubstituteTests.cs @@ -17,14 +17,16 @@ namespace SutProject.NUnitTests.NSubstitute; /// each other. /// [NSubstituteSupport] -public class NSubstituteTests { - +public class NSubstituteTests +{ [ModuleTest] [SutModule] public void MockTest( [Mock] IDependencyOne dependencyOne, [Mock] IScopedService scopedService, - ISingletonService singletonService) { + ISingletonService singletonService + ) + { dependencyOne.SingletonService.Returns(singletonService); dependencyOne.ScopedService.Returns(scopedService); @@ -36,7 +38,10 @@ public void MockTest( [ModuleTest] [SutModule] public void AMockReplacesTheRegistrationForTheWholeContainer( - [Mock] IScopedService scopedService, IDependencyOne dependencyOne) { + [Mock] IScopedService scopedService, + IDependencyOne dependencyOne + ) + { Assert.That(dependencyOne.ScopedService, Is.SameAs(scopedService)); } @@ -47,8 +52,13 @@ public void AMockReplacesTheRegistrationForTheWholeContainer( [ModuleTest] [SutModule] [Repeat(3)] - public void EachIterationGetsAFreshMock([Mock] IScopedService scopedService) { - Assert.That(Seen.Add(scopedService), Is.True, "a mock instance is never reused across iterations"); + public void EachIterationGetsAFreshMock([Mock] IScopedService scopedService) + { + Assert.That( + Seen.Add(scopedService), + Is.True, + "a mock instance is never reused across iterations" + ); } private static readonly HashSet Seen = []; diff --git a/integ-tests/SutProject.NUnitTests/ServiceProviderBuilderPrecedenceTests.cs b/integ-tests/SutProject.NUnitTests/ServiceProviderBuilderPrecedenceTests.cs index 170cf89..13949a2 100644 --- a/integ-tests/SutProject.NUnitTests/ServiceProviderBuilderPrecedenceTests.cs +++ b/integ-tests/SutProject.NUnitTests/ServiceProviderBuilderPrecedenceTests.cs @@ -8,20 +8,28 @@ namespace SutProject.NUnitTests; /// /// Records which actually built the container. /// -public interface IProviderBuiltBy { +public interface IProviderBuiltBy +{ string Scope { get; } } -public class ProviderBuiltBy(string scope) : IProviderBuiltBy { +public class ProviderBuiltBy(string scope) : IProviderBuiltBy +{ public string Scope => scope; } /// /// A builder that stamps the container with the scope it was declared at. /// -public class ScopeStampingProviderAttribute(string scope) : Attribute, IServiceProviderBuilderAttribute { +public class ScopeStampingProviderAttribute(string scope) + : Attribute, + IServiceProviderBuilderAttribute +{ public IServiceProvider BuildServiceProvider( - ITestMethodContext testMethod, IServiceCollection serviceCollection) { + ITestMethodContext testMethod, + IServiceCollection serviceCollection + ) + { serviceCollection.AddSingleton(new ProviderBuiltBy(scope)); return serviceCollection.BuildServiceProvider(); @@ -34,16 +42,18 @@ public IServiceProvider BuildServiceProvider( /// overridden by a broader default. /// [ScopeStampingProvider("class")] -public class ServiceProviderBuilderPrecedenceTests { - +public class ServiceProviderBuilderPrecedenceTests +{ [ModuleTest] [ScopeStampingProvider("method")] - public void MethodBeatsClass(IProviderBuiltBy builtBy) { + public void MethodBeatsClass(IProviderBuiltBy builtBy) + { Assert.That(builtBy.Scope, Is.EqualTo("method")); } [ModuleTest] - public void ClassAppliesWhenTheMethodDeclaresNone(IProviderBuiltBy builtBy) { + public void ClassAppliesWhenTheMethodDeclaresNone(IProviderBuiltBy builtBy) + { Assert.That(builtBy.Scope, Is.EqualTo("class")); } } diff --git a/integ-tests/SutProject.NUnitTests/TestExportTests.cs b/integ-tests/SutProject.NUnitTests/TestExportTests.cs index 24f5afb..de32e51 100644 --- a/integ-tests/SutProject.NUnitTests/TestExportTests.cs +++ b/integ-tests/SutProject.NUnitTests/TestExportTests.cs @@ -8,7 +8,8 @@ namespace SutProject.NUnitTests; /// /// Stands in for the real singleton, so a test can tell which of two registrations survived. /// -public class ExportedSingletonService : ISingletonService { +public class ExportedSingletonService : ISingletonService +{ public string GetName() => nameof(ExportedSingletonService); } @@ -20,12 +21,13 @@ public class ExportedSingletonService : ISingletonService { /// is why it is available here at all — it registers through ITestServiceSetupAttribute and /// never needed a test framework. /// -public class TestExportTests { - +public class TestExportTests +{ [ModuleTest] [SutModule] [TestExport(typeof(ISingletonService), Implementation = typeof(ExportedSingletonService))] - public void OverridesARegistrationForOneTest(ISingletonService singletonService) { + public void OverridesARegistrationForOneTest(ISingletonService singletonService) + { Assert.That(singletonService, Is.TypeOf()); } @@ -35,15 +37,23 @@ public void OverridesARegistrationForOneTest(ISingletonService singletonService) /// [ModuleTest] [SutModule] - public void TheOverrideDoesNotLeakIntoTheNextTest(ISingletonService singletonService) { + public void TheOverrideDoesNotLeakIntoTheNextTest(ISingletonService singletonService) + { Assert.That(singletonService, Is.TypeOf()); } [ModuleTest] [SutModule] - [TestExport(typeof(ISingletonService), Implementation = typeof(ExportedSingletonService), - Lifetime = ServiceLifetime.Singleton)] - public void HonoursTheLifetimeItIsGiven(ISingletonService first, IServiceProvider serviceProvider) { + [TestExport( + typeof(ISingletonService), + Implementation = typeof(ExportedSingletonService), + Lifetime = ServiceLifetime.Singleton + )] + public void HonoursTheLifetimeItIsGiven( + ISingletonService first, + IServiceProvider serviceProvider + ) + { Assert.That(serviceProvider.GetRequiredService(), Is.SameAs(first)); } } diff --git a/integ-tests/SutProject.Tests/CircularReferenceModules/CircularModuleTests.cs b/integ-tests/SutProject.Tests/CircularReferenceModules/CircularModuleTests.cs index 8cda3a8..0aa354a 100644 --- a/integ-tests/SutProject.Tests/CircularReferenceModules/CircularModuleTests.cs +++ b/integ-tests/SutProject.Tests/CircularReferenceModules/CircularModuleTests.cs @@ -4,29 +4,30 @@ namespace SutProject.Tests.CircularReferenceModules; -public class CircularModuleTests { - +public class CircularModuleTests +{ [ModuleTest] [ModuleA] - public void LoadModuleATest(ServiceA serviceA, ServiceB serviceB) { + public void LoadModuleATest(ServiceA serviceA, ServiceB serviceB) + { Assert.NotNull(serviceA); Assert.NotNull(serviceB); } - [ModuleTest] [ModuleB] - public void LoadModuleBTest(ServiceA serviceA, ServiceB serviceB) { + public void LoadModuleBTest(ServiceA serviceA, ServiceB serviceB) + { Assert.NotNull(serviceA); Assert.NotNull(serviceB); } - [ModuleTest] [ModuleA] [ModuleB] - public void LoadModuleBothTest(ServiceA serviceA, ServiceB serviceB) { + public void LoadModuleBothTest(ServiceA serviceA, ServiceB serviceB) + { Assert.NotNull(serviceA); Assert.NotNull(serviceB); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/ConventionTests/ConventionEdgeModules.cs b/integ-tests/SutProject.Tests/ConventionTests/ConventionEdgeModules.cs index 7006567..6399b26 100644 --- a/integ-tests/SutProject.Tests/ConventionTests/ConventionEdgeModules.cs +++ b/integ-tests/SutProject.Tests/ConventionTests/ConventionEdgeModules.cs @@ -1,5 +1,5 @@ -using DependencyModules.Runtime.Conventions; using DependencyModules.Runtime.Attributes; +using DependencyModules.Runtime.Conventions; using SecondarySutProject; using SutProject.Tests.ConventionTests.Nested; @@ -17,23 +17,28 @@ namespace SutProject.Tests.ConventionTests; // to use when the decorator or the service comes from an assembly you do not control. // --------------------------------------------------------------------------- -public interface IModuleDecorated { +public interface IModuleDecorated +{ string Describe(); } -public class ModuleDecoratedCore : IModuleDecorated { +public class ModuleDecoratedCore : IModuleDecorated +{ public string Describe() => "core"; } /// Carries no [Decorator]; the module names it instead. -public class ModuleDecoratedWrapper(IModuleDecorated inner) : IModuleDecorated { +public class ModuleDecoratedWrapper(IModuleDecorated inner) : IModuleDecorated +{ public string Describe() => $"wrapped({inner.Describe()})"; } [DependencyModule] [Decorate(typeof(IModuleDecorated), typeof(ModuleDecoratedWrapper))] -public partial class ConventionModuleDecorateModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionModuleDecorateModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { // The wrapper implements the interface too, so it would match. Excluding it by name is the // cost of declaring decoration on the module rather than on the class. conventions.RegisterAll().WithoutName("*Wrapper").AsSingleton(); @@ -44,28 +49,35 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // Two modules scanning one interface, composed into the same application. // --------------------------------------------------------------------------- -public interface IShared { +public interface IShared +{ string Name { get; } } -public class SharedFirst : IShared { +public class SharedFirst : IShared +{ public string Name => "first"; } -public class SharedSecond : IShared { +public class SharedSecond : IShared +{ public string Name => "second"; } [DependencyModule] -public partial class ConventionSharedFirstModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionSharedFirstModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().WithName("SharedFirst").AsSingleton(); } } [DependencyModule] -public partial class ConventionSharedSecondModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionSharedSecondModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().WithName("SharedSecond").AsSingleton(); conventions.RegisterAll().WithoutName("SharedFirst").AsSingleton(); } @@ -75,35 +87,48 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // Exact versus prefix namespaces, and the negative form. // --------------------------------------------------------------------------- -public interface INamespaceScanned { +public interface INamespaceScanned +{ string Name { get; } } -public class RootLevelScanned : INamespaceScanned { +public class RootLevelScanned : INamespaceScanned +{ public string Name => "root"; } /// Prefix filters reach into nested namespaces; exact ones do not. [DependencyModule] -public partial class ConventionPrefixNamespaceModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().InNamespaceOf().AsSingleton(); +public partial class ConventionPrefixNamespaceModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { + conventions + .RegisterAll() + .InNamespaceOf() + .AsSingleton(); } } [DependencyModule] -public partial class ConventionExactNamespaceModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll() +public partial class ConventionExactNamespaceModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { + conventions + .RegisterAll() .InExactNamespaces("SutProject.Tests.ConventionTests") .AsSingleton(); } } [DependencyModule] -public partial class ConventionExcludedNamespaceModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll() +public partial class ConventionExcludedNamespaceModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { + conventions + .RegisterAll() .NotInNamespaceOf() .AsSingleton(); } @@ -113,18 +138,22 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // An open generic implementation, resolved at several closings. // --------------------------------------------------------------------------- -public interface IOpenCache { +public interface IOpenCache +{ string Describe(); } /// Closes nothing, so it registers as the open generic. -public class OpenPassThroughCache : IOpenCache { +public class OpenPassThroughCache : IOpenCache +{ public string Describe() => "cache:" + typeof(T).Name; } [DependencyModule] -public partial class ConventionOpenGenericModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionOpenGenericModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll(typeof(IOpenCache<>)).AsSingleton(); } } @@ -133,36 +162,44 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // Lifetime, disposal, and an internal candidate. // --------------------------------------------------------------------------- -public interface IScopedByConvention { +public interface IScopedByConvention +{ Guid Id { get; } } -public class ScopedByConvention : IScopedByConvention { +public class ScopedByConvention : IScopedByConvention +{ public Guid Id { get; } = Guid.NewGuid(); } -public interface IDisposableByConvention { +public interface IDisposableByConvention +{ bool Disposed { get; } } -public class DisposableByConvention : IDisposableByConvention, IDisposable { +public class DisposableByConvention : IDisposableByConvention, IDisposable +{ public bool Disposed { get; private set; } public void Dispose() => Disposed = true; } /// Internal, and still a candidate — the compilation being built sees its own internals. -public interface IInternallyImplemented { +public interface IInternallyImplemented +{ string Name { get; } } -internal class InternalCandidate : IInternallyImplemented { +internal class InternalCandidate : IInternallyImplemented +{ public string Name => "internal"; } [DependencyModule] -public partial class ConventionLifetimeModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionLifetimeModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsScoped(); conventions.RegisterAll().AsSingleton(); conventions.RegisterAll().AsSingleton(); @@ -174,32 +211,38 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // ActivatorUtilities, so everything but the inner instance is resolved from the container. // --------------------------------------------------------------------------- -public interface IDecoratorDependency { +public interface IDecoratorDependency +{ string Value { get; } } -public class DecoratorDependency : IDecoratorDependency { +public class DecoratorDependency : IDecoratorDependency +{ public string Value => "dep"; } -public interface IDependentlyDecorated { +public interface IDependentlyDecorated +{ string Describe(); } -public class DependentlyDecoratedCore : IDependentlyDecorated { +public class DependentlyDecoratedCore : IDependentlyDecorated +{ public string Describe() => "core"; } [Decorator] public class DependentlyDecorating(IDependentlyDecorated inner, IDecoratorDependency dependency) - : IDependentlyDecorated { - + : IDependentlyDecorated +{ public string Describe() => $"{dependency.Value}({inner.Describe()})"; } [DependencyModule] -public partial class ConventionDecoratorDependencyModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionDecoratorDependencyModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton(); conventions.RegisterAll().AsSingleton(); } @@ -210,9 +253,12 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // --------------------------------------------------------------------------- [DependencyModule] -public partial class ConventionFilteredScanModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll() +public partial class ConventionFilteredScanModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { + conventions + .RegisterAll() .InAssemblyOf() .WithName("First*") .AsSelf() diff --git a/integ-tests/SutProject.Tests/ConventionTests/ConventionEdgeTests.cs b/integ-tests/SutProject.Tests/ConventionTests/ConventionEdgeTests.cs index 453b3ce..f421c4c 100644 --- a/integ-tests/SutProject.Tests/ConventionTests/ConventionEdgeTests.cs +++ b/integ-tests/SutProject.Tests/ConventionTests/ConventionEdgeTests.cs @@ -9,9 +9,10 @@ namespace SutProject.Tests.ConventionTests; /// /// The corners of convention registration. /// -public class ConventionEdgeTests { - - private static ServiceProvider Provider(params IDependencyModule[] modules) { +public class ConventionEdgeTests +{ + private static ServiceProvider Provider(params IDependencyModule[] modules) + { var collection = new ServiceCollection(); collection.AddModules(modules); @@ -24,7 +25,8 @@ private static ServiceProvider Provider(params IDependencyModule[] modules) { /// decorator or the service comes from an assembly you do not control. /// [Fact] - public void ModuleLevelDecorateWrapsAConventionRegistration() { + public void ModuleLevelDecorateWrapsAConventionRegistration() + { var provider = Provider(new ConventionModuleDecorateModule()); Assert.Equal("wrapped(core)", provider.GetRequiredService().Describe()); @@ -36,7 +38,8 @@ public void ModuleLevelDecorateWrapsAConventionRegistration() { /// other. /// [Fact] - public void TwoModulesScanningOneInterfaceBothContribute() { + public void TwoModulesScanningOneInterfaceBothContribute() + { var names = Provider(new ConventionSharedFirstModule(), new ConventionSharedSecondModule()) .GetServices() .Select(shared => shared.Name) @@ -51,7 +54,8 @@ public void TwoModulesScanningOneInterfaceBothContribute() { /// "MyApp.Order.Handlers". /// [Fact] - public void InNamespaceOfReachesNestedNamespaces() { + public void InNamespaceOfReachesNestedNamespaces() + { var names = Provider(new ConventionPrefixNamespaceModule()) .GetServices() .Select(scanned => scanned.Name) @@ -63,7 +67,8 @@ public void InNamespaceOfReachesNestedNamespaces() { /// And InExactNamespaces is how you say you meant only that one. [Fact] - public void InExactNamespacesExcludesNestedNamespaces() { + public void InExactNamespacesExcludesNestedNamespaces() + { var names = Provider(new ConventionExactNamespaceModule()) .GetServices() .Select(scanned => scanned.Name) @@ -73,7 +78,8 @@ public void InExactNamespacesExcludesNestedNamespaces() { } [Fact] - public void NotInNamespaceOfExcludesThatNamespace() { + public void NotInNamespaceOfExcludesThatNamespace() + { var names = Provider(new ConventionExcludedNamespaceModule()) .GetServices() .Select(scanned => scanned.Name) @@ -87,7 +93,8 @@ public void NotInNamespaceOfExcludesThatNamespace() { /// closes it per request. /// [Fact] - public void AnOpenGenericRegistrationResolvesAtEveryClosing() { + public void AnOpenGenericRegistrationResolvesAtEveryClosing() + { var provider = Provider(new ConventionOpenGenericModule()); Assert.Equal("cache:String", provider.GetRequiredService>().Describe()); @@ -95,7 +102,8 @@ public void AnOpenGenericRegistrationResolvesAtEveryClosing() { } [Fact] - public void ADeclaredScopedLifetimeActuallyScopes() { + public void ADeclaredScopedLifetimeActuallyScopes() + { var provider = Provider(new ConventionLifetimeModule()); using var first = provider.CreateScope(); @@ -111,7 +119,8 @@ public void ADeclaredScopedLifetimeActuallyScopes() { /// The container owns what it constructed, however the registration was declared. /// [Fact] - public void AConventionRegisteredSingletonIsDisposedWithTheProvider() { + public void AConventionRegisteredSingletonIsDisposedWithTheProvider() + { var provider = Provider(new ConventionLifetimeModule()); var disposable = provider.GetRequiredService(); @@ -128,7 +137,8 @@ public void AConventionRegisteredSingletonIsDisposedWithTheProvider() { /// nothing can report, since it cannot see what it cannot see. /// [Fact] - public void AnInternalImplementationIsACandidateInThisCompilation() { + public void AnInternalImplementationIsACandidateInThisCompilation() + { var provider = Provider(new ConventionLifetimeModule()); Assert.Equal("internal", provider.GetRequiredService().Name); @@ -139,7 +149,8 @@ public void AnInternalImplementationIsACandidateInThisCompilation() { /// container, so a decorator's own dependencies can be convention-registered too. /// [Fact] - public void ADecoratorResolvesItsOwnConventionRegisteredDependencies() { + public void ADecoratorResolvesItsOwnConventionRegisteredDependencies() + { var provider = Provider(new ConventionDecoratorDependencyModule()); Assert.Equal("dep(core)", provider.GetRequiredService().Describe()); @@ -149,7 +160,8 @@ public void ADecoratorResolvesItsOwnConventionRegisteredDependencies() { /// Filters and shapes apply to a metadata scan the same way they apply to local types. /// [Fact] - public void AFilteredMetadataScanRegistersTheConcreteType() { + public void AFilteredMetadataScanRegistersTheConcreteType() + { var provider = Provider(new ConventionFilteredScanModule()); Assert.Equal("first", provider.GetRequiredService().Name); diff --git a/integ-tests/SutProject.Tests/ConventionTests/ConventionFeatureModules.cs b/integ-tests/SutProject.Tests/ConventionTests/ConventionFeatureModules.cs index 1146c6a..7eac1c4 100644 --- a/integ-tests/SutProject.Tests/ConventionTests/ConventionFeatureModules.cs +++ b/integ-tests/SutProject.Tests/ConventionTests/ConventionFeatureModules.cs @@ -1,5 +1,5 @@ -using DependencyModules.Runtime.Conventions; using DependencyModules.Runtime.Attributes; +using DependencyModules.Runtime.Conventions; using SecondarySutProject; namespace SutProject.Tests.ConventionTests; @@ -14,17 +14,20 @@ namespace SutProject.Tests.ConventionTests; // Shape: AsSelf, AsSelfWithInterfaces, AlsoAsSelf. // --------------------------------------------------------------------------- -public interface IShapeService { +public interface IShapeService +{ string Name { get; } } public interface IAlsoShaped { } -public class SelfShaped : IShapeService { +public class SelfShaped : IShapeService +{ public string Name => "self"; } -public class CrossWiredShape : IShapeService, IAlsoShaped, IDisposable { +public class CrossWiredShape : IShapeService, IAlsoShaped, IDisposable +{ public string Name => "crosswired"; public void Dispose() { } @@ -33,14 +36,17 @@ public void Dispose() { } /// The only implementor of its interface, so AlsoAsSelf has one match to resolve. public interface IAlsoSelfService { } -public class AlsoSelfShape : IShapeService, IAlsoSelfService { +public class AlsoSelfShape : IShapeService, IAlsoSelfService +{ public string Name => "alsoself"; } /// Registers the concrete type instead of the interface. [DependencyModule] -public partial class ConventionAsSelfModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionAsSelfModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSelf().AsSingleton(); } } @@ -50,16 +56,20 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// which is reachable but is never what "as its interfaces" means. /// [DependencyModule] -public partial class ConventionCrossWireModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionCrossWireModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSelfWithInterfaces().AsSingleton(); } } /// Registers the matched interface and the concrete type, sharing one instance. [DependencyModule] -public partial class ConventionAlsoAsSelfModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionAlsoAsSelfModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AlsoAsSelf().AsSingleton(); } } @@ -72,44 +82,55 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { [AttributeUsage(AttributeTargets.Class)] public class PolicyAttribute : Attribute { } -public interface IFiltered { +public interface IFiltered +{ string Name { get; } } [Policy] -public class MarkedRepository : IFiltered { +public class MarkedRepository : IFiltered +{ public string Name => "marked"; } -public class UnmarkedRepository : IFiltered { +public class UnmarkedRepository : IFiltered +{ public string Name => "unmarked"; } -public class MarkedHelper : IFiltered { +public class MarkedHelper : IFiltered +{ public string Name => "helper"; } /// Only the type carrying the attribute. [DependencyModule] -public partial class ConventionAttributeFilterModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionAttributeFilterModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().WithAttribute().AsSingleton(); } } /// Only the names matching the glob. [DependencyModule] -public partial class ConventionNameFilterModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionNameFilterModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().WithName("*Repository").AsSingleton(); } } /// Selected by namespace alone, with no interface to match on. [DependencyModule] -public partial class ConventionNamespaceModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll() +public partial class ConventionNamespaceModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { + conventions + .RegisterAll() .InNamespaceOf() .WithName("NamespaceOnly*") .AsSelf() @@ -139,16 +160,20 @@ public class ExplicitlyRegistered : IExplicitSource, IExplicitTarget { } /// Registers each match as the interface named after it. [DependencyModule] -public partial class ConventionMatchingInterfaceModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionMatchingInterfaceModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsMatchingInterface().AsSingleton(); } } /// Registers every match as one named service type. [DependencyModule] -public partial class ConventionAsModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionAsModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().As().AsSingleton(); } } @@ -157,38 +182,47 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // Registration type and service key. // --------------------------------------------------------------------------- -public interface IKeyedService { +public interface IKeyedService +{ string Name { get; } } -public class KeyedOne : IKeyedService { +public class KeyedOne : IKeyedService +{ public string Name => "keyed-one"; } /// Registered under a service key. [DependencyModule] -public partial class ConventionKeyModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionKeyModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().WithKey("primary").AsSingleton(); } } -public interface ITriedService { +public interface ITriedService +{ string Name { get; } } -public class TriedOne : ITriedService { +public class TriedOne : ITriedService +{ public string Name => "one"; } -public class TriedTwo : ITriedService { +public class TriedTwo : ITriedService +{ public string Name => "two"; } /// Try registers the service type once and skips the second match. [DependencyModule] -public partial class ConventionUsingModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionUsingModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().Using(RegistrationType.Try).AsSingleton(); } } @@ -204,8 +238,10 @@ public interface ISecondRole { } public class TwoRoles : IFirstRole, ISecondRole { } [DependencyModule] -public partial class ConventionTwoRolesModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionTwoRolesModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton(); conventions.RegisterAll().AsScoped(); } @@ -220,8 +256,10 @@ public class OrderShipped { } public class OrderEvents : INotification, INotification { } [DependencyModule] -public partial class ConventionClosingsModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionClosingsModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll(typeof(INotification<>)).AsTransient(); } } @@ -230,7 +268,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // Conventions and decorators together — the MediatR shape. // --------------------------------------------------------------------------- -public interface IRequestHandler { +public interface IRequestHandler +{ TResponse Handle(TRequest request); } @@ -238,21 +277,25 @@ public class CreateThing { } public class RenameThing { } -public class ThingResult { +public class ThingResult +{ public string Value { get; set; } = ""; } -public class CreateThingHandler : IRequestHandler { +public class CreateThingHandler : IRequestHandler +{ public ThingResult Handle(CreateThing request) => new() { Value = "created" }; } -public class RenameThingHandler : IRequestHandler { +public class RenameThingHandler : IRequestHandler +{ public ThingResult Handle(RenameThing request) => new() { Value = "renamed" }; } /// Records what the decorator saw, so a test can prove it ran. [SingletonService] -public class HandlerLog { +public class HandlerLog +{ public List Lines { get; } = new(); } @@ -262,10 +305,12 @@ public class HandlerLog { /// [Decorator] public class LoggingRequestHandler( - IRequestHandler inner, HandlerLog log) - : IRequestHandler { - - public TResponse Handle(TRequest request) { + IRequestHandler inner, + HandlerLog log +) : IRequestHandler +{ + public TResponse Handle(TRequest request) + { log.Lines.Add("handling " + typeof(TRequest).Name); return inner.Handle(request); @@ -273,8 +318,10 @@ public TResponse Handle(TRequest request) { } [DependencyModule] -public partial class ConventionDecoratedHandlerModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionDecoratedHandlerModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll(typeof(IRequestHandler<,>)).AsScoped(); } } @@ -284,10 +331,10 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // --------------------------------------------------------------------------- [DependencyModule] -public partial class ConventionAssemblyScanModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll() - .InAssemblyOf() - .AsSingleton(); +public partial class ConventionAssemblyScanModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { + conventions.RegisterAll().InAssemblyOf().AsSingleton(); } } diff --git a/integ-tests/SutProject.Tests/ConventionTests/ConventionFeatureTests.cs b/integ-tests/SutProject.Tests/ConventionTests/ConventionFeatureTests.cs index d739bc4..bfae912 100644 --- a/integ-tests/SutProject.Tests/ConventionTests/ConventionFeatureTests.cs +++ b/integ-tests/SutProject.Tests/ConventionTests/ConventionFeatureTests.cs @@ -14,9 +14,12 @@ namespace SutProject.Tests.ConventionTests; /// colliding, or — for InAssemblyOf — that a genuine compile-time assembly reference is what /// gets scanned. That is what this file is for. /// -public class ConventionFeatureTests { - - private static ServiceProvider Provider(params DependencyModules.Runtime.Interfaces.IDependencyModule[] modules) { +public class ConventionFeatureTests +{ + private static ServiceProvider Provider( + params DependencyModules.Runtime.Interfaces.IDependencyModule[] modules + ) + { var collection = new ServiceCollection(); collection.AddModules(modules); @@ -25,8 +28,9 @@ private static ServiceProvider Provider(params DependencyModules.Runtime.Interfa } private static IServiceCollection Collection( - params DependencyModules.Runtime.Interfaces.IDependencyModule[] modules) { - + params DependencyModules.Runtime.Interfaces.IDependencyModule[] modules + ) + { var collection = new ServiceCollection(); collection.AddModules(modules); @@ -35,7 +39,8 @@ private static IServiceCollection Collection( } [Fact] - public void AsSelfRegistersTheConcreteType() { + public void AsSelfRegistersTheConcreteType() + { var provider = Provider(new ConventionAsSelfModule()); Assert.NotNull(provider.GetService()); @@ -43,7 +48,8 @@ public void AsSelfRegistersTheConcreteType() { } [Fact] - public void AsSelfWithInterfacesSharesOneInstanceAndSkipsSystemInterfaces() { + public void AsSelfWithInterfacesSharesOneInstanceAndSkipsSystemInterfaces() + { var provider = Provider(new ConventionCrossWireModule()); var asConcrete = provider.GetRequiredService(); @@ -56,7 +62,8 @@ public void AsSelfWithInterfacesSharesOneInstanceAndSkipsSystemInterfaces() { } [Fact] - public void AlsoAsSelfRegistersBothAndSharesOneInstance() { + public void AlsoAsSelfRegistersBothAndSharesOneInstance() + { var provider = Provider(new ConventionAlsoAsSelfModule()); var asInterface = provider.GetRequiredService(); @@ -69,7 +76,8 @@ public void AlsoAsSelfRegistersBothAndSharesOneInstance() { } [Fact] - public void WithAttributeSelectsOnlyMarkedTypes() { + public void WithAttributeSelectsOnlyMarkedTypes() + { var services = Provider(new ConventionAttributeFilterModule()) .GetServices() .Select(service => service.Name) @@ -79,7 +87,8 @@ public void WithAttributeSelectsOnlyMarkedTypes() { } [Fact] - public void WithNameSelectsOnTheGlob() { + public void WithNameSelectsOnTheGlob() + { var services = Provider(new ConventionNameFilterModule()) .GetServices() .Select(service => service.Name) @@ -93,7 +102,8 @@ public void WithNameSelectsOnTheGlob() { /// A concrete type with no interface, selected by namespace and name alone. /// [Fact] - public void RegisterAllWithFiltersRegistersTypesThatImplementNothing() { + public void RegisterAllWithFiltersRegistersTypesThatImplementNothing() + { var provider = Provider(new ConventionNamespaceModule()); Assert.NotNull(provider.GetService()); @@ -101,7 +111,8 @@ public void RegisterAllWithFiltersRegistersTypesThatImplementNothing() { } [Fact] - public void AsMatchingInterfaceRegistersRenamerAsIRenamer() { + public void AsMatchingInterfaceRegistersRenamerAsIRenamer() + { var provider = Provider(new ConventionMatchingInterfaceModule()); Assert.IsType(provider.GetRequiredService()); @@ -109,7 +120,8 @@ public void AsMatchingInterfaceRegistersRenamerAsIRenamer() { } [Fact] - public void AsRegistersUnderTheNamedServiceType() { + public void AsRegistersUnderTheNamedServiceType() + { var provider = Provider(new ConventionAsModule()); Assert.IsType(provider.GetRequiredService()); @@ -117,7 +129,8 @@ public void AsRegistersUnderTheNamedServiceType() { } [Fact] - public void WithKeyRegistersUnderAServiceKey() { + public void WithKeyRegistersUnderAServiceKey() + { var provider = Provider(new ConventionKeyModule()); Assert.IsType(provider.GetRequiredKeyedService("primary")); @@ -125,26 +138,31 @@ public void WithKeyRegistersUnderAServiceKey() { } [Fact] - public void UsingTryRegistersTheServiceTypeOnce() { + public void UsingTryRegistersTheServiceTypeOnce() + { Assert.Single( Collection(new ConventionUsingModule()), - descriptor => descriptor.ServiceType == typeof(ITriedService)); + descriptor => descriptor.ServiceType == typeof(ITriedService) + ); } /// /// A type filling two roles registers as both, each with its own lifetime. /// [Fact] - public void ATypeMatchedThroughTwoInterfacesRegistersAsBoth() { + public void ATypeMatchedThroughTwoInterfacesRegistersAsBoth() + { var collection = Collection(new ConventionTwoRolesModule()); Assert.Equal( ServiceLifetime.Singleton, - Assert.Single(collection, d => d.ServiceType == typeof(IFirstRole)).Lifetime); + Assert.Single(collection, d => d.ServiceType == typeof(IFirstRole)).Lifetime + ); Assert.Equal( ServiceLifetime.Scoped, - Assert.Single(collection, d => d.ServiceType == typeof(ISecondRole)).Lifetime); + Assert.Single(collection, d => d.ServiceType == typeof(ISecondRole)).Lifetime + ); } /// @@ -152,7 +170,8 @@ public void ATypeMatchedThroughTwoInterfacesRegistersAsBoth() { /// left the second silently unregistered. /// [Fact] - public void OneConventionRegistersEveryClosing() { + public void OneConventionRegistersEveryClosing() + { var provider = Provider(new ConventionClosingsModule()); Assert.IsType(provider.GetRequiredService>()); @@ -163,7 +182,8 @@ public void OneConventionRegistersEveryClosing() { /// One open generic decorator over every handler a convention registered — the MediatR shape. /// [Fact] - public void ADecoratorWrapsEveryConventionRegisteredHandler() { + public void ADecoratorWrapsEveryConventionRegisteredHandler() + { var provider = Provider(new ConventionDecoratedHandlerModule()); var log = provider.GetRequiredService(); @@ -185,23 +205,30 @@ public void ADecoratorWrapsEveryConventionRegisteredHandler() { /// nothing, it would register as the open generic and make the whole module undecoratable. /// [Fact] - public void ADecoratorIsNotRegisteredAsAService() { + public void ADecoratorIsNotRegisteredAsAService() + { var collection = Collection(new ConventionDecoratedHandlerModule()); - Assert.DoesNotContain(collection, descriptor => descriptor.ServiceType.IsGenericTypeDefinition); + Assert.DoesNotContain( + collection, + descriptor => descriptor.ServiceType.IsGenericTypeDefinition + ); Assert.Equal( 2, collection.Count(descriptor => - descriptor.ServiceType.IsGenericType && - descriptor.ServiceType.GetGenericTypeDefinition() == typeof(IRequestHandler<,>))); + descriptor.ServiceType.IsGenericType + && descriptor.ServiceType.GetGenericTypeDefinition() == typeof(IRequestHandler<,>) + ) + ); } /// /// Scanning a genuinely referenced assembly, where there is no syntax to read. /// [Fact] - public void InAssemblyOfRegistersPublicTypesFromTheReferencedAssembly() { + public void InAssemblyOfRegistersPublicTypesFromTheReferencedAssembly() + { var policies = Provider(new ConventionAssemblyScanModule()) .GetServices() .Select(policy => policy.Name) diff --git a/integ-tests/SutProject.Tests/ConventionTests/ConventionInteractionModules.cs b/integ-tests/SutProject.Tests/ConventionTests/ConventionInteractionModules.cs index 56374ff..9591563 100644 --- a/integ-tests/SutProject.Tests/ConventionTests/ConventionInteractionModules.cs +++ b/integ-tests/SutProject.Tests/ConventionTests/ConventionInteractionModules.cs @@ -1,5 +1,5 @@ -using DependencyModules.Runtime.Conventions; using DependencyModules.Runtime.Attributes; +using DependencyModules.Runtime.Conventions; using DependencyModules.Runtime.Interception; namespace SutProject.Tests.ConventionTests; @@ -18,31 +18,38 @@ namespace SutProject.Tests.ConventionTests; /// Records what the interceptor saw. [SingletonService] -public class InterceptLog { +public class InterceptLog +{ public List Lines { get; } = new(); } [SingletonService] -public class RecordingInterceptor(InterceptLog log) : IInterceptor { - public TResult Intercept(InvocationContext context) { +public class RecordingInterceptor(InterceptLog log) : IInterceptor +{ + public TResult Intercept(InvocationContext context) + { log.Lines.Add("intercepted " + context.Caller.MemberName); return context.Proceed(); } } -public interface IInterceptedByConvention { +public interface IInterceptedByConvention +{ string Work(); } [Intercept(typeof(RecordingInterceptor))] -public class InterceptedByConvention : IInterceptedByConvention { +public class InterceptedByConvention : IInterceptedByConvention +{ public string Work() => "worked"; } [DependencyModule] -public partial class ConventionInterceptModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionInterceptModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton(); } } @@ -51,27 +58,33 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // Two decorators with an order, over convention-registered services. // --------------------------------------------------------------------------- -public interface IOrdered { +public interface IOrdered +{ string Describe(); } -public class OrderedCore : IOrdered { +public class OrderedCore : IOrdered +{ public string Describe() => "core"; } [Decorator(Order = 10)] -public class InnerOrdered(IOrdered inner) : IOrdered { +public class InnerOrdered(IOrdered inner) : IOrdered +{ public string Describe() => $"inner({inner.Describe()})"; } [Decorator(Order = 20)] -public class OuterOrdered(IOrdered inner) : IOrdered { +public class OuterOrdered(IOrdered inner) : IOrdered +{ public string Describe() => $"outer({inner.Describe()})"; } [DependencyModule] -public partial class ConventionOrderedDecoratorModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionOrderedDecoratorModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton(); } } @@ -80,22 +93,27 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // A keyed convention registration, decorated. // --------------------------------------------------------------------------- -public interface IKeyedAndDecorated { +public interface IKeyedAndDecorated +{ string Describe(); } -public class KeyedCore : IKeyedAndDecorated { +public class KeyedCore : IKeyedAndDecorated +{ public string Describe() => "core"; } [Decorator] -public class KeyedWrapper(IKeyedAndDecorated inner) : IKeyedAndDecorated { +public class KeyedWrapper(IKeyedAndDecorated inner) : IKeyedAndDecorated +{ public string Describe() => $"wrapped({inner.Describe()})"; } [DependencyModule] -public partial class ConventionKeyedDecoratedModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionKeyedDecoratedModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().WithKey("main").AsSingleton(); } } @@ -104,38 +122,47 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // Type shapes that are not plain classes: records, nested types, primary constructors. // --------------------------------------------------------------------------- -public interface IShaped { +public interface IShaped +{ string Name { get; } } -public record ShapedRecord : IShaped { +public record ShapedRecord : IShaped +{ public string Name => "record"; } public record struct NotACandidate; -public class Outer { - public class NestedShaped : IShaped { +public class Outer +{ + public class NestedShaped : IShaped + { public string Name => "nested"; } } -public interface IShapedDependency { +public interface IShapedDependency +{ string Value { get; } } -public class ShapedDependency : IShapedDependency { +public class ShapedDependency : IShapedDependency +{ public string Value => "dep"; } /// Primary constructor, injected from another convention registration. -public class PrimaryConstructorShaped(IShapedDependency dependency) : IShaped { +public class PrimaryConstructorShaped(IShapedDependency dependency) : IShaped +{ public string Name => "primary-" + dependency.Value; } [DependencyModule] -public partial class ConventionShapesModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionShapesModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton(); conventions.RegisterAll().AsSingleton(); } @@ -145,17 +172,21 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // A realm module. OnlyRealm means it takes nothing that did not name it. // --------------------------------------------------------------------------- -public interface IRealmScoped { +public interface IRealmScoped +{ string Name { get; } } -public class RealmScoped : IRealmScoped { +public class RealmScoped : IRealmScoped +{ public string Name => "realm"; } [DependencyModule(OnlyRealm = true)] -public partial class ConventionRealmModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionRealmModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton(); } } @@ -164,17 +195,21 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // A module composed from another. The conventions of a dependency come along with it. // --------------------------------------------------------------------------- -public interface IComposedService { +public interface IComposedService +{ string Name { get; } } -public class ComposedService : IComposedService { +public class ComposedService : IComposedService +{ public string Name => "composed"; } [DependencyModule] -public partial class ConventionDependencyModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionDependencyModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton(); } } @@ -188,22 +223,27 @@ public partial class ConventionCompositionModule; // Environment conditions on convention candidates. // --------------------------------------------------------------------------- -public interface IConditionalByConvention { +public interface IConditionalByConvention +{ string Name { get; } } -public class AlwaysConditional : IConditionalByConvention { +public class AlwaysConditional : IConditionalByConvention +{ public string Name => "always"; } [IfEnvironment("Development")] -public class DevelopmentOnlyConditional : IConditionalByConvention { +public class DevelopmentOnlyConditional : IConditionalByConvention +{ public string Name => "development"; } [DependencyModule] -public partial class ConventionConditionalModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionConditionalModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton(); } } diff --git a/integ-tests/SutProject.Tests/ConventionTests/ConventionInteractionTests.cs b/integ-tests/SutProject.Tests/ConventionTests/ConventionInteractionTests.cs index 7ef4612..6b3987f 100644 --- a/integ-tests/SutProject.Tests/ConventionTests/ConventionInteractionTests.cs +++ b/integ-tests/SutProject.Tests/ConventionTests/ConventionInteractionTests.cs @@ -13,11 +13,13 @@ namespace SutProject.Tests.ConventionTests; /// decoration do not know a service was registered by convention, conventions do not know a /// candidate is intercepted, and the type shapes people write are not all plain classes. /// -public class ConventionInteractionTests { - +public class ConventionInteractionTests +{ private static ServiceProvider Provider( - IModuleEnvironment? environment, params IDependencyModule[] modules) { - + IModuleEnvironment? environment, + params IDependencyModule[] modules + ) + { var collection = new ServiceCollection(); collection.AddModules(environment, modules); @@ -34,7 +36,8 @@ private static ServiceProvider Provider(params IDependencyModule[] modules) => /// ends up registered. /// [Fact] - public void AConventionRegisteredServiceIsIntercepted() { + public void AConventionRegisteredServiceIsIntercepted() + { var provider = Provider(new ConventionInterceptModule()); var service = provider.GetRequiredService(); @@ -48,7 +51,8 @@ public void AConventionRegisteredServiceIsIntercepted() { /// Two decorators nest by declared order, lower closest to the implementation. /// [Fact] - public void DecoratorsNestByOrderOverAConventionRegistration() { + public void DecoratorsNestByOrderOverAConventionRegistration() + { var provider = Provider(new ConventionOrderedDecoratorModule()); Assert.Equal("outer(inner(core))", provider.GetRequiredService().Describe()); @@ -58,12 +62,14 @@ public void DecoratorsNestByOrderOverAConventionRegistration() { /// Decoration rewrites a keyed registration in place, keeping the key. /// [Fact] - public void AKeyedConventionRegistrationIsDecorated() { + public void AKeyedConventionRegistrationIsDecorated() + { var provider = Provider(new ConventionKeyedDecoratedModule()); Assert.Equal( "wrapped(core)", - provider.GetRequiredKeyedService("main").Describe()); + provider.GetRequiredKeyedService("main").Describe() + ); } /// @@ -71,7 +77,8 @@ public void AKeyedConventionRegistrationIsDecorated() { /// convention registration can be injected into another. /// [Fact] - public void RecordsNestedTypesAndPrimaryConstructorsAreCandidates() { + public void RecordsNestedTypesAndPrimaryConstructorsAreCandidates() + { var names = Provider(new ConventionShapesModule()) .GetServices() .Select(shaped => shaped.Name) @@ -85,7 +92,8 @@ public void RecordsNestedTypesAndPrimaryConstructorsAreCandidates() { /// An OnlyRealm module takes its own convention registrations, which name it as their realm. /// [Fact] - public void ARealmModuleTakesItsOwnConventionRegistrations() { + public void ARealmModuleTakesItsOwnConventionRegistrations() + { var provider = Provider(new ConventionRealmModule()); Assert.Equal("realm", provider.GetRequiredService().Name); @@ -95,7 +103,8 @@ public void ARealmModuleTakesItsOwnConventionRegistrations() { /// Composing a module brings its conventions with it, the same as its attribute registrations. /// [Fact] - public void ComposingAModuleBringsItsConventions() { + public void ComposingAModuleBringsItsConventions() + { var provider = Provider(new ConventionCompositionModule()); Assert.Equal("composed", provider.GetRequiredService().Name); @@ -110,9 +119,14 @@ public void ComposingAModuleBringsItsConventions() { [InlineData("Development", new[] { "always", "development" })] [InlineData("Production", new[] { "always" })] public void EnvironmentConditionsApplyToConventionCandidates( - string environmentName, string[] expected) { - - var names = Provider(new ModuleEnvironment(environmentName), new ConventionConditionalModule()) + string environmentName, + string[] expected + ) + { + var names = Provider( + new ModuleEnvironment(environmentName), + new ConventionConditionalModule() + ) .GetServices() .Select(service => service.Name) .OrderBy(name => name) @@ -126,7 +140,8 @@ public void EnvironmentConditionsApplyToConventionCandidates( /// declaring module, so two modules scanning the same interface do not leak into each other. /// [Fact] - public void ConventionRegistrationsDoNotLeakBetweenModules() { + public void ConventionRegistrationsDoNotLeakBetweenModules() + { var onlyShapes = Provider(new ConventionShapesModule()); Assert.Empty(onlyShapes.GetServices()); diff --git a/integ-tests/SutProject.Tests/ConventionTests/ConventionModules.cs b/integ-tests/SutProject.Tests/ConventionTests/ConventionModules.cs index 5285351..b570e0c 100644 --- a/integ-tests/SutProject.Tests/ConventionTests/ConventionModules.cs +++ b/integ-tests/SutProject.Tests/ConventionTests/ConventionModules.cs @@ -1,5 +1,5 @@ -using DependencyModules.Runtime.Conventions; using DependencyModules.Runtime.Attributes; +using DependencyModules.Runtime.Conventions; namespace SutProject.Tests.ConventionTests; @@ -10,18 +10,21 @@ namespace SutProject.Tests.ConventionTests; // Direct declaration, and reach through interface inheritance. // --------------------------------------------------------------------------- -public interface IConventionService { +public interface IConventionService +{ string Name { get; } } /// Extends the scanned interface, so implementing it is a declared match. public interface IAuditedConventionService : IConventionService { } -public class DirectService : IConventionService { +public class DirectService : IConventionService +{ public string Name => "direct"; } -public class InheritedService : IAuditedConventionService { +public class InheritedService : IAuditedConventionService +{ public string Name => "inherited"; } @@ -31,19 +34,23 @@ public class InheritedService : IAuditedConventionService { // through a base class and matches only with IncludeBaseClasses(). // --------------------------------------------------------------------------- -public interface IBaseClassReachService { +public interface IBaseClassReachService +{ string Name { get; } } -public class DirectReachService : IBaseClassReachService { +public class DirectReachService : IBaseClassReachService +{ public string Name => "direct-reach"; } -public abstract class ReachServiceBase : IBaseClassReachService { +public abstract class ReachServiceBase : IBaseClassReachService +{ public abstract string Name { get; } } -public class ThroughBaseClass : ReachServiceBase { +public class ThroughBaseClass : ReachServiceBase +{ public override string Name => "through-base"; } @@ -51,7 +58,8 @@ public class ThroughBaseClass : ReachServiceBase { // Open generic scanned against concrete closings. // --------------------------------------------------------------------------- -public interface IConventionHandler { +public interface IConventionHandler +{ TOut Handle(TIn input); } @@ -59,15 +67,18 @@ public class CreateOrder { } public class RenameOrder { } -public class OrderId { +public class OrderId +{ public int Value { get; set; } } -public class CreateOrderHandler : IConventionHandler { +public class CreateOrderHandler : IConventionHandler +{ public OrderId Handle(CreateOrder input) => new() { Value = 1 }; } -public class RenameOrderHandler : IConventionHandler { +public class RenameOrderHandler : IConventionHandler +{ public OrderId Handle(RenameOrder input) => new() { Value = 2 }; } @@ -75,11 +86,13 @@ public class RenameOrderHandler : IConventionHandler { // A generic implementation passing its own parameter through, which registers open. // --------------------------------------------------------------------------- -public interface IConventionCache { +public interface IConventionCache +{ string Describe(); } -public class PassThroughCache : IConventionCache { +public class PassThroughCache : IConventionCache +{ public string Describe() => "open:" + typeof(T).Name; } @@ -88,13 +101,15 @@ public class PassThroughCache : IConventionCache { // closed construction the type actually implements. // --------------------------------------------------------------------------- -public interface IConventionStore { +public interface IConventionStore +{ string Describe(); } public interface IAuditedStore : IConventionStore { } -public class StringStore : IAuditedStore { +public class StringStore : IAuditedStore +{ public string Describe() => "audited:string"; } @@ -102,16 +117,19 @@ public class StringStore : IAuditedStore { // An explicit attribute always wins; the convention picks up only the unattributed type. // --------------------------------------------------------------------------- -public interface IAttributeWinsService { +public interface IAttributeWinsService +{ string Name { get; } } [SingletonService] -public class AttributedService : IAttributeWinsService { +public class AttributedService : IAttributeWinsService +{ public string Name => "attributed"; } -public class ByConventionService : IAttributeWinsService { +public class ByConventionService : IAttributeWinsService +{ public string Name => "by-convention"; } @@ -124,8 +142,10 @@ public class ByConventionService : IAttributeWinsService { /// extends it. /// [DependencyModule] -public partial class ConventionSutModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionSutModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton(); conventions.RegisterAll(typeof(IConventionHandler<,>)).AsTransient(); conventions.RegisterAll(typeof(IConventionCache<>)).AsScoped(); @@ -136,16 +156,20 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// Base-class hop off, which is the default. Only the direct implementation matches. [DependencyModule] -public partial class ConventionNoBaseClassModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionNoBaseClassModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton(); } } /// The same scan with the base-class hop opted in. [DependencyModule] -public partial class ConventionBaseClassModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class ConventionBaseClassModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsSingleton().IncludeBaseClasses(); } } diff --git a/integ-tests/SutProject.Tests/ConventionTests/ConventionRegistrationTests.cs b/integ-tests/SutProject.Tests/ConventionTests/ConventionRegistrationTests.cs index 3c7c2ab..a51f364 100644 --- a/integ-tests/SutProject.Tests/ConventionTests/ConventionRegistrationTests.cs +++ b/integ-tests/SutProject.Tests/ConventionTests/ConventionRegistrationTests.cs @@ -15,28 +15,34 @@ namespace SutProject.Tests.ConventionTests; /// colliding. That is what this file is for. /// [ConventionSutModule] -public class ConventionRegistrationTests { - +public class ConventionRegistrationTests +{ [ModuleTest] - public void RegistersTypeDeclaringTheInterfaceDirectly(IEnumerable services) { + public void RegistersTypeDeclaringTheInterfaceDirectly(IEnumerable services) + { Assert.Contains(services, service => service.Name == "direct"); } [ModuleTest] public void RegistersTypeReachingTheInterfaceThroughInterfaceInheritance( - IEnumerable services) { + IEnumerable services + ) + { Assert.Contains(services, service => service.Name == "inherited"); } [ModuleTest] - public void RegistersExactlyTheTwoMatchingTypes(IEnumerable services) { + public void RegistersExactlyTheTwoMatchingTypes(IEnumerable services) + { Assert.Equal( new[] { "direct", "inherited" }, - services.Select(service => service.Name).OrderBy(name => name).ToArray()); + services.Select(service => service.Name).OrderBy(name => name).ToArray() + ); } [ModuleTest] - public void RegistersTheDeclaredLifetime(IConventionService first, IConventionService second) { + public void RegistersTheDeclaredLifetime(IConventionService first, IConventionService second) + { // Declared AsSingleton, so one instance serves both parameters. Assert.Same(first, second); } @@ -52,8 +58,9 @@ public void RegistersTheDeclaredLifetime(IConventionService first, IConventionSe [ModuleTest] public void ClosesAnOpenGenericAgainstEachImplementation( IConventionHandler create, - IConventionHandler rename) { - + IConventionHandler rename + ) + { Assert.IsType(create); Assert.IsType(rename); @@ -67,8 +74,10 @@ public void ClosesAnOpenGenericAgainstEachImplementation( /// [ModuleTest] public void RegistersAGenericImplementationAsAnOpenGeneric( - IConventionCache ints, IConventionCache strings) { - + IConventionCache ints, + IConventionCache strings + ) + { Assert.IsType>(ints); Assert.IsType>(strings); @@ -81,14 +90,18 @@ public void RegistersAGenericImplementationAsAnOpenGeneric( /// construction: StringStore declares IAuditedStore<string>, never IConventionStore<string>. /// [ModuleTest] - public void ClosesAnOpenGenericReachedThroughInterfaceInheritance(IConventionStore store) { + public void ClosesAnOpenGenericReachedThroughInterfaceInheritance( + IConventionStore store + ) + { Assert.IsType(store); Assert.Equal("audited:string", store.Describe()); } /// An open generic convention registers nothing for a construction nobody implements. [ModuleTest] - public void DoesNotRegisterAnUnimplementedConstruction(IServiceProvider provider) { + public void DoesNotRegisterAnUnimplementedConstruction(IServiceProvider provider) + { Assert.Null(provider.GetService>()); Assert.Null(provider.GetService>()); } @@ -99,8 +112,9 @@ public void DoesNotRegisterAnUnimplementedConstruction(IServiceProvider provider [ModuleTest] public void AnExplicitAttributeStillRegistersAlongsideTheConvention( - IEnumerable services) { - + IEnumerable services + ) + { var names = services.Select(service => service.Name).OrderBy(name => name).ToArray(); Assert.Equal(new[] { "attributed", "by-convention" }, names); @@ -108,8 +122,10 @@ public void AnExplicitAttributeStillRegistersAlongsideTheConvention( [ModuleTest] public void TheAttributedTypeKeepsItsOwnLifetime( - IEnumerable first, IEnumerable second) { - + IEnumerable first, + IEnumerable second + ) + { // The attribute declared Singleton and the convention declared Transient. The attributed // type is registered once, by its attribute, so it survives across resolutions while the // convention-registered one does not. @@ -128,21 +144,25 @@ public void TheAttributedTypeKeepsItsOwnLifetime( /// /// The base-class hop, proven from both sides against the same interface. /// -public class ConventionBaseClassReachTests { - +public class ConventionBaseClassReachTests +{ [ModuleTest] [ConventionNoBaseClassModule] - public void ABaseClassHopIsNotMatchedByDefault(IEnumerable services) { + public void ABaseClassHopIsNotMatchedByDefault(IEnumerable services) + { Assert.Equal( new[] { "direct-reach" }, - services.Select(service => service.Name).OrderBy(name => name).ToArray()); + services.Select(service => service.Name).OrderBy(name => name).ToArray() + ); } [ModuleTest] [ConventionBaseClassModule] - public void ABaseClassHopIsMatchedWhenOptedIn(IEnumerable services) { + public void ABaseClassHopIsMatchedWhenOptedIn(IEnumerable services) + { Assert.Equal( new[] { "direct-reach", "through-base" }, - services.Select(service => service.Name).OrderBy(name => name).ToArray()); + services.Select(service => service.Name).OrderBy(name => name).ToArray() + ); } } diff --git a/integ-tests/SutProject.Tests/ConventionTests/NestedNamespaceTypes.cs b/integ-tests/SutProject.Tests/ConventionTests/NestedNamespaceTypes.cs index 52c8fd5..d9a32f7 100644 --- a/integ-tests/SutProject.Tests/ConventionTests/NestedNamespaceTypes.cs +++ b/integ-tests/SutProject.Tests/ConventionTests/NestedNamespaceTypes.cs @@ -6,7 +6,8 @@ namespace SutProject.Tests.ConventionTests.Nested; /// Lives one namespace below the conventions, so a prefix filter reaches it and an exact one does /// not. That difference is the whole point of InExactNamespaces. /// -public class NestedScanned : INamespaceScanned { +public class NestedScanned : INamespaceScanned +{ /// public string Name => "nested"; } diff --git a/integ-tests/SutProject.Tests/CrossWire/CrossWireTests.cs b/integ-tests/SutProject.Tests/CrossWire/CrossWireTests.cs index 8cd3264..aefad6a 100644 --- a/integ-tests/SutProject.Tests/CrossWire/CrossWireTests.cs +++ b/integ-tests/SutProject.Tests/CrossWire/CrossWireTests.cs @@ -7,32 +7,20 @@ namespace SutProject.Tests.CrossWire; [DependencyModule(OnlyRealm = true)] -public partial class CrossWireModule { - -} +public partial class CrossWireModule { } [DependencyModule(OnlyRealm = true)] -public partial class CrossWireScopedModule { - -} - -public interface IInterface1 { - -} +public partial class CrossWireScopedModule { } -public interface IInterface2 { +public interface IInterface1 { } -} +public interface IInterface2 { } [CrossWireService(Realm = typeof(CrossWireModule))] -public class CrossWireService : IInterface1, IInterface2 { - -} +public class CrossWireService : IInterface1, IInterface2 { } [CrossWireService(Lifetime = ServiceLifetime.Scoped, Realm = typeof(CrossWireScopedModule))] -public class ScopedCrossWireService : IInterface1, IInterface2 { - -} +public class ScopedCrossWireService : IInterface1, IInterface2 { } /// /// The contract of [CrossWireService] is that one instance is reachable through the implementation @@ -40,11 +28,15 @@ public class ScopedCrossWireService : IInterface1, IInterface2 { /// would pass no matter what the generator emitted, so each test here compares the instances /// obtained through different service types. /// -public class CrossWireTests { - +public class CrossWireTests +{ [ModuleTest] [CrossWireModule] - public void CrossWire_SharesOneInstanceAcrossItsInterfaces(IInterface1 interface1, IInterface2 interface2) { + public void CrossWire_SharesOneInstanceAcrossItsInterfaces( + IInterface1 interface1, + IInterface2 interface2 + ) + { Assert.NotNull(interface1); Assert.NotNull(interface2); Assert.Same(interface1, interface2); @@ -53,26 +45,31 @@ public void CrossWire_SharesOneInstanceAcrossItsInterfaces(IInterface1 interface [ModuleTest] [CrossWireModule] public void CrossWire_ResolvesTheImplementationType( - IInterface1 interface1, CrossWireService implementation) { - + IInterface1 interface1, + CrossWireService implementation + ) + { Assert.NotNull(implementation); Assert.Same(interface1, implementation); } [ModuleTest] [CrossWireModule] - public void CrossWire_DefaultsToASingleInstanceAcrossScopes(IServiceProvider provider) { + public void CrossWire_DefaultsToASingleInstanceAcrossScopes(IServiceProvider provider) + { using var first = provider.CreateScope(); using var second = provider.CreateScope(); Assert.Same( first.ServiceProvider.GetService(), - second.ServiceProvider.GetService()); + second.ServiceProvider.GetService() + ); } [ModuleTest] [CrossWireScopedModule] - public void ScopedCrossWire_SharesOneInstanceWithinAScope(IServiceProvider provider) { + public void ScopedCrossWire_SharesOneInstanceWithinAScope(IServiceProvider provider) + { using var scope = provider.CreateScope(); var asInterface1 = scope.ServiceProvider.GetService(); @@ -87,12 +84,14 @@ public void ScopedCrossWire_SharesOneInstanceWithinAScope(IServiceProvider provi /// [ModuleTest] [CrossWireScopedModule] - public void ScopedCrossWire_DiffersBetweenScopes(IServiceProvider provider) { + public void ScopedCrossWire_DiffersBetweenScopes(IServiceProvider provider) + { using var first = provider.CreateScope(); using var second = provider.CreateScope(); Assert.NotSame( first.ServiceProvider.GetService(), - second.ServiceProvider.GetService()); + second.ServiceProvider.GetService() + ); } } diff --git a/integ-tests/SutProject.Tests/Customization/CustomDependencyTestCase.cs b/integ-tests/SutProject.Tests/Customization/CustomDependencyTestCase.cs index cb42dd2..de1a249 100644 --- a/integ-tests/SutProject.Tests/Customization/CustomDependencyTestCase.cs +++ b/integ-tests/SutProject.Tests/Customization/CustomDependencyTestCase.cs @@ -3,10 +3,12 @@ namespace SutProject.Tests.Customization; -public class CustomDependencyTestCase { +public class CustomDependencyTestCase +{ [ModuleTest] [CustomServiceProvider] - public void TestCase(ICustomTestDependency dependency) { + public void TestCase(ICustomTestDependency dependency) + { Assert.NotNull(dependency); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/Customization/CustomServiceProviderAttribute.cs b/integ-tests/SutProject.Tests/Customization/CustomServiceProviderAttribute.cs index 7566d7f..216fb8b 100644 --- a/integ-tests/SutProject.Tests/Customization/CustomServiceProviderAttribute.cs +++ b/integ-tests/SutProject.Tests/Customization/CustomServiceProviderAttribute.cs @@ -3,10 +3,13 @@ namespace SutProject.Tests.Customization; -public class CustomServiceProviderAttribute : Attribute, IServiceProviderBuilderAttribute { - +public class CustomServiceProviderAttribute : Attribute, IServiceProviderBuilderAttribute +{ public IServiceProvider BuildServiceProvider( - ITestMethodContext testMethod, IServiceCollection serviceCollection) { + ITestMethodContext testMethod, + IServiceCollection serviceCollection + ) + { serviceCollection.AddSingleton(); return serviceCollection.BuildServiceProvider(); } diff --git a/integ-tests/SutProject.Tests/Customization/CustomTestDependency.cs b/integ-tests/SutProject.Tests/Customization/CustomTestDependency.cs index d79407e..693a1a4 100644 --- a/integ-tests/SutProject.Tests/Customization/CustomTestDependency.cs +++ b/integ-tests/SutProject.Tests/Customization/CustomTestDependency.cs @@ -1,9 +1,5 @@ namespace SutProject.Tests.Customization; -public interface ICustomTestDependency { - -} +public interface ICustomTestDependency { } -public class CustomTestDependency : ICustomTestDependency { - -} \ No newline at end of file +public class CustomTestDependency : ICustomTestDependency { } diff --git a/integ-tests/SutProject.Tests/Customization/ServiceProviderBuilderPrecedenceTests.cs b/integ-tests/SutProject.Tests/Customization/ServiceProviderBuilderPrecedenceTests.cs index 07c0ea8..3af77e2 100644 --- a/integ-tests/SutProject.Tests/Customization/ServiceProviderBuilderPrecedenceTests.cs +++ b/integ-tests/SutProject.Tests/Customization/ServiceProviderBuilderPrecedenceTests.cs @@ -8,22 +8,30 @@ namespace SutProject.Tests.Customization; /// /// Records which actually built the container. /// -public interface IProviderBuiltBy { +public interface IProviderBuiltBy +{ string Scope { get; } } -public class ProviderBuiltBy(string scope) : IProviderBuiltBy { +public class ProviderBuiltBy(string scope) : IProviderBuiltBy +{ public string Scope => scope; } /// /// A builder that stamps the container with the scope it was declared at. /// -public class ScopeStampingProviderAttribute(string scope) : Attribute, IServiceProviderBuilderAttribute { +public class ScopeStampingProviderAttribute(string scope) + : Attribute, + IServiceProviderBuilderAttribute +{ public string Scope => scope; public IServiceProvider BuildServiceProvider( - ITestMethodContext testMethod, IServiceCollection serviceCollection) { + ITestMethodContext testMethod, + IServiceCollection serviceCollection + ) + { serviceCollection.AddSingleton(new ProviderBuiltBy(scope)); return serviceCollection.BuildServiceProvider(); @@ -36,16 +44,18 @@ public IServiceProvider BuildServiceProvider( /// overridden by a broader default. /// [ScopeStampingProvider("class")] -public class ServiceProviderBuilderPrecedenceTests { - +public class ServiceProviderBuilderPrecedenceTests +{ [ModuleTest] [ScopeStampingProvider("method")] - public void MethodBeatsClass(IProviderBuiltBy builtBy) { + public void MethodBeatsClass(IProviderBuiltBy builtBy) + { Assert.Equal("method", builtBy.Scope); } [ModuleTest] - public void ClassAppliesWhenTheMethodDeclaresNone(IProviderBuiltBy builtBy) { + public void ClassAppliesWhenTheMethodDeclaresNone(IProviderBuiltBy builtBy) + { Assert.Equal("class", builtBy.Scope); } } diff --git a/integ-tests/SutProject.Tests/DataTests/InlineDataTests.cs b/integ-tests/SutProject.Tests/DataTests/InlineDataTests.cs index fa39250..11ce908 100644 --- a/integ-tests/SutProject.Tests/DataTests/InlineDataTests.cs +++ b/integ-tests/SutProject.Tests/DataTests/InlineDataTests.cs @@ -3,25 +3,27 @@ namespace SutProject.Tests.DataTests; -public class InlineDataTests { - +public class InlineDataTests +{ [ModuleTest] [InlineData("Hello World")] [SutModule] - public void SimpleValueTests(string value, IDependencyOne one) { + public void SimpleValueTests(string value, IDependencyOne one) + { Assert.Equal("Hello World", value); Assert.NotNull(one); } } -public class MultiRowDataTests { - +public class MultiRowDataTests +{ [ModuleTest] [InlineData("one")] [InlineData("two")] [InlineData("three")] [SutModule] - public void MultipleRows(string value, IDependencyOne one) { + public void MultipleRows(string value, IDependencyOne one) + { Assert.NotNull(value); Assert.NotNull(one); } diff --git a/integ-tests/SutProject.Tests/DataTests/MemberDataTests.cs b/integ-tests/SutProject.Tests/DataTests/MemberDataTests.cs index a860cbf..207f43a 100644 --- a/integ-tests/SutProject.Tests/DataTests/MemberDataTests.cs +++ b/integ-tests/SutProject.Tests/DataTests/MemberDataTests.cs @@ -13,16 +13,18 @@ namespace SutProject.Tests.DataTests; /// this file needs to exist. A regression here is only visible as the suite getting smaller, so /// the unit-level counterpart in ModuleTestCaseDataTests asserts on the number of cases created. /// -public class MemberDataTests { - +public class MemberDataTests +{ public static TheoryData Rows => new("one", "two"); - public static IEnumerable RawRows() { + public static IEnumerable RawRows() + { yield return ["one"]; yield return ["two"]; } - public static IEnumerable> TypedRows() { + public static IEnumerable> TypedRows() + { yield return new TheoryDataRow("one"); yield return new TheoryDataRow("two"); } @@ -34,7 +36,8 @@ public static IEnumerable> TypedRows() { [ModuleTest] [MemberData(nameof(Rows))] [SutModule] - public void TheoryDataRowsAreSupplied(string value, IDependencyOne one) { + public void TheoryDataRowsAreSupplied(string value, IDependencyOne one) + { Assert.NotNull(value); Assert.NotNull(one); } @@ -42,7 +45,8 @@ public void TheoryDataRowsAreSupplied(string value, IDependencyOne one) { [ModuleTest] [MemberData(nameof(RawRows))] [SutModule] - public void ObjectArrayRowsAreSupplied(string value, IDependencyOne one) { + public void ObjectArrayRowsAreSupplied(string value, IDependencyOne one) + { Assert.NotNull(value); Assert.NotNull(one); } @@ -50,7 +54,8 @@ public void ObjectArrayRowsAreSupplied(string value, IDependencyOne one) { [ModuleTest] [MemberData(nameof(TypedRows))] [SutModule] - public void TheoryDataRowRowsAreSupplied(string value, IDependencyOne one) { + public void TheoryDataRowRowsAreSupplied(string value, IDependencyOne one) + { Assert.NotNull(value); Assert.NotNull(one); } @@ -61,7 +66,8 @@ public void TheoryDataRowRowsAreSupplied(string value, IDependencyOne one) { [ModuleTest] [MemberData(nameof(Rows), MemberType = typeof(MemberDataTests))] [SutModule] - public void ExplicitMemberTypeRowsAreSupplied(string value, IDependencyOne one) { + public void ExplicitMemberTypeRowsAreSupplied(string value, IDependencyOne one) + { Assert.NotNull(value); Assert.NotNull(one); } @@ -69,7 +75,8 @@ public void ExplicitMemberTypeRowsAreSupplied(string value, IDependencyOne one) [ModuleTest] [ClassData(typeof(ClassRows))] [SutModule] - public void ClassDataRowsAreSupplied(string value, IDependencyOne one) { + public void ClassDataRowsAreSupplied(string value, IDependencyOne one) + { Assert.NotNull(value); Assert.NotNull(one); } @@ -77,8 +84,10 @@ public void ClassDataRowsAreSupplied(string value, IDependencyOne one) { #pragma warning restore xUnit1037 } -public class ClassRows : TheoryData { - public ClassRows() { +public class ClassRows : TheoryData +{ + public ClassRows() + { Add("one"); Add("two"); } diff --git a/integ-tests/SutProject.Tests/DuplicateNames/FirstDuplicateNameTests.cs b/integ-tests/SutProject.Tests/DuplicateNames/FirstDuplicateNameTests.cs index 9622beb..d5e3bdd 100644 --- a/integ-tests/SutProject.Tests/DuplicateNames/FirstDuplicateNameTests.cs +++ b/integ-tests/SutProject.Tests/DuplicateNames/FirstDuplicateNameTests.cs @@ -8,10 +8,12 @@ namespace SutProject.Tests.DuplicateNames; /// same name. A test case unique ID built from the bare method name collides across classes, and /// xUnit silently drops the duplicate, so both of these must still run. /// -public class FirstDuplicateNameTests { +public class FirstDuplicateNameTests +{ [ModuleTest] [SutModule] - public void SharedMethodName(IDependencyOne dependency) { + public void SharedMethodName(IDependencyOne dependency) + { Assert.NotNull(dependency); } } diff --git a/integ-tests/SutProject.Tests/DuplicateNames/SecondDuplicateNameTests.cs b/integ-tests/SutProject.Tests/DuplicateNames/SecondDuplicateNameTests.cs index 1b70b04..23a56dd 100644 --- a/integ-tests/SutProject.Tests/DuplicateNames/SecondDuplicateNameTests.cs +++ b/integ-tests/SutProject.Tests/DuplicateNames/SecondDuplicateNameTests.cs @@ -6,10 +6,12 @@ namespace SutProject.Tests.DuplicateNames; /// /// See . /// -public class SecondDuplicateNameTests { +public class SecondDuplicateNameTests +{ [ModuleTest] [SutModule] - public void SharedMethodName(IDependencyOne dependency) { + public void SharedMethodName(IDependencyOne dependency) + { Assert.NotNull(dependency); } } diff --git a/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentConfigurationTests.cs b/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentConfigurationTests.cs index f4ca0ff..809dc86 100644 --- a/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentConfigurationTests.cs +++ b/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentConfigurationTests.cs @@ -6,56 +6,71 @@ namespace SutProject.Tests.EnvironmentTests; -public class TestEnvironment : IModuleEnvironment { +public class TestEnvironment : IModuleEnvironment +{ public string EnvironmentName { get; } private readonly Dictionary _values; - public TestEnvironment(string environmentName, Dictionary? values = null) { + public TestEnvironment(string environmentName, Dictionary? values = null) + { EnvironmentName = environmentName; _values = values ?? new Dictionary(); } - public string? Value(string name) { + public string? Value(string name) + { return _values.TryGetValue(name, out var value) ? value : null; } } -public interface IEnvironmentDependency { +public interface IEnvironmentDependency +{ string EnvironmentName { get; } } -public class EnvironmentDependency(string environmentName) : IEnvironmentDependency { +public class EnvironmentDependency(string environmentName) : IEnvironmentDependency +{ public string EnvironmentName { get; } = environmentName; } [DependencyModule] -public partial class EnvironmentAwareModule : IEnvironmentServiceCollectionConfiguration { - public void ConfigureServices(IServiceCollection services, IModuleEnvironment environment) { +public partial class EnvironmentAwareModule : IEnvironmentServiceCollectionConfiguration +{ + public void ConfigureServices(IServiceCollection services, IModuleEnvironment environment) + { var envName = environment.EnvironmentName; services.AddSingleton(new EnvironmentDependency(envName)); } } [DependencyModule] -public partial class DualConfigModule : IServiceCollectionConfiguration, IEnvironmentServiceCollectionConfiguration { - public void ConfigureServices(IServiceCollection services) { +public partial class DualConfigModule + : IServiceCollectionConfiguration, + IEnvironmentServiceCollectionConfiguration +{ + public void ConfigureServices(IServiceCollection services) + { services.AddSingleton(new StringMarker("from-configure")); } - public void ConfigureServices(IServiceCollection services, IModuleEnvironment environment) { + public void ConfigureServices(IServiceCollection services, IModuleEnvironment environment) + { var envName = environment.EnvironmentName; services.AddSingleton(new EnvironmentDependency(envName)); } } -public class StringMarker(string value) { +public class StringMarker(string value) +{ public string Value { get; } = value; } -public class EnvironmentConfigurationTests { +public class EnvironmentConfigurationTests +{ [Fact] - public void EnvironmentPassedToModule_WhenRegistered() { + public void EnvironmentPassedToModule_WhenRegistered() + { var serviceCollection = new ServiceCollection(); var environment = new TestEnvironment("Production"); @@ -71,7 +86,8 @@ public void EnvironmentPassedToModule_WhenRegistered() { /// No environment supplied means the process default rather than null. /// [Fact] - public void ProcessEnvironment_WhenNotRegistered() { + public void ProcessEnvironment_WhenNotRegistered() + { var serviceCollection = new ServiceCollection(); serviceCollection.AddModules(new EnvironmentAwareModule()); @@ -87,7 +103,8 @@ public void ProcessEnvironment_WhenNotRegistered() { /// null to branch on. /// [Fact] - public void ModuleEnvironmentNone_HasNoNameAndNoValues() { + public void ModuleEnvironmentNone_HasNoNameAndNoValues() + { var serviceCollection = new ServiceCollection(); serviceCollection.AddModules(ModuleEnvironment.None, new EnvironmentAwareModule()); @@ -99,7 +116,8 @@ public void ModuleEnvironmentNone_HasNoNameAndNoValues() { } [Fact] - public void EnvironmentRegisteredAsSingleton_WhenProvided() { + public void EnvironmentRegisteredAsSingleton_WhenProvided() + { var serviceCollection = new ServiceCollection(); var environment = new TestEnvironment("Development"); @@ -112,11 +130,13 @@ public void EnvironmentRegisteredAsSingleton_WhenProvided() { } [Fact] - public void EnvironmentValues_AccessibleInModule() { + public void EnvironmentValues_AccessibleInModule() + { var serviceCollection = new ServiceCollection(); - var values = new Dictionary { + var values = new Dictionary + { { "Region", "us-east-1" }, - { "Feature.NewUI", "true" } + { "Feature.NewUI", "true" }, }; var environment = new TestEnvironment("Staging", values); @@ -131,7 +151,8 @@ public void EnvironmentValues_AccessibleInModule() { } [Fact] - public void BothConfigurationInterfaces_CalledCorrectly() { + public void BothConfigurationInterfaces_CalledCorrectly() + { var serviceCollection = new ServiceCollection(); var environment = new TestEnvironment("Test"); @@ -150,7 +171,8 @@ public void BothConfigurationInterfaces_CalledCorrectly() { /// registrations is the same one that resolves. /// [Fact] - public void NullEnvironmentParameter_RegistersTheProcessDefault() { + public void NullEnvironmentParameter_RegistersTheProcessDefault() + { var serviceCollection = new ServiceCollection(); serviceCollection.AddModules((IModuleEnvironment?)null, new EnvironmentAwareModule()); @@ -159,22 +181,28 @@ public void NullEnvironmentParameter_RegistersTheProcessDefault() { // fresh environment per call, so the invariant is that these are the same object — not that // either matches something asked for later. var registered = Assert.Single( - serviceCollection, descriptor => descriptor.ServiceType == typeof(IModuleEnvironment)); + serviceCollection, + descriptor => descriptor.ServiceType == typeof(IModuleEnvironment) + ); var serviceProvider = serviceCollection.BuildServiceProvider(); Assert.Same( - registered.ImplementationInstance, serviceProvider.GetRequiredService()); + registered.ImplementationInstance, + serviceProvider.GetRequiredService() + ); Assert.Equal( ModuleEnvironment.CreateDefault().EnvironmentName, - serviceProvider.GetRequiredService().EnvironmentName); + serviceProvider.GetRequiredService().EnvironmentName + ); } /// /// An environment the application supplied is never displaced by the default. /// [Fact] - public void SuppliedEnvironmentIsNotReplacedByTheDefault() { + public void SuppliedEnvironmentIsNotReplacedByTheDefault() + { var serviceCollection = new ServiceCollection(); var environment = new TestEnvironment("Staging"); diff --git a/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentSeedingTests.cs b/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentSeedingTests.cs index 4f77114..d8a3971 100644 --- a/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentSeedingTests.cs +++ b/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentSeedingTests.cs @@ -14,7 +14,8 @@ namespace SutProject.Tests.EnvironmentTests; /// tests. /// [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] -public class SeededEnvironmentAttribute(string name) : Attribute, IModuleEnvironmentProvider { +public class SeededEnvironmentAttribute(string name) : Attribute, IModuleEnvironmentProvider +{ public IModuleEnvironment? ProvideEnvironment(MethodInfo testMethod) => new ModuleEnvironment(false, name); } @@ -38,12 +39,13 @@ public partial class SeededEnvironmentModule { } /// service-setup pass, so every condition had been decided against the process default before an /// attribute could register anything. /// -public class EnvironmentSeedingTests { - +public class EnvironmentSeedingTests +{ [ModuleTest] [SeededEnvironmentModule] [SeededEnvironment("seeded-environment")] - public void AGatedRegistrationAppliesUnderTheSeededEnvironment(IServiceProvider provider) { + public void AGatedRegistrationAppliesUnderTheSeededEnvironment(IServiceProvider provider) + { Assert.NotNull(provider.GetService()); } @@ -53,7 +55,8 @@ public void AGatedRegistrationAppliesUnderTheSeededEnvironment(IServiceProvider /// [ModuleTest] [SeededEnvironmentModule] - public void TheSameRegistrationIsAbsentWithoutASeed(IServiceProvider provider) { + public void TheSameRegistrationIsAbsentWithoutASeed(IServiceProvider provider) + { Assert.Null(provider.GetService()); } @@ -61,31 +64,35 @@ public void TheSameRegistrationIsAbsentWithoutASeed(IServiceProvider provider) { [ModuleTest] [EnvironmentAwareModule] [SeededEnvironment("seeded-environment")] - public void AModuleReadingTheEnvironmentSeesTheSeededOne(IEnvironmentDependency dependency) { + public void AModuleReadingTheEnvironmentSeesTheSeededOne(IEnvironmentDependency dependency) + { Assert.Equal("seeded-environment", dependency.EnvironmentName); } [ModuleTest] [EnvironmentAwareModule] - public void WithoutASeedTheProcessDefaultApplies(IEnvironmentDependency dependency) { + public void WithoutASeedTheProcessDefaultApplies(IEnvironmentDependency dependency) + { Assert.Equal(ModuleEnvironment.CreateDefault().EnvironmentName, dependency.EnvironmentName); } } /// Narrowest scope wins, matching how every other attribute here resolves. [SeededEnvironment("outer-environment")] -public class EnvironmentSeedingPrecedenceTests { - +public class EnvironmentSeedingPrecedenceTests +{ [ModuleTest] [EnvironmentAwareModule] - public void AClassLevelSeedApplies(IEnvironmentDependency dependency) { + public void AClassLevelSeedApplies(IEnvironmentDependency dependency) + { Assert.Equal("outer-environment", dependency.EnvironmentName); } [ModuleTest] [EnvironmentAwareModule] [SeededEnvironment("inner-environment")] - public void TheMethodsSeedBeatsTheClasses(IEnvironmentDependency dependency) { + public void TheMethodsSeedBeatsTheClasses(IEnvironmentDependency dependency) + { Assert.Equal("inner-environment", dependency.EnvironmentName); } } diff --git a/integ-tests/SutProject.Tests/FactoryTests/SimpleFactoryTests.cs b/integ-tests/SutProject.Tests/FactoryTests/SimpleFactoryTests.cs index 134e613..5cdd6a5 100644 --- a/integ-tests/SutProject.Tests/FactoryTests/SimpleFactoryTests.cs +++ b/integ-tests/SutProject.Tests/FactoryTests/SimpleFactoryTests.cs @@ -7,33 +7,38 @@ namespace SutProject.Tests.FactoryTests; [DependencyModule(OnlyRealm = true)] -public partial class FactoryModule { - -} - -public static class FactoryClass { +public partial class FactoryModule { } +public static class FactoryClass +{ [SingletonService(Realm = typeof(FactoryModule))] public static IDependencyOne FactoryService( - ISingletonService singletonService, IScopedService scopedService) { + ISingletonService singletonService, + IScopedService scopedService + ) + { return new DependencyOne(singletonService, scopedService); } [SingletonService(Realm = typeof(FactoryModule))] - public static ISingletonService SingletonService(IServiceProvider serviceProvider) { + public static ISingletonService SingletonService(IServiceProvider serviceProvider) + { return new SingletonService(); } [ScopedService(Realm = typeof(FactoryModule))] - public static IScopedService ScopedService() { + public static IScopedService ScopedService() + { return new ScopedService(); } } -public class SimpleFactoryTests { +public class SimpleFactoryTests +{ [ModuleTest] [FactoryModule] - public void FactoryTest(IDependencyOne dependencyOne) { + public void FactoryTest(IDependencyOne dependencyOne) + { Assert.NotNull(dependencyOne); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/FakeItEasy/FakeItEasyAttributeTests.cs b/integ-tests/SutProject.Tests/FakeItEasy/FakeItEasyAttributeTests.cs index cc8e16d..340ba83 100644 --- a/integ-tests/SutProject.Tests/FakeItEasy/FakeItEasyAttributeTests.cs +++ b/integ-tests/SutProject.Tests/FakeItEasy/FakeItEasyAttributeTests.cs @@ -10,12 +10,16 @@ namespace SutProject.Tests.FakeItEasy; /// The same scenario as the NSubstitute and Moq tests, so the three can be read against each other. /// [FakeItEasySupport] -public class FakeItEasyAttributeTests { - +public class FakeItEasyAttributeTests +{ [ModuleTest] [SutModule] - public void MockTest([Mock] IDependencyOne dependencyOne, - [Mock] IScopedService scopedService, ISingletonService singletonService) { + public void MockTest( + [Mock] IDependencyOne dependencyOne, + [Mock] IScopedService scopedService, + ISingletonService singletonService + ) + { A.CallTo(() => dependencyOne.SingletonService).Returns(singletonService); A.CallTo(() => dependencyOne.ScopedService).Returns(scopedService); @@ -28,7 +32,8 @@ public void MockTest([Mock] IDependencyOne dependencyOne, /// [ModuleTest] [SutModule] - public void TheInjectedInstanceIsTheFake([Mock] IDependencyOne dependencyOne) { + public void TheInjectedInstanceIsTheFake([Mock] IDependencyOne dependencyOne) + { Assert.True(Fake.GetFakeManager(dependencyOne) is not null); } } diff --git a/integ-tests/SutProject.Tests/Features/FeatureModules.cs b/integ-tests/SutProject.Tests/Features/FeatureModules.cs index bf87703..b29ce32 100644 --- a/integ-tests/SutProject.Tests/Features/FeatureModules.cs +++ b/integ-tests/SutProject.Tests/Features/FeatureModules.cs @@ -4,38 +4,45 @@ namespace SutProject.Tests.Features; -public interface IModuleFeatureValue { +public interface IModuleFeatureValue +{ string Value { get; } } [DependencyModule(OnlyRealm = true)] -public partial class FeatureModuleA : IModuleFeatureValue { - +public partial class FeatureModuleA : IModuleFeatureValue +{ public string Value => "A"; } [DependencyModule(OnlyRealm = true)] -public partial class FeatureModuleB : IModuleFeatureValue { - +public partial class FeatureModuleB : IModuleFeatureValue +{ public string Value => "B"; } [DependencyModule(OnlyRealm = true)] -public partial class FeatureModuleC : IModuleFeatureValue { - +public partial class FeatureModuleC : IModuleFeatureValue +{ public string Value => "C"; } -public class DependencyValue(string value) { +public class DependencyValue(string value) +{ public string Value { get; } = value; } [DependencyModule(OnlyRealm = true)] -public partial class FeatureModuleHandler : IDependencyModuleFeature { - - public void HandleFeature(IServiceCollection collection, IEnumerable feature) { - foreach (var featureValue in feature) { +public partial class FeatureModuleHandler : IDependencyModuleFeature +{ + public void HandleFeature( + IServiceCollection collection, + IEnumerable feature + ) + { + foreach (var featureValue in feature) + { collection.AddSingleton(_ => new DependencyValue(featureValue.Value)); } } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/Features/FeatureTests.cs b/integ-tests/SutProject.Tests/Features/FeatureTests.cs index 91bbfca..1f779d8 100644 --- a/integ-tests/SutProject.Tests/Features/FeatureTests.cs +++ b/integ-tests/SutProject.Tests/Features/FeatureTests.cs @@ -3,18 +3,20 @@ namespace SutProject.Tests.Features; -public class FeatureTests { +public class FeatureTests +{ [ModuleTest] [FeatureModuleHandler] [FeatureModuleA] [FeatureModuleB] [FeatureModuleC] - public void FeatureTest(IEnumerable values) { + public void FeatureTest(IEnumerable values) + { var valuesArray = values.ToArray(); - + Assert.Equal(3, valuesArray.Length); Assert.Single(valuesArray, v => v.Value == "A"); Assert.Single(valuesArray, v => v.Value == "B"); Assert.Single(valuesArray, v => v.Value == "C"); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/Features/OrderFeatureTests.cs b/integ-tests/SutProject.Tests/Features/OrderFeatureTests.cs index d17839a..2e60414 100644 --- a/integ-tests/SutProject.Tests/Features/OrderFeatureTests.cs +++ b/integ-tests/SutProject.Tests/Features/OrderFeatureTests.cs @@ -3,17 +3,19 @@ namespace SutProject.Tests.Features; -public class OrderFeatureTests { +public class OrderFeatureTests +{ [ModuleTest] [FirstFeatureHandler] [SecondFeatureHandler] [ThirdFeatureHandler] - public void OrderTest(IEnumerable values) { + public void OrderTest(IEnumerable values) + { var valuesList = values as List ?? values.ToList(); - + Assert.Equal(3, valuesList.Count); Assert.Equal("1", valuesList[0].Value); Assert.Equal("2", valuesList[1].Value); Assert.Equal("3", valuesList[2].Value); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/Features/OrderedFeatureModules.cs b/integ-tests/SutProject.Tests/Features/OrderedFeatureModules.cs index e73f9e1..be350ba 100644 --- a/integ-tests/SutProject.Tests/Features/OrderedFeatureModules.cs +++ b/integ-tests/SutProject.Tests/Features/OrderedFeatureModules.cs @@ -4,39 +4,37 @@ namespace SutProject.Tests.Features; -public interface IOrderedFeature { - -} - +public interface IOrderedFeature { } [DependencyModule(OnlyRealm = true)] -public partial class FirstFeatureHandler : IDependencyModuleFeature { - +public partial class FirstFeatureHandler : IDependencyModuleFeature +{ public int Order => 1; - public void HandleFeature(IServiceCollection collection, IEnumerable feature) { - collection.AddSingleton(_ => new DependencyValue("1")); + public void HandleFeature(IServiceCollection collection, IEnumerable feature) + { + collection.AddSingleton(_ => new DependencyValue("1")); } } [DependencyModule(OnlyRealm = true)] -public partial class SecondFeatureHandler : IDependencyModuleFeature { - +public partial class SecondFeatureHandler : IDependencyModuleFeature +{ public int Order => 2; - public void HandleFeature(IServiceCollection collection, IEnumerable feature) { + public void HandleFeature(IServiceCollection collection, IEnumerable feature) + { collection.AddSingleton(_ => new DependencyValue("2")); } } [DependencyModule(OnlyRealm = true)] -public partial class ThirdFeatureHandler : IDependencyModuleFeature { - +public partial class ThirdFeatureHandler : IDependencyModuleFeature +{ public int Order => 3; - public void HandleFeature(IServiceCollection collection, IEnumerable feature) { + public void HandleFeature(IServiceCollection collection, IEnumerable feature) + { collection.AddSingleton(_ => new DependencyValue("3")); } } - - diff --git a/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleAttributeTests.cs b/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleAttributeTests.cs index 1d0f3f4..c4caaf8 100644 --- a/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleAttributeTests.cs +++ b/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleAttributeTests.cs @@ -3,17 +3,23 @@ namespace SutProject.Tests.GenerateAttributeTests; -public class ModuleAttributeTests { +public class ModuleAttributeTests +{ [Fact] - public void AssertGenerateAttribute() { + public void AssertGenerateAttribute() + { var assembly = GetType().Assembly; - var withAttributeType = assembly.GetType(typeof(ModuleWithAttribute).FullName + "Attribute"); - + var withAttributeType = assembly.GetType( + typeof(ModuleWithAttribute).FullName + "Attribute" + ); + Assert.NotNull(withAttributeType); - - var withoutAttributeType = assembly.GetType(typeof(ModuleWithoutAttribute).FullName + "Attribute"); - + + var withoutAttributeType = assembly.GetType( + typeof(ModuleWithoutAttribute).FullName + "Attribute" + ); + Assert.Null(withoutAttributeType); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleWithAttribute.cs b/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleWithAttribute.cs index 74ebcee..a8f76e8 100644 --- a/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleWithAttribute.cs +++ b/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleWithAttribute.cs @@ -3,6 +3,4 @@ namespace SutProject.Tests.GenerateAttributeTests; [DependencyModule] -public partial class ModuleWithAttribute { - -} \ No newline at end of file +public partial class ModuleWithAttribute { } diff --git a/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleWithoutAttribute.cs b/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleWithoutAttribute.cs index 99ee40d..248d75e 100644 --- a/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleWithoutAttribute.cs +++ b/integ-tests/SutProject.Tests/GenerateAttributeTests/ModuleWithoutAttribute.cs @@ -3,6 +3,4 @@ namespace SutProject.Tests.GenerateAttributeTests; [DependencyModule(GenerateAttribute = false)] -public partial class ModuleWithoutAttribute { - -} \ No newline at end of file +public partial class ModuleWithoutAttribute { } diff --git a/integ-tests/SutProject.Tests/GenerateFactories/GenerateFactoryModule.cs b/integ-tests/SutProject.Tests/GenerateFactories/GenerateFactoryModule.cs index 8eb3e8b..bdf100c 100644 --- a/integ-tests/SutProject.Tests/GenerateFactories/GenerateFactoryModule.cs +++ b/integ-tests/SutProject.Tests/GenerateFactories/GenerateFactoryModule.cs @@ -9,35 +9,29 @@ public partial class GenerateFactoryModule; [SingletonService(Realm = typeof(GenerateFactoryModule))] public class FactoryDepOne( - ISingletonService? singletonService = null, - IScopedService? scopedService = null) : IDependencyOne { + ISingletonService? singletonService = null, + IScopedService? scopedService = null +) : IDependencyOne +{ + public ISingletonService SingletonService { get; } = singletonService!; - public ISingletonService SingletonService { - get; - } = singletonService!; - - public IScopedService ScopedService { - get; - } = scopedService!; + public IScopedService ScopedService { get; } = scopedService!; } [SingletonService(Realm = typeof(GenerateFactoryModule), Key = "Keyed")] -public class GenerateKeyed() : IKeyedRegistration { - public string Key { - get; - } = "Keyed"; +public class GenerateKeyed() : IKeyedRegistration +{ + public string Key { get; } = "Keyed"; } [SingletonService(Realm = typeof(GenerateFactoryModule))] -public class KeyedDependency([FromKeyedServices("Keyed")] IKeyedRegistration registration) { - public IKeyedRegistration Registration { - get; - } = registration; +public class KeyedDependency([FromKeyedServices("Keyed")] IKeyedRegistration registration) +{ + public IKeyedRegistration Registration { get; } = registration; } [SingletonService(Realm = typeof(GenerateFactoryModule))] -public class StandardConstructor { - public StandardConstructor(IDependencyOne dependencyOne) { - - } -} \ No newline at end of file +public class StandardConstructor +{ + public StandardConstructor(IDependencyOne dependencyOne) { } +} diff --git a/integ-tests/SutProject.Tests/GenerateFactories/GenerateFactoryTests.cs b/integ-tests/SutProject.Tests/GenerateFactories/GenerateFactoryTests.cs index 5be349c..558bc2c 100644 --- a/integ-tests/SutProject.Tests/GenerateFactories/GenerateFactoryTests.cs +++ b/integ-tests/SutProject.Tests/GenerateFactories/GenerateFactoryTests.cs @@ -3,11 +3,12 @@ namespace SutProject.Tests.GenerateFactories; -public class GenerateFactoryTests { +public class GenerateFactoryTests +{ [ModuleTest] [GenerateFactoryModule] - public void ConstructGeneratedFactories( - KeyedDependency keyedDependency, IDependencyOne dep) { + public void ConstructGeneratedFactories(KeyedDependency keyedDependency, IDependencyOne dep) + { Assert.NotNull(keyedDependency); Assert.NotNull(dep); @@ -16,4 +17,4 @@ public void ConstructGeneratedFactories( Assert.Equal("Keyed", keyedDependency.Registration.Key); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/GenericTests/ResolveGenericTypeTests.cs b/integ-tests/SutProject.Tests/GenericTests/ResolveGenericTypeTests.cs index 69d4bb2..21bf4e1 100644 --- a/integ-tests/SutProject.Tests/GenericTests/ResolveGenericTypeTests.cs +++ b/integ-tests/SutProject.Tests/GenericTests/ResolveGenericTypeTests.cs @@ -6,25 +6,28 @@ namespace SutProject.Tests.GenericTests; - [DependencyModule] -public partial class GenericListModule : IServiceCollectionConfiguration { - - public void ConfigureServices(IServiceCollection services) { +public partial class GenericListModule : IServiceCollectionConfiguration +{ + public void ConfigureServices(IServiceCollection services) + { services.AddTransient(typeof(IReadOnlyList<>), typeof(List<>)); } } [SutModule] -public class ResolveGenericTypeTests { +public class ResolveGenericTypeTests +{ [ModuleTest] - public void ResolveGenericType(IGenericInterface genericInterface) { + public void ResolveGenericType(IGenericInterface genericInterface) + { Assert.NotNull(genericInterface); Assert.NotNull(genericInterface.Value); } [ModuleTest] - public void ResolveClosedGeneric(IGenericInterface genericInterface) { + public void ResolveClosedGeneric(IGenericInterface genericInterface) + { Assert.NotNull(genericInterface); Assert.IsType(genericInterface); } @@ -32,7 +35,8 @@ public void ResolveClosedGeneric(IGenericInterface genericInterface) { [ModuleTest] [SutModule] [GenericListModule] - public void ResolveList(IReadOnlyList genericList) { + public void ResolveList(IReadOnlyList genericList) + { Assert.NotNull(genericList); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/KeyedTests/KeyedModule.cs b/integ-tests/SutProject.Tests/KeyedTests/KeyedModule.cs index b65b4d3..2ec2e33 100644 --- a/integ-tests/SutProject.Tests/KeyedTests/KeyedModule.cs +++ b/integ-tests/SutProject.Tests/KeyedTests/KeyedModule.cs @@ -3,6 +3,4 @@ namespace SutProject.Tests.KeyedTests; [DependencyModule] -public partial class KeyedModule { - -} \ No newline at end of file +public partial class KeyedModule { } diff --git a/integ-tests/SutProject.Tests/KeyedTests/KeyedRegistrationModels.cs b/integ-tests/SutProject.Tests/KeyedTests/KeyedRegistrationModels.cs index 0917ccb..e76c21b 100644 --- a/integ-tests/SutProject.Tests/KeyedTests/KeyedRegistrationModels.cs +++ b/integ-tests/SutProject.Tests/KeyedTests/KeyedRegistrationModels.cs @@ -3,39 +3,44 @@ namespace SutProject.Tests.KeyedTests; -public interface IKeyedRegistration { +public interface IKeyedRegistration +{ string Key { get; } } [SingletonService(Key = "A", Realm = typeof(KeyedModule))] -public class AKeyedRegistration : KeyedRegistration { - public AKeyedRegistration() : base("A") { } +public class AKeyedRegistration : KeyedRegistration +{ + public AKeyedRegistration() + : base("A") { } } [SingletonService(Key = "B", Realm = typeof(KeyedModule))] -public class BKeyedRegistration : KeyedRegistration { - public BKeyedRegistration() : base("B") { - } +public class BKeyedRegistration : KeyedRegistration +{ + public BKeyedRegistration() + : base("B") { } } [SingletonService(Key = "C", Realm = typeof(KeyedModule))] -public class CKeyedRegistration : KeyedRegistration { - public CKeyedRegistration() : base("C") { - } +public class CKeyedRegistration : KeyedRegistration +{ + public CKeyedRegistration() + : base("C") { } } [SingletonService(Realm = typeof(KeyedModule))] -public class CKeyedDependency([FromKeyedServices("C")] IKeyedRegistration registration) { - public IKeyedRegistration Registration { - get; - } = registration; - +public class CKeyedDependency([FromKeyedServices("C")] IKeyedRegistration registration) +{ + public IKeyedRegistration Registration { get; } = registration; } -public abstract class KeyedRegistration : IKeyedRegistration { - protected KeyedRegistration(string key) { +public abstract class KeyedRegistration : IKeyedRegistration +{ + protected KeyedRegistration(string key) + { Key = key; } public string Key { get; } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/KeyedTests/KeyedRegistrationTests.cs b/integ-tests/SutProject.Tests/KeyedTests/KeyedRegistrationTests.cs index f48d12b..8c7246d 100644 --- a/integ-tests/SutProject.Tests/KeyedTests/KeyedRegistrationTests.cs +++ b/integ-tests/SutProject.Tests/KeyedTests/KeyedRegistrationTests.cs @@ -4,40 +4,46 @@ namespace SutProject.Tests.KeyedTests; -public class KeyedRegistrationTests { +public class KeyedRegistrationTests +{ [ModuleTest] [KeyedModule] - public void AKeyTest(IServiceProvider serviceProvider) { + public void AKeyTest(IServiceProvider serviceProvider) + { var aService = serviceProvider.GetKeyedService("A"); - + Assert.NotNull(aService); Assert.Equal("A", aService.Key); } - + [ModuleTest] [KeyedModule] - public void BKeyTest(IServiceProvider serviceProvider) { + public void BKeyTest(IServiceProvider serviceProvider) + { var aService = serviceProvider.GetKeyedService("B"); - + Assert.NotNull(aService); Assert.Equal("B", aService.Key); } - + [ModuleTest] [KeyedModule] - public void CKeyTest(IServiceProvider serviceProvider) { + public void CKeyTest(IServiceProvider serviceProvider) + { var aService = serviceProvider.GetKeyedService("C"); - + Assert.NotNull(aService); Assert.Equal("C", aService.Key); } - + [ModuleTest] [KeyedModule] public void AllKeyTest( - [FromKeyedServices("A")] IKeyedRegistration aService, - [FromKeyedServices("B")] IKeyedRegistration bService, - [FromKeyedServices("C")] IKeyedRegistration cService) { + [FromKeyedServices("A")] IKeyedRegistration aService, + [FromKeyedServices("B")] IKeyedRegistration bService, + [FromKeyedServices("C")] IKeyedRegistration cService + ) + { Assert.NotNull(aService); Assert.NotNull(bService); Assert.NotNull(cService); @@ -45,4 +51,4 @@ public void AllKeyTest( Assert.Equal("B", bService.Key); Assert.Equal("C", cService.Key); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/Moq/MoqAttributeTests.cs b/integ-tests/SutProject.Tests/Moq/MoqAttributeTests.cs index dec82ac..fd01653 100644 --- a/integ-tests/SutProject.Tests/Moq/MoqAttributeTests.cs +++ b/integ-tests/SutProject.Tests/Moq/MoqAttributeTests.cs @@ -12,12 +12,16 @@ namespace SutProject.Tests.Moq; /// object, so a test can name either — and the two have to agree about which mock they mean. /// [MoqSupport] -public class MoqAttributeTests { - +public class MoqAttributeTests +{ [ModuleTest] [SutModule] - public void MockTest([Mock] IDependencyOne dependencyOne, - [Mock] IScopedService scopedService, ISingletonService singletonService) { + public void MockTest( + [Mock] IDependencyOne dependencyOne, + [Mock] IScopedService scopedService, + ISingletonService singletonService + ) + { Mock.Get(dependencyOne).Setup(x => x.SingletonService).Returns(singletonService); Mock.Get(dependencyOne).Setup(x => x.ScopedService).Returns(scopedService); @@ -30,7 +34,8 @@ public void MockTest([Mock] IDependencyOne dependencyOne, /// [ModuleTest] [SutModule] - public void UnconfiguredMembersAreLoose([Mock] IDependencyOne dependencyOne) { + public void UnconfiguredMembersAreLoose([Mock] IDependencyOne dependencyOne) + { Assert.Null(dependencyOne.ScopedService); } @@ -47,7 +52,10 @@ public void UnconfiguredMembersAreLoose([Mock] IDependencyOne dependencyOne) { [ModuleTest] [SutModule] public void MockOfTIsInjectedDirectly( - Mock mock, ISingletonService singletonService) { + Mock mock, + ISingletonService singletonService + ) + { mock.Setup(x => x.GetName()).Returns("mocked"); Assert.Same(mock.Object, singletonService); @@ -61,7 +69,10 @@ public void MockOfTIsInjectedDirectly( [ModuleTest] [SutModule] public void MockOfTIsInjectedWithTheAttributeToo( - [Mock] Mock mock, ISingletonService singletonService) { + [Mock] Mock mock, + ISingletonService singletonService + ) + { mock.Setup(x => x.GetName()).Returns("mocked"); Assert.Same(mock.Object, singletonService); @@ -76,7 +87,10 @@ public void MockOfTIsInjectedWithTheAttributeToo( [ModuleTest] [SutModule] public void ServiceUnderTestIsBuiltAgainstTheMock( - IDependencyOne dependencyOne, Mock singletonService) { + IDependencyOne dependencyOne, + Mock singletonService + ) + { singletonService.Setup(x => x.GetName()).Returns("mocked"); Assert.Same(singletonService.Object, dependencyOne.SingletonService); @@ -91,7 +105,10 @@ public void ServiceUnderTestIsBuiltAgainstTheMock( [ModuleTest] [SutModule] public void TheMockAndTheInstanceAreOnePair( - [Mock] ISingletonService instance, Mock mock) { + [Mock] ISingletonService instance, + Mock mock + ) + { mock.Setup(x => x.GetName()).Returns("mocked"); Assert.Same(mock.Object, instance); @@ -105,7 +122,10 @@ public void TheMockAndTheInstanceAreOnePair( [ModuleTest] [SutModule] public void RepeatedMockParametersShareOneMock( - Mock first, Mock second) { + Mock first, + Mock second + ) + { first.Setup(x => x.GetName()).Returns("mocked"); Assert.Same(first, second); @@ -122,7 +142,9 @@ public void RepeatedMockParametersShareOneMock( public void DifferentServicesGetDifferentMocks( Mock singletonService, Mock scopedService, - IDependencyOne dependencyOne) { + IDependencyOne dependencyOne + ) + { Assert.Same(singletonService.Object, dependencyOne.SingletonService); Assert.Same(scopedService.Object, dependencyOne.ScopedService); } @@ -136,7 +158,10 @@ public void DifferentServicesGetDifferentMocks( [SutModule] [TestExport(typeof(ISingletonService), Implementation = typeof(ExportedSingletonService))] public void TestExportStillWinsOverAMock( - ISingletonService instance, Mock mock) { + ISingletonService instance, + Mock mock + ) + { Assert.IsType(instance); Assert.NotSame(mock.Object, instance); } @@ -154,7 +179,8 @@ public void TestExportStillWinsOverAMock( [ModuleTest] [SutModule] [TestExport(typeof(ISingletonService), Implementation = typeof(ExportedSingletonService))] - public void AMockOnAParameterBeatsATestExportOnTheMethod([Mock] ISingletonService service) { + public void AMockOnAParameterBeatsATestExportOnTheMethod([Mock] ISingletonService service) + { Assert.IsNotType(service); global::Moq.Mock.Get(service).Setup(x => x.GetName()).Returns("mocked"); @@ -163,7 +189,8 @@ public void AMockOnAParameterBeatsATestExportOnTheMethod([Mock] ISingletonServic } #pragma warning restore DM0021 - public class ExportedSingletonService : ISingletonService { + public class ExportedSingletonService : ISingletonService + { public string GetName() => "exported"; } } @@ -176,13 +203,17 @@ public class ExportedSingletonService : ISingletonService { /// putting it on the fixture above would have changed every test there, which is the point of it. /// [MoqSupport] -[TestExport(typeof(IScopedService), Implementation = typeof(ClassLevelTestExportTests.ExportedScopedService))] -public class ClassLevelTestExportTests { - +[TestExport( + typeof(IScopedService), + Implementation = typeof(ClassLevelTestExportTests.ExportedScopedService) +)] +public class ClassLevelTestExportTests +{ /// The default, taken by any test that does not say otherwise. [ModuleTest] [SutModule] - public void WithoutAMockTheClassDefaultApplies(IScopedService service) { + public void WithoutAMockTheClassDefaultApplies(IScopedService service) + { Assert.IsType(service); } @@ -192,7 +223,8 @@ public void WithoutAMockTheClassDefaultApplies(IScopedService service) { /// [ModuleTest] [SutModule] - public void AMockOnAParameterBeatsIt([Mock] IScopedService service) { + public void AMockOnAParameterBeatsIt([Mock] IScopedService service) + { Assert.IsNotType(service); // Reachable as a mock, which is the point - the parameter is the double, not the export. diff --git a/integ-tests/SutProject.Tests/Moq/MoqSetupOrderingTests.cs b/integ-tests/SutProject.Tests/Moq/MoqSetupOrderingTests.cs index b4c52f8..bb95d11 100644 --- a/integ-tests/SutProject.Tests/Moq/MoqSetupOrderingTests.cs +++ b/integ-tests/SutProject.Tests/Moq/MoqSetupOrderingTests.cs @@ -13,7 +13,8 @@ namespace SutProject.Tests.Moq; /// Declared outside the fixture because the attribute naming it sits on the fixture itself, and /// attribute arguments there resolve in the enclosing scope rather than the class's own. /// -public class ExportedSingletonService : ISingletonService { +public class ExportedSingletonService : ISingletonService +{ public string GetName() => "exported"; } @@ -37,13 +38,16 @@ public class ExportedSingletonService : ISingletonService { /// that test still passes; this one does not. /// [TestExport(typeof(ISingletonService), Implementation = typeof(ExportedSingletonService))] -public class MoqSetupOrderingTests { - +public class MoqSetupOrderingTests +{ [ModuleTest] [SutModule] [MoqSupport] public void ExplicitRegistrationBeatsAMockDeclaredNearerTheMethod( - ISingletonService instance, Mock mock) { + ISingletonService instance, + Mock mock + ) + { Assert.IsType(instance); Assert.Equal("exported", instance.GetName()); diff --git a/integ-tests/SutProject.Tests/NSubstitute/NSubstituteAttributeTests.cs b/integ-tests/SutProject.Tests/NSubstitute/NSubstituteAttributeTests.cs index 34a2b4c..96d12bf 100644 --- a/integ-tests/SutProject.Tests/NSubstitute/NSubstituteAttributeTests.cs +++ b/integ-tests/SutProject.Tests/NSubstitute/NSubstituteAttributeTests.cs @@ -1,22 +1,26 @@ +using DependencyModules.NSubstitute; using DependencyModules.Testing.Attributes; using DependencyModules.xUnit.Attributes; -using DependencyModules.NSubstitute; using NSubstitute; using Xunit; namespace SutProject.Tests.NSubstitute; [NSubstituteSupport] -public class NSubstituteAttributeTests { - +public class NSubstituteAttributeTests +{ [ModuleTest] [SutModule] - public void MockTest([Mock] IDependencyOne dependencyOne, - [Mock] IScopedService scopedService, ISingletonService singletonService) { + public void MockTest( + [Mock] IDependencyOne dependencyOne, + [Mock] IScopedService scopedService, + ISingletonService singletonService + ) + { dependencyOne.SingletonService.Returns(singletonService); dependencyOne.ScopedService.Returns(scopedService); Assert.Same(dependencyOne.SingletonService, singletonService); Assert.Same(dependencyOne.ScopedService, scopedService); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/ParameterizedModuleTests/ArrayParameterModule.cs b/integ-tests/SutProject.Tests/ParameterizedModuleTests/ArrayParameterModule.cs index cfe6677..2297986 100644 --- a/integ-tests/SutProject.Tests/ParameterizedModuleTests/ArrayParameterModule.cs +++ b/integ-tests/SutProject.Tests/ParameterizedModuleTests/ArrayParameterModule.cs @@ -3,9 +3,10 @@ namespace SutProject.Tests.ParameterizedModuleTests; [DependencyModule] -public partial class ArrayParameterModule { +public partial class ArrayParameterModule +{ public string[]? ArrayParameter { get; set; } = []; - + public Type? TypeValue { get; set; } // The other answer DM0018 accepts: this fixture is composed once, so type-only identity is @@ -15,9 +16,6 @@ public partial class ArrayParameterModule { public override int GetHashCode() => typeof(ArrayParameterModule).GetHashCode(); } - [DependencyModule] [ArrayParameterModule(ArrayParameter = ["A", "B"], TypeValue = typeof(int))] -public partial class AnotherModule { - -} \ No newline at end of file +public partial class AnotherModule { } diff --git a/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModule.cs b/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModule.cs index 02913d9..38e9639 100644 --- a/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModule.cs +++ b/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModule.cs @@ -4,7 +4,8 @@ namespace SutProject.Tests.ParameterizedModuleTests; -public class DefaultedParameterValues(string label, int size) { +public class DefaultedParameterValues(string label, int size) +{ public string Label { get; } = label; public int Size { get; } = size; @@ -25,12 +26,14 @@ public class DefaultedParameterValues(string label, int size) { /// behaviour. /// [DependencyModule] -public partial class DefaultedParameterModule : IServiceCollectionConfiguration { +public partial class DefaultedParameterModule : IServiceCollectionConfiguration +{ public string Label { get; set; } = "default-label"; public int Size { get; set; } = 42; - public void ConfigureServices(IServiceCollection services) { + public void ConfigureServices(IServiceCollection services) + { services.AddSingleton(new DefaultedParameterValues(Label, Size)); } diff --git a/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModuleTests.cs b/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModuleTests.cs index 362c215..0acdf09 100644 --- a/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModuleTests.cs +++ b/integ-tests/SutProject.Tests/ParameterizedModuleTests/DefaultedParameterModuleTests.cs @@ -3,13 +3,14 @@ namespace SutProject.Tests.ParameterizedModuleTests; -public class DefaultedParameterModuleTests { - +public class DefaultedParameterModuleTests +{ /// /// A composition that names no parameters leaves the module's own initialiser in place. /// [ModuleTest(typeof(DefaultedParameterComposer))] - public void AnUnnamedReferenceParameter_KeepsItsDefault(DefaultedParameterValues values) { + public void AnUnnamedReferenceParameter_KeepsItsDefault(DefaultedParameterValues values) + { Assert.Equal("default-label", values.Label); } @@ -19,12 +20,14 @@ public void AnUnnamedReferenceParameter_KeepsItsDefault(DefaultedParameterValues /// module parameter's initialiser does not survive composition by attribute. /// [ModuleTest(typeof(DefaultedParameterComposer))] - public void AnUnnamedValueParameter_DoesNotKeepItsDefault(DefaultedParameterValues values) { + public void AnUnnamedValueParameter_DoesNotKeepItsDefault(DefaultedParameterValues values) + { Assert.Equal(0, values.Size); } [ModuleTest(typeof(NamedParameterComposer))] - public void NamedParameters_AreCarriedAcross(DefaultedParameterValues values) { + public void NamedParameters_AreCarriedAcross(DefaultedParameterValues values) + { Assert.Equal("named-label", values.Label); Assert.Equal(7, values.Size); } diff --git a/integ-tests/SutProject.Tests/ParameterizedModuleTests/LocalParameterizedModule.cs b/integ-tests/SutProject.Tests/ParameterizedModuleTests/LocalParameterizedModule.cs index 9d76157..6f7e408 100644 --- a/integ-tests/SutProject.Tests/ParameterizedModuleTests/LocalParameterizedModule.cs +++ b/integ-tests/SutProject.Tests/ParameterizedModuleTests/LocalParameterizedModule.cs @@ -7,8 +7,7 @@ namespace SutProject.Tests.ParameterizedModuleTests; [DependencyModule] [ParameterizedModule("local-string", 20, C = "CValue")] -public partial class LocalParameterizedModule : IServiceCollectionConfiguration { - public void ConfigureServices(IServiceCollection services) { - - } -} \ No newline at end of file +public partial class LocalParameterizedModule : IServiceCollectionConfiguration +{ + public void ConfigureServices(IServiceCollection services) { } +} diff --git a/integ-tests/SutProject.Tests/ParameterizedModuleTests/LocalParameterizedModuleTests.cs b/integ-tests/SutProject.Tests/ParameterizedModuleTests/LocalParameterizedModuleTests.cs index 53eb977..6e8da08 100644 --- a/integ-tests/SutProject.Tests/ParameterizedModuleTests/LocalParameterizedModuleTests.cs +++ b/integ-tests/SutProject.Tests/ParameterizedModuleTests/LocalParameterizedModuleTests.cs @@ -4,11 +4,13 @@ namespace SutProject.Tests.ParameterizedModuleTests; -public class LocalParameterizedModuleTests { +public class LocalParameterizedModuleTests +{ [ModuleTest(typeof(LocalParameterizedModule))] - public void ParameterTest(SomeRuntimeDependency someRuntimeDependency) { + public void ParameterTest(SomeRuntimeDependency someRuntimeDependency) + { Assert.Equal("local-string", someRuntimeDependency.SomeDependency); Assert.Equal(20, someRuntimeDependency.IntDependency); Assert.Equal("CValue", someRuntimeDependency.CValue); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/RealmTests/FirstRealmModule.cs b/integ-tests/SutProject.Tests/RealmTests/FirstRealmModule.cs index a9cad53..74f9168 100644 --- a/integ-tests/SutProject.Tests/RealmTests/FirstRealmModule.cs +++ b/integ-tests/SutProject.Tests/RealmTests/FirstRealmModule.cs @@ -3,4 +3,4 @@ namespace SutProject.Tests.RealmTests; [DependencyModule(OnlyRealm = true)] -public partial class FirstRealmModule { } \ No newline at end of file +public partial class FirstRealmModule { } diff --git a/integ-tests/SutProject.Tests/RealmTests/RealmDependencyOne.cs b/integ-tests/SutProject.Tests/RealmTests/RealmDependencyOne.cs index 169031c..175e36c 100644 --- a/integ-tests/SutProject.Tests/RealmTests/RealmDependencyOne.cs +++ b/integ-tests/SutProject.Tests/RealmTests/RealmDependencyOne.cs @@ -2,18 +2,20 @@ namespace SutProject.Tests.RealmTests; -[TransientService(As = typeof(IDependencyOne), Realm = typeof(FirstRealmModule), Using = RegistrationType.Add)] -public class RealmDependencyOne : IDependencyOne { - public RealmDependencyOne(ISingletonService singletonService, IScopedService scopedService) { +[TransientService( + As = typeof(IDependencyOne), + Realm = typeof(FirstRealmModule), + Using = RegistrationType.Add +)] +public class RealmDependencyOne : IDependencyOne +{ + public RealmDependencyOne(ISingletonService singletonService, IScopedService scopedService) + { SingletonService = singletonService; ScopedService = scopedService; } - public ISingletonService SingletonService { - get; - } + public ISingletonService SingletonService { get; } - public IScopedService ScopedService { - get; - } -} \ No newline at end of file + public IScopedService ScopedService { get; } +} diff --git a/integ-tests/SutProject.Tests/RealmTests/RealmIsolationTests.cs b/integ-tests/SutProject.Tests/RealmTests/RealmIsolationTests.cs index 6f85a9c..af8fd4f 100644 --- a/integ-tests/SutProject.Tests/RealmTests/RealmIsolationTests.cs +++ b/integ-tests/SutProject.Tests/RealmTests/RealmIsolationTests.cs @@ -4,11 +4,13 @@ namespace SutProject.Tests.RealmTests; -public class RealmIsolationTests { +public class RealmIsolationTests +{ [ModuleTest] [FirstRealmModule] [SecondarySutModule] - public void OverrideDependencyWithRealm(IDependencyOne dependencyOne) { + public void OverrideDependencyWithRealm(IDependencyOne dependencyOne) + { Assert.IsType(dependencyOne); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/RegistrationTypeTests/ReplaceTests.cs b/integ-tests/SutProject.Tests/RegistrationTypeTests/ReplaceTests.cs index df2c9c2..2d3e376 100644 --- a/integ-tests/SutProject.Tests/RegistrationTypeTests/ReplaceTests.cs +++ b/integ-tests/SutProject.Tests/RegistrationTypeTests/ReplaceTests.cs @@ -6,33 +6,30 @@ namespace SutProject.Tests.RegistrationTypeTests; [DependencyModule(OnlyRealm = true)] [SutModule] -public partial class ReplaceModule { - -} +public partial class ReplaceModule { } [SingletonService(Using = RegistrationType.Replace, Realm = typeof(ReplaceModule))] -public class ReplaceDependency : IDependencyOne { - - public ReplaceDependency(ISingletonService singletonService, IScopedService scopedService) { +public class ReplaceDependency : IDependencyOne +{ + public ReplaceDependency(ISingletonService singletonService, IScopedService scopedService) + { SingletonService = singletonService; ScopedService = scopedService; } - public ISingletonService SingletonService { - get; - } + public ISingletonService SingletonService { get; } - public IScopedService ScopedService { - get; - } + public IScopedService ScopedService { get; } } -public class ReplaceTests { +public class ReplaceTests +{ [ModuleTest] [ReplaceModule] - public void ReplaceTest(IEnumerable dependencies) { + public void ReplaceTest(IEnumerable dependencies) + { var dependenciesList = dependencies.ToList(); Assert.Single(dependenciesList); Assert.IsType(dependenciesList[0]); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/RegistrationTypeTests/TryRegistrationTests.cs b/integ-tests/SutProject.Tests/RegistrationTypeTests/TryRegistrationTests.cs index 9b87438..201c3ea 100644 --- a/integ-tests/SutProject.Tests/RegistrationTypeTests/TryRegistrationTests.cs +++ b/integ-tests/SutProject.Tests/RegistrationTypeTests/TryRegistrationTests.cs @@ -17,37 +17,36 @@ public partial class TryAtModuleLevelWithSutModule; [SingletonService(Using = RegistrationType.Try, Realm = typeof(TryWithSutModule))] [SingletonService(Using = RegistrationType.Try, Realm = typeof(TryWithoutSutModule))] -#pragma warning disable CS8618 -public class TryDependency : IDependencyOne { +#pragma warning disable CS8618 +public class TryDependency : IDependencyOne +{ + public ISingletonService SingletonService { get; } - public ISingletonService SingletonService { - get; - } - - public IScopedService ScopedService { - get; - } + public IScopedService ScopedService { get; } } - #pragma warning restore CS8618 -public class TryRegistrationTests { +public class TryRegistrationTests +{ [ModuleTest] [TryWithSutModule] - public void TryWithSut(IDependencyOne service) { + public void TryWithSut(IDependencyOne service) + { Assert.IsType(service); } [ModuleTest] [TryWithoutSutModule] - public void TryWithoutSut(IDependencyOne service) { + public void TryWithoutSut(IDependencyOne service) + { Assert.IsType(service); } [ModuleTest] [TryAtModuleLevelWithSutModule] - public void TryModuleLevelSut(IDependencyOne service) { + public void TryModuleLevelSut(IDependencyOne service) + { Assert.IsType(service); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/StandardTests/AttributeTest.cs b/integ-tests/SutProject.Tests/StandardTests/AttributeTest.cs index c651c13..07b755f 100644 --- a/integ-tests/SutProject.Tests/StandardTests/AttributeTest.cs +++ b/integ-tests/SutProject.Tests/StandardTests/AttributeTest.cs @@ -4,33 +4,27 @@ namespace SutProject.Tests.StandardTests; -public interface ICustomAttributeInterface { - -} +public interface ICustomAttributeInterface { } -public partial class AttributeTestModuleAttribute : ICustomAttributeInterface{ - -} +public partial class AttributeTestModuleAttribute : ICustomAttributeInterface { } [DependencyModule(OnlyRealm = true)] -public partial class AttributeTestModule { - -} +public partial class AttributeTestModule { } [DependencyModule] [AttributeTestModule] -public partial class SomeModule { - -} +public partial class SomeModule { } -public class AttributeTest { +public class AttributeTest +{ [Fact] - public void AttributePartialTest() { + public void AttributePartialTest() + { var attributeType = typeof(AttributeTestModuleAttribute); var interfaces = attributeType.GetInterfaces(); - + Assert.Contains(interfaces, i => i == typeof(ICustomAttributeInterface)); Assert.Contains(interfaces, i => i == typeof(IDependencyModuleProvider)); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/StandardTests/AutoRegistrationTest.cs b/integ-tests/SutProject.Tests/StandardTests/AutoRegistrationTest.cs index 69f24a4..b3b0681 100644 --- a/integ-tests/SutProject.Tests/StandardTests/AutoRegistrationTest.cs +++ b/integ-tests/SutProject.Tests/StandardTests/AutoRegistrationTest.cs @@ -6,21 +6,19 @@ namespace SutProject.Tests.StandardTests; [DependencyModule(OnlyRealm = true)] [SutModule] -public partial class AutoRegisterModule { - -} +public partial class AutoRegisterModule { } [SingletonService(Realm = typeof(AutoRegisterModule))] -public class InheritDependencyOne - (ISingletonService singletonService, IScopedService scopedService) - : DependencyOne(singletonService, scopedService), IDependencyOne { - -} +public class InheritDependencyOne(ISingletonService singletonService, IScopedService scopedService) + : DependencyOne(singletonService, scopedService), + IDependencyOne { } -public class AutoRegistrationTest { +public class AutoRegistrationTest +{ [ModuleTest] [AutoRegisterModule] - public void AutoRegisterClassWithInheritance(IDependencyOne dependencyOne) { + public void AutoRegisterClassWithInheritance(IDependencyOne dependencyOne) + { Assert.IsType(dependencyOne); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/StandardTests/DuplicateModules.cs b/integ-tests/SutProject.Tests/StandardTests/DuplicateModules.cs index ab1451c..89aff0b 100644 --- a/integ-tests/SutProject.Tests/StandardTests/DuplicateModules.cs +++ b/integ-tests/SutProject.Tests/StandardTests/DuplicateModules.cs @@ -20,4 +20,4 @@ public partial class CombinedModule; [TestModule] [TestModule2] [TestModule2] -public partial class DuplicateModule; \ No newline at end of file +public partial class DuplicateModule; diff --git a/integ-tests/SutProject.Tests/StandardTests/LifetimeSemanticsTests.cs b/integ-tests/SutProject.Tests/StandardTests/LifetimeSemanticsTests.cs index 02b1958..8f81d6e 100644 --- a/integ-tests/SutProject.Tests/StandardTests/LifetimeSemanticsTests.cs +++ b/integ-tests/SutProject.Tests/StandardTests/LifetimeSemanticsTests.cs @@ -14,11 +14,12 @@ namespace SutProject.Tests.StandardTests; /// Assert.Same(a, b) passes either way. Distinguishing them requires crossing a scope /// boundary, which is what these tests do. /// -public class LifetimeSemanticsTests { - +public class LifetimeSemanticsTests +{ [ModuleTest] [SutModule] - public void Singleton_IsTheSameInstanceAcrossScopes(IServiceProvider provider) { + public void Singleton_IsTheSameInstanceAcrossScopes(IServiceProvider provider) + { using var first = provider.CreateScope(); using var second = provider.CreateScope(); @@ -31,7 +32,8 @@ public void Singleton_IsTheSameInstanceAcrossScopes(IServiceProvider provider) { [ModuleTest] [SutModule] - public void Singleton_IsTheSameInstanceAsTheRootProviders(IServiceProvider provider) { + public void Singleton_IsTheSameInstanceAsTheRootProviders(IServiceProvider provider) + { var fromRoot = provider.GetService(); using var scope = provider.CreateScope(); @@ -41,7 +43,8 @@ public void Singleton_IsTheSameInstanceAsTheRootProviders(IServiceProvider provi [ModuleTest] [SutModule] - public void Scoped_IsSharedWithinAScope(IServiceProvider provider) { + public void Scoped_IsSharedWithinAScope(IServiceProvider provider) + { using var scope = provider.CreateScope(); var first = scope.ServiceProvider.GetService(); @@ -57,7 +60,8 @@ public void Scoped_IsSharedWithinAScope(IServiceProvider provider) { /// [ModuleTest] [SutModule] - public void Scoped_DiffersBetweenScopes(IServiceProvider provider) { + public void Scoped_DiffersBetweenScopes(IServiceProvider provider) + { using var first = provider.CreateScope(); using var second = provider.CreateScope(); @@ -74,7 +78,8 @@ public void Scoped_DiffersBetweenScopes(IServiceProvider provider) { /// [ModuleTest] [SutModule] - public void Transient_IsANewInstanceEveryResolution(IServiceProvider provider) { + public void Transient_IsANewInstanceEveryResolution(IServiceProvider provider) + { var first = provider.GetService(); var second = provider.GetService(); @@ -85,7 +90,8 @@ public void Transient_IsANewInstanceEveryResolution(IServiceProvider provider) { [ModuleTest] [SutModule] - public void Transient_IsANewInstanceWithinASingleScope(IServiceProvider provider) { + public void Transient_IsANewInstanceWithinASingleScope(IServiceProvider provider) + { using var scope = provider.CreateScope(); var first = scope.ServiceProvider.GetService(); @@ -96,21 +102,25 @@ public void Transient_IsANewInstanceWithinASingleScope(IServiceProvider provider [ModuleTest] [SutModule] - public void RegisteredLifetimes_MatchTheirAttributes(IServiceProvider provider) { + public void RegisteredLifetimes_MatchTheirAttributes(IServiceProvider provider) + { // Resolving proves the wiring; the descriptors prove the lifetime the generator chose. var collection = new ServiceCollection(); collection.AddModule(); Assert.Equal( ServiceLifetime.Singleton, - Assert.Single(collection, d => d.ServiceType == typeof(ISingletonService)).Lifetime); + Assert.Single(collection, d => d.ServiceType == typeof(ISingletonService)).Lifetime + ); Assert.Equal( ServiceLifetime.Scoped, - Assert.Single(collection, d => d.ServiceType == typeof(IScopedService)).Lifetime); + Assert.Single(collection, d => d.ServiceType == typeof(IScopedService)).Lifetime + ); Assert.Equal( ServiceLifetime.Transient, - Assert.Single(collection, d => d.ServiceType == typeof(IDependencyOne)).Lifetime); + Assert.Single(collection, d => d.ServiceType == typeof(IDependencyOne)).Lifetime + ); } } diff --git a/integ-tests/SutProject.Tests/StandardTests/ModuleDuplicationTests.cs b/integ-tests/SutProject.Tests/StandardTests/ModuleDuplicationTests.cs index e4b14a1..c7f17c7 100644 --- a/integ-tests/SutProject.Tests/StandardTests/ModuleDuplicationTests.cs +++ b/integ-tests/SutProject.Tests/StandardTests/ModuleDuplicationTests.cs @@ -5,49 +5,55 @@ namespace SutProject.Tests.StandardTests; -public class ModuleDuplicationTests { +public class ModuleDuplicationTests +{ [ModuleTest] [CombinedModule] - public void CombinedModuleTest(IEnumerable dependencies) { + public void CombinedModuleTest(IEnumerable dependencies) + { Assert.Single(dependencies); } - + [ModuleTest] [DuplicateModule] - public void DuplicateModuleTest(IEnumerable dependencies) { + public void DuplicateModuleTest(IEnumerable dependencies) + { Assert.Single(dependencies); } [ModuleTest] [CombinedModule] [DuplicateModule] - public void CombinedAndDuplicateModuleTest(IEnumerable dependencies) { + public void CombinedAndDuplicateModuleTest(IEnumerable dependencies) + { Assert.Single(dependencies); } [Fact] - public void CombinedModuleAddModules() { + public void CombinedModuleAddModules() + { var serviceCollection = new ServiceCollection(); serviceCollection.AddModules(new CombinedModule()); - + var serviceProvider = serviceCollection.BuildServiceProvider(); - + var dependencies = serviceProvider.GetServices(); - + Assert.Single(dependencies); } [Fact] - public void MultipleDuplicatesAddModules() { + public void MultipleDuplicatesAddModules() + { var serviceCollection = new ServiceCollection(); serviceCollection.AddModules(new CombinedModule(), new DuplicateModule()); - + var serviceProvider = serviceCollection.BuildServiceProvider(); - + var dependencies = serviceProvider.GetServices(); - + Assert.Single(dependencies); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/StandardTests/MultipleModulesTests.cs b/integ-tests/SutProject.Tests/StandardTests/MultipleModulesTests.cs index d76a7df..f8e65ec 100644 --- a/integ-tests/SutProject.Tests/StandardTests/MultipleModulesTests.cs +++ b/integ-tests/SutProject.Tests/StandardTests/MultipleModulesTests.cs @@ -6,51 +6,61 @@ namespace SutProject.Tests.StandardTests; -public class StringWrapper(string value) { +public class StringWrapper(string value) +{ public string Value { get; } = value; } [DependencyModule] -public partial class UniqueModule : IServiceCollectionConfiguration{ +public partial class UniqueModule : IServiceCollectionConfiguration +{ private readonly string _value; - public UniqueModule(string value) { + public UniqueModule(string value) + { _value = value; } - - public override bool Equals(object? obj) { - if (obj is UniqueModule other) { + + public override bool Equals(object? obj) + { + if (obj is UniqueModule other) + { return _value == other._value; } return false; } - - public override int GetHashCode() { + + public override int GetHashCode() + { return _value.GetHashCode(); } - public void ConfigureServices(IServiceCollection services) { + public void ConfigureServices(IServiceCollection services) + { services.AddTransient(_ => new StringWrapper(_value)); } } -public class MultipleModulesTests { +public class MultipleModulesTests +{ [ModuleTest] [UniqueModule("test-value")] [UniqueModule("test-value-2")] - public void MultipleModuleTest(IEnumerable wrappers) { + public void MultipleModuleTest(IEnumerable wrappers) + { var wrapperList = wrappers.ToList(); Assert.Equal(2, wrapperList.Count); Assert.Contains(wrapperList, wrapper => wrapper.Value == "test-value"); Assert.Contains(wrapperList, wrapper => wrapper.Value == "test-value-2"); } - + [ModuleTest] [UniqueModule("test-value")] [UniqueModule("test-value")] - public void SameMultipleModuleTest(IEnumerable wrappers) { + public void SameMultipleModuleTest(IEnumerable wrappers) + { var wrapperList = wrappers.ToList(); Assert.Single(wrapperList); Assert.Contains(wrapperList, wrapper => wrapper.Value == "test-value"); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/StandardTests/NoNamespaceTests.cs b/integ-tests/SutProject.Tests/StandardTests/NoNamespaceTests.cs index b823f35..411a5a0 100644 --- a/integ-tests/SutProject.Tests/StandardTests/NoNamespaceTests.cs +++ b/integ-tests/SutProject.Tests/StandardTests/NoNamespaceTests.cs @@ -4,33 +4,28 @@ using Xunit; [DependencyModule] -public partial class TestModule { - -} +public partial class TestModule { } [DependencyModule(OnlyRealm = true)] -public partial class NoNamespaceTestModule { - -} +public partial class NoNamespaceTestModule { } #pragma warning disable CS8618 [SingletonService(Realm = typeof(NoNamespaceTestModule))] -public class SomeDependency : IDependencyOne { - public ISingletonService SingletonService { - get; - } +public class SomeDependency : IDependencyOne +{ + public ISingletonService SingletonService { get; } - public IScopedService ScopedService { - get; - } + public IScopedService ScopedService { get; } } -#pragma warning restore CS8618 +#pragma warning restore CS8618 -public class NoNamespaceTests { +public class NoNamespaceTests +{ [ModuleTest] [NoNamespaceTestModule] - public void NoNamespaceTest(IDependencyOne dependency) { + public void NoNamespaceTest(IDependencyOne dependency) + { Assert.NotNull(dependency); Assert.IsType(dependency); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/StandardTests/RecordModuleModels.cs b/integ-tests/SutProject.Tests/StandardTests/RecordModuleModels.cs index fb6f455..2cf4b42 100644 --- a/integ-tests/SutProject.Tests/StandardTests/RecordModuleModels.cs +++ b/integ-tests/SutProject.Tests/StandardTests/RecordModuleModels.cs @@ -2,12 +2,14 @@ namespace SutProject.Tests.StandardTests; -public interface IRecordModuleService { +public interface IRecordModuleService +{ string Value { get; } } [SingletonService(Realm = typeof(RecordModule))] -public class RecordModuleService : IRecordModuleService { +public class RecordModuleService : IRecordModuleService +{ public string Value => "FromRecordModule"; } diff --git a/integ-tests/SutProject.Tests/StandardTests/RecordTests.cs b/integ-tests/SutProject.Tests/StandardTests/RecordTests.cs index 1e4577f..d51b70e 100644 --- a/integ-tests/SutProject.Tests/StandardTests/RecordTests.cs +++ b/integ-tests/SutProject.Tests/StandardTests/RecordTests.cs @@ -3,25 +3,30 @@ namespace SutProject.Tests.StandardTests; -public class RecordServiceTests { +public class RecordServiceTests +{ [ModuleTest] [SutModule] - public void ResolveRecordService(IRecordService recordService) { + public void ResolveRecordService(IRecordService recordService) + { Assert.NotNull(recordService); Assert.Equal("RecordService", recordService.GetName()); } [ModuleTest] [SutModule] - public void RecordServiceIsSingleton(IRecordService first, IRecordService second) { + public void RecordServiceIsSingleton(IRecordService first, IRecordService second) + { Assert.Same(first, second); } } -public class RecordModuleTests { +public class RecordModuleTests +{ [ModuleTest] [RecordModule] - public void ResolveServiceFromRecordModule(IRecordModuleService service) { + public void ResolveServiceFromRecordModule(IRecordModuleService service) + { Assert.NotNull(service); Assert.Equal("FromRecordModule", service.Value); } diff --git a/integ-tests/SutProject.Tests/StandardTests/SecondarySutProjectTests.cs b/integ-tests/SutProject.Tests/StandardTests/SecondarySutProjectTests.cs index fdc9d82..d5d48cb 100644 --- a/integ-tests/SutProject.Tests/StandardTests/SecondarySutProjectTests.cs +++ b/integ-tests/SutProject.Tests/StandardTests/SecondarySutProjectTests.cs @@ -4,10 +4,12 @@ namespace SutProject.Tests.StandardTests; -public class SecondarySutProjectTests { +public class SecondarySutProjectTests +{ [ModuleTest] [SecondarySutModule] - public void OverrideDependency(IDependencyOne dependencyOne) { + public void OverrideDependency(IDependencyOne dependencyOne) + { Assert.IsType(dependencyOne); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/StandardTests/SerializerTests.cs b/integ-tests/SutProject.Tests/StandardTests/SerializerTests.cs index b81429c..ef78070 100644 --- a/integ-tests/SutProject.Tests/StandardTests/SerializerTests.cs +++ b/integ-tests/SutProject.Tests/StandardTests/SerializerTests.cs @@ -5,12 +5,14 @@ namespace SutProject.Tests.StandardTests; -public class SerializerTests { +public class SerializerTests +{ [ModuleTest] [SerializerClasses] - public void LoadSerializer(IEnumerable resolvers) { + public void LoadSerializer(IEnumerable resolvers) + { var resolverList = resolvers.ToList(); - + Assert.Single(resolverList); Assert.IsType(resolverList.First()); } @@ -19,11 +21,11 @@ public void LoadSerializer(IEnumerable resolvers) { [SerializerClasses] [InlineData("A")] [InlineData("B")] - public void LoadKeyedASerializer(string key, IServiceProvider serviceProvider) { - var resolverList = - serviceProvider.GetKeyedServices(key).ToList(); - + public void LoadKeyedASerializer(string key, IServiceProvider serviceProvider) + { + var resolverList = serviceProvider.GetKeyedServices(key).ToList(); + Assert.Single(resolverList); Assert.EndsWith(key, resolverList[0].GetType().Name); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/StandardTests/SimpleResolveTests.cs b/integ-tests/SutProject.Tests/StandardTests/SimpleResolveTests.cs index 6774990..bc380d2 100644 --- a/integ-tests/SutProject.Tests/StandardTests/SimpleResolveTests.cs +++ b/integ-tests/SutProject.Tests/StandardTests/SimpleResolveTests.cs @@ -3,10 +3,12 @@ namespace SutProject.Tests.StandardTests; -public class SimpleResolveTests { +public class SimpleResolveTests +{ [ModuleTest] [SutModule] - public void SimpleTest(IDependencyOne dependencyOne) { + public void SimpleTest(IDependencyOne dependencyOne) + { Assert.NotNull(dependencyOne); Assert.NotNull(dependencyOne.SingletonService); Assert.NotNull(dependencyOne.ScopedService); @@ -14,8 +16,12 @@ public void SimpleTest(IDependencyOne dependencyOne) { [ModuleTest] [SutModule] - public void ResolveServiceProvider(IDependencyOne dependencyOne, IServiceProvider serviceProvider) { + public void ResolveServiceProvider( + IDependencyOne dependencyOne, + IServiceProvider serviceProvider + ) + { Assert.NotNull(dependencyOne); Assert.NotNull(serviceProvider); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/StandardTests/SingletonTests.cs b/integ-tests/SutProject.Tests/StandardTests/SingletonTests.cs index 9382c52..8c86473 100644 --- a/integ-tests/SutProject.Tests/StandardTests/SingletonTests.cs +++ b/integ-tests/SutProject.Tests/StandardTests/SingletonTests.cs @@ -3,12 +3,17 @@ namespace SutProject.Tests.StandardTests; -public class SingletonTests { +public class SingletonTests +{ [ModuleTest] [SutModule] - public void ResolveSingleton(ISingletonService singletonService, ISingletonService otherSingletonService) { + public void ResolveSingleton( + ISingletonService singletonService, + ISingletonService otherSingletonService + ) + { Assert.NotNull(singletonService); Assert.NotNull(otherSingletonService); Assert.Same(singletonService, otherSingletonService); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/TestFramework/ContainerLifetimeTests.cs b/integ-tests/SutProject.Tests/TestFramework/ContainerLifetimeTests.cs index f022f05..bc59209 100644 --- a/integ-tests/SutProject.Tests/TestFramework/ContainerLifetimeTests.cs +++ b/integ-tests/SutProject.Tests/TestFramework/ContainerLifetimeTests.cs @@ -8,22 +8,23 @@ namespace SutProject.Tests.TestFramework; public partial class LifetimeModule { } [ScopedService(Realm = typeof(LifetimeModule))] -public class TrackedService : IDisposable { - +public class TrackedService : IDisposable +{ private static int _next; public static readonly List Disposed = []; - public TrackedService() { + public TrackedService() + { Id = Interlocked.Increment(ref _next); } - public int Id { - get; - } + public int Id { get; } - public void Dispose() { - lock (Disposed) { + public void Dispose() + { + lock (Disposed) + { Disposed.Add(Id); } } @@ -46,21 +47,25 @@ public void Dispose() { /// are released together when the last row has run. /// /// -public class ContainerLifetimeTests { - +public class ContainerLifetimeTests +{ private static readonly object Sync = new(); private static readonly List Seen = []; [ModuleTest(typeof(LifetimeModule))] - public void TheContainerOfATestThatHasRunIsDisposed(TrackedService service) => AssertEarlierDisposed(service); + public void TheContainerOfATestThatHasRunIsDisposed(TrackedService service) => + AssertEarlierDisposed(service); [ModuleTest(typeof(LifetimeModule))] public void WhicheverOfTheTwoRanFirst(TrackedService service) => AssertEarlierDisposed(service); - private static void AssertEarlierDisposed(TrackedService current) { - lock (Sync) { - foreach (var earlier in Seen) { + private static void AssertEarlierDisposed(TrackedService current) + { + lock (Sync) + { + foreach (var earlier in Seen) + { Assert.Contains(earlier, TrackedService.Disposed); } diff --git a/integ-tests/SutProject.Tests/TestFramework/InjectValueTests.cs b/integ-tests/SutProject.Tests/TestFramework/InjectValueTests.cs index ea8a58c..d537f1e 100644 --- a/integ-tests/SutProject.Tests/TestFramework/InjectValueTests.cs +++ b/integ-tests/SutProject.Tests/TestFramework/InjectValueTests.cs @@ -6,13 +6,14 @@ namespace SutProject.Tests.TestFramework; public record InjectModel(IDependencyOne DependencyOne, string StringValue); -public class InjectValueTests { +public class InjectValueTests +{ [ModuleTest] [SutModule] - public void InjectTestValue( - [InjectValues("Hello World!")]InjectModel injectModel) { + public void InjectTestValue([InjectValues("Hello World!")] InjectModel injectModel) + { Assert.NotNull(injectModel); Assert.NotNull(injectModel.DependencyOne); Assert.Equal("Hello World!", injectModel.StringValue); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/TestFramework/ModuleTestCaseTests.cs b/integ-tests/SutProject.Tests/TestFramework/ModuleTestCaseTests.cs index 48a73c2..4804c5b 100644 --- a/integ-tests/SutProject.Tests/TestFramework/ModuleTestCaseTests.cs +++ b/integ-tests/SutProject.Tests/TestFramework/ModuleTestCaseTests.cs @@ -8,26 +8,32 @@ namespace SutProject.Tests.TestFramework; -public class AssemblyTestCaseTests { +public class AssemblyTestCaseTests +{ [ModuleTest] - public void AssemblyTest(ITestRealmService service) { + public void AssemblyTest(ITestRealmService service) + { Assert.IsType(service); } } [ClassLevelModule] -public class ClassTestCaseTests { +public class ClassTestCaseTests +{ [ModuleTest] - public void ClassTest(ITestRealmService service) { + public void ClassTest(ITestRealmService service) + { Assert.IsType(service); } } [ClassLevelModule] -public class MethodTestCaseTests { +public class MethodTestCaseTests +{ [ModuleTest] [MethodLevelModule] - public void MethodTest(ITestRealmService service) { + public void MethodTest(ITestRealmService service) + { Assert.IsType(service); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject.Tests/TestFramework/TestCaseModels.cs b/integ-tests/SutProject.Tests/TestFramework/TestCaseModels.cs index b7759ae..5d90fa0 100644 --- a/integ-tests/SutProject.Tests/TestFramework/TestCaseModels.cs +++ b/integ-tests/SutProject.Tests/TestFramework/TestCaseModels.cs @@ -2,18 +2,11 @@ namespace SutProject.Tests.TestFramework; - [SingletonService(Realm = typeof(AssemblyLevelModule))] -public class AssemblyTestCaseService : ITestRealmService { - -} +public class AssemblyTestCaseService : ITestRealmService { } [SingletonService(Realm = typeof(ClassLevelModule))] -public class ClassTestCaseService : ITestRealmService { - -} +public class ClassTestCaseService : ITestRealmService { } [SingletonService(Realm = typeof(MethodLevelModule))] -public class MethodTestCaseService : ITestRealmService { - -} +public class MethodTestCaseService : ITestRealmService { } diff --git a/integ-tests/SutProject.Tests/TestFramework/TestModules.cs b/integ-tests/SutProject.Tests/TestFramework/TestModules.cs index 1aa0a2e..9eca3fa 100644 --- a/integ-tests/SutProject.Tests/TestFramework/TestModules.cs +++ b/integ-tests/SutProject.Tests/TestFramework/TestModules.cs @@ -5,11 +5,8 @@ namespace SutProject.Tests.TestFramework; [DependencyModule(OnlyRealm = true)] public partial class AssemblyLevelModule { } - [DependencyModule(OnlyRealm = true)] -public partial class ClassLevelModule { - -} +public partial class ClassLevelModule { } [DependencyModule(OnlyRealm = true)] -public partial class MethodLevelModule { } \ No newline at end of file +public partial class MethodLevelModule { } diff --git a/integ-tests/SutProject.Tests/UseMethod/UseMethodTests.cs b/integ-tests/SutProject.Tests/UseMethod/UseMethodTests.cs index 790e0c1..5323aa9 100644 --- a/integ-tests/SutProject.Tests/UseMethod/UseMethodTests.cs +++ b/integ-tests/SutProject.Tests/UseMethod/UseMethodTests.cs @@ -5,22 +5,25 @@ namespace SutProject.Tests.UseMethod; [DependencyModule(GenerateUseMethod = "UseMethodModule", OnlyRealm = true)] -public partial class UseMethodModule(string name) { +public partial class UseMethodModule(string name) +{ public string Name => name; } [SingletonService(Realm = typeof(UseMethodModule))] public class SomeImplementation; -public class UseMethodTests { +public class UseMethodTests +{ [Fact] - public void UseMethodTest() { + public void UseMethodTest() + { var serviceCollection = new ServiceCollection(); serviceCollection.UseMethodModule("testMethod"); - + var serviceProvider = serviceCollection.BuildServiceProvider(); - + var instance = serviceProvider.GetService(); Assert.NotNull(instance); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject/DependencyOne.cs b/integ-tests/SutProject/DependencyOne.cs index d75c8b4..8381ce0 100644 --- a/integ-tests/SutProject/DependencyOne.cs +++ b/integ-tests/SutProject/DependencyOne.cs @@ -2,16 +2,16 @@ namespace SutProject; -public interface IDependencyOne { +public interface IDependencyOne +{ ISingletonService SingletonService { get; } IScopedService ScopedService { get; } } [TransientService] -public class DependencyOne( - ISingletonService singletonService, - IScopedService scopedService) : IDependencyOne { - +public class DependencyOne(ISingletonService singletonService, IScopedService scopedService) + : IDependencyOne +{ public ISingletonService SingletonService { get; } = singletonService; public IScopedService ScopedService { get; } = scopedService; -} \ No newline at end of file +} diff --git a/integ-tests/SutProject/GenericClass.cs b/integ-tests/SutProject/GenericClass.cs index 31dbaef..13d5f63 100644 --- a/integ-tests/SutProject/GenericClass.cs +++ b/integ-tests/SutProject/GenericClass.cs @@ -2,19 +2,19 @@ namespace SutProject; -public interface IGenericInterface { +public interface IGenericInterface +{ T Value { get; } } [SingletonService] -public class GenericClass(T value) : IGenericInterface { - public T Value { - get; - } = value; +public class GenericClass(T value) : IGenericInterface +{ + public T Value { get; } = value; } [SingletonService] -public class StringGeneric : IGenericInterface { - +public class StringGeneric : IGenericInterface +{ public string Value => "Hello World"; -} \ No newline at end of file +} diff --git a/integ-tests/SutProject/KeyedService.cs b/integ-tests/SutProject/KeyedService.cs index 29bb734..1a97b80 100644 --- a/integ-tests/SutProject/KeyedService.cs +++ b/integ-tests/SutProject/KeyedService.cs @@ -4,6 +4,4 @@ namespace SutProject; [TransientService(Key = Constants.StringValue)] -public class KeyedService { - -} \ No newline at end of file +public class KeyedService { } diff --git a/integ-tests/SutProject/RealmRegistrationService.cs b/integ-tests/SutProject/RealmRegistrationService.cs index fa004b4..3227afd 100644 --- a/integ-tests/SutProject/RealmRegistrationService.cs +++ b/integ-tests/SutProject/RealmRegistrationService.cs @@ -3,4 +3,4 @@ namespace SutProject; [SingletonService(Realm = typeof(SutRealmModule))] -public class RealmRegistrationService { } \ No newline at end of file +public class RealmRegistrationService { } diff --git a/integ-tests/SutProject/RecordService.cs b/integ-tests/SutProject/RecordService.cs index d7982d9..2bd2511 100644 --- a/integ-tests/SutProject/RecordService.cs +++ b/integ-tests/SutProject/RecordService.cs @@ -2,13 +2,16 @@ namespace SutProject; -public interface IRecordService { +public interface IRecordService +{ string GetName(); } [SingletonService] -public record RecordService : IRecordService { - public string GetName() { +public record RecordService : IRecordService +{ + public string GetName() + { return nameof(RecordService); } } diff --git a/integ-tests/SutProject/ScopedService.cs b/integ-tests/SutProject/ScopedService.cs index 567de8c..cea835f 100644 --- a/integ-tests/SutProject/ScopedService.cs +++ b/integ-tests/SutProject/ScopedService.cs @@ -5,4 +5,4 @@ namespace SutProject; public interface IScopedService { } [ScopedService] -public class ScopedService : IScopedService { } \ No newline at end of file +public class ScopedService : IScopedService { } diff --git a/integ-tests/SutProject/SerializerClasses.cs b/integ-tests/SutProject/SerializerClasses.cs index 70cfa94..4300e31 100644 --- a/integ-tests/SutProject/SerializerClasses.cs +++ b/integ-tests/SutProject/SerializerClasses.cs @@ -4,9 +4,7 @@ namespace SutProject; [DependencyModule(RegisterJsonSerializers = true)] -public partial class SerializerClasses { - -} +public partial class SerializerClasses { } public record SerialA(string A, string B); @@ -17,14 +15,12 @@ public record SerialB(string A, string B); [JsonSerializable(typeof(SerialB))] public partial class SerializerContext : JsonSerializerContext; - [JsonSourceGenerationOptions] [JsonSerializable(typeof(SerialA))] [TransientService(Key = "A", Realm = typeof(SerializerClasses))] public partial class SerializerContextA : JsonSerializerContext; - [JsonSourceGenerationOptions] [JsonSerializable(typeof(SerialA))] [TransientService(Key = "B", Realm = typeof(SerializerClasses))] -public partial class SerializerContextB : JsonSerializerContext; \ No newline at end of file +public partial class SerializerContextB : JsonSerializerContext; diff --git a/integ-tests/SutProject/SingletonService.cs b/integ-tests/SutProject/SingletonService.cs index 6d556e7..995dbd4 100644 --- a/integ-tests/SutProject/SingletonService.cs +++ b/integ-tests/SutProject/SingletonService.cs @@ -2,14 +2,16 @@ namespace SutProject; -public interface ISingletonService { +public interface ISingletonService +{ string GetName(); } [SingletonService] -public class SingletonService : ISingletonService { - - public string GetName() { +public class SingletonService : ISingletonService +{ + public string GetName() + { return nameof(SingletonService); } -} \ No newline at end of file +} diff --git a/integ-tests/SutProject/SubDirectory/Constants.cs b/integ-tests/SutProject/SubDirectory/Constants.cs index 0b1d404..8d45d4d 100644 --- a/integ-tests/SutProject/SubDirectory/Constants.cs +++ b/integ-tests/SutProject/SubDirectory/Constants.cs @@ -1,5 +1,6 @@ namespace SutProject.SubDirectory; -public class Constants { +public class Constants +{ public const string StringValue = "StringValue"; -} \ No newline at end of file +} diff --git a/integ-tests/SutProject/SutModule.cs b/integ-tests/SutProject/SutModule.cs index 654bc15..cc90532 100644 --- a/integ-tests/SutProject/SutModule.cs +++ b/integ-tests/SutProject/SutModule.cs @@ -3,8 +3,7 @@ namespace SutProject; [DependencyModule] -public partial class SutModule { - public static void Run() { - - } -} \ No newline at end of file +public partial class SutModule +{ + public static void Run() { } +} diff --git a/integ-tests/SutProject/SutRealmModule.cs b/integ-tests/SutProject/SutRealmModule.cs index 3f6fa45..45d2129 100644 --- a/integ-tests/SutProject/SutRealmModule.cs +++ b/integ-tests/SutProject/SutRealmModule.cs @@ -3,5 +3,4 @@ namespace SutProject; [DependencyModule(OnlyRealm = true)] -public partial class SutRealmModule { -} \ No newline at end of file +public partial class SutRealmModule { } diff --git a/integ-tests/SutProject/TestRealmModule.cs b/integ-tests/SutProject/TestRealmModule.cs index a96a9ec..dcf2cb7 100644 --- a/integ-tests/SutProject/TestRealmModule.cs +++ b/integ-tests/SutProject/TestRealmModule.cs @@ -5,9 +5,7 @@ namespace SutProject; [DependencyModule(OnlyRealm = true)] public partial class TestRealmModule { } -public interface ITestRealmService { - -} +public interface ITestRealmService { } [SingletonService(Realm = typeof(TestRealmModule))] -public class TestRealmService : ITestRealmService { } \ No newline at end of file +public class TestRealmService : ITestRealmService { } diff --git a/integ-tests/web/WebApiApp.Tests/Bootstrap.cs b/integ-tests/web/WebApiApp.Tests/Bootstrap.cs index ba4dff6..2ef066d 100644 --- a/integ-tests/web/WebApiApp.Tests/Bootstrap.cs +++ b/integ-tests/web/WebApiApp.Tests/Bootstrap.cs @@ -2,4 +2,4 @@ using WebApiApp; [assembly: NSubstituteSupport] -[assembly: ApplicationModule] \ No newline at end of file +[assembly: ApplicationModule] diff --git a/integ-tests/web/WebApiApp.Tests/WeatherTests.cs b/integ-tests/web/WebApiApp.Tests/WeatherTests.cs index 5ff7970..e306456 100644 --- a/integ-tests/web/WebApiApp.Tests/WeatherTests.cs +++ b/integ-tests/web/WebApiApp.Tests/WeatherTests.cs @@ -5,28 +5,33 @@ namespace WebApiApp.Tests; -public class WeatherTests { +public class WeatherTests +{ [ModuleTest] - public void GetForecast(Weather weather) { + public void GetForecast(Weather weather) + { var response = weather.GetWeatherForecast().ToArray(); - + Assert.Equal(5, response.Length); } [ModuleTest] public void GetStaticForecast( - Weather weather, + Weather weather, [Mock] ITemperatureProvider temperatureProvider, - [Mock] IAiSummaryProvider aiSummaryProvider) { + [Mock] IAiSummaryProvider aiSummaryProvider + ) + { temperatureProvider.GetTemperature().Returns(38); aiSummaryProvider.GetSummary().Returns("Sunny"); - + var response = weather.GetWeatherForecast().ToArray(); Assert.Equal(5, response.Length); - foreach (var weatherForecast in response) { + foreach (var weatherForecast in response) + { Assert.Equal(38, weatherForecast.TemperatureC); Assert.Equal("Sunny", weatherForecast.Summary); } } -} \ No newline at end of file +} diff --git a/integ-tests/web/WebApiApp/AiSummaryProvider.cs b/integ-tests/web/WebApiApp/AiSummaryProvider.cs index 26099ff..32233dc 100644 --- a/integ-tests/web/WebApiApp/AiSummaryProvider.cs +++ b/integ-tests/web/WebApiApp/AiSummaryProvider.cs @@ -2,18 +2,30 @@ namespace WebApiApp; -public interface IAiSummaryProvider { +public interface IAiSummaryProvider +{ string GetSummary(); } [SingletonService] -public class AiSummaryProvider : IAiSummaryProvider { - - private static string[] summaries = new[] { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +public class AiSummaryProvider : IAiSummaryProvider +{ + private static string[] summaries = new[] + { + "Freezing", + "Bracing", + "Chilly", + "Cool", + "Mild", + "Warm", + "Balmy", + "Hot", + "Sweltering", + "Scorching", }; - - public string GetSummary() { + + public string GetSummary() + { return summaries[Random.Shared.Next(summaries.Length)]; } -} \ No newline at end of file +} diff --git a/integ-tests/web/WebApiApp/Program.cs b/integ-tests/web/WebApiApp/Program.cs index 6f7abf7..012b0ed 100644 --- a/integ-tests/web/WebApiApp/Program.cs +++ b/integ-tests/web/WebApiApp/Program.cs @@ -15,4 +15,3 @@ .WithName("GetWeatherForecast"); app.Run(); - diff --git a/integ-tests/web/WebApiApp/SummaryProvider.cs b/integ-tests/web/WebApiApp/SummaryProvider.cs index b8291a8..4e993cd 100644 --- a/integ-tests/web/WebApiApp/SummaryProvider.cs +++ b/integ-tests/web/WebApiApp/SummaryProvider.cs @@ -2,14 +2,16 @@ namespace WebApiApp; -public interface ISummaryProvider { +public interface ISummaryProvider +{ string GetSummary(); } [SingletonService] -public class SummaryProvider(IAiSummaryProvider aiSummaryProvider) : ISummaryProvider { - - public string GetSummary() { +public class SummaryProvider(IAiSummaryProvider aiSummaryProvider) : ISummaryProvider +{ + public string GetSummary() + { return aiSummaryProvider.GetSummary(); } -} \ No newline at end of file +} diff --git a/integ-tests/web/WebApiApp/TemperatureProvider.cs b/integ-tests/web/WebApiApp/TemperatureProvider.cs index a7a3691..94a72ee 100644 --- a/integ-tests/web/WebApiApp/TemperatureProvider.cs +++ b/integ-tests/web/WebApiApp/TemperatureProvider.cs @@ -2,14 +2,16 @@ namespace WebApiApp; -public interface ITemperatureProvider { +public interface ITemperatureProvider +{ int GetTemperature(); -} +} [SingletonService] -public class TemperatureProvider : ITemperatureProvider { - - public int GetTemperature() { +public class TemperatureProvider : ITemperatureProvider +{ + public int GetTemperature() + { return Random.Shared.Next(-20, 55); } -} \ No newline at end of file +} diff --git a/integ-tests/web/WebApiApp/Weather.cs b/integ-tests/web/WebApiApp/Weather.cs index f5acd57..9476eba 100644 --- a/integ-tests/web/WebApiApp/Weather.cs +++ b/integ-tests/web/WebApiApp/Weather.cs @@ -3,19 +3,18 @@ namespace WebApiApp; [SingletonService] -public class Weather( - ISummaryProvider summaryProvider, - ITemperatureProvider temperatureProvider) { - - public IEnumerable GetWeatherForecast() { - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - temperatureProvider.GetTemperature(), - summaryProvider.GetSummary() - )) +public class Weather(ISummaryProvider summaryProvider, ITemperatureProvider temperatureProvider) +{ + public IEnumerable GetWeatherForecast() + { + var forecast = Enumerable + .Range(1, 5) + .Select(index => new WeatherForecast( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + temperatureProvider.GetTemperature(), + summaryProvider.GetSummary() + )) .ToArray(); return forecast; } -} \ No newline at end of file +} diff --git a/integ-tests/web/WebApiApp/WeatherForecast.cs b/integ-tests/web/WebApiApp/WeatherForecast.cs index 162deb2..c9d828d 100644 --- a/integ-tests/web/WebApiApp/WeatherForecast.cs +++ b/integ-tests/web/WebApiApp/WeatherForecast.cs @@ -1,5 +1,6 @@ namespace WebApiApp; -public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { +public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} \ No newline at end of file +} diff --git a/src/DependencyModules.FakeItEasy/FakeItEasySupportAttribute.cs b/src/DependencyModules.FakeItEasy/FakeItEasySupportAttribute.cs index 70d466f..fe9c2dc 100644 --- a/src/DependencyModules.FakeItEasy/FakeItEasySupportAttribute.cs +++ b/src/DependencyModules.FakeItEasy/FakeItEasySupportAttribute.cs @@ -22,8 +22,8 @@ namespace DependencyModules.FakeItEasy; /// /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)] -public class FakeItEasySupportAttribute : Attribute, IMockSupportAttribute { - +public class FakeItEasySupportAttribute : Attribute, IMockSupportAttribute +{ /// /// Provides a fake of the specified type. /// @@ -33,7 +33,8 @@ public class FakeItEasySupportAttribute : Attribute, IMockSupportAttribute { /// /// The type to fake. /// A fake implementing . - public object ProvideMock(Type type) { + public object ProvideMock(Type type) + { return global::FakeItEasy.Sdk.Create.Fake(type); } } diff --git a/src/DependencyModules.Moq/MoqSupportAttribute.cs b/src/DependencyModules.Moq/MoqSupportAttribute.cs index cd0584f..2d21609 100644 --- a/src/DependencyModules.Moq/MoqSupportAttribute.cs +++ b/src/DependencyModules.Moq/MoqSupportAttribute.cs @@ -31,8 +31,8 @@ namespace DependencyModules.Moq; /// /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)] -public class MoqSupportAttribute : Attribute, IMockSupportAttribute, ITestServiceSetupAttribute { - +public class MoqSupportAttribute : Attribute, IMockSupportAttribute, ITestServiceSetupAttribute +{ /// /// Registers a mock for every Mock<T> the test asked for, alongside the object that /// mock produces. @@ -54,14 +54,22 @@ public class MoqSupportAttribute : Attribute, IMockSupportAttribute, ITestServic /// /// The test the container is being built for. /// The collection backing the test's container. - public void SetupServiceCollection(ITestMethodContext testMethod, IServiceCollection serviceCollection) { + public void SetupServiceCollection( + ITestMethodContext testMethod, + IServiceCollection serviceCollection + ) + { var mocked = new HashSet(); - foreach (var parameter in testMethod.Method.GetParameters()) { + foreach (var parameter in testMethod.Method.GetParameters()) + { // Add returns false for a type already handled: two parameters naming the same // Mock are one mock, so configuring either is configuring what the test was given. - if (!TryGetMockedType(parameter.ParameterType, out var mockedType) || - !mocked.Add(mockedType)) { + if ( + !TryGetMockedType(parameter.ParameterType, out var mockedType) + || !mocked.Add(mockedType) + ) + { continue; } @@ -89,8 +97,10 @@ public void SetupServiceCollection(ITestMethodContext testMethod, IServiceCollec /// The mocked instance — Mock<T>.Object — or a Mock<T> when that is /// what was asked for. /// - public object ProvideMock(Type type) { - if (TryGetMockedType(type, out var mockedType)) { + public object ProvideMock(Type type) + { + if (TryGetMockedType(type, out var mockedType)) + { return CreateMock(mockedType); } @@ -107,13 +117,17 @@ public object ProvideMock(Type type) { /// [TestExport] naming the same service — and without this it would also beat the pairing here, /// leaving the test configuring one mock while the container handed out another. /// - public bool RegistersService(ITestMethodContext testMethod, Type serviceType) { - foreach (var parameter in testMethod.Method.GetParameters()) { - if (!TryGetMockedType(parameter.ParameterType, out var mockedType)) { + public bool RegistersService(ITestMethodContext testMethod, Type serviceType) + { + foreach (var parameter in testMethod.Method.GetParameters()) + { + if (!TryGetMockedType(parameter.ParameterType, out var mockedType)) + { continue; } - if (parameter.ParameterType == serviceType || mockedType == serviceType) { + if (parameter.ParameterType == serviceType || mockedType == serviceType) + { return true; } } @@ -127,9 +141,16 @@ private static MoqLib.Mock CreateMock(Type type) => /// /// Reads IFoo out of a Mock<IFoo>, and reports anything else as not ours. /// - private static bool TryGetMockedType(Type parameterType, [NotNullWhen(true)] out Type? mockedType) { - if (parameterType.IsGenericType && - parameterType.GetGenericTypeDefinition() == typeof(MoqLib.Mock<>)) { + private static bool TryGetMockedType( + Type parameterType, + [NotNullWhen(true)] out Type? mockedType + ) + { + if ( + parameterType.IsGenericType + && parameterType.GetGenericTypeDefinition() == typeof(MoqLib.Mock<>) + ) + { mockedType = parameterType.GetGenericArguments()[0]; return true; } diff --git a/src/DependencyModules.NSubstitute/NSubstituteSupportAttribute.cs b/src/DependencyModules.NSubstitute/NSubstituteSupportAttribute.cs index a3fea15..24f7614 100644 --- a/src/DependencyModules.NSubstitute/NSubstituteSupportAttribute.cs +++ b/src/DependencyModules.NSubstitute/NSubstituteSupportAttribute.cs @@ -24,14 +24,15 @@ namespace DependencyModules.NSubstitute; /// /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)] -public class NSubstituteSupportAttribute : Attribute, IMockSupportAttribute { - +public class NSubstituteSupportAttribute : Attribute, IMockSupportAttribute +{ /// /// Provides a substitute for the specified type. /// /// The type to substitute for. /// A substitute implementing . - public object ProvideMock(Type type) { + public object ProvideMock(Type type) + { return NSub.Substitute.For([type], []); } } diff --git a/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs b/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs index b441246..bf41e3c 100644 --- a/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs +++ b/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs @@ -35,8 +35,13 @@ namespace DependencyModules.NUnit.Attributes; /// /// [AttributeUsage(AttributeTargets.Method)] -public class ModuleTestAttribute : Attribute, ITestBuilder, IWrapSetUpTearDown, IImplyFixture, IModuleTestAttribute { - +public class ModuleTestAttribute + : Attribute, + ITestBuilder, + IWrapSetUpTearDown, + IImplyFixture, + IModuleTestAttribute +{ /// /// Where a row's arguments are stashed between building the test case and executing it. /// @@ -57,14 +62,13 @@ public class ModuleTestAttribute : Attribute, ITestBuilder, IWrapSetUpTearDown, /// parameters after a params array. NUnit takes navigation from the assembly's symbols instead, /// so nothing is lost by taking the params form here. /// - public ModuleTestAttribute(params Type[] modules) { + public ModuleTestAttribute(params Type[] modules) + { ModuleTypes = modules; } /// - public Type[] ModuleTypes { - get; - } + public Type[] ModuleTypes { get; } /// /// Builds one test case per data row, or a single case when the method has no rows. @@ -75,30 +79,42 @@ public Type[] ModuleTypes { /// this has to satisfy is NUnit's arity check, which an array of the right length does; the real /// arguments are written into that array at execution time, once there is a container. /// - public IEnumerable BuildFrom(IMethodInfo method, Test? suite) { + public IEnumerable BuildFrom(IMethodInfo method, Test? suite) + { var parameterCount = method.GetParameters().Length; - var rows = method.MethodInfo.GetCustomAttributes(false) + var rows = method + .MethodInfo.GetCustomAttributes(false) .OfType() .SelectMany(dataAttribute => dataAttribute.GetRows(method.MethodInfo)) .ToArray(); - if (rows.Length == 0) { - yield return BuildTestMethod(method, suite, new object?[parameterCount], null, method.Name); + if (rows.Length == 0) + { + yield return BuildTestMethod( + method, + suite, + new object?[parameterCount], + null, + method.Name + ); yield break; } - var names = method.MethodInfo.GetCustomAttributes(false) + var names = method + .MethodInfo.GetCustomAttributes(false) .OfType() .Select(attribute => attribute.TestName) .ToArray(); - for (var i = 0; i < rows.Length; i++) { + for (var i = 0; i < rows.Length; i++) + { var row = rows[i]; var arguments = new object?[parameterCount]; - if (row.Length <= parameterCount) { + if (row.Length <= parameterCount) + { Array.Copy(row, arguments, row.Length); } @@ -106,15 +122,17 @@ public IEnumerable BuildFrom(IMethodInfo method, Test? suite) { var testMethod = BuildTestMethod(method, suite, arguments, row, testName); - if (row.Length > parameterCount) { + if (row.Length > parameterCount) + { // Reported as a failing test rather than thrown, so one bad row names itself instead // of taking down discovery for the whole fixture. testMethod.RunState = RunState.NotRunnable; testMethod.Properties.Set( PropertyNames.SkipReason, - $"[ModuleTestCase] supplied {row.Length} arguments to a method taking " + - $"{parameterCount}. A row may supply fewer than the method takes — the remaining " + - "parameters are resolved from the container — but not more."); + $"[ModuleTestCase] supplied {row.Length} arguments to a method taking " + + $"{parameterCount}. A row may supply fewer than the method takes — the remaining " + + "parameters are resolved from the container — but not more." + ); } yield return testMethod; @@ -125,12 +143,19 @@ public IEnumerable BuildFrom(IMethodInfo method, Test? suite) { public TestCommand Wrap(TestCommand command) => new ModuleTestCommand(command); private static TestMethod BuildTestMethod( - IMethodInfo method, Test? suite, object?[] arguments, object?[]? row, string testName) { + IMethodInfo method, + Test? suite, + object?[] arguments, + object?[]? row, + string testName + ) + { var parameters = new TestCaseParameters(arguments) { TestName = testName }; var testMethod = new NUnitTestCaseBuilder().BuildTestMethod(method, suite, parameters); - if (row != null) { + if (row != null) + { testMethod.Properties.Set(RowPropertyName, row); } @@ -153,11 +178,12 @@ private static string DisplayName(string methodName, object?[] row) => /// keeps a name that a test explorer filters on from changing with the machine's locale. /// private static string FormatArgument(object? argument) => - argument switch { + argument switch + { null => "null", string text => $"\"{text}\"", char character => $"'{character}'", IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), - _ => argument.ToString() ?? string.Empty + _ => argument.ToString() ?? string.Empty, }; } diff --git a/src/DependencyModules.NUnit/Attributes/ModuleTestCaseAttribute.cs b/src/DependencyModules.NUnit/Attributes/ModuleTestCaseAttribute.cs index c6831c8..9668e66 100644 --- a/src/DependencyModules.NUnit/Attributes/ModuleTestCaseAttribute.cs +++ b/src/DependencyModules.NUnit/Attributes/ModuleTestCaseAttribute.cs @@ -10,8 +10,8 @@ namespace DependencyModules.NUnit.Attributes; /// row returned here, so a source of rows — a member, a file, a generator — only has to implement /// this to become usable. /// -public interface IModuleTestDataAttribute { - +public interface IModuleTestDataAttribute +{ /// /// The rows to build test cases from. A row covers the leading parameters of the method; the /// rest are resolved from the test's container. @@ -46,23 +46,20 @@ public interface IModuleTestDataAttribute { /// /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -public class ModuleTestCaseAttribute(params object?[] arguments) : Attribute, IModuleTestDataAttribute { - +public class ModuleTestCaseAttribute(params object?[] arguments) + : Attribute, + IModuleTestDataAttribute +{ /// /// The arguments for this row, covering the method's leading parameters in order. /// - public object?[] Arguments { - get; - } = arguments; + public object?[] Arguments { get; } = arguments; /// /// Overrides the name this row is reported under. Defaults to the method name followed by the /// row's arguments, which is what tells one row from another in a test explorer. /// - public string? TestName { - get; - set; - } + public string? TestName { get; set; } /// public IEnumerable GetRows(MethodInfo method) => [Arguments]; diff --git a/src/DependencyModules.NUnit/Impl/ModuleTestCommand.cs b/src/DependencyModules.NUnit/Impl/ModuleTestCommand.cs index c60c9ae..69b92e8 100644 --- a/src/DependencyModules.NUnit/Impl/ModuleTestCommand.cs +++ b/src/DependencyModules.NUnit/Impl/ModuleTestCommand.cs @@ -22,10 +22,11 @@ namespace DependencyModules.NUnit.Impl; /// teardown, timeouts, expected exceptions and the rest working normally — this only has to make /// sure the arguments are in place before delegating. /// -public class ModuleTestCommand(TestCommand innerCommand) : DelegatingTestCommand(innerCommand) { - +public class ModuleTestCommand(TestCommand innerCommand) : DelegatingTestCommand(innerCommand) +{ /// - public override TestResult Execute(TestExecutionContext context) { + public override TestResult Execute(TestExecutionContext context) + { var testMethod = (TestMethod)Test; var method = testMethod.Method!.MethodInfo; @@ -58,13 +59,18 @@ public override TestResult Execute(TestExecutionContext context) { // beats a [TestExport] naming the same service. resolver.SetupServiceCollection(serviceCollection); - var serviceProvider = BuildServiceProvider(moduleContext, serviceCollection, knownAttributes); + var serviceProvider = BuildServiceProvider( + moduleContext, + serviceCollection, + knownAttributes + ); // Every container the case built, the first and any the source was asked for, disposed // together in the finally below. var providers = new List { serviceProvider }; - try { + try + { Start(moduleContext, knownAttributes, serviceProvider); // Named throughout: three of these are delegates of shapes that would happily bind to @@ -74,24 +80,30 @@ public override TestResult Execute(TestExecutionContext context) { pinned: serviceProvider, pinnedServices: SharedRegistrations.Collect(method, knownAttributes), build: services => BuildServiceProvider(moduleContext, services, knownAttributes), - start: built => { + start: built => + { Start(moduleContext, knownAttributes, built); return ValueTask.CompletedTask; }, - track: providers.Add); + track: providers.Add + ); var arguments = resolver .ResolveArgumentsAsync(serviceProvider, RowArguments(testMethod)) - .GetAwaiter().GetResult(); + .GetAwaiter() + .GetResult(); PublishArguments(testMethod, serviceProvider, arguments); return innerCommand.Execute(context); - } finally { + } + finally + { // Backwards, so a container the source built goes before the one holding the instances // it was handed. - for (var i = providers.Count - 1; i >= 0; i--) { + for (var i = providers.Count - 1; i >= 0; i--) + { DisposeProvider(providers[i]); } } @@ -108,8 +120,13 @@ public override TestResult Execute(TestExecutionContext context) { /// hook is awaited here rather than up the stack. /// private static void Start( - ITestMethodContext context, Attribute[] knownAttributes, IServiceProvider provider) { - foreach (var startupAttribute in knownAttributes.OfType()) { + ITestMethodContext context, + Attribute[] knownAttributes, + IServiceProvider provider + ) + { + foreach (var startupAttribute in knownAttributes.OfType()) + { startupAttribute.StartupAsync(context, provider).GetAwaiter().GetResult(); } } @@ -131,7 +148,11 @@ private static void Start( /// NUnit's setup and teardown handling. /// private static void PublishArguments( - TestMethod testMethod, IServiceProvider serviceProvider, object?[] arguments) { + TestMethod testMethod, + IServiceProvider serviceProvider, + object?[] arguments + ) + { var target = testMethod.Arguments; Array.Copy(arguments, target, arguments.Length); @@ -140,12 +161,19 @@ private static void PublishArguments( } private static void SetupTestCaseInfo( - IServiceCollection serviceCollection, TestMethod testMethod, Attribute[] knownAttributes) { - serviceCollection.AddSingleton(provider => provider.GetRequiredService()); + IServiceCollection serviceCollection, + TestMethod testMethod, + Attribute[] knownAttributes + ) + { + serviceCollection.AddSingleton(provider => + provider.GetRequiredService() + ); serviceCollection.AddSingleton(_ => new TestCaseInfo( testMethod, ArraySegment.Empty, - knownAttributes)); + knownAttributes + )); } /// @@ -156,11 +184,17 @@ private static void SetupTestCaseInfo( /// which is the reverse of how every other attribute here resolves. /// private static IServiceProvider BuildServiceProvider( - ITestMethodContext context, IServiceCollection serviceCollection, Attribute[] knownAttributes) { - var serviceProviderBuilderAttribute = - knownAttributes.OfType().LastOrDefault(); - - if (serviceProviderBuilderAttribute != null) { + ITestMethodContext context, + IServiceCollection serviceCollection, + Attribute[] knownAttributes + ) + { + var serviceProviderBuilderAttribute = knownAttributes + .OfType() + .LastOrDefault(); + + if (serviceProviderBuilderAttribute != null) + { return serviceProviderBuilderAttribute.BuildServiceProvider(context, serviceCollection); } @@ -177,12 +211,17 @@ private static IServiceProvider BuildServiceProvider( /// which relying on attribute order alone would not guarantee. /// private static void SetupServiceSetupAttributes( - ITestMethodContext context, IServiceCollection serviceCollection, Attribute[] knownAttributes) { + ITestMethodContext context, + IServiceCollection serviceCollection, + Attribute[] knownAttributes + ) + { var setupAttributes = knownAttributes .OfType() .OrderBy(attribute => attribute is IMockSupportAttribute ? 0 : 1); - foreach (var setupAttribute in setupAttributes) { + foreach (var setupAttribute in setupAttributes) + { setupAttribute.SetupServiceCollection(context, serviceCollection); } } @@ -194,14 +233,20 @@ private static void SetupServiceSetupAttributes( /// service-setup pass runs too late to supply it. /// private static void SeedEnvironment( - IServiceCollection serviceCollection, MethodInfo method, Attribute[] knownAttributes) { + IServiceCollection serviceCollection, + MethodInfo method, + Attribute[] knownAttributes + ) + { IModuleEnvironment? environment = null; - foreach (var provider in knownAttributes.OfType()) { + foreach (var provider in knownAttributes.OfType()) + { environment = provider.ProvideEnvironment(method) ?? environment; } - if (environment != null) { + if (environment != null) + { serviceCollection.Add(new ServiceDescriptor(typeof(IModuleEnvironment), environment)); } } @@ -214,19 +259,27 @@ private static void SeedEnvironment( /// because it does not drag the runtime in behind it. /// private static void SetupModules( - IServiceCollection serviceCollection, MethodInfo method, IEnumerable knownAttributes) { + IServiceCollection serviceCollection, + MethodInfo method, + IEnumerable knownAttributes + ) + { var modules = new List(); - foreach (var loadModuleAttribute in knownAttributes.OfType()) { + foreach (var loadModuleAttribute in knownAttributes.OfType()) + { modules.Add(loadModuleAttribute.GetModule()); } var testAttribute = method.GetTestAttribute(); - if (testAttribute != null) { + if (testAttribute != null) + { var count = 0; - foreach (var moduleType in testAttribute.ModuleTypes) { - if (Activator.CreateInstance(moduleType, []) is IDependencyModule moduleInstance) { + foreach (var moduleType in testAttribute.ModuleTypes) + { + if (Activator.CreateInstance(moduleType, []) is IDependencyModule moduleInstance) + { modules.Insert(count++, moduleInstance); } } @@ -242,8 +295,10 @@ private static void SetupModules( /// implements makes ServiceProvider.Dispose throw rather /// than fall back. /// - private static void DisposeProvider(IServiceProvider serviceProvider) { - switch (serviceProvider) { + private static void DisposeProvider(IServiceProvider serviceProvider) + { + switch (serviceProvider) + { case IAsyncDisposable asyncDisposable: asyncDisposable.DisposeAsync().AsTask().GetAwaiter().GetResult(); break; diff --git a/src/DependencyModules.NUnit/Impl/NUnitTestMethodContext.cs b/src/DependencyModules.NUnit/Impl/NUnitTestMethodContext.cs index 3cb8bb3..268671c 100644 --- a/src/DependencyModules.NUnit/Impl/NUnitTestMethodContext.cs +++ b/src/DependencyModules.NUnit/Impl/NUnitTestMethodContext.cs @@ -14,14 +14,12 @@ namespace DependencyModules.NUnit.Impl; /// if (testMethod is INUnitTestMethodContext nunit) reaches NUnit's own model — the test's /// name and id, its properties, and the fixture it belongs to. /// -public interface INUnitTestMethodContext : ITestMethodContext { - +public interface INUnitTestMethodContext : ITestMethodContext +{ /// /// NUnit's own model of the test method being executed. /// - TestMethod NUnitTestMethod { - get; - } + TestMethod NUnitTestMethod { get; } } /// @@ -34,15 +32,12 @@ TestMethod NUnitTestMethod { /// internal sealed class NUnitTestMethodContext( TestMethod testMethod, - IReadOnlyList attributes) : INUnitTestMethodContext { - - public TestMethod NUnitTestMethod { - get; - } = testMethod; + IReadOnlyList attributes +) : INUnitTestMethodContext +{ + public TestMethod NUnitTestMethod { get; } = testMethod; public MethodInfo Method => NUnitTestMethod.Method!.MethodInfo; - public IReadOnlyList Attributes { - get; - } = attributes; + public IReadOnlyList Attributes { get; } = attributes; } diff --git a/src/DependencyModules.NUnit/Impl/TestCaseInfo.cs b/src/DependencyModules.NUnit/Impl/TestCaseInfo.cs index 405d939..96c7403 100644 --- a/src/DependencyModules.NUnit/Impl/TestCaseInfo.cs +++ b/src/DependencyModules.NUnit/Impl/TestCaseInfo.cs @@ -8,15 +8,13 @@ namespace DependencyModules.NUnit.Impl; /// /// Registered in every test's container, so a service can be told what it is being built for. /// -public interface ITestCaseInfo { - +public interface ITestCaseInfo +{ /// /// NUnit's model of the test method being executed, including the arguments the case was /// built with, its name and its properties. /// - TestMethod TestMethod { - get; - } + TestMethod TestMethod { get; } /// /// Gets the arguments passed to the test method for a specific test case. @@ -26,18 +24,13 @@ TestMethod TestMethod { /// registration itself is made — a service reading this in its constructor would be reading it /// too early. Read it from a method the test calls, not from a constructor. /// - IReadOnlyList TestMethodArguments { - get; - set; - } + IReadOnlyList TestMethodArguments { get; set; } /// /// Gets the collection of attributes associated with the test method of a specific test case, /// widest scope first: assembly, then declaring type, then the method. /// - IReadOnlyList TestMethodAttributes { - get; - } + IReadOnlyList TestMethodAttributes { get; } } /// @@ -46,21 +39,15 @@ IReadOnlyList TestMethodAttributes { public class TestCaseInfo( TestMethod testMethod, IReadOnlyList testMethodArguments, - IReadOnlyList testMethodAttributes) : ITestCaseInfo { - + IReadOnlyList testMethodAttributes +) : ITestCaseInfo +{ /// - public TestMethod TestMethod { - get; - } = testMethod; + public TestMethod TestMethod { get; } = testMethod; /// - public IReadOnlyList TestMethodArguments { - get; - set; - } = testMethodArguments; + public IReadOnlyList TestMethodArguments { get; set; } = testMethodArguments; /// - public IReadOnlyList TestMethodAttributes { - get; - } = testMethodAttributes; + public IReadOnlyList TestMethodAttributes { get; } = testMethodAttributes; } diff --git a/src/DependencyModules.Runtime/Attributes/BaseServiceAttribute.cs b/src/DependencyModules.Runtime/Attributes/BaseServiceAttribute.cs index ffbda9b..5f35565 100644 --- a/src/DependencyModules.Runtime/Attributes/BaseServiceAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/BaseServiceAttribute.cs @@ -6,7 +6,8 @@ namespace DependencyModules.Runtime.Attributes; /// /// Defines the method of registration for services in a dependency injection container. /// -public enum RegistrationType { +public enum RegistrationType +{ /// /// Registers the service unconditionally, adding a new registration even if another service of the same type exists. /// @@ -28,7 +29,7 @@ public enum RegistrationType { /// Replaces an existing service registration with the new one. /// Typically used to override default implementations in the dependency injection container. /// - Replace + Replace, } /// @@ -57,24 +58,27 @@ public enum RegistrationType { /// a registration consists of. /// /// -public interface IServiceRegistrationAttribute { +public interface IServiceRegistrationAttribute +{ /// /// Gets or sets a key used for service registration, /// typically to distinguish between multiple registrations /// of the same service type or to categorize services. /// - object? Key { + object? Key + { get => null; - set {} + set { } } /// /// Gets or sets the type that the service should be registered as in the dependency injection container. /// Typically used to specify an interface or a base type that the implementation will be registered and resolved as. /// - Type? As { + Type? As + { get => null; - set { } + set { } } /// @@ -82,7 +86,8 @@ public interface IServiceRegistrationAttribute { /// determining the duration for which the service instance is retained. /// Common lifetimes include Transient, Scoped, and Singleton. /// - ServiceLifetime Lifetime { + ServiceLifetime Lifetime + { get => ServiceLifetime.Transient; set { } } @@ -93,7 +98,8 @@ ServiceLifetime Lifetime { /// service if it doesn't already exist, adding a service to an enumerable, or replacing /// an existing service. /// - RegistrationType Using { + RegistrationType Using + { get => RegistrationType.Add; set { } } @@ -105,18 +111,17 @@ RegistrationType Using { /// registration attributes, such as specifying the service type, registration type, and /// associated service lifetime. /// -public abstract class BaseServiceAttribute : Attribute, IServiceRegistrationAttribute { - +public abstract class BaseServiceAttribute : Attribute, IServiceRegistrationAttribute +{ /// public object? Key { get; set; } - + /// public Type? As { get; set; } - + /// public RegistrationType Using { get; set; } = RegistrationType.Add; - /// /// Gets or sets the module or scope under which the service should be registered. /// This property allows organizing or segregating service registrations across different logical groups @@ -146,10 +151,11 @@ public abstract class BaseServiceAttribute : Attribute, IServiceRegistrationAttr /// /// public int Order { get; set; } - + /// [Browsable(false)] - ServiceLifetime IServiceRegistrationAttribute.Lifetime { + ServiceLifetime IServiceRegistrationAttribute.Lifetime + { get => Lifetime; set => throw new Exception("Setting lifetime is not supported"); } @@ -161,4 +167,4 @@ ServiceLifetime IServiceRegistrationAttribute.Lifetime { /// or singleton lifetime. /// protected abstract ServiceLifetime Lifetime { get; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Attributes/CrossWireServiceAttribute.cs b/src/DependencyModules.Runtime/Attributes/CrossWireServiceAttribute.cs index 41b7e23..163eb25 100644 --- a/src/DependencyModules.Runtime/Attributes/CrossWireServiceAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/CrossWireServiceAttribute.cs @@ -8,35 +8,30 @@ namespace DependencyModules.Runtime.Attributes; /// will be registered pointing to the implementation registration /// allowing for the same instance to be returned for multiple interfaces /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public class CrossWireServiceAttribute : Attribute, IServiceRegistrationAttribute { - +[AttributeUsage( + AttributeTargets.Class | AttributeTargets.Method, + AllowMultiple = true, + Inherited = false +)] +public class CrossWireServiceAttribute : Attribute, IServiceRegistrationAttribute +{ /// - public object? Key { - get; - set; - } - + public object? Key { get; set; } + /// [Browsable(false)] - Type? IServiceRegistrationAttribute.As { - get; - set; - } - + Type? IServiceRegistrationAttribute.As { get; set; } + /// - public ServiceLifetime Lifetime { - get; - set; - } - + public ServiceLifetime Lifetime { get; set; } + /// - /// Which method type to use, + /// Which method type to use, /// public RegistrationType Using { get; set; } = RegistrationType.Add; - + /// /// DependencyModule realm that this type should be associated with /// public Type? Realm { get; set; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Attributes/DecorateAttribute.cs b/src/DependencyModules.Runtime/Attributes/DecorateAttribute.cs index 6d4313f..4573611 100644 --- a/src/DependencyModules.Runtime/Attributes/DecorateAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/DecorateAttribute.cs @@ -21,7 +21,8 @@ namespace DependencyModules.Runtime.Attributes; /// /// The decorator, which must implement . [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public class DecorateAttribute(Type service, Type decorator) : Attribute { +public class DecorateAttribute(Type service, Type decorator) : Attribute +{ /// /// The service being decorated. /// diff --git a/src/DependencyModules.Runtime/Attributes/DecoratorAttribute.cs b/src/DependencyModules.Runtime/Attributes/DecoratorAttribute.cs index 17b005e..7a9ce30 100644 --- a/src/DependencyModules.Runtime/Attributes/DecoratorAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/DecoratorAttribute.cs @@ -17,7 +17,8 @@ namespace DependencyModules.Runtime.Attributes; /// /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public class DecoratorAttribute : Attribute { +public class DecoratorAttribute : Attribute +{ /// /// Controls how decorators nest. Lower values are applied first and therefore sit closer to the /// implementation; higher values wrap them. diff --git a/src/DependencyModules.Runtime/Attributes/DependencyModuleAttribute.cs b/src/DependencyModules.Runtime/Attributes/DependencyModuleAttribute.cs index f417ef6..c3c5cf9 100644 --- a/src/DependencyModules.Runtime/Attributes/DependencyModuleAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/DependencyModuleAttribute.cs @@ -3,8 +3,9 @@ namespace DependencyModules.Runtime.Attributes; /// /// Applied to partial classes to denote a module entry point /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly , Inherited = false)] -public class DependencyModuleAttribute : Attribute { +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly, Inherited = false)] +public class DependencyModuleAttribute : Attribute +{ /// /// Restrict registration to types that are registered for this realm (Type) /// @@ -24,16 +25,16 @@ public class DependencyModuleAttribute : Attribute { /// Register JsonSourceGenerationOptions classes /// public bool RegisterJsonSerializers { get; set; } = false; - + /// /// Generate a IServiceCollection extension method /// Attributes are usually preferred over UseXXX methods /// public string? GenerateUseMethod { get; set; } - + /// /// Setting this to true will generate registration using factories /// instead of allowing the container to construct the type /// public bool GenerateFactories { get; set; } = false; -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Attributes/IfEnvironmentAttribute.cs b/src/DependencyModules.Runtime/Attributes/IfEnvironmentAttribute.cs index 2cdee98..2062c8e 100644 --- a/src/DependencyModules.Runtime/Attributes/IfEnvironmentAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/IfEnvironmentAttribute.cs @@ -23,12 +23,14 @@ namespace DependencyModules.Runtime.Attributes; /// /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public class IfEnvironmentAttribute : Attribute { +public class IfEnvironmentAttribute : Attribute +{ /// /// Registers the service only in the given environments. /// /// The environment names to register in. - public IfEnvironmentAttribute(params string[] environmentNames) { + public IfEnvironmentAttribute(params string[] environmentNames) + { EnvironmentNames = environmentNames; } diff --git a/src/DependencyModules.Runtime/Attributes/IfEnvironmentValueAttribute.cs b/src/DependencyModules.Runtime/Attributes/IfEnvironmentValueAttribute.cs index bc981a6..e121b2a 100644 --- a/src/DependencyModules.Runtime/Attributes/IfEnvironmentValueAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/IfEnvironmentValueAttribute.cs @@ -22,12 +22,14 @@ namespace DependencyModules.Runtime.Attributes; /// /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public class IfEnvironmentValueAttribute : Attribute { +public class IfEnvironmentValueAttribute : Attribute +{ /// /// Registers the service only when the environment has any value for . /// /// The key that has to be present. - public IfEnvironmentValueAttribute(string key) { + public IfEnvironmentValueAttribute(string key) + { Key = key; } @@ -37,7 +39,8 @@ public IfEnvironmentValueAttribute(string key) { /// /// The key to read. /// The value it has to equal. - public IfEnvironmentValueAttribute(string key, string value) { + public IfEnvironmentValueAttribute(string key, string value) + { Key = key; Value = value; } diff --git a/src/DependencyModules.Runtime/Attributes/IfNotEnvironmentAttribute.cs b/src/DependencyModules.Runtime/Attributes/IfNotEnvironmentAttribute.cs index 5157e3b..8c588c4 100644 --- a/src/DependencyModules.Runtime/Attributes/IfNotEnvironmentAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/IfNotEnvironmentAttribute.cs @@ -15,12 +15,14 @@ namespace DependencyModules.Runtime.Attributes; /// /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public class IfNotEnvironmentAttribute : Attribute { +public class IfNotEnvironmentAttribute : Attribute +{ /// /// Registers the service except in the given environments. /// /// The environment names to exclude. - public IfNotEnvironmentAttribute(params string[] environmentNames) { + public IfNotEnvironmentAttribute(params string[] environmentNames) + { EnvironmentNames = environmentNames; } diff --git a/src/DependencyModules.Runtime/Attributes/IfNotEnvironmentValueAttribute.cs b/src/DependencyModules.Runtime/Attributes/IfNotEnvironmentValueAttribute.cs index 39eefa7..e71436b 100644 --- a/src/DependencyModules.Runtime/Attributes/IfNotEnvironmentValueAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/IfNotEnvironmentValueAttribute.cs @@ -15,13 +15,15 @@ namespace DependencyModules.Runtime.Attributes; /// /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public class IfNotEnvironmentValueAttribute : Attribute { +public class IfNotEnvironmentValueAttribute : Attribute +{ /// /// Registers the service except when the environment has any value for /// . /// /// The key whose presence skips the registration. - public IfNotEnvironmentValueAttribute(string key) { + public IfNotEnvironmentValueAttribute(string key) + { Key = key; } @@ -31,7 +33,8 @@ public IfNotEnvironmentValueAttribute(string key) { /// /// The key to read. /// The value that skips the registration. - public IfNotEnvironmentValueAttribute(string key, string value) { + public IfNotEnvironmentValueAttribute(string key, string value) + { Key = key; Value = value; } diff --git a/src/DependencyModules.Runtime/Attributes/InterceptAttribute.cs b/src/DependencyModules.Runtime/Attributes/InterceptAttribute.cs index e70a848..0e866bb 100644 --- a/src/DependencyModules.Runtime/Attributes/InterceptAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/InterceptAttribute.cs @@ -20,7 +20,8 @@ namespace DependencyModules.Runtime.Attributes; /// The interceptor types to apply, in order. Each is resolved from the container. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public class InterceptAttribute(params Type[] interceptors) : Attribute { +public class InterceptAttribute(params Type[] interceptors) : Attribute +{ /// /// The interceptors applied to the service. /// diff --git a/src/DependencyModules.Runtime/Attributes/InterceptedMembers.cs b/src/DependencyModules.Runtime/Attributes/InterceptedMembers.cs index c89b6e0..9790683 100644 --- a/src/DependencyModules.Runtime/Attributes/InterceptedMembers.cs +++ b/src/DependencyModules.Runtime/Attributes/InterceptedMembers.cs @@ -21,7 +21,8 @@ namespace DependencyModules.Runtime.Attributes; /// /// [Flags] -public enum InterceptedMembers { +public enum InterceptedMembers +{ /// Ordinary methods. Methods = 1, @@ -35,5 +36,5 @@ public enum InterceptedMembers { Events = 8, /// Everything the interface declares. The default. - All = Methods | Properties | Indexers | Events + All = Methods | Properties | Indexers | Events, } diff --git a/src/DependencyModules.Runtime/Attributes/ScopedServiceAttribute.cs b/src/DependencyModules.Runtime/Attributes/ScopedServiceAttribute.cs index ce3ab27..5bd8b63 100644 --- a/src/DependencyModules.Runtime/Attributes/ScopedServiceAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/ScopedServiceAttribute.cs @@ -5,9 +5,13 @@ namespace DependencyModules.Runtime.Attributes; /// /// Register service or factory as scoped /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public class ScopedServiceAttribute : BaseServiceAttribute { - +[AttributeUsage( + AttributeTargets.Class | AttributeTargets.Method, + AllowMultiple = true, + Inherited = false +)] +public class ScopedServiceAttribute : BaseServiceAttribute +{ /// protected override ServiceLifetime Lifetime => ServiceLifetime.Scoped; -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Attributes/SingletonServiceAttribute.cs b/src/DependencyModules.Runtime/Attributes/SingletonServiceAttribute.cs index a113f97..dc21fee 100644 --- a/src/DependencyModules.Runtime/Attributes/SingletonServiceAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/SingletonServiceAttribute.cs @@ -5,8 +5,13 @@ namespace DependencyModules.Runtime.Attributes; /// /// Register service or factory as singleton /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public class SingletonServiceAttribute : BaseServiceAttribute { +[AttributeUsage( + AttributeTargets.Class | AttributeTargets.Method, + AllowMultiple = true, + Inherited = false +)] +public class SingletonServiceAttribute : BaseServiceAttribute +{ /// protected override ServiceLifetime Lifetime => ServiceLifetime.Singleton; -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Attributes/TransientServiceAttribute.cs b/src/DependencyModules.Runtime/Attributes/TransientServiceAttribute.cs index edb139a..e475230 100644 --- a/src/DependencyModules.Runtime/Attributes/TransientServiceAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/TransientServiceAttribute.cs @@ -5,9 +5,13 @@ namespace DependencyModules.Runtime.Attributes; /// /// Register service or factory as Transient /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public class TransientServiceAttribute : BaseServiceAttribute { - +[AttributeUsage( + AttributeTargets.Class | AttributeTargets.Method, + AllowMultiple = true, + Inherited = false +)] +public class TransientServiceAttribute : BaseServiceAttribute +{ /// protected override ServiceLifetime Lifetime => ServiceLifetime.Transient; -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Conventions/ConventionContracts.cs b/src/DependencyModules.Runtime/Conventions/ConventionContracts.cs index 4ae19f2..be0be8c 100644 --- a/src/DependencyModules.Runtime/Conventions/ConventionContracts.cs +++ b/src/DependencyModules.Runtime/Conventions/ConventionContracts.cs @@ -16,8 +16,8 @@ // Nothing here has behaviour or is ever executed. The generator reads the chain out of the method // body at compile time and emits ordinary registrations; the body itself is never called. -namespace DependencyModules.Runtime.Conventions { - +namespace DependencyModules.Runtime.Conventions +{ /// /// Implement this on a [DependencyModule] class to register services by convention /// instead of attributing each one. @@ -55,8 +55,8 @@ namespace DependencyModules.Runtime.Conventions { /// /// /// - public interface IConventionModule { - + public interface IConventionModule + { /// /// Declares this module's conventions. Read at compile time; never invoked. /// @@ -70,8 +70,8 @@ public interface IConventionModule { /// /// Nothing implements this. The calls made on it are read from source at compile time. /// - public interface IConventionDefinitions { - + public interface IConventionDefinitions + { /// /// Registers every type in this compilation that implements /// , as . @@ -135,8 +135,8 @@ public interface IConventionDefinitions { /// A lifetime is required. There is no default, because a lifetime nobody wrote down is /// the most expensive thing for a registration to get wrong; omitting one is DM0009. /// - public interface IConventionRegistration { - + public interface IConventionRegistration + { /// Registers the matches as singletons. IConventionRegistration AsSingleton(); @@ -255,7 +255,8 @@ public interface IConventionRegistration { /// /// How to add the registration. IConventionRegistration Using( - global::DependencyModules.Runtime.Attributes.RegistrationType registrationType); + global::DependencyModules.Runtime.Attributes.RegistrationType registrationType + ); /// /// Registers every match under a service key. diff --git a/src/DependencyModules.Runtime/Features/FeatureApplicator.cs b/src/DependencyModules.Runtime/Features/FeatureApplicator.cs index c984b4e..83578c6 100644 --- a/src/DependencyModules.Runtime/Features/FeatureApplicator.cs +++ b/src/DependencyModules.Runtime/Features/FeatureApplicator.cs @@ -12,7 +12,9 @@ namespace DependencyModules.Runtime.Features; /// The type of the feature being applied, which must adhere to the constraints defined /// by the corresponding feature handler. /// -public class FeatureApplicator(IDependencyModuleFeature handler) : IFeatureApplicator { +public class FeatureApplicator(IDependencyModuleFeature handler) + : IFeatureApplicator +{ /// /// Gets the order of the feature applicator execution. /// The order determines the sequence in which feature applicators @@ -29,7 +31,11 @@ public class FeatureApplicator(IDependencyModuleFeature hand /// /// The list of dependency modules containing features to be applied. /// - public void Apply(IServiceCollection serviceCollection, IReadOnlyList modules) { + public void Apply( + IServiceCollection serviceCollection, + IReadOnlyList modules + ) + { handler.HandleFeature(serviceCollection, modules.OfType()); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Features/IDependencyModuleApplicatorProvider.cs b/src/DependencyModules.Runtime/Features/IDependencyModuleApplicatorProvider.cs index 9b8f085..887a0a9 100644 --- a/src/DependencyModules.Runtime/Features/IDependencyModuleApplicatorProvider.cs +++ b/src/DependencyModules.Runtime/Features/IDependencyModuleApplicatorProvider.cs @@ -6,8 +6,8 @@ namespace DependencyModules.Runtime.Features; /// Provides a mechanism to retrieve a collection of feature applicators used for handling /// and applying specific features in the dependency module system. /// -public interface IDependencyModuleApplicatorProvider { - +public interface IDependencyModuleApplicatorProvider +{ /// /// Retrieves a collection of feature applicators responsible for handling and applying /// specific features in the dependency module system. @@ -16,7 +16,8 @@ public interface IDependencyModuleApplicatorProvider { /// An enumerable collection of objects implementing the interface. /// If no feature applicators are available, an empty collection is returned. /// - IEnumerable FeatureApplicators() { + IEnumerable FeatureApplicators() + { return ArraySegment.Empty; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Features/IDependencyModuleFeature.cs b/src/DependencyModules.Runtime/Features/IDependencyModuleFeature.cs index 420b681..0da4561 100644 --- a/src/DependencyModules.Runtime/Features/IDependencyModuleFeature.cs +++ b/src/DependencyModules.Runtime/Features/IDependencyModuleFeature.cs @@ -11,7 +11,8 @@ namespace DependencyModules.Runtime.Features; /// The feature type to be handled by the module. Represents objects or details that can be utilized /// to configure or apply specific functionality during service registration. /// -public interface IDependencyModuleFeature { +public interface IDependencyModuleFeature +{ /// /// Gets the order in which the dependency module feature should be applied. /// Features with a lower order value are handled earlier during the service collection @@ -34,4 +35,4 @@ public interface IDependencyModuleFeature { /// Each feature provides specific information to configure related services. /// void HandleFeature(IServiceCollection collection, IEnumerable feature); -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Features/IFeatureApplicator.cs b/src/DependencyModules.Runtime/Features/IFeatureApplicator.cs index e2bb537..0a44913 100644 --- a/src/DependencyModules.Runtime/Features/IFeatureApplicator.cs +++ b/src/DependencyModules.Runtime/Features/IFeatureApplicator.cs @@ -7,7 +7,8 @@ namespace DependencyModules.Runtime.Features; /// Defines the contract for a feature applicator responsible for applying /// specific features within a dependency management system. /// -public interface IFeatureApplicator { +public interface IFeatureApplicator +{ /// /// Represents the order of execution for feature applicators when applying features /// to a service collection. This property determines the sequence in which @@ -25,4 +26,4 @@ public interface IFeatureApplicator { /// A read-only list of dependency modules containing the features to be applied. /// void Apply(IServiceCollection serviceCollection, IReadOnlyList modules); -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Helpers/DecoratorHelper.cs b/src/DependencyModules.Runtime/Helpers/DecoratorHelper.cs index 01976c8..fdfa6ea 100644 --- a/src/DependencyModules.Runtime/Helpers/DecoratorHelper.cs +++ b/src/DependencyModules.Runtime/Helpers/DecoratorHelper.cs @@ -24,8 +24,8 @@ namespace DependencyModules.Runtime.Helpers; /// decorator is constructed by a literal new and the code exists in the assembly. /// /// -public static class DecoratorHelper { - +public static class DecoratorHelper +{ /// /// Swaps an open generic registration for a generated wrapper that implements the same /// open generic service. @@ -63,25 +63,38 @@ public static void InterceptOpenGeneric( IServiceCollection services, Type serviceType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - Type implementationType, + Type implementationType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - Type wrapperType) { - + Type wrapperType + ) + { // Snapshotted: the loop appends the implementation's own registration, and re-reading Count // would walk into what it just added. var count = services.Count; - for (var i = 0; i < count; i++) { + for (var i = 0; i < count; i++) + { var descriptor = services[i]; - if (descriptor.ServiceType != serviceType || ImplementationOf(descriptor) != implementationType) { + if ( + descriptor.ServiceType != serviceType + || ImplementationOf(descriptor) != implementationType + ) + { continue; } - services.Add(new ServiceDescriptor(implementationType, implementationType, descriptor.Lifetime)); + services.Add( + new ServiceDescriptor(implementationType, implementationType, descriptor.Lifetime) + ); services[i] = descriptor.IsKeyedService - ? new ServiceDescriptor(serviceType, descriptor.ServiceKey, wrapperType, descriptor.Lifetime) + ? new ServiceDescriptor( + serviceType, + descriptor.ServiceKey, + wrapperType, + descriptor.Lifetime + ) : new ServiceDescriptor(serviceType, wrapperType, descriptor.Lifetime); } } @@ -94,7 +107,9 @@ public static void InterceptOpenGeneric( /// cannot be read through one property. /// private static Type? ImplementationOf(ServiceDescriptor descriptor) => - descriptor.IsKeyedService ? descriptor.KeyedImplementationType : descriptor.ImplementationType; + descriptor.IsKeyedService + ? descriptor.KeyedImplementationType + : descriptor.ImplementationType; /// /// Wraps every registration of using . @@ -114,8 +129,9 @@ public static void InterceptOpenGeneric( public static void Decorate( IServiceCollection services, Type serviceType, - Func decoratorFactory) { - + Func decoratorFactory + ) + { Decorate(services, serviceType, decoratorFactory, null, null); } @@ -142,8 +158,10 @@ public static void Decorate( public static void Decorate( IServiceCollection services, Type decoratorIdentity, - Func decoratorFactory) where TService : class { - + Func decoratorFactory + ) + where TService : class + { Decorate(services, decoratorIdentity, decoratorFactory, null); } @@ -182,15 +200,18 @@ public static void Decorate( IServiceCollection services, Type decoratorIdentity, Func decoratorFactory, - Type? implementationType) where TService : class { - + Type? implementationType + ) + where TService : class + { Decorate( services, typeof(TService), (provider, inner) => decoratorFactory(provider, (TService)inner), null, decoratorIdentity, - implementationType); + implementationType + ); } /// @@ -217,14 +238,18 @@ public static void Decorate( private static readonly ConditionalWeakTable> Applied = new(); private static bool AlreadyApplied(ServiceDescriptor descriptor, Type? decoratorIdentity) => - decoratorIdentity != null && - Applied.TryGetValue(descriptor, out var applied) && - applied.Contains(decoratorIdentity); + decoratorIdentity != null + && Applied.TryGetValue(descriptor, out var applied) + && applied.Contains(decoratorIdentity); private static void RecordApplied( - ServiceDescriptor original, ServiceDescriptor replacement, Type? decoratorIdentity) { - - if (decoratorIdentity == null) { + ServiceDescriptor original, + ServiceDescriptor replacement, + Type? decoratorIdentity + ) + { + if (decoratorIdentity == null) + { return; } @@ -232,7 +257,8 @@ private static void RecordApplied( // Stacked decorators each rewrite the slot, so the set has to follow the descriptor that // now occupies it rather than staying with the one that was replaced. - if (Applied.TryGetValue(original, out var existing)) { + if (Applied.TryGetValue(original, out var existing)) + { applied.UnionWith(existing); } @@ -251,7 +277,8 @@ private static void RecordApplied( /// then be unable to recognise its own registration, so the origin is carried across the /// replacement the same way is. /// - private static readonly ConditionalWeakTable OriginImplementation = new(); + private static readonly ConditionalWeakTable OriginImplementation = + new(); /// /// The implementation behind a descriptor, or null when it cannot be known — a registration made @@ -259,10 +286,14 @@ private static void RecordApplied( /// DependencyModules_GenerateFactories emits. /// private static Type? OriginImplementationOf(ServiceDescriptor descriptor) => - OriginImplementation.TryGetValue(descriptor, out var origin) ? origin : ImplementationOf(descriptor); - - private static void RecordOrigin(ServiceDescriptor original, ServiceDescriptor replacement) { - if (OriginImplementationOf(original) is not { } origin) { + OriginImplementation.TryGetValue(descriptor, out var origin) + ? origin + : ImplementationOf(descriptor); + + private static void RecordOrigin(ServiceDescriptor original, ServiceDescriptor replacement) + { + if (OriginImplementationOf(original) is not { } origin) + { return; } @@ -276,25 +307,30 @@ private static void Decorate( Func decoratorFactory, Type? decoratorType, Type? decoratorIdentity, - Type? implementationType = null) { - + Type? implementationType = null + ) + { var ordinal = 0; - for (var i = services.Count - 1; i >= 0; i--) { + for (var i = services.Count - 1; i >= 0; i--) + { var descriptor = services[i]; - if (!Matches(descriptor.ServiceType, serviceType)) { + if (!Matches(descriptor.ServiceType, serviceType)) + { continue; } // A registration this method displaced on an earlier pass is machinery, not a service. // Decorating it would wrap the implementation a second time, one layer further in. - if (descriptor.ServiceKey is DisplacedImplementationKey) { + if (descriptor.ServiceKey is DisplacedImplementationKey) + { continue; } // Emitted from two places for the same registration; the first one wins. - if (AlreadyApplied(descriptor, decoratorIdentity)) { + if (AlreadyApplied(descriptor, decoratorIdentity)) + { continue; } @@ -303,9 +339,12 @@ private static void Decorate( // registration made from an instance or a factory cannot be attributed to an // implementation, and refusing to wrap it there would silently stop intercepting a // service that had asked for it — a worse failure than the one this filter prevents. - if (implementationType != null && - OriginImplementationOf(descriptor) is { } origin && - origin != implementationType) { + if ( + implementationType != null + && OriginImplementationOf(descriptor) is { } origin + && origin != implementationType + ) + { continue; } @@ -321,12 +360,14 @@ private static void Decorate( descriptor.ServiceType, descriptor.ServiceKey, (provider, key) => decoratorFactory(provider, innerFactory(provider, key)), - descriptor.Lifetime) + descriptor.Lifetime + ) : new ServiceDescriptor( descriptor.ServiceType, provider => decoratorFactory(provider, innerFactory(provider, null)), // The decorator must not change how long the service lives. - descriptor.Lifetime); + descriptor.Lifetime + ); RecordApplied(descriptor, replacement, decoratorIdentity); RecordOrigin(descriptor, replacement); @@ -344,27 +385,31 @@ private static void Decorate( /// ordinal only separates descriptors that are otherwise identical, which is legal — /// AddSingleton<IFoo, Foo>() twice registers two services and must stay two. /// - private sealed class DisplacedImplementationKey : IEquatable { + private sealed class DisplacedImplementationKey : IEquatable + { private readonly Type _serviceType; private readonly Type _implementationType; private readonly int _ordinal; - public DisplacedImplementationKey(Type serviceType, Type implementationType, int ordinal) { + public DisplacedImplementationKey(Type serviceType, Type implementationType, int ordinal) + { _serviceType = serviceType; _implementationType = implementationType; _ordinal = ordinal; } public bool Equals(DisplacedImplementationKey? other) => - other is not null && - _serviceType == other._serviceType && - _implementationType == other._implementationType && - _ordinal == other._ordinal; + other is not null + && _serviceType == other._serviceType + && _implementationType == other._implementationType + && _ordinal == other._ordinal; public override bool Equals(object? obj) => Equals(obj as DisplacedImplementationKey); - public override int GetHashCode() { - unchecked { + public override int GetHashCode() + { + unchecked + { var hash = _serviceType.GetHashCode(); hash = hash * 31 + _implementationType.GetHashCode(); return hash * 31 + _ordinal; @@ -393,47 +438,70 @@ public override string ToString() => /// /// private static Func CaptureInner( - IServiceCollection services, ServiceDescriptor descriptor, int ordinal) { - - if (descriptor.IsKeyedService) { - if (descriptor.KeyedImplementationInstance is { } keyedInstance) { + IServiceCollection services, + ServiceDescriptor descriptor, + int ordinal + ) + { + if (descriptor.IsKeyedService) + { + if (descriptor.KeyedImplementationInstance is { } keyedInstance) + { return (_, _) => keyedInstance; } - if (descriptor.KeyedImplementationFactory is { } keyedFactory) { + if (descriptor.KeyedImplementationFactory is { } keyedFactory) + { return (provider, key) => keyedFactory(provider, key); } - if (descriptor.KeyedImplementationType is { } keyedImplementationType) { + if (descriptor.KeyedImplementationType is { } keyedImplementationType) + { var keyedInnerKey = Displace( - services, descriptor.ServiceType, keyedImplementationType, ordinal, descriptor.Lifetime); + services, + descriptor.ServiceType, + keyedImplementationType, + ordinal, + descriptor.Lifetime + ); - return (provider, _) => provider.GetRequiredKeyedService(keyedImplementationType, keyedInnerKey); + return (provider, _) => + provider.GetRequiredKeyedService(keyedImplementationType, keyedInnerKey); } throw new InvalidOperationException( - $"The keyed registration for '{descriptor.ServiceType}' has no implementation type, factory, " + - "or instance, so there is nothing to decorate."); + $"The keyed registration for '{descriptor.ServiceType}' has no implementation type, factory, " + + "or instance, so there is nothing to decorate." + ); } - if (descriptor.ImplementationInstance is { } instance) { + if (descriptor.ImplementationInstance is { } instance) + { return (_, _) => instance; } - if (descriptor.ImplementationFactory is { } factory) { + if (descriptor.ImplementationFactory is { } factory) + { return (provider, _) => factory(provider); } - if (descriptor.ImplementationType is { } implementationType) { + if (descriptor.ImplementationType is { } implementationType) + { var innerKey = Displace( - services, descriptor.ServiceType, implementationType, ordinal, descriptor.Lifetime); + services, + descriptor.ServiceType, + implementationType, + ordinal, + descriptor.Lifetime + ); return (provider, _) => provider.GetRequiredKeyedService(implementationType, innerKey); } throw new InvalidOperationException( - $"The registration for '{descriptor.ServiceType}' has no implementation type, factory, or " + - "instance, so there is nothing to decorate."); + $"The registration for '{descriptor.ServiceType}' has no implementation type, factory, or " + + "instance, so there is nothing to decorate." + ); } /// @@ -443,10 +511,11 @@ private static DisplacedImplementationKey Displace( IServiceCollection services, Type serviceType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - Type implementationType, + Type implementationType, int ordinal, - ServiceLifetime lifetime) { - + ServiceLifetime lifetime + ) + { var key = new DisplacedImplementationKey(serviceType, implementationType, ordinal); // Appended while the caller iterates backwards, so it is never revisited on this pass. @@ -472,32 +541,39 @@ private static DisplacedImplementationKey Displace( /// Closed registrations of a generic service are decorated normally, so declaring a closed /// construction is the way through. /// - private static void GuardOpenGenericRegistration(ServiceDescriptor descriptor, Type? decoratorType) { - if (!descriptor.ServiceType.IsGenericTypeDefinition) { + private static void GuardOpenGenericRegistration( + ServiceDescriptor descriptor, + Type? decoratorType + ) + { + if (!descriptor.ServiceType.IsGenericTypeDefinition) + { return; } var by = decoratorType == null ? "" : $" by '{decoratorType}'"; throw new InvalidOperationException( - $"'{descriptor.ServiceType}' is registered as an open generic and cannot be decorated{by}. " + - "Decorating replaces a registration with a factory, which the container does not allow " + - "for an open generic service type. Register closed constructions of the service instead, " + - "such as a class deriving from the generic implementation."); + $"'{descriptor.ServiceType}' is registered as an open generic and cannot be decorated{by}. " + + "Decorating replaces a registration with a factory, which the container does not allow " + + "for an open generic service type. Register closed constructions of the service instead, " + + "such as a class deriving from the generic implementation." + ); } /// /// True when a registered service type is the one being decorated, including a closed /// construction of a decorated open generic. /// - private static bool Matches(Type registeredServiceType, Type decoratedServiceType) { - if (registeredServiceType == decoratedServiceType) { + private static bool Matches(Type registeredServiceType, Type decoratedServiceType) + { + if (registeredServiceType == decoratedServiceType) + { return true; } - return decoratedServiceType.IsGenericTypeDefinition && - registeredServiceType.IsGenericType && - registeredServiceType.GetGenericTypeDefinition() == decoratedServiceType; + return decoratedServiceType.IsGenericTypeDefinition + && registeredServiceType.IsGenericType + && registeredServiceType.GetGenericTypeDefinition() == decoratedServiceType; } - } diff --git a/src/DependencyModules.Runtime/Helpers/DecoratorRegistration.cs b/src/DependencyModules.Runtime/Helpers/DecoratorRegistration.cs index c6fc7a9..dc2f346 100644 --- a/src/DependencyModules.Runtime/Helpers/DecoratorRegistration.cs +++ b/src/DependencyModules.Runtime/Helpers/DecoratorRegistration.cs @@ -19,7 +19,8 @@ namespace DependencyModules.Runtime.Helpers; /// The function that rewrites registrations in the collection, receiving the environment any /// condition on the decorator is evaluated against. /// -public sealed class DecoratorRegistration(int order, EnvironmentRegistryFunc registryFunc) { +public sealed class DecoratorRegistration(int order, EnvironmentRegistryFunc registryFunc) +{ /// /// A decorator with no environment condition. /// diff --git a/src/DependencyModules.Runtime/Helpers/DependencyRegistry.cs b/src/DependencyModules.Runtime/Helpers/DependencyRegistry.cs index 46e3811..b390c36 100644 --- a/src/DependencyModules.Runtime/Helpers/DependencyRegistry.cs +++ b/src/DependencyModules.Runtime/Helpers/DependencyRegistry.cs @@ -26,7 +26,9 @@ namespace DependencyModules.Runtime.Helpers; /// The IServiceCollection to which dependencies will be added. /// The environment conditions are evaluated against. Never null. public delegate void EnvironmentRegistryFunc( - IServiceCollection serviceCollection, IModuleEnvironment environment); + IServiceCollection serviceCollection, + IModuleEnvironment environment +); /// /// Static class used to store dependency registration functions @@ -34,7 +36,8 @@ public delegate void EnvironmentRegistryFunc( /// /// // ReSharper disable once ClassNeverInstantiated.Global -public class DependencyRegistry { +public class DependencyRegistry +{ // ReSharper disable StaticMemberInGenericType private static readonly object SyncLock = new(); private static List? RegistryFuncs; @@ -46,8 +49,10 @@ public class DependencyRegistry { /// /// /// - public static int Add(RegistryFunc registryFunc) { - lock (SyncLock) { + public static int Add(RegistryFunc registryFunc) + { + lock (SyncLock) + { (RegistryFuncs ??= []).Add((serviceCollection, _) => registryFunc(serviceCollection)); } @@ -59,8 +64,10 @@ public static int Add(RegistryFunc registryFunc) { /// /// /// - public static int Add(EnvironmentRegistryFunc registryFunc) { - lock (SyncLock) { + public static int Add(EnvironmentRegistryFunc registryFunc) + { + lock (SyncLock) + { (RegistryFuncs ??= []).Add(registryFunc); } @@ -76,15 +83,16 @@ public static int Add(EnvironmentRegistryFunc registryFunc) { /// public static int Add( Func provider, - ServiceLifetime lifetime = ServiceLifetime.Transient) where TInstance : class { - lock (SyncLock) { + ServiceLifetime lifetime = ServiceLifetime.Transient + ) + where TInstance : class + { + lock (SyncLock) + { (RegistryFuncs ??= []).Add( - (registry, _) => registry.Add( - new ServiceDescriptor( - typeof(TInstance), - provider, - lifetime - ))); + (registry, _) => + registry.Add(new ServiceDescriptor(typeof(TInstance), provider, lifetime)) + ); } return 1; } @@ -104,18 +112,25 @@ public static int Add( /// public static int Add( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - Type implementationType, + Type implementationType, ServiceLifetime lifetime = ServiceLifetime.Transient, - object? serviceKey = null) where TInstance : class { - lock (SyncLock) { + object? serviceKey = null + ) + where TInstance : class + { + lock (SyncLock) + { (RegistryFuncs ??= []).Add( - (registry, _) => registry.Add( - new ServiceDescriptor( - typeof(TInstance), - serviceKey, - implementationType, - lifetime - ))); + (registry, _) => + registry.Add( + new ServiceDescriptor( + typeof(TInstance), + serviceKey, + implementationType, + lifetime + ) + ) + ); } return 1; } @@ -133,8 +148,10 @@ public static int Add( /// that an application's decorators wrap those contributed by the libraries it consumes. /// /// - public static int AddDecorator(RegistryFunc registryFunc, int order = 0) { - lock (SyncLock) { + public static int AddDecorator(RegistryFunc registryFunc, int order = 0) + { + lock (SyncLock) + { (Decorators ??= []).Add(new DecoratorRegistration(order, registryFunc)); } @@ -152,8 +169,10 @@ public static int AddDecorator(RegistryFunc registryFunc, int order = 0) { /// Function that decorates registrations already in the collection. /// See the other overload; ordering is unaffected by the condition. /// - public static int AddDecorator(EnvironmentRegistryFunc registryFunc, int order = 0) { - lock (SyncLock) { + public static int AddDecorator(EnvironmentRegistryFunc registryFunc, int order = 0) + { + lock (SyncLock) + { (Decorators ??= []).Add(new DecoratorRegistration(order, registryFunc)); } @@ -165,8 +184,10 @@ public static int AddDecorator(EnvironmentRegistryFunc registryFunc, int order = /// /// /// - public static int AddModule(params IDependencyModule[] modules) { - lock (SyncLock) { + public static int AddModule(params IDependencyModule[] modules) + { + lock (SyncLock) + { (Modules ??= []).AddRange(modules); } @@ -178,21 +199,26 @@ public static int AddModule(params IDependencyModule[] modules) { /// /// /// - public static void LoadModules(IServiceCollection serviceCollection, params IDependencyModule[] dependencyModules) { + public static void LoadModules( + IServiceCollection serviceCollection, + params IDependencyModule[] dependencyModules + ) + { var modules = GetAllModules(dependencyModules); - + ApplyFeatures(serviceCollection, modules); ApplyServices(serviceCollection, modules); - + ApplyDecorators(serviceCollection, modules); } - + /// /// Apply all registration for a given type to the service collection /// /// - public static void ApplyServices(IServiceCollection serviceCollection) { + public static void ApplyServices(IServiceCollection serviceCollection) + { ApplyServices(serviceCollection, FindOrCreateEnvironment(serviceCollection)); } @@ -202,16 +228,23 @@ public static void ApplyServices(IServiceCollection serviceCollection) { /// /// /// - public static void ApplyServices(IServiceCollection serviceCollection, IModuleEnvironment environment) { + public static void ApplyServices( + IServiceCollection serviceCollection, + IModuleEnvironment environment + ) + { EnvironmentRegistryFunc[] snapshot; - lock (SyncLock) { - if (RegistryFuncs == null) { + lock (SyncLock) + { + if (RegistryFuncs == null) + { return; } snapshot = RegistryFuncs.ToArray(); } - foreach (var registryFunc in snapshot) { + foreach (var registryFunc in snapshot) + { registryFunc(serviceCollection, environment); } } @@ -220,7 +253,8 @@ public static void ApplyServices(IServiceCollection serviceCollection, IModuleEn /// Apply all decorators /// /// - public static void ApplyDecorators(IServiceCollection serviceCollection) { + public static void ApplyDecorators(IServiceCollection serviceCollection) + { ApplyDecorators(serviceCollection, FindOrCreateEnvironment(serviceCollection)); } @@ -234,7 +268,8 @@ public static void ApplyDecorators(IServiceCollection serviceCollection) { /// where two calls disagreeing would actually matter — two process defaults read the same /// variables and give the same answers. /// - private static IModuleEnvironment FindOrCreateEnvironment(IServiceCollection serviceCollection) { + private static IModuleEnvironment FindOrCreateEnvironment(IServiceCollection serviceCollection) + { var environment = FindModuleEnvironment(serviceCollection); return environment ?? ModuleEnvironment.CreateDefault(); @@ -244,12 +279,15 @@ private static IModuleEnvironment FindOrCreateEnvironment(IServiceCollection ser /// Stable insertion sort by Order. Replaces OrderBy so that no LINQ ordering machinery is /// instantiated for DecoratorRegistration at startup. /// - private static void SortByOrder(List list) { - for (var i = 1; i < list.Count; i++) { + private static void SortByOrder(List list) + { + for (var i = 1; i < list.Count; i++) + { var item = list[i]; var j = i - 1; - while (j >= 0 && list[j].Order > item.Order) { + while (j >= 0 && list[j].Order > item.Order) + { list[j + 1] = list[j]; j--; } @@ -268,10 +306,12 @@ private static void SortByOrder(List list) { /// what lets decoration find the same instance the registrations were decided against, which /// matters now that CreateDefault builds a fresh one per call. /// - private static IModuleEnvironment ResolveEnvironment(IServiceCollection serviceCollection) { + private static IModuleEnvironment ResolveEnvironment(IServiceCollection serviceCollection) + { var environment = FindModuleEnvironment(serviceCollection); - if (environment != null) { + if (environment != null) + { return environment; } @@ -290,10 +330,15 @@ private static IModuleEnvironment ResolveEnvironment(IServiceCollection serviceC /// /// /// - public static void ApplyDecorators(IServiceCollection serviceCollection, IModuleEnvironment environment) { + public static void ApplyDecorators( + IServiceCollection serviceCollection, + IModuleEnvironment environment + ) + { var list = new List(GetDecorators()); SortByOrder(list); - for (var i = 0; i < list.Count; i++) { + for (var i = 0; i < list.Count; i++) + { list[i].RegistryFunc(serviceCollection, environment); } } @@ -306,8 +351,10 @@ public static void ApplyDecorators(IServiceCollection serviceCollection, IModule /// that decorators from every module can be sorted together. Applying each module's decorators /// separately would make module discovery order outrank the declared order. /// - public static IReadOnlyList GetDecorators() { - lock (SyncLock) { + public static IReadOnlyList GetDecorators() + { + lock (SyncLock) + { return Decorators == null ? Array.Empty() : Decorators.ToArray(); } } @@ -317,9 +364,12 @@ public static IReadOnlyList GetDecorators() { /// /// /// - public static IEnumerable GetModules(params object[] modules) { - lock (SyncLock) { - if (Modules == null || Modules.Count == 0) { + public static IEnumerable GetModules(params object[] modules) + { + lock (SyncLock) + { + if (Modules == null || Modules.Count == 0) + { return modules; } @@ -338,35 +388,45 @@ public static IEnumerable GetModules(params object[] modules) { /// [MethodImpl(MethodImplOptions.NoInlining)] private static IEnumerable CombineWithAddedModules( - List registered, object[] modules) { - + List registered, + object[] modules + ) + { var snapshot = registered.ToList(); return modules.Length == 0 ? snapshot : snapshot.Concat(modules); } - private static void ApplyDecorators(IServiceCollection serviceCollection, IReadOnlyList modules) { + private static void ApplyDecorators( + IServiceCollection serviceCollection, + IReadOnlyList modules + ) + { // Gathered from every module and sorted together, the same way ApplyFeatures collects and // sorts feature applicators. Applying each module's decorators in turn would let module // discovery order outrank the declared order, which breaks a pipeline assembled from more // than one package. List? decorators = null; - for (var i = 0; i < modules.Count; i++) { + for (var i = 0; i < modules.Count; i++) + { var registrations = modules[i].InternalGetDecorators(); // Tested before enumerating. A module with no decorators is the common case, and asking // it for an enumerator to immediately find it empty built one per module per startup. - if (registrations is ICollection { Count: 0 }) { + if (registrations is ICollection { Count: 0 }) + { continue; } - foreach (var registration in registrations) { + foreach (var registration in registrations) + { (decorators ??= []).Add(registration); } } - if (decorators != null) { + if (decorators != null) + { // The same environment the registrations were decided against. ApplyServices runs first // and registers one when nothing supplied it, so this finds that instance rather than // building a second answer to "what environment is this" — a decorator gated on @@ -377,12 +437,14 @@ private static void ApplyDecorators(IServiceCollection serviceCollection, IReadO SortByOrder(decorators); - for (var i = 0; i < decorators.Count; i++) { + for (var i = 0; i < decorators.Count; i++) + { decorators[i].RegistryFunc(serviceCollection, environment); } } - for (var i = 0; i < modules.Count; i++) { + for (var i = 0; i < modules.Count; i++) + { var module = modules[i]; // Retained for hand-written modules that decorate directly. Generated modules use @@ -391,13 +453,18 @@ private static void ApplyDecorators(IServiceCollection serviceCollection, IReadO // Mirrors how ApplyServices invokes ConfigureServices. Runs last, so the manual escape // hatch sees every declared decorator already in place. - if (module is IServiceCollectionConfiguration serviceCollectionConfigure) { + if (module is IServiceCollectionConfiguration serviceCollectionConfigure) + { serviceCollectionConfigure.ConfigureDecorators(serviceCollection); } } } - private static void ApplyServices(IServiceCollection serviceCollection, IReadOnlyList modules) { + private static void ApplyServices( + IServiceCollection serviceCollection, + IReadOnlyList modules + ) + { // Always looked for now. Attribute conditions live on generated modules, which do not // implement IEnvironmentServiceCollectionConfiguration, so the old "only if some module // asked for it" gate would have missed them. It costs one scan of the collection per @@ -409,7 +476,8 @@ private static void ApplyServices(IServiceCollection serviceCollection, IReadOnl // environment at all. An application with no environment says so with ModuleEnvironment.None. // Nothing to apply means nothing to decide, so the collection is left exactly as it was // rather than picking up an environment nobody asked for. - if (modules.Count == 0) { + if (modules.Count == 0) + { return; } @@ -419,15 +487,18 @@ private static void ApplyServices(IServiceCollection serviceCollection, IReadOnl // never displaced — and registering it is what lets ApplyDecorators find the same instance. var environment = ResolveEnvironment(serviceCollection); - for (var i = 0; i < modules.Count; i++) { + for (var i = 0; i < modules.Count; i++) + { var module = modules[i]; module.InternalApplyServices(serviceCollection, environment); - if (module is IServiceCollectionConfiguration serviceCollectionConfigure) { + if (module is IServiceCollectionConfiguration serviceCollectionConfigure) + { serviceCollectionConfigure.ConfigureServices(serviceCollection); } - if (module is IEnvironmentServiceCollectionConfiguration environmentConfigure) { + if (module is IEnvironmentServiceCollectionConfiguration environmentConfigure) + { environmentConfigure.ConfigureServices(serviceCollection, environment); } } @@ -444,15 +515,18 @@ private static void ApplyServices(IServiceCollection serviceCollection, IReadOnl /// the registration that was ignored got shadowed by the one added in its place — a service /// gated on "Development" quietly took its production branch. /// - private static void RefuseUnusableEnvironment(ServiceDescriptor descriptor) { - if (descriptor.ServiceType == typeof(IModuleEnvironment)) { + private static void RefuseUnusableEnvironment(ServiceDescriptor descriptor) + { + if (descriptor.ServiceType == typeof(IModuleEnvironment)) + { throw new InvalidOperationException( - "An IModuleEnvironment is registered, but not as a singleton instance, so it cannot " + - "be used. The environment decides which services are registered, which happens " + - "while the service collection is being populated and before any provider exists to " + - "construct it from. Register the instance directly with " + - "AddSingleton(new MyEnvironment()), or pass it to " + - "AddModules(environment, modules)."); + "An IModuleEnvironment is registered, but not as a singleton instance, so it cannot " + + "be used. The environment decides which services are registered, which happens " + + "while the service collection is being populated and before any provider exists to " + + "construct it from. Register the instance directly with " + + "AddSingleton(new MyEnvironment()), or pass it to " + + "AddModules(environment, modules)." + ); } } @@ -467,18 +541,25 @@ private static void RefuseUnusableEnvironment(ServiceDescriptor descriptor) { /// The last matching descriptor decides, because that is the one the container would resolve. /// Anything earlier is shadowed and cannot be what the application meant. /// - private static IModuleEnvironment? FindModuleEnvironment(IServiceCollection serviceCollection) { - for (var i = serviceCollection.Count - 1; i >= 0; i--) { + private static IModuleEnvironment? FindModuleEnvironment(IServiceCollection serviceCollection) + { + for (var i = serviceCollection.Count - 1; i >= 0; i--) + { var descriptor = serviceCollection[i]; - if (descriptor.ServiceType != typeof(IModuleEnvironment)) { + if (descriptor.ServiceType != typeof(IModuleEnvironment)) + { continue; } - if (descriptor is { + if ( + descriptor is + { Lifetime: ServiceLifetime.Singleton, ImplementationInstance: IModuleEnvironment environment - }) { + } + ) + { return environment; } @@ -488,44 +569,59 @@ private static void RefuseUnusableEnvironment(ServiceDescriptor descriptor) { return null; } - private static void ApplyFeatures(IServiceCollection serviceCollection, IReadOnlyList modules) { + private static void ApplyFeatures( + IServiceCollection serviceCollection, + IReadOnlyList modules + ) + { List? features = null; - for (var i = 0; i < modules.Count; i++) { + for (var i = 0; i < modules.Count; i++) + { var module = modules[i]; - - if (module is IDependencyModuleApplicatorProvider provider) { - foreach (var featureApplicator in provider.FeatureApplicators()) { + + if (module is IDependencyModuleApplicatorProvider provider) + { + foreach (var featureApplicator in provider.FeatureApplicators()) + { (features ??= []).Add(featureApplicator); } } } - - if (features != null) { + + if (features != null) + { features.Sort((x, y) => x.Order.CompareTo(y.Order)); - for (var i = 0; i < features.Count; i++) { + for (var i = 0; i < features.Count; i++) + { var feature = features[i]; feature.Apply(serviceCollection, modules); } } } - - private static IReadOnlyList GetAllModules(IDependencyModule[] dependencyModules) { + private static IReadOnlyList GetAllModules( + IDependencyModule[] dependencyModules + ) + { var list = new List(); - foreach (var dependencyModule in dependencyModules) { + foreach (var dependencyModule in dependencyModules) + { InternalGetModules(dependencyModule, list); } return list; } - - private static void InternalGetModules(IDependencyModule dependencyModule, List allDependencyModules) { - if (!dependencyModule.LoadModule || - AlreadySeen(allDependencyModules, dependencyModule)) { + private static void InternalGetModules( + IDependencyModule dependencyModule, + List allDependencyModules + ) + { + if (!dependencyModule.LoadModule || AlreadySeen(allDependencyModules, dependencyModule)) + { return; } @@ -533,13 +629,17 @@ private static void InternalGetModules(IDependencyModule dependencyModule, List< var declared = dependencyModule.InternalGetModules(); - if (declared is not ICollection { Count: 0 }) { - foreach (var dependencyObject in declared) { - if (dependencyObject is IDependencyModuleProvider moduleProvider) { + if (declared is not ICollection { Count: 0 }) + { + foreach (var dependencyObject in declared) + { + if (dependencyObject is IDependencyModuleProvider moduleProvider) + { var dep = moduleProvider.GetModule(); InternalGetModules(dep, allDependencyModules); } - else if (dependencyObject is IDependencyModule module) { + else if (dependencyObject is IDependencyModule module) + { InternalGetModules(module, allDependencyModules); } } @@ -550,11 +650,13 @@ private static void InternalGetModules(IDependencyModule dependencyModule, List< // Both lists are empty for the overwhelming majority of modules, and both are reached // through an interface. Testing for an empty collection first avoids building an enumerator // for each of them on every module of every startup. - if (overridden is ICollection { Count: 0 }) { + if (overridden is ICollection { Count: 0 }) + { return; } - foreach (var module in overridden) { + foreach (var module in overridden) + { InternalGetModules(module, allDependencyModules); } } @@ -568,15 +670,18 @@ private static void InternalGetModules(IDependencyModule dependencyModule, List< /// for an interface is a runtime type-construction step - it showed up as the single most /// expensive thing in module discovery, to compare a list that usually holds one item. /// - private static bool AlreadySeen(List modules, IDependencyModule candidate) { - for (var i = 0; i < modules.Count; i++) { + private static bool AlreadySeen(List modules, IDependencyModule candidate) + { + for (var i = 0; i < modules.Count; i++) + { // Argument order matches EqualityComparer.Default, which asks the element rather // than the candidate, so a hand-written asymmetric Equals behaves as it always did. - if (modules[i].Equals(candidate)) { + if (modules[i].Equals(candidate)) + { return true; } } return false; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Helpers/EnvironmentConditions.cs b/src/DependencyModules.Runtime/Helpers/EnvironmentConditions.cs index a6ac683..4eedfa2 100644 --- a/src/DependencyModules.Runtime/Helpers/EnvironmentConditions.cs +++ b/src/DependencyModules.Runtime/Helpers/EnvironmentConditions.cs @@ -12,8 +12,8 @@ namespace DependencyModules.Runtime.Helpers; /// The negative forms of the attributes emit a ! around these rather than adding methods. /// [EditorBrowsable(EditorBrowsableState.Never)] -public static class EnvironmentConditions { - +public static class EnvironmentConditions +{ /// /// True when the environment name matches any of . /// @@ -25,13 +25,23 @@ public static class EnvironmentConditions { /// The environment to test. /// The names to accept. /// True when any name matches. - public static bool NameIs(IModuleEnvironment environment, params string[] names) { - if (environment == null) { + public static bool NameIs(IModuleEnvironment environment, params string[] names) + { + if (environment == null) + { return false; } - for (var i = 0; i < names.Length; i++) { - if (string.Equals(environment.EnvironmentName, names[i], StringComparison.OrdinalIgnoreCase)) { + for (var i = 0; i < names.Length; i++) + { + if ( + string.Equals( + environment.EnvironmentName, + names[i], + StringComparison.OrdinalIgnoreCase + ) + ) + { return true; } } diff --git a/src/DependencyModules.Runtime/Interception/CallerInfo.cs b/src/DependencyModules.Runtime/Interception/CallerInfo.cs index e7c2a76..fed6bac 100644 --- a/src/DependencyModules.Runtime/Interception/CallerInfo.cs +++ b/src/DependencyModules.Runtime/Interception/CallerInfo.cs @@ -14,7 +14,8 @@ namespace DependencyModules.Runtime.Interception; /// /// The interface being intercepted. /// The member being invoked. -public readonly struct CallerInfo(Type serviceType, string memberName) { +public readonly struct CallerInfo(Type serviceType, string memberName) +{ /// /// The intercepted interface. /// diff --git a/src/DependencyModules.Runtime/Interception/IArguments.cs b/src/DependencyModules.Runtime/Interception/IArguments.cs index 8451bf3..9633250 100644 --- a/src/DependencyModules.Runtime/Interception/IArguments.cs +++ b/src/DependencyModules.Runtime/Interception/IArguments.cs @@ -9,7 +9,8 @@ namespace DependencyModules.Runtime.Interception; /// writing one replaces the value the implementation will receive, and an interceptor that ignores /// them pays for neither. /// -public interface IArguments { +public interface IArguments +{ /// /// The number of arguments the intercepted member declares. /// diff --git a/src/DependencyModules.Runtime/Interception/IInterceptor.cs b/src/DependencyModules.Runtime/Interception/IInterceptor.cs index 8c8ba1f..7bd317f 100644 --- a/src/DependencyModules.Runtime/Interception/IInterceptor.cs +++ b/src/DependencyModules.Runtime/Interception/IInterceptor.cs @@ -28,7 +28,8 @@ namespace DependencyModules.Runtime.Interception; /// } /// /// -public interface IInterceptor { +public interface IInterceptor +{ /// /// Wraps one call. Call to run the rest of the /// pipeline, more than once to retry, or not at all to return without reaching the @@ -58,7 +59,8 @@ public interface IInterceptor { /// } /// /// -public interface IAsyncInterceptor { +public interface IAsyncInterceptor +{ /// /// Wraps one call. Call to run the /// rest of the pipeline, more than once to retry, or not at all to return without reaching the @@ -92,7 +94,8 @@ public interface IAsyncInterceptor { /// } /// /// -public interface IAsyncEnumerableInterceptor { +public interface IAsyncEnumerableInterceptor +{ /// /// Wraps one call. Enumerate to yield the /// implementation's items, or yield something else to replace them. diff --git a/src/DependencyModules.Runtime/Interception/InvocationContext.cs b/src/DependencyModules.Runtime/Interception/InvocationContext.cs index 59af76d..574a261 100644 --- a/src/DependencyModules.Runtime/Interception/InvocationContext.cs +++ b/src/DependencyModules.Runtime/Interception/InvocationContext.cs @@ -13,7 +13,8 @@ namespace DependencyModules.Runtime.Interception; /// /// The member's return type, or when it returns void. /// -public readonly struct InvocationContext { +public readonly struct InvocationContext +{ private readonly InvocationState _state; private readonly int _stage; @@ -22,7 +23,8 @@ public readonly struct InvocationContext { /// /// The state for this call. /// The position of the interceptor receiving this context. - public InvocationContext(InvocationState state, int stage) { + public InvocationContext(InvocationState state, int stage) + { _state = state; _stage = stage; } @@ -54,7 +56,8 @@ public InvocationContext(InvocationState state, int stage) { /// /// The type the task produces, or for a task with no result. /// -public readonly struct AsyncInvocationContext { +public readonly struct AsyncInvocationContext +{ private readonly AsyncInvocationState _state; private readonly int _stage; @@ -63,7 +66,8 @@ public readonly struct AsyncInvocationContext { /// /// The state for this call. /// The position of the interceptor receiving this context. - public AsyncInvocationContext(AsyncInvocationState state, int stage) { + public AsyncInvocationContext(AsyncInvocationState state, int stage) + { _state = state; _stage = stage; } @@ -93,7 +97,8 @@ public AsyncInvocationContext(AsyncInvocationState state, int stage) { /// observes each item as it is produced rather than only the call that produced the stream. /// /// The type the stream yields. -public readonly struct StreamInvocationContext { +public readonly struct StreamInvocationContext +{ private readonly StreamInvocationState _state; private readonly int _stage; @@ -102,7 +107,8 @@ public readonly struct StreamInvocationContext { /// /// The state for this call. /// The position of the interceptor receiving this context. - public StreamInvocationContext(StreamInvocationState state, int stage) { + public StreamInvocationContext(StreamInvocationState state, int stage) + { _state = state; _stage = stage; } diff --git a/src/DependencyModules.Runtime/Interception/InvocationState.cs b/src/DependencyModules.Runtime/Interception/InvocationState.cs index 34d1748..0fac998 100644 --- a/src/DependencyModules.Runtime/Interception/InvocationState.cs +++ b/src/DependencyModules.Runtime/Interception/InvocationState.cs @@ -12,7 +12,8 @@ namespace DependencyModules.Runtime.Interception; /// is a struct pointing at one of these plus a stage index, /// so proceeding is a virtual call rather than a closure allocation and a Func per interceptor. /// -public abstract class InvocationState : IArguments { +public abstract class InvocationState : IArguments +{ /// /// The member being invoked. /// @@ -34,7 +35,8 @@ public abstract class InvocationState : IArguments { /// /// The member's return type, or when it returns void. /// -public abstract class InvocationState : InvocationState { +public abstract class InvocationState : InvocationState +{ /// /// Runs the pipeline from onwards. /// @@ -51,7 +53,8 @@ public abstract class InvocationState : InvocationState { /// /// The type the task produces, or for a task with no result. /// -public abstract class AsyncInvocationState : InvocationState { +public abstract class AsyncInvocationState : InvocationState +{ /// /// Runs the pipeline from onwards. /// @@ -66,7 +69,8 @@ public abstract class AsyncInvocationState : InvocationState { /// Invocation state for a member returning an async stream. /// /// The type the stream yields. -public abstract class StreamInvocationState : InvocationState { +public abstract class StreamInvocationState : InvocationState +{ /// /// Runs the pipeline from onwards. /// diff --git a/src/DependencyModules.Runtime/Interception/NoResult.cs b/src/DependencyModules.Runtime/Interception/NoResult.cs index ac95a40..9f897ee 100644 --- a/src/DependencyModules.Runtime/Interception/NoResult.cs +++ b/src/DependencyModules.Runtime/Interception/NoResult.cs @@ -9,7 +9,8 @@ namespace DependencyModules.Runtime.Interception; /// as everything else, rather than each interceptor having to implement an overload it does not care /// about. An interceptor written generically never names this type. /// -public readonly struct NoResult { +public readonly struct NoResult +{ /// /// Renders as void, so an interceptor logging a result does not print a type name. /// diff --git a/src/DependencyModules.Runtime/Interfaces/IDependencyModule.cs b/src/DependencyModules.Runtime/Interfaces/IDependencyModule.cs index 1cdbff2..02f843e 100644 --- a/src/DependencyModules.Runtime/Interfaces/IDependencyModule.cs +++ b/src/DependencyModules.Runtime/Interfaces/IDependencyModule.cs @@ -8,12 +8,13 @@ namespace DependencyModules.Runtime.Interfaces; /// /// Internal interface not intended to be consumed by developers /// -public interface IDependencyModule { +public interface IDependencyModule +{ /// /// Flag to disable loading module and dependencies. /// bool LoadModule => true; - + /// /// Populate a service collection with registrations /// @@ -24,16 +25,18 @@ public interface IDependencyModule { /// Intended for developers to override and provide their own IDependencyModules /// /// - IEnumerable GetModules() { + IEnumerable GetModules() + { return Array.Empty(); } - + /// /// Internal method not intended to be called by general developers /// /// [Browsable(false)] - IEnumerable InternalGetModules() { + IEnumerable InternalGetModules() + { // Array.Empty() rather than an array of the interface, so the runtime's empty check // is a plain ICollection test rather than one relying on array covariance. return Array.Empty(); @@ -57,11 +60,11 @@ void InternalApplyServices(IServiceCollection serviceCollection) { } /// /// Never null; see ModuleEnvironment.Default. [Browsable(false)] - void InternalApplyServices(IServiceCollection serviceCollection, IModuleEnvironment environment) { + void InternalApplyServices(IServiceCollection serviceCollection, IModuleEnvironment environment) + { InternalApplyServices(serviceCollection); } - /// /// Internal method not intended to be called by general developers /// @@ -78,7 +81,8 @@ void InternalApplyDecorators(IServiceCollection serviceCollection) { } /// order the developer declared. /// [Browsable(false)] - IEnumerable InternalGetDecorators() { + IEnumerable InternalGetDecorators() + { return Array.Empty(); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Interfaces/IDependencyModuleProvider.cs b/src/DependencyModules.Runtime/Interfaces/IDependencyModuleProvider.cs index 61f6ba7..99ca435 100644 --- a/src/DependencyModules.Runtime/Interfaces/IDependencyModuleProvider.cs +++ b/src/DependencyModules.Runtime/Interfaces/IDependencyModuleProvider.cs @@ -3,7 +3,8 @@ namespace DependencyModules.Runtime.Interfaces; /// /// Internal interface not intended to be consumed by developers /// -public interface IDependencyModuleProvider { +public interface IDependencyModuleProvider +{ /// /// Retrieves an instance of a dependency module. /// @@ -11,4 +12,4 @@ public interface IDependencyModuleProvider { /// An instance of the IDependencyModule. /// IDependencyModule GetModule(); -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/Interfaces/IModuleEnvironment.cs b/src/DependencyModules.Runtime/Interfaces/IModuleEnvironment.cs index 62b5014..2535d4c 100644 --- a/src/DependencyModules.Runtime/Interfaces/IModuleEnvironment.cs +++ b/src/DependencyModules.Runtime/Interfaces/IModuleEnvironment.cs @@ -3,7 +3,8 @@ namespace DependencyModules.Runtime.Interfaces; /// /// Minimal environment interface for conditional service registration. /// -public interface IModuleEnvironment { +public interface IModuleEnvironment +{ /// /// The name of the current environment (e.g. "Development", "Production"). /// diff --git a/src/DependencyModules.Runtime/Interfaces/IModuleEnvironmentProvider.cs b/src/DependencyModules.Runtime/Interfaces/IModuleEnvironmentProvider.cs index a6ac21a..f5b79ca 100644 --- a/src/DependencyModules.Runtime/Interfaces/IModuleEnvironmentProvider.cs +++ b/src/DependencyModules.Runtime/Interfaces/IModuleEnvironmentProvider.cs @@ -27,8 +27,8 @@ namespace DependencyModules.Runtime.Interfaces; /// narrowest one that answers decides, matching how every other attribute resolves. /// /// -public interface IModuleEnvironmentProvider { - +public interface IModuleEnvironmentProvider +{ /// /// The environment module conditions are evaluated against, or null to leave the decision /// to a wider scope. diff --git a/src/DependencyModules.Runtime/Interfaces/IServiceCollectionConfiguration.cs b/src/DependencyModules.Runtime/Interfaces/IServiceCollectionConfiguration.cs index e8083d0..f29f2cd 100644 --- a/src/DependencyModules.Runtime/Interfaces/IServiceCollectionConfiguration.cs +++ b/src/DependencyModules.Runtime/Interfaces/IServiceCollectionConfiguration.cs @@ -5,7 +5,8 @@ namespace DependencyModules.Runtime.Interfaces; /// /// DependencyModules that want to do programmatic registration should implement this interface. /// -public interface IServiceCollectionConfiguration { +public interface IServiceCollectionConfiguration +{ /// /// Configure service in IServiceCollection /// @@ -22,7 +23,8 @@ void ConfigureDecorators(IServiceCollection services) { } /// /// DependencyModules that need access to the environment during registration should implement this interface. /// -public interface IEnvironmentServiceCollectionConfiguration { +public interface IEnvironmentServiceCollectionConfiguration +{ /// /// Configure services with access to the module environment. /// @@ -36,4 +38,4 @@ public interface IEnvironmentServiceCollectionConfiguration { /// /// The environment for this AddModules call. Never null. void ConfigureServices(IServiceCollection services, IModuleEnvironment environment); -} \ No newline at end of file +} diff --git a/src/DependencyModules.Runtime/ModuleEnvironment.cs b/src/DependencyModules.Runtime/ModuleEnvironment.cs index 234dd6e..a889b05 100644 --- a/src/DependencyModules.Runtime/ModuleEnvironment.cs +++ b/src/DependencyModules.Runtime/ModuleEnvironment.cs @@ -22,7 +22,8 @@ namespace DependencyModules.Runtime; /// new ApplicationModule()); /// /// -public class ModuleEnvironment : IModuleEnvironment, IEnumerable> { +public class ModuleEnvironment : IModuleEnvironment, IEnumerable> +{ private readonly Dictionary _values; private readonly bool _fallBackToEnvironmentVariables; @@ -40,7 +41,10 @@ public class ModuleEnvironment : IModuleEnvironment, IEnumerable /// The environment name conditions compare against. /// Values reachable through ; null means none. - public ModuleEnvironment(string environmentName, IReadOnlyDictionary? values = null) + public ModuleEnvironment( + string environmentName, + IReadOnlyDictionary? values = null + ) : this(true, environmentName, values) { } /// @@ -63,19 +67,25 @@ public ModuleEnvironment(string environmentName, IReadOnlyDictionary? values = null) { - EnvironmentName = environmentName ?? throw new ArgumentNullException(nameof(environmentName)); + IReadOnlyDictionary? values = null + ) + { + EnvironmentName = + environmentName ?? throw new ArgumentNullException(nameof(environmentName)); _fallBackToEnvironmentVariables = fallBackToEnvironmentVariables; // Copied rather than held by reference. Add writes to this dictionary, and writing into one // the caller still holds would be a side effect they did not ask for. A caller who supplied // a comparer picked it deliberately — most often OrdinalIgnoreCase, matching how Windows // treats variable names — so it is carried over instead of being reset to ordinal. - _values = values switch { - Dictionary dictionary => - new Dictionary(dictionary, dictionary.Comparer), + _values = values switch + { + Dictionary dictionary => new Dictionary( + dictionary, + dictionary.Comparer + ), not null => new Dictionary(values), - null => new Dictionary() + null => new Dictionary(), }; } @@ -95,12 +105,15 @@ public ModuleEnvironment( /// no longer seeing a variable changed mid-process, which nothing should be relying on; ask /// for a fresh view if you need one. /// - public string? Value(string name) { - if (_values.TryGetValue(name, out var value)) { + public string? Value(string name) + { + if (_values.TryGetValue(name, out var value)) + { return value; } - if (!_fallBackToEnvironmentVariables) { + if (!_fallBackToEnvironmentVariables) + { return null; } @@ -184,27 +197,37 @@ public ModuleEnvironment( /// collections and allocating its lock and bucket arrays to serve a cache most applications /// never touch. /// - private static class ProcessValueCache { - public static string? Read(ref Dictionary? cache, string name) { + private static class ProcessValueCache + { + public static string? Read(ref Dictionary? cache, string name) + { var map = Volatile.Read(ref cache); - if (map != null) { - lock (map) { - if (map.TryGetValue(name, out var cached)) { + if (map != null) + { + lock (map) + { + if (map.TryGetValue(name, out var cached)) + { return cached; } } } - else { + else + { // Whoever gets there first owns the cache; a loser simply fills the winner's. - map = Interlocked.CompareExchange( - ref cache, new Dictionary(StringComparer.Ordinal), null) - ?? Volatile.Read(ref cache)!; + map = + Interlocked.CompareExchange( + ref cache, + new Dictionary(StringComparer.Ordinal), + null + ) ?? Volatile.Read(ref cache)!; } var value = Environment.GetEnvironmentVariable(name); - lock (map) { + lock (map) + { map[name] = value; } @@ -212,20 +235,22 @@ private static class ProcessValueCache { } } - private sealed class ProcessModuleEnvironment : IModuleEnvironment { + private sealed class ProcessModuleEnvironment : IModuleEnvironment + { private Dictionary? _values; // Not cached. It is read once per AddModules call rather than per service, and a fresh // instance is what CreateDefault hands out anyway. public string EnvironmentName => - Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? - Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? - "Production"; + Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") + ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") + ?? "Production"; public string? Value(string name) => ProcessValueCache.Read(ref _values, name); } - private sealed class EmptyModuleEnvironment : IModuleEnvironment { + private sealed class EmptyModuleEnvironment : IModuleEnvironment + { public string EnvironmentName => ""; public string? Value(string name) => null; diff --git a/src/DependencyModules.Runtime/ServiceCollectionExtensions.cs b/src/DependencyModules.Runtime/ServiceCollectionExtensions.cs index c975bb6..2ef56b6 100644 --- a/src/DependencyModules.Runtime/ServiceCollectionExtensions.cs +++ b/src/DependencyModules.Runtime/ServiceCollectionExtensions.cs @@ -8,7 +8,8 @@ namespace DependencyModules.Runtime; /// /// Provides extension methods for adding dependency modules to the IServiceCollection. /// -public static class ServiceCollectionExtensions { +public static class ServiceCollectionExtensions +{ /// /// Add dependency module to service collection /// @@ -16,7 +17,8 @@ public static class ServiceCollectionExtensions { /// /// public static IServiceCollection AddModule(this IServiceCollection services) - where T : IDependencyModule, new() { + where T : IDependencyModule, new() + { return AddModule(services, new T()); } @@ -27,19 +29,27 @@ public static IServiceCollection AddModule(this IServiceCollection services) /// /// // ReSharper disable once MemberCanBePrivate.Global - public static IServiceCollection AddModule(this IServiceCollection services, IDependencyModule module) { + public static IServiceCollection AddModule( + this IServiceCollection services, + IDependencyModule module + ) + { module.PopulateServiceCollection(services); return services; } - + /// /// Add dependency modules to service collection /// /// /// /// - public static IServiceCollection AddModules(this IServiceCollection services, params IDependencyModule[] modules) { + public static IServiceCollection AddModules( + this IServiceCollection services, + params IDependencyModule[] modules + ) + { DependencyRegistry.LoadModules(services, modules); return services; @@ -60,8 +70,14 @@ public static IServiceCollection AddModules(this IServiceCollection services, pa /// written down. To layer one on another, read the existing one and combine before calling this; /// the collection is right there. /// - public static IServiceCollection AddModules(this IServiceCollection services, IModuleEnvironment? environment, params IDependencyModule[] modules) { - if (environment != null) { + public static IServiceCollection AddModules( + this IServiceCollection services, + IModuleEnvironment? environment, + params IDependencyModule[] modules + ) + { + if (environment != null) + { services.RemoveAll(); services.AddSingleton(environment); } @@ -70,4 +86,4 @@ public static IServiceCollection AddModules(this IServiceCollection services, IM return services; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/BaseAttributeSourceGenerator.cs b/src/DependencyModules.SourceGenerator.Impl/BaseAttributeSourceGenerator.cs index 485ac56..8432aaa 100644 --- a/src/DependencyModules.SourceGenerator.Impl/BaseAttributeSourceGenerator.cs +++ b/src/DependencyModules.SourceGenerator.Impl/BaseAttributeSourceGenerator.cs @@ -7,25 +7,39 @@ namespace DependencyModules.SourceGenerator.Impl; -public interface IDependencyModuleSourceGenerator { - void SetupGenerator(IncrementalGeneratorInitializationContext context, - IncrementalValuesProvider<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> incrementalValueProvider); +public interface IDependencyModuleSourceGenerator +{ + void SetupGenerator( + IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> incrementalValueProvider + ); } -public abstract class BaseAttributeSourceGenerator : IDependencyModuleSourceGenerator { - - public void SetupGenerator(IncrementalGeneratorInitializationContext context, - IncrementalValuesProvider<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> incrementalValueProvider) { - +public abstract class BaseAttributeSourceGenerator : IDependencyModuleSourceGenerator +{ + public void SetupGenerator( + IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> incrementalValueProvider + ) + { var attributeTypes = AttributeTypes().ToArray(); - if (attributeTypes.Length == 0) { + if (attributeTypes.Length == 0) + { return; } // One provider, two outputs. Sharing it means the transform runs once; registering // CollectModels twice would do the same discovery work twice over. - var models = incrementalValueProvider.Collect().Combine(CollectModels(context, attributeTypes)); + var models = incrementalValueProvider + .Collect() + .Combine(CollectModels(context, attributeTypes)); context.RegisterSourceOutput(models, WrapGenerateSourceOutput); @@ -35,22 +49,42 @@ public void SetupGenerator(IncrementalGeneratorInitializationContext context, // into the emitting output instead would re-emit every file on every keystroke, since the // compilation changes with each one; this output emits nothing, so re-running it is a walk // over models that are already cached. - context.RegisterSourceOutput(models.Combine(context.CompilationProvider), WrapReportDiagnostics); + context.RegisterSourceOutput( + models.Combine(context.CompilationProvider), + WrapReportDiagnostics + ); } /// /// One provider per attribute, merged. See . /// private IncrementalValueProvider> CollectModels( - IncrementalGeneratorInitializationContext context, ITypeDefinition[] attributeTypes) => + IncrementalGeneratorInitializationContext context, + ITypeDefinition[] attributeTypes + ) => AttributeModelCollector.Collect( - context, attributeTypes, GenerateAttributeModel, GetComparer(), IgnoredModel); - - private void WrapGenerateSourceOutput(SourceProductionContext context, - (ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, ImmutableArray Right) data) { + context, + attributeTypes, + GenerateAttributeModel, + GetComparer(), + IgnoredModel + ); + + private void WrapGenerateSourceOutput( + SourceProductionContext context, + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) data + ) + { var config = data.Left.FirstOrDefault().Right; - if (config != null) { + if (config != null) + { FileLogger.Wrap( LoggerName, config, @@ -58,29 +92,50 @@ private void WrapGenerateSourceOutput(SourceProductionContext context, // Surfaced as a build error rather than discarded. A generator that fails quietly // produces a green build with no registrations, which is far harder to diagnose // than a failed one. - exception => context.ReportDiagnostic( - Diagnostic.Create( - DependencyModuleDiagnostics.GeneratorFailure, - Location.None, - $"{exception.GetType().Name}: {exception.Message}"))); + exception => + context.ReportDiagnostic( + Diagnostic.Create( + DependencyModuleDiagnostics.GeneratorFailure, + Location.None, + $"{exception.GetType().Name}: {exception.Message}" + ) + ) + ); } } - private void WrapReportDiagnostics(SourceProductionContext context, - ((ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, ImmutableArray Right) Left, - Compilation Right) data) { + private void WrapReportDiagnostics( + SourceProductionContext context, + ( + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) Left, + Compilation Right + ) data + ) + { var config = data.Left.Left.FirstOrDefault().Right; - if (config != null) { + if (config != null) + { FileLogger.Wrap( LoggerName, config, - logger => ReportDiagnostics(context, data.Left, new SyntaxTreeLookup(data.Right), logger), - exception => context.ReportDiagnostic( - Diagnostic.Create( - DependencyModuleDiagnostics.GeneratorFailure, - Location.None, - $"{exception.GetType().Name}: {exception.Message}"))); + logger => + ReportDiagnostics(context, data.Left, new SyntaxTreeLookup(data.Right), logger), + exception => + context.ReportDiagnostic( + Diagnostic.Create( + DependencyModuleDiagnostics.GeneratorFailure, + Location.None, + $"{exception.GetType().Name}: {exception.Message}" + ) + ) + ); } } @@ -92,26 +147,45 @@ private void WrapReportDiagnostics(SourceProductionContext context, /// compilation and the emission does not have to. A generator with nothing to report leaves it /// alone; the conditions belong beside the ones emission uses to skip a model, not duplicated. /// - protected virtual void ReportDiagnostics(SourceProductionContext context, - (ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, ImmutableArray Right) data, + protected virtual void ReportDiagnostics( + SourceProductionContext context, + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) data, SyntaxTreeLookup lookup, - FileLogger logger) { } + FileLogger logger + ) { } protected virtual string LoggerName => GetType().Name; protected abstract IEnumerable AttributeTypes(); - protected abstract void GenerateSourceOutput(SourceProductionContext arg1, - (ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, ImmutableArray Right) valueTuple, - FileLogger logger); + protected abstract void GenerateSourceOutput( + SourceProductionContext arg1, + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) valueTuple, + FileLogger logger + ); protected abstract IEqualityComparer GetComparer(); - protected abstract T GenerateAttributeModel(GeneratorAttributeSyntaxContext arg1, CancellationToken arg2); + protected abstract T GenerateAttributeModel( + GeneratorAttributeSyntaxContext arg1, + CancellationToken arg2 + ); /// /// The sentinel this generator emits for a declaration it does not own. Every model type already /// has one, and the writers already skip it. /// protected abstract T IgnoredModel { get; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/BaseAttributeWithConfigSourceGenerator.cs b/src/DependencyModules.SourceGenerator.Impl/BaseAttributeWithConfigSourceGenerator.cs index 0c03959..d1d63bd 100644 --- a/src/DependencyModules.SourceGenerator.Impl/BaseAttributeWithConfigSourceGenerator.cs +++ b/src/DependencyModules.SourceGenerator.Impl/BaseAttributeWithConfigSourceGenerator.cs @@ -8,41 +8,60 @@ namespace DependencyModules.SourceGenerator.Impl; -public abstract class BaseAttributeWithConfigSourceGenerator : IDependencyModuleSourceGenerator { - - public void SetupGenerator(IncrementalGeneratorInitializationContext context, - IncrementalValuesProvider<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> incrementalValueProvider) { - var classSelector = new SyntaxSelector(AttributeTypes().ToArray()); +public abstract class BaseAttributeWithConfigSourceGenerator + : IDependencyModuleSourceGenerator +{ + public void SetupGenerator( + IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> incrementalValueProvider + ) + { + var classSelector = new SyntaxSelector( + AttributeTypes().ToArray() + ); - var serviceModelProvider = context.SyntaxProvider.CreateSyntaxProvider( - classSelector.Where, - GenerateAttributeModel - ).WithComparer(GetComparer()); + var serviceModelProvider = context + .SyntaxProvider.CreateSyntaxProvider(classSelector.Where, GenerateAttributeModel) + .WithComparer(GetComparer()); - var collection = - serviceModelProvider.Collect(); + var collection = serviceModelProvider.Collect(); - var config = - context.AnalyzerConfigOptionsProvider.Select(GenerateConfigAttributeModel).WithComparer(GetConfigComparer()); + var config = context + .AnalyzerConfigOptionsProvider.Select(GenerateConfigAttributeModel) + .WithComparer(GetConfigComparer()); var valuesProvider = incrementalValueProvider.Combine(config); - context.RegisterSourceOutput( - valuesProvider.Combine(collection), - GenerateSourceOutput - ); + context.RegisterSourceOutput(valuesProvider.Combine(collection), GenerateSourceOutput); } protected abstract IEnumerable AttributeTypes(); - protected abstract void GenerateSourceOutput(SourceProductionContext arg1, - (((ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right) Left, TConfig Right) Left, ImmutableArray Right) arg2); + protected abstract void GenerateSourceOutput( + SourceProductionContext arg1, + ( + ( + (ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right) Left, + TConfig Right + ) Left, + ImmutableArray Right + ) arg2 + ); - protected abstract TConfig GenerateConfigAttributeModel(AnalyzerConfigOptionsProvider arg1, CancellationToken arg2); + protected abstract TConfig GenerateConfigAttributeModel( + AnalyzerConfigOptionsProvider arg1, + CancellationToken arg2 + ); protected abstract IEqualityComparer GetComparer(); - protected abstract TModel GenerateAttributeModel(GeneratorSyntaxContext arg1, CancellationToken arg2); + protected abstract TModel GenerateAttributeModel( + GeneratorSyntaxContext arg1, + CancellationToken arg2 + ); protected abstract IEqualityComparer GetConfigComparer(); -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs b/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs index 6cbc849..4cd3e72 100644 --- a/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs +++ b/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs @@ -3,21 +3,23 @@ using DependencyModules.SourceGenerator.Impl.Models; using DependencyModules.SourceGenerator.Impl.Utilities; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; namespace DependencyModules.SourceGenerator.Impl; -public abstract class BaseSourceGenerator : IIncrementalGenerator { - - public void Initialize(IncrementalGeneratorInitializationContext context) { +public abstract class BaseSourceGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { var incrementalValueProvider = CreateSourceValueProvider(context); var dependencyConfigurationProvider = CreateConfigurationValueProvider(context); var valuesProvider = incrementalValueProvider.Combine(dependencyConfigurationProvider); - foreach (var attributeSourceGenerator in AttributeSourceGenerators()) { + foreach (var attributeSourceGenerator in AttributeSourceGenerators()) + { attributeSourceGenerator.SetupGenerator(context, valuesProvider); } @@ -30,72 +32,138 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { /// Returns the attribute types that trigger module source generation. /// Override to support custom trigger attributes (e.g. for framework-specific module attributes). /// - protected virtual ITypeDefinition[] ModuleAttributeTypes() { + protected virtual ITypeDefinition[] ModuleAttributeTypes() + { return new[] { KnownTypes.DependencyModules.Attributes.DependencyModuleAttribute }; } - private IncrementalValueProvider CreateConfigurationValueProvider(IncrementalGeneratorInitializationContext context) { - return context.AnalyzerConfigOptionsProvider.Select((options, _) => { - RegistrationType defaultRegistrationType = RegistrationType.Add; - bool registerSourceGenerator = false; - bool autoGenerateEntry = true; - bool generateFactories = false; - var rootNamespace = ""; - var projectDirectory = ""; - var logOutputFolder = ""; - - if (options.GlobalOptions.TryGetValue( - "build_property.DependencyModules_RegistrationType", out var value)) { - defaultRegistrationType = GetRegistrationType(value); - } - - if (options.GlobalOptions.TryGetValue( - "build_property.DependencyModules_LogOutputDirectory", out var logOutputFolderValue)) { - logOutputFolder = logOutputFolderValue; - } - - if (TryGetBoolean(options, "DependencyModules_RegisterGenerator", out var generator)) { - registerSourceGenerator = generator; - } + private IncrementalValueProvider CreateConfigurationValueProvider( + IncrementalGeneratorInitializationContext context + ) + { + return context + .AnalyzerConfigOptionsProvider.Select( + (options, _) => + { + RegistrationType defaultRegistrationType = RegistrationType.Add; + bool registerSourceGenerator = false; + bool autoGenerateEntry = true; + bool generateFactories = false; + var rootNamespace = ""; + var projectDirectory = ""; + var logOutputFolder = ""; + + if ( + options.GlobalOptions.TryGetValue( + "build_property.DependencyModules_RegistrationType", + out var value + ) + ) + { + defaultRegistrationType = GetRegistrationType(value); + } - if (options.GlobalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespaceString)) { - rootNamespace = rootNamespaceString; - } + if ( + options.GlobalOptions.TryGetValue( + "build_property.DependencyModules_LogOutputDirectory", + out var logOutputFolderValue + ) + ) + { + logOutputFolder = logOutputFolderValue; + } - if (options.GlobalOptions.TryGetValue("build_property.ProjectDir", out var projectDirString)) { - projectDirectory = projectDirString; - } - - if (TryGetBoolean(options, "DependencyModules_AutoGenerateModule", out var autoGenerateEntryValue)) { - autoGenerateEntry = autoGenerateEntryValue; - } - - if (TryGetBoolean(options, "DependencyModules_GenerateFactories", out var generateFactoriesValue)) { - generateFactories = generateFactoriesValue; - } + if ( + TryGetBoolean( + options, + "DependencyModules_RegisterGenerator", + out var generator + ) + ) + { + registerSourceGenerator = generator; + } - var excludeGeneratedCodeFromCoverage = true; - if (TryGetBoolean(options, "ExcludeGeneratedCodeFromCoverage", out var excludeCoverageValue)) { - excludeGeneratedCodeFromCoverage = excludeCoverageValue; - } + if ( + options.GlobalOptions.TryGetValue( + "build_property.RootNamespace", + out var rootNamespaceString + ) + ) + { + rootNamespace = rootNamespaceString; + } - var codeStyle = BraceStyle.Allman; - if (options.GlobalOptions.TryGetValue("build_property.GeneratedCodeStyle", out var codeStyleValue)) { - codeStyle = GetCodeStyle(codeStyleValue); - } + if ( + options.GlobalOptions.TryGetValue( + "build_property.ProjectDir", + out var projectDirString + ) + ) + { + projectDirectory = projectDirString; + } + + if ( + TryGetBoolean( + options, + "DependencyModules_AutoGenerateModule", + out var autoGenerateEntryValue + ) + ) + { + autoGenerateEntry = autoGenerateEntryValue; + } - return new DependencyModuleConfigurationModel( - defaultRegistrationType, - registerSourceGenerator, - rootNamespace, - projectDirectory, - autoGenerateEntry, - logOutputFolder, - LogOutputLevel.Debug, - generateFactories, - excludeGeneratedCodeFromCoverage, - codeStyle); - }).WithComparer(new DependencyModuleConfigurationModelComparer()); + if ( + TryGetBoolean( + options, + "DependencyModules_GenerateFactories", + out var generateFactoriesValue + ) + ) + { + generateFactories = generateFactoriesValue; + } + + var excludeGeneratedCodeFromCoverage = true; + if ( + TryGetBoolean( + options, + "ExcludeGeneratedCodeFromCoverage", + out var excludeCoverageValue + ) + ) + { + excludeGeneratedCodeFromCoverage = excludeCoverageValue; + } + + var codeStyle = BraceStyle.Allman; + if ( + options.GlobalOptions.TryGetValue( + "build_property.GeneratedCodeStyle", + out var codeStyleValue + ) + ) + { + codeStyle = GetCodeStyle(codeStyleValue); + } + + return new DependencyModuleConfigurationModel( + defaultRegistrationType, + registerSourceGenerator, + rootNamespace, + projectDirectory, + autoGenerateEntry, + logOutputFolder, + LogOutputLevel.Debug, + generateFactories, + excludeGeneratedCodeFromCoverage, + codeStyle + ); + } + ) + .WithComparer(new DependencyModuleConfigurationModelComparer()); } /// @@ -106,11 +174,19 @@ private IncrementalValueProvider CreateConfi /// the developer has not set it. Without this check every boolean default would be overwritten /// with false the moment the property was made visible. /// - private static bool TryGetBoolean(AnalyzerConfigOptionsProvider options, string propertyName, out bool value) { + private static bool TryGetBoolean( + AnalyzerConfigOptionsProvider options, + string propertyName, + out bool value + ) + { value = false; - if (!options.GlobalOptions.TryGetValue("build_property." + propertyName, out var raw) || - string.IsNullOrWhiteSpace(raw)) { + if ( + !options.GlobalOptions.TryGetValue("build_property." + propertyName, out var raw) + || string.IsNullOrWhiteSpace(raw) + ) + { return false; } @@ -142,10 +218,15 @@ private static bool TryGetBoolean(AnalyzerConfigOptionsProvider options, string /// contributing only providers — overrides this with an empty body. /// /// - protected virtual void SetupRootGenerator(IncrementalGeneratorInitializationContext context, - IncrementalValueProvider> valuesProvider) { - - if (TriggersOnDefaultModuleAttribute()) { + protected virtual void SetupRootGenerator( + IncrementalGeneratorInitializationContext context, + IncrementalValueProvider< + ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> + > valuesProvider + ) + { + if (TriggersOnDefaultModuleAttribute()) + { return; } @@ -171,79 +252,111 @@ protected virtual void SetupRootGenerator(IncrementalGeneratorInitializationCont /// Whether this generator reads the module attribute this package declares, rather than one of /// its own. It decides who writes a module, and who is contributing to someone else's. /// - private bool TriggersOnDefaultModuleAttribute() { + private bool TriggersOnDefaultModuleAttribute() + { var moduleAttributes = ModuleAttributeTypes(); - return moduleAttributes.Length == 1 && - moduleAttributes[0].Equals(KnownTypes.DependencyModules.Attributes.DependencyModuleAttribute); + return moduleAttributes.Length == 1 + && moduleAttributes[0] + .Equals(KnownTypes.DependencyModules.Attributes.DependencyModuleAttribute); } - private IncrementalValuesProvider CreateSourceValueProvider(IncrementalGeneratorInitializationContext context) { - var classSelector = new SyntaxSelector( - ModuleAttributeTypes()) { + private IncrementalValuesProvider CreateSourceValueProvider( + IncrementalGeneratorInitializationContext context + ) + { + var classSelector = new SyntaxSelector< + ClassDeclarationSyntax, + RecordDeclarationSyntax, + CompilationUnitSyntax + >(ModuleAttributeTypes()) + { AutoApproveCompilationUnit = ShouldAutoApproveCompilationUnit, ApproveFilter = "Program.cs", }; - return context.SyntaxProvider.CreateSyntaxProvider( - classSelector.Where, - GenerateEntryPointModel - ).WithComparer(new ModuleEntryPointModelComparer()); + return context + .SyntaxProvider.CreateSyntaxProvider(classSelector.Where, GenerateEntryPointModel) + .WithComparer(new ModuleEntryPointModelComparer()); } - protected virtual ModuleEntryPointModel GenerateEntryPointModel(GeneratorSyntaxContext context, CancellationToken cancellation) { + protected virtual ModuleEntryPointModel GenerateEntryPointModel( + GeneratorSyntaxContext context, + CancellationToken cancellation + ) + { cancellation.ThrowIfCancellationRequested(); - if (context.Node is TypeDeclarationSyntax typeDeclarationSyntax) { + if (context.Node is TypeDeclarationSyntax typeDeclarationSyntax) + { return GetClassEntryPointModel(context, cancellation, typeDeclarationSyntax); } return GetCompilationUnitSyntaxEntry(context, cancellation); } - private ModuleEntryPointModel GetClassEntryPointModel(GeneratorSyntaxContext context, CancellationToken cancellation, TypeDeclarationSyntax typeDeclarationSyntax) { + private ModuleEntryPointModel GetClassEntryPointModel( + GeneratorSyntaxContext context, + CancellationToken cancellation, + TypeDeclarationSyntax typeDeclarationSyntax + ) + { var featureTypes = new List(); ModuleEntryPointFeatures features = ModuleEntryPointFeatures.None; List? attributes = AttributeModelHelper .GetAttributes(context, typeDeclarationSyntax.AttributeLists, cancellation) .ToList(); - if (typeDeclarationSyntax.BaseList != null) { - foreach (var baseType in typeDeclarationSyntax.BaseList.Types) { + if (typeDeclarationSyntax.BaseList != null) + { + foreach (var baseType in typeDeclarationSyntax.BaseList.Types) + { var typeDefinition = baseType.Type.GetTypeDefinition(context); - if (typeDefinition is GenericTypeDefinition { TypeDefinitionEnum: TypeDefinitionEnum.InterfaceDefinition, Name: "IDependencyModuleFeature" } genericTypeDefinition) { + if ( + typeDefinition is GenericTypeDefinition + { + TypeDefinitionEnum: TypeDefinitionEnum.InterfaceDefinition, + Name: "IDependencyModuleFeature" + } genericTypeDefinition + ) + { featureTypes.Add(genericTypeDefinition.TypeArguments.First()); } } } - + var dependencyFlags = GetDependencyFlags(context); var implementsEqualsFlag = GetEqualsFlag(context); var modelInfo = AttributeModelHelper.GetAttributeClassInfo(context, cancellation); - if (dependencyFlags.OnlyRealm) { + if (dependencyFlags.OnlyRealm) + { features |= ModuleEntryPointFeatures.OnlyRealm; } - if (typeDeclarationSyntax is RecordDeclarationSyntax) { + if (typeDeclarationSyntax is RecordDeclarationSyntax) + { features |= ModuleEntryPointFeatures.IsRecord; } - else if (!implementsEqualsFlag) { + else if (!implementsEqualsFlag) + { features |= ModuleEntryPointFeatures.ShouldImplementEquals; } - if (!typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) { + if (!typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { features |= ModuleEntryPointFeatures.NotPartial; } // A module nested inside another type cannot be completed where it was written: the // generated half is emitted at namespace level, so it becomes a second, unrelated type and // the nested declaration never implements IDependencyModule. - if (typeDeclarationSyntax.Parent is TypeDeclarationSyntax) { + if (typeDeclarationSyntax.Parent is TypeDeclarationSyntax) + { features |= ModuleEntryPointFeatures.NestedInType; } - + return new ModuleEntryPointModel( features, context.Node.SyntaxTree?.FilePath ?? "", @@ -262,31 +375,52 @@ private ModuleEntryPointModel GetClassEntryPointModel(GeneratorSyntaxContext con ); } - private ModuleEntryPointModel GetCompilationUnitSyntaxEntry(GeneratorSyntaxContext context, CancellationToken cancellation) { + private ModuleEntryPointModel GetCompilationUnitSyntaxEntry( + GeneratorSyntaxContext context, + CancellationToken cancellation + ) + { var compilationUnitSyntax = (CompilationUnitSyntax)context.Node; var attributes = AttributeModelHelper .GetAttributes(context, compilationUnitSyntax.AttributeLists, cancellation) .ToList(); var additionalModules = new List(); - - foreach (var syntax in compilationUnitSyntax.Members) { - if (syntax is GlobalStatementSyntax { Statement: ExpressionStatementSyntax { Expression: InvocationExpressionSyntax invocationExpressionSyntax } expressionStatementSyntax }) { - if (context.SemanticModel.GetSymbolInfo(expressionStatementSyntax.Expression).Symbol - is IMethodSymbol { IsStatic: true } methodSymbol) { - + foreach (var syntax in compilationUnitSyntax.Members) + { + if ( + syntax is GlobalStatementSyntax + { + Statement: ExpressionStatementSyntax + { + Expression: InvocationExpressionSyntax invocationExpressionSyntax + } expressionStatementSyntax + } + ) + { + if ( + context.SemanticModel.GetSymbolInfo(expressionStatementSyntax.Expression).Symbol + is IMethodSymbol { IsStatic: true } methodSymbol + ) + { var typeSymbol = methodSymbol.ContainingSymbol as ITypeSymbol; var declaringType = methodSymbol.ContainingType; - var moduleInterface = typeSymbol?.AllInterfaces.Any(x => x.GetTypeDefinition().Equals(KnownTypes.DependencyModules.Interfaces.IDependencyModule)); - - if (moduleInterface.GetValueOrDefault(false) && - declaringType.Constructors.Any(c => c.Parameters.Length == 0)) { + var moduleInterface = typeSymbol?.AllInterfaces.Any(x => + x.GetTypeDefinition() + .Equals(KnownTypes.DependencyModules.Interfaces.IDependencyModule) + ); + + if ( + moduleInterface.GetValueOrDefault(false) + && declaringType.Constructors.Any(c => c.Parameters.Length == 0) + ) + { additionalModules.Add(declaringType.GetTypeDefinition()); } } } } - + return new ModuleEntryPointModel( ModuleEntryPointFeatures.AutoGenerateModule, context.Node.SyntaxTree?.FilePath ?? "", @@ -305,67 +439,92 @@ private ModuleEntryPointModel GetCompilationUnitSyntaxEntry(GeneratorSyntaxConte ); } - private bool GetEqualsFlag(GeneratorSyntaxContext context) { - return context.Node.DescendantNodes().OfType().Any(m => m.Identifier.ToString().Equals("Equals")); + private bool GetEqualsFlag(GeneratorSyntaxContext context) + { + return context + .Node.DescendantNodes() + .OfType() + .Any(m => m.Identifier.ToString().Equals("Equals")); } - private record DependencyFlags - (bool OnlyRealm, RegistrationType? RegistrationType, bool? GenerateAttribute, bool? GenerateFactories, bool? RegisterGenerator, string? UseMethod); - - private DependencyFlags - GetDependencyFlags(GeneratorSyntaxContext context) { + private record DependencyFlags( + bool OnlyRealm, + RegistrationType? RegistrationType, + bool? GenerateAttribute, + bool? GenerateFactories, + bool? RegisterGenerator, + string? UseMethod + ); + + private DependencyFlags GetDependencyFlags(GeneratorSyntaxContext context) + { var onlyRealm = false; RegistrationType? registrationType = null; bool? generateAttribute = null; bool? registerGenerator = null; bool? generateFactories = null; string? useMethod = null; - if (context.Node is TypeDeclarationSyntax typeDeclarationSyntax) { - var module = typeDeclarationSyntax.DescendantNodes().OfType().FirstOrDefault(attr => attr.Name.ToString().StartsWith("DependencyModule")); - - if (module is { ArgumentList: not null }) { - foreach (var argumentSyntax in module.ArgumentList.Arguments) { + if (context.Node is TypeDeclarationSyntax typeDeclarationSyntax) + { + var module = typeDeclarationSyntax + .DescendantNodes() + .OfType() + .FirstOrDefault(attr => attr.Name.ToString().StartsWith("DependencyModule")); + + if (module is { ArgumentList: not null }) + { + foreach (var argumentSyntax in module.ArgumentList.Arguments) + { var name = argumentSyntax.NameEquals?.Name.ToString(); - switch (name) { + switch (name) + { case "OnlyRealm": onlyRealm = argumentSyntax.Expression.ToString() == "true"; break; case "Using": - registrationType = GetRegistrationType(argumentSyntax.Expression.ToString()); + registrationType = GetRegistrationType( + argumentSyntax.Expression.ToString() + ); break; case "GenerateAttribute": - generateAttribute = argumentSyntax.Expression.ToString().Trim('"') == "true"; + generateAttribute = + argumentSyntax.Expression.ToString().Trim('"') == "true"; break; case "RegisterJsonSerializers": - registerGenerator = argumentSyntax.Expression.ToString().Trim('"') == "true"; + registerGenerator = + argumentSyntax.Expression.ToString().Trim('"') == "true"; break; case "GenerateUseMethod": useMethod = argumentSyntax.Expression.ToString().Trim('"'); break; case "GenerateFactories": - generateFactories = argumentSyntax.Expression.ToString().Trim('"') == "true"; + generateFactories = + argumentSyntax.Expression.ToString().Trim('"') == "true"; break; } } } } - + return new DependencyFlags( onlyRealm, - registrationType, + registrationType, generateAttribute, generateFactories, - registerGenerator, - useMethod); + registerGenerator, + useMethod + ); } - + /// /// Parses the GeneratedCodeStyle build property. The name carries no framework prefix on /// purpose: it is shared with other source generators, so one csproj line styles all of them. /// - public static BraceStyle GetCodeStyle(string value) { - switch (value.Trim().ToLowerInvariant()) { + public static BraceStyle GetCodeStyle(string value) + { + switch (value.Trim().ToLowerInvariant()) + { case "kandr": case "k&r": return BraceStyle.KAndR; @@ -374,24 +533,27 @@ public static BraceStyle GetCodeStyle(string value) { } } - public static RegistrationType GetRegistrationType(string toString) { + public static RegistrationType GetRegistrationType(string toString) + { var typeString = toString.Replace("RegistrationType.", ""); - if (string.IsNullOrEmpty(typeString)) { + if (string.IsNullOrEmpty(typeString)) + { return RegistrationType.Add; } - - switch (typeString) { + + switch (typeString) + { case "Add": return RegistrationType.Add; case "Try": return RegistrationType.Try; case "TryEnumerable": - return RegistrationType.TryEnumerable; + return RegistrationType.TryEnumerable; case "Replace": return RegistrationType.Replace; default: return RegistrationType.Add; } } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/DecoratorFileWriter.cs b/src/DependencyModules.SourceGenerator.Impl/DecoratorFileWriter.cs index 65a4143..6bbc533 100644 --- a/src/DependencyModules.SourceGenerator.Impl/DecoratorFileWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/DecoratorFileWriter.cs @@ -13,8 +13,8 @@ namespace DependencyModules.SourceGenerator.Impl; /// The bodies are a single call into DecoratorHelper; the rewrite itself is deliberately not /// generated. /// -public class DecoratorFileWriter { - +public class DecoratorFileWriter +{ /// /// Distinguishes the methods and fields this file declares from those another file declares on /// the same partial class. The attribute path and the convention path each emit decorations for @@ -24,8 +24,9 @@ public string Write( ModuleEntryPointModel entryPointModel, DependencyModuleConfigurationModel configurationModel, IReadOnlyList decorators, - string uniqueId = "") { - + string uniqueId = "" + ) + { var csharpFile = new CSharpFileDefinition(entryPointModel.EntryPointType.Namespace); var classDefinition = csharpFile.AddClass(entryPointModel.EntryPointType.Name); @@ -40,61 +41,93 @@ public string Write( // Anything that cannot be constructed by generated code has already been reported and // dropped. There is no reflective shape left to fall back to, so reaching the writer means // the decoration can be emitted. - for (var i = 0; i < decorators.Count; i++) { - WriteDecorator(entryPointModel, classDefinition, decorators[i], i, configurationModel, uniqueId); + for (var i = 0; i < decorators.Count; i++) + { + WriteDecorator( + entryPointModel, + classDefinition, + decorators[i], + i, + configurationModel, + uniqueId + ); } - var outputContext = new OutputContext(new OutputContextOptions { - TypeOutputMode = TypeOutputMode.Global, - BraceStyle = configurationModel.GeneratedCodeStyle - }); + var outputContext = new OutputContext( + new OutputContextOptions + { + TypeOutputMode = TypeOutputMode.Global, + BraceStyle = configurationModel.GeneratedCodeStyle, + } + ); csharpFile.WriteOutput(outputContext); return EntryModelUtil.ApplyRecordDeclaration(outputContext.Output(), entryPointModel); } - private static void WriteDecorator( ModuleEntryPointModel entryPointModel, ClassDefinition classDefinition, DecoratorModel decorator, int index, DependencyModuleConfigurationModel configurationModel, - string uniqueId) { - + string uniqueId + ) + { var methodName = $"Apply{uniqueId}Decorator{index}"; var method = classDefinition.AddMethod(methodName); method.Modifiers |= ComponentModifier.Private | ComponentModifier.Static; - if (configurationModel.ExcludeGeneratedCodeFromCoverage) { - method.AddAttribute(TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverage")); + if (configurationModel.ExcludeGeneratedCodeFromCoverage) + { + method.AddAttribute( + TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverage") + ); } var services = method.AddParameter( - KnownTypes.Microsoft.DependencyInjection.IServiceCollection, "services"); + KnownTypes.Microsoft.DependencyInjection.IServiceCollection, + "services" + ); // The parameter only appears when something tests it, so an unconditional decorator keeps // the RegistryFunc shape and the AddDecorator overload it always used. var hasConditions = decorator.Conditions is { Count: > 0 }; var environment = hasConditions - ? method.AddParameter(KnownTypes.DependencyModules.Interfaces.IModuleEnvironment, "environment") + ? method.AddParameter( + KnownTypes.DependencyModules.Interfaces.IModuleEnvironment, + "environment" + ) : null; - var decorate = Decoration(decorator, services.Name, decorator.ServiceType, decorator.DecoratorType); + var decorate = Decoration( + decorator, + services.Name, + decorator.ServiceType, + decorator.DecoratorType + ); - if (environment != null) { + if (environment != null) + { // Guarding the call rather than the registration: a decorator that does not apply is // simply not run, so the service resolves undecorated instead of being wrapped by // something that re-tests the environment on every call. var block = method.If( CodeOutputComponent.Get( - EnvironmentConditionWriter.BuildCondition(decorator.Conditions!, environment.Name))); + EnvironmentConditionWriter.BuildCondition( + decorator.Conditions!, + environment.Name + ) + ) + ); block.AddIndentedStatement(decorate); - } else { + } + else + { method.AddIndentedStatement(decorate); } @@ -124,30 +157,36 @@ private static IOutputComponent Decoration( DecoratorModel decorator, string servicesName, ITypeDefinition serviceType, - ITypeDefinition decoratorType) { - + ITypeDefinition decoratorType + ) + { // Shared with the service writer rather than reimplemented. Resolving every parameter with // GetRequiredService looked right and quietly ignored what the parameter declared: a // [FromKeyedServices] dependency resolved the unkeyed registration, and a nullable one threw // instead of resolving to null. var arguments = ConstructorArgumentWriter.Arguments( new ParameterDefinition( - KnownTypes.Microsoft.DependencyInjection.IServiceProvider, ProviderParameterName), + KnownTypes.Microsoft.DependencyInjection.IServiceProvider, + ProviderParameterName + ), decorator.Constructor!.Parameters, decorator.InnerParameterIndex, - CodeOutputComponent.Get(InnerParameterName)); + CodeOutputComponent.Get(InnerParameterName) + ); var construct = New(decoratorType, arguments); var lambda = new WrapStatement( CodeOutputComponent.Get(" => "), CodeOutputComponent.Get($"({ProviderParameterName}, {InnerParameterName})"), - construct); + construct + ); // The four-argument overload when an implementation is named, which is the one interception // already uses: it skips a descriptor whose origin is a different type, so the decorator // reaches one registration rather than every registration of the service. - if (decorator.Implementation != null) { + if (decorator.Implementation != null) + { return SyntaxHelpers.InvokeGeneric( KnownTypes.DependencyModules.Helpers.DecoratorHelper, "Decorate", @@ -155,7 +194,8 @@ private static IOutputComponent Decoration( CodeOutputComponent.Get(servicesName), TypeOf(decoratorType), lambda, - TypeOf(decorator.Implementation)); + TypeOf(decorator.Implementation) + ); } return SyntaxHelpers.InvokeGeneric( @@ -164,7 +204,8 @@ private static IOutputComponent Decoration( new[] { serviceType }, CodeOutputComponent.Get(servicesName), TypeOf(decoratorType), - lambda); + lambda + ); } private static string ToCamel(string value) => @@ -180,30 +221,39 @@ private static void WriteRegistration( DecoratorModel decorator, int index, string methodName, - string uniqueId) { - + string uniqueId + ) + { // A field initializer registers the method, matching how service registrations are hooked up. // DynamicDependency keeps the trimmer from removing a method only referenced this way. - var field = classDefinition.AddField(typeof(int), $"{ToCamel(uniqueId)}decoratorField{index}"); + var field = classDefinition.AddField( + typeof(int), + $"{ToCamel(uniqueId)}decoratorField{index}" + ); field.Modifiers |= ComponentModifier.Private | ComponentModifier.Static; field.AddAttribute( TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "DynamicDependency"), - $"nameof({methodName})"); + $"nameof({methodName})" + ); var registryType = new GenericTypeDefinition( TypeDefinitionEnum.ClassDefinition, KnownTypes.DependencyModules.Helpers.Namespace, "DependencyRegistry", - new[] { entryPointModel.EntryPointType }); + new[] { entryPointModel.EntryPointType } + ); field.InitializeValue = new StaticInvokeStatement( registryType, "AddDecorator", - new List { + new List + { CodeOutputComponent.Get(methodName), - CodeOutputComponent.Get(decorator.Order.ToString()) - }) { - Indented = false + CodeOutputComponent.Get(decorator.Order.ToString()), + } + ) + { + Indented = false, }; } } diff --git a/src/DependencyModules.SourceGenerator.Impl/DependencyFileWriter.cs b/src/DependencyModules.SourceGenerator.Impl/DependencyFileWriter.cs index a17d1a6..3d27b9b 100644 --- a/src/DependencyModules.SourceGenerator.Impl/DependencyFileWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/DependencyFileWriter.cs @@ -8,7 +8,8 @@ namespace DependencyModules.SourceGenerator.Impl; -public class DependencyFileWriter { +public class DependencyFileWriter +{ private readonly FileLogger _logger; private readonly bool _coverageAttributeOnMethod; @@ -21,7 +22,8 @@ public class DependencyFileWriter { /// own the class-level attribute; every other file contributing to the same partial has to apply /// it per member, which is what DecoratorFileWriter already does. /// - public DependencyFileWriter(FileLogger logger, bool coverageAttributeOnMethod = false) { + public DependencyFileWriter(FileLogger logger, bool coverageAttributeOnMethod = false) + { _logger = logger; _coverageAttributeOnMethod = coverageAttributeOnMethod; } @@ -30,28 +32,40 @@ public string Write( ModuleEntryPointModel entryPointModel, DependencyModuleConfigurationModel configurationModel, IEnumerable serviceModels, - string uniqueId) { - - if (entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule) && - string.IsNullOrEmpty(entryPointModel.EntryPointType.Namespace)) { - entryPointModel = entryPointModel with { - EntryPointType = TypeDefinition.Get(configurationModel.RootNamespace, entryPointModel.EntryPointType.Name) + string uniqueId + ) + { + if ( + entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule) + && string.IsNullOrEmpty(entryPointModel.EntryPointType.Namespace) + ) + { + entryPointModel = entryPointModel with + { + EntryPointType = TypeDefinition.Get( + configurationModel.RootNamespace, + entryPointModel.EntryPointType.Name + ), }; } _interceptedServiceTypes = InterceptedServiceTypes(serviceModels); - _logger.Info($"Generating Dependencies for {entryPointModel.EntryPointType.Namespace}.{entryPointModel.EntryPointType.Namespace}"); + _logger.Info( + $"Generating Dependencies for {entryPointModel.EntryPointType.Namespace}.{entryPointModel.EntryPointType.Namespace}" + ); var csharpFile = new CSharpFileDefinition(entryPointModel.EntryPointType.Namespace); GenerateClass(entryPointModel, configurationModel, serviceModels, csharpFile, uniqueId); var output = new OutputContext( - new OutputContextOptions { + new OutputContextOptions + { TypeOutputMode = TypeOutputMode.Global, - BraceStyle = configurationModel.GeneratedCodeStyle - }); + BraceStyle = configurationModel.GeneratedCodeStyle, + } + ); csharpFile.WriteOutput(output); @@ -62,12 +76,14 @@ public string Write( return result; } - private void GenerateClass(ModuleEntryPointModel entryPointModel, + private void GenerateClass( + ModuleEntryPointModel entryPointModel, DependencyModuleConfigurationModel configurationModel, IEnumerable serviceModels, CSharpFileDefinition csharpFile, - string uniqueId) { - + string uniqueId + ) + { var classDefinition = csharpFile.AddClass(entryPointModel.EntryPointType.Name); // Asked for by name because qualification cannot replace it: the registrations call @@ -86,45 +102,68 @@ private void GenerateClass(ModuleEntryPointModel entryPointModel, // writers already do this; the registrations file is where the annotations actually land. classDefinition.EnableNullable(); - if (configurationModel.ExcludeGeneratedCodeFromCoverage && !_coverageAttributeOnMethod) { + if (configurationModel.ExcludeGeneratedCodeFromCoverage && !_coverageAttributeOnMethod) + { classDefinition.AddAttribute( - TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverage")); + TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverage") + ); } - var methodName = - GenerateDependencyMethod(entryPointModel, configurationModel, serviceModels, classDefinition, uniqueId); + var methodName = GenerateDependencyMethod( + entryPointModel, + configurationModel, + serviceModels, + classDefinition, + uniqueId + ); CreateInvokeStatement(entryPointModel, methodName, classDefinition, uniqueId); } - private void CreateInvokeStatement(ModuleEntryPointModel entryPointModel, string methodName, ClassDefinition classDefinition, string uniqueId) { + private void CreateInvokeStatement( + ModuleEntryPointModel entryPointModel, + string methodName, + ClassDefinition classDefinition, + string uniqueId + ) + { var lowerName = uniqueId.ToLower() + "Field"; var field = classDefinition.AddField(typeof(int), lowerName); field.Modifiers |= ComponentModifier.Private | ComponentModifier.Static; - field.AddAttribute(TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "DynamicDependency"), $"nameof({methodName})"); + field.AddAttribute( + TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "DynamicDependency"), + $"nameof({methodName})" + ); var closedType = new GenericTypeDefinition( - TypeDefinitionEnum.ClassDefinition, KnownTypes.DependencyModules.Helpers.Namespace, "DependencyRegistry", new[] { - entryPointModel.EntryPointType - }); - - var invokeStatement = new StaticInvokeStatement(closedType, "Add", new List { - CodeOutputComponent.Get(methodName) - }) { - Indented = false + TypeDefinitionEnum.ClassDefinition, + KnownTypes.DependencyModules.Helpers.Namespace, + "DependencyRegistry", + new[] { entryPointModel.EntryPointType } + ); + + var invokeStatement = new StaticInvokeStatement( + closedType, + "Add", + new List { CodeOutputComponent.Get(methodName) } + ) + { + Indented = false, }; field.InitializeValue = invokeStatement; } - private string GenerateDependencyMethod(ModuleEntryPointModel entryPointModel, + private string GenerateDependencyMethod( + ModuleEntryPointModel entryPointModel, DependencyModuleConfigurationModel configurationModel, IEnumerable serviceModels, ClassDefinition classDefinition, - string uniqueId) { - + string uniqueId + ) + { classDefinition.AddUsingNamespace("Microsoft.Extensions.DependencyInjection.Extensions"); var method = classDefinition.AddMethod(uniqueId + "Dependencies"); @@ -134,11 +173,16 @@ private string GenerateDependencyMethod(ModuleEntryPointModel entryPointModel, // The glue factories GenerateGlueFactory may add are not covered here. They exist only for // [Factory] registrations, which the attribute path produces and the convention path — the // only caller that sets this — cannot. - if (configurationModel.ExcludeGeneratedCodeFromCoverage && _coverageAttributeOnMethod) { + if (configurationModel.ExcludeGeneratedCodeFromCoverage && _coverageAttributeOnMethod) + { method.AddAttribute( - TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverage")); + TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverage") + ); } - var services = method.AddParameter(KnownTypes.Microsoft.DependencyInjection.IServiceCollection, "services"); + var services = method.AddParameter( + KnownTypes.Microsoft.DependencyInjection.IServiceCollection, + "services" + ); var stringBuilder = new StringBuilder(); @@ -150,51 +194,75 @@ private string GenerateDependencyMethod(ModuleEntryPointModel entryPointModel, // without conditions generates exactly the method it always has and its Add call still // binds to the RegistryFunc overload. var environment = sortedServiceModels.Any(model => model.Conditions is { Count: > 0 }) - ? method.AddParameter(KnownTypes.DependencyModules.Interfaces.IModuleEnvironment, "environment") + ? method.AddParameter( + KnownTypes.DependencyModules.Interfaces.IModuleEnvironment, + "environment" + ) : null; - foreach (var serviceModel in sortedServiceModels) { - if (serviceModel.Equals(ServiceModel.Ignore)) { + foreach (var serviceModel in sortedServiceModels) + { + if (serviceModel.Equals(ServiceModel.Ignore)) + { continue; } - if ((serviceModel.Features & RegistrationFeature.AutoRegisterSourceGenerator) == - RegistrationFeature.AutoRegisterSourceGenerator && !autoRegisterGenerators) { + if ( + (serviceModel.Features & RegistrationFeature.AutoRegisterSourceGenerator) + == RegistrationFeature.AutoRegisterSourceGenerator + && !autoRegisterGenerators + ) + { continue; } // One guard around everything the service registers. The attributes are declared on the // class, so every registration it produces shares them. - var block = environment != null && serviceModel.Conditions is { Count: > 0 } conditions - ? method.If(CodeOutputComponent.Get(BuildCondition(conditions, environment.Name))) - : (BaseBlockDefinition)method; + var block = + environment != null && serviceModel.Conditions is { Count: > 0 } conditions + ? method.If( + CodeOutputComponent.Get(BuildCondition(conditions, environment.Name)) + ) + : (BaseBlockDefinition)method; var crossWire = false; - foreach (var registrationModel in serviceModel.Registrations) { + foreach (var registrationModel in serviceModel.Registrations) + { // skip registrations not for this realm - if (registrationModel.Realm != null) { - if (!registrationModel.Realm.Equals(entryPointModel.EntryPointType)) { + if (registrationModel.Realm != null) + { + if (!registrationModel.Realm.Equals(entryPointModel.EntryPointType)) + { continue; } } else if ( - (entryPointModel.ModuleFeatures & ModuleEntryPointFeatures.OnlyRealm) == - ModuleEntryPointFeatures.OnlyRealm) { + (entryPointModel.ModuleFeatures & ModuleEntryPointFeatures.OnlyRealm) + == ModuleEntryPointFeatures.OnlyRealm + ) + { continue; } - if (registrationModel.Namespaces != null) { - foreach (var namespaceString in registrationModel.Namespaces) { + if (registrationModel.Namespaces != null) + { + foreach (var namespaceString in registrationModel.Namespaces) + { classDefinition.AddUsingNamespace(namespaceString); } } crossWire |= registrationModel.CrossWire.GetValueOrDefault(false); - var registrationType = GetRegistrationType(entryPointModel, configurationModel, registrationModel); + var registrationType = GetRegistrationType( + entryPointModel, + configurationModel, + registrationModel + ); - switch (registrationType) { + switch (registrationType) + { case RegistrationType.Add: case RegistrationType.Try: HandleTryAndAddRegistrationTypes( @@ -207,7 +275,8 @@ private string GenerateDependencyMethod(ModuleEntryPointModel entryPointModel, serviceModel, block, services, - uniqueId); + uniqueId + ); break; case RegistrationType.Replace: @@ -221,13 +290,15 @@ private string GenerateDependencyMethod(ModuleEntryPointModel entryPointModel, serviceModel, block, services, - uniqueId); + uniqueId + ); break; } } - if (crossWire) { + if (crossWire) + { CrossWireRegisterImplementation( configurationModel, entryPointModel, @@ -235,7 +306,8 @@ private string GenerateDependencyMethod(ModuleEntryPointModel entryPointModel, block, services, serviceModel, - uniqueId); + uniqueId + ); } } @@ -245,8 +317,9 @@ private string GenerateDependencyMethod(ModuleEntryPointModel entryPointModel, // Shared with DecoratorFileWriter through EnvironmentConditionWriter, so a service and a // decorator carrying the same attributes cannot end up testing them differently. private static string BuildCondition( - IReadOnlyList conditions, string environmentParameter) => - EnvironmentConditionWriter.BuildCondition(conditions, environmentParameter); + IReadOnlyList conditions, + string environmentParameter + ) => EnvironmentConditionWriter.BuildCondition(conditions, environmentParameter); private void CrossWireRegisterImplementation( DependencyModuleConfigurationModel configurationModel, @@ -255,12 +328,16 @@ private void CrossWireRegisterImplementation( BaseBlockDefinition block, ParameterDefinition services, ServiceModel serviceModel, - string uniqueId) { - var registrationModel = - serviceModel.Registrations.First(r => r.CrossWire.GetValueOrDefault(false)); + string uniqueId + ) + { + var registrationModel = serviceModel.Registrations.First(r => + r.CrossWire.GetValueOrDefault(false) + ); var invokeMethod = ""; - switch (registrationModel.RegistrationType.GetValueOrDefault(RegistrationType.Add)) { + switch (registrationModel.RegistrationType.GetValueOrDefault(RegistrationType.Add)) + { case RegistrationType.Add: invokeMethod = "Add"; break; @@ -275,31 +352,39 @@ private void CrossWireRegisterImplementation( break; } - var parameters = new List { - TypeOf(serviceModel.ImplementationType) - }; + var parameters = new List { TypeOf(serviceModel.ImplementationType) }; - if (registrationModel.Key != null) { + if (registrationModel.Key != null) + { parameters.Add(registrationModel.Key); } - if (serviceModel.Factory == null) { - if (serviceModel.FactoryOutput != null) { + if (serviceModel.Factory == null) + { + if (serviceModel.FactoryOutput != null) + { parameters.Add(serviceModel.FactoryOutput); } - else if (serviceModel is { Constructor: not null, ImplementationType: not GenericTypeDefinition } && - ShouldGenerateFactory(serviceModel, entryPointModel, configurationModel)) { + else if ( + serviceModel + is { Constructor: not null, ImplementationType: not GenericTypeDefinition } + && ShouldGenerateFactory(serviceModel, entryPointModel, configurationModel) + ) + { parameters.Add(GenerateNewFactory(serviceModel, registrationModel)); } - else { + else + { parameters.Add(TypeOf(serviceModel.ImplementationType)); } } - else { + else + { AddFactoryParameter(serviceModel, classDefinition, parameters, uniqueId); } - switch (registrationModel.Lifestyle) { + switch (registrationModel.Lifestyle) + { case ServiceLifestyle.Transient: parameters.Add(CodeOutputComponent.Get("ServiceLifetime.Transient")); break; @@ -313,16 +398,12 @@ private void CrossWireRegisterImplementation( throw new ArgumentOutOfRangeException(); } - var serviceDescriptor = - New( - KnownTypes.Microsoft.DependencyInjection.ServiceDescriptor, - parameters.ToArray()); + var serviceDescriptor = New( + KnownTypes.Microsoft.DependencyInjection.ServiceDescriptor, + parameters.ToArray() + ); - block.AddIndentedStatement( - services.Invoke( - invokeMethod, - serviceDescriptor - )); + block.AddIndentedStatement(services.Invoke(invokeMethod, serviceDescriptor)); } /// @@ -344,9 +425,12 @@ private void CrossWireRegisterImplementation( private bool ShouldGenerateFactory( ServiceModel serviceModel, ModuleEntryPointModel entryPointModel, - DependencyModuleConfigurationModel configurationModel) => - !IsInterceptedServiceType(serviceModel) && - entryPointModel.GenerateFactories.GetValueOrDefault(configurationModel.GenerateFactories); + DependencyModuleConfigurationModel configurationModel + ) => + !IsInterceptedServiceType(serviceModel) + && entryPointModel.GenerateFactories.GetValueOrDefault( + configurationModel.GenerateFactories + ); /// /// Whether any registration of this service is one interception has to be able to pick out. @@ -359,18 +443,25 @@ private bool ShouldGenerateFactory( /// from the registration being wrapped, and it came back wearing another class's wrapper. /// private bool IsInterceptedServiceType(ServiceModel serviceModel) => - serviceModel.Registrations.Any( - registration => _interceptedServiceTypes.Contains(registration.ServiceType)); - - private static HashSet InterceptedServiceTypes(IEnumerable serviceModels) { + serviceModel.Registrations.Any(registration => + _interceptedServiceTypes.Contains(registration.ServiceType) + ); + + private static HashSet InterceptedServiceTypes( + IEnumerable serviceModels + ) + { var types = new HashSet(); - foreach (var serviceModel in serviceModels) { - if (!serviceModel.Features.HasFlag(RegistrationFeature.Intercepted)) { + foreach (var serviceModel in serviceModels) + { + if (!serviceModel.Features.HasFlag(RegistrationFeature.Intercepted)) + { continue; } - foreach (var registration in serviceModel.Registrations) { + foreach (var registration in serviceModel.Registrations) + { types.Add(registration.ServiceType); } } @@ -380,59 +471,85 @@ private static HashSet InterceptedServiceTypes(IEnumerable _interceptedServiceTypes = new(); - private static object GenerateNewFactory(ServiceModel serviceModel, ServiceRegistrationModel registrationModel) { - var parameter = - new ParameterDefinition(KnownTypes.Microsoft.DependencyInjection.IServiceProvider, "provider"); - - var providerParameters = registrationModel.Key == null ? "provider => " : "(provider, _) => "; + private static object GenerateNewFactory( + ServiceModel serviceModel, + ServiceRegistrationModel registrationModel + ) + { + var parameter = new ParameterDefinition( + KnownTypes.Microsoft.DependencyInjection.IServiceProvider, + "provider" + ); + + var providerParameters = + registrationModel.Key == null ? "provider => " : "(provider, _) => "; var provider = CodeOutputComponent.Get(providerParameters); var newStatement = New( serviceModel.ImplementationType, - GetArgumentsForParameterList(parameter, serviceModel.Constructor!.Parameters)); + GetArgumentsForParameterList(parameter, serviceModel.Constructor!.Parameters) + ); return new WrapStatement(newStatement, provider, null); } - private void HandleTryEnumerableAndReplaceRegistrationType(DependencyModuleConfigurationModel configurationModel, ModuleEntryPointModel entryPointModel, ClassDefinition classDefinition, + private void HandleTryEnumerableAndReplaceRegistrationType( + DependencyModuleConfigurationModel configurationModel, + ModuleEntryPointModel entryPointModel, + ClassDefinition classDefinition, RegistrationType registrationType, ServiceRegistrationModel registrationModel, ServiceModel serviceModel, BaseBlockDefinition block, - ParameterDefinition services, string uniqueId) { + ParameterDefinition services, + string uniqueId + ) + { var invokeMethod = registrationType == RegistrationType.Replace ? "Replace" : "TryAddEnumerable"; - var parameters = new List { - TypeOf(registrationModel.ServiceType) - }; + var parameters = new List { TypeOf(registrationModel.ServiceType) }; - if (registrationModel.Key != null) { + if (registrationModel.Key != null) + { parameters.Add(registrationModel.Key); } - if (registrationModel.CrossWire == true) { + if (registrationModel.CrossWire == true) + { AddCrossWireParameter(serviceModel, registrationModel, parameters); } - else if (serviceModel.Factory == null) { - if (serviceModel.FactoryOutput != null) { - var factoryOutput = serviceModel.FactoryOutput?.Invoke(serviceModel, registrationModel); + else if (serviceModel.Factory == null) + { + if (serviceModel.FactoryOutput != null) + { + var factoryOutput = serviceModel.FactoryOutput?.Invoke( + serviceModel, + registrationModel + ); parameters.Add(factoryOutput ?? TypeOf(serviceModel.ImplementationType)); } - else if (serviceModel is { Constructor: not null, ImplementationType: not GenericTypeDefinition } && - ShouldGenerateFactory(serviceModel, entryPointModel, configurationModel)) { + else if ( + serviceModel + is { Constructor: not null, ImplementationType: not GenericTypeDefinition } + && ShouldGenerateFactory(serviceModel, entryPointModel, configurationModel) + ) + { parameters.Add(GenerateNewFactory(serviceModel, registrationModel)); } - else { + else + { parameters.Add(TypeOf(serviceModel.ImplementationType)); } } - else { + else + { AddFactoryParameter(serviceModel, classDefinition, parameters, uniqueId); } - switch (registrationModel.Lifestyle) { + switch (registrationModel.Lifestyle) + { case ServiceLifestyle.Transient: parameters.Add(CodeOutputComponent.Get("ServiceLifetime.Transient")); break; @@ -446,38 +563,43 @@ private void HandleTryEnumerableAndReplaceRegistrationType(DependencyModuleConfi throw new ArgumentOutOfRangeException(); } - var serviceDescriptor = - New( - KnownTypes.Microsoft.DependencyInjection.ServiceDescriptor, - parameters.ToArray()); + var serviceDescriptor = New( + KnownTypes.Microsoft.DependencyInjection.ServiceDescriptor, + parameters.ToArray() + ); - block.AddIndentedStatement( - services.Invoke( - invokeMethod, - serviceDescriptor - )); + block.AddIndentedStatement(services.Invoke(invokeMethod, serviceDescriptor)); } - private void HandleTryAndAddRegistrationTypes(DependencyModuleConfigurationModel configurationModel, ModuleEntryPointModel entryPointModel, ClassDefinition classDefinition, StringBuilder stringBuilder, + private void HandleTryAndAddRegistrationTypes( + DependencyModuleConfigurationModel configurationModel, + ModuleEntryPointModel entryPointModel, + ClassDefinition classDefinition, + StringBuilder stringBuilder, RegistrationType registrationType, ServiceRegistrationModel registrationModel, ServiceModel serviceModel, BaseBlockDefinition block, ParameterDefinition services, - string uniqueId) { + string uniqueId + ) + { stringBuilder.Length = 0; - if (registrationType == RegistrationType.Try) { + if (registrationType == RegistrationType.Try) + { stringBuilder.Append("Try"); } stringBuilder.Append("Add"); - if (registrationModel.Key != null) { + if (registrationModel.Key != null) + { stringBuilder.Append("Keyed"); } - switch (registrationModel.Lifestyle) { + switch (registrationModel.Lifestyle) + { case ServiceLifestyle.Transient: stringBuilder.Append("Transient"); break; @@ -493,68 +615,81 @@ private void HandleTryAndAddRegistrationTypes(DependencyModuleConfigurationModel parameters.Add(TypeOf(registrationModel.ServiceType)); - if (registrationModel.Key != null) { + if (registrationModel.Key != null) + { parameters.Add(registrationModel.Key); } - if (registrationModel.CrossWire == true) { - AddCrossWireParameter( - serviceModel, registrationModel, parameters); + if (registrationModel.CrossWire == true) + { + AddCrossWireParameter(serviceModel, registrationModel, parameters); } - else if (serviceModel.Factory == null) { - if (serviceModel.FactoryOutput != null) { - var factoryOutput = serviceModel.FactoryOutput?.Invoke(serviceModel, registrationModel); + else if (serviceModel.Factory == null) + { + if (serviceModel.FactoryOutput != null) + { + var factoryOutput = serviceModel.FactoryOutput?.Invoke( + serviceModel, + registrationModel + ); parameters.Add(factoryOutput ?? TypeOf(serviceModel.ImplementationType)); } - else if (serviceModel is { Constructor: not null, ImplementationType: not GenericTypeDefinition } && - ShouldGenerateFactory(serviceModel, entryPointModel, configurationModel)) { + else if ( + serviceModel + is { Constructor: not null, ImplementationType: not GenericTypeDefinition } + && ShouldGenerateFactory(serviceModel, entryPointModel, configurationModel) + ) + { parameters.Add(GenerateNewFactory(serviceModel, registrationModel)); } - else { + else + { parameters.Add(TypeOf(serviceModel.ImplementationType)); } } - else { + else + { AddFactoryParameter(serviceModel, classDefinition, parameters, uniqueId); } - block.AddIndentedStatement( - services.Invoke( - stringBuilder.ToString(), - parameters.ToArray() - )); + block.AddIndentedStatement(services.Invoke(stringBuilder.ToString(), parameters.ToArray())); } private static void AddCrossWireParameter( ServiceModel serviceModel, ServiceRegistrationModel registrationModel, - List parameters) { + List parameters + ) + { IOutputComponent invoke; - var serviceProvider = - new ParameterDefinition(KnownTypes.Microsoft.DependencyInjection.IServiceProvider, "s"); + var serviceProvider = new ParameterDefinition( + KnownTypes.Microsoft.DependencyInjection.IServiceProvider, + "s" + ); - if (registrationModel.Key != null) { + if (registrationModel.Key != null) + { var key = registrationModel.Key; - if (key is string stringValue) { + if (key is string stringValue) + { key = QuoteString(stringValue); } - invoke = - serviceProvider.InvokeGeneric( - "GetRequiredKeyedServices", - new[] { - serviceModel.ImplementationType - }, - key); + invoke = serviceProvider.InvokeGeneric( + "GetRequiredKeyedServices", + new[] { serviceModel.ImplementationType }, + key + ); } - else { - invoke = - serviceProvider.InvokeGeneric("GetRequiredService", new[] { - serviceModel.ImplementationType - }); + else + { + invoke = serviceProvider.InvokeGeneric( + "GetRequiredService", + new[] { serviceModel.ImplementationType } + ); } var wrapper = new WrapStatement(CodeOutputComponent.Get(" => "), serviceProvider, invoke); @@ -562,20 +697,39 @@ private static void AddCrossWireParameter( parameters.Add(wrapper); } - private static void AddFactoryParameter(ServiceModel serviceModel, ClassDefinition classDefinition, List parameters, string uniqueId) { + private static void AddFactoryParameter( + ServiceModel serviceModel, + ClassDefinition classDefinition, + List parameters, + string uniqueId + ) + { var factory = serviceModel.Factory; - if (factory == null) { + if (factory == null) + { return; } - if (factory.Parameters.Count == 1 && factory.Parameters.Any(m => - m.ParameterType.Equals(KnownTypes.Microsoft.DependencyInjection.IServiceProvider))) { - parameters.Add(CodeOutputComponent.Get( - factory.TypeDefinition.Namespace + "." + factory.TypeDefinition.Name + "." + factory.MethodName)); + if ( + factory.Parameters.Count == 1 + && factory.Parameters.Any(m => + m.ParameterType.Equals(KnownTypes.Microsoft.DependencyInjection.IServiceProvider) + ) + ) + { + parameters.Add( + CodeOutputComponent.Get( + factory.TypeDefinition.Namespace + + "." + + factory.TypeDefinition.Name + + "." + + factory.MethodName + ) + ); } - else { - var glueFactory = GenerateGlueFactory( - serviceModel, factory, classDefinition, uniqueId); + else + { + var glueFactory = GenerateGlueFactory(serviceModel, factory, classDefinition, uniqueId); parameters.Add(CodeOutputComponent.Get(glueFactory.Name)); } @@ -585,7 +739,9 @@ private static MethodDefinition GenerateGlueFactory( ServiceModel serviceModel, ServiceFactoryModel factory, ClassDefinition classDefinition, - string uniqueId) { + string uniqueId + ) + { var glueFactoryName = uniqueId + "GlueFactory" + classDefinition.Methods.Count; var method = classDefinition.AddMethod(glueFactoryName); @@ -593,7 +749,9 @@ private static MethodDefinition GenerateGlueFactory( method.SetReturnType(serviceModel.ImplementationType); var serviceProvider = method.AddParameter( - KnownTypes.Microsoft.DependencyInjection.IServiceProvider, "serviceProvider"); + KnownTypes.Microsoft.DependencyInjection.IServiceProvider, + "serviceProvider" + ); var parameterList = GetArgumentsForParameterList(serviceProvider, factory.Parameters); @@ -603,15 +761,23 @@ private static MethodDefinition GenerateGlueFactory( } private static object[] GetArgumentsForParameterList( - ParameterDefinition serviceProvider, IReadOnlyList parameterList) => - ConstructorArgumentWriter.Arguments(serviceProvider, parameterList); + ParameterDefinition serviceProvider, + IReadOnlyList parameterList + ) => ConstructorArgumentWriter.Arguments(serviceProvider, parameterList); - private static RegistrationType GetRegistrationType(ModuleEntryPointModel entryPointModel, DependencyModuleConfigurationModel configurationModel, ServiceRegistrationModel registrationModel) { - if (registrationModel.RegistrationType.HasValue) { + private static RegistrationType GetRegistrationType( + ModuleEntryPointModel entryPointModel, + DependencyModuleConfigurationModel configurationModel, + ServiceRegistrationModel registrationModel + ) + { + if (registrationModel.RegistrationType.HasValue) + { return registrationModel.RegistrationType.Value; } - if (entryPointModel.RegistrationType.HasValue) { + if (entryPointModel.RegistrationType.HasValue) + { return entryPointModel.RegistrationType.Value; } @@ -649,35 +815,46 @@ private static RegistrationType GetRegistrationType(ModuleEntryPointModel entryP /// /// private List GetSortedServiceModels( - IEnumerable serviceModels, DependencyModuleConfigurationModel configurationModel) { - + IEnumerable serviceModels, + DependencyModuleConfigurationModel configurationModel + ) + { var list = new List(serviceModels); - list.Sort((x, y) => { - var byCondition = IsConditional(x).CompareTo(IsConditional(y)); - - if (byCondition != 0) { - return byCondition; - } + list.Sort( + (x, y) => + { + var byCondition = IsConditional(x).CompareTo(IsConditional(y)); - var byStrategy = ActsOnExistingRegistration(x, configurationModel) - .CompareTo(ActsOnExistingRegistration(y, configurationModel)); + if (byCondition != 0) + { + return byCondition; + } - if (byStrategy != 0) { - return byStrategy; - } + var byStrategy = ActsOnExistingRegistration(x, configurationModel) + .CompareTo(ActsOnExistingRegistration(y, configurationModel)); - // Order last of the deciding keys, so naming one cannot move a registration ahead of a - // condition or ahead of the Replace it depends on - both of which are about whether a - // registration works at all, where Order is only about where it lands among its peers. - var byOrder = OrderOf(x).CompareTo(OrderOf(y)); + if (byStrategy != 0) + { + return byStrategy; + } - // Name is the tie-break rather than the only key, so the order stays total and the - // output stays deterministic under List.Sort, which is not stable. - return byOrder != 0 - ? byOrder - : string.Compare(x.ImplementationType.Name, y.ImplementationType.Name, StringComparison.Ordinal); - }); + // Order last of the deciding keys, so naming one cannot move a registration ahead of a + // condition or ahead of the Replace it depends on - both of which are about whether a + // registration works at all, where Order is only about where it lands among its peers. + var byOrder = OrderOf(x).CompareTo(OrderOf(y)); + + // Name is the tie-break rather than the only key, so the order stays total and the + // output stays deterministic under List.Sort, which is not stable. + return byOrder != 0 + ? byOrder + : string.Compare( + x.ImplementationType.Name, + y.ImplementationType.Name, + StringComparison.Ordinal + ); + } + ); return list; } @@ -690,12 +867,15 @@ private List GetSortedServiceModels( /// to name its own order. They are emitted together, so the model needs one number, and the /// lowest is the one that matches what naming an order means — "put this early". /// - private static int OrderOf(ServiceModel serviceModel) { + private static int OrderOf(ServiceModel serviceModel) + { var order = 0; var first = true; - foreach (var registration in serviceModel.Registrations) { - if (first || registration.Order < order) { + foreach (var registration in serviceModel.Registrations) + { + if (first || registration.Order < order) + { order = registration.Order; first = false; } @@ -716,18 +896,23 @@ private static bool IsConditional(ServiceModel serviceModel) => /// they arrive in, and deferring it would change nothing. /// private static bool ActsOnExistingRegistration( - ServiceModel serviceModel, DependencyModuleConfigurationModel configurationModel) { - - foreach (var registration in serviceModel.Registrations) { + ServiceModel serviceModel, + DependencyModuleConfigurationModel configurationModel + ) + { + foreach (var registration in serviceModel.Registrations) + { // Null means the registration took the project-wide default, which is what // DependencyModules_RegistrationType sets. - var registrationType = registration.RegistrationType ?? configurationModel.RegistrationType; + var registrationType = + registration.RegistrationType ?? configurationModel.RegistrationType; - if (registrationType is RegistrationType.Try or RegistrationType.Replace) { + if (registrationType is RegistrationType.Try or RegistrationType.Replace) + { return true; } } return false; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs index b300087..38b4f4d 100644 --- a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs +++ b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs @@ -9,7 +9,8 @@ namespace DependencyModules.SourceGenerator.Impl; /// that succeeds and an application that misbehaves at run time. Everything here exists to move a /// failure from run time to build time, or at minimum to make it visible. /// -public static class DependencyModuleDiagnostics { +public static class DependencyModuleDiagnostics +{ private const string Category = "DependencyModules"; /// @@ -20,13 +21,13 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor GeneratorFailure = new( id: "DM0001", title: "DependencyModules generator failed", - messageFormat: - "The DependencyModules generator failed and registrations may be missing or incomplete: {0}. " + - "Set DependencyModules_LogOutputDirectory to capture a log, and report the issue at " + - "https://github.com/ipjohnson/DependencyModules/issues with that log attached.", + messageFormat: "The DependencyModules generator failed and registrations may be missing or incomplete: {0}. " + + "Set DependencyModules_LogOutputDirectory to capture a log, and report the issue at " + + "https://github.com/ipjohnson/DependencyModules/issues with that log attached.", category: Category, defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for a service the container could never construct. Without this the generator emits a @@ -35,12 +36,12 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor ServiceCannotBeConstructed = new( id: "DM0002", title: "Service type cannot be constructed", - messageFormat: - "'{0}' is {1} and cannot be instantiated, so it was not registered. " + - "Apply the service attribute to a concrete class, or register it with a static factory method.", + messageFormat: "'{0}' is {1} and cannot be instantiated, so it was not registered. " + + "Apply the service attribute to a concrete class, or register it with a static factory method.", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for a module that is not partial. The compiler also reports CS0260 once the generated @@ -49,12 +50,12 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor ModuleMustBePartial = new( id: "DM0003", title: "Dependency module must be partial", - messageFormat: - "'{0}' is marked with [DependencyModule] but is not declared partial. " + - "The generator completes the type with a second partial declaration, so add the partial modifier.", + messageFormat: "'{0}' is marked with [DependencyModule] but is not declared partial. " + + "The generator completes the type with a second partial declaration, so add the partial modifier.", category: Category, defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for a module declared inside another type. @@ -69,13 +70,13 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor ModuleCannotBeNested = new( id: "DM0017", title: "Dependency module cannot be nested inside another type", - messageFormat: - "'{0}' is marked with [DependencyModule] but is declared inside another type. " + - "The generator completes a module at namespace level, so this would produce a second, " + - "unrelated type and register nothing. Move it out to the namespace.", + messageFormat: "'{0}' is marked with [DependencyModule] but is declared inside another type. " + + "The generator completes a module at namespace level, so this would produce a second, " + + "unrelated type and register nothing. Move it out to the namespace.", category: Category, defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for an assembly-level module attribute in a file that is not the entry point. @@ -110,14 +111,14 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor DecoratorImplementationNeedsTypeRegistration = new( id: "DM0022", title: "Decorator names an implementation while factories are generated", - messageFormat: - "'{0}' decorates only '{1}', but DependencyModules_GenerateFactories is on for this project " + - "and a factory registration cannot say what implementation it built — so the decorator would " + - "wrap every registration of '{2}' instead of one. Turn the property off for this project, or " + - "drop Implementation and decorate them all.", + messageFormat: "'{0}' decorates only '{1}', but DependencyModules_GenerateFactories is on for this project " + + "and a factory registration cannot say what implementation it built — so the decorator would " + + "wrap every registration of '{2}' instead of one. Turn the property off for this project, or " + + "drop Implementation and decorate them all.", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for a [Mock] parameter and a [TestExport] on the same method, both @@ -136,14 +137,14 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor MockAndTestExportOnOneMethod = new( id: "DM0021", title: "[Mock] and [TestExport] name one service on the same method", - messageFormat: - "'{0}' carries [TestExport] for '{1}' and a [Mock] parameter naming the same service. The " + - "parameter wins, so the [TestExport] does nothing. Move the [TestExport] to the class or " + - "the assembly if it is the default this test is overriding, or drop whichever of the two " + - "was not meant.", + messageFormat: "'{0}' carries [TestExport] for '{1}' and a [Mock] parameter naming the same service. The " + + "parameter wins, so the [TestExport] does nothing. Move the [TestExport] to the class or " + + "the assembly if it is the default this test is overriding, or drop whichever of the two " + + "was not meant.", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for an interception no module applies, so it can never run. @@ -162,25 +163,25 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor InterceptionAppliedByNoModule = new( id: "DM0020", title: "Interception is applied by no module", - messageFormat: - "'{0}' is marked for interception, but no module in this compilation applies it, so the " + - "interceptors never run. This happens when the registration and the interception land in " + - "different realms — a realm-only module registering '{0}' by convention, for instance, while " + - "the interception names no realm. Name the module on [Intercept(Realm = typeof(...))].", + messageFormat: "'{0}' is marked for interception, but no module in this compilation applies it, so the " + + "interceptors never run. This happens when the registration and the interception land in " + + "different realms — a realm-only module registering '{0}' by convention, for instance, while " + + "the interception names no realm. Name the module on [Intercept(Realm = typeof(...))].", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); public static readonly DiagnosticDescriptor AssemblyModuleAttributeNotComposed = new( id: "DM0019", title: "Assembly-level module attribute is not composed", - messageFormat: - "'{0}' is applied at the assembly level in this file, but the generated ApplicationModule is " + - "built from '{1}', so this composition is ignored and the module's services are not " + - "registered. Move the attribute to '{1}', or load the module explicitly with AddModule.", + messageFormat: "'{0}' is applied at the assembly level in this file, but the generated ApplicationModule is " + + "built from '{1}', so this composition is ignored and the module's services are not " + + "registered. Move the attribute to '{1}', or load the module explicitly with AddModule.", category: Category, defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for a module carrying settable properties while relying on the generated @@ -196,11 +197,10 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor ModuleWithPropertiesShouldImplementEquals = new( id: "DM0018", title: "Module with properties relies on generated equality", - messageFormat: - "'{0}' has settable properties but does not declare Equals, so the generated equality " + - "compares by type alone. Two instances carrying different values count as the same module " + - "and the first one reached wins. Declare Equals and GetHashCode on '{0}' to say which " + - "instances are the same.", + messageFormat: "'{0}' has settable properties but does not declare Equals, so the generated equality " + + "compares by type alone. Two instances carrying different values count as the same module " + + "and the first one reached wins. Declare Equals and GetHashCode on '{0}' to say which " + + "instances are the same.", category: Category, // A warning rather than informational. The identity of a module with parameters is genuinely // ambiguous, and the generator picks type-only on the developer's behalf — so the choice is @@ -212,7 +212,8 @@ public static class DependencyModuleDiagnostics { // Silencing still works per project, through NoWarn or .editorconfig, for a codebase whose // parameterised modules are each composed once. defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised when two conventions in one module register a type as the same service type. @@ -232,12 +233,12 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor AmbiguousConventionMatch = new( id: "DM0004", title: "Convention match is ambiguous", - messageFormat: - "'{0}' is matched by two conventions in '{1}' that both register it as '{2}'. {3} " + - "Narrow one of them, or move it to another module.", + messageFormat: "'{0}' is matched by two conventions in '{1}' that both register it as '{2}'. {3} " + + "Narrow one of them, or move it to another module.", category: Category, defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for a convention that matched nothing. A convention naming a service type no type in @@ -254,7 +255,8 @@ public static class DependencyModuleDiagnostics { messageFormat: "The convention registering '{0}' in '{1}' matched no types. {2}.", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for a convention match the container could not construct. The abstract and static @@ -264,12 +266,12 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor ConventionMatchNotConstructable = new( id: "DM0006", title: "Convention matched a type that cannot be constructed", - messageFormat: - "'{0}' matches the convention registering '{1}' in '{2}', but has no accessible constructor, " + - "so it was not registered", + messageFormat: "'{0}' matches the convention registering '{1}' in '{2}', but has no accessible constructor, " + + "so it was not registered", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised when two decorators of one service share an order. Applying them in an arbitrary order @@ -278,12 +280,12 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor AmbiguousDecoratorOrder = new( id: "DM0007", title: "Decorator order is ambiguous", - messageFormat: - "'{0}' and '{1}' both decorate '{2}' with order {3}, so the order they nest in is undefined. " + - "Give them distinct Order values.", + messageFormat: "'{0}' and '{1}' both decorate '{2}' with order {3}, so the order they nest in is undefined. " + + "Give them distinct Order values.", category: Category, defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised when a class marked for interception cannot be wrapped. Interception works through an @@ -299,13 +301,13 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor CannotIntercept = new( id: "DM0008", title: "Service cannot be intercepted", - messageFormat: - "This service cannot be intercepted, so no wrapper was generated and none of its members are " + - "intercepted: {0}. Other members may be unsupported for the same reason. Write a decorator " + - "instead, or move the member to an interface that is not intercepted.", + messageFormat: "This service cannot be intercepted, so no wrapper was generated and none of its members are " + + "intercepted: {0}. Other members may be unsupported for the same reason. Write a decorator " + + "instead, or move the member to an interface that is not intercepted.", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for something in a Conventions method body the generator could not read. @@ -321,7 +323,8 @@ public static class DependencyModuleDiagnostics { messageFormat: "This convention declaration could not be read, because {0}: {1}", category: Category, defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Reports, on the class itself, that a convention registered it. @@ -342,7 +345,8 @@ public static class DependencyModuleDiagnostics { messageFormat: "Exposed as {0}", category: Category, defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Reports, on the class itself, that its registration is conditional and on what. @@ -362,7 +366,8 @@ public static class DependencyModuleDiagnostics { messageFormat: "Registered only when {0}", category: Category, defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for a condition that names nothing to test. @@ -379,7 +384,8 @@ public static class DependencyModuleDiagnostics { messageFormat: "{0} names no {1} to test, so it does not depend on the environment", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for an interceptor that cannot serve some of the members it was applied to. @@ -401,12 +407,12 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor InterceptorCannotServeMembers = new( id: "DM0015", title: "Interceptor does not apply to every member", - messageFormat: - "'{0}' does not implement '{1}', so it is not applied to {2} on '{3}': {4}. Those members run " + - "without it. Implement '{1}' on the interceptor, or apply it to a service that has no such member.", + messageFormat: "'{0}' does not implement '{1}', so it is not applied to {2} on '{3}': {4}. Those members run " + + "without it. Implement '{1}' on the interceptor, or apply it to a service that has no such member.", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for a decorator whose service is registered as an open generic. @@ -431,14 +437,14 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor OpenGenericCannotBeDecorated = new( id: "DM0013", title: "Open generic registration cannot be decorated", - messageFormat: - "'{0}' is registered as an open generic, so '{1}' cannot decorate it. Decoration replaces a " + - "registration with a factory, and the container does not allow one for an open generic " + - "service type. Register closed constructions of '{0}' instead — a convention over the open " + - "generic registers one per implementation, and a generic decorator is then expanded across them.", + messageFormat: "'{0}' is registered as an open generic, so '{1}' cannot decorate it. Decoration replaces a " + + "registration with a factory, and the container does not allow one for an open generic " + + "service type. Register closed constructions of '{0}' instead — a convention over the open " + + "generic registers one per implementation, and a generic decorator is then expanded across them.", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for [CrossWireService] on a generic type. @@ -459,14 +465,14 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor CrossWireCannotBeGeneric = new( id: "DM0014", title: "Generic type cannot be cross-wired", - messageFormat: - "'{0}' is generic, so [CrossWireService] cannot register it. Cross-wiring shares one instance " + - "across every service type, which needs a factory, and the container does not allow one for an " + - "open generic registration. Use [SingletonService], [ScopedService] or [TransientService] to " + - "register it, applying one per interface if it needs to answer to more than one.", + messageFormat: "'{0}' is generic, so [CrossWireService] cannot register it. Cross-wiring shares one instance " + + "across every service type, which needs a factory, and the container does not allow one for an " + + "open generic registration. Use [SingletonService], [ScopedService] or [TransientService] to " + + "register it, applying one per interface if it needs to answer to more than one.", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Raised for an assembly-level module attribute whose namespace the file does not import. @@ -489,12 +495,11 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor ModuleAttributeNamespaceNotImported = new( id: "DM0016", title: "Assembly-level module attribute needs its namespace imported", - messageFormat: - "'{0}' is declared in '{1}', and an assembly-level attribute has no namespace context, so " + - "this does not compile. Add 'using {1};' to this file, or write it qualified as " + - "'[assembly: {1}.{0}]'.", + messageFormat: "'{0}' is declared in '{1}', and an assembly-level attribute has no namespace context, so " + + "this does not compile. Add 'using {1};' to this file, or write it qualified as " + + "'[assembly: {1}.{0}]'.", category: Category, defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); - + isEnabledByDefault: true + ); } diff --git a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs index 6c4f56a..8cc7cad 100644 --- a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs @@ -8,11 +8,12 @@ namespace DependencyModules.SourceGenerator.Impl; - -public class DependencyModuleWriter { +public class DependencyModuleWriter +{ private readonly bool _generateAttribute; - public DependencyModuleWriter(bool generateAttribute) { + public DependencyModuleWriter(bool generateAttribute) + { _generateAttribute = generateAttribute; } @@ -28,34 +29,49 @@ public DependencyModuleWriter(bool generateAttribute) { /// public static void Register( IncrementalGeneratorInitializationContext context, - IncrementalValueProvider> valuesProvider, - bool generateAttribute) { - + IncrementalValueProvider< + ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> + > valuesProvider, + bool generateAttribute + ) + { context.RegisterSourceOutput( - valuesProvider, new DependencyModuleWriter(generateAttribute).GenerateSource); + valuesProvider, + new DependencyModuleWriter(generateAttribute).GenerateSource + ); context.RegisterSourceOutput( valuesProvider.Combine(context.CompilationProvider), - ModuleEntryPointDiagnostics.Report); + ModuleEntryPointDiagnostics.Report + ); } - public void GenerateSource(SourceProductionContext context, - ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> allEntryPoints) { - - if (allEntryPoints.Length == 0) { + public void GenerateSource( + SourceProductionContext context, + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> allEntryPoints + ) + { + if (allEntryPoints.Length == 0) + { return; } - - var (entryPointList, configurationModel) = - EntryModelUtil.ConsolidateEntryPointModels(allEntryPoints); - foreach (var entryPointModel in entryPointList) { + var (entryPointList, configurationModel) = EntryModelUtil.ConsolidateEntryPointModels( + allEntryPoints + ); + + foreach (var entryPointModel in entryPointList) + { context.CancellationToken.ThrowIfCancellationRequested(); ProcessEntryPoint( context, WithDelegateTarget(entryPointModel, entryPointList), - configurationModel); + configurationModel + ); } } @@ -69,52 +85,66 @@ public void GenerateSource(SourceProductionContext context, /// the emitted module changes, and a module with nothing to defer to is returned untouched. /// private static ModuleEntryPointModel WithDelegateTarget( - ModuleEntryPointModel entryPointModel, IList entryPointList) { - + ModuleEntryPointModel entryPointModel, + IList entryPointList + ) + { var target = EntryModelUtil.DelegateTargetFor(entryPointModel, entryPointList); - if (target == null) { + if (target == null) + { return entryPointModel; } var modules = new List(entryPointModel.AdditionalModules); - if (!modules.Contains(target)) { + if (!modules.Contains(target)) + { modules.Add(target); } - return entryPointModel with { AdditionalModules = modules }; + return entryPointModel with + { + AdditionalModules = modules, + }; } private void ProcessEntryPoint( - SourceProductionContext context, - ModuleEntryPointModel entryPointModel, - DependencyModuleConfigurationModel configurationModel) { - + SourceProductionContext context, + ModuleEntryPointModel entryPointModel, + DependencyModuleConfigurationModel configurationModel + ) + { // Both of these are reported by ModuleEntryPointDiagnostics, which owns the conditions. // Generating anyway is what makes them worth stopping for: a non-partial module produces // CS0260 against the developer's own declaration, and a nested one emits a same-named type // at namespace level that compiles and registers nothing. Either way the actionable message // would arrive buried under errors describing the symptom. - if (ModuleEntryPointDiagnostics.IsNotPartial(entryPointModel) || - ModuleEntryPointDiagnostics.IsNestedInType(entryPointModel)) { + if ( + ModuleEntryPointDiagnostics.IsNotPartial(entryPointModel) + || ModuleEntryPointDiagnostics.IsNestedInType(entryPointModel) + ) + { return; } - entryPointModel = EntryModelUtil.EnsureNamespace(entryPointModel,configurationModel); + entryPointModel = EntryModelUtil.EnsureNamespace(entryPointModel, configurationModel); var csharpFile = new CSharpFileDefinition(entryPointModel.EntryPointType.Namespace); GenerateModuleClass(entryPointModel, csharpFile); GenerateUseMethod(entryPointModel, configurationModel, csharpFile); - + GenerateAttribute(entryPointModel, csharpFile); - var outputContext = new OutputContext(new OutputContextOptions { - TypeOutputMode = TypeOutputMode.Global, - BraceStyle = configurationModel.GeneratedCodeStyle - }); + var outputContext = new OutputContext( + new OutputContextOptions + { + TypeOutputMode = TypeOutputMode.Global, + BraceStyle = configurationModel.GeneratedCodeStyle, + } + ); csharpFile.WriteOutput(outputContext); @@ -124,14 +154,21 @@ private void ProcessEntryPoint( context.AddSource( entryPointModel.EntryPointType.GetFileNameHint( - configurationModel.RootNamespace, "Module"), - source); + configurationModel.RootNamespace, + "Module" + ), + source + ); } - private void GenerateAttribute(ModuleEntryPointModel moduleEntryPoint, - CSharpFileDefinition csharpFile) { + private void GenerateAttribute( + ModuleEntryPointModel moduleEntryPoint, + CSharpFileDefinition csharpFile + ) + { var model = moduleEntryPoint; - if (_generateAttribute && model.GenerateAttribute != false) { + if (_generateAttribute && model.GenerateAttribute != false) + { var attributeGenerator = new ModuleAttributeWriter(); attributeGenerator.CreateAttributeClass(csharpFile, model); @@ -139,45 +176,58 @@ private void GenerateAttribute(ModuleEntryPointModel moduleEntryPoint, } private void GenerateUseMethod( - ModuleEntryPointModel entryPointModel, - DependencyModuleConfigurationModel configurationModel, - CSharpFileDefinition csharpFile) { - - if (string.IsNullOrEmpty(entryPointModel.UseMethod)) { + ModuleEntryPointModel entryPointModel, + DependencyModuleConfigurationModel configurationModel, + CSharpFileDefinition csharpFile + ) + { + if (string.IsNullOrEmpty(entryPointModel.UseMethod)) + { return; } - var extensionMethod = csharpFile.AddClass($"{entryPointModel.EntryPointType.Name}Extensions"); + var extensionMethod = csharpFile.AddClass( + $"{entryPointModel.EntryPointType.Name}Extensions" + ); + + extensionMethod.Modifiers = + ComponentModifier.Public | ComponentModifier.Static | ComponentModifier.Partial; - extensionMethod.Modifiers = ComponentModifier.Public | ComponentModifier.Static | ComponentModifier.Partial; - var method = extensionMethod.AddMethod(entryPointModel.UseMethod!); method.Modifiers = ComponentModifier.Public | ComponentModifier.Static; method.SetReturnType(KnownTypes.Microsoft.DependencyInjection.IServiceCollection); - var serviceProvider = method.AddParameter(KnownTypes.Microsoft.DependencyInjection.IServiceCollection, "serviceCollection"); + var serviceProvider = method.AddParameter( + KnownTypes.Microsoft.DependencyInjection.IServiceCollection, + "serviceCollection" + ); serviceProvider.This = true; - + var parameters = new List(); - - foreach (var parameterInfoModel in entryPointModel.Parameters) { - var param = method.AddParameter(parameterInfoModel.ParameterType, parameterInfoModel.ParameterName); - + + foreach (var parameterInfoModel in entryPointModel.Parameters) + { + var param = method.AddParameter( + parameterInfoModel.ParameterType, + parameterInfoModel.ParameterName + ); + parameters.Add(param); } - + var newStatement = New(entryPointModel.EntryPointType, parameters.ToArray()); - + method.Return(serviceProvider.Invoke("AddModules", newStatement)); method.AddUsingNamespace("DependencyModules.Runtime"); } - - private void GenerateModuleClass(ModuleEntryPointModel model, CSharpFileDefinition csharpFile) { + + private void GenerateModuleClass(ModuleEntryPointModel model, CSharpFileDefinition csharpFile) + { var classDefinition = csharpFile.AddClass(model.EntryPointType.Name); classDefinition.EnableNullable(); classDefinition.Modifiers |= ComponentModifier.Partial; - + SetupStaticConstructor(classDefinition); PopulateServiceCollectionMethod(classDefinition, model); @@ -190,46 +240,66 @@ private void GenerateModuleClass(ModuleEntryPointModel model, CSharpFileDefiniti FeatureMethod(classDefinition, model); - if ((model.ModuleFeatures & ModuleEntryPointFeatures.ShouldImplementEquals) == - ModuleEntryPointFeatures.ShouldImplementEquals) { + if ( + (model.ModuleFeatures & ModuleEntryPointFeatures.ShouldImplementEquals) + == ModuleEntryPointFeatures.ShouldImplementEquals + ) + { EqualMethod(classDefinition, model); HashMethod(classDefinition, model); } } - private void FeatureMethod(ClassDefinition classDefinition, ModuleEntryPointModel model) { - if (model.Features.Count == 0) { + private void FeatureMethod(ClassDefinition classDefinition, ModuleEntryPointModel model) + { + if (model.Features.Count == 0) + { return; } - classDefinition.AddBaseType(KnownTypes.DependencyModules.Features.IDependencyModuleApplicatorProvider); + classDefinition.AddBaseType( + KnownTypes.DependencyModules.Features.IDependencyModuleApplicatorProvider + ); var method = classDefinition.AddMethod("FeatureApplicators"); method.SetReturnType( - new GenericTypeDefinition(typeof(IEnumerable<>), new []{KnownTypes.DependencyModules.Features.IFeatureApplicator})); + new GenericTypeDefinition( + typeof(IEnumerable<>), + new[] { KnownTypes.DependencyModules.Features.IFeatureApplicator } + ) + ); method.Modifiers |= ComponentModifier.Virtual | ComponentModifier.Public; method.AddLeadingTrait(CodeOutputComponent.Get("[Browsable(false)]", true)); method.AddUsingNamespace("System.ComponentModel"); - - method.InterfaceImplementation = KnownTypes.DependencyModules.Features.IDependencyModuleApplicatorProvider; - - foreach (var typeDefinition in model.Features) { + + method.InterfaceImplementation = KnownTypes + .DependencyModules + .Features + .IDependencyModuleApplicatorProvider; + + foreach (var typeDefinition in model.Features) + { method.AddIndentedStatement( YieldReturn( New( - new GenericTypeDefinition(TypeDefinitionEnum.ClassDefinition, - KnownTypes.DependencyModules.Features.FeatureApplicator.Namespace, - KnownTypes.DependencyModules.Features.FeatureApplicator.Name, - new []{typeDefinition}), "this") - )); + new GenericTypeDefinition( + TypeDefinitionEnum.ClassDefinition, + KnownTypes.DependencyModules.Features.FeatureApplicator.Namespace, + KnownTypes.DependencyModules.Features.FeatureApplicator.Name, + new[] { typeDefinition } + ), + "this" + ) + ) + ); } } - private void HashMethod( - ClassDefinition classDefinition, ModuleEntryPointModel model) { + private void HashMethod(ClassDefinition classDefinition, ModuleEntryPointModel model) + { var hashMethod = classDefinition.AddMethod("GetHashCode"); hashMethod.Modifiers |= ComponentModifier.Override; @@ -243,14 +313,18 @@ private void HashMethod( hashMethod.Return(stableHash.ToString()); } - private static int GetStableHashCode(string str) { - unchecked { + private static int GetStableHashCode(string str) + { + unchecked + { var hash1 = 5381; var hash2 = hash1; - for (var i = 0; i < str.Length && str[i] != '\0'; i += 2) { + for (var i = 0; i < str.Length && str[i] != '\0'; i += 2) + { hash1 = ((hash1 << 5) + hash1) ^ str[i]; - if (i == str.Length - 1) { + if (i == str.Length - 1) + { break; } @@ -261,7 +335,8 @@ private static int GetStableHashCode(string str) { } } - private void EqualMethod(ClassDefinition classDefinition, ModuleEntryPointModel model) { + private void EqualMethod(ClassDefinition classDefinition, ModuleEntryPointModel model) + { var equalMethod = classDefinition.AddMethod("Equals"); equalMethod.Modifiers |= ComponentModifier.Override; @@ -277,7 +352,11 @@ private void EqualMethod(ClassDefinition classDefinition, ModuleEntryPointModel /// every module can be sorted together. Emitted unconditionally: a module may gain decorators /// from a source the registrations file never saw, and returning an empty list costs nothing. /// - private void InternalGetDecoratorsMethod(ClassDefinition classDefinition, ModuleEntryPointModel model) { + private void InternalGetDecoratorsMethod( + ClassDefinition classDefinition, + ModuleEntryPointModel model + ) + { var method = classDefinition.AddMethod("InternalGetDecorators"); method.AddLeadingTrait(CodeOutputComponent.Get("[Browsable(false)]", true)); @@ -287,76 +366,101 @@ private void InternalGetDecoratorsMethod(ClassDefinition classDefinition, Module method.SetReturnType( new GenericTypeDefinition( typeof(IEnumerable<>), - new[] { KnownTypes.DependencyModules.Helpers.DecoratorRegistration })); + new[] { KnownTypes.DependencyModules.Helpers.DecoratorRegistration } + ) + ); var closedType = new GenericTypeDefinition( - TypeDefinitionEnum.ClassDefinition, KnownTypes.DependencyModules.Helpers.Namespace, "DependencyRegistry", new[] { - model.EntryPointType - }); + TypeDefinitionEnum.ClassDefinition, + KnownTypes.DependencyModules.Helpers.Namespace, + "DependencyRegistry", + new[] { model.EntryPointType } + ); - method.Return(new StaticInvokeStatement(closedType, "GetDecorators", new List()) { - Indented = false - }); + method.Return( + new StaticInvokeStatement(closedType, "GetDecorators", new List()) + { + Indented = false, + } + ); } - private void InternalGetModulesMethod(ClassDefinition classDefinition, ModuleEntryPointModel model) { + private void InternalGetModulesMethod( + ClassDefinition classDefinition, + ModuleEntryPointModel model + ) + { var attributeModels = FilterAttributes(model.AttributeModels); - + var getModulesMethod = classDefinition.AddMethod("InternalGetModules"); getModulesMethod.AddLeadingTrait(CodeOutputComponent.Get("[Browsable(false)]", true)); getModulesMethod.AddUsingNamespace("System.ComponentModel"); - - getModulesMethod.InterfaceImplementation = KnownTypes.DependencyModules.Interfaces.IDependencyModule; + + getModulesMethod.InterfaceImplementation = KnownTypes + .DependencyModules + .Interfaces + .IDependencyModule; getModulesMethod.SetReturnType(TypeDefinition.Get(typeof(IEnumerable))); - + var parametersList = new List(); - - foreach (var additionalModule in model.AdditionalModules) { - if (additionalModule != null) { + + foreach (var additionalModule in model.AdditionalModules) + { + if (additionalModule != null) + { var newStatement = New(additionalModule); parametersList.Add(newStatement); } } - - foreach (var modelAttributeModel in attributeModels) { + + foreach (var modelAttributeModel in attributeModels) + { var newStatement = New( modelAttributeModel.TypeDefinition, - modelAttributeModel.GetArguments().Select(o => (object)o).ToArray()); - - foreach (var propertyValue in modelAttributeModel.PropertyValues()) { + modelAttributeModel.GetArguments().Select(o => (object)o).ToArray() + ); + + foreach (var propertyValue in modelAttributeModel.PropertyValues()) + { newStatement.AddInitValue(propertyValue); } - + parametersList.Add(newStatement); } - + var closedType = new GenericTypeDefinition( - TypeDefinitionEnum.ClassDefinition, KnownTypes.DependencyModules.Helpers.Namespace, "DependencyRegistry", new[] { - model.EntryPointType - }); - + TypeDefinitionEnum.ClassDefinition, + KnownTypes.DependencyModules.Helpers.Namespace, + "DependencyRegistry", + new[] { model.EntryPointType } + ); + getModulesMethod.Return( - new StaticInvokeStatement( - closedType, - "GetModules", - parametersList.ToArray()) { - Indented = false - }); + new StaticInvokeStatement(closedType, "GetModules", parametersList.ToArray()) + { + Indented = false, + } + ); } - private List FilterAttributes(IReadOnlyList modelAttributeModels) { + private List FilterAttributes( + IReadOnlyList modelAttributeModels + ) + { var attributeModels = new List(); - foreach (var modelAttributeModel in modelAttributeModels) { - if (modelAttributeModel.TypeDefinition.Name == "DependencyModuleAttribute") { + foreach (var modelAttributeModel in modelAttributeModels) + { + if (modelAttributeModel.TypeDefinition.Name == "DependencyModuleAttribute") + { continue; } attributeModels.Add(modelAttributeModel); } - + return attributeModels; } @@ -379,77 +483,98 @@ private List FilterAttributes(IReadOnlyList mode /// private void InternalApplyServicesMethod( ClassDefinition classDefinition, - ModuleEntryPointModel model) { - + ModuleEntryPointModel model + ) + { var closedType = new GenericTypeDefinition( - TypeDefinitionEnum.ClassDefinition, KnownTypes.DependencyModules.Helpers.Namespace, "DependencyRegistry", new[] { - model.EntryPointType - }); + TypeDefinitionEnum.ClassDefinition, + KnownTypes.DependencyModules.Helpers.Namespace, + "DependencyRegistry", + new[] { model.EntryPointType } + ); ApplyServicesOverload(classDefinition, closedType, withEnvironment: false); ApplyServicesOverload(classDefinition, closedType, withEnvironment: true); } private static void ApplyServicesOverload( - ClassDefinition classDefinition, ITypeDefinition closedType, bool withEnvironment) { - + ClassDefinition classDefinition, + ITypeDefinition closedType, + bool withEnvironment + ) + { var loadDependenciesMethod = classDefinition.AddMethod("InternalApplyServices"); loadDependenciesMethod.AddLeadingTrait(CodeOutputComponent.Get("[Browsable(false)]", true)); loadDependenciesMethod.AddUsingNamespace("System.ComponentModel"); - loadDependenciesMethod.InterfaceImplementation = - KnownTypes.DependencyModules.Interfaces.IDependencyModule; + loadDependenciesMethod.InterfaceImplementation = KnownTypes + .DependencyModules + .Interfaces + .IDependencyModule; - var arguments = new List { + var arguments = new List + { loadDependenciesMethod.AddParameter( - KnownTypes.Microsoft.DependencyInjection.IServiceCollection, "services") + KnownTypes.Microsoft.DependencyInjection.IServiceCollection, + "services" + ), }; - if (withEnvironment) { + if (withEnvironment) + { arguments.Add( loadDependenciesMethod.AddParameter( - KnownTypes.DependencyModules.Interfaces.IModuleEnvironment, "environment")); + KnownTypes.DependencyModules.Interfaces.IModuleEnvironment, + "environment" + ) + ); } loadDependenciesMethod.AddIndentedStatement( - new StaticInvokeStatement(closedType, "ApplyServices", arguments) { - Indented = false - }); + new StaticInvokeStatement(closedType, "ApplyServices", arguments) { Indented = false } + ); } - private void PopulateServiceCollectionMethod(ClassDefinition classDefinition, ModuleEntryPointModel model) { + private void PopulateServiceCollectionMethod( + ClassDefinition classDefinition, + ModuleEntryPointModel model + ) + { classDefinition.AddBaseType(KnownTypes.DependencyModules.Interfaces.IDependencyModule); var loadDependenciesMethod = classDefinition.AddMethod("PopulateServiceCollection"); - var parameter = - loadDependenciesMethod.AddParameter( - KnownTypes.Microsoft.DependencyInjection.IServiceCollection, "services"); + var parameter = loadDependenciesMethod.AddParameter( + KnownTypes.Microsoft.DependencyInjection.IServiceCollection, + "services" + ); var closedType = new GenericTypeDefinition( TypeDefinitionEnum.ClassDefinition, KnownTypes.DependencyModules.Helpers.Namespace, "DependencyRegistry", - new[] { - model.EntryPointType - }); + new[] { model.EntryPointType } + ); loadDependenciesMethod.AddIndentedStatement( new StaticInvokeStatement( closedType, "LoadModules", - new IOutputComponent[] { + new IOutputComponent[] + { parameter, - new CodeOutputComponent("this") { - Indented = false - } - }) { - Indented = false - }); + new CodeOutputComponent("this") { Indented = false }, + } + ) + { + Indented = false, + } + ); } - private void SetupStaticConstructor(ClassDefinition classDefinition) { + private void SetupStaticConstructor(ClassDefinition classDefinition) + { classDefinition.AddConstructor().Modifiers = ComponentModifier.Static; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/InterceptorFileWriter.cs b/src/DependencyModules.SourceGenerator.Impl/InterceptorFileWriter.cs index 2bc1294..58148b8 100644 --- a/src/DependencyModules.SourceGenerator.Impl/InterceptorFileWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/InterceptorFileWriter.cs @@ -23,12 +23,17 @@ namespace DependencyModules.SourceGenerator.Impl; /// Every generated name carries a _dm prefix. The wrapper implements a user's interface, and /// a member of that interface named _inner would otherwise collide with the field. /// -public class InterceptorFileWriter { - +public class InterceptorFileWriter +{ private const string InnerField = "_dmInner"; - public string Write(InterceptorModel model, string wrapperName, string namespaceName, - DependencyModuleConfigurationModel configurationModel) { + public string Write( + InterceptorModel model, + string wrapperName, + string namespaceName, + DependencyModuleConfigurationModel configurationModel + ) + { var csharpFile = new CSharpFileDefinition(namespaceName); var wrapper = csharpFile.AddClass(wrapperName); @@ -39,8 +44,10 @@ public string Write(InterceptorModel model, string wrapperName, string namespace // what lets the container register it as an open generic implementation. Only constraint-free // parameters reach here; a constrained one is refused upstream, because the wrapper would have // to repeat the constraint and there is no way to emit one. - if (model.IsOpenGeneric) { - foreach (var typeParameter in model.TypeParameters!) { + if (model.IsOpenGeneric) + { + foreach (var typeParameter in model.TypeParameters!) + { wrapper.AddGenericParameter(typeParameter.Name); WriteConstraint(wrapper.AddConstraint(typeParameter.Name), typeParameter); @@ -48,17 +55,22 @@ public string Write(InterceptorModel model, string wrapperName, string namespace } wrapper.AddBaseType(model.ServiceType); - wrapper.AddAttribute(TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverage")); + wrapper.AddAttribute( + TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverage") + ); WriteFields(wrapper, model); WriteConstructor(wrapper, model); - foreach (var declaration in model.Declarations) { + foreach (var declaration in model.Declarations) + { WriteDeclaration(wrapper, model, declaration); } - for (var index = 0; index < model.Members.Count; index++) { - if (IsIntercepted(model, model.Members[index])) { + for (var index = 0; index < model.Members.Count; index++) + { + if (IsIntercepted(model, model.Members[index])) + { WriteState(wrapper, model, model.Members[index], index, wrapperName, namespaceName); } } @@ -67,10 +79,12 @@ public string Write(InterceptorModel model, string wrapperName, string namespace csharpFile.EnableNullable(); var output = new OutputContext( - new OutputContextOptions { + new OutputContextOptions + { TypeOutputMode = TypeOutputMode.Global, - BraceStyle = configurationModel.GeneratedCodeStyle - }); + BraceStyle = configurationModel.GeneratedCodeStyle, + } + ); csharpFile.WriteOutput(output); @@ -90,25 +104,34 @@ public string Write(InterceptorModel model, string wrapperName, string namespace /// DecoratorHelper.InterceptOpenGeneric registers alongside it. /// private static ITypeDefinition InnerType(InterceptorModel model) => - model.IsOpenGeneric ? Closed(model.ImplementationType, model.TypeParameters!) : model.ServiceType; + model.IsOpenGeneric + ? Closed(model.ImplementationType, model.TypeParameters!) + : model.ServiceType; /// /// A type closed over the wrapper's own type parameters — Repository becomes /// Repository<T>. /// private static ITypeDefinition Closed( - ITypeDefinition type, IReadOnlyList typeParameters) { - + ITypeDefinition type, + IReadOnlyList typeParameters + ) + { var arguments = new ITypeDefinition[typeParameters.Count]; - for (var i = 0; i < arguments.Length; i++) { + for (var i = 0; i < arguments.Length; i++) + { // A TypeParameterDefinition, not TypeDefinition.Get("", name): an empty namespace now // means the global namespace, which Global mode qualifies - and global::T is not a type. arguments[i] = new TypeParameterDefinition(typeParameters[i].Name); } return new GenericTypeDefinition( - TypeDefinitionEnum.ClassDefinition, type.Namespace, type.Name, arguments); + TypeDefinitionEnum.ClassDefinition, + type.Namespace, + type.Name, + arguments + ); } /// @@ -119,7 +142,11 @@ private static ITypeDefinition Closed( /// Repository_Intercepted<T> the name is Repository_Intercepted<T>, and /// the bare name is CS0305. /// - private static ITypeDefinition SelfType(InterceptorModel model, string wrapperName, string namespaceName) => + private static ITypeDefinition SelfType( + InterceptorModel model, + string wrapperName, + string namespaceName + ) => model.IsOpenGeneric ? Closed(TypeDefinition.Get(namespaceName, wrapperName), model.TypeParameters!) : TypeDefinition.Get(namespaceName, wrapperName); @@ -129,9 +156,12 @@ private static ITypeDefinition SelfType(InterceptorModel model, string wrapperNa /// to repeat or the call they forward will not satisfy them. /// private static void WriteConstraints( - InterceptedMemberModel member, Func addConstraint) { - - foreach (var typeParameter in member.TypeParameters) { + InterceptedMemberModel member, + Func addConstraint + ) + { + foreach (var typeParameter in member.TypeParameters) + { WriteConstraint(addConstraint(typeParameter.Name), typeParameter); } } @@ -143,8 +173,13 @@ private static void WriteConstraints( /// The parts go in as the symbol reported them and come out in the order C# requires, which is /// ConstraintDefinition's job rather than this writer's. /// - private static void WriteConstraint(ConstraintDefinition constraint, TypeParameterModel typeParameter) { - switch (typeParameter.Primary) { + private static void WriteConstraint( + ConstraintDefinition constraint, + TypeParameterModel typeParameter + ) + { + switch (typeParameter.Primary) + { case "class": constraint.Class(); break; @@ -162,50 +197,62 @@ private static void WriteConstraint(ConstraintDefinition constraint, TypeParamet break; } - foreach (var constraintType in typeParameter.ConstraintTypes) { + foreach (var constraintType in typeParameter.ConstraintTypes) + { constraint.Implements(constraintType); } - if (typeParameter.DefaultConstructor) { + if (typeParameter.DefaultConstructor) + { constraint.DefaultConstructor(); } } - private static void WriteFields(ClassDefinition wrapper, InterceptorModel model) { + private static void WriteFields(ClassDefinition wrapper, InterceptorModel model) + { var inner = wrapper.AddField(InnerType(model), InnerField); inner.Modifiers |= ComponentModifier.Private | ComponentModifier.Readonly; - for (var index = 0; index < model.Interceptors.Count; index++) { - var interceptor = wrapper.AddField(model.Interceptors[index].Type, InterceptorField(index)); + for (var index = 0; index < model.Interceptors.Count; index++) + { + var interceptor = wrapper.AddField( + model.Interceptors[index].Type, + InterceptorField(index) + ); interceptor.Modifiers |= ComponentModifier.Private | ComponentModifier.Readonly; } // Everything identifying a member is known now, so one caller is built per member and shared // by every call rather than constructed per invocation. A member nothing intercepts has no // caller, because it has no pipeline to report itself to. - for (var index = 0; index < model.Members.Count; index++) { - if (!IsIntercepted(model, model.Members[index])) { + for (var index = 0; index < model.Members.Count; index++) + { + if (!IsIntercepted(model, model.Members[index])) + { continue; } - var caller = wrapper.AddField( - Interception.CallerInfo, CallerField(index)); + var caller = wrapper.AddField(Interception.CallerInfo, CallerField(index)); - caller.Modifiers |= ComponentModifier.Private | ComponentModifier.Static | ComponentModifier.Readonly; + caller.Modifiers |= + ComponentModifier.Private | ComponentModifier.Static | ComponentModifier.Readonly; caller.InitializeValue = New( Interception.CallerInfo, TypeOf(model.ServiceType), - QuoteString(model.Members[index].Name)); + QuoteString(model.Members[index].Name) + ); } } - private static void WriteConstructor(ClassDefinition wrapper, InterceptorModel model) { + private static void WriteConstructor(ClassDefinition wrapper, InterceptorModel model) + { var constructor = wrapper.AddConstructor(); constructor.AddParameter(InnerType(model), "inner"); constructor.AddIndentedStatement($"{InnerField} = inner"); - for (var index = 0; index < model.Interceptors.Count; index++) { + for (var index = 0; index < model.Interceptors.Count; index++) + { constructor.AddParameter(model.Interceptors[index].Type, $"interceptor{index}"); constructor.AddIndentedStatement($"{InterceptorField(index)} = interceptor{index}"); } @@ -215,11 +262,20 @@ private static void WriteConstructor(ClassDefinition wrapper, InterceptorModel m /// One member as the interface declares it, with each accessor forwarding into the pipeline. /// private static void WriteDeclaration( - ClassDefinition wrapper, InterceptorModel model, InterceptedDeclarationModel declaration) { - - switch (declaration.Kind) { + ClassDefinition wrapper, + InterceptorModel model, + InterceptedDeclarationModel declaration + ) + { + switch (declaration.Kind) + { case DeclarationKind.Method: - WriteForwardingMethod(wrapper, model, model.Members[declaration.First], declaration.First); + WriteForwardingMethod( + wrapper, + model, + model.Members[declaration.First], + declaration.First + ); break; case DeclarationKind.Property: @@ -237,56 +293,99 @@ private static void WriteDeclaration( } private static void WriteProperty( - ClassDefinition wrapper, InterceptorModel model, InterceptedDeclarationModel declaration) { - + ClassDefinition wrapper, + InterceptorModel model, + InterceptedDeclarationModel declaration + ) + { var property = wrapper.AddProperty(declaration.Type!, declaration.Identifier); property.Modifiers |= ComponentModifier.Public; - if (declaration.First >= 0) { - WriteAccessorBody(property.Get, model, model.Members[declaration.First], declaration.First); + if (declaration.First >= 0) + { + WriteAccessorBody( + property.Get, + model, + model.Members[declaration.First], + declaration.First + ); } - if (declaration.Second < 0) { + if (declaration.Second < 0) + { // A get-only property. Leaving the setter in place would declare one the interface does // not have, and PropertyDefinition writes an empty pair as an auto-property. property.Set = null; - } else { - WriteAccessorBody(property.Set!, model, model.Members[declaration.Second], declaration.Second); + } + else + { + WriteAccessorBody( + property.Set!, + model, + model.Members[declaration.Second], + declaration.Second + ); } } private static void WriteIndexer( - ClassDefinition wrapper, InterceptorModel model, InterceptedDeclarationModel declaration) { - + ClassDefinition wrapper, + InterceptorModel model, + InterceptedDeclarationModel declaration + ) + { var indexer = wrapper.AddProperty(declaration.Type!, "this"); indexer.Modifiers |= ComponentModifier.Public; - foreach (var index in declaration.Indices) { + foreach (var index in declaration.Indices) + { indexer.AddIndexParameter(index.Type, index.Identifier); } - if (declaration.First >= 0) { - WriteAccessorBody(indexer.Get, model, model.Members[declaration.First], declaration.First); + if (declaration.First >= 0) + { + WriteAccessorBody( + indexer.Get, + model, + model.Members[declaration.First], + declaration.First + ); } - if (declaration.Second < 0) { + if (declaration.Second < 0) + { indexer.Set = null; - } else { - WriteAccessorBody(indexer.Set!, model, model.Members[declaration.Second], declaration.Second); + } + else + { + WriteAccessorBody( + indexer.Set!, + model, + model.Members[declaration.Second], + declaration.Second + ); } } private static void WriteEvent( - ClassDefinition wrapper, InterceptorModel model, InterceptedDeclarationModel declaration) { - + ClassDefinition wrapper, + InterceptorModel model, + InterceptedDeclarationModel declaration + ) + { var declared = wrapper.AddEvent(declaration.Type!, declaration.Identifier); declared.Modifiers |= ComponentModifier.Public; WriteAccessorBody(declared.Add, model, model.Members[declaration.First], declaration.First); - WriteAccessorBody(declared.Remove, model, model.Members[declaration.Second], declaration.Second); + WriteAccessorBody( + declared.Remove, + model, + model.Members[declaration.Second], + declaration.Second + ); } /// @@ -294,9 +393,14 @@ private static void WriteEvent( /// order the CLR gives an accessor: any indices, then the assigned value. /// private static void WriteAccessorBody( - PropertyMethodDefinition accessor, InterceptorModel model, InterceptedMemberModel member, int index) { - - if (!IsIntercepted(model, member)) { + PropertyMethodDefinition accessor, + InterceptorModel model, + InterceptedMemberModel member, + int index + ) + { + if (!IsIntercepted(model, member)) + { WritePassThrough(accessor, member); return; @@ -307,13 +411,17 @@ private static void WriteAccessorBody( arguments.AddRange(member.Parameters.Select(parameter => parameter.Identifier)); accessor.AddIndentedStatement( - $"var state = new {ClosedStateName(member, index)}({string.Join(", ", arguments)})"); + $"var state = new {ClosedStateName(member, index)}({string.Join(", ", arguments)})" + ); accessor.NewLine(); - if (member.ReturnShape == ReturnShape.Void) { + if (member.ReturnShape == ReturnShape.Void) + { accessor.AddIndentedStatement("state.Invoke(0)"); - } else { + } + else + { accessor.Return("state.Invoke(0)"); } } @@ -327,12 +435,16 @@ private static void WriteAccessorBody( /// it can serve and has nothing to say about the rest. Those members build no state and /// allocate nothing. /// - private static void WritePassThrough(BaseBlockDefinition block, InterceptedMemberModel member) { + private static void WritePassThrough(BaseBlockDefinition block, InterceptedMemberModel member) + { var call = InnerCall(member, InnerField); - if (member.ReturnShape == ReturnShape.Void) { + if (member.ReturnShape == ReturnShape.Void) + { block.AddIndentedStatement(call); - } else { + } + else + { block.Return(call); } } @@ -341,17 +453,23 @@ private static void WritePassThrough(BaseBlockDefinition block, InterceptedMembe /// The method as the interface declares it, forwarding into the pipeline. /// private static void WriteForwardingMethod( - ClassDefinition wrapper, InterceptorModel model, InterceptedMemberModel member, int index) { - + ClassDefinition wrapper, + InterceptorModel model, + InterceptedMemberModel member, + int index + ) + { var method = wrapper.AddMethod(member.Identifier); method.Modifiers |= ComponentModifier.Public; - if (member.ReturnType != null) { + if (member.ReturnType != null) + { method.SetReturnType(member.ReturnType); } - foreach (var typeParameter in member.TypeParameters) { + foreach (var typeParameter in member.TypeParameters) + { method.AddGenericParameter(new TypeParameterDefinition(typeParameter.Name)); } @@ -359,21 +477,27 @@ private static void WriteForwardingMethod( var arguments = new List { "this" }; - foreach (var parameter in member.Parameters) { + foreach (var parameter in member.Parameters) + { var declared = method.AddParameter(parameter.Type, parameter.Identifier); // Dropping params does not merely lose sugar: an optional parameter ahead of it becomes // an optional parameter followed by a required one, which the compiler refuses. declared.IsParams = parameter.IsParams; - if (parameter.DefaultValue != null) { - declared.DefaultValue = new CodeOutputComponent(parameter.DefaultValue) { Indented = false }; + if (parameter.DefaultValue != null) + { + declared.DefaultValue = new CodeOutputComponent(parameter.DefaultValue) + { + Indented = false, + }; } arguments.Add(parameter.Identifier); } - if (!IsIntercepted(model, member)) { + if (!IsIntercepted(model, member)) + { WritePassThrough(method, member); return; @@ -381,16 +505,19 @@ private static void WriteForwardingMethod( // A ValueTask cannot be built from the pipeline's ValueTask without either an await // or an allocation, so this one shape is written as an async method. - if (member.ReturnShape == ReturnShape.ValueTask) { + if (member.ReturnShape == ReturnShape.ValueTask) + { method.Modifiers |= ComponentModifier.Async; } method.AddIndentedStatement( - $"var state = new {ClosedStateName(member, index)}({string.Join(", ", arguments)})"); + $"var state = new {ClosedStateName(member, index)}({string.Join(", ", arguments)})" + ); method.NewLine(); - switch (member.ReturnShape) { + switch (member.ReturnShape) + { case ReturnShape.Void: method.AddIndentedStatement("state.Invoke(0)"); break; @@ -424,12 +551,14 @@ private static void WriteState( InterceptedMemberModel member, int index, string wrapperName, - string namespaceName) { - - var baseType = member.Kind switch { + string namespaceName + ) + { + var baseType = member.Kind switch + { InterceptorKind.Async => Interception.AsyncInvocationState(member.ResultType), InterceptorKind.Stream => Interception.StreamInvocationState(member.ResultType), - _ => Interception.InvocationState(member.ResultType) + _ => Interception.InvocationState(member.ResultType), }; var state = wrapper.AddClass(StateName(index)); @@ -437,7 +566,8 @@ private static void WriteState( state.Modifiers |= ComponentModifier.Private | ComponentModifier.Sealed; state.AddBaseType(baseType); - foreach (var typeParameter in member.TypeParameters) { + foreach (var typeParameter in member.TypeParameters) + { state.AddGenericParameter(typeParameter.Name); } @@ -454,12 +584,16 @@ private static void WriteState( } private static void WriteStateFields( - ClassDefinition state, InterceptedMemberModel member, ITypeDefinition selfType) { - + ClassDefinition state, + InterceptedMemberModel member, + ITypeDefinition selfType + ) + { var self = state.AddField(selfType, "_self"); self.Modifiers |= ComponentModifier.Private | ComponentModifier.Readonly; - for (var index = 0; index < member.Parameters.Count; index++) { + for (var index = 0; index < member.Parameters.Count; index++) + { var argument = state.AddField(member.Parameters[index].Type, ArgumentField(index)); argument.Modifiers |= ComponentModifier.Private; } @@ -470,20 +604,30 @@ private static void WriteStateFields( /// non-nullable reference is definitely assigned and the wrapper needs no nullable suppression. /// private static void WriteStateConstructor( - ClassDefinition state, InterceptedMemberModel member, int index, ITypeDefinition selfType) { - + ClassDefinition state, + InterceptedMemberModel member, + int index, + ITypeDefinition selfType + ) + { var constructor = state.AddConstructor(); constructor.AddParameter(selfType, "self"); constructor.AddIndentedStatement("_self = self"); - for (var argument = 0; argument < member.Parameters.Count; argument++) { + for (var argument = 0; argument < member.Parameters.Count; argument++) + { constructor.AddParameter(member.Parameters[argument].Type, $"arg{argument}"); constructor.AddIndentedStatement($"{ArgumentField(argument)} = arg{argument}"); } } - private static void WriteCallerAndCount(ClassDefinition state, InterceptedMemberModel member, int index) { + private static void WriteCallerAndCount( + ClassDefinition state, + InterceptedMemberModel member, + int index + ) + { var caller = state.AddProperty(Interception.CallerInfo, "Caller"); caller.Modifiers |= ComponentModifier.Public | ComponentModifier.Override; @@ -503,7 +647,8 @@ private static void WriteCallerAndCount(ClassDefinition state, InterceptedMember /// Reading boxes, and writing replaces the field the last stage passes on, so an interceptor /// that ignores the arguments pays for neither. /// - private static void WriteArgumentsIndexer(ClassDefinition state, InterceptedMemberModel member) { + private static void WriteArgumentsIndexer(ClassDefinition state, InterceptedMemberModel member) + { var indexer = state.AddProperty(TypeDefinition.Get(typeof(object)).MakeNullable(), "this"); indexer.Modifiers |= ComponentModifier.Public | ComponentModifier.Override; @@ -512,7 +657,8 @@ private static void WriteArgumentsIndexer(ClassDefinition state, InterceptedMemb var get = indexer.Get.Switch("index"); - for (var index = 0; index < member.Parameters.Count; index++) { + for (var index = 0; index < member.Parameters.Count; index++) + { get.AddCase(index).Return(ArgumentField(index)); } @@ -520,10 +666,12 @@ private static void WriteArgumentsIndexer(ClassDefinition state, InterceptedMemb var set = indexer.Set!.Switch("index"); - for (var index = 0; index < member.Parameters.Count; index++) { + for (var index = 0; index < member.Parameters.Count; index++) + { var block = set.AddCase(index); - block.Assign(Bang(StaticCast(member.Parameters[index].Type, "value"))) + block + .Assign(Bang(StaticCast(member.Parameters[index].Type, "value"))) .To(ArgumentField(index)); block.Break(); } @@ -531,7 +679,8 @@ private static void WriteArgumentsIndexer(ClassDefinition state, InterceptedMemb set.AddDefault().Throw(SystemTypes.ArgumentOutOfRangeException, "nameof(index)"); } - private static void WriteNameAt(ClassDefinition state, InterceptedMemberModel member) { + private static void WriteNameAt(ClassDefinition state, InterceptedMemberModel member) + { var nameAt = state.AddMethod("NameAt"); nameAt.Modifiers |= ComponentModifier.Public | ComponentModifier.Override; @@ -540,7 +689,8 @@ private static void WriteNameAt(ClassDefinition state, InterceptedMemberModel me var switchBlock = nameAt.Switch("index"); - for (var index = 0; index < member.Parameters.Count; index++) { + for (var index = 0; index < member.Parameters.Count; index++) + { switchBlock.AddCase(index).Return(QuoteString(member.Parameters[index].Name)); } @@ -552,21 +702,29 @@ private static void WriteNameAt(ClassDefinition state, InterceptedMemberModel me /// rather than being held here, which is what lets an interceptor proceed more than once. /// private static void WriteInvoke( - ClassDefinition state, InterceptorModel model, InterceptedMemberModel member, string wrapperName) { - - var (returnType, contextType, interceptMethod) = member.Kind switch { + ClassDefinition state, + InterceptorModel model, + InterceptedMemberModel member, + string wrapperName + ) + { + var (returnType, contextType, interceptMethod) = member.Kind switch + { InterceptorKind.Async => ( SystemTypes.ValueTask(member.ResultType), Interception.AsyncInvocationContext(member.ResultType), - "InterceptAsync"), + "InterceptAsync" + ), InterceptorKind.Stream => ( SystemTypes.AsyncEnumerable(member.ResultType), Interception.StreamInvocationContext(member.ResultType), - "InterceptStream"), + "InterceptStream" + ), _ => ( member.ResultType, Interception.InvocationContext(member.ResultType), - "Intercept") + "Intercept" + ), }; var invoke = state.AddMethod("Invoke"); @@ -582,21 +740,28 @@ private static void WriteInvoke( // it. Proceed() walks stage + 1, so the numbering has to be contiguous. var stage = 0; - for (var index = 0; index < model.Interceptors.Count; index++) { - if (!model.Interceptors[index].CanServe(member.Kind)) { + for (var index = 0; index < model.Interceptors.Count; index++) + { + if (!model.Interceptors[index].CanServe(member.Kind)) + { continue; } - switchBlock.AddCase(stage).Return( - CodeOutputComponent.Get($"_self.{InterceptorField(index)}") - .Invoke(interceptMethod, New(contextType, "this", stage))); + switchBlock + .AddCase(stage) + .Return( + CodeOutputComponent + .Get($"_self.{InterceptorField(index)}") + .Invoke(interceptMethod, New(contextType, "this", stage)) + ); stage++; } var last = switchBlock.AddDefault(); - switch (member.ReturnShape) { + switch (member.ReturnShape) + { case ReturnShape.Void: last.AddIndentedStatement(InnerCall(member)); last.NewLine(); @@ -620,7 +785,8 @@ private static void WriteInvoke( // A task with no result has to be awaited to be turned into a NoResult, and an await needs a // method to sit in. Left un-configured so the interceptor's continuation resumes on the // context it started on, the way a hand-written decorator would. - if (member.ReturnShape is ReturnShape.Task or ReturnShape.ValueTask) { + if (member.ReturnShape is ReturnShape.Task or ReturnShape.ValueTask) + { var inner = state.AddMethod("DmInvokeInner"); inner.Modifiers |= ComponentModifier.Private | ComponentModifier.Async; @@ -650,24 +816,28 @@ private static void WriteInvoke( /// The last stage of a pipeline reads the arguments off the state; a member nothing intercepts /// has no state and passes on the parameters it was handed. /// - private static string InnerCall(InterceptedMemberModel member, string? passThroughTarget = null) { + private static string InnerCall(InterceptedMemberModel member, string? passThroughTarget = null) + { var target = passThroughTarget ?? $"_self.{InnerField}"; var last = member.Parameters.Count - 1; string Argument(int index) => passThroughTarget == null ? ArgumentField(index) : member.Parameters[index].Identifier; - string Arguments(int start, int end) { + string Arguments(int start, int end) + { var arguments = new List(); - for (var index = start; index < end; index++) { + for (var index = start; index < end; index++) + { arguments.Add(Argument(index)); } return string.Join(", ", arguments); } - switch (member.Form) { + switch (member.Form) + { case AccessorForm.PropertyGet: return $"{target}.{member.Identifier}"; @@ -688,26 +858,36 @@ string Arguments(int start, int end) { return $"{target}.{member.Identifier} -= {Argument(0)}"; default: - var typeArguments = member.TypeParameters.Count == 0 - ? "" - : "<" + string.Join(", ", member.TypeParameters.Select(parameter => parameter.Name)) + ">"; - - return $"{target}.{member.Identifier}{typeArguments}" + - $"({Arguments(0, member.Parameters.Count)})"; + var typeArguments = + member.TypeParameters.Count == 0 + ? "" + : "<" + + string.Join( + ", ", + member.TypeParameters.Select(parameter => parameter.Name) + ) + + ">"; + + return $"{target}.{member.Identifier}{typeArguments}" + + $"({Arguments(0, member.Parameters.Count)})"; } } /// /// Whether any of the service's interceptors can be placed around this member. /// - private static bool IsIntercepted(InterceptorModel model, InterceptedMemberModel member) { + private static bool IsIntercepted(InterceptorModel model, InterceptedMemberModel member) + { // Left out by [Intercept].Members. Still forwarded below, just not through the chain. - if (member.Excluded) { + if (member.Excluded) + { return false; } - foreach (var interceptor in model.Interceptors) { - if (interceptor.CanServe(member.Kind)) { + foreach (var interceptor in model.Interceptors) + { + if (interceptor.CanServe(member.Kind)) + { return true; } } @@ -721,17 +901,18 @@ private static bool IsIntercepted(InterceptorModel model, InterceptedMemberModel /// The state class is constructed closed over the member's type parameters, since a nested type /// cannot close over a method's. /// - private static string ClosedStateName(InterceptedMemberModel member, int index) { - if (member.TypeParameters.Count == 0) { + private static string ClosedStateName(InterceptedMemberModel member, int index) + { + if (member.TypeParameters.Count == 0) + { return StateName(index); } - return $"{StateName(index)}<" + - string.Join(", ", member.TypeParameters.Select(parameter => parameter.Name)) + - ">"; + return $"{StateName(index)}<" + + string.Join(", ", member.TypeParameters.Select(parameter => parameter.Name)) + + ">"; } - private static string InterceptorField(int index) => $"_dmInterceptor{index}"; private static string CallerField(int index) => $"_dmCaller{index}"; diff --git a/src/DependencyModules.SourceGenerator.Impl/InterceptorRegistrationWriter.cs b/src/DependencyModules.SourceGenerator.Impl/InterceptorRegistrationWriter.cs index 94c6ac0..b4d8c96 100644 --- a/src/DependencyModules.SourceGenerator.Impl/InterceptorRegistrationWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/InterceptorRegistrationWriter.cs @@ -1,6 +1,6 @@ -using DependencyModules.SourceGenerator.Impl.Utilities; using CSharpAuthor; using DependencyModules.SourceGenerator.Impl.Models; +using DependencyModules.SourceGenerator.Impl.Utilities; using static CSharpAuthor.SyntaxHelpers; namespace DependencyModules.SourceGenerator.Impl; @@ -9,13 +9,14 @@ namespace DependencyModules.SourceGenerator.Impl; /// Registers each generated wrapper as a decorator of the service it intercepts, and registers the /// interceptors themselves so the wrapper can be constructed. /// -public class InterceptorRegistrationWriter { - +public class InterceptorRegistrationWriter +{ public string Write( ModuleEntryPointModel entryPointModel, DependencyModuleConfigurationModel configurationModel, - IReadOnlyList models) { - + IReadOnlyList models + ) + { var csharpFile = new CSharpFileDefinition(entryPointModel.EntryPointType.Namespace); var classDefinition = csharpFile.AddClass(entryPointModel.EntryPointType.Name); @@ -25,14 +26,18 @@ public string Write( // else, so the namespace is asked for by name; Global mode derives no usings on its own. classDefinition.AddUsingNamespace("Microsoft.Extensions.DependencyInjection"); - for (var i = 0; i < models.Count; i++) { + for (var i = 0; i < models.Count; i++) + { WriteInterceptor(entryPointModel, classDefinition, models[i], i, configurationModel); } - var outputContext = new OutputContext(new OutputContextOptions { - TypeOutputMode = TypeOutputMode.Global, - BraceStyle = configurationModel.GeneratedCodeStyle - }); + var outputContext = new OutputContext( + new OutputContextOptions + { + TypeOutputMode = TypeOutputMode.Global, + BraceStyle = configurationModel.GeneratedCodeStyle, + } + ); csharpFile.WriteOutput(outputContext); @@ -48,15 +53,21 @@ public string Write( /// parameter names instead is CS0246, because no T is in scope at the registration. /// Blank-named arguments are how this codebase represents unbound throughout. /// - private static ITypeDefinition Unbound(ITypeDefinition type, int arity) { + private static ITypeDefinition Unbound(ITypeDefinition type, int arity) + { var arguments = new ITypeDefinition[arity]; - for (var i = 0; i < arity; i++) { + for (var i = 0; i < arity; i++) + { arguments[i] = TypeDefinition.Get("", ""); } return new GenericTypeDefinition( - TypeDefinitionEnum.ClassDefinition, type.Namespace, type.Name, arguments); + TypeDefinitionEnum.ClassDefinition, + type.Namespace, + type.Name, + arguments + ); } private static void WriteInterceptor( @@ -64,19 +75,25 @@ private static void WriteInterceptor( ClassDefinition classDefinition, InterceptorModel model, int index, - DependencyModuleConfigurationModel configurationModel) { - + DependencyModuleConfigurationModel configurationModel + ) + { var methodName = $"ApplyInterceptor{index}"; var method = classDefinition.AddMethod(methodName); method.Modifiers |= ComponentModifier.Private | ComponentModifier.Static; - if (configurationModel.ExcludeGeneratedCodeFromCoverage) { - method.AddAttribute(TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverage")); + if (configurationModel.ExcludeGeneratedCodeFromCoverage) + { + method.AddAttribute( + TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverage") + ); } var services = method.AddParameter( - KnownTypes.Microsoft.DependencyInjection.IServiceCollection, "services"); + KnownTypes.Microsoft.DependencyInjection.IServiceCollection, + "services" + ); // Each interceptor is registered as itself, not as IInterceptor. Registering the shared // interface instead made every interceptor visible to every wrapper, and two services with @@ -87,8 +104,10 @@ private static void WriteInterceptor( // theirs is the one already in the collection. var registered = new HashSet(); - foreach (var interceptor in model.Interceptors) { - if (!registered.Add(interceptor.Type)) { + foreach (var interceptor in model.Interceptors) + { + if (!registered.Add(interceptor.Type)) + { continue; } @@ -96,10 +115,13 @@ private static void WriteInterceptor( new StaticInvokeStatement( KnownTypes.Microsoft.DependencyInjection.ServiceCollectionDescriptorExtensions, TryAddMethodFor(interceptor.Lifestyle), - new List { + new List + { CodeOutputComponent.Get(services.Name), - TypeOf(interceptor.Type) - })); + TypeOf(interceptor.Type), + } + ) + ); } var wrapperName = $"{model.ImplementationType.Name.Replace(".", "_")}_Intercepted"; @@ -107,7 +129,8 @@ private static void WriteInterceptor( method.NewLine(); - if (model.IsOpenGeneric) { + if (model.IsOpenGeneric) + { // An open generic service cannot be decorated: decoration rewrites the registration into // a factory, and the container refuses a factory for one. It does accept an open generic // implementation type, and the wrapper is one — so the registration is swapped for the @@ -120,23 +143,33 @@ private static void WriteInterceptor( new StaticInvokeStatement( KnownTypes.DependencyModules.Helpers.DecoratorHelper, "InterceptOpenGeneric", - new List { + new List + { CodeOutputComponent.Get(services.Name), TypeOf(Unbound(model.ServiceType, model.TypeParameters!.Count)), TypeOf(Unbound(model.ImplementationType, model.TypeParameters!.Count)), - TypeOf(Unbound(wrapperType, model.TypeParameters!.Count)) - })); - } else { + TypeOf(Unbound(wrapperType, model.TypeParameters!.Count)), + } + ) + ); + } + else + { // The wrapper is generated right here, so its constructor is known exactly: the // intercepted instance, then one parameter per interceptor. Emitting the `new` rather than // handing the type to ActivatorUtilities is what keeps interception working in a published // Native AOT application — the same reason decorators are emitted closed. var arguments = new List { CodeOutputComponent.Get("inner") }; - for (var i = 0; i < model.Interceptors.Count; i++) { + for (var i = 0; i < model.Interceptors.Count; i++) + { arguments.Add( new InvokeGenericDefinition( - "provider", "GetRequiredService", new[] { model.Interceptors[i].Type })); + "provider", + "GetRequiredService", + new[] { model.Interceptors[i].Type } + ) + ); } // The implementation is named so the rewrite lands only on the registration this @@ -154,8 +187,11 @@ private static void WriteInterceptor( new WrapStatement( CodeOutputComponent.Get(" => "), CodeOutputComponent.Get("(provider, inner)"), - New(wrapperType, arguments.ToArray())), - TypeOf(model.ImplementationType))); + New(wrapperType, arguments.ToArray()) + ), + TypeOf(model.ImplementationType) + ) + ); } // A field initializer registers the method, matching how decorator registrations are hooked @@ -164,22 +200,27 @@ private static void WriteInterceptor( field.Modifiers |= ComponentModifier.Private | ComponentModifier.Static; field.AddAttribute( TypeDefinition.Get("System.Diagnostics.CodeAnalysis", "DynamicDependency"), - $"nameof({methodName})"); + $"nameof({methodName})" + ); var registryType = new GenericTypeDefinition( TypeDefinitionEnum.ClassDefinition, KnownTypes.DependencyModules.Helpers.Namespace, "DependencyRegistry", - new[] { entryPointModel.EntryPointType }); + new[] { entryPointModel.EntryPointType } + ); field.InitializeValue = new StaticInvokeStatement( registryType, "AddDecorator", - new List { + new List + { CodeOutputComponent.Get(methodName), - CodeOutputComponent.Get(model.Order.ToString()) - }) { - Indented = false + CodeOutputComponent.Get(model.Order.ToString()), + } + ) + { + Indented = false, }; } @@ -192,9 +233,10 @@ private static void WriteInterceptor( /// registration is already in the collection by the time this runs. /// private static string TryAddMethodFor(ServiceLifestyle lifestyle) => - lifestyle switch { + lifestyle switch + { ServiceLifestyle.Scoped => "TryAddScoped", ServiceLifestyle.Transient => "TryAddTransient", - _ => "TryAddSingleton" + _ => "TryAddSingleton", }; } diff --git a/src/DependencyModules.SourceGenerator.Impl/KnownTypes.cs b/src/DependencyModules.SourceGenerator.Impl/KnownTypes.cs index 1267e7f..e536fe7 100644 --- a/src/DependencyModules.SourceGenerator.Impl/KnownTypes.cs +++ b/src/DependencyModules.SourceGenerator.Impl/KnownTypes.cs @@ -3,23 +3,37 @@ namespace DependencyModules.SourceGenerator.Impl; // ReSharper disable InconsistentNaming -public static class KnownTypes { - public static class Microsoft { - public static class DependencyInjection { - +public static class KnownTypes +{ + public static class Microsoft + { + public static class DependencyInjection + { public const string Namespace = "Microsoft.Extensions.DependencyInjection"; - public static readonly ITypeDefinition IServiceCollection = - TypeDefinition.Get(TypeDefinitionEnum.InterfaceDefinition, Namespace, "IServiceCollection"); - - public static readonly ITypeDefinition IServiceProvider = - TypeDefinition.Get(TypeDefinitionEnum.InterfaceDefinition, "System", "IServiceProvider"); - - public static readonly ITypeDefinition ServiceDescriptor = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "ServiceDescriptor"); - - public static readonly ITypeDefinition FromKeyedServicesAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "FromKeyedServicesAttribute"); + public static readonly ITypeDefinition IServiceCollection = TypeDefinition.Get( + TypeDefinitionEnum.InterfaceDefinition, + Namespace, + "IServiceCollection" + ); + + public static readonly ITypeDefinition IServiceProvider = TypeDefinition.Get( + TypeDefinitionEnum.InterfaceDefinition, + "System", + "IServiceProvider" + ); + + public static readonly ITypeDefinition ServiceDescriptor = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "ServiceDescriptor" + ); + + public static readonly ITypeDefinition FromKeyedServicesAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "FromKeyedServicesAttribute" + ); /// /// Home of the TryAdd family. Invoked statically so the generated file needs no using. @@ -28,48 +42,82 @@ public static class DependencyInjection { TypeDefinition.Get( TypeDefinitionEnum.ClassDefinition, Namespace + ".Extensions", - "ServiceCollectionDescriptorExtensions"); + "ServiceCollectionDescriptorExtensions" + ); } - - public static class TextJson { + + public static class TextJson + { public const string Namespace = "System.Text.Json.Serialization"; - + public static readonly ITypeDefinition JsonSourceGenerationOptionsAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "JsonSourceGenerationOptionsAttribute"); - - public static readonly ITypeDefinition IJsonTypeInfoResolver = - TypeDefinition.Get(TypeDefinitionEnum.InterfaceDefinition, Namespace + ".Metadata", "IJsonTypeInfoResolver"); + TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "JsonSourceGenerationOptionsAttribute" + ); + + public static readonly ITypeDefinition IJsonTypeInfoResolver = TypeDefinition.Get( + TypeDefinitionEnum.InterfaceDefinition, + Namespace + ".Metadata", + "IJsonTypeInfoResolver" + ); } } - public static class DependencyModules { - public static class Attributes { + public static class DependencyModules + { + public static class Attributes + { public const string Namespace = "DependencyModules.Runtime.Attributes"; - public static readonly ITypeDefinition DependencyModuleAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "DependencyModuleAttribute"); - - public static readonly ITypeDefinition TransientServiceAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "TransientServiceAttribute"); - - public static readonly ITypeDefinition ScopedServiceAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "ScopedServiceAttribute"); - - public static readonly ITypeDefinition SingletonServiceAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "SingletonServiceAttribute"); - - public static readonly ITypeDefinition DecoratorAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "DecoratorAttribute"); - - public static readonly ITypeDefinition DecorateAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "DecorateAttribute"); - - public static readonly ITypeDefinition InterceptAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "InterceptAttribute"); - - public static readonly ITypeDefinition CrossWireServiceAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "CrossWireServiceAttribute"); - + public static readonly ITypeDefinition DependencyModuleAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "DependencyModuleAttribute" + ); + + public static readonly ITypeDefinition TransientServiceAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "TransientServiceAttribute" + ); + + public static readonly ITypeDefinition ScopedServiceAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "ScopedServiceAttribute" + ); + + public static readonly ITypeDefinition SingletonServiceAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "SingletonServiceAttribute" + ); + + public static readonly ITypeDefinition DecoratorAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "DecoratorAttribute" + ); + + public static readonly ITypeDefinition DecorateAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "DecorateAttribute" + ); + + public static readonly ITypeDefinition InterceptAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "InterceptAttribute" + ); + + public static readonly ITypeDefinition CrossWireServiceAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "CrossWireServiceAttribute" + ); } /// @@ -77,66 +125,101 @@ public static class Attributes { /// these at run time — but DM0021 is about two of them written together, and that is a /// question only a compiler can answer before the test runs. /// - public static class Testing { + public static class Testing + { public const string Namespace = "DependencyModules.Testing.Attributes"; - public static readonly ITypeDefinition MockAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "MockAttribute"); - - public static readonly ITypeDefinition TestExportAttribute = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "TestExportAttribute"); + public static readonly ITypeDefinition MockAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "MockAttribute" + ); + + public static readonly ITypeDefinition TestExportAttribute = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "TestExportAttribute" + ); } - public static class Interfaces { + public static class Interfaces + { public const string Namespace = "DependencyModules.Runtime.Interfaces"; // ReSharper disable once InconsistentNaming - public static readonly ITypeDefinition IDependencyModule = - TypeDefinition.Get(TypeDefinitionEnum.InterfaceDefinition, Namespace, "IDependencyModule"); + public static readonly ITypeDefinition IDependencyModule = TypeDefinition.Get( + TypeDefinitionEnum.InterfaceDefinition, + Namespace, + "IDependencyModule" + ); // ReSharper disable once InconsistentNaming - public static readonly ITypeDefinition IDependencyModuleProvider = - TypeDefinition.Get(TypeDefinitionEnum.InterfaceDefinition, Namespace, "IDependencyModuleProvider"); + public static readonly ITypeDefinition IDependencyModuleProvider = TypeDefinition.Get( + TypeDefinitionEnum.InterfaceDefinition, + Namespace, + "IDependencyModuleProvider" + ); // ReSharper disable once InconsistentNaming - public static readonly ITypeDefinition IModuleEnvironment = - TypeDefinition.Get(TypeDefinitionEnum.InterfaceDefinition, Namespace, "IModuleEnvironment"); + public static readonly ITypeDefinition IModuleEnvironment = TypeDefinition.Get( + TypeDefinitionEnum.InterfaceDefinition, + Namespace, + "IModuleEnvironment" + ); } - public static class Features { - + public static class Features + { public const string Namespace = "DependencyModules.Runtime.Features"; // ReSharper disable once InconsistentNaming - public static readonly ITypeDefinition IFeatureApplicator = - TypeDefinition.Get(TypeDefinitionEnum.InterfaceDefinition, Namespace, "IFeatureApplicator"); + public static readonly ITypeDefinition IFeatureApplicator = TypeDefinition.Get( + TypeDefinitionEnum.InterfaceDefinition, + Namespace, + "IFeatureApplicator" + ); // ReSharper disable once InconsistentNaming public static readonly ITypeDefinition IDependencyModuleApplicatorProvider = - TypeDefinition.Get(TypeDefinitionEnum.InterfaceDefinition, Namespace, "IDependencyModuleApplicatorProvider"); - - // ReSharper disable once InconsistentNaming - public static readonly ITypeDefinition FeatureApplicator = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "FeatureApplicator"); + TypeDefinition.Get( + TypeDefinitionEnum.InterfaceDefinition, + Namespace, + "IDependencyModuleApplicatorProvider" + ); + // ReSharper disable once InconsistentNaming + public static readonly ITypeDefinition FeatureApplicator = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "FeatureApplicator" + ); } - - public static class Helpers { + public static class Helpers + { public const string Namespace = "DependencyModules.Runtime.Helpers"; - public static readonly ITypeDefinition DecoratorHelper = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "DecoratorHelper"); + public static readonly ITypeDefinition DecoratorHelper = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "DecoratorHelper" + ); - public static readonly ITypeDefinition DecoratorRegistration = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "DecoratorRegistration"); + public static readonly ITypeDefinition DecoratorRegistration = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "DecoratorRegistration" + ); /// /// The tests a generated environment condition calls. Invoked statically, so the /// generated file needs no using. /// - public static readonly ITypeDefinition EnvironmentConditions = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "EnvironmentConditions"); + public static readonly ITypeDefinition EnvironmentConditions = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "EnvironmentConditions" + ); } /// @@ -146,14 +229,21 @@ public static class Helpers { /// The state and context types are generic over the member's result, so they are built per /// member rather than being constants. /// - public static class Interception { + public static class Interception + { public const string Namespace = "DependencyModules.Runtime.Interception"; - public static readonly ITypeDefinition CallerInfo = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "CallerInfo"); + public static readonly ITypeDefinition CallerInfo = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "CallerInfo" + ); - public static readonly ITypeDefinition NoResult = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, Namespace, "NoResult"); + public static readonly ITypeDefinition NoResult = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + Namespace, + "NoResult" + ); public static ITypeDefinition InvocationState(ITypeDefinition result) => Close("InvocationState", result); @@ -175,20 +265,36 @@ public static ITypeDefinition StreamInvocationContext(ITypeDefinition item) => private static ITypeDefinition Close(string name, ITypeDefinition argument) => new GenericTypeDefinition( - TypeDefinitionEnum.ClassDefinition, Namespace, name, new[] { argument }); + TypeDefinitionEnum.ClassDefinition, + Namespace, + name, + new[] { argument } + ); } } - public static class System { - public static readonly ITypeDefinition ArgumentOutOfRangeException = - TypeDefinition.Get(TypeDefinitionEnum.ClassDefinition, "System", "ArgumentOutOfRangeException"); + public static class System + { + public static readonly ITypeDefinition ArgumentOutOfRangeException = TypeDefinition.Get( + TypeDefinitionEnum.ClassDefinition, + "System", + "ArgumentOutOfRangeException" + ); public static ITypeDefinition ValueTask(ITypeDefinition result) => new GenericTypeDefinition( - TypeDefinitionEnum.ClassDefinition, "System.Threading.Tasks", "ValueTask", new[] { result }); + TypeDefinitionEnum.ClassDefinition, + "System.Threading.Tasks", + "ValueTask", + new[] { result } + ); public static ITypeDefinition AsyncEnumerable(ITypeDefinition item) => new GenericTypeDefinition( - TypeDefinitionEnum.InterfaceDefinition, "System.Collections.Generic", "IAsyncEnumerable", new[] { item }); + TypeDefinitionEnum.InterfaceDefinition, + "System.Collections.Generic", + "IAsyncEnumerable", + new[] { item } + ); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/AttributeModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/AttributeModel.cs index c649bfb..731806e 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/AttributeModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/AttributeModel.cs @@ -4,16 +4,17 @@ namespace DependencyModules.SourceGenerator.Impl.Models; -public record AttributeArgumentValue(string Name, object? Value) { +public record AttributeArgumentValue(string Name, object? Value) +{ // Value arrives as object and may be an array, which the compiler-generated record equality // would compare by reference. See ModelEquality for why that breaks incremental caching. public virtual bool Equals(AttributeArgumentValue? other) => - other is not null && - Name == other.Name && - ModelEquality.ValueEquals(Value, other.Value); + other is not null && Name == other.Name && ModelEquality.ValueEquals(Value, other.Value); - public override int GetHashCode() { - unchecked { + public override int GetHashCode() + { + unchecked + { return Name.GetHashCode() * 31 + ModelEquality.ValueHashCode(Value); } } @@ -23,18 +24,21 @@ public record AttributeModel( ITypeDefinition TypeDefinition, IReadOnlyList Arguments, IReadOnlyList Properties, - IReadOnlyList ImplementedInterfaces) { - + IReadOnlyList ImplementedInterfaces +) +{ // Structural equality over the list members; see ModelEquality. public virtual bool Equals(AttributeModel? other) => - other is not null && - TypeDefinition.Equals(other.TypeDefinition) && - ModelEquality.ListEquals(Arguments, other.Arguments) && - ModelEquality.ListEquals(Properties, other.Properties) && - ModelEquality.ListEquals(ImplementedInterfaces, other.ImplementedInterfaces); - - public override int GetHashCode() { - unchecked { + other is not null + && TypeDefinition.Equals(other.TypeDefinition) + && ModelEquality.ListEquals(Arguments, other.Arguments) + && ModelEquality.ListEquals(Properties, other.Properties) + && ModelEquality.ListEquals(ImplementedInterfaces, other.ImplementedInterfaces); + + public override int GetHashCode() + { + unchecked + { var hash = TypeDefinition.GetHashCode(); hash = hash * 31 + ModelEquality.ListHashCode(Arguments); hash = hash * 31 + ModelEquality.ListHashCode(Properties); @@ -43,103 +47,123 @@ public override int GetHashCode() { } } - - public IList GetArguments() { + public IList GetArguments() + { var list = new List(); - foreach (var argument in Arguments) { + foreach (var argument in Arguments) + { IOutputComponent? outputComponent = null; - if (argument.Value is IOutputComponent component) { + if (argument.Value is IOutputComponent component) + { outputComponent = component; } - else if (argument.Value is Array arrayValue) { + else if (argument.Value is Array arrayValue) + { var collectionSyntax = new CollectionSyntaxDeclaration(); - foreach (var objectValue in arrayValue) { - if (objectValue is string stringValue) { + foreach (var objectValue in arrayValue) + { + if (objectValue is string stringValue) + { // Raw: CollectionSyntaxDeclaration quotes strings itself. Quoting here too // produced ""a"" under 1.x's naive QuoteString and "\"a\"" under 2.0's // escaping one - both wrong, only the second visibly so. collectionSyntax.Add(stringValue); } - else if (argument.Value is ITypeDefinition typeDefinition) { + else if (argument.Value is ITypeDefinition typeDefinition) + { outputComponent = SyntaxHelpers.TypeOf(typeDefinition); collectionSyntax.Add(outputComponent); } - else if (objectValue is not null) { + else if (objectValue is not null) + { collectionSyntax.Add(CodeOutputComponent.Get(objectValue)); } } - + outputComponent = collectionSyntax; } - else if (argument.Value is string stringValue) { - outputComponent = CodeOutputComponent.Get( - SyntaxHelpers.QuoteString(stringValue) - ); - } - else if (argument.Value is ITypeDefinition typeDefinition) { + else if (argument.Value is string stringValue) + { + outputComponent = CodeOutputComponent.Get(SyntaxHelpers.QuoteString(stringValue)); + } + else if (argument.Value is ITypeDefinition typeDefinition) + { outputComponent = SyntaxHelpers.TypeOf(typeDefinition); } - else if (argument.Value is not null) { + else if (argument.Value is not null) + { outputComponent = CodeOutputComponent.Get(argument.Value); } - if (outputComponent != null) { + if (outputComponent != null) + { list.Add(outputComponent); } } return list; } - - public IList PropertyValues() { + public IList PropertyValues() + { var list = new List(); - foreach (var argument in Properties) { - + foreach (var argument in Properties) + { IOutputComponent? outputComponent = null; - if (argument.Value is IOutputComponent component) { + if (argument.Value is IOutputComponent component) + { outputComponent = component; } - else if (argument.Value is Array arrayValue) { + else if (argument.Value is Array arrayValue) + { var collectionSyntax = new CollectionSyntaxDeclaration(); - foreach (var objectValue in arrayValue) { - if (objectValue is string stringValue) { + foreach (var objectValue in arrayValue) + { + if (objectValue is string stringValue) + { collectionSyntax.Add(stringValue); } - else if (argument.Value is ITypeDefinition typeDefinition) { + else if (argument.Value is ITypeDefinition typeDefinition) + { outputComponent = SyntaxHelpers.TypeOf(typeDefinition); collectionSyntax.Add(outputComponent); } - else if (objectValue is not null) { + else if (objectValue is not null) + { collectionSyntax.Add(CodeOutputComponent.Get(objectValue)); } } - - outputComponent = collectionSyntax; + + outputComponent = collectionSyntax; } - else if (argument.Value is string stringValue) { + else if (argument.Value is string stringValue) + { outputComponent = CodeOutputComponent.Get( SyntaxHelpers.QuoteString(stringValue.Trim('"')) ); } - else if (argument.Value is ITypeDefinition typeDefinition) { + else if (argument.Value is ITypeDefinition typeDefinition) + { outputComponent = SyntaxHelpers.TypeOf(typeDefinition); } - else if (argument.Value is not null) { - + else if (argument.Value is not null) + { outputComponent = CodeOutputComponent.Get(argument.Value); } - - if (outputComponent != null) { + + if (outputComponent != null) + { list.Add( new WrapStatement( CodeOutputComponent.Get(" = "), CodeOutputComponent.Get(argument.Name), - outputComponent)); + outputComponent + ) + ); } } @@ -149,4 +173,5 @@ public IList PropertyValues() { public record AttributeClassInfo( ConstructorInfoModel ConstructorInfo, - IReadOnlyList Properties); \ No newline at end of file + IReadOnlyList Properties +); diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/ConstructorInfoModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/ConstructorInfoModel.cs index 4ab6086..dd1f4be 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/ConstructorInfoModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/ConstructorInfoModel.cs @@ -1,10 +1,10 @@ namespace DependencyModules.SourceGenerator.Impl.Models; -public record ConstructorInfoModel(IReadOnlyList Parameters) { - +public record ConstructorInfoModel(IReadOnlyList Parameters) +{ // Structural equality over Parameters; see ModelEquality. public virtual bool Equals(ConstructorInfoModel? other) => other is not null && ModelEquality.ListEquals(Parameters, other.Parameters); public override int GetHashCode() => ModelEquality.ListHashCode(Parameters); -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/DecoratorModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/DecoratorModel.cs index 642937d..7913802 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/DecoratorModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/DecoratorModel.cs @@ -43,20 +43,19 @@ public record DecoratorModel( ConstructorInfoModel? Constructor = null, int InnerParameterIndex = -1, bool TypeParametersMatchService = true, - /// /// The one implementation this decorator wraps, or null to wrap every registration of the /// service — which is the default and what a decorator declared against an interface means. /// ITypeDefinition? Implementation = null, - /// /// Where the decorator was declared, so DM0007 and DM0013 can point at it rather than at the /// project. Null for a decorator declared through [Decorate] on a module, which names two types /// and has no declaration of its own to point at. /// - LocationModel? Location = null) { - + LocationModel? Location = null +) +{ /// /// Whether the decorator can be constructed by generated code. /// @@ -87,8 +86,8 @@ public record DecoratorModel( /// mode this generator is built never to produce. /// public bool HasUnboundServiceType => - ServiceType is GenericTypeDefinition generic && - generic.TypeArguments.Any(argument => string.IsNullOrEmpty(argument.Name)); + ServiceType is GenericTypeDefinition generic + && generic.TypeArguments.Any(argument => string.IsNullOrEmpty(argument.Name)); /// /// Sentinel for a syntax node that carried the attribute but produced no usable model, matching @@ -98,7 +97,8 @@ ServiceType is GenericTypeDefinition generic && TypeDefinition.Get("", "Ignore"), TypeDefinition.Get("", "Ignore"), 0, - null); + null + ); public bool IsIgnored => ReferenceEquals(this, Ignore); } @@ -107,39 +107,45 @@ ServiceType is GenericTypeDefinition generic && /// Equality for the incremental pipeline. Every field affects generated output, so all of them are /// compared; missing one would serve stale output after an edit to it. /// -public class DecoratorModelComparer : IEqualityComparer { - - public bool Equals(DecoratorModel? x, DecoratorModel? y) { - if (ReferenceEquals(x, y)) { +public class DecoratorModelComparer : IEqualityComparer +{ + public bool Equals(DecoratorModel? x, DecoratorModel? y) + { + if (ReferenceEquals(x, y)) + { return true; } - if (x is null || y is null) { + if (x is null || y is null) + { return false; } - return x.Order == y.Order && - x.ServiceType.Equals(y.ServiceType) && - x.DecoratorType.Equals(y.DecoratorType) && - Equals(x.Realm, y.Realm) && - // Decides which registration is wrapped, so leaving it out would serve the previous - // emission when only Implementation changed. - Equals(x.Implementation, y.Implementation) && - x.InnerParameterIndex == y.InnerParameterIndex && - x.TypeParametersMatchService == y.TypeParametersMatchService && - Equals(x.Constructor, y.Constructor) && - ConditionsEqual(x.Conditions, y.Conditions); + return x.Order == y.Order + && x.ServiceType.Equals(y.ServiceType) + && x.DecoratorType.Equals(y.DecoratorType) + && Equals(x.Realm, y.Realm) + && + // Decides which registration is wrapped, so leaving it out would serve the previous + // emission when only Implementation changed. + Equals(x.Implementation, y.Implementation) + && x.InnerParameterIndex == y.InnerParameterIndex + && x.TypeParametersMatchService == y.TypeParametersMatchService + && Equals(x.Constructor, y.Constructor) + && ConditionsEqual(x.Conditions, y.Conditions); } // Structural rather than by reference: two runs build separate lists, so comparing references // would miss the cache on every keystroke and re-emit every decorator. private static bool ConditionsEqual( IReadOnlyList? x, - IReadOnlyList? y) => - (x?.Count ?? 0) == 0 && (y?.Count ?? 0) == 0 || ModelEquality.ListEquals(x, y); + IReadOnlyList? y + ) => (x?.Count ?? 0) == 0 && (y?.Count ?? 0) == 0 || ModelEquality.ListEquals(x, y); - public int GetHashCode(DecoratorModel obj) { - unchecked { + public int GetHashCode(DecoratorModel obj) + { + unchecked + { var hash = obj.ServiceType.GetHashCode(); hash = hash * 31 + obj.DecoratorType.GetHashCode(); hash = hash * 31 + obj.Order; diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/DependencyModuleConfigurationModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/DependencyModuleConfigurationModel.cs index fb4327e..351da7f 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/DependencyModuleConfigurationModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/DependencyModuleConfigurationModel.cs @@ -2,7 +2,8 @@ namespace DependencyModules.SourceGenerator.Impl.Models; -public enum LogOutputLevel { +public enum LogOutputLevel +{ Debug = 1, Info = 2, Warning = 3, @@ -27,27 +28,33 @@ public record DependencyModuleConfigurationModel( BraceStyle GeneratedCodeStyle = BraceStyle.Allman ); -public class DependencyModuleConfigurationModelComparer : - IEqualityComparer { - - public bool Equals(DependencyModuleConfigurationModel? x, DependencyModuleConfigurationModel? y) { - if (ReferenceEquals(x, y)) return true; - if (x is null) return false; - if (y is null) return false; - if (x.GetType() != y.GetType()) return false; - return x.RegistrationType == y.RegistrationType && - x.RegisterSourceGenerator == y.RegisterSourceGenerator && - x.RootNamespace == y.RootNamespace && - x.ProjectDir == y.ProjectDir && - x.LogOutputFolder == y.LogOutputFolder && - x.AutoGenerateEntry == y.AutoGenerateEntry && - x.LogOutputLevel == y.LogOutputLevel && - x.GenerateFactories == y.GenerateFactories && - x.ExcludeGeneratedCodeFromCoverage == y.ExcludeGeneratedCodeFromCoverage && - x.GeneratedCodeStyle == y.GeneratedCodeStyle; +public class DependencyModuleConfigurationModelComparer + : IEqualityComparer +{ + public bool Equals(DependencyModuleConfigurationModel? x, DependencyModuleConfigurationModel? y) + { + if (ReferenceEquals(x, y)) + return true; + if (x is null) + return false; + if (y is null) + return false; + if (x.GetType() != y.GetType()) + return false; + return x.RegistrationType == y.RegistrationType + && x.RegisterSourceGenerator == y.RegisterSourceGenerator + && x.RootNamespace == y.RootNamespace + && x.ProjectDir == y.ProjectDir + && x.LogOutputFolder == y.LogOutputFolder + && x.AutoGenerateEntry == y.AutoGenerateEntry + && x.LogOutputLevel == y.LogOutputLevel + && x.GenerateFactories == y.GenerateFactories + && x.ExcludeGeneratedCodeFromCoverage == y.ExcludeGeneratedCodeFromCoverage + && x.GeneratedCodeStyle == y.GeneratedCodeStyle; } - public int GetHashCode(DependencyModuleConfigurationModel obj) { + public int GetHashCode(DependencyModuleConfigurationModel obj) + { return obj.GetHashCode(); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/DiagnosticReporter.cs b/src/DependencyModules.SourceGenerator.Impl/Models/DiagnosticReporter.cs index ffb8c70..5d83e3f 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/DiagnosticReporter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/DiagnosticReporter.cs @@ -21,14 +21,16 @@ namespace DependencyModules.SourceGenerator.Impl.Models; /// first lets a caller skip building a message it is about to discard. /// /// -public sealed class DiagnosticReporter { +public sealed class DiagnosticReporter +{ private readonly Action? _sink; private readonly SyntaxTreeLookup _lookup; /// A reporter that discards everything. public static readonly DiagnosticReporter Silent = new(null, SyntaxTreeLookup.None); - public DiagnosticReporter(Action? sink, SyntaxTreeLookup lookup) { + public DiagnosticReporter(Action? sink, SyntaxTreeLookup lookup) + { _sink = sink; _lookup = lookup; } @@ -36,22 +38,36 @@ public DiagnosticReporter(Action? sink, SyntaxTreeLookup lookup) { /// True when nothing is listening, so there is no point composing a message. public bool IsSilent => _sink == null; - public void Report(DiagnosticDescriptor descriptor, LocationModel? location, params object?[] messageArgs) { - if (_sink == null) { + public void Report( + DiagnosticDescriptor descriptor, + LocationModel? location, + params object?[] messageArgs + ) + { + if (_sink == null) + { return; } - _sink(Diagnostic.Create( - descriptor, - location?.ToLocationOrNone(_lookup) ?? Location.None, - messageArgs)); + _sink( + Diagnostic.Create( + descriptor, + location?.ToLocationOrNone(_lookup) ?? Location.None, + messageArgs + ) + ); } /// /// Reports at a location that is already resolved — one read straight from syntax rather than /// carried through a model. /// - public void Report(DiagnosticDescriptor descriptor, Location location, params object?[] messageArgs) { + public void Report( + DiagnosticDescriptor descriptor, + Location location, + params object?[] messageArgs + ) + { _sink?.Invoke(Diagnostic.Create(descriptor, location, messageArgs)); } diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/EnvironmentConditionModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/EnvironmentConditionModel.cs index 386b8d8..5aa92c5 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/EnvironmentConditionModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/EnvironmentConditionModel.cs @@ -3,7 +3,8 @@ namespace DependencyModules.SourceGenerator.Impl.Models; /// /// What an environment condition tests. /// -public enum EnvironmentConditionKind { +public enum EnvironmentConditionKind +{ /// /// The environment's name, against one or more accepted names. /// @@ -41,18 +42,21 @@ public record EnvironmentConditionModel( EnvironmentConditionKind Kind, bool Negate, string? Key, - IReadOnlyList Values) { - + IReadOnlyList Values +) +{ // Structural equality over Values; see ModelEquality. public virtual bool Equals(EnvironmentConditionModel? other) => - other is not null && - Kind == other.Kind && - Negate == other.Negate && - Key == other.Key && - ModelEquality.ListEquals(Values, other.Values); + other is not null + && Kind == other.Kind + && Negate == other.Negate + && Key == other.Key + && ModelEquality.ListEquals(Values, other.Values); - public override int GetHashCode() { - unchecked { + public override int GetHashCode() + { + unchecked + { var hash = (int)Kind; hash = hash * 31 + Negate.GetHashCode(); hash = hash * 31 + (Key?.GetHashCode() ?? 0); diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/IClassModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/IClassModel.cs index 23d226e..cdc601d 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/IClassModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/IClassModel.cs @@ -2,10 +2,11 @@ namespace DependencyModules.SourceGenerator.Impl.Models; -public interface IClassModel { +public interface IClassModel +{ ITypeDefinition ClassType { get; } - + IReadOnlyList Parameters { get; } IReadOnlyList PropertyInfoModels { get; } IReadOnlyList AttributeModels { get; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/InterceptedMemberKinds.cs b/src/DependencyModules.SourceGenerator.Impl/Models/InterceptedMemberKinds.cs index 7b088a3..700d5e1 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/InterceptedMemberKinds.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/InterceptedMemberKinds.cs @@ -9,11 +9,12 @@ namespace DependencyModules.SourceGenerator.Impl.Models; /// namespace rather than as a type. /// [Flags] -public enum InterceptedMemberKinds { +public enum InterceptedMemberKinds +{ None = 0, Methods = 1, Properties = 2, Indexers = 4, Events = 8, - All = Methods | Properties | Indexers | Events + All = Methods | Properties | Indexers | Events, } diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/InterceptorModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/InterceptorModel.cs index c1a0de1..ad2aff8 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/InterceptorModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/InterceptorModel.cs @@ -11,14 +11,15 @@ namespace DependencyModules.SourceGenerator.Impl.Models; /// The return type is known at compile time, so the right shape is emitted per member instead of /// being sniffed at run time. /// -public enum ReturnShape { +public enum ReturnShape +{ Void, Value, Task, TaskOfValue, ValueTask, ValueTaskOfValue, - AsyncEnumerable + AsyncEnumerable, } /// @@ -29,10 +30,11 @@ public enum ReturnShape { /// one has nowhere to await inside a sync member. A stream is not awaitable at all, and wrapping it /// as a plain value would time the construction of the iterator rather than the work. /// -public enum InterceptorKind { +public enum InterceptorKind +{ Sync, Async, - Stream + Stream, } /// @@ -48,14 +50,15 @@ public record InterceptionRefusal(string Message); /// An accessor is invoked by its syntax, not by its name: the CLR calls it get_Count and /// reports it that way, but the call has to be written as Count. /// -public enum AccessorForm { +public enum AccessorForm +{ Method, PropertyGet, PropertySet, IndexerGet, IndexerSet, EventAdd, - EventRemove + EventRemove, } /// @@ -83,7 +86,8 @@ public record InterceptedParameterModel( string Identifier, ITypeDefinition Type, string? DefaultValue, - bool IsParams = false); + bool IsParams = false +); /// /// One type parameter of an intercepted member, with the constraints the wrapper has to repeat. @@ -114,22 +118,25 @@ public record TypeParameterModel( string Name, string? Primary, IReadOnlyList ConstraintTypes, - bool DefaultConstructor) { - + bool DefaultConstructor +) +{ /// /// Structural equality over the constraint types, which the compiler-generated version compares /// by reference — two identical models built on consecutive runs would never match, and the /// incremental cache would miss on every keystroke. /// public virtual bool Equals(TypeParameterModel? other) => - other is not null && - Name == other.Name && - Primary == other.Primary && - DefaultConstructor == other.DefaultConstructor && - ModelEquality.ListEquals(ConstraintTypes, other.ConstraintTypes); - - public override int GetHashCode() { - unchecked { + other is not null + && Name == other.Name + && Primary == other.Primary + && DefaultConstructor == other.DefaultConstructor + && ModelEquality.ListEquals(ConstraintTypes, other.ConstraintTypes); + + public override int GetHashCode() + { + unchecked + { var hash = Name.GetHashCode(); hash = hash * 31 + (Primary?.GetHashCode() ?? 0); @@ -157,17 +164,19 @@ public record InterceptorTypeModel( /// /// The lifetime this interceptor is registered with, from the [Intercept] that named it. /// - ServiceLifestyle Lifestyle = ServiceLifestyle.Singleton) { - + ServiceLifestyle Lifestyle = ServiceLifestyle.Singleton +) +{ /// /// Whether this interceptor can be placed around a member of the given kind. /// public bool CanServe(InterceptorKind kind) => - kind switch { + kind switch + { InterceptorKind.Sync => Sync, InterceptorKind.Async => Async, InterceptorKind.Stream => Stream, - _ => false + _ => false, }; } @@ -204,49 +213,57 @@ public record InterceptedMemberModel( IReadOnlyList Parameters, IReadOnlyList TypeParameters, ReturnShape ReturnShape, - /// /// The declaration left out by [Intercept].Members. Still forwarded — the wrapper /// implements the whole interface — but not through the interceptor chain. /// - bool Excluded = false) { - + bool Excluded = false +) +{ /// /// The interceptor interface this member has to be routed through. /// public InterceptorKind Kind => - ReturnShape switch { - ReturnShape.Task or ReturnShape.TaskOfValue or - ReturnShape.ValueTask or ReturnShape.ValueTaskOfValue => InterceptorKind.Async, + ReturnShape switch + { + ReturnShape.Task + or ReturnShape.TaskOfValue + or ReturnShape.ValueTask + or ReturnShape.ValueTaskOfValue => InterceptorKind.Async, ReturnShape.AsyncEnumerable => InterceptorKind.Stream, - _ => InterceptorKind.Sync + _ => InterceptorKind.Sync, }; /// /// Structural equality, because the compiler-generated version compares the two lists by /// reference and two identical models built on consecutive runs would never match. /// - public virtual bool Equals(InterceptedMemberModel? other) { - if (ReferenceEquals(this, other)) { + public virtual bool Equals(InterceptedMemberModel? other) + { + if (ReferenceEquals(this, other)) + { return true; } - if (other is null) { + if (other is null) + { return false; } - return Name == other.Name && - Identifier == other.Identifier && - Form == other.Form && - Equals(ReturnType, other.ReturnType) && - ResultType.Equals(other.ResultType) && - ReturnShape == other.ReturnShape && - ModelEquality.ListEquals(Parameters, other.Parameters) && - ModelEquality.ListEquals(TypeParameters, other.TypeParameters); + return Name == other.Name + && Identifier == other.Identifier + && Form == other.Form + && Equals(ReturnType, other.ReturnType) + && ResultType.Equals(other.ResultType) + && ReturnShape == other.ReturnShape + && ModelEquality.ListEquals(Parameters, other.Parameters) + && ModelEquality.ListEquals(TypeParameters, other.TypeParameters); } - public override int GetHashCode() { - unchecked { + public override int GetHashCode() + { + unchecked + { var hash = Name.GetHashCode(); hash = hash * 31 + Identifier.GetHashCode(); @@ -269,11 +286,12 @@ public override int GetHashCode() { /// Separate from the pipeline units because they do not line up: a property is one declaration and /// up to two of them, each with its own state class and its own caller. /// -public enum DeclarationKind { +public enum DeclarationKind +{ Method, Property, Indexer, - Event + Event, } /// @@ -301,27 +319,33 @@ public record InterceptedDeclarationModel( ITypeDefinition? Type, IReadOnlyList Indices, int First, - int Second) { - - public virtual bool Equals(InterceptedDeclarationModel? other) { - if (ReferenceEquals(this, other)) { + int Second +) +{ + public virtual bool Equals(InterceptedDeclarationModel? other) + { + if (ReferenceEquals(this, other)) + { return true; } - if (other is null) { + if (other is null) + { return false; } - return Kind == other.Kind && - Identifier == other.Identifier && - Equals(Type, other.Type) && - First == other.First && - Second == other.Second && - ModelEquality.ListEquals(Indices, other.Indices); + return Kind == other.Kind + && Identifier == other.Identifier + && Equals(Type, other.Type) + && First == other.First + && Second == other.Second + && ModelEquality.ListEquals(Indices, other.Indices); } - public override int GetHashCode() { - unchecked { + public override int GetHashCode() + { + unchecked + { var hash = (int)Kind; hash = hash * 31 + Identifier.GetHashCode(); @@ -360,13 +384,13 @@ public record InterceptorModel( InterceptionRefusal? Refusal = null, IReadOnlyList? TypeParameters = null, ITypeDefinition? Realm = null, - /// /// Where the intercepted class was declared, so DM0008 and DM0015 can point at it rather than /// at the project. /// - LocationModel? Location = null) { - + LocationModel? Location = null +) +{ /// /// Whether the intercepted service is an open generic, and so registers as an implementation type /// rather than through a factory. @@ -383,14 +407,19 @@ public record InterceptorModel( Array.Empty(), Array.Empty(), Array.Empty(), - 0); + 0 + ); /// /// A model that generates nothing and explains why, so an unsupported shape produces a /// diagnostic rather than a wrapper that does not compile. /// public static InterceptorModel Refused(string message, LocationModel? location = null) => - Ignore with { Refusal = new InterceptionRefusal(message), Location = location }; + Ignore with + { + Refusal = new InterceptionRefusal(message), + Location = location, + }; public bool IsIgnored => ReferenceEquals(this, Ignore); } @@ -398,34 +427,40 @@ public static InterceptorModel Refused(string message, LocationModel? location = /// /// Equality for the incremental pipeline. Every field affects the generated wrapper. /// -public class InterceptorModelComparer : IEqualityComparer { - - public bool Equals(InterceptorModel? x, InterceptorModel? y) { - if (ReferenceEquals(x, y)) { +public class InterceptorModelComparer : IEqualityComparer +{ + public bool Equals(InterceptorModel? x, InterceptorModel? y) + { + if (ReferenceEquals(x, y)) + { return true; } - if (x is null || y is null) { + if (x is null || y is null) + { return false; } - return x.Order == y.Order && - x.ServiceType.Equals(y.ServiceType) && - x.ImplementationType.Equals(y.ImplementationType) && - // Realm decides which module emits the applicator, so leaving it out meant editing - // only `Realm = typeof(X)` compared equal to the model before the edit, hit the - // cache and re-emitted nothing. DecoratorModelComparer and ServiceModelComparer both - // compare theirs; this was the odd one out. - Equals(x.Realm, y.Realm) && - Equals(x.Refusal, y.Refusal) && - ModelEquality.ListEquals(x.Interceptors, y.Interceptors) && - ModelEquality.ListEquals(x.Members, y.Members) && - ModelEquality.ListEquals(x.Declarations, y.Declarations) && - ModelEquality.ListEquals(x.TypeParameters, y.TypeParameters); + return x.Order == y.Order + && x.ServiceType.Equals(y.ServiceType) + && x.ImplementationType.Equals(y.ImplementationType) + && + // Realm decides which module emits the applicator, so leaving it out meant editing + // only `Realm = typeof(X)` compared equal to the model before the edit, hit the + // cache and re-emitted nothing. DecoratorModelComparer and ServiceModelComparer both + // compare theirs; this was the odd one out. + Equals(x.Realm, y.Realm) + && Equals(x.Refusal, y.Refusal) + && ModelEquality.ListEquals(x.Interceptors, y.Interceptors) + && ModelEquality.ListEquals(x.Members, y.Members) + && ModelEquality.ListEquals(x.Declarations, y.Declarations) + && ModelEquality.ListEquals(x.TypeParameters, y.TypeParameters); } - public int GetHashCode(InterceptorModel obj) { - unchecked { + public int GetHashCode(InterceptorModel obj) + { + unchecked + { var hash = obj.ServiceType.GetHashCode(); hash = hash * 31 + obj.ImplementationType.GetHashCode(); diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/LocationModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/LocationModel.cs index 06d7649..3651cf2 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/LocationModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/LocationModel.cs @@ -21,8 +21,9 @@ public record LocationModel( int StartLine, int StartCharacter, int EndLine, - int EndCharacter) { - + int EndCharacter +) +{ /// /// Rebuilds a reportable location. Safe to call only outside the incremental pipeline. /// @@ -39,7 +40,9 @@ public Location ToLocation() => new TextSpan(SpanStart, SpanLength), new LinePositionSpan( new LinePosition(StartLine, StartCharacter), - new LinePosition(EndLine, EndCharacter))); + new LinePosition(EndLine, EndCharacter) + ) + ); /// /// Rebuilds a reportable location against the syntax tree it came from, so that @@ -53,10 +56,12 @@ public Location ToLocation() => /// a replayed model can carry a span from a previous version of the file. Out of bounds, /// Location.Create throws, and a generator that throws reports nothing at all. /// - public Location ToLocation(SyntaxTreeLookup lookup) { + public Location ToLocation(SyntaxTreeLookup lookup) + { var tree = lookup.Find(FilePath); - if (tree == null) { + if (tree == null) + { return ToLocation(); } @@ -85,13 +90,15 @@ public Location ToLocation(SyntaxTreeLookup lookup) { /// /// private static SyntaxNodeOrToken NarrowToName(SyntaxNode node) => - node switch { + node switch + { TypeDeclarationSyntax type => type.Identifier, MethodDeclarationSyntax method => method.Identifier, - _ => node + _ => node, }; - private static LocationModel From(SyntaxNodeOrToken nodeOrToken) { + private static LocationModel From(SyntaxNodeOrToken nodeOrToken) + { var span = nodeOrToken.GetLocation()!.GetLineSpan(); return new LocationModel( @@ -101,7 +108,8 @@ private static LocationModel From(SyntaxNodeOrToken nodeOrToken) { span.StartLinePosition.Line, span.StartLinePosition.Character, span.EndLinePosition.Line, - span.EndLinePosition.Character); + span.EndLinePosition.Character + ); } /// diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/MethodInfoModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/MethodInfoModel.cs index 1c72a17..58087ca 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/MethodInfoModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/MethodInfoModel.cs @@ -2,7 +2,8 @@ namespace DependencyModules.SourceGenerator.Impl.Models; -public enum AccessModifier { +public enum AccessModifier +{ PublicModifier, PrivateModifier, ProtectedModifier, @@ -14,4 +15,5 @@ public record MethodInfoModel( string MethodName, ITypeDefinition ReturnType, IReadOnlyList Parameters, - IReadOnlyList GenericArguments); \ No newline at end of file + IReadOnlyList GenericArguments +); diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/ModelEquality.cs b/src/DependencyModules.SourceGenerator.Impl/Models/ModelEquality.cs index b28a732..a8ae207 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/ModelEquality.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/ModelEquality.cs @@ -11,19 +11,24 @@ namespace DependencyModules.SourceGenerator.Impl.Models; /// structurally identical models built on consecutive runs would never compare equal, and the /// generator would regenerate everything on every keystroke. /// -internal static class ModelEquality { - - public static bool ListEquals(IReadOnlyList? x, IReadOnlyList? y) { - if (ReferenceEquals(x, y)) { +internal static class ModelEquality +{ + public static bool ListEquals(IReadOnlyList? x, IReadOnlyList? y) + { + if (ReferenceEquals(x, y)) + { return true; } - if (x is null || y is null || x.Count != y.Count) { + if (x is null || y is null || x.Count != y.Count) + { return false; } - for (var i = 0; i < x.Count; i++) { - if (!EqualityComparer.Default.Equals(x[i], y[i])) { + for (var i = 0; i < x.Count; i++) + { + if (!EqualityComparer.Default.Equals(x[i], y[i])) + { return false; } } @@ -31,15 +36,18 @@ public static bool ListEquals(IReadOnlyList? x, IReadOnlyList? y) { return true; } - public static int ListHashCode(IReadOnlyList? list) { - if (list is null) { + public static int ListHashCode(IReadOnlyList? list) + { + if (list is null) + { return 0; } - - unchecked { + unchecked + { var hash = 19; - for (var i = 0; i < list.Count; i++) { + for (var i = 0; i < list.Count; i++) + { hash = hash * 31 + (list[i]?.GetHashCode() ?? 0); } @@ -50,32 +58,40 @@ public static int ListHashCode(IReadOnlyList? list) { /// /// Compares attribute argument values, which arrive as object and may be arrays. /// - public static bool ValueEquals(object? x, object? y) { - if (Equals(x, y)) { + public static bool ValueEquals(object? x, object? y) + { + if (Equals(x, y)) + { return true; } - if (x is string || y is string) { + if (x is string || y is string) + { return false; } - if (x is IEnumerable xs && y is IEnumerable ys) { + if (x is IEnumerable xs && y is IEnumerable ys) + { var xEnumerator = xs.GetEnumerator(); var yEnumerator = ys.GetEnumerator(); - while (true) { + while (true) + { var xMoved = xEnumerator.MoveNext(); var yMoved = yEnumerator.MoveNext(); - if (xMoved != yMoved) { + if (xMoved != yMoved) + { return false; } - if (!xMoved) { + if (!xMoved) + { return true; } - if (!ValueEquals(xEnumerator.Current, yEnumerator.Current)) { + if (!ValueEquals(xEnumerator.Current, yEnumerator.Current)) + { return false; } } @@ -84,20 +100,26 @@ public static bool ValueEquals(object? x, object? y) { return false; } - public static int ValueHashCode(object? value) { - if (value is null) { + public static int ValueHashCode(object? value) + { + if (value is null) + { return 0; } - if (value is string) { + if (value is string) + { return value.GetHashCode(); } - if (value is IEnumerable enumerable) { - unchecked { + if (value is IEnumerable enumerable) + { + unchecked + { var hash = 19; - foreach (var item in enumerable) { + foreach (var item in enumerable) + { hash = hash * 31 + ValueHashCode(item); } diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/ModuleEntryPointModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/ModuleEntryPointModel.cs index 01afeb2..1ee7cb1 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/ModuleEntryPointModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/ModuleEntryPointModel.cs @@ -3,7 +3,8 @@ namespace DependencyModules.SourceGenerator.Impl.Models; [Flags] -public enum ModuleEntryPointFeatures { +public enum ModuleEntryPointFeatures +{ None, AutoGenerateModule = 1, OnlyRealm = 2, @@ -40,15 +41,20 @@ public record ModuleEntryPointModel( IReadOnlyList PropertyInfoModels, IReadOnlyList AttributeModels, IReadOnlyList AdditionalModules, - IReadOnlyList Features) : IClassModel { + IReadOnlyList Features +) : IClassModel +{ public ITypeDefinition ClassType => EntryPointType; } -public class ModuleEntryPointModelComparer : IEqualityComparer { - - public bool Equals(ModuleEntryPointModel? x, ModuleEntryPointModel? y) { - if (x is null && y is null) return true; - if (x is null || y is null) return false; +public class ModuleEntryPointModelComparer : IEqualityComparer +{ + public bool Equals(ModuleEntryPointModel? x, ModuleEntryPointModel? y) + { + if (x is null && y is null) + return true; + if (x is null || y is null) + return false; // Location is deliberately absent. It is carried so diagnostics can point at the // declaration, but it shifts whenever anything above the module is edited — including a @@ -56,29 +62,33 @@ public bool Equals(ModuleEntryPointModel? x, ModuleEntryPointModel? y) { // module on a keystroke that changed nothing (IncrementalGenerationTests covers exactly // that). The cost is that a diagnostic replayed from cache can sit a line or two off until // the next semantic edit, which is the cheaper of the two mistakes. - return x.FileLocation == y.FileLocation && - x.EntryPointType.Equals(y.EntryPointType) && - x.ModuleFeatures == y.ModuleFeatures && - x.UseMethod == y.UseMethod && - x.RegistrationType == y.RegistrationType && - x.GenerateAttribute == y.GenerateAttribute && - x.RegisterJsonSerializers == y.RegisterJsonSerializers && - x.GenerateFactories == y.GenerateFactories && - x.Parameters.SequenceEqual(y.Parameters) && - x.PropertyInfoModels.SequenceEqual(y.PropertyInfoModels) && - x.Features.SequenceEqual(y.Features) && - x.AttributeModels.SequenceEqual(y.AttributeModels) && - x.AdditionalModules.SequenceEqual(y.AdditionalModules); + return x.FileLocation == y.FileLocation + && x.EntryPointType.Equals(y.EntryPointType) + && x.ModuleFeatures == y.ModuleFeatures + && x.UseMethod == y.UseMethod + && x.RegistrationType == y.RegistrationType + && x.GenerateAttribute == y.GenerateAttribute + && x.RegisterJsonSerializers == y.RegisterJsonSerializers + && x.GenerateFactories == y.GenerateFactories + && x.Parameters.SequenceEqual(y.Parameters) + && x.PropertyInfoModels.SequenceEqual(y.PropertyInfoModels) + && x.Features.SequenceEqual(y.Features) + && x.AttributeModels.SequenceEqual(y.AttributeModels) + && x.AdditionalModules.SequenceEqual(y.AdditionalModules); } - public int GetHashCode(ModuleEntryPointModel obj) { - unchecked { + public int GetHashCode(ModuleEntryPointModel obj) + { + unchecked + { var hash = 17; hash = hash * 31 + obj.EntryPointType.GetHashCode(); - if (obj.RegistrationType.HasValue) { + if (obj.RegistrationType.HasValue) + { hash = hash * 31 + obj.RegistrationType.Value.GetHashCode(); } - if (obj.GenerateAttribute.HasValue) { + if (obj.GenerateAttribute.HasValue) + { hash = hash * 31 + obj.GenerateAttribute.Value.GetHashCode(); } hash = hash * 31 + obj.FileLocation.GetHashCode(); @@ -90,15 +100,18 @@ public int GetHashCode(ModuleEntryPointModel obj) { hash = GetListHashCode(obj.PropertyInfoModels, hash); hash = GetListHashCode(obj.AttributeModels, hash); hash = GetListHashCode(obj.Features, hash); - + return hash; } } - private int GetListHashCode(IEnumerable list, int hashSeed) { + private int GetListHashCode(IEnumerable list, int hashSeed) + { int hash = hashSeed; - unchecked { - foreach (var obj in list) { + unchecked + { + foreach (var obj in list) + { hash = hash * 31 + (obj?.GetHashCode() ?? 1); } } @@ -106,14 +119,17 @@ private int GetListHashCode(IEnumerable list, int hashSeed) { } } -public static class ModuleEntryPointModelExtensions { - public static string UniqueId(this ModuleEntryPointModel model) { +public static class ModuleEntryPointModelExtensions +{ + public static string UniqueId(this ModuleEntryPointModel model) + { var count = 0; - foreach (var charValue in model.EntryPointType.Namespace + "." + model.EntryPointType.Name) { + foreach (var charValue in model.EntryPointType.Namespace + "." + model.EntryPointType.Name) + { count += charValue; } - + return count.ToString(); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/ParameterInfoModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/ParameterInfoModel.cs index 9504e4c..911bf39 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/ParameterInfoModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/ParameterInfoModel.cs @@ -6,18 +6,21 @@ public record ParameterInfoModel( string ParameterName, ITypeDefinition ParameterType, object? DefaultValue, - IReadOnlyList Attributes) { - + IReadOnlyList Attributes +) +{ // Structural equality over Attributes; see ModelEquality. public virtual bool Equals(ParameterInfoModel? other) => - other is not null && - ParameterName == other.ParameterName && - ParameterType.Equals(other.ParameterType) && - Equals(DefaultValue, other.DefaultValue) && - ModelEquality.ListEquals(Attributes, other.Attributes); + other is not null + && ParameterName == other.ParameterName + && ParameterType.Equals(other.ParameterType) + && Equals(DefaultValue, other.DefaultValue) + && ModelEquality.ListEquals(Attributes, other.Attributes); - public override int GetHashCode() { - unchecked { + public override int GetHashCode() + { + unchecked + { var hash = ParameterName.GetHashCode(); hash = hash * 31 + ParameterType.GetHashCode(); hash = hash * 31 + (DefaultValue?.GetHashCode() ?? 0); @@ -25,4 +28,4 @@ public override int GetHashCode() { return hash; } } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/PropertyInfoModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/PropertyInfoModel.cs index 6695036..2ff94b2 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/PropertyInfoModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/PropertyInfoModel.cs @@ -17,8 +17,9 @@ public record PropertyInfoModel( string PropertyName, bool IsReadOnly, bool IsStatic, - bool IsVisibleToAttribute) { - + bool IsVisibleToAttribute +) +{ /// /// Whether this property is carried across to the generated attribute as a module parameter. /// diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/ServiceModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/ServiceModel.cs index c03f3dc..5b91888 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/ServiceModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/ServiceModel.cs @@ -2,22 +2,25 @@ namespace DependencyModules.SourceGenerator.Impl.Models; -public enum ServiceLifestyle { +public enum ServiceLifestyle +{ Transient, Scoped, - Singleton + Singleton, } -public enum RegistrationType { +public enum RegistrationType +{ Add, Try, TryEnumerable, - Replace + Replace, } [Flags] -public enum RegistrationFeature { - None= 0, +public enum RegistrationFeature +{ + None = 0, AutoRegisterSourceGenerator = 1, /// @@ -54,17 +57,20 @@ public enum RegistrationFeature { public record ServiceFactoryModel( ITypeDefinition TypeDefinition, string MethodName, - IReadOnlyList Parameters) { - + IReadOnlyList Parameters +) +{ // Structural equality over Parameters; see ModelEquality. public virtual bool Equals(ServiceFactoryModel? other) => - other is not null && - TypeDefinition.Equals(other.TypeDefinition) && - MethodName == other.MethodName && - ModelEquality.ListEquals(Parameters, other.Parameters); + other is not null + && TypeDefinition.Equals(other.TypeDefinition) + && MethodName == other.MethodName + && ModelEquality.ListEquals(Parameters, other.Parameters); - public override int GetHashCode() { - unchecked { + public override int GetHashCode() + { + unchecked + { var hash = TypeDefinition.GetHashCode(); hash = hash * 31 + MethodName.GetHashCode(); hash = hash * 31 + ModelEquality.ListHashCode(Parameters); @@ -81,16 +87,18 @@ public record ServiceRegistrationModel( object? Key = null, bool? CrossWire = false, IReadOnlyList? Namespaces = null, - /// /// Where this registration sits among the others for the same service, lowest first. Decides the /// sequence an IEnumerable<T> dependency sees, and therefore which one a single /// resolve returns. /// - int Order = 0); + int Order = 0 +); public delegate IOutputComponent? FactoryOutputDelegate( - ServiceModel serviceModel, ServiceRegistrationModel registrationModel); + ServiceModel serviceModel, + ServiceRegistrationModel registrationModel +); public record ServiceModel( ITypeDefinition ImplementationType, @@ -106,14 +114,15 @@ public record ServiceModel( /// writer emits one guard around the lot. /// IReadOnlyList? Conditions = null, - /// /// Where the implementation was declared, so a diagnostic about it can point at the class /// rather than at the project. Deliberately absent from /// — it is not part of what makes two models the same /// registration, and including it would miss the incremental cache on an edit above the class. /// - LocationModel? Location = null) { + LocationModel? Location = null +) +{ public static ServiceModel Ignore = new ServiceModel( TypeDefinition.Get("", "Ignore"), null, @@ -121,35 +130,51 @@ public record ServiceModel( null, Array.Empty(), RegistrationFeature.None - ); + ); } -public class ServiceModelComparer : IEqualityComparer { - - public bool Equals(ServiceModel? x, ServiceModel? y) { - if (ReferenceEquals(x, y)) return true; - if (x is null) return false; - if (y is null) return false; - if (x.GetType() != y.GetType()) return false; - return - x.Features == y.Features && - x.ImplementationType.Equals(y.ImplementationType) && - CompareConstructor(x.Constructor, y.Constructor) && - CompareRegistrations(x.Registrations, y.Registrations) && - CompareFactory(x.Factory, y.Factory) && - CompareFactoryOutput(x.FactoryOutput, y.FactoryOutput) && - CompareConditions(x.Conditions, y.Conditions); +public class ServiceModelComparer : IEqualityComparer +{ + public bool Equals(ServiceModel? x, ServiceModel? y) + { + if (ReferenceEquals(x, y)) + return true; + if (x is null) + return false; + if (y is null) + return false; + if (x.GetType() != y.GetType()) + return false; + return x.Features == y.Features + && x.ImplementationType.Equals(y.ImplementationType) + && CompareConstructor(x.Constructor, y.Constructor) + && CompareRegistrations(x.Registrations, y.Registrations) + && CompareFactory(x.Factory, y.Factory) + && CompareFactoryOutput(x.FactoryOutput, y.FactoryOutput) + && CompareConditions(x.Conditions, y.Conditions); } - private bool CompareConstructor(ConstructorInfoModel? xConstructor, ConstructorInfoModel? yConstructor) { - if (xConstructor is null && yConstructor is null) return true; - if (xConstructor is null || yConstructor is null) return false; + private bool CompareConstructor( + ConstructorInfoModel? xConstructor, + ConstructorInfoModel? yConstructor + ) + { + if (xConstructor is null && yConstructor is null) + return true; + if (xConstructor is null || yConstructor is null) + return false; return xConstructor.Parameters.SequenceEqual(yConstructor.Parameters); } - private bool CompareFactoryOutput(FactoryOutputDelegate? xFactoryOutput, FactoryOutputDelegate? yFactoryOutput) { - if (xFactoryOutput is null && yFactoryOutput is null) return true; - if (xFactoryOutput is null || yFactoryOutput is null) return false; + private bool CompareFactoryOutput( + FactoryOutputDelegate? xFactoryOutput, + FactoryOutputDelegate? yFactoryOutput + ) + { + if (xFactoryOutput is null && yFactoryOutput is null) + return true; + if (xFactoryOutput is null || yFactoryOutput is null) + return false; return true; } @@ -159,35 +184,50 @@ private bool CompareFactoryOutput(FactoryOutputDelegate? xFactoryOutput, Factory /// private bool CompareConditions( IReadOnlyList? xConditions, - IReadOnlyList? yConditions) { - if ((xConditions?.Count ?? 0) == 0 && (yConditions?.Count ?? 0) == 0) { + IReadOnlyList? yConditions + ) + { + if ((xConditions?.Count ?? 0) == 0 && (yConditions?.Count ?? 0) == 0) + { return true; } return ModelEquality.ListEquals(xConditions, yConditions); } - private bool CompareFactory(ServiceFactoryModel? xFactory, ServiceFactoryModel? yFactory) { - if (xFactory is null && yFactory is null) return true; - if (xFactory is null) return false; - if (yFactory is null) return false; + private bool CompareFactory(ServiceFactoryModel? xFactory, ServiceFactoryModel? yFactory) + { + if (xFactory is null && yFactory is null) + return true; + if (xFactory is null) + return false; + if (yFactory is null) + return false; return xFactory.Equals(yFactory); } - public int GetHashCode(ServiceModel obj) { + public int GetHashCode(ServiceModel obj) + { return obj.ImplementationType.GetHashCode(); } - private bool CompareRegistrations(IReadOnlyList xRegistrations, IReadOnlyList yRegistrations) { - if (xRegistrations.Count != yRegistrations.Count) { + private bool CompareRegistrations( + IReadOnlyList xRegistrations, + IReadOnlyList yRegistrations + ) + { + if (xRegistrations.Count != yRegistrations.Count) + { return false; } - for (var i = 0; i < xRegistrations.Count; i++) { + for (var i = 0; i < xRegistrations.Count; i++) + { var x = xRegistrations[i]; var y = yRegistrations[i]; - if (!CompareRegistration(x, y)) { + if (!CompareRegistration(x, y)) + { return false; } } @@ -195,19 +235,27 @@ private bool CompareRegistrations(IReadOnlyList xRegis return true; } - private bool CompareRegistration(ServiceRegistrationModel x, ServiceRegistrationModel y) { - return x.ServiceType.Equals(y.ServiceType) && - x.Lifestyle == y.Lifestyle && - x.RegistrationType == y.RegistrationType && - CompareNamespaces(x.Namespaces, y.Namespaces) && - Equals(x.Realm, y.Realm) && - Equals(x.Key, y.Key); + private bool CompareRegistration(ServiceRegistrationModel x, ServiceRegistrationModel y) + { + return x.ServiceType.Equals(y.ServiceType) + && x.Lifestyle == y.Lifestyle + && x.RegistrationType == y.RegistrationType + && CompareNamespaces(x.Namespaces, y.Namespaces) + && Equals(x.Realm, y.Realm) + && Equals(x.Key, y.Key); } - private bool CompareNamespaces(IReadOnlyList? xNamespaces, IReadOnlyList? yNamespaces) { - if (xNamespaces is null && yNamespaces is null) return true; - if (xNamespaces is null || yNamespaces is null) return false; - if (xNamespaces.Count != yNamespaces.Count) return false; + private bool CompareNamespaces( + IReadOnlyList? xNamespaces, + IReadOnlyList? yNamespaces + ) + { + if (xNamespaces is null && yNamespaces is null) + return true; + if (xNamespaces is null || yNamespaces is null) + return false; + if (xNamespaces.Count != yNamespaces.Count) + return false; return xNamespaces.SequenceEqual(yNamespaces); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/SyntaxTreeLookup.cs b/src/DependencyModules.SourceGenerator.Impl/Models/SyntaxTreeLookup.cs index 4728364..bfeccc6 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/SyntaxTreeLookup.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/SyntaxTreeLookup.cs @@ -15,29 +15,36 @@ namespace DependencyModules.SourceGenerator.Impl.Models; /// with nothing. The map is built on first use, so a run that reports no diagnostics — the ordinary /// case — never walks the trees at all. /// -public sealed class SyntaxTreeLookup { +public sealed class SyntaxTreeLookup +{ private readonly Compilation? _compilation; private Dictionary? _byPath; /// A lookup that finds nothing, for callers with no compilation to hand. public static readonly SyntaxTreeLookup None = new(null); - public SyntaxTreeLookup(Compilation? compilation) { + public SyntaxTreeLookup(Compilation? compilation) + { _compilation = compilation; } - public SyntaxTree? Find(string filePath) { - if (_compilation == null || string.IsNullOrEmpty(filePath)) { + public SyntaxTree? Find(string filePath) + { + if (_compilation == null || string.IsNullOrEmpty(filePath)) + { return null; } - if (_byPath == null) { + if (_byPath == null) + { _byPath = new Dictionary(); - foreach (var tree in _compilation.SyntaxTrees) { + foreach (var tree in _compilation.SyntaxTrees) + { // First wins. Two trees can share a path — a linked file compiled into more than // one target — and either answers the question a location asks. - if (!string.IsNullOrEmpty(tree.FilePath) && !_byPath.ContainsKey(tree.FilePath)) { + if (!string.IsNullOrEmpty(tree.FilePath) && !_byPath.ContainsKey(tree.FilePath)) + { _byPath.Add(tree.FilePath, tree); } } diff --git a/src/DependencyModules.SourceGenerator.Impl/ModuleAttributeWriter.cs b/src/DependencyModules.SourceGenerator.Impl/ModuleAttributeWriter.cs index 9623f61..9efb1ed 100644 --- a/src/DependencyModules.SourceGenerator.Impl/ModuleAttributeWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/ModuleAttributeWriter.cs @@ -6,23 +6,34 @@ namespace DependencyModules.SourceGenerator.Impl; -public class ModuleAttributeWriter : BaseAttributeWriter { - - protected override void CustomImplementation(IConstructContainer container, ClassDefinition attributeClass, ModuleEntryPointModel model) { +public class ModuleAttributeWriter : BaseAttributeWriter +{ + protected override void CustomImplementation( + IConstructContainer container, + ClassDefinition attributeClass, + ModuleEntryPointModel model + ) + { var method = attributeClass.AddMethod("GetModule"); method.SetReturnType(KnownTypes.DependencyModules.Interfaces.IDependencyModule); - var newModule = - method.Assign( - New(model.EntryPointType, - attributeClass.Fields.Select(f => f.Instance).OfType().ToArray())).ToVar("newModule"); - - foreach (var propertyInfoModel in model.PropertyInfoModels) { - if (!propertyInfoModel.IsModuleParameter) { + var newModule = method + .Assign( + New( + model.EntryPointType, + attributeClass.Fields.Select(f => f.Instance).OfType().ToArray() + ) + ) + .ToVar("newModule"); + + foreach (var propertyInfoModel in model.PropertyInfoModels) + { + if (!propertyInfoModel.IsModuleParameter) + { continue; } - + // Guarded whatever the declared nullability. An attribute property is null until // somebody assigns it, and `?` is an annotation rather than a runtime fact — so gating // the guard on it meant `public string Label { get; set; } = "default";` had its @@ -35,9 +46,11 @@ protected override void CustomImplementation(IConstructContainer container, Clas // BaseAttributeWriter already wraps the class in a pragma for it. var block = method.If(NotEquals(propertyInfoModel.PropertyName, Null())); - block.Assign(propertyInfoModel.PropertyName).To(newModule.Property(propertyInfoModel.PropertyName)); + block + .Assign(propertyInfoModel.PropertyName) + .To(newModule.Property(propertyInfoModel.PropertyName)); } method.Return(newModule); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/ModuleEntryPointDiagnostics.cs b/src/DependencyModules.SourceGenerator.Impl/ModuleEntryPointDiagnostics.cs index a2c5427..b24495d 100644 --- a/src/DependencyModules.SourceGenerator.Impl/ModuleEntryPointDiagnostics.cs +++ b/src/DependencyModules.SourceGenerator.Impl/ModuleEntryPointDiagnostics.cs @@ -30,8 +30,8 @@ namespace DependencyModules.SourceGenerator.Impl; /// rather than repeating them. /// /// -public static class ModuleEntryPointDiagnostics { - +public static class ModuleEntryPointDiagnostics +{ /// /// Generating into a non-partial type produces CS0260 against the developer's own declaration, /// which describes the symptom rather than the fix. @@ -56,15 +56,22 @@ public static bool IsNestedInType(ModuleEntryPointModel model) => /// has already answered the question this asks about. /// public static bool ReliesOnGeneratedEquality(ModuleEntryPointModel model) => - model.PropertyInfoModels.Any(p => p.IsModuleParameter) && - model.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.ShouldImplementEquals); + model.PropertyInfoModels.Any(p => p.IsModuleParameter) + && model.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.ShouldImplementEquals); public static void Report( SourceProductionContext context, - (ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Models, - Compilation Compilation) input) { - - if (input.Models.Length == 0) { + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Models, + Compilation Compilation + ) input + ) + { + if (input.Models.Length == 0) + { return; } @@ -74,22 +81,40 @@ public static void Report( var lookup = new SyntaxTreeLookup(input.Compilation); - foreach (var entryPointModel in entryPointList) { + foreach (var entryPointModel in entryPointList) + { context.CancellationToken.ThrowIfCancellationRequested(); - if (IsNotPartial(entryPointModel)) { - Report(context, DependencyModuleDiagnostics.ModuleMustBePartial, entryPointModel, lookup); + if (IsNotPartial(entryPointModel)) + { + Report( + context, + DependencyModuleDiagnostics.ModuleMustBePartial, + entryPointModel, + lookup + ); continue; } - if (IsNestedInType(entryPointModel)) { - Report(context, DependencyModuleDiagnostics.ModuleCannotBeNested, entryPointModel, lookup); + if (IsNestedInType(entryPointModel)) + { + Report( + context, + DependencyModuleDiagnostics.ModuleCannotBeNested, + entryPointModel, + lookup + ); continue; } - if (ReliesOnGeneratedEquality(entryPointModel)) { - Report(context, DependencyModuleDiagnostics.ModuleWithPropertiesShouldImplementEquals, - entryPointModel, lookup); + if (ReliesOnGeneratedEquality(entryPointModel)) + { + Report( + context, + DependencyModuleDiagnostics.ModuleWithPropertiesShouldImplementEquals, + entryPointModel, + lookup + ); } } } @@ -98,10 +123,13 @@ private static void Report( SourceProductionContext context, DiagnosticDescriptor descriptor, ModuleEntryPointModel entryPointModel, - SyntaxTreeLookup lookup) => + SyntaxTreeLookup lookup + ) => context.ReportDiagnostic( Diagnostic.Create( descriptor, entryPointModel.Location.ToLocationOrNone(lookup), - entryPointModel.EntryPointType.Name)); + entryPointModel.EntryPointType.Name + ) + ); } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeModelCollector.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeModelCollector.cs index 64919c3..5d54489 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeModelCollector.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeModelCollector.cs @@ -21,8 +21,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// compilation — against 33 ms for the one visit of every syntax node this shape replaced. /// /// -public static class AttributeModelCollector { - +public static class AttributeModelCollector +{ /// /// Collects one model per declaration carrying any of . /// @@ -36,27 +36,34 @@ public static IncrementalValueProvider> Collect( ITypeDefinition[] attributeTypes, Func generate, IEqualityComparer comparer, - TModel ignored) { - + TModel ignored + ) + { IncrementalValueProvider>? merged = null; // ForAttributeWithMetadataName takes a single name, so an attribute set needs one provider // each. They share the index, so several indexed lookups still cost far less than one visit // of every syntax node. - foreach (var attributeType in attributeTypes) { + foreach (var attributeType in attributeTypes) + { var owner = attributeType; - var provider = context.SyntaxProvider.ForAttributeWithMetadataName( + var provider = context + .SyntaxProvider.ForAttributeWithMetadataName( MetadataName(owner), static (node, _) => node is MemberDeclarationSyntax, (syntaxContext, cancellation) => - Owned(syntaxContext, cancellation, attributeTypes, owner, generate, ignored)) + Owned(syntaxContext, cancellation, attributeTypes, owner, generate, ignored) + ) .WithComparer(comparer) .Collect(); - merged = merged == null - ? provider - : merged.Value.Combine(provider).Select(static (pair, _) => pair.Left.AddRange(pair.Right)); + merged = + merged == null + ? provider + : merged + .Value.Combine(provider) + .Select(static (pair, _) => pair.Left.AddRange(pair.Right)); } return merged!.Value; @@ -78,12 +85,15 @@ private static TModel Owned( ITypeDefinition[] attributeTypes, ITypeDefinition owner, Func generate, - TModel ignored) { - + TModel ignored + ) + { var present = context.TargetSymbol.GetAttributes(); - foreach (var candidate in attributeTypes) { - if (!IsPresent(present, candidate)) { + foreach (var candidate in attributeTypes) + { + if (!IsPresent(present, candidate)) + { continue; } @@ -93,11 +103,16 @@ private static TModel Owned( return generate(context, cancellation); } - private static bool IsPresent(ImmutableArray present, ITypeDefinition candidate) { - foreach (var attribute in present) { - if (attribute.AttributeClass is { } attributeClass && - attributeClass.Name == candidate.Name && - NamespaceOf(attributeClass) == candidate.Namespace) { + private static bool IsPresent(ImmutableArray present, ITypeDefinition candidate) + { + foreach (var attribute in present) + { + if ( + attribute.AttributeClass is { } attributeClass + && attributeClass.Name == candidate.Name + && NamespaceOf(attributeClass) == candidate.Namespace + ) + { return true; } } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeModelHelper.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeModelHelper.cs index fe2a0f1..5da88ef 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeModelHelper.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeModelHelper.cs @@ -6,28 +6,29 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; -public static class AttributeModelHelper { - +public static class AttributeModelHelper +{ public static IReadOnlyList GetAttributeModels( SyntaxTransformContext context, SyntaxNode node, CancellationToken cancellationToken, - Func? filter = null) { + Func? filter = null + ) + { SyntaxList? attributeLists = null; - if (node is BaseParameterSyntax parameterSyntax) { + if (node is BaseParameterSyntax parameterSyntax) + { attributeLists = parameterSyntax.AttributeLists; } - else if (node is MemberDeclarationSyntax memberDeclarationSyntax) { + else if (node is MemberDeclarationSyntax memberDeclarationSyntax) + { attributeLists = memberDeclarationSyntax.AttributeLists; } - if (attributeLists != null) { - var results = GetAttributes( - context, - attributeLists.Value, - cancellationToken, - filter); + if (attributeLists != null) + { + var results = GetAttributes(context, attributeLists.Value, cancellationToken, filter); return results.ToList(); } @@ -37,36 +38,45 @@ public static IReadOnlyList GetAttributeModels( public static AttributeClassInfo GetAttributeClassInfo( SyntaxTransformContext context, - CancellationToken cancellationToken) { + CancellationToken cancellationToken + ) + { var propertyList = new List(); - foreach (var syntax in - context.Node.DescendantNodes()) { + foreach (var syntax in context.Node.DescendantNodes()) + { cancellationToken.ThrowIfCancellationRequested(); - if (syntax is PropertyDeclarationSyntax propertyDeclarationSyntax) { - var setter = - propertyDeclarationSyntax.AccessorList?.Accessors.FirstOrDefault( - x => x.IsKind(SyntaxKind.SetAccessorDeclaration)); - - var propertyType = - propertyDeclarationSyntax.Type.GetTypeDefinition(context); - - if (propertyType != null) { - propertyList.Add(new PropertyInfoModel(propertyType, - propertyDeclarationSyntax.Identifier.ToString(), - setter == null, - propertyDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)), - IsVisibleToAttribute(propertyDeclarationSyntax.Modifiers) - )); + if (syntax is PropertyDeclarationSyntax propertyDeclarationSyntax) + { + var setter = propertyDeclarationSyntax.AccessorList?.Accessors.FirstOrDefault(x => + x.IsKind(SyntaxKind.SetAccessorDeclaration) + ); + + var propertyType = propertyDeclarationSyntax.Type.GetTypeDefinition(context); + + if (propertyType != null) + { + propertyList.Add( + new PropertyInfoModel( + propertyType, + propertyDeclarationSyntax.Identifier.ToString(), + setter == null, + propertyDeclarationSyntax.Modifiers.Any(m => + m.IsKind(SyntaxKind.StaticKeyword) + ), + IsVisibleToAttribute(propertyDeclarationSyntax.Modifiers) + ) + ); } } } return new AttributeClassInfo( - ServiceModelUtility.GetConstructorInfo(context, context.Node, cancellationToken) ?? - new ConstructorInfoModel(Array.Empty()), - propertyList); + ServiceModelUtility.GetConstructorInfo(context, context.Node, cancellationToken) + ?? new ConstructorInfoModel(Array.Empty()), + propertyList + ); } /// @@ -81,17 +91,20 @@ public static AttributeClassInfo GetAttributeClassInfo( /// access modifier at all is private by default, which is the case most likely to be written by /// accident and was the one that produced generated code that would not compile. /// - private static bool IsVisibleToAttribute(SyntaxTokenList modifiers) { + private static bool IsVisibleToAttribute(SyntaxTokenList modifiers) + { var isPrivate = modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)); var isProtected = modifiers.Any(m => m.IsKind(SyntaxKind.ProtectedKeyword)); var isInternal = modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword)); var isPublic = modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)); - if (isPrivate) { + if (isPrivate) + { return false; } - if (isProtected) { + if (isProtected) + { return isInternal; } @@ -102,15 +115,21 @@ public static IEnumerable GetAttributes( SyntaxTransformContext context, SyntaxList attributeListSyntax, CancellationToken cancellationToken, - Func? filter = null) { - foreach (var attributeList in attributeListSyntax) { - foreach (var attribute in attributeList.Attributes) { + Func? filter = null + ) + { + foreach (var attributeList in attributeListSyntax) + { + foreach (var attribute in attributeList.Attributes) + { cancellationToken.ThrowIfCancellationRequested(); var operation = ModelExtensions.GetTypeInfo(context.SemanticModel, attribute); - if (filter?.Invoke(attribute) ?? true) { - if (operation.Type != null) { + if (filter?.Invoke(attribute) ?? true) + { + if (operation.Type != null) + { yield return InternalAttributeModel(context, attribute, operation); } } @@ -118,89 +137,107 @@ public static IEnumerable GetAttributes( } } - public static AttributeModel? GetAttribute(SyntaxTransformContext context, AttributeSyntax attribute) { + public static AttributeModel? GetAttribute( + SyntaxTransformContext context, + AttributeSyntax attribute + ) + { var operation = ModelExtensions.GetTypeInfo(context.SemanticModel, attribute); - return operation.Type != null ? InternalAttributeModel(context, attribute, operation) : null; + return operation.Type != null + ? InternalAttributeModel(context, attribute, operation) + : null; } private static AttributeModel InternalAttributeModel( - SyntaxTransformContext context, AttributeSyntax attribute, TypeInfo operation) { + SyntaxTransformContext context, + AttributeSyntax attribute, + TypeInfo operation + ) + { var arguments = new List(); var properties = new List(); - if (attribute.ArgumentList != null) { - foreach (var attributeArgumentSyntax in - attribute.ArgumentList.Arguments) { - var operationValue = context.SemanticModel.GetOperation(attributeArgumentSyntax.Expression); + if (attribute.ArgumentList != null) + { + foreach (var attributeArgumentSyntax in attribute.ArgumentList.Arguments) + { + var operationValue = context.SemanticModel.GetOperation( + attributeArgumentSyntax.Expression + ); - if (operationValue == null) { + if (operationValue == null) + { continue; } var constantValue = GetOperationValue(context, operationValue); - if (attributeArgumentSyntax.NameColon != null) { + if (attributeArgumentSyntax.NameColon != null) + { arguments.Add( new AttributeArgumentValue( attributeArgumentSyntax.NameColon.ToString(), constantValue - )); + ) + ); } - else if (attributeArgumentSyntax.NameEquals != null) { - var name = - attributeArgumentSyntax.NameEquals.Name.ToString().Replace("=", "").Trim(); - - properties.Add( - new AttributeArgumentValue( - name, - constantValue - )); + else if (attributeArgumentSyntax.NameEquals != null) + { + var name = attributeArgumentSyntax + .NameEquals.Name.ToString() + .Replace("=", "") + .Trim(); + + properties.Add(new AttributeArgumentValue(name, constantValue)); } - else { - - arguments.Add( - new AttributeArgumentValue( - "", - constantValue - )); + else + { + arguments.Add(new AttributeArgumentValue("", constantValue)); } } } - if (operation.Type == null) { + if (operation.Type == null) + { throw new ArgumentNullException("operation.Type", "The type argument cannot be null."); } var type = operation.Type.GetTypeDefinition(); - if (!type.Name.EndsWith("Attribute")) { + if (!type.Name.EndsWith("Attribute")) + { type = TypeDefinition.Get(type.Namespace, type.Name + "Attribute"); } - return new AttributeModel(type, - arguments, - properties, - GetInterfaces(context, attribute)); + return new AttributeModel(type, arguments, properties, GetInterfaces(context, attribute)); } - private static object? GetOperationValue(SyntaxTransformContext context, IOperation operationValue) { - if (operationValue.ConstantValue.HasValue) { + private static object? GetOperationValue( + SyntaxTransformContext context, + IOperation operationValue + ) + { + if (operationValue.ConstantValue.HasValue) + { return operationValue.ConstantValue.Value; } return GetOperationValue(context, operationValue.Syntax); } - private static object GetOperationValue(SyntaxTransformContext context, SyntaxNode syntaxNode) { - - if (syntaxNode is CollectionExpressionSyntax collectionExpressionSyntax) { + private static object GetOperationValue(SyntaxTransformContext context, SyntaxNode syntaxNode) + { + if (syntaxNode is CollectionExpressionSyntax collectionExpressionSyntax) + { var collection = new List(); - foreach (var elementSyntax in collectionExpressionSyntax.Elements) { + foreach (var elementSyntax in collectionExpressionSyntax.Elements) + { var dec = elementSyntax.DescendantNodes().FirstOrDefault(); - if (dec != null) { + if (dec != null) + { collection.Add(GetOperationValue(context, dec)); } } @@ -208,14 +245,17 @@ private static object GetOperationValue(SyntaxTransformContext context, SyntaxNo return collection.ToArray(); } - if (syntaxNode is LiteralExpressionSyntax literalExpressionSyntax) { + if (syntaxNode is LiteralExpressionSyntax literalExpressionSyntax) + { return literalExpressionSyntax.Token.Value ?? "null"; } - if (syntaxNode is TypeOfExpressionSyntax typeOf) { + if (syntaxNode is TypeOfExpressionSyntax typeOf) + { var type = typeOf.Type.GetTypeDefinition(context); - if (type != null) { + if (type != null) + { // typeof(IRepo<>) binds to the unbound symbol, which carries the declaration's type // parameters as its arguments. Re-emitting that verbatim writes typeof(IRepo) // into the generated module, where T is not in scope — CS0246, in generated code, @@ -228,26 +268,32 @@ private static object GetOperationValue(SyntaxTransformContext context, SyntaxNo } private static IReadOnlyList GetInterfaces( - SyntaxTransformContext context, AttributeSyntax attribute) { + SyntaxTransformContext context, + AttributeSyntax attribute + ) + { var interfaces = new List(); - var symbol = - context.SemanticModel.GetTypeInfo(attribute); + var symbol = context.SemanticModel.GetTypeInfo(attribute); - if (symbol.Type is INamedTypeSymbol namespaceOrTypeSymbol) { - foreach (var interfaceSymbol in namespaceOrTypeSymbol.AllInterfaces) { + if (symbol.Type is INamedTypeSymbol namespaceOrTypeSymbol) + { + foreach (var interfaceSymbol in namespaceOrTypeSymbol.AllInterfaces) + { interfaces.Add(interfaceSymbol.GetTypeDefinition()); } } return interfaces; } + /// /// Whether the type was written as Foo<> rather than closed over anything. /// private static bool IsUnboundGeneric(TypeSyntax type) => - type is GenericNameSyntax generic && - generic.TypeArgumentList.Arguments.Count > 0 && - generic.TypeArgumentList.Arguments.All(argument => argument is OmittedTypeArgumentSyntax); - -} \ No newline at end of file + type is GenericNameSyntax generic + && generic.TypeArgumentList.Arguments.Count > 0 + && generic.TypeArgumentList.Arguments.All(argument => + argument is OmittedTypeArgumentSyntax + ); +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeTypeMatcher.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeTypeMatcher.cs index 14e50d9..6a6e214 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeTypeMatcher.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/AttributeTypeMatcher.cs @@ -26,8 +26,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// node in the compilation must not do. /// /// -public static class AttributeTypeMatcher { - +public static class AttributeTypeMatcher +{ /// /// Whether resolves to . /// @@ -40,11 +40,13 @@ public static bool Matches( SemanticModel semanticModel, AttributeSyntax attributeSyntax, ITypeDefinition attributeType, - CancellationToken cancellationToken) { - + CancellationToken cancellationToken + ) + { var symbol = Resolve(semanticModel, attributeSyntax, cancellationToken); - if (symbol == null) { + if (symbol == null) + { return MatchesAsWritten(attributeSyntax, attributeType); } @@ -60,30 +62,43 @@ public static bool Matches( /// argument list that does not match any overload still names the attribute unambiguously. /// private static INamedTypeSymbol? Resolve( - SemanticModel semanticModel, AttributeSyntax attributeSyntax, CancellationToken cancellationToken) { - + SemanticModel semanticModel, + AttributeSyntax attributeSyntax, + CancellationToken cancellationToken + ) + { var symbolInfo = semanticModel.GetSymbolInfo(attributeSyntax, cancellationToken); - if (symbolInfo.Symbol?.ContainingType is { } containingType) { + if (symbolInfo.Symbol?.ContainingType is { } containingType) + { return containingType; } - if (symbolInfo.CandidateSymbols.Length > 0 && - symbolInfo.CandidateSymbols[0].ContainingType is { } candidateType) { + if ( + symbolInfo.CandidateSymbols.Length > 0 + && symbolInfo.CandidateSymbols[0].ContainingType is { } candidateType + ) + { return candidateType; } - return semanticModel.GetTypeInfo(attributeSyntax, cancellationToken).Type as INamedTypeSymbol; + return semanticModel.GetTypeInfo(attributeSyntax, cancellationToken).Type + as INamedTypeSymbol; } /// /// The old comparison, kept only for the unresolvable case. /// - private static bool MatchesAsWritten(AttributeSyntax attributeSyntax, ITypeDefinition attributeType) { + private static bool MatchesAsWritten( + AttributeSyntax attributeSyntax, + ITypeDefinition attributeType + ) + { var written = attributeSyntax.Name.ToString(); var lastDot = written.LastIndexOf('.'); - if (lastDot >= 0) { + if (lastDot >= 0) + { written = written.Substring(lastDot + 1); } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/BaseAttributeWriter.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/BaseAttributeWriter.cs index 625eeb2..235e573 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/BaseAttributeWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/BaseAttributeWriter.cs @@ -4,8 +4,11 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; -public abstract class BaseAttributeWriter where T : IClassModel { - public void CreateAttributeClass(IConstructContainer container, T model) { +public abstract class BaseAttributeWriter + where T : IClassModel +{ + public void CreateAttributeClass(IConstructContainer container, T model) + { var attributeClass = ConstructClassDefinition(container, model); AddClassTraits(attributeClass); @@ -13,73 +16,108 @@ public void CreateAttributeClass(IConstructContainer container, T model) { CreateConstructor(container, attributeClass, model); CreateProperties(container, attributeClass, model); - + CustomImplementation(container, attributeClass, model); } - private static void AddClassTraits(ClassDefinition attributeClass) { + private static void AddClassTraits(ClassDefinition attributeClass) + { attributeClass.EnableNullable(); attributeClass.WrapInPragma("CS0472"); // Fully qualified: generated code must compile regardless of the consumer's using // directives, and nothing else in this file causes a "using System;" to be emitted. attributeClass.AddLeadingTrait( new UsageAttributeComponent( - "[global::System.AttributeUsage(" + - "global::System.AttributeTargets.Class | " + - "global::System.AttributeTargets.Assembly | " + - "global::System.AttributeTargets.Method | " + - "global::System.AttributeTargets.Parameter, AllowMultiple = true)]")); + "[global::System.AttributeUsage(" + + "global::System.AttributeTargets.Class | " + + "global::System.AttributeTargets.Assembly | " + + "global::System.AttributeTargets.Method | " + + "global::System.AttributeTargets.Parameter, AllowMultiple = true)]" + ) + ); } - protected virtual void CustomImplementation(IConstructContainer container, ClassDefinition attributeClass, T model) { - - } + protected virtual void CustomImplementation( + IConstructContainer container, + ClassDefinition attributeClass, + T model + ) { } + + private static ClassDefinition ConstructClassDefinition(IConstructContainer container, T model) + { + var attributeClass = container.AddClass(model.ClassType.Name + "Attribute"); - private static ClassDefinition ConstructClassDefinition(IConstructContainer container, T model) { - var attributeClass = container.AddClass( model.ClassType.Name + "Attribute"); - attributeClass.Modifiers |= ComponentModifier.Public | ComponentModifier.Partial; - attributeClass.AddBaseType(TypeDefinition.Get("System","Attribute")); - attributeClass.AddBaseType(KnownTypes.DependencyModules.Interfaces.IDependencyModuleProvider); - + attributeClass.AddBaseType(TypeDefinition.Get("System", "Attribute")); + attributeClass.AddBaseType( + KnownTypes.DependencyModules.Interfaces.IDependencyModuleProvider + ); + return attributeClass; } - protected virtual void CreateProperties(IConstructContainer container, ClassDefinition attributeClass, T model) { - foreach (var propertyInfoModel in model.PropertyInfoModels) { - if (!propertyInfoModel.IsModuleParameter) { + protected virtual void CreateProperties( + IConstructContainer container, + ClassDefinition attributeClass, + T model + ) + { + foreach (var propertyInfoModel in model.PropertyInfoModels) + { + if (!propertyInfoModel.IsModuleParameter) + { continue; } var propertyType = propertyInfoModel.PropertyType; - if (propertyType.IsNullable) { - propertyType = TypeDefinition.Get(propertyType.TypeDefinitionEnum, propertyType.Namespace, propertyType.Name, propertyType.IsArray); + if (propertyType.IsNullable) + { + propertyType = TypeDefinition.Get( + propertyType.TypeDefinitionEnum, + propertyType.Namespace, + propertyType.Name, + propertyType.IsArray + ); } var property = attributeClass.AddProperty(propertyType, propertyInfoModel.PropertyName); var stringBuilder = new StringBuilder(); propertyType.WriteTypeName(stringBuilder, TypeOutputMode.Global); - - property.DefaultValue = - new WrapStatement(CodeOutputComponent.Get(stringBuilder.ToString()), "default(", ")!"); + + property.DefaultValue = new WrapStatement( + CodeOutputComponent.Get(stringBuilder.ToString()), + "default(", + ")!" + ); } } - protected virtual void CreateConstructor(IConstructContainer container, ClassDefinition attributeClass, T model) { - if (model.Parameters.Count > 0) { + protected virtual void CreateConstructor( + IConstructContainer container, + ClassDefinition attributeClass, + T model + ) + { + if (model.Parameters.Count > 0) + { var constructor = attributeClass.AddConstructor(); - foreach (var constructorParameter in model.Parameters) { + foreach (var constructorParameter in model.Parameters) + { var field = attributeClass.AddField( - constructorParameter.ParameterType, constructorParameter.ParameterName + "Field"); + constructorParameter.ParameterType, + constructorParameter.ParameterName + "Field" + ); - var parameter = - constructor.AddParameter(constructorParameter.ParameterType, constructorParameter.ParameterName); + var parameter = constructor.AddParameter( + constructorParameter.ParameterType, + constructorParameter.ParameterName + ); constructor.Assign(parameter).To(field.Name); } } } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/BaseMethodHelper.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/BaseMethodHelper.cs index a451b4a..115b52b 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/BaseMethodHelper.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/BaseMethodHelper.cs @@ -5,34 +5,50 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; -public static class BaseMethodHelper { +public static class BaseMethodHelper +{ public static IReadOnlyList GetMethodParameters( this BaseMethodDeclarationSyntax methodDeclarationSyntax, SyntaxTransformContext context, CancellationToken cancellationToken - ) { + ) + { var parameterList = methodDeclarationSyntax.ParameterList; return GetParameters(parameterList, context, cancellationToken); } - public static IReadOnlyList GetParameters(this ParameterListSyntax? parameterList, SyntaxTransformContext context, CancellationToken cancellationToken) { - if (parameterList == null) { + public static IReadOnlyList GetParameters( + this ParameterListSyntax? parameterList, + SyntaxTransformContext context, + CancellationToken cancellationToken + ) + { + if (parameterList == null) + { return Array.Empty(); } - + var list = new List(); - foreach (var parameterSyntax in parameterList.Parameters) { + foreach (var parameterSyntax in parameterList.Parameters) + { cancellationToken.ThrowIfCancellationRequested(); - list.Add(new ParameterInfoModel( - parameterSyntax.Identifier.ToString(), - parameterSyntax.Type?.GetTypeDefinition(context) ?? TypeDefinition.Get(typeof(object)), - null, - AttributeModelHelper.GetAttributeModels(context, parameterSyntax, cancellationToken) - )); + list.Add( + new ParameterInfoModel( + parameterSyntax.Identifier.ToString(), + parameterSyntax.Type?.GetTypeDefinition(context) + ?? TypeDefinition.Get(typeof(object)), + null, + AttributeModelHelper.GetAttributeModels( + context, + parameterSyntax, + cancellationToken + ) + ) + ); } return list; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/CollectionSyntaxDeclaration.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/CollectionSyntaxDeclaration.cs index 17de9db..403e59d 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/CollectionSyntaxDeclaration.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/CollectionSyntaxDeclaration.cs @@ -2,32 +2,41 @@ namespace DependencyModules.SourceGenerator.Impl; -public class CollectionSyntaxDeclaration : BaseOutputComponent { +public class CollectionSyntaxDeclaration : BaseOutputComponent +{ private List _items = new(); - public void Add(object item) { + public void Add(object item) + { _items.Add(item); } - protected override void WriteComponentOutput(IOutputContext outputContext) { + protected override void WriteComponentOutput(IOutputContext outputContext) + { outputContext.Write("["); var first = true; - foreach (var value in _items) { - if (first == false) { + foreach (var value in _items) + { + if (first == false) + { outputContext.Write(", "); } - else { + else + { first = false; } - - if (value is IOutputComponent component) { + + if (value is IOutputComponent component) + { component.WriteOutput(outputContext); } - else if (value is string str) { + else if (value is string str) + { outputContext.Write(SyntaxHelpers.QuoteString(str)); } - else { + else + { outputContext.Write(value.ToString()); } } @@ -35,14 +44,19 @@ protected override void WriteComponentOutput(IOutputContext outputContext) { outputContext.Write("]"); } - public override bool Equals(object? obj) { - if (obj is CollectionSyntaxDeclaration other) { - if (other._items.Count != _items.Count) { + public override bool Equals(object? obj) + { + if (obj is CollectionSyntaxDeclaration other) + { + if (other._items.Count != _items.Count) + { return false; } - for (var i = 0; i < _items.Count; i++) { - if (other._items[i].Equals(_items[i]) == false) { + for (var i = 0; i < _items.Count; i++) + { + if (other._items[i].Equals(_items[i]) == false) + { return false; } } @@ -53,7 +67,8 @@ public override bool Equals(object? obj) { return false; } - public override int GetHashCode() { + public override int GetHashCode() + { return base.GetHashCode(); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/CompilerService.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/CompilerService.cs index dcde369..ed1a03e 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/CompilerService.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/CompilerService.cs @@ -1,3 +1,3 @@ namespace System.Runtime.CompilerServices; -internal static class IsExternalInit { } \ No newline at end of file +internal static class IsExternalInit { } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/ConstructorArgumentWriter.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/ConstructorArgumentWriter.cs index 9d778b1..218e45f 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/ConstructorArgumentWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/ConstructorArgumentWriter.cs @@ -23,14 +23,15 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// The decorator writer duplicated this and got the second one wrong, which is why it now lives in /// one place. /// -public static class ConstructorArgumentWriter { - +public static class ConstructorArgumentWriter +{ /// /// Arguments for every parameter. /// public static object[] Arguments( - ParameterDefinition serviceProvider, IReadOnlyList parameters) => - Arguments(serviceProvider, parameters, -1, null); + ParameterDefinition serviceProvider, + IReadOnlyList parameters + ) => Arguments(serviceProvider, parameters, -1, null); /// /// Arguments for every parameter, with one supplied rather than resolved. @@ -44,12 +45,15 @@ public static object[] Arguments( ParameterDefinition serviceProvider, IReadOnlyList parameters, int suppliedIndex, - object? supplied) { - + object? supplied + ) + { var arguments = new List(parameters.Count); - for (var i = 0; i < parameters.Count; i++) { - if (i == suppliedIndex && supplied != null) { + for (var i = 0; i < parameters.Count; i++) + { + if (i == suppliedIndex && supplied != null) + { arguments.Add(supplied); continue; @@ -61,28 +65,42 @@ public static object[] Arguments( return arguments.ToArray(); } - private static object Argument(ParameterDefinition serviceProvider, ParameterInfoModel parameter) { - if (parameter.ParameterType.Equals(KnownTypes.Microsoft.DependencyInjection.IServiceProvider)) { + private static object Argument( + ParameterDefinition serviceProvider, + ParameterInfoModel parameter + ) + { + if ( + parameter.ParameterType.Equals( + KnownTypes.Microsoft.DependencyInjection.IServiceProvider + ) + ) + { return serviceProvider; } - var keyed = parameter.Attributes.FirstOrDefault( - attribute => attribute.TypeDefinition.Equals( - KnownTypes.Microsoft.DependencyInjection.FromKeyedServicesAttribute)); + var keyed = parameter.Attributes.FirstOrDefault(attribute => + attribute.TypeDefinition.Equals( + KnownTypes.Microsoft.DependencyInjection.FromKeyedServicesAttribute + ) + ); var name = "Get"; var arguments = new List(); - if (!parameter.ParameterType.IsNullable) { + if (!parameter.ParameterType.IsNullable) + { name += "Required"; } - if (keyed != null) { + if (keyed != null) + { name += "Keyed"; var key = keyed.Arguments.First().Value!; - if (key is string text) { + if (key is string text) + { key = QuoteString(text); } @@ -94,6 +112,7 @@ private static object Argument(ParameterDefinition serviceProvider, ParameterInf return serviceProvider.InvokeGeneric( name, new[] { parameter.ParameterType.MakeNullable(false) }, - arguments.ToArray()); + arguments.ToArray() + ); } } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorConstraintChecker.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorConstraintChecker.cs index 9bfd641..b58083a 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorConstraintChecker.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorConstraintChecker.cs @@ -21,21 +21,30 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// which is better than a decoration going missing for a reason nothing reports. /// /// -public static class DecoratorConstraintChecker { - +public static class DecoratorConstraintChecker +{ public static bool CanClose( - Compilation compilation, ITypeDefinition decoratorType, GenericTypeDefinition closedService) { - + Compilation compilation, + ITypeDefinition decoratorType, + GenericTypeDefinition closedService + ) + { var decorator = Resolve(compilation, decoratorType); - if (decorator == null || decorator.TypeParameters.Length != closedService.TypeArguments.Count) { + if ( + decorator == null + || decorator.TypeParameters.Length != closedService.TypeArguments.Count + ) + { return true; } - for (var i = 0; i < decorator.TypeParameters.Length; i++) { + for (var i = 0; i < decorator.TypeParameters.Length; i++) + { var argument = Resolve(compilation, closedService.TypeArguments[i]); - if (argument != null && !Satisfies(decorator.TypeParameters[i], argument)) { + if (argument != null && !Satisfies(decorator.TypeParameters[i], argument)) + { return false; } } @@ -43,24 +52,33 @@ public static bool CanClose( return true; } - private static bool Satisfies(ITypeParameterSymbol parameter, INamedTypeSymbol argument) { - if (parameter.HasReferenceTypeConstraint && !argument.IsReferenceType) { + private static bool Satisfies(ITypeParameterSymbol parameter, INamedTypeSymbol argument) + { + if (parameter.HasReferenceTypeConstraint && !argument.IsReferenceType) + { return false; } - if (parameter.HasValueTypeConstraint && !argument.IsValueType) { + if (parameter.HasValueTypeConstraint && !argument.IsValueType) + { return false; } - if (parameter.HasConstructorConstraint && - !argument.InstanceConstructors.Any( - constructor => constructor.Parameters.Length == 0 && - constructor.DeclaredAccessibility == Accessibility.Public)) { + if ( + parameter.HasConstructorConstraint + && !argument.InstanceConstructors.Any(constructor => + constructor.Parameters.Length == 0 + && constructor.DeclaredAccessibility == Accessibility.Public + ) + ) + { return false; } - foreach (var constraint in parameter.ConstraintTypes) { - if (!Implements(argument, constraint)) { + foreach (var constraint in parameter.ConstraintTypes) + { + if (!Implements(argument, constraint)) + { return false; } } @@ -68,25 +86,42 @@ private static bool Satisfies(ITypeParameterSymbol parameter, INamedTypeSymbol a return true; } - private static bool Implements(INamedTypeSymbol argument, ITypeSymbol constraint) { + private static bool Implements(INamedTypeSymbol argument, ITypeSymbol constraint) + { // A constraint naming another type parameter cannot be checked without the whole // substitution, and the service's own constraints already cover the usual case. - if (constraint is ITypeParameterSymbol) { + if (constraint is ITypeParameterSymbol) + { return true; } - if (SymbolEqualityComparer.Default.Equals(argument, constraint)) { + if (SymbolEqualityComparer.Default.Equals(argument, constraint)) + { return true; } - foreach (var implemented in argument.AllInterfaces) { - if (SymbolEqualityComparer.Default.Equals(implemented.OriginalDefinition, constraint.OriginalDefinition)) { + foreach (var implemented in argument.AllInterfaces) + { + if ( + SymbolEqualityComparer.Default.Equals( + implemented.OriginalDefinition, + constraint.OriginalDefinition + ) + ) + { return true; } } - for (var baseType = argument.BaseType; baseType != null; baseType = baseType.BaseType) { - if (SymbolEqualityComparer.Default.Equals(baseType.OriginalDefinition, constraint.OriginalDefinition)) { + for (var baseType = argument.BaseType; baseType != null; baseType = baseType.BaseType) + { + if ( + SymbolEqualityComparer.Default.Equals( + baseType.OriginalDefinition, + constraint.OriginalDefinition + ) + ) + { return true; } } @@ -102,23 +137,40 @@ private static bool Implements(INamedTypeSymbol argument, ITypeSymbol constraint /// GetTypeByMetadataName("int") finds nothing — so a value type would look unresolvable /// and be allowed through, which is exactly the case this class exists to catch. /// - private static readonly Dictionary Aliases = new() { - ["bool"] = "System.Boolean", ["byte"] = "System.Byte", ["sbyte"] = "System.SByte", - ["char"] = "System.Char", ["decimal"] = "System.Decimal", ["double"] = "System.Double", - ["float"] = "System.Single", ["int"] = "System.Int32", ["uint"] = "System.UInt32", - ["long"] = "System.Int64", ["ulong"] = "System.UInt64", ["short"] = "System.Int16", - ["ushort"] = "System.UInt16", ["nint"] = "System.IntPtr", ["nuint"] = "System.UIntPtr", - ["object"] = "System.Object", ["string"] = "System.String", + private static readonly Dictionary Aliases = new() + { + ["bool"] = "System.Boolean", + ["byte"] = "System.Byte", + ["sbyte"] = "System.SByte", + ["char"] = "System.Char", + ["decimal"] = "System.Decimal", + ["double"] = "System.Double", + ["float"] = "System.Single", + ["int"] = "System.Int32", + ["uint"] = "System.UInt32", + ["long"] = "System.Int64", + ["ulong"] = "System.UInt64", + ["short"] = "System.Int16", + ["ushort"] = "System.UInt16", + ["nint"] = "System.IntPtr", + ["nuint"] = "System.UIntPtr", + ["object"] = "System.Object", + ["string"] = "System.String", }; - private static INamedTypeSymbol? Resolve(Compilation compilation, ITypeDefinition type) { - var name = string.IsNullOrEmpty(type.Namespace) ? type.Name : type.Namespace + "." + type.Name; + private static INamedTypeSymbol? Resolve(Compilation compilation, ITypeDefinition type) + { + var name = string.IsNullOrEmpty(type.Namespace) + ? type.Name + : type.Namespace + "." + type.Name; - if (Aliases.TryGetValue(name, out var metadataName)) { + if (Aliases.TryGetValue(name, out var metadataName)) + { name = metadataName; } - if (type is GenericTypeDefinition { TypeArguments.Count: > 0 } generic) { + if (type is GenericTypeDefinition { TypeArguments.Count: > 0 } generic) + { name += "`" + generic.TypeArguments.Count; } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorExpansion.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorExpansion.cs index c9f1a75..f27a829 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorExpansion.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorExpansion.cs @@ -20,8 +20,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// name the same closed service. /// /// -public static class DecoratorExpansion { - +public static class DecoratorExpansion +{ /// /// Decorators that name a service the compilation registers as an open generic. Nothing can be /// emitted for those — see — and the caller reports @@ -32,23 +32,28 @@ public static IReadOnlyList Expand( IReadOnlyList registeredServiceTypes, out IReadOnlyList refusedForOpenGenericRegistration, bool includeNonGeneric = true, - Func? canClose = null) { - + Func? canClose = null + ) + { var expanded = new List(decorators.Count); List? refused = null; - foreach (var decorator in decorators) { - if (decorator.IsIgnored) { + foreach (var decorator in decorators) + { + if (decorator.IsIgnored) + { continue; } - if (!decorator.IsOpenGeneric) { + if (!decorator.IsOpenGeneric) + { // An unbound service type has no legal emission at all: Decorate> is // CS7003. A generic decorator reaches this state only when nothing closed it, and is // handled below; a non-generic one never had an expansion step to catch it, so this // is where it stops. Refused whatever is registered, because the emission is invalid // on its own terms. - if (decorator.HasUnboundServiceType) { + if (decorator.HasUnboundServiceType) + { (refused ??= new List()).Add(decorator); continue; @@ -59,7 +64,8 @@ public static IReadOnlyList Expand( // by generated code is dropped: generated code builds the decorator with a literal // new, and the reflective overload that used to stand in for this is gone because // it never worked in a published application. - if (includeNonGeneric && decorator.CanMonomorphise) { + if (includeNonGeneric && decorator.CanMonomorphise) + { expanded.Add(decorator); } @@ -68,8 +74,10 @@ public static IReadOnlyList Expand( var closedCount = 0; - foreach (var serviceType in registeredServiceTypes) { - if (!ClosesTheSameGeneric(serviceType, decorator.ServiceType)) { + foreach (var serviceType in registeredServiceTypes) + { + if (!ClosesTheSameGeneric(serviceType, decorator.ServiceType)) + { continue; } @@ -77,13 +85,15 @@ public static IReadOnlyList Expand( // A decorator may constrain its type parameters more tightly than the service does. // Closing it over an argument that violates one emits code that does not compile. - if (canClose != null && !canClose(decorator.DecoratorType, closedService)) { + if (canClose != null && !canClose(decorator.DecoratorType, closedService)) + { continue; } var closed = DecoratorTypeUtility.Close(decorator, closedService); - if (closed == null) { + if (closed == null) + { continue; } @@ -96,12 +106,17 @@ public static IReadOnlyList Expand( // reporting, while a compilation that registers nothing at all is the ordinary // cross-assembly case — [Decorate] exists to name a service someone else registers, so // reporting that would fire on the feature's primary use. - if (closedCount == 0 && NamesAnOpenGenericRegistration(decorator, registeredServiceTypes)) { + if ( + closedCount == 0 + && NamesAnOpenGenericRegistration(decorator, registeredServiceTypes) + ) + { (refused ??= new List()).Add(decorator); } } - refusedForOpenGenericRegistration = (IReadOnlyList?)refused ?? Array.Empty(); + refusedForOpenGenericRegistration = + (IReadOnlyList?)refused ?? Array.Empty(); return expanded; } @@ -114,19 +129,25 @@ public static IReadOnlyList Expand( /// services.AddSingleton(typeof(IStore<>), typeof(Store<>)) produces. /// private static bool NamesAnOpenGenericRegistration( - DecoratorModel decorator, IReadOnlyList registeredServiceTypes) { - - if (decorator.ServiceType is not GenericTypeDefinition decorated) { + DecoratorModel decorator, + IReadOnlyList registeredServiceTypes + ) + { + if (decorator.ServiceType is not GenericTypeDefinition decorated) + { return false; } - foreach (var registered in registeredServiceTypes) { - if (registered is GenericTypeDefinition open && - open.TypeArguments.Count == decorated.TypeArguments.Count && - open.Name == decorated.Name && - open.Namespace == decorated.Namespace && - open.TypeArguments.All(argument => string.IsNullOrEmpty(argument.Name))) { - + foreach (var registered in registeredServiceTypes) + { + if ( + registered is GenericTypeDefinition open + && open.TypeArguments.Count == decorated.TypeArguments.Count + && open.Name == decorated.Name + && open.Namespace == decorated.Namespace + && open.TypeArguments.All(argument => string.IsNullOrEmpty(argument.Name)) + ) + { return true; } } @@ -137,12 +158,16 @@ private static bool NamesAnOpenGenericRegistration( /// /// Whether a registered service type is a closed construction of the decorated open generic. /// - private static bool ClosesTheSameGeneric(ITypeDefinition registered, ITypeDefinition decorated) => - registered is GenericTypeDefinition closed && - decorated is GenericTypeDefinition open && - closed.TypeArguments.Count == open.TypeArguments.Count && - closed.Name == open.Name && - closed.Namespace == open.Namespace && + private static bool ClosesTheSameGeneric( + ITypeDefinition registered, + ITypeDefinition decorated + ) => + registered is GenericTypeDefinition closed + && decorated is GenericTypeDefinition open + && closed.TypeArguments.Count == open.TypeArguments.Count + && closed.Name == open.Name + && closed.Namespace == open.Namespace + && // The decorated form has its arguments blanked; a registration that also has them blanked is // an open generic registration, which cannot be decorated at all. closed.TypeArguments.Any(argument => !string.IsNullOrEmpty(argument.Name)); diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs index 0bda5a8..728a2f6 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs @@ -9,21 +9,27 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// Builds instances from the two declaration surfaces: /// [Decorator] on the decorator class, and [Decorate] on a module. /// -public static class DecoratorModelUtility { - +public static class DecoratorModelUtility +{ /// /// Reads a [Decorator] class declaration. /// - public static DecoratorModel? GetDecoratorModel(SyntaxTransformContext context, CancellationToken cancellationToken) { + public static DecoratorModel? GetDecoratorModel( + SyntaxTransformContext context, + CancellationToken cancellationToken + ) + { cancellationToken.ThrowIfCancellationRequested(); - if (context.Node is not TypeDeclarationSyntax typeDeclarationSyntax) { + if (context.Node is not TypeDeclarationSyntax typeDeclarationSyntax) + { return null; } var attribute = FindAttribute(typeDeclarationSyntax, "Decorator"); - if (attribute == null) { + if (attribute == null) + { return null; } @@ -35,11 +41,15 @@ public static class DecoratorModelUtility { ITypeDefinition? explicitService = null; ITypeDefinition? implementation = null; - if (attribute.ArgumentList != null) { - foreach (var argument in attribute.ArgumentList.Arguments) { - switch (argument.NameEquals?.Name.ToString()) { + if (attribute.ArgumentList != null) + { + foreach (var argument in attribute.ArgumentList.Arguments) + { + switch (argument.NameEquals?.Name.ToString()) + { case "Order": - if (int.TryParse(argument.Expression.ToString(), out var parsed)) { + if (int.TryParse(argument.Expression.ToString(), out var parsed)) + { order = parsed; } break; @@ -56,9 +66,11 @@ public static class DecoratorModelUtility { } } - var written = explicitService ?? InferDecoratedService(typeDeclarationSyntax, context, implemented); + var written = + explicitService ?? InferDecoratedService(typeDeclarationSyntax, context, implemented); - if (written == null) { + if (written == null) + { return null; } @@ -67,17 +79,24 @@ public static class DecoratorModelUtility { // A generic decorator decorates the open service. Its base list names the service closed over // its own type parameters, as IHandler; the unbound IHandler<> is the form the model // carries, and the closed constructions to emit against are worked out from the registrations. - if (decoratorType is GenericTypeDefinition { TypeArguments.Count: > 0 }) { + if (decoratorType is GenericTypeDefinition { TypeArguments.Count: > 0 }) + { serviceType = ToUnboundGeneric(written); } // Read from the decorator class, exactly as they are for a service. A decorator is a // registration like any other, and one gated on Development has no other way to say so. var conditions = EnvironmentConditionUtility.GetConditions( - context, typeDeclarationSyntax, cancellationToken); + context, + typeDeclarationSyntax, + cancellationToken + ); var constructor = ServiceModelUtility.GetConstructorInfo( - context, typeDeclarationSyntax, cancellationToken); + context, + typeDeclarationSyntax, + cancellationToken + ); return new DecoratorModel( serviceType, @@ -89,7 +108,8 @@ public static class DecoratorModelUtility { IndexOfInnerParameter(constructor, written), TypeParametersMatchService(typeDeclarationSyntax, written), implementation, - LocationModel.From(typeDeclarationSyntax)); + LocationModel.From(typeDeclarationSyntax) + ); } /// @@ -102,15 +122,22 @@ public static class DecoratorModelUtility { /// carries an annotation the service type does not, and comparing them as written finds no /// parameter at all — which drops the decoration with nothing said. /// - private static int IndexOfInnerParameter(ConstructorInfoModel? constructor, ITypeDefinition serviceType) { - if (constructor == null) { + private static int IndexOfInnerParameter( + ConstructorInfoModel? constructor, + ITypeDefinition serviceType + ) + { + if (constructor == null) + { return -1; } var wanted = serviceType.MakeNullable(false); - for (var i = 0; i < constructor.Parameters.Count; i++) { - if (constructor.Parameters[i].ParameterType.MakeNullable(false).Equals(wanted)) { + for (var i = 0; i < constructor.Parameters.Count; i++) + { + if (constructor.Parameters[i].ParameterType.MakeNullable(false).Equals(wanted)) + { return i; } } @@ -129,21 +156,29 @@ private static int IndexOfInnerParameter(ConstructorInfoModel? constructor, ITyp /// emitted for them instead. /// private static bool TypeParametersMatchService( - TypeDeclarationSyntax typeDeclarationSyntax, ITypeDefinition serviceType) { - + TypeDeclarationSyntax typeDeclarationSyntax, + ITypeDefinition serviceType + ) + { var declared = typeDeclarationSyntax.TypeParameterList?.Parameters; - if (declared is not { Count: > 0 }) { + if (declared is not { Count: > 0 }) + { return true; } - if (serviceType is not GenericTypeDefinition generic || - generic.TypeArguments.Count != declared.Value.Count) { + if ( + serviceType is not GenericTypeDefinition generic + || generic.TypeArguments.Count != declared.Value.Count + ) + { return false; } - for (var i = 0; i < declared.Value.Count; i++) { - if (generic.TypeArguments[i].Name != declared.Value[i].Identifier.Text) { + for (var i = 0; i < declared.Value.Count; i++) + { + if (generic.TypeArguments[i].Name != declared.Value[i].Identifier.Text) + { return false; } } @@ -154,28 +189,46 @@ private static bool TypeParametersMatchService( /// /// Reads the [Decorate(service, decorator)] attributes declared on a module. /// - public static IEnumerable GetModuleDeclaredDecorators(ModuleEntryPointModel entryPointModel) { - foreach (var attribute in entryPointModel.AttributeModels) { - if (attribute.TypeDefinition.Name is not ("DecorateAttribute" or "Decorate")) { + public static IEnumerable GetModuleDeclaredDecorators( + ModuleEntryPointModel entryPointModel + ) + { + foreach (var attribute in entryPointModel.AttributeModels) + { + if (attribute.TypeDefinition.Name is not ("DecorateAttribute" or "Decorate")) + { continue; } - if (attribute.Arguments.Count < 2 || - attribute.Arguments[0].Value is not ITypeDefinition service || - attribute.Arguments[1].Value is not ITypeDefinition decorator) { + if ( + attribute.Arguments.Count < 2 + || attribute.Arguments[0].Value is not ITypeDefinition service + || attribute.Arguments[1].Value is not ITypeDefinition decorator + ) + { continue; } var order = 0; - foreach (var property in attribute.Properties) { - if (property.Name == "Order" && property.Value != null && - int.TryParse(property.Value.ToString(), out var parsed)) { + foreach (var property in attribute.Properties) + { + if ( + property.Name == "Order" + && property.Value != null + && int.TryParse(property.Value.ToString(), out var parsed) + ) + { order = parsed; } } - yield return new DecoratorModel(service, decorator, order, entryPointModel.EntryPointType); + yield return new DecoratorModel( + service, + decorator, + order, + entryPointModel.EntryPointType + ); } } @@ -187,20 +240,25 @@ attribute.Arguments[0].Value is not ITypeDefinition service || private static ITypeDefinition? InferDecoratedService( TypeDeclarationSyntax typeDeclarationSyntax, SyntaxTransformContext context, - IReadOnlyList implemented) { - - if (implemented.Count == 0) { + IReadOnlyList implemented + ) + { + if (implemented.Count == 0) + { return null; } - foreach (var parameterType in GetConstructorParameterTypes(typeDeclarationSyntax, context)) { + foreach (var parameterType in GetConstructorParameterTypes(typeDeclarationSyntax, context)) + { // Normalised, because `IGreeter? inner` is legal and its parameter type carries an // annotation the implemented interface does not. Compared as written, no parameter looks // like the service and the class stops being a decorator at all — silently. var declared = parameterType.MakeNullable(false); - foreach (var candidate in implemented) { - if (candidate.MakeNullable(false).Equals(declared)) { + foreach (var candidate in implemented) + { + if (candidate.MakeNullable(false).Equals(declared)) + { return candidate; } } @@ -210,23 +268,33 @@ attribute.Arguments[0].Value is not ITypeDefinition service || } private static IEnumerable GetConstructorParameterTypes( - TypeDeclarationSyntax typeDeclarationSyntax, SyntaxTransformContext context) { - - if (typeDeclarationSyntax.ParameterList != null) { - foreach (var parameter in typeDeclarationSyntax.ParameterList.Parameters) { + TypeDeclarationSyntax typeDeclarationSyntax, + SyntaxTransformContext context + ) + { + if (typeDeclarationSyntax.ParameterList != null) + { + foreach (var parameter in typeDeclarationSyntax.ParameterList.Parameters) + { var type = parameter.Type?.GetTypeDefinition(context); - if (type != null) { + if (type != null) + { yield return type; } } } - foreach (var constructor in typeDeclarationSyntax.Members.OfType()) { - foreach (var parameter in constructor.ParameterList.Parameters) { + foreach ( + var constructor in typeDeclarationSyntax.Members.OfType() + ) + { + foreach (var parameter in constructor.ParameterList.Parameters) + { var type = parameter.Type?.GetTypeDefinition(context); - if (type != null) { + if (type != null) + { yield return type; } } @@ -234,18 +302,23 @@ private static IEnumerable GetConstructorParameterTypes( } private static IReadOnlyList GetImplementedInterfaces( - TypeDeclarationSyntax typeDeclarationSyntax, SyntaxTransformContext context) { - + TypeDeclarationSyntax typeDeclarationSyntax, + SyntaxTransformContext context + ) + { var interfaces = new List(); - if (typeDeclarationSyntax.BaseList == null) { + if (typeDeclarationSyntax.BaseList == null) + { return interfaces; } - foreach (var baseType in typeDeclarationSyntax.BaseList.Types) { + foreach (var baseType in typeDeclarationSyntax.BaseList.Types) + { var type = baseType.Type.GetTypeDefinition(context); - if (type != null) { + if (type != null) + { interfaces.Add(type); } } @@ -256,8 +329,10 @@ private static IReadOnlyList GetImplementedInterfaces( /// /// Rewrites a generic type so its arguments render as the unbound <> form. /// - private static ITypeDefinition ToUnboundGeneric(ITypeDefinition type) { - if (type is not GenericTypeDefinition { TypeArguments.Count: > 0 } generic) { + private static ITypeDefinition ToUnboundGeneric(ITypeDefinition type) + { + if (type is not GenericTypeDefinition { TypeArguments.Count: > 0 } generic) + { return type; } @@ -265,41 +340,62 @@ private static ITypeDefinition ToUnboundGeneric(ITypeDefinition type) { generic.TypeDefinitionEnum, generic.Namespace, generic.Name, - generic.TypeArguments.Select(_ => (ITypeDefinition)TypeDefinition.Get("", "")).ToArray()); + generic.TypeArguments.Select(_ => (ITypeDefinition)TypeDefinition.Get("", "")).ToArray() + ); } - private static ITypeDefinition GetDeclaredType(TypeDeclarationSyntax typeDeclarationSyntax, SyntaxTransformContext context) { + private static ITypeDefinition GetDeclaredType( + TypeDeclarationSyntax typeDeclarationSyntax, + SyntaxTransformContext context + ) + { var name = typeDeclarationSyntax.Identifier.ToString(); - foreach (var containing in typeDeclarationSyntax.Ancestors().OfType()) { + foreach ( + var containing in typeDeclarationSyntax.Ancestors().OfType() + ) + { name = containing.Identifier + "." + name; } var namespaceName = typeDeclarationSyntax.GetNamespace(); - if (typeDeclarationSyntax.TypeParameterList is { Parameters.Count: > 0 } parameters) { + if (typeDeclarationSyntax.TypeParameterList is { Parameters.Count: > 0 } parameters) + { return new GenericTypeDefinition( TypeDefinitionEnum.ClassDefinition, namespaceName, name, - parameters.Parameters.Select(_ => TypeDefinition.Get("", "")).ToArray()); + parameters.Parameters.Select(_ => TypeDefinition.Get("", "")).ToArray() + ); } return TypeDefinition.Get(namespaceName, name); } - private static ITypeDefinition? GetTypeOfArgument(AttributeArgumentSyntax argument, SyntaxTransformContext context) { + private static ITypeDefinition? GetTypeOfArgument( + AttributeArgumentSyntax argument, + SyntaxTransformContext context + ) + { return argument.Expression is TypeOfExpressionSyntax typeOf ? typeOf.Type.GetTypeDefinition(context) : null; } - private static AttributeSyntax? FindAttribute(TypeDeclarationSyntax typeDeclarationSyntax, string name) { - foreach (var attributeList in typeDeclarationSyntax.AttributeLists) { - foreach (var attribute in attributeList.Attributes) { + private static AttributeSyntax? FindAttribute( + TypeDeclarationSyntax typeDeclarationSyntax, + string name + ) + { + foreach (var attributeList in typeDeclarationSyntax.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { var attributeName = attribute.Name.ToString(); - if (attributeName == name || attributeName == name + "Attribute") { + if (attributeName == name || attributeName == name + "Attribute") + { return attribute; } } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorTypeUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorTypeUtility.cs index f26c343..cc9a9c7 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorTypeUtility.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorTypeUtility.cs @@ -14,41 +14,53 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// a decorator taking IValidator<TReq> resolves IValidator<CreateOrder> /// rather than something the compiler would reject. /// -public static class DecoratorTypeUtility { - +public static class DecoratorTypeUtility +{ /// /// The decoration to emit for one closed registration, or null when the decorator cannot be /// closed over it. /// - public static DecoratorModel? Close(DecoratorModel decorator, GenericTypeDefinition closedService) { - if (!decorator.CanMonomorphise) { + public static DecoratorModel? Close( + DecoratorModel decorator, + GenericTypeDefinition closedService + ) + { + if (!decorator.CanMonomorphise) + { return null; } var parameterNames = TypeParameterNames(decorator); - if (parameterNames == null || parameterNames.Count != closedService.TypeArguments.Count) { + if (parameterNames == null || parameterNames.Count != closedService.TypeArguments.Count) + { return null; } var substitutions = new Dictionary(parameterNames.Count); - for (var i = 0; i < parameterNames.Count; i++) { + for (var i = 0; i < parameterNames.Count; i++) + { substitutions[parameterNames[i]] = closedService.TypeArguments[i]; } var parameters = new List(decorator.Constructor!.Parameters.Count); - foreach (var parameter in decorator.Constructor.Parameters) { - parameters.Add(parameter with { - ParameterType = Substitute(parameter.ParameterType, substitutions) - }); + foreach (var parameter in decorator.Constructor.Parameters) + { + parameters.Add( + parameter with + { + ParameterType = Substitute(parameter.ParameterType, substitutions), + } + ); } - return decorator with { + return decorator with + { ServiceType = closedService, DecoratorType = CloseDecorator(decorator.DecoratorType, closedService.TypeArguments), - Constructor = new ConstructorInfoModel(parameters) + Constructor = new ConstructorInfoModel(parameters), }; } @@ -64,17 +76,21 @@ public static class DecoratorTypeUtility { /// Safe only because is true, which is /// what guarantees these names are also the decorator's own parameters, in the same order. /// - private static IReadOnlyList? TypeParameterNames(DecoratorModel decorator) { + private static IReadOnlyList? TypeParameterNames(DecoratorModel decorator) + { var inner = decorator.Constructor!.Parameters[decorator.InnerParameterIndex].ParameterType; - if (inner is not GenericTypeDefinition generic || generic.TypeArguments.Count == 0) { + if (inner is not GenericTypeDefinition generic || generic.TypeArguments.Count == 0) + { return null; } var names = new List(generic.TypeArguments.Count); - foreach (var argument in generic.TypeArguments) { - if (string.IsNullOrEmpty(argument.Name)) { + foreach (var argument in generic.TypeArguments) + { + if (string.IsNullOrEmpty(argument.Name)) + { return null; } @@ -85,32 +101,48 @@ public static class DecoratorTypeUtility { } private static ITypeDefinition CloseDecorator( - ITypeDefinition decoratorType, IReadOnlyList typeArguments) => + ITypeDefinition decoratorType, + IReadOnlyList typeArguments + ) => decoratorType is GenericTypeDefinition generic ? new GenericTypeDefinition( - generic.TypeDefinitionEnum, generic.Namespace, generic.Name, typeArguments.ToArray()) + generic.TypeDefinitionEnum, + generic.Namespace, + generic.Name, + typeArguments.ToArray() + ) : decoratorType; /// /// Replaces type parameters with the arguments the registration closed them over, at any depth. /// private static ITypeDefinition Substitute( - ITypeDefinition type, Dictionary substitutions) { - - if (type is GenericTypeDefinition generic) { + ITypeDefinition type, + Dictionary substitutions + ) + { + if (type is GenericTypeDefinition generic) + { var arguments = new ITypeDefinition[generic.TypeArguments.Count]; - for (var i = 0; i < arguments.Length; i++) { + for (var i = 0; i < arguments.Length; i++) + { arguments[i] = Substitute(generic.TypeArguments[i], substitutions); } return new GenericTypeDefinition( - generic.TypeDefinitionEnum, generic.Namespace, generic.Name, arguments); + generic.TypeDefinitionEnum, + generic.Namespace, + generic.Name, + arguments + ); } // A type parameter has no namespace; anything with one is an ordinary type and is left alone // even if it shares a name with a parameter. - return string.IsNullOrEmpty(type.Namespace) && substitutions.TryGetValue(type.Name, out var closed) + return + string.IsNullOrEmpty(type.Namespace) + && substitutions.TryGetValue(type.Name, out var closed) ? closed : type; } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/EntryModelUtil.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/EntryModelUtil.cs index 9e920ba..46c805b 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/EntryModelUtil.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/EntryModelUtil.cs @@ -1,11 +1,12 @@ using System.Collections.Immutable; -using CSharpAuthor; using System.Text.RegularExpressions; +using CSharpAuthor; using DependencyModules.SourceGenerator.Impl.Models; namespace DependencyModules.SourceGenerator.Impl.Utilities; -public class EntryModelUtil { +public class EntryModelUtil +{ /// /// The declared module an auto-generated ApplicationModule should defer to, or null when /// it has to carry its own registrations. @@ -33,28 +34,41 @@ public class EntryModelUtil { /// /// public static ITypeDefinition? DelegateTargetFor( - ModuleEntryPointModel entryPointModel, IEnumerable allEntryPoints) { - - if (!entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule)) { + ModuleEntryPointModel entryPointModel, + IEnumerable allEntryPoints + ) + { + if (!entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule)) + { return null; } ModuleEntryPointModel? target = null; - foreach (var candidate in allEntryPoints) { - if (candidate.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule) || - candidate.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.OnlyRealm) || - candidate.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.NotPartial)) { + foreach (var candidate in allEntryPoints) + { + if ( + candidate.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule) + || candidate.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.OnlyRealm) + || candidate.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.NotPartial) + ) + { continue; } // A module the caller has to supply arguments for cannot be constructed by the auto // module, which has nothing to pass. - if (candidate.Parameters.Count > 0) { + if (candidate.Parameters.Count > 0) + { continue; } - if (target == null || string.Compare(FullName(candidate), FullName(target), StringComparison.Ordinal) < 0) { + if ( + target == null + || string.Compare(FullName(candidate), FullName(target), StringComparison.Ordinal) + < 0 + ) + { target = candidate; } } @@ -70,11 +84,16 @@ public class EntryModelUtil { /// through this, so an auto-generated module that defers to a declared one is skipped by all of /// them rather than by whichever ones remembered to. /// - public static IList RegistrationTargets(IList entryPoints) { + public static IList RegistrationTargets( + IList entryPoints + ) + { List? filtered = null; - for (var i = 0; i < entryPoints.Count; i++) { - if (DelegateTargetFor(entryPoints[i], entryPoints) == null) { + for (var i = 0; i < entryPoints.Count; i++) + { + if (DelegateTargetFor(entryPoints[i], entryPoints) == null) + { filtered?.Add(entryPoints[i]); continue; } @@ -99,47 +118,77 @@ private static string FullName(ModuleEntryPointModel model) => /// [Decorator] and the first [Intercept] to a project that happened to contain a record module. /// Shared so the next writer cannot get it wrong. /// - public static string ApplyRecordDeclaration(string output, ModuleEntryPointModel entryPointModel) { - if (!entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.IsRecord)) { + public static string ApplyRecordDeclaration( + string output, + ModuleEntryPointModel entryPointModel + ) + { + if (!entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.IsRecord)) + { return output; } return Regex.Replace( output, @"partial class " + Regex.Escape(entryPointModel.EntryPointType.Name) + @"(?!\w)", - $"partial record class {entryPointModel.EntryPointType.Name}"); + $"partial record class {entryPointModel.EntryPointType.Name}" + ); } - public static string GenerateFileName(ModuleEntryPointModel entryPointModel, string uniquePortion) { + public static string GenerateFileName( + ModuleEntryPointModel entryPointModel, + string uniquePortion + ) + { var namespaceName = entryPointModel.EntryPointType.Namespace; - if (string.IsNullOrEmpty(entryPointModel.EntryPointType.Namespace)) { + if (string.IsNullOrEmpty(entryPointModel.EntryPointType.Namespace)) + { namespaceName = "blank-namespace"; } - + return $"{namespaceName}.{entryPointModel.EntryPointType.GetShortName()}.{uniquePortion}.g.cs"; } - - public static ModuleEntryPointModel EnsureNamespace(ModuleEntryPointModel entryPointModel, DependencyModuleConfigurationModel configurationModel) { - - if (entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule) && - string.IsNullOrEmpty(entryPointModel.EntryPointType.Namespace)) { - entryPointModel = entryPointModel with { + + public static ModuleEntryPointModel EnsureNamespace( + ModuleEntryPointModel entryPointModel, + DependencyModuleConfigurationModel configurationModel + ) + { + if ( + entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule) + && string.IsNullOrEmpty(entryPointModel.EntryPointType.Namespace) + ) + { + entryPointModel = entryPointModel with + { EntryPointType = TypeDefinition.Get( - configurationModel.RootNamespace, - entryPointModel.EntryPointType.Name) + configurationModel.RootNamespace, + entryPointModel.EntryPointType.Name + ), }; } return entryPointModel; } - - public static (IList uniqueEntryPoints, DependencyModuleConfigurationModel configurationModel) ConsolidateEntryPointModels( - ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> entryPointList) { + + public static ( + IList uniqueEntryPoints, + DependencyModuleConfigurationModel configurationModel + ) ConsolidateEntryPointModels( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> entryPointList + ) + { var uniqueEntryPoints = new List(); var configurationModel = entryPointList.First().Right; var entryPointModels = entryPointList.Select(m => m.Left); - if (!configurationModel.AutoGenerateEntry) { - entryPointModels = entryPointModels.Where(m => !m.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule)); + if (!configurationModel.AutoGenerateEntry) + { + entryPointModels = entryPointModels.Where(m => + !m.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule) + ); } // Before grouping, not after. A generated module is created with no namespace and given the @@ -152,40 +201,59 @@ public static (IList uniqueEntryPoints, DependencyModuleC // they are one module and keep the declared one. entryPointModels = entryPointModels.Select(m => EnsureNamespace(m, configurationModel)); - var groupingEnumerable = - entryPointModels.GroupBy(m => m.EntryPointType.Namespace + "." + m.EntryPointType.GetShortName()); + var groupingEnumerable = entryPointModels.GroupBy(m => + m.EntryPointType.Namespace + "." + m.EntryPointType.GetShortName() + ); - foreach (var grouping in groupingEnumerable) { - if (grouping.Count() > 1) { + foreach (var grouping in groupingEnumerable) + { + if (grouping.Count() > 1) + { uniqueEntryPoints.Add( - ConsolidateEntryPointModelGrouping(grouping, configurationModel)); - } else { + ConsolidateEntryPointModelGrouping(grouping, configurationModel) + ); + } + else + { var entryPointModel = grouping.First(); - if (entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule)) { + if ( + entryPointModel.ModuleFeatures.HasFlag( + ModuleEntryPointFeatures.AutoGenerateModule + ) + ) + { var path = Path.Combine(configurationModel.ProjectDir, "Program.cs"); - if (entryPointModel.FileLocation == path) { + if (entryPointModel.FileLocation == path) + { uniqueEntryPoints.Add(grouping.First()); } } - else { + else + { uniqueEntryPoints.Add(grouping.First()); } } } - + return (uniqueEntryPoints, configurationModel); } - private static ModuleEntryPointModel ConsolidateEntryPointModelGrouping(IGrouping grouping, DependencyModuleConfigurationModel configurationModel) { - var firstNonAuto = grouping.FirstOrDefault( - m => m.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule) == false); - - if (firstNonAuto != null) { + private static ModuleEntryPointModel ConsolidateEntryPointModelGrouping( + IGrouping grouping, + DependencyModuleConfigurationModel configurationModel + ) + { + var firstNonAuto = grouping.FirstOrDefault(m => + m.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule) == false + ); + + if (firstNonAuto != null) + { return firstNonAuto; } - + return grouping.First(); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/EnvironmentConditionUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/EnvironmentConditionUtility.cs index a3f4f42..e54751c 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/EnvironmentConditionUtility.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/EnvironmentConditionUtility.cs @@ -19,8 +19,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// the incremental cache. /// /// -public static class EnvironmentConditionUtility { - +public static class EnvironmentConditionUtility +{ private const string IfEnvironment = "IfEnvironmentAttribute"; private const string IfNotEnvironment = "IfNotEnvironmentAttribute"; private const string IfEnvironmentValue = "IfEnvironmentValueAttribute"; @@ -34,21 +34,28 @@ public static class EnvironmentConditionUtility { /// has always had and existing snapshots do not move. /// public static IReadOnlyList? GetConditions( - SyntaxTransformContext context, SyntaxNode node, CancellationToken cancellationToken) { - - if (node is not MemberDeclarationSyntax memberDeclaration) { + SyntaxTransformContext context, + SyntaxNode node, + CancellationToken cancellationToken + ) + { + if (node is not MemberDeclarationSyntax memberDeclaration) + { return null; } List? conditions = null; - foreach (var attributeList in memberDeclaration.AttributeLists) { - foreach (var attribute in attributeList.Attributes) { + foreach (var attributeList in memberDeclaration.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { cancellationToken.ThrowIfCancellationRequested(); var condition = ReadCondition(context, attribute); - if (condition == null) { + if (condition == null) + { continue; } @@ -61,29 +68,43 @@ public static class EnvironmentConditionUtility { } private static EnvironmentConditionModel? ReadCondition( - SyntaxTransformContext context, AttributeSyntax attribute) { - - if (ModelExtensions.GetTypeInfo(context.SemanticModel, attribute).Type is not { } attributeType || - attributeType.ContainingNamespace.GetFullName() != KnownTypes.DependencyModules.Attributes.Namespace) { + SyntaxTransformContext context, + AttributeSyntax attribute + ) + { + if ( + ModelExtensions.GetTypeInfo(context.SemanticModel, attribute).Type + is not { } attributeType + || attributeType.ContainingNamespace.GetFullName() + != KnownTypes.DependencyModules.Attributes.Namespace + ) + { return null; } - var kind = attributeType.Name switch { + var kind = attributeType.Name switch + { IfEnvironment or IfNotEnvironment => EnvironmentConditionKind.Name, IfEnvironmentValue or IfNotEnvironmentValue => EnvironmentConditionKind.Value, _ => (EnvironmentConditionKind?)null, }; - if (kind == null) { + if (kind == null) + { return null; } var negate = attributeType.Name is IfNotEnvironment or IfNotEnvironmentValue; var arguments = ReadStringArguments(context, attribute); - if (kind == EnvironmentConditionKind.Name) { + if (kind == EnvironmentConditionKind.Name) + { return new EnvironmentConditionModel( - EnvironmentConditionKind.Name, negate, null, arguments); + EnvironmentConditionKind.Name, + negate, + null, + arguments + ); } // The key is the first argument; a second, when present, is the value it has to equal. @@ -92,8 +113,7 @@ public static class EnvironmentConditionUtility { var key = arguments.Count > 0 ? arguments[0] : ""; var values = arguments.Count > 1 ? new[] { arguments[1] } : Array.Empty(); - return new EnvironmentConditionModel( - EnvironmentConditionKind.Value, negate, key, values); + return new EnvironmentConditionModel(EnvironmentConditionKind.Value, negate, key, values); } /// @@ -101,22 +121,28 @@ public static class EnvironmentConditionUtility { /// attributes has a settable property, so one can only be a mistake. /// private static IReadOnlyList ReadStringArguments( - SyntaxTransformContext context, AttributeSyntax attribute) { - - if (attribute.ArgumentList == null) { + SyntaxTransformContext context, + AttributeSyntax attribute + ) + { + if (attribute.ArgumentList == null) + { return Array.Empty(); } var values = new List(); - foreach (var argument in attribute.ArgumentList.Arguments) { - if (argument.NameEquals != null) { + foreach (var argument in attribute.ArgumentList.Arguments) + { + if (argument.NameEquals != null) + { continue; } // GetConstantValue rather than the literal text, so nameof(...) and a const declared // elsewhere both read as the string they evaluate to. - if (context.SemanticModel.GetConstantValue(argument.Expression).Value is string value) { + if (context.SemanticModel.GetConstantValue(argument.Expression).Value is string value) + { values.Add(value); } } @@ -134,7 +160,8 @@ private static IReadOnlyList ReadStringArguments( /// anything, so it is reported rather than emitted. /// public static bool IsEmpty(EnvironmentConditionModel condition) => - condition.Kind switch { + condition.Kind switch + { EnvironmentConditionKind.Name => condition.Values.Count == 0, EnvironmentConditionKind.Value => string.IsNullOrEmpty(condition.Key), _ => false, @@ -143,10 +170,12 @@ public static bool IsEmpty(EnvironmentConditionModel condition) => /// /// A condition as it would read in a diagnostic message. /// - public static string Describe(EnvironmentConditionModel condition) { + public static string Describe(EnvironmentConditionModel condition) + { var not = condition.Negate ? "not " : ""; - if (condition.Kind == EnvironmentConditionKind.Name) { + if (condition.Kind == EnvironmentConditionKind.Name) + { return $"environment is {not}{string.Join(" or ", condition.Values)}"; } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/EnvironmentConditionWriter.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/EnvironmentConditionWriter.cs index 54659ef..cde7a98 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/EnvironmentConditionWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/EnvironmentConditionWriter.cs @@ -10,8 +10,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// Shared by every writer that emits something conditional — service registrations and decorators — /// so the two cannot drift into testing the same attributes differently. /// -public static class EnvironmentConditionWriter { - +public static class EnvironmentConditionWriter +{ private const string ConditionsType = "global::" + KnownTypes.DependencyModules.Helpers.Namespace + ".EnvironmentConditions"; @@ -27,22 +27,27 @@ public static class EnvironmentConditionWriter { /// Conditions to test, combined with and. /// Name of the IModuleEnvironment parameter in scope. public static string BuildCondition( - IReadOnlyList conditions, string environmentParameter) { - + IReadOnlyList conditions, + string environmentParameter + ) + { var parts = new List(conditions.Count); - foreach (var condition in conditions) { + foreach (var condition in conditions) + { // An empty condition tests nothing; it is reported as DM0012 and left out rather than // emitted as a call that is constant either way. - if (EnvironmentConditionUtility.IsEmpty(condition)) { + if (EnvironmentConditionUtility.IsEmpty(condition)) + { continue; } - var call = condition.Kind == EnvironmentConditionKind.Name - ? $"{ConditionsType}.NameIs({environmentParameter}, {QuoteAll(condition.Values)})" + var call = + condition.Kind == EnvironmentConditionKind.Name + ? $"{ConditionsType}.NameIs({environmentParameter}, {QuoteAll(condition.Values)})" : condition.Values.Count > 0 ? $"{ConditionsType}.ValueIs({environmentParameter}, {QuoteString(condition.Key!)}, {QuoteString(condition.Values[0])})" - : $"{ConditionsType}.HasValue({environmentParameter}, {QuoteString(condition.Key!)})"; + : $"{ConditionsType}.HasValue({environmentParameter}, {QuoteString(condition.Key!)})"; parts.Add(condition.Negate ? "!" + call : call); } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/FileLogger.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/FileLogger.cs index ba72b06..326fbbe 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/FileLogger.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/FileLogger.cs @@ -3,7 +3,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; -public class FileLogger : IDisposable { +public class FileLogger : IDisposable +{ private readonly string _loggerName; private readonly string _outputFolder; private StringBuilder? _sb; @@ -21,76 +22,94 @@ public static void Wrap( string loggerName, DependencyModuleConfigurationModel configurationModel, Action logger, - Action? reportFailure = null) { - + Action? reportFailure = null + ) + { var fileLogger = new FileLogger(configurationModel, loggerName); - try { + try + { logger(fileLogger); } - catch (Exception e) { + catch (Exception e) + { fileLogger.Error($"{e.Message}\n{e.StackTrace}"); - if (reportFailure == null) { + if (reportFailure == null) + { throw; } reportFailure(e); } - finally { + finally + { fileLogger.Dispose(); } } - - public FileLogger(DependencyModuleConfigurationModel configurationModel, string loggerName) { + + public FileLogger(DependencyModuleConfigurationModel configurationModel, string loggerName) + { _loggerName = loggerName; _outputFolder = configurationModel.LogOutputFolder; - if (!string.IsNullOrEmpty(_outputFolder)) { + if (!string.IsNullOrEmpty(_outputFolder)) + { _sb = new StringBuilder(); } } - public void Info(string message) { + public void Info(string message) + { WriteLog("INFO", message); } - public void Info(string message, object data) { + public void Info(string message, object data) + { WriteLog("INFO", message, data); } - - public void Error(string message) { + + public void Error(string message) + { WriteLog("ERROR", message); } - - public void Error(string message, object data) { + + public void Error(string message, object data) + { WriteLog("ERROR", message, data); } - private void WriteLog(string level, string message, object? data = null) { - if (_sb != null) { + private void WriteLog(string level, string message, object? data = null) + { + if (_sb != null) + { _sb.AppendLine($"{level}: {message}"); - if (data != null) { + if (data != null) + { _sb.AppendLine(data.ToString()); } } } - - public void Dispose() { - if (_sb == null) { + + public void Dispose() + { + if (_sb == null) + { return; } var fileName = $"{_loggerName}.{DateTimeOffset.Now.ToUnixTimeMilliseconds()}.txt"; #pragma warning disable RS1035 - try { + try + { // _sb is only allocated when an output folder was configured, so honour it here rather // than dropping the log into whatever directory the compiler happens to be running in. Directory.CreateDirectory(_outputFolder); File.WriteAllText(Path.Combine(_outputFolder, fileName), _sb.ToString()); } - catch (Exception) { + catch (Exception) + { // Diagnostic logging must never fail a build. } #pragma warning restore RS1035 } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/ITypeDefinitionExtensions.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/ITypeDefinitionExtensions.cs index ae4bec3..218abab 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/ITypeDefinitionExtensions.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/ITypeDefinitionExtensions.cs @@ -2,8 +2,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; -public static class ITypeDefinitionExtensions { - +public static class ITypeDefinitionExtensions +{ /// /// Rewrites a generic type's arguments to nothing, so it renders as IRepo<>. /// @@ -19,7 +19,10 @@ public static ITypeDefinition ToUnboundGeneric(this ITypeDefinition type) => generic.TypeDefinitionEnum, generic.Namespace, generic.Name, - generic.TypeArguments.Select(_ => (ITypeDefinition)TypeDefinition.Get("", "")).ToArray()) + generic + .TypeArguments.Select(_ => (ITypeDefinition)TypeDefinition.Get("", "")) + .ToArray() + ) : type; /// @@ -34,22 +37,29 @@ public static ITypeDefinition ToUnboundGeneric(this ITypeDefinition type) => /// a type in the global namespace, which had none to begin with. They are different types and /// need different files, so the global namespace is named rather than left blank. /// - public static string GetFileNameHint(this ITypeDefinition typeDefinition, string rootNamespace, string uniquePart) { + public static string GetFileNameHint( + this ITypeDefinition typeDefinition, + string rootNamespace, + string uniquePart + ) + { var nameString = typeDefinition.Namespace; - if (nameString == rootNamespace || - nameString.StartsWith(rootNamespace + ".")) { + if (nameString == rootNamespace || nameString.StartsWith(rootNamespace + ".")) + { nameString = nameString.Substring(rootNamespace.Length); nameString = nameString.TrimStart('.'); } - else if (string.IsNullOrWhiteSpace(nameString)) { + else if (string.IsNullOrWhiteSpace(nameString)) + { nameString = "global"; } - if (!string.IsNullOrWhiteSpace(nameString)) { + if (!string.IsNullOrWhiteSpace(nameString)) + { nameString += "."; } return $"{nameString}{typeDefinition.Name}.{uniquePart}.g.cs"; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptedMemberReader.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptedMemberReader.cs index 7c96ef3..ca8ef5d 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptedMemberReader.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptedMemberReader.cs @@ -14,8 +14,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// equatable across compilations, so holding one would defeat the incremental cache and regenerate /// every wrapper on every keystroke. /// -public static class InterceptedMemberReader { - +public static class InterceptedMemberReader +{ /// /// Everything a wrapper for has to implement, including what it /// inherits from base interfaces. @@ -28,8 +28,9 @@ public static bool Read( INamedTypeSymbol serviceType, out IReadOnlyList members, out IReadOnlyList declarations, - out string? unsupported) { - + out string? unsupported + ) + { unsupported = null; var memberList = new List(); @@ -38,27 +39,34 @@ public static bool Read( members = memberList; declarations = declarationList; - foreach (var candidate in EnumerateMembers(serviceType)) { - if (candidate.IsStatic) { - unsupported = $"'{candidate.Name}' is static, which cannot be forwarded through an instance"; + foreach (var candidate in EnumerateMembers(serviceType)) + { + if (candidate.IsStatic) + { + unsupported = + $"'{candidate.Name}' is static, which cannot be forwarded through an instance"; return false; } - switch (candidate) { + switch (candidate) + { case IMethodSymbol { MethodKind: MethodKind.Ordinary } method: - if (!ReadMethod(method, memberList, declarationList, out unsupported)) { + if (!ReadMethod(method, memberList, declarationList, out unsupported)) + { return false; } break; case IPropertySymbol property: - if (!ReadProperty(property, memberList, declarationList, out unsupported)) { + if (!ReadProperty(property, memberList, declarationList, out unsupported)) + { return false; } break; case IEventSymbol @event: - if (!ReadEvent(@event, memberList, declarationList, out unsupported)) { + if (!ReadEvent(@event, memberList, declarationList, out unsupported)) + { return false; } break; @@ -72,38 +80,47 @@ private static bool ReadMethod( IMethodSymbol method, List members, List declarations, - out string? unsupported) { - - if (method.ReturnsByRef || method.ReturnsByRefReadonly) { + out string? unsupported + ) + { + if (method.ReturnsByRef || method.ReturnsByRefReadonly) + { unsupported = $"'{method.Name}' returns by reference, which cannot be wrapped"; return false; } var parameters = ReadParameters(method.Parameters, method.Name, out unsupported); - if (parameters == null) { + if (parameters == null) + { return false; } var returnShape = GetReturnShape(method.ReturnType); - declarations.Add(new InterceptedDeclarationModel( - DeclarationKind.Method, - EscapeIdentifier(method.Name), - null, - Array.Empty(), - members.Count, - -1)); - - members.Add(new InterceptedMemberModel( - method.Name, - EscapeIdentifier(method.Name), - AccessorForm.Method, - returnShape == ReturnShape.Void ? null : method.ReturnType.GetTypeDefinition(), - GetResultType(method.ReturnType, returnShape), - parameters, - ReadTypeParameters(method), - returnShape)); + declarations.Add( + new InterceptedDeclarationModel( + DeclarationKind.Method, + EscapeIdentifier(method.Name), + null, + Array.Empty(), + members.Count, + -1 + ) + ); + + members.Add( + new InterceptedMemberModel( + method.Name, + EscapeIdentifier(method.Name), + AccessorForm.Method, + returnShape == ReturnShape.Void ? null : method.ReturnType.GetTypeDefinition(), + GetResultType(method.ReturnType, returnShape), + parameters, + ReadTypeParameters(method), + returnShape + ) + ); return true; } @@ -120,14 +137,17 @@ private static bool ReadProperty( IPropertySymbol property, List members, List declarations, - out string? unsupported) { - - if (property.ReturnsByRef || property.ReturnsByRefReadonly) { + out string? unsupported + ) + { + if (property.ReturnsByRef || property.ReturnsByRefReadonly) + { unsupported = $"'{property.Name}' returns by reference, which cannot be wrapped"; return false; } - if (property.SetMethod is { IsInitOnly: true }) { + if (property.SetMethod is { IsInitOnly: true }) + { unsupported = $"'{property.Name}' is init-only, and a wrapper cannot forward to an initializer"; return false; @@ -135,12 +155,15 @@ private static bool ReadProperty( var indices = ReadParameters(property.Parameters, property.Name, out unsupported); - if (indices == null) { + if (indices == null) + { return false; } - if (property.Type.IsRefLikeType) { - unsupported = $"'{property.Name}' is a ref struct and cannot be held for the duration of a call"; + if (property.Type.IsRefLikeType) + { + unsupported = + $"'{property.Name}' is a ref struct and cannot be held for the duration of a call"; return false; } @@ -148,47 +171,59 @@ private static bool ReadProperty( var getter = -1; var setter = -1; - if (property.GetMethod != null) { + if (property.GetMethod != null) + { getter = members.Count; - members.Add(new InterceptedMemberModel( - property.GetMethod.Name, - EscapeIdentifier(property.Name), - property.IsIndexer ? AccessorForm.IndexerGet : AccessorForm.PropertyGet, - type, - type, - indices, - Array.Empty(), - ReturnShape.Value)); + members.Add( + new InterceptedMemberModel( + property.GetMethod.Name, + EscapeIdentifier(property.Name), + property.IsIndexer ? AccessorForm.IndexerGet : AccessorForm.PropertyGet, + type, + type, + indices, + Array.Empty(), + ReturnShape.Value + ) + ); } - if (property.SetMethod != null) { + if (property.SetMethod != null) + { setter = members.Count; // The assigned value is the last argument, after any indices, matching how the CLR names // and orders a setter's parameters. - var arguments = new List(indices) { - new("value", "value", type, null) + var arguments = new List(indices) + { + new("value", "value", type, null), }; - members.Add(new InterceptedMemberModel( - property.SetMethod.Name, - EscapeIdentifier(property.Name), - property.IsIndexer ? AccessorForm.IndexerSet : AccessorForm.PropertySet, - null, - KnownTypes.DependencyModules.Interception.NoResult, - arguments, - Array.Empty(), - ReturnShape.Void)); + members.Add( + new InterceptedMemberModel( + property.SetMethod.Name, + EscapeIdentifier(property.Name), + property.IsIndexer ? AccessorForm.IndexerSet : AccessorForm.PropertySet, + null, + KnownTypes.DependencyModules.Interception.NoResult, + arguments, + Array.Empty(), + ReturnShape.Void + ) + ); } - declarations.Add(new InterceptedDeclarationModel( - property.IsIndexer ? DeclarationKind.Indexer : DeclarationKind.Property, - property.IsIndexer ? "this" : EscapeIdentifier(property.Name), - type, - indices, - getter, - setter)); + declarations.Add( + new InterceptedDeclarationModel( + property.IsIndexer ? DeclarationKind.Indexer : DeclarationKind.Property, + property.IsIndexer ? "this" : EscapeIdentifier(property.Name), + type, + indices, + getter, + setter + ) + ); return true; } @@ -197,11 +232,13 @@ private static bool ReadEvent( IEventSymbol @event, List members, List declarations, - out string? unsupported) { - + out string? unsupported + ) + { unsupported = null; - if (@event.AddMethod == null || @event.RemoveMethod == null) { + if (@event.AddMethod == null || @event.RemoveMethod == null) + { unsupported = $"'{@event.Name}' does not declare both add and remove"; return false; } @@ -213,41 +250,56 @@ private static bool ReadEvent( var remove = members.Count; - members.Add(EventAccessor(@event.RemoveMethod.Name, @event.Name, AccessorForm.EventRemove, type)); + members.Add( + EventAccessor(@event.RemoveMethod.Name, @event.Name, AccessorForm.EventRemove, type) + ); - declarations.Add(new InterceptedDeclarationModel( - DeclarationKind.Event, - EscapeIdentifier(@event.Name), - type, - Array.Empty(), - add, - remove)); + declarations.Add( + new InterceptedDeclarationModel( + DeclarationKind.Event, + EscapeIdentifier(@event.Name), + type, + Array.Empty(), + add, + remove + ) + ); return true; } private static InterceptedMemberModel EventAccessor( - string name, string eventName, AccessorForm form, ITypeDefinition handlerType) => - new(name, + string name, + string eventName, + AccessorForm form, + ITypeDefinition handlerType + ) => + new( + name, EscapeIdentifier(eventName), form, null, KnownTypes.DependencyModules.Interception.NoResult, new InterceptedParameterModel[] { new("value", "value", handlerType, null) }, Array.Empty(), - ReturnShape.Void); + ReturnShape.Void + ); /// /// The interface's own members plus everything it inherits. Skipping base interfaces would /// produce a wrapper that does not satisfy the interface. /// - private static IEnumerable EnumerateMembers(INamedTypeSymbol serviceType) { - foreach (var member in serviceType.GetMembers()) { + private static IEnumerable EnumerateMembers(INamedTypeSymbol serviceType) + { + foreach (var member in serviceType.GetMembers()) + { yield return member; } - foreach (var baseInterface in serviceType.AllInterfaces) { - foreach (var member in baseInterface.GetMembers()) { + foreach (var baseInterface in serviceType.AllInterfaces) + { + foreach (var member in baseInterface.GetMembers()) + { yield return member; } } @@ -262,41 +314,51 @@ private static IEnumerable EnumerateMembers(INamedTypeSymbol serviceTyp /// as code that does not compile. A hand-written decorator remains the answer for those. /// private static IReadOnlyList? ReadParameters( - IReadOnlyList declared, string memberName, out string? unsupported) { - + IReadOnlyList declared, + string memberName, + out string? unsupported + ) + { unsupported = null; var parameters = new List(); - foreach (var parameter in declared) { - if (parameter.RefKind != RefKind.None) { - var keyword = parameter.RefKind switch { + foreach (var parameter in declared) + { + if (parameter.RefKind != RefKind.None) + { + var keyword = parameter.RefKind switch + { RefKind.Ref => "ref", RefKind.Out => "out", - _ => "in" + _ => "in", }; unsupported = - $"'{memberName}' takes '{parameter.Name}' by {keyword}, and an argument passed by " + - "reference cannot be held for the duration of a call"; + $"'{memberName}' takes '{parameter.Name}' by {keyword}, and an argument passed by " + + "reference cannot be held for the duration of a call"; return null; } - if (parameter.Type.IsRefLikeType) { + if (parameter.Type.IsRefLikeType) + { unsupported = - $"'{memberName}' takes '{parameter.Name}', which is a ref struct and cannot be " + - "held for the duration of a call"; + $"'{memberName}' takes '{parameter.Name}', which is a ref struct and cannot be " + + "held for the duration of a call"; return null; } - parameters.Add(new InterceptedParameterModel( - parameter.Name, - EscapeIdentifier(parameter.Name), - parameter.Type.GetTypeDefinition(), - RenderDefaultValue(parameter), - parameter.IsParams)); + parameters.Add( + new InterceptedParameterModel( + parameter.Name, + EscapeIdentifier(parameter.Name), + parameter.Type.GetTypeDefinition(), + RenderDefaultValue(parameter), + parameter.IsParams + ) + ); } return parameters; @@ -306,45 +368,56 @@ private static IEnumerable EnumerateMembers(INamedTypeSymbol serviceTyp /// A parameter's default as it should be written on the wrapper. Dropping it would narrow the /// signature the interface promised. /// - private static string? RenderDefaultValue(IParameterSymbol parameter) { - if (!parameter.HasExplicitDefaultValue) { + private static string? RenderDefaultValue(IParameterSymbol parameter) + { + if (!parameter.HasExplicitDefaultValue) + { return null; } var value = parameter.ExplicitDefaultValue; - if (value == null) { + if (value == null) + { return parameter.Type.IsValueType ? "default" : "null"; } // An enum default arrives as its underlying value, so it is cast back rather than guessed at // by matching the value against the enum's members. - if (parameter.Type.TypeKind == TypeKind.Enum) { + if (parameter.Type.TypeKind == TypeKind.Enum) + { var builder = new StringBuilder("("); parameter.Type.GetTypeDefinition().WriteTypeName(builder, TypeOutputMode.Global); - return builder.Append(')') + return builder + .Append(')') .Append(Convert.ToString(value, CultureInfo.InvariantCulture)) .ToString(); } return Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatPrimitive( - value, quoteStrings: true, useHexadecimalNumbers: false); + value, + quoteStrings: true, + useHexadecimalNumbers: false + ); } /// /// The member's type parameters and their constraints. The state class repeats both, or the call /// it forwards will not satisfy the constraints the interface declared. /// - private static IReadOnlyList ReadTypeParameters(IMethodSymbol method) { - if (method.TypeParameters.Length == 0) { + private static IReadOnlyList ReadTypeParameters(IMethodSymbol method) + { + if (method.TypeParameters.Length == 0) + { return Array.Empty(); } var typeParameters = new List(); - foreach (var parameter in method.TypeParameters) { + foreach (var parameter in method.TypeParameters) + { typeParameters.Add(TypeParameterReader.Read(parameter)); } @@ -352,12 +425,15 @@ private static IReadOnlyList ReadTypeParameters(IMethodSymbo } private static string EscapeIdentifier(string name) => - Microsoft.CodeAnalysis.CSharp.SyntaxFacts.GetKeywordKind(name) == Microsoft.CodeAnalysis.CSharp.SyntaxKind.None + Microsoft.CodeAnalysis.CSharp.SyntaxFacts.GetKeywordKind(name) + == Microsoft.CodeAnalysis.CSharp.SyntaxKind.None ? name : "@" + name; - private static ReturnShape GetReturnShape(ITypeSymbol returnType) { - if (returnType.SpecialType == SpecialType.System_Void) { + private static ReturnShape GetReturnShape(ITypeSymbol returnType) + { + if (returnType.SpecialType == SpecialType.System_Void) + { return ReturnShape.Void; } @@ -369,13 +445,14 @@ private static ReturnShape GetReturnShape(ITypeSymbol returnType) { ? containing.ToDisplayString() + "." + definition.MetadataName : definition.MetadataName; - return name switch { + return name switch + { "System.Threading.Tasks.Task" => ReturnShape.Task, "System.Threading.Tasks.Task`1" => ReturnShape.TaskOfValue, "System.Threading.Tasks.ValueTask" => ReturnShape.ValueTask, "System.Threading.Tasks.ValueTask`1" => ReturnShape.ValueTaskOfValue, "System.Collections.Generic.IAsyncEnumerable`1" => ReturnShape.AsyncEnumerable, - _ => ReturnShape.Value + _ => ReturnShape.Value, }; } @@ -384,8 +461,10 @@ private static ReturnShape GetReturnShape(ITypeSymbol returnType) { /// through the same pipeline, standing on NoResult so an interceptor never needs an /// overload for the void case. /// - private static ITypeDefinition GetResultType(ITypeSymbol returnType, ReturnShape shape) { - switch (shape) { + private static ITypeDefinition GetResultType(ITypeSymbol returnType, ReturnShape shape) + { + switch (shape) + { case ReturnShape.Void: case ReturnShape.Task: case ReturnShape.ValueTask: diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs index e90bb21..57337c3 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs @@ -9,8 +9,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// /// Builds an from a class carrying [Intercept]. /// -public static class InterceptorModelUtility { - +public static class InterceptorModelUtility +{ private const string InterceptionNamespace = "DependencyModules.Runtime.Interception"; /// @@ -23,22 +23,33 @@ public static class InterceptorModelUtility { /// A diagnostic cannot be raised from here — the transform holds no context that can. /// public static InterceptorModel GetInterceptorModel( - SyntaxTransformContext context, CancellationToken cancellationToken) { - + SyntaxTransformContext context, + CancellationToken cancellationToken + ) + { cancellationToken.ThrowIfCancellationRequested(); - if (context.Node is not TypeDeclarationSyntax typeDeclarationSyntax) { + if (context.Node is not TypeDeclarationSyntax typeDeclarationSyntax) + { return InterceptorModel.Ignore; } - var attributes = FindAttributes(typeDeclarationSyntax, context.SemanticModel, cancellationToken); + var attributes = FindAttributes( + typeDeclarationSyntax, + context.SemanticModel, + cancellationToken + ); - if (attributes.Count == 0) { + if (attributes.Count == 0) + { return InterceptorModel.Ignore; } - if (context.SemanticModel.GetDeclaredSymbol(typeDeclarationSyntax, cancellationToken) - is not INamedTypeSymbol implementationSymbol) { + if ( + context.SemanticModel.GetDeclaredSymbol(typeDeclarationSyntax, cancellationToken) + is not INamedTypeSymbol implementationSymbol + ) + { return InterceptorModel.Ignore; } @@ -61,37 +72,62 @@ public static InterceptorModel GetInterceptorModel( // Every [Intercept], not the first. The attribute is AllowMultiple, so stacking them is a // supported way to write what one attribute can also express as a params list — and reading // only the first dropped every interceptor after it, with nothing to say so. - foreach (var attribute in attributes) { + foreach (var attribute in attributes) + { ReadAttribute( - attribute, context, cancellationToken, interceptorSymbols, ref order, ref explicitService, ref realm); + attribute, + context, + cancellationToken, + interceptorSymbols, + ref order, + ref explicitService, + ref realm + ); memberKinds &= ReadMemberKinds(attribute); } - if (interceptorSymbols.Count == 0) { + if (interceptorSymbols.Count == 0) + { return InterceptorModel.Ignore; } - var serviceSymbol = ResolveServiceInterface(implementationSymbol, explicitService, out var unsupported); + var serviceSymbol = ResolveServiceInterface( + implementationSymbol, + explicitService, + out var unsupported + ); - if (serviceSymbol == null) { + if (serviceSymbol == null) + { return Refuse(unsupported, context); } - if (!InterceptedMemberReader.Read(serviceSymbol, out var members, out var declarations, out unsupported)) { + if ( + !InterceptedMemberReader.Read( + serviceSymbol, + out var members, + out var declarations, + out unsupported + ) + ) + { return Refuse(unsupported, context); } - if (members.Count == 0) { + if (members.Count == 0) + { return InterceptorModel.Refused( - $"'{serviceSymbol.Name}' declares nothing to intercept"); + $"'{serviceSymbol.Name}' declares nothing to intercept" + ); } members = ApplyMemberKinds(members, declarations, memberKinds); var interceptors = new List(); - foreach (var (interceptorSymbol, lifestyle) in interceptorSymbols) { + foreach (var (interceptorSymbol, lifestyle) in interceptorSymbols) + { interceptors.Add(ReadInterceptorType(interceptorSymbol, lifestyle)); } @@ -109,9 +145,12 @@ public static InterceptorModel GetInterceptorModel( declarations, order, TypeParameters: TypeParameterModels(implementationSymbol), - Realm: (realm?.GetTypeDefinition() ?? - RegistrationRealm(typeDeclarationSyntax, context, cancellationToken)), - Location: LocationModel.From(context.Node)); + Realm: ( + realm?.GetTypeDefinition() + ?? RegistrationRealm(typeDeclarationSyntax, context, cancellationToken) + ), + Location: LocationModel.From(context.Node) + ); } /// @@ -139,19 +178,30 @@ public static InterceptorModel GetInterceptorModel( private static ITypeDefinition? RegistrationRealm( TypeDeclarationSyntax typeDeclarationSyntax, SyntaxTransformContext context, - CancellationToken cancellationToken) { - - foreach (var attributeList in typeDeclarationSyntax.AttributeLists) { - foreach (var attribute in attributeList.Attributes) { - if (!IsServiceAttribute(attribute, context, cancellationToken)) { + CancellationToken cancellationToken + ) + { + foreach (var attributeList in typeDeclarationSyntax.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + if (!IsServiceAttribute(attribute, context, cancellationToken)) + { continue; } - foreach (var argument in attribute.ArgumentList?.Arguments ?? - default(SeparatedSyntaxList)) { - if (argument.NameEquals?.Name.ToString() == "Realm" && - argument.Expression is TypeOfExpressionSyntax realmTypeOf) { - return ResolveType(realmTypeOf, context, cancellationToken)?.GetTypeDefinition(); + foreach ( + var argument in attribute.ArgumentList?.Arguments + ?? default(SeparatedSyntaxList) + ) + { + if ( + argument.NameEquals?.Name.ToString() == "Realm" + && argument.Expression is TypeOfExpressionSyntax realmTypeOf + ) + { + return ResolveType(realmTypeOf, context, cancellationToken) + ?.GetTypeDefinition(); } } } @@ -161,11 +211,22 @@ public static InterceptorModel GetInterceptorModel( } private static bool IsServiceAttribute( - AttributeSyntax attribute, SyntaxTransformContext context, CancellationToken cancellationToken) { - - foreach (var serviceAttribute in ServiceAttributeTypes) { - if (AttributeTypeMatcher.Matches( - context.SemanticModel, attribute, serviceAttribute, cancellationToken)) { + AttributeSyntax attribute, + SyntaxTransformContext context, + CancellationToken cancellationToken + ) + { + foreach (var serviceAttribute in ServiceAttributeTypes) + { + if ( + AttributeTypeMatcher.Matches( + context.SemanticModel, + attribute, + serviceAttribute, + cancellationToken + ) + ) + { return true; } } @@ -173,11 +234,12 @@ private static bool IsServiceAttribute( return false; } - private static readonly ITypeDefinition[] ServiceAttributeTypes = { + private static readonly ITypeDefinition[] ServiceAttributeTypes = + { KnownTypes.DependencyModules.Attributes.SingletonServiceAttribute, KnownTypes.DependencyModules.Attributes.ScopedServiceAttribute, KnownTypes.DependencyModules.Attributes.TransientServiceAttribute, - KnownTypes.DependencyModules.Attributes.CrossWireServiceAttribute + KnownTypes.DependencyModules.Attributes.CrossWireServiceAttribute, }; /// @@ -189,24 +251,41 @@ private static bool IsServiceAttribute( /// segment of each part handles `Methods`, `InterceptedMembers.Methods` and any qualification of /// either. /// - private static InterceptedMemberKinds ReadMemberKinds(AttributeSyntax attribute) { - foreach (var argument in attribute.ArgumentList?.Arguments ?? - default(SeparatedSyntaxList)) { - if (argument.NameEquals?.Name.ToString() != "Members") { + private static InterceptedMemberKinds ReadMemberKinds(AttributeSyntax attribute) + { + foreach ( + var argument in attribute.ArgumentList?.Arguments + ?? default(SeparatedSyntaxList) + ) + { + if (argument.NameEquals?.Name.ToString() != "Members") + { continue; } var kinds = InterceptedMemberKinds.None; - foreach (var part in argument.Expression.ToString().Split('|')) { + foreach (var part in argument.Expression.ToString().Split('|')) + { var member = part.Substring(part.LastIndexOf('.') + 1).Trim(); - switch (member) { - case "Methods": kinds |= InterceptedMemberKinds.Methods; break; - case "Properties": kinds |= InterceptedMemberKinds.Properties; break; - case "Indexers": kinds |= InterceptedMemberKinds.Indexers; break; - case "Events": kinds |= InterceptedMemberKinds.Events; break; - case "All": kinds |= InterceptedMemberKinds.All; break; + switch (member) + { + case "Methods": + kinds |= InterceptedMemberKinds.Methods; + break; + case "Properties": + kinds |= InterceptedMemberKinds.Properties; + break; + case "Indexers": + kinds |= InterceptedMemberKinds.Indexers; + break; + case "Events": + kinds |= InterceptedMemberKinds.Events; + break; + case "All": + kinds |= InterceptedMemberKinds.All; + break; } } @@ -228,46 +307,56 @@ private static InterceptedMemberKinds ReadMemberKinds(AttributeSyntax attribute) private static IReadOnlyList ApplyMemberKinds( IReadOnlyList members, IReadOnlyList declarations, - InterceptedMemberKinds kinds) { - - if (kinds == InterceptedMemberKinds.All) { + InterceptedMemberKinds kinds + ) + { + if (kinds == InterceptedMemberKinds.All) + { return members; } var excluded = new HashSet(); - foreach (var declaration in declarations) { - if (KindOf(declaration.Kind) is var kind && (kinds & kind) != 0) { + foreach (var declaration in declarations) + { + if (KindOf(declaration.Kind) is var kind && (kinds & kind) != 0) + { continue; } excluded.Add(declaration.First); - if (declaration.Second >= 0) { + if (declaration.Second >= 0) + { excluded.Add(declaration.Second); } } - if (excluded.Count == 0) { + if (excluded.Count == 0) + { return members; } var result = new List(members.Count); - for (var index = 0; index < members.Count; index++) { - result.Add(excluded.Contains(index) ? members[index] with { Excluded = true } : members[index]); + for (var index = 0; index < members.Count; index++) + { + result.Add( + excluded.Contains(index) ? members[index] with { Excluded = true } : members[index] + ); } return result; } private static InterceptedMemberKinds KindOf(DeclarationKind kind) => - kind switch { + kind switch + { DeclarationKind.Method => InterceptedMemberKinds.Methods, DeclarationKind.Property => InterceptedMemberKinds.Properties, DeclarationKind.Indexer => InterceptedMemberKinds.Indexers, DeclarationKind.Event => InterceptedMemberKinds.Events, - _ => InterceptedMemberKinds.All + _ => InterceptedMemberKinds.All, }; private static InterceptorModel Refuse(string? reason, SyntaxTransformContext context) => @@ -279,17 +368,23 @@ private static InterceptorModel Refuse(string? reason, SyntaxTransformContext co /// The interfaces an interceptor implements, which decide the members it can be placed around. /// private static InterceptorTypeModel ReadInterceptorType( - INamedTypeSymbol symbol, ServiceLifestyle lifestyle) { + INamedTypeSymbol symbol, + ServiceLifestyle lifestyle + ) + { var sync = false; var async = false; var stream = false; - foreach (var implemented in symbol.AllInterfaces) { - if (implemented.ContainingNamespace?.ToDisplayString() != InterceptionNamespace) { + foreach (var implemented in symbol.AllInterfaces) + { + if (implemented.ContainingNamespace?.ToDisplayString() != InterceptionNamespace) + { continue; } - switch (implemented.Name) { + switch (implemented.Name) + { case "IInterceptor": sync = true; break; @@ -315,11 +410,16 @@ private static InterceptorTypeModel ReadInterceptorType( /// when none of them matches at all is there no wrapper worth generating. /// private static bool AnyMemberIsIntercepted( - IReadOnlyList interceptors, IReadOnlyList members) { - - foreach (var member in members) { - foreach (var interceptor in interceptors) { - if (interceptor.CanServe(member.Kind)) { + IReadOnlyList interceptors, + IReadOnlyList members + ) + { + foreach (var member in members) + { + foreach (var interceptor in interceptors) + { + if (interceptor.CanServe(member.Kind)) + { return true; } } @@ -335,9 +435,11 @@ private static void ReadAttribute( List<(INamedTypeSymbol Symbol, ServiceLifestyle Lifestyle)> interceptors, ref int order, ref INamedTypeSymbol? explicitService, - ref INamedTypeSymbol? realm) { - - if (attribute.ArgumentList == null) { + ref INamedTypeSymbol? realm + ) + { + if (attribute.ArgumentList == null) + { return; } @@ -346,14 +448,18 @@ private static void ReadAttribute( // lifetime happened to be at that point in the argument list. var lifestyle = ReadLifetime(attribute); - foreach (var argument in attribute.ArgumentList.Arguments) { + foreach (var argument in attribute.ArgumentList.Arguments) + { var name = argument.NameEquals?.Name.ToString(); - if (name == null) { - if (argument.Expression is TypeOfExpressionSyntax typeOf) { + if (name == null) + { + if (argument.Expression is TypeOfExpressionSyntax typeOf) + { var symbol = ResolveType(typeOf, context, cancellationToken); - if (symbol != null) { + if (symbol != null) + { interceptors.Add((symbol, lifestyle)); } } @@ -361,19 +467,23 @@ private static void ReadAttribute( continue; } - switch (name) { + switch (name) + { case "Order": - if (int.TryParse(argument.Expression.ToString(), out var parsed)) { + if (int.TryParse(argument.Expression.ToString(), out var parsed)) + { order = parsed; } break; case "Service": - if (argument.Expression is TypeOfExpressionSyntax serviceTypeOf) { + if (argument.Expression is TypeOfExpressionSyntax serviceTypeOf) + { explicitService = ResolveType(serviceTypeOf, context, cancellationToken); } break; case "Realm": - if (argument.Expression is TypeOfExpressionSyntax realmTypeOf) { + if (argument.Expression is TypeOfExpressionSyntax realmTypeOf) + { realm = ResolveType(realmTypeOf, context, cancellationToken); } break; @@ -391,17 +501,23 @@ private static void ReadAttribute( /// last segment is the answer for `Scoped`, `ServiceLifetime.Scoped` and any qualification of /// it alike. /// - private static ServiceLifestyle ReadLifetime(AttributeSyntax attribute) { - foreach (var argument in attribute.ArgumentList?.Arguments ?? - default(SeparatedSyntaxList)) { - if (argument.NameEquals?.Name.ToString() != "Lifetime") { + private static ServiceLifestyle ReadLifetime(AttributeSyntax attribute) + { + foreach ( + var argument in attribute.ArgumentList?.Arguments + ?? default(SeparatedSyntaxList) + ) + { + if (argument.NameEquals?.Name.ToString() != "Lifetime") + { continue; } var written = argument.Expression.ToString(); var member = written.Substring(written.LastIndexOf('.') + 1).Trim(); - switch (member) { + switch (member) + { case "Scoped": return ServiceLifestyle.Scoped; case "Transient": @@ -415,8 +531,10 @@ private static ServiceLifestyle ReadLifetime(AttributeSyntax attribute) { } private static INamedTypeSymbol? ResolveType( - TypeOfExpressionSyntax typeOf, SyntaxTransformContext context, CancellationToken cancellationToken) => - context.SemanticModel.GetTypeInfo(typeOf.Type, cancellationToken).Type as INamedTypeSymbol; + TypeOfExpressionSyntax typeOf, + SyntaxTransformContext context, + CancellationToken cancellationToken + ) => context.SemanticModel.GetTypeInfo(typeOf.Type, cancellationToken).Type as INamedTypeSymbol; /// /// The interface to wrap. Interception works through an interface: a call the implementation @@ -424,13 +542,19 @@ private static ServiceLifestyle ReadLifetime(AttributeSyntax attribute) { /// to wrap. /// private static INamedTypeSymbol? ResolveServiceInterface( - INamedTypeSymbol implementation, INamedTypeSymbol? explicitService, out string? unsupported) { - + INamedTypeSymbol implementation, + INamedTypeSymbol? explicitService, + out string? unsupported + ) + { unsupported = null; - if (explicitService != null) { - foreach (var candidate in implementation.AllInterfaces) { - if (SymbolEqualityComparer.Default.Equals(candidate, explicitService)) { + if (explicitService != null) + { + foreach (var candidate in implementation.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(candidate, explicitService)) + { return candidate; } } @@ -441,12 +565,15 @@ private static ServiceLifestyle ReadLifetime(AttributeSyntax attribute) { var interfaces = DeclaredInterfaces(implementation); - if (interfaces.Length == 0) { - unsupported = $"'{implementation.Name}' implements no interface, so there is nothing to intercept"; + if (interfaces.Length == 0) + { + unsupported = + $"'{implementation.Name}' implements no interface, so there is nothing to intercept"; return null; } - if (interfaces.Length > 1) { + if (interfaces.Length > 1) + { unsupported = $"'{implementation.Name}' implements more than one interface; set Service to choose which to intercept"; return null; @@ -469,9 +596,14 @@ private static ServiceLifestyle ReadLifetime(AttributeSyntax attribute) { /// Not AllInterfaces, which flattens what the interfaces themselves extend and would /// report a plain IDerived as ambiguous with the IBase behind it. /// - private static ImmutableArray DeclaredInterfaces(INamedTypeSymbol implementation) { - for (var type = implementation; type != null; type = type.BaseType) { - if (type.Interfaces.Length > 0) { + private static ImmutableArray DeclaredInterfaces( + INamedTypeSymbol implementation + ) + { + for (var type = implementation; type != null; type = type.BaseType) + { + if (type.Interfaces.Length > 0) + { return type.Interfaces; } } @@ -483,28 +615,37 @@ private static ImmutableArray DeclaredInterfaces(INamedTypeSym /// The implementation's type parameters and their constraints, which the wrapper repeats so its /// own parameters line up with the ones the service and the implementation are closed over. /// - private static IReadOnlyList TypeParameterModels(INamedTypeSymbol symbol) { - if (symbol.TypeParameters.Length == 0) { + private static IReadOnlyList TypeParameterModels(INamedTypeSymbol symbol) + { + if (symbol.TypeParameters.Length == 0) + { return Array.Empty(); } var models = new TypeParameterModel[symbol.TypeParameters.Length]; - for (var i = 0; i < models.Length; i++) { + for (var i = 0; i < models.Length; i++) + { models[i] = TypeParameterReader.Read(symbol.TypeParameters[i]); } return models; } - private static ITypeDefinition ToTypeDefinition(INamedTypeSymbol symbol) { + private static ITypeDefinition ToTypeDefinition(INamedTypeSymbol symbol) + { var namespaceName = symbol.ContainingNamespace.IsGlobalNamespace ? "" : symbol.ContainingNamespace.ToDisplayString(); var name = symbol.Name; - for (var containing = symbol.ContainingType; containing != null; containing = containing.ContainingType) { + for ( + var containing = symbol.ContainingType; + containing != null; + containing = containing.ContainingType + ) + { name = containing.Name + "." + name; } @@ -527,17 +668,24 @@ private static ITypeDefinition ToTypeDefinition(INamedTypeSymbol symbol) { private static List FindAttributes( TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel semanticModel, - CancellationToken cancellationToken) { - + CancellationToken cancellationToken + ) + { var attributes = new List(); - foreach (var attributeList in typeDeclarationSyntax.AttributeLists) { - foreach (var attribute in attributeList.Attributes) { - if (AttributeTypeMatcher.Matches( + foreach (var attributeList in typeDeclarationSyntax.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + if ( + AttributeTypeMatcher.Matches( semanticModel, attribute, KnownTypes.DependencyModules.Attributes.InterceptAttribute, - cancellationToken)) { + cancellationToken + ) + ) + { attributes.Add(attribute); } } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/ModuleDecoratorResolver.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/ModuleDecoratorResolver.cs index ea22b61..88b9b57 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/ModuleDecoratorResolver.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/ModuleDecoratorResolver.cs @@ -18,8 +18,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// new for it, and emitting one is what makes the decoration survive publishing. /// /// -public static class ModuleDecoratorResolver { - +public static class ModuleDecoratorResolver +{ /// /// A resolved decorator, or the reason it could not be. /// @@ -35,11 +35,15 @@ public record Resolution(DecoratorModel Model, string? Reason); public static IReadOnlyList Resolve( ModuleEntryPointModel entryPointModel, Compilation compilation, - CancellationToken cancellationToken) { - + CancellationToken cancellationToken + ) + { var resolutions = new List(); - foreach (var decorator in DecoratorModelUtility.GetModuleDeclaredDecorators(entryPointModel)) { + foreach ( + var decorator in DecoratorModelUtility.GetModuleDeclaredDecorators(entryPointModel) + ) + { cancellationToken.ThrowIfCancellationRequested(); resolutions.Add(Resolve(decorator, compilation)); @@ -48,32 +52,44 @@ public static IReadOnlyList Resolve( return resolutions; } - private static Resolution Resolve(DecoratorModel decorator, Compilation compilation) { + private static Resolution Resolve(DecoratorModel decorator, Compilation compilation) + { var symbol = Find(compilation, decorator.DecoratorType); - if (symbol == null) { + if (symbol == null) + { return new Resolution( - decorator, "its type could not be resolved from this compilation or its references"); + decorator, + "its type could not be resolved from this compilation or its references" + ); } var constructor = SymbolConstructorReader.Read(symbol); - if (constructor == null) { + if (constructor == null) + { return new Resolution(decorator, "it has no public constructor"); } var innerIndex = IndexOfInner(constructor, decorator.ServiceType); - if (innerIndex < 0) { + if (innerIndex < 0) + { return new Resolution( decorator, - $"no constructor parameter takes '{decorator.ServiceType.Name}', so there is nowhere " + - "to pass the instance being wrapped"); + $"no constructor parameter takes '{decorator.ServiceType.Name}', so there is nowhere " + + "to pass the instance being wrapped" + ); } return new Resolution( - decorator with { Constructor = constructor, InnerParameterIndex = innerIndex }, - null); + decorator with + { + Constructor = constructor, + InnerParameterIndex = innerIndex, + }, + null + ); } /// @@ -86,13 +102,18 @@ private static Resolution Resolve(DecoratorModel decorator, Compilation compilat /// parameter type keeps its names, because closing the decorator over a registration reads the /// type parameter order back off it. /// - private static int IndexOfInner(ConstructorInfoModel constructor, ITypeDefinition serviceType) { + private static int IndexOfInner(ConstructorInfoModel constructor, ITypeDefinition serviceType) + { var wanted = serviceType.ToUnboundGeneric(); - for (var i = 0; i < constructor.Parameters.Count; i++) { + for (var i = 0; i < constructor.Parameters.Count; i++) + { var parameterType = constructor.Parameters[i].ParameterType.MakeNullable(false); - if (parameterType.Equals(serviceType) || parameterType.ToUnboundGeneric().Equals(wanted)) { + if ( + parameterType.Equals(serviceType) || parameterType.ToUnboundGeneric().Equals(wanted) + ) + { return i; } } @@ -107,10 +128,14 @@ private static int IndexOfInner(ConstructorInfoModel constructor, ITypeDefinitio /// A generic decorator arrives with its arguments blanked, because an unbound generic is what a /// typeof can carry — so the metadata name needs the arity back on it. /// - private static INamedTypeSymbol? Find(Compilation compilation, ITypeDefinition type) { - var name = string.IsNullOrEmpty(type.Namespace) ? type.Name : type.Namespace + "." + type.Name; - - if (type is GenericTypeDefinition { TypeArguments.Count: > 0 } generic) { + private static INamedTypeSymbol? Find(Compilation compilation, ITypeDefinition type) + { + var name = string.IsNullOrEmpty(type.Namespace) + ? type.Name + : type.Namespace + "." + type.Name; + + if (type is GenericTypeDefinition { TypeArguments.Count: > 0 } generic) + { name += "`" + generic.TypeArguments.Count; } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs index 65866b6..39cb958 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs @@ -7,7 +7,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; -public class ServiceModelUtility { +public class ServiceModelUtility +{ /// /// Interfaces that describe a capability rather than a role, keyed by namespace and name. /// @@ -41,11 +42,12 @@ public class ServiceModelUtility { /// typeof(IDisposable))] still registers IDisposable. This governs inference only. /// /// - private static readonly HashSet _capabilityInterfaces = new() { + private static readonly HashSet _capabilityInterfaces = new() + { "System.IDisposable", "System.IAsyncDisposable", "System.ICloneable", - "System.IComparable", // covers IComparable, same name + "System.IComparable", // covers IComparable, same name "System.IEquatable", "System.IConvertible", "System.IFormattable", @@ -57,28 +59,44 @@ public class ServiceModelUtility { "System.Runtime.Serialization.ISerializable", "System.ComponentModel.INotifyPropertyChanged", "System.ComponentModel.INotifyPropertyChanging", - "System.Collections.Specialized.INotifyCollectionChanged" + "System.Collections.Specialized.INotifyCollectionChanged", }; - private static readonly ITypeDefinition _crossWireService = - KnownTypes.DependencyModules.Attributes.CrossWireServiceAttribute; - - private static readonly ITypeDefinition _serializerService = - KnownTypes.Microsoft.TextJson.JsonSourceGenerationOptionsAttribute; - - private static readonly ITypeDefinition[] _attributeTypes = { - KnownTypes.DependencyModules.Attributes.TransientServiceAttribute, KnownTypes.DependencyModules.Attributes.ScopedServiceAttribute, KnownTypes.DependencyModules.Attributes.SingletonServiceAttribute, + private static readonly ITypeDefinition _crossWireService = KnownTypes + .DependencyModules + .Attributes + .CrossWireServiceAttribute; + + private static readonly ITypeDefinition _serializerService = KnownTypes + .Microsoft + .TextJson + .JsonSourceGenerationOptionsAttribute; + + private static readonly ITypeDefinition[] _attributeTypes = + { + KnownTypes.DependencyModules.Attributes.TransientServiceAttribute, + KnownTypes.DependencyModules.Attributes.ScopedServiceAttribute, + KnownTypes.DependencyModules.Attributes.SingletonServiceAttribute, }; /// /// The lifetime a service attribute type carries. /// - private static ServiceLifestyle LifestyleOf(ITypeDefinition attributeType) { - if (attributeType.Name == KnownTypes.DependencyModules.Attributes.SingletonServiceAttribute.Name) { + private static ServiceLifestyle LifestyleOf(ITypeDefinition attributeType) + { + if ( + attributeType.Name + == KnownTypes.DependencyModules.Attributes.SingletonServiceAttribute.Name + ) + { return ServiceLifestyle.Singleton; } - if (attributeType.Name == KnownTypes.DependencyModules.Attributes.ScopedServiceAttribute.Name) { + if ( + attributeType.Name + == KnownTypes.DependencyModules.Attributes.ScopedServiceAttribute.Name + ) + { return ServiceLifestyle.Scoped; } @@ -86,53 +104,84 @@ private static ServiceLifestyle LifestyleOf(ITypeDefinition attributeType) { } public static ServiceModel? GetServiceModel( - SyntaxTransformContext context, CancellationToken cancellationToken) { + SyntaxTransformContext context, + CancellationToken cancellationToken + ) + { cancellationToken.ThrowIfCancellationRequested(); - if (context.Node is ClassDeclarationSyntax or RecordDeclarationSyntax) { + if (context.Node is ClassDeclarationSyntax or RecordDeclarationSyntax) + { return GetClassDeclarationServiceModel(context, cancellationToken); } - if (context.Node is MethodDeclarationSyntax methodDeclarationSyntax) { - return MethodDeclarationServiceModel(context, methodDeclarationSyntax, cancellationToken); + if (context.Node is MethodDeclarationSyntax methodDeclarationSyntax) + { + return MethodDeclarationServiceModel( + context, + methodDeclarationSyntax, + cancellationToken + ); } return null; } - private static ServiceModel? MethodDeclarationServiceModel(SyntaxTransformContext context, MethodDeclarationSyntax methodDeclarationSyntax, CancellationToken cancellationToken) { + private static ServiceModel? MethodDeclarationServiceModel( + SyntaxTransformContext context, + MethodDeclarationSyntax methodDeclarationSyntax, + CancellationToken cancellationToken + ) + { // only support public or internal factory methods - if (methodDeclarationSyntax.Modifiers.Any( - m => m.IsKind(SyntaxKind.PrivateKeyword) || m.IsKind(SyntaxKind.ProtectedKeyword))) { + if ( + methodDeclarationSyntax.Modifiers.Any(m => + m.IsKind(SyntaxKind.PrivateKeyword) || m.IsKind(SyntaxKind.ProtectedKeyword) + ) + ) + { return null; } // only support static methods - if (!methodDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword))) { + if (!methodDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword))) + { return null; } var returnType = methodDeclarationSyntax.ReturnType.GetTypeDefinition(context); var factoryModel = GetFactoryModel(context, methodDeclarationSyntax, cancellationToken); - if (returnType == null || factoryModel == null) { + if (returnType == null || factoryModel == null) + { return null; } - var models = - AttributeModelHelper.GetAttributeModels(context, context.Node, cancellationToken); + var models = AttributeModelHelper.GetAttributeModels( + context, + context.Node, + cancellationToken + ); return new ServiceModel( returnType, null, - factoryModel, null, + factoryModel, + null, GetRegistrations(context, returnType, models, cancellationToken), RegistrationFeature.None, - Location: LocationModel.From(context.Node)); + Location: LocationModel.From(context.Node) + ); } - private static ServiceFactoryModel? GetFactoryModel(SyntaxTransformContext context, MethodDeclarationSyntax methodDeclarationSyntax, CancellationToken cancellationToken) { + private static ServiceFactoryModel? GetFactoryModel( + SyntaxTransformContext context, + MethodDeclarationSyntax methodDeclarationSyntax, + CancellationToken cancellationToken + ) + { var factoryClass = methodDeclarationSyntax.FirstAncestorOrSelf(); - if (factoryClass == null) { + if (factoryClass == null) + { return null; } @@ -141,7 +190,8 @@ private static ServiceLifestyle LifestyleOf(ITypeDefinition attributeType) { return new ServiceFactoryModel( factoryType, methodDeclarationSyntax.Identifier.ToString().Trim('"'), - methodDeclarationSyntax.GetMethodParameters(context, cancellationToken)); + methodDeclarationSyntax.GetMethodParameters(context, cancellationToken) + ); } /// @@ -149,22 +199,28 @@ private static ServiceLifestyle LifestyleOf(ITypeDefinition attributeType) { /// report it instead of emitting a registration that fails when the provider is built. /// private static RegistrationFeature GetConstructionFeatures( - SyntaxTransformContext context, CancellationToken cancellationToken) { - - if (context.Node is not TypeDeclarationSyntax typeDeclarationSyntax) { + SyntaxTransformContext context, + CancellationToken cancellationToken + ) + { + if (context.Node is not TypeDeclarationSyntax typeDeclarationSyntax) + { return RegistrationFeature.None; } var features = RegistrationFeature.None; - if (typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword))) { + if (typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword))) + { features |= RegistrationFeature.StaticImplementation; } - else if (typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.AbstractKeyword))) { + else if (typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.AbstractKeyword))) + { features |= RegistrationFeature.AbstractImplementation; } - if (IsIntercepted(typeDeclarationSyntax, context, cancellationToken)) { + if (IsIntercepted(typeDeclarationSyntax, context, cancellationToken)) + { features |= RegistrationFeature.Intercepted; } @@ -183,15 +239,22 @@ private static RegistrationFeature GetConstructionFeatures( private static bool IsIntercepted( TypeDeclarationSyntax typeDeclarationSyntax, SyntaxTransformContext context, - CancellationToken cancellationToken) { - - foreach (var attributeList in typeDeclarationSyntax.AttributeLists) { - foreach (var attribute in attributeList.Attributes) { - if (AttributeTypeMatcher.Matches( + CancellationToken cancellationToken + ) + { + foreach (var attributeList in typeDeclarationSyntax.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + if ( + AttributeTypeMatcher.Matches( context.SemanticModel, attribute, KnownTypes.DependencyModules.Attributes.InterceptAttribute, - cancellationToken)) { + cancellationToken + ) + ) + { return true; } } @@ -200,29 +263,44 @@ private static bool IsIntercepted( return false; } - private static ServiceModel? GetClassDeclarationServiceModel(SyntaxTransformContext context, CancellationToken cancellationToken) { + private static ServiceModel? GetClassDeclarationServiceModel( + SyntaxTransformContext context, + CancellationToken cancellationToken + ) + { var classDefinition = GetClassDefinition(context); - if (classDefinition == null) { + if (classDefinition == null) + { return null; } - var attributes = - AttributeModelHelper.GetAttributeModels(context, context.Node, cancellationToken); + var attributes = AttributeModelHelper.GetAttributeModels( + context, + context.Node, + cancellationToken + ); - var registrations = GetRegistrations(context, classDefinition, attributes, cancellationToken); + var registrations = GetRegistrations( + context, + classDefinition, + attributes, + cancellationToken + ); - if (registrations.Count == 0) { + if (registrations.Count == 0) + { return new ServiceModel( classDefinition, GetConstructorInfo(context, context.Node, cancellationToken), null, FactoryOutput, - new[] { + new[] + { new ServiceRegistrationModel( KnownTypes.Microsoft.TextJson.IJsonTypeInfoResolver, ServiceLifestyle.Transient - ) + ), }, RegistrationFeature.AutoRegisterSourceGenerator, Location: LocationModel.From(context.Node) @@ -231,19 +309,25 @@ private static bool IsIntercepted( FactoryOutputDelegate? factoryOutput = null; - if (registrations.Any( - r => r.ServiceType.Equals(KnownTypes.Microsoft.TextJson.IJsonTypeInfoResolver))) { + if ( + registrations.Any(r => + r.ServiceType.Equals(KnownTypes.Microsoft.TextJson.IJsonTypeInfoResolver) + ) + ) + { factoryOutput = FactoryOutput; } - return new ServiceModel(classDefinition, + return new ServiceModel( + classDefinition, GetConstructorInfo(context, context.Node, cancellationToken), null, factoryOutput, registrations, GetConstructionFeatures(context, cancellationToken), EnvironmentConditionUtility.GetConditions(context, context.Node, cancellationToken), - LocationModel.From(context.Node)); + LocationModel.From(context.Node) + ); } /// @@ -263,92 +347,132 @@ private static bool IsIntercepted( /// type's parameters. /// /// - public static ConstructorInfoModel? GetConstructorInfo(SyntaxTransformContext context, SyntaxNode node, CancellationToken cancellationToken) { + public static ConstructorInfoModel? GetConstructorInfo( + SyntaxTransformContext context, + SyntaxNode node, + CancellationToken cancellationToken + ) + { var constructorList = new List(); var members = node is TypeDeclarationSyntax declaration ? declaration.Members.OfType() : node.DescendantNodes().OfType(); - foreach (var constructor in members) { - if (constructor.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword))) { + foreach (var constructor in members) + { + if (constructor.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword))) + { continue; } - if (constructor.AttributeLists.Any(attributeList => - attributeList.Attributes.Any( - a => a.Name.ToString() == "ActivatorUtilitiesConstructorAttribute" || - a.Name.ToString() == "ActivatorUtilitiesConstructor"))) { - - return new ConstructorInfoModel(constructor.GetMethodParameters(context, cancellationToken)); + if ( + constructor.AttributeLists.Any(attributeList => + attributeList.Attributes.Any(a => + a.Name.ToString() == "ActivatorUtilitiesConstructorAttribute" + || a.Name.ToString() == "ActivatorUtilitiesConstructor" + ) + ) + ) + { + return new ConstructorInfoModel( + constructor.GetMethodParameters(context, cancellationToken) + ); } constructorList.Add(constructor); } - if (node is TypeDeclarationSyntax { ParameterList.Parameters.Count: > 0 } typeDeclarationSyntax) { + if ( + node is TypeDeclarationSyntax + { + ParameterList.Parameters.Count: > 0 + } typeDeclarationSyntax + ) + { return new ConstructorInfoModel( - typeDeclarationSyntax.ParameterList.GetParameters(context, cancellationToken)); + typeDeclarationSyntax.ParameterList.GetParameters(context, cancellationToken) + ); } - - if (constructorList.Count == 0) { + + if (constructorList.Count == 0) + { return new ConstructorInfoModel(ImmutableArray.Empty); } - if (constructorList.Count == 1) { + if (constructorList.Count == 1) + { var constructor = constructorList[0]; - return new ConstructorInfoModel(constructor.GetMethodParameters(context, cancellationToken)); + return new ConstructorInfoModel( + constructor.GetMethodParameters(context, cancellationToken) + ); } constructorList.Sort( - (a, b) => - a.ParameterList.Parameters.Count.CompareTo(b.ParameterList.Parameters.Count)); + (a, b) => a.ParameterList.Parameters.Count.CompareTo(b.ParameterList.Parameters.Count) + ); return new ConstructorInfoModel( constructorList.Last().GetMethodParameters(context, cancellationToken) ); } - private static IOutputComponent? FactoryOutput(ServiceModel servicemodel, ServiceRegistrationModel registrationmodel) { + private static IOutputComponent? FactoryOutput( + ServiceModel servicemodel, + ServiceRegistrationModel registrationmodel + ) + { var signature = "_ => "; - if (registrationmodel.Key != null) { + if (registrationmodel.Key != null) + { signature = "(_,_) => "; } var component = CodeOutputComponent.Get( - $"{signature}{servicemodel.ImplementationType.Namespace}.{servicemodel.ImplementationType.Name}.Default"); + $"{signature}{servicemodel.ImplementationType.Namespace}.{servicemodel.ImplementationType.Name}.Default" + ); return component; } - private static ITypeDefinition? GetClassDefinition(SyntaxTransformContext context) { + private static ITypeDefinition? GetClassDefinition(SyntaxTransformContext context) + { ITypeDefinition? classTypeDefinition = null; - if (context.Node is TypeDeclarationSyntax typeDeclarationSyntax) { + if (context.Node is TypeDeclarationSyntax typeDeclarationSyntax) + { classTypeDefinition = GetTypeDeclarationDefinition(typeDeclarationSyntax); } return classTypeDefinition; } - private static ITypeDefinition GetTypeDeclarationDefinition(TypeDeclarationSyntax typeDeclarationSyntax) { + private static ITypeDefinition GetTypeDeclarationDefinition( + TypeDeclarationSyntax typeDeclarationSyntax + ) + { ITypeDefinition classTypeDefinition; var declaredName = GetDeclaredName(typeDeclarationSyntax); - if (typeDeclarationSyntax.TypeParameterList is { Parameters.Count: > 0 }) { - classTypeDefinition = - new GenericTypeDefinition( - TypeDefinitionEnum.ClassDefinition, - typeDeclarationSyntax.GetNamespace(), - declaredName, - typeDeclarationSyntax.TypeParameterList.Parameters.Select(_ => TypeDefinition.Get("", "")) - .ToArray() - ); + if (typeDeclarationSyntax.TypeParameterList is { Parameters.Count: > 0 }) + { + classTypeDefinition = new GenericTypeDefinition( + TypeDefinitionEnum.ClassDefinition, + typeDeclarationSyntax.GetNamespace(), + declaredName, + typeDeclarationSyntax + .TypeParameterList.Parameters.Select(_ => TypeDefinition.Get("", "")) + .ToArray() + ); } - else { - classTypeDefinition = TypeDefinition.Get(typeDeclarationSyntax.GetNamespace(), declaredName); + else + { + classTypeDefinition = TypeDefinition.Get( + typeDeclarationSyntax.GetNamespace(), + declaredName + ); } return classTypeDefinition; @@ -358,35 +482,67 @@ private static ITypeDefinition GetTypeDeclarationDefinition(TypeDeclarationSynta /// The type's name qualified by any containing types, so a nested service is referenced as /// Outer.Inner rather than Inner, which would resolve against the namespace and fail to compile. /// - private static string GetDeclaredName(TypeDeclarationSyntax typeDeclarationSyntax) { + private static string GetDeclaredName(TypeDeclarationSyntax typeDeclarationSyntax) + { var name = typeDeclarationSyntax.Identifier.ToString(); - foreach (var containingType in typeDeclarationSyntax.Ancestors().OfType()) { + foreach ( + var containingType in typeDeclarationSyntax.Ancestors().OfType() + ) + { name = containingType.Identifier + "." + name; } return name; } - private static List GetRegistrations(SyntaxTransformContext context, ITypeDefinition classDefinition, IReadOnlyList attributes, CancellationToken cancellationToken) { + private static List GetRegistrations( + SyntaxTransformContext context, + ITypeDefinition classDefinition, + IReadOnlyList attributes, + CancellationToken cancellationToken + ) + { var list = new List(); - foreach (var attributeSyntax in - context.Node.DescendantNodes().OfType()) { - foreach (var typeDefinition in _attributeTypes) { + foreach (var attributeSyntax in context.Node.DescendantNodes().OfType()) + { + foreach (var typeDefinition in _attributeTypes) + { cancellationToken.ThrowIfCancellationRequested(); // Resolved, not compared as written: a namespace-qualified usage, a global:: prefix // and a using alias all name the same attribute, and all of them used to be silently // skipped — leaving the class unregistered with nothing to say so. - if (AttributeTypeMatcher.Matches( - context.SemanticModel, attributeSyntax, typeDefinition, cancellationToken)) { - list.Add(GetServiceRegistration(context, attributeSyntax, classDefinition, typeDefinition)); + if ( + AttributeTypeMatcher.Matches( + context.SemanticModel, + attributeSyntax, + typeDefinition, + cancellationToken + ) + ) + { + list.Add( + GetServiceRegistration( + context, + attributeSyntax, + classDefinition, + typeDefinition + ) + ); } } - if (AttributeTypeMatcher.Matches( - context.SemanticModel, attributeSyntax, _crossWireService, cancellationToken)) { + if ( + AttributeTypeMatcher.Matches( + context.SemanticModel, + attributeSyntax, + _crossWireService, + cancellationToken + ) + ) + { list.AddRange(GetCrossWiredService(context, attributeSyntax, classDefinition)); } } @@ -394,8 +550,12 @@ private static List GetRegistrations(SyntaxTransformCo return list; } - private static IEnumerable GetCrossWiredService(SyntaxTransformContext context, AttributeSyntax attributeSyntax, ITypeDefinition classDefinition) { - + private static IEnumerable GetCrossWiredService( + SyntaxTransformContext context, + AttributeSyntax attributeSyntax, + ITypeDefinition classDefinition + ) + { RegistrationType? registrationType = null; ITypeDefinition? realm = null; object? key = null; @@ -403,23 +563,33 @@ private static IEnumerable GetCrossWiredService(Syntax var order = 0; var namespaces = new List(); - if (attributeSyntax.ArgumentList != null) { - foreach (var argumentSyntax in attributeSyntax.ArgumentList.Arguments) { - if (argumentSyntax.NameEquals != null) { - switch (argumentSyntax.NameEquals.Name.ToString()) { + if (attributeSyntax.ArgumentList != null) + { + foreach (var argumentSyntax in attributeSyntax.ArgumentList.Arguments) + { + if (argumentSyntax.NameEquals != null) + { + switch (argumentSyntax.NameEquals.Name.ToString()) + { case "Key": key = argumentSyntax.Expression.ToString(); - if (argumentSyntax.Expression is MemberAccessExpressionSyntax accessExpressionSyntax) { + if ( + argumentSyntax.Expression + is MemberAccessExpressionSyntax accessExpressionSyntax + ) + { var type = accessExpressionSyntax.GetTypeDefinition(context); - if (type != null) { + if (type != null) + { namespaces.AddRange(type.KnownNamespaces); } } break; case "Using": - registrationType = - BaseSourceGenerator.GetRegistrationType(argumentSyntax.Expression.ToString()); + registrationType = BaseSourceGenerator.GetRegistrationType( + argumentSyntax.Expression.ToString() + ); break; case "Lifetime": @@ -427,13 +597,20 @@ private static IEnumerable GetCrossWiredService(Syntax break; case "Realm": - if (argumentSyntax.Expression is TypeOfExpressionSyntax realmType) { + if (argumentSyntax.Expression is TypeOfExpressionSyntax realmType) + { realm = realmType.Type.GetTypeDefinition(context); } break; case "Order": - if (int.TryParse(argumentSyntax.Expression.ToString(), out var parsedOrder)) { + if ( + int.TryParse( + argumentSyntax.Expression.ToString(), + out var parsedOrder + ) + ) + { order = parsedOrder; } break; @@ -442,11 +619,14 @@ private static IEnumerable GetCrossWiredService(Syntax } } - if (context.Node is TypeDeclarationSyntax { BaseList: not null } typeDeclarationSyntax) { - foreach (var baseTypeSyntax in typeDeclarationSyntax.BaseList.Types) { + if (context.Node is TypeDeclarationSyntax { BaseList: not null } typeDeclarationSyntax) + { + foreach (var baseTypeSyntax in typeDeclarationSyntax.BaseList.Types) + { var type = baseTypeSyntax.Type.GetTypeDefinition(context); - if (type?.TypeDefinitionEnum == TypeDefinitionEnum.InterfaceDefinition) { + if (type?.TypeDefinitionEnum == TypeDefinitionEnum.InterfaceDefinition) + { yield return new ServiceRegistrationModel( type, lifestyle, @@ -462,17 +642,18 @@ private static IEnumerable GetCrossWiredService(Syntax } } - private static ServiceLifestyle GetLifestyle(string toString) { + private static ServiceLifestyle GetLifestyle(string toString) + { // The value arrives as written in source, normally qualified: "ServiceLifetime.Scoped". // Parsing that whole string fails, and the silent fallback below then registered every // cross-wired service as a singleton regardless of the lifetime the developer asked for. var separatorIndex = toString.LastIndexOf('.'); - var value = separatorIndex >= 0 - ? toString.Substring(separatorIndex + 1).Trim() - : toString.Trim(); + var value = + separatorIndex >= 0 ? toString.Substring(separatorIndex + 1).Trim() : toString.Trim(); - if (Enum.TryParse(value, out ServiceLifestyle lifestyle)) { + if (Enum.TryParse(value, out ServiceLifestyle lifestyle)) + { return lifestyle; } @@ -483,8 +664,9 @@ private static ServiceRegistrationModel GetServiceRegistration( SyntaxTransformContext context, AttributeSyntax attributeSyntax, ITypeDefinition classDefinition, - ITypeDefinition attributeType) { - + ITypeDefinition attributeType + ) + { // The lifetime comes from the attribute type the usage resolved to, not from how the usage // was spelled. Reading it back off `attributeSyntax.Name` meant only a spelling that literally // began with "Singleton" or "Scoped" produced that lifetime: a qualified name, a global:: @@ -503,43 +685,66 @@ private static ServiceRegistrationModel GetServiceRegistration( object? key = null; var namespaces = new List(); - if (attributeSyntax.ArgumentList != null) { - foreach (var argumentSyntax in attributeSyntax.ArgumentList.Arguments) { - if (argumentSyntax.NameEquals != null) { - switch (argumentSyntax.NameEquals.Name.ToString()) { + if (attributeSyntax.ArgumentList != null) + { + foreach (var argumentSyntax in attributeSyntax.ArgumentList.Arguments) + { + if (argumentSyntax.NameEquals != null) + { + switch (argumentSyntax.NameEquals.Name.ToString()) + { case "Key": key = argumentSyntax.Expression.ToString(); - if (argumentSyntax.Expression is MemberAccessExpressionSyntax accessExpressionSyntax) { + if ( + argumentSyntax.Expression + is MemberAccessExpressionSyntax accessExpressionSyntax + ) + { var type = accessExpressionSyntax.GetTypeDefinition(context); - if (type != null) { + if (type != null) + { namespaces.AddRange(type.KnownNamespaces); } } break; case "Using": - registrationType = - BaseSourceGenerator.GetRegistrationType(argumentSyntax.Expression.ToString()); + registrationType = BaseSourceGenerator.GetRegistrationType( + argumentSyntax.Expression.ToString() + ); break; case "As": - if (argumentSyntax.Expression is TypeOfExpressionSyntax typeOfExpression) { + if ( + argumentSyntax.Expression is TypeOfExpressionSyntax typeOfExpression + ) + { registration = typeOfExpression.Type.GetTypeDefinition(context); - if (registration is GenericTypeDefinition) { - registration = ReplaceGenericParametersForRegistration(registration); + if (registration is GenericTypeDefinition) + { + registration = ReplaceGenericParametersForRegistration( + registration + ); } } break; case "Realm": - if (argumentSyntax.Expression is TypeOfExpressionSyntax realmType) { + if (argumentSyntax.Expression is TypeOfExpressionSyntax realmType) + { realm = realmType.Type.GetTypeDefinition(context); } break; case "Order": - if (int.TryParse(argumentSyntax.Expression.ToString(), out var parsedOrder)) { + if ( + int.TryParse( + argumentSyntax.Expression.ToString(), + out var parsedOrder + ) + ) + { order = parsedOrder; } break; @@ -561,7 +766,10 @@ private static ServiceRegistrationModel GetServiceRegistration( } private static ITypeDefinition GetServiceTypeFromClass( - SyntaxTransformContext context, ITypeDefinition classDefinition) { + SyntaxTransformContext context, + ITypeDefinition classDefinition + ) + { return GetBaseTypeRegistration(context) ?? classDefinition; } @@ -570,27 +778,41 @@ private static ITypeDefinition GetServiceTypeFromClass( /// declared interface that is not a capability, else the /// first one a base class provides. /// - private static ITypeDefinition? GetBaseTypeRegistration(SyntaxTransformContext context) { - if (context.Node is TypeDeclarationSyntax { BaseList: not null } typeDeclarationSyntax) { + private static ITypeDefinition? GetBaseTypeRegistration(SyntaxTransformContext context) + { + if (context.Node is TypeDeclarationSyntax { BaseList: not null } typeDeclarationSyntax) + { INamedTypeSymbol? baseClassSymbol = null; - foreach (var baseTypeSyntax in typeDeclarationSyntax.BaseList.Types) { - var symbolInfo = ModelExtensions.GetSymbolInfo(context.SemanticModel, baseTypeSyntax.Type); + foreach (var baseTypeSyntax in typeDeclarationSyntax.BaseList.Types) + { + var symbolInfo = ModelExtensions.GetSymbolInfo( + context.SemanticModel, + baseTypeSyntax.Type + ); - if (symbolInfo.Symbol is INamedTypeSymbol namedTypeSymbol) { - var baseTypeDefinition = - namedTypeSymbol.GetTypeDefinitionFromNamedSymbol(); + if (symbolInfo.Symbol is INamedTypeSymbol namedTypeSymbol) + { + var baseTypeDefinition = namedTypeSymbol.GetTypeDefinitionFromNamedSymbol(); // only auto register interfaces - if (baseTypeDefinition is { TypeDefinitionEnum: TypeDefinitionEnum.InterfaceDefinition }) { + if ( + baseTypeDefinition is + { TypeDefinitionEnum: TypeDefinitionEnum.InterfaceDefinition } + ) + { // Passed over rather than remembered: a skipped interface must not become the // symbol walked below, or IEnumerable would hand back IEnumerable. - if (SkipInterface(baseTypeDefinition)) { + if (SkipInterface(baseTypeDefinition)) + { continue; } - if (baseTypeDefinition is GenericTypeDefinition) { - baseTypeDefinition = ReplaceGenericParametersForRegistration(baseTypeDefinition); + if (baseTypeDefinition is GenericTypeDefinition) + { + baseTypeDefinition = ReplaceGenericParametersForRegistration( + baseTypeDefinition + ); } return baseTypeDefinition; @@ -600,7 +822,8 @@ private static ITypeDefinition GetServiceTypeFromClass( } } - if (baseClassSymbol != null) { + if (baseClassSymbol != null) + { return GetBaseInterface(context, baseClassSymbol); } } @@ -608,26 +831,31 @@ private static ITypeDefinition GetServiceTypeFromClass( return null; } - - private static ITypeDefinition? GetBaseInterface(SyntaxTransformContext context, INamedTypeSymbol baseTypeSymbol) { - foreach (var interfaceSymbol in baseTypeSymbol.Interfaces) { - var interfaceType = - interfaceSymbol.GetTypeDefinitionFromNamedSymbol(); + private static ITypeDefinition? GetBaseInterface( + SyntaxTransformContext context, + INamedTypeSymbol baseTypeSymbol + ) + { + foreach (var interfaceSymbol in baseTypeSymbol.Interfaces) + { + var interfaceType = interfaceSymbol.GetTypeDefinitionFromNamedSymbol(); // only auto register interfaces - if (interfaceType == null || - SkipInterface(interfaceType)) { + if (interfaceType == null || SkipInterface(interfaceType)) + { continue; } - if (interfaceType is GenericTypeDefinition) { + if (interfaceType is GenericTypeDefinition) + { interfaceType = ReplaceGenericParametersForRegistration(interfaceType); } return interfaceType; } - if (baseTypeSymbol.BaseType == null) { + if (baseTypeSymbol.BaseType == null) + { return null; } @@ -645,10 +873,15 @@ private static ITypeDefinition GetServiceTypeFromClass( private static bool SkipInterface(ITypeDefinition interfaceType) => _capabilityInterfaces.Contains($"{interfaceType.Namespace}.{interfaceType.Name}"); - private static ITypeDefinition ReplaceGenericParametersForRegistration(ITypeDefinition registration) { - var argumentTypes = - registration.TypeArguments.Select( - _ => _ is TypeParameterDefinition ? TypeDefinition.Get("", "") : _).ToArray(); + private static ITypeDefinition ReplaceGenericParametersForRegistration( + ITypeDefinition registration + ) + { + var argumentTypes = registration + .TypeArguments.Select(_ => + _ is TypeParameterDefinition ? TypeDefinition.Get("", "") : _ + ) + .ToArray(); registration = new GenericTypeDefinition( registration.TypeDefinitionEnum, @@ -656,7 +889,7 @@ private static ITypeDefinition ReplaceGenericParametersForRegistration(ITypeDefi registration.Name, argumentTypes ); - + return registration; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/SymbolConstructorReader.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/SymbolConstructorReader.cs index ca933b4..f29960c 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/SymbolConstructorReader.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/SymbolConstructorReader.cs @@ -21,8 +21,8 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// every resolution and is the shape a published Native AOT build has no code for. /// /// -public static class SymbolConstructorReader { - +public static class SymbolConstructorReader +{ private const string ActivatorUtilitiesConstructor = "ActivatorUtilitiesConstructorAttribute"; private const string FromKeyedServices = "FromKeyedServicesAttribute"; @@ -35,7 +35,8 @@ public static class SymbolConstructorReader { /// it. Falling back to something reflective would trade a build error for a failure at resolve /// time in a published application. /// - public static ConstructorInfoModel? Read(INamedTypeSymbol type) { + public static ConstructorInfoModel? Read(INamedTypeSymbol type) + { var chosen = Choose(type); return chosen == null ? null : new ConstructorInfoModel(Parameters(chosen)); @@ -49,21 +50,27 @@ public static class SymbolConstructorReader { /// opted into a specific constructor and silently got a different one is the kind of difference /// nobody looks for. /// - private static IMethodSymbol? Choose(INamedTypeSymbol type) { + private static IMethodSymbol? Choose(INamedTypeSymbol type) + { IMethodSymbol? greediest = null; - foreach (var constructor in type.InstanceConstructors) { - if (constructor.DeclaredAccessibility != Accessibility.Public || constructor.IsStatic) { + foreach (var constructor in type.InstanceConstructors) + { + if (constructor.DeclaredAccessibility != Accessibility.Public || constructor.IsStatic) + { continue; } - foreach (var attribute in constructor.GetAttributes()) { - if (attribute.AttributeClass?.Name == ActivatorUtilitiesConstructor) { + foreach (var attribute in constructor.GetAttributes()) + { + if (attribute.AttributeClass?.Name == ActivatorUtilitiesConstructor) + { return constructor; } } - if (greediest == null || constructor.Parameters.Length > greediest.Parameters.Length) { + if (greediest == null || constructor.Parameters.Length > greediest.Parameters.Length) + { greediest = constructor; } } @@ -71,15 +78,20 @@ public static class SymbolConstructorReader { return greediest; } - private static IReadOnlyList Parameters(IMethodSymbol constructor) { + private static IReadOnlyList Parameters(IMethodSymbol constructor) + { var parameters = new List(constructor.Parameters.Length); - foreach (var parameter in constructor.Parameters) { - parameters.Add(new ParameterInfoModel( - parameter.Name, - TypeOf(parameter), - parameter.HasExplicitDefaultValue ? parameter.ExplicitDefaultValue : null, - Attributes(parameter))); + foreach (var parameter in constructor.Parameters) + { + parameters.Add( + new ParameterInfoModel( + parameter.Name, + TypeOf(parameter), + parameter.HasExplicitDefaultValue ? parameter.ExplicitDefaultValue : null, + Attributes(parameter) + ) + ); } return parameters; @@ -93,7 +105,8 @@ private static IReadOnlyList Parameters(IMethodSymbol constr /// GetService or GetRequiredService, so an optional dependency that lost its /// annotation would start throwing when the container simply does not have one. /// - private static ITypeDefinition TypeOf(IParameterSymbol parameter) { + private static ITypeDefinition TypeOf(IParameterSymbol parameter) + { var definition = parameter.Type.GetTypeDefinition(); return parameter.NullableAnnotation == NullableAnnotation.Annotated @@ -110,22 +123,33 @@ private static ITypeDefinition TypeOf(IParameterSymbol parameter) { /// resolves, and getting it wrong returns the right type and the wrong instance with nothing /// reported. /// - private static IReadOnlyList Attributes(IParameterSymbol parameter) { + private static IReadOnlyList Attributes(IParameterSymbol parameter) + { List? attributes = null; - foreach (var attribute in parameter.GetAttributes()) { - if (attribute.AttributeClass?.Name != FromKeyedServices || - attribute.ConstructorArguments.Length == 0) { + foreach (var attribute in parameter.GetAttributes()) + { + if ( + attribute.AttributeClass?.Name != FromKeyedServices + || attribute.ConstructorArguments.Length == 0 + ) + { continue; } attributes ??= new List(1); - attributes.Add(new AttributeModel( - KnownTypes.Microsoft.DependencyInjection.FromKeyedServicesAttribute, - new[] { new AttributeArgumentValue("key", attribute.ConstructorArguments[0].Value) }, - Array.Empty(), - Array.Empty())); + attributes.Add( + new AttributeModel( + KnownTypes.Microsoft.DependencyInjection.FromKeyedServicesAttribute, + new[] + { + new AttributeArgumentValue("key", attribute.ConstructorArguments[0].Value), + }, + Array.Empty(), + Array.Empty() + ) + ); } return (IReadOnlyList?)attributes ?? Array.Empty(); diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxNodeExtensions.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxNodeExtensions.cs index 84fcd52..d593868 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxNodeExtensions.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxNodeExtensions.cs @@ -5,28 +5,37 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; -public static class SyntaxNodeExtensions { - public static string GetNamespace(this BaseTypeDeclarationSyntax syntax) { +public static class SyntaxNodeExtensions +{ + public static string GetNamespace(this BaseTypeDeclarationSyntax syntax) + { var parentSyntaxNode = syntax.Parent; - while (parentSyntaxNode != null && - parentSyntaxNode is not NamespaceDeclarationSyntax && - parentSyntaxNode is not FileScopedNamespaceDeclarationSyntax) { + while ( + parentSyntaxNode != null + && parentSyntaxNode is not NamespaceDeclarationSyntax + && parentSyntaxNode is not FileScopedNamespaceDeclarationSyntax + ) + { parentSyntaxNode = parentSyntaxNode.Parent; } - if (parentSyntaxNode is BaseNamespaceDeclarationSyntax namespaceNode) { + if (parentSyntaxNode is BaseNamespaceDeclarationSyntax namespaceNode) + { return WalkNamespaceNodes(namespaceNode); } return ""; } - private static string WalkNamespaceNodes(BaseNamespaceDeclarationSyntax? namespaceNode) { + private static string WalkNamespaceNodes(BaseNamespaceDeclarationSyntax? namespaceNode) + { var stringBuilder = new StringBuilder(); - while (namespaceNode != null) { - if (stringBuilder.Length > 0) { + while (namespaceNode != null) + { + if (stringBuilder.Length > 0) + { stringBuilder.Insert(0, '.'); } @@ -38,51 +47,79 @@ private static string WalkNamespaceNodes(BaseNamespaceDeclarationSyntax? namespa return stringBuilder.ToString(); } - public static ITypeDefinition GetTypeDefinition(this TypeDeclarationSyntax typeDeclarationSyntax) { - var namespaceSyntax = typeDeclarationSyntax.Ancestors().OfType().FirstOrDefault(); - - return TypeDefinition.Get(namespaceSyntax?.Name.ToFullString().TrimEnd() ?? "", - typeDeclarationSyntax.Identifier.Text); + public static ITypeDefinition GetTypeDefinition( + this TypeDeclarationSyntax typeDeclarationSyntax + ) + { + var namespaceSyntax = typeDeclarationSyntax + .Ancestors() + .OfType() + .FirstOrDefault(); + + return TypeDefinition.Get( + namespaceSyntax?.Name.ToFullString().TrimEnd() ?? "", + typeDeclarationSyntax.Identifier.Text + ); } - public static AttributeSyntax? GetAttribute(this SyntaxNode node, string attributeName, string ns = "") { + public static AttributeSyntax? GetAttribute( + this SyntaxNode node, + string attributeName, + string ns = "" + ) + { return node.DescendantNodes() - .OfType().FirstOrDefault( - a => { - var name = a.Name.ToString(); - - return name.Equals(attributeName) || name.Equals(attributeName + "Attribute") || - name.Equals(ns + "." + attributeName) || name.Equals(ns + "." + attributeName + "Attribute"); - }); + .OfType() + .FirstOrDefault(a => + { + var name = a.Name.ToString(); + + return name.Equals(attributeName) + || name.Equals(attributeName + "Attribute") + || name.Equals(ns + "." + attributeName) + || name.Equals(ns + "." + attributeName + "Attribute"); + }); } - public static IEnumerable - GetAttributes(this SyntaxNode node, string attributeName, string ns = "") { + public static IEnumerable GetAttributes( + this SyntaxNode node, + string attributeName, + string ns = "" + ) + { return node.DescendantNodes() - .OfType().Where( - a => { - var name = a.Name.ToString(); - - return name.Equals(attributeName) || name.Equals(attributeName + "Attribute") || - name.Equals(ns + "." + attributeName) || name.Equals(ns + "." + attributeName + "Attribute"); - }); + .OfType() + .Where(a => + { + var name = a.Name.ToString(); + + return name.Equals(attributeName) + || name.Equals(attributeName + "Attribute") + || name.Equals(ns + "." + attributeName) + || name.Equals(ns + "." + attributeName + "Attribute"); + }); } - public static bool IsAttributed(this SyntaxNode node, string attributeName, string ns = "") { + public static bool IsAttributed(this SyntaxNode node, string attributeName, string ns = "") + { return node.DescendantNodes() - .OfType().Any( - a => { - var name = a.Name.ToString(); - - return name.Equals(attributeName) || name.Equals(attributeName + "Attribute") || - name.Equals(ns + "." + attributeName) || name.Equals(ns + "." + attributeName + "Attribute"); - }); + .OfType() + .Any(a => + { + var name = a.Name.ToString(); + + return name.Equals(attributeName) + || name.Equals(attributeName + "Attribute") + || name.Equals(ns + "." + attributeName) + || name.Equals(ns + "." + attributeName + "Attribute"); + }); } - public static bool IsAttributed(this SyntaxNode node, ITypeDefinition typeDefinition) { + public static bool IsAttributed(this SyntaxNode node, ITypeDefinition typeDefinition) + { var ns = typeDefinition.Namespace; var attributeName = typeDefinition.Name.Replace("Attribute", ""); return IsAttributed(node, attributeName, ns); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxSelector.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxSelector.cs index a01f5e9..f9cc9ee 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxSelector.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxSelector.cs @@ -4,15 +4,17 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; -public abstract class BaseSyntaxSelector { - private const string _attributeString = "Attribute"; +public abstract class BaseSyntaxSelector +{ + private const string _attributeString = "Attribute"; private readonly List _names; public bool AutoApproveCompilationUnit { get; set; } = false; - + public string ApproveFilter { get; set; } = ""; - - protected BaseSyntaxSelector(params ITypeDefinition[] attributes) { + + protected BaseSyntaxSelector(params ITypeDefinition[] attributes) + { _names = GetAttributeStrings(attributes); } @@ -27,14 +29,20 @@ protected BaseSyntaxSelector(params ITypeDefinition[] attributes) { /// module: no partial written, no diagnostic, and a CS0311 at the consumer's /// AddModule<T>() naming neither the attribute nor the omission. /// - private List GetAttributeStrings(ITypeDefinition[] attributes) { + private List GetAttributeStrings(ITypeDefinition[] attributes) + { var returnList = new List(); - foreach (var attribute in attributes) { + foreach (var attribute in attributes) + { returnList.Add(attribute.Name); - if (attribute.Name.EndsWith(_attributeString)) { - var simpleName = attribute.Name.Substring(0, attribute.Name.Length - _attributeString.Length); + if (attribute.Name.EndsWith(_attributeString)) + { + var simpleName = attribute.Name.Substring( + 0, + attribute.Name.Length - _attributeString.Length + ); returnList.Add(simpleName); } @@ -56,10 +64,12 @@ private List GetAttributeStrings(ITypeDefinition[] attributes) { /// regardless of which namespace it came from, so a same-named attribute from elsewhere was /// always a candidate and is filtered downstream as it always was. /// - private static string LastSegment(string attributeName) { + private static string LastSegment(string attributeName) + { var lastDot = attributeName.LastIndexOf('.'); - if (lastDot >= 0) { + if (lastDot >= 0) + { return attributeName.Substring(lastDot + 1); } @@ -69,75 +79,95 @@ private static string LastSegment(string attributeName) { } protected abstract bool TestForTypes(SyntaxNode node, CancellationToken token); - - public bool Where(SyntaxNode node, CancellationToken token) { - - if (!TestForTypes(node, token)) { + + public bool Where(SyntaxNode node, CancellationToken token) + { + if (!TestForTypes(node, token)) + { return false; } - if (node is MemberDeclarationSyntax memberDeclarationSyntax) { + if (node is MemberDeclarationSyntax memberDeclarationSyntax) + { return ProcessAttributeList(memberDeclarationSyntax.AttributeLists); } - if (node is CompilationUnitSyntax compilationUnitSyntax) { - return IsAutoApprove(compilationUnitSyntax) || - ProcessAttributeList(compilationUnitSyntax.AttributeLists); + if (node is CompilationUnitSyntax compilationUnitSyntax) + { + return IsAutoApprove(compilationUnitSyntax) + || ProcessAttributeList(compilationUnitSyntax.AttributeLists); } - + var found = node.DescendantNodes() - .OfType().Any(a => _names.Contains(LastSegment(a.Name.ToString()))); - + .OfType() + .Any(a => _names.Contains(LastSegment(a.Name.ToString()))); + return found; } - private bool IsAutoApprove(CompilationUnitSyntax compilationUnitSyntax) { - if (!AutoApproveCompilationUnit) { + private bool IsAutoApprove(CompilationUnitSyntax compilationUnitSyntax) + { + if (!AutoApproveCompilationUnit) + { return false; } - - return ApproveFilter == "" || - compilationUnitSyntax.SyntaxTree.FilePath.EndsWith(ApproveFilter); + + return ApproveFilter == "" + || compilationUnitSyntax.SyntaxTree.FilePath.EndsWith(ApproveFilter); } - private bool ProcessAttributeList(SyntaxList attributeLists) { + private bool ProcessAttributeList(SyntaxList attributeLists) + { var foundAttribute = false; - foreach (var attributeListSyntax in attributeLists) { - foreach (var attributeSyntax in attributeListSyntax.Attributes) { + foreach (var attributeListSyntax in attributeLists) + { + foreach (var attributeSyntax in attributeListSyntax.Attributes) + { foundAttribute = _names.Contains(LastSegment(attributeSyntax.Name.ToString())); - - if (foundAttribute) { + + if (foundAttribute) + { break; } } - if (foundAttribute) { + if (foundAttribute) + { break; } } - + return foundAttribute; } - } -public class SyntaxSelector : BaseSyntaxSelector where T : SyntaxNode { - public SyntaxSelector(params ITypeDefinition[] attributes) : base(attributes) {} - - protected override bool TestForTypes(SyntaxNode node, CancellationToken token) { - if (node is T) { +public class SyntaxSelector : BaseSyntaxSelector + where T : SyntaxNode +{ + public SyntaxSelector(params ITypeDefinition[] attributes) + : base(attributes) { } + + protected override bool TestForTypes(SyntaxNode node, CancellationToken token) + { + if (node is T) + { return true; } - + return false; } } - -public class SyntaxSelector : BaseSyntaxSelector where T1 : SyntaxNode where T2 : SyntaxNode { - public SyntaxSelector(params ITypeDefinition[] attributes) : base(attributes) {} - - protected override bool TestForTypes(SyntaxNode node, CancellationToken token) { - if (node is T1 or T2) { +public class SyntaxSelector : BaseSyntaxSelector + where T1 : SyntaxNode + where T2 : SyntaxNode +{ + public SyntaxSelector(params ITypeDefinition[] attributes) + : base(attributes) { } + + protected override bool TestForTypes(SyntaxNode node, CancellationToken token) + { + if (node is T1 or T2) + { return true; } @@ -145,14 +175,21 @@ protected override bool TestForTypes(SyntaxNode node, CancellationToken token) { } } -public class SyntaxSelector : BaseSyntaxSelector where T1 : SyntaxNode where T2 : SyntaxNode where T3 : SyntaxNode { - public SyntaxSelector(params ITypeDefinition[] attributes) : base(attributes) {} - - protected override bool TestForTypes(SyntaxNode node, CancellationToken token) { - if (node is T1 or T2 or T3) { +public class SyntaxSelector : BaseSyntaxSelector + where T1 : SyntaxNode + where T2 : SyntaxNode + where T3 : SyntaxNode +{ + public SyntaxSelector(params ITypeDefinition[] attributes) + : base(attributes) { } + + protected override bool TestForTypes(SyntaxNode node, CancellationToken token) + { + if (node is T1 or T2 or T3) + { return true; } return false; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxTransformContext.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxTransformContext.cs index 096d090..92415d7 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxTransformContext.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/SyntaxTransformContext.cs @@ -14,9 +14,10 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// Everything below the provider takes this instead, and the implicit conversions keep the call sites /// identical whichever provider they came from. /// -public readonly struct SyntaxTransformContext { - - public SyntaxTransformContext(SyntaxNode node, SemanticModel semanticModel) { +public readonly struct SyntaxTransformContext +{ + public SyntaxTransformContext(SyntaxNode node, SemanticModel semanticModel) + { Node = node; SemanticModel = semanticModel; } @@ -32,6 +33,7 @@ public static implicit operator SyntaxTransformContext(GeneratorSyntaxContext co /// The target node is the declaration the attribute was found on, which is the node a /// CreateSyntaxProvider predicate would have selected for the same attribute. /// - public static implicit operator SyntaxTransformContext(GeneratorAttributeSyntaxContext context) => - new(context.TargetNode, context.SemanticModel); + public static implicit operator SyntaxTransformContext( + GeneratorAttributeSyntaxContext context + ) => new(context.TargetNode, context.SemanticModel); } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/TypeParameterReader.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/TypeParameterReader.cs index 8d9d0fa..66ddacc 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/TypeParameterReader.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/TypeParameterReader.cs @@ -14,31 +14,41 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; /// implies it, and Roslyn reports a constructor constraint for a struct-constrained parameter /// even though repeating new() alongside it is CS0451. /// -public static class TypeParameterReader { - - public static TypeParameterModel Read(ITypeParameterSymbol parameter) { +public static class TypeParameterReader +{ + public static TypeParameterModel Read(ITypeParameterSymbol parameter) + { string? primary = null; - if (parameter.HasUnmanagedTypeConstraint) { + if (parameter.HasUnmanagedTypeConstraint) + { primary = "unmanaged"; - } else if (parameter.HasValueTypeConstraint) { + } + else if (parameter.HasValueTypeConstraint) + { primary = "struct"; - } else if (parameter.HasReferenceTypeConstraint) { - primary = parameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated - ? "class?" - : "class"; - } else if (parameter.HasNotNullConstraint) { + } + else if (parameter.HasReferenceTypeConstraint) + { + primary = + parameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated + ? "class?" + : "class"; + } + else if (parameter.HasNotNullConstraint) + { primary = "notnull"; } var constraintTypes = new ITypeDefinition[parameter.ConstraintTypes.Length]; - for (var i = 0; i < constraintTypes.Length; i++) { + for (var i = 0; i < constraintTypes.Length; i++) + { constraintTypes[i] = parameter.ConstraintTypes[i].GetTypeDefinition(); } - var defaultConstructor = parameter.HasConstructorConstraint && - primary is not ("struct" or "unmanaged"); + var defaultConstructor = + parameter.HasConstructorConstraint && primary is not ("struct" or "unmanaged"); return new TypeParameterModel(parameter.Name, primary, constraintTypes, defaultConstructor); } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/TypeSyntaxExtensions.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/TypeSyntaxExtensions.cs index 86f827d..bd214f7 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/TypeSyntaxExtensions.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/TypeSyntaxExtensions.cs @@ -4,53 +4,69 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; -public static class TypeSyntaxExtensions { - public static ITypeDefinition? GetTypeDefinition(this SyntaxNode typeSyntax, - SyntaxTransformContext generatorSyntaxContext) { +public static class TypeSyntaxExtensions +{ + public static ITypeDefinition? GetTypeDefinition( + this SyntaxNode typeSyntax, + SyntaxTransformContext generatorSyntaxContext + ) + { var symbolInfo = generatorSyntaxContext.SemanticModel.GetSymbolInfo(typeSyntax); var type = GetTypeDefinitionFromSymbolInfo(symbolInfo); - if (typeSyntax.ToString().EndsWith("?")) { + if (typeSyntax.ToString().EndsWith("?")) + { return type?.MakeNullable(); } return type; } - public static ITypeDefinition? GetTypeDefinition(this MemberAccessExpressionSyntax syntax, SyntaxTransformContext generatorSyntaxContext) { + public static ITypeDefinition? GetTypeDefinition( + this MemberAccessExpressionSyntax syntax, + SyntaxTransformContext generatorSyntaxContext + ) + { var typeInfo = generatorSyntaxContext.SemanticModel.GetSymbolInfo(syntax.Expression); - - if (typeInfo.Symbol is INamedTypeSymbol namedTypeSymbol) { + + if (typeInfo.Symbol is INamedTypeSymbol namedTypeSymbol) + { return GetTypeDefinition(namedTypeSymbol); } - + return null; } - - public static string GetFullName(this INamespaceSymbol? namespaceSymbol) { - if (namespaceSymbol == null) { + + public static string GetFullName(this INamespaceSymbol? namespaceSymbol) + { + if (namespaceSymbol == null) + { return ""; } var baseString = namespaceSymbol.ContainingNamespace?.GetFullName(); - if (string.IsNullOrEmpty(baseString)) { + if (string.IsNullOrEmpty(baseString)) + { return namespaceSymbol.Name; } return baseString + "." + namespaceSymbol.Name; } - public static ITypeDefinition GetTypeDefinition(this ITypeSymbol typeSymbol) { - if (typeSymbol is INamedTypeSymbol namedTypeSymbol) { + public static ITypeDefinition GetTypeDefinition(this ITypeSymbol typeSymbol) + { + if (typeSymbol is INamedTypeSymbol namedTypeSymbol) + { return InternalGetTypeDefinitionFromNamedSymbol(namedTypeSymbol); } // A type parameter has no namespace and no containing type to qualify it with: T is written // as T. Falling through to the qualified path below renders it as the type that declared it, // IWork.T, which names nothing. - if (typeSymbol is ITypeParameterSymbol) { + if (typeSymbol is ITypeParameterSymbol) + { var typeParameter = new TypeParameterDefinition(typeSymbol.Name); return typeSymbol.NullableAnnotation == NullableAnnotation.Annotated @@ -58,60 +74,81 @@ public static ITypeDefinition GetTypeDefinition(this ITypeSymbol typeSymbol) { : typeParameter; } - if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) { + if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) + { return arrayTypeSymbol.ElementType.GetTypeDefinition().MakeArray(); } var typeEnum = GetTypeSymbolKind(typeSymbol); - return TypeDefinition.Get(typeEnum, typeSymbol.ContainingNamespace.GetFullName(), GetTypeName(typeSymbol)); + return TypeDefinition.Get( + typeEnum, + typeSymbol.ContainingNamespace.GetFullName(), + GetTypeName(typeSymbol) + ); } - private static TypeDefinitionEnum GetTypeSymbolKind(ITypeSymbol typeSymbol) { + private static TypeDefinitionEnum GetTypeSymbolKind(ITypeSymbol typeSymbol) + { var typeEnum = TypeDefinitionEnum.ClassDefinition; - if (typeSymbol.TypeKind == TypeKind.Enum) { + if (typeSymbol.TypeKind == TypeKind.Enum) + { typeEnum = TypeDefinitionEnum.EnumDefinition; } - else if (typeSymbol.TypeKind == TypeKind.Interface) { + else if (typeSymbol.TypeKind == TypeKind.Interface) + { typeEnum = TypeDefinitionEnum.InterfaceDefinition; } return typeEnum; } - private static string GetTypeName(ITypeSymbol typeSymbol) { - if (typeSymbol.ContainingType != null) { + private static string GetTypeName(ITypeSymbol typeSymbol) + { + if (typeSymbol.ContainingType != null) + { return GetTypeName(typeSymbol.ContainingType) + "." + typeSymbol.Name; } return typeSymbol.Name; } - public static ITypeDefinition? GetTypeDefinitionFromSymbolInfo(SymbolInfo symbolInfo) { - if (symbolInfo.Symbol is INamedTypeSymbol namedTypeSymbol) { + public static ITypeDefinition? GetTypeDefinitionFromSymbolInfo(SymbolInfo symbolInfo) + { + if (symbolInfo.Symbol is INamedTypeSymbol namedTypeSymbol) + { return GetTypeDefinitionFromNamedSymbol(namedTypeSymbol); } - if (symbolInfo.Symbol is IArrayTypeSymbol arrayTypeSymbol) { + if (symbolInfo.Symbol is IArrayTypeSymbol arrayTypeSymbol) + { return GetTypeDefinitionFromType(arrayTypeSymbol.ElementType).MakeArray(); } - + return null; } - - public static ITypeDefinition? GetTypeDefinitionFromNamedSymbol(this INamedTypeSymbol? namedTypeSymbol) { - if (namedTypeSymbol == null) { + + public static ITypeDefinition? GetTypeDefinitionFromNamedSymbol( + this INamedTypeSymbol? namedTypeSymbol + ) + { + if (namedTypeSymbol == null) + { return null; } - + return InternalGetTypeDefinitionFromNamedSymbol(namedTypeSymbol); } - - private static ITypeDefinition InternalGetTypeDefinitionFromNamedSymbol(INamedTypeSymbol namedTypeSymbol) { - if (namedTypeSymbol.IsGenericType) { - if (namedTypeSymbol.Name == "Nullable") { + private static ITypeDefinition InternalGetTypeDefinitionFromNamedSymbol( + INamedTypeSymbol namedTypeSymbol + ) + { + if (namedTypeSymbol.IsGenericType) + { + if (namedTypeSymbol.Name == "Nullable") + { var baseType = namedTypeSymbol.TypeArguments.First(); return GetTypeDefinitionFromType(baseType).MakeNullable(); } @@ -120,7 +157,8 @@ private static ITypeDefinition InternalGetTypeDefinitionFromNamedSymbol(INamedTy var closingTypes = new List(); - foreach (var typeSymbol in closingTypeSymbols) { + foreach (var typeSymbol in closingTypeSymbols) + { var finalType = GetTypeDefinitionFromType(typeSymbol); closingTypes.Add(finalType); @@ -133,7 +171,8 @@ private static ITypeDefinition InternalGetTypeDefinitionFromNamedSymbol(INamedTy closingTypes ); - if (namedTypeSymbol.NullableAnnotation == NullableAnnotation.Annotated) { + if (namedTypeSymbol.NullableAnnotation == NullableAnnotation.Annotated) + { return genericType.MakeNullable(); } @@ -146,17 +185,22 @@ private static ITypeDefinition InternalGetTypeDefinitionFromNamedSymbol(INamedTy var typeDef = TypeDefinition.Get( GetTypeSymbolKind(namedTypeSymbol), - namedTypeSymbol.ContainingNamespace.GetFullName(), GetTypeName(namedTypeSymbol)); + namedTypeSymbol.ContainingNamespace.GetFullName(), + GetTypeName(namedTypeSymbol) + ); - if (namedTypeSymbol.NullableAnnotation == NullableAnnotation.Annotated) { + if (namedTypeSymbol.NullableAnnotation == NullableAnnotation.Annotated) + { return typeDef.MakeNullable(); } return typeDef; } - private static ITypeDefinition GetTypeDefinitionFromType(ITypeSymbol typeSymbol) { - switch (typeSymbol.SpecialType) { + private static ITypeDefinition GetTypeDefinitionFromType(ITypeSymbol typeSymbol) + { + switch (typeSymbol.SpecialType) + { case SpecialType.System_Int16: return TypeDefinition.Get(typeof(short)); @@ -178,23 +222,27 @@ private static ITypeDefinition GetTypeDefinitionFromType(ITypeSymbol typeSymbol) case SpecialType.System_String: return TypeDefinition.Get(typeof(string)); } - - if (typeSymbol is ITypeParameterSymbol || typeSymbol is IErrorTypeSymbol) { + + if (typeSymbol is ITypeParameterSymbol || typeSymbol is IErrorTypeSymbol) + { return new TypeParameterDefinition(typeSymbol.Name); } - if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) { + if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) + { return GetTypeDefinitionFromType(arrayTypeSymbol.ElementType).MakeArray(); } - if (typeSymbol is INamedTypeSymbol namedTypeSymbol) { + if (typeSymbol is INamedTypeSymbol namedTypeSymbol) + { return GetTypeDefinitionFromNamedSymbol(namedTypeSymbol)!; } return TypeDefinition.Get(typeSymbol.ContainingNamespace.GetFullName(), typeSymbol.Name); } - private static bool IsKnownType(string name) { + private static bool IsKnownType(string name) + { return false; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/UsageAttributeComponent.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/UsageAttributeComponent.cs index 355aa7f..0bb0e42 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/UsageAttributeComponent.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/UsageAttributeComponent.cs @@ -2,12 +2,12 @@ namespace DependencyModules.SourceGenerator.Impl.Utilities; - public class UsageAttributeComponent : BaseOutputComponent { private readonly string _usage; - public UsageAttributeComponent(string usage) { + public UsageAttributeComponent(string usage) + { _usage = usage; } diff --git a/src/DependencyModules.SourceGenerator/AssemblyModuleAttributeDiagnostics.cs b/src/DependencyModules.SourceGenerator/AssemblyModuleAttributeDiagnostics.cs index 5908459..b908830 100644 --- a/src/DependencyModules.SourceGenerator/AssemblyModuleAttributeDiagnostics.cs +++ b/src/DependencyModules.SourceGenerator/AssemblyModuleAttributeDiagnostics.cs @@ -24,18 +24,20 @@ namespace DependencyModules.SourceGenerator; /// usage already says where to look. /// /// -internal static class AssemblyModuleAttributeDiagnostics { - +internal static class AssemblyModuleAttributeDiagnostics +{ /// /// One compilation unit's assembly attributes and the namespaces in scope for them. /// - internal sealed class UnitModel : IEquatable { - + internal sealed class UnitModel : IEquatable + { public UnitModel( string filePath, ImmutableArray usages, ImmutableArray fileUsings, - ImmutableArray globalUsings) { + ImmutableArray globalUsings + ) + { FilePath = filePath; Usages = usages; FileUsings = fileUsings; @@ -54,20 +56,21 @@ public UnitModel( public ImmutableArray GlobalUsings { get; } public bool Equals(UnitModel? other) => - other != null && - FilePath == other.FilePath && - Usages.SequenceEqual(other.Usages) && - FileUsings.SequenceEqual(other.FileUsings) && - GlobalUsings.SequenceEqual(other.GlobalUsings); + other != null + && FilePath == other.FilePath + && Usages.SequenceEqual(other.Usages) + && FileUsings.SequenceEqual(other.FileUsings) + && GlobalUsings.SequenceEqual(other.GlobalUsings); public override bool Equals(object? obj) => Equals(obj as UnitModel); public override int GetHashCode() => Usages.Length * 397 ^ FileUsings.Length; } - internal sealed class Usage : IEquatable { - - public Usage(string name, Location location) { + internal sealed class Usage : IEquatable + { + public Usage(string name, Location location) + { Name = name; Location = location; } @@ -86,14 +89,18 @@ public bool Equals(Usage? other) => } internal static IncrementalValueProvider> Collect( - IncrementalGeneratorInitializationContext context) => - context.SyntaxProvider.CreateSyntaxProvider( + IncrementalGeneratorInitializationContext context + ) => + context + .SyntaxProvider.CreateSyntaxProvider( static (node, _) => node is CompilationUnitSyntax, - static (syntaxContext, cancellation) => Read(syntaxContext, cancellation)) + static (syntaxContext, cancellation) => Read(syntaxContext, cancellation) + ) .Where(static model => !model.Usages.IsEmpty || !model.GlobalUsings.IsEmpty) .Collect(); - private static UnitModel Read(GeneratorSyntaxContext context, CancellationToken cancellation) { + private static UnitModel Read(GeneratorSyntaxContext context, CancellationToken cancellation) + { cancellation.ThrowIfCancellationRequested(); var unit = (CompilationUnitSyntax)context.Node; @@ -102,10 +109,12 @@ private static UnitModel Read(GeneratorSyntaxContext context, CancellationToken var fileUsings = ImmutableArray.CreateBuilder(); var globalUsings = ImmutableArray.CreateBuilder(); - foreach (var usingDirective in unit.Usings) { + foreach (var usingDirective in unit.Usings) + { // An alias imports one name rather than a namespace, so it cannot bring a module // attribute into scope under the name this checks for. - if (usingDirective.Alias != null || usingDirective.Name == null) { + if (usingDirective.Alias != null || usingDirective.Name == null) + { continue; } @@ -116,14 +125,18 @@ private static UnitModel Read(GeneratorSyntaxContext context, CancellationToken target.Add(usingDirective.Name.ToString()); } - foreach (var attributeList in unit.AttributeLists) { - if (!attributeList.Target?.Identifier.IsKind(SyntaxKind.AssemblyKeyword) ?? true) { + foreach (var attributeList in unit.AttributeLists) + { + if (!attributeList.Target?.Identifier.IsKind(SyntaxKind.AssemblyKeyword) ?? true) + { continue; } - foreach (var attribute in attributeList.Attributes) { + foreach (var attribute in attributeList.Attributes) + { // A qualified usage already names where the attribute lives. - if (attribute.Name is not SimpleNameSyntax simpleName) { + if (attribute.Name is not SimpleNameSyntax simpleName) + { continue; } @@ -135,17 +148,28 @@ private static UnitModel Read(GeneratorSyntaxContext context, CancellationToken unit.SyntaxTree.FilePath, usages.ToImmutable(), fileUsings.ToImmutable(), - globalUsings.ToImmutable()); + globalUsings.ToImmutable() + ); } internal static void Report( SourceProductionContext context, - ((ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Modules, - ImmutableArray Units) Left, Compilation Right) data) { - + ( + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Modules, + ImmutableArray Units + ) Left, + Compilation Right + ) data + ) + { var input = data.Left; - if (input.Units.IsDefaultOrEmpty || input.Modules.IsDefaultOrEmpty) { + if (input.Units.IsDefaultOrEmpty || input.Modules.IsDefaultOrEmpty) + { return; } @@ -157,11 +181,15 @@ internal static void Report( // is never written by hand at the assembly level, so neither can produce this mistake. var modulesByName = new Dictionary(StringComparer.Ordinal); - foreach (var (module, _) in input.Modules) { + foreach (var (module, _) in input.Modules) + { var moduleNamespace = module.EntryPointType.Namespace; - if (string.IsNullOrEmpty(moduleNamespace) || - module.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule)) { + if ( + string.IsNullOrEmpty(moduleNamespace) + || module.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule) + ) + { continue; } @@ -174,8 +202,10 @@ internal static void Report( // by the test integration instead and are perfectly at home in any file. string? entryPointFile = null; - foreach (var (module, _) in input.Modules) { - if (module.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule)) { + foreach (var (module, _) in input.Modules) + { + if (module.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.AutoGenerateModule)) + { entryPointFile = module.FileLocation; break; } @@ -183,14 +213,18 @@ internal static void Report( var globalUsings = new HashSet(StringComparer.Ordinal); - foreach (var unit in input.Units) { - foreach (var globalUsing in unit.GlobalUsings) { + foreach (var unit in input.Units) + { + foreach (var globalUsing in unit.GlobalUsings) + { globalUsings.Add(globalUsing); } } - foreach (var unit in input.Units) { - foreach (var usage in unit.Usages) { + foreach (var unit in input.Units) + { + foreach (var usage in unit.Usages) + { context.CancellationToken.ThrowIfCancellationRequested(); // Written as [assembly: Foo] or [assembly: FooAttribute]; both name module Foo. @@ -201,49 +235,71 @@ internal static void Report( // takes; the exhaustive walk is only worth doing for a name that resolved to // nothing, which is a build that is already red. moduleNamespace ??= referenced.FindImported( - usage.Name, unit.FileUsings.Concat(globalUsings)); + usage.Name, + unit.FileUsings.Concat(globalUsings) + ); moduleNamespace ??= referenced.FindAnywhere(usage.Name); - if (moduleNamespace == null) { + if (moduleNamespace == null) + { continue; } - if (!unit.FileUsings.Contains(moduleNamespace) && !globalUsings.Contains(moduleNamespace)) { + if ( + !unit.FileUsings.Contains(moduleNamespace) + && !globalUsings.Contains(moduleNamespace) + ) + { context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.ModuleAttributeNamespaceNotImported, usage.Location, usage.Name, - moduleNamespace)); + moduleNamespace + ) + ); // It does not compile yet, so which file it belongs in is a later question. continue; } - if (entryPointFile != null && - !string.IsNullOrEmpty(unit.FilePath) && - !string.Equals(unit.FilePath, entryPointFile, StringComparison.Ordinal)) { + if ( + entryPointFile != null + && !string.IsNullOrEmpty(unit.FilePath) + && !string.Equals(unit.FilePath, entryPointFile, StringComparison.Ordinal) + ) + { context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.AssemblyModuleAttributeNotComposed, usage.Location, usage.Name, - System.IO.Path.GetFileName(entryPointFile))); + System.IO.Path.GetFileName(entryPointFile) + ) + ); } } } } /// The namespace of a module declared in this compilation, or null. - private static string? LocalModuleNamespace(Dictionary modulesByName, string name) { - if (modulesByName.TryGetValue(name, out var found)) { + private static string? LocalModuleNamespace( + Dictionary modulesByName, + string name + ) + { + if (modulesByName.TryGetValue(name, out var found)) + { return found; } - return name.EndsWith("Attribute", StringComparison.Ordinal) && - modulesByName.TryGetValue( - name.Substring(0, name.Length - "Attribute".Length), out found) + return + name.EndsWith("Attribute", StringComparison.Ordinal) + && modulesByName.TryGetValue( + name.Substring(0, name.Length - "Attribute".Length), + out found + ) ? found : null; } diff --git a/src/DependencyModules.SourceGenerator/Conventions/ConventionContractSource.cs b/src/DependencyModules.SourceGenerator/Conventions/ConventionContractSource.cs index ca901d9..1e27897 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/ConventionContractSource.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/ConventionContractSource.cs @@ -16,8 +16,8 @@ namespace DependencyModules.Conventions; /// a rename on either side would stop every convention matching, silently. /// /// -public static class ConventionContractSource { - +public static class ConventionContractSource +{ /// /// The namespace the contracts are declared in, and the metadata prefix the generator matches /// declarations against. Deliberately not this assembly's own namespace: the contracts ship in @@ -35,5 +35,4 @@ public static class ConventionContractSource { /// The method the generator reads. Implemented explicitly, so the name is fixed. /// public const string ConventionMethod = "Conventions"; - } diff --git a/src/DependencyModules.SourceGenerator/Conventions/ConventionGenerator.cs b/src/DependencyModules.SourceGenerator/Conventions/ConventionGenerator.cs index f963a02..0fa1603 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/ConventionGenerator.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/ConventionGenerator.cs @@ -1,6 +1,6 @@ -using CSharpAuthor; using System.Collections.Immutable; using System.Text; +using CSharpAuthor; using DependencyModules.Conventions.Models; using DependencyModules.Conventions.Utilities; using DependencyModules.SourceGenerator.Impl; @@ -15,7 +15,10 @@ namespace DependencyModules.Conventions; /// A module-level [Decorate] after its decorator's constructor has been looked up. /// public record ResolvedModuleDecorator( - ITypeDefinition ModuleType, DecoratorModel Model, string? Reason); + ITypeDefinition ModuleType, + DecoratorModel Model, + string? Reason +); /// /// The module-level decorations, and the compilation they were resolved from. @@ -31,8 +34,10 @@ public record ResolvedModuleDecorator( /// only symbols can answer. /// public record ModuleDecorators( - EquatableList Resolved, Compilation Compilation) { - + EquatableList Resolved, + Compilation Compilation +) +{ public virtual bool Equals(ModuleDecorators? other) => other is not null && Resolved.Equals(other.Resolved); @@ -55,14 +60,18 @@ public virtual bool Equals(ModuleDecorators? other) => /// stamp of everything that can change what a name binds to, which took the per-keystroke cost from /// 11–39 ms at 2,000 classes to a flat ~11 ms, and the convention half of that to ~2.4 ms. /// -public class ConventionGenerator : IDependencyModuleSourceGenerator { - +public class ConventionGenerator : IDependencyModuleSourceGenerator +{ private const string LoggerName = "ConventionSourceGenerator"; public void SetupGenerator( IncrementalGeneratorInitializationContext context, - IncrementalValuesProvider<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> incrementalValueProvider) { - + IncrementalValuesProvider<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> incrementalValueProvider + ) + { // The contracts used to be emitted here through RegisterPostInitializationOutput. They now // live in DependencyModules.Runtime, which is what lets them be public — and public is what // retires the explicit implementation requirement and the CS0436 between two assemblies that @@ -75,11 +84,12 @@ public void SetupGenerator( // Lambdas rather than method groups: SyntaxTransformContext converts implicitly from // GeneratorSyntaxContext, but a method group conversion will not apply a user-defined // conversion to a parameter. - var conventionModules = context.SyntaxProvider - .CreateSyntaxProvider( + var conventionModules = context + .SyntaxProvider.CreateSyntaxProvider( ConventionModelUtility.IsConventionModuleCandidate, (syntaxContext, cancellation) => - ConventionModelUtility.GetConventionModuleModel(syntaxContext, cancellation)) + ConventionModelUtility.GetConventionModuleModel(syntaxContext, cancellation) + ) .Where(model => !model.IsIgnored) .Collect(); @@ -93,9 +103,11 @@ public void SetupGenerator( context, new[] { KnownTypes.DependencyModules.Attributes.DecoratorAttribute }, static (syntaxContext, cancellation) => - DecoratorModelUtility.GetDecoratorModel(syntaxContext, cancellation) ?? DecoratorModel.Ignore, + DecoratorModelUtility.GetDecoratorModel(syntaxContext, cancellation) + ?? DecoratorModel.Ignore, new DecoratorModelComparer(), - DecoratorModel.Ignore); + DecoratorModel.Ignore + ); // The registrations the *attributes* made. Decoration needs them and the convention ones in // the same place: a generic decorator is closed over the type arguments a registration used, @@ -103,44 +115,66 @@ public void SetupGenerator( // up emitted from two stages that could not see each other. var attributeServices = AttributeModelCollector.Collect( context, - new[] { + new[] + { KnownTypes.DependencyModules.Attributes.TransientServiceAttribute, KnownTypes.DependencyModules.Attributes.ScopedServiceAttribute, KnownTypes.DependencyModules.Attributes.SingletonServiceAttribute, - KnownTypes.DependencyModules.Attributes.CrossWireServiceAttribute + KnownTypes.DependencyModules.Attributes.CrossWireServiceAttribute, }, static (syntaxContext, cancellation) => - ServiceModelUtility.GetServiceModel(syntaxContext, cancellation) ?? ServiceModel.Ignore, + ServiceModelUtility.GetServiceModel(syntaxContext, cancellation) + ?? ServiceModel.Ignore, new ServiceModelComparer(), - ServiceModel.Ignore); + ServiceModel.Ignore + ); // [Decorate] on a module names its decorator by typeof(), so the constructor has to be // looked up from the compilation. Resolved here rather than in the output stage: the // compilation changes on every keystroke, and combining it into the output would re-emit // everything every time. The result is compared by value, so an unchanged lookup propagates // nothing — the same shape the metadata scan below uses, and for the same reason. - var moduleDecorators = incrementalValueProvider.Collect() + var moduleDecorators = incrementalValueProvider + .Collect() .Combine(context.CompilationProvider) - .Select((pair, cancellation) => { - var resolved = new List(); - - foreach (var (entryPoint, _) in pair.Left) { - foreach (var resolution in - ModuleDecoratorResolver.Resolve(entryPoint, pair.Right, cancellation)) { - resolved.Add(new ResolvedModuleDecorator( - entryPoint.EntryPointType, resolution.Model, resolution.Reason)); + .Select( + (pair, cancellation) => + { + var resolved = new List(); + + foreach (var (entryPoint, _) in pair.Left) + { + foreach ( + var resolution in ModuleDecoratorResolver.Resolve( + entryPoint, + pair.Right, + cancellation + ) + ) + { + resolved.Add( + new ResolvedModuleDecorator( + entryPoint.EntryPointType, + resolution.Model, + resolution.Reason + ) + ); + } } - } - return new ModuleDecorators( - new EquatableList(resolved), pair.Right); - }); + return new ModuleDecorators( + new EquatableList(resolved), + pair.Right + ); + } + ); - var candidates = context.SyntaxProvider - .CreateSyntaxProvider( + var candidates = context + .SyntaxProvider.CreateSyntaxProvider( ConventionCandidateUtility.IsCandidate, (syntaxContext, cancellation) => - ConventionCandidateCache.GetOrAdd(syntaxContext, cancellation)) + ConventionCandidateCache.GetOrAdd(syntaxContext, cancellation) + ) .Where(model => !model.IsIgnored) .Collect(); @@ -152,11 +186,15 @@ public void SetupGenerator( // case and costs nothing. var metadataCandidates = conventionModules .Combine(context.CompilationProvider) - .Select((pair, cancellation) => - new EquatableList( - MetadataCandidateUtility.Collect(pair.Left, pair.Right, cancellation))); - - var everything = incrementalValueProvider.Collect() + .Select( + (pair, cancellation) => + new EquatableList( + MetadataCandidateUtility.Collect(pair.Left, pair.Right, cancellation) + ) + ); + + var everything = incrementalValueProvider + .Collect() .Combine(conventionModules) .Combine(candidates) .Combine(metadataCandidates) @@ -180,19 +218,38 @@ public void SetupGenerator( // The cost is that matching runs twice, once per output. The emitting pass is silent, so // nothing composes a diagnostic message it is about to discard, and this pass writes no // source. - context.RegisterSourceOutput(everything.Combine(context.CompilationProvider), ReportDiagnostics); + context.RegisterSourceOutput( + everything.Combine(context.CompilationProvider), + ReportDiagnostics + ); } private void GenerateSourceOutput( SourceProductionContext context, - ((((((ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, - ImmutableArray Right) Left, - ImmutableArray Right) Left, - EquatableList Right) Left, - ImmutableArray Right) Left, - ImmutableArray Right) Left, - ModuleDecorators Right) data) { - + ( + ( + ( + ( + ( + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) Left, + ImmutableArray Right + ) Left, + EquatableList Right + ) Left, + ImmutableArray Right + ) Left, + ImmutableArray Right + ) Left, + ModuleDecorators Right + ) data + ) + { var entryPoints = data.Left.Left.Left.Left.Left.Left; var conventionModules = data.Left.Left.Left.Left.Left.Right; var decorators = data.Left.Left.Right; @@ -201,13 +258,15 @@ private void GenerateSourceOutput( // In-compilation candidates and metadata candidates travel together; a convention sees one // source or the other, decided by whether it named an assembly. - var candidates = data.Left.Left.Left.Left.Right.Length == 0 - ? (IReadOnlyList)data.Left.Left.Left.Right - : data.Left.Left.Left.Left.Right.Concat(data.Left.Left.Left.Right).ToArray(); + var candidates = + data.Left.Left.Left.Left.Right.Length == 0 + ? (IReadOnlyList)data.Left.Left.Left.Right + : data.Left.Left.Left.Left.Right.Concat(data.Left.Left.Left.Right).ToArray(); // Decoration runs whether or not anything declares a convention, so the early-out is on // entry points alone. - if (entryPoints.Length == 0) { + if (entryPoints.Length == 0) + { return; } @@ -216,16 +275,30 @@ private void GenerateSourceOutput( FileLogger.Wrap( LoggerName, configuration, - logger => Generate( - context, entryPoints, conventionModules, candidates, decorators, attributeServices, - moduleDecorators, DiagnosticReporter.Silent, emit: true, logger), + logger => + Generate( + context, + entryPoints, + conventionModules, + candidates, + decorators, + attributeServices, + moduleDecorators, + DiagnosticReporter.Silent, + emit: true, + logger + ), // Surfaced as a build error rather than discarded, matching the attribute generators. A // generator that fails quietly produces a green build with no registrations. - exception => context.ReportDiagnostic( - Diagnostic.Create( - DependencyModuleDiagnostics.GeneratorFailure, - Location.None, - $"{exception.GetType().Name}: {exception.Message}"))); + exception => + context.ReportDiagnostic( + Diagnostic.Create( + DependencyModuleDiagnostics.GeneratorFailure, + Location.None, + $"{exception.GetType().Name}: {exception.Message}" + ) + ) + ); } /// @@ -233,46 +306,85 @@ private void GenerateSourceOutput( /// private void ReportDiagnostics( SourceProductionContext context, - (((((((ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, - ImmutableArray Right) Left, - ImmutableArray Right) Left, - EquatableList Right) Left, - ImmutableArray Right) Left, - ImmutableArray Right) Left, - ModuleDecorators Right) Left, - Compilation Right) data) { - + ( + ( + ( + ( + ( + ( + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) Left, + ImmutableArray Right + ) Left, + EquatableList Right + ) Left, + ImmutableArray Right + ) Left, + ImmutableArray Right + ) Left, + ModuleDecorators Right + ) Left, + Compilation Right + ) data + ) + { var models = data.Left; var entryPoints = models.Left.Left.Left.Left.Left.Left; - if (entryPoints.Length == 0) { + if (entryPoints.Length == 0) + { return; } - var candidates = models.Left.Left.Left.Left.Right.Length == 0 - ? (IReadOnlyList)models.Left.Left.Left.Right - : models.Left.Left.Left.Left.Right.Concat(models.Left.Left.Left.Right).ToArray(); + var candidates = + models.Left.Left.Left.Left.Right.Length == 0 + ? (IReadOnlyList)models.Left.Left.Left.Right + : models.Left.Left.Left.Left.Right.Concat(models.Left.Left.Left.Right).ToArray(); var configuration = entryPoints.First().Right; - var report = new DiagnosticReporter(context.ReportDiagnostic, new SyntaxTreeLookup(data.Right)); + var report = new DiagnosticReporter( + context.ReportDiagnostic, + new SyntaxTreeLookup(data.Right) + ); FileLogger.Wrap( LoggerName, configuration, - logger => Generate( - context, entryPoints, models.Left.Left.Left.Left.Left.Right, candidates, - models.Left.Left.Right, models.Left.Right, models.Right, - report, emit: false, logger), - exception => context.ReportDiagnostic( - Diagnostic.Create( - DependencyModuleDiagnostics.GeneratorFailure, - Location.None, - $"{exception.GetType().Name}: {exception.Message}"))); + logger => + Generate( + context, + entryPoints, + models.Left.Left.Left.Left.Left.Right, + candidates, + models.Left.Left.Right, + models.Left.Right, + models.Right, + report, + emit: false, + logger + ), + exception => + context.ReportDiagnostic( + Diagnostic.Create( + DependencyModuleDiagnostics.GeneratorFailure, + Location.None, + $"{exception.GetType().Name}: {exception.Message}" + ) + ) + ); } private void Generate( SourceProductionContext context, - ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> entryPoints, + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> entryPoints, ImmutableArray conventionModules, IReadOnlyList candidates, ImmutableArray decorators, @@ -280,13 +392,17 @@ private void Generate( ModuleDecorators moduleDecorators, DiagnosticReporter report, bool emit, - FileLogger logger) { - - var (entryPointList, configurationModel) = EntryModelUtil.ConsolidateEntryPointModels(entryPoints); + FileLogger logger + ) + { + var (entryPointList, configurationModel) = EntryModelUtil.ConsolidateEntryPointModels( + entryPoints + ); logger.Info( - $"Discovered {conventionModules.Length} convention module(s) and " + - $"{candidates.Count} candidate type(s)."); + $"Discovered {conventionModules.Length} convention module(s) and " + + $"{candidates.Count} candidate type(s)." + ); var claimed = new HashSet(); @@ -294,19 +410,32 @@ private void Generate( // and convention registrations are emitted once rather than alongside an identical copy on // the module it defers to. It can never be a convention module itself - IConventionModule is // implemented by hand - so nothing here goes unclaimed as a result. - foreach (var entryPointModel in EntryModelUtil.RegistrationTargets(entryPointList)) { + foreach (var entryPointModel in EntryModelUtil.RegistrationTargets(entryPointList)) + { context.CancellationToken.ThrowIfCancellationRequested(); - var conventionModule = conventionModules.FirstOrDefault( - module => module.ModuleType.Equals(entryPointModel.EntryPointType)); + var conventionModule = conventionModules.FirstOrDefault(module => + module.ModuleType.Equals(entryPointModel.EntryPointType) + ); - if (conventionModule != null) { + if (conventionModule != null) + { claimed.Add(conventionModule); } GenerateForModule( - context, entryPointModel, configurationModel, conventionModule, candidates, decorators, - attributeServices, moduleDecorators, report, emit, logger); + context, + entryPointModel, + configurationModel, + conventionModule, + candidates, + decorators, + attributeServices, + moduleDecorators, + report, + emit, + logger + ); } ReportUnclaimedModules(report, conventionModules, claimed, logger); @@ -323,23 +452,38 @@ private void GenerateForModule( ModuleDecorators moduleDecorators, DiagnosticReporter report, bool emit, - FileLogger logger) { - + FileLogger logger + ) + { var withNamespace = EntryModelUtil.EnsureNamespace(entryPointModel, configurationModel); - var serviceModels = conventionModule == null - ? Array.Empty() - : ConventionMatcher.Match( - withNamespace, conventionModule, candidates, report, logger); + var serviceModels = + conventionModule == null + ? Array.Empty() + : ConventionMatcher.Match( + withNamespace, + conventionModule, + candidates, + report, + logger + ); // Every registration this compilation makes, however it was declared. This is the whole // point of the single stage: a generic decorator is expanded once, against all of them. WriteDecorators( - context, withNamespace, configurationModel, - ServiceTypes(attributeServices, serviceModels), decorators, moduleDecorators, - report, emit, logger); - - if (serviceModels.Count == 0 || !emit) { + context, + withNamespace, + configurationModel, + ServiceTypes(attributeServices, serviceModels), + decorators, + moduleDecorators, + report, + emit, + logger + ); + + if (serviceModels.Count == 0 || !emit) + { return; } @@ -352,27 +496,37 @@ private void GenerateForModule( context.AddSource( withNamespace.EntryPointType.GetFileNameHint( - configurationModel.RootNamespace, "ConventionDependencies"), - output); + configurationModel.RootNamespace, + "ConventionDependencies" + ), + output + ); } /// /// Every service type the compilation registers, in the closed form it registers it as. /// private static IReadOnlyList ServiceTypes( - ImmutableArray attributeServices, IReadOnlyList conventionServices) { - + ImmutableArray attributeServices, + IReadOnlyList conventionServices + ) + { var seen = new HashSet(); var ordered = new List(); - void Add(IEnumerable models) { - foreach (var model in models) { - if (model.Equals(ServiceModel.Ignore)) { + void Add(IEnumerable models) + { + foreach (var model in models) + { + if (model.Equals(ServiceModel.Ignore)) + { continue; } - foreach (var registration in model.Registrations) { - if (seen.Add(registration.ServiceType)) { + foreach (var registration in model.Registrations) + { + if (seen.Add(registration.ServiceType)) + { ordered.Add(registration.ServiceType); } } @@ -409,11 +563,19 @@ private static void WriteDecorators( ModuleDecorators moduleDecorators, DiagnosticReporter report, bool emit, - FileLogger logger) { - - var decorators = CollectDecorators(report, entryPointModel, declared, moduleDecorators, logger); - - if (decorators.Count == 0) { + FileLogger logger + ) + { + var decorators = CollectDecorators( + report, + entryPointModel, + declared, + moduleDecorators, + logger + ); + + if (decorators.Count == 0) + { return; } @@ -423,14 +585,24 @@ private static void WriteDecorators( out var refusedForOpenGenericRegistration, canClose: (decoratorType, closedService) => DecoratorConstraintChecker.CanClose( - moduleDecorators.Compilation, decoratorType, closedService)); + moduleDecorators.Compilation, + decoratorType, + closedService + ) + ); ReportOpenGenericDecoration(report, refusedForOpenGenericRegistration, logger); ReportImplementationUnderFactories( - report, decorators, entryPointModel, configurationModel, logger); + report, + decorators, + entryPointModel, + configurationModel, + logger + ); - if (expanded.Count == 0 || !emit) { + if (expanded.Count == 0 || !emit) + { return; } @@ -440,8 +612,11 @@ private static void WriteDecorators( context.AddSource( entryPointModel.EntryPointType.GetFileNameHint( - configurationModel.RootNamespace, "Decorators"), - output); + configurationModel.RootNamespace, + "Decorators" + ), + output + ); } /// @@ -453,26 +628,32 @@ private static IReadOnlyList CollectDecorators( ModuleEntryPointModel entryPointModel, ImmutableArray declared, ModuleDecorators moduleDecorators, - FileLogger logger) { - + FileLogger logger + ) + { var decorators = new List(); - foreach (var decorator in declared) { - if (decorator.IsIgnored) { + foreach (var decorator in declared) + { + if (decorator.IsIgnored) + { continue; } // A realm-scoped decorator belongs only to its realm. An unscoped one belongs to every // module that is not realm-only, matching how service registrations behave. - if (decorator.Realm != null) { - if (decorator.Realm.Equals(entryPointModel.EntryPointType)) { + if (decorator.Realm != null) + { + if (decorator.Realm.Equals(entryPointModel.EntryPointType)) + { decorators.Add(decorator); } continue; } - if (!entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.OnlyRealm)) { + if (!entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.OnlyRealm)) + { decorators.Add(decorator); } } @@ -480,15 +661,19 @@ private static IReadOnlyList CollectDecorators( // [Decorate] carries two type names and nothing else, so its decorator's constructor is // looked up rather than read from a declaration — the only route for one declared in a // referenced assembly, which is the case the module-level form exists for. - foreach (var resolution in moduleDecorators.Resolved) { - if (!resolution.ModuleType.Equals(entryPointModel.EntryPointType)) { + foreach (var resolution in moduleDecorators.Resolved) + { + if (!resolution.ModuleType.Equals(entryPointModel.EntryPointType)) + { continue; } - if (resolution.Reason != null) { + if (resolution.Reason != null) + { logger.Error( - $"'{resolution.Model.DecoratorType.Name}' cannot be constructed by generated " + - $"code: {resolution.Reason}."); + $"'{resolution.Model.DecoratorType.Name}' cannot be constructed by generated " + + $"code: {resolution.Reason}." + ); } decorators.Add(resolution.Model); @@ -514,27 +699,37 @@ private static void ReportImplementationUnderFactories( IReadOnlyList decorators, ModuleEntryPointModel entryPointModel, DependencyModuleConfigurationModel configurationModel, - FileLogger logger) { - - if (!entryPointModel.GenerateFactories.GetValueOrDefault(configurationModel.GenerateFactories)) { + FileLogger logger + ) + { + if ( + !entryPointModel.GenerateFactories.GetValueOrDefault( + configurationModel.GenerateFactories + ) + ) + { return; } - foreach (var decorator in decorators) { - if (decorator.Implementation == null) { + foreach (var decorator in decorators) + { + if (decorator.Implementation == null) + { continue; } logger.Error( - $"'{decorator.DecoratorType.Name}' names an implementation, which generated " + - "factories cannot be told apart by."); + $"'{decorator.DecoratorType.Name}' names an implementation, which generated " + + "factories cannot be told apart by." + ); report.Report( DependencyModuleDiagnostics.DecoratorImplementationNeedsTypeRegistration, decorator.Location, decorator.DecoratorType.Name, decorator.Implementation.Name, - decorator.ServiceType.Name); + decorator.ServiceType.Name + ); } } @@ -550,21 +745,25 @@ private static void ReportImplementationUnderFactories( private static void ReportOpenGenericDecoration( DiagnosticReporter report, IReadOnlyList refused, - FileLogger logger) { - - foreach (var decorator in refused) { + FileLogger logger + ) + { + foreach (var decorator in refused) + { var serviceName = decorator.ServiceType.Name; var decoratorName = decorator.DecoratorType.Name; logger.Error( - $"'{decoratorName}' cannot decorate '{serviceName}' because it is registered as an " + - "open generic."); + $"'{decoratorName}' cannot decorate '{serviceName}' because it is registered as an " + + "open generic." + ); report.Report( DependencyModuleDiagnostics.OpenGenericCannotBeDecorated, decorator.Location, serviceName, - decoratorName); + decoratorName + ); } } @@ -573,18 +772,27 @@ private static void ReportOpenGenericDecoration( /// reported rather than resolved arbitrarily. /// private static void ReportAmbiguousOrdering( - DiagnosticReporter report, IReadOnlyList decorators, FileLogger logger) { - - for (var i = 0; i < decorators.Count; i++) { - for (var j = i + 1; j < decorators.Count; j++) { - if (decorators[i].Order != decorators[j].Order || - !decorators[i].ServiceType.Equals(decorators[j].ServiceType)) { + DiagnosticReporter report, + IReadOnlyList decorators, + FileLogger logger + ) + { + for (var i = 0; i < decorators.Count; i++) + { + for (var j = i + 1; j < decorators.Count; j++) + { + if ( + decorators[i].Order != decorators[j].Order + || !decorators[i].ServiceType.Equals(decorators[j].ServiceType) + ) + { continue; } logger.Error( - $"'{decorators[i].DecoratorType.Name}' and '{decorators[j].DecoratorType.Name}' both " + - $"decorate '{decorators[i].ServiceType.Name}' with order {decorators[i].Order}."); + $"'{decorators[i].DecoratorType.Name}' and '{decorators[j].DecoratorType.Name}' both " + + $"decorate '{decorators[i].ServiceType.Name}' with order {decorators[i].Order}." + ); report.Report( DependencyModuleDiagnostics.AmbiguousDecoratorOrder, @@ -592,7 +800,8 @@ private static void ReportAmbiguousOrdering( decorators[i].DecoratorType.Name, decorators[j].DecoratorType.Name, decorators[i].ServiceType.Name, - decorators[i].Order); + decorators[i].Order + ); } } } @@ -608,10 +817,13 @@ private static void ReportUnclaimedModules( DiagnosticReporter report, ImmutableArray conventionModules, HashSet claimed, - FileLogger logger) { - - foreach (var conventionModule in conventionModules) { - if (claimed.Contains(conventionModule)) { + FileLogger logger + ) + { + foreach (var conventionModule in conventionModules) + { + if (claimed.Contains(conventionModule)) + { continue; } @@ -623,7 +835,8 @@ private static void ReportUnclaimedModules( DependencyModuleDiagnostics.ConventionCannotBeRead, conventionModule.Location, "the declaring type is not marked with [DependencyModule], so it registers nothing", - name); + name + ); } } } diff --git a/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionCandidateModel.cs b/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionCandidateModel.cs index 865b07f..6a93e5d 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionCandidateModel.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionCandidateModel.cs @@ -22,7 +22,8 @@ namespace DependencyModules.Conventions.Models; public record ImplementedInterfaceModel( ITypeDefinition InterfaceType, string DefinitionKey, - string? ViaTypeName); + string? ViaTypeName +); /// /// A class or record a convention could match. @@ -71,17 +72,20 @@ public record ConventionCandidateModel( /// should not silently pick up a type from a package, and a scan of a package should not pick up /// a local one. /// - string? AssemblyName = null) { - + string? AssemblyName = null +) +{ public static readonly ConventionCandidateModel Ignore = new( TypeDefinition.Get("", "Ignore"), Array.Empty(), Array.Empty(), null, true, - LocationModel.None); + LocationModel.None + ); - public bool IsIgnored => ReferenceEquals(this, Ignore) || ImplementationType.Equals(Ignore.ImplementationType); + public bool IsIgnored => + ReferenceEquals(this, Ignore) || ImplementationType.Equals(Ignore.ImplementationType); /// /// The interfaces visible to a convention with the given reach. @@ -90,30 +94,40 @@ public IEnumerable InterfacesInReach(bool includeBase includeBaseClasses ? DeclaredInterfaces.Concat(BaseClassInterfaces) : DeclaredInterfaces; public virtual bool Equals(ConventionCandidateModel? other) => - other is not null && - ImplementationType.Equals(other.ImplementationType) && - HasAccessibleConstructor == other.HasAccessibleConstructor && - Location == other.Location && - ModelEquality.ListEquals(DeclaredInterfaces, other.DeclaredInterfaces) && - ModelEquality.ListEquals(BaseClassInterfaces, other.BaseClassInterfaces) && - CompareConstructor(Constructor, other.Constructor) && + other is not null + && ImplementationType.Equals(other.ImplementationType) + && HasAccessibleConstructor == other.HasAccessibleConstructor + && Location == other.Location + && ModelEquality.ListEquals(DeclaredInterfaces, other.DeclaredInterfaces) + && ModelEquality.ListEquals(BaseClassInterfaces, other.BaseClassInterfaces) + && CompareConstructor(Constructor, other.Constructor) + && // Null and empty both mean unconditional and have to compare equal, or an edit elsewhere // in the file would miss the incremental cache. - ((Conditions?.Count ?? 0) == 0 && (other.Conditions?.Count ?? 0) == 0 || - ModelEquality.ListEquals(Conditions, other.Conditions)) && - ((AttributeTypeKeys?.Count ?? 0) == 0 && (other.AttributeTypeKeys?.Count ?? 0) == 0 || - ModelEquality.ListEquals(AttributeTypeKeys, other.AttributeTypeKeys)) && - AssemblyName == other.AssemblyName; + ( + (Conditions?.Count ?? 0) == 0 && (other.Conditions?.Count ?? 0) == 0 + || ModelEquality.ListEquals(Conditions, other.Conditions) + ) + && ( + (AttributeTypeKeys?.Count ?? 0) == 0 && (other.AttributeTypeKeys?.Count ?? 0) == 0 + || ModelEquality.ListEquals(AttributeTypeKeys, other.AttributeTypeKeys) + ) + && AssemblyName == other.AssemblyName; - private static bool CompareConstructor(ConstructorInfoModel? x, ConstructorInfoModel? y) { - if (x is null && y is null) return true; - if (x is null || y is null) return false; + private static bool CompareConstructor(ConstructorInfoModel? x, ConstructorInfoModel? y) + { + if (x is null && y is null) + return true; + if (x is null || y is null) + return false; return ModelEquality.ListEquals(x.Parameters, y.Parameters); } - public override int GetHashCode() { - unchecked { + public override int GetHashCode() + { + unchecked + { var hash = ImplementationType.GetHashCode(); hash = hash * 31 + HasAccessibleConstructor.GetHashCode(); hash = hash * 31 + ModelEquality.ListHashCode(DeclaredInterfaces); diff --git a/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionModel.cs b/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionModel.cs index df23c2c..31af506 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionModel.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionModel.cs @@ -13,11 +13,14 @@ namespace DependencyModules.Conventions.Models; /// — structurally different, same service. A key built from namespace, name and arity is equal for /// both, and is a string, so it costs nothing to keep in an incremental model. /// -public static class ConventionTypeKey { - - public static string For(ITypeDefinition type) { +public static class ConventionTypeKey +{ + public static string For(ITypeDefinition type) + { var arity = type.TypeArguments?.Count ?? 0; - var name = string.IsNullOrEmpty(type.Namespace) ? type.Name : type.Namespace + "." + type.Name; + var name = string.IsNullOrEmpty(type.Namespace) + ? type.Name + : type.Namespace + "." + type.Name; return arity == 0 ? name : name + "`" + arity; } @@ -26,7 +29,8 @@ public static string For(ITypeDefinition type) { /// /// What a convention registers each match as. /// -public enum ConventionRegisterAs { +public enum ConventionRegisterAs +{ /// /// As the service type the convention matched through. The default. /// @@ -70,8 +74,8 @@ public enum ConventionRegisterAs { /// once per pattern rather than once per candidate. /// /// True for the Without form. -public record NameFilterModel(string Pattern, bool Exclude) { - +public record NameFilterModel(string Pattern, bool Exclude) +{ /// /// True when the pattern is matched against the full name rather than the bare type name. /// @@ -87,25 +91,29 @@ public record NameFilterModel(string Pattern, bool Exclude) { /// InNamespaceOf means and what people expect of a namespace filter. /// /// True for the NotIn forms. -public record NamespaceFilterModel(string Namespace, bool Exact, bool Exclude) { - +public record NamespaceFilterModel(string Namespace, bool Exact, bool Exclude) +{ /// /// Whether a type's namespace falls inside this filter, ignoring whether it includes or /// excludes. /// - public bool Covers(string? candidateNamespace) { + public bool Covers(string? candidateNamespace) + { var value = candidateNamespace ?? ""; - if (Exact) { + if (Exact) + { return string.Equals(value, Namespace, StringComparison.Ordinal); } // A prefix match has to stop at a namespace separator, or "MyApp.Order" would swallow // "MyApp.Ordering". - return value.Equals(Namespace, StringComparison.Ordinal) || - (value.Length > Namespace.Length && - value[Namespace.Length] == '.' && - value.StartsWith(Namespace, StringComparison.Ordinal)); + return value.Equals(Namespace, StringComparison.Ordinal) + || ( + value.Length > Namespace.Length + && value[Namespace.Length] == '.' + && value.StartsWith(Namespace, StringComparison.Ordinal) + ); } } @@ -175,8 +183,9 @@ public record ConventionModel( IReadOnlyList? NameFilters = null, ITypeDefinition? ExplicitServiceType = null, string? AssemblyName = null, - IReadOnlyList? Conditions = null) { - + IReadOnlyList? Conditions = null +) +{ /// /// Whether the attributes a candidate carries pass the filters. /// @@ -184,15 +193,20 @@ public record ConventionModel( /// Requirements combine with and: a type has to carry every attribute asked for and none /// of the excluded ones. Alternatives would need an or, which no Scrutor overload offers either. /// - public bool AttributesMatch(IReadOnlyList? candidateAttributes) { - if (AttributeFilters == null) { + public bool AttributesMatch(IReadOnlyList? candidateAttributes) + { + if (AttributeFilters == null) + { return true; } - foreach (var filter in AttributeFilters) { - var carried = candidateAttributes != null && candidateAttributes.Contains(filter.TypeKey); + foreach (var filter in AttributeFilters) + { + var carried = + candidateAttributes != null && candidateAttributes.Contains(filter.TypeKey); - if (carried == filter.Exclude) { + if (carried == filter.Exclude) + { return false; } } @@ -208,17 +222,22 @@ public bool AttributesMatch(IReadOnlyList? candidateAttributes) { /// /// Whether a candidate's namespace passes the filters. /// - public bool NamespaceMatches(string? candidateNamespace) { - if (NamespaceFilters == null) { + public bool NamespaceMatches(string? candidateNamespace) + { + if (NamespaceFilters == null) + { return true; } var included = false; var anyInclusion = false; - foreach (var filter in NamespaceFilters) { - if (filter.Exclude) { - if (filter.Covers(candidateNamespace)) { + foreach (var filter in NamespaceFilters) + { + if (filter.Exclude) + { + if (filter.Covers(candidateNamespace)) + { return false; } @@ -235,26 +254,28 @@ public bool NamespaceMatches(string? candidateNamespace) { // Structural equality over the filter list; a positional record would compare it by reference // and never hit the incremental cache. See ModelEquality. public virtual bool Equals(ConventionModel? other) => - other is not null && - Equals(ServiceType, other.ServiceType) && - DefinitionKey == other.DefinitionKey && - IsOpenGeneric == other.IsOpenGeneric && - Lifestyle == other.Lifestyle && - IncludeBaseClasses == other.IncludeBaseClasses && - Location == other.Location && - RegisterAs == other.RegisterAs && - RegistrationType == other.RegistrationType && - Equals(Key, other.Key) && - ModelEquality.ListEquals(NamespaceFilters, other.NamespaceFilters) && - ModelEquality.ListEquals(KeyNamespaces, other.KeyNamespaces) && - ModelEquality.ListEquals(AttributeFilters, other.AttributeFilters) && - ModelEquality.ListEquals(NameFilters, other.NameFilters) && - Equals(ExplicitServiceType, other.ExplicitServiceType) && - AssemblyName == other.AssemblyName && - ModelEquality.ListEquals(Conditions, other.Conditions); - - public override int GetHashCode() { - unchecked { + other is not null + && Equals(ServiceType, other.ServiceType) + && DefinitionKey == other.DefinitionKey + && IsOpenGeneric == other.IsOpenGeneric + && Lifestyle == other.Lifestyle + && IncludeBaseClasses == other.IncludeBaseClasses + && Location == other.Location + && RegisterAs == other.RegisterAs + && RegistrationType == other.RegistrationType + && Equals(Key, other.Key) + && ModelEquality.ListEquals(NamespaceFilters, other.NamespaceFilters) + && ModelEquality.ListEquals(KeyNamespaces, other.KeyNamespaces) + && ModelEquality.ListEquals(AttributeFilters, other.AttributeFilters) + && ModelEquality.ListEquals(NameFilters, other.NameFilters) + && Equals(ExplicitServiceType, other.ExplicitServiceType) + && AssemblyName == other.AssemblyName + && ModelEquality.ListEquals(Conditions, other.Conditions); + + public override int GetHashCode() + { + unchecked + { var hash = ServiceType?.GetHashCode() ?? 0; hash = hash * 31 + (DefinitionKey?.GetHashCode() ?? 0); hash = hash * 31 + IsOpenGeneric.GetHashCode(); @@ -292,12 +313,12 @@ public record ConventionModuleModel( ITypeDefinition ModuleType, IReadOnlyList Conventions, IReadOnlyList Unreadable, - /// /// Where the declaring type sits, so a diagnostic about the module itself can point at it. /// - LocationModel? Location = null) { - + LocationModel? Location = null +) +{ /// /// The sentinel for a declaration this generator does not own, matching how every other model /// in this codebase signals "nothing to do". @@ -305,20 +326,23 @@ public record ConventionModuleModel( public static readonly ConventionModuleModel Ignore = new( TypeDefinition.Get("", "Ignore"), Array.Empty(), - Array.Empty()); + Array.Empty() + ); public bool IsIgnored => ReferenceEquals(this, Ignore) || ModuleType.Equals(Ignore.ModuleType); // Structural equality over the lists; a positional record would compare them by reference and // never hit the incremental cache. See ModelEquality. public virtual bool Equals(ConventionModuleModel? other) => - other is not null && - ModuleType.Equals(other.ModuleType) && - ModelEquality.ListEquals(Conventions, other.Conventions) && - ModelEquality.ListEquals(Unreadable, other.Unreadable); - - public override int GetHashCode() { - unchecked { + other is not null + && ModuleType.Equals(other.ModuleType) + && ModelEquality.ListEquals(Conventions, other.Conventions) + && ModelEquality.ListEquals(Unreadable, other.Unreadable); + + public override int GetHashCode() + { + unchecked + { var hash = ModuleType.GetHashCode(); hash = hash * 31 + ModelEquality.ListHashCode(Conventions); hash = hash * 31 + ModelEquality.ListHashCode(Unreadable); diff --git a/src/DependencyModules.SourceGenerator/Conventions/Models/EquatableList.cs b/src/DependencyModules.SourceGenerator/Conventions/Models/EquatableList.cs index 7d1ebac..fd77b4f 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Models/EquatableList.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Models/EquatableList.cs @@ -11,10 +11,12 @@ namespace DependencyModules.Conventions.Models; /// is measurable rather than theoretical: the metadata scan re-runs on every keystroke by /// construction, so without this the emission would too. /// -public sealed class EquatableList : IReadOnlyList { +public sealed class EquatableList : IReadOnlyList +{ private readonly IReadOnlyList _items; - public EquatableList(IReadOnlyList items) { + public EquatableList(IReadOnlyList items) + { _items = items; } diff --git a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateCache.cs b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateCache.cs index 55b243a..9864b59 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateCache.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateCache.cs @@ -1,8 +1,8 @@ using System.Runtime.CompilerServices; using DependencyModules.Conventions.Models; +using DependencyModules.SourceGenerator.Impl.Models; using DependencyModules.SourceGenerator.Impl.Utilities; using Microsoft.CodeAnalysis; -using DependencyModules.SourceGenerator.Impl.Models; namespace DependencyModules.Conventions.Utilities; @@ -24,12 +24,14 @@ namespace DependencyModules.Conventions.Utilities; /// entirely on being complete. /// /// -public static class ConventionCandidateCache { - +public static class ConventionCandidateCache +{ private static readonly ConditionalWeakTable Entries = new(); - private sealed class Entry { - public Entry(long stamp, ConventionCandidateModel model) { + private sealed class Entry + { + public Entry(long stamp, ConventionCandidateModel model) + { Stamp = stamp; Model = model; } @@ -40,11 +42,14 @@ public Entry(long stamp, ConventionCandidateModel model) { } public static ConventionCandidateModel GetOrAdd( - SyntaxTransformContext context, CancellationToken cancellationToken) { - + SyntaxTransformContext context, + CancellationToken cancellationToken + ) + { var stamp = DeclarationStamp.Of(context.SemanticModel.Compilation); - if (Entries.TryGetValue(context.Node, out var entry) && entry.Stamp == stamp) { + if (Entries.TryGetValue(context.Node, out var entry) && entry.Stamp == stamp) + { return entry.Model; } diff --git a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateUtility.cs b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateUtility.cs index 2130f15..61a97c5 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateUtility.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateUtility.cs @@ -27,8 +27,8 @@ namespace DependencyModules.Conventions.Utilities; /// convention decides whether they count rather than the type graph deciding for it. /// /// -public static class ConventionCandidateUtility { - +public static class ConventionCandidateUtility +{ /// /// Attributes that take a type out of convention matching. /// @@ -42,12 +42,18 @@ public static class ConventionCandidateUtility { /// decorated. One open generic decorator over convention-registered handlers — the ordinary /// MediatR and FluentValidation shape — failed at the composition root because of it. /// - private static readonly string[] ServiceAttributeNames = { - "SingletonService", "SingletonServiceAttribute", - "ScopedService", "ScopedServiceAttribute", - "TransientService", "TransientServiceAttribute", - "CrossWireService", "CrossWireServiceAttribute", - "Decorator", "DecoratorAttribute", + private static readonly string[] ServiceAttributeNames = + { + "SingletonService", + "SingletonServiceAttribute", + "ScopedService", + "ScopedServiceAttribute", + "TransientService", + "TransientServiceAttribute", + "CrossWireService", + "CrossWireServiceAttribute", + "Decorator", + "DecoratorAttribute", }; /// @@ -70,20 +76,26 @@ public static class ConventionCandidateUtility { /// base list there is no interface walk. /// /// - public static bool IsCandidate(SyntaxNode node, CancellationToken cancellationToken) { + public static bool IsCandidate(SyntaxNode node, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); - if (node is not ClassDeclarationSyntax and not RecordDeclarationSyntax) { + if (node is not ClassDeclarationSyntax and not RecordDeclarationSyntax) + { return false; } var typeDeclaration = (TypeDeclarationSyntax)node; - foreach (var modifier in typeDeclaration.Modifiers) { - if (modifier.IsKind(SyntaxKind.StaticKeyword) || - modifier.IsKind(SyntaxKind.AbstractKeyword) || - modifier.IsKind(SyntaxKind.PrivateKeyword) || - modifier.IsKind(SyntaxKind.ProtectedKeyword)) { + foreach (var modifier in typeDeclaration.Modifiers) + { + if ( + modifier.IsKind(SyntaxKind.StaticKeyword) + || modifier.IsKind(SyntaxKind.AbstractKeyword) + || modifier.IsKind(SyntaxKind.PrivateKeyword) + || modifier.IsKind(SyntaxKind.ProtectedKeyword) + ) + { return false; } } @@ -99,13 +111,17 @@ public static bool IsCandidate(SyntaxNode node, CancellationToken cancellationTo /// static factory method marked [SingletonService] would otherwise disqualify itself as a /// candidate for reasons that have nothing to do with the class. /// - private static bool HasServiceAttribute(TypeDeclarationSyntax typeDeclaration) { - foreach (var attributeList in typeDeclaration.AttributeLists) { - foreach (var attribute in attributeList.Attributes) { + private static bool HasServiceAttribute(TypeDeclarationSyntax typeDeclaration) + { + foreach (var attributeList in typeDeclaration.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { var name = attribute.Name.ToString(); var simpleName = name.Substring(name.LastIndexOf('.') + 1); - if (Array.IndexOf(ServiceAttributeNames, simpleName) >= 0) { + if (Array.IndexOf(ServiceAttributeNames, simpleName) >= 0) + { return true; } } @@ -115,11 +131,14 @@ private static bool HasServiceAttribute(TypeDeclarationSyntax typeDeclaration) { } public static ConventionCandidateModel GetCandidateModel( - SyntaxTransformContext context, CancellationToken cancellationToken) { - + SyntaxTransformContext context, + CancellationToken cancellationToken + ) + { cancellationToken.ThrowIfCancellationRequested(); - if (context.Node is not TypeDeclarationSyntax typeDeclaration) { + if (context.Node is not TypeDeclarationSyntax typeDeclaration) + { return ConventionCandidateModel.Ignore; } @@ -127,7 +146,8 @@ public static ConventionCandidateModel GetCandidateModel( // semantic question. Binding a symbol for every such type — which is most types in most // projects — was the largest remaining cost of admitting them: measured on 2,000 classes, // the second run after an edit took 40 ms with the symbol and 12 ms without. - if (typeDeclaration.BaseList is not { Types.Count: > 0 }) { + if (typeDeclaration.BaseList is not { Types.Count: > 0 }) + { return new ConventionCandidateModel( typeDeclaration.GetTypeDefinition(), Array.Empty(), @@ -135,18 +155,31 @@ public static ConventionCandidateModel GetCandidateModel( ServiceModelUtility.GetConstructorInfo(context, typeDeclaration, cancellationToken), DeclaresAccessibleConstructor(typeDeclaration), LocationModel.From(typeDeclaration), - EnvironmentConditionUtility.GetConditions(context, typeDeclaration, cancellationToken), - CollectAttributeKeys(context, typeDeclaration, cancellationToken)); + EnvironmentConditionUtility.GetConditions( + context, + typeDeclaration, + cancellationToken + ), + CollectAttributeKeys(context, typeDeclaration, cancellationToken) + ); } - if (context.SemanticModel.GetDeclaredSymbol(typeDeclaration) is not INamedTypeSymbol symbol) { + if (context.SemanticModel.GetDeclaredSymbol(typeDeclaration) is not INamedTypeSymbol symbol) + { return ConventionCandidateModel.Ignore; } var declared = new List(); var viaBaseClass = new List(); - CollectInterfaces(symbol, typeDeclaration, context, declared, viaBaseClass, cancellationToken); + CollectInterfaces( + symbol, + typeDeclaration, + context, + declared, + viaBaseClass, + cancellationToken + ); return new ConventionCandidateModel( ImplementationTypeOf(symbol), @@ -156,7 +189,8 @@ public static ConventionCandidateModel GetCandidateModel( HasAccessibleConstructor(symbol), LocationModel.From(typeDeclaration), EnvironmentConditionUtility.GetConditions(context, typeDeclaration, cancellationToken), - CollectAttributeKeys(context, typeDeclaration, cancellationToken)); + CollectAttributeKeys(context, typeDeclaration, cancellationToken) + ); } /// @@ -169,16 +203,24 @@ public static ConventionCandidateModel GetCandidateModel( /// of admitting every class as a candidate. /// private static IReadOnlyList? CollectAttributeKeys( - SyntaxTransformContext context, TypeDeclarationSyntax typeDeclaration, - CancellationToken cancellationToken) { - + SyntaxTransformContext context, + TypeDeclarationSyntax typeDeclaration, + CancellationToken cancellationToken + ) + { List? keys = null; - foreach (var attributeList in typeDeclaration.AttributeLists) { - foreach (var attribute in attributeList.Attributes) { + foreach (var attributeList in typeDeclaration.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { cancellationToken.ThrowIfCancellationRequested(); - if (ModelExtensions.GetTypeInfo(context.SemanticModel, attribute).Type is not { } type) { + if ( + ModelExtensions.GetTypeInfo(context.SemanticModel, attribute).Type + is not { } type + ) + { continue; } @@ -199,32 +241,41 @@ private static void CollectInterfaces( SyntaxTransformContext context, List declared, List viaBaseClass, - CancellationToken cancellationToken) { - + CancellationToken cancellationToken + ) + { // Deduped on the type definition rather than on the arity key, which is deliberately equal // for every closing of one generic — IHandler and IHandler are distinct services. var seen = new HashSet(); - foreach (var baseTypeSyntax in typeDeclaration.BaseList!.Types) { + foreach (var baseTypeSyntax in typeDeclaration.BaseList!.Types) + { cancellationToken.ThrowIfCancellationRequested(); - if (context.SemanticModel.GetSymbolInfo(baseTypeSyntax.Type).Symbol - is not INamedTypeSymbol baseSymbol) { + if ( + context.SemanticModel.GetSymbolInfo(baseTypeSyntax.Type).Symbol + is not INamedTypeSymbol baseSymbol + ) + { continue; } - if (baseSymbol.TypeKind == TypeKind.Interface) { + if (baseSymbol.TypeKind == TypeKind.Interface) + { // Written on the declaration, plus everything that interface extends. An interface // declaring that it extends another is a deliberate statement of substitutability, // so a convention naming the base interface matches this type by declaration. Add(declared, seen, symbol, baseSymbol, null); - foreach (var inherited in baseSymbol.AllInterfaces) { + foreach (var inherited in baseSymbol.AllInterfaces) + { Add(declared, seen, symbol, inherited, baseSymbol.Name); } } - else if (baseSymbol.TypeKind == TypeKind.Class) { - foreach (var inherited in baseSymbol.AllInterfaces) { + else if (baseSymbol.TypeKind == TypeKind.Class) + { + foreach (var inherited in baseSymbol.AllInterfaces) + { Add(viaBaseClass, seen, symbol, inherited, baseSymbol.Name); } } @@ -236,22 +287,30 @@ private static void Add( HashSet seen, INamedTypeSymbol implementation, INamedTypeSymbol interfaceSymbol, - string? viaTypeName) { - + string? viaTypeName + ) + { var interfaceType = RegistrationFormOf(interfaceSymbol, implementation); - if (interfaceType == null) { + if (interfaceType == null) + { return; } // Deduped across both lists: an interface reached by declaration is a declared match even if // a base class also brings it, and listing it twice would register the same service twice. - if (!seen.Add(interfaceType)) { + if (!seen.Add(interfaceType)) + { return; } - target.Add(new ImplementedInterfaceModel( - interfaceType, ConventionTypeKey.For(interfaceType), viaTypeName)); + target.Add( + new ImplementedInterfaceModel( + interfaceType, + ConventionTypeKey.For(interfaceType), + viaTypeName + ) + ); } /// @@ -266,27 +325,39 @@ private static void Add( /// built. /// private static ITypeDefinition? RegistrationFormOf( - INamedTypeSymbol interfaceSymbol, INamedTypeSymbol implementation) { - + INamedTypeSymbol interfaceSymbol, + INamedTypeSymbol implementation + ) + { var definition = interfaceSymbol.GetTypeDefinition(); - if (!ContainsTypeParameter(interfaceSymbol)) { + if (!ContainsTypeParameter(interfaceSymbol)) + { return definition; } - if (!implementation.IsGenericType) { + if (!implementation.IsGenericType) + { return null; } // Every argument is one of the implementation's own parameters, used once, in order. var arguments = interfaceSymbol.TypeArguments; - if (arguments.Length != implementation.TypeParameters.Length) { + if (arguments.Length != implementation.TypeParameters.Length) + { return null; } - for (var i = 0; i < arguments.Length; i++) { - if (!SymbolEqualityComparer.Default.Equals(arguments[i], implementation.TypeParameters[i])) { + for (var i = 0; i < arguments.Length; i++) + { + if ( + !SymbolEqualityComparer.Default.Equals( + arguments[i], + implementation.TypeParameters[i] + ) + ) + { return null; } } @@ -294,13 +365,17 @@ private static void Add( return OpenFormOf(definition); } - private static bool ContainsTypeParameter(INamedTypeSymbol symbol) { - foreach (var argument in symbol.TypeArguments) { - if (argument is ITypeParameterSymbol) { + private static bool ContainsTypeParameter(INamedTypeSymbol symbol) + { + foreach (var argument in symbol.TypeArguments) + { + if (argument is ITypeParameterSymbol) + { return true; } - if (argument is INamedTypeSymbol nested && ContainsTypeParameter(nested)) { + if (argument is INamedTypeSymbol nested && ContainsTypeParameter(nested)) + { return true; } } @@ -317,9 +392,11 @@ private static ITypeDefinition OpenFormOf(ITypeDefinition definition) => definition.TypeDefinitionEnum, definition.Namespace, definition.Name, - definition.TypeArguments.Select(_ => TypeDefinition.Get("", "")).ToArray()); + definition.TypeArguments.Select(_ => TypeDefinition.Get("", "")).ToArray() + ); - private static ITypeDefinition ImplementationTypeOf(INamedTypeSymbol symbol) { + private static ITypeDefinition ImplementationTypeOf(INamedTypeSymbol symbol) + { var definition = symbol.GetTypeDefinition(); return symbol.IsGenericType ? OpenFormOf(definition) : definition; @@ -341,24 +418,30 @@ private static ITypeDefinition ImplementationTypeOf(INamedTypeSymbol symbol) { /// A type declaring no constructor has the implicit public parameterless one; a primary /// constructor is public; otherwise any constructor not marked private or protected will do. /// - private static bool DeclaresAccessibleConstructor(TypeDeclarationSyntax typeDeclaration) { - if (typeDeclaration.ParameterList is { Parameters.Count: >= 0 }) { + private static bool DeclaresAccessibleConstructor(TypeDeclarationSyntax typeDeclaration) + { + if (typeDeclaration.ParameterList is { Parameters.Count: >= 0 }) + { return true; } var declaredAny = false; - foreach (var constructor in typeDeclaration.Members.OfType()) { - if (constructor.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword))) { + foreach (var constructor in typeDeclaration.Members.OfType()) + { + if (constructor.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword))) + { continue; } declaredAny = true; - var hidden = constructor.Modifiers.Any( - m => m.IsKind(SyntaxKind.PrivateKeyword) || m.IsKind(SyntaxKind.ProtectedKeyword)); + var hidden = constructor.Modifiers.Any(m => + m.IsKind(SyntaxKind.PrivateKeyword) || m.IsKind(SyntaxKind.ProtectedKeyword) + ); - if (!hidden) { + if (!hidden) + { return true; } } @@ -366,14 +449,22 @@ private static bool DeclaresAccessibleConstructor(TypeDeclarationSyntax typeDecl return !declaredAny; } - private static bool HasAccessibleConstructor(INamedTypeSymbol symbol) { - if (symbol.InstanceConstructors.Length == 0) { + private static bool HasAccessibleConstructor(INamedTypeSymbol symbol) + { + if (symbol.InstanceConstructors.Length == 0) + { return true; } - foreach (var constructor in symbol.InstanceConstructors) { - if (constructor.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal - or Accessibility.ProtectedOrInternal) { + foreach (var constructor in symbol.InstanceConstructors) + { + if ( + constructor.DeclaredAccessibility + is Accessibility.Public + or Accessibility.Internal + or Accessibility.ProtectedOrInternal + ) + { return true; } } diff --git a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionMatcher.cs b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionMatcher.cs index 4269bbd..3b83325 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionMatcher.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionMatcher.cs @@ -1,9 +1,9 @@ +using System.Text.RegularExpressions; using CSharpAuthor; using DependencyModules.Conventions.Models; using DependencyModules.SourceGenerator.Impl; using DependencyModules.SourceGenerator.Impl.Models; using DependencyModules.SourceGenerator.Impl.Utilities; -using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; namespace DependencyModules.Conventions.Utilities; @@ -18,7 +18,8 @@ namespace DependencyModules.Conventions.Utilities; public record ConventionRegistrationMatch( ConventionModel Convention, ConventionCandidateModel Candidate, - ImplementedInterfaceModel? Interface); + ImplementedInterfaceModel? Interface +); /// /// One registration a match produces, before the ambiguity check has run. @@ -31,7 +32,8 @@ public record ConventionRegistrationMatch( /// internal record PendingRegistration( ConventionRegistrationMatch Match, - ServiceRegistrationModel Registration); + ServiceRegistrationModel Registration +); /// /// Matches a module's conventions against the candidates in the compilation. @@ -41,32 +43,36 @@ internal record PendingRegistration( /// have to be cacheable. Everything it works from is already rendered to strings and /// s, so no symbol is touched here. /// -public static class ConventionMatcher { - +public static class ConventionMatcher +{ public static IReadOnlyList Match( ModuleEntryPointModel entryPointModel, ConventionModuleModel conventionModule, IReadOnlyList candidates, DiagnosticReporter report, - FileLogger logger) { - + FileLogger logger + ) + { var moduleName = entryPointModel.EntryPointType.Name; - foreach (var unreadable in conventionModule.Unreadable) { + foreach (var unreadable in conventionModule.Unreadable) + { logger.Error($"{moduleName}: refused '{unreadable.Text}' — {unreadable.Reason}."); report.Report( DependencyModuleDiagnostics.ConventionCannotBeRead, unreadable.Location, unreadable.Reason, - unreadable.Text); + unreadable.Text + ); } var merged = MergePartialDeclarations(candidates); var matches = new List(); - foreach (var convention in conventionModule.Conventions) { + foreach (var convention in conventionModule.Conventions) + { CollectMatches(convention, merged, moduleName, matches, report, logger); } @@ -76,8 +82,10 @@ public static IReadOnlyList Match( // drag a perfectly good interface registration down with a duplicated self one. var pending = new List(); - foreach (var match in matches) { - foreach (var registration in BuildRegistrations(match, entryPointModel)) { + foreach (var match in matches) + { + foreach (var registration in BuildRegistrations(match, entryPointModel)) + { pending.Add(new PendingRegistration(match, registration)); } } @@ -95,18 +103,23 @@ private static void CollectMatches( string moduleName, List matches, DiagnosticReporter report, - FileLogger logger) { - + FileLogger logger + ) + { var serviceName = convention.DisplayName; - if (convention.Lifestyle == null) { - logger.Error($"{moduleName}: the convention registering '{serviceName}' declared no lifetime."); + if (convention.Lifestyle == null) + { + logger.Error( + $"{moduleName}: the convention registering '{serviceName}' declared no lifetime." + ); report.Report( DependencyModuleDiagnostics.ConventionCannotBeRead, convention.Location, "no lifetime was declared; call AsSingleton(), AsScoped() or AsTransient()", - $"RegisterAll({serviceName})"); + $"RegisterAll({serviceName})" + ); return; } @@ -117,20 +130,26 @@ private static void CollectMatches( // the model keeps patterns as strings. var nameFilters = CompileNameFilters(convention); - foreach (var candidate in candidates) { - if (candidate.IsIgnored) { + foreach (var candidate in candidates) + { + if (candidate.IsIgnored) + { continue; } // One source or the other. A scan of the project being built must not pick up a type // from a package, and a scan of a package must not pick up a local one. - if (candidate.AssemblyName != convention.AssemblyName) { + if (candidate.AssemblyName != convention.AssemblyName) + { continue; } - if (!convention.NamespaceMatches(candidate.ImplementationType.Namespace) || - !convention.AttributesMatch(candidate.AttributeTypeKeys) || - !NameMatches(nameFilters, candidate.ImplementationType)) { + if ( + !convention.NamespaceMatches(candidate.ImplementationType.Namespace) + || !convention.AttributesMatch(candidate.AttributeTypeKeys) + || !NameMatches(nameFilters, candidate.ImplementationType) + ) + { continue; } @@ -138,21 +157,25 @@ private static void CollectMatches( // as itself — one match, no interface. List matched; - if (convention.ServiceType == null) { + if (convention.ServiceType == null) + { matched = new List { null }; } - else { + else + { var reachable = AllMatchingInterfaces(convention, candidate); - if (reachable.Count == 0) { + if (reachable.Count == 0) + { continue; } // AsSelf and AsSelfWithInterfaces name the implementation, so several matching // closings still produce one registration rather than one per closing. The default // and AlsoAsSelf register each matched closing. - matched = convention.RegisterAs is ConventionRegisterAs.Interfaces - or ConventionRegisterAs.AlsoSelf + matched = convention.RegisterAs + is ConventionRegisterAs.Interfaces + or ConventionRegisterAs.AlsoSelf ? reachable.Cast().ToList() : new List { reachable[0] }; } @@ -161,35 +184,47 @@ or ConventionRegisterAs.AlsoSelf // Reported rather than registered: a registration the container cannot construct throws // when the provider is built, a long way from the convention responsible. - if (!candidate.HasAccessibleConstructor) { + if (!candidate.HasAccessibleConstructor) + { logger.Error( - $"{moduleName}: '{candidate.ImplementationType.Name}' matched '{serviceName}' " + - "but has no accessible constructor."); + $"{moduleName}: '{candidate.ImplementationType.Name}' matched '{serviceName}' " + + "but has no accessible constructor." + ); report.Report( DependencyModuleDiagnostics.ConventionMatchNotConstructable, - candidate.Location == LocationModel.None ? convention.Location : candidate.Location, + candidate.Location == LocationModel.None + ? convention.Location + : candidate.Location, candidate.ImplementationType.Name, serviceName, - moduleName); + moduleName + ); continue; } - foreach (var candidateInterface in matched) { - matches.Add(new ConventionRegistrationMatch(convention, candidate, candidateInterface)); + foreach (var candidateInterface in matched) + { + matches.Add( + new ConventionRegistrationMatch(convention, candidate, candidateInterface) + ); } } - if (found == 0) { - logger.Info($"{moduleName}: the convention registering '{serviceName}' matched nothing."); + if (found == 0) + { + logger.Info( + $"{moduleName}: the convention registering '{serviceName}' matched nothing." + ); report.Report( DependencyModuleDiagnostics.ConventionMatchedNothing, convention.Location, serviceName, moduleName, - AdviceForEmptyMatch(convention, serviceName)); + AdviceForEmptyMatch(convention, serviceName) + ); } } @@ -202,24 +237,24 @@ or ConventionRegisterAs.AlsoSelf /// the shape every MVVM project starts from — class FooViewModel : ViewModelBase — so /// saying "call IncludeBaseClasses()" there sent readers looking for a mistake they had not made. /// - private static string AdviceForEmptyMatch(ConventionModel convention, string serviceName) { - if (convention.ServiceType is { TypeDefinitionEnum: TypeDefinitionEnum.ClassDefinition }) { - return - $"Conventions match the interfaces a type declares, and '{serviceName}' is a class, " + - "so no type can match it. Register an interface the types declare — a marker " + - "interface on the base class is enough — or register them by attribute instead"; + private static string AdviceForEmptyMatch(ConventionModel convention, string serviceName) + { + if (convention.ServiceType is { TypeDefinitionEnum: TypeDefinitionEnum.ClassDefinition }) + { + return $"Conventions match the interfaces a type declares, and '{serviceName}' is a class, " + + "so no type can match it. Register an interface the types declare — a marker " + + "interface on the base class is enough — or register them by attribute instead"; } - if (convention.IncludeBaseClasses) { - return - "Conventions match a type that declares the service type, or declares an interface " + - "extending it. IncludeBaseClasses() is already applied, so check the name, namespace " + - "and assembly filters on this convention"; + if (convention.IncludeBaseClasses) + { + return "Conventions match a type that declares the service type, or declares an interface " + + "extending it. IncludeBaseClasses() is already applied, so check the name, namespace " + + "and assembly filters on this convention"; } - return - "Conventions match a type that declares the service type, or declares an interface " + - "extending it; call IncludeBaseClasses() to also match types that reach it through a base class"; + return "Conventions match a type that declares the service type, or declares an interface " + + "extending it; call IncludeBaseClasses() to also match types that reach it through a base class"; } /// @@ -230,31 +265,41 @@ private static string AdviceForEmptyMatch(ConventionModel convention, string ser /// one. Everything else is escaped, so a pattern cannot smuggle in a regex. /// private static List<(Regex Pattern, bool Qualified, bool Exclude)>? CompileNameFilters( - ConventionModel convention) { - - if (convention.NameFilters == null) { + ConventionModel convention + ) + { + if (convention.NameFilters == null) + { return null; } var compiled = new List<(Regex, bool, bool)>(convention.NameFilters.Count); - foreach (var filter in convention.NameFilters) { - var expression = "^" + - Regex.Escape(filter.Pattern).Replace("\\*", ".*").Replace("\\?", ".") + - "$"; + foreach (var filter in convention.NameFilters) + { + var expression = + "^" + Regex.Escape(filter.Pattern).Replace("\\*", ".*").Replace("\\?", ".") + "$"; // Ordinal and case-sensitive, consistent with C# identifiers. - compiled.Add((new Regex(expression, RegexOptions.CultureInvariant), filter.IsQualified, - filter.Exclude)); + compiled.Add( + ( + new Regex(expression, RegexOptions.CultureInvariant), + filter.IsQualified, + filter.Exclude + ) + ); } return compiled; } private static bool NameMatches( - List<(Regex Pattern, bool Qualified, bool Exclude)>? filters, ITypeDefinition implementation) { - - if (filters == null) { + List<(Regex Pattern, bool Qualified, bool Exclude)>? filters, + ITypeDefinition implementation + ) + { + if (filters == null) + { return true; } @@ -267,11 +312,14 @@ private static bool NameMatches( var included = false; var anyInclusion = false; - foreach (var (pattern, qualified, exclude) in filters) { + foreach (var (pattern, qualified, exclude) in filters) + { var subject = qualified ? qualifiedName : bareName; - if (exclude) { - if (pattern.IsMatch(subject)) { + if (exclude) + { + if (pattern.IsMatch(subject)) + { return false; } @@ -296,11 +344,16 @@ private static bool NameMatches( /// implementation appearing twice. /// private static List AllMatchingInterfaces( - ConventionModel convention, ConventionCandidateModel candidate) { - + ConventionModel convention, + ConventionCandidateModel candidate + ) + { var matched = new List(); - foreach (var candidateInterface in candidate.InterfacesInReach(convention.IncludeBaseClasses)) { + foreach ( + var candidateInterface in candidate.InterfacesInReach(convention.IncludeBaseClasses) + ) + { // An open convention matches any closing, and the candidate is registered against the // construction it actually implements. A closed one has to match exactly, or // RegisterAll>() would pick up every other closing as well. @@ -308,7 +361,8 @@ private static List AllMatchingInterfaces( ? convention.DefinitionKey == candidateInterface.DefinitionKey : convention.ServiceType!.Equals(candidateInterface.InterfaceType); - if (isMatch) { + if (isMatch) + { matched.Add(candidateInterface); } } @@ -335,32 +389,38 @@ private static List AllMatchingInterfaces( /// /// private static IReadOnlyList MergePartialDeclarations( - IReadOnlyList candidates) { - + IReadOnlyList candidates + ) + { var byType = new Dictionary>(); var order = new List(); var anyPartial = false; - foreach (var candidate in candidates) { - if (!byType.TryGetValue(candidate.ImplementationType, out var parts)) { + foreach (var candidate in candidates) + { + if (!byType.TryGetValue(candidate.ImplementationType, out var parts)) + { parts = new List(); byType[candidate.ImplementationType] = parts; order.Add(candidate.ImplementationType); } - else { + else + { anyPartial = true; } parts.Add(candidate); } - if (!anyPartial) { + if (!anyPartial) + { return candidates; } var merged = new List(order.Count); - foreach (var implementationType in order) { + foreach (var implementationType in order) + { var parts = byType[implementationType]; merged.Add(parts.Count == 1 ? parts[0] : MergeParts(parts)); @@ -369,7 +429,8 @@ private static IReadOnlyList MergePartialDeclarations( return merged; } - private static ConventionCandidateModel MergeParts(List parts) { + private static ConventionCandidateModel MergeParts(List parts) + { var declared = new List(); var viaBaseClass = new List(); var seen = new HashSet(); @@ -377,17 +438,23 @@ private static ConventionCandidateModel MergeParts(List(); // Attributes on partial parts combine, so the conditions do too. - foreach (var part in parts) { - if (part.Conditions != null) { + foreach (var part in parts) + { + if (part.Conditions != null) + { conditions.AddRange(part.Conditions); } } @@ -405,17 +474,22 @@ private static ConventionCandidateModel MergeParts(List(); // Attributes on partial parts combine, so the keys do too. - foreach (var part in parts) { - if (part.AttributeTypeKeys != null) { - foreach (var key in part.AttributeTypeKeys) { - if (!attributeKeys.Contains(key)) { + foreach (var part in parts) + { + if (part.AttributeTypeKeys != null) + { + foreach (var key in part.AttributeTypeKeys) + { + if (!attributeKeys.Contains(key)) + { attributeKeys.Add(key); } } } } - return parts[0] with { + return parts[0] with + { AttributeTypeKeys = attributeKeys.Count > 0 ? attributeKeys : null, DeclaredInterfaces = declared, BaseClassInterfaces = viaBaseClass, @@ -427,15 +501,19 @@ private static ConventionCandidateModel MergeParts(List parts) { + private static ConstructorInfoModel? GreediestConstructor(List parts) + { ConstructorInfoModel? greediest = null; - foreach (var part in parts) { - if (part.Constructor == null) { + foreach (var part in parts) + { + if (part.Constructor == null) + { continue; } - if (greediest == null || part.Constructor.Parameters.Count > greediest.Parameters.Count) { + if (greediest == null || part.Constructor.Parameters.Count > greediest.Parameters.Count) + { greediest = part.Constructor; } } @@ -454,21 +532,26 @@ private static List RemoveAmbiguous( List pending, string moduleName, DiagnosticReporter report, - FileLogger logger) { - + FileLogger logger + ) + { // Keyed on what actually reaches the container: this implementation, under this service // type. A type filling two roles registers twice; one service type claimed twice is the // ambiguity. var byRegistration = - new Dictionary<(ITypeDefinition Implementation, ITypeDefinition Service), - List>(); + new Dictionary< + (ITypeDefinition Implementation, ITypeDefinition Service), + List + >(); var order = new List<(ITypeDefinition Implementation, ITypeDefinition Service)>(); - foreach (var entry in pending) { + foreach (var entry in pending) + { var key = (entry.Match.Candidate.ImplementationType, entry.Registration.ServiceType); - if (!byRegistration.TryGetValue(key, out var list)) { + if (!byRegistration.TryGetValue(key, out var list)) + { list = new List(); byRegistration[key] = list; order.Add(key); @@ -481,10 +564,12 @@ private static List RemoveAmbiguous( // Insertion order rather than dictionary order: the emitted registration order feeds the // module snapshots, and a hash order would move them for unrelated reasons. - foreach (var key in order) { + foreach (var key in order) + { var group = byRegistration[key]; - if (group.Count == 1) { + if (group.Count == 1) + { usable.Add(group[0]); continue; @@ -494,14 +579,16 @@ private static List RemoveAmbiguous( var second = group[1]; var serviceName = key.Service.Name; - var difference = first.Match.Convention.Lifestyle == second.Match.Convention.Lifestyle - ? "The declaration is duplicated." - : $"They declare different lifetimes ({first.Match.Convention.Lifestyle} and " + - $"{second.Match.Convention.Lifestyle})."; + var difference = + first.Match.Convention.Lifestyle == second.Match.Convention.Lifestyle + ? "The declaration is duplicated." + : $"They declare different lifetimes ({first.Match.Convention.Lifestyle} and " + + $"{second.Match.Convention.Lifestyle})."; logger.Error( - $"{moduleName}: '{key.Implementation.Name}' is registered as " + - $"'{serviceName}' by two conventions. {difference}"); + $"{moduleName}: '{key.Implementation.Name}' is registered as " + + $"'{serviceName}' by two conventions. {difference}" + ); report.Report( DependencyModuleDiagnostics.AmbiguousConventionMatch, @@ -509,7 +596,8 @@ private static List RemoveAmbiguous( key.Implementation.Name, moduleName, serviceName, - difference); + difference + ); } return usable; @@ -533,16 +621,25 @@ private static List RemoveAmbiguous( /// /// private static (ITypeDefinition Implementation, ITypeDefinition Service) RegistrationKey( - ConventionRegistrationMatch match) { - + ConventionRegistrationMatch match + ) + { var implementation = match.Candidate.ImplementationType; - return match.Convention.RegisterAs switch { - ConventionRegisterAs.Explicit => (implementation, match.Convention.ExplicitServiceType!), - ConventionRegisterAs.MatchingInterface => - (implementation, MatchingInterfaceOf(match) ?? implementation), - ConventionRegisterAs.Interfaces when match.Interface != null => - (implementation, match.Interface.InterfaceType), + return match.Convention.RegisterAs switch + { + ConventionRegisterAs.Explicit => ( + implementation, + match.Convention.ExplicitServiceType! + ), + ConventionRegisterAs.MatchingInterface => ( + implementation, + MatchingInterfaceOf(match) ?? implementation + ), + ConventionRegisterAs.Interfaces when match.Interface != null => ( + implementation, + match.Interface.InterfaceType + ), _ => (implementation, implementation), }; } @@ -585,22 +682,38 @@ private static LocationModel LocationOf(ConventionRegistrationMatch match) => /// so that line does not hold the way this one does. /// /// - private static bool IsFrameworkInterface(ITypeDefinition interfaceType) { + private static bool IsFrameworkInterface(ITypeDefinition interfaceType) + { var namespaceName = interfaceType.Namespace; - return namespaceName == "System" || - (namespaceName != null && namespaceName.StartsWith("System.", StringComparison.Ordinal)); + return namespaceName == "System" + || ( + namespaceName != null + && namespaceName.StartsWith("System.", StringComparison.Ordinal) + ); } /// /// The interface named after the implementation — Foo as IFoo. /// - private static ITypeDefinition? MatchingInterfaceOf(ConventionRegistrationMatch match) { + private static ITypeDefinition? MatchingInterfaceOf(ConventionRegistrationMatch match) + { var wanted = "I" + match.Candidate.ImplementationType.Name; - foreach (var candidateInterface in - match.Candidate.InterfacesInReach(match.Convention.IncludeBaseClasses)) { - if (string.Equals(candidateInterface.InterfaceType.Name, wanted, StringComparison.Ordinal)) { + foreach ( + var candidateInterface in match.Candidate.InterfacesInReach( + match.Convention.IncludeBaseClasses + ) + ) + { + if ( + string.Equals( + candidateInterface.InterfaceType.Name, + wanted, + StringComparison.Ordinal + ) + ) + { return candidateInterface.InterfaceType; } } @@ -616,23 +729,29 @@ private static string ServiceTypeNameOf(ConventionRegistrationMatch match) => /// match was not direct. /// private static void ReportExposure( - IReadOnlyList usable, string moduleName, DiagnosticReporter report) { - - foreach (var entry in usable) { + IReadOnlyList usable, + string moduleName, + DiagnosticReporter report + ) + { + foreach (var entry in usable) + { var serviceType = entry.Registration.ServiceType; // The interface a convention matched through explains a match that was not direct, and // only applies when this registration is that interface. - var via = entry.Match.Interface != null && - entry.Match.Interface.InterfaceType.Equals(serviceType) && - entry.Match.Interface.ViaTypeName != null - ? $" (via {entry.Match.Interface.ViaTypeName})" - : ""; + var via = + entry.Match.Interface != null + && entry.Match.Interface.InterfaceType.Equals(serviceType) + && entry.Match.Interface.ViaTypeName != null + ? $" (via {entry.Match.Interface.ViaTypeName})" + : ""; report.Report( DependencyModuleDiagnostics.ExposedByConvention, LocationOf(entry.Match), - $"{serviceType.Name} in {moduleName}{via}"); + $"{serviceType.Name} in {moduleName}{via}" + ); } } @@ -640,8 +759,10 @@ private static void ReportExposure( /// Produces the same models the attribute path produces, so emission needs no special case. /// private static IReadOnlyList BuildServiceModels( - IReadOnlyList usable, FileLogger logger) { - + IReadOnlyList usable, + FileLogger logger + ) + { // One model per implementation, carrying every registration it produces — the shape // ServiceModelUtility builds for the attribute path. Two models with the same // ImplementationType would duplicate the per-implementation state the writer reads: @@ -653,11 +774,13 @@ private static IReadOnlyList BuildServiceModels( var byGroup = new Dictionary>(); var order = new List(); - foreach (var entry in usable) { + foreach (var entry in usable) + { var conditions = MergeConditions(entry.Match); var key = GroupKey(entry.Match.Candidate.ImplementationType, conditions); - if (!byGroup.TryGetValue(key, out var list)) { + if (!byGroup.TryGetValue(key, out var list)) + { list = new List(); byGroup[key] = list; order.Add(new ConventionModelGroup(entry.Match, conditions, key)); @@ -668,27 +791,32 @@ private static IReadOnlyList BuildServiceModels( var models = new List(order.Count); - foreach (var group in order) { + foreach (var group in order) + { var registrations = byGroup[group.Key]; logger.Info( - $" {group.Match.Candidate.ImplementationType.Name} -> " + - $"{string.Join(", ", registrations.Select(r => r.ServiceType.Name))} " + - "(by convention)"); - - models.Add(new ServiceModel( - group.Match.Candidate.ImplementationType, - group.Match.Candidate.Constructor, - null, - null, - registrations, - // Carried across for the same reason the attribute path sets it: interception picks - // its registration out by asking each descriptor what implementation it was built - // from, and a factory descriptor cannot say. A convention-registered class carrying - // [Intercept] needs the exemption as much as an attribute-registered one, and - // reaches this writer by a different route. - InterceptionFeature(group.Match.Candidate), - group.Conditions)); + $" {group.Match.Candidate.ImplementationType.Name} -> " + + $"{string.Join(", ", registrations.Select(r => r.ServiceType.Name))} " + + "(by convention)" + ); + + models.Add( + new ServiceModel( + group.Match.Candidate.ImplementationType, + group.Match.Candidate.Constructor, + null, + null, + registrations, + // Carried across for the same reason the attribute path sets it: interception picks + // its registration out by asking each descriptor what implementation it was built + // from, and a factory descriptor cannot say. A convention-registered class carrying + // [Intercept] needs the exemption as much as an attribute-registered one, and + // reaches this writer by a different route. + InterceptionFeature(group.Match.Candidate), + group.Conditions + ) + ); } return models; @@ -707,8 +835,9 @@ private static RegistrationFeature InterceptionFeature(ConventionCandidateModel ? RegistrationFeature.Intercepted : RegistrationFeature.None; - private static readonly string InterceptAttributeKey = - ConventionTypeKey.For(KnownTypes.DependencyModules.Attributes.InterceptAttribute); + private static readonly string InterceptAttributeKey = ConventionTypeKey.For( + KnownTypes.DependencyModules.Attributes.InterceptAttribute + ); /// /// One ServiceModel's worth of matches: the same implementation under the same conditions. @@ -716,7 +845,8 @@ private static RegistrationFeature InterceptionFeature(ConventionCandidateModel private record ConventionModelGroup( ConventionRegistrationMatch Match, IReadOnlyList? Conditions, - string Key); + string Key + ); /// /// The conditions in force for one match: the convention's and the class's, combined. @@ -727,20 +857,25 @@ private record ConventionModelGroup( /// written in the other, which is the kind of thing nobody finds until production. /// private static IReadOnlyList? MergeConditions( - ConventionRegistrationMatch match) { - + ConventionRegistrationMatch match + ) + { var fromConvention = match.Convention.Conditions; var fromCandidate = match.Candidate.Conditions; - if ((fromConvention?.Count ?? 0) == 0) { + if ((fromConvention?.Count ?? 0) == 0) + { return fromCandidate; } - if ((fromCandidate?.Count ?? 0) == 0) { + if ((fromCandidate?.Count ?? 0) == 0) + { return fromConvention; } - var merged = new List(fromConvention!.Count + fromCandidate!.Count); + var merged = new List( + fromConvention!.Count + fromCandidate!.Count + ); merged.AddRange(fromConvention); merged.AddRange(fromCandidate); @@ -751,14 +886,18 @@ private record ConventionModelGroup( /// A stable key for grouping, so equal condition sets share a model and different ones do not. /// private static string GroupKey( - ITypeDefinition implementation, IReadOnlyList? conditions) { - - if ((conditions?.Count ?? 0) == 0) { + ITypeDefinition implementation, + IReadOnlyList? conditions + ) + { + if ((conditions?.Count ?? 0) == 0) + { return implementation.ToString(); } var parts = conditions!.Select(condition => - $"{condition.Kind}|{condition.Negate}|{condition.Key}|{string.Join(",", condition.Values)}"); + $"{condition.Kind}|{condition.Negate}|{condition.Key}|{string.Join(",", condition.Values)}" + ); return implementation + "::" + string.Join(";", parts); } @@ -774,8 +913,10 @@ private static string GroupKey( /// instance per service type instead of the shared instance the contract promises. /// private static IReadOnlyList BuildRegistrations( - ConventionRegistrationMatch match, ModuleEntryPointModel entryPointModel) { - + ConventionRegistrationMatch match, + ModuleEntryPointModel entryPointModel + ) + { var convention = match.Convention; var lifestyle = convention.Lifestyle!.Value; @@ -784,16 +925,22 @@ private static IReadOnlyList BuildRegistrations( // module declares one. var realm = entryPointModel.EntryPointType; - ServiceRegistrationModel Registration(ITypeDefinition serviceType, bool crossWire = false) => - new(serviceType, + ServiceRegistrationModel Registration( + ITypeDefinition serviceType, + bool crossWire = false + ) => + new( + serviceType, lifestyle, convention.RegistrationType, realm, convention.Key, crossWire, - convention.KeyNamespaces); + convention.KeyNamespaces + ); - switch (convention.RegisterAs) { + switch (convention.RegisterAs) + { case ConventionRegisterAs.Self: return new[] { Registration(match.Candidate.ImplementationType) }; @@ -821,9 +968,14 @@ ServiceRegistrationModel Registration(ITypeDefinition serviceType, bool crossWir case ConventionRegisterAs.SelfAndInterfaces: var crossWired = new List(); - foreach (var reachable in - match.Candidate.InterfacesInReach(convention.IncludeBaseClasses)) { - if (IsFrameworkInterface(reachable.InterfaceType)) { + foreach ( + var reachable in match.Candidate.InterfacesInReach( + convention.IncludeBaseClasses + ) + ) + { + if (IsFrameworkInterface(reachable.InterfaceType)) + { continue; } @@ -832,7 +984,8 @@ ServiceRegistrationModel Registration(ITypeDefinition serviceType, bool crossWir // Nothing left to expand to still registers the type itself, so filtering everything // away degrades to AsSelf() rather than to nothing. - if (crossWired.Count == 0) { + if (crossWired.Count == 0) + { crossWired.Add(Registration(match.Candidate.ImplementationType)); } diff --git a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs index 7a32e00..0b9dfdd 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs @@ -26,8 +26,8 @@ namespace DependencyModules.Conventions.Utilities; /// breaks the cache. /// /// -public static class ConventionModelUtility { - +public static class ConventionModelUtility +{ private const string RegisterAll = "RegisterAll"; private const string IncludeBaseClasses = "IncludeBaseClasses"; private const string UsingCall = "Using"; @@ -47,20 +47,26 @@ public static class ConventionModelUtility { /// Named after the attributes rather than something fluent-sounding like OnlyIn, so the /// two ways of saying the same thing read the same and one is discoverable from the other. /// - private static readonly Dictionary ConditionCalls = new() { + private static readonly Dictionary< + string, + (EnvironmentConditionKind Kind, bool Negate) + > ConditionCalls = new() + { ["IfEnvironment"] = (EnvironmentConditionKind.Name, false), ["IfNotEnvironment"] = (EnvironmentConditionKind.Name, true), ["IfEnvironmentValue"] = (EnvironmentConditionKind.Value, false), ["IfNotEnvironmentValue"] = (EnvironmentConditionKind.Value, true), }; - private static readonly Dictionary LifetimeCalls = new() { + private static readonly Dictionary LifetimeCalls = new() + { ["AsSingleton"] = ServiceLifestyle.Singleton, ["AsScoped"] = ServiceLifestyle.Scoped, ["AsTransient"] = ServiceLifestyle.Transient, }; - private static readonly Dictionary RegisterAsCalls = new() { + private static readonly Dictionary RegisterAsCalls = new() + { ["AsSelf"] = ConventionRegisterAs.Self, ["AsSelfWithInterfaces"] = ConventionRegisterAs.SelfAndInterfaces, ["AlsoAsSelf"] = ConventionRegisterAs.AlsoSelf, @@ -69,7 +75,8 @@ public static class ConventionModelUtility { /// /// The namespace filter calls, and the shape of filter each produces. /// - private static readonly Dictionary NamespaceCalls = new() { + private static readonly Dictionary NamespaceCalls = new() + { ["InNamespaceOf"] = (false, false), ["InNamespaces"] = (false, false), ["InExactNamespaces"] = (true, false), @@ -86,15 +93,22 @@ public static class ConventionModelUtility { /// the semantic model; the transform confirms it is the right IConventionModule before /// reading anything. /// - public static bool IsConventionModuleCandidate(SyntaxNode node, CancellationToken cancellationToken) { + public static bool IsConventionModuleCandidate( + SyntaxNode node, + CancellationToken cancellationToken + ) + { cancellationToken.ThrowIfCancellationRequested(); - if (node is not TypeDeclarationSyntax { BaseList: not null } typeDeclaration) { + if (node is not TypeDeclarationSyntax { BaseList: not null } typeDeclaration) + { return false; } - foreach (var baseType in typeDeclaration.BaseList.Types) { - if (SimpleNameOf(baseType.Type) == ConventionContractSource.ConventionModule) { + foreach (var baseType in typeDeclaration.BaseList.Types) + { + if (SimpleNameOf(baseType.Type) == ConventionContractSource.ConventionModule) + { return true; } } @@ -103,51 +117,65 @@ public static bool IsConventionModuleCandidate(SyntaxNode node, CancellationToke } public static ConventionModuleModel GetConventionModuleModel( - SyntaxTransformContext context, CancellationToken cancellationToken) { - + SyntaxTransformContext context, + CancellationToken cancellationToken + ) + { cancellationToken.ThrowIfCancellationRequested(); - if (context.Node is not TypeDeclarationSyntax typeDeclaration) { + if (context.Node is not TypeDeclarationSyntax typeDeclaration) + { return ConventionModuleModel.Ignore; } - if (!ImplementsConventionModule(context, typeDeclaration)) { + if (!ImplementsConventionModule(context, typeDeclaration)) + { return ConventionModuleModel.Ignore; } var method = FindConventionsMethod(typeDeclaration); - if (method?.Body == null) { + if (method?.Body == null) + { // An expression-bodied or abstract declaration has nothing to read. Reported rather than // ignored: the module said it had conventions and produced none. return new ConventionModuleModel( typeDeclaration.GetTypeDefinition(), Array.Empty(), - new[] { + new[] + { new UnreadableStatementModel( method?.ToString() ?? ConventionContractSource.ConventionMethod, "the Conventions method needs a statement body containing RegisterAll calls", - LocationModel.From((SyntaxNode?)method ?? typeDeclaration)) - }); + LocationModel.From((SyntaxNode?)method ?? typeDeclaration) + ), + } + ); } var parameterName = method.ParameterList.Parameters.FirstOrDefault()?.Identifier.Text; - if (string.IsNullOrEmpty(parameterName)) { + if (string.IsNullOrEmpty(parameterName)) + { return ConventionModuleModel.Ignore; } var conventions = new List(); var unreadable = new List(); - foreach (var statement in method.Body.Statements) { + foreach (var statement in method.Body.Statements) + { cancellationToken.ThrowIfCancellationRequested(); ReadStatement(context, statement, parameterName!, conventions, unreadable); } return new ConventionModuleModel( - typeDeclaration.GetTypeDefinition(), conventions, unreadable, LocationModel.From(typeDeclaration)); + typeDeclaration.GetTypeDefinition(), + conventions, + unreadable, + LocationModel.From(typeDeclaration) + ); } private static void ReadStatement( @@ -155,30 +183,38 @@ private static void ReadStatement( StatementSyntax statement, string parameterName, List conventions, - List unreadable) { - - if (statement is not ExpressionStatementSyntax { Expression: InvocationExpressionSyntax invocation }) { - unreadable.Add(Refuse( - statement, - "only RegisterAll chains can appear here, because this body is read at compile time " + - "rather than executed")); + List unreadable + ) + { + if ( + statement + is not ExpressionStatementSyntax { Expression: InvocationExpressionSyntax invocation } + ) + { + unreadable.Add( + Refuse( + statement, + "only RegisterAll chains can appear here, because this body is read at compile time " + + "rather than executed" + ) + ); return; } var chain = UnwrapChain(invocation, parameterName); - if (chain == null) { - unreadable.Add(Refuse( - statement, - $"expected a chain of calls on '{parameterName}'")); + if (chain == null) + { + unreadable.Add(Refuse(statement, $"expected a chain of calls on '{parameterName}'")); return; } var convention = BuildConvention(context, statement, chain, out var reason); - if (convention == null) { + if (convention == null) + { unreadable.Add(Refuse(statement, reason!)); return; @@ -197,13 +233,17 @@ private static void ReadStatement( /// a helper call or an unrelated statement being read as a convention. /// private static List? UnwrapChain( - InvocationExpressionSyntax invocation, string parameterName) { - + InvocationExpressionSyntax invocation, + string parameterName + ) + { var calls = new List(); ExpressionSyntax current = invocation; - while (current is InvocationExpressionSyntax candidate) { - if (candidate.Expression is not MemberAccessExpressionSyntax access) { + while (current is InvocationExpressionSyntax candidate) + { + if (candidate.Expression is not MemberAccessExpressionSyntax access) + { return null; } @@ -211,8 +251,11 @@ private static void ReadStatement( current = access.Expression; } - if (current is not IdentifierNameSyntax identifier || - identifier.Identifier.Text != parameterName) { + if ( + current is not IdentifierNameSyntax identifier + || identifier.Identifier.Text != parameterName + ) + { return null; } @@ -225,14 +268,16 @@ private static void ReadStatement( SyntaxTransformContext context, StatementSyntax statement, List chain, - out string? reason) { - + out string? reason + ) + { reason = null; var head = chain[0]; var headName = MethodNameOf(head); - if (headName != RegisterAll) { + if (headName != RegisterAll) + { reason = $"a convention has to start with {RegisterAll}, not '{headName}'"; return null; @@ -241,19 +286,21 @@ private static void ReadStatement( // No type argument and no argument at all is the filter-selected form, which is valid and // has no service type. Anything else that fails to resolve is a mistake. var selectsByFilter = - head.Expression is MemberAccessExpressionSyntax { Name: not GenericNameSyntax } && - head.ArgumentList.Arguments.Count == 0; + head.Expression is MemberAccessExpressionSyntax { Name: not GenericNameSyntax } + && head.ArgumentList.Arguments.Count == 0; ITypeDefinition? serviceType = null; var isOpenGeneric = false; - if (!selectsByFilter) { + if (!selectsByFilter) + { serviceType = ReadServiceType(context, head, out isOpenGeneric); - if (serviceType == null) { + if (serviceType == null) + { reason = - $"could not resolve the service type; write {RegisterAll}(), " + - $"{RegisterAll}(typeof(IService<>)) or {RegisterAll}() with a filter"; + $"could not resolve the service type; write {RegisterAll}(), " + + $"{RegisterAll}(typeof(IService<>)) or {RegisterAll}() with a filter"; return null; } @@ -272,13 +319,17 @@ private static void ReadStatement( ITypeDefinition? explicitServiceType = null; string? assemblyName = null; - for (var i = 1; i < chain.Count; i++) { + for (var i = 1; i < chain.Count; i++) + { var call = chain[i]; var name = MethodNameOf(call); - if (LifetimeCalls.TryGetValue(name, out var candidateLifestyle)) { - if (lifestyle != null) { - reason = "a convention declares one lifetime, and this one declares more than one"; + if (LifetimeCalls.TryGetValue(name, out var candidateLifestyle)) + { + if (lifestyle != null) + { + reason = + "a convention declares one lifetime, and this one declares more than one"; return null; } @@ -288,15 +339,22 @@ private static void ReadStatement( continue; } - if (name == IncludeBaseClasses) { + if (name == IncludeBaseClasses) + { includeBaseClasses = true; continue; } - if (RegisterAsCalls.TryGetValue(name, out var candidateRegisterAs)) { - if (registerAs != ConventionRegisterAs.Interfaces && registerAs != candidateRegisterAs) { - reason = "a convention registers matches one way, and this one says more than one"; + if (RegisterAsCalls.TryGetValue(name, out var candidateRegisterAs)) + { + if ( + registerAs != ConventionRegisterAs.Interfaces + && registerAs != candidateRegisterAs + ) + { + reason = + "a convention registers matches one way, and this one says more than one"; return null; } @@ -306,14 +364,19 @@ private static void ReadStatement( continue; } - if (name == UsingCall) { + if (name == UsingCall) + { var argument = call.ArgumentList.Arguments.FirstOrDefault()?.Expression; - registrationType = argument == null - ? null - : SourceGenerator.Impl.BaseSourceGenerator.GetRegistrationType(argument.ToString()); + registrationType = + argument == null + ? null + : SourceGenerator.Impl.BaseSourceGenerator.GetRegistrationType( + argument.ToString() + ); - if (registrationType == null) { + if (registrationType == null) + { reason = $"'{name}' needs a RegistrationType it can read at compile time"; return null; @@ -322,10 +385,12 @@ private static void ReadStatement( continue; } - if (name == WithKeyCall) { + if (name == WithKeyCall) + { var argument = call.ArgumentList.Arguments.FirstOrDefault()?.Expression; - if (argument == null) { + if (argument == null) + { reason = $"'{name}' needs a key"; return null; @@ -335,17 +400,22 @@ private static void ReadStatement( // and an enum member all reach the emitted registration unchanged. key = argument.ToString(); - if (argument is MemberAccessExpressionSyntax memberAccess) { - keyNamespaces = memberAccess.GetTypeDefinition(context)?.KnownNamespaces.ToArray(); + if (argument is MemberAccessExpressionSyntax memberAccess) + { + keyNamespaces = memberAccess + .GetTypeDefinition(context) + ?.KnownNamespaces.ToArray(); } continue; } - if (name == InAssemblyOfCall) { + if (name == InAssemblyOfCall) + { assemblyName = MarkerAssemblyNameOf(context, call); - if (assemblyName == null) { + if (assemblyName == null) + { reason = $"'{name}' needs a type argument from the assembly to scan"; return null; @@ -354,16 +424,19 @@ private static void ReadStatement( continue; } - if (name == AsMatchingInterfaceCall) { + if (name == AsMatchingInterfaceCall) + { registerAs = ConventionRegisterAs.MatchingInterface; continue; } - if (name == AsCall) { + if (name == AsCall) + { explicitServiceType = SingleTypeArgumentOf(context, call); - if (explicitServiceType == null) { + if (explicitServiceType == null) + { reason = $"'{name}' needs a service type argument"; return null; @@ -374,10 +447,12 @@ private static void ReadStatement( continue; } - if (name is WithNameCall or WithoutNameCall) { + if (name is WithNameCall or WithoutNameCall) + { var patterns = ReadPatterns(context, call); - if (patterns.Count == 0) { + if (patterns.Count == 0) + { reason = $"'{name}' needs at least one pattern it can read at compile time"; return null; @@ -385,71 +460,96 @@ private static void ReadStatement( nameFilters ??= new List(); - foreach (var pattern in patterns) { + foreach (var pattern in patterns) + { nameFilters.Add(new NameFilterModel(pattern, name == WithoutNameCall)); } continue; } - if (ConditionCalls.TryGetValue(name, out var conditionCall)) { + if (ConditionCalls.TryGetValue(name, out var conditionCall)) + { // Read as literals for the same reason every other filter is: the declaration is // parsed, never executed, so anything the build cannot see is refused rather than // quietly dropped. var arguments = ReadPatterns(context, call); - if (arguments.Count == 0) { - reason = conditionCall.Kind == EnvironmentConditionKind.Name - ? $"'{name}' needs at least one environment name it can read at compile time" - : $"'{name}' needs an environment key it can read at compile time"; + if (arguments.Count == 0) + { + reason = + conditionCall.Kind == EnvironmentConditionKind.Name + ? $"'{name}' needs at least one environment name it can read at compile time" + : $"'{name}' needs an environment key it can read at compile time"; return null; } conditions ??= new List(); - if (conditionCall.Kind == EnvironmentConditionKind.Name) { - conditions.Add(new EnvironmentConditionModel( - EnvironmentConditionKind.Name, conditionCall.Negate, null, arguments)); - } else { + if (conditionCall.Kind == EnvironmentConditionKind.Name) + { + conditions.Add( + new EnvironmentConditionModel( + EnvironmentConditionKind.Name, + conditionCall.Negate, + null, + arguments + ) + ); + } + else + { // (key) tests presence, (key, value) tests equality. More than two would be a // call that does not exist on the interface. - if (arguments.Count > 2) { + if (arguments.Count > 2) + { reason = $"'{name}' takes a key and an optional value"; return null; } - conditions.Add(new EnvironmentConditionModel( - EnvironmentConditionKind.Value, - conditionCall.Negate, - arguments[0], - arguments.Count > 1 ? new[] { arguments[1] } : Array.Empty())); + conditions.Add( + new EnvironmentConditionModel( + EnvironmentConditionKind.Value, + conditionCall.Negate, + arguments[0], + arguments.Count > 1 ? new[] { arguments[1] } : Array.Empty() + ) + ); } continue; } - if (name is WithAttributeCall or WithoutAttributeCall) { + if (name is WithAttributeCall or WithoutAttributeCall) + { var attributeType = SingleTypeArgumentOf(context, call); - if (attributeType == null) { + if (attributeType == null) + { reason = $"'{name}' needs an attribute type argument"; return null; } attributeFilters ??= new List(); - attributeFilters.Add(new AttributeFilterModel( - ConventionTypeKey.For(attributeType), name == WithoutAttributeCall)); + attributeFilters.Add( + new AttributeFilterModel( + ConventionTypeKey.For(attributeType), + name == WithoutAttributeCall + ) + ); continue; } - if (NamespaceCalls.TryGetValue(name, out var namespaceCall)) { + if (NamespaceCalls.TryGetValue(name, out var namespaceCall)) + { var read = ReadNamespaceFilters(context, call, namespaceCall); - if (read == null) { + if (read == null) + { reason = $"'{name}' needs a namespace it can read at compile time"; return null; @@ -466,24 +566,27 @@ private static void ReadStatement( return null; } - if (selectsByFilter) { - if (registerAs == ConventionRegisterAs.Interfaces) { + if (selectsByFilter) + { + if (registerAs == ConventionRegisterAs.Interfaces) + { reason = - $"{RegisterAll}() names no service type, so there is nothing to register the " + - "matches as; call AsSelf() or AsSelfWithInterfaces()"; + $"{RegisterAll}() names no service type, so there is nothing to register the " + + "matches as; call AsSelf() or AsSelfWithInterfaces()"; return null; } var hasInclusion = - namespaceFilters?.Any(filter => !filter.Exclude) == true || - nameFilters?.Any(filter => !filter.Exclude) == true || - attributeFilters?.Any(filter => !filter.Exclude) == true; + namespaceFilters?.Any(filter => !filter.Exclude) == true + || nameFilters?.Any(filter => !filter.Exclude) == true + || attributeFilters?.Any(filter => !filter.Exclude) == true; - if (!hasInclusion) { + if (!hasInclusion) + { reason = - $"{RegisterAll}() with no filter matches every class in the compilation; " + - "narrow it with InNamespaceOf() or InNamespaces(...)"; + $"{RegisterAll}() with no filter matches every class in the compilation; " + + "narrow it with InNamespaceOf() or InNamespaces(...)"; return null; } @@ -507,7 +610,8 @@ private static void ReadStatement( nameFilters, explicitServiceType, assemblyName, - conditions); + conditions + ); } /// @@ -518,12 +622,16 @@ private static void ReadStatement( /// reads as the string it evaluates to. /// private static IReadOnlyList ReadPatterns( - SyntaxTransformContext context, InvocationExpressionSyntax call) { - + SyntaxTransformContext context, + InvocationExpressionSyntax call + ) + { var values = new List(); - foreach (var argument in call.ArgumentList.Arguments) { - if (context.SemanticModel.GetConstantValue(argument.Expression).Value is string value) { + foreach (var argument in call.ArgumentList.Arguments) + { + if (context.SemanticModel.GetConstantValue(argument.Expression).Value is string value) + { values.Add(value); } } @@ -540,14 +648,21 @@ private static IReadOnlyList ReadPatterns( /// is what keeps a metadata scan affordable. /// private static string? MarkerAssemblyNameOf( - SyntaxTransformContext context, InvocationExpressionSyntax call) { - - if (call.Expression is not MemberAccessExpressionSyntax { Name: GenericNameSyntax generic } || - generic.TypeArgumentList.Arguments.Count != 1) { + SyntaxTransformContext context, + InvocationExpressionSyntax call + ) + { + if ( + call.Expression is not MemberAccessExpressionSyntax { Name: GenericNameSyntax generic } + || generic.TypeArgumentList.Arguments.Count != 1 + ) + { return null; } - var symbol = context.SemanticModel.GetSymbolInfo(generic.TypeArgumentList.Arguments[0]).Symbol; + var symbol = context + .SemanticModel.GetSymbolInfo(generic.TypeArgumentList.Arguments[0]) + .Symbol; return symbol?.ContainingAssembly?.Name; } @@ -556,9 +671,11 @@ private static IReadOnlyList ReadPatterns( /// The single type argument of a generic filter call, resolved. /// private static ITypeDefinition? SingleTypeArgumentOf( - SyntaxTransformContext context, InvocationExpressionSyntax call) => - call.Expression is MemberAccessExpressionSyntax { Name: GenericNameSyntax generic } && - generic.TypeArgumentList.Arguments.Count == 1 + SyntaxTransformContext context, + InvocationExpressionSyntax call + ) => + call.Expression is MemberAccessExpressionSyntax { Name: GenericNameSyntax generic } + && generic.TypeArgumentList.Arguments.Count == 1 ? generic.TypeArgumentList.Arguments[0].GetTypeDefinition(context) : null; @@ -567,16 +684,23 @@ private static IReadOnlyList ReadPatterns( /// literals. /// private static List? ReadNamespaceFilters( - SyntaxTransformContext context, InvocationExpressionSyntax call, (bool Exact, bool Exclude) form) { - + SyntaxTransformContext context, + InvocationExpressionSyntax call, + (bool Exact, bool Exclude) form + ) + { var filters = new List(); // InNamespaceOf() — the namespace is wherever the marker type lives. - if (call.Expression is MemberAccessExpressionSyntax { Name: GenericNameSyntax generic } && - generic.TypeArgumentList.Arguments.Count == 1) { + if ( + call.Expression is MemberAccessExpressionSyntax { Name: GenericNameSyntax generic } + && generic.TypeArgumentList.Arguments.Count == 1 + ) + { var marker = generic.TypeArgumentList.Arguments[0].GetTypeDefinition(context); - if (marker == null) { + if (marker == null) + { return null; } @@ -585,8 +709,13 @@ private static IReadOnlyList ReadPatterns( return filters; } - foreach (var argument in call.ArgumentList.Arguments) { - if (context.SemanticModel.GetConstantValue(argument.Expression).Value is not string value) { + foreach (var argument in call.ArgumentList.Arguments) + { + if ( + context.SemanticModel.GetConstantValue(argument.Expression).Value + is not string value + ) + { return null; } @@ -601,19 +730,30 @@ private static IReadOnlyList ReadPatterns( /// RegisterAll(typeof(T)). /// private static ITypeDefinition? ReadServiceType( - SyntaxTransformContext context, InvocationExpressionSyntax invocation, out bool isOpenGeneric) { - + SyntaxTransformContext context, + InvocationExpressionSyntax invocation, + out bool isOpenGeneric + ) + { isOpenGeneric = false; - if (invocation.Expression is MemberAccessExpressionSyntax { Name: GenericNameSyntax { TypeArgumentList.Arguments.Count: 1 } generic }) { + if ( + invocation.Expression is MemberAccessExpressionSyntax + { + Name: GenericNameSyntax { TypeArgumentList.Arguments.Count: 1 } generic + } + ) + { return generic.TypeArgumentList.Arguments[0].GetTypeDefinition(context); } - var argument = invocation.ArgumentList.Arguments.Count == 1 - ? invocation.ArgumentList.Arguments[0].Expression - : null; + var argument = + invocation.ArgumentList.Arguments.Count == 1 + ? invocation.ArgumentList.Arguments[0].Expression + : null; - if (argument is not TypeOfExpressionSyntax typeOf) { + if (argument is not TypeOfExpressionSyntax typeOf) + { return null; } @@ -625,12 +765,15 @@ private static IReadOnlyList ReadPatterns( /// /// True for typeof(IHandler<,>), where the type arguments are omitted. /// - private static bool IsUnboundGeneric(TypeSyntax type) { - var generic = type as GenericNameSyntax ?? - (type as QualifiedNameSyntax)?.Right as GenericNameSyntax; - - return generic != null && - generic.TypeArgumentList.Arguments.Any(argument => argument is OmittedTypeArgumentSyntax); + private static bool IsUnboundGeneric(TypeSyntax type) + { + var generic = + type as GenericNameSyntax ?? (type as QualifiedNameSyntax)?.Right as GenericNameSyntax; + + return generic != null + && generic.TypeArgumentList.Arguments.Any(argument => + argument is OmittedTypeArgumentSyntax + ); } private static UnreadableStatementModel Refuse(StatementSyntax statement, string reason) => @@ -640,22 +783,31 @@ private static UnreadableStatementModel Refuse(StatementSyntax statement, string /// A single line of the refused statement, so the diagnostic message stays readable when what /// was refused is a loop or a block. /// - private static string Summarise(StatementSyntax statement) { + private static string Summarise(StatementSyntax statement) + { var text = statement.ToString().Replace("\r", " ").Replace("\n", " ").Trim(); - while (text.Contains(" ")) { + while (text.Contains(" ")) + { text = text.Replace(" ", " "); } return text.Length <= 80 ? text : text.Substring(0, 77) + "..."; } - private static MethodDeclarationSyntax? FindConventionsMethod(TypeDeclarationSyntax typeDeclaration) { + private static MethodDeclarationSyntax? FindConventionsMethod( + TypeDeclarationSyntax typeDeclaration + ) + { MethodDeclarationSyntax? fallback = null; - foreach (var method in typeDeclaration.Members.OfType()) { - if (method.Identifier.Text != ConventionContractSource.ConventionMethod || - method.ParameterList.Parameters.Count != 1) { + foreach (var method in typeDeclaration.Members.OfType()) + { + if ( + method.Identifier.Text != ConventionContractSource.ConventionMethod + || method.ParameterList.Parameters.Count != 1 + ) + { continue; } @@ -665,7 +817,8 @@ private static string Summarise(StatementSyntax statement) { // against an internal parameter type, which is why explicit implementation was the only // form that compiled. The explicit one is still preferred when a type carries both, // because that is the one the interface is actually satisfied by. - if (method.ExplicitInterfaceSpecifier != null) { + if (method.ExplicitInterfaceSpecifier != null) + { return method; } @@ -676,21 +829,29 @@ private static string Summarise(StatementSyntax statement) { } private static bool ImplementsConventionModule( - SyntaxTransformContext context, TypeDeclarationSyntax typeDeclaration) { - - if (typeDeclaration.BaseList == null) { + SyntaxTransformContext context, + TypeDeclarationSyntax typeDeclaration + ) + { + if (typeDeclaration.BaseList == null) + { return false; } - foreach (var baseType in typeDeclaration.BaseList.Types) { - if (SimpleNameOf(baseType.Type) != ConventionContractSource.ConventionModule) { + foreach (var baseType in typeDeclaration.BaseList.Types) + { + if (SimpleNameOf(baseType.Type) != ConventionContractSource.ConventionModule) + { continue; } // Confirms it is the emitted contract rather than a same-named interface of the // developer's own, which the syntactic predicate cannot tell apart. - if (context.SemanticModel.GetSymbolInfo(baseType.Type).Symbol is INamedTypeSymbol symbol && - symbol.ContainingNamespace.GetFullName() == ConventionContractSource.Namespace) { + if ( + context.SemanticModel.GetSymbolInfo(baseType.Type).Symbol is INamedTypeSymbol symbol + && symbol.ContainingNamespace.GetFullName() == ConventionContractSource.Namespace + ) + { return true; } } @@ -707,7 +868,8 @@ invocation.Expression is MemberAccessExpressionSyntax access /// The unqualified name of a written type, without namespace or type arguments. /// private static string SimpleNameOf(TypeSyntax type) => - type switch { + type switch + { SimpleNameSyntax simple => simple.Identifier.Text, QualifiedNameSyntax qualified => qualified.Right.Identifier.Text, AliasQualifiedNameSyntax alias => alias.Name.Identifier.Text, diff --git a/src/DependencyModules.SourceGenerator/Conventions/Utilities/DeclarationStamp.cs b/src/DependencyModules.SourceGenerator/Conventions/Utilities/DeclarationStamp.cs index df660fd..d2a1cdb 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Utilities/DeclarationStamp.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Utilities/DeclarationStamp.cs @@ -32,12 +32,13 @@ namespace DependencyModules.Conventions.Utilities; /// from it needs an argument. /// /// -public static class DeclarationStamp { - +public static class DeclarationStamp +{ private static readonly ConditionalWeakTable PerTree = new(); private static readonly ConditionalWeakTable PerCompilation = new(); - private sealed class StampBox { + private sealed class StampBox + { public StampBox(long value) => Value = value; public long Value { get; } @@ -47,8 +48,10 @@ private sealed class StampBox { /// The stamp for a whole compilation. Memoised on the compilation, and on each tree beneath it, /// so an edit re-hashes one tree and re-combines the rest. /// - public static long Of(Compilation compilation) { - if (PerCompilation.TryGetValue(compilation, out var cached)) { + public static long Of(Compilation compilation) + { + if (PerCompilation.TryGetValue(compilation, out var cached)) + { return cached.Value; } @@ -56,11 +59,13 @@ public static long Of(Compilation compilation) { // a stale semantic model is served, so the width is a correctness property. var hash = 14695981039346656037UL; - foreach (var tree in compilation.SyntaxTrees) { + foreach (var tree in compilation.SyntaxTrees) + { hash = Mix(hash, (ulong)TreeStamp(tree)); } - foreach (var reference in compilation.References) { + foreach (var reference in compilation.References) + { hash = Mix(hash, (ulong)(reference.Display?.GetHashCode() ?? 0)); } @@ -75,8 +80,10 @@ public static long Of(Compilation compilation) { /// One tree's contribution. Cached on the tree, which is immutable, so only an edited tree is /// ever re-hashed. /// - private static long TreeStamp(SyntaxTree tree) { - if (PerTree.TryGetValue(tree, out var cached)) { + private static long TreeStamp(SyntaxTree tree) + { + if (PerTree.TryGetValue(tree, out var cached)) + { return cached.Value; } @@ -84,10 +91,18 @@ private static long TreeStamp(SyntaxTree tree) { // Descends into containers only. A method body is not a container of anything that can // change a binding, and skipping them is what makes the common keystroke free. - foreach (var node in tree.GetRoot().DescendantNodes(descendIntoChildren: n => - n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax or TypeDeclarationSyntax)) { - - switch (node) { + foreach ( + var node in tree.GetRoot() + .DescendantNodes(descendIntoChildren: n => + n + is CompilationUnitSyntax + or BaseNamespaceDeclarationSyntax + or TypeDeclarationSyntax + ) + ) + { + switch (node) + { case UsingDirectiveSyntax usingDirective: hash = Mix(hash, Hash(usingDirective.ToString())); break; @@ -111,7 +126,8 @@ private static long TreeStamp(SyntaxTree tree) { // Signatures, not bodies. A constructor added to one part of a partial changes // what another part's symbol reports about itself. - foreach (var member in type.Members) { + foreach (var member in type.Members) + { hash = Mix(hash, MemberSignature(member)); } @@ -127,31 +143,44 @@ private static long TreeStamp(SyntaxTree tree) { } private static ulong MemberSignature(MemberDeclarationSyntax member) => - member switch { - ConstructorDeclarationSyntax constructor => - Hash(constructor.Modifiers + constructor.ParameterList.ToString()), - MethodDeclarationSyntax method => - Hash(method.Modifiers + method.ReturnType.ToString() + method.Identifier.Text + - method.TypeParameterList + method.ParameterList), - PropertyDeclarationSyntax property => - Hash(property.Modifiers + property.Type.ToString() + property.Identifier.Text), - FieldDeclarationSyntax field => - Hash(field.Modifiers + field.Declaration.Type.ToString() + - string.Join(",", field.Declaration.Variables.Select(v => v.Identifier.Text))), - EventDeclarationSyntax @event => - Hash(@event.Modifiers + @event.Type.ToString() + @event.Identifier.Text), + member switch + { + ConstructorDeclarationSyntax constructor => Hash( + constructor.Modifiers + constructor.ParameterList.ToString() + ), + MethodDeclarationSyntax method => Hash( + method.Modifiers + + method.ReturnType.ToString() + + method.Identifier.Text + + method.TypeParameterList + + method.ParameterList + ), + PropertyDeclarationSyntax property => Hash( + property.Modifiers + property.Type.ToString() + property.Identifier.Text + ), + FieldDeclarationSyntax field => Hash( + field.Modifiers + + field.Declaration.Type.ToString() + + string.Join(",", field.Declaration.Variables.Select(v => v.Identifier.Text)) + ), + EventDeclarationSyntax @event => Hash( + @event.Modifiers + @event.Type.ToString() + @event.Identifier.Text + ), // Nested types are reached by the walk above, so they need nothing here. - _ => Hash(member.Kind().ToString()) + _ => Hash(member.Kind().ToString()), }; - private static ulong Hash(string? text) { - if (text == null) { + private static ulong Hash(string? text) + { + if (text == null) + { return 0; } var hash = 14695981039346656037UL; - foreach (var c in text) { + foreach (var c in text) + { hash = (hash ^ c) * 1099511628211UL; } diff --git a/src/DependencyModules.SourceGenerator/Conventions/Utilities/MetadataCandidateUtility.cs b/src/DependencyModules.SourceGenerator/Conventions/Utilities/MetadataCandidateUtility.cs index 4532730..582214d 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Utilities/MetadataCandidateUtility.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Utilities/MetadataCandidateUtility.cs @@ -28,8 +28,8 @@ namespace DependencyModules.Conventions.Utilities; /// documented rather than diagnosed. /// /// -public static class MetadataCandidateUtility { - +public static class MetadataCandidateUtility +{ /// /// Candidates from every assembly the given conventions name. /// @@ -40,33 +40,47 @@ public static class MetadataCandidateUtility { public static IReadOnlyList Collect( IReadOnlyList conventionModules, Compilation compilation, - CancellationToken cancellationToken) { - + CancellationToken cancellationToken + ) + { var wanted = new HashSet(StringComparer.Ordinal); - foreach (var module in conventionModules) { - foreach (var convention in module.Conventions) { - if (convention.AssemblyName != null) { + foreach (var module in conventionModules) + { + foreach (var convention in module.Conventions) + { + if (convention.AssemblyName != null) + { wanted.Add(convention.AssemblyName); } } } - if (wanted.Count == 0) { + if (wanted.Count == 0) + { return Array.Empty(); } var candidates = new List(); - foreach (var reference in compilation.References) { + foreach (var reference in compilation.References) + { cancellationToken.ThrowIfCancellationRequested(); - if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assembly || - !wanted.Contains(assembly.Name)) { + if ( + compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assembly + || !wanted.Contains(assembly.Name) + ) + { continue; } - CollectFromNamespace(assembly.GlobalNamespace, assembly.Name, candidates, cancellationToken); + CollectFromNamespace( + assembly.GlobalNamespace, + assembly.Name, + candidates, + cancellationToken + ); } return candidates; @@ -76,12 +90,15 @@ private static void CollectFromNamespace( INamespaceSymbol namespaceSymbol, string assemblyName, List candidates, - CancellationToken cancellationToken) { - - foreach (var member in namespaceSymbol.GetMembers()) { + CancellationToken cancellationToken + ) + { + foreach (var member in namespaceSymbol.GetMembers()) + { cancellationToken.ThrowIfCancellationRequested(); - switch (member) { + switch (member) + { case INamespaceSymbol nested: CollectFromNamespace(nested, assemblyName, candidates, cancellationToken); break; @@ -106,17 +123,20 @@ private static void CollectFromNamespace( /// scanning that interface would match the decorator and register it as a service. /// private static bool IsCandidate(INamedTypeSymbol type) => - type.TypeKind == TypeKind.Class && - !type.IsAbstract && - !type.IsStatic && - type.DeclaredAccessibility == Accessibility.Public && - !DeclaresRegistration(type); - - private static bool DeclaresRegistration(INamedTypeSymbol type) { - foreach (var attribute in type.GetAttributes()) { + type.TypeKind == TypeKind.Class + && !type.IsAbstract + && !type.IsStatic + && type.DeclaredAccessibility == Accessibility.Public + && !DeclaresRegistration(type); + + private static bool DeclaresRegistration(INamedTypeSymbol type) + { + foreach (var attribute in type.GetAttributes()) + { var name = attribute.AttributeClass?.Name; - if (name != null && Array.IndexOf(ExcludedAttributeNames, name) >= 0) { + if (name != null && Array.IndexOf(ExcludedAttributeNames, name) >= 0) + { return true; } } @@ -124,7 +144,8 @@ private static bool DeclaresRegistration(INamedTypeSymbol type) { return false; } - private static readonly string[] ExcludedAttributeNames = { + private static readonly string[] ExcludedAttributeNames = + { "SingletonServiceAttribute", "ScopedServiceAttribute", "TransientServiceAttribute", @@ -132,7 +153,11 @@ private static bool DeclaresRegistration(INamedTypeSymbol type) { "DecoratorAttribute", }; - private static ConventionCandidateModel BuildCandidate(INamedTypeSymbol type, string assemblyName) { + private static ConventionCandidateModel BuildCandidate( + INamedTypeSymbol type, + string assemblyName + ) + { var declared = new List(); var viaBaseClass = new List(); var seen = new HashSet(); @@ -140,15 +165,18 @@ private static ConventionCandidateModel BuildCandidate(INamedTypeSymbol type, st // Directly implemented interfaces, and what those extend, are the metadata equivalent of // "written on the declaration". Anything else in AllInterfaces arrived through a base class, // which is the distinction IncludeBaseClasses turns on. - foreach (var interfaceSymbol in type.Interfaces) { + foreach (var interfaceSymbol in type.Interfaces) + { Add(declared, seen, interfaceSymbol, null); - foreach (var inherited in interfaceSymbol.AllInterfaces) { + foreach (var inherited in interfaceSymbol.AllInterfaces) + { Add(declared, seen, inherited, interfaceSymbol.Name); } } - foreach (var interfaceSymbol in type.AllInterfaces) { + foreach (var interfaceSymbol in type.AllInterfaces) + { Add(viaBaseClass, seen, interfaceSymbol, type.BaseType?.Name); } @@ -161,23 +189,31 @@ private static ConventionCandidateModel BuildCandidate(INamedTypeSymbol type, st LocationModel.None, null, AttributeKeysOf(type), - assemblyName); + assemblyName + ); } private static void Add( List target, HashSet seen, INamedTypeSymbol interfaceSymbol, - string? viaTypeName) { - + string? viaTypeName + ) + { var definition = interfaceSymbol.GetTypeDefinition(); - if (!seen.Add(definition)) { + if (!seen.Add(definition)) + { return; } - target.Add(new ImplementedInterfaceModel( - definition, ConventionTypeKey.For(definition), viaTypeName)); + target.Add( + new ImplementedInterfaceModel( + definition, + ConventionTypeKey.For(definition), + viaTypeName + ) + ); } /// @@ -192,17 +228,21 @@ private static void Add( private static ConstructorInfoModel? GreediestConstructor(INamedTypeSymbol type) => SymbolConstructorReader.Read(type); - private static IReadOnlyList? AttributeKeysOf(INamedTypeSymbol type) { + private static IReadOnlyList? AttributeKeysOf(INamedTypeSymbol type) + { var attributes = type.GetAttributes(); - if (attributes.Length == 0) { + if (attributes.Length == 0) + { return null; } var keys = new List(attributes.Length); - foreach (var attribute in attributes) { - if (attribute.AttributeClass is { } attributeClass) { + foreach (var attribute in attributes) + { + if (attribute.AttributeClass is { } attributeClass) + { keys.Add(ConventionTypeKey.For(attributeClass.GetTypeDefinition())); } } diff --git a/src/DependencyModules.SourceGenerator/InterceptorSourceGenerator.cs b/src/DependencyModules.SourceGenerator/InterceptorSourceGenerator.cs index 0da8c3d..ae47cc3 100644 --- a/src/DependencyModules.SourceGenerator/InterceptorSourceGenerator.cs +++ b/src/DependencyModules.SourceGenerator/InterceptorSourceGenerator.cs @@ -16,26 +16,34 @@ namespace DependencyModules.SourceGenerator; /// generated rather than written. Everything that path already handles — lifetime preservation, the /// three descriptor shapes, global ordering — applies without change. /// -public class InterceptorSourceGenerator : BaseAttributeSourceGenerator { +public class InterceptorSourceGenerator : BaseAttributeSourceGenerator +{ private readonly IEqualityComparer _comparer = new InterceptorModelComparer(); - private static readonly ITypeDefinition[] _attributeTypes = { - KnownTypes.DependencyModules.Attributes.InterceptAttribute + private static readonly ITypeDefinition[] _attributeTypes = + { + KnownTypes.DependencyModules.Attributes.InterceptAttribute, }; protected override string LoggerName => "InterceptorSourceGenerator"; - protected override IEnumerable AttributeTypes() { + protected override IEnumerable AttributeTypes() + { return _attributeTypes; } protected override InterceptorModel IgnoredModel => InterceptorModel.Ignore; - protected override IEqualityComparer GetComparer() { + protected override IEqualityComparer GetComparer() + { return _comparer; } - protected override InterceptorModel GenerateAttributeModel(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) { + protected override InterceptorModel GenerateAttributeModel( + GeneratorAttributeSyntaxContext context, + CancellationToken cancellationToken + ) + { // A refusal travels on the model so the output stage, which owns the diagnostic context, can // report it. Reporting from the transform is not possible. return InterceptorModelUtility.GetInterceptorModel(context, cancellationToken); @@ -43,36 +51,55 @@ protected override InterceptorModel GenerateAttributeModel(GeneratorAttributeSyn protected override void GenerateSourceOutput( SourceProductionContext context, - (ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, - ImmutableArray Right) inputData, - FileLogger logger) { - - if (inputData.Left.Length == 0 || inputData.Right.Length == 0) { + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) inputData, + FileLogger logger + ) + { + if (inputData.Left.Length == 0 || inputData.Right.Length == 0) + { return; } - var (entryPointList, configurationModel) = EntryModelUtil.ConsolidateEntryPointModels(inputData.Left); + var (entryPointList, configurationModel) = EntryModelUtil.ConsolidateEntryPointModels( + inputData.Left + ); // Filtering only; ReportUnsupported's explanations live in ReportDiagnostics, which shares // the same predicate. var usable = Usable(inputData.Right); - if (usable.Count == 0) { + if (usable.Count == 0) + { return; } var writer = new InterceptorFileWriter(); - foreach (var model in usable) { + foreach (var model in usable) + { context.CancellationToken.ThrowIfCancellationRequested(); var wrapperName = $"{model.ImplementationType.Name.Replace(".", "_")}_Intercepted"; - logger.Info($"Generating '{wrapperName}' for '{model.ServiceType}' with {model.Members.Count} member(s)."); + logger.Info( + $"Generating '{wrapperName}' for '{model.ServiceType}' with {model.Members.Count} member(s)." + ); context.AddSource( $"{wrapperName}.g.cs", - writer.Write(model, wrapperName, model.ImplementationType.Namespace, configurationModel)); + writer.Write( + model, + wrapperName, + model.ImplementationType.Namespace, + configurationModel + ) + ); } // One registration file per module, carrying the interceptions that belong to that module. @@ -80,16 +107,23 @@ protected override void GenerateSourceOutput( // applicators for interceptions that named no realm and had nothing to do with it, which // either wrapped an unrelated service or — when the leaked interceptor needed a dependency // the isolated container did not have — threw while building the provider. - foreach (var entryPointModel in EntryModelUtil.RegistrationTargets(entryPointList)) { + foreach (var entryPointModel in EntryModelUtil.RegistrationTargets(entryPointList)) + { var registrationWriter = new InterceptorRegistrationWriter(); context.AddSource( - EntryModelUtil.EnsureNamespace(entryPointModel, configurationModel) - .EntryPointType.GetFileNameHint(configurationModel.RootNamespace, "Interceptors"), + EntryModelUtil + .EnsureNamespace(entryPointModel, configurationModel) + .EntryPointType.GetFileNameHint( + configurationModel.RootNamespace, + "Interceptors" + ), registrationWriter.Write( EntryModelUtil.EnsureNamespace(entryPointModel, configurationModel), configurationModel, - ForModule(usable, entryPointModel))); + ForModule(usable, entryPointModel) + ) + ); } } @@ -102,21 +136,27 @@ protected override void GenerateSourceOutput( /// realm-only. /// private static IReadOnlyList ForModule( - IReadOnlyList models, ModuleEntryPointModel entryPointModel) { - + IReadOnlyList models, + ModuleEntryPointModel entryPointModel + ) + { var onlyRealm = entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.OnlyRealm); var selected = new List(); - foreach (var model in models) { - if (model.Realm != null) { - if (model.Realm.Equals(entryPointModel.EntryPointType)) { + foreach (var model in models) + { + if (model.Realm != null) + { + if (model.Realm.Equals(entryPointModel.EntryPointType)) + { selected.Add(model); } continue; } - if (!onlyRealm) { + if (!onlyRealm) + { selected.Add(model); } } @@ -132,15 +172,19 @@ private static IReadOnlyList ForModule( /// interceptor able to serve it — a wrapper for the second would forward every call untouched. /// Both are explained by ; this only decides what to write. /// - private static IReadOnlyList Usable(ImmutableArray models) { + private static IReadOnlyList Usable(ImmutableArray models) + { var usable = new List(); - foreach (var model in models) { - if (model.Refusal != null || model.IsIgnored || model.Members.Count == 0) { + foreach (var model in models) + { + if (model.Refusal != null || model.IsIgnored || model.Members.Count == 0) + { continue; } - if (!ServesAnyMember(model)) { + if (!ServesAnyMember(model)) + { continue; } @@ -153,8 +197,9 @@ private static IReadOnlyList Usable(ImmutableArrayWhether any member has an interceptor that can serve it. private static bool ServesAnyMember(InterceptorModel model) => model.Members.Any(member => - !member.Excluded && - model.Interceptors.Any(interceptor => interceptor.CanServe(member.Kind))); + !member.Excluded + && model.Interceptors.Any(interceptor => interceptor.CanServe(member.Kind)) + ); /// /// Why an interception was refused, or is quietly absent from members it was applied to. @@ -163,32 +208,45 @@ private static bool ServesAnyMember(InterceptorModel model) => /// Reported apart from emission so the locations carry their syntax tree, which is what lets /// one of these be silenced where it is written rather than only across the whole project. /// - protected override void ReportDiagnostics(SourceProductionContext context, - (ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, - ImmutableArray Right) data, + protected override void ReportDiagnostics( + SourceProductionContext context, + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) data, SyntaxTreeLookup lookup, - FileLogger logger) { - - if (data.Left.Length == 0 || data.Right.Length == 0) { + FileLogger logger + ) + { + if (data.Left.Length == 0 || data.Right.Length == 0) + { return; } - foreach (var model in data.Right) { + foreach (var model in data.Right) + { context.CancellationToken.ThrowIfCancellationRequested(); - if (model.Refusal != null) { + if (model.Refusal != null) + { logger.Error($"Cannot intercept: {model.Refusal.Message}"); context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.CannotIntercept, model.Location?.ToLocationOrNone(lookup) ?? Location.None, - model.Refusal.Message)); + model.Refusal.Message + ) + ); continue; } - if (model.IsIgnored || model.Members.Count == 0) { + if (model.IsIgnored || model.Members.Count == 0) + { continue; } @@ -217,46 +275,59 @@ protected override void ReportDiagnostics(SourceProductionContext context, /// private static void ReportUnappliedInterceptions( SourceProductionContext context, - ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> entryPoints, + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> entryPoints, IReadOnlyList usable, SyntaxTreeLookup lookup, - FileLogger logger) { - - if (usable.Count == 0) { + FileLogger logger + ) + { + if (usable.Count == 0) + { return; } var (entryPointList, _) = EntryModelUtil.ConsolidateEntryPointModels(entryPoints); var targets = EntryModelUtil.RegistrationTargets(entryPointList).ToArray(); - if (targets.Length == 0) { + if (targets.Length == 0) + { return; } var applied = new HashSet(); - foreach (var entryPointModel in targets) { - foreach (var model in ForModule(usable, entryPointModel)) { + foreach (var entryPointModel in targets) + { + foreach (var model in ForModule(usable, entryPointModel)) + { applied.Add(model.ImplementationType); } } - foreach (var model in usable) { + foreach (var model in usable) + { context.CancellationToken.ThrowIfCancellationRequested(); - if (applied.Contains(model.ImplementationType)) { + if (applied.Contains(model.ImplementationType)) + { continue; } logger.Error( - $"No module applies the interception on '{model.ImplementationType.Name}', " + - "so its interceptors never run."); + $"No module applies the interception on '{model.ImplementationType.Name}', " + + "so its interceptors never run." + ); context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.InterceptionAppliedByNoModule, model.Location?.ToLocationOrNone(lookup) ?? Location.None, - model.ImplementationType.Name)); + model.ImplementationType.Name + ) + ); } } @@ -273,19 +344,30 @@ private static void ReportUnappliedInterceptions( /// than one per member. /// private static void ReportUnservedMembers( - SourceProductionContext context, InterceptorModel model, SyntaxTreeLookup lookup, - FileLogger logger) { - - foreach (var interceptor in model.Interceptors) { - foreach (var kind in new[] { - InterceptorKind.Sync, InterceptorKind.Async, InterceptorKind.Stream - }) { - - if (interceptor.CanServe(kind)) { + SourceProductionContext context, + InterceptorModel model, + SyntaxTreeLookup lookup, + FileLogger logger + ) + { + foreach (var interceptor in model.Interceptors) + { + foreach ( + var kind in new[] + { + InterceptorKind.Sync, + InterceptorKind.Async, + InterceptorKind.Stream, + } + ) + { + if (interceptor.CanServe(kind)) + { continue; } - var unserved = model.Members + var unserved = model + .Members // An excluded member was never meant to be served, so an interceptor not // covering it is not the omission DM0015 is about. .Where(member => !member.Excluded && member.Kind == kind) @@ -294,13 +376,15 @@ private static void ReportUnservedMembers( .OrderBy(name => name, StringComparer.Ordinal) .ToArray(); - if (unserved.Length == 0) { + if (unserved.Length == 0) + { continue; } logger.Error( - $"'{interceptor.Type.Name}' does not implement {InterfaceFor(kind)}, so it is not " + - $"applied to {string.Join(", ", unserved)} on '{model.ServiceType.Name}'."); + $"'{interceptor.Type.Name}' does not implement {InterfaceFor(kind)}, so it is not " + + $"applied to {string.Join(", ", unserved)} on '{model.ServiceType.Name}'." + ); context.ReportDiagnostic( Diagnostic.Create( @@ -310,22 +394,26 @@ private static void ReportUnservedMembers( InterfaceFor(kind), DescriptionFor(kind), model.ServiceType.Name, - string.Join(", ", unserved))); + string.Join(", ", unserved) + ) + ); } } } private static string InterfaceFor(InterceptorKind kind) => - kind switch { + kind switch + { InterceptorKind.Async => "IAsyncInterceptor", InterceptorKind.Stream => "IAsyncEnumerableInterceptor", - _ => "IInterceptor" + _ => "IInterceptor", }; private static string DescriptionFor(InterceptorKind kind) => - kind switch { + kind switch + { InterceptorKind.Async => "the members returning a task", InterceptorKind.Stream => "the members returning an async stream", - _ => "the members returning a value directly" + _ => "the members returning a value directly", }; } diff --git a/src/DependencyModules.SourceGenerator/ReferencedModuleLookup.cs b/src/DependencyModules.SourceGenerator/ReferencedModuleLookup.cs index 96b27a6..db0e54f 100644 --- a/src/DependencyModules.SourceGenerator/ReferencedModuleLookup.cs +++ b/src/DependencyModules.SourceGenerator/ReferencedModuleLookup.cs @@ -23,14 +23,17 @@ namespace DependencyModules.SourceGenerator; /// point of DM0016. /// /// -internal sealed class ReferencedModuleLookup { - private const string ProviderInterface = "DependencyModules.Runtime.Interfaces.IDependencyModuleProvider"; +internal sealed class ReferencedModuleLookup +{ + private const string ProviderInterface = + "DependencyModules.Runtime.Interfaces.IDependencyModuleProvider"; private readonly Compilation? _compilation; private readonly INamedTypeSymbol? _providerInterface; private Dictionary? _byName; - public ReferencedModuleLookup(Compilation? compilation) { + public ReferencedModuleLookup(Compilation? compilation) + { _compilation = compilation; _providerInterface = compilation?.GetTypeByMetadataName(ProviderInterface); } @@ -39,20 +42,26 @@ public ReferencedModuleLookup(Compilation? compilation) { /// The namespace of a module whose attribute is and which one of /// brings into scope, or null. /// - public string? FindImported(string name, IEnumerable candidateNamespaces) { - if (_compilation == null || _providerInterface == null) { + public string? FindImported(string name, IEnumerable candidateNamespaces) + { + if (_compilation == null || _providerInterface == null) + { return null; } - foreach (var candidate in candidateNamespaces) { - if (string.IsNullOrEmpty(candidate)) { + foreach (var candidate in candidateNamespaces) + { + if (string.IsNullOrEmpty(candidate)) + { continue; } - foreach (var typeName in AttributeNames(name)) { + foreach (var typeName in AttributeNames(name)) + { var symbol = _compilation.GetTypeByMetadataName($"{candidate}.{typeName}"); - if (symbol != null && IsModuleAttribute(symbol)) { + if (symbol != null && IsModuleAttribute(symbol)) + { return candidate; } } @@ -68,15 +77,19 @@ public ReferencedModuleLookup(Compilation? compilation) { /// Walks the referenced assemblies. Reached only for a usage that resolved to nothing, so the /// compilation it walks is one that is already failing to build. /// - public string? FindAnywhere(string name) { - if (_compilation == null || _providerInterface == null) { + public string? FindAnywhere(string name) + { + if (_compilation == null || _providerInterface == null) + { return null; } _byName ??= BuildIndex(); - foreach (var typeName in AttributeNames(name)) { - if (_byName.TryGetValue(typeName, out var moduleNamespace)) { + foreach (var typeName in AttributeNames(name)) + { + if (_byName.TryGetValue(typeName, out var moduleNamespace)) + { return moduleNamespace; } } @@ -85,10 +98,12 @@ public ReferencedModuleLookup(Compilation? compilation) { } /// Written as [assembly: Foo] or [assembly: FooAttribute]. - private static IEnumerable AttributeNames(string name) { + private static IEnumerable AttributeNames(string name) + { yield return name; - if (!name.EndsWith("Attribute", System.StringComparison.Ordinal)) { + if (!name.EndsWith("Attribute", System.StringComparison.Ordinal)) + { yield return name + "Attribute"; } } @@ -96,11 +111,14 @@ private static IEnumerable AttributeNames(string name) { private bool IsModuleAttribute(INamedTypeSymbol symbol) => symbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, _providerInterface)); - private Dictionary BuildIndex() { + private Dictionary BuildIndex() + { var index = new Dictionary(System.StringComparer.Ordinal); - foreach (var reference in _compilation!.References) { - if (_compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assembly) { + foreach (var reference in _compilation!.References) + { + if (_compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assembly) + { continue; } @@ -110,18 +128,23 @@ private Dictionary BuildIndex() { return index; } - private void Walk(INamespaceSymbol namespaceSymbol, Dictionary index) { - foreach (var type in namespaceSymbol.GetTypeMembers()) { - if (type.DeclaredAccessibility == Accessibility.Public && IsModuleAttribute(type)) { + private void Walk(INamespaceSymbol namespaceSymbol, Dictionary index) + { + foreach (var type in namespaceSymbol.GetTypeMembers()) + { + if (type.DeclaredAccessibility == Accessibility.Public && IsModuleAttribute(type)) + { // First wins. Two packages can ship a same-named module, and naming one of them is // more useful than naming neither. - if (!index.ContainsKey(type.Name)) { + if (!index.ContainsKey(type.Name)) + { index.Add(type.Name, namespaceSymbol.ToDisplayString()); } } } - foreach (var nested in namespaceSymbol.GetNamespaceMembers()) { + foreach (var nested in namespaceSymbol.GetNamespaceMembers()) + { Walk(nested, index); } } diff --git a/src/DependencyModules.SourceGenerator/ServiceSourceGenerator.cs b/src/DependencyModules.SourceGenerator/ServiceSourceGenerator.cs index 58553e6..81c145a 100644 --- a/src/DependencyModules.SourceGenerator/ServiceSourceGenerator.cs +++ b/src/DependencyModules.SourceGenerator/ServiceSourceGenerator.cs @@ -10,29 +10,47 @@ namespace DependencyModules.SourceGenerator; -public class ServiceSourceGenerator : BaseAttributeSourceGenerator { - private static ITypeDefinition[] _skipTypes = new [] { TypeDefinition.Get(typeof(INotifyPropertyChanged))}; - private static readonly ITypeDefinition[] _attributeTypes = { - KnownTypes.DependencyModules.Attributes.TransientServiceAttribute, - KnownTypes.DependencyModules.Attributes.ScopedServiceAttribute, +public class ServiceSourceGenerator : BaseAttributeSourceGenerator +{ + private static ITypeDefinition[] _skipTypes = new[] + { + TypeDefinition.Get(typeof(INotifyPropertyChanged)), + }; + private static readonly ITypeDefinition[] _attributeTypes = + { + KnownTypes.DependencyModules.Attributes.TransientServiceAttribute, + KnownTypes.DependencyModules.Attributes.ScopedServiceAttribute, KnownTypes.DependencyModules.Attributes.SingletonServiceAttribute, KnownTypes.DependencyModules.Attributes.CrossWireServiceAttribute, - KnownTypes.Microsoft.TextJson.JsonSourceGenerationOptionsAttribute + KnownTypes.Microsoft.TextJson.JsonSourceGenerationOptionsAttribute, }; - private readonly IEqualityComparer _serviceEqualityComparer = new ServiceModelComparer(); + private readonly IEqualityComparer _serviceEqualityComparer = + new ServiceModelComparer(); - protected override IEnumerable AttributeTypes() { + protected override IEnumerable AttributeTypes() + { return _attributeTypes; } - protected override void GenerateSourceOutput(SourceProductionContext context, - (ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, ImmutableArray Right) inputData, - FileLogger logger) { - if (inputData.Left.Length == 0 || inputData.Right.Length == 0) { + protected override void GenerateSourceOutput( + SourceProductionContext context, + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) inputData, + FileLogger logger + ) + { + if (inputData.Left.Length == 0 || inputData.Right.Length == 0) + { logger.Info( - $"Nothing to generate: {inputData.Left.Length} module(s) and " + - $"{inputData.Right.Length} service(s) were discovered."); + $"Nothing to generate: {inputData.Left.Length} module(s) and " + + $"{inputData.Right.Length} service(s) were discovered." + ); return; } @@ -43,38 +61,55 @@ protected override void GenerateSourceOutput(SourceProductionContext context, // generic that cannot be cross-wired, is dropped here and explained there. var serviceModels = Registerable(inputData.Right); - if (serviceModels.Length == 0) { + if (serviceModels.Length == 0) + { return; } - var (entryPointList, configurationModel) = - EntryModelUtil.ConsolidateEntryPointModels(inputData.Left); + var (entryPointList, configurationModel) = EntryModelUtil.ConsolidateEntryPointModels( + inputData.Left + ); - foreach (var entryPointModel in EntryModelUtil.RegistrationTargets(entryPointList)) { + foreach (var entryPointModel in EntryModelUtil.RegistrationTargets(entryPointList)) + { context.CancellationToken.ThrowIfCancellationRequested(); - GenerateSourceOutput(context, entryPointModel, configurationModel, serviceModels, logger); + GenerateSourceOutput( + context, + entryPointModel, + configurationModel, + serviceModels, + logger + ); } } - protected void GenerateSourceOutput(SourceProductionContext context, + protected void GenerateSourceOutput( + SourceProductionContext context, ModuleEntryPointModel entryPointModel, DependencyModuleConfigurationModel configurationModel, - ImmutableArray serviceModels, FileLogger logger) { - + ImmutableArray serviceModels, + FileLogger logger + ) + { // don't generate empty dependency registrations - if (serviceModels.Length == 0) { + if (serviceModels.Length == 0) + { return; } - - entryPointModel = EntryModelUtil.EnsureNamespace(entryPointModel,configurationModel); - + + entryPointModel = EntryModelUtil.EnsureNamespace(entryPointModel, configurationModel); + var writer = new DependencyFileWriter(logger); var output = writer.Write(entryPointModel, configurationModel, serviceModels, "Module"); - + context.AddSource( entryPointModel.EntryPointType.GetFileNameHint( - configurationModel.RootNamespace,"Dependencies"), output); + configurationModel.RootNamespace, + "Dependencies" + ), + output + ); } /// @@ -86,38 +121,54 @@ protected void GenerateSourceOutput(SourceProductionContext context, /// what configuration was in effect — none of which is visible from the generated output alone. /// private static void LogDiscovery( - (ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, - ImmutableArray Right) inputData, - FileLogger logger) { - + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) inputData, + FileLogger logger + ) + { var configuration = inputData.Left.First().Right; logger.Info( - "Configuration: " + - $"RootNamespace='{configuration.RootNamespace}', " + - $"ProjectDir='{configuration.ProjectDir}', " + - $"RegistrationType={configuration.RegistrationType}, " + - $"AutoGenerateModule={configuration.AutoGenerateEntry}, " + - $"GenerateFactories={configuration.GenerateFactories}, " + - $"ExcludeGeneratedCodeFromCoverage={configuration.ExcludeGeneratedCodeFromCoverage}"); + "Configuration: " + + $"RootNamespace='{configuration.RootNamespace}', " + + $"ProjectDir='{configuration.ProjectDir}', " + + $"RegistrationType={configuration.RegistrationType}, " + + $"AutoGenerateModule={configuration.AutoGenerateEntry}, " + + $"GenerateFactories={configuration.GenerateFactories}, " + + $"ExcludeGeneratedCodeFromCoverage={configuration.ExcludeGeneratedCodeFromCoverage}" + ); logger.Info($"Discovered {inputData.Left.Length} module(s):"); - foreach (var (entryPoint, _) in inputData.Left) { + foreach (var (entryPoint, _) in inputData.Left) + { logger.Info( - $" {entryPoint.EntryPointType.Namespace}.{entryPoint.EntryPointType.Name} " + - $"[{entryPoint.ModuleFeatures}] from '{entryPoint.FileLocation}'"); + $" {entryPoint.EntryPointType.Namespace}.{entryPoint.EntryPointType.Name} " + + $"[{entryPoint.ModuleFeatures}] from '{entryPoint.FileLocation}'" + ); } logger.Info($"Discovered {inputData.Right.Length} service(s):"); - foreach (var serviceModel in inputData.Right) { - foreach (var registration in serviceModel.Registrations) { + foreach (var serviceModel in inputData.Right) + { + foreach (var registration in serviceModel.Registrations) + { logger.Info( - $" {serviceModel.ImplementationType.Name} -> {registration.ServiceType.Name} " + - $"({registration.Lifestyle}" + - (registration.Key != null ? $", key={registration.Key}" : "") + - (registration.Realm != null ? $", realm={registration.Realm.Name}" : "") + - (registration.RegistrationType != null ? $", using={registration.RegistrationType}" : "") + - ")"); + $" {serviceModel.ImplementationType.Name} -> {registration.ServiceType.Name} " + + $"({registration.Lifestyle}" + + (registration.Key != null ? $", key={registration.Key}" : "") + + (registration.Realm != null ? $", realm={registration.Realm.Name}" : "") + + ( + registration.RegistrationType != null + ? $", using={registration.RegistrationType}" + : "" + ) + + ")" + ); } } } @@ -131,15 +182,21 @@ private static void LogDiscovery( /// build for an abstract or static type, and at compile time for a cross-wired generic. Each is /// explained by a diagnostic; this only decides what not to write. /// - private static ImmutableArray Registerable(ImmutableArray serviceModels) { - if (!serviceModels.Any(m => IsUnconstructable(m) || IsCrossWiredGeneric(m))) { + private static ImmutableArray Registerable( + ImmutableArray serviceModels + ) + { + if (!serviceModels.Any(m => IsUnconstructable(m) || IsCrossWiredGeneric(m))) + { return serviceModels; } var builder = ImmutableArray.CreateBuilder(serviceModels.Length); - foreach (var serviceModel in serviceModels) { - if (!IsUnconstructable(serviceModel) && !IsCrossWiredGeneric(serviceModel)) { + foreach (var serviceModel in serviceModels) + { + if (!IsUnconstructable(serviceModel) && !IsCrossWiredGeneric(serviceModel)) + { builder.Add(serviceModel); } } @@ -172,8 +229,8 @@ private static string UnconstructableReason(ServiceModel serviceModel) => /// A cross-wired registration on an implementation that is itself generic. /// private static bool IsCrossWiredGeneric(ServiceModel serviceModel) => - serviceModel.ImplementationType is GenericTypeDefinition { TypeArguments.Count: > 0 } && - serviceModel.Registrations.Any(registration => registration.CrossWire == true); + serviceModel.ImplementationType is GenericTypeDefinition { TypeArguments.Count: > 0 } + && serviceModel.Registrations.Any(registration => registration.CrossWire == true); /// /// Everything this generator has to say about the services it found. @@ -184,20 +241,30 @@ private static bool IsCrossWiredGeneric(ServiceModel serviceModel) => /// project. The two halves share their predicates: what drops is /// exactly what the first two loops here explain. /// - protected override void ReportDiagnostics(SourceProductionContext context, - (ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> Left, - ImmutableArray Right) data, + protected override void ReportDiagnostics( + SourceProductionContext context, + ( + ImmutableArray<( + ModuleEntryPointModel Left, + DependencyModuleConfigurationModel Right + )> Left, + ImmutableArray Right + ) data, SyntaxTreeLookup lookup, - FileLogger logger) { - - if (data.Left.Length == 0 || data.Right.Length == 0) { + FileLogger logger + ) + { + if (data.Left.Length == 0 || data.Right.Length == 0) + { return; } - foreach (var serviceModel in data.Right) { + foreach (var serviceModel in data.Right) + { context.CancellationToken.ThrowIfCancellationRequested(); - if (!IsUnconstructable(serviceModel)) { + if (!IsUnconstructable(serviceModel)) + { continue; } @@ -211,13 +278,17 @@ protected override void ReportDiagnostics(SourceProductionContext context, DependencyModuleDiagnostics.ServiceCannotBeConstructed, serviceModel.Location?.ToLocationOrNone(lookup) ?? Location.None, typeName, - reason)); + reason + ) + ); } - foreach (var serviceModel in data.Right) { + foreach (var serviceModel in data.Right) + { context.CancellationToken.ThrowIfCancellationRequested(); - if (!IsCrossWiredGeneric(serviceModel)) { + if (!IsCrossWiredGeneric(serviceModel)) + { continue; } @@ -229,12 +300,15 @@ protected override void ReportDiagnostics(SourceProductionContext context, Diagnostic.Create( DependencyModuleDiagnostics.CrossWireCannotBeGeneric, serviceModel.Location?.ToLocationOrNone(lookup) ?? Location.None, - typeName)); + typeName + ) + ); } var registerable = Registerable(data.Right); - if (registerable.Length > 0) { + if (registerable.Length > 0) + { ReportEnvironmentConditions(context, registerable, lookup, logger); } } @@ -250,22 +324,30 @@ protected override void ReportDiagnostics(SourceProductionContext context, /// meant to say. /// private static void ReportEnvironmentConditions( - SourceProductionContext context, ImmutableArray serviceModels, - SyntaxTreeLookup lookup, FileLogger logger) { - - foreach (var serviceModel in serviceModels) { - if (serviceModel.Conditions is not { Count: > 0 } conditions) { + SourceProductionContext context, + ImmutableArray serviceModels, + SyntaxTreeLookup lookup, + FileLogger logger + ) + { + foreach (var serviceModel in serviceModels) + { + if (serviceModel.Conditions is not { Count: > 0 } conditions) + { continue; } var typeName = serviceModel.ImplementationType.Name; - foreach (var condition in conditions) { - if (!EnvironmentConditionUtility.IsEmpty(condition)) { + foreach (var condition in conditions) + { + if (!EnvironmentConditionUtility.IsEmpty(condition)) + { continue; } - var kind = condition.Kind == EnvironmentConditionKind.Name ? "environment name" : "key"; + var kind = + condition.Kind == EnvironmentConditionKind.Name ? "environment name" : "key"; logger.Error($"'{typeName}' has an environment condition that names no {kind}."); @@ -274,7 +356,9 @@ private static void ReportEnvironmentConditions( DependencyModuleDiagnostics.EmptyEnvironmentCondition, serviceModel.Location?.ToLocationOrNone(lookup) ?? Location.None, typeName, - kind)); + kind + ) + ); } var described = conditions @@ -282,7 +366,8 @@ private static void ReportEnvironmentConditions( .Select(EnvironmentConditionUtility.Describe) .ToArray(); - if (described.Length == 0) { + if (described.Length == 0) + { continue; } @@ -294,25 +379,34 @@ private static void ReportEnvironmentConditions( Diagnostic.Create( DependencyModuleDiagnostics.RegisteredConditionally, serviceModel.Location?.ToLocationOrNone(lookup) ?? Location.None, - summary)); + summary + ) + ); } } private static bool IsUnconstructable(ServiceModel serviceModel) => // A factory supplies the instance, so the declaring type never has to be constructed. - serviceModel.Factory == null && - (serviceModel.Features.HasFlag(RegistrationFeature.AbstractImplementation) || - serviceModel.Features.HasFlag(RegistrationFeature.StaticImplementation)); + serviceModel.Factory == null + && ( + serviceModel.Features.HasFlag(RegistrationFeature.AbstractImplementation) + || serviceModel.Features.HasFlag(RegistrationFeature.StaticImplementation) + ); protected override ServiceModel IgnoredModel => ServiceModel.Ignore; - protected override IEqualityComparer GetComparer() { + protected override IEqualityComparer GetComparer() + { return _serviceEqualityComparer; } - protected override ServiceModel GenerateAttributeModel(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) { + protected override ServiceModel GenerateAttributeModel( + GeneratorAttributeSyntaxContext context, + CancellationToken cancellationToken + ) + { var serviceModel = ServiceModelUtility.GetServiceModel(context, cancellationToken); - + return serviceModel ?? ServiceModel.Ignore; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator/SourceGenerator.cs b/src/DependencyModules.SourceGenerator/SourceGenerator.cs index 230a717..25b7731 100644 --- a/src/DependencyModules.SourceGenerator/SourceGenerator.cs +++ b/src/DependencyModules.SourceGenerator/SourceGenerator.cs @@ -10,9 +10,10 @@ namespace DependencyModules.SourceGenerator; /// module partial and a generator built on the same base class does not. /// [Generator] -public class SourceGenerator : BaseSourceGenerator { - - protected override IEnumerable AttributeSourceGenerators() { +public class SourceGenerator : BaseSourceGenerator +{ + protected override IEnumerable AttributeSourceGenerators() + { yield return new ServiceSourceGenerator(); yield return new InterceptorSourceGenerator(); yield return new global::DependencyModules.Conventions.ConventionGenerator(); @@ -23,9 +24,13 @@ protected override IEnumerable AttributeSource /// default, so that a third party building on it contributes to these modules rather than /// declaring every one of them a second time; this is the generator that claim belongs to. /// - protected override void SetupRootGenerator(IncrementalGeneratorInitializationContext context, - IncrementalValueProvider> valuesProvider) { - + protected override void SetupRootGenerator( + IncrementalGeneratorInitializationContext context, + IncrementalValueProvider< + ImmutableArray<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> + > valuesProvider + ) + { DependencyModuleWriter.Register(context, valuesProvider, generateAttribute: true); // DM0021. Registered here for the same reason DM0016 is: a framework generator loaded @@ -35,8 +40,10 @@ protected override void SetupRootGenerator(IncrementalGeneratorInitializationCon // DM0016. Registered here rather than on the base class so that a framework generator loaded // alongside this one does not report the same usage twice. context.RegisterSourceOutput( - valuesProvider.Combine(AssemblyModuleAttributeDiagnostics.Collect(context)) + valuesProvider + .Combine(AssemblyModuleAttributeDiagnostics.Collect(context)) .Combine(context.CompilationProvider), - AssemblyModuleAttributeDiagnostics.Report); + AssemblyModuleAttributeDiagnostics.Report + ); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.SourceGenerator/TestAttributeDiagnostics.cs b/src/DependencyModules.SourceGenerator/TestAttributeDiagnostics.cs index eeae4d6..ca257b9 100644 --- a/src/DependencyModules.SourceGenerator/TestAttributeDiagnostics.cs +++ b/src/DependencyModules.SourceGenerator/TestAttributeDiagnostics.cs @@ -26,14 +26,21 @@ namespace DependencyModules.SourceGenerator; /// for — reporting that would be reporting the feature. /// /// -internal static class TestAttributeDiagnostics { - - internal static void Setup(IncrementalGeneratorInitializationContext context) { - var methods = context.SyntaxProvider - .CreateSyntaxProvider( +internal static class TestAttributeDiagnostics +{ + internal static void Setup(IncrementalGeneratorInitializationContext context) + { + var methods = context + .SyntaxProvider.CreateSyntaxProvider( static (node, _) => - node is MethodDeclarationSyntax { AttributeLists.Count: > 0, ParameterList.Parameters.Count: > 0 }, - Read) + node + is MethodDeclarationSyntax + { + AttributeLists.Count: > 0, + ParameterList.Parameters.Count: > 0 + }, + Read + ) .Where(static finding => finding != null) .Collect(); @@ -53,35 +60,46 @@ internal static void Setup(IncrementalGeneratorInitializationContext context) { /// private record Finding(string MethodName, string ServiceName, LocationModel Location); - private static Finding? Read(GeneratorSyntaxContext syntaxContext, System.Threading.CancellationToken cancellationToken) { + private static Finding? Read( + GeneratorSyntaxContext syntaxContext, + System.Threading.CancellationToken cancellationToken + ) + { var context = (SyntaxTransformContext)syntaxContext; var method = (MethodDeclarationSyntax)context.Node; var exported = ExportedServices(method, context, cancellationToken); - if (exported.Count == 0) { + if (exported.Count == 0) + { return null; } - foreach (var parameter in method.ParameterList.Parameters) { + foreach (var parameter in method.ParameterList.Parameters) + { cancellationToken.ThrowIfCancellationRequested(); - if (!CarriesMock(parameter, context, cancellationToken)) { + if (!CarriesMock(parameter, context, cancellationToken)) + { continue; } var parameterType = parameter.Type?.GetTypeDefinition(context); - if (parameterType == null) { + if (parameterType == null) + { continue; } - foreach (var service in exported) { - if (service.Equals(parameterType)) { + foreach (var service in exported) + { + if (service.Equals(parameterType)) + { return new Finding( method.Identifier.ToString(), service.Name, - LocationModel.From(parameter)); + LocationModel.From(parameter) + ); } } } @@ -95,26 +113,37 @@ private record Finding(string MethodName, string ServiceName, LocationModel Loca private static List ExportedServices( MethodDeclarationSyntax method, SyntaxTransformContext context, - System.Threading.CancellationToken cancellationToken) { - + System.Threading.CancellationToken cancellationToken + ) + { var services = new List(); - foreach (var attributeList in method.AttributeLists) { - foreach (var attribute in attributeList.Attributes) { - if (!AttributeTypeMatcher.Matches( + foreach (var attributeList in method.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + if ( + !AttributeTypeMatcher.Matches( context.SemanticModel, attribute, KnownTypes.DependencyModules.Testing.TestExportAttribute, - cancellationToken)) { + cancellationToken + ) + ) + { continue; } // The service is the first positional argument: [TestExport(typeof(IFoo), ...)]. - var first = attribute.ArgumentList?.Arguments.FirstOrDefault( - argument => argument.NameEquals == null); - - if (first?.Expression is TypeOfExpressionSyntax typeOf && - typeOf.Type.GetTypeDefinition(context) is { } service) { + var first = attribute.ArgumentList?.Arguments.FirstOrDefault(argument => + argument.NameEquals == null + ); + + if ( + first?.Expression is TypeOfExpressionSyntax typeOf + && typeOf.Type.GetTypeDefinition(context) is { } service + ) + { services.Add(service); } } @@ -126,15 +155,22 @@ private static List ExportedServices( private static bool CarriesMock( ParameterSyntax parameter, SyntaxTransformContext context, - System.Threading.CancellationToken cancellationToken) { - - foreach (var attributeList in parameter.AttributeLists) { - foreach (var attribute in attributeList.Attributes) { - if (AttributeTypeMatcher.Matches( + System.Threading.CancellationToken cancellationToken + ) + { + foreach (var attributeList in parameter.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + if ( + AttributeTypeMatcher.Matches( context.SemanticModel, attribute, KnownTypes.DependencyModules.Testing.MockAttribute, - cancellationToken)) { + cancellationToken + ) + ) + { return true; } } @@ -145,12 +181,18 @@ private static bool CarriesMock( private static void Report( SourceProductionContext context, - (System.Collections.Immutable.ImmutableArray Findings, Compilation Compilation) data) { - + ( + System.Collections.Immutable.ImmutableArray Findings, + Compilation Compilation + ) data + ) + { var lookup = new SyntaxTreeLookup(data.Compilation); - foreach (var finding in data.Findings) { - if (finding == null) { + foreach (var finding in data.Findings) + { + if (finding == null) + { continue; } @@ -161,7 +203,9 @@ private static void Report( DependencyModuleDiagnostics.MockAndTestExportOnOneMethod, finding.Location.ToLocationOrNone(lookup), finding.MethodName, - finding.ServiceName)); + finding.ServiceName + ) + ); } } } diff --git a/src/DependencyModules.Testing/Attributes/InjectValuesAttribute.cs b/src/DependencyModules.Testing/Attributes/InjectValuesAttribute.cs index a6a4a2b..ed0dd82 100644 --- a/src/DependencyModules.Testing/Attributes/InjectValuesAttribute.cs +++ b/src/DependencyModules.Testing/Attributes/InjectValuesAttribute.cs @@ -28,8 +28,10 @@ namespace DependencyModules.Testing.Attributes; /// [AttributeUsage(AttributeTargets.Parameter)] public class InjectValuesAttribute(params object[] value) - : Attribute, IInjectValueAttribute, ISharedTestRegistration { - + : Attribute, + IInjectValueAttribute, + ISharedTestRegistration +{ /// /// Provides the specified values for a method parameter during dependency /// injection and test execution. This method is part of the @@ -48,7 +50,8 @@ public class InjectValuesAttribute(params object[] value) /// An array of objects representing the values to be injected into the specified /// method parameter. /// - public object[] ProvideValue(IServiceProvider serviceProvider, ParameterInfo parameter) { + public object[] ProvideValue(IServiceProvider serviceProvider, ParameterInfo parameter) + { return value; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/IInjectValueAttribute.cs b/src/DependencyModules.Testing/Attributes/Interfaces/IInjectValueAttribute.cs index ae4d711..babf97f 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/IInjectValueAttribute.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/IInjectValueAttribute.cs @@ -7,7 +7,8 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// This attribute is only applicable when the type is not registered with DI /// rather it's instantiated using ActivatorUtilities.CreateInstance /// -public interface IInjectValueAttribute { +public interface IInjectValueAttribute +{ /// /// Provides predefined values to method parameters during runtime. This method /// is part of the dependency injection process and retrieves values based on @@ -26,4 +27,4 @@ public interface IInjectValueAttribute { /// respective method parameters. /// object[] ProvideValue(IServiceProvider serviceProvider, ParameterInfo parameter); -} \ No newline at end of file +} diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/IMockSupportAttribute.cs b/src/DependencyModules.Testing/Attributes/Interfaces/IMockSupportAttribute.cs index 6595acc..c726c86 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/IMockSupportAttribute.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/IMockSupportAttribute.cs @@ -8,7 +8,8 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// for specified types. It is typically used in conjunction with dependency injection /// to enable mocking capabilities in testing frameworks. /// -public interface IMockSupportAttribute { +public interface IMockSupportAttribute +{ /// /// Provides a mock object instance for the specified type. /// @@ -36,4 +37,4 @@ public interface IMockSupportAttribute { /// The test the container is being built for. /// The service a [Mock] parameter is about to register. bool RegistersService(ITestMethodContext testMethod, Type serviceType) => false; -} \ No newline at end of file +} diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/IModuleTestAttribute.cs b/src/DependencyModules.Testing/Attributes/Interfaces/IModuleTestAttribute.cs index 3a82b0b..852a3f4 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/IModuleTestAttribute.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/IModuleTestAttribute.cs @@ -12,12 +12,10 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// Implemented by the integrations, not by test authors. A test names its modules through the /// [ModuleTest] attribute of whichever framework it is written against. /// -public interface IModuleTestAttribute { - +public interface IModuleTestAttribute +{ /// /// The module types to load, in declaration order. Empty when a test names none. /// - Type[] ModuleTypes { - get; - } + Type[] ModuleTypes { get; } } diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/IOrderedAttribute.cs b/src/DependencyModules.Testing/Attributes/Interfaces/IOrderedAttribute.cs index 260d3bd..f16dd9c 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/IOrderedAttribute.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/IOrderedAttribute.cs @@ -10,7 +10,8 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// their precedence. This can be used in frameworks, test cases, or other /// systems requiring deterministic or prioritized execution flows. /// -public interface IOrderedAttribute { +public interface IOrderedAttribute +{ /// /// Represents the execution or processing order assigned to an object or component. /// This property is used to specify the precedence or priority of the object @@ -20,4 +21,4 @@ public interface IOrderedAttribute { /// The default value of the property is 10 if not explicitly overridden by the implementer. /// int Order => 10; -} \ No newline at end of file +} diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/IServiceProviderBuilderAttribute.cs b/src/DependencyModules.Testing/Attributes/Interfaces/IServiceProviderBuilderAttribute.cs index a2bdd4d..a4913fd 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/IServiceProviderBuilderAttribute.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/IServiceProviderBuilderAttribute.cs @@ -15,13 +15,16 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// it would not otherwise get, such as scope validation. It runs last, after every other hook has /// contributed, so it is also the final chance to inspect or amend the collection. /// -public interface IServiceProviderBuilderAttribute { - +public interface IServiceProviderBuilderAttribute +{ /// /// Builds the container for the test. /// /// The test the container is being built for. /// The fully populated collection. /// The container the test resolves its parameters and services from. - IServiceProvider BuildServiceProvider(ITestMethodContext testMethod, IServiceCollection serviceCollection); + IServiceProvider BuildServiceProvider( + ITestMethodContext testMethod, + IServiceCollection serviceCollection + ); } diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/ISharedTestRegistration.cs b/src/DependencyModules.Testing/Attributes/Interfaces/ISharedTestRegistration.cs index bb99c00..748e335 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/ISharedTestRegistration.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/ISharedTestRegistration.cs @@ -1,4 +1,5 @@ using System.Reflection; + namespace DependencyModules.Testing.Attributes.Interfaces; /// @@ -33,8 +34,8 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// attribute to work is a rule most tests will not get. /// /// -public interface ISharedTestRegistration { - +public interface ISharedTestRegistration +{ /// /// Whether what this attribute registered is pinned. /// diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/ITestContainerSource.cs b/src/DependencyModules.Testing/Attributes/Interfaces/ITestContainerSource.cs index f2d7fcd..7cd3453 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/ITestContainerSource.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/ITestContainerSource.cs @@ -23,8 +23,8 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// request through a chain that was never assembled. /// /// -public interface ITestContainerSource { - +public interface ITestContainerSource +{ /// /// A container built from the test's composition, started, and owned by the runner. /// diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/ITestMethodContext.cs b/src/DependencyModules.Testing/Attributes/Interfaces/ITestMethodContext.cs index 11c0ff1..3b043ad 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/ITestMethodContext.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/ITestMethodContext.cs @@ -12,8 +12,8 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// dependency on it would bind every mocking package to a single test framework. This carries the part /// that is common; a framework integration supplies its own implementation over its own model. /// -public interface ITestMethodContext { - +public interface ITestMethodContext +{ /// /// The method under test. /// @@ -22,9 +22,7 @@ public interface ITestMethodContext { /// AttributeUtility hang off this, and it is the same instance the framework integration /// reads parameters from, so a hook sees exactly the signature the test will be invoked with. /// - MethodInfo Method { - get; - } + MethodInfo Method { get; } /// /// Every attribute in scope for the method, widest scope first: assembly, then declaring type, @@ -38,7 +36,5 @@ MethodInfo Method { /// "the most specific one wins" and so looks at the method first. This list is in the order things /// are applied, where the most specific runs last and therefore wins. /// - IReadOnlyList Attributes { - get; - } + IReadOnlyList Attributes { get; } } diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/ITestParameterValueProvider.cs b/src/DependencyModules.Testing/Attributes/Interfaces/ITestParameterValueProvider.cs index 3c2248a..4450b92 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/ITestParameterValueProvider.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/ITestParameterValueProvider.cs @@ -15,8 +15,8 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// runs before the container is built, so a parameter can change what the service under test is /// constructed with, not merely what the test itself ends up holding. /// -public interface ITestParameterValueProvider { - +public interface ITestParameterValueProvider +{ /// /// Adds whatever services are needed to supply this parameter. /// @@ -24,7 +24,10 @@ public interface ITestParameterValueProvider { /// The collection backing the test's container. /// The parameter being supplied. void SetupServiceCollection( - ITestMethodContext testMethod, IServiceCollection serviceCollection, ParameterInfo parameter); + ITestMethodContext testMethod, + IServiceCollection serviceCollection, + ParameterInfo parameter + ); /// /// Produces the value to pass for this parameter. @@ -37,5 +40,8 @@ void SetupServiceCollection( /// that the parameter is resolved from the container like any other. /// Task GetParameterValueAsync( - ITestMethodContext testMethod, IServiceProvider serviceProvider, ParameterInfo parameter); + ITestMethodContext testMethod, + IServiceProvider serviceProvider, + ParameterInfo parameter + ); } diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/ITestServiceSetupAttribute.cs b/src/DependencyModules.Testing/Attributes/Interfaces/ITestServiceSetupAttribute.cs index db2cdb0..6f34721 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/ITestServiceSetupAttribute.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/ITestServiceSetupAttribute.cs @@ -17,12 +17,15 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// Runs before the container is built, in widest-scope-first order. Registrations are last-one-wins, /// so an attribute on the method overrides the same service registered from the assembly. /// -public interface ITestServiceSetupAttribute { - +public interface ITestServiceSetupAttribute +{ /// /// Adds services for the given test. /// /// The test the container is being built for. /// The collection backing the test's container. - void SetupServiceCollection(ITestMethodContext testMethod, IServiceCollection serviceCollection); + void SetupServiceCollection( + ITestMethodContext testMethod, + IServiceCollection serviceCollection + ); } diff --git a/src/DependencyModules.Testing/Attributes/Interfaces/ITestStartupAttribute.cs b/src/DependencyModules.Testing/Attributes/Interfaces/ITestStartupAttribute.cs index ff2ef4b..a989db0 100644 --- a/src/DependencyModules.Testing/Attributes/Interfaces/ITestStartupAttribute.cs +++ b/src/DependencyModules.Testing/Attributes/Interfaces/ITestStartupAttribute.cs @@ -11,8 +11,8 @@ namespace DependencyModules.Testing.Attributes.Interfaces; /// /// Applies to a method, a class or an assembly, and is found by walking that chain. /// -public interface ITestStartupAttribute { - +public interface ITestStartupAttribute +{ /// /// Performs whatever asynchronous setup the test needs — seeding a store, opening a connection, /// priming a cache. diff --git a/src/DependencyModules.Testing/Attributes/MockAttribute.cs b/src/DependencyModules.Testing/Attributes/MockAttribute.cs index c3bff27..ed812f6 100644 --- a/src/DependencyModules.Testing/Attributes/MockAttribute.cs +++ b/src/DependencyModules.Testing/Attributes/MockAttribute.cs @@ -32,11 +32,9 @@ namespace DependencyModules.Testing.Attributes; /// the whole bar for declaring an attribute shared - not that isolating it would be unusual, but that /// it would have no coherent reading. /// -[AttributeUsage( - AttributeTargets.Parameter, - AllowMultiple = true)] -public class MockAttribute : Attribute, ITestParameterValueProvider, ISharedTestRegistration { - +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] +public class MockAttribute : Attribute, ITestParameterValueProvider, ISharedTestRegistration +{ /// /// Registers the double in place of the parameter's service. /// @@ -53,17 +51,25 @@ public class MockAttribute : Attribute, ITestParameterValueProvider, ISharedTest /// Thrown when a required mock library is not found, indicating that the type or assembly is not correctly attributed. /// public void SetupServiceCollection( - ITestMethodContext testMethod, IServiceCollection serviceCollection, ParameterInfo parameter) { + ITestMethodContext testMethod, + IServiceCollection serviceCollection, + ParameterInfo parameter + ) + { var mockAttribute = testMethod.Method.GetTestAttribute(); - if (mockAttribute == null) { - throw new Exception("Mock library not found, please ensure the Type or Assembly is attributed correctly."); + if (mockAttribute == null) + { + throw new Exception( + "Mock library not found, please ensure the Type or Assembly is attributed correctly." + ); } // The mock library owns this type for this test - a Moq test naming both Mock and // [Mock] IFoo wants one mock seen two ways, and registering a second one here would leave // the test configuring one while the container handed out another. - if (mockAttribute.RegistersService(testMethod, parameter.ParameterType)) { + if (mockAttribute.RegistersService(testMethod, parameter.ParameterType)) + { return; } @@ -74,10 +80,17 @@ public void SetupServiceCollection( // the keyed registration — the one the consumer actually injects — untouched, so the service // under test kept the real implementation while the test held a double it believed was wired // in. The arrangement ran, the double recorded nothing, and the assertion failed elsewhere. - if (key == null) { + if (key == null) + { serviceCollection.AddSingleton(parameter.ParameterType, _ => mockedValue); - } else { - serviceCollection.AddKeyedSingleton(parameter.ParameterType, key, (_, _) => mockedValue); + } + else + { + serviceCollection.AddKeyedSingleton( + parameter.ParameterType, + key, + (_, _) => mockedValue + ); } } @@ -98,11 +111,18 @@ public void SetupServiceCollection( /// or null if the parameter could not be resolved. /// public Task GetParameterValueAsync( - ITestMethodContext testMethod, IServiceProvider serviceProvider, ParameterInfo parameter) { + ITestMethodContext testMethod, + IServiceProvider serviceProvider, + ParameterInfo parameter + ) + { var key = ServiceKeyOf(parameter); - if (key != null && serviceProvider is IKeyedServiceProvider keyedServiceProvider) { - return Task.FromResult(keyedServiceProvider.GetKeyedService(parameter.ParameterType, key)); + if (key != null && serviceProvider is IKeyedServiceProvider keyedServiceProvider) + { + return Task.FromResult( + keyedServiceProvider.GetKeyedService(parameter.ParameterType, key) + ); } return Task.FromResult(serviceProvider.GetService(parameter.ParameterType)); diff --git a/src/DependencyModules.Testing/Attributes/TestExportAttribute.cs b/src/DependencyModules.Testing/Attributes/TestExportAttribute.cs index b745b27..4122e53 100644 --- a/src/DependencyModules.Testing/Attributes/TestExportAttribute.cs +++ b/src/DependencyModules.Testing/Attributes/TestExportAttribute.cs @@ -29,11 +29,11 @@ namespace DependencyModules.Testing.Attributes; /// /// [AttributeUsage( - AttributeTargets.Assembly | - AttributeTargets.Class | - AttributeTargets.Method, - AllowMultiple = true)] -public class TestExportAttribute : Attribute, ITestServiceSetupAttribute, ISharedTestRegistration { + AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method, + AllowMultiple = true +)] +public class TestExportAttribute : Attribute, ITestServiceSetupAttribute, ISharedTestRegistration +{ /// /// An attribute that configures and exports services to the dependency injection container /// for test scenarios. This supports customized service registrations with specific lifetimes @@ -49,7 +49,8 @@ public class TestExportAttribute : Attribute, ITestServiceSetupAttribute, IShare /// This attribute is not guaranteed to be thread-safe and should be used carefully when /// dealing with parallel or concurrent test execution. /// - public TestExportAttribute(Type service) { + public TestExportAttribute(Type service) + { Service = service; } @@ -57,27 +58,19 @@ public TestExportAttribute(Type service) { /// Gets the service type to be registered in the service collection. /// The specified type represents the service interface or base type for dependency injection. /// - public Type Service { - get; - } + public Type Service { get; } /// /// Gets or sets the implementation type to be registered for the associated service in the service collection. /// If no value is provided, the service type will be used as the implementation type by default. /// - public Type? Implementation { - get; - set; - } + public Type? Implementation { get; set; } /// /// Gets or sets the lifetime of the service in the dependency injection container. /// Determines whether the service is registered as singleton, scoped, or transient. /// - public ServiceLifetime Lifetime { - get; - set; - } = ServiceLifetime.Transient; + public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Transient; /// /// Whether this export is kept across every container the test builds, rather than registered @@ -106,10 +99,7 @@ public ServiceLifetime Lifetime { /// what was asked for. /// /// - public bool Shared { - get; - set; - } + public bool Shared { get; set; } /// /// The exported service, which is what pinning applies to when is set. @@ -120,7 +110,6 @@ public bool Shared { /// IReadOnlyList ISharedTestRegistration.SharedServices => [Service]; - /// /// Configures the service collection for a test method by adding services with specified lifetimes. /// This method enables dynamic service registration during test execution, supporting dependency injection setup. @@ -133,10 +122,15 @@ public bool Shared { /// The service collection to which services are added. This collection is used to configure /// the dependency injection container for the test's execution environment. /// - public void SetupServiceCollection(ITestMethodContext testMethod, IServiceCollection serviceCollection) { + public void SetupServiceCollection( + ITestMethodContext testMethod, + IServiceCollection serviceCollection + ) + { var implementation = Implementation ?? Service; - switch (Lifetime) { + switch (Lifetime) + { case ServiceLifetime.Singleton: serviceCollection.AddSingleton(Service, implementation); break; @@ -148,4 +142,4 @@ public void SetupServiceCollection(ITestMethodContext testMethod, IServiceCollec break; } } -} \ No newline at end of file +} diff --git a/src/DependencyModules.Testing/Impl/AttributeUtility.cs b/src/DependencyModules.Testing/Impl/AttributeUtility.cs index c2253ec..4411569 100644 --- a/src/DependencyModules.Testing/Impl/AttributeUtility.cs +++ b/src/DependencyModules.Testing/Impl/AttributeUtility.cs @@ -6,8 +6,8 @@ namespace DependencyModules.Testing.Impl; /// /// Provides utility methods for retrieving attributes from methods, parameters, classes, or assemblies. /// -public static class AttributeUtility { - +public static class AttributeUtility +{ /// /// Retrieves an attribute of the specified type from a method. /// Searches on the method, then its declaring type, and finally its assembly. @@ -15,12 +15,19 @@ public static class AttributeUtility { /// The type of the attribute to retrieve. /// The method from which to retrieve the attribute. /// The first matching attribute of the specified type, or null if no attribute is found. - public static T? GetTestAttribute(this MethodInfo methodInfo) where T : class { - var returnAttribute = methodInfo.GetOrderedCustomAttributes().FirstOrDefault(a => a is T) ?? - methodInfo.DeclaringType?.GetTypeInfo().GetOrderedCustomAttributes() - .FirstOrDefault(a => a is T) ?? - methodInfo.DeclaringType?.GetTypeInfo().Assembly.GetOrderedCustomAttributes() - .FirstOrDefault(a => a is T); + public static T? GetTestAttribute(this MethodInfo methodInfo) + where T : class + { + var returnAttribute = + methodInfo.GetOrderedCustomAttributes().FirstOrDefault(a => a is T) + ?? methodInfo + .DeclaringType?.GetTypeInfo() + .GetOrderedCustomAttributes() + .FirstOrDefault(a => a is T) + ?? methodInfo + .DeclaringType?.GetTypeInfo() + .Assembly.GetOrderedCustomAttributes() + .FirstOrDefault(a => a is T); return returnAttribute as T; } @@ -32,25 +39,32 @@ public static class AttributeUtility { /// The type of the attribute to retrieve. /// The parameter from which to retrieve the attribute. /// The first matching attribute of the specified type, or null if no attribute is found. - public static T? GetTestAttribute(this ParameterInfo parameterInfo) where T : class { + public static T? GetTestAttribute(this ParameterInfo parameterInfo) + where T : class + { var attribute = parameterInfo.GetOrderedCustomAttributes().FirstOrDefault(a => a is T); - if (attribute != null) { + if (attribute != null) + { return attribute as T; } var methodInfo = parameterInfo.Member; - var returnAttribute = methodInfo.GetOrderedCustomAttributes().FirstOrDefault(a => a is T) ?? - methodInfo.DeclaringType?.GetTypeInfo().GetOrderedCustomAttributes() - .FirstOrDefault(a => a is T) ?? - methodInfo.DeclaringType?.GetTypeInfo().Assembly.GetOrderedCustomAttributes() - .FirstOrDefault(a => a is T); + var returnAttribute = + methodInfo.GetOrderedCustomAttributes().FirstOrDefault(a => a is T) + ?? methodInfo + .DeclaringType?.GetTypeInfo() + .GetOrderedCustomAttributes() + .FirstOrDefault(a => a is T) + ?? methodInfo + .DeclaringType?.GetTypeInfo() + .Assembly.GetOrderedCustomAttributes() + .FirstOrDefault(a => a is T); return returnAttribute as T; } - /// /// Retrieves all attributes of the specified type from a method. /// Searches the method, its declaring type, and its assembly in order to accumulate matching attributes. @@ -58,13 +72,23 @@ public static class AttributeUtility { /// The type of the attributes to retrieve. /// The method from which to retrieve the attributes. /// An enumerable collection of attributes of the specified type. - public static IEnumerable GetTestAttributes(this MethodInfo methodInfo) where T : class { + public static IEnumerable GetTestAttributes(this MethodInfo methodInfo) + where T : class + { var returnList = new List(); - if (methodInfo.DeclaringType != null) { - returnList.AddRange(methodInfo.DeclaringType.GetTypeInfo().Assembly.GetOrderedCustomAttributes().OfType()); - - returnList.AddRange(methodInfo.DeclaringType.GetTypeInfo().GetOrderedCustomAttributes().OfType()); + if (methodInfo.DeclaringType != null) + { + returnList.AddRange( + methodInfo + .DeclaringType.GetTypeInfo() + .Assembly.GetOrderedCustomAttributes() + .OfType() + ); + + returnList.AddRange( + methodInfo.DeclaringType.GetTypeInfo().GetOrderedCustomAttributes().OfType() + ); } returnList.AddRange(methodInfo.GetOrderedCustomAttributes().OfType()); @@ -72,7 +96,6 @@ public static IEnumerable GetTestAttributes(this MethodInfo methodInfo) wh return returnList; } - /// /// Retrieves all attributes of the specified type from a parameterInfo. /// Searches on the method, its declaring type, and its assembly in order. @@ -80,15 +103,25 @@ public static IEnumerable GetTestAttributes(this MethodInfo methodInfo) wh /// The type of the attributes to retrieve. /// The parameter from which to retrieve the attributes. /// A collection of matching attributes of the specified type, or an empty collection if no attributes are found. - public static IEnumerable GetTestAttributes(this ParameterInfo parameterInfo) where T : class { + public static IEnumerable GetTestAttributes(this ParameterInfo parameterInfo) + where T : class + { var returnList = new List(); var methodInfo = parameterInfo.Member; - if (methodInfo.DeclaringType != null) { - returnList.AddRange(methodInfo.DeclaringType.GetTypeInfo().Assembly.GetOrderedCustomAttributes().OfType()); - - returnList.AddRange(methodInfo.DeclaringType.GetTypeInfo().GetOrderedCustomAttributes().OfType()); + if (methodInfo.DeclaringType != null) + { + returnList.AddRange( + methodInfo + .DeclaringType.GetTypeInfo() + .Assembly.GetOrderedCustomAttributes() + .OfType() + ); + + returnList.AddRange( + methodInfo.DeclaringType.GetTypeInfo().GetOrderedCustomAttributes().OfType() + ); } returnList.AddRange(methodInfo.GetOrderedCustomAttributes().OfType()); @@ -98,51 +131,63 @@ public static IEnumerable GetTestAttributes(this ParameterInfo parameterIn return returnList; } - private static IEnumerable GetOrderedCustomAttributes(this Assembly assembly) { - return assembly.GetCustomAttributes() - .Order(new SortCustomAttribute(assembly)); + private static IEnumerable GetOrderedCustomAttributes(this Assembly assembly) + { + return assembly.GetCustomAttributes().Order(new SortCustomAttribute(assembly)); } - - private static IEnumerable GetOrderedCustomAttributes(this Type type) { - return type.GetCustomAttributes() - .Order(new SortCustomAttribute(type.Assembly)); + + private static IEnumerable GetOrderedCustomAttributes(this Type type) + { + return type.GetCustomAttributes().Order(new SortCustomAttribute(type.Assembly)); } - - private static IEnumerable GetOrderedCustomAttributes(this MemberInfo? memberInfo) { - if (memberInfo?.DeclaringType == null) { + + private static IEnumerable GetOrderedCustomAttributes(this MemberInfo? memberInfo) + { + if (memberInfo?.DeclaringType == null) + { return ArraySegment.Empty; } - return memberInfo.GetCustomAttributes() + return memberInfo + .GetCustomAttributes() .Order(new SortCustomAttribute(memberInfo.DeclaringType.Assembly)); } - - private static IEnumerable GetOrderedCustomAttributes(this ParameterInfo? parameterInfo) { - if (parameterInfo?.Member.DeclaringType == null) { + + private static IEnumerable GetOrderedCustomAttributes( + this ParameterInfo? parameterInfo + ) + { + if (parameterInfo?.Member.DeclaringType == null) + { return ArraySegment.Empty; } - return parameterInfo.GetCustomAttributes() + return parameterInfo + .GetCustomAttributes() .Order(new SortCustomAttribute(parameterInfo.Member.DeclaringType.Assembly)); } /// /// Provides a custom attribute sorting mechanism based on the association of attributes with a specified assembly. /// - private class SortCustomAttribute(Assembly testAssembly) : IComparer { - - public int Compare(Attribute? x, Attribute? y) { - if (testAssembly.Equals(x?.GetType().Assembly)) { - if (testAssembly.Equals(y?.GetType().Assembly)) { + private class SortCustomAttribute(Assembly testAssembly) : IComparer + { + public int Compare(Attribute? x, Attribute? y) + { + if (testAssembly.Equals(x?.GetType().Assembly)) + { + if (testAssembly.Equals(y?.GetType().Assembly)) + { return 0; } return 1; } - else if (testAssembly.Equals(y?.GetType().Assembly)) { + else if (testAssembly.Equals(y?.GetType().Assembly)) + { return -1; } - + return 0; } } -} \ No newline at end of file +} diff --git a/src/DependencyModules.Testing/Impl/SharedRegistrations.cs b/src/DependencyModules.Testing/Impl/SharedRegistrations.cs index ff21654..91de293 100644 --- a/src/DependencyModules.Testing/Impl/SharedRegistrations.cs +++ b/src/DependencyModules.Testing/Impl/SharedRegistrations.cs @@ -10,8 +10,8 @@ namespace DependencyModules.Testing.Impl; /// Shared by every integration, because the rule is the same one wherever the test runs and only the /// discovery around it differs. /// -public static class SharedRegistrations { - +public static class SharedRegistrations +{ /// /// Types the harness itself can never pin, whatever anything else says. /// @@ -56,21 +56,30 @@ public static class SharedRegistrations { /// /// The attributes in scope for the test, widest first, as the runner collected them. /// - public static IReadOnlyCollection Collect(MethodInfo method, IEnumerable knownAttributes) { - var attributes = knownAttributes as IReadOnlyCollection ?? knownAttributes.ToArray(); + public static IReadOnlyCollection Collect( + MethodInfo method, + IEnumerable knownAttributes + ) + { + var attributes = + knownAttributes as IReadOnlyCollection ?? knownAttributes.ToArray(); var isolated = new HashSet(NeverPinned); - foreach (var registration in attributes.OfType()) { - foreach (var service in registration.IsolatedServices(method)) { + foreach (var registration in attributes.OfType()) + { + foreach (var service in registration.IsolatedServices(method)) + { isolated.Add(service); } } var pinned = new HashSet(); - foreach (var parameter in method.GetParameters()) { - var declarations = parameter.GetCustomAttributes() + foreach (var parameter in method.GetParameters()) + { + var declarations = parameter + .GetCustomAttributes() .OfType() .ToArray(); @@ -78,25 +87,31 @@ public static IReadOnlyCollection Collect(MethodInfo method, IEnumerable !declaration.Shared)) { + if (declarations.Any(declaration => !declaration.Shared)) + { continue; } pinned.Add(parameter.ParameterType); - foreach (var declaration in declarations) { - foreach (var service in declaration.SharedServices) { + foreach (var declaration in declarations) + { + foreach (var service in declaration.SharedServices) + { pinned.Add(service); } } } - foreach (var registration in attributes.OfType()) { - if (!registration.Shared) { + foreach (var registration in attributes.OfType()) + { + if (!registration.Shared) + { continue; } - foreach (var service in registration.SharedServices) { + foreach (var service in registration.SharedServices) + { pinned.Add(service); } } diff --git a/src/DependencyModules.Testing/Impl/TestContainerSource.cs b/src/DependencyModules.Testing/Impl/TestContainerSource.cs index 147917a..8a929eb 100644 --- a/src/DependencyModules.Testing/Impl/TestContainerSource.cs +++ b/src/DependencyModules.Testing/Impl/TestContainerSource.cs @@ -22,7 +22,8 @@ namespace DependencyModules.Testing.Impl; /// them - pays for none of this. /// /// -public sealed class TestContainerSource : ITestContainerSource { +public sealed class TestContainerSource : ITestContainerSource +{ private readonly object _gate = new(); private Composition? _composition; @@ -43,17 +44,22 @@ public void Initialize( IReadOnlyCollection pinnedServices, Func build, Func start, - Action track) { + Action track + ) + { _composition = new Composition(services, pinned, pinnedServices, build, start, track); } /// - public async ValueTask CreateAsync() { - var composition = _composition - ?? throw new InvalidOperationException( - $"This {nameof(TestContainerSource)} was never initialized, so there is " + - "nothing to build a container from. The runner does that once the test's " + - "own container exists."); + public async ValueTask CreateAsync() + { + var composition = + _composition + ?? throw new InvalidOperationException( + $"This {nameof(TestContainerSource)} was never initialized, so there is " + + "nothing to build a container from. The runner does that once the test's " + + "own container exists." + ); var provider = composition.Build(Template(composition)); @@ -69,12 +75,15 @@ public async ValueTask CreateAsync() { /// twice would take a second set of pinned instances out of the first container - leaving two /// objects where the whole point is one. /// - private IServiceCollection Template(Composition composition) { - if (_template != null) { + private IServiceCollection Template(Composition composition) + { + if (_template != null) + { return _template; } - lock (_gate) { + lock (_gate) + { return _template ??= BuildTemplate(composition); } } @@ -103,27 +112,34 @@ private IServiceCollection Template(Composition composition) { /// without anyone asking. An open generic is left alone too, having no closed type to resolve. /// /// - private static IServiceCollection BuildTemplate(Composition composition) { + private static IServiceCollection BuildTemplate(Composition composition) + { var instances = Resolve(composition); IServiceCollection template = new ServiceCollection(); var taken = new HashSet(); - foreach (var descriptor in composition.Services) { + foreach (var descriptor in composition.Services) + { var serviceType = descriptor.ServiceType; - if (!instances.TryGetValue(serviceType, out var pinned) || - descriptor.ImplementationInstance != null) { + if ( + !instances.TryGetValue(serviceType, out var pinned) + || descriptor.ImplementationInstance != null + ) + { template.Add(descriptor); continue; } - if (!taken.Add(serviceType)) { + if (!taken.Add(serviceType)) + { continue; } - foreach (var instance in pinned) { + foreach (var instance in pinned) + { template.Add(new ServiceDescriptor(serviceType, instance)); } } @@ -160,28 +176,35 @@ private static IServiceCollection BuildTemplate(Composition composition) { /// An open generic is skipped, having no closed type to resolve. /// /// - private static Dictionary Resolve(Composition composition) { + private static Dictionary Resolve(Composition composition) + { var instances = new Dictionary(); - foreach (var serviceType in composition.PinnedServices) { - if (serviceType.IsGenericTypeDefinition || serviceType.IsByRef || serviceType.IsPointer) { + foreach (var serviceType in composition.PinnedServices) + { + if (serviceType.IsGenericTypeDefinition || serviceType.IsByRef || serviceType.IsPointer) + { continue; } object[] resolved; - try { + try + { var sequence = typeof(IEnumerable<>).MakeGenericType(serviceType); resolved = ((IEnumerable)composition.Pinned.GetRequiredService(sequence)) .Cast() .Where(instance => instance != null) .ToArray(); - } catch (Exception) { + } + catch (Exception) + { continue; } - if (resolved.Length > 0) { + if (resolved.Length > 0) + { instances[serviceType] = resolved; } } @@ -195,5 +218,6 @@ private sealed record Composition( IReadOnlyCollection PinnedServices, Func Build, Func Start, - Action Track); + Action Track + ); } diff --git a/src/DependencyModules.Testing/Impl/TestParameterResolver.cs b/src/DependencyModules.Testing/Impl/TestParameterResolver.cs index 3fca5b4..afe69fc 100644 --- a/src/DependencyModules.Testing/Impl/TestParameterResolver.cs +++ b/src/DependencyModules.Testing/Impl/TestParameterResolver.cs @@ -20,16 +20,19 @@ namespace DependencyModules.Testing.Impl; /// once there is a provider to resolve from. One instance belongs /// to one container: a data-driven test that builds a container per row wants a resolver per row too. /// -public sealed class TestParameterResolver { +public sealed class TestParameterResolver +{ private readonly ITestMethodContext _testMethod; - private readonly Dictionary> _valueProviders = new(); + private readonly Dictionary> _valueProviders = + new(); private bool _setupRan; /// /// Creates a resolver for one test method and one container. /// /// The test whose parameters are being supplied. - public TestParameterResolver(ITestMethodContext testMethod) { + public TestParameterResolver(ITestMethodContext testMethod) + { _testMethod = testMethod; } @@ -42,13 +45,19 @@ public TestParameterResolver(ITestMethodContext testMethod) { /// constructed against it — rather than the test being handed a double nothing else can see. /// /// The collection backing the test's container. - public void SetupServiceCollection(IServiceCollection serviceCollection) { - foreach (var parameterInfo in _testMethod.Method.GetParameters()) { - var providers = parameterInfo.GetCustomAttributes().OfType().ToList(); + public void SetupServiceCollection(IServiceCollection serviceCollection) + { + foreach (var parameterInfo in _testMethod.Method.GetParameters()) + { + var providers = parameterInfo + .GetCustomAttributes() + .OfType() + .ToList(); _valueProviders.Add(parameterInfo, providers); - foreach (var valueProvider in providers) { + foreach (var valueProvider in providers) + { valueProvider.SetupServiceCollection(_testMethod, serviceCollection, parameterInfo); } } @@ -76,17 +85,24 @@ public void SetupServiceCollection(IServiceCollection serviceCollection) { /// would quietly skip every parameter attribute, so a [Mock] parameter would hand back the /// real service instead of a substitute. /// - public async Task ResolveArgumentsAsync(IServiceProvider serviceProvider, object?[] data) { - if (!_setupRan) { + public async Task ResolveArgumentsAsync( + IServiceProvider serviceProvider, + object?[] data + ) + { + if (!_setupRan) + { throw new InvalidOperationException( - $"{nameof(SetupServiceCollection)} must be called before {nameof(ResolveArgumentsAsync)}, " + - "while the service collection can still be added to."); + $"{nameof(SetupServiceCollection)} must be called before {nameof(ResolveArgumentsAsync)}, " + + "while the service collection can still be added to." + ); } var parameterList = _testMethod.Method.GetParameters(); var arguments = new List(data); - for (var i = data.Length; i < parameterList.Length; i++) { + for (var i = data.Length; i < parameterList.Length; i++) + { var parameterInfo = parameterList[i]; var value = await ResolveFromParameterProviders(parameterInfo, serviceProvider); @@ -103,15 +119,25 @@ public void SetupServiceCollection(IServiceCollection serviceCollection) { /// because a test asking for the container itself cannot be resolved from it. /// private async Task ResolveFromParameterProviders( - ParameterInfo parameterInfo, IServiceProvider serviceProvider) { - if (parameterInfo.ParameterType == typeof(IServiceProvider)) { + ParameterInfo parameterInfo, + IServiceProvider serviceProvider + ) + { + if (parameterInfo.ParameterType == typeof(IServiceProvider)) + { return serviceProvider; } - foreach (var valueProvider in _valueProviders[parameterInfo]) { - var value = await valueProvider.GetParameterValueAsync(_testMethod, serviceProvider, parameterInfo); + foreach (var valueProvider in _valueProviders[parameterInfo]) + { + var value = await valueProvider.GetParameterValueAsync( + _testMethod, + serviceProvider, + parameterInfo + ); - if (value != null) { + if (value != null) + { return value; } } @@ -119,15 +145,26 @@ public void SetupServiceCollection(IServiceCollection serviceCollection) { return null; } - private object? ResolveFromContainer(ParameterInfo parameterInfo, IServiceProvider serviceProvider) { + private object? ResolveFromContainer( + ParameterInfo parameterInfo, + IServiceProvider serviceProvider + ) + { var keyedServicesAttribute = parameterInfo.GetCustomAttribute(); - if (keyedServicesAttribute != null && serviceProvider is IKeyedServiceProvider keyedServiceProvider) { - return keyedServiceProvider.GetKeyedService(parameterInfo.ParameterType, keyedServicesAttribute.Key); + if ( + keyedServicesAttribute != null + && serviceProvider is IKeyedServiceProvider keyedServiceProvider + ) + { + return keyedServiceProvider.GetKeyedService( + parameterInfo.ParameterType, + keyedServicesAttribute.Key + ); } return serviceProvider.GetService(parameterInfo.ParameterType) - ?? ConstructValueFromType(parameterInfo, serviceProvider); + ?? ConstructValueFromType(parameterInfo, serviceProvider); } /// @@ -138,15 +175,25 @@ public void SetupServiceCollection(IServiceCollection serviceCollection) { /// An on the parameter supplies the constructor arguments the /// container cannot work out for itself. The last one on the parameter wins. /// - private static object? ConstructValueFromType(ParameterInfo parameterInfo, IServiceProvider serviceProvider) { + private static object? ConstructValueFromType( + ParameterInfo parameterInfo, + IServiceProvider serviceProvider + ) + { object[] parameterValues = []; - foreach (var attribute in parameterInfo.GetCustomAttributes()) { - if (attribute is IInjectValueAttribute injectValueAttribute) { + foreach (var attribute in parameterInfo.GetCustomAttributes()) + { + if (attribute is IInjectValueAttribute injectValueAttribute) + { parameterValues = injectValueAttribute.ProvideValue(serviceProvider, parameterInfo); } } - return ActivatorUtilities.CreateInstance(serviceProvider, parameterInfo.ParameterType, parameterValues); + return ActivatorUtilities.CreateInstance( + serviceProvider, + parameterInfo.ParameterType, + parameterValues + ); } } diff --git a/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs b/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs index 9401fda..d76a20c 100644 --- a/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs +++ b/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs @@ -21,8 +21,8 @@ namespace DependencyModules.xUnit.Attributes; /// [XunitTestCaseDiscoverer(typeof(ModuleTestDiscoverer))] [AttributeUsage(AttributeTargets.Method)] -public class ModuleTestAttribute : FactAttribute, IModuleTestAttribute { - +public class ModuleTestAttribute : FactAttribute, IModuleTestAttribute +{ /// /// Marks a test method, taking no modules. /// @@ -34,9 +34,9 @@ public class ModuleTestAttribute : FactAttribute, IModuleTestAttribute { /// public ModuleTestAttribute( [CallerFilePath] string? sourceFilePath = null, - [CallerLineNumber] int sourceLineNumber = -1) - : base(sourceFilePath, sourceLineNumber) => - ModuleTypes = []; + [CallerLineNumber] int sourceLineNumber = -1 + ) + : base(sourceFilePath, sourceLineNumber) => ModuleTypes = []; /// /// Marks a test method and names one module to configure the test's container with. @@ -50,9 +50,9 @@ public ModuleTestAttribute( public ModuleTestAttribute( Type module, [CallerFilePath] string? sourceFilePath = null, - [CallerLineNumber] int sourceLineNumber = -1) - : base(sourceFilePath, sourceLineNumber) => - ModuleTypes = [module]; + [CallerLineNumber] int sourceLineNumber = -1 + ) + : base(sourceFilePath, sourceLineNumber) => ModuleTypes = [module]; /// /// Marks a test method and names several modules to configure the test's container with. @@ -63,8 +63,7 @@ public ModuleTestAttribute( /// still runs and still reports correctly; only navigation from a test explorer back to the /// source is unavailable. Naming one module, or none, takes an overload that does capture it. /// - public ModuleTestAttribute(params Type[] modules) => - ModuleTypes = modules; + public ModuleTestAttribute(params Type[] modules) => ModuleTypes = modules; /// /// Gets an array of module types associated with the test method decorated with @@ -78,7 +77,5 @@ public ModuleTestAttribute(params Type[] modules) => /// Declared by , so the module loading itself is shared with /// every other test framework integration rather than reading this attribute by name. /// - public Type[] ModuleTypes { - get; - } -} \ No newline at end of file + public Type[] ModuleTypes { get; } +} diff --git a/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs b/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs index 9319605..ee64158 100644 --- a/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs +++ b/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs @@ -22,8 +22,8 @@ namespace DependencyModules.xUnit.Impl; /// ModuleTestCommand has always disposed in a finally around the test; this is the /// same lifetime for xUnit. /// -public class ModuleTestCase : XunitTestCase, ISelfExecutingXunitTestCase { - +public class ModuleTestCase : XunitTestCase, ISelfExecutingXunitTestCase +{ /// /// One per container this case built: one for a plain test, one per row for a data-driven /// one. Runtime state only, never serialized with the case. @@ -56,26 +56,29 @@ public ModuleTestCase( object?[]? testMethodArguments = null, string? sourceFilePath = null, int? sourceLineNumber = null, - int? timeout = null) : base( - // Named rather than positional throughout. XunitTestCase's constructor takes thirteen - // parameters, eleven of them optional, and a version that inserts one mid-list rebinds - // every argument after it — silently where the types happen to line up, and as a wall of - // unrelated-looking conversion errors where they do not. Named arguments make an insertion - // either invisible or a single precise error. - testMethod: testMethod, - testCaseDisplayName: testCaseDisplayName, - uniqueID: uniqueID, - @explicit: @explicit, - skipExceptions: skipExceptions, - skipReason: skipReason, - skipType: skipType, - skipUnless: skipUnless, - skipWhen: skipWhen, - traits: traits, - testMethodArguments: testMethodArguments, - sourceFilePath: sourceFilePath, - sourceLineNumber: sourceLineNumber, - timeout: timeout) { } + int? timeout = null + ) + : base( + // Named rather than positional throughout. XunitTestCase's constructor takes thirteen + // parameters, eleven of them optional, and a version that inserts one mid-list rebinds + // every argument after it — silently where the types happen to line up, and as a wall of + // unrelated-looking conversion errors where they do not. Named arguments make an insertion + // either invisible or a single precise error. + testMethod: testMethod, + testCaseDisplayName: testCaseDisplayName, + uniqueID: uniqueID, + @explicit: @explicit, + skipExceptions: skipExceptions, + skipReason: skipReason, + skipType: skipType, + skipUnless: skipUnless, + skipWhen: skipWhen, + traits: traits, + testMethodArguments: testMethodArguments, + sourceFilePath: sourceFilePath, + sourceLineNumber: sourceLineNumber, + timeout: timeout + ) { } /// /// Executes logic before the invocation of the test method associated with the current test case. @@ -83,11 +86,10 @@ public ModuleTestCase( /// public override void PreInvoke() { } - private record StartupValues( - IServiceProvider ServiceProvider, - TestParameterResolver Resolver); + private record StartupValues(IServiceProvider ServiceProvider, TestParameterResolver Resolver); - private async Task SetupServiceCollection() { + private async Task SetupServiceCollection() + { var serviceCollection = new ServiceCollection(); var knownAttributes = TestMethod.Method.GetTestAttributes().ToArray(); @@ -135,7 +137,8 @@ private async Task SetupServiceCollection() { pinnedServices: SharedRegistrations.Collect(TestMethod.Method, knownAttributes), build: services => BuildServiceProvider(context, services, knownAttributes), start: built => StartAsync(context, knownAttributes, built), - track: _providers.Add); + track: _providers.Add + ); return new StartupValues(provider, resolver); } @@ -149,20 +152,27 @@ private async Task SetupServiceCollection() { /// that looks composed and is not. /// private static async ValueTask StartAsync( - ITestMethodContext context, Attribute[] knownAttributes, IServiceProvider provider) { - foreach (var startupAttribute in knownAttributes.OfType()) { + ITestMethodContext context, + Attribute[] knownAttributes, + IServiceProvider provider + ) + { + foreach (var startupAttribute in knownAttributes.OfType()) + { await startupAttribute.StartupAsync(context, provider); } } - private void SetupTestCaseInfo(ServiceCollection serviceCollection, Attribute[] knownAttributes) { - - serviceCollection.AddSingleton(provider => provider.GetRequiredService()); + private void SetupTestCaseInfo(ServiceCollection serviceCollection, Attribute[] knownAttributes) + { + serviceCollection.AddSingleton(provider => + provider.GetRequiredService() + ); serviceCollection.AddSingleton(_ => new TestCaseInfo( TestMethod, - ArraySegment.Empty, + ArraySegment.Empty, knownAttributes - )); + )); } /// @@ -173,11 +183,17 @@ private void SetupTestCaseInfo(ServiceCollection serviceCollection, Attribute[] /// which is the reverse of how every other attribute here resolves. /// private IServiceProvider BuildServiceProvider( - ITestMethodContext context, IServiceCollection serviceCollection, Attribute[] knownAttributes) { - var serviceProviderBuilderAttribute = - knownAttributes.OfType().LastOrDefault(); - - if (serviceProviderBuilderAttribute != null) { + ITestMethodContext context, + IServiceCollection serviceCollection, + Attribute[] knownAttributes + ) + { + var serviceProviderBuilderAttribute = knownAttributes + .OfType() + .LastOrDefault(); + + if (serviceProviderBuilderAttribute != null) + { return serviceProviderBuilderAttribute.BuildServiceProvider(context, serviceCollection); } @@ -199,12 +215,17 @@ private IServiceProvider BuildServiceProvider( /// mocked, which is what [Mock] is for. /// private void SetupServiceSetupAttributes( - ITestMethodContext context, IServiceCollection serviceCollection, Attribute[] knownAttributes) { + ITestMethodContext context, + IServiceCollection serviceCollection, + Attribute[] knownAttributes + ) + { var setupAttributes = knownAttributes .OfType() .OrderBy(attribute => attribute is IMockSupportAttribute ? 0 : 1); - foreach (var setupAttribute in setupAttributes) { + foreach (var setupAttribute in setupAttributes) + { setupAttribute.SetupServiceCollection(context, serviceCollection); } } @@ -221,42 +242,52 @@ private void SetupServiceSetupAttributes( /// had been decided against the default. Widest scope first, so the narrowest attribute that /// answers decides, matching how every other attribute here resolves. /// - private void SeedEnvironment(IServiceCollection serviceCollection, Attribute[] knownAttributes) { + private void SeedEnvironment(IServiceCollection serviceCollection, Attribute[] knownAttributes) + { IModuleEnvironment? environment = null; - foreach (var provider in knownAttributes.OfType()) { + foreach (var provider in knownAttributes.OfType()) + { environment = provider.ProvideEnvironment(TestMethod.Method) ?? environment; } - if (environment != null) { + if (environment != null) + { serviceCollection.Add(new ServiceDescriptor(typeof(IModuleEnvironment), environment)); } } - private void SetupModules(ServiceCollection serviceCollection, IEnumerable knownAttributes) { + private void SetupModules( + ServiceCollection serviceCollection, + IEnumerable knownAttributes + ) + { var modules = new List(); - foreach (var loadModuleAttribute in knownAttributes.OfType()) { - + foreach (var loadModuleAttribute in knownAttributes.OfType()) + { var moduleTypes = loadModuleAttribute.GetModule(); - + modules.Add(moduleTypes); } // The interface rather than ModuleTestAttribute, so this reads the same for any integration. var testAttribute = TestMethod.Method.GetTestAttribute(); - if (testAttribute != null) { + if (testAttribute != null) + { var count = 0; - foreach (var moduleType in testAttribute.ModuleTypes) { - if (Activator.CreateInstance(moduleType, []) is IDependencyModule moduleInstance) { + foreach (var moduleType in testAttribute.ModuleTypes) + { + if (Activator.CreateInstance(moduleType, []) is IDependencyModule moduleInstance) + { modules.Insert(count++, moduleInstance); } } } modules.Reverse(); - + DependencyRegistry.LoadModules(serviceCollection, modules.ToArray()); } @@ -277,23 +308,36 @@ public async ValueTask Run( IMessageBus messageBus, object?[] constructorArguments, ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) { - try { + CancellationTokenSource cancellationTokenSource + ) + { + try + { return await XunitRunnerHelper.RunXunitTestCase( - this, messageBus, cancellationTokenSource, aggregator, explicitOption, constructorArguments); + this, + messageBus, + cancellationTokenSource, + aggregator, + explicitOption, + constructorArguments + ); } - finally { + finally + { await DisposeProviders(); } } - private async ValueTask DisposeProviders() { + private async ValueTask DisposeProviders() + { var providers = _providers.ToArray(); _providers.Clear(); - foreach (var provider in providers) { - switch (provider) { + foreach (var provider in providers) + { + switch (provider) + { case IAsyncDisposable asyncDisposable: await asyncDisposable.DisposeAsync(); break; @@ -310,11 +354,12 @@ private async ValueTask DisposeProviders() { /// tests that are associated with this test case. /// /// - public override async ValueTask> CreateTests() { - var dataAttributes = - TestMethod.Method.GetTestAttributes().ToArray(); + public override async ValueTask> CreateTests() + { + var dataAttributes = TestMethod.Method.GetTestAttributes().ToArray(); - if (dataAttributes.Length == 0) { + if (dataAttributes.Length == 0) + { return await UnitTestWithNoDataAttributes(); } @@ -341,26 +386,33 @@ public override async ValueTask> CreateTests() { /// Conditional, as the interface requires: an explicit MemberType is the author's answer and is /// never overwritten. /// - private void SupplyReflectedType(IDataAttribute[] dataAttributes) { + private void SupplyReflectedType(IDataAttribute[] dataAttributes) + { var reflectedType = TestMethod.Method.ReflectedType; - if (reflectedType == null) { + if (reflectedType == null) + { return; } - foreach (var typeAware in dataAttributes.OfType()) { + foreach (var typeAware in dataAttributes.OfType()) + { typeAware.MemberType ??= reflectedType; } } - private async Task> UnitTestFromDataAttributes(IDataAttribute[] dataAttributes) { + private async Task> UnitTestFromDataAttributes( + IDataAttribute[] dataAttributes + ) + { var unitTests = new List(); - foreach (var dataAttribute in dataAttributes) { - var dataRowCollection = - await dataAttribute.GetData(TestMethod.Method, DisposalTracker); + foreach (var dataAttribute in dataAttributes) + { + var dataRowCollection = await dataAttribute.GetData(TestMethod.Method, DisposalTracker); - foreach (var theoryDataRow in dataRowCollection) { + foreach (var theoryDataRow in dataRowCollection) + { var data = theoryDataRow.GetData(); var startupValues = await SetupServiceCollection(); @@ -383,7 +435,8 @@ private async Task> UnitTestFromDataAttributes(I skipWhen: theoryDataRow.SkipWhen ?? SkipWhen, testDisplayName: GetRowDisplayName(theoryDataRow, data), testIndex: unitTests.Count, - traits: theoryDataRow.Traits?.ToReadOnlyTraits() ?? Traits.ToReadOnlyTraits(), + traits: theoryDataRow.Traits?.ToReadOnlyTraits() + ?? Traits.ToReadOnlyTraits(), timeout: theoryDataRow.Timeout ?? Timeout, testMethodArguments: await ResolveArguments(data, startupValues) ) @@ -391,7 +444,8 @@ private async Task> UnitTestFromDataAttributes(I } } - if (unitTests.Count == 0) { + if (unitTests.Count == 0) + { // Failing rather than returning nothing, which is what xUnit's own delay-enumerated // theory does for a theory without data. Returning an empty collection here is reported // as a pass, so a row source that stopped producing rows — for any reason, not only the @@ -401,16 +455,18 @@ private async Task> UnitTestFromDataAttributes(I // Exceptions thrown from CreateTests are caught and converted into a test case failure, // which is the documented way to surface this. throw new InvalidOperationException( - $"No data was found for '{TestMethod.TestClass.TestClassName}.{TestMethod.MethodName}'. " + - $"It carries {DescribeAttributes(dataAttributes)}, and every one of them returned no rows. " + - "A data-driven test with no rows runs nothing, so it is reported as a failure rather " + - "than as a pass."); + $"No data was found for '{TestMethod.TestClass.TestClassName}.{TestMethod.MethodName}'. " + + $"It carries {DescribeAttributes(dataAttributes)}, and every one of them returned no rows. " + + "A data-driven test with no rows runs nothing, so it is reported as a failure rather " + + "than as a pass." + ); } return unitTests; } - private static string DescribeAttributes(IDataAttribute[] dataAttributes) { + private static string DescribeAttributes(IDataAttribute[] dataAttributes) + { var names = dataAttributes .Select(attribute => "[" + TrimAttributeSuffix(attribute.GetType().Name) + "]") .ToArray(); @@ -433,7 +489,8 @@ private static string TrimAttributeSuffix(string name) => /// at execution time, and naming a test after a service instance would be neither readable /// nor stable between runs. /// - private string GetRowDisplayName(Xunit.ITheoryDataRow theoryDataRow, object?[] data) { + private string GetRowDisplayName(Xunit.ITheoryDataRow theoryDataRow, object?[] data) + { var baseDisplayName = theoryDataRow.TestDisplayName ?? TestCaseDisplayName; return TestMethod.GetDisplayName( @@ -442,13 +499,16 @@ private string GetRowDisplayName(Xunit.ITheoryDataRow theoryDataRow, object?[] d // for [Theory]; passing null would compile and quietly drop it for module tests. label: theoryDataRow.Label, testMethodArguments: data, - methodGenericTypes: null); + methodGenericTypes: null + ); } - private async Task> UnitTestWithNoDataAttributes() { + private async Task> UnitTestWithNoDataAttributes() + { var startupValues = await SetupServiceCollection(); - return [ + return + [ new XunitTest( testCase: this, testMethod: TestMethod, @@ -462,7 +522,7 @@ private async Task> UnitTestWithNoDataAttributes traits: Traits.ToReadOnlyTraits(), timeout: Timeout, testMethodArguments: await ResolveArguments([], startupValues) - ) + ), ]; } @@ -470,11 +530,19 @@ private async Task> UnitTestWithNoDataAttributes /// The arguments are published on so a test can read what it was /// invoked with. That is xUnit's own object, which is why this is not part of the shared resolver. /// - private static async Task ResolveArguments(object?[] data, StartupValues startupValues) { - var arguments = await startupValues.Resolver.ResolveArgumentsAsync(startupValues.ServiceProvider, data); - - startupValues.ServiceProvider.GetRequiredService().TestMethodArguments = arguments; + private static async Task ResolveArguments( + object?[] data, + StartupValues startupValues + ) + { + var arguments = await startupValues.Resolver.ResolveArgumentsAsync( + startupValues.ServiceProvider, + data + ); + + startupValues.ServiceProvider.GetRequiredService().TestMethodArguments = + arguments; return arguments; } -} \ No newline at end of file +} diff --git a/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs b/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs index c8d0cb6..f49adad 100644 --- a/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs +++ b/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs @@ -14,8 +14,8 @@ namespace DependencyModules.xUnit.Impl; /// /// /// -public class ModuleTestDiscoverer : IXunitTestCaseDiscoverer { - +public class ModuleTestDiscoverer : IXunitTestCaseDiscoverer +{ /// /// Discovers test cases for the provided method using the xUnit framework /// and returns a collection of test cases to be executed. @@ -33,8 +33,11 @@ public class ModuleTestDiscoverer : IXunitTestCaseDiscoverer { /// A task that, when completed, contains a read-only collection of discovered test cases specific to the provided method. /// public ValueTask> Discover( - ITestFrameworkDiscoveryOptions discoveryOptions, IXunitTestMethod testMethod, IFactAttribute factAttribute) { - + ITestFrameworkDiscoveryOptions discoveryOptions, + IXunitTestMethod testMethod, + IFactAttribute factAttribute + ) + { // Delegate to xUnit's own introspection rather than deriving these by hand. The bare method // name is not unique across test classes, and xUnit silently drops a test case whose ID // collides with one already discovered. This also picks up display name formatting, the @@ -46,10 +49,15 @@ public ValueTask> Discover( // Naming a parameter only the newer one declares resolves it. A module test has no label, // which is what null says. var details = TestIntrospectionHelper.GetTestCaseDetails( - discoveryOptions, testMethod, factAttribute, label: null); + discoveryOptions, + testMethod, + factAttribute, + label: null + ); return new ValueTask>( - new[] { + new[] + { new ModuleTestCase( testMethod: details.ResolvedTestMethod, testCaseDisplayName: details.TestCaseDisplayName, @@ -70,8 +78,8 @@ public ValueTask> Discover( sourceFilePath: details.SourceFilePath, sourceLineNumber: details.SourceLineNumber, timeout: details.Timeout - ) + ), } ); } -} \ No newline at end of file +} diff --git a/src/DependencyModules.xUnit/Impl/TestCaseInfo.cs b/src/DependencyModules.xUnit/Impl/TestCaseInfo.cs index d9c009b..e419055 100644 --- a/src/DependencyModules.xUnit/Impl/TestCaseInfo.cs +++ b/src/DependencyModules.xUnit/Impl/TestCaseInfo.cs @@ -5,8 +5,8 @@ namespace DependencyModules.xUnit.Impl; /// /// Defines the contract for retrieving information about a specific test case. /// -public interface ITestCaseInfo { - +public interface ITestCaseInfo +{ /// /// Gets the test method associated with a specific test case. /// @@ -16,9 +16,7 @@ public interface ITestCaseInfo { /// useful when retrieving metadata or executing logic related to /// the underlying test method in the context of xUnit.net testing framework. /// - IXunitTestMethod TestMethod { - get; - } + IXunitTestMethod TestMethod { get; } /// /// Gets or sets the arguments passed to the test method for a specific test case. @@ -29,10 +27,7 @@ IXunitTestMethod TestMethod { /// is particularly useful in scenarios where the arguments need to be examined /// or manipulated, such as parameterized test cases within the xUnit.net testing framework. /// - IReadOnlyList TestMethodArguments { - get; - set; - } + IReadOnlyList TestMethodArguments { get; set; } /// /// Gets the collection of attributes associated with the test method of a specific test case. @@ -42,9 +37,7 @@ IXunitTestMethod TestMethod { /// applied to the test method. This property can be utilized to retrieve additional /// behavioral or descriptive information tied to the associated test method. /// - IReadOnlyList TestMethodAttributes { - get; - } + IReadOnlyList TestMethodAttributes { get; } } /// @@ -53,8 +46,9 @@ IReadOnlyList TestMethodAttributes { public class TestCaseInfo( IXunitTestMethod testMethod, IReadOnlyList testMethodArguments, - IReadOnlyList testMethodAttributes) : ITestCaseInfo { - + IReadOnlyList testMethodAttributes +) : ITestCaseInfo +{ /// /// Gets the test method associated with the test case. /// @@ -62,9 +56,7 @@ public class TestCaseInfo( /// The TestMethod property provides access to the underlying test method for a given test case. /// It can be utilized to retrieve metadata or invoke specific logic related to the corresponding xUnit test method. /// - public IXunitTestMethod TestMethod { - get; - } = testMethod; + public IXunitTestMethod TestMethod { get; } = testMethod; /// /// Gets or sets the arguments used for invoking the test method associated with the test case. @@ -74,10 +66,7 @@ public IXunitTestMethod TestMethod { /// the test method during execution. This is particularly useful when preparing customized or dynamically /// resolved arguments for parameterized test cases. /// - public IReadOnlyList TestMethodArguments { - get; - set; - } = testMethodArguments; + public IReadOnlyList TestMethodArguments { get; set; } = testMethodArguments; /// /// Gets the collection of attributes applied to the test method associated with a test case. @@ -88,7 +77,5 @@ public IReadOnlyList TestMethodArguments { /// This property is particularly helpful when injecting behaviors or inspecting the attributes for /// parameterized or decorated test methods. /// - public IReadOnlyList TestMethodAttributes { - get; - } = testMethodAttributes; -} \ No newline at end of file + public IReadOnlyList TestMethodAttributes { get; } = testMethodAttributes; +} diff --git a/src/DependencyModules.xUnit/Impl/TraitDictionaryExtensions.cs b/src/DependencyModules.xUnit/Impl/TraitDictionaryExtensions.cs index c72ff9f..cd87bf7 100644 --- a/src/DependencyModules.xUnit/Impl/TraitDictionaryExtensions.cs +++ b/src/DependencyModules.xUnit/Impl/TraitDictionaryExtensions.cs @@ -16,8 +16,8 @@ namespace DependencyModules.xUnit.Impl; /// ToReadOnlyTraits also avoids colliding with the AsReadOnly that /// supplies for dictionaries. /// -internal static class TraitDictionaryExtensions { - +internal static class TraitDictionaryExtensions +{ /// /// Widens the mutable form xUnit stores traits in to the read-only form its constructors take. /// @@ -30,11 +30,13 @@ internal static class TraitDictionaryExtensions { /// equivalent under today's xUnit. /// public static IReadOnlyDictionary> ToReadOnlyTraits( - this Dictionary> traits) => + this Dictionary> traits + ) => traits.ToDictionary( pair => pair.Key, pair => (IReadOnlyCollection)pair.Value, - traits.Comparer); + traits.Comparer + ); /// /// Copies the read-only form into the mutable one, under the supplied key comparer. @@ -50,11 +52,13 @@ public static IReadOnlyDictionary> ToReadOnl /// public static Dictionary> ToWritableTraits( this IReadOnlyDictionary> traits, - IEqualityComparer comparer) { - + IEqualityComparer comparer + ) + { var result = new Dictionary>(comparer); - foreach (var pair in traits) { + foreach (var pair in traits) + { result[pair.Key] = new HashSet(pair.Value); } diff --git a/src/DependencyModules.xUnit/Impl/XunitTestMethodContext.cs b/src/DependencyModules.xUnit/Impl/XunitTestMethodContext.cs index 15168b4..fed9239 100644 --- a/src/DependencyModules.xUnit/Impl/XunitTestMethodContext.cs +++ b/src/DependencyModules.xUnit/Impl/XunitTestMethodContext.cs @@ -14,14 +14,12 @@ namespace DependencyModules.xUnit.Impl; /// if (testMethod is IXunitTestMethodContext xunit) reaches the full model — unique ID, merged /// traits, generic resolution, the test class and its collection. /// -public interface IXunitTestMethodContext : ITestMethodContext { - +public interface IXunitTestMethodContext : ITestMethodContext +{ /// /// xUnit's own model of the test method. /// - IXunitTestMethod XunitTestMethod { - get; - } + IXunitTestMethod XunitTestMethod { get; } } /// @@ -34,15 +32,12 @@ IXunitTestMethod XunitTestMethod { /// internal sealed class XunitTestMethodContext( IXunitTestMethod testMethod, - IReadOnlyList attributes) : IXunitTestMethodContext { - - public IXunitTestMethod XunitTestMethod { - get; - } = testMethod; + IReadOnlyList attributes +) : IXunitTestMethodContext +{ + public IXunitTestMethod XunitTestMethod { get; } = testMethod; public MethodInfo Method => XunitTestMethod.Method; - public IReadOnlyList Attributes { - get; - } = attributes; + public IReadOnlyList Attributes { get; } = attributes; } diff --git a/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs b/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs index 976d85c..e28ad66 100644 --- a/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs +++ b/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs @@ -19,15 +19,17 @@ namespace DependencyModules.Tests.ApiTests; /// UPDATE_SNAPSHOTS=1 dotnet test tests/DependencyModules.Tests /// then read the diff carefully before committing it. /// -public class PublicApiTests { - +public class PublicApiTests +{ [Fact] - public void RuntimeApi() { + public void RuntimeApi() + { Snapshot.Match(ApiOf(typeof(DependencyModuleAttribute))); } [Fact] - public void XUnitApi() { + public void XUnitApi() + { Snapshot.Match(ApiOf(typeof(ModuleTestAttribute))); } @@ -37,8 +39,11 @@ public void XUnitApi() { /// IModuleTestAttribute is common to both. /// [Fact] - public void NUnitApi() { - Snapshot.Match(ApiOf(typeof(global::DependencyModules.NUnit.Attributes.ModuleTestAttribute))); + public void NUnitApi() + { + Snapshot.Match( + ApiOf(typeof(global::DependencyModules.NUnit.Attributes.ModuleTestAttribute)) + ); } /// @@ -46,23 +51,31 @@ public void NUnitApi() { /// test framework dependency, which is the point of it — a change here reaches all of them. /// [Fact] - public void TestingApi() { + public void TestingApi() + { Snapshot.Match(ApiOf(typeof(Testing.Attributes.Interfaces.IMockSupportAttribute))); } [Fact] - public void NSubstituteApi() { - Snapshot.Match(ApiOf(typeof(global::DependencyModules.NSubstitute.NSubstituteSupportAttribute))); + public void NSubstituteApi() + { + Snapshot.Match( + ApiOf(typeof(global::DependencyModules.NSubstitute.NSubstituteSupportAttribute)) + ); } [Fact] - public void MoqApi() { + public void MoqApi() + { Snapshot.Match(ApiOf(typeof(global::DependencyModules.Moq.MoqSupportAttribute))); } [Fact] - public void FakeItEasyApi() { - Snapshot.Match(ApiOf(typeof(global::DependencyModules.FakeItEasy.FakeItEasySupportAttribute))); + public void FakeItEasyApi() + { + Snapshot.Match( + ApiOf(typeof(global::DependencyModules.FakeItEasy.FakeItEasySupportAttribute)) + ); } /// @@ -72,21 +85,25 @@ public void FakeItEasyApi() { /// generators on top of these base classes. /// [Fact] - public void SourceGeneratorApi() { + public void SourceGeneratorApi() + { Snapshot.Match(ApiOf(typeof(SourceGenerator.SourceGenerator))); } private static string ApiOf(Type typeFromAssembly) => typeFromAssembly.Assembly.GeneratePublicApi( - new ApiGeneratorOptions { + new ApiGeneratorOptions + { // Assembly-level attributes are build metadata, not API, and several of them // (SourceLink, InternalsVisibleTo, TFM) change with build configuration. - ExcludeAttributes = [ + ExcludeAttributes = + [ "System.Runtime.Versioning.TargetFrameworkAttribute", "System.Reflection.AssemblyMetadataAttribute", "System.Runtime.CompilerServices.InternalsVisibleToAttribute", "System.Diagnostics.DebuggableAttribute", - "System.Runtime.CompilerServices.CompilationRelaxationsAttribute" - ] - }); + "System.Runtime.CompilerServices.CompilationRelaxationsAttribute", + ], + } + ); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/ApplicationModuleCollisionTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ApplicationModuleCollisionTests.cs index c84c289..62b5b33 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ApplicationModuleCollisionTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ApplicationModuleCollisionTests.cs @@ -26,10 +26,11 @@ namespace DependencyModules.Tests.GeneratorTests; /// The variants below are kept as a set on purpose: every one of them was reported as the trigger /// at some point, and only the last is. /// -public class ApplicationModuleCollisionTests { - +public class ApplicationModuleCollisionTests +{ [Fact] - public void DeclaringApplicationModuleInTheRootNamespace_DoesNotCollide() { + public void DeclaringApplicationModuleInTheRootNamespace_DoesNotCollide() + { var result = Run(DeclaredModule("ConfiguredRoot")); Assert.Empty(result.DuplicateHintNames); @@ -41,12 +42,14 @@ public void DeclaringApplicationModuleInTheRootNamespace_DoesNotCollide() { /// the reason this shipped. Asserted separately from AssertNoErrors for that reason. /// [Fact] - public void DeclaringApplicationModuleInTheRootNamespace_DoesNotWarn() { + public void DeclaringApplicationModuleInTheRootNamespace_DoesNotWarn() + { var result = Run(DeclaredModule("ConfiguredRoot")); Assert.DoesNotContain( result.CompilationDiagnostics.Concat(result.GeneratorDiagnostics), - diagnostic => diagnostic.Id == "CS8785"); + diagnostic => diagnostic.Id == "CS8785" + ); } /// @@ -55,7 +58,8 @@ public void DeclaringApplicationModuleInTheRootNamespace_DoesNotWarn() { /// recognised the two as the same — this only ever failed to recognise them. /// [Fact] - public void TheDeclaredModuleIsTheOneGenerated() { + public void TheDeclaredModuleIsTheOneGenerated() + { var result = Run(DeclaredModule("ConfiguredRoot")); var module = result.SourceContaining("ApplicationModule.Module"); @@ -68,7 +72,8 @@ public void TheDeclaredModuleIsTheOneGenerated() { /// surviving one carries what the project asked for. /// [Fact] - public void TheSurvivingModuleStillRegistersTheProjectsServices() { + public void TheSurvivingModuleStillRegistersTheProjectsServices() + { var result = Run(DeclaredModule("ConfiguredRoot")); Assert.Contains("Thing", result.SourceContaining("ApplicationModule.Dependencies")); @@ -80,23 +85,33 @@ public void TheSurvivingModuleStillRegistersTheProjectsServices() { /// [Theory] // A different name never collides with the generated ApplicationModule. - [InlineData("different name", """ - namespace ConfiguredRoot; - [DependencyModules.Runtime.Attributes.DependencyModule] - public partial class CompositionModule; - """)] + [InlineData( + "different name", + """ + namespace ConfiguredRoot; + [DependencyModules.Runtime.Attributes.DependencyModule] + public partial class CompositionModule; + """ + )] // A namespace other than RootNamespace produces a different hint name. - [InlineData("different namespace", """ - namespace SomewhereElse; - [DependencyModules.Runtime.Attributes.DependencyModule] - public partial class ApplicationModule; - """)] + [InlineData( + "different namespace", + """ + namespace SomewhereElse; + [DependencyModules.Runtime.Attributes.DependencyModule] + public partial class ApplicationModule; + """ + )] // The global namespace, likewise. - [InlineData("global namespace", """ - [DependencyModules.Runtime.Attributes.DependencyModule] - public partial class ApplicationModule; - """)] - public void AShapeThatNeverCollided_StillBuildsCleanly(string variant, string declaration) { + [InlineData( + "global namespace", + """ + [DependencyModules.Runtime.Attributes.DependencyModule] + public partial class ApplicationModule; + """ + )] + public void AShapeThatNeverCollided_StillBuildsCleanly(string variant, string declaration) + { var result = Run(declaration); Assert.Empty(result.DuplicateHintNames); @@ -115,22 +130,24 @@ public void AShapeThatNeverCollided_StillBuildsCleanly(string variant, string de /// lose it, and this fix makes the shape work either way. /// [Fact] - public void AnExplicitMainWithADeclaredApplicationModule_BuildsCleanly() { + public void AnExplicitMainWithADeclaredApplicationModule_BuildsCleanly() + { var result = GeneratorTestHarness.Run( - new Dictionary { - ["Program.cs"] = - """ - namespace ConfiguredRoot; - - public static class Program { - public static void Main() => System.Console.WriteLine("hello"); - } - """, + new Dictionary + { + ["Program.cs"] = """ + namespace ConfiguredRoot; + + public static class Program { + public static void Main() => System.Console.WriteLine("hello"); + } + """, ["Composition.cs"] = DeclaredModule("ConfiguredRoot"), - ["Services.cs"] = Services + ["Services.cs"] = Services, }, new Dictionary { ["RootNamespace"] = "ConfiguredRoot" }, - OutputKind.ConsoleApplication); + OutputKind.ConsoleApplication + ); Assert.Empty(result.DuplicateHintNames); result.AssertNoErrors(); @@ -138,14 +155,13 @@ public static class Program { private static string DeclaredModule(string namespaceName) => $$""" - namespace {{namespaceName}}; + namespace {{namespaceName}}; - [DependencyModules.Runtime.Attributes.DependencyModule] - public partial class ApplicationModule; - """; + [DependencyModules.Runtime.Attributes.DependencyModule] + public partial class ApplicationModule; + """; - private const string Services = - """ + private const string Services = """ namespace ConfiguredRoot; public interface IThing; @@ -156,13 +172,15 @@ public class Thing : IThing; private static GeneratorResult Run(string declaration) => GeneratorTestHarness.Run( - new Dictionary { + new Dictionary + { // Top-level statements: this is what makes the generator emit its own // ApplicationModule into RootNamespace. ["Program.cs"] = """System.Console.WriteLine("hello");""", ["Composition.cs"] = declaration, - ["Services.cs"] = Services + ["Services.cs"] = Services, }, new Dictionary { ["RootNamespace"] = "ConfiguredRoot" }, - OutputKind.ConsoleApplication); + OutputKind.ConsoleApplication + ); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/AssemblyModuleAttributeDiagnosticsTests.cs b/tests/DependencyModules.Tests/GeneratorTests/AssemblyModuleAttributeDiagnosticsTests.cs index 2ba099a..5d1f2e2 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/AssemblyModuleAttributeDiagnosticsTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/AssemblyModuleAttributeDiagnosticsTests.cs @@ -12,10 +12,9 @@ namespace DependencyModules.Tests.GeneratorTests; /// running, so it does not exist in the compilation being examined and nothing about it resolves. /// That makes the false positives the interesting cases, and most of these tests are one. /// -public class AssemblyModuleAttributeDiagnosticsTests { - - private const string ModuleInNamespace = - """ +public class AssemblyModuleAttributeDiagnosticsTests +{ + private const string ModuleInNamespace = """ namespace MyApp.Composition; [DependencyModules.Runtime.Attributes.DependencyModule] @@ -23,7 +22,8 @@ public partial class ApplicationModule; """; [Fact] - public void MissingUsing_IsReported() { + public void MissingUsing_IsReported() + { var result = Run("[assembly: ApplicationModule]"); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0016"); @@ -34,20 +34,23 @@ public void MissingUsing_IsReported() { /// The suffixed spelling names the same module. [Fact] - public void MissingUsing_IsReported_ForTheAttributeSuffixedSpelling() { + public void MissingUsing_IsReported_ForTheAttributeSuffixedSpelling() + { var result = Run("[assembly: ApplicationModuleAttribute]"); Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0016"); } [Fact] - public void TheUsingBeingPresent_IsSilent() { + public void TheUsingBeingPresent_IsSilent() + { var result = Run( """ using MyApp.Composition; [assembly: ApplicationModule] - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0016"); } @@ -57,19 +60,23 @@ public void TheUsingBeingPresent_IsSilent() { /// attribute sits in would report a build that is already correct. /// [Fact] - public void AGlobalUsingInAnotherFile_IsSilent() { + public void AGlobalUsingInAnotherFile_IsSilent() + { var result = GeneratorTestHarness.Run( - new Dictionary { + new Dictionary + { ["Module.cs"] = ModuleInNamespace, ["GlobalUsings.cs"] = "global using MyApp.Composition;", - ["Bootstrap.cs"] = "[assembly: ApplicationModule]" - }); + ["Bootstrap.cs"] = "[assembly: ApplicationModule]", + } + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0016"); } [Fact] - public void AQualifiedUsage_IsSilent() { + public void AQualifiedUsage_IsSilent() + { var result = Run("[assembly: MyApp.Composition.ApplicationModule]"); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0016"); @@ -77,7 +84,8 @@ public void AQualifiedUsage_IsSilent() { /// An attribute this compilation declares no module for belongs to somebody else. [Fact] - public void AnUnrelatedAssemblyAttribute_IsSilent() { + public void AnUnrelatedAssemblyAttribute_IsSilent() + { var result = Run("[assembly: System.Reflection.AssemblyMetadata(\"key\", \"value\")]"); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0016"); @@ -85,16 +93,18 @@ public void AnUnrelatedAssemblyAttribute_IsSilent() { /// A module in the global namespace has no namespace to import. [Fact] - public void AModuleInTheGlobalNamespace_IsSilent() { + public void AModuleInTheGlobalNamespace_IsSilent() + { var result = GeneratorTestHarness.Run( - new Dictionary { - ["Module.cs"] = - """ - [DependencyModules.Runtime.Attributes.DependencyModule] - public partial class ApplicationModule; - """, - ["Bootstrap.cs"] = "[assembly: ApplicationModule]" - }); + new Dictionary + { + ["Module.cs"] = """ + [DependencyModules.Runtime.Attributes.DependencyModule] + public partial class ApplicationModule; + """, + ["Bootstrap.cs"] = "[assembly: ApplicationModule]", + } + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0016"); } @@ -104,13 +114,15 @@ public partial class ApplicationModule; /// into scope under the name written here and the report still stands. /// [Fact] - public void AUsingAlias_DoesNotCountAsTheImport() { + public void AUsingAlias_DoesNotCountAsTheImport() + { var result = Run( """ using Composition = MyApp.Composition; [assembly: ApplicationModule] - """); + """ + ); Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0016"); } @@ -122,15 +134,16 @@ public void AUsingAlias_DoesNotCountAsTheImport() { /// resolve. /// [Fact] - public void AnAssemblyAttributeOutsideTheEntryPointFile_IsReported() { + public void AnAssemblyAttributeOutsideTheEntryPointFile_IsReported() + { var result = RunWithEntryPoint( - bootstrap: - """ + bootstrap: """ using MyApp.Composition; [assembly: ApplicationModule] """, - program: "System.Console.WriteLine();"); + program: "System.Console.WriteLine();" + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0019"); @@ -139,17 +152,18 @@ public void AnAssemblyAttributeOutsideTheEntryPointFile_IsReported() { } [Fact] - public void AnAssemblyAttributeInTheEntryPointFile_IsSilent() { + public void AnAssemblyAttributeInTheEntryPointFile_IsSilent() + { var result = RunWithEntryPoint( bootstrap: "", - program: - """ + program: """ using MyApp.Composition; [assembly: ApplicationModule] System.Console.WriteLine(); - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0019"); } @@ -161,29 +175,35 @@ public void AnAssemblyAttributeInTheEntryPointFile_IsSilent() { /// what the testing guide shows. /// [Fact] - public void WithNoGeneratedApplicationModule_IsSilent() { + public void WithNoGeneratedApplicationModule_IsSilent() + { var result = Run( """ using MyApp.Composition; [assembly: ApplicationModule] - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0019"); } private static GeneratorResult Run(string bootstrap) => GeneratorTestHarness.Run( - new Dictionary { + new Dictionary + { ["Module.cs"] = ModuleInNamespace, - ["Bootstrap.cs"] = bootstrap - }); + ["Bootstrap.cs"] = bootstrap, + } + ); private static GeneratorResult RunWithEntryPoint(string bootstrap, string program) => GeneratorTestHarness.Run( - new Dictionary { + new Dictionary + { ["Module.cs"] = ModuleInNamespace, ["Bootstrap.cs"] = bootstrap, - ["Program.cs"] = program - }); + ["Program.cs"] = program, + } + ); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/AttributeModelOutputTests.cs b/tests/DependencyModules.Tests/GeneratorTests/AttributeModelOutputTests.cs index 9c04975..757ce82 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/AttributeModelOutputTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/AttributeModelOutputTests.cs @@ -9,53 +9,72 @@ namespace DependencyModules.Tests.GeneratorTests; /// AttributeModel turns the values captured from a source attribute back into C# for the generated /// module attribute. Each supported value shape has to round-trip into code that compiles. /// -public class AttributeModelOutputTests { - +public class AttributeModelOutputTests +{ private static readonly ITypeDefinition SomeType = TypeDefinition.Get("Ns", "SomeType"); [Fact] - public void GetArguments_WithNoArguments_ReturnsNothing() { + public void GetArguments_WithNoArguments_ReturnsNothing() + { Assert.Empty(Attribute().GetArguments()); } [Fact] - public void GetArguments_QuotesStringValues() { - var rendered = Render(Attribute(arguments: [new AttributeArgumentValue("name", "hello")]).GetArguments()); + public void GetArguments_QuotesStringValues() + { + var rendered = Render( + Attribute(arguments: [new AttributeArgumentValue("name", "hello")]).GetArguments() + ); Assert.Contains("\"hello\"", rendered); } [Fact] - public void GetArguments_WritesTypeValuesAsTypeof() { - var rendered = Render(Attribute(arguments: [new AttributeArgumentValue("type", SomeType)]).GetArguments()); + public void GetArguments_WritesTypeValuesAsTypeof() + { + var rendered = Render( + Attribute(arguments: [new AttributeArgumentValue("type", SomeType)]).GetArguments() + ); Assert.Contains("typeof(", rendered); Assert.Contains("SomeType", rendered); } [Fact] - public void GetArguments_WritesPrimitiveValuesVerbatim() { - var rendered = Render(Attribute(arguments: [new AttributeArgumentValue("count", 42)]).GetArguments()); + public void GetArguments_WritesPrimitiveValuesVerbatim() + { + var rendered = Render( + Attribute(arguments: [new AttributeArgumentValue("count", 42)]).GetArguments() + ); Assert.Contains("42", rendered); } [Fact] - public void GetArguments_WritesBooleanValues() { - var rendered = Render(Attribute(arguments: [new AttributeArgumentValue("flag", true)]).GetArguments()); + public void GetArguments_WritesBooleanValues() + { + var rendered = Render( + Attribute(arguments: [new AttributeArgumentValue("flag", true)]).GetArguments() + ); Assert.Contains("True", rendered, StringComparison.OrdinalIgnoreCase); } [Fact] - public void GetArguments_SkipsNullValues() { - Assert.Empty(Attribute(arguments: [new AttributeArgumentValue("nothing", null)]).GetArguments()); + public void GetArguments_SkipsNullValues() + { + Assert.Empty( + Attribute(arguments: [new AttributeArgumentValue("nothing", null)]).GetArguments() + ); } [Fact] - public void GetArguments_WritesStringArraysAsACollection() { - var rendered = Render(Attribute( - arguments: [new AttributeArgumentValue("names", new[] { "a", "b" })]).GetArguments()); + public void GetArguments_WritesStringArraysAsACollection() + { + var rendered = Render( + Attribute(arguments: [new AttributeArgumentValue("names", new[] { "a", "b" })]) + .GetArguments() + ); Assert.StartsWith("[", rendered); Assert.EndsWith("]", rendered); @@ -65,34 +84,50 @@ public void GetArguments_WritesStringArraysAsACollection() { } [Fact] - public void GetArguments_PassesThroughOutputComponents() { + public void GetArguments_PassesThroughOutputComponents() + { var component = CodeOutputComponent.Get("SomeExpression"); - var rendered = Render(Attribute(arguments: [new AttributeArgumentValue("value", component)]).GetArguments()); + var rendered = Render( + Attribute(arguments: [new AttributeArgumentValue("value", component)]).GetArguments() + ); Assert.Contains("SomeExpression", rendered); } [Fact] - public void GetArguments_PreservesArgumentOrder() { - var rendered = Render(Attribute(arguments: [ - new AttributeArgumentValue("first", "one"), - new AttributeArgumentValue("second", "two") - ]).GetArguments()); - - Assert.True(rendered.IndexOf("one", StringComparison.Ordinal) < rendered.IndexOf("two", StringComparison.Ordinal), - $"Arguments came out in the wrong order: {rendered}"); + public void GetArguments_PreservesArgumentOrder() + { + var rendered = Render( + Attribute( + arguments: + [ + new AttributeArgumentValue("first", "one"), + new AttributeArgumentValue("second", "two"), + ] + ) + .GetArguments() + ); + + Assert.True( + rendered.IndexOf("one", StringComparison.Ordinal) + < rendered.IndexOf("two", StringComparison.Ordinal), + $"Arguments came out in the wrong order: {rendered}" + ); } [Fact] - public void PropertyValues_WithNoProperties_ReturnsNothing() { + public void PropertyValues_WithNoProperties_ReturnsNothing() + { Assert.Empty(Attribute().PropertyValues()); } [Fact] - public void PropertyValues_WritesNamedAssignments() { - var rendered = Render(Attribute( - properties: [new AttributeArgumentValue("Name", "value")]).PropertyValues()); + public void PropertyValues_WritesNamedAssignments() + { + var rendered = Render( + Attribute(properties: [new AttributeArgumentValue("Name", "value")]).PropertyValues() + ); Assert.Contains("Name", rendered); Assert.Contains("=", rendered); @@ -104,34 +139,44 @@ public void PropertyValues_WritesNamedAssignments() { /// a doubly-quoted literal. /// [Fact] - public void PropertyValues_DoesNotDoubleQuoteAnAlreadyQuotedString() { - var rendered = Render(Attribute( - properties: [new AttributeArgumentValue("Name", "\"value\"")]).PropertyValues()); + public void PropertyValues_DoesNotDoubleQuoteAnAlreadyQuotedString() + { + var rendered = Render( + Attribute(properties: [new AttributeArgumentValue("Name", "\"value\"")]) + .PropertyValues() + ); Assert.DoesNotContain("\"\"", rendered); Assert.Contains("\"value\"", rendered); } [Fact] - public void PropertyValues_WritesTypeValuesAsTypeof() { - var rendered = Render(Attribute( - properties: [new AttributeArgumentValue("As", SomeType)]).PropertyValues()); + public void PropertyValues_WritesTypeValuesAsTypeof() + { + var rendered = Render( + Attribute(properties: [new AttributeArgumentValue("As", SomeType)]).PropertyValues() + ); Assert.Contains("typeof(", rendered); } [Fact] - public void PropertyValues_SkipsNullValues() { - Assert.Empty(Attribute(properties: [new AttributeArgumentValue("Name", null)]).PropertyValues()); + public void PropertyValues_SkipsNullValues() + { + Assert.Empty( + Attribute(properties: [new AttributeArgumentValue("Name", null)]).PropertyValues() + ); } [Fact] - public void CollectionSyntax_WithNoItems_WritesEmptyBrackets() { + public void CollectionSyntax_WithNoItems_WritesEmptyBrackets() + { Assert.Equal("[]", Render(new CollectionSyntaxDeclaration())); } [Fact] - public void CollectionSyntax_QuotesStringItems() { + public void CollectionSyntax_QuotesStringItems() + { var collection = new CollectionSyntaxDeclaration(); collection.Add("value"); @@ -139,7 +184,8 @@ public void CollectionSyntax_QuotesStringItems() { } [Fact] - public void CollectionSyntax_SeparatesItemsWithCommas() { + public void CollectionSyntax_SeparatesItemsWithCommas() + { var collection = new CollectionSyntaxDeclaration(); collection.Add("one"); collection.Add("two"); @@ -148,7 +194,8 @@ public void CollectionSyntax_SeparatesItemsWithCommas() { } [Fact] - public void CollectionSyntax_WritesNonStringItemsVerbatim() { + public void CollectionSyntax_WritesNonStringItemsVerbatim() + { var collection = new CollectionSyntaxDeclaration(); collection.Add(1); collection.Add(2); @@ -157,7 +204,8 @@ public void CollectionSyntax_WritesNonStringItemsVerbatim() { } [Fact] - public void CollectionSyntax_WithTheSameItems_IsEqual() { + public void CollectionSyntax_WithTheSameItems_IsEqual() + { var first = new CollectionSyntaxDeclaration(); first.Add("a"); @@ -168,7 +216,8 @@ public void CollectionSyntax_WithTheSameItems_IsEqual() { } [Fact] - public void CollectionSyntax_WithDifferentItems_IsNotEqual() { + public void CollectionSyntax_WithDifferentItems_IsNotEqual() + { var first = new CollectionSyntaxDeclaration(); first.Add("a"); @@ -179,7 +228,8 @@ public void CollectionSyntax_WithDifferentItems_IsNotEqual() { } [Fact] - public void CollectionSyntax_WithDifferentCounts_IsNotEqual() { + public void CollectionSyntax_WithDifferentCounts_IsNotEqual() + { var first = new CollectionSyntaxDeclaration(); first.Add("a"); @@ -187,19 +237,24 @@ public void CollectionSyntax_WithDifferentCounts_IsNotEqual() { } [Fact] - public void CollectionSyntax_IsNotEqualToOtherTypes() { + public void CollectionSyntax_IsNotEqualToOtherTypes() + { Assert.False(new CollectionSyntaxDeclaration().Equals("not a collection")); } private static AttributeModel Attribute( IReadOnlyList? arguments = null, - IReadOnlyList? properties = null) => - new(SomeType, arguments ?? [], properties ?? [], []); + IReadOnlyList? properties = null + ) => new(SomeType, arguments ?? [], properties ?? [], []); - private static string Render(IEnumerable components) { - var context = new OutputContext(new OutputContextOptions { TypeOutputMode = TypeOutputMode.Global }); + private static string Render(IEnumerable components) + { + var context = new OutputContext( + new OutputContextOptions { TypeOutputMode = TypeOutputMode.Global } + ); - foreach (var component in components) { + foreach (var component in components) + { component.WriteOutput(context); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/AutoModuleDelegationTests.cs b/tests/DependencyModules.Tests/GeneratorTests/AutoModuleDelegationTests.cs index 74b35c6..2e55b68 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/AutoModuleDelegationTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/AutoModuleDelegationTests.cs @@ -19,45 +19,62 @@ namespace DependencyModules.Tests.GeneratorTests; /// and the runtime loads it. These tests pin both halves - that the duplicate is gone, and that /// AddModule<ApplicationModule>() still registers exactly what it did before. /// -public class AutoModuleDelegationTests { - +public class AutoModuleDelegationTests +{ [Fact] - public void ApplicationModule_DoesNotRepeatTheRegistrationsOfADeclaredModule() { - var result = Run(TopLevelProgramWith( - """ - public interface IThing; + public void ApplicationModule_DoesNotRepeatTheRegistrationsOfADeclaredModule() + { + var result = Run( + TopLevelProgramWith( + """ + public interface IThing; - [SingletonService] - public class Thing : IThing; + [SingletonService] + public class Thing : IThing; - [DependencyModule] - public partial class TestModule; - """)); + [DependencyModule] + public partial class TestModule; + """ + ) + ); result.AssertNoErrors(); - Assert.Contains(result.GeneratedSources.Keys, key => key.Contains("TestModule.Dependencies")); - Assert.DoesNotContain(result.GeneratedSources.Keys, key => key.Contains("ApplicationModule.Dependencies")); + Assert.Contains( + result.GeneratedSources.Keys, + key => key.Contains("TestModule.Dependencies") + ); + Assert.DoesNotContain( + result.GeneratedSources.Keys, + key => key.Contains("ApplicationModule.Dependencies") + ); } /// /// The class is still generated, and still reachable - only its registrations moved. /// [Fact] - public void ApplicationModule_NamesTheModuleItDefersTo() { - var result = Run(TopLevelProgramWith( - """ - public interface IThing; + public void ApplicationModule_NamesTheModuleItDefersTo() + { + var result = Run( + TopLevelProgramWith( + """ + public interface IThing; - [SingletonService] - public class Thing : IThing; + [SingletonService] + public class Thing : IThing; - [DependencyModule] - public partial class TestModule; - """)); + [DependencyModule] + public partial class TestModule; + """ + ) + ); result.AssertNoErrors(); - Assert.Contains("new global::TestNamespace.TestModule()", result.SourceContaining("ApplicationModule.Module")); + Assert.Contains( + "new global::TestNamespace.TestModule()", + result.SourceContaining("ApplicationModule.Module") + ); } /// @@ -65,42 +82,56 @@ public partial class TestModule; /// same way and have to stop being duplicated the same way. /// [Fact] - public void ApplicationModule_DoesNotRepeatDecorationsEither() { - var result = Run(TopLevelProgramWith( - """ - public interface IThing; + public void ApplicationModule_DoesNotRepeatDecorationsEither() + { + var result = Run( + TopLevelProgramWith( + """ + public interface IThing; - [SingletonService] - public class Thing : IThing; + [SingletonService] + public class Thing : IThing; - [Decorator] - public class ThingDecorator(IThing inner) : IThing; + [Decorator] + public class ThingDecorator(IThing inner) : IThing; - [DependencyModule] - public partial class TestModule; - """)); + [DependencyModule] + public partial class TestModule; + """ + ) + ); result.AssertNoErrors(); Assert.Contains(result.GeneratedSources.Keys, key => key.Contains("TestModule.Decorators")); - Assert.DoesNotContain(result.GeneratedSources.Keys, key => key.Contains("ApplicationModule.Decorators")); + Assert.DoesNotContain( + result.GeneratedSources.Keys, + key => key.Contains("ApplicationModule.Decorators") + ); } /// /// With nothing to defer to, the auto module carries its own registrations exactly as before. /// [Fact] - public void ApplicationModule_KeepsItsRegistrationsWhenNoModuleIsDeclared() { - var result = Run(TopLevelProgramWith( - """ - public interface IThing; + public void ApplicationModule_KeepsItsRegistrationsWhenNoModuleIsDeclared() + { + var result = Run( + TopLevelProgramWith( + """ + public interface IThing; - [SingletonService] - public class Thing : IThing; - """)); + [SingletonService] + public class Thing : IThing; + """ + ) + ); result.AssertNoErrors(); - Assert.Contains(result.GeneratedSources.Keys, key => key.Contains("ApplicationModule.Dependencies")); + Assert.Contains( + result.GeneratedSources.Keys, + key => key.Contains("ApplicationModule.Dependencies") + ); } /// @@ -108,46 +139,55 @@ public class Thing : IThing; /// drop everything else. The auto module keeps its own registrations in that case. /// [Fact] - public void ApplicationModule_KeepsItsRegistrationsWhenTheOnlyModuleIsRealmRestricted() { - var result = Run(TopLevelProgramWith( - """ - public interface IThing; + public void ApplicationModule_KeepsItsRegistrationsWhenTheOnlyModuleIsRealmRestricted() + { + var result = Run( + TopLevelProgramWith( + """ + public interface IThing; - [SingletonService] - public class Thing : IThing; + [SingletonService] + public class Thing : IThing; - [DependencyModule(OnlyRealm = true)] - public partial class RealmModule; - """)); + [DependencyModule(OnlyRealm = true)] + public partial class RealmModule; + """ + ) + ); result.AssertNoErrors(); - Assert.Contains(result.GeneratedSources.Keys, key => key.Contains("ApplicationModule.Dependencies")); + Assert.Contains( + result.GeneratedSources.Keys, + key => key.Contains("ApplicationModule.Dependencies") + ); } /// /// The point of the whole exercise: what reaches the service collection is unchanged. /// [Fact] - public void ApplicationModule_RegistersTheSameServicesAsTheModuleItDefersTo() { - var assembly = Compile(TopLevelProgramWith( - """ - public interface IThing; + public void ApplicationModule_RegistersTheSameServicesAsTheModuleItDefersTo() + { + var assembly = Compile( + TopLevelProgramWith( + """ + public interface IThing; - [SingletonService] - public class Thing : IThing; + [SingletonService] + public class Thing : IThing; - [DependencyModule] - public partial class TestModule; - """)); + [DependencyModule] + public partial class TestModule; + """ + ) + ); var viaApplicationModule = Apply(assembly, "TestNamespace.ApplicationModule"); var viaDeclaredModule = Apply(assembly, "TestNamespace.TestModule"); var thing = assembly.GetType("TestNamespace.IThing")!; - Assert.Equal( - Describe(viaDeclaredModule, thing), - Describe(viaApplicationModule, thing)); + Assert.Equal(Describe(viaDeclaredModule, thing), Describe(viaApplicationModule, thing)); Assert.NotNull(viaApplicationModule.BuildServiceProvider().GetService(thing)); } @@ -158,20 +198,27 @@ public partial class TestModule; /// deduplication sees them as one. /// [Fact] - public void LoadingBothModules_RegistersEachServiceOnce() { - var assembly = Compile(TopLevelProgramWith( - """ - public interface IThing; + public void LoadingBothModules_RegistersEachServiceOnce() + { + var assembly = Compile( + TopLevelProgramWith( + """ + public interface IThing; - [SingletonService] - public class Thing : IThing; + [SingletonService] + public class Thing : IThing; - [DependencyModule] - public partial class TestModule; - """)); + [DependencyModule] + public partial class TestModule; + """ + ) + ); var both = new ServiceCollection(); - both.AddModules(Module(assembly, "TestNamespace.ApplicationModule"), Module(assembly, "TestNamespace.TestModule")); + both.AddModules( + Module(assembly, "TestNamespace.ApplicationModule"), + Module(assembly, "TestNamespace.TestModule") + ); var thing = assembly.GetType("TestNamespace.IThing")!; @@ -183,9 +230,13 @@ private static string Describe(IServiceCollection services, Type serviceType) => ", ", services .Where(descriptor => descriptor.ServiceType == serviceType) - .Select(descriptor => $"{descriptor.Lifetime}:{descriptor.ImplementationType?.FullName}")); + .Select(descriptor => + $"{descriptor.Lifetime}:{descriptor.ImplementationType?.FullName}" + ) + ); - private static IServiceCollection Apply(Assembly assembly, string moduleName) { + private static IServiceCollection Apply(Assembly assembly, string moduleName) + { var services = new ServiceCollection(); services.AddModules(Module(assembly, moduleName)); @@ -193,21 +244,26 @@ private static IServiceCollection Apply(Assembly assembly, string moduleName) { return services; } - private static IDependencyModule Module(Assembly assembly, string moduleName) { - var type = assembly.GetType(moduleName) - ?? throw new InvalidOperationException( - $"No type '{moduleName}'. Present: " + - string.Join(", ", assembly.GetTypes().Select(t => t.FullName))); + private static IDependencyModule Module(Assembly assembly, string moduleName) + { + var type = + assembly.GetType(moduleName) + ?? throw new InvalidOperationException( + $"No type '{moduleName}'. Present: " + + string.Join(", ", assembly.GetTypes().Select(t => t.FullName)) + ); return (IDependencyModule)Activator.CreateInstance(type)!; } - private static Assembly Compile(IReadOnlyDictionary sources) { + private static Assembly Compile(IReadOnlyDictionary sources) + { var result = GeneratorTestHarness.Run( sources, null, OutputKind.ConsoleApplication, - assemblyName: "AutoModuleDelegation" + Interlocked.Increment(ref _counter)); + assemblyName: "AutoModuleDelegation" + Interlocked.Increment(ref _counter) + ); result.AssertNoErrors(); @@ -218,9 +274,13 @@ private static Assembly Compile(IReadOnlyDictionary sources) { emitted.Success, string.Join( Environment.NewLine, - emitted.Diagnostics - .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) - .Select(diagnostic => $" {diagnostic.Id} {diagnostic.GetMessage()}"))); + emitted + .Diagnostics.Where(diagnostic => + diagnostic.Severity == DiagnosticSeverity.Error + ) + .Select(diagnostic => $" {diagnostic.Id} {diagnostic.GetMessage()}") + ) + ); return Assembly.Load(stream.ToArray()); } @@ -231,18 +291,17 @@ private static GeneratorResult Run(IReadOnlyDictionary sources) GeneratorTestHarness.Run(sources, null, OutputKind.ConsoleApplication); private static Dictionary TopLevelProgramWith(string services) => - new() { - ["Program.cs"] = - """ + new() + { + ["Program.cs"] = """ System.Console.WriteLine("hello"); """, - ["Services.cs"] = - $$""" - using DependencyModules.Runtime.Attributes; + ["Services.cs"] = $$""" + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - {{services}} - """ + {{services}} + """, }; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/ConfigurationTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ConfigurationTests.cs index 36b2fb5..baee62a 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ConfigurationTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ConfigurationTests.cs @@ -10,10 +10,11 @@ namespace DependencyModules.Tests.GeneratorTests; /// The generator is configured through MSBuild properties surfaced as CompilerVisibleProperty in /// the packaged build/*.targets. These tests pin the mapping from property value to generated code. /// -public class ConfigurationTests { - +public class ConfigurationTests +{ [Fact] - public void ExcludeGeneratedCodeFromCoverage_DefaultsToOn() { + public void ExcludeGeneratedCodeFromCoverage_DefaultsToOn() + { var result = GeneratorTestHarness.Run(ModuleWithService); result.AssertNoErrors(); @@ -21,20 +22,24 @@ public void ExcludeGeneratedCodeFromCoverage_DefaultsToOn() { } [Fact] - public void ExcludeGeneratedCodeFromCoverage_False_OmitsTheAttribute() { + public void ExcludeGeneratedCodeFromCoverage_False_OmitsTheAttribute() + { var result = GeneratorTestHarness.Run( ModuleWithService, - new Dictionary { ["ExcludeGeneratedCodeFromCoverage"] = "false" }); + new Dictionary { ["ExcludeGeneratedCodeFromCoverage"] = "false" } + ); result.AssertNoErrors(); Assert.DoesNotContain("ExcludeFromCodeCoverage", result.SourceContaining("Dependencies")); } [Fact] - public void ExcludeGeneratedCodeFromCoverage_IsCaseInsensitive() { + public void ExcludeGeneratedCodeFromCoverage_IsCaseInsensitive() + { var result = GeneratorTestHarness.Run( ModuleWithService, - new Dictionary { ["ExcludeGeneratedCodeFromCoverage"] = "FALSE" }); + new Dictionary { ["ExcludeGeneratedCodeFromCoverage"] = "FALSE" } + ); result.AssertNoErrors(); Assert.DoesNotContain("ExcludeFromCodeCoverage", result.SourceContaining("Dependencies")); @@ -45,60 +50,68 @@ public void ExcludeGeneratedCodeFromCoverage_IsCaseInsensitive() { /// statement Program.cs; explicitly declared modules keep whatever namespace they are written in. /// [Fact] - public void RootNamespace_NamesTheAutoGeneratedApplicationModule() { + public void RootNamespace_NamesTheAutoGeneratedApplicationModule() + { var result = GeneratorTestHarness.Run( - new Dictionary { - ["Program.cs"] = - """ - using DependencyModules.Runtime.Attributes; - - System.Console.WriteLine("hello"); - """, - ["Services.cs"] = - """ - namespace TestNamespace; - - public interface IThing; - - [DependencyModules.Runtime.Attributes.SingletonService] - public class Thing : IThing; - """ + new Dictionary + { + ["Program.cs"] = """ + using DependencyModules.Runtime.Attributes; + + System.Console.WriteLine("hello"); + """, + ["Services.cs"] = """ + namespace TestNamespace; + + public interface IThing; + + [DependencyModules.Runtime.Attributes.SingletonService] + public class Thing : IThing; + """, }, new Dictionary { ["RootNamespace"] = "ConfiguredRoot" }, - OutputKind.ConsoleApplication); + OutputKind.ConsoleApplication + ); result.AssertNoErrors(); - Assert.Contains("namespace ConfiguredRoot", result.SourceContaining("ApplicationModule.Module")); + Assert.Contains( + "namespace ConfiguredRoot", + result.SourceContaining("ApplicationModule.Module") + ); } [Fact] - public void AutoGenerateModule_False_SuppressesTheApplicationModule() { + public void AutoGenerateModule_False_SuppressesTheApplicationModule() + { var result = GeneratorTestHarness.Run( - new Dictionary { - ["Program.cs"] = - """ - System.Console.WriteLine("hello"); - """, - ["Services.cs"] = - """ - using DependencyModules.Runtime.Attributes; + new Dictionary + { + ["Program.cs"] = """ + System.Console.WriteLine("hello"); + """, + ["Services.cs"] = """ + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - public interface IThing; + public interface IThing; - [SingletonService] - public class Thing : IThing; + [SingletonService] + public class Thing : IThing; - [DependencyModule] - public partial class TestModule; - """ + [DependencyModule] + public partial class TestModule; + """, }, new Dictionary { ["DependencyModules_AutoGenerateModule"] = "false" }, - OutputKind.ConsoleApplication); + OutputKind.ConsoleApplication + ); result.AssertNoErrors(); - Assert.DoesNotContain(result.GeneratedSources.Keys, key => key.Contains("ApplicationModule")); + Assert.DoesNotContain( + result.GeneratedSources.Keys, + key => key.Contains("ApplicationModule") + ); } [Theory] @@ -109,22 +122,29 @@ public partial class TestModule; [InlineData("RegistrationType.Try", RegistrationType.Try)] [InlineData("", RegistrationType.Add)] [InlineData("nonsense", RegistrationType.Add)] - public void GetRegistrationType_ParsesKnownValuesAndFallsBackToAdd(string input, RegistrationType expected) { + public void GetRegistrationType_ParsesKnownValuesAndFallsBackToAdd( + string input, + RegistrationType expected + ) + { Assert.Equal(expected, BaseSourceGenerator.GetRegistrationType(input)); } [Fact] - public void DependencyModulesRegistrationType_ChangesTheDefaultRegistrationMethod() { + public void DependencyModulesRegistrationType_ChangesTheDefaultRegistrationMethod() + { var result = GeneratorTestHarness.Run( ModuleWithService, - new Dictionary { ["DependencyModules_RegistrationType"] = "Try" }); + new Dictionary { ["DependencyModules_RegistrationType"] = "Try" } + ); result.AssertNoErrors(); Assert.Contains("TryAdd", result.SourceContaining("Dependencies")); } [Fact] - public void GeneratedCodeStyle_DefaultsToAllman() { + public void GeneratedCodeStyle_DefaultsToAllman() + { var result = GeneratorTestHarness.Run(ModuleWithService); result.AssertNoErrors(); @@ -132,21 +152,28 @@ public void GeneratedCodeStyle_DefaultsToAllman() { } [Fact] - public void GeneratedCodeStyle_KAndR_PutsTheBraceOnTheOpeningLine() { + public void GeneratedCodeStyle_KAndR_PutsTheBraceOnTheOpeningLine() + { var result = GeneratorTestHarness.Run( ModuleWithService, - new Dictionary { ["GeneratedCodeStyle"] = "KAndR" }); + new Dictionary { ["GeneratedCodeStyle"] = "KAndR" } + ); result.AssertNoErrors(); Assert.Contains("services) {", result.SourceContaining("Dependencies")); - Assert.Contains("public partial class TestModule {", result.SourceContaining("Dependencies")); + Assert.Contains( + "public partial class TestModule {", + result.SourceContaining("Dependencies") + ); } [Fact] - public void GeneratedCodeStyle_IsCaseInsensitive() { + public void GeneratedCodeStyle_IsCaseInsensitive() + { var result = GeneratorTestHarness.Run( ModuleWithService, - new Dictionary { ["GeneratedCodeStyle"] = "kandr" }); + new Dictionary { ["GeneratedCodeStyle"] = "kandr" } + ); result.AssertNoErrors(); Assert.Contains("services) {", result.SourceContaining("Dependencies")); @@ -157,17 +184,18 @@ public void GeneratedCodeStyle_IsCaseInsensitive() { /// stance DependencyModules_RegistrationType takes. /// [Fact] - public void GeneratedCodeStyle_UnknownValue_FallsBackToAllman() { + public void GeneratedCodeStyle_UnknownValue_FallsBackToAllman() + { var result = GeneratorTestHarness.Run( ModuleWithService, - new Dictionary { ["GeneratedCodeStyle"] = "whitesmiths" }); + new Dictionary { ["GeneratedCodeStyle"] = "whitesmiths" } + ); result.AssertNoErrors(); Assert.Contains("services)\n {", result.SourceContaining("Dependencies")); } - private const string ModuleWithService = - """ + private const string ModuleWithService = """ using DependencyModules.Runtime.Attributes; namespace TestNamespace; diff --git a/tests/DependencyModules.Tests/GeneratorTests/ConventionContractTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ConventionContractTests.cs index 3e0c57b..297113b 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ConventionContractTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ConventionContractTests.cs @@ -3,7 +3,6 @@ using DependencyModules.Tests.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Xunit; - using GeneratorNames = DependencyModules.Conventions.ConventionContractSource; namespace DependencyModules.Tests.GeneratorTests; @@ -23,20 +22,23 @@ namespace DependencyModules.Tests.GeneratorTests; /// registers nothing, which is the failure mode this generator exists to prevent everywhere else. /// /// -public class ConventionContractTests { - +public class ConventionContractTests +{ [Fact] - public void TheGeneratorLooksForTheNamespaceTheContractsAreDeclaredIn() { + public void TheGeneratorLooksForTheNamespaceTheContractsAreDeclaredIn() + { Assert.Equal(GeneratorNames.Namespace, typeof(IConventionModule).Namespace); } [Fact] - public void TheGeneratorLooksForTheInterfaceTheContractsDeclare() { + public void TheGeneratorLooksForTheInterfaceTheContractsDeclare() + { Assert.Equal(GeneratorNames.ConventionModule, nameof(IConventionModule)); } [Fact] - public void TheGeneratorLooksForTheMethodTheInterfaceDeclares() { + public void TheGeneratorLooksForTheMethodTheInterfaceDeclares() + { var method = Assert.Single(typeof(IConventionModule).GetMethods()); Assert.Equal(GeneratorNames.ConventionMethod, method.Name); @@ -51,8 +53,10 @@ public void TheGeneratorLooksForTheMethodTheInterfaceDeclares() { /// hurried addition gets wrong and nothing else would catch. /// [Fact] - public void EveryRegistrationVerbContinuesTheChain() { - var breaks = typeof(IConventionRegistration).GetMethods() + public void EveryRegistrationVerbContinuesTheChain() + { + var breaks = typeof(IConventionRegistration) + .GetMethods() .Where(method => method.ReturnType != typeof(IConventionRegistration)) .Select(method => method.Name) .ToArray(); @@ -64,8 +68,10 @@ public void EveryRegistrationVerbContinuesTheChain() { /// Every entry point produces a registration to continue from. /// [Fact] - public void EveryRegisterAllOverloadStartsTheChain() { - var breaks = typeof(IConventionDefinitions).GetMethods() + public void EveryRegisterAllOverloadStartsTheChain() + { + var breaks = typeof(IConventionDefinitions) + .GetMethods() .Where(method => method.ReturnType != typeof(IConventionRegistration)) .Select(method => method.Name) .ToArray(); @@ -73,8 +79,7 @@ public void EveryRegisterAllOverloadStartsTheChain() { Assert.Empty(breaks); } - private const string Preamble = - """ + private const string Preamble = """ using DependencyModules.Runtime.Attributes; using DependencyModules.Runtime.Conventions; @@ -98,41 +103,51 @@ public class Greeter : IGreeter { public string Greet() => "hello"; } /// that it stays retired. /// [Fact] - public void AnImplicitPublicImplementationDeclaresConventions() { + public void AnImplicitPublicImplementationDeclaresConventions() + { var assembly = GeneratedAssembly.Create( - Preamble + - """ - [DependencyModule] - public partial class TestModule : IConventionModule { - public void Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().AsSingleton(); + Preamble + + """ + [DependencyModule] + public partial class TestModule : IConventionModule { + public void Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().AsSingleton(); + } } - } - """); + """ + ); var provider = assembly.BuildProvider(); - Assert.Equal("hello", ((dynamic)provider.GetRequiredService(assembly.Type("IGreeter"))).Greet()); + Assert.Equal( + "hello", + ((dynamic)provider.GetRequiredService(assembly.Type("IGreeter"))).Greet() + ); } /// /// The explicit form still compiles and still registers, so nobody has to rewrite anything. /// [Fact] - public void TheExplicitImplementationStillDeclaresConventions() { + public void TheExplicitImplementationStillDeclaresConventions() + { var assembly = GeneratedAssembly.Create( - Preamble + - """ - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().AsSingleton(); + Preamble + + """ + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().AsSingleton(); + } } - } - """); + """ + ); var provider = assembly.BuildProvider(); - Assert.Equal("hello", ((dynamic)provider.GetRequiredService(assembly.Type("IGreeter"))).Greet()); + Assert.Equal( + "hello", + ((dynamic)provider.GetRequiredService(assembly.Type("IGreeter"))).Greet() + ); } } diff --git a/tests/DependencyModules.Tests/GeneratorTests/ConventionDecoratorTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ConventionDecoratorTests.cs index 184c03a..678838a 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ConventionDecoratorTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ConventionDecoratorTests.cs @@ -14,10 +14,9 @@ namespace DependencyModules.Tests.GeneratorTests; /// because an open generic registration cannot be decorated. The error blamed the open generic /// limitation, which is a long way from the cause. /// -public class ConventionDecoratorTests { - - private const string Preamble = - """ +public class ConventionDecoratorTests +{ + private const string Preamble = """ using System.Collections.Generic; using DependencyModules.Runtime.Attributes; using DependencyModules.Runtime.Conventions; @@ -62,7 +61,8 @@ private static GeneratedAssembly Compile(string module) => /// shape, and the reason this combination has to work. /// [Fact] - public void OneDecoratorWrapsEveryConventionRegisteredHandler() { + public void OneDecoratorWrapsEveryConventionRegisteredHandler() + { var assembly = Compile( """ [DependencyModule] @@ -71,7 +71,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(IRequestHandler<,>)).AsScoped(); } } - """); + """ + ); var provider = assembly.BuildProvider(); var log = provider.GetRequiredService(assembly.Type("Log")); @@ -80,15 +81,20 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { var handler = assembly.Type("IRequestHandler`2"); var orderId = assembly.Type("OrderId"); - foreach (var request in new[] { "CreateOrder", "RenameOrder" }) { + foreach (var request in new[] { "CreateOrder", "RenameOrder" }) + { var requestType = assembly.Type(request); - var service = provider.GetRequiredService(handler.MakeGenericType(requestType, orderId)); + var service = provider.GetRequiredService( + handler.MakeGenericType(requestType, orderId) + ); // Every handler resolves as the decorator, not as the implementation. Assert.Equal("LoggingHandler`2", service.GetType().Name); - service.GetType().GetMethod("Handle")!.Invoke( - service, new[] { Activator.CreateInstance(requestType) }); + service + .GetType() + .GetMethod("Handle")! + .Invoke(service, new[] { Activator.CreateInstance(requestType) }); } Assert.Equal(["handling CreateOrder", "handling RenameOrder"], lines); @@ -98,7 +104,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// The decorator itself is not registered as a service. /// [Fact] - public void ADecoratorIsNotAConventionCandidate() { + public void ADecoratorIsNotAConventionCandidate() + { var assembly = Compile( """ [DependencyModule] @@ -107,7 +114,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(IRequestHandler<,>)).AsScoped(); } } - """); + """ + ); // Two handlers, two registrations. The decorator rewrote them in place rather than adding // a third, and never registered itself as an open generic. @@ -120,7 +128,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// The exclusion is on the declaration, so it holds however the convention selects. /// [Fact] - public void ADecoratorIsExcludedWhenSelectedByFilterRatherThanInterface() { + public void ADecoratorIsExcludedWhenSelectedByFilterRatherThanInterface() + { var assembly = Compile( """ [DependencyModule] @@ -129,10 +138,17 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().WithName("*Handler").AsSelf().AsScoped(); } } - """); - - Assert.Contains(assembly.Services, d => d.ServiceType == assembly.Type("CreateOrderHandler")); - Assert.Contains(assembly.Services, d => d.ServiceType == assembly.Type("RenameOrderHandler")); + """ + ); + + Assert.Contains( + assembly.Services, + d => d.ServiceType == assembly.Type("CreateOrderHandler") + ); + Assert.Contains( + assembly.Services, + d => d.ServiceType == assembly.Type("RenameOrderHandler") + ); // LoggingHandler ends in "Handler" and would otherwise match. Assert.DoesNotContain(assembly.Services, d => d.ServiceType.Name == "LoggingHandler`2"); diff --git a/tests/DependencyModules.Tests/GeneratorTests/ConventionRegistrationTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ConventionRegistrationTests.cs index 91dfd86..0a0f137 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ConventionRegistrationTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ConventionRegistrationTests.cs @@ -14,10 +14,9 @@ namespace DependencyModules.Tests.GeneratorTests; /// source passes happily while the wrong service type is registered, which is exactly the class of /// mistake convention matching is most likely to make. /// -public class ConventionRegistrationTests { - - private const string Preamble = - """ +public class ConventionRegistrationTests +{ + private const string Preamble = """ using System; using DependencyModules.Runtime.Attributes; using DependencyModules.Runtime.Conventions; @@ -44,9 +43,9 @@ private static GeneratorResult Run(string source) => [Theory] [InlineData("Development", 2)] [InlineData("Production", 1)] - public void ConventionsHonourEnvironmentConditions(string environmentName, int expected) { - const string source = - """ + public void ConventionsHonourEnvironmentConditions(string environmentName, int expected) + { + const string source = """ public interface IFoo { } public class AlwaysFoo : IFoo { } @@ -64,7 +63,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { var assembly = GeneratedAssembly.Create( Preamble + source, - environment: new ModuleEnvironment(environmentName)); + environment: new ModuleEnvironment(environmentName) + ); Assert.Equal(expected, assembly.Descriptors("IFoo").Count); } @@ -76,9 +76,9 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { [Theory] [InlineData("Development", 2)] [InlineData("Production", 0)] - public void ConventionsCarryTheirOwnEnvironmentCondition(string environmentName, int expected) { - const string source = - """ + public void ConventionsCarryTheirOwnEnvironmentCondition(string environmentName, int expected) + { + const string source = """ public interface IFoo { } public class OneFoo : IFoo { } @@ -94,7 +94,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { var assembly = GeneratedAssembly.Create( Preamble + source, - environment: new ModuleEnvironment(environmentName)); + environment: new ModuleEnvironment(environmentName) + ); Assert.Equal(expected, assembly.Descriptors("IFoo").Count); } @@ -112,10 +113,12 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { [InlineData("Development", "us", 1)] [InlineData("Production", "eu", 0)] public void ConventionAndClassConditionsCombineWithAnd( - string environmentName, string region, int expected) { - - const string source = - """ + string environmentName, + string region, + int expected + ) + { + const string source = """ public interface IFoo { } public class PlainFoo : IFoo { } @@ -133,7 +136,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { var assembly = GeneratedAssembly.Create( Preamble + source, - environment: new ModuleEnvironment(false, environmentName) { { "REGION", region } }); + environment: new ModuleEnvironment(false, environmentName) { { "REGION", region } } + ); Assert.Equal(expected, assembly.Descriptors("IFoo").Count); } @@ -141,9 +145,9 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { [Theory] [InlineData("on", 1)] [InlineData("off", 0)] - public void ConventionsCarryValueConditions(string flag, int expected) { - const string source = - """ + public void ConventionsCarryValueConditions(string flag, int expected) + { + const string source = """ public interface IFoo { } public class OneFoo : IFoo { } @@ -158,7 +162,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { var assembly = GeneratedAssembly.Create( Preamble + source, - environment: new ModuleEnvironment(false, "Development") { { "FLAG", flag } }); + environment: new ModuleEnvironment(false, "Development") { { "FLAG", flag } } + ); Assert.Equal(expected, assembly.Descriptors("IFoo").Count); } @@ -166,9 +171,9 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { [Theory] [InlineData("Development", 0)] [InlineData("Production", 1)] - public void ConventionsCarryNegatedConditions(string environmentName, int expected) { - const string source = - """ + public void ConventionsCarryNegatedConditions(string environmentName, int expected) + { + const string source = """ public interface IFoo { } public class OneFoo : IFoo { } @@ -183,7 +188,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { var assembly = GeneratedAssembly.Create( Preamble + source, - environment: new ModuleEnvironment(environmentName)); + environment: new ModuleEnvironment(environmentName) + ); Assert.Equal(expected, assembly.Descriptors("IFoo").Count); } @@ -200,10 +206,12 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { [InlineData("Development", 1, 1)] [InlineData("Production", 0, 1)] public void ConventionsWithDifferentConditionsDoNotShareAGuard( - string environmentName, int expectedFoo, int expectedBar) { - - const string source = - """ + string environmentName, + int expectedFoo, + int expectedBar + ) + { + const string source = """ public interface IFoo { } public interface IBar { } @@ -220,7 +228,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { var assembly = GeneratedAssembly.Create( Preamble + source, - environment: new ModuleEnvironment(environmentName)); + environment: new ModuleEnvironment(environmentName) + ); Assert.Equal(expectedFoo, assembly.Descriptors("IFoo").Count); Assert.Equal(expectedBar, assembly.Descriptors("IBar").Count); @@ -236,9 +245,9 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// and registered nothing. One convention, named twice. /// [Fact] - public void APartialClassReachingTheServiceFromTwoPartsIsNotAmbiguous() { - const string source = - """ + public void APartialClassReachingTheServiceFromTwoPartsIsNotAmbiguous() + { + const string source = """ public interface IFoo { } public abstract class FooBase : IFoo { } @@ -265,9 +274,9 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// Two parts declaring different interfaces that both reach the scanned one is still one class. /// [Fact] - public void APartialClassDeclaringTheServiceTwiceIsNotAmbiguous() { - const string source = - """ + public void APartialClassDeclaringTheServiceTwiceIsNotAmbiguous() + { + const string source = """ public interface IFoo { } public interface IFooPrime : IFoo { } @@ -292,9 +301,9 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// needed even when another part reaches the same interface through a base class. /// [Fact] - public void APartDeclaringTheServiceMakesItADeclaredMatch() { - const string source = - """ + public void APartDeclaringTheServiceMakesItADeclaredMatch() + { + const string source = """ public interface IFoo { } public abstract class FooBase : IFoo { } @@ -322,9 +331,9 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// have, which is a CS error inside generated code. /// [Fact] - public void TheConstructorMayBeDeclaredInAnotherPart() { - const string source = - """ + public void TheConstructorMayBeDeclaredInAnotherPart() + { + const string source = """ public interface IDep { } [SingletonService] @@ -359,9 +368,9 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// and the two registrations are independently predictable from reading the module. /// [Fact] - public void ATypeMatchedThroughDifferentInterfacesRegistersAsBoth() { - const string source = - """ + public void ATypeMatchedThroughDifferentInterfacesRegistersAsBoth() + { + const string source = """ public interface IFoo { } public interface IBar { } @@ -391,7 +400,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// Sharing one instance is AsSelfWithInterfaces, and it is opt-in. /// [Fact] - public void EachRoleKeepsItsOwnLifetimeAndItsOwnInstance() { + public void EachRoleKeepsItsOwnLifetimeAndItsOwnInstance() + { var assembly = Compile( """ public interface IFoo { } @@ -406,7 +416,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsScoped(); } } - """); + """ + ); Assert.Equal(ServiceLifetime.Singleton, assembly.Descriptor("IFoo").Lifetime); Assert.Equal(ServiceLifetime.Scoped, assembly.Descriptor("IBar").Lifetime); @@ -415,7 +426,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { Assert.NotSame( provider.GetService(assembly.Type("IFoo")), - provider.GetService(assembly.Type("IBar"))); + provider.GetService(assembly.Type("IBar")) + ); } /// @@ -426,7 +438,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// green build, no diagnostic, and an event that never fires. /// [Fact] - public void OneConventionRegistersEveryClosingACandidateImplements() { + public void OneConventionRegistersEveryClosingACandidateImplements() + { var assembly = Compile( """ public interface INotificationHandler { } @@ -442,7 +455,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(INotificationHandler<>)).AsTransient(); } } - """); + """ + ); var handlerType = assembly.Type("INotificationHandler`1"); @@ -460,7 +474,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// implementations — the shape the attribute path produces and the writer relies on. /// [Fact] - public void SeveralClosingsProduceOneImplementationWithSeveralRegistrations() { + public void SeveralClosingsProduceOneImplementationWithSeveralRegistrations() + { var assembly = Compile( """ public interface IHandler { } @@ -476,17 +491,17 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(IHandler<>)).AsTransient(); } } - """); + """ + ); - var registered = assembly.Services - .Where(d => d.ImplementationType == assembly.Type("Both")) + var registered = assembly + .Services.Where(d => d.ImplementationType == assembly.Type("Both")) .ToArray(); Assert.Equal(2, registered.Length); } - private const string Marker = - """ + private const string Marker = """ public class HandlerAttribute : System.Attribute { } public class LegacyAttribute : System.Attribute { } @@ -503,20 +518,25 @@ public class MarkedLegacy : IFoo { } """; [Fact] - public void WithAttributeLimitsMatchesToTypesCarryingIt() { + public void WithAttributeLimitsMatchesToTypesCarryingIt() + { var assembly = Compile( - Marker + - """ - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().WithAttribute().AsSingleton(); + Marker + + """ + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().WithAttribute().AsSingleton(); + } } - } - """); + """ + ); - var implementations = assembly.Descriptors("IFoo").Select(d => d.ImplementationType).ToArray(); + var implementations = assembly + .Descriptors("IFoo") + .Select(d => d.ImplementationType) + .ToArray(); Assert.Equal(2, implementations.Length); Assert.Contains(assembly.Type("Marked"), implementations); @@ -525,41 +545,48 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { } [Fact] - public void WithoutAttributeExcludesTypesCarryingIt() { + public void WithoutAttributeExcludesTypesCarryingIt() + { var assembly = Compile( - Marker + - """ - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().WithoutAttribute().AsSingleton(); + Marker + + """ + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().WithoutAttribute().AsSingleton(); + } } - } - """); + """ + ); - var implementations = assembly.Descriptors("IFoo").Select(d => d.ImplementationType).ToArray(); + var implementations = assembly + .Descriptors("IFoo") + .Select(d => d.ImplementationType) + .ToArray(); Assert.Equal(2, implementations.Length); Assert.DoesNotContain(assembly.Type("MarkedLegacy"), implementations); } [Fact] - public void AttributeFiltersCombineWithAnd() { + public void AttributeFiltersCombineWithAnd() + { var assembly = Compile( - Marker + - """ - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll() - .WithAttribute() - .WithoutAttribute() - .AsSingleton(); + Marker + + """ + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll() + .WithAttribute() + .WithoutAttribute() + .AsSingleton(); + } } - } - """); + """ + ); Assert.Equal(assembly.Type("Marked"), assembly.Descriptor("IFoo").ImplementationType); } @@ -568,20 +595,22 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// Resolved rather than matched on how it was written, so the qualified form counts. /// [Fact] - public void ANamespaceQualifiedAttributeStillMatches() { + public void ANamespaceQualifiedAttributeStillMatches() + { var assembly = Compile( - Marker + - """ - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll() - .WithAttribute() - .AsSingleton(); + Marker + + """ + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll() + .WithAttribute() + .AsSingleton(); + } } - } - """); + """ + ); Assert.Equal(2, assembly.Descriptors("IFoo").Count); } @@ -592,48 +621,56 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { [InlineData("Order?epository", 1)] [InlineData("TestNamespace.*Repository", 2)] [InlineData("*repository", 0)] - public void WithNameMatchesTheGlob(string pattern, int expected) { + public void WithNameMatchesTheGlob(string pattern, int expected) + { var result = Run( $$""" - public class OrderRepository { } - public class UserRepository { } - public class OrderService { } - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().WithName("{{pattern}}").AsSelf().AsScoped(); - } - } - """); + public class OrderRepository { } + public class UserRepository { } + public class OrderService { } + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().WithName("{{pattern}}").AsSelf().AsScoped(); + } + } + """ + ); // A pattern matching nothing is DM0005 rather than silence. - if (expected == 0) { + if (expected == 0) + { Assert.Contains(result.GeneratorDiagnostics, d => d.Id == "DM0005"); return; } var assembly = GeneratedAssembly.Create( - Preamble + - $$""" - public class OrderRepository { } - public class UserRepository { } - public class OrderService { } - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().WithName("{{pattern}}").AsSelf().AsScoped(); - } - } - """); - - Assert.Equal(expected, assembly.Services.Count(d => d.ImplementationType?.Namespace == "TestNamespace")); + Preamble + + $$""" + public class OrderRepository { } + public class UserRepository { } + public class OrderService { } + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().WithName("{{pattern}}").AsSelf().AsScoped(); + } + } + """ + ); + + Assert.Equal( + expected, + assembly.Services.Count(d => d.ImplementationType?.Namespace == "TestNamespace") + ); } [Fact] - public void AsRegistersEveryMatchAsOneNamedService() { + public void AsRegistersEveryMatchAsOneNamedService() + { var assembly = Compile( """ public interface IFoo { } @@ -647,14 +684,16 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().As().AsSingleton(); } } - """); + """ + ); Assert.Empty(assembly.Descriptors("IFoo")); Assert.Equal(assembly.Type("Foo"), assembly.Descriptor("IMarker").ImplementationType); } [Fact] - public void AsMatchingInterfaceRegistersFooAsIFoo() { + public void AsMatchingInterfaceRegistersFooAsIFoo() + { var assembly = Compile( """ public interface IMarker { } @@ -670,7 +709,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsMatchingInterface().AsSingleton(); } } - """); + """ + ); Assert.Equal(assembly.Type("Foo"), assembly.Descriptor("IFoo").ImplementationType); Assert.Equal(assembly.Type("Bar"), assembly.Descriptor("IBar").ImplementationType); @@ -685,7 +725,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// which is never what "register this as its interfaces" means. /// [Fact] - public void AsSelfWithInterfacesSkipsSystemInterfaces() { + public void AsSelfWithInterfacesSkipsSystemInterfaces() + { var assembly = Compile( """ public interface IMarker { } @@ -702,7 +743,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSelfWithInterfaces().AsSingleton().IncludeBaseClasses(); } } - """); + """ + ); Assert.Single(assembly.Descriptors("IMarker")); @@ -710,7 +752,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { } [Fact] - public void AsSelfWithInterfacesSkipsGenericSystemInterfaces() { + public void AsSelfWithInterfacesSkipsGenericSystemInterfaces() + { var assembly = Compile( """ public interface IRule { } @@ -731,12 +774,16 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(IValidator<>)).AsSelfWithInterfaces().AsScoped().IncludeBaseClasses(); } } - """); + """ + ); // The validator interface survives; the enumerable ones do not. Assert.Single(assembly.Services, d => d.ServiceType.Name == "IValidator`1"); - Assert.DoesNotContain(assembly.Services, d => d.ServiceType.Namespace?.StartsWith("System") == true); + Assert.DoesNotContain( + assembly.Services, + d => d.ServiceType.Namespace?.StartsWith("System") == true + ); } /// @@ -744,7 +791,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// whatever namespace it lives in — refusing that is a different kind of wrong. /// [Fact] - public void ANamedSystemServiceTypeIsStillRegistered() { + public void ANamedSystemServiceTypeIsStillRegistered() + { var assembly = Compile( """ public class Closeable : System.IDisposable { @@ -757,7 +805,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); Assert.Single(assembly.Services, d => d.ServiceType == typeof(IDisposable)); } @@ -766,7 +815,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// Filtering everything away degrades to AsSelf rather than to nothing. /// [Fact] - public void ATypeReachingOnlySystemInterfacesRegistersItself() { + public void ATypeReachingOnlySystemInterfacesRegistersItself() + { var assembly = Compile( """ public interface IMarker { } @@ -781,15 +831,15 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSelf().AsSingleton().IncludeBaseClasses(); } } - """); + """ + ); // Nothing to assert beyond the module compiling and registering something; the shape that // matters is covered by AsSelfWithInterfacesSkipsSystemInterfaces. Assert.NotEmpty(assembly.Services); } - private const string Validators = - """ + private const string Validators = """ public interface IRule { } public interface IValidator { } public interface IValidator : IValidator { } @@ -808,18 +858,20 @@ public class FooValidator : AbstractValidator { } /// The FluentValidation shape: registered as the matched interface and as the concrete type. /// [Fact] - public void AlsoAsSelfRegistersTheInterfaceAndTheType() { + public void AlsoAsSelfRegistersTheInterfaceAndTheType() + { var assembly = Compile( - Validators + - """ - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll(typeof(IValidator<>)).IncludeBaseClasses().AlsoAsSelf().AsScoped(); + Validators + + """ + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll(typeof(IValidator<>)).IncludeBaseClasses().AlsoAsSelf().AsScoped(); + } } - } - """); + """ + ); Assert.Contains(assembly.Services, d => d.ServiceType.Name == "IValidator`1"); Assert.Contains(assembly.Services, d => d.ServiceType == assembly.Type("FooValidator")); @@ -830,54 +882,64 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// independently and hands you two; this is a deliberate difference. /// [Fact] - public void AlsoAsSelfSharesOneInstance() { + public void AlsoAsSelfSharesOneInstance() + { var assembly = Compile( - Validators + - """ - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll(typeof(IValidator<>)).IncludeBaseClasses().AlsoAsSelf().AsScoped(); + Validators + + """ + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll(typeof(IValidator<>)).IncludeBaseClasses().AlsoAsSelf().AsScoped(); + } } - } - """); + """ + ); var provider = assembly.BuildProvider(); using var scope = provider.CreateScope(); - var validatorInterface = assembly.Services - .First(d => d.ServiceType.Name == "IValidator`1").ServiceType; + var validatorInterface = assembly + .Services.First(d => d.ServiceType.Name == "IValidator`1") + .ServiceType; Assert.Same( scope.ServiceProvider.GetService(validatorInterface), - scope.ServiceProvider.GetService(assembly.Type("FooValidator"))); + scope.ServiceProvider.GetService(assembly.Type("FooValidator")) + ); } /// /// Only the interfaces the convention matched, not everything the type can reach. /// [Fact] - public void AlsoAsSelfDoesNotPullInUnmatchedInterfaces() { + public void AlsoAsSelfDoesNotPullInUnmatchedInterfaces() + { var assembly = Compile( - Validators + - """ - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll(typeof(IValidator<>)).IncludeBaseClasses().AlsoAsSelf().AsScoped(); + Validators + + """ + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll(typeof(IValidator<>)).IncludeBaseClasses().AlsoAsSelf().AsScoped(); + } } - } - """); + """ + ); // IValidator and the enumerable interfaces are reachable but were not matched. Assert.DoesNotContain(assembly.Services, d => d.ServiceType == assembly.Type("IValidator")); - Assert.DoesNotContain(assembly.Services, d => d.ServiceType.Namespace?.StartsWith("System") == true); + Assert.DoesNotContain( + assembly.Services, + d => d.ServiceType.Namespace?.StartsWith("System") == true + ); } [Fact] - public void AsSelfAndAlsoAsSelfTogetherIsRefused() { + public void AsSelfAndAlsoAsSelfTogetherIsRefused() + { var result = Run( """ public interface IFoo { } @@ -889,7 +951,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSelf().AlsoAsSelf().AsSingleton(); } } - """); + """ + ); Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0009"); } @@ -899,7 +962,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// convention repeating itself is not an ambiguity. /// [Fact] - public void AlsoAsSelfRegistersTheTypeOnceAcrossSeveralClosings() { + public void AlsoAsSelfRegistersTheTypeOnceAcrossSeveralClosings() + { var result = Run( """ public interface IHandler { } @@ -915,7 +979,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(IHandler<>)).AlsoAsSelf().AsScoped(); } } - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0004"); @@ -934,14 +999,16 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(IHandler<>)).AlsoAsSelf().AsScoped(); } } - """); + """ + ); Assert.Single(assembly.Services, d => d.ServiceType == assembly.Type("Both")); Assert.Equal(2, assembly.Services.Count(d => d.ServiceType.Name == "IHandler`1")); } [Fact] - public void UsingChoosesHowTheRegistrationIsAdded() { + public void UsingChoosesHowTheRegistrationIsAdded() + { var assembly = Compile( """ public interface IFoo { } @@ -954,14 +1021,16 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton().Using(RegistrationType.Try); } } - """); + """ + ); // Try registers the service type once and skips the second match. Assert.Single(assembly.Descriptors("IFoo")); } [Fact] - public void WithKeyRegistersUnderAServiceKey() { + public void WithKeyRegistersUnderAServiceKey() + { var assembly = Compile( """ public interface IFoo { } @@ -973,7 +1042,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton().WithKey("primary"); } } - """); + """ + ); var descriptor = assembly.Descriptor("IFoo"); @@ -982,7 +1052,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { Assert.IsType( assembly.Type("Foo"), - assembly.BuildProvider().GetRequiredKeyedService(assembly.Type("IFoo"), "primary")); + assembly.BuildProvider().GetRequiredKeyedService(assembly.Type("IFoo"), "primary") + ); } /// @@ -990,7 +1061,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// the source does not say which. /// [Fact] - public void TwoConventionsRegisteringOneServiceTypeIsAmbiguous() { + public void TwoConventionsRegisteringOneServiceTypeIsAmbiguous() + { var result = Run( """ public interface IFoo { } @@ -1005,7 +1077,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0004"); @@ -1018,7 +1091,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// redundant and collapsing it silently is the failure mode this codebase avoids. /// [Fact] - public void ADuplicatedConventionIsAmbiguousEvenWithEqualLifetimes() { + public void ADuplicatedConventionIsAmbiguousEvenWithEqualLifetimes() + { var result = Run( """ public interface IFoo { } @@ -1032,7 +1106,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0004"); @@ -1040,7 +1115,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { } [Fact] - public void AsSelfRegistersTheConcreteTypeRatherThanTheService() { + public void AsSelfRegistersTheConcreteTypeRatherThanTheService() + { var assembly = Compile( """ public interface IFoo { } @@ -1052,7 +1128,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSelf().AsSingleton(); } } - """); + """ + ); Assert.Empty(assembly.Descriptors("IFoo")); Assert.Single(assembly.Descriptors("Foo")); @@ -1063,7 +1140,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// which is why it emits the cross-wire shape rather than two independent registrations. /// [Fact] - public void AsSelfWithInterfacesSharesOneInstance() { + public void AsSelfWithInterfacesSharesOneInstance() + { var assembly = Compile( """ public interface IFoo { } @@ -1076,7 +1154,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSelfWithInterfaces().AsSingleton(); } } - """); + """ + ); var provider = assembly.BuildProvider(); @@ -1093,7 +1172,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// The hole this closes: a concrete class implementing nothing, selected by namespace. /// [Fact] - public void AConcreteTypeWithNoInterfaceRegistersByNamespace() { + public void AConcreteTypeWithNoInterfaceRegistersByNamespace() + { var assembly = Compile( """ public class OrderCalculator { } @@ -1105,14 +1185,16 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().InNamespaceOf().AsSelf().AsScoped(); } } - """); + """ + ); Assert.Single(assembly.Descriptors("OrderCalculator")); Assert.Single(assembly.Descriptors("OrderValidator")); } [Fact] - public void NamespaceFiltersNarrowAnAssignabilityConvention() { + public void NamespaceFiltersNarrowAnAssignabilityConvention() + { var result = Run( """ public interface IFoo { } @@ -1124,14 +1206,16 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().InNamespaces("SomewhereElse").AsSingleton(); } } - """); + """ + ); // Filtered out entirely, which is DM0005 rather than silence. Assert.Contains(result.GeneratorDiagnostics, d => d.Id == "DM0005"); } [Fact] - public void NotInNamespacesExcludesAfterInclusions() { + public void NotInNamespacesExcludesAfterInclusions() + { var assembly = Compile( """ public interface IFoo { } @@ -1143,13 +1227,15 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().NotInNamespaces("TestNamespace").AsSingleton(); } } - """); + """ + ); Assert.Empty(assembly.Descriptors("IFoo")); } [Fact] - public void RegisterAllWithNoServiceTypeNeedsAShape() { + public void RegisterAllWithNoServiceTypeNeedsAShape() + { var result = Run( """ public class Thing { } @@ -1160,7 +1246,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().InNamespaceOf().AsScoped(); } } - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0009"); @@ -1171,7 +1258,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// Without a filter it would match every class in the compilation, so it is refused. /// [Fact] - public void RegisterAllWithNoServiceTypeAndNoFilterIsRefused() { + public void RegisterAllWithNoServiceTypeAndNoFilterIsRefused() + { var result = Run( """ public class Thing { } @@ -1182,7 +1270,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSelf().AsScoped(); } } - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0009"); @@ -1190,7 +1279,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { } [Fact] - public void RegistersEveryTypeDeclaringTheServiceInterface() { + public void RegistersEveryTypeDeclaringTheServiceInterface() + { var assembly = Compile( """ public interface IFoo { } @@ -1205,7 +1295,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); var descriptors = assembly.Descriptors("IFoo"); @@ -1216,7 +1307,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { } [Fact] - public void RegisteredServiceResolves() { + public void RegisteredServiceResolves() + { var assembly = Compile( """ public interface IFoo { } @@ -1228,7 +1320,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsScoped(); } } - """); + """ + ); Assert.IsType(assembly.Type("Foo"), assembly.ResolveRequired("IFoo")); Assert.Equal(ServiceLifetime.Scoped, assembly.Descriptor("IFoo").Lifetime); @@ -1239,7 +1332,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// so a convention naming the base interface matches by declaration. /// [Fact] - public void MatchesThroughInterfaceInheritance() { + public void MatchesThroughInterfaceInheritance() + { var assembly = Compile( """ public interface IFoo { } @@ -1253,7 +1347,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); Assert.Equal(assembly.Type("Thing"), assembly.Descriptor("IFoo").ImplementationType); } @@ -1264,7 +1359,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// takes an explicit opt-in. /// [Fact] - public void DoesNotMatchThroughABaseClassByDefault() { + public void DoesNotMatchThroughABaseClassByDefault() + { var result = Run( """ public interface IFoo { } @@ -1277,14 +1373,16 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); // Nothing matched, which is DM0005 rather than silence. Assert.Contains(result.GeneratorDiagnostics, d => d.Id == "DM0005"); } [Fact] - public void MatchesThroughABaseClassWhenAskedTo() { + public void MatchesThroughABaseClassWhenAskedTo() + { var assembly = Compile( """ public interface IFoo { } @@ -1297,7 +1395,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton().IncludeBaseClasses(); } } - """); + """ + ); Assert.Equal(assembly.Type("Thing"), assembly.Descriptor("IFoo").ImplementationType); } @@ -1307,7 +1406,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// actually implements, not the open definition. /// [Fact] - public void ClosesAnOpenGenericAgainstEachImplementation() { + public void ClosesAnOpenGenericAgainstEachImplementation() + { var assembly = Compile( """ public interface IHandler { } @@ -1325,11 +1425,15 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(IHandler<,>)).AsTransient(); } } - """); + """ + ); var handler = assembly.Type("IHandler`2"); - var createOrder = handler.MakeGenericType(assembly.Type("CreateOrder"), assembly.Type("OrderId")); + var createOrder = handler.MakeGenericType( + assembly.Type("CreateOrder"), + assembly.Type("OrderId") + ); var rename = handler.MakeGenericType(assembly.Type("Rename"), assembly.Type("OrderId")); var provider = assembly.BuildProvider(); @@ -1343,7 +1447,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// RegisterAll<IHandler<A,B>>() would pick up every other closing too. /// [Fact] - public void AClosedGenericConventionMatchesOnlyThatConstruction() { + public void AClosedGenericConventionMatchesOnlyThatConstruction() + { var assembly = Compile( """ public interface IRepo { } @@ -1356,20 +1461,23 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll>().AsSingleton(); } } - """); + """ + ); var repo = assembly.Type("IRepo`1"); var provider = assembly.BuildProvider(); Assert.IsType( assembly.Type("IntRepo"), - provider.GetService(repo.MakeGenericType(typeof(int)))); + provider.GetService(repo.MakeGenericType(typeof(int))) + ); Assert.Null(provider.GetService(repo.MakeGenericType(typeof(string)))); } [Fact] - public void AnExplicitServiceAttributeBeatsTheConvention() { + public void AnExplicitServiceAttributeBeatsTheConvention() + { var assembly = Compile( """ public interface IFoo { } @@ -1385,25 +1493,31 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsTransient(); } } - """); + """ + ); var descriptors = assembly.Descriptors("IFoo"); // The attributed type is registered once, by its attribute, at the lifetime the attribute // declared — not a second time at the convention's lifetime. - var attributed = descriptors.Where(d => d.ImplementationType == assembly.Type("Attributed")).ToArray(); + var attributed = descriptors + .Where(d => d.ImplementationType == assembly.Type("Attributed")) + .ToArray(); Assert.Single(attributed); Assert.Equal(ServiceLifetime.Singleton, attributed[0].Lifetime); - var byConvention = descriptors.Where(d => d.ImplementationType == assembly.Type("ByConvention")).ToArray(); + var byConvention = descriptors + .Where(d => d.ImplementationType == assembly.Type("ByConvention")) + .ToArray(); Assert.Single(byConvention); Assert.Equal(ServiceLifetime.Transient, byConvention[0].Lifetime); } [Fact] - public void OmittingTheLifetimeIsRefused() { + public void OmittingTheLifetimeIsRefused() + { var result = Run( """ public interface IFoo { } @@ -1415,7 +1529,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(); } } - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0009"); @@ -1428,26 +1543,30 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// build stayed green. /// [Theory] - [InlineData("foreach (var t in new Type[0]) { conventions.RegisterAll().AsSingleton(); }")] + [InlineData( + "foreach (var t in new Type[0]) { conventions.RegisterAll().AsSingleton(); }" + )] [InlineData("if (DateTime.Now.Day > 1) { conventions.RegisterAll().AsSingleton(); }")] [InlineData("var x = 5;")] [InlineData("Helper(conventions);")] [InlineData("conventions.RegisterAll().AsSingleton().AsScoped();")] - public void UnreadableStatementsAreRefused(string statement) { + public void UnreadableStatementsAreRefused(string statement) + { var result = Run( $$""" - public interface IFoo { } - public class Foo : IFoo { } + public interface IFoo { } + public class Foo : IFoo { } - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - {{statement}} - } + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + {{statement}} + } - private static void Helper(IConventionDefinitions c) { } - } - """); + private static void Helper(IConventionDefinitions c) { } + } + """ + ); Assert.Contains(result.GeneratorDiagnostics, d => d.Id == "DM0009"); } @@ -1459,7 +1578,8 @@ private static void Helper(IConventionDefinitions c) { } // keeps DM0004 honest for the case it exists for. [Fact] - public void AConcreteTypeWithNoAccessibleConstructorIsReported() { + public void AConcreteTypeWithNoAccessibleConstructorIsReported() + { var result = Run( """ public interface IFoo { } @@ -1474,7 +1594,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0006"); @@ -1486,7 +1607,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// declaration that explains why it is in the container. /// [Fact] - public void ReportsWhatEachClassIsExposedAs() { + public void ReportsWhatEachClassIsExposedAs() + { var result = Run( """ public interface IFoo { } @@ -1501,7 +1623,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); var exposures = result.GeneratorDiagnostics.Where(d => d.Id == "DM0010").ToArray(); @@ -1510,7 +1633,10 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { // The indirect match names the hop, which is what keeps it from reading as luck. Assert.Contains(exposures, d => d.GetMessage() == "Exposed as IFoo in TestModule"); - Assert.Contains(exposures, d => d.GetMessage() == "Exposed as IFoo in TestModule (via IFooPrime)"); + Assert.Contains( + exposures, + d => d.GetMessage() == "Exposed as IFoo in TestModule (via IFooPrime)" + ); } /// @@ -1519,9 +1645,9 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// primitives at output. This asserts the rebuild lands on the right line. /// [Fact] - public void ExposureIsReportedOnTheClassItself() { - const string body = - """ + public void ExposureIsReportedOnTheClassItself() + { + const string body = """ public interface IFoo { } public class Foo : IFoo { } @@ -1551,7 +1677,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// reported rather than left to fail silently. /// [Fact] - public void ConventionsOnANonModuleAreReported() { + public void ConventionsOnANonModuleAreReported() + { var result = Run( """ public interface IFoo { } @@ -1565,7 +1692,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0009"); @@ -1573,9 +1701,9 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { } [Fact] - public void EditingAnUnrelatedMethodBodyReusesTheCachedOutput() { - const string template = - """ + public void EditingAnUnrelatedMethodBodyReusesTheCachedOutput() + { + const string template = """ using DependencyModules.Runtime.Attributes; using DependencyModules.Runtime.Conventions; @@ -1597,10 +1725,13 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { var result = GeneratorTestHarness.RunIncremental( new Dictionary { ["Test.cs"] = template.Replace("VALUE", "1") }, - new Dictionary { ["Test.cs"] = template.Replace("VALUE", "2") }); + new Dictionary { ["Test.cs"] = template.Replace("VALUE", "2") } + ); Assert.Equal(result.FirstRun, result.SecondRun); - Assert.True(result.AllOutputsCached, - "editing a method body cannot change any registration, so every output should be cached"); + Assert.True( + result.AllOutputsCached, + "editing a method body cannot change any registration, so every output should be cached" + ); } } diff --git a/tests/DependencyModules.Tests/GeneratorTests/DecoratorGenerationTests.cs b/tests/DependencyModules.Tests/GeneratorTests/DecoratorGenerationTests.cs index 5ce0169..6ba19ed 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/DecoratorGenerationTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/DecoratorGenerationTests.cs @@ -10,30 +10,38 @@ namespace DependencyModules.Tests.GeneratorTests; /// Decoration verified by resolving from a real container built from generated code, rather than by /// matching the generated text. The registrations are the point; their shape is not. /// -public class DecoratorGenerationTests { - +public class DecoratorGenerationTests +{ [Fact] - public void Decorator_WrapsTheRegisteredImplementation() { - var generated = GeneratedAssembly.Create(Module( - """ - [Decorator] - public class LoudGreeter(IGreeter inner) : IGreeter { - public string Greet() => inner.Greet().ToUpperInvariant(); - } - """)); + public void Decorator_WrapsTheRegisteredImplementation() + { + var generated = GeneratedAssembly.Create( + Module( + """ + [Decorator] + public class LoudGreeter(IGreeter inner) : IGreeter { + public string Greet() => inner.Greet().ToUpperInvariant(); + } + """ + ) + ); Assert.Equal("HELLO", Greet(generated)); } [Fact] - public void Decorator_PreservesTheServiceLifetime() { - var generated = GeneratedAssembly.Create(Module( - """ - [Decorator] - public class LoudGreeter(IGreeter inner) : IGreeter { - public string Greet() => inner.Greet(); - } - """)); + public void Decorator_PreservesTheServiceLifetime() + { + var generated = GeneratedAssembly.Create( + Module( + """ + [Decorator] + public class LoudGreeter(IGreeter inner) : IGreeter { + public string Greet() => inner.Greet(); + } + """ + ) + ); Assert.Equal(ServiceLifetime.Singleton, generated.Descriptor("IGreeter").Lifetime); } @@ -42,43 +50,52 @@ public class LoudGreeter(IGreeter inner) : IGreeter { /// Lower order sits closer to the implementation, so the higher-order decorator is outermost. /// [Fact] - public void Decorators_NestByAscendingOrder() { - var generated = GeneratedAssembly.Create(Module( - """ - [Decorator(Order = 10)] - public class InnerGreeter(IGreeter inner) : IGreeter { - public string Greet() => $"inner({inner.Greet()})"; - } + public void Decorators_NestByAscendingOrder() + { + var generated = GeneratedAssembly.Create( + Module( + """ + [Decorator(Order = 10)] + public class InnerGreeter(IGreeter inner) : IGreeter { + public string Greet() => $"inner({inner.Greet()})"; + } - [Decorator(Order = 20)] - public class OuterGreeter(IGreeter inner) : IGreeter { - public string Greet() => $"outer({inner.Greet()})"; - } - """)); + [Decorator(Order = 20)] + public class OuterGreeter(IGreeter inner) : IGreeter { + public string Greet() => $"outer({inner.Greet()})"; + } + """ + ) + ); Assert.Equal("outer(inner(hello))", Greet(generated)); } [Fact] - public void Decorator_ReceivesItsOwnDependencies() { - var generated = GeneratedAssembly.Create(Module( - """ - public interface IPrefix { string Value { get; } } + public void Decorator_ReceivesItsOwnDependencies() + { + var generated = GeneratedAssembly.Create( + Module( + """ + public interface IPrefix { string Value { get; } } - [SingletonService] - public class Prefix : IPrefix { public string Value => "pre"; } + [SingletonService] + public class Prefix : IPrefix { public string Value => "pre"; } - [Decorator] - public class PrefixedGreeter(IGreeter inner, IPrefix prefix) : IGreeter { - public string Greet() => $"{prefix.Value}-{inner.Greet()}"; - } - """)); + [Decorator] + public class PrefixedGreeter(IGreeter inner, IPrefix prefix) : IGreeter { + public string Greet() => $"{prefix.Value}-{inner.Greet()}"; + } + """ + ) + ); Assert.Equal("pre-hello", Greet(generated)); } [Fact] - public void OpenGenericDecorator_WrapsEveryClosedRegistration() { + public void OpenGenericDecorator_WrapsEveryClosedRegistration() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -100,15 +117,20 @@ public class ValidatingHandler(IHandler inner) : IHandler { [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = generated.BuildProvider(); var handler = generated.Type("IHandler`1"); - Assert.Equal("validated(string)", - Invoke(provider.GetService(handler.MakeGenericType(typeof(string)))!, "Handle")); - Assert.Equal("validated(int)", - Invoke(provider.GetService(handler.MakeGenericType(typeof(int)))!, "Handle")); + Assert.Equal( + "validated(string)", + Invoke(provider.GetService(handler.MakeGenericType(typeof(string)))!, "Handle") + ); + Assert.Equal( + "validated(int)", + Invoke(provider.GetService(handler.MakeGenericType(typeof(int)))!, "Handle") + ); } /// @@ -126,7 +148,8 @@ public partial class TestModule; /// /// [Fact] - public void OpenGenericRegistration_IsNotDecorated() { + public void OpenGenericRegistration_IsNotDecorated() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -145,7 +168,8 @@ public class LoggingRepo(IRepo inner) : IRepo { [DependencyModule] public partial class TestModule; - """); + """ + ); // Nothing is emitted for it, so the registration stands undecorated rather than the // provider throwing when it is built. @@ -153,14 +177,16 @@ public partial class TestModule; Assert.DoesNotContain( result.GeneratedSources, - source => source.Key.Contains("Decorators") && source.Value.Contains("LoggingRepo")); + source => source.Key.Contains("Decorators") && source.Value.Contains("LoggingRepo") + ); } /// /// The way through, end to end: a closed construction of the generic service is decorated. /// [Fact] - public void ClosedConstructionOfAGenericService_IsDecorated() { + public void ClosedConstructionOfAGenericService_IsDecorated() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -181,16 +207,20 @@ public class LoggingRepo(IRepo inner) : IRepo { [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = generated.BuildProvider(); - var resolved = provider.GetService(generated.Type("IRepo`1").MakeGenericType(typeof(string)))!; + var resolved = provider.GetService( + generated.Type("IRepo`1").MakeGenericType(typeof(string)) + )!; Assert.Equal("logged(repo)", Invoke(resolved, "Name")); } [Fact] - public void ModuleLevelDecorate_WrapsAServiceTheModuleDoesNotDeclare() { + public void ModuleLevelDecorate_WrapsAServiceTheModuleDoesNotDeclare() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -209,26 +239,32 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule] [Decorate(typeof(IGreeter), typeof(LoudGreeter))] public partial class TestModule; - """); + """ + ); Assert.Equal("HELLO", Greet(generated)); } [Fact] - public void Decorator_WithExplicitService_UsesIt() { - var generated = GeneratedAssembly.Create(Module( - """ - [Decorator(Service = typeof(IGreeter))] - public class LoudGreeter(IGreeter inner) : IGreeter { - public string Greet() => inner.Greet().ToUpperInvariant(); - } - """)); + public void Decorator_WithExplicitService_UsesIt() + { + var generated = GeneratedAssembly.Create( + Module( + """ + [Decorator(Service = typeof(IGreeter))] + public class LoudGreeter(IGreeter inner) : IGreeter { + public string Greet() => inner.Greet().ToUpperInvariant(); + } + """ + ) + ); Assert.Equal("HELLO", Greet(generated)); } [Fact] - public void NoDecorator_LeavesTheServiceAlone() { + public void NoDecorator_LeavesTheServiceAlone() + { var generated = GeneratedAssembly.Create(Module("")); Assert.Equal("hello", Greet(generated)); @@ -238,19 +274,23 @@ public void NoDecorator_LeavesTheServiceAlone() { /// Two decorators of one service with the same order would nest in an order nobody declared. /// [Fact] - public void DecoratorsSharingAnOrder_ReportDM0007() { - var result = GeneratorTestHarness.Run(Module( - """ - [Decorator(Order = 5)] - public class FirstGreeter(IGreeter inner) : IGreeter { - public string Greet() => inner.Greet(); - } + public void DecoratorsSharingAnOrder_ReportDM0007() + { + var result = GeneratorTestHarness.Run( + Module( + """ + [Decorator(Order = 5)] + public class FirstGreeter(IGreeter inner) : IGreeter { + public string Greet() => inner.Greet(); + } - [Decorator(Order = 5)] - public class SecondGreeter(IGreeter inner) : IGreeter { - public string Greet() => inner.Greet(); - } - """)); + [Decorator(Order = 5)] + public class SecondGreeter(IGreeter inner) : IGreeter { + public string Greet() => inner.Greet(); + } + """ + ) + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0007"); @@ -259,19 +299,23 @@ public class SecondGreeter(IGreeter inner) : IGreeter { } [Fact] - public void DecoratorsWithDistinctOrders_ReportNothing() { - var result = GeneratorTestHarness.Run(Module( - """ - [Decorator(Order = 1)] - public class FirstGreeter(IGreeter inner) : IGreeter { - public string Greet() => inner.Greet(); - } + public void DecoratorsWithDistinctOrders_ReportNothing() + { + var result = GeneratorTestHarness.Run( + Module( + """ + [Decorator(Order = 1)] + public class FirstGreeter(IGreeter inner) : IGreeter { + public string Greet() => inner.Greet(); + } - [Decorator(Order = 2)] - public class SecondGreeter(IGreeter inner) : IGreeter { - public string Greet() => inner.Greet(); - } - """)); + [Decorator(Order = 2)] + public class SecondGreeter(IGreeter inner) : IGreeter { + public string Greet() => inner.Greet(); + } + """ + ) + ); result.AssertNoErrors(); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0007"); @@ -288,8 +332,10 @@ public class SecondGreeter(IGreeter inner) : IGreeter { [InlineData("Development", "HELLO")] [InlineData("Production", "hello")] public void Decorator_AppliesOnlyWhenItsEnvironmentConditionHolds( - string environmentName, string expected) { - + string environmentName, + string expected + ) + { var generated = GeneratedAssembly.Create( Module( """ @@ -298,8 +344,10 @@ public void Decorator_AppliesOnlyWhenItsEnvironmentConditionHolds( public class LoudGreeter(IGreeter inner) : IGreeter { public string Greet() => inner.Greet().ToUpperInvariant(); } - """), - environment: new ModuleEnvironment(environmentName)); + """ + ), + environment: new ModuleEnvironment(environmentName) + ); Assert.Equal(expected, Greet(generated)); } @@ -307,7 +355,8 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [Theory] [InlineData("on", "HELLO")] [InlineData("off", "hello")] - public void Decorator_HonoursValueConditions(string flag, string expected) { + public void Decorator_HonoursValueConditions(string flag, string expected) + { var generated = GeneratedAssembly.Create( Module( """ @@ -316,8 +365,10 @@ public void Decorator_HonoursValueConditions(string flag, string expected) { public class LoudGreeter(IGreeter inner) : IGreeter { public string Greet() => inner.Greet().ToUpperInvariant(); } - """), - environment: new ModuleEnvironment(false, "Development") { { "LOUD", flag } }); + """ + ), + environment: new ModuleEnvironment(false, "Development") { { "LOUD", flag } } + ); Assert.Equal(expected, Greet(generated)); } @@ -326,7 +377,8 @@ public class LoudGreeter(IGreeter inner) : IGreeter { /// A condition changes whether a decorator applies, never where it sits in the nesting. /// [Fact] - public void Decorator_ConditionDoesNotDisturbOrdering() { + public void Decorator_ConditionDoesNotDisturbOrdering() + { var generated = GeneratedAssembly.Create( Module( """ @@ -340,8 +392,10 @@ public class Inner(IGreeter inner) : IGreeter { public class Outer(IGreeter inner) : IGreeter { public string Greet() => "outer(" + inner.Greet() + ")"; } - """), - environment: new ModuleEnvironment("Development")); + """ + ), + environment: new ModuleEnvironment("Development") + ); Assert.Equal("outer(inner(hello))", Greet(generated)); } @@ -351,7 +405,8 @@ public class Outer(IGreeter inner) : IGreeter { /// the whole chain going with it. /// [Fact] - public void Decorator_UnconditionalOneSurvivesWhenAConditionalOneDoesNot() { + public void Decorator_UnconditionalOneSurvivesWhenAConditionalOneDoesNot() + { var generated = GeneratedAssembly.Create( Module( """ @@ -365,8 +420,10 @@ public class Inner(IGreeter inner) : IGreeter { public class Outer(IGreeter inner) : IGreeter { public string Greet() => "outer(" + inner.Greet() + ")"; } - """), - environment: new ModuleEnvironment("Production")); + """ + ), + environment: new ModuleEnvironment("Production") + ); Assert.Equal("outer(hello)", Greet(generated)); } @@ -376,28 +433,30 @@ private static string Greet(GeneratedAssembly generated) => /// Calls Handle on a resolved handler with a fresh request. private static void Handle(object handler, GeneratedAssembly assembly) => - handler.GetType().GetMethod("Handle")!.Invoke( - handler, new[] { System.Activator.CreateInstance(assembly.Type("Create")) }); + handler + .GetType() + .GetMethod("Handle")! + .Invoke(handler, new[] { System.Activator.CreateInstance(assembly.Type("Create")) }); private static string Invoke(object target, string method) => (string)target.GetType().GetMethod(method)!.Invoke(target, null)!; private static string Module(string body) => $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - public interface IGreeter { string Greet(); } + public interface IGreeter { string Greet(); } - [SingletonService] - public class Greeter : IGreeter { public string Greet() => "hello"; } + [SingletonService] + public class Greeter : IGreeter { public string Greet() => "hello"; } - {{body}} + {{body}} - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; /// /// A decorator's own dependencies are resolved on the terms each parameter declares. @@ -416,7 +475,8 @@ public partial class TestModule; /// /// [Fact] - public void Decorator_ResolvesAKeyedDependencyFromTheKeyItDeclares() { + public void Decorator_ResolvesAKeyedDependencyFromTheKeyItDeclares() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -448,7 +508,8 @@ public class StampedGreeter( [DependencyModule] public partial class TestModule; - """); + """ + ); var greeter = assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")); @@ -460,7 +521,8 @@ public partial class TestModule; /// A nullable dependency the container does not have resolves to null rather than throwing. /// [Fact] - public void Decorator_ResolvesAnOptionalDependencyToNull() { + public void Decorator_ResolvesAnOptionalDependencyToNull() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -481,7 +543,8 @@ public class AuditedGreeter(IGreeter inner, IAudit? audit) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); var greeter = assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")); @@ -502,7 +565,8 @@ public partial class TestModule; /// parameter was resolved on the terms it declares. /// [Fact] - public void ModuleLevelDecorate_ConstructsTheDecoratorFromItsResolvedConstructor() { + public void ModuleLevelDecorate_ConstructsTheDecoratorFromItsResolvedConstructor() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -533,7 +597,8 @@ public class StampedGreeter( [DependencyModule] [Decorate(typeof(IGreeter), typeof(StampedGreeter))] public partial class TestModule; - """); + """ + ); var greeter = assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")); @@ -549,7 +614,8 @@ public partial class TestModule; /// published application. /// [Fact] - public void ModuleLevelDecorate_WithNoPublicConstructor_IsNotDecorated() { + public void ModuleLevelDecorate_WithNoPublicConstructor_IsNotDecorated() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -570,7 +636,8 @@ public class HiddenGreeter : IGreeter { [DependencyModule] [Decorate(typeof(IGreeter), typeof(HiddenGreeter))] public partial class TestModule; - """); + """ + ); // Generated code constructs the decorator, so a private constructor means there is nothing // to emit. The build stays green and the service resolves undecorated. @@ -578,7 +645,8 @@ public partial class TestModule; Assert.DoesNotContain( result.GeneratedSources, - source => source.Value.Contains("new global::TestNamespace.HiddenGreeter")); + source => source.Value.Contains("new global::TestNamespace.HiddenGreeter") + ); } // ------------------------------------------------------------------------------------------ @@ -587,8 +655,7 @@ public partial class TestModule; // exercises DecoratorTypeUtility.Close as much as it does the emission. // ------------------------------------------------------------------------------------------ - private const string HandlerPreamble = - """ + private const string HandlerPreamble = """ using DependencyModules.Runtime.Attributes; using Microsoft.Extensions.DependencyInjection; @@ -621,41 +688,46 @@ public class CountHandler : IHandler { /// decorator resolves the unkeyed registration, which is the right type and the wrong instance. /// [Fact] - public void GenericDecorator_KeyedDependencySurvivesTypeSubstitution() { + public void GenericDecorator_KeyedDependencySurvivesTypeSubstitution() + { var assembly = GeneratedAssembly.Create( - HandlerPreamble + - """ - public interface IStamp { string Value { get; } } + HandlerPreamble + + """ + public interface IStamp { string Value { get; } } - [SingletonService] - public class DefaultStamp : IStamp { public string Value => "?"; } + [SingletonService] + public class DefaultStamp : IStamp { public string Value => "?"; } - [SingletonService(Key = "quiet")] - public class QuietStamp : IStamp { public string Value => "."; } - - [Decorator] - public class StampedHandler( - IHandler inner, - [FromKeyedServices("quiet")] IStamp stamp) : IHandler { + [SingletonService(Key = "quiet")] + public class QuietStamp : IStamp { public string Value => "."; } - public TResponse Handle(TRequest request) { - Log.Lines.Add(stamp.Value); - return inner.Handle(request); + [Decorator] + public class StampedHandler( + IHandler inner, + [FromKeyedServices("quiet")] IStamp stamp) : IHandler { + + public TResponse Handle(TRequest request) { + Log.Lines.Add(stamp.Value); + return inner.Handle(request); + } } - } - public static class Log { public static System.Collections.Generic.List Lines = new(); } + public static class Log { public static System.Collections.Generic.List Lines = new(); } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); var provider = assembly.BuildProvider(); var handler = assembly.Type("IHandler`2"); Handle( - provider.GetRequiredService(handler.MakeGenericType(assembly.Type("Create"), assembly.Type("Id")))!, - assembly); + provider.GetRequiredService( + handler.MakeGenericType(assembly.Type("Create"), assembly.Type("Id")) + )!, + assembly + ); var lines = (System.Collections.Generic.List) assembly.Type("Log").GetField("Lines")!.GetValue(null)!; @@ -667,29 +739,34 @@ public partial class TestModule; /// A generic decorator's optional dependency resolves to null rather than throwing. /// [Fact] - public void GenericDecorator_OptionalDependencyResolvesToNull() { + public void GenericDecorator_OptionalDependencyResolvesToNull() + { var assembly = GeneratedAssembly.Create( - HandlerPreamble + - """ - public interface IAudit { } + HandlerPreamble + + """ + public interface IAudit { } - [Decorator] - public class AuditedHandler( - IHandler inner, IAudit? audit) : IHandler { + [Decorator] + public class AuditedHandler( + IHandler inner, IAudit? audit) : IHandler { - public bool Audited => audit != null; + public bool Audited => audit != null; - public TResponse Handle(TRequest request) => inner.Handle(request); - } + public TResponse Handle(TRequest request) => inner.Handle(request); + } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); var handler = assembly.Type("IHandler`2"); - var resolved = assembly.BuildProvider().GetRequiredService( - handler.MakeGenericType(assembly.Type("Create"), assembly.Type("Id"))); + var resolved = assembly + .BuildProvider() + .GetRequiredService( + handler.MakeGenericType(assembly.Type("Create"), assembly.Type("Id")) + ); Assert.False((bool)resolved.GetType().GetProperty("Audited")!.GetValue(resolved)!); } @@ -703,33 +780,38 @@ public partial class TestModule; /// compile, which is the failure this pins. /// [Fact] - public void GenericDecorator_ClosesADependencyOverItsOwnTypeParameters() { + public void GenericDecorator_ClosesADependencyOverItsOwnTypeParameters() + { var assembly = GeneratedAssembly.Create( - HandlerPreamble + - """ - public interface IValidator { string Name { get; } } + HandlerPreamble + + """ + public interface IValidator { string Name { get; } } - [SingletonService] - public class CreateValidator : IValidator { public string Name => "create"; } + [SingletonService] + public class CreateValidator : IValidator { public string Name => "create"; } - [Decorator] - public class ValidatedHandler( - IHandler inner, - IValidator validator) : IHandler { + [Decorator] + public class ValidatedHandler( + IHandler inner, + IValidator validator) : IHandler { - public string ValidatorName => validator.Name; + public string ValidatorName => validator.Name; - public TResponse Handle(TRequest request) => inner.Handle(request); - } + public TResponse Handle(TRequest request) => inner.Handle(request); + } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); var handler = assembly.Type("IHandler`2"); - var resolved = assembly.BuildProvider().GetRequiredService( - handler.MakeGenericType(assembly.Type("Create"), assembly.Type("Id"))); + var resolved = assembly + .BuildProvider() + .GetRequiredService( + handler.MakeGenericType(assembly.Type("Create"), assembly.Type("Id")) + ); Assert.Equal("create", resolved.GetType().GetProperty("ValidatorName")!.GetValue(resolved)); } @@ -743,32 +825,36 @@ public partial class TestModule; /// shape rather than the outcome: the emitted call must name the closed decorator. /// [Fact] - public void GenericDecorator_ClosesOverAValueTypeArgument() { + public void GenericDecorator_ClosesOverAValueTypeArgument() + { var result = GeneratorTestHarness.Run( - HandlerPreamble + - """ - [Decorator] - public class LoggingHandler( - IHandler inner) : IHandler { + HandlerPreamble + + """ + [Decorator] + public class LoggingHandler( + IHandler inner) : IHandler { - public TResponse Handle(TRequest request) => inner.Handle(request); - } + public TResponse Handle(TRequest request) => inner.Handle(request); + } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); Assert.Empty(result.Errors); - var decorators = Assert.Single( - result.GeneratedSources, source => source.Key.Contains("Decorators")).Value; + var decorators = Assert + .Single(result.GeneratedSources, source => source.Key.Contains("Decorators")) + .Value; // One closed call per registration, each naming the decorator closed over the same // arguments — including the value-type one, which is the instantiation Native AOT cannot // produce at run time and the reason the open-generic call had to go. Assert.True( System.Text.RegularExpressions.Regex.Matches(decorators, "Decorate<").Count == 2, - "expected one closed Decorate call per registration, got:\n" + decorators); + "expected one closed Decorate call per registration, got:\n" + decorators + ); // Nothing is closed at run time any more. Assert.DoesNotContain("IHandler<,>", decorators); @@ -778,34 +864,39 @@ public partial class TestModule; /// Two generic decorators over one service nest in their declared order. /// [Fact] - public void GenericDecorators_StackInOrder() { + public void GenericDecorators_StackInOrder() + { var assembly = GeneratedAssembly.Create( - HandlerPreamble + - """ - public static class Log { public static System.Collections.Generic.List Lines = new(); } - - [Decorator(Order = 1)] - public class InnerMost( - IHandler inner) : IHandler { - public TResponse Handle(TRequest r) { Log.Lines.Add("inner"); return inner.Handle(r); } - } + HandlerPreamble + + """ + public static class Log { public static System.Collections.Generic.List Lines = new(); } + + [Decorator(Order = 1)] + public class InnerMost( + IHandler inner) : IHandler { + public TResponse Handle(TRequest r) { Log.Lines.Add("inner"); return inner.Handle(r); } + } - [Decorator(Order = 2)] - public class OuterMost( - IHandler inner) : IHandler { - public TResponse Handle(TRequest r) { Log.Lines.Add("outer"); return inner.Handle(r); } - } + [Decorator(Order = 2)] + public class OuterMost( + IHandler inner) : IHandler { + public TResponse Handle(TRequest r) { Log.Lines.Add("outer"); return inner.Handle(r); } + } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); var provider = assembly.BuildProvider(); var handler = assembly.Type("IHandler`2"); Handle( - provider.GetRequiredService(handler.MakeGenericType(assembly.Type("Create"), assembly.Type("Id")))!, - assembly); + provider.GetRequiredService( + handler.MakeGenericType(assembly.Type("Create"), assembly.Type("Id")) + )!, + assembly + ); var lines = (System.Collections.Generic.List) assembly.Type("Log").GetField("Lines")!.GetValue(null)!; @@ -825,7 +916,8 @@ public partial class TestModule; /// the build. /// [Fact] - public void GenericDecorator_RegisteredByBothPaths_IsAppliedOnce() { + public void GenericDecorator_RegisteredByBothPaths_IsAppliedOnce() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -864,13 +956,16 @@ public void Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(IHandler<,>)).AsSingleton(); } } - """); + """ + ); var provider = assembly.BuildProvider(); - var handler = assembly.Type("IHandler`2") + var handler = assembly + .Type("IHandler`2") .MakeGenericType(assembly.Type("Create"), assembly.Type("Id")); - foreach (var service in (System.Collections.IEnumerable)provider.GetServices(handler)) { + foreach (var service in (System.Collections.IEnumerable)provider.GetServices(handler)) + { Handle(service!, assembly); } @@ -896,7 +991,8 @@ public void Conventions(IConventionDefinitions conventions) { /// different way than the author asked for, which nothing would report. /// [Fact] - public void Decorator_HonoursTheConstructorItMarked() { + public void Decorator_HonoursTheConstructorItMarked() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -927,10 +1023,13 @@ public class PickyGreeter : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("hello:marked", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "hello:marked", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// @@ -941,7 +1040,8 @@ public partial class TestModule; /// Resolving it would work by accident on Microsoft's container and is wrong in principle. /// [Fact] - public void Decorator_TakingTheProviderGetsTheProvider() { + public void Decorator_TakingTheProviderGetsTheProvider() + { var assembly = GeneratedAssembly.Create( """ using System; @@ -966,10 +1066,13 @@ public string Greet() => [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("hello!", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "hello!", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// @@ -981,7 +1084,8 @@ public partial class TestModule; /// working, which looks like the service moved rather than broke. /// [Fact] - public void Decorator_KeyedRegistrationKeepsItsKey() { + public void Decorator_KeyedRegistrationKeepsItsKey() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1001,12 +1105,16 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = assembly.BuildProvider(); var greeter = assembly.Type("IGreeter"); - Assert.Equal("GOOD DAY", Invoke(provider.GetRequiredKeyedService(greeter, "formal"), "Greet")); + Assert.Equal( + "GOOD DAY", + Invoke(provider.GetRequiredKeyedService(greeter, "formal"), "Greet") + ); Assert.Null(provider.GetService(greeter)); } @@ -1019,7 +1127,8 @@ public partial class TestModule; /// that does not compile, which is the failure mode this pins. /// [Fact] - public void GenericDecorator_ClosesOverANestedTypeArgument() { + public void GenericDecorator_ClosesOverANestedTypeArgument() + { var assembly = GeneratedAssembly.Create( """ using System.Collections.Generic; @@ -1046,10 +1155,15 @@ public class LoggingHandler( [DependencyModule] public partial class TestModule; - """); + """ + ); - var handler = assembly.Type("IHandler`2").MakeGenericType( - typeof(List<>).MakeGenericType(assembly.Type("Create")), assembly.Type("Id")); + var handler = assembly + .Type("IHandler`2") + .MakeGenericType( + typeof(List<>).MakeGenericType(assembly.Type("Create")), + assembly.Type("Id") + ); var resolved = assembly.BuildProvider().GetRequiredService(handler); @@ -1064,7 +1178,8 @@ public partial class TestModule; /// registration. Emitting for a construction nothing registers would be dead code at best. /// [Fact] - public void GenericDecorator_DecoratesOnlyTheRegisteredClosings() { + public void GenericDecorator_DecoratesOnlyTheRegisteredClosings() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -1088,12 +1203,14 @@ public class LoggingHandler(IHandler inner) : IHandler { [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.Empty(result.Errors); - var decorators = Assert.Single( - result.GeneratedSources, source => source.Key.Contains("Decorators")).Value; + var decorators = Assert + .Single(result.GeneratedSources, source => source.Key.Contains("Decorators")) + .Value; Assert.Contains("Registered", decorators); Assert.DoesNotContain("NeverRegistered", decorators); @@ -1103,7 +1220,8 @@ public partial class TestModule; /// A generic decorator keeps the lifetime each registration declared. /// [Fact] - public void GenericDecorator_PreservesEachRegistrationsLifetime() { + public void GenericDecorator_PreservesEachRegistrationsLifetime() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1128,17 +1246,20 @@ public class LoggingHandler(IHandler inner) : IHandler { [DependencyModule] public partial class TestModule; - """); + """ + ); var handler = assembly.Type("IHandler`1"); var singleton = Assert.Single( assembly.Services, - d => d.ServiceType == handler.MakeGenericType(assembly.Type("A"))); + d => d.ServiceType == handler.MakeGenericType(assembly.Type("A")) + ); var transient = Assert.Single( assembly.Services, - d => d.ServiceType == handler.MakeGenericType(assembly.Type("B"))); + d => d.ServiceType == handler.MakeGenericType(assembly.Type("B")) + ); Assert.Equal(ServiceLifetime.Singleton, singleton.Lifetime); Assert.Equal(ServiceLifetime.Transient, transient.Lifetime); @@ -1153,7 +1274,8 @@ public partial class TestModule; /// produced. /// [Fact] - public void GenericDecorator_LeavesTheInnerOwnedByTheContainer() { + public void GenericDecorator_LeavesTheInnerOwnedByTheContainer() + { var assembly = GeneratedAssembly.Create( """ using System; @@ -1180,12 +1302,14 @@ public class LoggingHandler(IHandler inner) : IHandler { [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = assembly.BuildProvider(); var handler = assembly.Type("IHandler`1").MakeGenericType(assembly.Type("A")); - using (var scope = provider.CreateScope()) { + using (var scope = provider.CreateScope()) + { Assert.Equal("a", Invoke(scope.ServiceProvider.GetRequiredService(handler), "Handle")); } @@ -1196,7 +1320,8 @@ public partial class TestModule; /// A decorator scoped to a realm decorates only that module's registrations. /// [Fact] - public void Decorator_ScopedToARealm_DecoratesOnlyThatModule() { + public void Decorator_ScopedToARealm_DecoratesOnlyThatModule() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1219,10 +1344,13 @@ public partial class DecoratedModule; [DependencyModule(OnlyRealm = true)] public partial class PlainModule; """, - moduleName: "DecoratedModule"); + moduleName: "DecoratedModule" + ); - Assert.Equal("HELLO", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "HELLO", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// @@ -1237,8 +1365,10 @@ public partial class PlainModule; [InlineData("Development", "HELLO")] [InlineData("Production", "hello")] public void Decorator_WithAnEnvironmentCondition_AppliesOnlyWhenItHolds( - string environment, string expected) { - + string environment, + string expected + ) + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1259,17 +1389,21 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule] public partial class TestModule; """, - environment: new ModuleEnvironment(environment)); + environment: new ModuleEnvironment(environment) + ); - Assert.Equal(expected, Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + expected, + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// /// Every implementation behind one service is decorated, not just the last registered. /// [Fact] - public void Decorator_WrapsEveryImplementationOfTheService() { + public void Decorator_WrapsEveryImplementationOfTheService() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1291,10 +1425,13 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); - var all = ((System.Collections.IEnumerable)assembly.BuildProvider() - .GetServices(assembly.Type("IGreeter"))) + var all = ( + (System.Collections.IEnumerable) + assembly.BuildProvider().GetServices(assembly.Type("IGreeter")) + ) .Cast() .Select(service => Invoke(service, "Greet")) .ToArray(); @@ -1311,7 +1448,8 @@ public partial class TestModule; /// asserts that the second sees what the first produced. /// [Fact] - public void Decorator_AndInterceptor_BothWrapTheService() { + public void Decorator_AndInterceptor_BothWrapTheService() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1342,10 +1480,13 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); var greeted = Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet"); + assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), + "Greet" + ); var lines = (System.Collections.Generic.List) assembly.Type("Log").GetField("Lines")!.GetValue(null)!; @@ -1364,7 +1505,8 @@ public partial class TestModule; /// service type is the implementation. /// [Fact] - public void Decorator_OverAConventionRegisteredAsSelf() { + public void Decorator_OverAConventionRegisteredAsSelf() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1387,10 +1529,13 @@ public void Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSelf().AsSingleton(); } } - """); + """ + ); - Assert.Equal("WORK", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("Worker")), "Work")); + Assert.Equal( + "WORK", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("Worker")), "Work") + ); } /// @@ -1410,7 +1555,8 @@ public void Conventions(IConventionDefinitions conventions) { /// /// [Fact] - public void ModuleLevelDecorate_CanNameAGenericDecorator() { + public void ModuleLevelDecorate_CanNameAGenericDecorator() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1436,15 +1582,26 @@ public class LoudHandler(IHandler inner) : IHandler { [DependencyModule] [Decorate(typeof(IHandler<>), typeof(LoudHandler<>))] public partial class TestModule; - """); + """ + ); var provider = assembly.BuildProvider(); var handler = assembly.Type("IHandler`1"); - Assert.Equal("A", Invoke( - provider.GetRequiredService(handler.MakeGenericType(assembly.Type("A"))), "Handle")); - Assert.Equal("B", Invoke( - provider.GetRequiredService(handler.MakeGenericType(assembly.Type("B"))), "Handle")); + Assert.Equal( + "A", + Invoke( + provider.GetRequiredService(handler.MakeGenericType(assembly.Type("A"))), + "Handle" + ) + ); + Assert.Equal( + "B", + Invoke( + provider.GetRequiredService(handler.MakeGenericType(assembly.Type("B"))), + "Handle" + ) + ); } // ------------------------------------------------------------------------------------------ @@ -1453,7 +1610,8 @@ public partial class TestModule; /// The wrapped service does not have to be the first constructor parameter. [Fact] - public void Decorator_InnerParameterNeedNotComeFirst() { + public void Decorator_InnerParameterNeedNotComeFirst() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1475,15 +1633,19 @@ public class LoudGreeter(Suffix suffix, IGreeter inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("hello!", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "hello!", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// A record decorator is constructed through its primary constructor. [Fact] - public void Decorator_DeclaredAsARecord() { + public void Decorator_DeclaredAsARecord() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1502,15 +1664,19 @@ public record LoudGreeter(IGreeter Inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("HELLO", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "HELLO", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// A decorator nested inside another type is named correctly in the emitted new. [Fact] - public void Decorator_NestedInsideAnotherType() { + public void Decorator_NestedInsideAnotherType() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1531,10 +1697,13 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("HELLO", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "HELLO", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// A decorator whose inner parameter is nullable still finds it. @@ -1544,7 +1713,8 @@ public partial class TestModule; /// the decoration is dropped with nothing said. /// [Fact] - public void Decorator_WithANullableInnerParameter() { + public void Decorator_WithANullableInnerParameter() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1563,10 +1733,13 @@ public class LoudGreeter(IGreeter? inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("HELLO", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "HELLO", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// @@ -1579,7 +1752,8 @@ public partial class TestModule; /// wrong way round, which compiles whenever the two types happen to be compatible. /// [Fact] - public void GenericDecorator_WithReorderedTypeParameters_IsNotEmitted() { + public void GenericDecorator_WithReorderedTypeParameters_IsNotEmitted() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -1605,20 +1779,23 @@ public class Swapped( [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.Empty(result.Errors); Assert.DoesNotContain( result.GeneratedSources, - source => source.Value.Contains("new global::TestNamespace.Swapped")); + source => source.Value.Contains("new global::TestNamespace.Swapped") + ); } /// /// A generic decorator with fewer type parameters than the service has arguments is not emitted. /// [Fact] - public void GenericDecorator_WithMismatchedArity_IsNotEmitted() { + public void GenericDecorator_WithMismatchedArity_IsNotEmitted() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -1641,14 +1818,16 @@ public class Same(IHandler inner) : IHandler { [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.Empty(result.Errors); } /// A cross-wired registration is a factory descriptor, and decorates like one. [Fact] - public void Decorator_OverACrossWiredRegistration() { + public void Decorator_OverACrossWiredRegistration() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1667,14 +1846,21 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = assembly.BuildProvider(); - Assert.Equal("HELLO", Invoke(provider.GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "HELLO", + Invoke(provider.GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); // The implementation stays resolvable as itself, undecorated — that is what cross-wiring is. - Assert.Equal("hello", Invoke(provider.GetRequiredService(assembly.Type("Greeter")), "Greet")); + Assert.Equal( + "hello", + Invoke(provider.GetRequiredService(assembly.Type("Greeter")), "Greet") + ); } /// @@ -1682,7 +1868,8 @@ public partial class TestModule; /// convention. /// [Fact] - public void GenericDecorator_OverConventionRegistrations_WithAGenericDependency() { + public void GenericDecorator_OverConventionRegistrations_WithAGenericDependency() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1712,11 +1899,15 @@ public void Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(IHandler<>)).AsSingleton(); } } - """); + """ + ); var handler = assembly.Type("IHandler`1").MakeGenericType(assembly.Type("A")); - Assert.Equal("[A]a", Invoke(assembly.BuildProvider().GetRequiredService(handler), "Handle")); + Assert.Equal( + "[A]a", + Invoke(assembly.BuildProvider().GetRequiredService(handler), "Handle") + ); } /// @@ -1729,7 +1920,8 @@ public void Conventions(IConventionDefinitions conventions) { /// decorator's side effects happening twice per call and nothing else. /// [Fact] - public void Decorator_WithTwoModulesInTheCompilation_IsAppliedOnce() { + public void Decorator_WithTwoModulesInTheCompilation_IsAppliedOnce() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1753,7 +1945,8 @@ public partial class TestModule; [DependencyModule] public partial class OtherModule; - """); + """ + ); Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet"); @@ -1766,7 +1959,8 @@ public partial class OtherModule; /// between two decorators and a nesting order nobody declared. /// [Fact] - public void Decorators_SharingAnOrder_AreReported() { + public void Decorators_SharingAnOrder_AreReported() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -1786,7 +1980,8 @@ public class Second(IGreeter inner) : IGreeter { public string Greet() => inner. [DependencyModule] public partial class TestModule; - """); + """ + ); var reported = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0007"); @@ -1802,7 +1997,8 @@ public partial class TestModule; /// through — so this pins that the ordering applies across both rather than within each. /// [Fact] - public void GenericAndNonGenericDecorators_NestByOrder() { + public void GenericAndNonGenericDecorators_NestByOrder() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1830,7 +2026,8 @@ public class SpecificOuter(IHandler inner) : IHandler { [DependencyModule] public partial class TestModule; - """); + """ + ); var handler = assembly.Type("IHandler`1").MakeGenericType(assembly.Type("A")); @@ -1844,7 +2041,8 @@ public partial class TestModule; /// An unscoped decorator does not reach a realm-only module. [Fact] - public void Decorator_Unscoped_DoesNotReachARealmOnlyModule() { + public void Decorator_Unscoped_DoesNotReachARealmOnlyModule() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1864,15 +2062,19 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule(OnlyRealm = true)] public partial class RealmModule; """, - moduleName: "RealmModule"); + moduleName: "RealmModule" + ); - Assert.Equal("hello", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "hello", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// A convention registering matches under a key is decorated under that key. [Fact] - public void Decorator_OverAKeyedConventionRegistration() { + public void Decorator_OverAKeyedConventionRegistration() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1896,7 +2098,8 @@ public void Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().WithKey("loud").AsSingleton(); } } - """); + """ + ); var provider = assembly.BuildProvider(); var greeter = assembly.Type("IGreeter"); @@ -1914,7 +2117,8 @@ public void Conventions(IConventionDefinitions conventions) { /// promises has to survive the rewrite. /// [Fact] - public void Decorator_OverAConventionRegisteredAsSelfWithInterfaces() { + public void Decorator_OverAConventionRegisteredAsSelfWithInterfaces() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1937,19 +2141,27 @@ public void Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSelfWithInterfaces().AsSingleton(); } } - """); + """ + ); var provider = assembly.BuildProvider(); - Assert.Equal("HELLO", Invoke(provider.GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "HELLO", + Invoke(provider.GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); // The implementation itself stays undecorated, which is what cross-wiring means. - Assert.Equal("hello", Invoke(provider.GetRequiredService(assembly.Type("Greeter")), "Greet")); + Assert.Equal( + "hello", + Invoke(provider.GetRequiredService(assembly.Type("Greeter")), "Greet") + ); } /// A decorator with no matching registration emits nothing and breaks nothing. [Fact] - public void Decorator_WithNothingToDecorate_EmitsNothing() { + public void Decorator_WithNothingToDecorate_EmitsNothing() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -1965,7 +2177,8 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.Empty(result.Errors); } @@ -1980,7 +2193,8 @@ public partial class TestModule; /// for two declarations that are each perfectly legal. /// [Fact] - public void GenericDecorator_ConstrainedToReferenceTypes_SkipsValueTypeClosings() { + public void GenericDecorator_ConstrainedToReferenceTypes_SkipsValueTypeClosings() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -2004,7 +2218,8 @@ public class Logging(IHandler inner) : IHandler where T : class { [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.Empty(result.Errors); } @@ -2013,7 +2228,8 @@ public partial class TestModule; /// A constraint the closing does satisfy still emits. /// [Fact] - public void GenericDecorator_ConstrainedToAnInterface_EmitsForSatisfyingClosings() { + public void GenericDecorator_ConstrainedToAnInterface_EmitsForSatisfyingClosings() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -2036,11 +2252,15 @@ public class Logging(IHandler inner) : IHandler where T : IRequest { [DependencyModule] public partial class TestModule; - """); + """ + ); var handler = assembly.Type("IHandler`1").MakeGenericType(assembly.Type("Thing")); - Assert.Equal("THING", Invoke(assembly.BuildProvider().GetRequiredService(handler), "Handle")); + Assert.Equal( + "THING", + Invoke(assembly.BuildProvider().GetRequiredService(handler), "Handle") + ); } /// @@ -2052,7 +2272,8 @@ public partial class TestModule; /// it wins over inference rather than being one more candidate. /// [Fact] - public void Decorator_WithAnExplicitService_DecoratesThatOne() { + public void Decorator_WithAnExplicitService_DecoratesThatOne() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -2076,19 +2297,24 @@ public class Loud(IGreeter greeter, IFarewell farewell) : IGreeter, IFarewell { [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = assembly.BuildProvider(); Assert.Equal("BYE", Invoke(provider.GetRequiredService(assembly.Type("IFarewell")), "Bye")); // The other interface it implements is not decorated. - Assert.Equal("hello", Invoke(provider.GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "hello", + Invoke(provider.GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// A registration declared with Try is still decorated. [Fact] - public void Decorator_OverATryRegistration() { + public void Decorator_OverATryRegistration() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -2107,10 +2333,13 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("HELLO", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "HELLO", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// A decorator reaching the service through a base class is still a decorator. @@ -2119,7 +2348,8 @@ public partial class TestModule; /// directly-written interfaces count, this stops being recognised and is silently not applied. /// [Fact] - public void Decorator_ImplementingTheServiceThroughABaseClass() { + public void Decorator_ImplementingTheServiceThroughABaseClass() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -2140,7 +2370,8 @@ public class LoudGreeter(IGreeter inner) : GreeterBase { [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.Empty(result.Errors); } @@ -2150,8 +2381,10 @@ public partial class TestModule; [InlineData("Development", "A")] [InlineData("Production", "a")] public void GenericDecorator_WithAnEnvironmentCondition_GuardsEachClosing( - string environment, string expected) { - + string environment, + string expected + ) + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -2178,11 +2411,15 @@ public class Loud(IHandler inner) : IHandler { [DependencyModule] public partial class TestModule; """, - environment: new ModuleEnvironment(environment)); + environment: new ModuleEnvironment(environment) + ); var handler = assembly.Type("IHandler`1").MakeGenericType(assembly.Type("A")); - Assert.Equal(expected, Invoke(assembly.BuildProvider().GetRequiredService(handler), "Handle")); + Assert.Equal( + expected, + Invoke(assembly.BuildProvider().GetRequiredService(handler), "Handle") + ); } /// Two closings of one generic service each get their own decoration. @@ -2191,7 +2428,8 @@ public partial class TestModule; /// seen first. /// [Fact] - public void GenericDecorator_OverTwoClosingsOfOneService() { + public void GenericDecorator_OverTwoClosingsOfOneService() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -2216,20 +2454,32 @@ public class Loud(IHandler inner) : IHandler { [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = assembly.BuildProvider(); var handler = assembly.Type("IHandler`1"); - Assert.Equal("MULTI", Invoke( - provider.GetRequiredService(handler.MakeGenericType(assembly.Type("A"))), "Handle")); - Assert.Equal("MULTI", Invoke( - provider.GetRequiredService(handler.MakeGenericType(assembly.Type("B"))), "Handle")); + Assert.Equal( + "MULTI", + Invoke( + provider.GetRequiredService(handler.MakeGenericType(assembly.Type("A"))), + "Handle" + ) + ); + Assert.Equal( + "MULTI", + Invoke( + provider.GetRequiredService(handler.MakeGenericType(assembly.Type("B"))), + "Handle" + ) + ); } /// A deeply nested type argument is substituted at every level. [Fact] - public void GenericDecorator_ClosesOverADeeplyNestedTypeArgument() { + public void GenericDecorator_ClosesOverADeeplyNestedTypeArgument() + { var assembly = GeneratedAssembly.Create( """ using System.Collections.Generic; @@ -2253,18 +2503,27 @@ public class Loud(IHandler inner) : IHandler { [DependencyModule] public partial class TestModule; - """); + """ + ); - var handler = assembly.Type("IHandler`1").MakeGenericType( - typeof(IReadOnlyList<>).MakeGenericType( - typeof(Dictionary<,>).MakeGenericType(typeof(string), assembly.Type("Create")))); + var handler = assembly + .Type("IHandler`1") + .MakeGenericType( + typeof(IReadOnlyList<>).MakeGenericType( + typeof(Dictionary<,>).MakeGenericType(typeof(string), assembly.Type("Create")) + ) + ); - Assert.Equal("DEEP", Invoke(assembly.BuildProvider().GetRequiredService(handler), "Handle")); + Assert.Equal( + "DEEP", + Invoke(assembly.BuildProvider().GetRequiredService(handler), "Handle") + ); } /// Three type parameters are substituted in order. [Fact] - public void GenericDecorator_WithThreeTypeParameters() { + public void GenericDecorator_WithThreeTypeParameters() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -2287,17 +2546,20 @@ public class Loud(IPipe inner) : IPipeA keyed registration with a keyed dependency on the decorator. [Fact] - public void Decorator_KeyedRegistrationAndKeyedDependency() { + public void Decorator_KeyedRegistrationAndKeyedDependency() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -2327,15 +2589,24 @@ public class StampedGreeter( [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("good day.", Invoke( - assembly.BuildProvider().GetRequiredKeyedService(assembly.Type("IGreeter"), "formal"), "Greet")); + Assert.Equal( + "good day.", + Invoke( + assembly + .BuildProvider() + .GetRequiredKeyedService(assembly.Type("IGreeter"), "formal"), + "Greet" + ) + ); } /// A decorator can depend on the module environment. [Fact] - public void Decorator_DependingOnTheModuleEnvironment() { + public void Decorator_DependingOnTheModuleEnvironment() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime; @@ -2357,15 +2628,19 @@ public class NamedGreeter(IGreeter inner, IModuleEnvironment environment) : IGre [DependencyModule] public partial class TestModule; """, - environment: new ModuleEnvironment("Staging")); + environment: new ModuleEnvironment("Staging") + ); - Assert.Equal("hello:env", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "hello:env", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// A TryEnumerable registration is decorated. [Fact] - public void Decorator_OverATryEnumerableRegistration() { + public void Decorator_OverATryEnumerableRegistration() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -2384,15 +2659,19 @@ public class LoudGreeter(IGreeter inner) : IGreeter { [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("HELLO", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "HELLO", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// A convention reaching the interface through a base class is decorated. [Fact] - public void Decorator_OverAConventionUsingIncludeBaseClasses() { + public void Decorator_OverAConventionUsingIncludeBaseClasses() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -2417,15 +2696,19 @@ public void Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().IncludeBaseClasses().AsSingleton(); } } - """); + """ + ); - Assert.Equal("HELLO", Invoke( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "HELLO", + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// Interception over a closed construction of a generic service. [Fact] - public void Interceptor_OverAClosedGenericService() { + public void Interceptor_OverAClosedGenericService() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -2453,7 +2736,8 @@ public TResult Intercept(InvocationContext context) { [DependencyModule] public partial class TestModule; - """); + """ + ); var handler = assembly.Type("IHandler`1").MakeGenericType(assembly.Type("A")); diff --git a/tests/DependencyModules.Tests/GeneratorTests/DecoratorImplementationTests.cs b/tests/DependencyModules.Tests/GeneratorTests/DecoratorImplementationTests.cs index 7ca798b..c01de68 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/DecoratorImplementationTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/DecoratorImplementationTests.cs @@ -19,21 +19,23 @@ namespace DependencyModules.Tests.GeneratorTests; /// implementation type and skips descriptors built from anything else, which is the overload /// interception uses. Only the attribute could not say so. /// -public class DecoratorImplementationTests { - +public class DecoratorImplementationTests +{ /// /// The default, unchanged: no Implementation named, so every registration of the service is /// wrapped. /// [Fact] - public void WithNoImplementationNamed_EveryRegistrationIsWrapped() { + public void WithNoImplementationNamed_EveryRegistrationIsWrapped() + { var resolved = Resolve("[Decorator]"); Assert.Equal(["Logged", "Logged"], resolved.Select(Outer)); } [Fact] - public void NamingAnImplementation_WrapsOnlyThatOne() { + public void NamingAnImplementation_WrapsOnlyThatOne() + { var resolved = Resolve("[Decorator(Implementation = typeof(Loud))]"); Assert.Equal(["Logged", "Quiet"], resolved.Select(Outer)); @@ -44,7 +46,8 @@ public void NamingAnImplementation_WrapsOnlyThatOne() { /// implementation, not whichever registration happened to be first. /// [Fact] - public void TheWrappedInstance_IsTheNamedImplementation() { + public void TheWrappedInstance_IsTheNamedImplementation() + { var resolved = Resolve("[Decorator(Implementation = typeof(Loud))]"); var logged = Assert.Single(resolved, greeter => Outer(greeter) == "Logged"); @@ -53,7 +56,8 @@ public void TheWrappedInstance_IsTheNamedImplementation() { } [Fact] - public void TheUnnamedImplementation_IsUntouched() { + public void TheUnnamedImplementation_IsUntouched() + { var resolved = Resolve("[Decorator(Implementation = typeof(Loud))]"); var quiet = Assert.Single(resolved, greeter => Outer(greeter) == "Quiet"); @@ -67,7 +71,8 @@ public void TheUnnamedImplementation_IsUntouched() { /// default. /// [Fact] - public void NamingAnImplementationThatIsNotRegistered_WrapsNothing() { + public void NamingAnImplementationThatIsNotRegistered_WrapsNothing() + { var resolved = Resolve("[Decorator(Implementation = typeof(Unregistered))]"); Assert.Equal(["Loud", "Quiet"], resolved.Select(Outer)); @@ -84,8 +89,12 @@ public void NamingAnImplementationThatIsNotRegistered_WrapsNothing() { /// learns about it. Reported rather than silently doing the wrong thing. /// [Fact] - public void NamingAnImplementationUnderGenerateFactories_ReportsDM0022() { - var result = Generate("[Decorator(Implementation = typeof(Loud))]", generateFactories: true); + public void NamingAnImplementationUnderGenerateFactories_ReportsDM0022() + { + var result = Generate( + "[Decorator(Implementation = typeof(Loud))]", + generateFactories: true + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0022"); @@ -94,15 +103,20 @@ public void NamingAnImplementationUnderGenerateFactories_ReportsDM0022() { } [Fact] - public void NamingNoImplementationUnderGenerateFactories_IsNotReported() { + public void NamingNoImplementationUnderGenerateFactories_IsNotReported() + { var result = Generate("[Decorator]", generateFactories: true); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0022"); } [Fact] - public void NamingAnImplementationWithoutGenerateFactories_IsNotReported() { - var result = Generate("[Decorator(Implementation = typeof(Loud))]", generateFactories: false); + public void NamingAnImplementationWithoutGenerateFactories_IsNotReported() + { + var result = Generate( + "[Decorator(Implementation = typeof(Loud))]", + generateFactories: false + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0022"); } @@ -115,48 +129,57 @@ private static string Greet(object greeter) => private static GeneratorResult Generate(string decoratorAttribute, bool generateFactories) => GeneratorTestHarness.Run( Source(decoratorAttribute), - new Dictionary { - ["DependencyModules_GenerateFactories"] = generateFactories ? "true" : "false" - }); - - private static object[] Resolve(string decoratorAttribute, bool generateFactories = false) { + new Dictionary + { + ["DependencyModules_GenerateFactories"] = generateFactories ? "true" : "false", + } + ); + + private static object[] Resolve(string decoratorAttribute, bool generateFactories = false) + { var generated = GeneratedAssembly.Create( Source(decoratorAttribute), - buildProperties: new Dictionary { - ["DependencyModules_GenerateFactories"] = generateFactories ? "true" : "false" - }); + buildProperties: new Dictionary + { + ["DependencyModules_GenerateFactories"] = generateFactories ? "true" : "false", + } + ); var provider = generated.BuildProvider(); - return ((System.Collections.IEnumerable)provider - .GetService(typeof(IEnumerable<>).MakeGenericType(generated.Type("IGreeter")))!) + return ( + (System.Collections.IEnumerable) + provider.GetService( + typeof(IEnumerable<>).MakeGenericType(generated.Type("IGreeter")) + )! + ) .Cast() .OrderBy(greeter => greeter.GetType().Name, System.StringComparer.Ordinal) .ToArray(); } private static string Source(string decoratorAttribute) => - $$""" - using DependencyModules.Runtime.Attributes; + $$""" + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - public interface IGreeter { string Greet(); } + public interface IGreeter { string Greet(); } - [SingletonService] - public class Loud : IGreeter { public string Greet() => "loud"; } + [SingletonService] + public class Loud : IGreeter { public string Greet() => "loud"; } - [SingletonService] - public class Quiet : IGreeter { public string Greet() => "quiet"; } + [SingletonService] + public class Quiet : IGreeter { public string Greet() => "quiet"; } - public class Unregistered : IGreeter { public string Greet() => "nowhere"; } + public class Unregistered : IGreeter { public string Greet() => "nowhere"; } - {{decoratorAttribute}} - public class Logged(IGreeter inner) : IGreeter { - public string Greet() => inner.Greet(); - } + {{decoratorAttribute}} + public class Logged(IGreeter inner) : IGreeter { + public string Greet() => inner.Greet(); + } - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/DiagnosticSuppressionTests.cs b/tests/DependencyModules.Tests/GeneratorTests/DiagnosticSuppressionTests.cs index f4c04a2..e639df6 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/DiagnosticSuppressionTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/DiagnosticSuppressionTests.cs @@ -28,23 +28,28 @@ namespace DependencyModules.Tests.GeneratorTests; /// because a generator that failed has nothing to point at. Adding a code without adding it here /// means shipping one more diagnostic nobody can turn off where they wrote it. /// -public class DiagnosticSuppressionTests { - +public class DiagnosticSuppressionTests +{ [Theory] [MemberData(nameof(Triggers))] - public void ADiagnostic_IsReportedAgainstASyntaxTree(string code, string source) { + public void ADiagnostic_IsReportedAgainstASyntaxTree(string code, string source) + { var result = GeneratorTestHarness.Run(source); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == code); - Assert.True(diagnostic.Location.SourceTree != null, - $"{code} was reported at {diagnostic.Location.Kind} with no syntax tree, so neither " + - ".editorconfig nor #pragma can silence it. Location: " + diagnostic.Location); + Assert.True( + diagnostic.Location.SourceTree != null, + $"{code} was reported at {diagnostic.Location.Kind} with no syntax tree, so neither " + + ".editorconfig nor #pragma can silence it. Location: " + + diagnostic.Location + ); } [Theory] [MemberData(nameof(Triggers))] - public void ADiagnostic_IsReportedInTheFileThatCausedIt(string code, string source) { + public void ADiagnostic_IsReportedInTheFileThatCausedIt(string code, string source) + { var result = GeneratorTestHarness.Run(source); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == code); @@ -58,282 +63,383 @@ public void ADiagnostic_IsReportedInTheFileThatCausedIt(string code, string sour /// because they are the two that were never moved and already carry a real location — they are /// covered by AssemblyModuleAttributeDiagnosticsTests. /// - public static IEnumerable Triggers() { - yield return ["DM0002", Module(""" - public interface IThing; - [SingletonService] - public abstract class Thing : IThing; - """)]; - - yield return ["DM0003", """ - using DependencyModules.Runtime.Attributes; - namespace TestNamespace; - [DependencyModule] - public class NotPartialModule; - """]; - - yield return ["DM0012", Module(""" - public interface IThing; - [SingletonService] - [IfEnvironment] - public class Thing : IThing; - """)]; - - yield return ["DM0014", Module(""" - public interface IThing; - [CrossWireService] - public class Thing : IThing; - """)]; - - yield return ["DM0017", """ - using DependencyModules.Runtime.Attributes; - namespace TestNamespace; - public static class Outer { - [DependencyModule] - public partial class NestedModule; - } - """]; - - yield return ["DM0004", Convention(""" - public interface IFoo { } - public class Foo : IFoo { } - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().AsSingleton(); - conventions.RegisterAll().AsScoped(); - } - } - """)]; - - yield return ["DM0005", Convention(""" - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().WithName("NothingMatchesThis").AsSelf().AsScoped(); - } - } - """)]; - - yield return ["DM0006", Convention(""" - public interface IFoo { } - public class Foo : IFoo { private Foo() { } } - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().AsSingleton(); - } - } - """)]; - - yield return ["DM0009", Convention(""" - public interface IFoo { } - public class Foo : IFoo { } - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().AsSelf().AlsoAsSelf().AsSingleton(); - } - } - """)]; - - yield return ["DM0022", """ - using DependencyModules.Runtime.Attributes; - - namespace TestNamespace; - - public interface IGreeter { string Greet(); } - - [SingletonService] - public class Loud : IGreeter { public string Greet() => "loud"; } - - [Decorator(Implementation = typeof(Loud))] - public class Logged(IGreeter inner) : IGreeter { - public string Greet() => inner.Greet(); - } - - [DependencyModule(GenerateFactories = true)] - public partial class TestModule; - """]; - - yield return ["DM0021", """ - using DependencyModules.Runtime.Attributes; - using DependencyModules.Testing.Attributes; - - namespace TestNamespace; - - public interface IThing; - - public class RealThing : IThing; - - public class Fixture { - [TestExport(typeof(IThing), Implementation = typeof(RealThing))] - public void Conflicting([Mock] IThing thing) { } - } - """]; - - yield return ["DM0020", Convention(""" - using DependencyModules.Runtime.Interception; - - public interface IGreeter { string Greet(); } - - public sealed class CountingInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) => context.Proceed(); - } - - [Intercept(typeof(CountingInterceptor))] - public sealed class Greeter : IGreeter { public string Greet() => "hi"; } - - [DependencyModule(OnlyRealm = true)] - public partial class ConventionModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().AsSingleton(); - } - } - """)]; - - yield return ["DM0010", Convention(""" - public interface IFoo { } - public class Foo : IFoo { } - - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().AsSingleton(); - } - } - """)]; - - yield return ["DM0011", Module(""" - public interface IThing; - [SingletonService] - [IfEnvironment("Development")] - public class Thing : IThing; - """)]; - - yield return ["DM0007", """ - using DependencyModules.Runtime.Attributes; - - namespace TestNamespace; - - public interface IThing { string Read(); } - - [SingletonService] - public class Thing : IThing { - public string Read() => ""; - } - - [Decorator(Order = 1)] - public class FirstDecorator(IThing inner) : IThing { - public string Read() => inner.Read(); - } - - [Decorator(Order = 1)] - public class SecondDecorator(IThing inner) : IThing { - public string Read() => inner.Read(); - } - - [DependencyModule] - public partial class TestModule; - """]; - - yield return ["DM0013", """ - using DependencyModules.Runtime.Attributes; - - namespace TestNamespace; - - public interface IStore { string Read(T key); } - - [SingletonService] - public class Store : IStore { - public string Read(T key) => ""; - } - - [Decorator] - public class LoggingStore(IStore inner) : IStore { - public string Read(T key) => inner.Read(key); - } - - [DependencyModule] - public partial class TestModule; - """]; - - yield return ["DM0008", Intercepted(""" - public interface IAwkward { - bool TryGet(string key, out string value); - } - - [SingletonService] - [Intercept(typeof(SyncOnlyInterceptor))] - public class Awkward : IAwkward { - public bool TryGet(string key, out string value) { value = key; return true; } - } - """)]; - - yield return ["DM0015", Intercepted(""" - public interface IAsyncOnly { - Task GetAsync(string key); - } - - [SingletonService] - [Intercept(typeof(SyncOnlyInterceptor))] - public class AsyncOnly : IAsyncOnly { - public Task GetAsync(string key) => Task.FromResult(key); - } - """)]; - - yield return ["DM0018", """ - using DependencyModules.Runtime.Attributes; - namespace TestNamespace; - [DependencyModule] - public partial class TestModule { - public int SizeLimit { get; set; } - } - """]; + public static IEnumerable Triggers() + { + yield return + [ + "DM0002", + Module( + """ + public interface IThing; + [SingletonService] + public abstract class Thing : IThing; + """ + ), + ]; + + yield return + [ + "DM0003", + """ + using DependencyModules.Runtime.Attributes; + namespace TestNamespace; + [DependencyModule] + public class NotPartialModule; + """, + ]; + + yield return + [ + "DM0012", + Module( + """ + public interface IThing; + [SingletonService] + [IfEnvironment] + public class Thing : IThing; + """ + ), + ]; + + yield return + [ + "DM0014", + Module( + """ + public interface IThing; + [CrossWireService] + public class Thing : IThing; + """ + ), + ]; + + yield return + [ + "DM0017", + """ + using DependencyModules.Runtime.Attributes; + namespace TestNamespace; + public static class Outer { + [DependencyModule] + public partial class NestedModule; + } + """, + ]; + + yield return + [ + "DM0004", + Convention( + """ + public interface IFoo { } + public class Foo : IFoo { } + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().AsSingleton(); + conventions.RegisterAll().AsScoped(); + } + } + """ + ), + ]; + + yield return + [ + "DM0005", + Convention( + """ + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().WithName("NothingMatchesThis").AsSelf().AsScoped(); + } + } + """ + ), + ]; + + yield return + [ + "DM0006", + Convention( + """ + public interface IFoo { } + public class Foo : IFoo { private Foo() { } } + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().AsSingleton(); + } + } + """ + ), + ]; + + yield return + [ + "DM0009", + Convention( + """ + public interface IFoo { } + public class Foo : IFoo { } + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().AsSelf().AlsoAsSelf().AsSingleton(); + } + } + """ + ), + ]; + + yield return + [ + "DM0022", + """ + using DependencyModules.Runtime.Attributes; + + namespace TestNamespace; + + public interface IGreeter { string Greet(); } + + [SingletonService] + public class Loud : IGreeter { public string Greet() => "loud"; } + + [Decorator(Implementation = typeof(Loud))] + public class Logged(IGreeter inner) : IGreeter { + public string Greet() => inner.Greet(); + } + + [DependencyModule(GenerateFactories = true)] + public partial class TestModule; + """, + ]; + + yield return + [ + "DM0021", + """ + using DependencyModules.Runtime.Attributes; + using DependencyModules.Testing.Attributes; + + namespace TestNamespace; + + public interface IThing; + + public class RealThing : IThing; + + public class Fixture { + [TestExport(typeof(IThing), Implementation = typeof(RealThing))] + public void Conflicting([Mock] IThing thing) { } + } + """, + ]; + + yield return + [ + "DM0020", + Convention( + """ + using DependencyModules.Runtime.Interception; + + public interface IGreeter { string Greet(); } + + public sealed class CountingInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) => context.Proceed(); + } + + [Intercept(typeof(CountingInterceptor))] + public sealed class Greeter : IGreeter { public string Greet() => "hi"; } + + [DependencyModule(OnlyRealm = true)] + public partial class ConventionModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().AsSingleton(); + } + } + """ + ), + ]; + + yield return + [ + "DM0010", + Convention( + """ + public interface IFoo { } + public class Foo : IFoo { } + + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().AsSingleton(); + } + } + """ + ), + ]; + + yield return + [ + "DM0011", + Module( + """ + public interface IThing; + [SingletonService] + [IfEnvironment("Development")] + public class Thing : IThing; + """ + ), + ]; + + yield return + [ + "DM0007", + """ + using DependencyModules.Runtime.Attributes; + + namespace TestNamespace; + + public interface IThing { string Read(); } + + [SingletonService] + public class Thing : IThing { + public string Read() => ""; + } + + [Decorator(Order = 1)] + public class FirstDecorator(IThing inner) : IThing { + public string Read() => inner.Read(); + } + + [Decorator(Order = 1)] + public class SecondDecorator(IThing inner) : IThing { + public string Read() => inner.Read(); + } + + [DependencyModule] + public partial class TestModule; + """, + ]; + + yield return + [ + "DM0013", + """ + using DependencyModules.Runtime.Attributes; + + namespace TestNamespace; + + public interface IStore { string Read(T key); } + + [SingletonService] + public class Store : IStore { + public string Read(T key) => ""; + } + + [Decorator] + public class LoggingStore(IStore inner) : IStore { + public string Read(T key) => inner.Read(key); + } + + [DependencyModule] + public partial class TestModule; + """, + ]; + + yield return + [ + "DM0008", + Intercepted( + """ + public interface IAwkward { + bool TryGet(string key, out string value); + } + + [SingletonService] + [Intercept(typeof(SyncOnlyInterceptor))] + public class Awkward : IAwkward { + public bool TryGet(string key, out string value) { value = key; return true; } + } + """ + ), + ]; + + yield return + [ + "DM0015", + Intercepted( + """ + public interface IAsyncOnly { + Task GetAsync(string key); + } + + [SingletonService] + [Intercept(typeof(SyncOnlyInterceptor))] + public class AsyncOnly : IAsyncOnly { + public Task GetAsync(string key) => Task.FromResult(key); + } + """ + ), + ]; + + yield return + [ + "DM0018", + """ + using DependencyModules.Runtime.Attributes; + namespace TestNamespace; + [DependencyModule] + public partial class TestModule { + public int SizeLimit { get; set; } + } + """, + ]; } private static string Convention(string body) => $$""" - using System; - using DependencyModules.Runtime.Attributes; - using DependencyModules.Runtime.Conventions; + using System; + using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Conventions; - namespace TestNamespace; + namespace TestNamespace; - {{body}} - """; + {{body}} + """; private static string Intercepted(string body) => $$""" - using System.Threading.Tasks; - using DependencyModules.Runtime.Attributes; - using DependencyModules.Runtime.Interception; + using System.Threading.Tasks; + using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Interception; - namespace TestNamespace; + namespace TestNamespace; - [SingletonService] - public class SyncOnlyInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) => context.Proceed(); - } + [SingletonService] + public class SyncOnlyInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) => context.Proceed(); + } - {{body}} + {{body}} - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; private static string Module(string body) => $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - [DependencyModule] - public partial class TestModule; + [DependencyModule] + public partial class TestModule; - {{body}} - """; + {{body}} + """; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/DiagnosticsTests.cs b/tests/DependencyModules.Tests/GeneratorTests/DiagnosticsTests.cs index d4947b8..354a5d5 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/DiagnosticsTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/DiagnosticsTests.cs @@ -9,16 +9,19 @@ namespace DependencyModules.Tests.GeneratorTests; /// mistake that previously produced either a crash when the container was built or, worse, a /// successful build that quietly registered nothing. /// -public class DiagnosticsTests { - +public class DiagnosticsTests +{ /// /// An abstract implementation used to be registered anyway, and the resulting /// AddSingleton(typeof(IThing), typeof(AbstractThing)) threw when the provider was built, /// a long way from the declaration responsible. /// [Fact] - public void AbstractService_ReportsDM0002() { - var result = GeneratorTestHarness.Run(Module("[SingletonService] public abstract class Thing : IThing;")); + public void AbstractService_ReportsDM0002() + { + var result = GeneratorTestHarness.Run( + Module("[SingletonService] public abstract class Thing : IThing;") + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0002"); @@ -28,14 +31,18 @@ public void AbstractService_ReportsDM0002() { } [Fact] - public void AbstractService_IsNotRegistered() { - var result = GeneratorTestHarness.Run(Module("[SingletonService] public abstract class Thing : IThing;")); + public void AbstractService_IsNotRegistered() + { + var result = GeneratorTestHarness.Run( + Module("[SingletonService] public abstract class Thing : IThing;") + ); Assert.DoesNotContain(result.GeneratedSources.Keys, key => key.Contains("Dependencies")); } [Fact] - public void StaticService_ReportsDM0002() { + public void StaticService_ReportsDM0002() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -47,7 +54,8 @@ public static class StaticThing; [DependencyModule] public partial class TestModule; - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0002"); @@ -59,7 +67,8 @@ public partial class TestModule; /// bad one should not discard the good ones. /// [Fact] - public void ConcreteServices_AreStillRegisteredAlongsideARejectedOne() { + public void ConcreteServices_AreStillRegisteredAlongsideARejectedOne() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -75,7 +84,8 @@ public interface IAbstract; [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = generated.BuildProvider(); @@ -88,7 +98,8 @@ public partial class TestModule; /// DM0003 names the fix, and generation is skipped so it is the only error shown. /// [Fact] - public void NonPartialModule_ReportsDM0003() { + public void NonPartialModule_ReportsDM0003() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -101,7 +112,8 @@ public interface IThing; [DependencyModule] public class NotPartialModule; - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0003"); @@ -111,7 +123,8 @@ public class NotPartialModule; } [Fact] - public void NonPartialModule_DoesNotGenerateAConflictingDeclaration() { + public void NonPartialModule_DoesNotGenerateAConflictingDeclaration() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -120,23 +133,31 @@ namespace TestNamespace; [DependencyModule] public class NotPartialModule; - """); + """ + ); // Emitting the module half would add CS0260 on top of DM0003 and point at the wrong thing. - Assert.DoesNotContain(result.GeneratedSources.Keys, key => key.Contains("NotPartialModule.Module")); + Assert.DoesNotContain( + result.GeneratedSources.Keys, + key => key.Contains("NotPartialModule.Module") + ); Assert.DoesNotContain(result.CompilationDiagnostics, d => d.Id == "CS0260"); } [Fact] - public void PartialModule_ReportsNothing() { - var result = GeneratorTestHarness.Run(Module("[SingletonService] public class Thing : IThing;")); + public void PartialModule_ReportsNothing() + { + var result = GeneratorTestHarness.Run( + Module("[SingletonService] public class Thing : IThing;") + ); result.AssertNoErrors(); Assert.Empty(result.GeneratorDiagnostics); } [Fact] - public void AbstractFactoryHost_IsAllowedBecauseTheFactorySuppliesTheInstance() { + public void AbstractFactoryHost_IsAllowedBecauseTheFactorySuppliesTheInstance() + { // The declaring type is never constructed, so an abstract host is legitimate here. var result = GeneratorTestHarness.Run( """ @@ -153,7 +174,8 @@ public abstract class Factories { [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0002"); } @@ -164,7 +186,8 @@ public partial class TestModule; /// a green build — a decorator in the source that never ran. /// [Fact] - public void GenericDecoratorOverOpenGenericRegistration_ReportsDM0013() { + public void GenericDecoratorOverOpenGenericRegistration_ReportsDM0013() + { var result = GeneratorTestHarness.Run( OpenGenericStore( """ @@ -172,7 +195,9 @@ public void GenericDecoratorOverOpenGenericRegistration_ReportsDM0013() { public class LoggingStore(IStore inner) : IStore { public string Read(T key) => inner.Read(key); } - """)); + """ + ) + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0013"); @@ -186,7 +211,8 @@ public class LoggingStore(IStore inner) : IStore { /// expansion, so both have to report. /// [Fact] - public void ModuleDeclaredDecoratorOverOpenGenericRegistration_ReportsDM0013() { + public void ModuleDeclaredDecoratorOverOpenGenericRegistration_ReportsDM0013() + { var result = GeneratorTestHarness.Run( OpenGenericStore( """ @@ -194,7 +220,9 @@ public class LoggingStore(IStore inner) : IStore { public string Read(T key) => inner.Read(key); } """, - moduleAttributes: "[Decorate(typeof(IStore<>), typeof(LoggingStore<>))]")); + moduleAttributes: "[Decorate(typeof(IStore<>), typeof(LoggingStore<>))]" + ) + ); Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0013"); } @@ -205,7 +233,8 @@ public class LoggingStore(IStore inner) : IStore { /// which is CS7003 in generated code. /// [Fact] - public void NonGenericDecoratorOverOpenGenericRegistration_ReportsDM0013() { + public void NonGenericDecoratorOverOpenGenericRegistration_ReportsDM0013() + { var result = GeneratorTestHarness.Run( OpenGenericStore( """ @@ -213,7 +242,9 @@ public class StringStoreDecorator(IStore inner) : IStore { public string Read(string key) => inner.Read(key); } """, - moduleAttributes: "[Decorate(typeof(IStore<>), typeof(StringStoreDecorator))]")); + moduleAttributes: "[Decorate(typeof(IStore<>), typeof(StringStoreDecorator))]" + ) + ); Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0013"); } @@ -223,7 +254,8 @@ public class StringStoreDecorator(IStore inner) : IStore { /// asserts the compilation is clean and emits. /// [Fact] - public void NonGenericDecoratorOverOpenGenericRegistration_StillCompiles() { + public void NonGenericDecoratorOverOpenGenericRegistration_StillCompiles() + { var generated = GeneratedAssembly.Create( OpenGenericStore( """ @@ -231,7 +263,9 @@ public class StringStoreDecorator(IStore inner) : IStore { public string Read(string key) => inner.Read(key); } """, - moduleAttributes: "[Decorate(typeof(IStore<>), typeof(StringStoreDecorator))]")); + moduleAttributes: "[Decorate(typeof(IStore<>), typeof(StringStoreDecorator))]" + ) + ); Assert.Contains(generated.Services, d => d.ServiceType == generated.Type("IStore`1")); } @@ -241,7 +275,8 @@ public class StringStoreDecorator(IStore inner) : IStore { /// across. This is the shape a MediatR-style pipeline is built from. /// [Fact] - public void GenericDecoratorOverClosedRegistrations_DoesNotReportDM0013() { + public void GenericDecoratorOverClosedRegistrations_DoesNotReportDM0013() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -260,7 +295,8 @@ public class LoggingStore(IStore inner) : IStore { [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0013"); } @@ -271,7 +307,8 @@ public partial class TestModule; /// on the feature's primary use. /// [Fact] - public void DecoratorForAServiceThisCompilationDoesNotRegister_DoesNotReportDM0013() { + public void DecoratorForAServiceThisCompilationDoesNotRegister_DoesNotReportDM0013() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -287,7 +324,8 @@ public class LoggingElsewhere(IElsewhere inner) : IElsewhere { [DependencyModule] [Decorate(typeof(IElsewhere<>), typeof(LoggingElsewhere<>))] public partial class TestModule; - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0013"); } @@ -299,8 +337,11 @@ public partial class TestModule; /// GetRequiredService<Ledger<>>(). /// [Fact] - public void CrossWiredGenericType_ReportsDM0014() { - var result = GeneratorTestHarness.Run(CrossWiredLedger("public class Ledger : ILedger, IAudit;")); + public void CrossWiredGenericType_ReportsDM0014() + { + var result = GeneratorTestHarness.Run( + CrossWiredLedger("public class Ledger : ILedger, IAudit;") + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0014"); @@ -309,8 +350,11 @@ public void CrossWiredGenericType_ReportsDM0014() { } [Fact] - public void CrossWiredGenericType_IsNotRegistered() { - var result = GeneratorTestHarness.Run(CrossWiredLedger("public class Ledger : ILedger, IAudit;")); + public void CrossWiredGenericType_IsNotRegistered() + { + var result = GeneratorTestHarness.Run( + CrossWiredLedger("public class Ledger : ILedger, IAudit;") + ); Assert.DoesNotContain(result.GeneratedSources.Keys, key => key.Contains("Dependencies")); } @@ -320,15 +364,19 @@ public void CrossWiredGenericType_IsNotRegistered() { /// interfaces. /// [Fact] - public void CrossWiredNonGenericType_StillRegisters() { - var generated = GeneratedAssembly.Create(CrossWiredLedger("public class Ledger : ILedger, IAudit;")); + public void CrossWiredNonGenericType_StillRegisters() + { + var generated = GeneratedAssembly.Create( + CrossWiredLedger("public class Ledger : ILedger, IAudit;") + ); var provider = generated.BuildProvider(); // The point of cross-wiring: both interfaces answer with the one instance. Assert.Same( provider.GetService(generated.Type("ILedger`1").MakeGenericType(typeof(int))), - provider.GetService(generated.Type("IAudit`1").MakeGenericType(typeof(int)))); + provider.GetService(generated.Type("IAudit`1").MakeGenericType(typeof(int))) + ); } /// @@ -337,7 +385,8 @@ public void CrossWiredNonGenericType_StillRegisters() { /// anything could report on it. /// [Fact] - public void InterceptorThatServesNoMember_ReportsDM0015() { + public void InterceptorThatServesNoMember_ReportsDM0015() + { var result = GeneratorTestHarness.Run( Intercepted( """ @@ -350,7 +399,9 @@ public interface IAsyncOnly { public class AsyncOnly : IAsyncOnly { public Task GetAsync(string key) => Task.FromResult(key); } - """)); + """ + ) + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0015"); @@ -365,7 +416,8 @@ public class AsyncOnly : IAsyncOnly { /// one, which is how an argument-rewriting interceptor stops rewriting halfway through a service. /// [Fact] - public void InterceptorThatServesSomeMembers_ReportsDM0015ForTheRest() { + public void InterceptorThatServesSomeMembers_ReportsDM0015ForTheRest() + { var result = GeneratorTestHarness.Run( Intercepted( """ @@ -380,7 +432,9 @@ public class Mixed : IMixed { public int Count(string key) => key.Length; public Task CountAsync(string key) => Task.FromResult(key.Length); } - """)); + """ + ) + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0015"); @@ -392,7 +446,8 @@ public class Mixed : IMixed { /// An interceptor covering every shape the service uses says nothing. /// [Fact] - public void InterceptorThatServesEveryMember_DoesNotReportDM0015() { + public void InterceptorThatServesEveryMember_DoesNotReportDM0015() + { var result = GeneratorTestHarness.Run( Intercepted( """ @@ -405,7 +460,9 @@ public interface ISyncOnly { public class SyncOnly : ISyncOnly { public int Count(string key) => key.Length; } - """)); + """ + ) + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0015"); } @@ -415,7 +472,8 @@ public class SyncOnly : ISyncOnly { /// the guide read as though the other members were still intercepted. /// [Fact] - public void UnsupportedMember_ReportsThatNoMemberIsIntercepted() { + public void UnsupportedMember_ReportsThatNoMemberIsIntercepted() + { var result = GeneratorTestHarness.Run( Intercepted( """ @@ -430,7 +488,9 @@ public class Awkward : IAwkward { public bool TryGet(string key, out string value) { value = key; return true; } public int Fine(string key) => key.Length; } - """)); + """ + ) + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0008"); @@ -440,68 +500,68 @@ public class Awkward : IAwkward { private static string Intercepted(string body) => $$""" - using System.Threading.Tasks; - using DependencyModules.Runtime.Attributes; - using DependencyModules.Runtime.Interception; + using System.Threading.Tasks; + using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Interception; - namespace TestNamespace; + namespace TestNamespace; - [SingletonService] - public class SyncOnlyInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) => context.Proceed(); - } + [SingletonService] + public class SyncOnlyInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) => context.Proceed(); + } - {{body}} + {{body}} - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; private static string OpenGenericStore(string body, string moduleAttributes = "") => $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - public interface IStore { string Read(T key); } + public interface IStore { string Read(T key); } - [SingletonService] - public class Store : IStore { public string Read(T key) => "store"; } + [SingletonService] + public class Store : IStore { public string Read(T key) => "store"; } - {{body}} + {{body}} - [DependencyModule] - {{moduleAttributes}} - public partial class TestModule; - """; + [DependencyModule] + {{moduleAttributes}} + public partial class TestModule; + """; private static string CrossWiredLedger(string implementation) => $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - public interface ILedger; - public interface IAudit; + public interface ILedger; + public interface IAudit; - [CrossWireService] - {{implementation}} + [CrossWireService] + {{implementation}} - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; private static string Module(string body) => $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - public interface IThing; + public interface IThing; - {{body}} + {{body}} - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/EntryModelUtilTests.cs b/tests/DependencyModules.Tests/GeneratorTests/EntryModelUtilTests.cs index 74dea2c..876b13b 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/EntryModelUtilTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/EntryModelUtilTests.cs @@ -11,55 +11,82 @@ namespace DependencyModules.Tests.GeneratorTests; /// EntryModelUtil decides the generated file names and which entry points survive consolidation. /// Getting consolidation wrong either drops a module's registrations or emits duplicate files. /// -public class EntryModelUtilTests { - +public class EntryModelUtilTests +{ [Fact] - public void GenerateFileName_CombinesNamespaceTypeAndSuffix() { - var model = ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("My.Namespace", "MyModule")); + public void GenerateFileName_CombinesNamespaceTypeAndSuffix() + { + var model = ModelFactory.EntryPoint( + entryPointType: TypeDefinition.Get("My.Namespace", "MyModule") + ); - Assert.Equal("My.Namespace.MyModule.Dependencies.g.cs", EntryModelUtil.GenerateFileName(model, "Dependencies")); + Assert.Equal( + "My.Namespace.MyModule.Dependencies.g.cs", + EntryModelUtil.GenerateFileName(model, "Dependencies") + ); } [Fact] - public void GenerateFileName_ForTheGlobalNamespace_UsesAPlaceholder() { + public void GenerateFileName_ForTheGlobalNamespace_UsesAPlaceholder() + { var model = ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("", "MyModule")); - Assert.Equal("blank-namespace.MyModule.Module.g.cs", EntryModelUtil.GenerateFileName(model, "Module")); + Assert.Equal( + "blank-namespace.MyModule.Module.g.cs", + EntryModelUtil.GenerateFileName(model, "Module") + ); } [Fact] - public void EnsureNamespace_GivesAutoGeneratedModulesTheRootNamespace() { + public void EnsureNamespace_GivesAutoGeneratedModulesTheRootNamespace() + { var model = ModelFactory.EntryPoint( features: ModuleEntryPointFeatures.AutoGenerateModule, - entryPointType: TypeDefinition.Get("", "ApplicationModule")); + entryPointType: TypeDefinition.Get("", "ApplicationModule") + ); - var result = EntryModelUtil.EnsureNamespace(model, ModelFactory.Configuration(rootNamespace: "ConfiguredRoot")); + var result = EntryModelUtil.EnsureNamespace( + model, + ModelFactory.Configuration(rootNamespace: "ConfiguredRoot") + ); Assert.Equal("ConfiguredRoot", result.EntryPointType.Namespace); } [Fact] - public void EnsureNamespace_LeavesAutoGeneratedModulesThatAlreadyHaveANamespace() { + public void EnsureNamespace_LeavesAutoGeneratedModulesThatAlreadyHaveANamespace() + { var model = ModelFactory.EntryPoint( features: ModuleEntryPointFeatures.AutoGenerateModule, - entryPointType: TypeDefinition.Get("Existing", "ApplicationModule")); + entryPointType: TypeDefinition.Get("Existing", "ApplicationModule") + ); - var result = EntryModelUtil.EnsureNamespace(model, ModelFactory.Configuration(rootNamespace: "ConfiguredRoot")); + var result = EntryModelUtil.EnsureNamespace( + model, + ModelFactory.Configuration(rootNamespace: "ConfiguredRoot") + ); Assert.Equal("Existing", result.EntryPointType.Namespace); } [Fact] - public void EnsureNamespace_LeavesExplicitlyDeclaredModulesAlone() { - var model = ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("", "DeclaredModule")); + public void EnsureNamespace_LeavesExplicitlyDeclaredModulesAlone() + { + var model = ModelFactory.EntryPoint( + entryPointType: TypeDefinition.Get("", "DeclaredModule") + ); - var result = EntryModelUtil.EnsureNamespace(model, ModelFactory.Configuration(rootNamespace: "ConfiguredRoot")); + var result = EntryModelUtil.EnsureNamespace( + model, + ModelFactory.Configuration(rootNamespace: "ConfiguredRoot") + ); Assert.Equal("", result.EntryPointType.Namespace); } [Fact] - public void Consolidate_KeepsADeclaredModule() { + public void Consolidate_KeepsADeclaredModule() + { var model = ModelFactory.EntryPoint(); var (entryPoints, _) = EntryModelUtil.ConsolidateEntryPointModels(Input(model)); @@ -68,10 +95,12 @@ public void Consolidate_KeepsADeclaredModule() { } [Fact] - public void Consolidate_KeepsAnAutoGeneratedModuleDeclaredInTheProjectsProgramFile() { + public void Consolidate_KeepsAnAutoGeneratedModuleDeclaredInTheProjectsProgramFile() + { var model = ModelFactory.EntryPoint( features: ModuleEntryPointFeatures.AutoGenerateModule, - fileLocation: Path.Combine("/project/", "Program.cs")); + fileLocation: Path.Combine("/project/", "Program.cs") + ); var (entryPoints, _) = EntryModelUtil.ConsolidateEntryPointModels(Input(model)); @@ -82,10 +111,12 @@ public void Consolidate_KeepsAnAutoGeneratedModuleDeclaredInTheProjectsProgramFi /// A Program.cs belonging to a referenced project must not generate an ApplicationModule here. /// [Fact] - public void Consolidate_DropsAnAutoGeneratedModuleFromAnotherProject() { + public void Consolidate_DropsAnAutoGeneratedModuleFromAnotherProject() + { var model = ModelFactory.EntryPoint( features: ModuleEntryPointFeatures.AutoGenerateModule, - fileLocation: "/somewhere-else/Program.cs"); + fileLocation: "/somewhere-else/Program.cs" + ); var (entryPoints, _) = EntryModelUtil.ConsolidateEntryPointModels(Input(model)); @@ -93,29 +124,35 @@ public void Consolidate_DropsAnAutoGeneratedModuleFromAnotherProject() { } [Fact] - public void Consolidate_WithAutoGenerateDisabled_DropsAutoGeneratedModules() { + public void Consolidate_WithAutoGenerateDisabled_DropsAutoGeneratedModules() + { var model = ModelFactory.EntryPoint( features: ModuleEntryPointFeatures.AutoGenerateModule, - fileLocation: Path.Combine("/project/", "Program.cs")); + fileLocation: Path.Combine("/project/", "Program.cs") + ); var (entryPoints, _) = EntryModelUtil.ConsolidateEntryPointModels( - Input(model, ModelFactory.Configuration(autoGenerateEntry: false))); + Input(model, ModelFactory.Configuration(autoGenerateEntry: false)) + ); Assert.Empty(entryPoints); } [Fact] - public void Consolidate_WithAutoGenerateDisabled_KeepsDeclaredModules() { + public void Consolidate_WithAutoGenerateDisabled_KeepsDeclaredModules() + { var model = ModelFactory.EntryPoint(); var (entryPoints, _) = EntryModelUtil.ConsolidateEntryPointModels( - Input(model, ModelFactory.Configuration(autoGenerateEntry: false))); + Input(model, ModelFactory.Configuration(autoGenerateEntry: false)) + ); Assert.Single(entryPoints); } [Fact] - public void Consolidate_GroupsModelsSharingANamespaceAndName() { + public void Consolidate_GroupsModelsSharingANamespaceAndName() + { var first = ModelFactory.EntryPoint(fileLocation: "/project/A.cs"); var second = ModelFactory.EntryPoint(fileLocation: "/project/B.cs"); @@ -129,13 +166,16 @@ public void Consolidate_GroupsModelsSharingANamespaceAndName() { /// carries the configuration. /// [Fact] - public void Consolidate_PrefersTheDeclaredModuleOverTheAutoGeneratedOne() { + public void Consolidate_PrefersTheDeclaredModuleOverTheAutoGeneratedOne() + { var auto = ModelFactory.EntryPoint( features: ModuleEntryPointFeatures.AutoGenerateModule, - entryPointType: TypeDefinition.Get("TestNamespace", "SharedName")); + entryPointType: TypeDefinition.Get("TestNamespace", "SharedName") + ); var declared = ModelFactory.EntryPoint( entryPointType: TypeDefinition.Get("TestNamespace", "SharedName"), - useMethod: "UseIt"); + useMethod: "UseIt" + ); var (entryPoints, _) = EntryModelUtil.ConsolidateEntryPointModels(Input(auto, declared)); @@ -143,9 +183,14 @@ public void Consolidate_PrefersTheDeclaredModuleOverTheAutoGeneratedOne() { } [Fact] - public void Consolidate_KeepsDistinctModulesSeparate() { - var first = ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("TestNamespace", "First")); - var second = ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("TestNamespace", "Second")); + public void Consolidate_KeepsDistinctModulesSeparate() + { + var first = ModelFactory.EntryPoint( + entryPointType: TypeDefinition.Get("TestNamespace", "First") + ); + var second = ModelFactory.EntryPoint( + entryPointType: TypeDefinition.Get("TestNamespace", "Second") + ); var (entryPoints, _) = EntryModelUtil.ConsolidateEntryPointModels(Input(first, second)); @@ -153,17 +198,20 @@ public void Consolidate_KeepsDistinctModulesSeparate() { } [Fact] - public void Consolidate_ReturnsTheConfigurationItWasGiven() { + public void Consolidate_ReturnsTheConfigurationItWasGiven() + { var configuration = ModelFactory.Configuration(rootNamespace: "SomeRoot"); var (_, returned) = EntryModelUtil.ConsolidateEntryPointModels( - Input(ModelFactory.EntryPoint(), configuration)); + Input(ModelFactory.EntryPoint(), configuration) + ); Assert.Same(configuration, returned); } [Fact] - public void UniqueId_IsStableForTheSameType() { + public void UniqueId_IsStableForTheSameType() + { var first = ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("A.B", "Module")); var second = ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("A.B", "Module")); @@ -171,22 +219,32 @@ public void UniqueId_IsStableForTheSameType() { } [Fact] - public void UniqueId_DiffersBetweenTypes() { + public void UniqueId_DiffersBetweenTypes() + { var first = ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("A.B", "ModuleOne")); - var second = ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("A.B", "ModuleTwo")); + var second = ModelFactory.EntryPoint( + entryPointType: TypeDefinition.Get("A.B", "ModuleTwo") + ); Assert.NotEqual(first.UniqueId(), second.UniqueId()); } - private static ImmutableArray<(ModuleEntryPointModel, DependencyModuleConfigurationModel)> Input( - params ModuleEntryPointModel[] models) => - Input(ModelFactory.Configuration(), models); + private static ImmutableArray<( + ModuleEntryPointModel, + DependencyModuleConfigurationModel + )> Input(params ModuleEntryPointModel[] models) => Input(ModelFactory.Configuration(), models); - private static ImmutableArray<(ModuleEntryPointModel, DependencyModuleConfigurationModel)> Input( - ModuleEntryPointModel model, DependencyModuleConfigurationModel configuration) => + private static ImmutableArray<( + ModuleEntryPointModel, + DependencyModuleConfigurationModel + )> Input(ModuleEntryPointModel model, DependencyModuleConfigurationModel configuration) => Input(configuration, model); - private static ImmutableArray<(ModuleEntryPointModel, DependencyModuleConfigurationModel)> Input( - DependencyModuleConfigurationModel configuration, params ModuleEntryPointModel[] models) => - models.Select(model => (model, configuration)).ToImmutableArray(); + private static ImmutableArray<( + ModuleEntryPointModel, + DependencyModuleConfigurationModel + )> Input( + DependencyModuleConfigurationModel configuration, + params ModuleEntryPointModel[] models + ) => models.Select(model => (model, configuration)).ToImmutableArray(); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/EnvironmentConditionTests.cs b/tests/DependencyModules.Tests/GeneratorTests/EnvironmentConditionTests.cs index dc85d7f..8170870 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/EnvironmentConditionTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/EnvironmentConditionTests.cs @@ -13,10 +13,9 @@ namespace DependencyModules.Tests.GeneratorTests; /// actually in the collection. A condition emitted into the wrong branch still produces plausible /// generated text, so text assertions would pass while the service registered in production. /// -public class EnvironmentConditionTests { - - private const string Preamble = - """ +public class EnvironmentConditionTests +{ + private const string Preamble = """ using System; using DependencyModules.Runtime.Attributes; @@ -34,11 +33,12 @@ private static GeneratedAssembly Compile(string source, IModuleEnvironment? envi private static IModuleEnvironment Env(string name) => Env(name, []); /// - private static IModuleEnvironment Env(string name, params (string Key, string? Value)[] values) => - new ModuleEnvironment(false, name, values.ToDictionary(v => v.Key, v => v.Value)); + private static IModuleEnvironment Env( + string name, + params (string Key, string? Value)[] values + ) => new ModuleEnvironment(false, name, values.ToDictionary(v => v.Key, v => v.Value)); - private const string NameGated = - """ + private const string NameGated = """ public interface IEmailSender { } [SingletonService] @@ -46,8 +46,7 @@ public interface IEmailSender { } public class FakeEmailSender : IEmailSender { } """; - private const string Module = - """ + private const string Module = """ [DependencyModule] public partial class TestModule; @@ -59,16 +58,20 @@ public partial class TestModule; [InlineData("development", true)] [InlineData("Production", false)] [InlineData("", false)] - public void IfEnvironmentRegistersOnlyInTheNamedEnvironments(string environmentName, bool expected) { + public void IfEnvironmentRegistersOnlyInTheNamedEnvironments( + string environmentName, + bool expected + ) + { var assembly = Compile(NameGated + Module, Env(environmentName)); Assert.Equal(expected, assembly.Descriptors("IEmailSender").Count == 1); } [Fact] - public void IfNotEnvironmentRegistersEverywhereElse() { - const string source = - """ + public void IfNotEnvironmentRegistersEverywhereElse() + { + const string source = """ public interface IProfiler { } [SingletonService] @@ -81,9 +84,9 @@ public class RequestProfiler : IProfiler { } } [Fact] - public void IfEnvironmentValueTestsPresenceWhenGivenOnlyAKey() { - const string source = - """ + public void IfEnvironmentValueTestsPresenceWhenGivenOnlyAKey() + { + const string source = """ public interface IBilling { } [SingletonService] @@ -91,18 +94,23 @@ public interface IBilling { } public class Billing : IBilling { } """; - Assert.Single(Compile(source + Module, Env("Any", ("FEATURE_BILLING", "anything"))).Descriptors("IBilling")); + Assert.Single( + Compile(source + Module, Env("Any", ("FEATURE_BILLING", "anything"))) + .Descriptors("IBilling") + ); // Set to empty is still set; only absence is absence. - Assert.Single(Compile(source + Module, Env("Any", ("FEATURE_BILLING", ""))).Descriptors("IBilling")); + Assert.Single( + Compile(source + Module, Env("Any", ("FEATURE_BILLING", ""))).Descriptors("IBilling") + ); Assert.Empty(Compile(source + Module, Env("Any")).Descriptors("IBilling")); } [Fact] - public void IfEnvironmentValueComparesTheValueExactly() { - const string source = - """ + public void IfEnvironmentValueComparesTheValueExactly() + { + const string source = """ public interface IBilling { } [SingletonService] @@ -118,9 +126,9 @@ public class Billing : IBilling { } } [Fact] - public void ConditionsOfDifferentKindsCombineWithAnd() { - const string source = - """ + public void ConditionsOfDifferentKindsCombineWithAnd() + { + const string source = """ public interface IThing { } [SingletonService] @@ -129,15 +137,21 @@ public interface IThing { } public class Thing : IThing { } """; - Assert.Single(Compile(source + Module, Env("Development", ("MODE", "on"))).Descriptors("IThing")); - Assert.Empty(Compile(source + Module, Env("Development", ("MODE", "off"))).Descriptors("IThing")); - Assert.Empty(Compile(source + Module, Env("Production", ("MODE", "on"))).Descriptors("IThing")); + Assert.Single( + Compile(source + Module, Env("Development", ("MODE", "on"))).Descriptors("IThing") + ); + Assert.Empty( + Compile(source + Module, Env("Development", ("MODE", "off"))).Descriptors("IThing") + ); + Assert.Empty( + Compile(source + Module, Env("Production", ("MODE", "on"))).Descriptors("IThing") + ); } [Fact] - public void SeveralValueConditionsAllHaveToHold() { - const string source = - """ + public void SeveralValueConditionsAllHaveToHold() + { + const string source = """ public interface IThing { } [SingletonService] @@ -146,7 +160,9 @@ public interface IThing { } public class Thing : IThing { } """; - Assert.Single(Compile(source + Module, Env("Any", ("A", "1"), ("B", "2"))).Descriptors("IThing")); + Assert.Single( + Compile(source + Module, Env("Any", ("A", "1"), ("B", "2"))).Descriptors("IThing") + ); Assert.Empty(Compile(source + Module, Env("Any", ("A", "1"))).Descriptors("IThing")); } @@ -163,9 +179,9 @@ public class Thing : IThing { } /// "Smtp" and the default would land last and win in every environment. /// [Fact] - public void AConditionalRegistrationOverridesAnUnconditionalDefault() { - const string source = - """ + public void AConditionalRegistrationOverridesAnUnconditionalDefault() + { + const string source = """ public interface IEmailSender { } [SingletonService] @@ -181,20 +197,22 @@ public class FakeEmailSender : IEmailSender { } Assert.Equal( development.Type("FakeEmailSender"), - development.BuildProvider().GetService(development.Type("IEmailSender"))!.GetType()); + development.BuildProvider().GetService(development.Type("IEmailSender"))!.GetType() + ); Assert.Equal( production.Type("SmtpEmailSender"), - production.BuildProvider().GetService(production.Type("IEmailSender"))!.GetType()); + production.BuildProvider().GetService(production.Type("IEmailSender"))!.GetType() + ); } /// /// The motivating case: one service type, two implementations, exactly one registered. /// [Fact] - public void TwoImplementationsOfOneServiceSelectByEnvironment() { - const string source = - """ + public void TwoImplementationsOfOneServiceSelectByEnvironment() + { + const string source = """ public interface IEmailSender { string Name { get; } } [SingletonService] @@ -209,14 +227,20 @@ public class SmtpEmailSender : IEmailSender { public string Name => "smtp"; } var development = Compile(source + Module, Env("Development")); var production = Compile(source + Module, Env("Production")); - Assert.Equal(development.Type("FakeEmailSender"), development.Descriptor("IEmailSender").ImplementationType); - Assert.Equal(production.Type("SmtpEmailSender"), production.Descriptor("IEmailSender").ImplementationType); + Assert.Equal( + development.Type("FakeEmailSender"), + development.Descriptor("IEmailSender").ImplementationType + ); + Assert.Equal( + production.Type("SmtpEmailSender"), + production.Descriptor("IEmailSender").ImplementationType + ); } [Fact] - public void UnconditionalServicesInTheSameModuleAreUnaffected() { - const string source = - """ + public void UnconditionalServicesInTheSameModuleAreUnaffected() + { + const string source = """ public interface IAlways { } public interface ISometimes { } @@ -235,7 +259,8 @@ public class Sometimes : ISometimes { } } [Fact] - public void ModuleEnvironmentNoneRegistersNothingConditional() { + public void ModuleEnvironmentNoneRegistersNothingConditional() + { var assembly = Compile(NameGated + Module, ModuleEnvironment.None); Assert.Empty(assembly.Descriptors("IEmailSender")); @@ -250,13 +275,15 @@ public void ModuleEnvironmentNoneRegistersNothingConditional() { /// ModuleEnvironmentDefaultNameTests moves on purpose. /// [Fact] - public void NoEnvironmentSuppliedUsesTheProcessEnvironment() { + public void NoEnvironmentSuppliedUsesTheProcessEnvironment() + { var supplied = Compile(NameGated + Module, ModuleEnvironment.CreateDefault()); var omitted = Compile(NameGated + Module, environment: null); Assert.Equal( supplied.Descriptors("IEmailSender").Count, - omitted.Descriptors("IEmailSender").Count); + omitted.Descriptors("IEmailSender").Count + ); } /// @@ -276,9 +303,11 @@ public void NoEnvironmentSuppliedUsesTheProcessEnvironment() { [Theory] [InlineData("Development")] [InlineData("Production")] - public void AReferencedModuleConditionalDoesNotOverrideTheReferencingModule(string environmentName) { - const string source = - """ + public void AReferencedModuleConditionalDoesNotOverrideTheReferencingModule( + string environmentName + ) + { + const string source = """ public interface IEmailSender { } [SingletonService(Realm = typeof(LibraryModule))] @@ -300,7 +329,8 @@ public partial class TestModule; Assert.Equal( assembly.Type("ApplicationOwn"), - assembly.BuildProvider().GetService(assembly.Type("IEmailSender"))!.GetType()); + assembly.BuildProvider().GetService(assembly.Type("IEmailSender"))!.GetType() + ); } /// @@ -311,10 +341,11 @@ public partial class TestModule; [InlineData("Development", "ApplicationFake")] [InlineData("Production", "LibrarySmtp")] public void AnApplicationConditionalOverridesAReferencedModuleDefault( - string environmentName, string expected) { - - const string source = - """ + string environmentName, + string expected + ) + { + const string source = """ public interface IEmailSender { } [SingletonService(Realm = typeof(LibraryModule))] @@ -336,7 +367,8 @@ public partial class TestModule; Assert.Equal( assembly.Type(expected), - assembly.BuildProvider().GetService(assembly.Type("IEmailSender"))!.GetType()); + assembly.BuildProvider().GetService(assembly.Type("IEmailSender"))!.GetType() + ); } /// @@ -344,9 +376,9 @@ public partial class TestModule; /// both are still in the enumerable. /// [Fact] - public void AConditionalOverrideLeavesTheDefaultInTheEnumerable() { - const string source = - """ + public void AConditionalOverrideLeavesTheDefaultInTheEnumerable() + { + const string source = """ public interface IEmailSender { } [SingletonService] @@ -367,9 +399,9 @@ public class FakeEmailSender : IEmailSender { } /// kind of registration it is, and the two compose. /// [Fact] - public void AConditionCombinesWithReplace() { - const string source = - """ + public void AConditionCombinesWithReplace() + { + const string source = """ public interface IEmailSender { } [SingletonService] @@ -384,14 +416,21 @@ public class FakeEmailSender : IEmailSender { } var production = Compile(source + Module, Env("Production")); Assert.Single(development.Descriptors("IEmailSender")); - Assert.Equal(development.Type("FakeEmailSender"), development.Descriptor("IEmailSender").ImplementationType); + Assert.Equal( + development.Type("FakeEmailSender"), + development.Descriptor("IEmailSender").ImplementationType + ); Assert.Single(production.Descriptors("IEmailSender")); - Assert.Equal(production.Type("SmtpEmailSender"), production.Descriptor("IEmailSender").ImplementationType); + Assert.Equal( + production.Type("SmtpEmailSender"), + production.Descriptor("IEmailSender").ImplementationType + ); } [Fact] - public void ConditionsAreReportedAtBuildTime() { + public void ConditionsAreReportedAtBuildTime() + { var result = GeneratorTestHarness.Run(Preamble + NameGated + Module); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0011"); @@ -400,9 +439,9 @@ public void ConditionsAreReportedAtBuildTime() { } [Fact] - public void UnconditionalServicesReportNothing() { - const string source = - """ + public void UnconditionalServicesReportNothing() + { + const string source = """ public interface IThing { } [SingletonService] @@ -418,15 +457,15 @@ public class Thing : IThing { } [InlineData("[IfEnvironment]", "environment name")] [InlineData("[IfNotEnvironment]", "environment name")] [InlineData("[IfEnvironmentValue(\"\")]", "key")] - public void AConditionThatTestsNothingIsRefused(string attribute, string expectedKind) { - var source = - $$""" - public interface IThing { } + public void AConditionThatTestsNothingIsRefused(string attribute, string expectedKind) + { + var source = $$""" + public interface IThing { } - [SingletonService] - {{attribute}} - public class Thing : IThing { } - """; + [SingletonService] + {{attribute}} + public class Thing : IThing { } + """; var result = GeneratorTestHarness.Run(Preamble + source + Module); @@ -436,8 +475,10 @@ public class Thing : IThing { } // Reported, not silently dropped: the service still registers rather than vanishing. Assert.Single( - GeneratedAssembly.Create(Preamble + source + Module, environment: ModuleEnvironment.None) - .Descriptors("IThing")); + GeneratedAssembly + .Create(Preamble + source + Module, environment: ModuleEnvironment.None) + .Descriptors("IThing") + ); } /// @@ -445,9 +486,9 @@ public class Thing : IThing { } /// keeps its environment names in one place would silently never match. /// [Fact] - public void ConditionArgumentsMayBeConstants() { - const string source = - """ + public void ConditionArgumentsMayBeConstants() + { + const string source = """ public static class Environments { public const string Development = "Development"; } @@ -469,9 +510,9 @@ public class Thing : IThing { } /// already, and here it would register a development service in production. /// [Fact] - public void ANamespaceQualifiedConditionStillApplies() { - const string source = - """ + public void ANamespaceQualifiedConditionStillApplies() + { + const string source = """ public interface IThing { } [SingletonService] diff --git a/tests/DependencyModules.Tests/GeneratorTests/ExtensionSeamTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ExtensionSeamTests.cs index 0d36ad3..ccdbe25 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ExtensionSeamTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ExtensionSeamTests.cs @@ -11,18 +11,16 @@ namespace DependencyModules.Tests.GeneratorTests; /// its own attribute generators through AttributeSourceGenerators(). Both of these pin /// behaviour a framework only finds out about in a consuming application, which is too late. /// -public class ExtensionSeamTests { - - private const string FrameworkAttribute = - """ +public class ExtensionSeamTests +{ + private const string FrameworkAttribute = """ namespace Test.Framework; [System.AttributeUsage(System.AttributeTargets.Class)] public class FrameworkModuleAttribute : System.Attribute; """; - private const string ModuleAndService = - """ + private const string ModuleAndService = """ using DependencyModules.Runtime; using DependencyModules.Runtime.Attributes; using Microsoft.Extensions.DependencyInjection; @@ -55,13 +53,19 @@ public static IServiceCollection Compose() => /// SetupRootGenerator was empty by default and easy to miss. /// [Fact] - public void FrameworkGenerator_EmitsTheModule_WithoutOverridingSetupRootGenerator() { + public void FrameworkGenerator_EmitsTheModule_WithoutOverridingSetupRootGenerator() + { var result = GeneratorTestHarness.Run( - new Dictionary { + new Dictionary + { ["Framework.cs"] = FrameworkAttribute, - ["App.cs"] = ModuleAndService + ["App.cs"] = ModuleAndService, }, - generators: new ISourceGenerator[] { new FrameworkShapedGenerator().AsSourceGenerator() }); + generators: new ISourceGenerator[] + { + new FrameworkShapedGenerator().AsSourceGenerator(), + } + ); result.AssertNoErrors(); Assert.Contains("IDependencyModule", result.SourceContaining("AppModule.Module")); @@ -71,15 +75,21 @@ public void FrameworkGenerator_EmitsTheModule_WithoutOverridingSetupRootGenerato /// A generator that only contributes providers opts out, and then nothing declares the module. /// [Fact] - public void FrameworkGenerator_OptingOut_EmitsNoModule() { + public void FrameworkGenerator_OptingOut_EmitsNoModule() + { var result = GeneratorTestHarness.Run( - new Dictionary { + new Dictionary + { ["Framework.cs"] = FrameworkAttribute, - ["App.cs"] = ModuleAndService + ["App.cs"] = ModuleAndService, }, - generators: new ISourceGenerator[] { new ProvidersOnlyGenerator().AsSourceGenerator() }); + generators: new ISourceGenerator[] { new ProvidersOnlyGenerator().AsSourceGenerator() } + ); - Assert.DoesNotContain(result.GeneratedSources.Keys, key => key.Contains("AppModule.Module")); + Assert.DoesNotContain( + result.GeneratedSources.Keys, + key => key.Contains("AppModule.Module") + ); } /// @@ -93,26 +103,32 @@ public void FrameworkGenerator_OptingOut_EmitsNoModule() { /// have to work together. /// [Fact] - public void StackedGenerators_OverAConsoleApplication_EmitOneApplicationModule() { + public void StackedGenerators_OverAConsoleApplication_EmitOneApplicationModule() + { var result = GeneratorTestHarness.Run( - new Dictionary { + new Dictionary + { ["Framework.cs"] = FrameworkAttribute, - ["Program.cs"] = - """ - System.Console.WriteLine("hello"); - """, - ["App.cs"] = ModuleAndService + ["Program.cs"] = """ + System.Console.WriteLine("hello"); + """, + ["App.cs"] = ModuleAndService, }, outputKind: OutputKind.ConsoleApplication, - generators: new ISourceGenerator[] { + generators: new ISourceGenerator[] + { new SourceGenerator.SourceGenerator().AsSourceGenerator(), - new FrameworkShapedGenerator().AsSourceGenerator() - }); + new FrameworkShapedGenerator().AsSourceGenerator(), + } + ); result.AssertNoErrors(); Assert.Empty(result.DuplicateHintNames); - Assert.Single(result.GeneratedSources.Keys, key => key.Contains("ApplicationModule.Module")); + Assert.Single( + result.GeneratedSources.Keys, + key => key.Contains("ApplicationModule.Module") + ); } /// @@ -121,9 +137,9 @@ public void StackedGenerators_OverAConsoleApplication_EmitOneApplicationModule() /// this package ships, and writing them from both declares every module twice. /// [Fact] - public void ThirdPartyGenerator_OnTheDefaultModuleAttribute_WritesNoModuleOfItsOwn() { - var source = - """ + public void ThirdPartyGenerator_OnTheDefaultModuleAttribute_WritesNoModuleOfItsOwn() + { + var source = """ using DependencyModules.Runtime; using DependencyModules.Runtime.Attributes; using Microsoft.Extensions.DependencyInjection; @@ -146,10 +162,12 @@ public static IServiceCollection Compose() => var result = GeneratorTestHarness.Run( new Dictionary { ["App.cs"] = source }, - generators: new ISourceGenerator[] { + generators: new ISourceGenerator[] + { new SourceGenerator.SourceGenerator().AsSourceGenerator(), - new ThirdPartyGenerator().AsSourceGenerator() - }); + new ThirdPartyGenerator().AsSourceGenerator(), + } + ); result.AssertNoErrors(); @@ -160,12 +178,13 @@ public static IServiceCollection Compose() => /// /// What a framework declares: its module attribute, and the generators that read its own. /// - private class FrameworkShapedGenerator : BaseSourceGenerator { - + private class FrameworkShapedGenerator : BaseSourceGenerator + { protected override ITypeDefinition[] ModuleAttributeTypes() => new[] { TypeDefinition.Get("Test.Framework", "FrameworkModuleAttribute") }; - protected override IEnumerable AttributeSourceGenerators() { + protected override IEnumerable AttributeSourceGenerators() + { yield return new global::DependencyModules.SourceGenerator.ServiceSourceGenerator(); } } @@ -174,19 +193,22 @@ protected override IEnumerable AttributeSource /// A generator taking the base class defaults, triggering on [DependencyModule]: the /// shape the extension guide documents. /// - private class ThirdPartyGenerator : BaseSourceGenerator { - - protected override IEnumerable AttributeSourceGenerators() { + private class ThirdPartyGenerator : BaseSourceGenerator + { + protected override IEnumerable AttributeSourceGenerators() + { yield break; } } - private class ProvidersOnlyGenerator : FrameworkShapedGenerator { - + private class ProvidersOnlyGenerator : FrameworkShapedGenerator + { protected override void SetupRootGenerator( IncrementalGeneratorInitializationContext context, IncrementalValueProvider> valuesProvider) { } + SourceGenerator.Impl.Models.DependencyModuleConfigurationModel Right + )>> valuesProvider + ) { } } } diff --git a/tests/DependencyModules.Tests/GeneratorTests/FileLoggerTests.cs b/tests/DependencyModules.Tests/GeneratorTests/FileLoggerTests.cs index 86b270e..c38011c 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/FileLoggerTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/FileLoggerTests.cs @@ -8,16 +8,22 @@ namespace DependencyModules.Tests.GeneratorTests; /// FileLogger backs the DependencyModules_LogOutputDirectory build property. It runs inside the /// compiler, so it must be inert unless explicitly switched on and must never fail a build. /// -public class FileLoggerTests : IDisposable { - private readonly string _outputFolder = - Path.Combine(Path.GetTempPath(), "DependencyModulesLoggerTests", Guid.NewGuid().ToString("n")); +public class FileLoggerTests : IDisposable +{ + private readonly string _outputFolder = Path.Combine( + Path.GetTempPath(), + "DependencyModulesLoggerTests", + Guid.NewGuid().ToString("n") + ); [Fact] - public void WithNoOutputFolder_WritesNothing() { + public void WithNoOutputFolder_WritesNothing() + { var before = Directory.GetCurrentDirectory(); var filesBefore = Directory.GetFiles(before); - using (var logger = new FileLogger(Configuration(logOutputFolder: ""), "test")) { + using (var logger = new FileLogger(Configuration(logOutputFolder: ""), "test")) + { logger.Info("a message"); logger.Error("a problem"); } @@ -30,8 +36,10 @@ public void WithNoOutputFolder_WritesNothing() { /// configured folder entirely. /// [Fact] - public void WithAnOutputFolder_WritesIntoThatFolder() { - using (var logger = new FileLogger(Configuration(_outputFolder), "generator")) { + public void WithAnOutputFolder_WritesIntoThatFolder() + { + using (var logger = new FileLogger(Configuration(_outputFolder), "generator")) + { logger.Info("a message"); } @@ -43,10 +51,12 @@ public void WithAnOutputFolder_WritesIntoThatFolder() { } [Fact] - public void WithAnOutputFolder_CreatesTheFolderIfMissing() { + public void WithAnOutputFolder_CreatesTheFolderIfMissing() + { Assert.False(Directory.Exists(_outputFolder)); - using (var logger = new FileLogger(Configuration(_outputFolder), "generator")) { + using (var logger = new FileLogger(Configuration(_outputFolder), "generator")) + { logger.Info("a message"); } @@ -54,8 +64,10 @@ public void WithAnOutputFolder_CreatesTheFolderIfMissing() { } [Fact] - public void RecordsLevelsAndMessages() { - using (var logger = new FileLogger(Configuration(_outputFolder), "generator")) { + public void RecordsLevelsAndMessages() + { + using (var logger = new FileLogger(Configuration(_outputFolder), "generator")) + { logger.Info("an info message"); logger.Error("an error message"); logger.Info("with data", "the data"); @@ -73,37 +85,44 @@ public void RecordsLevelsAndMessages() { /// Swallowing it produced a successful build with no registrations and no message at all. /// [Fact] - public void Wrap_WithoutAReporter_RethrowsSoTheFailureIsVisible() { - var exception = Assert.Throws( - () => FileLogger.Wrap( + public void Wrap_WithoutAReporter_RethrowsSoTheFailureIsVisible() + { + var exception = Assert.Throws(() => + FileLogger.Wrap( "generator", Configuration(_outputFolder), - _ => throw new InvalidOperationException("generator blew up"))); + _ => throw new InvalidOperationException("generator blew up") + ) + ); Assert.Equal("generator blew up", exception.Message); } [Fact] - public void Wrap_WithAReporter_HandsItTheExceptionInsteadOfPropagating() { + public void Wrap_WithAReporter_HandsItTheExceptionInsteadOfPropagating() + { Exception? reported = null; FileLogger.Wrap( "generator", Configuration(_outputFolder), _ => throw new InvalidOperationException("generator blew up"), - exception => reported = exception); + exception => reported = exception + ); Assert.NotNull(reported); Assert.Equal("generator blew up", reported!.Message); } [Fact] - public void Wrap_RecordsTheExceptionInTheLog() { + public void Wrap_RecordsTheExceptionInTheLog() + { FileLogger.Wrap( "generator", Configuration(_outputFolder), _ => throw new InvalidOperationException("generator blew up"), - _ => { }); + _ => { } + ); var content = File.ReadAllText(Directory.GetFiles(_outputFolder).Single()); @@ -112,25 +131,33 @@ public void Wrap_RecordsTheExceptionInTheLog() { } [Fact] - public void Wrap_RunsTheCallbackAndDisposesTheLogger() { + public void Wrap_RunsTheCallbackAndDisposesTheLogger() + { var ran = false; - FileLogger.Wrap("generator", Configuration(_outputFolder), logger => { - ran = true; - logger.Info("inside"); - }); + FileLogger.Wrap( + "generator", + Configuration(_outputFolder), + logger => + { + ran = true; + logger.Info("inside"); + } + ); Assert.True(ran); Assert.Contains("inside", File.ReadAllText(Directory.GetFiles(_outputFolder).Single())); } [Fact] - public void AnUnwritableOutputFolder_DoesNotThrow() { + public void AnUnwritableOutputFolder_DoesNotThrow() + { // A path whose parent is a file cannot be created; logging must still not fail the build. var file = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); File.WriteAllText(file, "not a directory"); - try { + try + { var logger = new FileLogger(Configuration(Path.Combine(file, "nested")), "generator"); logger.Info("a message"); @@ -138,7 +165,8 @@ public void AnUnwritableOutputFolder_DoesNotThrow() { Assert.Null(exception); } - finally { + finally + { File.Delete(file); } } @@ -152,10 +180,13 @@ private static DependencyModuleConfigurationModel Configuration(string logOutput AutoGenerateEntry: true, LogOutputFolder: logOutputFolder, LogOutputLevel.Debug, - GenerateFactories: false); + GenerateFactories: false + ); - public void Dispose() { - if (Directory.Exists(_outputFolder)) { + public void Dispose() + { + if (Directory.Exists(_outputFolder)) + { Directory.Delete(_outputFolder, recursive: true); } } diff --git a/tests/DependencyModules.Tests/GeneratorTests/GenerateFactoriesInterceptionTests.cs b/tests/DependencyModules.Tests/GeneratorTests/GenerateFactoriesInterceptionTests.cs index 4d265ff..b68cc4b 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/GenerateFactoriesInterceptionTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/GenerateFactoriesInterceptionTests.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.DependencyInjection; using DependencyModules.Tests.Infrastructure; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace DependencyModules.Tests.GeneratorTests; @@ -24,10 +24,9 @@ namespace DependencyModules.Tests.GeneratorTests; /// property's whole contract — it is meant to change how a service is constructed, not what is /// registered or what wraps it. /// -public class GenerateFactoriesInterceptionTests { - - private const string Interceptor = - """ +public class GenerateFactoriesInterceptionTests +{ + private const string Interceptor = """ public sealed class CountingInterceptor : IInterceptor { public TResult Intercept(InvocationContext context) => context.Proceed(); } @@ -39,7 +38,8 @@ public sealed class CountingInterceptor : IInterceptor { [Theory] [InlineData(false)] [InlineData(true)] - public void AnUnmarkedSibling_IsNotWrapped(bool generateFactories) { + public void AnUnmarkedSibling_IsNotWrapped(bool generateFactories) + { var resolved = Resolve( """ [SingletonService] [Intercept(typeof(CountingInterceptor))] @@ -48,7 +48,8 @@ public sealed class Loud : IGreeter { public string Greet() => "loud"; } [SingletonService] public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } """, - generateFactories); + generateFactories + ); Assert.Equal(["Loud_Intercepted", "Quiet"], resolved); } @@ -60,7 +61,8 @@ public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } [Theory] [InlineData(false)] [InlineData(true)] - public void TwoMarkedImplementations_EachGetTheirOwnWrapper(bool generateFactories) { + public void TwoMarkedImplementations_EachGetTheirOwnWrapper(bool generateFactories) + { var resolved = Resolve( """ [SingletonService] [Intercept(typeof(CountingInterceptor))] @@ -69,7 +71,8 @@ public sealed class Loud : IGreeter { public string Greet() => "loud"; } [SingletonService] [Intercept(typeof(CountingInterceptor))] public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } """, - generateFactories); + generateFactories + ); Assert.Equal(["Loud_Intercepted", "Quiet_Intercepted"], resolved); } @@ -81,7 +84,8 @@ public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } [Theory] [InlineData(false)] [InlineData(true)] - public void AKeyedSibling_IsNotWrapped(bool generateFactories) { + public void AKeyedSibling_IsNotWrapped(bool generateFactories) + { var generated = Build( """ [SingletonService(Key = "loud")] [Intercept(typeof(CountingInterceptor))] @@ -90,7 +94,8 @@ public sealed class Loud : IGreeter { public string Greet() => "loud"; } [SingletonService(Key = "quiet")] public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } """, - generateFactories); + generateFactories + ); var provider = generated.BuildProvider(); var serviceType = generated.Type("IGreeter"); @@ -109,39 +114,46 @@ public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } [Theory] [InlineData(false)] [InlineData(true)] - public void AConventionRegisteredSibling_IsNotWrapped(bool generateFactories) { + public void AConventionRegisteredSibling_IsNotWrapped(bool generateFactories) + { var generated = GeneratedAssembly.Create( $$""" - using DependencyModules.Runtime.Attributes; - using DependencyModules.Runtime.Conventions; - using DependencyModules.Runtime.Interception; + using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Conventions; + using DependencyModules.Runtime.Interception; - namespace TestNamespace; + namespace TestNamespace; - public interface IGreeter { string Greet(); } + public interface IGreeter { string Greet(); } - {{Interceptor}} + {{Interceptor}} - [Intercept(typeof(CountingInterceptor))] - public sealed class Loud : IGreeter { public string Greet() => "loud"; } + [Intercept(typeof(CountingInterceptor))] + public sealed class Loud : IGreeter { public string Greet() => "loud"; } - public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } + public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().AsSingleton(); - } - } - """, - buildProperties: new Dictionary { - ["DependencyModules_GenerateFactories"] = generateFactories ? "true" : "false" - }); + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().AsSingleton(); + } + } + """, + buildProperties: new Dictionary + { + ["DependencyModules_GenerateFactories"] = generateFactories ? "true" : "false", + } + ); var provider = generated.BuildProvider(); - var resolved = ((System.Collections.IEnumerable)provider - .GetService(typeof(IEnumerable<>).MakeGenericType(generated.Type("IGreeter")))!) + var resolved = ( + (System.Collections.IEnumerable) + provider.GetService( + typeof(IEnumerable<>).MakeGenericType(generated.Type("IGreeter")) + )! + ) .Cast() .Select(greeter => greeter.GetType().Name) .OrderBy(name => name, System.StringComparer.Ordinal) @@ -150,12 +162,17 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { Assert.Equal(["Loud_Intercepted", "Quiet"], resolved); } - private static string[] Resolve(string body, bool generateFactories) { + private static string[] Resolve(string body, bool generateFactories) + { var generated = Build(body, generateFactories); var provider = generated.BuildProvider(); - return ((System.Collections.IEnumerable)provider - .GetService(typeof(IEnumerable<>).MakeGenericType(generated.Type("IGreeter")))!) + return ( + (System.Collections.IEnumerable) + provider.GetService( + typeof(IEnumerable<>).MakeGenericType(generated.Type("IGreeter")) + )! + ) .Cast() .Select(greeter => greeter.GetType().Name) .OrderBy(name => name, System.StringComparer.Ordinal) @@ -165,21 +182,23 @@ private static string[] Resolve(string body, bool generateFactories) { private static GeneratedAssembly Build(string body, bool generateFactories) => GeneratedAssembly.Create( $$""" - using DependencyModules.Runtime.Attributes; - using DependencyModules.Runtime.Interception; + using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Interception; - namespace TestNamespace; + namespace TestNamespace; - public interface IGreeter { string Greet(); } + public interface IGreeter { string Greet(); } - {{Interceptor}} + {{Interceptor}} - {{body}} + {{body}} - [DependencyModule] - public partial class TestModule; - """, - buildProperties: new Dictionary { - ["DependencyModules_GenerateFactories"] = generateFactories ? "true" : "false" - }); + [DependencyModule] + public partial class TestModule; + """, + buildProperties: new Dictionary + { + ["DependencyModules_GenerateFactories"] = generateFactories ? "true" : "false", + } + ); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/GeneratedBehaviourTests.cs b/tests/DependencyModules.Tests/GeneratorTests/GeneratedBehaviourTests.cs index 0ea87a3..bb164a5 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/GeneratedBehaviourTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/GeneratedBehaviourTests.cs @@ -12,11 +12,14 @@ namespace DependencyModules.Tests.GeneratorTests; /// These are the tests that would fail if the generator emitted well-formed code that registered /// the wrong thing. Asserting on the shape of generated text cannot catch that. /// -public class GeneratedBehaviourTests { - +public class GeneratedBehaviourTests +{ [Fact] - public void SingletonService_ResolvesTheImplementation() { - var generated = GeneratedAssembly.Create(Module("[SingletonService] public class Thing : IThing;")); + public void SingletonService_ResolvesTheImplementation() + { + var generated = GeneratedAssembly.Create( + Module("[SingletonService] public class Thing : IThing;") + ); var resolved = generated.ResolveRequired("IThing"); @@ -24,8 +27,11 @@ public void SingletonService_ResolvesTheImplementation() { } [Fact] - public void SingletonService_ReturnsTheSameInstanceEveryTime() { - var generated = GeneratedAssembly.Create(Module("[SingletonService] public class Thing : IThing;")); + public void SingletonService_ReturnsTheSameInstanceEveryTime() + { + var generated = GeneratedAssembly.Create( + Module("[SingletonService] public class Thing : IThing;") + ); var provider = generated.BuildProvider(); var serviceType = generated.Type("IThing"); @@ -33,8 +39,11 @@ public void SingletonService_ReturnsTheSameInstanceEveryTime() { } [Fact] - public void TransientService_ReturnsANewInstanceEveryTime() { - var generated = GeneratedAssembly.Create(Module("[TransientService] public class Thing : IThing;")); + public void TransientService_ReturnsANewInstanceEveryTime() + { + var generated = GeneratedAssembly.Create( + Module("[TransientService] public class Thing : IThing;") + ); var provider = generated.BuildProvider(); var serviceType = generated.Type("IThing"); @@ -42,8 +51,11 @@ public void TransientService_ReturnsANewInstanceEveryTime() { } [Fact] - public void ScopedService_IsSharedWithinAScopeAndDiffersAcrossScopes() { - var generated = GeneratedAssembly.Create(Module("[ScopedService] public class Thing : IThing;")); + public void ScopedService_IsSharedWithinAScopeAndDiffersAcrossScopes() + { + var generated = GeneratedAssembly.Create( + Module("[ScopedService] public class Thing : IThing;") + ); var provider = generated.BuildProvider(); var serviceType = generated.Type("IThing"); @@ -62,19 +74,29 @@ public void ScopedService_IsSharedWithinAScopeAndDiffersAcrossScopes() { [InlineData("SingletonService", ServiceLifetime.Singleton)] [InlineData("ScopedService", ServiceLifetime.Scoped)] [InlineData("TransientService", ServiceLifetime.Transient)] - public void ServiceAttribute_RegistersTheMatchingLifetime(string attribute, ServiceLifetime expected) { - var generated = GeneratedAssembly.Create(Module($"[{attribute}] public class Thing : IThing;")); + public void ServiceAttribute_RegistersTheMatchingLifetime( + string attribute, + ServiceLifetime expected + ) + { + var generated = GeneratedAssembly.Create( + Module($"[{attribute}] public class Thing : IThing;") + ); Assert.Equal(expected, generated.Descriptor("IThing").Lifetime); } [Fact] - public void AsProperty_ResolvesUnderTheRequestedServiceTypeOnly() { - var generated = GeneratedAssembly.Create(Module( - """ - public interface IOther; - [SingletonService(As = typeof(IOther))] public class Thing : IThing, IOther; - """)); + public void AsProperty_ResolvesUnderTheRequestedServiceTypeOnly() + { + var generated = GeneratedAssembly.Create( + Module( + """ + public interface IOther; + [SingletonService(As = typeof(IOther))] public class Thing : IThing, IOther; + """ + ) + ); var provider = generated.BuildProvider(); @@ -83,9 +105,11 @@ public interface IOther; } [Fact] - public void KeyedService_ResolvesOnlyThroughItsKey() { - var generated = GeneratedAssembly.Create(Module( - """[SingletonService(Key = "the-key")] public class Thing : IThing;""")); + public void KeyedService_ResolvesOnlyThroughItsKey() + { + var generated = GeneratedAssembly.Create( + Module("""[SingletonService(Key = "the-key")] public class Thing : IThing;""") + ); var provider = generated.BuildProvider(); var serviceType = generated.Type("IThing"); @@ -95,18 +119,28 @@ public void KeyedService_ResolvesOnlyThroughItsKey() { } [Fact] - public void KeyedServices_WithDifferentKeysResolveDifferentImplementations() { - var generated = GeneratedAssembly.Create(Module( - """ - [SingletonService(Key = "first")] public class FirstThing : IThing; - [SingletonService(Key = "second")] public class SecondThing : IThing; - """)); + public void KeyedServices_WithDifferentKeysResolveDifferentImplementations() + { + var generated = GeneratedAssembly.Create( + Module( + """ + [SingletonService(Key = "first")] public class FirstThing : IThing; + [SingletonService(Key = "second")] public class SecondThing : IThing; + """ + ) + ); var provider = generated.BuildProvider(); var serviceType = generated.Type("IThing"); - Assert.Equal(generated.Type("FirstThing"), provider.GetKeyedService(serviceType, "first")!.GetType()); - Assert.Equal(generated.Type("SecondThing"), provider.GetKeyedService(serviceType, "second")!.GetType()); + Assert.Equal( + generated.Type("FirstThing"), + provider.GetKeyedService(serviceType, "first")!.GetType() + ); + Assert.Equal( + generated.Type("SecondThing"), + provider.GetKeyedService(serviceType, "second")!.GetType() + ); } /// @@ -114,14 +148,18 @@ public void KeyedServices_WithDifferentKeysResolveDifferentImplementations() { /// and through every interface it implements. /// [Fact] - public void CrossWireService_SharesOneInstanceAcrossAllItsInterfaces() { - var generated = GeneratedAssembly.Create(Module( - """ - public interface IOther; - [CrossWireService(Lifetime = ServiceLifetime.Singleton)] - public class Thing : IThing, IOther; - """, - extraUsings: "using Microsoft.Extensions.DependencyInjection;")); + public void CrossWireService_SharesOneInstanceAcrossAllItsInterfaces() + { + var generated = GeneratedAssembly.Create( + Module( + """ + public interface IOther; + [CrossWireService(Lifetime = ServiceLifetime.Singleton)] + public class Thing : IThing, IOther; + """, + extraUsings: "using Microsoft.Extensions.DependencyInjection;" + ) + ); var provider = generated.BuildProvider(); @@ -135,18 +173,24 @@ public class Thing : IThing, IOther; } [Fact] - public void TryRegistration_DoesNotReplaceAnExistingRegistration() { - var generated = GeneratedAssembly.Create(Module( - "[SingletonService(Using = RegistrationType.Try)] public class Thing : IThing;")); + public void TryRegistration_DoesNotReplaceAnExistingRegistration() + { + var generated = GeneratedAssembly.Create( + Module("[SingletonService(Using = RegistrationType.Try)] public class Thing : IThing;") + ); Assert.Equal(ServiceLifetime.Singleton, generated.Descriptor("IThing").Lifetime); Assert.Equal(generated.Type("Thing"), generated.ResolveRequired("IThing").GetType()); } [Fact] - public void ReplaceRegistration_LeavesASingleRegistration() { - var generated = GeneratedAssembly.Create(Module( - "[SingletonService(Using = RegistrationType.Replace)] public class Thing : IThing;")); + public void ReplaceRegistration_LeavesASingleRegistration() + { + var generated = GeneratedAssembly.Create( + Module( + "[SingletonService(Using = RegistrationType.Replace)] public class Thing : IThing;" + ) + ); Assert.Single(generated.Descriptors("IThing")); } @@ -159,43 +203,55 @@ public void ReplaceRegistration_LeavesASingleRegistration() { /// won. Renaming the class fixed it, and nothing said so. /// [Fact] - public void ReplaceRegistration_WinsEvenWhenItsTypeNameSortsFirst() { - var generated = GeneratedAssembly.Create(Module( - """ - [SingletonService(Using = RegistrationType.Replace)] public class AaaThing : IThing; - [SingletonService] public class ZzzThing : IThing; - """)); + public void ReplaceRegistration_WinsEvenWhenItsTypeNameSortsFirst() + { + var generated = GeneratedAssembly.Create( + Module( + """ + [SingletonService(Using = RegistrationType.Replace)] public class AaaThing : IThing; + [SingletonService] public class ZzzThing : IThing; + """ + ) + ); Assert.Single(generated.Descriptors("IThing")); Assert.Equal(generated.Type("AaaThing"), generated.ResolveRequired("IThing").GetType()); } [Fact] - public void TryRegistration_DeclinesEvenWhenItsTypeNameSortsFirst() { - var generated = GeneratedAssembly.Create(Module( - """ - [SingletonService(Using = RegistrationType.Try)] public class AaaThing : IThing; - [SingletonService] public class ZzzThing : IThing; - """)); + public void TryRegistration_DeclinesEvenWhenItsTypeNameSortsFirst() + { + var generated = GeneratedAssembly.Create( + Module( + """ + [SingletonService(Using = RegistrationType.Try)] public class AaaThing : IThing; + [SingletonService] public class ZzzThing : IThing; + """ + ) + ); Assert.Single(generated.Descriptors("IThing")); Assert.Equal(generated.Type("ZzzThing"), generated.ResolveRequired("IThing").GetType()); } [Fact] - public void ConstructorDependencies_AreInjectedFromTheContainer() { - var generated = GeneratedAssembly.Create(Module( - """ - public interface IDependency; + public void ConstructorDependencies_AreInjectedFromTheContainer() + { + var generated = GeneratedAssembly.Create( + Module( + """ + public interface IDependency; - [SingletonService] public class Dependency : IDependency; + [SingletonService] public class Dependency : IDependency; - [SingletonService] - public class Thing : IThing { - public IDependency Injected { get; } - public Thing(IDependency dependency) => Injected = dependency; - } - """)); + [SingletonService] + public class Thing : IThing { + public IDependency Injected { get; } + public Thing(IDependency dependency) => Injected = dependency; + } + """ + ) + ); var provider = generated.BuildProvider(); var thing = provider.GetService(generated.Type("IThing"))!; @@ -207,16 +263,20 @@ public class Thing : IThing { } [Fact] - public void StaticFactory_IsInvokedToCreateTheService() { - var generated = GeneratedAssembly.Create(Module( - """ - public class Thing : IThing { - public string Origin { get; private set; } = "constructor"; + public void StaticFactory_IsInvokedToCreateTheService() + { + var generated = GeneratedAssembly.Create( + Module( + """ + public class Thing : IThing { + public string Origin { get; private set; } = "constructor"; - [SingletonService] - public static IThing Create() => new Thing { Origin = "factory" }; - } - """)); + [SingletonService] + public static IThing Create() => new Thing { Origin = "factory" }; + } + """ + ) + ); var thing = generated.ResolveRequired("IThing"); @@ -224,31 +284,37 @@ public class Thing : IThing { } [Fact] - public void StaticFactory_ReceivesItsDependenciesFromTheContainer() { - var generated = GeneratedAssembly.Create(Module( - """ - public interface IDependency; + public void StaticFactory_ReceivesItsDependenciesFromTheContainer() + { + var generated = GeneratedAssembly.Create( + Module( + """ + public interface IDependency; - [SingletonService] public class Dependency : IDependency; + [SingletonService] public class Dependency : IDependency; - public class Thing : IThing { - public IDependency? Injected { get; private set; } + public class Thing : IThing { + public IDependency? Injected { get; private set; } - [SingletonService] - public static IThing Create(IDependency dependency) => new Thing { Injected = dependency }; - } - """)); + [SingletonService] + public static IThing Create(IDependency dependency) => new Thing { Injected = dependency }; + } + """ + ) + ); var provider = generated.BuildProvider(); var thing = provider.GetService(generated.Type("IThing"))!; Assert.Same( provider.GetService(generated.Type("IDependency")), - thing.GetType().GetProperty("Injected")!.GetValue(thing)); + thing.GetType().GetProperty("Injected")!.GetValue(thing) + ); } [Fact] - public void OpenGenericService_ResolvesForAnyTypeArgument() { + public void OpenGenericService_ResolvesForAnyTypeArgument() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -262,7 +328,8 @@ public class GenericThing : IGeneric; [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = generated.BuildProvider(); var closed = generated.Type("IGeneric`1").MakeGenericType(typeof(string)); @@ -271,7 +338,8 @@ public partial class TestModule; } [Fact] - public void ClosedGenericService_ResolvesForItsOwnArgumentOnly() { + public void ClosedGenericService_ResolvesForItsOwnArgumentOnly() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -285,7 +353,8 @@ public class StringGeneric : IGeneric; [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = generated.BuildProvider(); var generic = generated.Type("IGeneric`1"); @@ -295,27 +364,35 @@ public partial class TestModule; } [Fact] - public void NestedService_ResolvesThroughItsContainingType() { - var generated = GeneratedAssembly.Create(Module( - """ - public static class Outer { - [SingletonService] - public class Inner : IThing; - } - """)); + public void NestedService_ResolvesThroughItsContainingType() + { + var generated = GeneratedAssembly.Create( + Module( + """ + public static class Outer { + [SingletonService] + public class Inner : IThing; + } + """ + ) + ); Assert.Equal("Inner", generated.ResolveRequired("IThing").GetType().Name); } [Fact] - public void RecordService_Resolves() { - var generated = GeneratedAssembly.Create(Module("[SingletonService] public record ThingRecord : IThing;")); + public void RecordService_Resolves() + { + var generated = GeneratedAssembly.Create( + Module("[SingletonService] public record ThingRecord : IThing;") + ); Assert.Equal(generated.Type("ThingRecord"), generated.ResolveRequired("IThing").GetType()); } [Fact] - public void ModuleConfiguration_RunsAlongsideGeneratedRegistrations() { + public void ModuleConfiguration_RunsAlongsideGeneratedRegistrations() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -337,7 +414,8 @@ public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); } } - """); + """ + ); var provider = generated.BuildProvider(); @@ -346,7 +424,8 @@ public void ConfigureServices(IServiceCollection services) { } [Fact] - public void OnlyRealm_RegistersOnlyServicesInThatRealm() { + public void OnlyRealm_RegistersOnlyServicesInThatRealm() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -365,7 +444,8 @@ public class InRealm : IInRealm; [SingletonService] public class OutsideRealm : IOutsideRealm; """, - "RealmModule"); + "RealmModule" + ); var provider = generated.BuildProvider(); @@ -374,7 +454,8 @@ public class OutsideRealm : IOutsideRealm; } [Fact] - public void ComposedModule_AppliesTheModulesItReferences() { + public void ComposedModule_AppliesTheModulesItReferences() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -393,13 +474,15 @@ public class FromBase : IFromBase; [BaseModule] public partial class ComposedModule; """, - "ComposedModule"); + "ComposedModule" + ); Assert.NotNull(generated.BuildProvider().GetService(generated.Type("IFromBase"))); } [Fact] - public void ServiceWithNoInterface_ResolvesAsItself() { + public void ServiceWithNoInterface_ResolvesAsItself() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -411,23 +494,24 @@ public class Standalone; [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.NotNull(generated.BuildProvider().GetService(generated.Type("Standalone"))); } private static string Module(string body, string extraUsings = "") => $$""" - using DependencyModules.Runtime.Attributes; - {{extraUsings}} + using DependencyModules.Runtime.Attributes; + {{extraUsings}} - namespace TestNamespace; + namespace TestNamespace; - public interface IThing; + public interface IThing; - {{body}} + {{body}} - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/GeneratedCodeRobustnessTests.cs b/tests/DependencyModules.Tests/GeneratorTests/GeneratedCodeRobustnessTests.cs index d3e3398..988af8c 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/GeneratedCodeRobustnessTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/GeneratedCodeRobustnessTests.cs @@ -9,8 +9,8 @@ namespace DependencyModules.Tests.GeneratorTests; /// configured. The harness compiles without implicit usings and without any project-level using /// directives, which is the strictest environment a consumer can present. /// -public class GeneratedCodeRobustnessTests { - +public class GeneratedCodeRobustnessTests +{ /// /// Regression test: the generated module attribute used to be emitted as a bare /// [AttributeUsage(AttributeTargets...)], which only compiled because the consuming @@ -18,7 +18,8 @@ public class GeneratedCodeRobustnessTests { /// failed with CS0246/CS0103 on every generated module. /// [Fact] - public void GeneratedCode_CompilesWithoutImplicitUsings() { + public void GeneratedCode_CompilesWithoutImplicitUsings() + { var result = GeneratorTestHarness.Run( """ namespace TestNamespace; @@ -30,13 +31,15 @@ public class Thing : IThing; [DependencyModules.Runtime.Attributes.DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); } [Fact] - public void GeneratedModuleAttribute_UsesFullyQualifiedAttributeUsage() { + public void GeneratedModuleAttribute_UsesFullyQualifiedAttributeUsage() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -45,7 +48,8 @@ namespace TestNamespace; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); var generated = result.SourceContaining(".Module.g.cs"); @@ -58,7 +62,8 @@ public partial class TestModule; /// A consumer that treats warnings as errors should not be broken by generated code. /// [Fact] - public void GeneratedCode_ProducesNoWarnings() { + public void GeneratedCode_ProducesNoWarnings() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -72,23 +77,36 @@ public class Thing : IThing; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); - var generatedTreePaths = result.Compilation.SyntaxTrees - .Where(tree => tree.FilePath.EndsWith(".g.cs", StringComparison.Ordinal)) + var generatedTreePaths = result + .Compilation.SyntaxTrees.Where(tree => + tree.FilePath.EndsWith(".g.cs", StringComparison.Ordinal) + ) .Select(tree => tree.FilePath) .ToHashSet(StringComparer.Ordinal); - var warnings = result.CompilationDiagnostics - .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Warning) - .Where(diagnostic => generatedTreePaths.Contains(diagnostic.Location.SourceTree?.FilePath ?? "")) + var warnings = result + .CompilationDiagnostics.Where(diagnostic => + diagnostic.Severity == DiagnosticSeverity.Warning + ) + .Where(diagnostic => + generatedTreePaths.Contains(diagnostic.Location.SourceTree?.FilePath ?? "") + ) .ToArray(); - Assert.True(warnings.Length == 0, - "Generated code produced warnings:" + Environment.NewLine + - string.Join(Environment.NewLine, warnings.Select(w => $" {w.Id} {w.GetMessage()}"))); + Assert.True( + warnings.Length == 0, + "Generated code produced warnings:" + + Environment.NewLine + + string.Join( + Environment.NewLine, + warnings.Select(w => $" {w.Id} {w.GetMessage()}") + ) + ); } /// @@ -105,58 +123,77 @@ public partial class TestModule; [Theory] [InlineData("attribute", "[SingletonService]")] [InlineData("convention", "")] - public void GeneratedCode_ProducesNoWarnings_ForANullableTypeArgument(string _, string attribute) { + public void GeneratedCode_ProducesNoWarnings_ForANullableTypeArgument( + string _, + string attribute + ) + { var result = GeneratorTestHarness.Run( $$""" - using DependencyModules.Runtime.Attributes; - using DependencyModules.Runtime.Conventions; + using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Conventions; - namespace TestNamespace; + namespace TestNamespace; - public class Book; + public class Book; - public interface IHandler { - TResult Handle(TQuery query); - } + public interface IHandler { + TResult Handle(TQuery query); + } - public record GetBook(string Isbn); + public record GetBook(string Isbn); - {{attribute}} - public class GetBookHandler : IHandler { - public Book? Handle(GetBook query) => null; - } + {{attribute}} + public class GetBookHandler : IHandler { + public Book? Handle(GetBook query) => null; + } - [DependencyModule] - public partial class TestModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll(typeof(IHandler<,>)).AsScoped(); - } - } - """); + [DependencyModule] + public partial class TestModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll(typeof(IHandler<,>)).AsScoped(); + } + } + """ + ); result.AssertNoErrors(); AssertNoWarningsFromGeneratedCode(result); } - private static void AssertNoWarningsFromGeneratedCode(GeneratorResult result) { - var generatedTreePaths = result.Compilation.SyntaxTrees - .Where(tree => tree.FilePath.EndsWith(".g.cs", StringComparison.Ordinal)) + private static void AssertNoWarningsFromGeneratedCode(GeneratorResult result) + { + var generatedTreePaths = result + .Compilation.SyntaxTrees.Where(tree => + tree.FilePath.EndsWith(".g.cs", StringComparison.Ordinal) + ) .Select(tree => tree.FilePath) .ToHashSet(StringComparer.Ordinal); - var warnings = result.CompilationDiagnostics - .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Warning) - .Where(diagnostic => generatedTreePaths.Contains(diagnostic.Location.SourceTree?.FilePath ?? "")) + var warnings = result + .CompilationDiagnostics.Where(diagnostic => + diagnostic.Severity == DiagnosticSeverity.Warning + ) + .Where(diagnostic => + generatedTreePaths.Contains(diagnostic.Location.SourceTree?.FilePath ?? "") + ) .ToArray(); - Assert.True(warnings.Length == 0, - "Generated code produced warnings:" + Environment.NewLine + - string.Join(Environment.NewLine, warnings.Select(w => $" {w.Id} {w.GetMessage()}"))); + Assert.True( + warnings.Length == 0, + "Generated code produced warnings:" + + Environment.NewLine + + string.Join( + Environment.NewLine, + warnings.Select(w => $" {w.Id} {w.GetMessage()}") + ) + ); } [Fact] - public void GeneratedCode_QualifiesReferencesToTheRuntime() { + public void GeneratedCode_QualifiesReferencesToTheRuntime() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -170,17 +207,20 @@ public class Thing : IThing; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); Assert.Contains( "global::DependencyModules.Runtime.Helpers.DependencyRegistry", - result.SourceContaining(".Module.g.cs")); + result.SourceContaining(".Module.g.cs") + ); } [Fact] - public void GeneratorRun_ReportsNoDiagnosticsForValidInput() { + public void GeneratorRun_ReportsNoDiagnosticsForValidInput() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -194,7 +234,8 @@ public class Thing : IThing; [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.Empty(result.GeneratorDiagnostics); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/GeneratorFeatureTests.cs b/tests/DependencyModules.Tests/GeneratorTests/GeneratorFeatureTests.cs index 359d590..3d3eab9 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/GeneratorFeatureTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/GeneratorFeatureTests.cs @@ -8,10 +8,11 @@ namespace DependencyModules.Tests.GeneratorTests; /// handlers, factories, and module composition. Each asserts that the generator produces output /// that compiles, which is the property most easily broken by a change to the writers. /// -public class GeneratorFeatureTests { - +public class GeneratorFeatureTests +{ [Fact] - public void OnlyRealm_RegistersOnlyServicesMarkedForThatRealm() { + public void OnlyRealm_RegistersOnlyServicesMarkedForThatRealm() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -29,7 +30,8 @@ public class InRealm : IInRealm; [SingletonService] public class OutsideRealm : IOutsideRealm; - """); + """ + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -39,7 +41,8 @@ public class OutsideRealm : IOutsideRealm; } [Fact] - public void ModuleWithoutOnlyRealm_RegistersUnmarkedServices() { + public void ModuleWithoutOnlyRealm_RegistersUnmarkedServices() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -53,14 +56,16 @@ public class Thing : IThing; [DependencyModule] public partial class OpenModule; - """); + """ + ); result.AssertNoErrors(); Assert.Contains("Thing", result.SourceContaining("Dependencies")); } [Fact] - public void GenerateUseMethod_EmitsTheNamedMethod() { + public void GenerateUseMethod_EmitsTheNamedMethod() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -74,14 +79,16 @@ public partial class UseMethodModule(string name) { [SingletonService(Realm = typeof(UseMethodModule))] public class RealmService; - """); + """ + ); result.AssertNoErrors(); Assert.Contains("UseTestModule", result.SourceContaining(".Module.g.cs")); } [Fact] - public void GenerateAttributeFalse_SuppressesTheModuleAttribute() { + public void GenerateAttributeFalse_SuppressesTheModuleAttribute() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -95,14 +102,19 @@ public class Thing : IThing; [DependencyModule(GenerateAttribute = false)] public partial class NoAttributeModule; - """); + """ + ); result.AssertNoErrors(); - Assert.DoesNotContain("class NoAttributeModuleAttribute", result.SourceContaining(".Module.g.cs")); + Assert.DoesNotContain( + "class NoAttributeModuleAttribute", + result.SourceContaining(".Module.g.cs") + ); } [Fact] - public void ModuleImplementingAFeature_EmitsAFeatureApplicator() { + public void ModuleImplementingAFeature_EmitsAFeatureApplicator() + { var result = GeneratorTestHarness.Run( """ using System.Collections.Generic; @@ -121,7 +133,8 @@ public partial class FeatureHandlerModule : IDependencyModuleFeature feature) { } } - """); + """ + ); result.AssertNoErrors(); var generated = result.SourceContaining(".Module.g.cs"); @@ -131,7 +144,8 @@ public void HandleFeature(IServiceCollection collection, IEnumerable [Fact] - public void ServiceImplementingSeveralInterfaces_RegistersTheFirstOne() { + public void ServiceImplementingSeveralInterfaces_RegistersTheFirstOne() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -304,7 +330,8 @@ public class Both : IFirst, ISecond; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -314,7 +341,8 @@ public partial class TestModule; } [Fact] - public void CrossWireService_RegistersTheImplementationAndItsInterfaces() { + public void CrossWireService_RegistersTheImplementationAndItsInterfaces() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -329,7 +357,8 @@ public class Both : IFirst, ISecond; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -340,7 +369,8 @@ public partial class TestModule; } [Fact] - public void ModuleLevelRegistrationType_AppliesToItsServices() { + public void ModuleLevelRegistrationType_AppliesToItsServices() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -354,14 +384,16 @@ public class Thing : IThing; [DependencyModule(Using = RegistrationType.Try)] public partial class TryModule; - """); + """ + ); result.AssertNoErrors(); Assert.Contains("TryAdd", result.SourceContaining("Dependencies")); } [Fact] - public void SeveralModulesInOneCompilation_EachGetTheirOwnFiles() { + public void SeveralModulesInOneCompilation_EachGetTheirOwnFiles() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -378,7 +410,8 @@ public partial class FirstModule; [DependencyModule] public partial class SecondModule; - """); + """ + ); result.AssertNoErrors(); @@ -387,7 +420,8 @@ public partial class SecondModule; } [Fact] - public void ModuleInANestedNamespace_GeneratesIntoThatNamespace() { + public void ModuleInANestedNamespace_GeneratesIntoThatNamespace() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -401,14 +435,16 @@ public class Thing : IThing; [DependencyModule] public partial class NestedModule; - """); + """ + ); result.AssertNoErrors(); Assert.Contains("namespace Outer.Inner", result.SourceContaining(".Module.g.cs")); } [Fact] - public void ServiceWithNoInterfaces_RegistersItselfAsTheServiceType() { + public void ServiceWithNoInterfaces_RegistersItselfAsTheServiceType() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -420,14 +456,16 @@ public class Standalone; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); Assert.Contains("Standalone", result.SourceContaining("Dependencies")); } [Fact] - public void ModuleThatOverridesEquals_KeepsTheDeveloperImplementation() { + public void ModuleThatOverridesEquals_KeepsTheDeveloperImplementation() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -447,14 +485,19 @@ public partial class CustomEqualsModule(string key) { public override int GetHashCode() => Key.GetHashCode(); } - """); + """ + ); result.AssertNoErrors(); - Assert.DoesNotContain("public override bool Equals", result.SourceContaining(".Module.g.cs")); + Assert.DoesNotContain( + "public override bool Equals", + result.SourceContaining(".Module.g.cs") + ); } [Fact] - public void ScopedAndTransientFactories_AreSupported() { + public void ScopedAndTransientFactories_AreSupported() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -474,7 +517,8 @@ public class Factories { [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); diff --git a/tests/DependencyModules.Tests/GeneratorTests/IncrementalGenerationTests.cs b/tests/DependencyModules.Tests/GeneratorTests/IncrementalGenerationTests.cs index 9244438..3f14578 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/IncrementalGenerationTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/IncrementalGenerationTests.cs @@ -9,77 +9,95 @@ namespace DependencyModules.Tests.GeneratorTests; /// and a real change serves stale output. These tests pin that behaviour from the outside, which /// is more durable than asserting on the comparers directly. /// -public class IncrementalGenerationTests { - +public class IncrementalGenerationTests +{ [Fact] - public void RerunningOnUnchangedSource_ReusesCachedOutput() { + public void RerunningOnUnchangedSource_ReusesCachedOutput() + { var result = GeneratorTestHarness.RunIncremental(Sources(Service), Sources(Service)); - Assert.True(result.AllOutputsCached, - "Re-running on identical source recomputed output: " + - string.Join(", ", result.OutputReasons)); + Assert.True( + result.AllOutputsCached, + "Re-running on identical source recomputed output: " + + string.Join(", ", result.OutputReasons) + ); } [Fact] - public void EditingAnUnrelatedMethodBody_ReusesCachedOutput() { + public void EditingAnUnrelatedMethodBody_ReusesCachedOutput() + { var before = Sources( - Service + - """ + Service + + """ - public class Unrelated { - public int Compute() => 1; - } - """); + public class Unrelated { + public int Compute() => 1; + } + """ + ); var after = Sources( - Service + - """ + Service + + """ - public class Unrelated { - public int Compute() => 2; - } - """); + public class Unrelated { + public int Compute() => 2; + } + """ + ); var result = GeneratorTestHarness.RunIncremental(before, after); Assert.Equal(result.FirstRun.Keys.OrderBy(k => k), result.SecondRun.Keys.OrderBy(k => k)); - Assert.True(result.AllOutputsCached, - "Editing an unrelated method body regenerated output: " + - string.Join(", ", result.OutputReasons)); + Assert.True( + result.AllOutputsCached, + "Editing an unrelated method body regenerated output: " + + string.Join(", ", result.OutputReasons) + ); } [Fact] - public void AddingAComment_ReusesCachedOutput() { + public void AddingAComment_ReusesCachedOutput() + { var result = GeneratorTestHarness.RunIncremental( Sources(Service), - Sources("// a new comment\n" + Service)); + Sources("// a new comment\n" + Service) + ); - Assert.True(result.AllOutputsCached, - "Adding a comment regenerated output: " + string.Join(", ", result.OutputReasons)); + Assert.True( + result.AllOutputsCached, + "Adding a comment regenerated output: " + string.Join(", ", result.OutputReasons) + ); } [Fact] - public void AddingAService_RegeneratesAndIncludesIt() { + public void AddingAService_RegeneratesAndIncludesIt() + { var after = Sources( - Service + - """ + Service + + """ - public interface ISecond; + public interface ISecond; - [SingletonService] - public class SecondThing : ISecond; - """); + [SingletonService] + public class SecondThing : ISecond; + """ + ); var result = GeneratorTestHarness.RunIncremental(Sources(Service), after); var dependencies = result.SecondRun.Single(pair => pair.Key.Contains("Dependencies")).Value; Assert.Contains("SecondThing", dependencies); - Assert.DoesNotContain("SecondThing", result.FirstRun.Single(pair => pair.Key.Contains("Dependencies")).Value); + Assert.DoesNotContain( + "SecondThing", + result.FirstRun.Single(pair => pair.Key.Contains("Dependencies")).Value + ); } [Fact] - public void ChangingAServiceLifetime_RegeneratesWithTheNewLifetime() { + public void ChangingAServiceLifetime_RegeneratesWithTheNewLifetime() + { var before = Sources("[SingletonService]\npublic class Thing : IThing;"); var after = Sources("[ScopedService]\npublic class Thing : IThing;"); @@ -94,16 +112,18 @@ public void ChangingAServiceLifetime_RegeneratesWithTheNewLifetime() { } [Fact] - public void RemovingAService_RegeneratesWithoutIt() { + public void RemovingAService_RegeneratesWithoutIt() + { var before = Sources( - Service + - """ + Service + + """ - public interface ISecond; + public interface ISecond; - [SingletonService] - public class SecondThing : ISecond; - """); + [SingletonService] + public class SecondThing : ISecond; + """ + ); var result = GeneratorTestHarness.RunIncremental(before, Sources(Service)); @@ -112,26 +132,25 @@ public class SecondThing : ISecond; Assert.DoesNotContain("SecondThing", second); } - private const string Service = - """ + private const string Service = """ [SingletonService] public class Thing : IThing; """; private static Dictionary Sources(string body) => - new() { - ["Test.cs"] = - $$""" - using DependencyModules.Runtime.Attributes; + new() + { + ["Test.cs"] = $$""" + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - public interface IThing; + public interface IThing; - {{body}} + {{body}} - [DependencyModule] - public partial class TestModule; - """ + [DependencyModule] + public partial class TestModule; + """, }; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/InterceptAttributeSpellingTests.cs b/tests/DependencyModules.Tests/GeneratorTests/InterceptAttributeSpellingTests.cs index d4c54be..8511511 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/InterceptAttributeSpellingTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/InterceptAttributeSpellingTests.cs @@ -17,17 +17,24 @@ namespace DependencyModules.Tests.GeneratorTests; /// Interceptors are audit, authorisation, retry and metrics, which is the worst set of things to /// silently not run. /// -public class InterceptAttributeSpellingTests { - +public class InterceptAttributeSpellingTests +{ [Theory] [InlineData("[Intercept(typeof(LoggingInterceptor))]")] [InlineData("[InterceptAttribute(typeof(LoggingInterceptor))]")] [InlineData("[DependencyModules.Runtime.Attributes.Intercept(typeof(LoggingInterceptor))]")] - [InlineData("[DependencyModules.Runtime.Attributes.InterceptAttribute(typeof(LoggingInterceptor))]")] - [InlineData("[global::DependencyModules.Runtime.Attributes.Intercept(typeof(LoggingInterceptor))]")] - [InlineData("[global::DependencyModules.Runtime.Attributes.InterceptAttribute(typeof(LoggingInterceptor))]")] + [InlineData( + "[DependencyModules.Runtime.Attributes.InterceptAttribute(typeof(LoggingInterceptor))]" + )] + [InlineData( + "[global::DependencyModules.Runtime.Attributes.Intercept(typeof(LoggingInterceptor))]" + )] + [InlineData( + "[global::DependencyModules.Runtime.Attributes.InterceptAttribute(typeof(LoggingInterceptor))]" + )] [InlineData("[Wrap(typeof(LoggingInterceptor))]")] - public void EverySpelling_GeneratesTheWrapper(string attribute) { + public void EverySpelling_GeneratesTheWrapper(string attribute) + { var result = Run(attribute).AssertNoErrors(); Assert.Contains("Thing_Intercepted", string.Join(", ", result.GeneratedSources.Keys)); @@ -36,9 +43,12 @@ public void EverySpelling_GeneratesTheWrapper(string attribute) { [Theory] [InlineData("[Intercept(typeof(LoggingInterceptor))]")] [InlineData("[DependencyModules.Runtime.Attributes.Intercept(typeof(LoggingInterceptor))]")] - [InlineData("[global::DependencyModules.Runtime.Attributes.InterceptAttribute(typeof(LoggingInterceptor))]")] + [InlineData( + "[global::DependencyModules.Runtime.Attributes.InterceptAttribute(typeof(LoggingInterceptor))]" + )] [InlineData("[Wrap(typeof(LoggingInterceptor))]")] - public void EverySpelling_AppliesTheInterception(string attribute) { + public void EverySpelling_AppliesTheInterception(string attribute) + { var interceptors = Run(attribute).SourceContaining("Interceptors"); Assert.Contains("Thing_Intercepted", interceptors); @@ -47,28 +57,29 @@ public void EverySpelling_AppliesTheInterception(string attribute) { private static GeneratorResult Run(string attribute) => GeneratorTestHarness.Run( $$""" - using DependencyModules.Runtime.Attributes; - using DependencyModules.Runtime.Interception; - using Wrap = DependencyModules.Runtime.Attributes.InterceptAttribute; + using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Interception; + using Wrap = DependencyModules.Runtime.Attributes.InterceptAttribute; - namespace TestNamespace; + namespace TestNamespace; - public interface IThing { - string Read(string key); - } + public interface IThing { + string Read(string key); + } - [SingletonService] - public class LoggingInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) => context.Proceed(); - } + [SingletonService] + public class LoggingInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) => context.Proceed(); + } - [SingletonService] - {{attribute}} - public class Thing : IThing { - public string Read(string key) => key; - } + [SingletonService] + {{attribute}} + public class Thing : IThing { + public string Read(string key) => key; + } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/InterceptedMembersTests.cs b/tests/DependencyModules.Tests/GeneratorTests/InterceptedMembersTests.cs index 90d16ac..8bb8209 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/InterceptedMembersTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/InterceptedMembersTests.cs @@ -17,36 +17,43 @@ namespace DependencyModules.Tests.GeneratorTests; /// just does not run through the chain, which is the same path a member no interceptor can serve /// already took. /// -public class InterceptedMembersTests { - +public class InterceptedMembersTests +{ [Fact] - public void WithNoKindsNamed_EveryMemberIsIntercepted() { + public void WithNoKindsNamed_EveryMemberIsIntercepted() + { var calls = Run("[Intercept(typeof(CountingInterceptor))]"); Assert.Equal(["Handle", "get_Name"], calls); } [Fact] - public void NamingMethods_LeavesPropertiesAlone() { + public void NamingMethods_LeavesPropertiesAlone() + { var calls = Run( - "[Intercept(typeof(CountingInterceptor), Members = InterceptedMembers.Methods)]"); + "[Intercept(typeof(CountingInterceptor), Members = InterceptedMembers.Methods)]" + ); Assert.Equal(["Handle"], calls); } [Fact] - public void NamingProperties_LeavesMethodsAlone() { + public void NamingProperties_LeavesMethodsAlone() + { var calls = Run( - "[Intercept(typeof(CountingInterceptor), Members = InterceptedMembers.Properties)]"); + "[Intercept(typeof(CountingInterceptor), Members = InterceptedMembers.Properties)]" + ); Assert.Equal(["get_Name"], calls); } [Fact] - public void KindsCombine() { + public void KindsCombine() + { var calls = Run( - "[Intercept(typeof(CountingInterceptor), " + - "Members = InterceptedMembers.Methods | InterceptedMembers.Properties)]"); + "[Intercept(typeof(CountingInterceptor), " + + "Members = InterceptedMembers.Methods | InterceptedMembers.Properties)]" + ); Assert.Equal(["Handle", "get_Name"], calls); } @@ -56,9 +63,11 @@ public void KindsCombine() { /// implements the interface. /// [Fact] - public void AnExcludedMember_IsStillForwarded() { + public void AnExcludedMember_IsStillForwarded() + { var generated = Build( - "[Intercept(typeof(CountingInterceptor), Members = InterceptedMembers.Methods)]"); + "[Intercept(typeof(CountingInterceptor), Members = InterceptedMembers.Methods)]" + ); var service = generated.BuildProvider().GetService(generated.Type("IHandler"))!; @@ -71,15 +80,20 @@ public void AnExcludedMember_IsStillForwarded() { /// asked to cover it is not — reporting that would report the feature. /// [Fact] - public void AnExcludedMember_IsNotReportedAsUnserved() { + public void AnExcludedMember_IsNotReportedAsUnserved() + { var result = GeneratorTestHarness.Run( - Source("[Intercept(typeof(SyncOnlyInterceptor), Members = InterceptedMembers.Methods)]", - interceptor: SyncOnly)); + Source( + "[Intercept(typeof(SyncOnlyInterceptor), Members = InterceptedMembers.Methods)]", + interceptor: SyncOnly + ) + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0015"); } - private static string[] Run(string attribute) { + private static string[] Run(string attribute) + { var generated = Build(attribute); var provider = generated.BuildProvider(); var service = provider.GetService(generated.Type("IHandler"))!; @@ -98,8 +112,7 @@ private static string[] Run(string attribute) { private static GeneratedAssembly Build(string attribute) => GeneratedAssembly.Create(Source(attribute)); - private const string Counting = - """ + private const string Counting = """ public sealed class CountingInterceptor : IInterceptor { public static readonly System.Collections.Generic.List Calls = new(); @@ -110,8 +123,7 @@ public TResult Intercept(InvocationContext context) { } """; - private const string SyncOnly = - """ + private const string SyncOnly = """ public sealed class SyncOnlyInterceptor : IInterceptor { public TResult Intercept(InvocationContext context) => context.Proceed(); } @@ -119,26 +131,26 @@ public sealed class SyncOnlyInterceptor : IInterceptor { private static string Source(string attribute, string interceptor = Counting) => $$""" - using DependencyModules.Runtime.Attributes; - using DependencyModules.Runtime.Interception; + using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Interception; - namespace TestNamespace; + namespace TestNamespace; - public interface IHandler { - string Name { get; } - string Handle(string input); - } + public interface IHandler { + string Name { get; } + string Handle(string input); + } - {{interceptor}} + {{interceptor}} - [SingletonService] - {{attribute}} - public class Handler : IHandler { - public string Name => "named"; - public string Handle(string input) => input; - } + [SingletonService] + {{attribute}} + public class Handler : IHandler { + public string Name => "named"; + public string Handle(string input) => input; + } - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/InterceptionRealmTests.cs b/tests/DependencyModules.Tests/GeneratorTests/InterceptionRealmTests.cs index a941fa6..9877bd3 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/InterceptionRealmTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/InterceptionRealmTests.cs @@ -18,10 +18,9 @@ namespace DependencyModules.Tests.GeneratorTests; /// with a realm-scoped service; agent 08 reached it by accident through a convention, because /// convention registrations are always stamped with their declaring module's realm. /// -public class InterceptionRealmTests { - - private const string Preamble = - """ +public class InterceptionRealmTests +{ + private const string Preamble = """ using DependencyModules.Runtime.Attributes; using DependencyModules.Runtime.Conventions; using DependencyModules.Runtime.Interception; @@ -41,19 +40,22 @@ public sealed class CountingInterceptor : IInterceptor { /// registration, because a per-implementation interception is about that one registration. /// [Fact] - public void ARealmScopedService_WithAnUnrealmedInterception_IsInterceptedInThatRealm() { + public void ARealmScopedService_WithAnUnrealmedInterception_IsInterceptedInThatRealm() + { var result = Run( - """ - [DependencyModule(OnlyRealm = true)] - public partial class RealmModule; + """ + [DependencyModule(OnlyRealm = true)] + public partial class RealmModule; - [SingletonService(Realm = typeof(RealmModule))] - [Intercept(typeof(CountingInterceptor))] - public sealed class Greeter : IGreeter { public string Greet() => "hi"; } + [SingletonService(Realm = typeof(RealmModule))] + [Intercept(typeof(CountingInterceptor))] + public sealed class Greeter : IGreeter { public string Greet() => "hi"; } - [DependencyModule] - public partial class TestModule; - """).AssertNoErrors(); + [DependencyModule] + public partial class TestModule; + """ + ) + .AssertNoErrors(); Assert.Contains("Greeter_Intercepted", result.SourceContaining("RealmModule.Interceptors")); } @@ -63,21 +65,27 @@ public partial class TestModule; /// wrapper to, so an applicator there is dead weight at best. /// [Fact] - public void ARealmScopedService_IsNotInterceptedOutsideItsRealm() { + public void ARealmScopedService_IsNotInterceptedOutsideItsRealm() + { var result = Run( - """ - [DependencyModule(OnlyRealm = true)] - public partial class RealmModule; - - [SingletonService(Realm = typeof(RealmModule))] - [Intercept(typeof(CountingInterceptor))] - public sealed class Greeter : IGreeter { public string Greet() => "hi"; } - - [DependencyModule] - public partial class TestModule; - """).AssertNoErrors(); - - Assert.DoesNotContain("Greeter_Intercepted", result.SourceContaining("TestModule.Interceptors")); + """ + [DependencyModule(OnlyRealm = true)] + public partial class RealmModule; + + [SingletonService(Realm = typeof(RealmModule))] + [Intercept(typeof(CountingInterceptor))] + public sealed class Greeter : IGreeter { public string Greet() => "hi"; } + + [DependencyModule] + public partial class TestModule; + """ + ) + .AssertNoErrors(); + + Assert.DoesNotContain( + "Greeter_Intercepted", + result.SourceContaining("TestModule.Interceptors") + ); } /// @@ -85,22 +93,28 @@ public partial class TestModule; /// changelog documents, and following the registration must not take it away. /// [Fact] - public void AnExplicitInterceptRealm_StillDecides() { + public void AnExplicitInterceptRealm_StillDecides() + { var result = Run( - """ - [DependencyModule(OnlyRealm = true)] - public partial class RealmModule; + """ + [DependencyModule(OnlyRealm = true)] + public partial class RealmModule; - [SingletonService] - [Intercept(typeof(CountingInterceptor), Realm = typeof(RealmModule))] - public sealed class Greeter : IGreeter { public string Greet() => "hi"; } + [SingletonService] + [Intercept(typeof(CountingInterceptor), Realm = typeof(RealmModule))] + public sealed class Greeter : IGreeter { public string Greet() => "hi"; } - [DependencyModule] - public partial class TestModule; - """).AssertNoErrors(); + [DependencyModule] + public partial class TestModule; + """ + ) + .AssertNoErrors(); Assert.Contains("Greeter_Intercepted", result.SourceContaining("RealmModule.Interceptors")); - Assert.DoesNotContain("Greeter_Intercepted", result.SourceContaining("TestModule.Interceptors")); + Assert.DoesNotContain( + "Greeter_Intercepted", + result.SourceContaining("TestModule.Interceptors") + ); } /// @@ -108,16 +122,19 @@ public partial class TestModule; /// module that is not realm-only. /// [Fact] - public void AnUnrealmedService_WithAnUnrealmedInterception_IsInterceptedNormally() { + public void AnUnrealmedService_WithAnUnrealmedInterception_IsInterceptedNormally() + { var result = Run( - """ - [SingletonService] - [Intercept(typeof(CountingInterceptor))] - public sealed class Greeter : IGreeter { public string Greet() => "hi"; } + """ + [SingletonService] + [Intercept(typeof(CountingInterceptor))] + public sealed class Greeter : IGreeter { public string Greet() => "hi"; } - [DependencyModule] - public partial class TestModule; - """).AssertNoErrors(); + [DependencyModule] + public partial class TestModule; + """ + ) + .AssertNoErrors(); Assert.Contains("Greeter_Intercepted", result.SourceContaining("TestModule.Interceptors")); } @@ -139,7 +156,8 @@ public partial class TestModule; /// provider. /// [Fact] - public void AConventionRegisteredClass_InAnOnlyRealmModule_ReportsDM0020() { + public void AConventionRegisteredClass_InAnOnlyRealmModule_ReportsDM0020() + { var result = Run( """ [Intercept(typeof(CountingInterceptor))] @@ -151,7 +169,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0020"); @@ -164,7 +183,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// can never run, not about every realm arrangement that looks unusual. /// [Fact] - public void AnInterceptionSomeModuleApplies_IsNotReported() { + public void AnInterceptionSomeModuleApplies_IsNotReported() + { var result = Run( """ [SingletonService] @@ -173,7 +193,8 @@ public sealed class Greeter : IGreeter { public string Greet() => "hi"; } [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0020"); } @@ -183,7 +204,8 @@ public partial class TestModule; /// module applies it and there is nothing to report. /// [Fact] - public void ARealmScopedServiceFollowingItsRegistration_IsNotReported() { + public void ARealmScopedServiceFollowingItsRegistration_IsNotReported() + { var result = Run( """ [DependencyModule(OnlyRealm = true)] @@ -195,7 +217,8 @@ public sealed class Greeter : IGreeter { public string Greet() => "hi"; } [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0020"); } @@ -205,21 +228,27 @@ public partial class TestModule; /// so the unrealmed interception landed on it by luck rather than by rule. /// [Fact] - public void AConventionRegisteredClass_InAPlainModule_IsStillIntercepted() { + public void AConventionRegisteredClass_InAPlainModule_IsStillIntercepted() + { var result = Run( - """ - [Intercept(typeof(CountingInterceptor))] - public sealed class Greeter : IGreeter { public string Greet() => "hi"; } - - [DependencyModule] - public partial class ConventionModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().AsSingleton(); + """ + [Intercept(typeof(CountingInterceptor))] + public sealed class Greeter : IGreeter { public string Greet() => "hi"; } + + [DependencyModule] + public partial class ConventionModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().AsSingleton(); + } } - } - """).AssertNoErrors(); - - Assert.Contains("Greeter_Intercepted", result.SourceContaining("ConventionModule.Interceptors")); + """ + ) + .AssertNoErrors(); + + Assert.Contains( + "Greeter_Intercepted", + result.SourceContaining("ConventionModule.Interceptors") + ); } /// @@ -229,23 +258,28 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// about which module the applicator landed on. /// [Fact] - public void AConventionRegisteringAsSelf_StillInterceptsTheInterface() { + public void AConventionRegisteringAsSelf_StillInterceptsTheInterface() + { var result = Run( - """ - [Intercept(typeof(CountingInterceptor))] - public sealed class Greeter : IGreeter { public string Greet() => "hi"; } - - [DependencyModule] - public partial class ConventionModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { - conventions.RegisterAll().AsSelf().AsSingleton(); + """ + [Intercept(typeof(CountingInterceptor))] + public sealed class Greeter : IGreeter { public string Greet() => "hi"; } + + [DependencyModule] + public partial class ConventionModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll().AsSelf().AsSingleton(); + } } - } - """).AssertNoErrors(); - - Assert.Contains("Greeter_Intercepted", result.SourceContaining("ConventionModule.Interceptors")); + """ + ) + .AssertNoErrors(); + + Assert.Contains( + "Greeter_Intercepted", + result.SourceContaining("ConventionModule.Interceptors") + ); } - private static GeneratorResult Run(string body) => - GeneratorTestHarness.Run(Preamble + body); + private static GeneratorResult Run(string body) => GeneratorTestHarness.Run(Preamble + body); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/InterceptionScopeTests.cs b/tests/DependencyModules.Tests/GeneratorTests/InterceptionScopeTests.cs index 4189126..221c14a 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/InterceptionScopeTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/InterceptionScopeTests.cs @@ -15,10 +15,9 @@ namespace DependencyModules.Tests.GeneratorTests; /// [Intercept] coming back wrapped in another class's wrapper, and, with two implementations /// marked, every interceptor running twice per call. Neither threw. /// -public class InterceptionScopeTests { - - private const string Interceptor = - """ +public class InterceptionScopeTests +{ + private const string Interceptor = """ public sealed class CountingInterceptor : IInterceptor { public static int Calls; public TResult Intercept(InvocationContext context) { @@ -29,7 +28,8 @@ public TResult Intercept(InvocationContext context) { """; [Fact] - public void AnUnmarkedSiblingImplementation_IsNotWrapped() { + public void AnUnmarkedSiblingImplementation_IsNotWrapped() + { var generated = GeneratedAssembly.Create( Source( """ @@ -38,13 +38,20 @@ public sealed class Loud : IGreeter { public string Greet() => "loud"; } [SingletonService] public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } - """)); + """ + ) + ); var provider = generated.BuildProvider(); - var resolved = ((System.Collections.IEnumerable)provider - .GetService(typeof(System.Collections.Generic.IEnumerable<>) - .MakeGenericType(generated.Type("IGreeter")))!) + var resolved = ( + (System.Collections.IEnumerable) + provider.GetService( + typeof(System.Collections.Generic.IEnumerable<>).MakeGenericType( + generated.Type("IGreeter") + ) + )! + ) .Cast() .Select(g => g.GetType().Name) .OrderBy(n => n) @@ -54,7 +61,8 @@ public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } } [Fact] - public void TwoMarkedImplementations_EachGetTheirOwnWrapper() { + public void TwoMarkedImplementations_EachGetTheirOwnWrapper() + { var generated = GeneratedAssembly.Create( Source( """ @@ -63,13 +71,20 @@ public sealed class Loud : IGreeter { public string Greet() => "loud"; } [SingletonService] [Intercept(typeof(CountingInterceptor))] public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } - """)); + """ + ) + ); var provider = generated.BuildProvider(); - var resolved = ((System.Collections.IEnumerable)provider - .GetService(typeof(System.Collections.Generic.IEnumerable<>) - .MakeGenericType(generated.Type("IGreeter")))!) + var resolved = ( + (System.Collections.IEnumerable) + provider.GetService( + typeof(System.Collections.Generic.IEnumerable<>).MakeGenericType( + generated.Type("IGreeter") + ) + )! + ) .Cast() .Select(g => g.GetType().Name) .OrderBy(n => n) @@ -86,7 +101,8 @@ public sealed class Quiet : IGreeter { public string Greet() => "quiet"; } /// service that had asked for it. /// [Fact] - public void AnInterceptorOrderedOutsideADecorator_StillApplies() { + public void AnInterceptorOrderedOutsideADecorator_StillApplies() + { var generated = GeneratedAssembly.Create( Source( """ @@ -97,7 +113,9 @@ public sealed class Core : IGreeter { public string Greet() => "core"; } public sealed class Bracketed(IGreeter inner) : IGreeter { public string Greet() => "[" + inner.Greet() + "]"; } - """)); + """ + ) + ); var resolved = generated.ResolveRequired("IGreeter"); @@ -111,18 +129,18 @@ public sealed class Bracketed(IGreeter inner) : IGreeter { private static string Source(string body) => $$""" - using DependencyModules.Runtime.Attributes; - using DependencyModules.Runtime.Interception; + using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Interception; - namespace TestNamespace; + namespace TestNamespace; - public interface IGreeter { string Greet(); } + public interface IGreeter { string Greet(); } - {{Interceptor}} + {{Interceptor}} - {{body}} + {{body}} - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/InterceptorGenerationTests.cs b/tests/DependencyModules.Tests/GeneratorTests/InterceptorGenerationTests.cs index 6431b36..0e4cd5d 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/InterceptorGenerationTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/InterceptorGenerationTests.cs @@ -8,11 +8,14 @@ namespace DependencyModules.Tests.GeneratorTests; /// Interception verified by compiling, loading and calling the generated wrapper, since the point is /// what an interceptor observes and can change rather than what the emitted text looks like. /// -public class InterceptorGenerationTests { - +public class InterceptorGenerationTests +{ [Fact] - public void InterceptedService_ResolvesAsTheGeneratedWrapper() { - var generated = GeneratedAssembly.Create(Source("int Sync(int a);", "public int Sync(int a) => a;")); + public void InterceptedService_ResolvesAsTheGeneratedWrapper() + { + var generated = GeneratedAssembly.Create( + Source("int Sync(int a);", "public int Sync(int a) => a;") + ); var resolved = generated.ResolveRequired("IWork"); @@ -20,8 +23,11 @@ public void InterceptedService_ResolvesAsTheGeneratedWrapper() { } [Fact] - public void SyncMethod_ReturnsTheInnerValueThroughThePipeline() { - var generated = GeneratedAssembly.Create(Source("int Sync(int a);", "public int Sync(int a) => a * 2;")); + public void SyncMethod_ReturnsTheInnerValueThroughThePipeline() + { + var generated = GeneratedAssembly.Create( + Source("int Sync(int a);", "public int Sync(int a) => a * 2;") + ); var work = generated.ResolveRequired("IWork"); @@ -30,7 +36,8 @@ public void SyncMethod_ReturnsTheInnerValueThroughThePipeline() { } [Fact] - public void VoidMethod_RunsThroughThePipeline() { + public void VoidMethod_RunsThroughThePipeline() + { var generated = GeneratedAssembly.Create(Source("void Run();", "public void Run() { }")); Invoke(generated.ResolveRequired("IWork"), "Run"); @@ -45,10 +52,14 @@ public void VoidMethod_RunsThroughThePipeline() { /// call has actually finished. /// [Fact] - public async Task AsyncMethod_ExitsAfterTheWorkCompletes() { - var generated = GeneratedAssembly.Create(Source( - "System.Threading.Tasks.Task Compute(int a);", - "public async System.Threading.Tasks.Task Compute(int a) { await Recorder.Gate.Task; return a * 2; }")); + public async Task AsyncMethod_ExitsAfterTheWorkCompletes() + { + var generated = GeneratedAssembly.Create( + Source( + "System.Threading.Tasks.Task Compute(int a);", + "public async System.Threading.Tasks.Task Compute(int a) { await Recorder.Gate.Task; return a * 2; }" + ) + ); var task = (Task)Invoke(generated.ResolveRequired("IWork"), "Compute", 21)!; @@ -64,10 +75,14 @@ public async Task AsyncMethod_ExitsAfterTheWorkCompletes() { } [Fact] - public async Task AsyncVoidMethod_RunsThroughThePipeline() { - var generated = GeneratedAssembly.Create(Source( - "System.Threading.Tasks.Task Run();", - "public async System.Threading.Tasks.Task Run() { await System.Threading.Tasks.Task.Delay(5); }")); + public async Task AsyncVoidMethod_RunsThroughThePipeline() + { + var generated = GeneratedAssembly.Create( + Source( + "System.Threading.Tasks.Task Run();", + "public async System.Threading.Tasks.Task Run() { await System.Threading.Tasks.Task.Delay(5); }" + ) + ); await (Task)Invoke(generated.ResolveRequired("IWork"), "Run")!; @@ -75,10 +90,14 @@ public async Task AsyncVoidMethod_RunsThroughThePipeline() { } [Fact] - public async Task ValueTaskMethod_ReturnsTheInnerValue() { - var generated = GeneratedAssembly.Create(Source( - "System.Threading.Tasks.ValueTask Fetch();", - "public async System.Threading.Tasks.ValueTask Fetch() { await System.Threading.Tasks.Task.Delay(5); return \"done\"; }")); + public async Task ValueTaskMethod_ReturnsTheInnerValue() + { + var generated = GeneratedAssembly.Create( + Source( + "System.Threading.Tasks.ValueTask Fetch();", + "public async System.Threading.Tasks.ValueTask Fetch() { await System.Threading.Tasks.Task.Delay(5); return \"done\"; }" + ) + ); var result = await (ValueTask)Invoke(generated.ResolveRequired("IWork"), "Fetch")!; @@ -87,10 +106,14 @@ public async Task ValueTaskMethod_ReturnsTheInnerValue() { } [Fact] - public async Task ValueTaskWithNoResult_RunsThroughThePipeline() { - var generated = GeneratedAssembly.Create(Source( - "System.Threading.Tasks.ValueTask Save();", - "public async System.Threading.Tasks.ValueTask Save() { await System.Threading.Tasks.Task.Delay(5); }")); + public async Task ValueTaskWithNoResult_RunsThroughThePipeline() + { + var generated = GeneratedAssembly.Create( + Source( + "System.Threading.Tasks.ValueTask Save();", + "public async System.Threading.Tasks.ValueTask Save() { await System.Threading.Tasks.Task.Delay(5); }" + ) + ); await (ValueTask)Invoke(generated.ResolveRequired("IWork"), "Save")!; @@ -103,18 +126,26 @@ public async Task ValueTaskWithNoResult_RunsThroughThePipeline() { /// item as it is produced. /// [Fact] - public async Task AsyncEnumerableMethod_ObservesEachItem() { - var generated = GeneratedAssembly.Create(Source( - "System.Collections.Generic.IAsyncEnumerable Stream(int count);", - """ - public async System.Collections.Generic.IAsyncEnumerable Stream(int count) { - for (var i = 0; i < count; i++) { await System.Threading.Tasks.Task.Yield(); yield return i; } - } - """)); + public async Task AsyncEnumerableMethod_ObservesEachItem() + { + var generated = GeneratedAssembly.Create( + Source( + "System.Collections.Generic.IAsyncEnumerable Stream(int count);", + """ + public async System.Collections.Generic.IAsyncEnumerable Stream(int count) { + for (var i = 0; i < count; i++) { await System.Threading.Tasks.Task.Yield(); yield return i; } + } + """ + ) + ); var items = new List(); - await foreach (var item in (IAsyncEnumerable)Invoke(generated.ResolveRequired("IWork"), "Stream", 3)!) { + await foreach ( + var item in (IAsyncEnumerable) + Invoke(generated.ResolveRequired("IWork"), "Stream", 3)! + ) + { items.Add(item); } @@ -123,10 +154,14 @@ public async System.Collections.Generic.IAsyncEnumerable Stream(int count) } [Fact] - public void GenericMethod_ForwardsWithItsConstraints() { - var generated = GeneratedAssembly.Create(Source( - "T Pick(T item) where T : class;", - "public T Pick(T item) where T : class => item;")); + public void GenericMethod_ForwardsWithItsConstraints() + { + var generated = GeneratedAssembly.Create( + Source( + "T Pick(T item) where T : class;", + "public T Pick(T item) where T : class => item;" + ) + ); var work = generated.ResolveRequired("IWork"); var method = work.GetType().GetMethod("Pick")!.MakeGenericMethod(typeof(string)); @@ -140,10 +175,11 @@ public void GenericMethod_ForwardsWithItsConstraints() { /// and the name matches what appears in a stack trace. /// [Fact] - public void Property_RoutesBothAccessorsAndReportsThemByTheirClrNames() { - var generated = GeneratedAssembly.Create(Source( - "string Name { get; set; }", - "public string Name { get; set; } = \"initial\";")); + public void Property_RoutesBothAccessorsAndReportsThemByTheirClrNames() + { + var generated = GeneratedAssembly.Create( + Source("string Name { get; set; }", "public string Name { get; set; } = \"initial\";") + ); var work = generated.ResolveRequired("IWork"); var property = work.GetType().GetProperty("Name")!; @@ -151,12 +187,18 @@ public void Property_RoutesBothAccessorsAndReportsThemByTheirClrNames() { property.SetValue(work, "assigned"); Assert.Equal("assigned", property.GetValue(work)); - Assert.Equal(["enter set_Name", "exit set_Name", "enter get_Name", "exit get_Name"], Log(generated)); + Assert.Equal( + ["enter set_Name", "exit set_Name", "enter get_Name", "exit get_Name"], + Log(generated) + ); } [Fact] - public void ReadOnlyProperty_DeclaresNoSetter() { - var generated = GeneratedAssembly.Create(Source("int Count { get; }", "public int Count => 7;")); + public void ReadOnlyProperty_DeclaresNoSetter() + { + var generated = GeneratedAssembly.Create( + Source("int Count { get; }", "public int Count => 7;") + ); var work = generated.ResolveRequired("IWork"); var property = work.GetType().GetProperty("Count")!; @@ -171,10 +213,14 @@ public void ReadOnlyProperty_DeclaresNoSetter() { /// and hands the task itself to the interceptor. /// [Fact] - public async Task PropertyReturningATask_TakesTheSyncPath() { - var generated = GeneratedAssembly.Create(Source( - "System.Threading.Tasks.Task Pending { get; }", - "public System.Threading.Tasks.Task Pending => System.Threading.Tasks.Task.FromResult(3);")); + public async Task PropertyReturningATask_TakesTheSyncPath() + { + var generated = GeneratedAssembly.Create( + Source( + "System.Threading.Tasks.Task Pending { get; }", + "public System.Threading.Tasks.Task Pending => System.Threading.Tasks.Task.FromResult(3);" + ) + ); var work = generated.ResolveRequired("IWork"); var pending = (Task)work.GetType().GetProperty("Pending")!.GetValue(work)!; @@ -184,16 +230,20 @@ public async Task PropertyReturningATask_TakesTheSyncPath() { } [Fact] - public void Indexer_ForwardsItsIndicesAndAssignedValue() { - var generated = GeneratedAssembly.Create(Source( - "int this[int row, int column] { get; set; }", - """ - private readonly System.Collections.Generic.Dictionary _cells = new(); - public int this[int row, int column] { - get => _cells.TryGetValue($"{row},{column}", out var value) ? value : -1; - set => _cells[$"{row},{column}"] = value; - } - """)); + public void Indexer_ForwardsItsIndicesAndAssignedValue() + { + var generated = GeneratedAssembly.Create( + Source( + "int this[int row, int column] { get; set; }", + """ + private readonly System.Collections.Generic.Dictionary _cells = new(); + public int this[int row, int column] { + get => _cells.TryGetValue($"{row},{column}", out var value) ? value : -1; + set => _cells[$"{row},{column}"] = value; + } + """ + ) + ); var work = generated.ResolveRequired("IWork"); var indexer = work.GetType().GetProperty("Item")!; @@ -203,28 +253,40 @@ public void Indexer_ForwardsItsIndicesAndAssignedValue() { Assert.Equal(42, indexer.GetValue(work, [2, 3])); Assert.Equal(-1, indexer.GetValue(work, [9, 9])); Assert.Equal( - ["enter set_Item", "exit set_Item", "enter get_Item", "exit get_Item", "enter get_Item", "exit get_Item"], - Log(generated)); + [ + "enter set_Item", + "exit set_Item", + "enter get_Item", + "exit get_Item", + "enter get_Item", + "exit get_Item", + ], + Log(generated) + ); } [Fact] - public void IndexerSetter_ExposesItsIndicesAndValueAsArguments() { - var generated = GeneratedAssembly.Create(Source( - "int this[int row] { get; set; }", - "public int this[int row] { get => row; set { } }", - """ - public class TracingInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) { - var arguments = context.Arguments; - - for (var i = 0; i < arguments.Count; i++) { - Recorder.Entries.Add($"{arguments.NameAt(i)}={arguments[i]}"); + public void IndexerSetter_ExposesItsIndicesAndValueAsArguments() + { + var generated = GeneratedAssembly.Create( + Source( + "int this[int row] { get; set; }", + "public int this[int row] { get => row; set { } }", + """ + public class TracingInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) { + var arguments = context.Arguments; + + for (var i = 0; i < arguments.Count; i++) { + Recorder.Entries.Add($"{arguments.NameAt(i)}={arguments[i]}"); + } + + return context.Proceed(); } - - return context.Proceed(); } - } - """)); + """ + ) + ); var work = generated.ResolveRequired("IWork"); @@ -234,10 +296,14 @@ public TResult Intercept(InvocationContext context) { } [Fact] - public void Event_RoutesAddAndRemove() { - var generated = GeneratedAssembly.Create(Source( - "event System.EventHandler Changed;", - "public event System.EventHandler Changed { add { } remove { } }")); + public void Event_RoutesAddAndRemove() + { + var generated = GeneratedAssembly.Create( + Source( + "event System.EventHandler Changed;", + "public event System.EventHandler Changed { add { } remove { } }" + ) + ); var work = generated.ResolveRequired("IWork"); var changed = work.GetType().GetEvent("Changed")!; @@ -247,64 +313,82 @@ public void Event_RoutesAddAndRemove() { changed.RemoveEventHandler(work, handler); Assert.Equal( - ["enter add_Changed", "exit add_Changed", "enter remove_Changed", "exit remove_Changed"], - Log(generated)); + [ + "enter add_Changed", + "exit add_Changed", + "enter remove_Changed", + "exit remove_Changed", + ], + Log(generated) + ); } [Fact] - public void ThrowingMethod_PropagatesThroughThePipeline() { - var generated = GeneratedAssembly.Create(Source( - "void Run();", - "public void Run() => throw new System.InvalidOperationException(\"boom\");")); + public void ThrowingMethod_PropagatesThroughThePipeline() + { + var generated = GeneratedAssembly.Create( + Source( + "void Run();", + "public void Run() => throw new System.InvalidOperationException(\"boom\");" + ) + ); - var exception = Assert.Throws( - () => Invoke(generated.ResolveRequired("IWork"), "Run")); + var exception = Assert.Throws(() => + Invoke(generated.ResolveRequired("IWork"), "Run") + ); Assert.IsType(exception.InnerException); Assert.Equal(["enter Run", "exit Run"], Log(generated)); } [Fact] - public void SeveralInterceptors_NestInDeclarationOrder() { + public void SeveralInterceptors_NestInDeclarationOrder() + { var generated = GeneratedAssembly.Create( $$""" - {{Preamble}} + {{Preamble}} - {{Tracing("First", "first")}} + {{Tracing("First", "first")}} - {{Tracing("Second", "second")}} + {{Tracing("Second", "second")}} - public interface IWork { void Run(); } + public interface IWork { void Run(); } - [SingletonService] - [Intercept(typeof(FirstInterceptor), typeof(SecondInterceptor))] - public class Work : IWork { public void Run() { } } + [SingletonService] + [Intercept(typeof(FirstInterceptor), typeof(SecondInterceptor))] + public class Work : IWork { public void Run() { } } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); Invoke(generated.ResolveRequired("IWork"), "Run"); Assert.Equal( ["enter first Run", "enter second Run", "exit second Run", "exit first Run"], - Log(generated)); + Log(generated) + ); } [Fact] - public void Caller_CarriesTheServiceTypeAndMember() { - var generated = GeneratedAssembly.Create(Source( - "void Run();", - "public void Run() { }", - """ - public class TracingInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) { - Recorder.Entries.Add(context.Caller.ToString()); - Recorder.Entries.Add(context.Caller.ServiceType.Name); - return context.Proceed(); + public void Caller_CarriesTheServiceTypeAndMember() + { + var generated = GeneratedAssembly.Create( + Source( + "void Run();", + "public void Run() { }", + """ + public class TracingInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) { + Recorder.Entries.Add(context.Caller.ToString()); + Recorder.Entries.Add(context.Caller.ServiceType.Name); + return context.Proceed(); + } } - } - """)); + """ + ) + ); Invoke(generated.ResolveRequired("IWork"), "Run"); @@ -316,24 +400,28 @@ public TResult Intercept(InvocationContext context) { /// so writing one replaces the value the implementation receives. /// [Fact] - public void Arguments_AreReadableByNameAndReplaceable() { - var generated = GeneratedAssembly.Create(Source( - "int Sync(int a, string b);", - "public int Sync(int a, string b) { Recorder.Entries.Add($\"inner {a} {b}\"); return a; }", - """ - public class TracingInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) { - var arguments = context.Arguments; - - for (var i = 0; i < arguments.Count; i++) { - Recorder.Entries.Add($"{arguments.NameAt(i)}={arguments[i]}"); + public void Arguments_AreReadableByNameAndReplaceable() + { + var generated = GeneratedAssembly.Create( + Source( + "int Sync(int a, string b);", + "public int Sync(int a, string b) { Recorder.Entries.Add($\"inner {a} {b}\"); return a; }", + """ + public class TracingInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) { + var arguments = context.Arguments; + + for (var i = 0; i < arguments.Count; i++) { + Recorder.Entries.Add($"{arguments.NameAt(i)}={arguments[i]}"); + } + + arguments[0] = 99; + return context.Proceed(); } - - arguments[0] = 99; - return context.Proceed(); } - } - """)); + """ + ) + ); var result = Invoke(generated.ResolveRequired("IWork"), "Sync", 5, "text"); @@ -346,18 +434,22 @@ public TResult Intercept(InvocationContext context) { /// second time re-enters the same next stage rather than walking past it. /// [Fact] - public void ProceedingTwice_CallsTheImplementationTwice() { - var generated = GeneratedAssembly.Create(Source( - "int Sync(int a);", - "public int Sync(int a) { Recorder.Entries.Add(\"inner\"); return a; }", - """ - public class TracingInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) { - context.Proceed(); - return context.Proceed(); + public void ProceedingTwice_CallsTheImplementationTwice() + { + var generated = GeneratedAssembly.Create( + Source( + "int Sync(int a);", + "public int Sync(int a) { Recorder.Entries.Add(\"inner\"); return a; }", + """ + public class TracingInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) { + context.Proceed(); + return context.Proceed(); + } } - } - """)); + """ + ) + ); var result = Invoke(generated.ResolveRequired("IWork"), "Sync", 5); @@ -366,15 +458,19 @@ public TResult Intercept(InvocationContext context) { } [Fact] - public void NotProceeding_SkipsTheImplementation() { - var generated = GeneratedAssembly.Create(Source( - "int Sync(int a);", - "public int Sync(int a) { Recorder.Entries.Add(\"inner\"); return a; }", - """ - public class TracingInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) => default!; - } - """)); + public void NotProceeding_SkipsTheImplementation() + { + var generated = GeneratedAssembly.Create( + Source( + "int Sync(int a);", + "public int Sync(int a) { Recorder.Entries.Add(\"inner\"); return a; }", + """ + public class TracingInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) => default!; + } + """ + ) + ); Assert.Equal(0, Invoke(generated.ResolveRequired("IWork"), "Sync", 5)); Assert.Empty(Log(generated)); @@ -386,29 +482,31 @@ public class TracingInterceptor : IInterceptor { /// wrapper holds its interceptors as typed fields now, so a wrapper can only reach its own. /// [Fact] - public void TwoServices_DoNotCrossApplyEachOthersInterceptors() { + public void TwoServices_DoNotCrossApplyEachOthersInterceptors() + { var generated = GeneratedAssembly.Create( $$""" - {{Preamble}} + {{Preamble}} - {{Tracing("Alpha", "alpha")}} + {{Tracing("Alpha", "alpha")}} - {{Tracing("Beta", "beta")}} + {{Tracing("Beta", "beta")}} - public interface IAlpha { void Run(); } - public interface IBeta { void Run(); } + public interface IAlpha { void Run(); } + public interface IBeta { void Run(); } - [SingletonService] - [Intercept(typeof(AlphaInterceptor))] - public class Alpha : IAlpha { public void Run() { } } + [SingletonService] + [Intercept(typeof(AlphaInterceptor))] + public class Alpha : IAlpha { public void Run() { } } - [SingletonService] - [Intercept(typeof(BetaInterceptor))] - public class Beta : IBeta { public void Run() { } } + [SingletonService] + [Intercept(typeof(BetaInterceptor))] + public class Beta : IBeta { public void Run() { } } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); var provider = generated.BuildProvider(); @@ -423,38 +521,40 @@ public partial class TestModule; /// rest, which are forwarded untouched rather than costing the sync members their interception. /// [Fact] - public async Task SyncOnlyInterceptor_ServesSyncMembersAndPassesAsyncOnesThrough() { + public async Task SyncOnlyInterceptor_ServesSyncMembersAndPassesAsyncOnesThrough() + { var generated = GeneratedAssembly.Create( $$""" - {{Preamble}} - - public class SyncOnlyInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) { - Recorder.Entries.Add($"enter {context.Caller.MemberName}"); - - try { - return context.Proceed(); - } finally { - Recorder.Entries.Add($"exit {context.Caller.MemberName}"); - } - } - } - - public interface IWork { - int Sync(int a); - Task Async(int a); - } - - [SingletonService] - [Intercept(typeof(SyncOnlyInterceptor))] - public class Work : IWork { - public int Sync(int a) => a * 2; - public Task Async(int a) => Task.FromResult(a * 3); - } - - [DependencyModule] - public partial class TestModule; - """); + {{Preamble}} + + public class SyncOnlyInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) { + Recorder.Entries.Add($"enter {context.Caller.MemberName}"); + + try { + return context.Proceed(); + } finally { + Recorder.Entries.Add($"exit {context.Caller.MemberName}"); + } + } + } + + public interface IWork { + int Sync(int a); + Task Async(int a); + } + + [SingletonService] + [Intercept(typeof(SyncOnlyInterceptor))] + public class Work : IWork { + public int Sync(int a) => a * 2; + public Task Async(int a) => Task.FromResult(a * 3); + } + + [DependencyModule] + public partial class TestModule; + """ + ); var work = generated.ResolveRequired("IWork"); @@ -469,38 +569,40 @@ public partial class TestModule; /// The mirror of the above, so neither direction is the special case. /// [Fact] - public async Task AsyncOnlyInterceptor_ServesAsyncMembersAndPassesSyncOnesThrough() { + public async Task AsyncOnlyInterceptor_ServesAsyncMembersAndPassesSyncOnesThrough() + { var generated = GeneratedAssembly.Create( $$""" - {{Preamble}} - - public class AsyncOnlyInterceptor : IAsyncInterceptor { - public async ValueTask InterceptAsync(AsyncInvocationContext context) { - Recorder.Entries.Add($"enter {context.Caller.MemberName}"); - - try { - return await context.ProceedAsync(); - } finally { - Recorder.Entries.Add($"exit {context.Caller.MemberName}"); - } - } - } - - public interface IWork { - int Sync(int a); - Task Async(int a); - } - - [SingletonService] - [Intercept(typeof(AsyncOnlyInterceptor))] - public class Work : IWork { - public int Sync(int a) => a * 2; - public Task Async(int a) => Task.FromResult(a * 3); - } - - [DependencyModule] - public partial class TestModule; - """); + {{Preamble}} + + public class AsyncOnlyInterceptor : IAsyncInterceptor { + public async ValueTask InterceptAsync(AsyncInvocationContext context) { + Recorder.Entries.Add($"enter {context.Caller.MemberName}"); + + try { + return await context.ProceedAsync(); + } finally { + Recorder.Entries.Add($"exit {context.Caller.MemberName}"); + } + } + } + + public interface IWork { + int Sync(int a); + Task Async(int a); + } + + [SingletonService] + [Intercept(typeof(AsyncOnlyInterceptor))] + public class Work : IWork { + public int Sync(int a) => a * 2; + public Task Async(int a) => Task.FromResult(a * 3); + } + + [DependencyModule] + public partial class TestModule; + """ + ); var work = generated.ResolveRequired("IWork"); @@ -516,40 +618,43 @@ public partial class TestModule; /// walks to is its position in that pipeline rather than in the attribute. /// [Fact] - public void MixedInterceptors_NestOnlyThoseThatServeTheMember() { + public void MixedInterceptors_NestOnlyThoseThatServeTheMember() + { var generated = GeneratedAssembly.Create( $$""" - {{Preamble}} + {{Preamble}} - public class SyncOnlyInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) { - Recorder.Entries.Add("enter sync-only"); + public class SyncOnlyInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) { + Recorder.Entries.Add("enter sync-only"); - try { - return context.Proceed(); - } finally { - Recorder.Entries.Add("exit sync-only"); - } - } - } + try { + return context.Proceed(); + } finally { + Recorder.Entries.Add("exit sync-only"); + } + } + } - {{Tracing("Both", "both")}} + {{Tracing("Both", "both")}} - public interface IWork { int Sync(int a); } + public interface IWork { int Sync(int a); } - [SingletonService] - [Intercept(typeof(SyncOnlyInterceptor), typeof(BothInterceptor))] - public class Work : IWork { public int Sync(int a) => a; } + [SingletonService] + [Intercept(typeof(SyncOnlyInterceptor), typeof(BothInterceptor))] + public class Work : IWork { public int Sync(int a) => a; } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); Invoke(generated.ResolveRequired("IWork"), "Sync", 1); Assert.Equal( ["enter sync-only", "enter both Sync", "exit both Sync", "exit sync-only"], - Log(generated)); + Log(generated) + ); } /// @@ -557,24 +662,26 @@ public partial class TestModule; /// service resolves as the implementation registered it. /// [Fact] - public void InterceptorThatServesNothing_GeneratesNoWrapper() { + public void InterceptorThatServesNothing_GeneratesNoWrapper() + { var generated = GeneratedAssembly.Create( $$""" - {{Preamble}} + {{Preamble}} - public class SyncOnlyInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) => context.Proceed(); - } + public class SyncOnlyInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) => context.Proceed(); + } - public interface IWork { Task Async(int a); } + public interface IWork { Task Async(int a); } - [SingletonService] - [Intercept(typeof(SyncOnlyInterceptor))] - public class Work : IWork { public Task Async(int a) => Task.FromResult(a); } + [SingletonService] + [Intercept(typeof(SyncOnlyInterceptor))] + public class Work : IWork { public Task Async(int a) => Task.FromResult(a); } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); var resolved = generated.ResolveRequired("IWork"); @@ -588,8 +695,11 @@ public partial class TestModule; /// and an open generic implementation type is what the container does accept. /// [Fact] - public void GenericImplementation_IsIntercepted() { - var result = GeneratorTestHarness.Run(GenericRepo("public class Repo : IRepo { public void Run() { } }")); + public void GenericImplementation_IsIntercepted() + { + var result = GeneratorTestHarness.Run( + GenericRepo("public class Repo : IRepo { public void Run() { } }") + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0008"); Assert.Contains(result.GeneratedSources.Keys, key => key.Contains("Repo_Intercepted")); @@ -600,10 +710,15 @@ public void GenericImplementation_IsIntercepted() { /// generic implementation type and close it per construction. /// [Fact] - public void GenericImplementation_EmitsAGenericWrapper() { - var result = GeneratorTestHarness.Run(GenericRepo("public class Repo : IRepo { public void Run() { } }")); + public void GenericImplementation_EmitsAGenericWrapper() + { + var result = GeneratorTestHarness.Run( + GenericRepo("public class Repo : IRepo { public void Run() { } }") + ); - var wrapper = Assert.Single(result.GeneratedSources, pair => pair.Key.Contains("Repo_Intercepted")).Value; + var wrapper = Assert + .Single(result.GeneratedSources, pair => pair.Key.Contains("Repo_Intercepted")) + .Value; Assert.Contains("class Repo_Intercepted", wrapper); @@ -618,10 +733,15 @@ public void GenericImplementation_EmitsAGenericWrapper() { /// `T` is in scope at the registration. /// [Fact] - public void GenericImplementation_RegistersAsAnOpenGenericImplementation() { - var result = GeneratorTestHarness.Run(GenericRepo("public class Repo : IRepo { public void Run() { } }")); + public void GenericImplementation_RegistersAsAnOpenGenericImplementation() + { + var result = GeneratorTestHarness.Run( + GenericRepo("public class Repo : IRepo { public void Run() { } }") + ); - var registration = Assert.Single(result.GeneratedSources, pair => pair.Key.Contains("Interceptors")).Value; + var registration = Assert + .Single(result.GeneratedSources, pair => pair.Key.Contains("Interceptors")) + .Value; Assert.Contains("InterceptOpenGeneric", registration); Assert.Contains("typeof(global::TestNamespace.IRepo<>)", registration); @@ -633,15 +753,20 @@ public void GenericImplementation_RegistersAsAnOpenGenericImplementation() { /// what it wraps. /// [Fact] - public void ConstrainedGenericImplementation_RepeatsTheConstraints() { + public void ConstrainedGenericImplementation_RepeatsTheConstraints() + { var result = GeneratorTestHarness.Run( GenericRepo( "public class Repo : IRepo where T : class, IMarker, new() { public void Run() { } }", - supporting: "public interface IMarker;")); + supporting: "public interface IMarker;" + ) + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0008"); - var wrapper = Assert.Single(result.GeneratedSources, pair => pair.Key.Contains("Repo_Intercepted")).Value; + var wrapper = Assert + .Single(result.GeneratedSources, pair => pair.Key.Contains("Repo_Intercepted")) + .Value; Assert.Contains("where T : class, global::TestNamespace.IMarker, new()", wrapper); } @@ -652,11 +777,17 @@ public void ConstrainedGenericImplementation_RepeatsTheConstraints() { /// reader has to drop it rather than pass it through. /// [Fact] - public void StructConstrainedGeneric_DoesNotRepeatTheDefaultConstructor() { + public void StructConstrainedGeneric_DoesNotRepeatTheDefaultConstructor() + { var result = GeneratorTestHarness.Run( - GenericRepo("public class Repo : IRepo where T : struct { public void Run() { } }")); + GenericRepo( + "public class Repo : IRepo where T : struct { public void Run() { } }" + ) + ); - var wrapper = Assert.Single(result.GeneratedSources, pair => pair.Key.Contains("Repo_Intercepted")).Value; + var wrapper = Assert + .Single(result.GeneratedSources, pair => pair.Key.Contains("Repo_Intercepted")) + .Value; Assert.Contains("where T : struct", wrapper); Assert.DoesNotContain("new()", wrapper); @@ -666,15 +797,18 @@ public void StructConstrainedGeneric_DoesNotRepeatTheDefaultConstructor() { /// And the constrained wrapper is not merely well-formed text: it compiles, loads and runs. /// [Fact] - public void ConstrainedGenericImplementation_ResolvesAndIntercepts() { + public void ConstrainedGenericImplementation_ResolvesAndIntercepts() + { var generated = GeneratedAssembly.Create( GenericRepo( "public class Repo : IRepo where T : class, IMarker, new() { public void Run() { } }", supporting: """ - public interface IMarker; + public interface IMarker; - public class Marked : IMarker; - """)); + public class Marked : IMarker; + """ + ) + ); var closed = generated.Type("IRepo`1").MakeGenericType(generated.Type("Marked")); var resolved = generated.BuildProvider().GetService(closed); @@ -689,21 +823,21 @@ public class Marked : IMarker; /// private static string GenericRepo(string implementation, string supporting = "") => $$""" - {{Preamble}} + {{Preamble}} - {{Tracing("Tracing", "tracing")}} + {{Tracing("Tracing", "tracing")}} - public interface IRepo { void Run(); } + public interface IRepo { void Run(); } - {{supporting}} + {{supporting}} - [SingletonService] - [Intercept(typeof(TracingInterceptor))] - {{implementation}} + [SingletonService] + [Intercept(typeof(TracingInterceptor))] + {{implementation}} - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; /// /// A closed construction of a generic service, which is the answer to the refusal above. The @@ -711,27 +845,31 @@ public partial class TestModule; /// it that way too — interception used to disagree and report that the class implemented none. /// [Fact] - public void ClosedConstructionOfAGenericService_IsIntercepted() { + public void ClosedConstructionOfAGenericService_IsIntercepted() + { var generated = GeneratedAssembly.Create( $$""" - {{Preamble}} + {{Preamble}} - {{Tracing("Tracing", "tracing")}} + {{Tracing("Tracing", "tracing")}} - public interface IRepo { string Name(); } + public interface IRepo { string Name(); } - public class Repo : IRepo { public string Name() => "repo"; } + public class Repo : IRepo { public string Name() => "repo"; } - [SingletonService] - [Intercept(typeof(TracingInterceptor))] - public class StringRepo : Repo { } + [SingletonService] + [Intercept(typeof(TracingInterceptor))] + public class StringRepo : Repo { } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); var provider = generated.BuildProvider(); - var resolved = provider.GetService(generated.Type("IRepo`1").MakeGenericType(typeof(string)))!; + var resolved = provider.GetService( + generated.Type("IRepo`1").MakeGenericType(typeof(string)) + )!; Assert.EndsWith("_Intercepted", resolved.GetType().Name); Assert.Equal("repo", resolved.GetType().GetMethod("Name")!.Invoke(resolved, null)); @@ -739,22 +877,24 @@ public partial class TestModule; } [Fact] - public void RefParameter_ReportsDM0008() { + public void RefParameter_ReportsDM0008() + { var result = GeneratorTestHarness.Run( $$""" - {{Preamble}} + {{Preamble}} - {{Tracing("Tracing", "tracing")}} + {{Tracing("Tracing", "tracing")}} - public interface IWork { void Run(ref int a); } + public interface IWork { void Run(ref int a); } - [SingletonService] - [Intercept(typeof(TracingInterceptor))] - public class Work : IWork { public void Run(ref int a) { } } + [SingletonService] + [Intercept(typeof(TracingInterceptor))] + public class Work : IWork { public void Run(ref int a) { } } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0008"); @@ -762,20 +902,22 @@ public partial class TestModule; } [Fact] - public void ServiceWithNoInterface_ReportsDM0008() { + public void ServiceWithNoInterface_ReportsDM0008() + { var result = GeneratorTestHarness.Run( $$""" - {{Preamble}} + {{Preamble}} - {{Tracing("Tracing", "tracing")}} + {{Tracing("Tracing", "tracing")}} - [SingletonService] - [Intercept(typeof(TracingInterceptor))] - public class Standalone { public void Run() { } } + [SingletonService] + [Intercept(typeof(TracingInterceptor))] + public class Standalone { public void Run() { } } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0008"); @@ -784,23 +926,25 @@ public partial class TestModule; } [Fact] - public void ServiceWithSeveralInterfaces_ReportsDM0008() { + public void ServiceWithSeveralInterfaces_ReportsDM0008() + { var result = GeneratorTestHarness.Run( $$""" - {{Preamble}} + {{Preamble}} - {{Tracing("Tracing", "tracing")}} + {{Tracing("Tracing", "tracing")}} - public interface IOne { void Run(); } - public interface ITwo { void Walk(); } + public interface IOne { void Run(); } + public interface ITwo { void Walk(); } - [SingletonService] - [Intercept(typeof(TracingInterceptor))] - public class Both : IOne, ITwo { public void Run() { } public void Walk() { } } + [SingletonService] + [Intercept(typeof(TracingInterceptor))] + public class Both : IOne, ITwo { public void Run() { } public void Walk() { } } - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0008"); @@ -820,8 +964,7 @@ private static TaskCompletionSource Gate(GeneratedAssembly generated) => private static object? Invoke(object target, string method, params object?[] arguments) => target.GetType().GetMethod(method)!.Invoke(target, arguments); - private const string Preamble = - """ + private const string Preamble = """ using DependencyModules.Runtime.Attributes; using DependencyModules.Runtime.Interception; using System.Collections.Generic; @@ -846,60 +989,65 @@ public static class Recorder { /// private static string Tracing(string prefix, string label) => $$""" - public class {{prefix}}Interceptor : IInterceptor, IAsyncInterceptor, IAsyncEnumerableInterceptor { - public TResult Intercept(InvocationContext context) { - Recorder.Entries.Add($"enter {{label}} {context.Caller.MemberName}"); + public class {{prefix}}Interceptor : IInterceptor, IAsyncInterceptor, IAsyncEnumerableInterceptor { + public TResult Intercept(InvocationContext context) { + Recorder.Entries.Add($"enter {{label}} {context.Caller.MemberName}"); - try { - return context.Proceed(); - } finally { - Recorder.Entries.Add($"exit {{label}} {context.Caller.MemberName}"); - } - } + try { + return context.Proceed(); + } finally { + Recorder.Entries.Add($"exit {{label}} {context.Caller.MemberName}"); + } + } - public async ValueTask InterceptAsync(AsyncInvocationContext context) { - Recorder.Entries.Add($"enter {{label}} {context.Caller.MemberName}"); + public async ValueTask InterceptAsync(AsyncInvocationContext context) { + Recorder.Entries.Add($"enter {{label}} {context.Caller.MemberName}"); - try { - return await context.ProceedAsync(); - } finally { - Recorder.Entries.Add($"exit {{label}} {context.Caller.MemberName}"); - } - } + try { + return await context.ProceedAsync(); + } finally { + Recorder.Entries.Add($"exit {{label}} {context.Caller.MemberName}"); + } + } - public async IAsyncEnumerable InterceptStream(StreamInvocationContext context) { - Recorder.Entries.Add($"enter {{label}} {context.Caller.MemberName}"); + public async IAsyncEnumerable InterceptStream(StreamInvocationContext context) { + Recorder.Entries.Add($"enter {{label}} {context.Caller.MemberName}"); - await foreach (var item in context.Proceed()) { - Recorder.Entries.Add($"item {item}"); + await foreach (var item in context.Proceed()) { + Recorder.Entries.Add($"item {item}"); - yield return item; - } + yield return item; + } - Recorder.Entries.Add($"exit {{label}} {context.Caller.MemberName}"); - } - } - """; + Recorder.Entries.Add($"exit {{label}} {context.Caller.MemberName}"); + } + } + """; /// /// The default interceptor logs without a label, so the single-interceptor tests read as /// "enter Sync" rather than repeating which interceptor produced the entry. /// - private static readonly string DefaultInterceptor = Tracing("Tracing", "").Replace(" {context.Caller", "{context.Caller"); - - private static string Source(string interfaceMember, string implementation, string? interceptor = null) => + private static readonly string DefaultInterceptor = Tracing("Tracing", "") + .Replace(" {context.Caller", "{context.Caller"); + + private static string Source( + string interfaceMember, + string implementation, + string? interceptor = null + ) => $$""" - {{Preamble}} + {{Preamble}} - {{interceptor ?? DefaultInterceptor}} + {{interceptor ?? DefaultInterceptor}} - public interface IWork { {{interfaceMember}} } + public interface IWork { {{interfaceMember}} } - [SingletonService] - [Intercept(typeof(TracingInterceptor))] - public class Work : IWork { {{implementation}} } + [SingletonService] + [Intercept(typeof(TracingInterceptor))] + public class Work : IWork { {{implementation}} } - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/InterceptorLifetimeTests.cs b/tests/DependencyModules.Tests/GeneratorTests/InterceptorLifetimeTests.cs index 4fc3eee..f634233 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/InterceptorLifetimeTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/InterceptorLifetimeTests.cs @@ -17,10 +17,11 @@ namespace DependencyModules.Tests.GeneratorTests; /// decorators, but only somebody reading the generated code would ever find it. Naming the lifetime /// where the interception is declared says it out loud. /// -public class InterceptorLifetimeTests { - +public class InterceptorLifetimeTests +{ [Fact] - public void WithNoLifetimeNamed_TheInterceptorIsASingleton() { + public void WithNoLifetimeNamed_TheInterceptorIsASingleton() + { var interceptors = Run("[Intercept(typeof(CountingInterceptor))]"); Assert.Contains("TryAddSingleton", interceptors); @@ -30,9 +31,11 @@ public void WithNoLifetimeNamed_TheInterceptorIsASingleton() { [InlineData("Scoped", "TryAddScoped")] [InlineData("Transient", "TryAddTransient")] [InlineData("Singleton", "TryAddSingleton")] - public void ANamedLifetime_IsTheOneRegistered(string lifetime, string expected) { + public void ANamedLifetime_IsTheOneRegistered(string lifetime, string expected) + { var interceptors = Run( - $"[Intercept(typeof(CountingInterceptor), Lifetime = ServiceLifetime.{lifetime})]"); + $"[Intercept(typeof(CountingInterceptor), Lifetime = ServiceLifetime.{lifetime})]" + ); Assert.Contains(expected, interceptors); } @@ -42,9 +45,11 @@ public void ANamedLifetime_IsTheOneRegistered(string lifetime, string expected) /// not to add a second registration the container resolves ahead of the first. /// [Fact] - public void AScopedInterceptor_IsNotAlsoRegisteredAsASingleton() { + public void AScopedInterceptor_IsNotAlsoRegisteredAsASingleton() + { var interceptors = Run( - "[Intercept(typeof(CountingInterceptor), Lifetime = ServiceLifetime.Scoped)]"); + "[Intercept(typeof(CountingInterceptor), Lifetime = ServiceLifetime.Scoped)]" + ); Assert.DoesNotContain("TryAddSingleton", interceptors); } @@ -53,9 +58,11 @@ public void AScopedInterceptor_IsNotAlsoRegisteredAsASingleton() { /// The registration still has to work end to end, not merely be emitted with the right name. /// [Fact] - public void AScopedInterceptor_Runs() { + public void AScopedInterceptor_Runs() + { var generated = GeneratedAssembly.Create( - Source("[Intercept(typeof(CountingInterceptor), Lifetime = ServiceLifetime.Scoped)]")); + Source("[Intercept(typeof(CountingInterceptor), Lifetime = ServiceLifetime.Scoped)]") + ); var provider = generated.BuildProvider(); var greeter = provider.GetService(generated.Type("IGreeter"))!; @@ -65,29 +72,30 @@ public void AScopedInterceptor_Runs() { } private static string Run(string attribute) => - GeneratorTestHarness.Run(Source(attribute)) + GeneratorTestHarness + .Run(Source(attribute)) .AssertNoErrors() .SourceContaining("Interceptors"); private static string Source(string attribute) => $$""" - using DependencyModules.Runtime.Attributes; - using DependencyModules.Runtime.Interception; - using Microsoft.Extensions.DependencyInjection; + using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Interception; + using Microsoft.Extensions.DependencyInjection; - namespace TestNamespace; + namespace TestNamespace; - public interface IGreeter { string Greet(); } + public interface IGreeter { string Greet(); } - public sealed class CountingInterceptor : IInterceptor { - public TResult Intercept(InvocationContext context) => context.Proceed(); - } + public sealed class CountingInterceptor : IInterceptor { + public TResult Intercept(InvocationContext context) => context.Proceed(); + } - [SingletonService] - {{attribute}} - public sealed class Greeter : IGreeter { public string Greet() => "hi"; } + [SingletonService] + {{attribute}} + public sealed class Greeter : IGreeter { public string Greet() => "hi"; } - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/MockTestExportDiagnosticTests.cs b/tests/DependencyModules.Tests/GeneratorTests/MockTestExportDiagnosticTests.cs index 53172d9..8bf09b2 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/MockTestExportDiagnosticTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/MockTestExportDiagnosticTests.cs @@ -16,15 +16,17 @@ namespace DependencyModules.Tests.GeneratorTests; /// everything under it, and one test overriding it for one argument is exactly what having both /// scopes is for. Reporting that would be reporting the feature. /// -public class MockTestExportDiagnosticTests { - +public class MockTestExportDiagnosticTests +{ [Fact] - public void BothOnOneMethod_ReportsDM0021() { + public void BothOnOneMethod_ReportsDM0021() + { var result = Run( """ [TestExport(typeof(IThing), Implementation = typeof(RealThing))] public void Conflicting([Mock] IThing thing) { } - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0021"); @@ -37,12 +39,14 @@ public void Conflicting([Mock] IThing thing) { } /// to see what actually happens. /// [Fact] - public void ItIsReportedAtTheParameter() { + public void ItIsReportedAtTheParameter() + { var result = Run( """ [TestExport(typeof(IThing), Implementation = typeof(RealThing))] public void Conflicting([Mock] IThing thing) { } - """); + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0021"); @@ -54,32 +58,36 @@ public void Conflicting([Mock] IThing thing) { } /// The class-level default a test opts out of. This is the shape the override exists for. /// [Fact] - public void TestExportOnTheClass_IsNotReported() { + public void TestExportOnTheClass_IsNotReported() + { var result = GeneratorTestHarness.Run( $$""" - {{Preamble}} + {{Preamble}} - [TestExport(typeof(IThing), Implementation = typeof(RealThing))] - public class Fixture { - public void Overriding([Mock] IThing thing) { } - } - """); + [TestExport(typeof(IThing), Implementation = typeof(RealThing))] + public class Fixture { + public void Overriding([Mock] IThing thing) { } + } + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0021"); } [Fact] - public void TestExportOnTheAssembly_IsNotReported() { + public void TestExportOnTheAssembly_IsNotReported() + { var result = GeneratorTestHarness.Run( $$""" - {{Preamble}} + {{Preamble}} - [assembly: TestExport(typeof(IThing), Implementation = typeof(RealThing))] + [assembly: TestExport(typeof(IThing), Implementation = typeof(RealThing))] - public class Fixture { - public void Overriding([Mock] IThing thing) { } - } - """); + public class Fixture { + public void Overriding([Mock] IThing thing) { } + } + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0021"); } @@ -88,30 +96,35 @@ public void Overriding([Mock] IThing thing) { } /// Naming different services is two unrelated declarations, not a disagreement. /// [Fact] - public void BothOnOneMethodNamingDifferentServices_IsNotReported() { + public void BothOnOneMethodNamingDifferentServices_IsNotReported() + { var result = Run( """ [TestExport(typeof(IOther), Implementation = typeof(RealOther))] public void Unrelated([Mock] IThing thing) { } - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0021"); } [Fact] - public void AMockWithNoTestExport_IsNotReported() { + public void AMockWithNoTestExport_IsNotReported() + { var result = Run("public void JustAMock([Mock] IThing thing) { }"); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0021"); } [Fact] - public void ATestExportWithNoMock_IsNotReported() { + public void ATestExportWithNoMock_IsNotReported() + { var result = Run( """ [TestExport(typeof(IThing), Implementation = typeof(RealThing))] public void JustAnExport(IThing thing) { } - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0021"); } @@ -120,18 +133,19 @@ public void JustAnExport(IThing thing) { } /// Resolved rather than string-matched, the way every other attribute this generator reads is. /// [Fact] - public void QualifiedSpellings_AreStillReported() { + public void QualifiedSpellings_AreStillReported() + { var result = Run( """ [DependencyModules.Testing.Attributes.TestExport(typeof(IThing), Implementation = typeof(RealThing))] public void Conflicting([global::DependencyModules.Testing.Attributes.Mock] IThing thing) { } - """); + """ + ); Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0021"); } - private const string Preamble = - """ + private const string Preamble = """ using DependencyModules.Runtime.Attributes; using DependencyModules.Testing.Attributes; @@ -149,10 +163,11 @@ public class RealOther : IOther; private static GeneratorResult Run(string body) => GeneratorTestHarness.Run( $$""" - {{Preamble}} + {{Preamble}} - public class Fixture { - {{body}} - } - """); + public class Fixture { + {{body}} + } + """ + ); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/ModelComparerTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ModelComparerTests.cs index 913e1b6..9ac5d55 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ModelComparerTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ModelComparerTests.cs @@ -10,186 +10,300 @@ namespace DependencyModules.Tests.GeneratorTests; /// Every field that affects generated code must make two models compare unequal, or an edit to /// that field will serve stale output. /// -public class ModelComparerTests { - +public class ModelComparerTests +{ private readonly ModuleEntryPointModelComparer _entryPointComparer = new(); private readonly DependencyModuleConfigurationModelComparer _configurationComparer = new(); [Fact] - public void EntryPoints_BuiltFromTheSameValues_AreEqual() { - Assert.True(_entryPointComparer.Equals(ModelFactory.EntryPoint(), ModelFactory.EntryPoint())); + public void EntryPoints_BuiltFromTheSameValues_AreEqual() + { + Assert.True( + _entryPointComparer.Equals(ModelFactory.EntryPoint(), ModelFactory.EntryPoint()) + ); } [Fact] - public void EqualEntryPoints_ShareAHashCode() { + public void EqualEntryPoints_ShareAHashCode() + { Assert.Equal( _entryPointComparer.GetHashCode(ModelFactory.EntryPoint()), - _entryPointComparer.GetHashCode(ModelFactory.EntryPoint())); + _entryPointComparer.GetHashCode(ModelFactory.EntryPoint()) + ); } [Fact] - public void EntryPoints_BothNull_AreEqual() { + public void EntryPoints_BothNull_AreEqual() + { Assert.True(_entryPointComparer.Equals(null, null)); } [Fact] - public void EntryPoints_OneNull_AreNotEqual() { + public void EntryPoints_OneNull_AreNotEqual() + { Assert.False(_entryPointComparer.Equals(ModelFactory.EntryPoint(), null)); Assert.False(_entryPointComparer.Equals(null, ModelFactory.EntryPoint())); } [Fact] - public void EntryPoints_DifferingByFileLocation_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(fileLocation: "/project/A.cs"), - ModelFactory.EntryPoint(fileLocation: "/project/B.cs"))); + public void EntryPoints_DifferingByFileLocation_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(fileLocation: "/project/A.cs"), + ModelFactory.EntryPoint(fileLocation: "/project/B.cs") + ) + ); } [Fact] - public void EntryPoints_DifferingByType_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("Ns", "One")), - ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("Ns", "Two")))); + public void EntryPoints_DifferingByType_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("Ns", "One")), + ModelFactory.EntryPoint(entryPointType: TypeDefinition.Get("Ns", "Two")) + ) + ); } [Fact] - public void EntryPoints_DifferingByFeatures_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(features: ModuleEntryPointFeatures.None), - ModelFactory.EntryPoint(features: ModuleEntryPointFeatures.OnlyRealm))); + public void EntryPoints_DifferingByFeatures_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(features: ModuleEntryPointFeatures.None), + ModelFactory.EntryPoint(features: ModuleEntryPointFeatures.OnlyRealm) + ) + ); } [Fact] - public void EntryPoints_DifferingByUseMethod_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(useMethod: "UseOne"), - ModelFactory.EntryPoint(useMethod: "UseTwo"))); + public void EntryPoints_DifferingByUseMethod_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(useMethod: "UseOne"), + ModelFactory.EntryPoint(useMethod: "UseTwo") + ) + ); } [Fact] - public void EntryPoints_DifferingByRegistrationType_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(registrationType: RegistrationType.Add), - ModelFactory.EntryPoint(registrationType: RegistrationType.Try))); + public void EntryPoints_DifferingByRegistrationType_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(registrationType: RegistrationType.Add), + ModelFactory.EntryPoint(registrationType: RegistrationType.Try) + ) + ); } [Fact] - public void EntryPoints_DifferingByGenerateAttribute_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(generateAttribute: true), - ModelFactory.EntryPoint(generateAttribute: false))); + public void EntryPoints_DifferingByGenerateAttribute_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(generateAttribute: true), + ModelFactory.EntryPoint(generateAttribute: false) + ) + ); } [Fact] - public void EntryPoints_DifferingByJsonSerializerRegistration_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(registerJsonSerializers: true), - ModelFactory.EntryPoint(registerJsonSerializers: false))); + public void EntryPoints_DifferingByJsonSerializerRegistration_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(registerJsonSerializers: true), + ModelFactory.EntryPoint(registerJsonSerializers: false) + ) + ); } [Fact] - public void EntryPoints_DifferingByFactoryGeneration_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(generateFactories: true), - ModelFactory.EntryPoint(generateFactories: false))); + public void EntryPoints_DifferingByFactoryGeneration_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(generateFactories: true), + ModelFactory.EntryPoint(generateFactories: false) + ) + ); } [Fact] - public void EntryPoints_DifferingByParameters_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(parameters: [Parameter("one")]), - ModelFactory.EntryPoint(parameters: [Parameter("two")]))); + public void EntryPoints_DifferingByParameters_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(parameters: [Parameter("one")]), + ModelFactory.EntryPoint(parameters: [Parameter("two")]) + ) + ); } [Fact] - public void EntryPoints_WithSeparateButEqualParameters_AreEqual() { - Assert.True(_entryPointComparer.Equals( - ModelFactory.EntryPoint(parameters: [Parameter("same")]), - ModelFactory.EntryPoint(parameters: [Parameter("same")]))); + public void EntryPoints_WithSeparateButEqualParameters_AreEqual() + { + Assert.True( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(parameters: [Parameter("same")]), + ModelFactory.EntryPoint(parameters: [Parameter("same")]) + ) + ); } [Fact] - public void EntryPoints_DifferingByProperties_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(properties: [Property("One")]), - ModelFactory.EntryPoint(properties: [Property("Two")]))); + public void EntryPoints_DifferingByProperties_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(properties: [Property("One")]), + ModelFactory.EntryPoint(properties: [Property("Two")]) + ) + ); } [Fact] - public void EntryPoints_DifferingByAttributes_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(attributes: [Attribute("One")]), - ModelFactory.EntryPoint(attributes: [Attribute("Two")]))); + public void EntryPoints_DifferingByAttributes_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(attributes: [Attribute("One")]), + ModelFactory.EntryPoint(attributes: [Attribute("Two")]) + ) + ); } [Fact] - public void EntryPoints_WithSeparateButEqualAttributes_AreEqual() { - Assert.True(_entryPointComparer.Equals( - ModelFactory.EntryPoint(attributes: [Attribute("Same")]), - ModelFactory.EntryPoint(attributes: [Attribute("Same")]))); + public void EntryPoints_WithSeparateButEqualAttributes_AreEqual() + { + Assert.True( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(attributes: [Attribute("Same")]), + ModelFactory.EntryPoint(attributes: [Attribute("Same")]) + ) + ); } [Fact] - public void EntryPoints_DifferingByFeatureTypes_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(featureTypes: [TypeDefinition.Get("Ns", "IOne")]), - ModelFactory.EntryPoint(featureTypes: [TypeDefinition.Get("Ns", "ITwo")]))); + public void EntryPoints_DifferingByFeatureTypes_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(featureTypes: [TypeDefinition.Get("Ns", "IOne")]), + ModelFactory.EntryPoint(featureTypes: [TypeDefinition.Get("Ns", "ITwo")]) + ) + ); } [Fact] - public void EntryPoints_DifferingByAdditionalModules_AreNotEqual() { - Assert.False(_entryPointComparer.Equals( - ModelFactory.EntryPoint(additionalModules: [TypeDefinition.Get("Ns", "One")]), - ModelFactory.EntryPoint(additionalModules: [TypeDefinition.Get("Ns", "Two")]))); + public void EntryPoints_DifferingByAdditionalModules_AreNotEqual() + { + Assert.False( + _entryPointComparer.Equals( + ModelFactory.EntryPoint(additionalModules: [TypeDefinition.Get("Ns", "One")]), + ModelFactory.EntryPoint(additionalModules: [TypeDefinition.Get("Ns", "Two")]) + ) + ); } [Fact] - public void Configurations_BuiltFromTheSameValues_AreEqual() { - Assert.True(_configurationComparer.Equals(ModelFactory.Configuration(), ModelFactory.Configuration())); + public void Configurations_BuiltFromTheSameValues_AreEqual() + { + Assert.True( + _configurationComparer.Equals( + ModelFactory.Configuration(), + ModelFactory.Configuration() + ) + ); } [Fact] - public void SameConfigurationInstance_IsEqualToItself() { + public void SameConfigurationInstance_IsEqualToItself() + { var configuration = ModelFactory.Configuration(); Assert.True(_configurationComparer.Equals(configuration, configuration)); } [Fact] - public void Configurations_OneNull_AreNotEqual() { + public void Configurations_OneNull_AreNotEqual() + { Assert.False(_configurationComparer.Equals(ModelFactory.Configuration(), null)); Assert.False(_configurationComparer.Equals(null, ModelFactory.Configuration())); } [Fact] - public void EqualConfigurations_ShareAHashCode() { + public void EqualConfigurations_ShareAHashCode() + { Assert.Equal( _configurationComparer.GetHashCode(ModelFactory.Configuration()), - _configurationComparer.GetHashCode(ModelFactory.Configuration())); + _configurationComparer.GetHashCode(ModelFactory.Configuration()) + ); } [Theory] [MemberData(nameof(DifferingConfigurations))] public void Configurations_DifferingByAnyField_AreNotEqual( - string field, DependencyModuleConfigurationModel other) { - + string field, + DependencyModuleConfigurationModel other + ) + { Assert.False( _configurationComparer.Equals(ModelFactory.Configuration(), other), - $"Configurations differing by {field} compared equal, so a change to it would serve stale output."); + $"Configurations differing by {field} compared equal, so a change to it would serve stale output." + ); } - public static TheoryData DifferingConfigurations() => - new() { - { nameof(DependencyModuleConfigurationModel.RegistrationType), ModelFactory.Configuration(registrationType: RegistrationType.Try) }, - { nameof(DependencyModuleConfigurationModel.RegisterSourceGenerator), ModelFactory.Configuration(registerSourceGenerator: true) }, - { nameof(DependencyModuleConfigurationModel.RootNamespace), ModelFactory.Configuration(rootNamespace: "Other") }, - { nameof(DependencyModuleConfigurationModel.ProjectDir), ModelFactory.Configuration(projectDir: "/other/") }, - { nameof(DependencyModuleConfigurationModel.AutoGenerateEntry), ModelFactory.Configuration(autoGenerateEntry: false) }, - { nameof(DependencyModuleConfigurationModel.LogOutputFolder), ModelFactory.Configuration(logOutputFolder: "/logs") }, - { nameof(DependencyModuleConfigurationModel.LogOutputLevel), ModelFactory.Configuration(logOutputLevel: LogOutputLevel.Error) }, - { nameof(DependencyModuleConfigurationModel.GenerateFactories), ModelFactory.Configuration(generateFactories: true) }, - { nameof(DependencyModuleConfigurationModel.ExcludeGeneratedCodeFromCoverage), ModelFactory.Configuration(excludeGeneratedCodeFromCoverage: false) }, - { nameof(DependencyModuleConfigurationModel.GeneratedCodeStyle), ModelFactory.Configuration(generatedCodeStyle: BraceStyle.KAndR) } + public static TheoryData< + string, + DependencyModuleConfigurationModel + > DifferingConfigurations() => + new() + { + { + nameof(DependencyModuleConfigurationModel.RegistrationType), + ModelFactory.Configuration(registrationType: RegistrationType.Try) + }, + { + nameof(DependencyModuleConfigurationModel.RegisterSourceGenerator), + ModelFactory.Configuration(registerSourceGenerator: true) + }, + { + nameof(DependencyModuleConfigurationModel.RootNamespace), + ModelFactory.Configuration(rootNamespace: "Other") + }, + { + nameof(DependencyModuleConfigurationModel.ProjectDir), + ModelFactory.Configuration(projectDir: "/other/") + }, + { + nameof(DependencyModuleConfigurationModel.AutoGenerateEntry), + ModelFactory.Configuration(autoGenerateEntry: false) + }, + { + nameof(DependencyModuleConfigurationModel.LogOutputFolder), + ModelFactory.Configuration(logOutputFolder: "/logs") + }, + { + nameof(DependencyModuleConfigurationModel.LogOutputLevel), + ModelFactory.Configuration(logOutputLevel: LogOutputLevel.Error) + }, + { + nameof(DependencyModuleConfigurationModel.GenerateFactories), + ModelFactory.Configuration(generateFactories: true) + }, + { + nameof(DependencyModuleConfigurationModel.ExcludeGeneratedCodeFromCoverage), + ModelFactory.Configuration(excludeGeneratedCodeFromCoverage: false) + }, + { + nameof(DependencyModuleConfigurationModel.GeneratedCodeStyle), + ModelFactory.Configuration(generatedCodeStyle: BraceStyle.KAndR) + }, }; private readonly InterceptorModelComparer _interceptorComparer = new(); @@ -201,51 +315,69 @@ public static TheoryData DifferingCo /// compared theirs. /// [Fact] - public void Interceptors_DifferingByRealm_AreNotEqual() { - Assert.False(_interceptorComparer.Equals( - Interceptor(realm: TypeDefinition.Get("Ns", "OneRealm")), - Interceptor(realm: TypeDefinition.Get("Ns", "OtherRealm")))); + public void Interceptors_DifferingByRealm_AreNotEqual() + { + Assert.False( + _interceptorComparer.Equals( + Interceptor(realm: TypeDefinition.Get("Ns", "OneRealm")), + Interceptor(realm: TypeDefinition.Get("Ns", "OtherRealm")) + ) + ); } [Fact] - public void Interceptors_GainingARealm_AreNotEqual() { - Assert.False(_interceptorComparer.Equals( - Interceptor(), - Interceptor(realm: TypeDefinition.Get("Ns", "OneRealm")))); + public void Interceptors_GainingARealm_AreNotEqual() + { + Assert.False( + _interceptorComparer.Equals( + Interceptor(), + Interceptor(realm: TypeDefinition.Get("Ns", "OneRealm")) + ) + ); } [Fact] - public void Interceptors_BuiltFromTheSameValues_AreEqual() { + public void Interceptors_BuiltFromTheSameValues_AreEqual() + { Assert.True(_interceptorComparer.Equals(Interceptor(), Interceptor())); } [Fact] - public void EqualInterceptors_ShareAHashCode() { + public void EqualInterceptors_ShareAHashCode() + { Assert.Equal( _interceptorComparer.GetHashCode(Interceptor()), - _interceptorComparer.GetHashCode(Interceptor())); + _interceptorComparer.GetHashCode(Interceptor()) + ); } [Fact] - public void Interceptors_DifferingByRealm_DoNotShareAHashCode() { + public void Interceptors_DifferingByRealm_DoNotShareAHashCode() + { Assert.NotEqual( _interceptorComparer.GetHashCode(Interceptor()), - _interceptorComparer.GetHashCode(Interceptor(realm: TypeDefinition.Get("Ns", "OneRealm")))); + _interceptorComparer.GetHashCode( + Interceptor(realm: TypeDefinition.Get("Ns", "OneRealm")) + ) + ); } [Fact] - public void Interceptors_DifferingByOrder_AreNotEqual() { + public void Interceptors_DifferingByOrder_AreNotEqual() + { Assert.False(_interceptorComparer.Equals(Interceptor(order: 1), Interceptor(order: 2))); } private static InterceptorModel Interceptor(ITypeDefinition? realm = null, int order = 0) => - new(TypeDefinition.Get("Ns", "IService"), + new( + TypeDefinition.Get("Ns", "IService"), TypeDefinition.Get("Ns", "Service"), [], [], [], order, - Realm: realm); + Realm: realm + ); private static ParameterInfoModel Parameter(string name) => new(name, TypeDefinition.Get("Ns", "SomeType"), null, []); diff --git a/tests/DependencyModules.Tests/GeneratorTests/ModelEqualityTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ModelEqualityTests.cs index 36650dd..8bfabee 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ModelEqualityTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ModelEqualityTests.cs @@ -10,13 +10,14 @@ namespace DependencyModules.Tests.GeneratorTests; /// IReadOnlyList members by reference, which silently disables caching — these tests pin the /// structural semantics that replaced it. /// -public class ModelEqualityTests { - +public class ModelEqualityTests +{ private static readonly ITypeDefinition SomeType = TypeDefinition.Get("Ns", "SomeType"); private static readonly ITypeDefinition OtherType = TypeDefinition.Get("Ns", "OtherType"); [Fact] - public void AttributeModel_WithSeparateButEqualLists_IsEqual() { + public void AttributeModel_WithSeparateButEqualLists_IsEqual() + { var first = Attribute(arguments: [new AttributeArgumentValue("key", "value")]); var second = Attribute(arguments: [new AttributeArgumentValue("key", "value")]); @@ -25,12 +26,14 @@ public void AttributeModel_WithSeparateButEqualLists_IsEqual() { } [Fact] - public void AttributeModel_WithEmptyLists_IsEqual() { + public void AttributeModel_WithEmptyLists_IsEqual() + { Assert.Equal(Attribute(), Attribute()); } [Fact] - public void AttributeModel_WithDifferentArguments_IsNotEqual() { + public void AttributeModel_WithDifferentArguments_IsNotEqual() + { var first = Attribute(arguments: [new AttributeArgumentValue("key", "one")]); var second = Attribute(arguments: [new AttributeArgumentValue("key", "two")]); @@ -38,7 +41,8 @@ public void AttributeModel_WithDifferentArguments_IsNotEqual() { } [Fact] - public void AttributeModel_WithDifferentArgumentCounts_IsNotEqual() { + public void AttributeModel_WithDifferentArgumentCounts_IsNotEqual() + { var first = Attribute(arguments: [new AttributeArgumentValue("key", "one")]); var second = Attribute(); @@ -46,7 +50,8 @@ public void AttributeModel_WithDifferentArgumentCounts_IsNotEqual() { } [Fact] - public void AttributeModel_WithDifferentProperties_IsNotEqual() { + public void AttributeModel_WithDifferentProperties_IsNotEqual() + { var first = Attribute(properties: [new AttributeArgumentValue("P", 1)]); var second = Attribute(properties: [new AttributeArgumentValue("P", 2)]); @@ -54,7 +59,8 @@ public void AttributeModel_WithDifferentProperties_IsNotEqual() { } [Fact] - public void AttributeModel_WithDifferentImplementedInterfaces_IsNotEqual() { + public void AttributeModel_WithDifferentImplementedInterfaces_IsNotEqual() + { var first = Attribute(interfaces: [SomeType]); var second = Attribute(interfaces: [OtherType]); @@ -62,7 +68,8 @@ public void AttributeModel_WithDifferentImplementedInterfaces_IsNotEqual() { } [Fact] - public void AttributeModel_WithDifferentType_IsNotEqual() { + public void AttributeModel_WithDifferentType_IsNotEqual() + { var first = new AttributeModel(SomeType, [], [], []); var second = new AttributeModel(OtherType, [], [], []); @@ -70,7 +77,8 @@ public void AttributeModel_WithDifferentType_IsNotEqual() { } [Fact] - public void AttributeArgumentValue_WithEqualArrayValues_IsEqual() { + public void AttributeArgumentValue_WithEqualArrayValues_IsEqual() + { var first = new AttributeArgumentValue("names", new[] { "a", "b" }); var second = new AttributeArgumentValue("names", new[] { "a", "b" }); @@ -79,7 +87,8 @@ public void AttributeArgumentValue_WithEqualArrayValues_IsEqual() { } [Fact] - public void AttributeArgumentValue_WithDifferentArrayValues_IsNotEqual() { + public void AttributeArgumentValue_WithDifferentArrayValues_IsNotEqual() + { var first = new AttributeArgumentValue("names", new[] { "a", "b" }); var second = new AttributeArgumentValue("names", new[] { "a", "c" }); @@ -87,7 +96,8 @@ public void AttributeArgumentValue_WithDifferentArrayValues_IsNotEqual() { } [Fact] - public void AttributeArgumentValue_WithDifferentArrayLengths_IsNotEqual() { + public void AttributeArgumentValue_WithDifferentArrayLengths_IsNotEqual() + { var first = new AttributeArgumentValue("names", new[] { "a" }); var second = new AttributeArgumentValue("names", new[] { "a", "b" }); @@ -95,24 +105,30 @@ public void AttributeArgumentValue_WithDifferentArrayLengths_IsNotEqual() { } [Fact] - public void AttributeArgumentValue_WithDifferentNames_IsNotEqual() { + public void AttributeArgumentValue_WithDifferentNames_IsNotEqual() + { Assert.NotEqual( new AttributeArgumentValue("one", "value"), - new AttributeArgumentValue("two", "value")); + new AttributeArgumentValue("two", "value") + ); } [Fact] - public void AttributeArgumentValue_WithNullValues_IsEqual() { + public void AttributeArgumentValue_WithNullValues_IsEqual() + { Assert.Equal( new AttributeArgumentValue("key", null), - new AttributeArgumentValue("key", null)); + new AttributeArgumentValue("key", null) + ); } [Fact] - public void AttributeArgumentValue_NullVersusValue_IsNotEqual() { + public void AttributeArgumentValue_NullVersusValue_IsNotEqual() + { Assert.NotEqual( new AttributeArgumentValue("key", null), - new AttributeArgumentValue("key", "value")); + new AttributeArgumentValue("key", "value") + ); } /// @@ -120,14 +136,17 @@ public void AttributeArgumentValue_NullVersusValue_IsNotEqual() { /// char collection. /// [Fact] - public void AttributeArgumentValue_StringVersusCharArray_IsNotEqual() { + public void AttributeArgumentValue_StringVersusCharArray_IsNotEqual() + { Assert.NotEqual( new AttributeArgumentValue("key", "ab"), - new AttributeArgumentValue("key", new[] { 'a', 'b' })); + new AttributeArgumentValue("key", new[] { 'a', 'b' }) + ); } [Fact] - public void ParameterInfoModel_WithSeparateButEqualAttributes_IsEqual() { + public void ParameterInfoModel_WithSeparateButEqualAttributes_IsEqual() + { var first = new ParameterInfoModel("name", SomeType, null, [Attribute()]); var second = new ParameterInfoModel("name", SomeType, null, [Attribute()]); @@ -136,28 +155,35 @@ public void ParameterInfoModel_WithSeparateButEqualAttributes_IsEqual() { } [Fact] - public void ParameterInfoModel_WithDifferentNames_IsNotEqual() { + public void ParameterInfoModel_WithDifferentNames_IsNotEqual() + { Assert.NotEqual( new ParameterInfoModel("one", SomeType, null, []), - new ParameterInfoModel("two", SomeType, null, [])); + new ParameterInfoModel("two", SomeType, null, []) + ); } [Fact] - public void ParameterInfoModel_WithDifferentTypes_IsNotEqual() { + public void ParameterInfoModel_WithDifferentTypes_IsNotEqual() + { Assert.NotEqual( new ParameterInfoModel("name", SomeType, null, []), - new ParameterInfoModel("name", OtherType, null, [])); + new ParameterInfoModel("name", OtherType, null, []) + ); } [Fact] - public void ParameterInfoModel_WithDifferentDefaultValues_IsNotEqual() { + public void ParameterInfoModel_WithDifferentDefaultValues_IsNotEqual() + { Assert.NotEqual( new ParameterInfoModel("name", SomeType, 1, []), - new ParameterInfoModel("name", SomeType, 2, [])); + new ParameterInfoModel("name", SomeType, 2, []) + ); } [Fact] - public void ConstructorInfoModel_WithSeparateButEqualParameters_IsEqual() { + public void ConstructorInfoModel_WithSeparateButEqualParameters_IsEqual() + { var first = new ConstructorInfoModel([new ParameterInfoModel("a", SomeType, null, [])]); var second = new ConstructorInfoModel([new ParameterInfoModel("a", SomeType, null, [])]); @@ -166,41 +192,59 @@ public void ConstructorInfoModel_WithSeparateButEqualParameters_IsEqual() { } [Fact] - public void ConstructorInfoModel_WithDifferentParameters_IsNotEqual() { + public void ConstructorInfoModel_WithDifferentParameters_IsNotEqual() + { Assert.NotEqual( new ConstructorInfoModel([new ParameterInfoModel("a", SomeType, null, [])]), - new ConstructorInfoModel([new ParameterInfoModel("b", SomeType, null, [])])); + new ConstructorInfoModel([new ParameterInfoModel("b", SomeType, null, [])]) + ); } [Fact] - public void ServiceFactoryModel_WithSeparateButEqualParameters_IsEqual() { - var first = new ServiceFactoryModel(SomeType, "Create", [new ParameterInfoModel("a", SomeType, null, [])]); - var second = new ServiceFactoryModel(SomeType, "Create", [new ParameterInfoModel("a", SomeType, null, [])]); + public void ServiceFactoryModel_WithSeparateButEqualParameters_IsEqual() + { + var first = new ServiceFactoryModel( + SomeType, + "Create", + [new ParameterInfoModel("a", SomeType, null, [])] + ); + var second = new ServiceFactoryModel( + SomeType, + "Create", + [new ParameterInfoModel("a", SomeType, null, [])] + ); Assert.Equal(first, second); Assert.Equal(first.GetHashCode(), second.GetHashCode()); } [Fact] - public void ServiceFactoryModel_WithDifferentMethodNames_IsNotEqual() { + public void ServiceFactoryModel_WithDifferentMethodNames_IsNotEqual() + { Assert.NotEqual( new ServiceFactoryModel(SomeType, "Create", []), - new ServiceFactoryModel(SomeType, "Build", [])); + new ServiceFactoryModel(SomeType, "Build", []) + ); } [Fact] - public void ServiceFactoryModel_WithDifferentDeclaringTypes_IsNotEqual() { + public void ServiceFactoryModel_WithDifferentDeclaringTypes_IsNotEqual() + { Assert.NotEqual( new ServiceFactoryModel(SomeType, "Create", []), - new ServiceFactoryModel(OtherType, "Create", [])); + new ServiceFactoryModel(OtherType, "Create", []) + ); } private static AttributeModel Attribute( IReadOnlyList? arguments = null, IReadOnlyList? properties = null, - IReadOnlyList? interfaces = null) => - new(SomeType, + IReadOnlyList? interfaces = null + ) => + new( + SomeType, arguments ?? new List(), properties ?? new List(), - interfaces ?? new List()); + interfaces ?? new List() + ); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/ModuleEqualityDiagnosticTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ModuleEqualityDiagnosticTests.cs index f318bff..662136e 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ModuleEqualityDiagnosticTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ModuleEqualityDiagnosticTests.cs @@ -12,10 +12,11 @@ namespace DependencyModules.Tests.GeneratorTests; /// The generator has to pick an identity either way. This reports that it picked, so the choice is /// the developer's. /// -public class ModuleEqualityDiagnosticTests { - +public class ModuleEqualityDiagnosticTests +{ [Fact] - public void ASettableProperty_IsReported() { + public void ASettableProperty_IsReported() + { var result = Run("public int SizeLimit { get; set; }"); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0018"); @@ -29,28 +30,32 @@ public void ASettableProperty_IsReported() { /// attribute never assigns it — so there is no identity question to answer and nothing to report. /// [Fact] - public void AnExpressionBodiedProperty_IsNotReported() { + public void AnExpressionBodiedProperty_IsNotReported() + { var result = Run("""public string Value => "A";"""); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); } [Fact] - public void AGetOnlyProperty_IsNotReported() { + public void AGetOnlyProperty_IsNotReported() + { var result = Run("public string Value { get; } = \"A\";"); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); } [Fact] - public void AStaticProperty_IsNotReported() { + public void AStaticProperty_IsNotReported() + { var result = Run("public static int Shared { get; set; }"); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); } [Fact] - public void NoPropertiesAtAll_IsNotReported() { + public void NoPropertiesAtAll_IsNotReported() + { var result = Run(""); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); @@ -61,7 +66,8 @@ public void NoPropertiesAtAll_IsNotReported() { /// The generator already stands aside when a module declares its own Equals. /// [Fact] - public void DeclaringEquals_SilencesIt() { + public void DeclaringEquals_SilencesIt() + { var result = Run( """ public int SizeLimit { get; set; } @@ -70,14 +76,16 @@ public override bool Equals(object? obj) => obj is TestModule other && other.SizeLimit == SizeLimit; public override int GetHashCode() => SizeLimit; - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); } /// A record gets its equality from the language, so it never faces the question. [Fact] - public void ARecordModule_IsNotReported() { + public void ARecordModule_IsNotReported() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -88,7 +96,8 @@ namespace TestNamespace; public partial record TestModule { public int SizeLimit { get; set; } } - """); + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); } @@ -96,13 +105,14 @@ public partial record TestModule { private static GeneratorResult Run(string body) => GeneratorTestHarness.Run( $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - [DependencyModule] - public partial class TestModule { - {{body}} - } - """); + [DependencyModule] + public partial class TestModule { + {{body}} + } + """ + ); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/ModuleGenerationSnapshotTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ModuleGenerationSnapshotTests.cs index 1cdd339..23722e6 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ModuleGenerationSnapshotTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ModuleGenerationSnapshotTests.cs @@ -12,10 +12,11 @@ namespace DependencyModules.Tests.GeneratorTests; /// UPDATE_SNAPSHOTS=1 dotnet test tests/DependencyModules.Tests /// then review the diff under tests/DependencyModules.Tests/Snapshots. /// -public class ModuleGenerationSnapshotTests { - +public class ModuleGenerationSnapshotTests +{ [Fact] - public void SimpleModule() { + public void SimpleModule() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -29,7 +30,8 @@ public class Thing : IThing; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); Snapshot.Match(result.ToSnapshot()); @@ -45,7 +47,8 @@ public partial class TestModule; /// descriptor and the conditional registration is the override. /// [Fact] - public void ModuleWithEnvironmentConditions() { + public void ModuleWithEnvironmentConditions() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -73,14 +76,16 @@ public class Billing : IBilling; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); Snapshot.Match(result.ToSnapshot()); } [Fact] - public void ModuleWithAllServiceLifetimes() { + public void ModuleWithAllServiceLifetimes() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -102,14 +107,16 @@ public class TransientThing : ITransient; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); Snapshot.Match(result.ToSnapshot()); } [Fact] - public void ModuleWithConstructorParametersAndProperties() { + public void ModuleWithConstructorParametersAndProperties() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -130,14 +137,16 @@ public partial class TestModule(bool someFlag) : IServiceCollectionConfiguration public void ConfigureServices(IServiceCollection services) { } } - """); + """ + ); result.AssertNoErrors(); Snapshot.Match(result.ToSnapshot()); } [Fact] - public void KeyedAndAsRegistrations() { + public void KeyedAndAsRegistrations() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -155,14 +164,16 @@ public class AsThing : IThing, IOther; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); Snapshot.Match(result.ToSnapshot()); } [Fact] - public void RegistrationTypeVariants() { + public void RegistrationTypeVariants() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -184,14 +195,16 @@ public class ReplaceThing : IReplace; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); Snapshot.Match(result.ToSnapshot()); } [Fact] - public void RecordModule() { + public void RecordModule() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -205,14 +218,16 @@ public class Thing : IThing; [DependencyModule] public partial record TestModule; - """); + """ + ); result.AssertNoErrors(); Snapshot.Match(result.ToSnapshot()); } [Fact] - public void GenericServiceRegistrations() { + public void GenericServiceRegistrations() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -229,14 +244,16 @@ public class ClosedGeneric : IGeneric; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); Snapshot.Match(result.ToSnapshot()); } [Fact] - public void ModuleWithCoverageExclusionDisabled() { + public void ModuleWithCoverageExclusionDisabled() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -251,7 +268,8 @@ public class Thing : IThing; [DependencyModule] public partial class TestModule; """, - new Dictionary { ["ExcludeGeneratedCodeFromCoverage"] = "false" }); + new Dictionary { ["ExcludeGeneratedCodeFromCoverage"] = "false" } + ); result.AssertNoErrors(); Snapshot.Match(result.ToSnapshot()); diff --git a/tests/DependencyModules.Tests/GeneratorTests/ModuleParameterAccessibilityTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ModuleParameterAccessibilityTests.cs index 2f35473..939cffe 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ModuleParameterAccessibilityTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ModuleParameterAccessibilityTests.cs @@ -17,14 +17,15 @@ namespace DependencyModules.Tests.GeneratorTests; /// Accessibility is judged from where the generated attribute sits: same assembly, different type. /// So internal reaches it and private does not, and an unmodified property is private by default. /// -public class ModuleParameterAccessibilityTests { - +public class ModuleParameterAccessibilityTests +{ [Theory] [InlineData("private int SizeLimit { get; set; }")] [InlineData("protected int SizeLimit { get; set; }")] [InlineData("private protected int SizeLimit { get; set; }")] [InlineData("int SizeLimit { get; set; }")] - public void APropertyTheAttributeCannotReach_CompilesCleanly(string property) { + public void APropertyTheAttributeCannotReach_CompilesCleanly(string property) + { Run(property).AssertNoErrors(); } @@ -33,7 +34,8 @@ public void APropertyTheAttributeCannotReach_CompilesCleanly(string property) { [InlineData("protected int SizeLimit { get; set; }")] [InlineData("private protected int SizeLimit { get; set; }")] [InlineData("int SizeLimit { get; set; }")] - public void APropertyTheAttributeCannotReach_IsNotCopiedOntoIt(string property) { + public void APropertyTheAttributeCannotReach_IsNotCopiedOntoIt(string property) + { var attribute = Run(property).SourceContaining("TestModule.Module"); Assert.DoesNotContain("SizeLimit", attribute); @@ -48,7 +50,8 @@ public void APropertyTheAttributeCannotReach_IsNotCopiedOntoIt(string property) [InlineData("protected int SizeLimit { get; set; }")] [InlineData("private protected int SizeLimit { get; set; }")] [InlineData("int SizeLimit { get; set; }")] - public void APropertyTheAttributeCannotReach_IsNotReportedAsDM0018(string property) { + public void APropertyTheAttributeCannotReach_IsNotReportedAsDM0018(string property) + { var result = Run(property); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0018"); @@ -62,7 +65,8 @@ public void APropertyTheAttributeCannotReach_IsNotReportedAsDM0018(string proper [InlineData("public int SizeLimit { get; set; }")] [InlineData("internal int SizeLimit { get; set; }")] [InlineData("protected internal int SizeLimit { get; set; }")] - public void AReachableProperty_IsStillAParameter(string property) { + public void AReachableProperty_IsStillAParameter(string property) + { var result = Run(property).AssertNoErrors(); Assert.Contains("SizeLimit", result.SourceContaining("TestModule.Module")); @@ -72,13 +76,14 @@ public void AReachableProperty_IsStillAParameter(string property) { private static GeneratorResult Run(string body) => GeneratorTestHarness.Run( $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - [DependencyModule] - public partial class TestModule { - {{body}} - } - """); + [DependencyModule] + public partial class TestModule { + {{body}} + } + """ + ); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/ReferencedAssemblyModuleAttributeTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ReferencedAssemblyModuleAttributeTests.cs index d4ae220..0c57f77 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ReferencedAssemblyModuleAttributeTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ReferencedAssemblyModuleAttributeTests.cs @@ -16,10 +16,9 @@ namespace DependencyModules.Tests.GeneratorTests; /// its DM0019 example — using MyApp.Library; above [assembly: LibraryModule] — and it /// did not fire, because both codes were built only from the modules declared in this compilation. /// -public class ReferencedAssemblyModuleAttributeTests { - - private const string LibrarySource = - """ +public class ReferencedAssemblyModuleAttributeTests +{ + private const string LibrarySource = """ using DependencyModules.Runtime.Attributes; namespace ThePackage.Composition; @@ -38,13 +37,15 @@ public partial class LibraryModule; /// file the generated ApplicationModule was not built from, so nothing reads it. /// [Fact] - public void AnAssemblyAttributeOutsideTheEntryPointFile_ReportsDM0019() { + public void AnAssemblyAttributeOutsideTheEntryPointFile_ReportsDM0019() + { var result = Run( bootstrap: """ - using ThePackage.Composition; + using ThePackage.Composition; - [assembly: LibraryModule] - """); + [assembly: LibraryModule] + """ + ); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0019"); @@ -53,14 +54,16 @@ public void AnAssemblyAttributeOutsideTheEntryPointFile_ReportsDM0019() { } [Fact] - public void AnAssemblyAttributeInTheEntryPointFile_IsSilent() { + public void AnAssemblyAttributeInTheEntryPointFile_IsSilent() + { var result = Run( bootstrap: null, programExtra: """ - using ThePackage.Composition; + using ThePackage.Composition; - [assembly: LibraryModule] - """); + [assembly: LibraryModule] + """ + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0019"); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0016"); @@ -72,7 +75,8 @@ public void AnAssemblyAttributeInTheEntryPointFile_IsSilent() { /// turns that into the one-line fix. /// [Fact] - public void AnAssemblyAttributeWithoutItsNamespaceImported_ReportsDM0016() { + public void AnAssemblyAttributeWithoutItsNamespaceImported_ReportsDM0016() + { var result = Run(bootstrap: "[assembly: LibraryModule]"); var diagnostic = Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0016"); @@ -86,7 +90,8 @@ public void AnAssemblyAttributeWithoutItsNamespaceImported_ReportsDM0016() { /// the in-compilation path already reports in. /// [Fact] - public void AnAssemblyAttributeWithoutItsNamespaceImported_DoesNotAlsoReportDM0019() { + public void AnAssemblyAttributeWithoutItsNamespaceImported_DoesNotAlsoReportDM0019() + { var result = Run(bootstrap: "[assembly: LibraryModule]"); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0019"); @@ -96,12 +101,15 @@ public void AnAssemblyAttributeWithoutItsNamespaceImported_DoesNotAlsoReportDM00 /// A global using satisfies the import wherever it is written, exactly as for a local module. /// [Fact] - public void AGlobalUsingSatisfiesTheImport() { + public void AGlobalUsingSatisfiesTheImport() + { var result = Run( bootstrap: "[assembly: LibraryModule]", - extraFiles: new Dictionary { - ["Usings.cs"] = "global using ThePackage.Composition;" - }); + extraFiles: new Dictionary + { + ["Usings.cs"] = "global using ThePackage.Composition;", + } + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0016"); Assert.Single(result.GeneratorDiagnostics, d => d.Id == "DM0019"); @@ -113,18 +121,25 @@ public void AGlobalUsingSatisfiesTheImport() { /// the same shape, and that is where these attributes are supposed to live in their own file. /// [Fact] - public void AClassLibrary_IsSilent() { - var library = GeneratorTestHarness.CompileLibrary(LibrarySource, "TheModulePackage", runGenerator: true); + public void AClassLibrary_IsSilent() + { + var library = GeneratorTestHarness.CompileLibrary( + LibrarySource, + "TheModulePackage", + runGenerator: true + ); var result = GeneratorTestHarness.Run( - new Dictionary { + new Dictionary + { ["Bootstrap.cs"] = """ - using ThePackage.Composition; + using ThePackage.Composition; - [assembly: LibraryModule] - """ + [assembly: LibraryModule] + """, }, - additionalReferences: [library.Reference]); + additionalReferences: [library.Reference] + ); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0019"); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0016"); @@ -135,7 +150,8 @@ public void AClassLibrary_IsSilent() { /// what keeps this from reporting on every unrelated assembly-level attribute in the project. /// [Fact] - public void AnAttributeThatNamesNoModule_IsSilent() { + public void AnAttributeThatNamesNoModule_IsSilent() + { var result = Run(bootstrap: "[assembly: System.CLSCompliant(true)]"); Assert.DoesNotContain(result.GeneratorDiagnostics, d => d.Id == "DM0016"); @@ -145,27 +161,36 @@ public void AnAttributeThatNamesNoModule_IsSilent() { private static GeneratorResult Run( string? bootstrap, string? programExtra = null, - IReadOnlyDictionary? extraFiles = null) { - - var library = GeneratorTestHarness.CompileLibrary(LibrarySource, "TheModulePackage", runGenerator: true); - - var sources = new Dictionary { + IReadOnlyDictionary? extraFiles = null + ) + { + var library = GeneratorTestHarness.CompileLibrary( + LibrarySource, + "TheModulePackage", + runGenerator: true + ); + + var sources = new Dictionary + { // Top-level statements, so an ApplicationModule is generated and there is an entry // point file for DM0019 to measure against. - ["Program.cs"] = (programExtra ?? "") + "\nSystem.Console.WriteLine(\"hello\");" + ["Program.cs"] = (programExtra ?? "") + "\nSystem.Console.WriteLine(\"hello\");", }; - if (bootstrap != null) { + if (bootstrap != null) + { sources["Bootstrap.cs"] = bootstrap; } - foreach (var extra in extraFiles ?? new Dictionary()) { + foreach (var extra in extraFiles ?? new Dictionary()) + { sources[extra.Key] = extra.Value; } return GeneratorTestHarness.Run( sources, outputKind: OutputKind.ConsoleApplication, - additionalReferences: [library.Reference]); + additionalReferences: [library.Reference] + ); } } diff --git a/tests/DependencyModules.Tests/GeneratorTests/ReferencedAssemblyScanTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ReferencedAssemblyScanTests.cs index df333c3..42e4497 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ReferencedAssemblyScanTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ReferencedAssemblyScanTests.cs @@ -12,10 +12,9 @@ namespace DependencyModules.Tests.GeneratorTests; /// syntax tree for any of its types. That is the whole point: this path reads symbols out of /// metadata, where the in-compilation path reads declarations. /// -public class ReferencedAssemblyScanTests { - - private const string LibrarySource = - """ +public class ReferencedAssemblyScanTests +{ + private const string LibrarySource = """ namespace ThePackage; public interface IHandler { } @@ -38,8 +37,7 @@ private UnconstructableHandler() { } public class Unrelated { } """; - private const string Preamble = - """ + private const string Preamble = """ using DependencyModules.Runtime.Attributes; using DependencyModules.Runtime.Conventions; using ThePackage; @@ -49,18 +47,20 @@ namespace TestNamespace; """; private static (GeneratorResult Result, GeneratedAssembly? Assembly) Run( - string module, bool compile = true) { - + string module, + bool compile = true + ) + { var library = GeneratorTestHarness.CompileLibrary(LibrarySource, "ThePackage"); var references = new[] { library.Reference }; var result = GeneratorTestHarness.Run( new Dictionary { ["Test.cs"] = Preamble + module }, - additionalReferences: references); + additionalReferences: references + ); var assembly = compile - ? GeneratedAssembly.Create( - Preamble + module, additionalReferences: references) + ? GeneratedAssembly.Create(Preamble + module, additionalReferences: references) : null; return (result, assembly); @@ -71,7 +71,8 @@ private static (GeneratorResult Result, GeneratedAssembly? Assembly) Run( /// the closed construction it actually implements. /// [Fact] - public void RegistersTypesFromAReferencedAssembly() { + public void RegistersTypesFromAReferencedAssembly() + { var (result, assembly) = Run( """ [DependencyModule] @@ -82,16 +83,17 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { .AsScoped(); } } - """); + """ + ); result.AssertNoErrors(); - var handlerType = assembly!.Services - .Select(d => d.ServiceType) + var handlerType = assembly! + .Services.Select(d => d.ServiceType) .First(t => t.Name == "IHandler`2"); - var registered = assembly.Services - .Where(d => d.ServiceType.Name == "IHandler`2") + var registered = assembly + .Services.Where(d => d.ServiceType.Name == "IHandler`2") .Select(d => d.ImplementationType!.Name) .OrderBy(name => name) .ToArray(); @@ -105,7 +107,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// the same way it would be in the compilation being built. /// [Fact] - public void SkipsWhatItCannotSeeOrConstruct() { + public void SkipsWhatItCannotSeeOrConstruct() + { var (result, assembly) = Run( """ [DependencyModule] @@ -116,10 +119,11 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { .AsScoped(); } } - """); + """ + ); - var registered = assembly!.Services - .Where(d => d.ServiceType.Name == "IHandler`2") + var registered = assembly! + .Services.Where(d => d.ServiceType.Name == "IHandler`2") .Select(d => d.ImplementationType!.Name) .ToArray(); @@ -134,7 +138,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// A registered service from the package resolves. /// [Fact] - public void TheRegistrationsResolve() { + public void TheRegistrationsResolve() + { var (_, assembly) = Run( """ [DependencyModule] @@ -145,11 +150,12 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { .AsScoped(); } } - """); + """ + ); - var descriptor = assembly!.Services.First( - d => d.ServiceType.Name == "IHandler`2" && - d.ImplementationType!.Name == "CreateOrderHandler"); + var descriptor = assembly!.Services.First(d => + d.ServiceType.Name == "IHandler`2" && d.ImplementationType!.Name == "CreateOrderHandler" + ); var provider = assembly.BuildProvider(); @@ -161,7 +167,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// one that names none must not reach into the package. /// [Fact] - public void AConventionSeesOneSourceOnly() { + public void AConventionSeesOneSourceOnly() + { var (_, assembly) = Run( """ public class LocalHandler : IHandler { } @@ -174,10 +181,11 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { .AsScoped(); } } - """); + """ + ); - var registered = assembly!.Services - .Where(d => d.ServiceType.Name == "IHandler`2") + var registered = assembly! + .Services.Where(d => d.ServiceType.Name == "IHandler`2") .Select(d => d.ImplementationType!.Name) .ToArray(); @@ -188,7 +196,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// Absent the call, a convention scans the compilation being built — unchanged behaviour. /// [Fact] - public void WithoutTheCallOnlyLocalTypesMatch() { + public void WithoutTheCallOnlyLocalTypesMatch() + { var (_, assembly) = Run( """ public class LocalHandler : IHandler { } @@ -199,10 +208,11 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { conventions.RegisterAll(typeof(IHandler<,>)).AsScoped(); } } - """); + """ + ); - var registered = assembly!.Services - .Where(d => d.ServiceType.Name == "IHandler`2") + var registered = assembly! + .Services.Where(d => d.ServiceType.Name == "IHandler`2") .Select(d => d.ImplementationType!.Name) .ToArray(); @@ -214,7 +224,8 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { /// the convention that asked for it rather than nowhere. /// [Fact] - public void DiagnosticsForMetadataMatchesReportAtTheConvention() { + public void DiagnosticsForMetadataMatchesReportAtTheConvention() + { var (result, _) = Run( """ [DependencyModule] @@ -226,23 +237,29 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { } } """, - compile: false); + compile: false + ); var exposures = result.GeneratorDiagnostics.Where(d => d.Id == "DM0010").ToArray(); Assert.NotEmpty(exposures); - Assert.All(exposures, diagnostic => { - Assert.NotEqual(Location.None, diagnostic.Location); - Assert.Contains("Test.cs", diagnostic.Location.GetLineSpan().Path); - }); + Assert.All( + exposures, + diagnostic => + { + Assert.NotEqual(Location.None, diagnostic.Location); + Assert.Contains("Test.cs", diagnostic.Location.GetLineSpan().Path); + } + ); } /// /// Filters apply to metadata types the same way they apply to local ones. /// [Fact] - public void FiltersApplyToMetadataTypes() { + public void FiltersApplyToMetadataTypes() + { var (_, assembly) = Run( """ [DependencyModule] @@ -254,10 +271,11 @@ void IConventionModule.Conventions(IConventionDefinitions conventions) { .AsScoped(); } } - """); + """ + ); - var registered = assembly!.Services - .Where(d => d.ServiceType.Name == "IHandler`2") + var registered = assembly! + .Services.Where(d => d.ServiceType.Name == "IHandler`2") .Select(d => d.ImplementationType!.Name) .ToArray(); diff --git a/tests/DependencyModules.Tests/GeneratorTests/RobustnessTests.cs b/tests/DependencyModules.Tests/GeneratorTests/RobustnessTests.cs index 864aa0f..0b4c37e 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/RobustnessTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/RobustnessTests.cs @@ -14,8 +14,8 @@ namespace DependencyModules.Tests.GeneratorTests; /// Each of these is something a real application does and something the generator could plausibly /// lose quietly — a registration that never happens reads exactly like one that was never asked for. /// -public class RobustnessTests { - +public class RobustnessTests +{ private static string Call(object target, string method) => (string)target.GetType().GetMethod(method)!.Invoke(target, null)!; @@ -26,7 +26,8 @@ private static string Call(object target, string method) => /// singleton, which is the kind of thing found much later. /// [Fact] - public void Module_AddedTwice_RegistersItsServicesOnce() { + public void Module_AddedTwice_RegistersItsServicesOnce() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -40,15 +41,18 @@ public class Greeter : IGreeter { public string Greet() => "hello"; } [DependencyModule] public partial class TestModule; - """); + """ + ); - var module = (IDependencyModule)System.Activator.CreateInstance(assembly.Type("TestModule"))!; + var module = (IDependencyModule) + System.Activator.CreateInstance(assembly.Type("TestModule"))!; var services = new ServiceCollection(); services.AddModules(module, module); Assert.Single( - services.BuildServiceProvider().GetServices(assembly.Type("IGreeter")).Cast()); + services.BuildServiceProvider().GetServices(assembly.Type("IGreeter")).Cast() + ); } /// Two environment conditions on one service combine with and. @@ -57,8 +61,11 @@ public partial class TestModule; [InlineData("Development", false, 0)] [InlineData("Production", true, 0)] public void Service_WithTwoConditions_RegistersOnlyWhenBothHold( - string environment, bool flag, int expected) { - + string environment, + bool flag, + int expected + ) + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -77,16 +84,22 @@ public partial class TestModule; """, environment: new ModuleEnvironment( environment, - flag ? new Dictionary { ["feature"] = "on" } : new Dictionary())); + flag + ? new Dictionary { ["feature"] = "on" } + : new Dictionary() + ) + ); Assert.Equal( expected, - assembly.BuildProvider().GetServices(assembly.Type("IGreeter")).Cast().Count()); + assembly.BuildProvider().GetServices(assembly.Type("IGreeter")).Cast().Count() + ); } /// An explicit service attribute wins over a convention that also matches. [Fact] - public void Convention_DoesNotAlsoRegisterAnAttributedType() { + public void Convention_DoesNotAlsoRegisterAnAttributedType() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -105,15 +118,18 @@ public void Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); Assert.Single( - assembly.BuildProvider().GetServices(assembly.Type("IGreeter")).Cast()); + assembly.BuildProvider().GetServices(assembly.Type("IGreeter")).Cast() + ); } /// A keyed and an unkeyed registration of one service coexist. [Fact] - public void Service_KeyedAndUnkeyed_AreBothResolvable() { + public void Service_KeyedAndUnkeyed_AreBothResolvable() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -130,7 +146,8 @@ public class Loud : IGreeter { public string Greet() => "LOUD"; } [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = assembly.BuildProvider(); var greeter = assembly.Type("IGreeter"); @@ -141,7 +158,8 @@ public partial class TestModule; /// A cross-wired generic service shares one instance across its interfaces. [Fact] - public void CrossWire_SharesOneInstanceAcrossServiceTypes() { + public void CrossWire_SharesOneInstanceAcrossServiceTypes() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -160,18 +178,21 @@ public class Store : IReader, IWriter { [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = assembly.BuildProvider(); Assert.Equal( Call(provider.GetRequiredService(assembly.Type("IReader")), "Read"), - Call(provider.GetRequiredService(assembly.Type("IWriter")), "Write")); + Call(provider.GetRequiredService(assembly.Type("IWriter")), "Write") + ); } /// A convention excluding a namespace does not register from it. [Fact] - public void Convention_NotInNamespaces_ExcludesThatNamespace() { + public void Convention_NotInNamespaces_ExcludesThatNamespace() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -197,17 +218,23 @@ public class Kept : TestNamespace.IGreeter { public string Greet() => "kept"; } namespace TestNamespace.Excluded { public class Dropped : TestNamespace.IGreeter { public string Greet() => "dropped"; } } - """); + """ + ); - var all = assembly.BuildProvider().GetServices(assembly.Type("IGreeter")) - .Cast().Select(g => Call(g, "Greet")).ToArray(); + var all = assembly + .BuildProvider() + .GetServices(assembly.Type("IGreeter")) + .Cast() + .Select(g => Call(g, "Greet")) + .ToArray(); Assert.Equal(["kept"], all); } /// An interceptor sees an async method through to its result. [Fact] - public async Task Interceptor_OverAnAsyncMethod() { + public async Task Interceptor_OverAnAsyncMethod() + { var assembly = GeneratedAssembly.Create( """ using System.Threading.Tasks; @@ -238,7 +265,8 @@ public async ValueTask InterceptAsync( [DependencyModule] public partial class TestModule; - """); + """ + ); var fetcher = assembly.BuildProvider().GetRequiredService(assembly.Type("IFetcher")); @@ -250,7 +278,8 @@ public partial class TestModule; /// A service depending on a collection of a service gets every registration. [Fact] - public void Service_DependingOnAnEnumerableOfAService_GetsAllOfThem() { + public void Service_DependingOnAnEnumerableOfAService_GetsAllOfThem() + { var assembly = GeneratedAssembly.Create( """ using System.Collections.Generic; @@ -274,10 +303,13 @@ public class Engine(IEnumerable rules) { [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("first,second", Call( - assembly.BuildProvider().GetRequiredService(assembly.Type("Engine")), "Describe")); + Assert.Equal( + "first,second", + Call(assembly.BuildProvider().GetRequiredService(assembly.Type("Engine")), "Describe") + ); } // ------------------------------------------------------------------------------------------ @@ -287,7 +319,8 @@ public partial class TestModule; /// An explicitly named service type is the one registered. [Fact] - public void Service_WithAnExplicitServiceType_RegistersThatOne() { + public void Service_WithAnExplicitServiceType_RegistersThatOne() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -305,7 +338,8 @@ public class Store : IReader, IWriter { [DependencyModule] public partial class TestModule; - """); + """ + ); var provider = assembly.BuildProvider(); @@ -322,7 +356,8 @@ public partial class TestModule; /// also a service is a policy question; what must not happen is the declared one going missing. /// [Fact] - public void Service_ImplementingADerivedInterface_RegistersTheDeclaredOne() { + public void Service_ImplementingADerivedInterface_RegistersTheDeclaredOne() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -337,15 +372,19 @@ public class Store : IAudited { public string Read() => "read"; } [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("read", Call( - assembly.BuildProvider().GetRequiredService(assembly.Type("IAudited")), "Read")); + Assert.Equal( + "read", + Call(assembly.BuildProvider().GetRequiredService(assembly.Type("IAudited")), "Read") + ); } /// A generic implementation registers as the open generic it closes nothing of. [Fact] - public void Service_GenericImplementation_RegistersAsAnOpenGeneric() { + public void Service_GenericImplementation_RegistersAsAnOpenGeneric() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -359,7 +398,8 @@ public class Repo : IRepo { public string Name() => "repo"; } [DependencyModule] public partial class TestModule; - """); + """ + ); var closed = assembly.Type("IRepo`1").MakeGenericType(typeof(string)); @@ -372,7 +412,8 @@ public partial class TestModule; /// from the declaration responsible. /// [Fact] - public void Service_ThatIsAbstract_IsReported() { + public void Service_ThatIsAbstract_IsReported() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -386,7 +427,8 @@ public abstract class Greeter : IGreeter { public abstract string Greet(); } [DependencyModule] public partial class TestModule; - """); + """ + ); Assert.Empty(result.Errors); Assert.Contains(result.GeneratorDiagnostics, d => d.Id == "DM0002"); @@ -394,7 +436,8 @@ public partial class TestModule; /// A record registers like any other class. [Fact] - public void Service_DeclaredAsARecord() { + public void Service_DeclaredAsARecord() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -408,15 +451,19 @@ public record Greeter : IGreeter { public string Greet() => "hello"; } [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("hello", Call( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "hello", + Call(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// A service nested inside another type registers under its nested name. [Fact] - public void Service_NestedInsideAnotherType() { + public void Service_NestedInsideAnotherType() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -432,15 +479,19 @@ public class Greeter : IGreeter { public string Greet() => "hello"; } [DependencyModule] public partial class TestModule; - """); + """ + ); - Assert.Equal("hello", Call( - assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "hello", + Call(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); } /// Replace leaves one registration standing, not two. [Fact] - public void Service_RegisteredWithReplace_LeavesOne() { + public void Service_RegisteredWithReplace_LeavesOne() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -457,17 +508,23 @@ public class Second : IGreeter { public string Greet() => "second"; } [DependencyModule] public partial class TestModule; - """); + """ + ); - var all = assembly.BuildProvider().GetServices(assembly.Type("IGreeter")) - .Cast().Select(g => Call(g, "Greet")).ToArray(); + var all = assembly + .BuildProvider() + .GetServices(assembly.Type("IGreeter")) + .Cast() + .Select(g => Call(g, "Greet")) + .ToArray(); Assert.Equal(["second"], all); } /// A service whose only constructor is private is reported rather than registered. [Fact] - public void Service_WithNoAccessibleConstructor_IsReported() { + public void Service_WithNoAccessibleConstructor_IsReported() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -488,7 +545,8 @@ public void Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().AsSingleton(); } } - """); + """ + ); Assert.Contains(result.GeneratorDiagnostics, d => d.Id == "DM0006"); } @@ -499,8 +557,7 @@ public void Conventions(IConventionDefinitions conventions) { // refused with a diagnostic rather than mis-generated; these check both halves. // ------------------------------------------------------------------------------------------ - private const string InterceptorPreamble = - """ + private const string InterceptorPreamble = """ using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -522,128 +579,164 @@ public TResult Intercept(InvocationContext context) { """; private static GeneratorResult RunIntercepted(string body) => - GeneratorTestHarness.Run(InterceptorPreamble + body + """ + GeneratorTestHarness.Run( + InterceptorPreamble + + body + + """ - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); /// A void method is forwarded. [Fact] - public void Interceptor_OverAVoidMethod() { - Assert.Empty(RunIntercepted( - """ - public interface IWorker { void Work(); } + public void Interceptor_OverAVoidMethod() + { + Assert.Empty( + RunIntercepted( + """ + public interface IWorker { void Work(); } - [SingletonService] - [Intercept(typeof(CountingInterceptor))] - public class Worker : IWorker { public void Work() { } } - """).Errors); + [SingletonService] + [Intercept(typeof(CountingInterceptor))] + public class Worker : IWorker { public void Work() { } } + """ + ).Errors + ); } /// A property is forwarded. [Fact] - public void Interceptor_OverAProperty() { - Assert.Empty(RunIntercepted( - """ - public interface IWorker { string Name { get; set; } } + public void Interceptor_OverAProperty() + { + Assert.Empty( + RunIntercepted( + """ + public interface IWorker { string Name { get; set; } } - [SingletonService] - [Intercept(typeof(CountingInterceptor))] - public class Worker : IWorker { public string Name { get; set; } = ""; } - """).Errors); + [SingletonService] + [Intercept(typeof(CountingInterceptor))] + public class Worker : IWorker { public string Name { get; set; } = ""; } + """ + ).Errors + ); } /// An indexer is forwarded. [Fact] - public void Interceptor_OverAnIndexer() { - Assert.Empty(RunIntercepted( - """ - public interface IWorker { string this[int index] { get; set; } } + public void Interceptor_OverAnIndexer() + { + Assert.Empty( + RunIntercepted( + """ + public interface IWorker { string this[int index] { get; set; } } - [SingletonService] - [Intercept(typeof(CountingInterceptor))] - public class Worker : IWorker { - public string this[int index] { get => ""; set { } } - } - """).Errors); + [SingletonService] + [Intercept(typeof(CountingInterceptor))] + public class Worker : IWorker { + public string this[int index] { get => ""; set { } } + } + """ + ).Errors + ); } /// An event is forwarded. [Fact] - public void Interceptor_OverAnEvent() { - Assert.Empty(RunIntercepted( - """ - public interface IWorker { event EventHandler? Done; } + public void Interceptor_OverAnEvent() + { + Assert.Empty( + RunIntercepted( + """ + public interface IWorker { event EventHandler? Done; } - [SingletonService] - [Intercept(typeof(CountingInterceptor))] - public class Worker : IWorker { public event EventHandler? Done; } - """).Errors); + [SingletonService] + [Intercept(typeof(CountingInterceptor))] + public class Worker : IWorker { public event EventHandler? Done; } + """ + ).Errors + ); } /// A generic method is forwarded with its type parameters. [Fact] - public void Interceptor_OverAGenericMethod() { - Assert.Empty(RunIntercepted( - """ - public interface IWorker { T Echo(T value); } + public void Interceptor_OverAGenericMethod() + { + Assert.Empty( + RunIntercepted( + """ + public interface IWorker { T Echo(T value); } - [SingletonService] - [Intercept(typeof(CountingInterceptor))] - public class Worker : IWorker { public T Echo(T value) => value; } - """).Errors); + [SingletonService] + [Intercept(typeof(CountingInterceptor))] + public class Worker : IWorker { public T Echo(T value) => value; } + """ + ).Errors + ); } /// Default values and params survive forwarding. [Fact] - public void Interceptor_OverDefaultAndParamsArguments() { - Assert.Empty(RunIntercepted( - """ - public interface IWorker { string Join(string separator = ",", params string[] parts); } + public void Interceptor_OverDefaultAndParamsArguments() + { + Assert.Empty( + RunIntercepted( + """ + public interface IWorker { string Join(string separator = ",", params string[] parts); } - [SingletonService] - [Intercept(typeof(CountingInterceptor))] - public class Worker : IWorker { - public string Join(string separator = ",", params string[] parts) => - string.Join(separator, parts); - } - """).Errors); + [SingletonService] + [Intercept(typeof(CountingInterceptor))] + public class Worker : IWorker { + public string Join(string separator = ",", params string[] parts) => + string.Join(separator, parts); + } + """ + ).Errors + ); } /// Members inherited from a base interface are forwarded too. [Fact] - public void Interceptor_OverAnInheritedInterfaceMember() { - Assert.Empty(RunIntercepted( - """ - public interface IBase { string Read(); } - public interface IWorker : IBase { string Write(); } + public void Interceptor_OverAnInheritedInterfaceMember() + { + Assert.Empty( + RunIntercepted( + """ + public interface IBase { string Read(); } + public interface IWorker : IBase { string Write(); } - [SingletonService] - [Intercept(typeof(CountingInterceptor))] - public class Worker : IWorker { - public string Read() => "read"; - public string Write() => "write"; - } - """).Errors); + [SingletonService] + [Intercept(typeof(CountingInterceptor))] + public class Worker : IWorker { + public string Read() => "read"; + public string Write() => "write"; + } + """ + ).Errors + ); } /// An IAsyncEnumerable member is forwarded. [Fact] - public void Interceptor_OverAnAsyncEnumerable() { - Assert.Empty(RunIntercepted( - """ - public interface IWorker { IAsyncEnumerable StreamAsync(); } + public void Interceptor_OverAnAsyncEnumerable() + { + Assert.Empty( + RunIntercepted( + """ + public interface IWorker { IAsyncEnumerable StreamAsync(); } - [SingletonService] - [Intercept(typeof(CountingInterceptor))] - public class Worker : IWorker { - public async IAsyncEnumerable StreamAsync() { - await Task.Yield(); - yield return "one"; + [SingletonService] + [Intercept(typeof(CountingInterceptor))] + public class Worker : IWorker { + public async IAsyncEnumerable StreamAsync() { + await Task.Yield(); + yield return "one"; + } } - } - """).Errors); + """ + ).Errors + ); } /// @@ -655,7 +748,8 @@ public async IAsyncEnumerable StreamAsync() { /// the generated wrapper is not. /// [Fact] - public void Interceptor_OverRefAndOutParameters_IsForwardedOrRefused() { + public void Interceptor_OverRefAndOutParameters_IsForwardedOrRefused() + { var result = RunIntercepted( """ public interface IWorker { bool TryRead(out string value); } @@ -665,7 +759,8 @@ public interface IWorker { bool TryRead(out string value); } public class Worker : IWorker { public bool TryRead(out string value) { value = "read"; return true; } } - """); + """ + ); Assert.Empty(result.Errors); } @@ -675,8 +770,7 @@ public class Worker : IWorker { // arguments, replacing results, short-circuiting, retrying, and observing failures. // ------------------------------------------------------------------------------------------ - private const string ArgumentPreamble = - """ + private const string ArgumentPreamble = """ using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -693,23 +787,41 @@ public static class Log { """; private static object Resolve(string body, string serviceName) => - GeneratedAssembly.Create(ArgumentPreamble + body + """ - - [DependencyModule] - public partial class TestModule; - """).BuildProvider().GetRequiredService( - GeneratedAssembly.Create(ArgumentPreamble + body + """ - - [DependencyModule] - public partial class TestModule; - """).Type(serviceName)); + GeneratedAssembly + .Create( + ArgumentPreamble + + body + + """ + + [DependencyModule] + public partial class TestModule; + """ + ) + .BuildProvider() + .GetRequiredService( + GeneratedAssembly + .Create( + ArgumentPreamble + + body + + """ + + [DependencyModule] + public partial class TestModule; + """ + ) + .Type(serviceName) + ); private static GeneratedAssembly Build(string body) => - GeneratedAssembly.Create(ArgumentPreamble + body + """ + GeneratedAssembly.Create( + ArgumentPreamble + + body + + """ - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); private static object Invoke(object target, string method, params object?[] arguments) => target.GetType().GetMethod(method)!.Invoke(target, arguments)!; @@ -721,7 +833,8 @@ private static object Invoke(object target, string method, params object?[] argu /// original — with nothing to show for it. /// [Fact] - public void Interceptor_CanReplaceAnArgument() { + public void Interceptor_CanReplaceAnArgument() + { var assembly = Build( """ public interface IGreeter { string Greet(string name); } @@ -737,16 +850,23 @@ public TResult Intercept(InvocationContext context) { return context.Proceed(); } } - """); + """ + ); Assert.Equal( "hello replaced", - Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet", "original")); + Invoke( + assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), + "Greet", + "original" + ) + ); } /// Arguments carry the names they were declared with. [Fact] - public void Interceptor_SeesArgumentNamesAndCount() { + public void Interceptor_SeesArgumentNamesAndCount() + { var assembly = Build( """ public interface IGreeter { string Greet(string name, int times); } @@ -764,18 +884,26 @@ public TResult Intercept(InvocationContext context) { return context.Proceed(); } } - """); + """ + ); - Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet", "ian", 2); + Invoke( + assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), + "Greet", + "ian", + 2 + ); Assert.Equal( ["name=ian", "times=2"], - (List)assembly.Type("Log").GetField("Lines")!.GetValue(null)!); + (List)assembly.Type("Log").GetField("Lines")!.GetValue(null)! + ); } /// An interceptor that never proceeds returns its own result. [Fact] - public void Interceptor_CanShortCircuitWithoutProceeding() { + public void Interceptor_CanShortCircuitWithoutProceeding() + { var assembly = Build( """ public interface IGreeter { string Greet(); } @@ -791,18 +919,21 @@ public class CachingInterceptor : IInterceptor { public TResult Intercept(InvocationContext context) => (TResult)(object)"cached"; } - """); + """ + ); Assert.Equal( "cached", - Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); Assert.Equal(0, (int)assembly.Type("Log").GetField("Calls")!.GetValue(null)!); } /// Proceeding twice runs the implementation twice — the retry shape. [Fact] - public void Interceptor_CanProceedMoreThanOnce() { + public void Interceptor_CanProceedMoreThanOnce() + { var assembly = Build( """ public interface IGreeter { string Greet(); } @@ -820,18 +951,21 @@ public TResult Intercept(InvocationContext context) { return context.Proceed(); } } - """); + """ + ); Assert.Equal( "call2", - Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); Assert.Equal(2, (int)assembly.Type("Log").GetField("Calls")!.GetValue(null)!); } /// An interceptor observes an exception the implementation throws. [Fact] - public void Interceptor_SeesAnExceptionFromTheImplementation() { + public void Interceptor_SeesAnExceptionFromTheImplementation() + { var assembly = Build( """ public interface IGreeter { string Greet(); } @@ -852,18 +986,24 @@ public TResult Intercept(InvocationContext context) { } } } - """); + """ + ); Assert.Equal( "recovered", - Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet") + ); - Assert.Equal(["boom"], (List)assembly.Type("Log").GetField("Lines")!.GetValue(null)!); + Assert.Equal( + ["boom"], + (List)assembly.Type("Log").GetField("Lines")!.GetValue(null)! + ); } /// Two interceptors nest in declaration order, outermost first. [Fact] - public void Interceptors_NestInDeclarationOrder() { + public void Interceptors_NestInDeclarationOrder() + { var assembly = Build( """ public interface IGreeter { string Greet(); } @@ -890,18 +1030,21 @@ public TResult Intercept(InvocationContext context) { return context.Proceed(); } } - """); + """ + ); Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet"); Assert.Equal( ["outer", "inner", "impl"], - (List)assembly.Type("Log").GetField("Lines")!.GetValue(null)!); + (List)assembly.Type("Log").GetField("Lines")!.GetValue(null)! + ); } /// An async interceptor can rewrite an argument and replace the result. [Fact] - public async Task AsyncInterceptor_CanReplaceArgumentsAndResult() { + public async Task AsyncInterceptor_CanReplaceArgumentsAndResult() + { var assembly = Build( """ public interface IFetcher { Task FetchAsync(string key); } @@ -925,7 +1068,8 @@ public async ValueTask InterceptAsync( return (TResult)(object)((string)(object)result! + ":seen"); } } - """); + """ + ); var fetcher = assembly.BuildProvider().GetRequiredService(assembly.Type("IFetcher")); var task = (Task)Invoke(fetcher, "FetchAsync", "original"); @@ -935,7 +1079,8 @@ public async ValueTask InterceptAsync( /// An async interceptor can short-circuit without awaiting the implementation. [Fact] - public async Task AsyncInterceptor_CanShortCircuit() { + public async Task AsyncInterceptor_CanShortCircuit() + { var assembly = Build( """ public interface IFetcher { Task FetchAsync(); } @@ -956,7 +1101,8 @@ public ValueTask InterceptAsync( AsyncInvocationContext context) => new ValueTask((TResult)(object)"cached"); } - """); + """ + ); var fetcher = assembly.BuildProvider().GetRequiredService(assembly.Type("IFetcher")); @@ -966,7 +1112,8 @@ public ValueTask InterceptAsync( /// A stream interceptor can replace the items the implementation yields. [Fact] - public async Task StreamInterceptor_CanReplaceTheYieldedItems() { + public async Task StreamInterceptor_CanReplaceTheYieldedItems() + { var assembly = Build( """ public interface IStreamer { IAsyncEnumerable StreamAsync(string prefix); } @@ -994,13 +1141,15 @@ public async IAsyncEnumerable InterceptStream( } } } - """); + """ + ); var streamer = assembly.BuildProvider().GetRequiredService(assembly.Type("IStreamer")); var stream = (IAsyncEnumerable)Invoke(streamer, "StreamAsync", "original"); var seen = new List(); - await foreach (var item in stream) { + await foreach (var item in stream) + { seen.Add(item); } @@ -1014,7 +1163,8 @@ public async IAsyncEnumerable InterceptStream( /// unboxed back to the parameter's type on the way out. /// [Fact] - public void Interceptor_CanReplaceAValueTypeArgument() { + public void Interceptor_CanReplaceAValueTypeArgument() + { var assembly = Build( """ public interface ICounter { int Add(int value); } @@ -1030,11 +1180,13 @@ public TResult Intercept(InvocationContext context) { return context.Proceed(); } } - """); + """ + ); Assert.Equal( 41, - Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("ICounter")), "Add", 4)); + Invoke(assembly.BuildProvider().GetRequiredService(assembly.Type("ICounter")), "Add", 4) + ); } // ------------------------------------------------------------------------------------------ @@ -1048,7 +1200,8 @@ private static object Instance(GeneratedAssembly assembly, string moduleName) => /// Two modules compose, and each contributes its own registrations. [Fact] - public void Modules_ComposedTogether_BothContribute() { + public void Modules_ComposedTogether_BothContribute() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1070,13 +1223,15 @@ public partial class ReadModule; [DependencyModule(OnlyRealm = true)] public partial class WriteModule; """, - moduleName: "ReadModule"); + moduleName: "ReadModule" + ); var services = new ServiceCollection(); services.AddModules( (IDependencyModule)Instance(assembly, "ReadModule"), - (IDependencyModule)Instance(assembly, "WriteModule")); + (IDependencyModule)Instance(assembly, "WriteModule") + ); var provider = services.BuildServiceProvider(); @@ -1092,7 +1247,8 @@ public partial class WriteModule; [Theory] [InlineData(true)] [InlineData(false)] - public void Modules_CrossModuleDependency_ResolvesInEitherOrder(bool readFirst) { + public void Modules_CrossModuleDependency_ResolvesInEitherOrder(bool readFirst) + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1115,7 +1271,8 @@ public partial class ReadModule; [DependencyModule(OnlyRealm = true)] public partial class UseModule; """, - moduleName: "ReadModule"); + moduleName: "ReadModule" + ); var read = (IDependencyModule)Instance(assembly, "ReadModule"); var use = (IDependencyModule)Instance(assembly, "UseModule"); @@ -1123,8 +1280,13 @@ public partial class UseModule; var services = new ServiceCollection(); services.AddModules(readFirst ? new[] { read, use } : new[] { use, read }); - Assert.Equal("using:read", Call( - services.BuildServiceProvider().GetRequiredService(assembly.Type("Consumer")), "Describe")); + Assert.Equal( + "using:read", + Call( + services.BuildServiceProvider().GetRequiredService(assembly.Type("Consumer")), + "Describe" + ) + ); } /// Two equal instances of one module register its services once. @@ -1133,7 +1295,8 @@ public partial class UseModule; /// arriving twice is normal. Registering twice gives two instances behind one singleton. /// [Fact] - public void Module_ArrivingTwiceAsSeparateInstances_RegistersOnce() { + public void Module_ArrivingTwiceAsSeparateInstances_RegistersOnce() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1147,21 +1310,25 @@ public class Greeter : IGreeter { public string Greet() => "hello"; } [DependencyModule] public partial class TestModule; - """); + """ + ); var services = new ServiceCollection(); services.AddModules( (IDependencyModule)Instance(assembly, "TestModule"), - (IDependencyModule)Instance(assembly, "TestModule")); + (IDependencyModule)Instance(assembly, "TestModule") + ); Assert.Single( - services.BuildServiceProvider().GetServices(assembly.Type("IGreeter")).Cast()); + services.BuildServiceProvider().GetServices(assembly.Type("IGreeter")).Cast() + ); } /// A realm-only module contributes nothing to a composition it is not part of. [Fact] - public void Module_RealmOnly_DoesNotLeakIntoAnotherComposition() { + public void Module_RealmOnly_DoesNotLeakIntoAnotherComposition() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1179,13 +1346,15 @@ public partial class HiddenModule; [DependencyModule(OnlyRealm = true)] public partial class PlainModule; """, - moduleName: "PlainModule"); + moduleName: "PlainModule" + ); var services = new ServiceCollection(); services.AddModules((IDependencyModule)Instance(assembly, "PlainModule")); Assert.Empty( - services.BuildServiceProvider().GetServices(assembly.Type("IGreeter")).Cast()); + services.BuildServiceProvider().GetServices(assembly.Type("IGreeter")).Cast() + ); } /// A decorator in one module wraps a service another module registered. @@ -1195,7 +1364,8 @@ public partial class PlainModule; /// open, that phase ordering still has to hold. /// [Fact] - public void Module_DecoratesAServiceAnotherModuleRegistered() { + public void Module_DecoratesAServiceAnotherModuleRegistered() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1218,16 +1388,23 @@ public partial class ServiceModule; [DependencyModule(OnlyRealm = true)] public partial class DecoratorModule; """, - moduleName: "ServiceModule"); + moduleName: "ServiceModule" + ); var services = new ServiceCollection(); services.AddModules( (IDependencyModule)Instance(assembly, "ServiceModule"), - (IDependencyModule)Instance(assembly, "DecoratorModule")); + (IDependencyModule)Instance(assembly, "DecoratorModule") + ); - Assert.Equal("HELLO", Call( - services.BuildServiceProvider().GetRequiredService(assembly.Type("IGreeter")), "Greet")); + Assert.Equal( + "HELLO", + Call( + services.BuildServiceProvider().GetRequiredService(assembly.Type("IGreeter")), + "Greet" + ) + ); } // ------------------------------------------------------------------------------------------ @@ -1245,12 +1422,15 @@ namespace TestNamespace; public interface IGreeter { string Greet(); } - """ + body + """ + """ + + body + + """ - [DependencyModule] - public partial class TestModule; - """, - environment: environment); + [DependencyModule] + public partial class TestModule; + """, + environment: environment + ); private static int Count(GeneratedAssembly assembly) => assembly.BuildProvider().GetServices(assembly.Type("IGreeter")).Cast().Count(); @@ -1259,14 +1439,21 @@ private static int Count(GeneratedAssembly assembly) => [Theory] [InlineData("Production", 0)] [InlineData("Development", 1)] - public void Condition_IfNotEnvironment(string environment, int expected) { - Assert.Equal(expected, Count(WithEnvironment( - """ - [SingletonService] - [IfNotEnvironment("Production")] - public class Greeter : IGreeter { public string Greet() => "hello"; } - """, - new ModuleEnvironment(environment)))); + public void Condition_IfNotEnvironment(string environment, int expected) + { + Assert.Equal( + expected, + Count( + WithEnvironment( + """ + [SingletonService] + [IfNotEnvironment("Production")] + public class Greeter : IGreeter { public string Greet() => "hello"; } + """, + new ModuleEnvironment(environment) + ) + ) + ); } /// One condition listing several names matches any of them. @@ -1274,47 +1461,71 @@ public class Greeter : IGreeter { public string Greet() => "hello"; } [InlineData("Development", 1)] [InlineData("Staging", 1)] [InlineData("Production", 0)] - public void Condition_IfEnvironment_WithSeveralNames(string environment, int expected) { - Assert.Equal(expected, Count(WithEnvironment( - """ - [SingletonService] - [IfEnvironment("Development", "Staging")] - public class Greeter : IGreeter { public string Greet() => "hello"; } - """, - new ModuleEnvironment(environment)))); + public void Condition_IfEnvironment_WithSeveralNames(string environment, int expected) + { + Assert.Equal( + expected, + Count( + WithEnvironment( + """ + [SingletonService] + [IfEnvironment("Development", "Staging")] + public class Greeter : IGreeter { public string Greet() => "hello"; } + """, + new ModuleEnvironment(environment) + ) + ) + ); } /// A value condition with no expected value tests only that the key is present. [Theory] [InlineData(true, 1)] [InlineData(false, 0)] - public void Condition_IfEnvironmentValue_KeyPresence(bool present, int expected) { - Assert.Equal(expected, Count(WithEnvironment( - """ - [SingletonService] - [IfEnvironmentValue("feature")] - public class Greeter : IGreeter { public string Greet() => "hello"; } - """, - new ModuleEnvironment( - "Development", - present - ? new Dictionary { ["feature"] = "anything" } - : new Dictionary())))); + public void Condition_IfEnvironmentValue_KeyPresence(bool present, int expected) + { + Assert.Equal( + expected, + Count( + WithEnvironment( + """ + [SingletonService] + [IfEnvironmentValue("feature")] + public class Greeter : IGreeter { public string Greet() => "hello"; } + """, + new ModuleEnvironment( + "Development", + present + ? new Dictionary { ["feature"] = "anything" } + : new Dictionary() + ) + ) + ) + ); } /// IfNotEnvironmentValue is the negation. [Theory] [InlineData("on", 0)] [InlineData("off", 1)] - public void Condition_IfNotEnvironmentValue(string value, int expected) { - Assert.Equal(expected, Count(WithEnvironment( - """ - [SingletonService] - [IfNotEnvironmentValue("feature", "on")] - public class Greeter : IGreeter { public string Greet() => "hello"; } - """, - new ModuleEnvironment( - "Development", new Dictionary { ["feature"] = value })))); + public void Condition_IfNotEnvironmentValue(string value, int expected) + { + Assert.Equal( + expected, + Count( + WithEnvironment( + """ + [SingletonService] + [IfNotEnvironmentValue("feature", "on")] + public class Greeter : IGreeter { public string Greet() => "hello"; } + """, + new ModuleEnvironment( + "Development", + new Dictionary { ["feature"] = value } + ) + ) + ) + ); } /// A condition on the convention and one on the class combine with and. @@ -1327,8 +1538,11 @@ public class Greeter : IGreeter { public string Greet() => "hello"; } [InlineData("Development", "off", 0)] [InlineData("Production", "on", 0)] public void Condition_OnConventionAndOnClass_BothMustHold( - string environment, string flag, int expected) { - + string environment, + string flag, + int expected + ) + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1349,14 +1563,22 @@ public void Conventions(IConventionDefinitions conventions) { } """, environment: new ModuleEnvironment( - environment, new Dictionary { ["feature"] = flag })); + environment, + new Dictionary { ["feature"] = flag } + ) + ); Assert.Equal(expected, Count(assembly)); } private static string[] Names(GeneratedAssembly assembly) => - assembly.BuildProvider().GetServices(assembly.Type("IGreeter")) - .Cast().Select(g => Call(g, "Greet")).OrderBy(n => n).ToArray(); + assembly + .BuildProvider() + .GetServices(assembly.Type("IGreeter")) + .Cast() + .Select(g => Call(g, "Greet")) + .OrderBy(n => n) + .ToArray(); private static GeneratedAssembly WithConvention(string chain) => GeneratedAssembly.Create( @@ -1378,29 +1600,37 @@ public void Conventions(IConventionDefinitions conventions) { conventions.RegisterAll()CHAIN.AsSingleton(); } } - """.Replace("CHAIN", chain)); + """.Replace("CHAIN", chain) + ); /// A name glob selects by the type's own name. [Fact] - public void Convention_WithName_SelectsByGlob() { + public void Convention_WithName_SelectsByGlob() + { Assert.Equal(["evening", "morning"], Names(WithConvention(""".WithName("*Greeter")"""))); } /// An excluding glob removes what it matches. [Fact] - public void Convention_WithoutName_ExcludesByGlob() { - Assert.Equal(["evening", "salutation"], Names(WithConvention(""".WithoutName("Morning*")"""))); + public void Convention_WithoutName_ExcludesByGlob() + { + Assert.Equal( + ["evening", "salutation"], + Names(WithConvention(""".WithoutName("Morning*")""")) + ); } /// A single-character wildcard matches exactly one character. [Fact] - public void Convention_WithName_SingleCharacterWildcard() { + public void Convention_WithName_SingleCharacterWildcard() + { Assert.Equal(["salutation"], Names(WithConvention(""".WithName("Salutatio?")"""))); } /// An attribute filter selects only what carries it. [Fact] - public void Convention_WithAttribute_SelectsOnlyMarkedTypes() { + public void Convention_WithAttribute_SelectsOnlyMarkedTypes() + { var assembly = GeneratedAssembly.Create( """ using System; @@ -1425,14 +1655,16 @@ public void Conventions(IConventionDefinitions conventions) { conventions.RegisterAll().WithAttribute().AsSingleton(); } } - """); + """ + ); Assert.Equal(["marked"], Names(assembly)); } /// An exact-namespace filter does not match a nested namespace. [Fact] - public void Convention_InExactNamespaces_DoesNotMatchNested() { + public void Convention_InExactNamespaces_DoesNotMatchNested() + { var assembly = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -1458,7 +1690,8 @@ public class Here : TestNamespace.IGreeter { public string Greet() => "here"; } namespace TestNamespace.Direct.Nested { public class Deeper : TestNamespace.IGreeter { public string Greet() => "deeper"; } } - """); + """ + ); Assert.Equal(["here"], Names(assembly)); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/ServiceOrderTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ServiceOrderTests.cs index 18dc3b9..2ed6e2b 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ServiceOrderTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ServiceOrderTests.cs @@ -18,18 +18,21 @@ namespace DependencyModules.Tests.GeneratorTests; /// The default is 0 for everything, and the sort is stable within an order, so a project that names /// no orders sees exactly what it saw before. /// -public class ServiceOrderTests { - +public class ServiceOrderTests +{ [Fact] - public void WithNoOrderNamed_TheExistingOrderIsKept() { + public void WithNoOrderNamed_TheExistingOrderIsKept() + { Assert.Equal(["Alpha", "Beta", "Gamma"], Resolve("", "", "")); } [Fact] - public void OrderDecidesTheSequence() { + public void OrderDecidesTheSequence() + { Assert.Equal( ["Gamma", "Beta", "Alpha"], - Resolve(", Order = 30", ", Order = 20", ", Order = 10")); + Resolve(", Order = 30", ", Order = 20", ", Order = 10") + ); } /// @@ -37,7 +40,8 @@ public void OrderDecidesTheSequence() { /// looks like when the rest of the project has never named an order. /// [Fact] - public void ANegativeOrder_SortsAhead() { + public void ANegativeOrder_SortsAhead() + { Assert.Equal(["Gamma", "Alpha", "Beta"], Resolve("", "", ", Order = -1")); } @@ -46,8 +50,12 @@ public void ANegativeOrder_SortsAhead() { /// not scramble the rest. /// [Fact] - public void WithinOneOrder_TheSortIsStable() { - Assert.Equal(["Alpha", "Beta", "Gamma"], Resolve(", Order = 5", ", Order = 5", ", Order = 5")); + public void WithinOneOrder_TheSortIsStable() + { + Assert.Equal( + ["Alpha", "Beta", "Gamma"], + Resolve(", Order = 5", ", Order = 5", ", Order = 5") + ); } /// @@ -55,7 +63,8 @@ public void WithinOneOrder_TheSortIsStable() { /// too — worth pinning, because it is the half a reader does not think about. /// [Fact] - public void TheLastInOrder_IsWhatASingleResolveReturns() { + public void TheLastInOrder_IsWhatASingleResolveReturns() + { var generated = Build(", Order = 30", ", Order = 20", ", Order = 10"); var resolved = generated.BuildProvider().GetService(generated.Type("IStep"))!; @@ -63,11 +72,16 @@ public void TheLastInOrder_IsWhatASingleResolveReturns() { Assert.Equal("Alpha", resolved.GetType().Name); } - private static string[] Resolve(string alpha, string beta, string gamma) { + private static string[] Resolve(string alpha, string beta, string gamma) + { var generated = Build(alpha, beta, gamma); - return ((System.Collections.IEnumerable)generated.BuildProvider() - .GetService(typeof(IEnumerable<>).MakeGenericType(generated.Type("IStep")))!) + return ( + (System.Collections.IEnumerable) + generated + .BuildProvider() + .GetService(typeof(IEnumerable<>).MakeGenericType(generated.Type("IStep")))! + ) .Cast() .Select(step => step.GetType().Name) .ToArray(); @@ -76,22 +90,23 @@ private static string[] Resolve(string alpha, string beta, string gamma) { private static GeneratedAssembly Build(string alpha, string beta, string gamma) => GeneratedAssembly.Create( $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - public interface IStep; + public interface IStep; - [SingletonService(As = typeof(IStep){{alpha}})] - public class Alpha : IStep; + [SingletonService(As = typeof(IStep){{alpha}})] + public class Alpha : IStep; - [SingletonService(As = typeof(IStep){{beta}})] - public class Beta : IStep; + [SingletonService(As = typeof(IStep){{beta}})] + public class Beta : IStep; - [SingletonService(As = typeof(IStep){{gamma}})] - public class Gamma : IStep; + [SingletonService(As = typeof(IStep){{gamma}})] + public class Gamma : IStep; - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); } diff --git a/tests/DependencyModules.Tests/GeneratorTests/ServiceRegistrationTests.cs b/tests/DependencyModules.Tests/GeneratorTests/ServiceRegistrationTests.cs index 39de7e3..ccf0866 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/ServiceRegistrationTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/ServiceRegistrationTests.cs @@ -8,35 +8,47 @@ namespace DependencyModules.Tests.GeneratorTests; /// These assert on the shape of the output rather than an exact snapshot, so they stay readable /// when unrelated parts of the generated file change. /// -public class ServiceRegistrationTests { - +public class ServiceRegistrationTests +{ [Fact] - public void SingletonService_EmitsAddSingleton() { - var result = GeneratorTestHarness.Run(Module("[SingletonService] public class Thing : IThing;")); + public void SingletonService_EmitsAddSingleton() + { + var result = GeneratorTestHarness.Run( + Module("[SingletonService] public class Thing : IThing;") + ); result.AssertNoErrors(); Assert.Contains("AddSingleton", result.SourceContaining("Dependencies")); } [Fact] - public void ScopedService_EmitsAddScoped() { - var result = GeneratorTestHarness.Run(Module("[ScopedService] public class Thing : IThing;")); + public void ScopedService_EmitsAddScoped() + { + var result = GeneratorTestHarness.Run( + Module("[ScopedService] public class Thing : IThing;") + ); result.AssertNoErrors(); Assert.Contains("AddScoped", result.SourceContaining("Dependencies")); } [Fact] - public void TransientService_EmitsAddTransient() { - var result = GeneratorTestHarness.Run(Module("[TransientService] public class Thing : IThing;")); + public void TransientService_EmitsAddTransient() + { + var result = GeneratorTestHarness.Run( + Module("[TransientService] public class Thing : IThing;") + ); result.AssertNoErrors(); Assert.Contains("AddTransient", result.SourceContaining("Dependencies")); } [Fact] - public void Service_RegistersImplementedInterfaceAsServiceType() { - var result = GeneratorTestHarness.Run(Module("[SingletonService] public class Thing : IThing;")); + public void Service_RegistersImplementedInterfaceAsServiceType() + { + var result = GeneratorTestHarness.Run( + Module("[SingletonService] public class Thing : IThing;") + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -46,9 +58,11 @@ public void Service_RegistersImplementedInterfaceAsServiceType() { } [Fact] - public void KeyedService_EmitsKeyedRegistration() { + public void KeyedService_EmitsKeyedRegistration() + { var result = GeneratorTestHarness.Run( - Module("""[SingletonService(Key = "the-key")] public class Thing : IThing;""")); + Module("""[SingletonService(Key = "the-key")] public class Thing : IThing;""") + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -58,12 +72,16 @@ public void KeyedService_EmitsKeyedRegistration() { } [Fact] - public void AsProperty_RegistersTheRequestedServiceType() { - var result = GeneratorTestHarness.Run(Module( - """ - public interface IOther; - [SingletonService(As = typeof(IOther))] public class Thing : IThing, IOther; - """)); + public void AsProperty_RegistersTheRequestedServiceType() + { + var result = GeneratorTestHarness.Run( + Module( + """ + public interface IOther; + [SingletonService(As = typeof(IOther))] public class Thing : IThing, IOther; + """ + ) + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -75,17 +93,25 @@ public interface IOther; [InlineData("RegistrationType.Try", "TryAddSingleton")] [InlineData("RegistrationType.TryEnumerable", "TryAddEnumerable")] [InlineData("RegistrationType.Replace", "Replace")] - public void UsingProperty_ChangesTheRegistrationMethod(string registrationType, string expectedCall) { + public void UsingProperty_ChangesTheRegistrationMethod( + string registrationType, + string expectedCall + ) + { var result = GeneratorTestHarness.Run( - Module($"[SingletonService(Using = {registrationType})] public class Thing : IThing;")); + Module($"[SingletonService(Using = {registrationType})] public class Thing : IThing;") + ); result.AssertNoErrors(); Assert.Contains(expectedCall, result.SourceContaining("Dependencies")); } [Fact] - public void CrossWireService_RegistersImplementationAndInterface() { - var result = GeneratorTestHarness.Run(Module("[CrossWireService] public class Thing : IThing;")); + public void CrossWireService_RegistersImplementationAndInterface() + { + var result = GeneratorTestHarness.Run( + Module("[CrossWireService] public class Thing : IThing;") + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -105,21 +131,23 @@ public void CrossWireService_RegistersImplementationAndInterface() { [Theory] [InlineData("[SingletonService]")] [InlineData("[SingletonServiceAttribute]")] - public void ServiceAttribute_IsMatchedHoweverItIsWritten(string attribute) { + public void ServiceAttribute_IsMatchedHoweverItIsWritten(string attribute) + { var generated = GeneratedAssembly.Create( $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - public interface IThing; + public interface IThing; - {{attribute}} - public class Thing : IThing; + {{attribute}} + public class Thing : IThing; - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); Assert.NotNull(generated.ResolveRequired("IThing")); } @@ -128,7 +156,8 @@ public partial class TestModule; /// And an attribute that merely shares a name is not one of ours. /// [Fact] - public void SameNamedAttributeFromAnotherNamespace_IsIgnored() { + public void SameNamedAttributeFromAnotherNamespace_IsIgnored() + { var generated = GeneratedAssembly.Create( """ using DependencyModules.Runtime.Attributes; @@ -146,13 +175,15 @@ public class Thing : IThing; [DependencyModule] public partial class TestModule; } - """); + """ + ); Assert.Empty(generated.Descriptors("IThing")); } [Fact] - public void OpenGenericService_RegistersOpenGenericTypes() { + public void OpenGenericService_RegistersOpenGenericTypes() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -166,7 +197,8 @@ public class GenericThing : IGeneric; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -176,7 +208,8 @@ public partial class TestModule; } [Fact] - public void StaticFactoryMethod_IsRegisteredAsAFactory() { + public void StaticFactoryMethod_IsRegisteredAsAFactory() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -192,7 +225,8 @@ public class Thing : IThing { [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -202,7 +236,8 @@ public partial class TestModule; } [Fact] - public void ModuleWithNoServices_DoesNotEmitADependenciesFile() { + public void ModuleWithNoServices_DoesNotEmitADependenciesFile() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -211,14 +246,16 @@ namespace TestNamespace; [DependencyModule] public partial class TestModule; - """); + """ + ); result.AssertNoErrors(); Assert.DoesNotContain(result.GeneratedSources.Keys, key => key.Contains("Dependencies")); } [Fact] - public void RecordModule_IsSupported() { + public void RecordModule_IsSupported() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -232,7 +269,8 @@ public class Thing : IThing; [DependencyModule] public partial record TestModule; - """); + """ + ); result.AssertNoErrors(); Assert.Contains("AddSingleton", result.SourceContaining("Dependencies")); @@ -243,14 +281,18 @@ public partial record TestModule; /// itself registered as IDisposable and was unreachable through the interface it existed for. /// [Fact] - public void CapabilityInterface_DoesNotWinOverTheServiceInterface() { - var result = GeneratorTestHarness.Run(Module( - """ - [SingletonService] - public class Thing : System.IDisposable, IThing { - public void Dispose() { } - } - """)); + public void CapabilityInterface_DoesNotWinOverTheServiceInterface() + { + var result = GeneratorTestHarness.Run( + Module( + """ + [SingletonService] + public class Thing : System.IDisposable, IThing { + public void Dispose() { } + } + """ + ) + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -260,14 +302,18 @@ public void Dispose() { } } [Fact] - public void CapabilityInterfaceAlone_RegistersAsSelf() { - var result = GeneratorTestHarness.Run(Module( - """ - [SingletonService] - public class Thing : System.IDisposable { - public void Dispose() { } - } - """)); + public void CapabilityInterfaceAlone_RegistersAsSelf() + { + var result = GeneratorTestHarness.Run( + Module( + """ + [SingletonService] + public class Thing : System.IDisposable { + public void Dispose() { } + } + """ + ) + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -277,16 +323,20 @@ public void Dispose() { } } [Fact] - public void CapabilityInterfaceThroughABaseClass_RegistersAsSelf() { - var result = GeneratorTestHarness.Run(Module( - """ - public abstract class DisposableBase : System.IDisposable { - public void Dispose() { } - } + public void CapabilityInterfaceThroughABaseClass_RegistersAsSelf() + { + var result = GeneratorTestHarness.Run( + Module( + """ + public abstract class DisposableBase : System.IDisposable { + public void Dispose() { } + } - [SingletonService] - public class Thing : DisposableBase; - """)); + [SingletonService] + public class Thing : DisposableBase; + """ + ) + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -301,15 +351,19 @@ public class Thing : DisposableBase; /// IHttpClientFactory are the same shape. /// [Fact] - public void FrameworkRoleInterface_IsStillTheServiceType() { - var result = GeneratorTestHarness.Run(Module( - """ - [SingletonService] - public class Thing : System.Collections.Generic.IEqualityComparer { - public bool Equals(IThing? a, IThing? b) => false; - public int GetHashCode(IThing o) => 0; - } - """)); + public void FrameworkRoleInterface_IsStillTheServiceType() + { + var result = GeneratorTestHarness.Run( + Module( + """ + [SingletonService] + public class Thing : System.Collections.Generic.IEqualityComparer { + public bool Equals(IThing? a, IThing? b) => false; + public int GetHashCode(IThing o) => 0; + } + """ + ) + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -318,14 +372,18 @@ public class Thing : System.Collections.Generic.IEqualityComparer { } [Fact] - public void CapabilityInterface_IsHonouredWhenNamedExplicitly() { - var result = GeneratorTestHarness.Run(Module( - """ - [SingletonService(As = typeof(System.IDisposable))] - public class Thing : System.IDisposable, IThing { - public void Dispose() { } - } - """)); + public void CapabilityInterface_IsHonouredWhenNamedExplicitly() + { + var result = GeneratorTestHarness.Run( + Module( + """ + [SingletonService(As = typeof(System.IDisposable))] + public class Thing : System.IDisposable, IThing { + public void Dispose() { } + } + """ + ) + ); result.AssertNoErrors(); var generated = result.SourceContaining("Dependencies"); @@ -335,15 +393,15 @@ public void Dispose() { } private static string Module(string body) => $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - public interface IThing; + public interface IThing; - {{body}} + {{body}} - [DependencyModule] - public partial class TestModule; - """; + [DependencyModule] + public partial class TestModule; + """; } diff --git a/tests/DependencyModules.Tests/GeneratorTests/TypeShapeTests.cs b/tests/DependencyModules.Tests/GeneratorTests/TypeShapeTests.cs index 18c47d4..b8e9d9c 100644 --- a/tests/DependencyModules.Tests/GeneratorTests/TypeShapeTests.cs +++ b/tests/DependencyModules.Tests/GeneratorTests/TypeShapeTests.cs @@ -8,10 +8,11 @@ namespace DependencyModules.Tests.GeneratorTests; /// shapes most likely to be written wrong: nullables, arrays, nested generics, and nested classes. /// Each case asserts the output still compiles, which is what a mis-rendered type breaks. /// -public class TypeShapeTests { - +public class TypeShapeTests +{ [Fact] - public void NullableConstructorParameter_Compiles() { + public void NullableConstructorParameter_Compiles() + { Generate( """ public interface IThing; @@ -20,11 +21,13 @@ public interface IThing; public class Thing : IThing { public Thing(string? optional) { } } - """); + """ + ); } [Fact] - public void NullableValueTypeParameter_Compiles() { + public void NullableValueTypeParameter_Compiles() + { Generate( """ public interface IThing; @@ -33,11 +36,13 @@ public interface IThing; public class Thing : IThing { public Thing(int? count) { } } - """); + """ + ); } [Fact] - public void ArrayParameter_Compiles() { + public void ArrayParameter_Compiles() + { Generate( """ public interface IDependency; @@ -50,11 +55,13 @@ public class Dependency : IDependency; public class Thing : IThing { public Thing(IDependency[] dependencies) { } } - """); + """ + ); } [Fact] - public void NestedGenericParameter_Compiles() { + public void NestedGenericParameter_Compiles() + { Generate( """ using System.Collections.Generic; @@ -69,11 +76,13 @@ public class Dependency : IDependency; public class Thing : IThing { public Thing(IEnumerable dependencies) { } } - """); + """ + ); } [Fact] - public void DeeplyNestedGenericParameter_Compiles() { + public void DeeplyNestedGenericParameter_Compiles() + { Generate( """ using System.Collections.Generic; @@ -84,44 +93,51 @@ public interface IThing; public class Thing : IThing { public Thing(IDictionary> map) { } } - """); + """ + ); } [Fact] - public void GenericServiceWithConstraints_Compiles() { + public void GenericServiceWithConstraints_Compiles() + { var generated = Generate( """ public interface IGeneric where T : class; [SingletonService] public class Generic : IGeneric where T : class; - """); + """ + ); Assert.Contains("IGeneric<>", generated); } [Fact] - public void GenericServiceWithSeveralParameters_Compiles() { + public void GenericServiceWithSeveralParameters_Compiles() + { var generated = Generate( """ public interface IPair; [SingletonService] public class Pair : IPair; - """); + """ + ); Assert.Contains("IPair<,>", generated); } [Fact] - public void ClosedGenericService_RegistersTheClosedType() { + public void ClosedGenericService_RegistersTheClosedType() + { var generated = Generate( """ public interface IGeneric; [SingletonService] public class StringGeneric : IGeneric; - """); + """ + ); Assert.Contains("string", generated); } @@ -131,7 +147,8 @@ public class StringGeneric : IGeneric; /// containing type, so the generated registration failed to compile with CS0234. /// [Fact] - public void NestedClassService_IsQualifiedByItsContainingType() { + public void NestedClassService_IsQualifiedByItsContainingType() + { var generated = Generate( """ public interface IThing; @@ -140,13 +157,15 @@ public static class Outer { [SingletonService] public class Inner : IThing; } - """); + """ + ); Assert.Contains("global::TestNamespace.Outer.Inner", generated); } [Fact] - public void DeeplyNestedClassService_IsQualifiedByEveryContainingType() { + public void DeeplyNestedClassService_IsQualifiedByEveryContainingType() + { var generated = Generate( """ public interface IThing; @@ -157,13 +176,15 @@ public static class Middle { public class Inner : IThing; } } - """); + """ + ); Assert.Contains("global::TestNamespace.Outer.Middle.Inner", generated); } [Fact] - public void NestedGenericService_IsQualifiedByItsContainingType() { + public void NestedGenericService_IsQualifiedByItsContainingType() + { var generated = Generate( """ public interface IGeneric; @@ -172,13 +193,15 @@ public static class Outer { [SingletonService] public class Inner : IGeneric; } - """); + """ + ); Assert.Contains("global::TestNamespace.Outer.Inner<>", generated); } [Fact] - public void ServiceWithAnEnumKey_Compiles() { + public void ServiceWithAnEnumKey_Compiles() + { var generated = Generate( """ public interface IThing; @@ -187,26 +210,30 @@ public enum Flavour { Sweet, Savoury } [SingletonService(Key = Flavour.Sweet)] public class Thing : IThing; - """); + """ + ); Assert.Contains("AddKeyedSingleton", generated); } [Fact] - public void ServiceWithAnIntegerKey_Compiles() { + public void ServiceWithAnIntegerKey_Compiles() + { var generated = Generate( """ public interface IThing; [SingletonService(Key = 42)] public class Thing : IThing; - """); + """ + ); Assert.Contains("42", generated); } [Fact] - public void ServiceKeyedByAConstant_Compiles() { + public void ServiceKeyedByAConstant_Compiles() + { var generated = Generate( """ public interface IThing; @@ -217,26 +244,30 @@ public static class Keys { [SingletonService(Key = Keys.Primary)] public class Thing : IThing; - """); + """ + ); Assert.Contains("AddKeyedSingleton", generated); } [Fact] - public void RecordService_Compiles() { + public void RecordService_Compiles() + { var generated = Generate( """ public interface IThing; [SingletonService] public record ThingRecord : IThing; - """); + """ + ); Assert.Contains("ThingRecord", generated); } [Fact] - public void ServiceWithSeveralConstructors_Compiles() { + public void ServiceWithSeveralConstructors_Compiles() + { Generate( """ public interface IDependency; @@ -250,11 +281,13 @@ public class Thing : IThing { public Thing() { } public Thing(IDependency dependency) { } } - """); + """ + ); } [Fact] - public void AbstractBaseAndConcreteService_Compiles() { + public void AbstractBaseAndConcreteService_Compiles() + { var generated = Generate( """ public interface IThing; @@ -263,13 +296,15 @@ public abstract class ThingBase : IThing; [SingletonService] public class Thing : ThingBase; - """); + """ + ); Assert.Contains("Thing", generated); } [Fact] - public void ModuleWithNullableProperty_Compiles() { + public void ModuleWithNullableProperty_Compiles() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -280,14 +315,16 @@ namespace TestNamespace; public partial class NullablePropertyModule { public string? Optional { get; set; } } - """); + """ + ); result.AssertNoErrors(); Assert.Contains("Optional", result.SourceContaining(".Module.g.cs")); } [Fact] - public void ModuleWithStaticProperty_LeavesItOffTheAttribute() { + public void ModuleWithStaticProperty_LeavesItOffTheAttribute() + { var result = GeneratorTestHarness.Run( """ using DependencyModules.Runtime.Attributes; @@ -299,7 +336,8 @@ public partial class StaticPropertyModule { public static string Shared { get; set; } = ""; public string Instance { get; set; } = ""; } - """); + """ + ); result.AssertNoErrors(); var generated = result.SourceContaining(".Module.g.cs"); @@ -317,7 +355,8 @@ public partial class StaticPropertyModule { /// that constructor's parameters. It now reads the type's own members. /// [Fact] - public void ANestedTypesConstructorIsNotTheOuterTypes() { + public void ANestedTypesConstructorIsNotTheOuterTypes() + { // With factory generation on, the constructor reaches the emitted code as a literal // new Outer(...) call. Without it the container picks the constructor at run time and the // mistake is invisible, which is why this test sets the property. @@ -340,9 +379,11 @@ public class Nested { [DependencyModule] public partial class TestModule; """, - buildProperties: new Dictionary { - ["DependencyModules_GenerateFactories"] = "true" - }); + buildProperties: new Dictionary + { + ["DependencyModules_GenerateFactories"] = "true", + } + ); Assert.IsType(assembly.Type("Outer"), assembly.ResolveRequired("IOuter")); } @@ -350,22 +391,23 @@ public partial class TestModule; /// /// Compiles the supplied declarations alongside a module and returns the registration file. /// - private static string Generate(string body) { + private static string Generate(string body) + { var result = GeneratorTestHarness.Run( $$""" - using DependencyModules.Runtime.Attributes; + using DependencyModules.Runtime.Attributes; - namespace TestNamespace; + namespace TestNamespace; - {{body}} + {{body}} - [DependencyModule] - public partial class TestModule; - """); + [DependencyModule] + public partial class TestModule; + """ + ); result.AssertNoErrors(); return result.SourceContaining("Dependencies"); } - } diff --git a/tests/DependencyModules.Tests/Infrastructure/GeneratedAssembly.cs b/tests/DependencyModules.Tests/Infrastructure/GeneratedAssembly.cs index 17454b0..210b00a 100644 --- a/tests/DependencyModules.Tests/Infrastructure/GeneratedAssembly.cs +++ b/tests/DependencyModules.Tests/Infrastructure/GeneratedAssembly.cs @@ -18,12 +18,14 @@ namespace DependencyModules.Tests.Infrastructure; /// registrations the same way an application would, so a change that still emits plausible-looking /// code but breaks behaviour fails here. /// -public class GeneratedAssembly { +public class GeneratedAssembly +{ private static int _assemblyCounter; private readonly Assembly _assembly; - private GeneratedAssembly(Assembly assembly, IServiceCollection services) { + private GeneratedAssembly(Assembly assembly, IServiceCollection services) + { _assembly = assembly; Services = services; } @@ -49,30 +51,36 @@ public static GeneratedAssembly Create( string source, string moduleName = "TestModule", IReadOnlyDictionary? buildProperties = null, - IModuleEnvironment? environment = null, - IReadOnlyList? additionalReferences = null) { - + IModuleEnvironment? environment = null, + IReadOnlyList? additionalReferences = null + ) + { var assemblyName = "GeneratedAssemblyTest" + Interlocked.Increment(ref _assemblyCounter); var result = GeneratorTestHarness.Run( new Dictionary { ["Test.cs"] = source }, buildProperties, assemblyName: assemblyName, - additionalReferences: additionalReferences); + additionalReferences: additionalReferences + ); result.AssertNoErrors(); var assembly = Emit(result, assemblyName); - var moduleType = assembly.GetType($"TestNamespace.{moduleName}") - ?? throw new InvalidOperationException( - $"The compiled assembly has no type 'TestNamespace.{moduleName}'. " + - $"Types present: {string.Join(", ", assembly.GetTypes().Select(t => t.FullName))}"); + var moduleType = + assembly.GetType($"TestNamespace.{moduleName}") + ?? throw new InvalidOperationException( + $"The compiled assembly has no type 'TestNamespace.{moduleName}'. " + + $"Types present: {string.Join(", ", assembly.GetTypes().Select(t => t.FullName))}" + ); - if (Activator.CreateInstance(moduleType) is not IDependencyModule module) { + if (Activator.CreateInstance(moduleType) is not IDependencyModule module) + { throw new InvalidOperationException( - $"'{moduleType.FullName}' did not implement IDependencyModule. The generator should " + - "have added that to the partial declaration."); + $"'{moduleType.FullName}' did not implement IDependencyModule. The generator should " + + "have added that to the partial declaration." + ); } var services = new ServiceCollection(); @@ -84,17 +92,24 @@ public static GeneratedAssembly Create( return new GeneratedAssembly(assembly, services); } - private static Assembly Emit(GeneratorResult result, string assemblyName) { + private static Assembly Emit(GeneratorResult result, string assemblyName) + { using var stream = new MemoryStream(); EmitResult emitResult = result.Compilation.Emit(stream); - Assert.True(emitResult.Success, - $"The generated code did not emit. Errors:{Environment.NewLine}" + - string.Join(Environment.NewLine, - emitResult.Diagnostics - .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) - .Select(diagnostic => $" {diagnostic.Id} {diagnostic.GetMessage()}"))); + Assert.True( + emitResult.Success, + $"The generated code did not emit. Errors:{Environment.NewLine}" + + string.Join( + Environment.NewLine, + emitResult + .Diagnostics.Where(diagnostic => + diagnostic.Severity == DiagnosticSeverity.Error + ) + .Select(diagnostic => $" {diagnostic.Id} {diagnostic.GetMessage()}") + ) + ); // Loaded into the default context so it binds against the DependencyModules.Runtime already // loaded by this test assembly. That is what lets tests cast to IDependencyModule and @@ -108,7 +123,8 @@ private static Assembly Emit(GeneratorResult result, string assemblyName) { public Type Type(string name) => _assembly.GetType($"TestNamespace.{name}") ?? throw new InvalidOperationException( - $"No type 'TestNamespace.{name}'. Present: {string.Join(", ", _assembly.GetTypes().Select(t => t.Name))}"); + $"No type 'TestNamespace.{name}'. Present: {string.Join(", ", _assembly.GetTypes().Select(t => t.Name))}" + ); /// /// Builds a provider from the applied registrations. @@ -118,13 +134,16 @@ public Type Type(string name) => /// /// Resolves a service by type name, failing the test if it was never registered. /// - public object ResolveRequired(string typeName) { + public object ResolveRequired(string typeName) + { var provider = BuildProvider(); var service = provider.GetService(Type(typeName)); - Assert.True(service != null, - $"'{typeName}' did not resolve. Registered service types: " + - string.Join(", ", Services.Select(descriptor => descriptor.ServiceType.Name))); + Assert.True( + service != null, + $"'{typeName}' did not resolve. Registered service types: " + + string.Join(", ", Services.Select(descriptor => descriptor.ServiceType.Name)) + ); return service!; } @@ -132,17 +151,21 @@ public object ResolveRequired(string typeName) { /// /// The descriptor registered for a service type, failing the test if there is not exactly one. /// - public ServiceDescriptor Descriptor(string serviceTypeName) { + public ServiceDescriptor Descriptor(string serviceTypeName) + { var serviceType = Type(serviceTypeName); var matches = Services.Where(descriptor => descriptor.ServiceType == serviceType).ToArray(); - Assert.True(matches.Length == 1, - $"Expected exactly one registration for '{serviceTypeName}', found {matches.Length}."); + Assert.True( + matches.Length == 1, + $"Expected exactly one registration for '{serviceTypeName}', found {matches.Length}." + ); return matches[0]; } - public IReadOnlyList Descriptors(string serviceTypeName) { + public IReadOnlyList Descriptors(string serviceTypeName) + { var serviceType = Type(serviceTypeName); return Services.Where(descriptor => descriptor.ServiceType == serviceType).ToArray(); diff --git a/tests/DependencyModules.Tests/Infrastructure/GeneratorTestHarness.cs b/tests/DependencyModules.Tests/Infrastructure/GeneratorTestHarness.cs index d016661..3700cc4 100644 --- a/tests/DependencyModules.Tests/Infrastructure/GeneratorTestHarness.cs +++ b/tests/DependencyModules.Tests/Infrastructure/GeneratorTestHarness.cs @@ -1,8 +1,7 @@ - using System.Collections; using System.Collections.Immutable; -using System.Runtime.CompilerServices; using System.Reflection; +using System.Runtime.CompilerServices; using DependencyModules.Runtime.Attributes; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -16,8 +15,11 @@ namespace DependencyModules.Tests.Infrastructure; /// Runs the DependencyModules source generator over an in-memory compilation so tests can assert /// on the code it produces without going through a real build. /// -public static class GeneratorTestHarness { - private static readonly Lazy> References = new(BuildReferences); +public static class GeneratorTestHarness +{ + private static readonly Lazy> References = new( + BuildReferences + ); /// /// Compiles , runs the generator, and returns everything it emitted. @@ -34,18 +36,22 @@ public static GeneratorResult Run( OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, string assemblyName = "GeneratorTestAssembly", IReadOnlyList? additionalReferences = null, - IReadOnlyList? generators = null) { - + IReadOnlyList? generators = null + ) + { // MSBuild hands the compiler absolute paths, and the generator compares a file's location // against ProjectDir to decide whether it owns the auto-generated ApplicationModule. // Rooting the test sources under ProjectDir keeps that comparison meaningful. var projectDir = ResolveProjectDir(buildProperties); var syntaxTrees = sources - .Select(pair => CSharpSyntaxTree.ParseText( - pair.Value, - new CSharpParseOptions(LanguageVersion.Latest), - path: Path.Combine(projectDir, pair.Key))) + .Select(pair => + CSharpSyntaxTree.ParseText( + pair.Value, + new CSharpParseOptions(LanguageVersion.Latest), + path: Path.Combine(projectDir, pair.Key) + ) + ) .ToArray(); var compilation = CSharpCompilation.Create( @@ -54,15 +60,24 @@ public static GeneratorResult Run( additionalReferences == null ? References.Value : References.Value.Concat(additionalReferences), - new CSharpCompilationOptions(outputKind, nullableContextOptions: NullableContextOptions.Enable)); + new CSharpCompilationOptions( + outputKind, + nullableContextOptions: NullableContextOptions.Enable + ) + ); var driver = CSharpGeneratorDriver.Create( generators == null ? Generators() : generators.ToArray(), optionsProvider: new TestAnalyzerConfigOptionsProvider(buildProperties), - parseOptions: new CSharpParseOptions(LanguageVersion.Latest)); + parseOptions: new CSharpParseOptions(LanguageVersion.Latest) + ); - driver = (CSharpGeneratorDriver)driver.RunGeneratorsAndUpdateCompilation( - compilation, out var outputCompilation, out var generatorDiagnostics); + driver = (CSharpGeneratorDriver) + driver.RunGeneratorsAndUpdateCompilation( + compilation, + out var outputCompilation, + out var generatorDiagnostics + ); var runResult = driver.GetRunResult(); @@ -70,8 +85,8 @@ public static GeneratorResult Run( // generator can produce the same name twice. Keyed rather than grouped it threw, hiding the // duplication behind a dictionary error; two generators emitting one type's partial twice // is a real defect, so it is recorded and asserted on instead. - var emitted = runResult.Results - .SelectMany(result => result.GeneratedSources) + var emitted = runResult + .Results.SelectMany(result => result.GeneratedSources) .Select(generated => (generated.HintName, Source: generated.SourceText.ToString())) .ToArray(); @@ -87,8 +102,8 @@ public static GeneratorResult Run( // The generator catches its own exceptions and, with no log folder configured, discards // them. Surface them here so a crashing generator fails loudly instead of producing nothing. - var exceptions = runResult.Results - .Select(result => result.Exception) + var exceptions = runResult + .Results.Select(result => result.Exception) .Where(exception => exception != null) .ToArray(); @@ -98,7 +113,8 @@ public static GeneratorResult Run( outputCompilation.GetDiagnostics(), outputCompilation, exceptions!, - duplicateHintNames); + duplicateHintNames + ); } /// @@ -106,8 +122,8 @@ public static GeneratorResult Run( /// public static GeneratorResult Run( string source, - IReadOnlyDictionary? buildProperties = null) => - Run(new Dictionary { ["Test.cs"] = source }, buildProperties); + IReadOnlyDictionary? buildProperties = null + ) => Run(new Dictionary { ["Test.cs"] = source }, buildProperties); /// /// One generator. Conventions, services, decorators and interception all come from it. @@ -119,9 +135,7 @@ public static GeneratorResult Run( /// closed over a registration a convention produced. /// private static ISourceGenerator[] Generators() => - new ISourceGenerator[] { - new SourceGenerator.SourceGenerator().AsSourceGenerator(), - }; + new ISourceGenerator[] { new SourceGenerator.SourceGenerator().AsSourceGenerator() }; /// /// Runs the generator over , then re-runs the same driver over @@ -135,25 +149,34 @@ public static IncrementalRunResult RunIncremental( IReadOnlyDictionary first, IReadOnlyDictionary second, IReadOnlyDictionary? buildProperties = null, - bool withConventions = false) { - + bool withConventions = false + ) + { var projectDir = ResolveProjectDir(buildProperties); Compilation Compile(IReadOnlyDictionary sources) => CSharpCompilation.Create( "GeneratorTestAssembly", - sources.Select(pair => CSharpSyntaxTree.ParseText( - pair.Value, - new CSharpParseOptions(LanguageVersion.Latest), - path: Path.Combine(projectDir, pair.Key))), + sources.Select(pair => + CSharpSyntaxTree.ParseText( + pair.Value, + new CSharpParseOptions(LanguageVersion.Latest), + path: Path.Combine(projectDir, pair.Key) + ) + ), References.Value, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); + new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + nullableContextOptions: NullableContextOptions.Enable + ) + ); GeneratorDriver driver = CSharpGeneratorDriver.Create( Generators(), optionsProvider: new TestAnalyzerConfigOptionsProvider(buildProperties), parseOptions: new CSharpParseOptions(LanguageVersion.Latest), - driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true)); + driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true) + ); driver = driver.RunGenerators(Compile(first)); var firstOutputs = Outputs(driver.GetRunResult()); @@ -161,8 +184,8 @@ Compilation Compile(IReadOnlyDictionary sources) => driver = driver.RunGenerators(Compile(second)); var secondRun = driver.GetRunResult(); - var outputs = secondRun.Results - .SelectMany(result => result.TrackedOutputSteps) + var outputs = secondRun + .Results.SelectMany(result => result.TrackedOutputSteps) .SelectMany(step => step.Value) .SelectMany(step => step.Outputs) .Select(output => (output.Reason, EmittedSource: EmittedSourceCount(output.Value))) @@ -186,10 +209,15 @@ Compilation Compile(IReadOnlyDictionary sources) => private static int EmittedSourceCount(object? value) => value is ITuple { Length: 2 } tuple && tuple[0] is ICollection sources ? sources.Count : 0; - private static IReadOnlyDictionary Outputs(GeneratorDriverRunResult runResult) => - runResult.Results - .SelectMany(result => result.GeneratedSources) - .ToDictionary(generated => generated.HintName, generated => generated.SourceText.ToString()); + private static IReadOnlyDictionary Outputs( + GeneratorDriverRunResult runResult + ) => + runResult + .Results.SelectMany(result => result.GeneratedSources) + .ToDictionary( + generated => generated.HintName, + generated => generated.SourceText.ToString() + ); internal static string DefaultProjectDir { get; } = Path.Combine(Path.GetTempPath(), "GeneratorTest") + Path.DirectorySeparatorChar; @@ -214,19 +242,29 @@ private static string ResolveProjectDir(IReadOnlyDictionary? bui /// sees that in metadata. Off by default because most callers only need plain types to scan. /// public static (MetadataReference Reference, System.Reflection.Assembly Assembly) CompileLibrary( - string source, string assemblyName, bool runGenerator = false) { - + string source, + string assemblyName, + bool runGenerator = false + ) + { Compilation compilation = CSharpCompilation.Create( assemblyName, - new[] { CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Latest)) }, + new[] + { + CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Latest)), + }, References.Value, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + ); - if (runGenerator) { - CSharpGeneratorDriver.Create( + if (runGenerator) + { + CSharpGeneratorDriver + .Create( Generators(), optionsProvider: new TestAnalyzerConfigOptionsProvider(null), - parseOptions: new CSharpParseOptions(LanguageVersion.Latest)) + parseOptions: new CSharpParseOptions(LanguageVersion.Latest) + ) .RunGeneratorsAndUpdateCompilation(compilation, out compilation, out _); } @@ -234,36 +272,47 @@ public static (MetadataReference Reference, System.Reflection.Assembly Assembly) var result = compilation.Emit(stream); - Xunit.Assert.True(result.Success, - "The test library did not compile: " + string.Join( - Environment.NewLine, - result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error))); + Xunit.Assert.True( + result.Success, + "The test library did not compile: " + + string.Join( + Environment.NewLine, + result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error) + ) + ); var bytes = stream.ToArray(); var assembly = System.Reflection.Assembly.Load(bytes); // Assembly.Load(byte[]) puts it in the default context but does not make it discoverable by // name, so generated code referencing it fails to bind at run time. The resolver closes that. - lock (LoadedLibraries) { + lock (LoadedLibraries) + { // Two test classes compiling different libraries under one name is a trap worth being // loud about. The resolver below is keyed by name, so the second registration decides // what every earlier reference binds to at run time - and the tests that break are // whichever ones happened to run second, which is a failure that appears and disappears // with the filter you ran. Xunit.Assert.True( - !LibrarySources.TryGetValue(assemblyName, out var previousSource) || - previousSource == source, - $"'{assemblyName}' was compiled more than once from different source. The runtime " + - "resolver is keyed by assembly name, so one of them would silently stand in for the " + - "other. Give each test class its own assembly name."); + !LibrarySources.TryGetValue(assemblyName, out var previousSource) + || previousSource == source, + $"'{assemblyName}' was compiled more than once from different source. The runtime " + + "resolver is keyed by assembly name, so one of them would silently stand in for the " + + "other. Give each test class its own assembly name." + ); LibrarySources[assemblyName] = source; LoadedLibraries[assemblyName] = assembly; - if (!_resolverHooked) { - System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += (_, name) => { - lock (LoadedLibraries) { - return name.Name != null && LoadedLibraries.TryGetValue(name.Name, out var found) + if (!_resolverHooked) + { + System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += (_, name) => + { + lock (LoadedLibraries) + { + return + name.Name != null + && LoadedLibraries.TryGetValue(name.Name, out var found) ? found : null; } @@ -283,39 +332,54 @@ public static (MetadataReference Reference, System.Reflection.Assembly Assembly) private static bool _resolverHooked; - private static ImmutableArray BuildReferences() { + private static ImmutableArray BuildReferences() + { var builder = ImmutableArray.CreateBuilder(); // The framework reference set the test host was resolved against. - if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string trusted) { - foreach (var path in trusted.Split(Path.PathSeparator)) { - if (path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && File.Exists(path)) { + if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string trusted) + { + foreach (var path in trusted.Split(Path.PathSeparator)) + { + if (path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && File.Exists(path)) + { builder.Add(MetadataReference.CreateFromFile(path)); } } } // The assemblies the generated code actually binds against. - foreach (var assembly in new[] { - typeof(DependencyModuleAttribute).Assembly, - typeof(IServiceCollection).Assembly, - typeof(ServiceCollection).Assembly - }) { + foreach ( + var assembly in new[] + { + typeof(DependencyModuleAttribute).Assembly, + typeof(IServiceCollection).Assembly, + typeof(ServiceCollection).Assembly, + } + ) + { AddAssembly(builder, assembly); } return builder.ToImmutable(); } - private static void AddAssembly(ImmutableArray.Builder builder, Assembly assembly) { - if (string.IsNullOrEmpty(assembly.Location)) { + private static void AddAssembly( + ImmutableArray.Builder builder, + Assembly assembly + ) + { + if (string.IsNullOrEmpty(assembly.Location)) + { return; } var alreadyPresent = builder.Any(reference => - string.Equals(reference.Display, assembly.Location, StringComparison.OrdinalIgnoreCase)); + string.Equals(reference.Display, assembly.Location, StringComparison.OrdinalIgnoreCase) + ); - if (!alreadyPresent) { + if (!alreadyPresent) + { builder.Add(MetadataReference.CreateFromFile(assembly.Location)); } } @@ -331,8 +395,9 @@ public class GeneratorResult( ImmutableArray compilationDiagnostics, Compilation compilation, IReadOnlyList generatorExceptions, - IReadOnlyList? duplicateHintNames = null) { - + IReadOnlyList? duplicateHintNames = null +) +{ public IReadOnlyDictionary GeneratedSources { get; } = generatedSources; public ImmutableArray GeneratorDiagnostics { get; } = generatorDiagnostics; @@ -346,24 +411,31 @@ public class GeneratorResult( /// /// Hint names emitted by more than one generator in the same run. /// - public IReadOnlyList DuplicateHintNames { get; } = duplicateHintNames ?? Array.Empty(); + public IReadOnlyList DuplicateHintNames { get; } = + duplicateHintNames ?? Array.Empty(); public IEnumerable Errors => - GeneratorDiagnostics.Concat(CompilationDiagnostics) + GeneratorDiagnostics + .Concat(CompilationDiagnostics) .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error); /// /// The single generated file whose hint name contains . /// - public string SourceContaining(string fragment) { + public string SourceContaining(string fragment) + { var matches = GeneratedSources .Where(pair => pair.Key.Contains(fragment, StringComparison.OrdinalIgnoreCase)) .ToArray(); - Assert.True(matches.Length > 0, - $"No generated file matched '{fragment}'. Generated: {string.Join(", ", GeneratedSources.Keys)}"); - Assert.True(matches.Length == 1, - $"'{fragment}' matched more than one generated file: {string.Join(", ", matches.Select(m => m.Key))}"); + Assert.True( + matches.Length > 0, + $"No generated file matched '{fragment}'. Generated: {string.Join(", ", GeneratedSources.Keys)}" + ); + Assert.True( + matches.Length == 1, + $"'{fragment}' matched more than one generated file: {string.Join(", ", matches.Select(m => m.Key))}" + ); return matches[0].Value; } @@ -371,16 +443,26 @@ public string SourceContaining(string fragment) { /// /// Asserts the generator produced output that compiles cleanly. /// - public GeneratorResult AssertNoErrors() { - Assert.True(GeneratorExceptions.Count == 0, - "The generator threw:" + Environment.NewLine + - string.Join(Environment.NewLine, GeneratorExceptions.Select(e => $" {e}"))); + public GeneratorResult AssertNoErrors() + { + Assert.True( + GeneratorExceptions.Count == 0, + "The generator threw:" + + Environment.NewLine + + string.Join(Environment.NewLine, GeneratorExceptions.Select(e => $" {e}")) + ); var errors = Errors.ToArray(); - Assert.True(errors.Length == 0, - "Expected no errors, got:" + Environment.NewLine + - string.Join(Environment.NewLine, errors.Select(e => $" {e.Id} {e.GetMessage()} @ {e.Location.GetLineSpan()}"))); + Assert.True( + errors.Length == 0, + "Expected no errors, got:" + + Environment.NewLine + + string.Join( + Environment.NewLine, + errors.Select(e => $" {e.Id} {e.GetMessage()} @ {e.Location.GetLineSpan()}") + ) + ); return this; } @@ -388,10 +470,12 @@ public GeneratorResult AssertNoErrors() { /// /// All generated files concatenated in a stable order, suitable for snapshotting. /// - public string ToSnapshot() { + public string ToSnapshot() + { var builder = new System.Text.StringBuilder(); - foreach (var pair in GeneratedSources.OrderBy(p => p.Key, StringComparer.Ordinal)) { + foreach (var pair in GeneratedSources.OrderBy(p => p.Key, StringComparer.Ordinal)) + { builder.AppendLine($"// ---- {pair.Key} ----"); builder.AppendLine(pair.Value.Replace("\r\n", "\n").TrimEnd()); builder.AppendLine(); @@ -408,8 +492,9 @@ public string ToSnapshot() { public class IncrementalRunResult( IReadOnlyDictionary firstRun, IReadOnlyDictionary secondRun, - IReadOnlyList<(IncrementalStepRunReason Reason, int EmittedSource)> outputs) { - + IReadOnlyList<(IncrementalStepRunReason Reason, int EmittedSource)> outputs +) +{ public IReadOnlyDictionary FirstRun { get; } = firstRun; public IReadOnlyDictionary SecondRun { get; } = secondRun; @@ -431,38 +516,49 @@ public class IncrementalRunResult( /// what this measures. /// public bool AllOutputsCached => - outputs.Any(output => output.EmittedSource > 0) && - outputs + outputs.Any(output => output.EmittedSource > 0) + && outputs .Where(output => output.EmittedSource > 0) - .All(output => output.Reason - is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged); + .All(output => + output.Reason + is IncrementalStepRunReason.Cached + or IncrementalStepRunReason.Unchanged + ); } -internal class TestAnalyzerConfigOptionsProvider(IReadOnlyDictionary? buildProperties) - : AnalyzerConfigOptionsProvider { - - public override AnalyzerConfigOptions GlobalOptions { get; } = new TestAnalyzerConfigOptions(buildProperties); +internal class TestAnalyzerConfigOptionsProvider( + IReadOnlyDictionary? buildProperties +) : AnalyzerConfigOptionsProvider +{ + public override AnalyzerConfigOptions GlobalOptions { get; } = + new TestAnalyzerConfigOptions(buildProperties); public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => GlobalOptions; public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => GlobalOptions; } -internal class TestAnalyzerConfigOptions : AnalyzerConfigOptions { +internal class TestAnalyzerConfigOptions : AnalyzerConfigOptions +{ private readonly Dictionary _options; - public TestAnalyzerConfigOptions(IReadOnlyDictionary? buildProperties) { - _options = new Dictionary(StringComparer.OrdinalIgnoreCase) { + public TestAnalyzerConfigOptions(IReadOnlyDictionary? buildProperties) + { + _options = new Dictionary(StringComparer.OrdinalIgnoreCase) + { ["build_property.RootNamespace"] = "TestNamespace", - ["build_property.ProjectDir"] = GeneratorTestHarness.DefaultProjectDir + ["build_property.ProjectDir"] = GeneratorTestHarness.DefaultProjectDir, }; - if (buildProperties != null) { - foreach (var pair in buildProperties) { + if (buildProperties != null) + { + foreach (var pair in buildProperties) + { _options["build_property." + pair.Key] = pair.Value; } } } - public override bool TryGetValue(string key, out string value) => _options.TryGetValue(key, out value!); + public override bool TryGetValue(string key, out string value) => + _options.TryGetValue(key, out value!); } diff --git a/tests/DependencyModules.Tests/Infrastructure/ModelFactory.cs b/tests/DependencyModules.Tests/Infrastructure/ModelFactory.cs index 11f3e06..6df7cc5 100644 --- a/tests/DependencyModules.Tests/Infrastructure/ModelFactory.cs +++ b/tests/DependencyModules.Tests/Infrastructure/ModelFactory.cs @@ -7,8 +7,8 @@ namespace DependencyModules.Tests.Infrastructure; /// Builds generator model objects for tests. The model records have wide positional constructors, /// so tests name only the field they care about and take defaults for the rest. /// -public static class ModelFactory { - +public static class ModelFactory +{ public static ModuleEntryPointModel EntryPoint( ModuleEntryPointFeatures features = ModuleEntryPointFeatures.None, string fileLocation = "/project/Module.cs", @@ -22,7 +22,8 @@ public static ModuleEntryPointModel EntryPoint( IReadOnlyList? properties = null, IReadOnlyList? attributes = null, IReadOnlyList? additionalModules = null, - IReadOnlyList? featureTypes = null) => + IReadOnlyList? featureTypes = null + ) => new( features, fileLocation, @@ -37,7 +38,8 @@ public static ModuleEntryPointModel EntryPoint( properties ?? new List(), attributes ?? new List(), additionalModules ?? new List(), - featureTypes ?? new List()); + featureTypes ?? new List() + ); public static DependencyModuleConfigurationModel Configuration( RegistrationType registrationType = RegistrationType.Add, @@ -49,7 +51,8 @@ public static DependencyModuleConfigurationModel Configuration( LogOutputLevel logOutputLevel = LogOutputLevel.Debug, bool generateFactories = false, bool excludeGeneratedCodeFromCoverage = true, - BraceStyle generatedCodeStyle = BraceStyle.Allman) => + BraceStyle generatedCodeStyle = BraceStyle.Allman + ) => new( registrationType, registerSourceGenerator, @@ -60,5 +63,6 @@ public static DependencyModuleConfigurationModel Configuration( logOutputLevel, generateFactories, excludeGeneratedCodeFromCoverage, - generatedCodeStyle); + generatedCodeStyle + ); } diff --git a/tests/DependencyModules.Tests/Infrastructure/Snapshot.cs b/tests/DependencyModules.Tests/Infrastructure/Snapshot.cs index e409436..ee60a21 100644 --- a/tests/DependencyModules.Tests/Infrastructure/Snapshot.cs +++ b/tests/DependencyModules.Tests/Infrastructure/Snapshot.cs @@ -16,7 +16,8 @@ namespace DependencyModules.Tests.Infrastructure; /// [CallerFilePath] is deliberately not used to locate them: deterministic builds /// (ContinuousIntegrationBuild) rewrite source paths to /_/..., which does not exist on disk. /// -public static class Snapshot { +public static class Snapshot +{ private const string UpdateVariable = "UPDATE_SNAPSHOTS"; private const string SnapshotFolderName = "Snapshots"; @@ -31,42 +32,58 @@ public static class Snapshot { /// when updating, and only meaningful when the tests run on the machine that built them. /// private static string? SourceSnapshotDirectory { get; } = - typeof(Snapshot).Assembly - .GetCustomAttributes() + typeof(Snapshot) + .Assembly.GetCustomAttributes() .FirstOrDefault(attribute => attribute.Key == "SnapshotDirectory") ?.Value; public static void Match( string actual, [CallerFilePath] string callerFilePath = "", - [CallerMemberName] string callerMemberName = "") { - + [CallerMemberName] string callerMemberName = "" + ) + { var testClass = Path.GetFileNameWithoutExtension(callerFilePath); var fileName = $"{testClass}.{callerMemberName}.verified.txt"; var normalized = Normalize(actual); - if (ShouldUpdate) { + if (ShouldUpdate) + { Update(fileName, normalized); return; } var snapshotPath = Path.Combine(OutputSnapshotDirectory, fileName); - Assert.True(File.Exists(snapshotPath), - $"Missing snapshot '{fileName}'. Re-run with {UpdateVariable}=1 to create it." + - Environment.NewLine + "Actual output was:" + Environment.NewLine + normalized); + Assert.True( + File.Exists(snapshotPath), + $"Missing snapshot '{fileName}'. Re-run with {UpdateVariable}=1 to create it." + + Environment.NewLine + + "Actual output was:" + + Environment.NewLine + + normalized + ); var expected = Normalize(File.ReadAllText(snapshotPath)); - if (expected != normalized) { - var receivedPath = Path.Combine(OutputSnapshotDirectory, fileName.Replace(".verified.txt", ".received.txt")); + if (expected != normalized) + { + var receivedPath = Path.Combine( + OutputSnapshotDirectory, + fileName.Replace(".verified.txt", ".received.txt") + ); File.WriteAllText(receivedPath, normalized); Assert.Fail( - $"Generated output does not match '{fileName}'." + Environment.NewLine + - $"Wrote actual output to '{receivedPath}'." + Environment.NewLine + - $"If the change is intended, re-run with {UpdateVariable}=1." + Environment.NewLine + - Environment.NewLine + FirstDifference(expected, normalized)); + $"Generated output does not match '{fileName}'." + + Environment.NewLine + + $"Wrote actual output to '{receivedPath}'." + + Environment.NewLine + + $"If the change is intended, re-run with {UpdateVariable}=1." + + Environment.NewLine + + Environment.NewLine + + FirstDifference(expected, normalized) + ); } } @@ -74,14 +91,19 @@ public static void Match( /// Writes to the source tree so the change can be reviewed and committed, and to the build /// output so a subsequent run in the same session reads the updated value. /// - private static void Update(string fileName, string content) { - Assert.True(!string.IsNullOrEmpty(SourceSnapshotDirectory), - $"Cannot update snapshots: the assembly has no SnapshotDirectory metadata. " + - $"Rebuild the test project, then re-run with {UpdateVariable}=1."); - - Assert.True(Directory.Exists(Path.GetDirectoryName(SourceSnapshotDirectory!)), - $"Cannot update snapshots: '{SourceSnapshotDirectory}' is not reachable from this machine. " + - "Snapshots can only be updated from a checkout of the source tree."); + private static void Update(string fileName, string content) + { + Assert.True( + !string.IsNullOrEmpty(SourceSnapshotDirectory), + $"Cannot update snapshots: the assembly has no SnapshotDirectory metadata. " + + $"Rebuild the test project, then re-run with {UpdateVariable}=1." + ); + + Assert.True( + Directory.Exists(Path.GetDirectoryName(SourceSnapshotDirectory!)), + $"Cannot update snapshots: '{SourceSnapshotDirectory}' is not reachable from this machine. " + + "Snapshots can only be updated from a checkout of the source tree." + ); Directory.CreateDirectory(SourceSnapshotDirectory!); File.WriteAllText(Path.Combine(SourceSnapshotDirectory!, fileName), content); @@ -90,29 +112,36 @@ private static void Update(string fileName, string content) { File.WriteAllText(Path.Combine(OutputSnapshotDirectory, fileName), content); } - private static bool ShouldUpdate { - get { + private static bool ShouldUpdate + { + get + { var value = Environment.GetEnvironmentVariable(UpdateVariable); - return !string.IsNullOrEmpty(value) && !value.Equals("0", StringComparison.Ordinal) && - !value.Equals("false", StringComparison.OrdinalIgnoreCase); + return !string.IsNullOrEmpty(value) + && !value.Equals("0", StringComparison.Ordinal) + && !value.Equals("false", StringComparison.OrdinalIgnoreCase); } } - private static string Normalize(string value) => - value.Replace("\r\n", "\n").TrimEnd() + "\n"; + private static string Normalize(string value) => value.Replace("\r\n", "\n").TrimEnd() + "\n"; - private static string FirstDifference(string expected, string actual) { + private static string FirstDifference(string expected, string actual) + { var expectedLines = expected.Split('\n'); var actualLines = actual.Split('\n'); - for (var i = 0; i < Math.Max(expectedLines.Length, actualLines.Length); i++) { + for (var i = 0; i < Math.Max(expectedLines.Length, actualLines.Length); i++) + { var expectedLine = i < expectedLines.Length ? expectedLines[i] : ""; var actualLine = i < actualLines.Length ? actualLines[i] : ""; - if (expectedLine != actualLine) { - return $"First difference at line {i + 1}:" + Environment.NewLine + - $" expected: {expectedLine}" + Environment.NewLine + - $" actual: {actualLine}"; + if (expectedLine != actualLine) + { + return $"First difference at line {i + 1}:" + + Environment.NewLine + + $" expected: {expectedLine}" + + Environment.NewLine + + $" actual: {actualLine}"; } } diff --git a/tests/DependencyModules.Tests/NUnitTests/ModuleTestAttributeTests.cs b/tests/DependencyModules.Tests/NUnitTests/ModuleTestAttributeTests.cs index 1a4bda1..9b420c9 100644 --- a/tests/DependencyModules.Tests/NUnitTests/ModuleTestAttributeTests.cs +++ b/tests/DependencyModules.Tests/NUnitTests/ModuleTestAttributeTests.cs @@ -17,16 +17,16 @@ namespace DependencyModules.Tests.NUnitTests; /// services, and the row kept aside so execution knows which leading arguments are real. Building a /// container here instead would construct every mock in an assembly during discovery. /// -public class ModuleTestAttributeTests { - +public class ModuleTestAttributeTests +{ private interface IService; /// /// The methods under test, carrying real attributes — the same reflection BuildFrom reads /// from at discovery. /// - private class Samples { - + private class Samples + { public void NoParameters() { } public void OneServiceParameter(IService service) { } @@ -56,7 +56,8 @@ public void OneGoodRowAndOneBad(int first, int second) { } } [Fact] - public void BuildsOneCaseWhenThereAreNoRows() { + public void BuildsOneCaseWhenThereAreNoRows() + { var testMethod = Assert.Single(Build(nameof(Samples.OneServiceParameter))); Assert.Equal(nameof(Samples.OneServiceParameter), testMethod.Name); @@ -68,7 +69,8 @@ public void BuildsOneCaseWhenThereAreNoRows() { /// NUnit checks the argument count against the method's parameters when the case is built. /// [Fact] - public void APlaceholderIsSuppliedForEveryParameter() { + public void APlaceholderIsSuppliedForEveryParameter() + { var testMethod = Assert.Single(Build(nameof(Samples.NumberThenService))); Assert.Equal(2, testMethod.Arguments.Length); @@ -76,12 +78,14 @@ public void APlaceholderIsSuppliedForEveryParameter() { } [Fact] - public void AMethodWithNoParametersBuildsWithNoArguments() { + public void AMethodWithNoParametersBuildsWithNoArguments() + { Assert.Empty(Assert.Single(Build(nameof(Samples.NoParameters))).Arguments); } [Fact] - public void BuildsOneCasePerRow() { + public void BuildsOneCasePerRow() + { var built = Build(nameof(Samples.TwoRows)); Assert.Equal(2, built.Length); @@ -93,7 +97,8 @@ public void BuildsOneCasePerRow() { /// A row covers the leading parameters only; the rest stay null until the container fills them. /// [Fact] - public void ARowLeavesTheRemainingParametersToTheContainer() { + public void ARowLeavesTheRemainingParametersToTheContainer() + { var testMethod = Assert.Single(Build(nameof(Samples.OneRowCoveringOneOfTwoParameters))); Assert.Equal(7, testMethod.Arguments[0]); @@ -101,9 +106,12 @@ public void ARowLeavesTheRemainingParametersToTheContainer() { } [Fact] - public void RowsAreNamedAfterTheirOwnArguments() { - Assert.Equal("OneRowCoveringOneOfTwoParameters(7)", - Assert.Single(Build(nameof(Samples.OneRowCoveringOneOfTwoParameters))).Name); + public void RowsAreNamedAfterTheirOwnArguments() + { + Assert.Equal( + "OneRowCoveringOneOfTwoParameters(7)", + Assert.Single(Build(nameof(Samples.OneRowCoveringOneOfTwoParameters))).Name + ); } /// @@ -112,13 +120,17 @@ public void RowsAreNamedAfterTheirOwnArguments() { /// the time the test runs. /// [Fact] - public void ARowsNameOmitsTheParametersTheContainerSupplies() { - Assert.DoesNotContain("null", - Assert.Single(Build(nameof(Samples.OneRowCoveringOneOfTwoParameters))).Name); + public void ARowsNameOmitsTheParametersTheContainerSupplies() + { + Assert.DoesNotContain( + "null", + Assert.Single(Build(nameof(Samples.OneRowCoveringOneOfTwoParameters))).Name + ); } [Fact] - public void StringsAreQuotedAndNullsSpelledOutInARowsName() { + public void StringsAreQuotedAndNullsSpelledOutInARowsName() + { var built = Build(nameof(Samples.RowsNeedingQuoting)); Assert.Equal("RowsNeedingQuoting(1, \"text\")", built[0].Name); @@ -126,7 +138,8 @@ public void StringsAreQuotedAndNullsSpelledOutInARowsName() { } [Fact] - public void ARowCanNameItself() { + public void ARowCanNameItself() + { Assert.Equal("the first one", Assert.Single(Build(nameof(Samples.NamedRow))).Name); } @@ -134,7 +147,8 @@ public void ARowCanNameItself() { /// The case a live fixture cannot cover, because a non-runnable test is a failing one. /// [Fact] - public void ARowWithTooManyArgumentsIsReportedRatherThanThrown() { + public void ARowWithTooManyArgumentsIsReportedRatherThanThrown() + { var testMethod = Assert.Single(Build(nameof(Samples.TooManyArguments))); Assert.Equal(RunState.NotRunnable, testMethod.RunState); @@ -149,7 +163,8 @@ public void ARowWithTooManyArgumentsIsReportedRatherThanThrown() { /// discovery would do. /// [Fact] - public void AGoodRowStillBuildsAlongsideABadOne() { + public void AGoodRowStillBuildsAlongsideABadOne() + { var built = Build(nameof(Samples.OneGoodRowAndOneBad)); Assert.Equal(2, built.Length); @@ -157,7 +172,8 @@ public void AGoodRowStillBuildsAlongsideABadOne() { Assert.Equal(RunState.NotRunnable, built[1].RunState); } - private static TestMethod[] Build(string methodName) { + private static TestMethod[] Build(string methodName) + { var method = typeof(Samples).GetMethod(methodName)!; return new ModuleTestAttribute() diff --git a/tests/DependencyModules.Tests/RuntimeTests/AttributeTests.cs b/tests/DependencyModules.Tests/RuntimeTests/AttributeTests.cs index 896f7ad..6c04419 100644 --- a/tests/DependencyModules.Tests/RuntimeTests/AttributeTests.cs +++ b/tests/DependencyModules.Tests/RuntimeTests/AttributeTests.cs @@ -9,27 +9,31 @@ namespace DependencyModules.Tests.RuntimeTests; /// The service attributes are read by the generator at compile time, but they are also public API: /// their property surface and attribute targets are part of the 1.0 contract. /// -public class AttributeTests { - +public class AttributeTests +{ private interface IThing; [Fact] - public void SingletonService_ReportsSingletonLifetime() { + public void SingletonService_ReportsSingletonLifetime() + { Assert.Equal(ServiceLifetime.Singleton, LifetimeOf(new SingletonServiceAttribute())); } [Fact] - public void ScopedService_ReportsScopedLifetime() { + public void ScopedService_ReportsScopedLifetime() + { Assert.Equal(ServiceLifetime.Scoped, LifetimeOf(new ScopedServiceAttribute())); } [Fact] - public void TransientService_ReportsTransientLifetime() { + public void TransientService_ReportsTransientLifetime() + { Assert.Equal(ServiceLifetime.Transient, LifetimeOf(new TransientServiceAttribute())); } [Fact] - public void SettingLifetimeThroughTheInterface_IsRejected() { + public void SettingLifetimeThroughTheInterface_IsRejected() + { IServiceRegistrationAttribute attribute = new SingletonServiceAttribute(); var exception = Assert.Throws(() => attribute.Lifetime = ServiceLifetime.Scoped); @@ -38,17 +42,20 @@ public void SettingLifetimeThroughTheInterface_IsRejected() { } [Fact] - public void ServiceAttribute_DefaultsToAddRegistration() { + public void ServiceAttribute_DefaultsToAddRegistration() + { Assert.Equal(RegistrationType.Add, new SingletonServiceAttribute().Using); } [Fact] - public void ServiceAttribute_RoundTripsItsProperties() { - var attribute = new SingletonServiceAttribute { + public void ServiceAttribute_RoundTripsItsProperties() + { + var attribute = new SingletonServiceAttribute + { Key = "the-key", As = typeof(IThing), Using = RegistrationType.Try, - Realm = typeof(AttributeTests) + Realm = typeof(AttributeTests), }; Assert.Equal("the-key", attribute.Key); @@ -58,7 +65,8 @@ public void ServiceAttribute_RoundTripsItsProperties() { } [Fact] - public void ServiceAttribute_DefaultsItsOptionalPropertiesToNull() { + public void ServiceAttribute_DefaultsItsOptionalPropertiesToNull() + { var attribute = new TransientServiceAttribute(); Assert.Null(attribute.Key); @@ -67,7 +75,8 @@ public void ServiceAttribute_DefaultsItsOptionalPropertiesToNull() { } [Fact] - public void DependencyModuleAttribute_HasTheDocumentedDefaults() { + public void DependencyModuleAttribute_HasTheDocumentedDefaults() + { var attribute = new DependencyModuleAttribute(); Assert.False(attribute.OnlyRealm); @@ -79,14 +88,16 @@ public void DependencyModuleAttribute_HasTheDocumentedDefaults() { } [Fact] - public void DependencyModuleAttribute_RoundTripsItsProperties() { - var attribute = new DependencyModuleAttribute { + public void DependencyModuleAttribute_RoundTripsItsProperties() + { + var attribute = new DependencyModuleAttribute + { OnlyRealm = true, Using = RegistrationType.Replace, GenerateAttribute = false, RegisterJsonSerializers = true, GenerateFactories = true, - GenerateUseMethod = "UseThing" + GenerateUseMethod = "UseThing", }; Assert.True(attribute.OnlyRealm); @@ -102,8 +113,10 @@ public void DependencyModuleAttribute_RoundTripsItsProperties() { [InlineData(typeof(ScopedServiceAttribute))] [InlineData(typeof(TransientServiceAttribute))] [InlineData(typeof(CrossWireServiceAttribute))] - public void ServiceAttributes_TargetClassesAndMethods(Type attributeType) { - var usage = attributeType.GetCustomAttributes(typeof(AttributeUsageAttribute), false) + public void ServiceAttributes_TargetClassesAndMethods(Type attributeType) + { + var usage = attributeType + .GetCustomAttributes(typeof(AttributeUsageAttribute), false) .Cast() .Single(); @@ -114,7 +127,8 @@ public void ServiceAttributes_TargetClassesAndMethods(Type attributeType) { } [Fact] - public void DependencyModuleAttribute_TargetsClassesAndAssemblies() { + public void DependencyModuleAttribute_TargetsClassesAndAssemblies() + { var usage = typeof(DependencyModuleAttribute) .GetCustomAttributes(typeof(AttributeUsageAttribute), false) .Cast() @@ -125,5 +139,6 @@ public void DependencyModuleAttribute_TargetsClassesAndAssemblies() { Assert.False(usage.Inherited); } - private static ServiceLifetime LifetimeOf(IServiceRegistrationAttribute attribute) => attribute.Lifetime; + private static ServiceLifetime LifetimeOf(IServiceRegistrationAttribute attribute) => + attribute.Lifetime; } diff --git a/tests/DependencyModules.Tests/RuntimeTests/DecoratorHelperTests.cs b/tests/DependencyModules.Tests/RuntimeTests/DecoratorHelperTests.cs index af799eb..713c149 100644 --- a/tests/DependencyModules.Tests/RuntimeTests/DecoratorHelperTests.cs +++ b/tests/DependencyModules.Tests/RuntimeTests/DecoratorHelperTests.cs @@ -8,45 +8,54 @@ namespace DependencyModules.Tests.RuntimeTests; /// The descriptor rewrite is where decoration actually goes wrong, so it is tested directly rather /// than only through generated code. /// -public class DecoratorHelperTests { - - private interface IThing { +public class DecoratorHelperTests +{ + private interface IThing + { string Describe(); } - private class Thing : IThing { + private class Thing : IThing + { public string Describe() => "thing"; } - private class OtherThing : IThing { + private class OtherThing : IThing + { public string Describe() => "other"; } - private class Wrapper(IThing inner) : IThing { + private class Wrapper(IThing inner) : IThing + { public IThing Inner { get; } = inner; public string Describe() => $"wrapped({Inner.Describe()})"; } - private class SecondWrapper(IThing inner) : IThing { + private class SecondWrapper(IThing inner) : IThing + { public string Describe() => $"second({inner.Describe()})"; } - private interface IRepo { + private interface IRepo + { string Describe(); } - private class Repo : IRepo { + private class Repo : IRepo + { public string Describe() => "repo"; } - private class RepoWrapper(IRepo inner) : IRepo { + private class RepoWrapper(IRepo inner) : IRepo + { public string Describe() => $"wrapped({inner.Describe()})"; } private class StringRepo : Repo; - private class DisposableThing : IThing, IDisposable { + private class DisposableThing : IThing, IDisposable + { public int Disposals { get; private set; } public string Describe() => "thing"; @@ -54,7 +63,8 @@ private class DisposableThing : IThing, IDisposable { public void Dispose() => Disposals++; } - private class DisposableWrapper(IThing inner) : IThing, IDisposable { + private class DisposableWrapper(IThing inner) : IThing, IDisposable + { public IThing Inner { get; } = inner; public int Disposals { get; private set; } @@ -68,59 +78,81 @@ private static IThing Resolve(IServiceCollection services) => services.BuildServiceProvider().GetRequiredService(); [Fact] - public void Decorate_WrapsAnImplementationTypeRegistration() { + public void Decorate_WrapsAnImplementationTypeRegistration() + { var services = new ServiceCollection(); services.AddSingleton(); - DecoratorHelper.Decorate(services, typeof(IThing), (_, inner) => new Wrapper((IThing)inner)); + DecoratorHelper.Decorate( + services, + typeof(IThing), + (_, inner) => new Wrapper((IThing)inner) + ); Assert.Equal("wrapped(thing)", Resolve(services).Describe()); } [Fact] - public void Decorate_WrapsAFactoryRegistration() { + public void Decorate_WrapsAFactoryRegistration() + { var services = new ServiceCollection(); services.AddSingleton(_ => new Thing()); - DecoratorHelper.Decorate(services, typeof(IThing), (_, inner) => new Wrapper((IThing)inner)); + DecoratorHelper.Decorate( + services, + typeof(IThing), + (_, inner) => new Wrapper((IThing)inner) + ); Assert.Equal("wrapped(thing)", Resolve(services).Describe()); } [Fact] - public void Decorate_WrapsAnInstanceRegistration() { + public void Decorate_WrapsAnInstanceRegistration() + { var services = new ServiceCollection(); services.AddSingleton(new Thing()); - DecoratorHelper.Decorate(services, typeof(IThing), (_, inner) => new Wrapper((IThing)inner)); + DecoratorHelper.Decorate( + services, + typeof(IThing), + (_, inner) => new Wrapper((IThing)inner) + ); Assert.Equal("wrapped(thing)", Resolve(services).Describe()); } - - - /// /// The generic overload wraps the same shapes the type-driven one does, without a cast at the /// call site. /// [Fact] - public void DecorateOfT_WrapsAnImplementationTypeRegistration() { + public void DecorateOfT_WrapsAnImplementationTypeRegistration() + { var services = new ServiceCollection(); services.AddSingleton(); - DecoratorHelper.Decorate(services, typeof(Wrapper), (_, inner) => new Wrapper(inner)); + DecoratorHelper.Decorate( + services, + typeof(Wrapper), + (_, inner) => new Wrapper(inner) + ); Assert.Equal("wrapped(thing)", Resolve(services).Describe()); } [Fact] - public void DecorateOfT_WrapsEveryRegistrationOfTheService() { + public void DecorateOfT_WrapsEveryRegistrationOfTheService() + { var services = new ServiceCollection(); services.AddSingleton(); services.AddSingleton(); - DecoratorHelper.Decorate(services, typeof(Wrapper), (_, inner) => new Wrapper(inner)); + DecoratorHelper.Decorate( + services, + typeof(Wrapper), + (_, inner) => new Wrapper(inner) + ); var all = services.BuildServiceProvider().GetServices().ToArray(); @@ -129,12 +161,21 @@ public void DecorateOfT_WrapsEveryRegistrationOfTheService() { } [Fact] - public void DecorateOfT_StacksInApplicationOrder() { + public void DecorateOfT_StacksInApplicationOrder() + { var services = new ServiceCollection(); services.AddSingleton(); - DecoratorHelper.Decorate(services, typeof(Wrapper), (_, inner) => new Wrapper(inner)); - DecoratorHelper.Decorate(services, typeof(SecondWrapper), (_, inner) => new SecondWrapper(inner)); + DecoratorHelper.Decorate( + services, + typeof(Wrapper), + (_, inner) => new Wrapper(inner) + ); + DecoratorHelper.Decorate( + services, + typeof(SecondWrapper), + (_, inner) => new SecondWrapper(inner) + ); Assert.Equal("second(wrapped(thing))", Resolve(services).Describe()); } @@ -144,13 +185,22 @@ public void DecorateOfT_StacksInApplicationOrder() { /// generic decorator: one call per closed registration rather than one open-generic call. /// [Fact] - public void DecorateOfT_WrapsEachClosedConstructionIndependently() { + public void DecorateOfT_WrapsEachClosedConstructionIndependently() + { var services = new ServiceCollection(); services.AddSingleton, StringRepo>(); services.AddSingleton(typeof(IRepo), typeof(Repo)); - DecoratorHelper.Decorate>(services, typeof(RepoWrapper), (_, inner) => new RepoWrapper(inner)); - DecoratorHelper.Decorate>(services, typeof(RepoWrapper), (_, inner) => new RepoWrapper(inner)); + DecoratorHelper.Decorate>( + services, + typeof(RepoWrapper), + (_, inner) => new RepoWrapper(inner) + ); + DecoratorHelper.Decorate>( + services, + typeof(RepoWrapper), + (_, inner) => new RepoWrapper(inner) + ); var provider = services.BuildServiceProvider(); @@ -162,17 +212,23 @@ public void DecorateOfT_WrapsEachClosedConstructionIndependently() { /// The inner stays owned by the container here too. /// [Fact] - public void DecorateOfT_LeavesTheInnerImplementationOwnedByTheContainer() { + public void DecorateOfT_LeavesTheInnerImplementationOwnedByTheContainer() + { var services = new ServiceCollection(); services.AddScoped(); - DecoratorHelper.Decorate(services, typeof(DisposableWrapper), (_, inner) => new DisposableWrapper(inner)); + DecoratorHelper.Decorate( + services, + typeof(DisposableWrapper), + (_, inner) => new DisposableWrapper(inner) + ); var provider = services.BuildServiceProvider(); DisposableWrapper wrapper; - using (var scope = provider.CreateScope()) { + using (var scope = provider.CreateScope()) + { wrapper = (DisposableWrapper)scope.ServiceProvider.GetRequiredService(); } diff --git a/tests/DependencyModules.Tests/RuntimeTests/DependencyRegistryTests.cs b/tests/DependencyModules.Tests/RuntimeTests/DependencyRegistryTests.cs index 04350fe..9d752e6 100644 --- a/tests/DependencyModules.Tests/RuntimeTests/DependencyRegistryTests.cs +++ b/tests/DependencyModules.Tests/RuntimeTests/DependencyRegistryTests.cs @@ -10,8 +10,8 @@ namespace DependencyModules.Tests.RuntimeTests; /// DependencyRegistry keeps its state in static fields on a generic type, so every test here uses /// its own marker type to stay isolated from the others. /// -public class DependencyRegistryTests { - +public class DependencyRegistryTests +{ private interface IThing; private class Thing : IThing; @@ -19,9 +19,12 @@ private class Thing : IThing; private class OtherThing : IThing; [Fact] - public void ApplyServices_RunsRegisteredFunctionsInOrder() { + public void ApplyServices_RunsRegisteredFunctionsInOrder() + { DependencyRegistry.Add(services => services.AddSingleton()); - DependencyRegistry.Add(services => services.AddSingleton()); + DependencyRegistry.Add(services => + services.AddSingleton() + ); var collection = new ServiceCollection(); DependencyRegistry.ApplyServices(collection); @@ -34,8 +37,11 @@ public void ApplyServices_RunsRegisteredFunctionsInOrder() { private class OrderMarker; [Fact] - public void Registry_IsIsolatedPerTypeArgument() { - DependencyRegistry.Add(services => services.AddSingleton()); + public void Registry_IsIsolatedPerTypeArgument() + { + DependencyRegistry.Add(services => + services.AddSingleton() + ); var collection = new ServiceCollection(); DependencyRegistry.ApplyServices(collection); @@ -48,7 +54,8 @@ private class IsolationMarkerA; private class IsolationMarkerB; [Fact] - public void Add_WithFactory_RegistersWithRequestedLifetime() { + public void Add_WithFactory_RegistersWithRequestedLifetime() + { DependencyRegistry.Add(_ => new Thing(), ServiceLifetime.Scoped); var collection = new ServiceCollection(); @@ -63,8 +70,13 @@ public void Add_WithFactory_RegistersWithRequestedLifetime() { private class FactoryMarker; [Fact] - public void Add_WithImplementationTypeAndKey_RegistersKeyedService() { - DependencyRegistry.Add(typeof(Thing), ServiceLifetime.Singleton, "the-key"); + public void Add_WithImplementationTypeAndKey_RegistersKeyedService() + { + DependencyRegistry.Add( + typeof(Thing), + ServiceLifetime.Singleton, + "the-key" + ); var collection = new ServiceCollection(); DependencyRegistry.ApplyServices(collection); @@ -84,7 +96,8 @@ private class KeyedMarker; /// ordering has to hold across every decorator in the registry, not just within one group. /// [Fact] - public void ApplyDecorators_AppliesInAscendingOrder() { + public void ApplyDecorators_AppliesInAscendingOrder() + { var applied = new List(); DependencyRegistry.AddDecorator(_ => applied.Add("third"), 30); @@ -99,7 +112,8 @@ public void ApplyDecorators_AppliesInAscendingOrder() { private class OrderedDecoratorMarker; [Fact] - public void ApplyDecorators_WithoutAnOrder_AppliesInRegistrationOrder() { + public void ApplyDecorators_WithoutAnOrder_AppliesInRegistrationOrder() + { var applied = new List(); DependencyRegistry.AddDecorator(_ => applied.Add("first")); @@ -117,7 +131,8 @@ private class UnorderedDecoratorMarker; /// reproducible between runs. /// [Fact] - public void ApplyDecorators_WithEqualOrders_KeepsRegistrationOrder() { + public void ApplyDecorators_WithEqualOrders_KeepsRegistrationOrder() + { var applied = new List(); DependencyRegistry.AddDecorator(_ => applied.Add("first"), 5); @@ -132,12 +147,16 @@ public void ApplyDecorators_WithEqualOrders_KeepsRegistrationOrder() { private class StableDecoratorMarker; [Fact] - public void ApplyDecorators_MixesOrderedAndUnorderedRegistrations() { + public void ApplyDecorators_MixesOrderedAndUnorderedRegistrations() + { var applied = new List(); // An unordered decorator defaults to 0, so a negative order still sits inside it. DependencyRegistry.AddDecorator(_ => applied.Add("default")); - DependencyRegistry.AddDecorator(_ => applied.Add("application"), 1000); + DependencyRegistry.AddDecorator( + _ => applied.Add("application"), + 1000 + ); DependencyRegistry.AddDecorator(_ => applied.Add("innermost"), -10); DependencyRegistry.ApplyDecorators(new ServiceCollection()); @@ -148,9 +167,12 @@ public void ApplyDecorators_MixesOrderedAndUnorderedRegistrations() { private class MixedDecoratorMarker; [Fact] - public void ApplyDecorators_RunsSeparatelyFromServices() { + public void ApplyDecorators_RunsSeparatelyFromServices() + { DependencyRegistry.Add(services => services.AddSingleton()); - DependencyRegistry.AddDecorator(services => services.AddSingleton()); + DependencyRegistry.AddDecorator(services => + services.AddSingleton() + ); var servicesOnly = new ServiceCollection(); DependencyRegistry.ApplyServices(servicesOnly); @@ -165,7 +187,8 @@ public void ApplyDecorators_RunsSeparatelyFromServices() { private class DecoratorMarker; [Fact] - public void GetModules_WithNoRegisteredModules_ReturnsSuppliedModules() { + public void GetModules_WithNoRegisteredModules_ReturnsSuppliedModules() + { var module = new StubModule(); var result = DependencyRegistry.GetModules(module); @@ -174,7 +197,8 @@ public void GetModules_WithNoRegisteredModules_ReturnsSuppliedModules() { } [Fact] - public void GetModules_ConcatenatesRegisteredAndSuppliedModules() { + public void GetModules_ConcatenatesRegisteredAndSuppliedModules() + { var registered = new StubModule(); var supplied = new StubModule(); DependencyRegistry.AddModule(registered); @@ -196,45 +220,69 @@ private class GetModulesConcatMarker; /// thread can be enumerating the same list in ApplyServices. /// [Fact] - public void Add_IsSafeUnderConcurrentWritersAndReaders() { + public void Add_IsSafeUnderConcurrentWritersAndReaders() + { const int writerCount = 8; const int perWriter = 250; var failures = new ConcurrentBag(); using var start = new ManualResetEventSlim(false); - var writers = Enumerable.Range(0, writerCount).Select(_ => new Thread(() => { - try { - start.Wait(TestContext.Current.CancellationToken); - for (var i = 0; i < perWriter; i++) { - DependencyRegistry.Add(services => services.AddSingleton()); + var writers = Enumerable + .Range(0, writerCount) + .Select(_ => new Thread(() => + { + try + { + start.Wait(TestContext.Current.CancellationToken); + for (var i = 0; i < perWriter; i++) + { + DependencyRegistry.Add(services => + services.AddSingleton() + ); + } } - } - catch (Exception e) { - failures.Add(e); - } - })).ToArray(); - - var readers = Enumerable.Range(0, 4).Select(_ => new Thread(() => { - try { - start.Wait(TestContext.Current.CancellationToken); - for (var i = 0; i < perWriter; i++) { - DependencyRegistry.ApplyServices(new ServiceCollection()); + catch (Exception e) + { + failures.Add(e); } - } - catch (Exception e) { - failures.Add(e); - } - })).ToArray(); + })) + .ToArray(); + + var readers = Enumerable + .Range(0, 4) + .Select(_ => new Thread(() => + { + try + { + start.Wait(TestContext.Current.CancellationToken); + for (var i = 0; i < perWriter; i++) + { + DependencyRegistry.ApplyServices( + new ServiceCollection() + ); + } + } + catch (Exception e) + { + failures.Add(e); + } + })) + .ToArray(); - foreach (var thread in writers.Concat(readers)) { + foreach (var thread in writers.Concat(readers)) + { thread.Start(); } start.Set(); - foreach (var thread in writers.Concat(readers)) { - Assert.True(thread.Join(TimeSpan.FromSeconds(30)), "A registry thread did not finish in time."); + foreach (var thread in writers.Concat(readers)) + { + Assert.True( + thread.Join(TimeSpan.FromSeconds(30)), + "A registry thread did not finish in time." + ); } Assert.Empty(failures); @@ -247,42 +295,59 @@ public void Add_IsSafeUnderConcurrentWritersAndReaders() { private class ConcurrencyMarker; [Fact] - public void AddModule_IsSafeUnderConcurrentWriters() { + public void AddModule_IsSafeUnderConcurrentWriters() + { const int writerCount = 8; const int perWriter = 100; var failures = new ConcurrentBag(); using var start = new ManualResetEventSlim(false); - var threads = Enumerable.Range(0, writerCount).Select(_ => new Thread(() => { - try { - start.Wait(TestContext.Current.CancellationToken); - for (var i = 0; i < perWriter; i++) { - DependencyRegistry.AddModule(new StubModule()); + var threads = Enumerable + .Range(0, writerCount) + .Select(_ => new Thread(() => + { + try + { + start.Wait(TestContext.Current.CancellationToken); + for (var i = 0; i < perWriter; i++) + { + DependencyRegistry.AddModule(new StubModule()); + } } - } - catch (Exception e) { - failures.Add(e); - } - })).ToArray(); + catch (Exception e) + { + failures.Add(e); + } + })) + .ToArray(); - foreach (var thread in threads) { + foreach (var thread in threads) + { thread.Start(); } start.Set(); - foreach (var thread in threads) { - Assert.True(thread.Join(TimeSpan.FromSeconds(30)), "A registry thread did not finish in time."); + foreach (var thread in threads) + { + Assert.True( + thread.Join(TimeSpan.FromSeconds(30)), + "A registry thread did not finish in time." + ); } Assert.Empty(failures); - Assert.Equal(writerCount * perWriter, DependencyRegistry.GetModules().Count()); + Assert.Equal( + writerCount * perWriter, + DependencyRegistry.GetModules().Count() + ); } private class ModuleConcurrencyMarker; - private class StubModule : IDependencyModule { + private class StubModule : IDependencyModule + { public void PopulateServiceCollection(IServiceCollection serviceCollection) { } } @@ -293,30 +358,39 @@ public void PopulateServiceCollection(IServiceCollection serviceCollection) { } /// unnoticed. /// [Fact] - public void ApplyServices_UsesAnEnvironmentAlreadyInTheCollection() { + public void ApplyServices_UsesAnEnvironmentAlreadyInTheCollection() + { DependencyRegistry.Add( - (services, environment) => { - if (environment.EnvironmentName == "Development") { + (services, environment) => + { + if (environment.EnvironmentName == "Development") + { services.AddSingleton(); } - }); + } + ); var collection = new ServiceCollection(); collection.AddSingleton(new StubEnvironment("Development")); DependencyRegistry.ApplyServices(collection); - Assert.Contains(collection, descriptor => descriptor.ImplementationType == typeof(OtherThing)); + Assert.Contains( + collection, + descriptor => descriptor.ImplementationType == typeof(OtherThing) + ); } private class SuppliedEnvironmentMarker; [Fact] - public void ApplyDecorators_UsesAnEnvironmentAlreadyInTheCollection() { + public void ApplyDecorators_UsesAnEnvironmentAlreadyInTheCollection() + { var seen = ""; DependencyRegistry.AddDecorator( - (EnvironmentRegistryFunc)((_, environment) => seen = environment.EnvironmentName)); + (EnvironmentRegistryFunc)((_, environment) => seen = environment.EnvironmentName) + ); var collection = new ServiceCollection(); collection.AddSingleton(new StubEnvironment("Staging")); @@ -335,23 +409,28 @@ private class SuppliedDecoratorEnvironmentMarker; [Theory] [InlineData(true)] [InlineData(false)] - public void ApplyServices_RefusesAnEnvironmentItCannotUse(bool registeredByType) { + public void ApplyServices_RefusesAnEnvironmentItCannotUse(bool registeredByType) + { var collection = new ServiceCollection(); - if (registeredByType) { + if (registeredByType) + { collection.AddSingleton(); } - else { + else + { collection.AddSingleton(_ => new StubEnvironment("Development")); } - Assert.Throws( - () => DependencyRegistry.ApplyServices(collection)); + Assert.Throws(() => + DependencyRegistry.ApplyServices(collection) + ); } private class RefusedEnvironmentMarker; - private class StubEnvironment(string name) : IModuleEnvironment { + private class StubEnvironment(string name) : IModuleEnvironment + { public string EnvironmentName => name; public string? Value(string valueName) => null; diff --git a/tests/DependencyModules.Tests/RuntimeTests/FeatureApplicatorTests.cs b/tests/DependencyModules.Tests/RuntimeTests/FeatureApplicatorTests.cs index fa3801f..3bf343d 100644 --- a/tests/DependencyModules.Tests/RuntimeTests/FeatureApplicatorTests.cs +++ b/tests/DependencyModules.Tests/RuntimeTests/FeatureApplicatorTests.cs @@ -10,12 +10,13 @@ namespace DependencyModules.Tests.RuntimeTests; /// modules carrying that feature. It must hand the handler exactly the modules that implement the /// feature type, and nothing else. /// -public class FeatureApplicatorTests { - +public class FeatureApplicatorTests +{ private interface ISomeFeature; [Fact] - public void Apply_PassesOnlyModulesImplementingTheFeature() { + public void Apply_PassesOnlyModulesImplementingTheFeature() + { var handler = new RecordingHandler(); var applicator = new FeatureApplicator(handler); @@ -29,7 +30,8 @@ public void Apply_PassesOnlyModulesImplementingTheFeature() { } [Fact] - public void Apply_WithNoMatchingModules_PassesAnEmptySequence() { + public void Apply_WithNoMatchingModules_PassesAnEmptySequence() + { var handler = new RecordingHandler(); var applicator = new FeatureApplicator(handler); @@ -39,7 +41,8 @@ public void Apply_WithNoMatchingModules_PassesAnEmptySequence() { } [Fact] - public void Apply_PassesTheServiceCollectionThrough() { + public void Apply_PassesTheServiceCollectionThrough() + { var handler = new RecordingHandler(); var applicator = new FeatureApplicator(handler); var collection = new ServiceCollection(); @@ -50,18 +53,23 @@ public void Apply_PassesTheServiceCollectionThrough() { } [Fact] - public void Order_ComesFromTheHandler() { - var applicator = new FeatureApplicator(new RecordingHandler { HandlerOrder = 42 }); + public void Order_ComesFromTheHandler() + { + var applicator = new FeatureApplicator( + new RecordingHandler { HandlerOrder = 42 } + ); Assert.Equal(42, applicator.Order); } [Fact] - public void Order_DefaultsToZero() { + public void Order_DefaultsToZero() + { Assert.Equal(0, new FeatureApplicator(new DefaultOrderHandler()).Order); } - private class RecordingHandler : IDependencyModuleFeature { + private class RecordingHandler : IDependencyModuleFeature + { public int HandlerOrder { get; init; } public int Order => HandlerOrder; @@ -70,21 +78,28 @@ private class RecordingHandler : IDependencyModuleFeature { public IServiceCollection? ReceivedCollection { get; private set; } - public void HandleFeature(IServiceCollection collection, IEnumerable feature) { + public void HandleFeature(IServiceCollection collection, IEnumerable feature) + { ReceivedCollection = collection; Received = feature.ToList(); } } - private class DefaultOrderHandler : IDependencyModuleFeature { - public void HandleFeature(IServiceCollection collection, IEnumerable feature) { } + private class DefaultOrderHandler : IDependencyModuleFeature + { + public void HandleFeature( + IServiceCollection collection, + IEnumerable feature + ) { } } - private class FeatureModule : IDependencyModule, ISomeFeature { + private class FeatureModule : IDependencyModule, ISomeFeature + { public void PopulateServiceCollection(IServiceCollection serviceCollection) { } } - private class PlainModule : IDependencyModule { + private class PlainModule : IDependencyModule + { public void PopulateServiceCollection(IServiceCollection serviceCollection) { } } } diff --git a/tests/DependencyModules.Tests/RuntimeTests/InvocationPipelineTests.cs b/tests/DependencyModules.Tests/RuntimeTests/InvocationPipelineTests.cs index 2eace70..c1d810e 100644 --- a/tests/DependencyModules.Tests/RuntimeTests/InvocationPipelineTests.cs +++ b/tests/DependencyModules.Tests/RuntimeTests/InvocationPipelineTests.cs @@ -17,9 +17,10 @@ namespace DependencyModules.Tests.RuntimeTests; /// named after their member, because overloads would collide; argument fields are _arg0 /// onwards, because a parameter named Self or a keyword would collide too. /// -public class InvocationPipelineTests { - - public interface IWork { +public class InvocationPipelineTests +{ + public interface IWork + { int Double(int value, string label); void Record(string entry); @@ -30,7 +31,8 @@ public interface IWork { } [Fact] - public void SyncMember_ReturnsTheInnerResultThroughBothInterceptors() { + public void SyncMember_ReturnsTheInnerResultThroughBothInterceptors() + { var fixture = new Fixture(); var result = fixture.Service.Double(21, "label"); @@ -43,21 +45,26 @@ public void SyncMember_ReturnsTheInnerResultThroughBothInterceptors() { /// Interceptors nest: the first declared wraps the second, so it enters first and exits last. /// [Fact] - public void SeveralInterceptors_NestInDeclarationOrder() { + public void SeveralInterceptors_NestInDeclarationOrder() + { var fixture = new Fixture(); fixture.Service.Double(1, "label"); - Assert.Equal([ - "first enter IWork.Double", - "second enter IWork.Double", - "second exit IWork.Double", - "first exit IWork.Double" - ], fixture.Log); + Assert.Equal( + [ + "first enter IWork.Double", + "second enter IWork.Double", + "second exit IWork.Double", + "first exit IWork.Double", + ], + fixture.Log + ); } [Fact] - public void VoidMember_RoundTripsThroughNoResult() { + public void VoidMember_RoundTripsThroughNoResult() + { var fixture = new Fixture(); fixture.Service.Record("entry"); @@ -71,7 +78,8 @@ public void VoidMember_RoundTripsThroughNoResult() { /// through the indexer are the fields the last stage passes on. /// [Fact] - public void WritingAnArgument_ReplacesWhatTheImplementationReceives() { + public void WritingAnArgument_ReplacesWhatTheImplementationReceives() + { var fixture = new Fixture(); fixture.First.BeforeProceed = arguments => arguments[0] = 5; @@ -82,12 +90,15 @@ public void WritingAnArgument_ReplacesWhatTheImplementationReceives() { } [Fact] - public void Arguments_ReadByPositionAndName() { + public void Arguments_ReadByPositionAndName() + { var fixture = new Fixture(); var seen = new List(); - fixture.First.BeforeProceed = arguments => { - for (var i = 0; i < arguments.Count; i++) { + fixture.First.BeforeProceed = arguments => + { + for (var i = 0; i < arguments.Count; i++) + { seen.Add($"{arguments.NameAt(i)}={arguments[i]}"); } }; @@ -98,7 +109,8 @@ public void Arguments_ReadByPositionAndName() { } [Fact] - public void Caller_CarriesTheInterfaceAndTheMember() { + public void Caller_CarriesTheInterfaceAndTheMember() + { var fixture = new Fixture(); CallerInfo caller = default; @@ -115,7 +127,8 @@ public void Caller_CarriesTheInterfaceAndTheMember() { /// same next stage. A mutable index would walk past it and call the implementation once. /// [Fact] - public void ProceedingTwice_ReEntersTheSameStage() { + public void ProceedingTwice_ReEntersTheSameStage() + { var fixture = new Fixture(); fixture.First.ProceedCount = 2; @@ -127,7 +140,8 @@ public void ProceedingTwice_ReEntersTheSameStage() { } [Fact] - public void NotProceeding_SkipsTheImplementationAndEverythingBelow() { + public void NotProceeding_SkipsTheImplementationAndEverythingBelow() + { var fixture = new Fixture(); fixture.First.Substitute = 7; @@ -139,14 +153,17 @@ public void NotProceeding_SkipsTheImplementationAndEverythingBelow() { } [Fact] - public void AnException_PropagatesThroughThePipeline() { + public void AnException_PropagatesThroughThePipeline() + { var fixture = new Fixture(); fixture.Implementation.Throw = true; Assert.Throws(() => fixture.Service.Double(1, "label")); - Assert.Equal(["second exit IWork.Double", "first exit IWork.Double"], - fixture.Log.Where(entry => entry.Contains("exit")).ToArray()); + Assert.Equal( + ["second exit IWork.Double", "first exit IWork.Double"], + fixture.Log.Where(entry => entry.Contains("exit")).ToArray() + ); } /// @@ -155,7 +172,8 @@ public void AnException_PropagatesThroughThePipeline() { /// has finished. /// [Fact] - public async Task AsyncMember_ExitsWhenTheWorkFinishesRatherThanWhenTheTaskIsHandedBack() { + public async Task AsyncMember_ExitsWhenTheWorkFinishesRatherThanWhenTheTaskIsHandedBack() + { var fixture = new Fixture(); var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); fixture.Implementation.ComputeGate = gate.Task; @@ -170,12 +188,15 @@ public async Task AsyncMember_ExitsWhenTheWorkFinishesRatherThanWhenTheTaskIsHan var result = await task; Assert.Equal(42, result); - Assert.Equal([ - "first enter IWork.ComputeAsync", - "second enter IWork.ComputeAsync", - "second exit IWork.ComputeAsync", - "first exit IWork.ComputeAsync" - ], fixture.Log); + Assert.Equal( + [ + "first enter IWork.ComputeAsync", + "second enter IWork.ComputeAsync", + "second exit IWork.ComputeAsync", + "first exit IWork.ComputeAsync", + ], + fixture.Log + ); } /// @@ -183,33 +204,39 @@ public async Task AsyncMember_ExitsWhenTheWorkFinishesRatherThanWhenTheTaskIsHan /// ordinary value would observe the construction of the iterator and nothing else. /// [Fact] - public async Task StreamMember_ObservesEachItemAsItIsProduced() { + public async Task StreamMember_ObservesEachItemAsItIsProduced() + { var fixture = new Fixture(); var items = new List(); - await foreach (var item in fixture.Service.Stream(3)) { + await foreach (var item in fixture.Service.Stream(3)) + { items.Add(item); } Assert.Equal([0, 1, 2], items); - Assert.Equal([ - "first enter IWork.Stream", - "second enter IWork.Stream", - "second item 0", - "first item 0", - "second item 1", - "first item 1", - "second item 2", - "first item 2", - "second exit IWork.Stream", - "first exit IWork.Stream" - ], fixture.Log); + Assert.Equal( + [ + "first enter IWork.Stream", + "second enter IWork.Stream", + "second item 0", + "first item 0", + "second item 1", + "first item 1", + "second item 2", + "first item 2", + "second exit IWork.Stream", + "first exit IWork.Stream", + ], + fixture.Log + ); } - private sealed class Fixture { - - public Fixture() { + private sealed class Fixture + { + public Fixture() + { Log = []; Implementation = new WorkImplementation(); First = new TestInterceptor("first", Log); @@ -228,23 +255,26 @@ public Fixture() { public IWork Service { get; } } - private sealed class WorkImplementation : IWork { - + private sealed class WorkImplementation : IWork + { public List Calls { get; } = []; public bool Throw { get; set; } - public int Double(int value, string label) { + public int Double(int value, string label) + { Calls.Add($"Double({value}, {label})"); - if (Throw) { + if (Throw) + { throw new InvalidOperationException("boom"); } return value * 2; } - public void Record(string entry) { + public void Record(string entry) + { Calls.Add($"Record({entry})"); } @@ -255,7 +285,8 @@ public void Record(string entry) { /// public Task ComputeGate { get; set; } = Task.CompletedTask; - public async Task ComputeAsync(int value) { + public async Task ComputeAsync(int value) + { await ComputeGate; Calls.Add($"ComputeAsync({value})"); @@ -263,8 +294,10 @@ public async Task ComputeAsync(int value) { return value * 2; } - public async IAsyncEnumerable Stream(int count) { - for (var i = 0; i < count; i++) { + public async IAsyncEnumerable Stream(int count) + { + for (var i = 0; i < count; i++) + { await Task.Yield(); yield return i; @@ -277,8 +310,10 @@ public async IAsyncEnumerable Stream(int count) { /// rather than expressed as another type, so the wrapper's typed fields stay one type. /// private sealed class TestInterceptor(string name, List log) - : IInterceptor, IAsyncInterceptor, IAsyncEnumerableInterceptor { - + : IInterceptor, + IAsyncInterceptor, + IAsyncEnumerableInterceptor + { public Action? BeforeProceed { get; set; } public Action? BeforeCall { get; set; } @@ -287,56 +322,74 @@ private sealed class TestInterceptor(string name, List log) public object? Substitute { get; set; } - public TResult Intercept(InvocationContext context) { + public TResult Intercept(InvocationContext context) + { log.Add($"{name} enter {context.Caller}"); BeforeCall?.Invoke(context.Caller); BeforeProceed?.Invoke(context.Arguments); - if (Substitute != null) { + if (Substitute != null) + { return (TResult)Substitute; } - try { + try + { var result = default(TResult)!; - for (var i = 0; i < ProceedCount; i++) { + for (var i = 0; i < ProceedCount; i++) + { result = context.Proceed(); } return result; - } finally { + } + finally + { log.Add($"{name} exit {context.Caller}"); } } - public async ValueTask InterceptAsync(AsyncInvocationContext context) { + public async ValueTask InterceptAsync( + AsyncInvocationContext context + ) + { log.Add($"{name} enter {context.Caller}"); BeforeCall?.Invoke(context.Caller); BeforeProceed?.Invoke(context.Arguments); - if (Substitute != null) { + if (Substitute != null) + { return (TResult)Substitute; } - try { + try + { var result = default(TResult)!; - for (var i = 0; i < ProceedCount; i++) { + for (var i = 0; i < ProceedCount; i++) + { result = await context.ProceedAsync(); } return result; - } finally { + } + finally + { log.Add($"{name} exit {context.Caller}"); } } - public async IAsyncEnumerable InterceptStream(StreamInvocationContext context) { + public async IAsyncEnumerable InterceptStream( + StreamInvocationContext context + ) + { log.Add($"{name} enter {context.Caller}"); BeforeCall?.Invoke(context.Caller); BeforeProceed?.Invoke(context.Arguments); - await foreach (var item in context.Proceed()) { + await foreach (var item in context.Proceed()) + { log.Add($"{name} item {item}"); yield return item; @@ -349,7 +402,8 @@ public async IAsyncEnumerable InterceptStream(StreamInvocationCont /// /// Stands in for generated output. Every construct here is one the generator emits. /// - private sealed class Work_Intercepted : IWork { + private sealed class Work_Intercepted : IWork + { private readonly IWork _inner; private readonly TestInterceptor _i0; private readonly TestInterceptor _i1; @@ -359,42 +413,49 @@ private sealed class Work_Intercepted : IWork { private static readonly CallerInfo Caller2 = new(typeof(IWork), "ComputeAsync"); private static readonly CallerInfo Caller3 = new(typeof(IWork), "Stream"); - public Work_Intercepted(IWork inner, TestInterceptor i0, TestInterceptor i1) { + public Work_Intercepted(IWork inner, TestInterceptor i0, TestInterceptor i1) + { _inner = inner; _i0 = i0; _i1 = i1; } - public int Double(int value, string label) { + public int Double(int value, string label) + { var state = new State0(this, value, label); return state.Invoke(0); } - public void Record(string entry) { + public void Record(string entry) + { var state = new State1(this, entry); state.Invoke(0); } - public Task ComputeAsync(int value) { + public Task ComputeAsync(int value) + { var state = new State2(this, value); return state.Invoke(0).AsTask(); } - public IAsyncEnumerable Stream(int count) { + public IAsyncEnumerable Stream(int count) + { var state = new State3(this, count); return state.Invoke(0); } - private sealed class State0 : InvocationState { + private sealed class State0 : InvocationState + { private readonly Work_Intercepted _self; private int _arg0; private string _arg1; - public State0(Work_Intercepted self, int arg0, string arg1) { + public State0(Work_Intercepted self, int arg0, string arg1) + { _self = self; _arg0 = arg0; _arg1 = arg1; @@ -404,15 +465,19 @@ public State0(Work_Intercepted self, int arg0, string arg1) { public override int Count => 2; - public override object? this[int index] { + public override object? this[int index] + { get => - index switch { + index switch + { 0 => _arg0, 1 => _arg1, - _ => throw new ArgumentOutOfRangeException(nameof(index)) + _ => throw new ArgumentOutOfRangeException(nameof(index)), }; - set { - switch (index) { + set + { + switch (index) + { case 0: _arg0 = (int)value!; break; @@ -426,14 +491,17 @@ public override object? this[int index] { } public override string NameAt(int index) => - index switch { + index switch + { 0 => "value", 1 => "label", - _ => throw new ArgumentOutOfRangeException(nameof(index)) + _ => throw new ArgumentOutOfRangeException(nameof(index)), }; - public override int Invoke(int stage) { - switch (stage) { + public override int Invoke(int stage) + { + switch (stage) + { case 0: return _self._i0.Intercept(new InvocationContext(this, 0)); case 1: @@ -444,11 +512,13 @@ public override int Invoke(int stage) { } } - private sealed class State1 : InvocationState { + private sealed class State1 : InvocationState + { private readonly Work_Intercepted _self; private string _arg0; - public State1(Work_Intercepted self, string arg0) { + public State1(Work_Intercepted self, string arg0) + { _self = self; _arg0 = arg0; } @@ -457,14 +527,18 @@ public State1(Work_Intercepted self, string arg0) { public override int Count => 1; - public override object? this[int index] { + public override object? this[int index] + { get => - index switch { + index switch + { 0 => _arg0, - _ => throw new ArgumentOutOfRangeException(nameof(index)) + _ => throw new ArgumentOutOfRangeException(nameof(index)), }; - set { - switch (index) { + set + { + switch (index) + { case 0: _arg0 = (string)value!; break; @@ -475,13 +549,16 @@ public override object? this[int index] { } public override string NameAt(int index) => - index switch { + index switch + { 0 => "entry", - _ => throw new ArgumentOutOfRangeException(nameof(index)) + _ => throw new ArgumentOutOfRangeException(nameof(index)), }; - public override NoResult Invoke(int stage) { - switch (stage) { + public override NoResult Invoke(int stage) + { + switch (stage) + { case 0: return _self._i0.Intercept(new InvocationContext(this, 0)); case 1: @@ -494,11 +571,13 @@ public override NoResult Invoke(int stage) { } } - private sealed class State2 : AsyncInvocationState { + private sealed class State2 : AsyncInvocationState + { private readonly Work_Intercepted _self; private int _arg0; - public State2(Work_Intercepted self, int arg0) { + public State2(Work_Intercepted self, int arg0) + { _self = self; _arg0 = arg0; } @@ -507,14 +586,18 @@ public State2(Work_Intercepted self, int arg0) { public override int Count => 1; - public override object? this[int index] { + public override object? this[int index] + { get => - index switch { + index switch + { 0 => _arg0, - _ => throw new ArgumentOutOfRangeException(nameof(index)) + _ => throw new ArgumentOutOfRangeException(nameof(index)), }; - set { - switch (index) { + set + { + switch (index) + { case 0: _arg0 = (int)value!; break; @@ -525,13 +608,16 @@ public override object? this[int index] { } public override string NameAt(int index) => - index switch { + index switch + { 0 => "value", - _ => throw new ArgumentOutOfRangeException(nameof(index)) + _ => throw new ArgumentOutOfRangeException(nameof(index)), }; - public override ValueTask Invoke(int stage) { - switch (stage) { + public override ValueTask Invoke(int stage) + { + switch (stage) + { case 0: return _self._i0.InterceptAsync(new AsyncInvocationContext(this, 0)); case 1: @@ -542,11 +628,13 @@ public override ValueTask Invoke(int stage) { } } - private sealed class State3 : StreamInvocationState { + private sealed class State3 : StreamInvocationState + { private readonly Work_Intercepted _self; private int _arg0; - public State3(Work_Intercepted self, int arg0) { + public State3(Work_Intercepted self, int arg0) + { _self = self; _arg0 = arg0; } @@ -555,14 +643,18 @@ public State3(Work_Intercepted self, int arg0) { public override int Count => 1; - public override object? this[int index] { + public override object? this[int index] + { get => - index switch { + index switch + { 0 => _arg0, - _ => throw new ArgumentOutOfRangeException(nameof(index)) + _ => throw new ArgumentOutOfRangeException(nameof(index)), }; - set { - switch (index) { + set + { + switch (index) + { case 0: _arg0 = (int)value!; break; @@ -573,13 +665,16 @@ public override object? this[int index] { } public override string NameAt(int index) => - index switch { + index switch + { 0 => "count", - _ => throw new ArgumentOutOfRangeException(nameof(index)) + _ => throw new ArgumentOutOfRangeException(nameof(index)), }; - public override IAsyncEnumerable Invoke(int stage) { - switch (stage) { + public override IAsyncEnumerable Invoke(int stage) + { + switch (stage) + { case 0: return _self._i0.InterceptStream(new StreamInvocationContext(this, 0)); case 1: diff --git a/tests/DependencyModules.Tests/RuntimeTests/ModuleEnvironmentTests.cs b/tests/DependencyModules.Tests/RuntimeTests/ModuleEnvironmentTests.cs index f9ab88e..4dd3dd5 100644 --- a/tests/DependencyModules.Tests/RuntimeTests/ModuleEnvironmentTests.cs +++ b/tests/DependencyModules.Tests/RuntimeTests/ModuleEnvironmentTests.cs @@ -1,18 +1,20 @@ -using Microsoft.Extensions.DependencyInjection; using DependencyModules.Runtime; using DependencyModules.Runtime.Helpers; using DependencyModules.Runtime.Interfaces; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace DependencyModules.Tests.RuntimeTests; -public class ModuleEnvironmentTests { - +public class ModuleEnvironmentTests +{ [Fact] - public void ValuesComeBackByName() { + public void ValuesComeBackByName() + { var environment = new ModuleEnvironment( "Development", - new Dictionary { ["A"] = "1", ["Empty"] = "" }); + new Dictionary { ["A"] = "1", ["Empty"] = "" } + ); Assert.Equal("Development", environment.EnvironmentName); Assert.Equal("1", environment.Value("A")); @@ -21,18 +23,17 @@ public void ValuesComeBackByName() { } [Fact] - public void ValuesAreOptional() { + public void ValuesAreOptional() + { var environment = new ModuleEnvironment("Production"); Assert.Null(environment.Value("Anything")); } [Fact] - public void ValuesCanBeWrittenInAnInitializer() { - var environment = new ModuleEnvironment("Development") { - { "A", "1" }, - { "Null", null } - }; + public void ValuesCanBeWrittenInAnInitializer() + { + var environment = new ModuleEnvironment("Development") { { "A", "1" }, { "Null", null } }; Assert.Equal("1", environment.Value("A")); Assert.Null(environment.Value("Null")); @@ -43,11 +44,14 @@ public void ValuesCanBeWrittenInAnInitializer() { /// So a fixed set can be seeded and then adjusted, rather than the two forms being exclusive. /// [Fact] - public void AnInitializerOverridesAValueFromTheConstructor() { + public void AnInitializerOverridesAValueFromTheConstructor() + { var environment = new ModuleEnvironment( "Development", - new Dictionary { ["Seed"] = "original", ["Kept"] = "kept" }) { - { "Seed", "replaced" } + new Dictionary { ["Seed"] = "original", ["Kept"] = "kept" } + ) + { + { "Seed", "replaced" }, }; Assert.Equal("replaced", environment.Value("Seed")); @@ -58,7 +62,8 @@ public void AnInitializerOverridesAValueFromTheConstructor() { /// The values are copied, so the dictionary the caller still holds is not written to. /// [Fact] - public void AddDoesNotWriteToTheCallersDictionary() { + public void AddDoesNotWriteToTheCallersDictionary() + { var values = new Dictionary { ["A"] = "1" }; var environment = new ModuleEnvironment("Development", values) { { "B", "2" } }; @@ -71,9 +76,11 @@ public void AddDoesNotWriteToTheCallersDictionary() { /// so copying the values must not quietly reset it to ordinal. /// [Fact] - public void ACallersComparerSurvivesTheCopy() { - var values = new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["Key"] = "value" + public void ACallersComparerSurvivesTheCopy() + { + var values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Key"] = "value", }; var environment = new ModuleEnvironment("Development", values); @@ -82,22 +89,22 @@ public void ACallersComparerSurvivesTheCopy() { } [Fact] - public void ValuesEnumerate() { - var environment = new ModuleEnvironment("Development") { - { "A", "1" }, - { "B", "2" } - }; + public void ValuesEnumerate() + { + var environment = new ModuleEnvironment("Development") { { "A", "1" }, { "B", "2" } }; Assert.Equal( new Dictionary { ["A"] = "1", ["B"] = "2" }, - environment.ToDictionary(pair => pair.Key, pair => pair.Value)); + environment.ToDictionary(pair => pair.Key, pair => pair.Value) + ); } /// /// Shared by every application in the process, so it cannot be one of the mutable ones. /// [Fact] - public void NoneCannotBeGivenValues() { + public void NoneCannotBeGivenValues() + { Assert.IsNotType(ModuleEnvironment.None); } @@ -107,10 +114,12 @@ public void NoneCannotBeGivenValues() { private static string UniqueKey() => "DM_TEST_" + Guid.NewGuid().ToString("N"); [Fact] - public void AKeyNotSuppliedFallsBackToAnEnvironmentVariable() { + public void AKeyNotSuppliedFallsBackToAnEnvironmentVariable() + { var key = UniqueKey(); - try { + try + { // Set before the environment is built. An instance caches what it reads, so reading the // key first and setting the variable afterwards would be testing the cache instead of // the fallback — see FallBackToTheProcessIsCachedPerInstance for that. @@ -121,22 +130,28 @@ public void AKeyNotSuppliedFallsBackToAnEnvironmentVariable() { Assert.Equal("from-process", environment.Value(key)); Assert.Equal("value", environment.Value("Supplied")); Assert.Null(environment.Value(UniqueKey())); - } finally { + } + finally + { Environment.SetEnvironmentVariable(key, null); } } [Fact] - public void ASuppliedValueWinsOverAnEnvironmentVariable() { + public void ASuppliedValueWinsOverAnEnvironmentVariable() + { var key = UniqueKey(); - try { + try + { Environment.SetEnvironmentVariable(key, "from-process"); var environment = new ModuleEnvironment("Development") { { key, "supplied" } }; Assert.Equal("supplied", environment.Value(key)); - } finally { + } + finally + { Environment.SetEnvironmentVariable(key, null); } } @@ -145,33 +160,44 @@ public void ASuppliedValueWinsOverAnEnvironmentVariable() { /// Saying a key has no value is how an environment variable of the same name is hidden. /// [Fact] - public void ASuppliedNullHidesAnEnvironmentVariable() { + public void ASuppliedNullHidesAnEnvironmentVariable() + { var key = UniqueKey(); - try { + try + { Environment.SetEnvironmentVariable(key, "from-process"); var environment = new ModuleEnvironment("Development") { { key, null } }; Assert.Null(environment.Value(key)); Assert.False(EnvironmentConditions.HasValue(environment, key)); - } finally { + } + finally + { Environment.SetEnvironmentVariable(key, null); } } [Fact] - public void FallBackCanBeTurnedOff() { + public void FallBackCanBeTurnedOff() + { var key = UniqueKey(); - try { + try + { Environment.SetEnvironmentVariable(key, "from-process"); - var environment = new ModuleEnvironment(false, "Development") { { "Supplied", "value" } }; + var environment = new ModuleEnvironment(false, "Development") + { + { "Supplied", "value" }, + }; Assert.Null(environment.Value(key)); Assert.Equal("value", environment.Value("Supplied")); - } finally { + } + finally + { Environment.SetEnvironmentVariable(key, null); } } @@ -181,11 +207,13 @@ public void FallBackCanBeTurnedOff() { /// constructor that takes a dictionary. /// [Fact] - public void FallBackCanBeTurnedOffWithValuesSuppliedUpFront() { + public void FallBackCanBeTurnedOffWithValuesSuppliedUpFront() + { var environment = new ModuleEnvironment( false, "Development", - new Dictionary { ["A"] = "1" }); + new Dictionary { ["A"] = "1" } + ); Assert.Equal("Development", environment.EnvironmentName); Assert.Equal("1", environment.Value("A")); @@ -195,20 +223,25 @@ public void FallBackCanBeTurnedOffWithValuesSuppliedUpFront() { /// An empty name and no values, whatever the machine running this has set. /// [Fact] - public void NoneDoesNotFallBack() { + public void NoneDoesNotFallBack() + { var key = UniqueKey(); - try { + try + { Environment.SetEnvironmentVariable(key, "from-process"); Assert.Null(ModuleEnvironment.None.Value(key)); - } finally { + } + finally + { Environment.SetEnvironmentVariable(key, null); } } [Fact] - public void NoneHasNoNameAndNoValues() { + public void NoneHasNoNameAndNoValues() + { Assert.Equal("", ModuleEnvironment.None.EnvironmentName); Assert.Null(ModuleEnvironment.None.Value("Anything")); } @@ -217,17 +250,21 @@ public void NoneHasNoNameAndNoValues() { /// A fresh default reads the process as it is now, which is what asking again is for. /// [Fact] - public void DefaultReadsValuesFromTheProcess() { + public void DefaultReadsValuesFromTheProcess() + { // Uniquely named so nothing else in the suite can be looking at it. var key = "DM_TEST_" + Guid.NewGuid().ToString("N"); Assert.Null(ModuleEnvironment.CreateDefault().Value(key)); - try { + try + { Environment.SetEnvironmentVariable(key, "set-after-startup"); Assert.Equal("set-after-startup", ModuleEnvironment.CreateDefault().Value(key)); - } finally { + } + finally + { Environment.SetEnvironmentVariable(key, null); } } @@ -242,14 +279,16 @@ public void DefaultReadsValuesFromTheProcess() { /// default exists for, and re-reading it each call would leave the common path uncached. /// [Fact] - public void AHeldDefaultCachesWhatItRead() { + public void AHeldDefaultCachesWhatItRead() + { var key = "DM_TEST_" + Guid.NewGuid().ToString("N"); var held = ModuleEnvironment.CreateDefault(); Assert.Null(held.Value(key)); - try { + try + { Environment.SetEnvironmentVariable(key, "set-after-the-read"); // The miss was cached, so this instance keeps answering with what it saw. @@ -257,7 +296,9 @@ public void AHeldDefaultCachesWhatItRead() { // Asking for a new one is how a current view is obtained. Assert.Equal("set-after-the-read", ModuleEnvironment.CreateDefault().Value(key)); - } finally { + } + finally + { Environment.SetEnvironmentVariable(key, null); } } @@ -266,10 +307,12 @@ public void AHeldDefaultCachesWhatItRead() { /// The fallback on a named environment caches the same way. /// [Fact] - public void FallBackToTheProcessIsCachedPerInstance() { + public void FallBackToTheProcessIsCachedPerInstance() + { var key = "DM_TEST_" + Guid.NewGuid().ToString("N"); - try { + try + { Environment.SetEnvironmentVariable(key, "first"); var environment = new ModuleEnvironment("Development"); @@ -280,7 +323,9 @@ public void FallBackToTheProcessIsCachedPerInstance() { Assert.Equal("first", environment.Value(key)); Assert.Equal("second", new ModuleEnvironment("Development").Value(key)); - } finally { + } + finally + { Environment.SetEnvironmentVariable(key, null); } } @@ -289,10 +334,12 @@ public void FallBackToTheProcessIsCachedPerInstance() { /// Caching the process must not make the environment report values nobody supplied. /// [Fact] - public void CachedProcessValuesDoNotAppearInEnumeration() { + public void CachedProcessValuesDoNotAppearInEnumeration() + { var key = "DM_TEST_" + Guid.NewGuid().ToString("N"); - try { + try + { Environment.SetEnvironmentVariable(key, "from-process"); var environment = new ModuleEnvironment("Development") { { "Supplied", "yes" } }; @@ -301,8 +348,11 @@ public void CachedProcessValuesDoNotAppearInEnumeration() { Assert.Equal( new Dictionary { ["Supplied"] = "yes" }, - environment.ToDictionary(pair => pair.Key, pair => pair.Value)); - } finally { + environment.ToDictionary(pair => pair.Key, pair => pair.Value) + ); + } + finally + { Environment.SetEnvironmentVariable(key, null); } } @@ -317,8 +367,8 @@ public void CachedProcessValuesDoNotAppearInEnumeration() { /// rather than against a literal name, for this reason. /// [Collection("ProcessEnvironment")] -public class ModuleEnvironmentDefaultNameTests { - +public class ModuleEnvironmentDefaultNameTests +{ private const string AspNetCore = "ASPNETCORE_ENVIRONMENT"; private const string DotNet = "DOTNET_ENVIRONMENT"; @@ -328,16 +378,24 @@ public class ModuleEnvironmentDefaultNameTests { [InlineData(null, "Staging", "Staging")] // ASPNETCORE_ENVIRONMENT wins, matching how a web host resolves it. [InlineData("Development", "Staging", "Development")] - public void DefaultResolvesTheEnvironmentName(string? aspNetCore, string? dotNet, string expected) { + public void DefaultResolvesTheEnvironmentName( + string? aspNetCore, + string? dotNet, + string expected + ) + { var originalAspNetCore = Environment.GetEnvironmentVariable(AspNetCore); var originalDotNet = Environment.GetEnvironmentVariable(DotNet); - try { + try + { Environment.SetEnvironmentVariable(AspNetCore, aspNetCore); Environment.SetEnvironmentVariable(DotNet, dotNet); Assert.Equal(expected, ModuleEnvironment.CreateDefault().EnvironmentName); - } finally { + } + finally + { Environment.SetEnvironmentVariable(AspNetCore, originalAspNetCore); Environment.SetEnvironmentVariable(DotNet, originalDotNet); } @@ -352,24 +410,30 @@ public void DefaultResolvesTheEnvironmentName(string? aspNetCore, string? dotNet /// single answer; several would need a rule for which one the conditions read, and that rule would /// fall out of module ordering rather than out of anything the developer wrote. /// -public class EnvironmentDiscoveryTests { - - private class StubEnvironment(string name) : IModuleEnvironment { +public class EnvironmentDiscoveryTests +{ + private class StubEnvironment(string name) : IModuleEnvironment + { public string EnvironmentName => name; + public string? Value(string valueName) => null; } - private class ProbeModule : IDependencyModule, IEnvironmentServiceCollectionConfiguration { + private class ProbeModule : IDependencyModule, IEnvironmentServiceCollectionConfiguration + { public IModuleEnvironment? Seen { get; private set; } public void PopulateServiceCollection(IServiceCollection serviceCollection) { } - public void ConfigureServices(IServiceCollection services, IModuleEnvironment environment) => - Seen = environment; + public void ConfigureServices( + IServiceCollection services, + IModuleEnvironment environment + ) => Seen = environment; } [Fact] - public void AnInstanceRegisteredBeforeAddModulesIsUsed() { + public void AnInstanceRegisteredBeforeAddModulesIsUsed() + { var environment = new StubEnvironment("Staging"); var module = new ProbeModule(); @@ -382,7 +446,8 @@ public void AnInstanceRegisteredBeforeAddModulesIsUsed() { } [Fact] - public void AnEnvironmentPassedToAddModulesReplacesOneAlreadyRegistered() { + public void AnEnvironmentPassedToAddModulesReplacesOneAlreadyRegistered() + { var registered = new StubEnvironment("Staging"); var passed = new StubEnvironment("Development"); var module = new ProbeModule(); @@ -394,7 +459,10 @@ public void AnEnvironmentPassedToAddModulesReplacesOneAlreadyRegistered() { Assert.Same(passed, module.Seen); // Replaced rather than joined, so what resolves is what decided the registrations. - var descriptor = Assert.Single(collection, d => d.ServiceType == typeof(IModuleEnvironment)); + var descriptor = Assert.Single( + collection, + d => d.ServiceType == typeof(IModuleEnvironment) + ); Assert.Same(passed, descriptor.ImplementationInstance); } @@ -403,38 +471,45 @@ public void AnEnvironmentPassedToAddModulesReplacesOneAlreadyRegistered() { /// rather than ignored in favour of the process default. /// [Fact] - public void AnEnvironmentRegisteredByTypeIsRefused() { + public void AnEnvironmentRegisteredByTypeIsRefused() + { var collection = new ServiceCollection(); collection.AddSingleton(); - var exception = Assert.Throws( - () => collection.AddModules(new ProbeModule())); + var exception = Assert.Throws(() => + collection.AddModules(new ProbeModule()) + ); Assert.Contains("singleton instance", exception.Message); } [Fact] - public void AnEnvironmentRegisteredByFactoryIsRefused() { + public void AnEnvironmentRegisteredByFactoryIsRefused() + { var collection = new ServiceCollection(); collection.AddSingleton(_ => new StubEnvironment("Development")); Assert.Throws(() => collection.AddModules(new ProbeModule())); } - private class StubByType : IModuleEnvironment { + private class StubByType : IModuleEnvironment + { public string EnvironmentName => "Development"; + public string? Value(string valueName) => null; } } -public class EnvironmentConditionsTests { - +public class EnvironmentConditionsTests +{ /// /// Pinned to the values written here. These assert what a condition does with a given set of /// values, so a variable set on the machine running them must not reach a key they never name. /// - private static IModuleEnvironment Env(string name, params (string Key, string? Value)[] values) => - new ModuleEnvironment(false, name, values.ToDictionary(v => v.Key, v => v.Value)); + private static IModuleEnvironment Env( + string name, + params (string Key, string? Value)[] values + ) => new ModuleEnvironment(false, name, values.ToDictionary(v => v.Key, v => v.Value)); [Theory] [InlineData("Development", true)] @@ -445,7 +520,8 @@ public void NameIsIgnoresCase(string environmentName, bool expected) => Assert.Equal(expected, EnvironmentConditions.NameIs(Env(environmentName), "Development")); [Fact] - public void NameIsAcceptsAnyOfSeveral() { + public void NameIsAcceptsAnyOfSeveral() + { Assert.True(EnvironmentConditions.NameIs(Env("Staging"), "Development", "Staging")); Assert.False(EnvironmentConditions.NameIs(Env("Production"), "Development", "Staging")); } @@ -455,14 +531,16 @@ public void NameIsWithNoNamesMatchesNothing() => Assert.False(EnvironmentConditions.NameIs(Env("Development"))); [Fact] - public void HasValueIsPresenceNotTruthiness() { + public void HasValueIsPresenceNotTruthiness() + { Assert.True(EnvironmentConditions.HasValue(Env("Any", ("K", "v")), "K")); Assert.True(EnvironmentConditions.HasValue(Env("Any", ("K", "")), "K")); Assert.False(EnvironmentConditions.HasValue(Env("Any"), "K")); } [Fact] - public void ValueIsComparesOrdinally() { + public void ValueIsComparesOrdinally() + { Assert.True(EnvironmentConditions.ValueIs(Env("Any", ("K", "on")), "K", "on")); Assert.False(EnvironmentConditions.ValueIs(Env("Any", ("K", "On")), "K", "on")); Assert.False(EnvironmentConditions.ValueIs(Env("Any"), "K", "on")); @@ -473,7 +551,8 @@ public void ValueIsComparesOrdinally() { /// module can reach them. Refusing to match beats throwing out of a registration. /// [Fact] - public void ANullEnvironmentMatchesNothing() { + public void ANullEnvironmentMatchesNothing() + { Assert.False(EnvironmentConditions.NameIs(null!, "Development")); Assert.False(EnvironmentConditions.HasValue(null!, "K")); Assert.False(EnvironmentConditions.ValueIs(null!, "K", "v")); diff --git a/tests/DependencyModules.Tests/RuntimeTests/ModuleLoadingTests.cs b/tests/DependencyModules.Tests/RuntimeTests/ModuleLoadingTests.cs index 8c34b4b..34aae0f 100644 --- a/tests/DependencyModules.Tests/RuntimeTests/ModuleLoadingTests.cs +++ b/tests/DependencyModules.Tests/RuntimeTests/ModuleLoadingTests.cs @@ -11,14 +11,15 @@ namespace DependencyModules.Tests.RuntimeTests; /// Covers how DependencyRegistry.LoadModules walks a module graph: de-duplication, ordering, /// feature application, and environment-aware configuration. /// -public class ModuleLoadingTests { - +public class ModuleLoadingTests +{ private interface IThing; private class Thing : IThing; [Fact] - public void LoadModules_AppliesEachModuleOnce() { + public void LoadModules_AppliesEachModuleOnce() + { var module = new CountingModule(); var collection = new ServiceCollection(); @@ -28,7 +29,8 @@ public void LoadModules_AppliesEachModuleOnce() { } [Fact] - public void LoadModules_DeduplicatesEqualModules() { + public void LoadModules_DeduplicatesEqualModules() + { var first = new EquatableModule("same"); var second = new EquatableModule("same"); @@ -39,7 +41,8 @@ public void LoadModules_DeduplicatesEqualModules() { } [Fact] - public void LoadModules_KeepsModulesThatCompareUnequal() { + public void LoadModules_KeepsModulesThatCompareUnequal() + { var first = new EquatableModule("one"); var second = new EquatableModule("two"); @@ -51,7 +54,8 @@ public void LoadModules_KeepsModulesThatCompareUnequal() { } [Fact] - public void LoadModules_SkipsModulesThatOptOut() { + public void LoadModules_SkipsModulesThatOptOut() + { var module = new CountingModule { LoadModule = false }; var collection = new ServiceCollection(); @@ -61,7 +65,8 @@ public void LoadModules_SkipsModulesThatOptOut() { } [Fact] - public void LoadModules_LoadsNestedModulesReturnedByGetModules() { + public void LoadModules_LoadsNestedModulesReturnedByGetModules() + { var child = new CountingModule(); var parent = new CountingModule { Children = [child] }; @@ -73,7 +78,8 @@ public void LoadModules_LoadsNestedModulesReturnedByGetModules() { } [Fact] - public void LoadModules_TerminatesOnCircularModuleReferences() { + public void LoadModules_TerminatesOnCircularModuleReferences() + { var first = new CountingModule(); var second = new CountingModule { Children = [first] }; first.Children = [second]; @@ -86,7 +92,8 @@ public void LoadModules_TerminatesOnCircularModuleReferences() { } [Fact] - public void LoadModules_InvokesServiceCollectionConfiguration() { + public void LoadModules_InvokesServiceCollectionConfiguration() + { var module = new ConfiguringModule(); var collection = new ServiceCollection(); @@ -96,7 +103,8 @@ public void LoadModules_InvokesServiceCollectionConfiguration() { } [Fact] - public void LoadModules_AppliesFeaturesBeforeServices() { + public void LoadModules_AppliesFeaturesBeforeServices() + { var module = new FeatureModule(); var collection = new ServiceCollection(); @@ -111,7 +119,8 @@ public void LoadModules_AppliesFeaturesBeforeServices() { /// never invoked by anything, so a module implementing it silently did nothing. /// [Fact] - public void LoadModules_InvokesConfigureDecorators() { + public void LoadModules_InvokesConfigureDecorators() + { var module = new DecoratingModule(); DependencyRegistry.LoadModules(new ServiceCollection(), module); @@ -124,7 +133,8 @@ public void LoadModules_InvokesConfigureDecorators() { /// registered its services or there would be nothing to decorate. /// [Fact] - public void LoadModules_RunsConfigureDecoratorsAfterAllServices() { + public void LoadModules_RunsConfigureDecoratorsAfterAllServices() + { var decorating = new DecoratingModule(); var registering = new ConfiguringModule(); @@ -136,9 +146,14 @@ public void LoadModules_RunsConfigureDecoratorsAfterAllServices() { } [Fact] - public void LoadModules_CanDecorateARegistrationFromAnotherModule() { + public void LoadModules_CanDecorateARegistrationFromAnotherModule() + { var collection = new ServiceCollection(); - DependencyRegistry.LoadModules(collection, new DecoratingModule(), new ConfiguringModule()); + DependencyRegistry.LoadModules( + collection, + new DecoratingModule(), + new ConfiguringModule() + ); var provider = collection.BuildServiceProvider(); @@ -146,7 +161,8 @@ public void LoadModules_CanDecorateARegistrationFromAnotherModule() { } [Fact] - public void LoadModules_AppliesFeaturesInOrder() { + public void LoadModules_AppliesFeaturesInOrder() + { var module = new OrderedFeatureModule(); var collection = new ServiceCollection(); @@ -156,7 +172,8 @@ public void LoadModules_AppliesFeaturesInOrder() { } [Fact] - public void AddModules_WithEnvironment_PassesEnvironmentToConfiguration() { + public void AddModules_WithEnvironment_PassesEnvironmentToConfiguration() + { var environment = new StubEnvironment("Staging"); var module = new EnvironmentModule(); @@ -175,7 +192,8 @@ public void AddModules_WithEnvironment_PassesEnvironmentToConfiguration() { /// looking at two different answers to the same question. /// [Fact] - public void AddModules_WithoutEnvironment_PassesTheProcessDefaultToConfiguration() { + public void AddModules_WithoutEnvironment_PassesTheProcessDefaultToConfiguration() + { var module = new EnvironmentModule(); var collection = new ServiceCollection(); @@ -185,17 +203,21 @@ public void AddModules_WithoutEnvironment_PassesTheProcessDefaultToConfiguration // The instance registered into the collection, rather than whatever CreateDefault hands out // next — it builds a fresh one per call, so comparing against it would test nothing. var registered = Assert.Single( - collection, descriptor => descriptor.ServiceType == typeof(IModuleEnvironment)); + collection, + descriptor => descriptor.ServiceType == typeof(IModuleEnvironment) + ); Assert.Same(registered.ImplementationInstance, module.ObservedEnvironment); Assert.Equal( ModuleEnvironment.CreateDefault().EnvironmentName, - module.ObservedEnvironment!.EnvironmentName); + module.ObservedEnvironment!.EnvironmentName + ); Assert.True(module.ConfigureCalled); } [Fact] - public void AddModules_WithModuleEnvironmentNone_PassesNoneRatherThanTheDefault() { + public void AddModules_WithModuleEnvironmentNone_PassesNoneRatherThanTheDefault() + { var module = new EnvironmentModule(); var collection = new ServiceCollection(); @@ -205,7 +227,8 @@ public void AddModules_WithModuleEnvironmentNone_PassesNoneRatherThanTheDefault( } [Fact] - public void AddModules_WithEnvironment_RegistersEnvironmentAsSingleton() { + public void AddModules_WithEnvironment_RegistersEnvironmentAsSingleton() + { var environment = new StubEnvironment("Production"); var collection = new ServiceCollection(); @@ -215,7 +238,8 @@ public void AddModules_WithEnvironment_RegistersEnvironmentAsSingleton() { Assert.Same(environment, provider.GetService()); } - private class CountingModule : IDependencyModule { + private class CountingModule : IDependencyModule + { public int ApplyCount { get; private set; } public bool LoadModule { get; init; } = true; @@ -229,7 +253,8 @@ public void PopulateServiceCollection(IServiceCollection serviceCollection) { } public void InternalApplyServices(IServiceCollection serviceCollection) => ApplyCount++; } - private class EquatableModule(string key) : IDependencyModule { + private class EquatableModule(string key) : IDependencyModule + { private string Key { get; } = key; public int ApplyCount { get; private set; } @@ -238,16 +263,19 @@ public void PopulateServiceCollection(IServiceCollection serviceCollection) { } public void InternalApplyServices(IServiceCollection serviceCollection) => ApplyCount++; - public override bool Equals(object? obj) => obj is EquatableModule other && other.Key == Key; + public override bool Equals(object? obj) => + obj is EquatableModule other && other.Key == Key; public override int GetHashCode() => Key.GetHashCode(); } - private class DecoratedThing(IThing inner) : IThing { + private class DecoratedThing(IThing inner) : IThing + { public IThing Inner { get; } = inner; } - private class DecoratingModule : IDependencyModule, IServiceCollectionConfiguration { + private class DecoratingModule : IDependencyModule, IServiceCollectionConfiguration + { public bool ConfigureDecoratorsCalled { get; private set; } public IReadOnlyList? ServicesVisibleWhenDecorating { get; private set; } @@ -256,12 +284,17 @@ public void PopulateServiceCollection(IServiceCollection serviceCollection) { } public void ConfigureServices(IServiceCollection services) { } - public void ConfigureDecorators(IServiceCollection services) { + public void ConfigureDecorators(IServiceCollection services) + { ConfigureDecoratorsCalled = true; - ServicesVisibleWhenDecorating = services.Select(descriptor => descriptor.ServiceType).ToList(); - - for (var i = services.Count - 1; i >= 0; i--) { - if (services[i].ServiceType != typeof(IThing)) { + ServicesVisibleWhenDecorating = services + .Select(descriptor => descriptor.ServiceType) + .ToList(); + + for (var i = services.Count - 1; i >= 0; i--) + { + if (services[i].ServiceType != typeof(IThing)) + { continue; } @@ -270,19 +303,28 @@ public void ConfigureDecorators(IServiceCollection services) { services[i] = new ServiceDescriptor( typeof(IThing), provider => new DecoratedThing( - (IThing)ActivatorUtilities.CreateInstance(provider, inner.ImplementationType!)), - inner.Lifetime); + (IThing) + ActivatorUtilities.CreateInstance(provider, inner.ImplementationType!) + ), + inner.Lifetime + ); } } } - private class ConfiguringModule : IDependencyModule, IServiceCollectionConfiguration { + private class ConfiguringModule : IDependencyModule, IServiceCollectionConfiguration + { public void PopulateServiceCollection(IServiceCollection serviceCollection) { } - public void ConfigureServices(IServiceCollection services) => services.AddSingleton(); + public void ConfigureServices(IServiceCollection services) => + services.AddSingleton(); } - private class FeatureModule : IDependencyModule, IDependencyModuleApplicatorProvider, IServiceCollectionConfiguration { + private class FeatureModule + : IDependencyModule, + IDependencyModuleApplicatorProvider, + IServiceCollectionConfiguration + { public bool FeatureApplied { get; private set; } public bool FeatureAppliedBeforeConfigure { get; private set; } @@ -291,48 +333,60 @@ private class FeatureModule : IDependencyModule, IDependencyModuleApplicatorProv public void PopulateServiceCollection(IServiceCollection serviceCollection) { } - public IEnumerable FeatureApplicators() { - yield return new DelegateApplicator(0, () => { - FeatureApplied = true; - FeatureAppliedBeforeConfigure = !_configured; - }); + public IEnumerable FeatureApplicators() + { + yield return new DelegateApplicator( + 0, + () => + { + FeatureApplied = true; + FeatureAppliedBeforeConfigure = !_configured; + } + ); } public void ConfigureServices(IServiceCollection services) => _configured = true; } - private class OrderedFeatureModule : IDependencyModule, IDependencyModuleApplicatorProvider { + private class OrderedFeatureModule : IDependencyModule, IDependencyModuleApplicatorProvider + { public List AppliedOrders { get; } = []; public void PopulateServiceCollection(IServiceCollection serviceCollection) { } - public IEnumerable FeatureApplicators() { + public IEnumerable FeatureApplicators() + { yield return new DelegateApplicator(10, () => AppliedOrders.Add(10)); yield return new DelegateApplicator(1, () => AppliedOrders.Add(1)); yield return new DelegateApplicator(5, () => AppliedOrders.Add(5)); } } - private class DelegateApplicator(int order, Action onApply) : IFeatureApplicator { + private class DelegateApplicator(int order, Action onApply) : IFeatureApplicator + { public int Order => order; - public void Apply(IServiceCollection services, IReadOnlyList modules) => onApply(); + public void Apply(IServiceCollection services, IReadOnlyList modules) => + onApply(); } - private class EnvironmentModule : IDependencyModule, IEnvironmentServiceCollectionConfiguration { + private class EnvironmentModule : IDependencyModule, IEnvironmentServiceCollectionConfiguration + { public IModuleEnvironment? ObservedEnvironment { get; private set; } public bool ConfigureCalled { get; private set; } public void PopulateServiceCollection(IServiceCollection serviceCollection) { } - public void ConfigureServices(IServiceCollection services, IModuleEnvironment environment) { + public void ConfigureServices(IServiceCollection services, IModuleEnvironment environment) + { ConfigureCalled = true; ObservedEnvironment = environment; } } - private class StubEnvironment(string name) : IModuleEnvironment { + private class StubEnvironment(string name) : IModuleEnvironment + { public string EnvironmentName => name; public string? Value(string valueName) => null; diff --git a/tests/DependencyModules.Tests/RuntimeTests/ServiceCollectionExtensionsTests.cs b/tests/DependencyModules.Tests/RuntimeTests/ServiceCollectionExtensionsTests.cs index 374b0e2..7d28670 100644 --- a/tests/DependencyModules.Tests/RuntimeTests/ServiceCollectionExtensionsTests.cs +++ b/tests/DependencyModules.Tests/RuntimeTests/ServiceCollectionExtensionsTests.cs @@ -8,14 +8,15 @@ namespace DependencyModules.Tests.RuntimeTests; /// /// The AddModule/AddModules overloads are the library's entry point, so each one is covered here. /// -public class ServiceCollectionExtensionsTests { - +public class ServiceCollectionExtensionsTests +{ private interface IThing; private class Thing : IThing; [Fact] - public void AddModule_Generic_PopulatesTheCollection() { + public void AddModule_Generic_PopulatesTheCollection() + { var collection = new ServiceCollection(); collection.AddModule(); @@ -24,7 +25,8 @@ public void AddModule_Generic_PopulatesTheCollection() { } [Fact] - public void AddModule_Generic_ReturnsTheSameCollectionForChaining() { + public void AddModule_Generic_ReturnsTheSameCollectionForChaining() + { var collection = new ServiceCollection(); var returned = collection.AddModule(); @@ -33,7 +35,8 @@ public void AddModule_Generic_ReturnsTheSameCollectionForChaining() { } [Fact] - public void AddModule_Instance_PopulatesTheCollection() { + public void AddModule_Instance_PopulatesTheCollection() + { var collection = new ServiceCollection(); collection.AddModule(new RegisteringModule()); @@ -42,7 +45,8 @@ public void AddModule_Instance_PopulatesTheCollection() { } [Fact] - public void AddModule_Instance_ReturnsTheSameCollectionForChaining() { + public void AddModule_Instance_ReturnsTheSameCollectionForChaining() + { var collection = new ServiceCollection(); var module = new RegisteringModule(); @@ -52,7 +56,8 @@ public void AddModule_Instance_ReturnsTheSameCollectionForChaining() { } [Fact] - public void AddModules_AppliesEveryModule() { + public void AddModules_AppliesEveryModule() + { var first = new RegisteringModule(); var second = new OtherRegisteringModule(); @@ -64,7 +69,8 @@ public void AddModules_AppliesEveryModule() { } [Fact] - public void AddModules_ReturnsTheSameCollectionForChaining() { + public void AddModules_ReturnsTheSameCollectionForChaining() + { var collection = new ServiceCollection(); var returned = collection.AddModules(new RegisteringModule()); @@ -73,7 +79,8 @@ public void AddModules_ReturnsTheSameCollectionForChaining() { } [Fact] - public void AddModules_WithNoModules_LeavesTheCollectionEmpty() { + public void AddModules_WithNoModules_LeavesTheCollectionEmpty() + { var collection = new ServiceCollection(); collection.AddModules(); @@ -82,7 +89,8 @@ public void AddModules_WithNoModules_LeavesTheCollectionEmpty() { } [Fact] - public void AddModules_WithEnvironment_ReturnsTheSameCollectionForChaining() { + public void AddModules_WithEnvironment_ReturnsTheSameCollectionForChaining() + { var collection = new ServiceCollection(); var returned = collection.AddModules(new StubEnvironment(), new RegisteringModule()); @@ -91,7 +99,8 @@ public void AddModules_WithEnvironment_ReturnsTheSameCollectionForChaining() { } [Fact] - public void AddedServices_ResolveFromTheBuiltProvider() { + public void AddedServices_ResolveFromTheBuiltProvider() + { var collection = new ServiceCollection(); collection.AddModule(); @@ -104,21 +113,26 @@ private interface IOther; private class Other : IOther; - private class RegisteringModule : IDependencyModule, IServiceCollectionConfiguration { + private class RegisteringModule : IDependencyModule, IServiceCollectionConfiguration + { public void PopulateServiceCollection(IServiceCollection serviceCollection) => DependencyRegistryTestHelper.LoadSingleModule(serviceCollection, this); - public void ConfigureServices(IServiceCollection services) => services.AddSingleton(); + public void ConfigureServices(IServiceCollection services) => + services.AddSingleton(); } - private class OtherRegisteringModule : IDependencyModule, IServiceCollectionConfiguration { + private class OtherRegisteringModule : IDependencyModule, IServiceCollectionConfiguration + { public void PopulateServiceCollection(IServiceCollection serviceCollection) => DependencyRegistryTestHelper.LoadSingleModule(serviceCollection, this); - public void ConfigureServices(IServiceCollection services) => services.AddSingleton(); + public void ConfigureServices(IServiceCollection services) => + services.AddSingleton(); } - private class StubEnvironment : IModuleEnvironment { + private class StubEnvironment : IModuleEnvironment + { public string EnvironmentName => "Test"; public string? Value(string name) => null; @@ -129,7 +143,8 @@ private class StubEnvironment : IModuleEnvironment { /// Generated modules route PopulateServiceCollection through DependencyRegistry; hand-written test /// modules do the same so they exercise the real code path. /// -internal static class DependencyRegistryTestHelper { +internal static class DependencyRegistryTestHelper +{ public static void LoadSingleModule(IServiceCollection services, IDependencyModule module) => Runtime.Helpers.DependencyRegistry.LoadModules(services, module); } diff --git a/tests/DependencyModules.Tests/TestingTests/SharedRegistrationsTests.cs b/tests/DependencyModules.Tests/TestingTests/SharedRegistrationsTests.cs index 1109c48..318410b 100644 --- a/tests/DependencyModules.Tests/TestingTests/SharedRegistrationsTests.cs +++ b/tests/DependencyModules.Tests/TestingTests/SharedRegistrationsTests.cs @@ -14,8 +14,8 @@ namespace DependencyModules.Tests.TestingTests; /// One test per row of the rule, because the rule has been wrong once already and the way it was /// wrong was a case nobody had written down. /// -public class SharedRegistrationsTests { - +public class SharedRegistrationsTests +{ private interface IThing; private interface IOther; @@ -26,14 +26,17 @@ private class Thing : IThing; private interface IDriver; /// An attribute that declines, which is the only thing worth saying on a parameter. - private class NotSharedAttribute : Attribute, ISharedTestRegistration { + private class NotSharedAttribute : Attribute, ISharedTestRegistration + { public bool Shared => false; } /// A harness naming what it supplies, the way the trigger and web attributes do. - private class DrivenByAttribute : Attribute, ISharedTestRegistration { + private class DrivenByAttribute : Attribute, ISharedTestRegistration + { public IReadOnlyList IsolatedServices(MethodInfo testMethod) => - testMethod.GetParameters() + testMethod + .GetParameters() .Where(parameter => parameter.ParameterType == typeof(IDriver)) .Select(parameter => parameter.ParameterType) .ToArray(); @@ -41,8 +44,12 @@ public IReadOnlyList IsolatedServices(MethodInfo testMethod) => private static IReadOnlyCollection Collect(string method, params Attribute[] known) => SharedRegistrations.Collect( - typeof(SharedRegistrationsTests).GetMethod(method, BindingFlags.NonPublic | BindingFlags.Static)!, - known); + typeof(SharedRegistrationsTests).GetMethod( + method, + BindingFlags.NonPublic | BindingFlags.Static + )!, + known + ); private static void Plain(IThing thing) { } @@ -69,29 +76,34 @@ private static void None() { } /// first: a plain application service the handler writes to and the test reads. /// [Fact] - public void AnUnmarkedParameterIsPinned() { + public void AnUnmarkedParameterIsPinned() + { Assert.Equal([typeof(IThing)], Collect(nameof(Plain))); } [Fact] - public void EveryParameterIsPinned() { + public void EveryParameterIsPinned() + { Assert.Equal([typeof(IThing), typeof(IOther)], Collect(nameof(Bare))); } /// A mock needs no attribute to be pinned, which is what makes the rule general. [Fact] - public void AMockIsPinnedLikeAnythingElse() { + public void AMockIsPinnedLikeAnythingElse() + { Assert.Equal([typeof(IThing)], Collect(nameof(Mocked))); } /// Redundant now, and still allowed: it is how a driving parameter asks for reuse. [Fact] - public void SharedOnAValueParameterChangesNothing() { + public void SharedOnAValueParameterChangesNothing() + { Assert.Equal(Collect(nameof(Plain)), Collect(nameof(Marked))); } [Fact] - public void ATestWithNoParametersPinsNothing() { + public void ATestWithNoParametersPinsNothing() + { Assert.Empty(Collect(nameof(None))); } @@ -101,7 +113,8 @@ public void ATestWithNoParametersPinsNothing() { /// The parameter that drives the application is not pinned, because it builds the containers. /// [Fact] - public void AParameterTheHarnessDrivesWithIsNotPinned() { + public void AParameterTheHarnessDrivesWithIsNotPinned() + { var pinned = Collect(nameof(Driving), new DrivenByAttribute()); Assert.Equal([typeof(IThing)], pinned); @@ -112,18 +125,21 @@ public void AParameterTheHarnessDrivesWithIsNotPinned() { /// either way; it turns the isolation off while the test believes it is on. /// [Fact] - public void IsolatedWinsOverAnExplicitShared() { + public void IsolatedWinsOverAnExplicitShared() + { Assert.Empty(Collect(nameof(DrivingAndMarked), new DrivenByAttribute())); } [Fact] - public void AnAttributeDecliningUnpinsItsParameter() { + public void AnAttributeDecliningUnpinsItsParameter() + { Assert.Empty(Collect(nameof(Declined))); } /// The container itself is the one question pinning cannot answer. [Fact] - public void TheServiceProviderIsNeverPinned() { + public void TheServiceProviderIsNeverPinned() + { Assert.Equal([typeof(IThing)], Collect(nameof(Container))); } @@ -133,9 +149,12 @@ public void TheServiceProviderIsNeverPinned() { /// A harness keeps its own per-test services by naming them, since no parameter holds them. /// [Fact] - public void AnAttributeCanPinWhatNoParameterHolds() { - var shared = new TestExportAttribute(typeof(IOther)) { - Implementation = typeof(Thing), Shared = true + public void AnAttributeCanPinWhatNoParameterHolds() + { + var shared = new TestExportAttribute(typeof(IOther)) + { + Implementation = typeof(Thing), + Shared = true, }; Assert.Equal([typeof(IThing), typeof(IOther)], Collect(nameof(Plain), shared)); @@ -143,7 +162,8 @@ public void AnAttributeCanPinWhatNoParameterHolds() { /// An export nothing holds stays per container until it asks. [Fact] - public void AnExportIsNotPinnedUnlessItAsks() { + public void AnExportIsNotPinnedUnlessItAsks() + { var isolated = new TestExportAttribute(typeof(IOther)) { Implementation = typeof(Thing) }; Assert.Equal([typeof(IThing)], Collect(nameof(Plain), isolated)); @@ -154,7 +174,8 @@ public void AnExportIsNotPinnedUnlessItAsks() { /// harness resolves. Isolating it would recreate the bug the rule exists to fix. /// [Fact] - public void AnExportTheTestHoldsIsPinnedEvenWhenItDeclined() { + public void AnExportTheTestHoldsIsPinnedEvenWhenItDeclined() + { var isolated = new TestExportAttribute(typeof(IThing)) { Implementation = typeof(Thing) }; Assert.Equal([typeof(IThing)], Collect(nameof(Plain), isolated)); diff --git a/tests/DependencyModules.Tests/TestingTests/TestContainerSourceTests.cs b/tests/DependencyModules.Tests/TestingTests/TestContainerSourceTests.cs index 603a051..2c4fa20 100644 --- a/tests/DependencyModules.Tests/TestingTests/TestContainerSourceTests.cs +++ b/tests/DependencyModules.Tests/TestingTests/TestContainerSourceTests.cs @@ -19,26 +19,29 @@ namespace DependencyModules.Tests.TestingTests; [DependencyModule] public partial class ContainerSourceModule { } -public interface ICounter { +public interface ICounter +{ int Value { get; } void Bump(); } [SingletonService] -public class Counter : ICounter { +public class Counter : ICounter +{ public int Value { get; private set; } public void Bump() => Value++; } -public interface IAudit { +public interface IAudit +{ void Record(string what); } [NSubstituteSupport] -public class TestContainerSourceTests { - +public class TestContainerSourceTests +{ /// /// A parameter the test holds crosses every container, with nothing said about it. /// @@ -50,7 +53,10 @@ public class TestContainerSourceTests { /// [ModuleTest(typeof(ContainerSourceModule))] public async Task ABareParameterIsTheSameObjectInEveryContainer( - ITestContainerSource source, ICounter counter) { + ITestContainerSource source, + ICounter counter + ) + { var first = await source.CreateAsync(); var second = await source.CreateAsync(); @@ -71,7 +77,8 @@ public async Task ABareParameterIsTheSameObjectInEveryContainer( /// anything. /// [ModuleTest(typeof(ContainerSourceModule))] - public async Task EachContainerGetsItsOwnApplicationSingleton(ITestContainerSource source) { + public async Task EachContainerGetsItsOwnApplicationSingleton(ITestContainerSource source) + { var first = await source.CreateAsync(); var second = await source.CreateAsync(); @@ -89,7 +96,11 @@ public async Task EachContainerGetsItsOwnApplicationSingleton(ITestContainerSour /// is the one every container was built against, so what a container did is visible here. /// [ModuleTest(typeof(ContainerSourceModule))] - public async Task AMockIsTheSameObjectInEveryContainer(ITestContainerSource source, [Mock] IAudit audit) { + public async Task AMockIsTheSameObjectInEveryContainer( + ITestContainerSource source, + [Mock] IAudit audit + ) + { var first = await source.CreateAsync(); var second = await source.CreateAsync(); @@ -99,7 +110,8 @@ public async Task AMockIsTheSameObjectInEveryContainer(ITestContainerSource sour first.GetRequiredService().Record("one"); second.GetRequiredService().Record("two"); - Received.InOrder(() => { + Received.InOrder(() => + { audit.Record("one"); audit.Record("two"); }); @@ -110,7 +122,11 @@ public async Task AMockIsTheSameObjectInEveryContainer(ITestContainerSource sour /// [ModuleTest(typeof(ContainerSourceModule))] public async Task TheTestsOwnContainerHoldsTheSameMock( - ITestContainerSource source, IServiceProvider own, [Mock] IAudit audit) { + ITestContainerSource source, + IServiceProvider own, + [Mock] IAudit audit + ) + { var built = await source.CreateAsync(); Assert.Same(audit, own.GetRequiredService()); @@ -123,7 +139,8 @@ public async Task TheTestsOwnContainerHoldsTheSameMock( /// that wants a cold one. /// [ModuleTest(typeof(ContainerSourceModule))] - public async Task EveryCallBuildsAContainer(ITestContainerSource source) { + public async Task EveryCallBuildsAContainer(ITestContainerSource source) + { var first = await source.CreateAsync(); var second = await source.CreateAsync(); @@ -140,7 +157,11 @@ public async Task EveryCallBuildsAContainer(ITestContainerSource source) { /// to know which kind of parameter they are looking at before writing it. /// [ModuleTest(typeof(ContainerSourceModule))] - public async Task SharedOnAValueParameterIsRedundant(ITestContainerSource source, [Shared] ICounter counter) { + public async Task SharedOnAValueParameterIsRedundant( + ITestContainerSource source, + [Shared] ICounter counter + ) + { var first = await source.CreateAsync(); var second = await source.CreateAsync(); @@ -162,11 +183,16 @@ public async Task SharedOnAValueParameterIsRedundant(ITestContainerSource source /// clear the bar for sharing without a word at the use site. /// [NSubstituteSupport] -[TestExport(typeof(ICounter), Implementation = typeof(Counter), Lifetime = ServiceLifetime.Singleton)] -public class IsolatedTestExportTests { - +[TestExport( + typeof(ICounter), + Implementation = typeof(Counter), + Lifetime = ServiceLifetime.Singleton +)] +public class IsolatedTestExportTests +{ [ModuleTest] - public async Task AnExportIsRebuiltWithEachContainer(ITestContainerSource source) { + public async Task AnExportIsRebuiltWithEachContainer(ITestContainerSource source) + { var first = await source.CreateAsync(); var second = await source.CreateAsync(); @@ -179,12 +205,17 @@ public async Task AnExportIsRebuiltWithEachContainer(ITestContainerSource source } [NSubstituteSupport] -[TestExport(typeof(ICounter), Implementation = typeof(Counter), Lifetime = ServiceLifetime.Singleton, - Shared = true)] -public class SharedTestExportTests { - +[TestExport( + typeof(ICounter), + Implementation = typeof(Counter), + Lifetime = ServiceLifetime.Singleton, + Shared = true +)] +public class SharedTestExportTests +{ [ModuleTest] - public async Task AnExportAskingToBeSharedCrossesEveryContainer(ITestContainerSource source) { + public async Task AnExportAskingToBeSharedCrossesEveryContainer(ITestContainerSource source) + { var first = await source.CreateAsync(); var second = await source.CreateAsync(); @@ -200,18 +231,23 @@ public async Task AnExportAskingToBeSharedCrossesEveryContainer(ITestContainerSo /// registration said. /// [ModuleTest] - [TestExport(typeof(IAudit), Implementation = typeof(RecordingAudit), - Lifetime = ServiceLifetime.Transient, Shared = true)] - public async Task SharedOverridesATransientLifetime(ITestContainerSource source) { + [TestExport( + typeof(IAudit), + Implementation = typeof(RecordingAudit), + Lifetime = ServiceLifetime.Transient, + Shared = true + )] + public async Task SharedOverridesATransientLifetime(ITestContainerSource source) + { var built = await source.CreateAsync(); Assert.Same(built.GetRequiredService(), built.GetRequiredService()); } } -public class RecordingAudit : IAudit { +public class RecordingAudit : IAudit +{ public List Records { get; } = []; public void Record(string what) => Records.Add(what); } - diff --git a/tests/DependencyModules.Tests/TestingTests/TestContainerSourceUnitTests.cs b/tests/DependencyModules.Tests/TestingTests/TestContainerSourceUnitTests.cs index 316e833..3856308 100644 --- a/tests/DependencyModules.Tests/TestingTests/TestContainerSourceUnitTests.cs +++ b/tests/DependencyModules.Tests/TestingTests/TestContainerSourceUnitTests.cs @@ -8,19 +8,21 @@ namespace DependencyModules.Tests.TestingTests; /// /// The source driven directly, for the parts a test running through a framework cannot observe. /// -public class TestContainerSourceUnitTests { - +public class TestContainerSourceUnitTests +{ private interface IThing; private class Thing : IThing; - private sealed class Tracked : IDisposable { + private sealed class Tracked : IDisposable + { public int Disposals { get; private set; } public void Dispose() => Disposals++; } - private sealed class Harness { + private sealed class Harness + { public List Tracked { get; } = []; public List Started { get; } = []; @@ -29,7 +31,8 @@ private sealed class Harness { public IServiceProvider Pin { get; } - public Harness(Action compose, params Type[] pinned) { + public Harness(Action compose, params Type[] pinned) + { var services = new ServiceCollection(); services.AddSingleton(Source); @@ -43,12 +46,14 @@ public Harness(Action compose, params Type[] pinned) { Pin, pinned, collection => collection.BuildServiceProvider(), - provider => { + provider => + { Started.Add(provider); return ValueTask.CompletedTask; }, - Tracked.Add); + Tracked.Add + ); } } @@ -57,7 +62,8 @@ public Harness(Action compose, params Type[] pinned) { /// rather than the caller's. /// [Fact] - public async Task EveryContainerBuiltIsHandedToTheRunner() { + public async Task EveryContainerBuiltIsHandedToTheRunner() + { var harness = new Harness(services => services.AddSingleton()); var first = await harness.Source.CreateAsync(); @@ -70,7 +76,8 @@ public async Task EveryContainerBuiltIsHandedToTheRunner() { /// Startup runs against each one. A container that skipped it is not the one the test composed. /// [Fact] - public async Task StartupRunsForEveryContainer() { + public async Task StartupRunsForEveryContainer() + { var harness = new Harness(services => services.AddSingleton()); var first = await harness.Source.CreateAsync(); @@ -84,12 +91,11 @@ public async Task StartupRunsForEveryContainer() { /// uses an instance registration rather than a factory returning the same object. /// [Fact] - public async Task APinnedDisposableIsNotDisposedByTheContainersItIsHandedTo() { + public async Task APinnedDisposableIsNotDisposedByTheContainersItIsHandedTo() + { var tracked = new Tracked(); - var harness = new Harness( - services => services.AddSingleton(_ => tracked), - typeof(Tracked)); + var harness = new Harness(services => services.AddSingleton(_ => tracked), typeof(Tracked)); var first = await harness.Source.CreateAsync(); var second = await harness.Source.CreateAsync(); @@ -108,27 +114,29 @@ public async Task APinnedDisposableIsNotDisposedByTheContainersItIsHandedTo() { /// last member would leave anything injecting the sequence one element long. /// [Fact] - public async Task PinningKeepsEveryRegistrationOfAService() { + public async Task PinningKeepsEveryRegistrationOfAService() + { var harness = new Harness( - services => { + services => + { services.AddSingleton(); services.AddSingleton(); }, - typeof(IThing)); + typeof(IThing) + ); var built = await harness.Source.CreateAsync(); Assert.Equal(2, built.GetServices().Count()); - Assert.Equal( - harness.Pin.GetServices(), - built.GetServices()); + Assert.Equal(harness.Pin.GetServices(), built.GetServices()); } /// /// Nothing is built until something asks, so a test that never rebuilds pays for none of this. /// [Fact] - public void NothingIsBuiltUntilAsked() { + public void NothingIsBuiltUntilAsked() + { var harness = new Harness(services => services.AddSingleton()); Assert.Empty(harness.Tracked); @@ -140,11 +148,13 @@ public void NothingIsBuiltUntilAsked() { /// nothing. /// [Fact] - public async Task AnUninitializedSourceRefuses() { + public async Task AnUninitializedSourceRefuses() + { var source = new TestContainerSource(); - var refused = await Assert.ThrowsAsync( - async () => await source.CreateAsync()); + var refused = await Assert.ThrowsAsync(async () => + await source.CreateAsync() + ); Assert.Contains("never initialized", refused.Message); } @@ -160,19 +170,27 @@ public async Task AnUninitializedSourceRefuses() { /// such a parameter and never resolved it. /// [Fact] - public async Task APinnedServiceThatCannotBeBuiltIsLeftAlone() { + public async Task APinnedServiceThatCannotBeBuiltIsLeftAlone() + { var harness = new Harness( - services => { + services => + { services.AddSingleton(); - services.AddSingleton(_ => throw new InvalidOperationException("name the fix")); + services.AddSingleton(_ => + throw new InvalidOperationException("name the fix") + ); }, - typeof(IThing), typeof(string)); + typeof(IThing), + typeof(string) + ); var built = await harness.Source.CreateAsync(); Assert.NotNull(built.GetRequiredService()); - var refused = Assert.Throws(() => built.GetRequiredService()); + var refused = Assert.Throws(() => + built.GetRequiredService() + ); Assert.Equal("name the fix", refused.Message); } @@ -182,10 +200,14 @@ public async Task APinnedServiceThatCannotBeBuiltIsLeftAlone() { /// signature without filtering it first. /// [Fact] - public async Task APinnedTypeNothingRegisteredIsInert() { + public async Task APinnedTypeNothingRegisteredIsInert() + { var harness = new Harness( services => services.AddSingleton(), - typeof(IThing), typeof(int), typeof(Uri)); + typeof(IThing), + typeof(int), + typeof(Uri) + ); var built = await harness.Source.CreateAsync(); @@ -193,4 +215,3 @@ public async Task APinnedTypeNothingRegisteredIsInert() { Assert.Null(built.GetService()); } } - diff --git a/tests/DependencyModules.Tests/TestingTests/TestExportAttributeTests.cs b/tests/DependencyModules.Tests/TestingTests/TestExportAttributeTests.cs index 8d6a9f3..dc76e00 100644 --- a/tests/DependencyModules.Tests/TestingTests/TestExportAttributeTests.cs +++ b/tests/DependencyModules.Tests/TestingTests/TestExportAttributeTests.cs @@ -9,8 +9,8 @@ namespace DependencyModules.Tests.TestingTests; /// [TestExport] lets a test override a registration for the duration of that test, so the lifetime /// and implementation it produces have to match what was asked for. /// -public class TestExportAttributeTests { - +public class TestExportAttributeTests +{ private interface IThing; private class Thing : IThing; @@ -18,17 +18,20 @@ private class Thing : IThing; private class OtherThing : IThing; [Fact] - public void DefaultsToTransient() { + public void DefaultsToTransient() + { Assert.Equal(ServiceLifetime.Transient, new TestExportAttribute(typeof(IThing)).Lifetime); } [Fact] - public void DefaultsToNoSeparateImplementation() { + public void DefaultsToNoSeparateImplementation() + { Assert.Null(new TestExportAttribute(typeof(IThing)).Implementation); } [Fact] - public void ExposesTheServiceItWasGiven() { + public void ExposesTheServiceItWasGiven() + { Assert.Equal(typeof(IThing), new TestExportAttribute(typeof(IThing)).Service); } @@ -36,10 +39,12 @@ public void ExposesTheServiceItWasGiven() { [InlineData(ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Transient)] - public void RegistersWithTheRequestedLifetime(ServiceLifetime lifetime) { - var attribute = new TestExportAttribute(typeof(IThing)) { + public void RegistersWithTheRequestedLifetime(ServiceLifetime lifetime) + { + var attribute = new TestExportAttribute(typeof(IThing)) + { Implementation = typeof(Thing), - Lifetime = lifetime + Lifetime = lifetime, }; var collection = Setup(attribute); @@ -51,7 +56,8 @@ public void RegistersWithTheRequestedLifetime(ServiceLifetime lifetime) { } [Fact] - public void WithoutAnImplementation_RegistersTheServiceAsItsOwnImplementation() { + public void WithoutAnImplementation_RegistersTheServiceAsItsOwnImplementation() + { var collection = Setup(new TestExportAttribute(typeof(Thing))); var descriptor = Assert.Single(collection); @@ -60,11 +66,15 @@ public void WithoutAnImplementation_RegistersTheServiceAsItsOwnImplementation() } [Fact] - public void RegisteredServiceResolvesFromTheProvider() { - var collection = Setup(new TestExportAttribute(typeof(IThing)) { - Implementation = typeof(OtherThing), - Lifetime = ServiceLifetime.Singleton - }); + public void RegisteredServiceResolvesFromTheProvider() + { + var collection = Setup( + new TestExportAttribute(typeof(IThing)) + { + Implementation = typeof(OtherThing), + Lifetime = ServiceLifetime.Singleton, + } + ); var provider = collection.BuildServiceProvider(); @@ -77,13 +87,17 @@ public void RegisteredServiceResolvesFromTheProvider() { /// the split here means re-adding a lifecycle hook has to be a decision rather than an accident. /// [Fact] - public void RegistersServicesWithoutTakingPartInTestStartup() { - Assert.IsAssignableFrom(new TestExportAttribute(typeof(IThing))); + public void RegistersServicesWithoutTakingPartInTestStartup() + { + Assert.IsAssignableFrom( + new TestExportAttribute(typeof(IThing)) + ); Assert.False(new TestExportAttribute(typeof(IThing)) is ITestStartupAttribute); } [Fact] - public void AppliesToAssembliesClassesAndMethods() { + public void AppliesToAssembliesClassesAndMethods() + { var usage = typeof(TestExportAttribute) .GetCustomAttributes(typeof(AttributeUsageAttribute), false) .Cast() @@ -98,7 +112,8 @@ public void AppliesToAssembliesClassesAndMethods() { /// /// SetupServiceCollection does not read the test method, so tests supply none. /// - private static IServiceCollection Setup(TestExportAttribute attribute) { + private static IServiceCollection Setup(TestExportAttribute attribute) + { var collection = new ServiceCollection(); attribute.SetupServiceCollection(null!, collection); return collection; diff --git a/tests/DependencyModules.Tests/TestingTests/TestParameterResolverTests.cs b/tests/DependencyModules.Tests/TestingTests/TestParameterResolverTests.cs index 0625d32..da59d94 100644 --- a/tests/DependencyModules.Tests/TestingTests/TestParameterResolverTests.cs +++ b/tests/DependencyModules.Tests/TestingTests/TestParameterResolverTests.cs @@ -15,8 +15,8 @@ namespace DependencyModules.Tests.TestingTests; /// unrelated integration test failing, if at all. The resolver is the piece an NUnit integration /// would share, which makes its behaviour a contract rather than an implementation detail. /// -public class TestParameterResolverTests { - +public class TestParameterResolverTests +{ private interface IThing; private class Thing : IThing; @@ -30,14 +30,19 @@ private class Other2 : IThing; /// Takes a dependency the container has and a value it cannot possibly know, which is what /// [InjectValues] is for. /// - private class NeedsAValue(IThing thing, string text) { + private class NeedsAValue(IThing thing, string text) + { public IThing Thing { get; } = thing; public string Text { get; } = text; } [Fact] - public async Task ResolvesAServiceFromTheContainer() { - var arguments = await Resolve(nameof(Samples.OneService), services => services.AddSingleton()); + public async Task ResolvesAServiceFromTheContainer() + { + var arguments = await Resolve( + nameof(Samples.OneService), + services => services.AddSingleton() + ); Assert.IsType(Assert.Single(arguments)); } @@ -46,7 +51,8 @@ public async Task ResolvesAServiceFromTheContainer() { /// A test asking for the container itself cannot have it resolved from the container. /// [Fact] - public async Task ServiceProviderParameterGetsTheContainerItself() { + public async Task ServiceProviderParameterGetsTheContainerItself() + { var (resolver, provider) = Build(nameof(Samples.WantsTheProvider), _ => { }); var arguments = await resolver.ResolveArgumentsAsync(provider, []); @@ -59,9 +65,12 @@ public async Task ServiceProviderParameterGetsTheContainerItself() { /// when the container could have supplied that type. /// [Fact] - public async Task DataTakesTheLeadingParametersAndTheContainerTakesTheRest() { + public async Task DataTakesTheLeadingParametersAndTheContainerTakesTheRest() + { var (resolver, provider) = Build( - nameof(Samples.DataThenService), services => services.AddSingleton()); + nameof(Samples.DataThenService), + services => services.AddSingleton() + ); var arguments = await resolver.ResolveArgumentsAsync(provider, [42]); @@ -76,9 +85,12 @@ public async Task DataTakesTheLeadingParametersAndTheContainerTakesTheRest() { /// only for the parameter holding it. /// [Fact] - public async Task ParameterAttributeRegistrationBeatsTheModuleRegistration() { + public async Task ParameterAttributeRegistrationBeatsTheModuleRegistration() + { var arguments = await Resolve( - nameof(Samples.RegisteringAttribute), services => services.AddSingleton()); + nameof(Samples.RegisteringAttribute), + services => services.AddSingleton() + ); Assert.IsType(Assert.Single(arguments)); } @@ -88,20 +100,27 @@ public async Task ParameterAttributeRegistrationBeatsTheModuleRegistration() { /// attributes can sit on one parameter with the first that answers winning. /// [Fact] - public async Task AProviderReturningNullDefersToTheNextOne() { + public async Task AProviderReturningNullDefersToTheNextOne() + { var arguments = await Resolve( - nameof(Samples.AbstainingThenAnswering), services => services.AddSingleton()); + nameof(Samples.AbstainingThenAnswering), + services => services.AddSingleton() + ); Assert.IsType(Assert.Single(arguments)); } [Fact] - public async Task ResolvesKeyedServices() { + public async Task ResolvesKeyedServices() + { var arguments = await Resolve( - nameof(Samples.Keyed), services => { + nameof(Samples.Keyed), + services => + { services.AddSingleton(); services.AddKeyedSingleton("other"); - }); + } + ); Assert.IsType(Assert.Single(arguments)); } @@ -111,9 +130,12 @@ public async Task ResolvesKeyedServices() { /// under test without registering it. /// [Fact] - public async Task ConstructsAnUnregisteredConcreteType() { + public async Task ConstructsAnUnregisteredConcreteType() + { var arguments = await Resolve( - nameof(Samples.UnregisteredWithInjectedValue), services => services.AddSingleton()); + nameof(Samples.UnregisteredWithInjectedValue), + services => services.AddSingleton() + ); var value = Assert.IsType(Assert.Single(arguments)); @@ -126,12 +148,14 @@ public async Task ConstructsAnUnregisteredConcreteType() { /// parameter would hand back the real service. It fails loudly instead. /// [Fact] - public async Task ResolvingBeforeSetupThrows() { + public async Task ResolvingBeforeSetupThrows() + { var resolver = new TestParameterResolver(ContextFor(nameof(Samples.OneService))); var provider = new ServiceCollection().BuildServiceProvider(); - var exception = await Assert.ThrowsAsync( - () => resolver.ResolveArgumentsAsync(provider, [])); + var exception = await Assert.ThrowsAsync(() => + resolver.ResolveArgumentsAsync(provider, []) + ); Assert.Contains(nameof(TestParameterResolver.SetupServiceCollection), exception.Message); } @@ -143,10 +167,12 @@ public async Task ResolvingBeforeSetupThrows() { /// was wired in. /// [Fact] - public async Task KeyedMockReplacesTheKeyedRegistration() { + public async Task KeyedMockReplacesTheKeyedRegistration() + { var (resolver, provider) = Build( nameof(Samples.KeyedMock), - services => services.AddKeyedSingleton("primary")); + services => services.AddKeyedSingleton("primary") + ); var arguments = await resolver.ResolveArgumentsAsync(provider, []); @@ -158,10 +184,12 @@ public async Task KeyedMockReplacesTheKeyedRegistration() { /// And it does not spill into the unkeyed slot, where nothing asked for it. /// [Fact] - public void KeyedMockRegistersNothingUnkeyed() { + public void KeyedMockRegistersNothingUnkeyed() + { var (_, provider) = Build( nameof(Samples.KeyedMock), - services => services.AddKeyedSingleton("primary")); + services => services.AddKeyedSingleton("primary") + ); Assert.Null(provider.GetService()); } @@ -171,22 +199,28 @@ public void KeyedMockRegistersNothingUnkeyed() { /// siblings real. /// [Fact] - public void KeyedMockLeavesOtherKeysAlone() { + public void KeyedMockLeavesOtherKeysAlone() + { var (_, provider) = Build( nameof(Samples.KeyedMock), - services => { + services => + { services.AddKeyedSingleton("primary"); services.AddKeyedSingleton("secondary"); - }); + } + ); Assert.IsType(provider.GetRequiredKeyedService("secondary")); } /// Control: an unkeyed mock still replaces the unkeyed registration. [Fact] - public async Task UnkeyedMockReplacesTheUnkeyedRegistration() { + public async Task UnkeyedMockReplacesTheUnkeyedRegistration() + { var arguments = await Resolve( - nameof(Samples.UnkeyedMock), services => services.AddSingleton()); + nameof(Samples.UnkeyedMock), + services => services.AddSingleton() + ); Assert.IsType(Assert.Single(arguments)); } @@ -201,7 +235,8 @@ public async Task UnkeyedMockReplacesTheUnkeyedRegistration() { /// is the one the container ends up with. /// [Fact] - public async Task AMockOnAParameter_BeatsARegistrationMadeBeforeIt() { + public async Task AMockOnAParameter_BeatsARegistrationMadeBeforeIt() + { var services = new ServiceCollection(); var resolver = new TestParameterResolver(ContextFor(nameof(Samples.UnkeyedMock))); @@ -222,7 +257,8 @@ public async Task AMockOnAParameter_BeatsARegistrationMadeBeforeIt() { /// than merely supplying an argument. /// [Fact] - public void AMockOnAParameter_IsWhatTheContainerHandsOut() { + public void AMockOnAParameter_IsWhatTheContainerHandsOut() + { var services = new ServiceCollection(); var resolver = new TestParameterResolver(ContextFor(nameof(Samples.UnkeyedMock))); @@ -233,25 +269,34 @@ public void AMockOnAParameter_IsWhatTheContainerHandsOut() { } [Fact] - public void SetupIsOfferedEveryParameter() { + public void SetupIsOfferedEveryParameter() + { var services = new ServiceCollection(); - new TestParameterResolver(ContextFor(nameof(Samples.TwoRegisteringAttributes))) - .SetupServiceCollection(services); + new TestParameterResolver( + ContextFor(nameof(Samples.TwoRegisteringAttributes)) + ).SetupServiceCollection(services); Assert.Equal(2, services.Count); } // ---- harness ------------------------------------------------------------------------------- - private static async Task Resolve(string methodName, Action configure) { + private static async Task Resolve( + string methodName, + Action configure + ) + { var (resolver, provider) = Build(methodName, configure); return await resolver.ResolveArgumentsAsync(provider, []); } private static (TestParameterResolver Resolver, IServiceProvider Provider) Build( - string methodName, Action configure) { + string methodName, + Action configure + ) + { var services = new ServiceCollection(); var resolver = new TestParameterResolver(ContextFor(methodName)); @@ -263,9 +308,12 @@ private static (TestParameterResolver Resolver, IServiceProvider Provider) Build } private static ITestMethodContext ContextFor(string methodName) => - new StubContext(typeof(Samples).GetMethod(methodName, BindingFlags.Public | BindingFlags.Static)!); + new StubContext( + typeof(Samples).GetMethod(methodName, BindingFlags.Public | BindingFlags.Static)! + ); - private class StubContext(MethodInfo method) : ITestMethodContext { + private class StubContext(MethodInfo method) : ITestMethodContext + { public MethodInfo Method { get; } = method; public IReadOnlyList Attributes { get; } = []; } @@ -275,7 +323,8 @@ private class StubContext(MethodInfo method) : ITestMethodContext { /// within this private class so one set of binding flags finds them all. /// [StubMockSupport] - private static class Samples { + private static class Samples + { public static void OneService(IThing thing) { } public static void KeyedMock([Mock] [FromKeyedServices("primary")] IThing thing) { } @@ -292,9 +341,14 @@ public static void AbstainingThenAnswering([Abstains] [RegistersOther] IThing th public static void Keyed([FromKeyedServices("other")] IThing thing) { } - public static void UnregisteredWithInjectedValue([InjectValues("supplied")] NeedsAValue value) { } + public static void UnregisteredWithInjectedValue( + [InjectValues("supplied")] NeedsAValue value + ) { } - public static void TwoRegisteringAttributes([RegistersOther] IThing first, [RegistersOther] IThing second) { } + public static void TwoRegisteringAttributes( + [RegistersOther] IThing first, + [RegistersOther] IThing second + ) { } } /// @@ -302,7 +356,8 @@ public static void TwoRegisteringAttributes([RegistersOther] IThing first, [Regi /// double actually is does not matter here; where it gets registered does. /// [AttributeUsage(AttributeTargets.Class)] - private class StubMockSupportAttribute : Attribute, IMockSupportAttribute { + private class StubMockSupportAttribute : Attribute, IMockSupportAttribute + { public object ProvideMock(Type type) => new Other(); } @@ -311,26 +366,37 @@ private class StubMockSupportAttribute : Attribute, IMockSupportAttribute { /// resolution hand it back. /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] - private class RegistersOtherAttribute : Attribute, ITestParameterValueProvider { + private class RegistersOtherAttribute : Attribute, ITestParameterValueProvider + { public void SetupServiceCollection( - ITestMethodContext testMethod, IServiceCollection serviceCollection, ParameterInfo parameter) => - serviceCollection.AddSingleton(parameter.ParameterType, new Other()); + ITestMethodContext testMethod, + IServiceCollection serviceCollection, + ParameterInfo parameter + ) => serviceCollection.AddSingleton(parameter.ParameterType, new Other()); public Task GetParameterValueAsync( - ITestMethodContext testMethod, IServiceProvider serviceProvider, ParameterInfo parameter) => - Task.FromResult(serviceProvider.GetService(parameter.ParameterType)); + ITestMethodContext testMethod, + IServiceProvider serviceProvider, + ParameterInfo parameter + ) => Task.FromResult(serviceProvider.GetService(parameter.ParameterType)); } /// /// Registers nothing and answers null, so the next provider on the parameter gets its turn. /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] - private class AbstainsAttribute : Attribute, ITestParameterValueProvider { + private class AbstainsAttribute : Attribute, ITestParameterValueProvider + { public void SetupServiceCollection( - ITestMethodContext testMethod, IServiceCollection serviceCollection, ParameterInfo parameter) { } + ITestMethodContext testMethod, + IServiceCollection serviceCollection, + ParameterInfo parameter + ) { } public Task GetParameterValueAsync( - ITestMethodContext testMethod, IServiceProvider serviceProvider, ParameterInfo parameter) => - Task.FromResult(null); + ITestMethodContext testMethod, + IServiceProvider serviceProvider, + ParameterInfo parameter + ) => Task.FromResult(null); } } diff --git a/tests/DependencyModules.Tests/xUnitTests/AttributeUtilityTests.cs b/tests/DependencyModules.Tests/xUnitTests/AttributeUtilityTests.cs index 6b9b160..576d66f 100644 --- a/tests/DependencyModules.Tests/xUnitTests/AttributeUtilityTests.cs +++ b/tests/DependencyModules.Tests/xUnitTests/AttributeUtilityTests.cs @@ -8,10 +8,11 @@ namespace DependencyModules.Tests.xUnitTests; /// AttributeUtility backs the documented "test attributes can be applied at the assembly, class, /// and test method level" behaviour, so the lookup order across those levels is the contract. /// -public class AttributeUtilityTests { - +public class AttributeUtilityTests +{ [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] - private class MarkerAttribute(string source) : Attribute { + private class MarkerAttribute(string source) : Attribute + { public string Source { get; } = source; } @@ -19,7 +20,8 @@ private class MarkerAttribute(string source) : Attribute { private class UnusedAttribute : Attribute; [Marker("class")] - private class WithClassAttribute { + private class WithClassAttribute + { [Marker("method")] public void MethodWithItsOwn(string plain) { } @@ -31,29 +33,38 @@ public void MethodWithParameterAttribute([Marker("parameter")] string annotated) private static MethodInfo Method(string name) => typeof(WithClassAttribute).GetMethod(name, BindingFlags.Public | BindingFlags.Instance)!; - private static ParameterInfo Parameter(string methodName) => Method(methodName).GetParameters()[0]; + private static ParameterInfo Parameter(string methodName) => + Method(methodName).GetParameters()[0]; [Fact] - public void GetTestAttribute_FindsAnAttributeOnTheMethod() { - var attribute = Method(nameof(WithClassAttribute.MethodWithItsOwn)).GetTestAttribute(); + public void GetTestAttribute_FindsAnAttributeOnTheMethod() + { + var attribute = Method(nameof(WithClassAttribute.MethodWithItsOwn)) + .GetTestAttribute(); Assert.Equal("method", attribute?.Source); } [Fact] - public void GetTestAttribute_FallsBackToTheDeclaringType() { - var attribute = Method(nameof(WithClassAttribute.MethodWithout)).GetTestAttribute(); + public void GetTestAttribute_FallsBackToTheDeclaringType() + { + var attribute = Method(nameof(WithClassAttribute.MethodWithout)) + .GetTestAttribute(); Assert.Equal("class", attribute?.Source); } [Fact] - public void GetTestAttribute_ReturnsNullWhenNothingMatches() { - Assert.Null(Method(nameof(WithClassAttribute.MethodWithout)).GetTestAttribute()); + public void GetTestAttribute_ReturnsNullWhenNothingMatches() + { + Assert.Null( + Method(nameof(WithClassAttribute.MethodWithout)).GetTestAttribute() + ); } [Fact] - public void GetTestAttribute_OnAParameter_PrefersTheParameterAttribute() { + public void GetTestAttribute_OnAParameter_PrefersTheParameterAttribute() + { var attribute = Parameter(nameof(WithClassAttribute.MethodWithParameterAttribute)) .GetTestAttribute(); @@ -61,26 +72,34 @@ public void GetTestAttribute_OnAParameter_PrefersTheParameterAttribute() { } [Fact] - public void GetTestAttribute_OnAnUnannotatedParameter_FallsBackToTheMethod() { - var attribute = Parameter(nameof(WithClassAttribute.MethodWithItsOwn)).GetTestAttribute(); + public void GetTestAttribute_OnAnUnannotatedParameter_FallsBackToTheMethod() + { + var attribute = Parameter(nameof(WithClassAttribute.MethodWithItsOwn)) + .GetTestAttribute(); Assert.Equal("method", attribute?.Source); } [Fact] - public void GetTestAttribute_OnAnUnannotatedParameter_FallsBackToTheDeclaringType() { - var attribute = Parameter(nameof(WithClassAttribute.MethodWithout)).GetTestAttribute(); + public void GetTestAttribute_OnAnUnannotatedParameter_FallsBackToTheDeclaringType() + { + var attribute = Parameter(nameof(WithClassAttribute.MethodWithout)) + .GetTestAttribute(); Assert.Equal("class", attribute?.Source); } [Fact] - public void GetTestAttribute_OnAParameter_ReturnsNullWhenNothingMatches() { - Assert.Null(Parameter(nameof(WithClassAttribute.MethodWithout)).GetTestAttribute()); + public void GetTestAttribute_OnAParameter_ReturnsNullWhenNothingMatches() + { + Assert.Null( + Parameter(nameof(WithClassAttribute.MethodWithout)).GetTestAttribute() + ); } [Fact] - public void GetTestAttributes_AccumulatesTypeThenMethod() { + public void GetTestAttributes_AccumulatesTypeThenMethod() + { var sources = Method(nameof(WithClassAttribute.MethodWithItsOwn)) .GetTestAttributes() .Select(attribute => attribute.Source) @@ -90,7 +109,8 @@ public void GetTestAttributes_AccumulatesTypeThenMethod() { } [Fact] - public void GetTestAttributes_ReturnsJustTheTypeAttributeWhenTheMethodHasNone() { + public void GetTestAttributes_ReturnsJustTheTypeAttributeWhenTheMethodHasNone() + { var sources = Method(nameof(WithClassAttribute.MethodWithout)) .GetTestAttributes() .Select(attribute => attribute.Source) @@ -100,12 +120,16 @@ public void GetTestAttributes_ReturnsJustTheTypeAttributeWhenTheMethodHasNone() } [Fact] - public void GetTestAttributes_ReturnsEmptyWhenNothingMatches() { - Assert.Empty(Method(nameof(WithClassAttribute.MethodWithout)).GetTestAttributes()); + public void GetTestAttributes_ReturnsEmptyWhenNothingMatches() + { + Assert.Empty( + Method(nameof(WithClassAttribute.MethodWithout)).GetTestAttributes() + ); } [Fact] - public void GetTestAttributes_OnAParameter_AccumulatesTypeMethodThenParameter() { + public void GetTestAttributes_OnAParameter_AccumulatesTypeMethodThenParameter() + { var sources = Parameter(nameof(WithClassAttribute.MethodWithParameterAttribute)) .GetTestAttributes() .Select(attribute => attribute.Source) @@ -115,7 +139,10 @@ public void GetTestAttributes_OnAParameter_AccumulatesTypeMethodThenParameter() } [Fact] - public void GetTestAttributes_OnAParameter_ReturnsEmptyWhenNothingMatches() { - Assert.Empty(Parameter(nameof(WithClassAttribute.MethodWithout)).GetTestAttributes()); + public void GetTestAttributes_OnAParameter_ReturnsEmptyWhenNothingMatches() + { + Assert.Empty( + Parameter(nameof(WithClassAttribute.MethodWithout)).GetTestAttributes() + ); } } diff --git a/tests/DependencyModules.Tests/xUnitTests/ModuleTestCaseDataTests.cs b/tests/DependencyModules.Tests/xUnitTests/ModuleTestCaseDataTests.cs index db9093b..88b1e3e 100644 --- a/tests/DependencyModules.Tests/xUnitTests/ModuleTestCaseDataTests.cs +++ b/tests/DependencyModules.Tests/xUnitTests/ModuleTestCaseDataTests.cs @@ -16,8 +16,8 @@ namespace DependencyModules.Tests.xUnitTests; /// count of tests produced is the only way that becomes visible, because every integration test /// runs through [ModuleTest] and a case that is never created is a suite that is quietly smaller. /// -public class ModuleTestCaseDataTests { - +public class ModuleTestCaseDataTests +{ /// /// Regression test. [MemberData] resolves its member off ITypeAwareDataAttribute.MemberType, /// which xUnit back-fills in ExtensibilityPointFactory.GetMethodDataAttributes — a path @@ -26,21 +26,24 @@ public class ModuleTestCaseDataTests { /// throwing, so the rows vanished without a diagnostic. /// [Fact] - public async Task MemberData_WithoutExplicitMemberType_ProducesOneTestPerRow() { + public async Task MemberData_WithoutExplicitMemberType_ProducesOneTestPerRow() + { var tests = await CreateTests(nameof(DataSample.FromTheoryData)); Assert.Equal(2, tests.Count); } [Fact] - public async Task MemberData_ReturningObjectArrays_ProducesOneTestPerRow() { + public async Task MemberData_ReturningObjectArrays_ProducesOneTestPerRow() + { var tests = await CreateTests(nameof(DataSample.FromObjectArrays)); Assert.Equal(2, tests.Count); } [Fact] - public async Task MemberData_ReturningTheoryDataRows_ProducesOneTestPerRow() { + public async Task MemberData_ReturningTheoryDataRows_ProducesOneTestPerRow() + { var tests = await CreateTests(nameof(DataSample.FromTheoryDataRows)); Assert.Equal(2, tests.Count); @@ -51,14 +54,16 @@ public async Task MemberData_ReturningTheoryDataRows_ProducesOneTestPerRow() { /// shape worked throughout. /// [Fact] - public async Task MemberData_WithExplicitMemberType_ProducesOneTestPerRow() { + public async Task MemberData_WithExplicitMemberType_ProducesOneTestPerRow() + { var tests = await CreateTests(nameof(DataSample.FromExplicitMemberType)); Assert.Equal(2, tests.Count); } [Fact] - public async Task ClassData_ProducesOneTestPerRow() { + public async Task ClassData_ProducesOneTestPerRow() + { var tests = await CreateTests(nameof(DataSample.FromClassData)); Assert.Equal(2, tests.Count); @@ -68,7 +73,8 @@ public async Task ClassData_ProducesOneTestPerRow() { /// The other control. [InlineData] carries its own literals and never needed the back-fill. /// [Fact] - public async Task InlineData_ProducesOneTestPerRow() { + public async Task InlineData_ProducesOneTestPerRow() + { var tests = await CreateTests(nameof(DataSample.FromInlineData)); Assert.Equal(2, tests.Count); @@ -87,7 +93,10 @@ public async Task InlineData_ProducesOneTestPerRow() { [InlineData(nameof(DataSample.TheoryDataRowsWithContainerParameter))] [InlineData(nameof(DataSample.ClassDataWithContainerParameter))] [InlineData(nameof(DataSample.InlineDataWithContainerParameter))] - public async Task RowSupplyingFewerArgumentsThanTheMethodTakes_ProducesOneTestPerRow(string methodName) { + public async Task RowSupplyingFewerArgumentsThanTheMethodTakes_ProducesOneTestPerRow( + string methodName + ) + { var tests = await CreateTests(methodName); Assert.Equal(2, tests.Count); @@ -99,9 +108,11 @@ public async Task RowSupplyingFewerArgumentsThanTheMethodTakes_ProducesOneTestPe /// above into a green suite instead of a red one. /// [Fact] - public async Task DataAttributeYieldingNoRows_Fails() { - var exception = await Assert.ThrowsAnyAsync( - async () => await CreateTests(nameof(DataSample.FromEmptySource))); + public async Task DataAttributeYieldingNoRows_Fails() + { + var exception = await Assert.ThrowsAnyAsync(async () => + await CreateTests(nameof(DataSample.FromEmptySource)) + ); Assert.Contains(nameof(DataSample.FromEmptySource), exception.Message); } @@ -111,24 +122,30 @@ public async Task DataAttributeYieldingNoRows_Fails() { /// test — the guard above must not catch it. /// [Fact] - public async Task NoDataAttribute_ProducesOneTest() { + public async Task NoDataAttribute_ProducesOneTest() + { var tests = await CreateTests(nameof(DataSample.NoRows)); Assert.Single(tests); } - private static async Task> CreateTests(string methodName) { + private static async Task> CreateTests(string methodName) + { var testMethod = BuildTestMethod(typeof(DataSample), methodName); var testCases = await new ModuleTestDiscoverer().Discover( - new DiscoveryOptions(), testMethod, new ModuleTestAttribute()); + new DiscoveryOptions(), + testMethod, + new ModuleTestAttribute() + ); var testCase = Assert.Single(testCases); return await testCase.CreateTests(); } - private static IXunitTestMethod BuildTestMethod(Type testClass, string methodName) { + private static IXunitTestMethod BuildTestMethod(Type testClass, string methodName) + { var assembly = new XunitTestAssembly(testClass.Assembly); var collection = new XunitTestCollection(assembly, null, false, "Test collection"); var xunitClass = new XunitTestClass(testClass, collection); @@ -137,8 +154,11 @@ private static IXunitTestMethod BuildTestMethod(Type testClass, string methodNam return new XunitTestMethod(xunitClass, method, []); } - private class DiscoveryOptions : ITestFrameworkDiscoveryOptions { - private readonly Dictionary _values = new(StringComparer.OrdinalIgnoreCase); + private class DiscoveryOptions : ITestFrameworkDiscoveryOptions + { + private readonly Dictionary _values = new( + StringComparer.OrdinalIgnoreCase + ); public TValue? GetValue(string name) => _values.TryGetValue(name, out var value) && value is TValue typed ? typed : default; @@ -161,10 +181,15 @@ private class DiscoveryOptions : ITestFrameworkDiscoveryOptions { // documented feature — the container supplies the rest — so the rule cannot hold here. Worth // noting that the guide teaches this shape without mentioning that it trips an analyzer error. #pragma warning disable xUnit1008, xUnit1037 - private class DataSample { + private class DataSample + { public static TheoryData Rows => new("first", "second"); - public static IEnumerable ObjectArrayRows => [["first"], ["second"]]; + public static IEnumerable ObjectArrayRows => + [ + ["first"], + ["second"], + ]; public static IEnumerable> TheoryDataRows => [new TheoryDataRow("first"), new TheoryDataRow("second")]; @@ -204,7 +229,10 @@ public void TheoryDataWithContainerParameter(string value, ContainerSupplied sup public void ObjectArraysWithContainerParameter(string value, ContainerSupplied supplied) { } [MemberData(nameof(TheoryDataRows))] - public void TheoryDataRowsWithContainerParameter(string value, ContainerSupplied supplied) { } + public void TheoryDataRowsWithContainerParameter( + string value, + ContainerSupplied supplied + ) { } [ClassData(typeof(SampleClassData))] public void ClassDataWithContainerParameter(string value, ContainerSupplied supplied) { } @@ -221,8 +249,10 @@ public void InlineDataWithContainerParameter(string value, ContainerSupplied sup private class ContainerSupplied { } #pragma warning restore xUnit1008, xUnit1037 - private class SampleClassData : TheoryData { - public SampleClassData() { + private class SampleClassData : TheoryData + { + public SampleClassData() + { Add("first"); Add("second"); } diff --git a/tests/DependencyModules.Tests/xUnitTests/ModuleTestDiscovererTests.cs b/tests/DependencyModules.Tests/xUnitTests/ModuleTestDiscovererTests.cs index 78d9caf..29a7bba 100644 --- a/tests/DependencyModules.Tests/xUnitTests/ModuleTestDiscovererTests.cs +++ b/tests/DependencyModules.Tests/xUnitTests/ModuleTestDiscovererTests.cs @@ -15,14 +15,15 @@ namespace DependencyModules.Tests.xUnitTests; /// exactly how a unique ID collision shipped undetected. Testing the discoverer from outside the /// framework it provides is the only way those failures become visible. /// -public class ModuleTestDiscovererTests { - +public class ModuleTestDiscovererTests +{ /// /// Regression test. Unique IDs used to be the bare method name, so two test classes each /// declaring a same-named test produced colliding IDs and xUnit silently discarded one. /// [Fact] - public async Task SameMethodNameInDifferentClasses_ProducesDifferentUniqueIDs() { + public async Task SameMethodNameInDifferentClasses_ProducesDifferentUniqueIDs() + { var first = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.SharedName)); var second = await DiscoverSingle(typeof(SecondSample), nameof(SecondSample.SharedName)); @@ -30,7 +31,8 @@ public async Task SameMethodNameInDifferentClasses_ProducesDifferentUniqueIDs() } [Fact] - public async Task SameMethodNameInDifferentClasses_ProducesDifferentDisplayNames() { + public async Task SameMethodNameInDifferentClasses_ProducesDifferentDisplayNames() + { var first = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.SharedName)); var second = await DiscoverSingle(typeof(SecondSample), nameof(SecondSample.SharedName)); @@ -38,14 +40,16 @@ public async Task SameMethodNameInDifferentClasses_ProducesDifferentDisplayNames } [Fact] - public async Task UniqueID_IsNotJustTheMethodName() { + public async Task UniqueID_IsNotJustTheMethodName() + { var testCase = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.SharedName)); Assert.NotEqual(nameof(FirstSample.SharedName), testCase.UniqueID); } [Fact] - public async Task UniqueID_IsStableAcrossRepeatedDiscovery() { + public async Task UniqueID_IsStableAcrossRepeatedDiscovery() + { var first = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.SharedName)); var second = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.SharedName)); @@ -53,7 +57,8 @@ public async Task UniqueID_IsStableAcrossRepeatedDiscovery() { } [Fact] - public async Task DifferentMethodsInOneClass_ProduceDifferentUniqueIDs() { + public async Task DifferentMethodsInOneClass_ProduceDifferentUniqueIDs() + { var first = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.SharedName)); var second = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.AnotherName)); @@ -61,21 +66,24 @@ public async Task DifferentMethodsInOneClass_ProduceDifferentUniqueIDs() { } [Fact] - public async Task DisplayName_IsQualifiedByItsDeclaringClass() { + public async Task DisplayName_IsQualifiedByItsDeclaringClass() + { var testCase = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.SharedName)); Assert.Contains(nameof(FirstSample), testCase.TestCaseDisplayName); } [Fact] - public async Task Discovery_ProducesExactlyOneTestCasePerMethod() { + public async Task Discovery_ProducesExactlyOneTestCasePerMethod() + { var cases = await Discover(typeof(FirstSample), nameof(FirstSample.SharedName)); Assert.Single(cases); } [Fact] - public async Task Discovery_ProducesAModuleTestCase() { + public async Task Discovery_ProducesAModuleTestCase() + { var testCase = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.SharedName)); Assert.IsType(testCase); @@ -86,15 +94,20 @@ public async Task Discovery_ProducesAModuleTestCase() { /// every sample method asserts the set is distinct, which is the property that was violated. /// [Fact] - public async Task EveryDiscoveredTestCase_HasADistinctUniqueID() { + public async Task EveryDiscoveredTestCase_HasADistinctUniqueID() + { var ids = new List(); - foreach (var (type, method) in new[] { - (typeof(FirstSample), nameof(FirstSample.SharedName)), - (typeof(FirstSample), nameof(FirstSample.AnotherName)), - (typeof(SecondSample), nameof(SecondSample.SharedName)), - (typeof(SecondSample), nameof(SecondSample.AnotherName)) - }) { + foreach ( + var (type, method) in new[] + { + (typeof(FirstSample), nameof(FirstSample.SharedName)), + (typeof(FirstSample), nameof(FirstSample.AnotherName)), + (typeof(SecondSample), nameof(SecondSample.SharedName)), + (typeof(SecondSample), nameof(SecondSample.AnotherName)), + } + ) + { ids.Add((await DiscoverSingle(type, method)).UniqueID); } @@ -109,7 +122,8 @@ public async Task EveryDiscoveredTestCase_HasADistinctUniqueID() { /// [ModuleTest]. /// [Fact] - public async Task Traits_OnTheTestMethod_ReachTheTestCase() { + public async Task Traits_OnTheTestMethod_ReachTheTestCase() + { var testCase = await DiscoverSingle(typeof(TraitSample), nameof(TraitSample.Categorised)); Assert.Contains("Fast", testCase.Traits["Category"]); @@ -127,14 +141,16 @@ public async Task Traits_OnTheTestMethod_ReachTheTestCase() { /// Traits_SurviveOntoTheCreatedTests do that. /// [Fact] - public async Task Traits_AreKeyedCaseInsensitively() { + public async Task Traits_AreKeyedCaseInsensitively() + { var testCase = await DiscoverSingle(typeof(TraitSample), nameof(TraitSample.Categorised)); Assert.True(testCase.Traits.ContainsKey("cAtEgOrY")); } [Fact] - public async Task Traits_KeepEveryValueOfARepeatedKey() { + public async Task Traits_KeepEveryValueOfARepeatedKey() + { var testCase = await DiscoverSingle(typeof(TraitSample), nameof(TraitSample.MultiValued)); Assert.Equal(["one", "two"], testCase.Traits["Category"].OrderBy(value => value)); @@ -153,9 +169,10 @@ public async Task Traits_KeepEveryValueOfARepeatedKey() { /// under the Xunit.Internal helpers or their replacement. /// [Fact] - public async Task Traits_SurviveOntoTheCreatedTests() { - var testCase = (ModuleTestCase)await DiscoverSingle( - typeof(TraitSample), nameof(TraitSample.Categorised)); + public async Task Traits_SurviveOntoTheCreatedTests() + { + var testCase = (ModuleTestCase) + await DiscoverSingle(typeof(TraitSample), nameof(TraitSample.Categorised)); var test = Assert.Single(await testCase.CreateTests()); @@ -163,8 +180,12 @@ public async Task Traits_SurviveOntoTheCreatedTests() { } [Fact] - public async Task Traits_AreNotSharedBetweenTestCases() { - var categorised = await DiscoverSingle(typeof(TraitSample), nameof(TraitSample.Categorised)); + public async Task Traits_AreNotSharedBetweenTestCases() + { + var categorised = await DiscoverSingle( + typeof(TraitSample), + nameof(TraitSample.Categorised) + ); var untraited = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.SharedName)); Assert.False(untraited.Traits.ContainsKey("Category")); @@ -185,7 +206,8 @@ public async Task Traits_AreNotSharedBetweenTestCases() { /// the location captured is this file. That is the point: it is a real usage site. /// [Fact] - public async Task TestCase_CarriesTheSourceLocationOfItsAttribute() { + public async Task TestCase_CarriesTheSourceLocationOfItsAttribute() + { var testCase = await DiscoverSingle(typeof(FirstSample), nameof(FirstSample.SharedName)); Assert.EndsWith("ModuleTestDiscovererTests.cs", testCase.SourceFilePath); @@ -203,7 +225,8 @@ public async Task TestCase_CarriesTheSourceLocationOfItsAttribute() { /// lose its source location. /// [Fact] - public void NamingOneModule_StillCapturesTheSourceLocation() { + public void NamingOneModule_StillCapturesTheSourceLocation() + { var attribute = new ModuleTestAttribute(typeof(FirstSample)); Assert.Single(attribute.ModuleTypes); @@ -220,7 +243,8 @@ public void NamingOneModule_StillCapturesTheSourceLocation() { /// says so. /// [Fact] - public void NamingSeveralModules_FallsBackToTheOverloadWithoutASourceLocation() { + public void NamingSeveralModules_FallsBackToTheOverloadWithoutASourceLocation() + { var attribute = new ModuleTestAttribute(typeof(FirstSample), typeof(SecondSample)); Assert.Equal(2, attribute.ModuleTypes.Length); @@ -230,21 +254,31 @@ public void NamingSeveralModules_FallsBackToTheOverloadWithoutASourceLocation() private static async Task DiscoverSingle(Type testClass, string methodName) => Assert.Single(await Discover(testClass, methodName)); - private static async Task> Discover(Type testClass, string methodName) { + private static async Task> Discover( + Type testClass, + string methodName + ) + { var testMethod = BuildTestMethod(testClass, methodName); // Supplied directly rather than read off the sample methods: annotating private nested // classes with [ModuleTest] makes the xUnit analyzer treat them as test classes. return await new ModuleTestDiscoverer().Discover( - new DiscoveryOptions(), testMethod, new ModuleTestAttribute()); + new DiscoveryOptions(), + testMethod, + new ModuleTestAttribute() + ); } /// /// The discoverer only reads method display settings, and xUnit falls back to its defaults for /// anything unset, so an empty option bag is enough to exercise it. /// - private class DiscoveryOptions : ITestFrameworkDiscoveryOptions { - private readonly Dictionary _values = new(StringComparer.OrdinalIgnoreCase); + private class DiscoveryOptions : ITestFrameworkDiscoveryOptions + { + private readonly Dictionary _values = new( + StringComparer.OrdinalIgnoreCase + ); public TValue? GetValue(string name) => _values.TryGetValue(name, out var value) && value is TValue typed ? typed : default; @@ -254,7 +288,8 @@ private class DiscoveryOptions : ITestFrameworkDiscoveryOptions { public string ToJson() => "{}"; } - private static IXunitTestMethod BuildTestMethod(Type testClass, string methodName) { + private static IXunitTestMethod BuildTestMethod(Type testClass, string methodName) + { var assembly = new XunitTestAssembly(testClass.Assembly); var collection = new XunitTestCollection(assembly, null, false, "Test collection"); var xunitClass = new XunitTestClass(testClass, collection); @@ -263,19 +298,22 @@ private static IXunitTestMethod BuildTestMethod(Type testClass, string methodNam return new XunitTestMethod(xunitClass, method, []); } - private class FirstSample { + private class FirstSample + { public void SharedName() { } public void AnotherName() { } } - private class SecondSample { + private class SecondSample + { public void SharedName() { } public void AnotherName() { } } - private class TraitSample { + private class TraitSample + { [Trait("Category", "Fast")] public void Categorised() { } From 9cc0bbabef6a2dfce84e9d8b789f70c7af20da61 Mon Sep 17 00:00:00 2001 From: Ian Johnson Date: Sun, 13 Sep 2026 12:45:27 -0400 Subject: [PATCH 2/3] Check CSharpier formatting on commit and in CI The formatter only holds if something runs it. The pre-commit hook is the fast answer and the CI step is the guarantee, since a hook lives in a clone and --no-verify skips it. .editorconfig repeats the same layout for the IDE. Without it Rider and Visual Studio format to their own defaults as you type, and the next commit undoes it. .git-blame-ignore-revs keeps the reformat from claiming the last change to every line. Co-Authored-By: Claude Opus 5 (1M context) --- .editorconfig | 53 ++++++++++++++++++++++++++++ .git-blame-ignore-revs | 6 ++++ .githooks/pre-commit | 45 +++++++++++++++++++++++ .github/workflows/build-package.yaml | 8 +++++ CONTRIBUTING.md | 46 ++++++++++++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 .editorconfig create mode 100644 .git-blame-ignore-revs create mode 100755 .githooks/pre-commit create mode 100644 CONTRIBUTING.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..41aceaf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,53 @@ +root = true + +# CSharpier is what enforces the layout below; this file exists so an IDE reaches the same answer +# while you type, rather than reformatting to its own defaults and losing the change at the next +# commit. CSharpier reads indent_style, indent_size, max_line_length and end_of_line from here, so +# those four are shared settings rather than a second opinion. Everything else describes what +# CSharpier already does and is not configurable in it. +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{json,yaml,yml,md}] +indent_size = 2 + +[*.md] +# Two trailing spaces is a line break in Markdown. +trim_trailing_whitespace = false + +[*.{csproj,props,targets}] +# Not formatted by CSharpier — see .csharpierignore. +indent_size = 4 + +[*.cs] +max_line_length = 100 + +# Allman. This is the one CSharpier does not offer a choice about, and the reason the repo moved +# off K&R: matching the formatter is cheaper than arguing with it. +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after + +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..31210aa --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +# Revisions that only moved whitespace. GitHub honours this file automatically; for local blame: +# +# git config blame.ignoreRevsFile .git-blame-ignore-revs + +# Reformat every C# file with CSharpier +79a2881a3fd722dd95f8aa589567003acb896678 diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..0429329 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,45 @@ +#!/bin/sh +# +# Rejects a commit whose C# is not CSharpier-formatted. +# +# Enable it once per clone: +# +# git config core.hooksPath .githooks +# +# A hook is local and skippable with --no-verify, so it is the fast answer rather than the +# guarantee; build-package.yaml runs the same check on every pull request. + +set -eu + +staged=$(git diff --cached --name-only --diff-filter=ACMR -- '*.cs') + +if [ -z "$staged" ]; then + exit 0 +fi + +if ! command -v dotnet >/dev/null 2>&1; then + echo "pre-commit: dotnet is not on PATH, skipping the format check." >&2 + exit 0 +fi + +# The manifest pins the version, so every clone and CI agree on what formatted means. +if ! dotnet tool run csharpier --version >/dev/null 2>&1; then + dotnet tool restore >/dev/null +fi + +# Split on newlines only, so a path with spaces stays one argument, and with globbing off so one +# with a bracket in it is not expanded. Named paths skip CSharpier's cache and cost about 50ms +# each, which is worth paying to check the files being committed rather than the whole tree. +IFS=' +' +set -f +set -- $staged +set +f + +# The working tree is what gets checked, not the staged content. They differ only when a file is +# staged in part, which is rare enough not to pay for a temporary checkout on every commit. +if ! dotnet csharpier check "$@"; then + echo >&2 + echo "pre-commit: run 'dotnet csharpier format .' and stage the result." >&2 + exit 1 +fi diff --git a/.github/workflows/build-package.yaml b/.github/workflows/build-package.yaml index 6d0bbeb..2c8632c 100644 --- a/.github/workflows/build-package.yaml +++ b/.github/workflows/build-package.yaml @@ -43,6 +43,14 @@ jobs: - run: dotnet restore DependencyModules.sln + # The pre-commit hook in .githooks runs this too, but a hook is local and skippable. This is + # what actually holds the formatting. It runs before the build so a style-only failure costs + # seconds rather than a full compile and test cycle. + - name: Check formatting + run: | + dotnet tool restore + dotnet csharpier check . + - run: dotnet build DependencyModules.sln --no-restore --configuration Release # Runs every test suite with coverage collection and merges the results. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6784ad7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing + +## Setup + +```sh +git config core.hooksPath .githooks +git config blame.ignoreRevsFile .git-blame-ignore-revs +dotnet tool restore +``` + +The first line turns on the pre-commit hook, which rejects a commit whose C# is not formatted. Git +does not carry hooks across a clone, so this is the one step that cannot be automated for you. + +The second keeps the CSharpier reformat out of `git blame`, which otherwise reports it as the last +change to nearly every line in the repo. GitHub already reads that file without being asked. + +## Formatting + +C# layout is [CSharpier](https://csharpier.com)'s, and the version is pinned in +`.config/dotnet-tools.json` so every clone and CI agree on what formatted means. Braces are Allman. +Nothing about the style is up for discussion in review — run the formatter: + +```sh +dotnet csharpier format . +``` + +`.editorconfig` describes the same layout for your IDE, so typing and formatting do not disagree. +Project files are excluded (see `.csharpierignore`); CSharpier reindents MSBuild XML but leaves the +interior of multi-line comments where it was, which this repo has a lot of. + +`build-package.yaml` runs `dotnet csharpier check .` on every pull request. The hook is the fast +answer, that check is the guarantee. + +## Build and test + +```sh +dotnet build DependencyModules.sln +dotnet test DependencyModules.sln +``` + +Both target frameworks are built, so running the tests needs the .NET 8 runtime alongside the .NET +10 SDK that `global.json` selects. + +`./scripts/coverage.sh 85` runs every suite with coverage and fails under the threshold, the same +way CI does. `./scripts/verify-packages.sh` packs the libraries and consumes them from a real +package reference, which is the only thing that catches a packaging fault. From 51a3ecbe2e8e323a931601d87ed4cfe274b1427a Mon Sep 17 00:00:00 2001 From: Ian Johnson Date: Sun, 13 Sep 2026 12:58:05 -0400 Subject: [PATCH 3/3] Move the code samples in the docs to Allman CSharpier does not reach these: a sample in a ``` fence is prose, and one in a block is a comment. They were the last K&R left outside the generator tests. Brace placement only, rather than running the samples through CSharpier. It agrees on every brace, and disagrees on much else a doc sample decides deliberately: it collapses the fluent chain in the README onto one line, squashes comments that are aligned to be read down the column, and expands a one-line `interface IEmailSender { void Send(); }` into four. Checked by extracting all 227 fences and diffing against CSharpier's own output, which now differs on no brace. The generator tests keep theirs. Those samples are C# in a string, fed to Roslyn to compile; several tests assert on the line and column a diagnostic lands at. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 13 +++++--- .../aot-decorators-and-convention-cost.md | 3 +- .../convention-registration-and-decorators.md | 27 ++++++++++------ docs/design/runtime-module-graph.md | 6 ++-- .../FakeItEasySupportAttribute.cs | 3 +- .../MoqSupportAttribute.cs | 3 +- .../NSubstituteSupportAttribute.cs | 3 +- .../Attributes/ModuleTestAttribute.cs | 3 +- .../Attributes/ModuleTestCaseAttribute.cs | 3 +- .../Attributes/DecoratorAttribute.cs | 3 +- .../Conventions/ConventionContracts.cs | 6 ++-- .../Interception/IInterceptor.cs | 19 ++++++++---- .../ModuleEnvironment.cs | 3 +- website/guide/conventions.md | 15 ++++++--- website/guide/decorators.md | 22 ++++++++----- website/guide/environments.md | 22 ++++++++----- website/guide/extending.md | 25 ++++++++------- website/guide/getting-started.md | 9 ++++-- website/guide/interception.md | 31 +++++++++++++------ website/guide/modules.md | 9 ++++-- website/guide/services.md | 9 ++++-- website/guide/testing-mocking.md | 27 +++++++++------- website/guide/testing-nunit.md | 15 ++++++--- website/guide/testing-registrations.md | 3 +- website/guide/testing-xunit.md | 21 ++++++++----- website/guide/testing.md | 24 +++++++++----- website/index.md | 6 ++-- website/reference/conventions-api.md | 6 ++-- website/reference/diagnostics.md | 16 ++++++---- website/reference/interfaces.md | 9 ++++-- 30 files changed, 237 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 4aede94..ef76acf 100644 --- a/README.md +++ b/README.md @@ -106,8 +106,10 @@ time: ```csharp [DependencyModule] -public partial class HandlerModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class HandlerModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll(typeof(IRequestHandler<,>)).AsScoped(); conventions.RegisterAll(typeof(IValidator<>)) @@ -172,12 +174,13 @@ Tests receive their dependencies as method parameters, against the real registra [assembly: ApplicationModule] [assembly: NSubstituteSupport] -public class OrderTests { +public class OrderTests +{ [ModuleTest] public async Task PlaceOrder_PricesThroughTheChannel( IRequestHandler handler, - [Mock] IBookRepository books) { - + [Mock] IBookRepository books) + { books.Find("isbn-1", Arg.Any()) .Returns(new Book("isbn-1", 20m)); diff --git a/docs/design/aot-decorators-and-convention-cost.md b/docs/design/aot-decorators-and-convention-cost.md index 61dcc85..5151fdc 100644 --- a/docs/design/aot-decorators-and-convention-cost.md +++ b/docs/design/aot-decorators-and-convention-cost.md @@ -695,7 +695,8 @@ has to learn from `P`, and who owns the knowledge of how to build `P`'s decorato `P`'s generator emits, next to the decorator, a generic static method: ```csharp -public static class LoggingBehaviorRegistration { +public static class LoggingBehaviorRegistration +{ public static void ApplyTo(IServiceCollection services) => DecoratorHelper.Decorate(services, typeof(IRequestHandler), (provider, inner) => new LoggingBehavior( diff --git a/docs/design/convention-registration-and-decorators.md b/docs/design/convention-registration-and-decorators.md index 3c2c797..ad684eb 100644 --- a/docs/design/convention-registration-and-decorators.md +++ b/docs/design/convention-registration-and-decorators.md @@ -116,8 +116,10 @@ attributes: ```csharp [DependencyModule] -public partial class DataModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class DataModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsScoped(); } } @@ -254,7 +256,8 @@ real DLL, then a **second compilation referencing it and containing no handlers was run through an `IIncrementalGenerator`, which emitted: ```csharp -private static void ModuleDependencies(IServiceCollection services) { +private static void ModuleDependencies(IServiceCollection services) +{ services.AddScoped(typeof(global::TheLibrary.IHandler), typeof(global::TheLibrary.CreateOrderHandler)); services.AddScoped(typeof(global::TheLibrary.IHandler), typeof(global::TheLibrary.RenameOrderHandler)); } @@ -567,7 +570,8 @@ public interface IRepository { Item Get(int id); } public class Repository : IRepository { ... } [Decorator(Order = 1)] -public class CachingRepository(IRepository inner, IMemoryCache cache) : IRepository { +public class CachingRepository(IRepository inner, IMemoryCache cache) : IRepository +{ public Item Get(int id) => cache.GetOrCreate(id, _ => inner.Get(id))!; } ``` @@ -603,8 +607,10 @@ Step 4 is two lines and fixes a public API that is currently a no-op, independen Descriptor rewrite, the same approach Scrutor takes: ```csharp -private static void ModuleDecorators(IServiceCollection services) { - for (var i = services.Count - 1; i >= 0; i--) { +private static void ModuleDecorators(IServiceCollection services) +{ + for (var i = services.Count - 1; i >= 0; i--) + { var descriptor = services[i]; if (descriptor.ServiceType != typeof(global::App.IRepository)) continue; @@ -693,7 +699,8 @@ Removes the boilerplate of writing a decorator that overrides one member and for ```csharp [Decorator] -public partial class CachingRepository(IRepository inner, IMemoryCache cache) : IRepository { +public partial class CachingRepository(IRepository inner, IMemoryCache cache) : IRepository +{ public Item Get(int id) => cache.GetOrCreate(id, _ => inner.Get(id))!; // every other IRepository member is generated, forwarding to inner } @@ -786,7 +793,8 @@ interceptor does. That is a stronger differentiator than Scrutor parity. **Tier 1, lifecycle hooks.** No boxing, no argument array. ```csharp -public interface IInterceptor { +public interface IInterceptor +{ void OnEnter(string member); void OnExit(string member); void OnError(string member, Exception exception); @@ -801,7 +809,8 @@ signature. **Tier 2, full invocation.** Opt-in, for caching, retry and authorisation. ```csharp -public interface IInvocationInterceptor { +public interface IInvocationInterceptor +{ ValueTask InterceptAsync(IInvocation invocation); // arguments, ProceedAsync, short-circuit } ``` diff --git a/docs/design/runtime-module-graph.md b/docs/design/runtime-module-graph.md index 597fe27..85c9619 100644 --- a/docs/design/runtime-module-graph.md +++ b/docs/design/runtime-module-graph.md @@ -29,7 +29,8 @@ and the dependency methods name each registration. The smallest useful thing: `LoadModules` registers what it loaded. ```csharp -public interface IModuleGraph { +public interface IModuleGraph +{ IReadOnlyList Modules { get; } bool Contains() where TModule : IDependencyModule; } @@ -44,7 +45,8 @@ two questions with the sharpest failure modes today. The generator emits, per module, the service types it registers, and the runtime assembles them: ```csharp -public interface IModuleGraph { +public interface IModuleGraph +{ IReadOnlyList Modules { get; } } diff --git a/src/DependencyModules.FakeItEasy/FakeItEasySupportAttribute.cs b/src/DependencyModules.FakeItEasy/FakeItEasySupportAttribute.cs index fe9c2dc..8b9720a 100644 --- a/src/DependencyModules.FakeItEasy/FakeItEasySupportAttribute.cs +++ b/src/DependencyModules.FakeItEasy/FakeItEasySupportAttribute.cs @@ -16,7 +16,8 @@ namespace DependencyModules.FakeItEasy; /// /// [ModuleTest] /// [FakeItEasySupport] -/// public void SendsTheMail(IEmailSender sender, [Mock] IAuditLog log) { +/// public void SendsTheMail(IEmailSender sender, [Mock] IAuditLog log) +/// { /// A.CallTo(() => log.Write(A<string>._)).MustHaveHappened(); /// } /// diff --git a/src/DependencyModules.Moq/MoqSupportAttribute.cs b/src/DependencyModules.Moq/MoqSupportAttribute.cs index 2d21609..b22295e 100644 --- a/src/DependencyModules.Moq/MoqSupportAttribute.cs +++ b/src/DependencyModules.Moq/MoqSupportAttribute.cs @@ -25,7 +25,8 @@ namespace DependencyModules.Moq; /// /// [ModuleTest] /// [MoqSupport] -/// public void SendsTheMail(IEmailSender sender, Mock<IAuditLog> log) { +/// public void SendsTheMail(IEmailSender sender, Mock<IAuditLog> log) +/// { /// log.Verify(x => x.Write(It.IsAny<string>())); /// } /// diff --git a/src/DependencyModules.NSubstitute/NSubstituteSupportAttribute.cs b/src/DependencyModules.NSubstitute/NSubstituteSupportAttribute.cs index 24f7614..19c05c9 100644 --- a/src/DependencyModules.NSubstitute/NSubstituteSupportAttribute.cs +++ b/src/DependencyModules.NSubstitute/NSubstituteSupportAttribute.cs @@ -17,7 +17,8 @@ namespace DependencyModules.NSubstitute; /// /// [ModuleTest] /// [NSubstituteSupport] -/// public void SendsTheMail(IEmailSender sender, [Mock] IAuditLog log) { +/// public void SendsTheMail(IEmailSender sender, [Mock] IAuditLog log) +/// { /// log.Received().Write(Arg.Any<string>()); /// } /// diff --git a/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs b/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs index bf41e3c..2462e61 100644 --- a/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs +++ b/src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs @@ -29,7 +29,8 @@ namespace DependencyModules.NUnit.Attributes; /// /// /// [ModuleTest(typeof(MyModule))] -/// public void ResolvesTheService(IMyService service) { +/// public void ResolvesTheService(IMyService service) +/// { /// Assert.That(service, Is.Not.Null); /// } /// diff --git a/src/DependencyModules.NUnit/Attributes/ModuleTestCaseAttribute.cs b/src/DependencyModules.NUnit/Attributes/ModuleTestCaseAttribute.cs index 9668e66..1c2141f 100644 --- a/src/DependencyModules.NUnit/Attributes/ModuleTestCaseAttribute.cs +++ b/src/DependencyModules.NUnit/Attributes/ModuleTestCaseAttribute.cs @@ -40,7 +40,8 @@ public interface IModuleTestDataAttribute /// [ModuleTest(typeof(MyModule))] /// [ModuleTestCase(1, "one")] /// [ModuleTestCase(2, "two")] -/// public void Converts(int number, string word, INumberFormatter formatter) { +/// public void Converts(int number, string word, INumberFormatter formatter) +/// { /// Assert.That(formatter.Spell(number), Is.EqualTo(word)); /// } /// diff --git a/src/DependencyModules.Runtime/Attributes/DecoratorAttribute.cs b/src/DependencyModules.Runtime/Attributes/DecoratorAttribute.cs index 7a9ce30..599e97b 100644 --- a/src/DependencyModules.Runtime/Attributes/DecoratorAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/DecoratorAttribute.cs @@ -11,7 +11,8 @@ namespace DependencyModules.Runtime.Attributes; /// /// /// [Decorator(Order = 100)] -/// public class CachingRepository(IRepository inner, IMemoryCache cache) : IRepository { +/// public class CachingRepository(IRepository inner, IMemoryCache cache) : IRepository +/// { /// public Item Get(int id) => cache.GetOrCreate(id, _ => inner.Get(id))!; /// } /// diff --git a/src/DependencyModules.Runtime/Conventions/ConventionContracts.cs b/src/DependencyModules.Runtime/Conventions/ConventionContracts.cs index be0be8c..1b7bcda 100644 --- a/src/DependencyModules.Runtime/Conventions/ConventionContracts.cs +++ b/src/DependencyModules.Runtime/Conventions/ConventionContracts.cs @@ -46,8 +46,10 @@ namespace DependencyModules.Runtime.Conventions /// /// /// [DependencyModule] - /// public partial class DataModule : IConventionModule { - /// void IConventionModule.Conventions(IConventionDefinitions conventions) { + /// public partial class DataModule : IConventionModule + /// { + /// void IConventionModule.Conventions(IConventionDefinitions conventions) + /// { /// conventions.RegisterAll<IRepository>().AsScoped(); /// conventions.RegisterAll(typeof(IRequestHandler<,>)).AsTransient(); /// } diff --git a/src/DependencyModules.Runtime/Interception/IInterceptor.cs b/src/DependencyModules.Runtime/Interception/IInterceptor.cs index 7bd317f..46bf3e3 100644 --- a/src/DependencyModules.Runtime/Interception/IInterceptor.cs +++ b/src/DependencyModules.Runtime/Interception/IInterceptor.cs @@ -17,12 +17,16 @@ namespace DependencyModules.Runtime.Interception; /// cannot serve rather than skipping them silently. /// /// -/// public TResult Intercept<TResult>(InvocationContext<TResult> context) { +/// public TResult Intercept<TResult>(InvocationContext<TResult> context) +/// { /// var stopwatch = Stopwatch.StartNew(); /// -/// try { +/// try +/// { /// return context.Proceed(); -/// } finally { +/// } +/// finally +/// { /// _log.Record(context.Caller, stopwatch.Elapsed); /// } /// } @@ -52,7 +56,8 @@ public interface IInterceptor /// body, state spanning the call is an ordinary local and a scope may be held across it. /// /// -/// public async ValueTask<TResult> InterceptAsync<TResult>(AsyncInvocationContext<TResult> context) { +/// public async ValueTask<TResult> InterceptAsync<TResult>(AsyncInvocationContext<TResult> context) +/// { /// using var scope = _tracer.StartSpan(context.Caller.MemberName); /// /// return await context.ProceedAsync(); @@ -82,10 +87,12 @@ public interface IAsyncInterceptor /// An interceptor here enumerates the stream, and so observes each item as it is produced. /// /// -/// public async IAsyncEnumerable<TItem> InterceptStream<TItem>(StreamInvocationContext<TItem> context) { +/// public async IAsyncEnumerable<TItem> InterceptStream<TItem>(StreamInvocationContext<TItem> context) +/// { /// var count = 0; /// -/// await foreach (var item in context.Proceed()) { +/// await foreach (var item in context.Proceed()) +/// { /// count++; /// yield return item; /// } diff --git a/src/DependencyModules.Runtime/ModuleEnvironment.cs b/src/DependencyModules.Runtime/ModuleEnvironment.cs index a889b05..b74e371 100644 --- a/src/DependencyModules.Runtime/ModuleEnvironment.cs +++ b/src/DependencyModules.Runtime/ModuleEnvironment.cs @@ -15,7 +15,8 @@ namespace DependencyModules.Runtime; /// Values can be supplied inline, since this is a collection of them: /// /// services.AddModules( -/// new ModuleEnvironment("Development") { +/// new ModuleEnvironment("Development") +/// { /// { "FEATURE_PROFILING", "on" }, /// { "REGION", "eu" } /// }, diff --git a/website/guide/conventions.md b/website/guide/conventions.md index 89f6556..ca0701d 100644 --- a/website/guide/conventions.md +++ b/website/guide/conventions.md @@ -24,8 +24,10 @@ State the rule once, and let the generator find the types that fit **while it bu using DependencyModules.Runtime.Conventions; [DependencyModule] -public partial class DataModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class DataModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll(typeof(IRequestHandler<,>)).AsTransient(); } } @@ -311,12 +313,15 @@ Use `IServiceCollectionConfiguration` for those, alongside your conventions: ```csharp [DependencyModule] -public partial class DataModule : IConventionModule, IServiceCollectionConfiguration { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class DataModule : IConventionModule, IServiceCollectionConfiguration +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().AsScoped(); } - public void ConfigureServices(IServiceCollection services) { + public void ConfigureServices(IServiceCollection services) + { // unrestricted access to IServiceCollection, at run time } } diff --git a/website/guide/decorators.md b/website/guide/decorators.md index f8fb57d..8e41b50 100644 --- a/website/guide/decorators.md +++ b/website/guide/decorators.md @@ -6,7 +6,8 @@ You want to cache the results of a repository: ```csharp [SingletonService] -public class SqlRepository : IRepository { +public class SqlRepository : IRepository +{ public Item Get(int id) => /* a database round trip */; } ``` @@ -25,12 +26,14 @@ Write the wrapper as an ordinary class, mark it `[Decorator]`, and it takes over public interface IRepository { Item Get(int id); } [SingletonService] -public class SqlRepository : IRepository { +public class SqlRepository : IRepository +{ public Item Get(int id) => /* … */; } [Decorator] -public class CachingRepository(IRepository inner, IMemoryCache cache) : IRepository { +public class CachingRepository(IRepository inner, IMemoryCache cache) : IRepository +{ public Item Get(int id) => cache.GetOrCreate(id, _ => inner.Get(id))!; } ``` @@ -80,9 +83,10 @@ validators, written once: [Decorator] public class LoggingHandler( IRequestHandler inner, ILogger log) - : IRequestHandler { - - public TResponse Handle(TRequest request) { + : IRequestHandler +{ + public TResponse Handle(TRequest request) + { log.LogInformation("handling {Request}", typeof(TRequest).Name); return inner.Handle(request); } @@ -106,8 +110,10 @@ circuit breaker only in production: ```csharp [Decorator] [IfEnvironment("Development")] -public class LoggingRepository(IRepository inner, ILogger log) : IRepository { - public Item Get(int id) { +public class LoggingRepository(IRepository inner, ILogger log) : IRepository +{ + public Item Get(int id) + { log.LogInformation("getting {Id}", id); return inner.Get(id); } diff --git a/website/guide/environments.md b/website/guide/environments.md index c41cc48..fe6c5be 100644 --- a/website/guide/environments.md +++ b/website/guide/environments.md @@ -6,9 +6,12 @@ You do not want your development machine sending real email. So the registration ```csharp // Program.cs -if (builder.Environment.IsDevelopment()) { +if (builder.Environment.IsDevelopment()) +{ services.AddSingleton(); -} else { +} +else +{ services.AddSingleton(); } ``` @@ -78,7 +81,8 @@ Values go inline, since a `ModuleEnvironment` is a collection of them: ```csharp services.AddModules( - new ModuleEnvironment("Development") { + new ModuleEnvironment("Development") + { { "FEATURE_PROFILING", "on" }, { "REGION", "eu" } }, @@ -97,7 +101,8 @@ A key you did write wins — including one written as `null`, which is how you h same name: ```csharp -new ModuleEnvironment("Development") { +new ModuleEnvironment("Development") +{ { "REGION", "eu" }, // wins over any REGION variable { "FEATURE_PROFILING", null } // hides a FEATURE_PROFILING variable } @@ -255,9 +260,12 @@ For registration that depends on the environment but is not a simple condition: ```csharp [DependencyModule] -public partial class ApplicationModule : IEnvironmentServiceCollectionConfiguration { - public void ConfigureServices(IServiceCollection services, IModuleEnvironment environment) { - if (environment.Value("REGION") == "eu") { +public partial class ApplicationModule : IEnvironmentServiceCollectionConfiguration +{ + public void ConfigureServices(IServiceCollection services, IModuleEnvironment environment) + { + if (environment.Value("REGION") == "eu") + { services.AddSingleton(); } } diff --git a/website/guide/extending.md b/website/guide/extending.md index 9c96dca..2506956 100644 --- a/website/guide/extending.md +++ b/website/guide/extending.md @@ -45,9 +45,10 @@ that want module models: ```csharp [Generator] -public class MySourceGenerator : BaseSourceGenerator { - - protected override IEnumerable AttributeSourceGenerators() { +public class MySourceGenerator : BaseSourceGenerator +{ + protected override IEnumerable AttributeSourceGenerators() + { yield return new MyGenerator(); } @@ -62,12 +63,13 @@ match: nothing else can write those modules, so the base class writes them for y ```csharp [Generator] -public class MyFrameworkGenerator : BaseSourceGenerator { - +public class MyFrameworkGenerator : BaseSourceGenerator +{ protected override ITypeDefinition[] ModuleAttributeTypes() => [TypeDefinition.Get("My.Framework", "MyModuleAttribute")]; - protected override IEnumerable AttributeSourceGenerators() { + protected override IEnumerable AttributeSourceGenerators() + { yield return new MyGenerator(); } } @@ -88,12 +90,12 @@ things follow from declaring your own attribute: provider of every discovered module paired with the configuration in effect: ```csharp -public class MyGenerator : IDependencyModuleSourceGenerator { - +public class MyGenerator : IDependencyModuleSourceGenerator +{ public void SetupGenerator( IncrementalGeneratorInitializationContext context, - IncrementalValuesProvider<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> modules) { - + IncrementalValuesProvider<(ModuleEntryPointModel Left, DependencyModuleConfigurationModel Right)> modules) + { var candidates = context.SyntaxProvider .CreateSyntaxProvider(IsCandidate, GetModel) .Where(model => !model.IsIgnored) @@ -108,7 +110,8 @@ For an attribute-driven mechanism, `BaseAttributeSourceGenerator` does m supply the attribute types, a transform, a comparer and an ignored sentinel: ```csharp -public class MyGenerator : BaseAttributeSourceGenerator { +public class MyGenerator : BaseAttributeSourceGenerator +{ protected override IEnumerable AttributeTypes() => [MyAttributeType]; protected override MyModel GenerateAttributeModel(GeneratorAttributeSyntaxContext c, CancellationToken t) => …; protected override IEqualityComparer GetComparer() => new MyModelComparer(); diff --git a/website/guide/getting-started.md b/website/guide/getting-started.md index 72428a8..9e5ce82 100644 --- a/website/guide/getting-started.md +++ b/website/guide/getting-started.md @@ -79,7 +79,8 @@ namespace MyApp; public interface IEmailSender { void Send(string to); } [SingletonService] -public class SmtpEmailSender : IEmailSender { +public class SmtpEmailSender : IEmailSender +{ public void Send(string to) { } } ``` @@ -104,7 +105,8 @@ If you want to add a `ConfigureServices` to that generated module, declare the p `[DependencyModule]` and implement `IServiceCollectionConfiguration`: ```csharp -public partial class ApplicationModule : IServiceCollectionConfiguration { +public partial class ApplicationModule : IServiceCollectionConfiguration +{ public void ConfigureServices(IServiceCollection services) => services.AddHttpClient(); } @@ -148,7 +150,8 @@ reads as concrete rather than magic. Turn it on: Build, then open `obj/…/ApplicationModule.Dependencies.g.cs`. Inside it: ```csharp -private static void ModuleDependencies(IServiceCollection services) { +private static void ModuleDependencies(IServiceCollection services) +{ services.AddSingleton(typeof(MyApp.IEmailSender), typeof(MyApp.SmtpEmailSender)); } ``` diff --git a/website/guide/interception.md b/website/guide/interception.md index 28c123e..c94798c 100644 --- a/website/guide/interception.md +++ b/website/guide/interception.md @@ -21,13 +21,18 @@ interface and routes its members through it — every member by default, and [the kinds you name](#covering-some-members-and-not-others) when that is too much: ```csharp -public class TimingInterceptor(ILogger log) : IInterceptor { - public TResult Intercept(InvocationContext context) { +public class TimingInterceptor(ILogger log) : IInterceptor +{ + public TResult Intercept(InvocationContext context) + { var stopwatch = Stopwatch.StartNew(); - try { + try + { return context.Proceed(); - } finally { + } + finally + { log.LogInformation("{Member} took {Elapsed}", context.Caller.MemberName, stopwatch.Elapsed); } } @@ -64,10 +69,12 @@ Implement whichever kinds your services actually have: One type may implement any combination, and **the generator picks per member**: ```csharp -public class TracingInterceptor : IInterceptor, IAsyncInterceptor { +public class TracingInterceptor : IInterceptor, IAsyncInterceptor +{ public TResult Intercept(InvocationContext context) => context.Proceed(); - public async ValueTask InterceptAsync(AsyncInvocationContext context) { + public async ValueTask InterceptAsync(AsyncInvocationContext context) + { using var span = tracer.StartSpan(context.Caller.MemberName); return await context.ProceedAsync(); @@ -86,7 +93,8 @@ which means anything after the await runs once the work has genuinely finished whole call sits in one method body, state that spans it is an ordinary local: ```csharp -public async ValueTask InterceptAsync(AsyncInvocationContext context) { +public async ValueTask InterceptAsync(AsyncInvocationContext context) +{ using var scope = _tracer.StartSpan(context.Caller.MemberName); // spans the whole call return await context.ProceedAsync(); @@ -101,10 +109,12 @@ An `IAsyncEnumerable` member returns its stream immediately, before any item interceptor enumerates it, so it observes each item as it is produced: ```csharp -public async IAsyncEnumerable InterceptStream(StreamInvocationContext context) { +public async IAsyncEnumerable InterceptStream(StreamInvocationContext context) +{ var count = 0; - await foreach (var item in context.Proceed()) { + await foreach (var item in context.Proceed()) + { count++; yield return item; } @@ -271,7 +281,8 @@ public class AuditInterceptor : IInterceptor { … } // sync only [SingletonService] [Intercept(typeof(AuditInterceptor))] -public class Orders : IOrders { +public class Orders : IOrders +{ public int Count(string customer) { … } // audited public Task CountAsync(string customer) { … } // not audited } diff --git a/website/guide/modules.md b/website/guide/modules.md index 58756ca..30897a9 100644 --- a/website/guide/modules.md +++ b/website/guide/modules.md @@ -179,7 +179,8 @@ properties, and the generated attribute mirrors them: ```csharp [DependencyModule] -public partial class ApplicationModule { +public partial class ApplicationModule +{ public string? ConnectionString { get; set; } } ``` @@ -218,8 +219,10 @@ binding, anything from a third-party library with its own extension method. Impl ```csharp [DependencyModule] -public partial class ApplicationModule : IServiceCollectionConfiguration { - public void ConfigureServices(IServiceCollection services) { +public partial class ApplicationModule : IServiceCollectionConfiguration +{ + public void ConfigureServices(IServiceCollection services) + { services.AddHttpClient(); } } diff --git a/website/guide/services.md b/website/guide/services.md index 2bfad8a..c570e92 100644 --- a/website/guide/services.md +++ b/website/guide/services.md @@ -129,8 +129,10 @@ public class BusinessRules : IValidator { } ```csharp [ScopedService] -public class OrderService(IEnumerable validators) { - public void Place(Order order) { +public class OrderService(IEnumerable validators) +{ + public void Place(Order order) + { foreach (var validator in validators) { // RequiredFields, then BusinessRules validator.Validate(order); } @@ -179,7 +181,8 @@ configuration, an object built by a factory somewhere else. Put the attribute on method** instead of on the class: ```csharp -public class SomeClass : ISomeInterface { +public class SomeClass : ISomeInterface +{ public SomeClass(IDep one, IDepTwo two, DateTime timestamp) { } [SingletonService] diff --git a/website/guide/testing-mocking.md b/website/guide/testing-mocking.md index 939cf42..e8f1269 100644 --- a/website/guide/testing-mocking.md +++ b/website/guide/testing-mocking.md @@ -7,7 +7,8 @@ occasionally the problem. One of the services behind `Weather` is non-determinis ```csharp [SingletonService] -public class TemperatureProvider : ITemperatureProvider { +public class TemperatureProvider : ITemperatureProvider +{ public int GetTemperature() => Random.Shared.Next(-20, 55); } ``` @@ -26,8 +27,8 @@ resolved. Everything constructed afterwards gets the substitute: public void GetStaticForecast( Weather weather, [Mock] ITemperatureProvider temperatureProvider, - [Mock] IAiSummaryProvider aiSummaryProvider) { - + [Mock] IAiSummaryProvider aiSummaryProvider) +{ temperatureProvider.GetTemperature().Returns(38); aiSummaryProvider.GetSummary().Returns("Sunny"); @@ -135,7 +136,8 @@ using DependencyModules.NSubstitute; ```csharp [ModuleTest] -public void SendsTheMail(IEmailSender sender, [Mock] IAuditLog log) { +public void SendsTheMail(IEmailSender sender, [Mock] IAuditLog log) +{ sender.Send("someone@example.com"); log.Received().Write(Arg.Any()); @@ -156,7 +158,8 @@ using DependencyModules.FakeItEasy; ```csharp [ModuleTest] -public void SendsTheMail(IEmailSender sender, [Mock] IAuditLog log) { +public void SendsTheMail(IEmailSender sender, [Mock] IAuditLog log) +{ sender.Send("someone@example.com"); A.CallTo(() => log.Write(A._)).MustHaveHappened(); @@ -173,7 +176,8 @@ Moq is the one that needs a paragraph, because it keeps the mock and the object ```csharp [ModuleTest] -public void SendsTheMail(IEmailSender sender, [Mock] IAuditLog log) { +public void SendsTheMail(IEmailSender sender, [Mock] IAuditLog log) +{ Mock.Get(log).Verify(x => x.Write(It.IsAny())); } ``` @@ -188,8 +192,8 @@ says what it is: public void GetStaticForecast( Weather weather, Mock temperatureProvider, - Mock aiSummaryProvider) { - + Mock aiSummaryProvider) +{ temperatureProvider.Setup(x => x.GetTemperature()).Returns(38); aiSummaryProvider.Setup(x => x.GetSummary()).Returns("Sunny"); @@ -227,8 +231,8 @@ default and one test opts out: ```csharp [TestExport(typeof(IClock), Implementation = typeof(SystemClock))] // the fixture default -public class ExpiryTests { - +public class ExpiryTests +{ [ModuleTest] public void UsesTheRealClock(IClock clock) { } // SystemClock @@ -247,7 +251,8 @@ service mocked, so a `[TestExport]` still beats it: ```csharp [ModuleTest] [TestExport(typeof(IClock), Implementation = typeof(SystemClock))] -public void RealClock(IClock clock, Mock mock) { +public void RealClock(IClock clock, Mock mock) +{ Assert.IsType(clock); // the export won } ``` diff --git a/website/guide/testing-nunit.md b/website/guide/testing-nunit.md index ad1f761..d59c594 100644 --- a/website/guide/testing-nunit.md +++ b/website/guide/testing-nunit.md @@ -10,10 +10,12 @@ dotnet add package DependencyModules.NUnit ```csharp using DependencyModules.NUnit.Attributes; -public class WeatherTests { +public class WeatherTests +{ [ModuleTest] [ApplicationModule] - public void GetForecast(Weather weather) { + public void GetForecast(Weather weather) + { var forecast = weather.GetWeatherForecast().ToArray(); Assert.That(forecast, Has.Length.EqualTo(5)); @@ -38,7 +40,8 @@ each `[Repeat]` pass and each `[Retry]` attempt, not just each test case: [ModuleTest] [ApplicationModule] [Repeat(3)] -public void EachPassStartsClean(ICallCounter counter) { +public void EachPassStartsClean(ICallCounter counter) +{ counter.Record(); Assert.That(counter.Count, Is.EqualTo(1)); // never 2, never 3 @@ -66,7 +69,8 @@ after: [ApplicationModule] [ModuleTestCase("one")] [ModuleTestCase("two")] -public void MultipleRows(string value, ITemperatureProvider provider) { +public void MultipleRows(string value, ITemperatureProvider provider) +{ Assert.That(value, Is.Not.Null); // from [ModuleTestCase] Assert.That(provider, Is.Not.Null); // from the container } @@ -90,7 +94,8 @@ To supply rows from somewhere other than an attribute literal, implement `IModul ```csharp [AttributeUsage(AttributeTargets.Method)] -public class CsvRowsAttribute(string path) : Attribute, IModuleTestDataAttribute { +public class CsvRowsAttribute(string path) : Attribute, IModuleTestDataAttribute +{ public IEnumerable GetRows(MethodInfo method) => File.ReadLines(path).Select(line => line.Split(',').Cast().ToArray()); } diff --git a/website/guide/testing-registrations.md b/website/guide/testing-registrations.md index f63421b..ff916e5 100644 --- a/website/guide/testing-registrations.md +++ b/website/guide/testing-registrations.md @@ -75,7 +75,8 @@ Both sides fit in one theory: [Theory] [InlineData("Development", typeof(FakeEmailSender))] [InlineData("Production", typeof(SmtpEmailSender))] -public void SelectsTheSenderByEnvironment(string environment, Type expected) { +public void SelectsTheSenderByEnvironment(string environment, Type expected) +{ var services = new ServiceCollection(); services.AddModules(new ModuleEnvironment(environment), new ApplicationModule()); diff --git a/website/guide/testing-xunit.md b/website/guide/testing-xunit.md index 44a2363..5258524 100644 --- a/website/guide/testing-xunit.md +++ b/website/guide/testing-xunit.md @@ -10,10 +10,12 @@ dotnet add package DependencyModules.xUnit ```csharp using DependencyModules.xUnit.Attributes; -public class WeatherTests { +public class WeatherTests +{ [ModuleTest] [ApplicationModule] - public void GetForecast(Weather weather) { + public void GetForecast(Weather weather) + { var forecast = weather.GetWeatherForecast().ToArray(); Assert.Equal(5, forecast.Length); @@ -89,7 +91,8 @@ implementing `IDataAttribute`. Row arguments come first, injected ones after: [ModuleTest] [InlineData("one")] [InlineData("two")] -public void MultipleRows(string value, ITemperatureProvider provider) { +public void MultipleRows(string value, ITemperatureProvider provider) +{ Assert.NotNull(value); // from [InlineData] Assert.NotNull(provider); // from the container } @@ -119,7 +122,8 @@ green. `TheoryDataRow`'s own metadata is honoured per row, so a single row can skip or carry its own traits: ```csharp -public static TheoryData Cases => new() { +public static TheoryData Cases => new() +{ new TheoryDataRow("ok"), new TheoryDataRow("broken") { Skip = "pending #412" }, }; @@ -136,7 +140,8 @@ test: ```csharp [ModuleTest] -public void KnowsWhatItIs(ITestCaseInfo testCase) { +public void KnowsWhatItIs(ITestCaseInfo testCase) +{ IXunitTestMethod method = testCase.TestMethod; Assert.Equal(nameof(KnowsWhatItIs), method.MethodName); @@ -167,10 +172,12 @@ a different container: ```csharp [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)] -public class ValidatingProviderAttribute : Attribute, IServiceProviderBuilderAttribute { +public class ValidatingProviderAttribute : Attribute, IServiceProviderBuilderAttribute +{ public IServiceProvider BuildServiceProvider( ITestMethodContext testMethod, IServiceCollection serviceCollection) => - serviceCollection.BuildServiceProvider(new ServiceProviderOptions { + serviceCollection.BuildServiceProvider(new ServiceProviderOptions + { ValidateScopes = true, ValidateOnBuild = true, }); diff --git a/website/guide/testing.md b/website/guide/testing.md index 8e289a7..b7194a6 100644 --- a/website/guide/testing.md +++ b/website/guide/testing.md @@ -6,7 +6,8 @@ Here is a service with two dependencies, one of which has a dependency of its ow ```csharp [SingletonService] -public class Weather(ISummaryProvider summaryProvider, ITemperatureProvider temperatureProvider) { +public class Weather(ISummaryProvider summaryProvider, ITemperatureProvider temperatureProvider) +{ public IEnumerable GetWeatherForecast() { /* … */ } } ``` @@ -42,10 +43,12 @@ services your test needs arrive as **method parameters**, resolved from a provid real modules: ```csharp -public class WeatherTests { +public class WeatherTests +{ [ModuleTest] [ApplicationModule] - public void GetForecast(Weather weather) { + public void GetForecast(Weather weather) + { var forecast = weather.GetWeatherForecast().ToArray(); // assert on forecast @@ -123,7 +126,8 @@ they belong. Every test in the project now gets `ApplicationModule` without saying so: ```csharp -public class WeatherTests { +public class WeatherTests +{ [ModuleTest] public void UsesTheAssemblyModules(Weather weather) { } @@ -147,7 +151,8 @@ Within a test, ask for `IServiceProvider` and create scopes as usual: ```csharp [ModuleTest] -public void ScopedServicesAreScoped(IServiceProvider provider) { +public void ScopedServicesAreScoped(IServiceProvider provider) +{ using var first = provider.CreateScope(); using var second = provider.CreateScope(); @@ -201,7 +206,8 @@ differently, a mock makes you stub out every member you touch. `[TestExport]` registers a real type into the test's container without touching the module: ```csharp -public class FixedClock : IClock { +public class FixedClock : IClock +{ public DateTime UtcNow => new(2026, 1, 1); } @@ -231,7 +237,8 @@ data rather than a service. `[InjectValues]` supplies the parts the container ca public record InjectModel(IDependencyOne DependencyOne, string StringValue); [ModuleTest] -public void InjectTestValue([InjectValues("Hello World!")] InjectModel model) { +public void InjectTestValue([InjectValues("Hello World!")] InjectModel model) +{ // model.DependencyOne came from the container // model.StringValue came from the attribute } @@ -248,7 +255,8 @@ parameter that should simply **be** a value wants a data row instead. `[InlineDa [ModuleTest] [InlineData("978-0132350884")] [InlineData("978-0201616224")] -public async Task GetBook_FindsEachIsbn(string isbn, IRequestHandler handler) { +public async Task GetBook_FindsEachIsbn(string isbn, IRequestHandler handler) +{ // isbn came from the row, handler from the container } ``` diff --git a/website/index.md b/website/index.md index 47171ec..677ee99 100644 --- a/website/index.md +++ b/website/index.md @@ -109,8 +109,10 @@ next year. ```csharp [DependencyModule] -public partial class HandlerModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class HandlerModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll(typeof(IRequestHandler<,>)).AsScoped(); conventions.RegisterAll().InNamespaceOf().AsScoped(); } diff --git a/website/reference/conventions-api.md b/website/reference/conventions-api.md index d96ea91..4ea449f 100644 --- a/website/reference/conventions-api.md +++ b/website/reference/conventions-api.md @@ -5,8 +5,10 @@ fit together. ```csharp [DependencyModule] -public partial class DataModule : IConventionModule { - void IConventionModule.Conventions(IConventionDefinitions conventions) { +public partial class DataModule : IConventionModule +{ + void IConventionModule.Conventions(IConventionDefinitions conventions) + { conventions.RegisterAll().InNamespaceOf().AsScoped(); } } diff --git a/website/reference/diagnostics.md b/website/reference/diagnostics.md index 851b8e1..fb4e3cd 100644 --- a/website/reference/diagnostics.md +++ b/website/reference/diagnostics.md @@ -235,7 +235,8 @@ public class AuditInterceptor : IInterceptor { … } // sync only [SingletonService] [Intercept(typeof(AuditInterceptor))] -public class Orders : IOrders { +public class Orders : IOrders +{ public int Count(string customer) { … } // audited public Task CountAsync(string customer) { … } // DM0015 — not audited } @@ -298,7 +299,8 @@ while the nested declaration never implemented `IDependencyModule` — a green b nothing. ```csharp -public static class Outer { +public static class Outer +{ [DependencyModule] public partial class NestedModule; // DM0017 } @@ -322,7 +324,8 @@ silently. ```csharp [DependencyModule] -public partial class CacheModule : IServiceCollectionConfiguration { +public partial class CacheModule : IServiceCollectionConfiguration +{ public int SizeLimit { get; set; } // DM0018 public void ConfigureServices(IServiceCollection services) => services.AddSingleton(new CacheSettings(SizeLimit)); @@ -401,7 +404,8 @@ The case that reaches here is a realm-only module registering the class *by conv public class Greeter : IGreeter { … } [DependencyModule(OnlyRealm = true)] -public partial class GreetingModule : IConventionModule { +public partial class GreetingModule : IConventionModule +{ void IConventionModule.Conventions(IConventionDefinitions conventions) => conventions.RegisterAll().AsSingleton(); } @@ -440,8 +444,8 @@ under it, and one test overriding that for one argument is what having both scop ```csharp [TestExport(typeof(IClock), Implementation = typeof(SystemClock))] // the fixture default -public class ExpiryTests { - +public class ExpiryTests +{ [ModuleTest] public void UsesTheRealClock(IClock clock) { } // SystemClock diff --git a/website/reference/interfaces.md b/website/reference/interfaces.md index e30e3e7..4f94fe2 100644 --- a/website/reference/interfaces.md +++ b/website/reference/interfaces.md @@ -42,8 +42,10 @@ calls it alongside the registrations it wrote: ```csharp [DependencyModule] -public partial class ApplicationModule : IServiceCollectionConfiguration { - public void ConfigureServices(IServiceCollection services) { +public partial class ApplicationModule : IServiceCollectionConfiguration +{ + public void ConfigureServices(IServiceCollection services) + { services.AddHttpClient(); services.Configure(options => options.SizeLimit = 1024); } @@ -56,7 +58,8 @@ gets an `ApplicationModule` it did not declare; adding a partial for it **withou with it: ```csharp -public partial class ApplicationModule : IServiceCollectionConfiguration { +public partial class ApplicationModule : IServiceCollectionConfiguration +{ public void ConfigureServices(IServiceCollection services) => services.AddHttpClient(); }