diff --git a/src/DependencyModules.Runtime/Attributes/InterceptAttribute.cs b/src/DependencyModules.Runtime/Attributes/InterceptAttribute.cs index 16cd910..1adba36 100644 --- a/src/DependencyModules.Runtime/Attributes/InterceptAttribute.cs +++ b/src/DependencyModules.Runtime/Attributes/InterceptAttribute.cs @@ -34,4 +34,16 @@ public class InterceptAttribute(params Type[] interceptors) : Attribute { /// . /// public int Order { get; set; } + + /// + /// Restricts the interception to a single module, matching the Realm property on the service + /// registration attributes and on . + /// + /// + /// Without a realm the interception belongs to every module that is not realm-only, which is how + /// services and decorators already behave. An OnlyRealm module previously received every + /// interception in the compilation regardless — including ones naming no realm at all — so a + /// module built to be isolated wrapped services it had never heard of. + /// + public Type? Realm { get; set; } } diff --git a/src/DependencyModules.SourceGenerator.Impl/AnalyzerReleases.Unshipped.md b/src/DependencyModules.SourceGenerator.Impl/AnalyzerReleases.Unshipped.md index f2b7fad..fa9d395 100644 --- a/src/DependencyModules.SourceGenerator.Impl/AnalyzerReleases.Unshipped.md +++ b/src/DependencyModules.SourceGenerator.Impl/AnalyzerReleases.Unshipped.md @@ -1,2 +1,9 @@ ; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +DM0017 | DependencyModules | Error | A dependency module is declared inside another type. +DM0018 | DependencyModules | Info | A module with settable properties relies on generated equality. diff --git a/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs b/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs index de44627..a1ab779 100644 --- a/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs +++ b/src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs @@ -230,10 +230,18 @@ private ModuleEntryPointModel GetClassEntryPointModel(GeneratorSyntaxContext con 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) { + features |= ModuleEntryPointFeatures.NestedInType; + } return new ModuleEntryPointModel( features, context.Node.SyntaxTree?.FilePath ?? "", + LocationModel.From(context.Node), ((TypeDeclarationSyntax)context.Node).GetTypeDefinition(), dependencyFlags.RegistrationType, dependencyFlags.GenerateAttribute, @@ -276,6 +284,7 @@ private ModuleEntryPointModel GetCompilationUnitSyntaxEntry(GeneratorSyntaxConte return new ModuleEntryPointModel( ModuleEntryPointFeatures.AutoGenerateModule, context.Node.SyntaxTree?.FilePath ?? "", + LocationModel.From(context.Node), TypeDefinition.Get("", "ApplicationModule"), null, true, diff --git a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs index 5505421..cb5a394 100644 --- a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs +++ b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleDiagnostics.cs @@ -56,6 +56,55 @@ public static class DependencyModuleDiagnostics { defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); + /// + /// Raised for a module declared inside another type. + /// + /// + /// The generated half is written at namespace level, so a nested declaration produces a second, + /// detached type of the same name while the nested one never implements + /// IDependencyModule. AddModule<Outer.Nested>() then fails to compile, but + /// [assembly: Nested] binds to the detached type's attribute and builds green, registering + /// nothing. + /// + 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.", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// Raised for a module carrying settable properties while relying on the generated + /// Equals/GetHashCode. + /// + /// + /// Modules de-duplicate by type, which is what stops a module reached twice from registering + /// everything twice. A module with parameters is a different case: two instances carrying + /// different values are the same module by that rule, so the first one reached wins and the + /// other is silently discarded. Supplying an Equals that accounts for the properties + /// suppresses the generated one and makes the intent explicit either way. + /// + 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.", + category: Category, + // Info, not Warning. Dedupe-by-type is correct for the common case — a parameterised module + // composed once — and this repo's own integration tests carry eleven such modules that are + // not doing anything wrong. Only a module reached twice with different values is actually + // bitten, and that is not decidable here. Promote it per project with + // dotnet_diagnostic.DM0018.severity = warning if you want it enforced. + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); + /// /// Raised when two conventions in one module register a type as the same service type. /// @@ -89,10 +138,11 @@ public static class DependencyModuleDiagnostics { public static readonly DiagnosticDescriptor ConventionMatchedNothing = new( id: "DM0005", title: "Convention matched no types", - messageFormat: - "The convention registering '{0}' in '{1}' matched no types. " + - "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.", + // The advice is a parameter rather than a fixed tail, because the causes need different + // fixes. Suggesting IncludeBaseClasses() unconditionally told a reader who had already + // called it — or who had named a class, which can never match whatever they call — to go + // looking for a typo in their own code. + messageFormat: "The convention registering '{0}' in '{1}' matched no types. {2}.", category: Category, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); diff --git a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs index 7e36124..4345f0c 100644 --- a/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs +++ b/src/DependencyModules.SourceGenerator.Impl/DependencyModuleWriter.cs @@ -75,12 +75,35 @@ private void ProcessEntryPoint( context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.ModuleMustBePartial, - Location.None, + entryPointModel.Location.ToLocationOrNone(), entryPointModel.EntryPointType.Name)); return; } + // Generating anyway would emit a same-named type at namespace level, which compiles and + // registers nothing — the failure this is here to replace. + if (entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.NestedInType)) { + context.ReportDiagnostic( + Diagnostic.Create( + DependencyModuleDiagnostics.ModuleCannotBeNested, + entryPointModel.Location.ToLocationOrNone(), + entryPointModel.EntryPointType.Name)); + + return; + } + + // Only when the generator is supplying equality. A module that declares its own Equals has + // already answered the question this asks about. + if (entryPointModel.PropertyInfoModels.Count > 0 && + entryPointModel.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.ShouldImplementEquals)) { + context.ReportDiagnostic( + Diagnostic.Create( + DependencyModuleDiagnostics.ModuleWithPropertiesShouldImplementEquals, + entryPointModel.Location.ToLocationOrNone(), + entryPointModel.EntryPointType.Name)); + } + entryPointModel = EntryModelUtil.EnsureNamespace(entryPointModel,configurationModel); var csharpFile = new CSharpFileDefinition(entryPointModel.EntryPointType.Namespace); diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/DecoratorModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/DecoratorModel.cs index 2acc6c6..496e90d 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/DecoratorModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/DecoratorModel.cs @@ -42,7 +42,14 @@ public record DecoratorModel( IReadOnlyList? Conditions = null, ConstructorInfoModel? Constructor = null, int InnerParameterIndex = -1, - bool TypeParametersMatchService = true) { + bool TypeParametersMatchService = true, + + /// + /// 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) { /// /// Whether the decorator can be constructed by generated code. diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/InterceptorModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/InterceptorModel.cs index 08e8669..ee273a2 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/InterceptorModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/InterceptorModel.cs @@ -344,7 +344,14 @@ public record InterceptorModel( IReadOnlyList Declarations, int Order, InterceptionRefusal? Refusal = null, - IReadOnlyList? TypeParameters = 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) { /// /// Whether the intercepted service is an open generic, and so registers as an implementation type @@ -368,8 +375,8 @@ public record InterceptorModel( /// 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) => - Ignore with { Refusal = new InterceptionRefusal(message) }; + public static InterceptorModel Refused(string message, LocationModel? location = null) => + Ignore with { Refusal = new InterceptionRefusal(message), Location = location }; public bool IsIgnored => ReferenceEquals(this, Ignore); } diff --git a/src/DependencyModules.SourceGenerator/Conventions/Models/LocationModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/LocationModel.cs similarity index 98% rename from src/DependencyModules.SourceGenerator/Conventions/Models/LocationModel.cs rename to src/DependencyModules.SourceGenerator.Impl/Models/LocationModel.cs index 93c302f..1c3a68e 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Models/LocationModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/LocationModel.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -namespace DependencyModules.Conventions.Models; +namespace DependencyModules.SourceGenerator.Impl.Models; /// /// Where a declaration sits in source, in a form that can live in an incremental model. diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/ModuleEntryPointModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/ModuleEntryPointModel.cs index 7e76bc6..01afeb2 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/ModuleEntryPointModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/ModuleEntryPointModel.cs @@ -14,11 +14,22 @@ public enum ModuleEntryPointFeatures { /// The module was declared without the partial modifier, so the generator cannot complete it. /// NotPartial = 16, + + /// + /// The module was declared inside another type rather than directly in a namespace. + /// + /// + /// Documented as always wrong in the README, the modules guide and the troubleshooting guide, + /// and until now reported by none of them: the generator emitted a second, detached type at + /// namespace level, the nested declaration never became a module, and the build stayed green. + /// + NestedInType = 32, } public record ModuleEntryPointModel( ModuleEntryPointFeatures ModuleFeatures, string FileLocation, + LocationModel Location, ITypeDefinition EntryPointType, RegistrationType? RegistrationType, bool? GenerateAttribute, @@ -39,6 +50,12 @@ 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 + // comment — and this comparer is the incremental cache key. Including it regenerated every + // 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 && diff --git a/src/DependencyModules.SourceGenerator.Impl/Models/ServiceModel.cs b/src/DependencyModules.SourceGenerator.Impl/Models/ServiceModel.cs index 874fe42..aa2f5ed 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Models/ServiceModel.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Models/ServiceModel.cs @@ -79,7 +79,15 @@ public record ServiceModel( /// attributes are declared on the class, so every registration it produces shares them and the /// writer emits one guard around the lot. /// - IReadOnlyList? Conditions = null) { + 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) { public static ServiceModel Ignore = new ServiceModel( TypeDefinition.Get("", "Ignore"), null, diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs index 405a28b..21a3d14 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/DecoratorModelUtility.cs @@ -83,7 +83,8 @@ public static class DecoratorModelUtility { conditions, constructor, IndexOfInnerParameter(constructor, written), - TypeParametersMatchService(typeDeclarationSyntax, written)); + TypeParametersMatchService(typeDeclarationSyntax, written), + LocationModel.From(typeDeclarationSyntax)); } /// diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs index 65b3f23..6f7ddf3 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/InterceptorModelUtility.cs @@ -55,12 +55,14 @@ public static InterceptorModel GetInterceptorModel( var interceptorSymbols = new List(); var order = 0; INamedTypeSymbol? explicitService = null; + INamedTypeSymbol? realm = null; // 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) { - ReadAttribute(attribute, context, cancellationToken, interceptorSymbols, ref order, ref explicitService); + ReadAttribute( + attribute, context, cancellationToken, interceptorSymbols, ref order, ref explicitService, ref realm); } if (interceptorSymbols.Count == 0) { @@ -70,11 +72,11 @@ public static InterceptorModel GetInterceptorModel( var serviceSymbol = ResolveServiceInterface(implementationSymbol, explicitService, out var unsupported); if (serviceSymbol == null) { - return Refuse(unsupported); + return Refuse(unsupported, context); } if (!InterceptedMemberReader.Read(serviceSymbol, out var members, out var declarations, out unsupported)) { - return Refuse(unsupported); + return Refuse(unsupported, context); } if (members.Count == 0) { @@ -101,13 +103,15 @@ public static InterceptorModel GetInterceptorModel( members, declarations, order, - TypeParameters: TypeParameterModels(implementationSymbol)); + TypeParameters: TypeParameterModels(implementationSymbol), + Realm: realm?.GetTypeDefinition(), + Location: LocationModel.From(context.Node)); } - private static InterceptorModel Refuse(string? reason) => + private static InterceptorModel Refuse(string? reason, SyntaxTransformContext context) => reason == null ? InterceptorModel.Ignore - : InterceptorModel.Refused(reason); + : InterceptorModel.Refused(reason, LocationModel.From(context.Node)); /// /// The interfaces an interceptor implements, which decide the members it can be placed around. @@ -167,7 +171,8 @@ private static void ReadAttribute( CancellationToken cancellationToken, List interceptors, ref int order, - ref INamedTypeSymbol? explicitService) { + ref INamedTypeSymbol? explicitService, + ref INamedTypeSymbol? realm) { if (attribute.ArgumentList == null) { return; @@ -199,6 +204,11 @@ private static void ReadAttribute( explicitService = ResolveType(serviceTypeOf, context, cancellationToken); } break; + case "Realm": + if (argument.Expression is TypeOfExpressionSyntax realmTypeOf) { + realm = ResolveType(realmTypeOf, context, cancellationToken); + } + break; } } } diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs index 019b700..60cc613 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs @@ -70,6 +70,21 @@ public class ServiceModelUtility { 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) { + return ServiceLifestyle.Singleton; + } + + if (attributeType.Name == KnownTypes.DependencyModules.Attributes.ScopedServiceAttribute.Name) { + return ServiceLifestyle.Scoped; + } + + return ServiceLifestyle.Transient; + } + public static ServiceModel? GetServiceModel( SyntaxTransformContext context, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -111,7 +126,8 @@ public class ServiceModelUtility { null, factoryModel, null, GetRegistrations(context, returnType, models, cancellationToken), - RegistrationFeature.None); + RegistrationFeature.None, + Location: LocationModel.From(context.Node)); } private static ServiceFactoryModel? GetFactoryModel(SyntaxTransformContext context, MethodDeclarationSyntax methodDeclarationSyntax, CancellationToken cancellationToken) { @@ -173,7 +189,8 @@ private static RegistrationFeature GetConstructionFeatures(SyntaxNode node) { ServiceLifestyle.Transient ) }, - RegistrationFeature.AutoRegisterSourceGenerator + RegistrationFeature.AutoRegisterSourceGenerator, + Location: LocationModel.From(context.Node) ); } @@ -190,7 +207,8 @@ private static RegistrationFeature GetConstructionFeatures(SyntaxNode node) { factoryOutput, registrations, GetConstructionFeatures(context.Node), - EnvironmentConditionUtility.GetConditions(context, context.Node, cancellationToken)); + EnvironmentConditionUtility.GetConditions(context, context.Node, cancellationToken), + LocationModel.From(context.Node)); } /// @@ -328,7 +346,7 @@ private static List GetRegistrations(SyntaxTransformCo // skipped — leaving the class unregistered with nothing to say so. if (AttributeTypeMatcher.Matches( context.SemanticModel, attributeSyntax, typeDefinition, cancellationToken)) { - list.Add(GetServiceRegistration(context, attributeSyntax, classDefinition)); + list.Add(GetServiceRegistration(context, attributeSyntax, classDefinition, typeDefinition)); } } @@ -418,15 +436,22 @@ private static ServiceLifestyle GetLifestyle(string toString) { return ServiceLifestyle.Singleton; } - private static ServiceRegistrationModel GetServiceRegistration(SyntaxTransformContext context, AttributeSyntax attributeSyntax, ITypeDefinition classDefinition) { - var lifestyle = ServiceLifestyle.Transient; - - if (attributeSyntax.Name.ToString().StartsWith("Singleton")) { - lifestyle = ServiceLifestyle.Singleton; - } - else if (attributeSyntax.Name.ToString().StartsWith("Scoped")) { - lifestyle = ServiceLifestyle.Scoped; - } + private static ServiceRegistrationModel GetServiceRegistration( + SyntaxTransformContext context, + AttributeSyntax attributeSyntax, + ITypeDefinition classDefinition, + 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:: + // prefix, and any `using` alias not named after its target all fell through to Transient — + // registered, with the wrong lifetime, and nothing said so. + // + // This costs nothing extra. AttributeTypeMatcher has already resolved the usage through the + // semantic model to decide that this attribute matched at all; the caller simply passes on + // which one it was rather than discarding the answer. + var lifestyle = LifestyleOf(attributeType); ITypeDefinition? registration = null; RegistrationType? registrationType = null; diff --git a/src/DependencyModules.SourceGenerator/Conventions/ConventionGenerator.cs b/src/DependencyModules.SourceGenerator/Conventions/ConventionGenerator.cs index 89177dc..845ba5f 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/ConventionGenerator.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/ConventionGenerator.cs @@ -456,7 +456,7 @@ private static void ReportOpenGenericDecoration( context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.OpenGenericCannotBeDecorated, - Location.None, + decorator.Location?.ToLocationOrNone() ?? Location.None, serviceName, decoratorName)); } @@ -483,7 +483,7 @@ private static void ReportAmbiguousOrdering( context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.AmbiguousDecoratorOrder, - Location.None, + decorators[i].Location?.ToLocationOrNone() ?? Location.None, decorators[i].DecoratorType.Name, decorators[j].DecoratorType.Name, decorators[i].ServiceType.Name, @@ -516,7 +516,7 @@ private static void ReportUnclaimedModules( context.ReportDiagnostic(Diagnostic.Create( DependencyModuleDiagnostics.ConventionCannotBeRead, - Location.None, + conventionModule.Location?.ToLocationOrNone() ?? Location.None, "the declaring type is not marked with [DependencyModule], so it registers nothing", name)); } diff --git a/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionModel.cs b/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionModel.cs index 0a0a0ef..df23c2c 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionModel.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Models/ConventionModel.cs @@ -291,7 +291,12 @@ public record UnreadableStatementModel(string Text, string Reason, LocationModel public record ConventionModuleModel( ITypeDefinition ModuleType, IReadOnlyList Conventions, - IReadOnlyList Unreadable) { + IReadOnlyList Unreadable, + + /// + /// Where the declaring type sits, so a diagnostic about the module itself can point at it. + /// + LocationModel? Location = null) { /// /// The sentinel for a declaration this generator does not own, matching how every other model diff --git a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateCache.cs b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateCache.cs index 4daf43b..55b243a 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateCache.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionCandidateCache.cs @@ -2,6 +2,7 @@ using DependencyModules.Conventions.Models; using DependencyModules.SourceGenerator.Impl.Utilities; using Microsoft.CodeAnalysis; +using DependencyModules.SourceGenerator.Impl.Models; namespace DependencyModules.Conventions.Utilities; diff --git a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionMatcher.cs b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionMatcher.cs index 3c3c635..62bbc55 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionMatcher.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionMatcher.cs @@ -190,10 +190,40 @@ or ConventionRegisterAs.AlsoSelf DependencyModuleDiagnostics.ConventionMatchedNothing, convention.Location.ToLocationOrNone(), serviceName, - moduleName)); + moduleName, + AdviceForEmptyMatch(convention, serviceName))); } } + /// + /// What to tell someone whose convention matched nothing, chosen by why it could not match. + /// + /// + /// A class named as the service type is the sharpest case: conventions match a type through the + /// interfaces it declares, so no call and no filter change can ever make one match. That is also + /// 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"; + } + + 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"; + } + /// /// Translates each name glob to an anchored regular expression, once per convention. /// diff --git a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs index a8f1a79..7a32e00 100644 --- a/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs +++ b/src/DependencyModules.SourceGenerator/Conventions/Utilities/ConventionModelUtility.cs @@ -146,7 +146,8 @@ public static ConventionModuleModel GetConventionModuleModel( ReadStatement(context, statement, parameterName!, conventions, unreadable); } - return new ConventionModuleModel(typeDeclaration.GetTypeDefinition(), conventions, unreadable); + return new ConventionModuleModel( + typeDeclaration.GetTypeDefinition(), conventions, unreadable, LocationModel.From(typeDeclaration)); } private static void ReadStatement( diff --git a/src/DependencyModules.SourceGenerator/InterceptorSourceGenerator.cs b/src/DependencyModules.SourceGenerator/InterceptorSourceGenerator.cs index d894a98..a989df6 100644 --- a/src/DependencyModules.SourceGenerator/InterceptorSourceGenerator.cs +++ b/src/DependencyModules.SourceGenerator/InterceptorSourceGenerator.cs @@ -73,7 +73,11 @@ protected override void GenerateSourceOutput( writer.Write(model, wrapperName, model.ImplementationType.Namespace)); } - // One registration file per module, so every wrapper is applied wherever its service is. + // One registration file per module, carrying the interceptions that belong to that module. + // This used to hand every module the whole compilation's worth: an OnlyRealm module emitted + // 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)) { var registrationWriter = new InterceptorRegistrationWriter(); @@ -83,10 +87,41 @@ protected override void GenerateSourceOutput( registrationWriter.Write( EntryModelUtil.EnsureNamespace(entryPointModel, configurationModel), configurationModel, - usable)); + ForModule(usable, entryPointModel))); } } + /// + /// The interceptions one module is responsible for applying. + /// + /// + /// The same rule services and decorators already follow: a realm-scoped interception belongs + /// only to the module it names, and an unscoped one belongs to every module that is not + /// realm-only. + /// + private static IReadOnlyList ForModule( + 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)) { + selected.Add(model); + } + + continue; + } + + if (!onlyRealm) { + selected.Add(model); + } + } + + return selected; + } + /// /// Reports declarations that cannot be intercepted and drops them, so an unsupported shape /// produces an explanation rather than a wrapper that does not compile. @@ -103,7 +138,7 @@ private static IReadOnlyList ReportUnsupported( context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.CannotIntercept, - Location.None, + model.Location?.ToLocationOrNone() ?? Location.None, model.Refusal.Message)); continue; @@ -172,7 +207,7 @@ private static void ReportUnservedMembers( context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.InterceptorCannotServeMembers, - Location.None, + model.Location?.ToLocationOrNone() ?? Location.None, interceptor.Type.Name, InterfaceFor(kind), DescriptionFor(kind), diff --git a/src/DependencyModules.SourceGenerator/ServiceSourceGenerator.cs b/src/DependencyModules.SourceGenerator/ServiceSourceGenerator.cs index b356e56..6acc624 100644 --- a/src/DependencyModules.SourceGenerator/ServiceSourceGenerator.cs +++ b/src/DependencyModules.SourceGenerator/ServiceSourceGenerator.cs @@ -154,7 +154,7 @@ private static ImmutableArray ReportUnconstructableServices( context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.ServiceCannotBeConstructed, - Location.None, + serviceModel.Location?.ToLocationOrNone() ?? Location.None, typeName, reason)); } @@ -200,7 +200,7 @@ private static ImmutableArray ReportCrossWiredGenerics( context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.CrossWireCannotBeGeneric, - Location.None, + serviceModel.Location?.ToLocationOrNone() ?? Location.None, typeName)); } @@ -246,7 +246,7 @@ private static void ReportEnvironmentConditions( context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.EmptyEnvironmentCondition, - Location.None, + serviceModel.Location?.ToLocationOrNone() ?? Location.None, typeName, kind)); } @@ -267,7 +267,7 @@ private static void ReportEnvironmentConditions( context.ReportDiagnostic( Diagnostic.Create( DependencyModuleDiagnostics.RegisteredConditionally, - Location.None, + serviceModel.Location?.ToLocationOrNone() ?? Location.None, summary)); } } diff --git a/tests/DependencyModules.Tests/Infrastructure/ModelFactory.cs b/tests/DependencyModules.Tests/Infrastructure/ModelFactory.cs index 6e0b922..b4c5721 100644 --- a/tests/DependencyModules.Tests/Infrastructure/ModelFactory.cs +++ b/tests/DependencyModules.Tests/Infrastructure/ModelFactory.cs @@ -26,6 +26,7 @@ public static ModuleEntryPointModel EntryPoint( new( features, fileLocation, + LocationModel.None, entryPointType ?? TypeDefinition.Get("TestNamespace", "TestModule"), registrationType, generateAttribute, diff --git a/tests/DependencyModules.Tests/Snapshots/PublicApiTests.RuntimeApi.verified.txt b/tests/DependencyModules.Tests/Snapshots/PublicApiTests.RuntimeApi.verified.txt index 6f3b991..33fc788 100644 --- a/tests/DependencyModules.Tests/Snapshots/PublicApiTests.RuntimeApi.verified.txt +++ b/tests/DependencyModules.Tests/Snapshots/PublicApiTests.RuntimeApi.verified.txt @@ -86,6 +86,7 @@ namespace DependencyModules.Runtime.Attributes public InterceptAttribute(params System.Type[] interceptors) { } public System.Type[] Interceptors { get; } public int Order { get; set; } + public System.Type? Realm { get; set; } public System.Type? Service { get; set; } } public enum RegistrationType diff --git a/tests/DependencyModules.Tests/Snapshots/PublicApiTests.SourceGeneratorApi.verified.txt b/tests/DependencyModules.Tests/Snapshots/PublicApiTests.SourceGeneratorApi.verified.txt index ccfaa07..bdf9ec7 100644 --- a/tests/DependencyModules.Tests/Snapshots/PublicApiTests.SourceGeneratorApi.verified.txt +++ b/tests/DependencyModules.Tests/Snapshots/PublicApiTests.SourceGeneratorApi.verified.txt @@ -838,7 +838,7 @@ namespace DependencyModules.Conventions.Models public class ConventionCandidateModel : System.IEquatable { public static readonly DependencyModules.Conventions.Models.ConventionCandidateModel Ignore; - public ConventionCandidateModel(CSharpAuthor.ITypeDefinition ImplementationType, System.Collections.Generic.IReadOnlyList DeclaredInterfaces, System.Collections.Generic.IReadOnlyList BaseClassInterfaces, DependencyModules.SourceGenerator.Impl.Models.ConstructorInfoModel? Constructor, bool HasAccessibleConstructor, DependencyModules.Conventions.Models.LocationModel Location, System.Collections.Generic.IReadOnlyList? Conditions = null, System.Collections.Generic.IReadOnlyList? AttributeTypeKeys = null, string? AssemblyName = null) { } + public ConventionCandidateModel(CSharpAuthor.ITypeDefinition ImplementationType, System.Collections.Generic.IReadOnlyList DeclaredInterfaces, System.Collections.Generic.IReadOnlyList BaseClassInterfaces, DependencyModules.SourceGenerator.Impl.Models.ConstructorInfoModel? Constructor, bool HasAccessibleConstructor, DependencyModules.SourceGenerator.Impl.Models.LocationModel Location, System.Collections.Generic.IReadOnlyList? Conditions = null, System.Collections.Generic.IReadOnlyList? AttributeTypeKeys = null, string? AssemblyName = null) { } public string? AssemblyName { get; init; } public System.Collections.Generic.IReadOnlyList? AttributeTypeKeys { get; init; } public System.Collections.Generic.IReadOnlyList BaseClassInterfaces { get; init; } @@ -848,7 +848,7 @@ namespace DependencyModules.Conventions.Models public bool HasAccessibleConstructor { get; init; } public CSharpAuthor.ITypeDefinition ImplementationType { get; init; } public bool IsIgnored { get; } - public DependencyModules.Conventions.Models.LocationModel Location { get; init; } + public DependencyModules.SourceGenerator.Impl.Models.LocationModel Location { get; init; } public virtual bool Equals(DependencyModules.Conventions.Models.ConventionCandidateModel? other) { } public override int GetHashCode() { } public System.Collections.Generic.IEnumerable InterfacesInReach(bool includeBaseClasses) { } @@ -861,7 +861,7 @@ namespace DependencyModules.Conventions.Models bool IsOpenGeneric, DependencyModules.SourceGenerator.Impl.Models.ServiceLifestyle? Lifestyle, bool IncludeBaseClasses, - DependencyModules.Conventions.Models.LocationModel Location, + DependencyModules.SourceGenerator.Impl.Models.LocationModel Location, DependencyModules.Conventions.Models.ConventionRegisterAs RegisterAs = 0, System.Collections.Generic.IReadOnlyList? NamespaceFilters = null, DependencyModules.SourceGenerator.Impl.Models.RegistrationType? RegistrationType = default, @@ -883,7 +883,7 @@ namespace DependencyModules.Conventions.Models public object? Key { get; init; } public System.Collections.Generic.IReadOnlyList? KeyNamespaces { get; init; } public DependencyModules.SourceGenerator.Impl.Models.ServiceLifestyle? Lifestyle { get; init; } - public DependencyModules.Conventions.Models.LocationModel Location { get; init; } + public DependencyModules.SourceGenerator.Impl.Models.LocationModel Location { get; init; } public System.Collections.Generic.IReadOnlyList? NameFilters { get; init; } public System.Collections.Generic.IReadOnlyList? NamespaceFilters { get; init; } public DependencyModules.Conventions.Models.ConventionRegisterAs RegisterAs { get; init; } @@ -897,9 +897,10 @@ namespace DependencyModules.Conventions.Models public class ConventionModuleModel : System.IEquatable { public static readonly DependencyModules.Conventions.Models.ConventionModuleModel Ignore; - public ConventionModuleModel(CSharpAuthor.ITypeDefinition ModuleType, System.Collections.Generic.IReadOnlyList Conventions, System.Collections.Generic.IReadOnlyList Unreadable) { } + public ConventionModuleModel(CSharpAuthor.ITypeDefinition ModuleType, System.Collections.Generic.IReadOnlyList Conventions, System.Collections.Generic.IReadOnlyList Unreadable, DependencyModules.SourceGenerator.Impl.Models.LocationModel? Location = null) { } public System.Collections.Generic.IReadOnlyList Conventions { get; init; } public bool IsIgnored { get; } + public DependencyModules.SourceGenerator.Impl.Models.LocationModel? Location { get; init; } public CSharpAuthor.ITypeDefinition ModuleType { get; init; } public System.Collections.Generic.IReadOnlyList Unreadable { get; init; } public virtual bool Equals(DependencyModules.Conventions.Models.ConventionModuleModel? other) { } @@ -934,21 +935,6 @@ namespace DependencyModules.Conventions.Models public CSharpAuthor.ITypeDefinition InterfaceType { get; init; } public string? ViaTypeName { get; init; } } - public class LocationModel : System.IEquatable - { - public static readonly DependencyModules.Conventions.Models.LocationModel None; - public LocationModel(string FilePath, int SpanStart, int SpanLength, int StartLine, int StartCharacter, int EndLine, int EndCharacter) { } - public int EndCharacter { get; init; } - public int EndLine { get; init; } - public string FilePath { get; init; } - public int SpanLength { get; init; } - public int SpanStart { get; init; } - public int StartCharacter { get; init; } - public int StartLine { get; init; } - public Microsoft.CodeAnalysis.Location ToLocation() { } - public Microsoft.CodeAnalysis.Location ToLocationOrNone() { } - public static DependencyModules.Conventions.Models.LocationModel From(Microsoft.CodeAnalysis.SyntaxNode node) { } - } public class NameFilterModel : System.IEquatable { public NameFilterModel(string Pattern, bool Exclude) { } @@ -966,8 +952,8 @@ namespace DependencyModules.Conventions.Models } public class UnreadableStatementModel : System.IEquatable { - public UnreadableStatementModel(string Text, string Reason, DependencyModules.Conventions.Models.LocationModel Location) { } - public DependencyModules.Conventions.Models.LocationModel Location { get; init; } + public UnreadableStatementModel(string Text, string Reason, DependencyModules.SourceGenerator.Impl.Models.LocationModel Location) { } + public DependencyModules.SourceGenerator.Impl.Models.LocationModel Location { get; init; } public string Reason { get; init; } public string Text { get; init; } } @@ -1091,7 +1077,9 @@ namespace DependencyModules.SourceGenerator.Impl public static readonly Microsoft.CodeAnalysis.DiagnosticDescriptor GeneratorFailure; public static readonly Microsoft.CodeAnalysis.DiagnosticDescriptor InterceptorCannotServeMembers; public static readonly Microsoft.CodeAnalysis.DiagnosticDescriptor ModuleAttributeNamespaceNotImported; + public static readonly Microsoft.CodeAnalysis.DiagnosticDescriptor ModuleCannotBeNested; public static readonly Microsoft.CodeAnalysis.DiagnosticDescriptor ModuleMustBePartial; + public static readonly Microsoft.CodeAnalysis.DiagnosticDescriptor ModuleWithPropertiesShouldImplementEquals; public static readonly Microsoft.CodeAnalysis.DiagnosticDescriptor OpenGenericCannotBeDecorated; public static readonly Microsoft.CodeAnalysis.DiagnosticDescriptor RegisteredConditionally; public static readonly Microsoft.CodeAnalysis.DiagnosticDescriptor ServiceCannotBeConstructed; @@ -1262,7 +1250,7 @@ namespace DependencyModules.SourceGenerator.Impl.Models public class DecoratorModel : System.IEquatable { public static readonly DependencyModules.SourceGenerator.Impl.Models.DecoratorModel Ignore; - public DecoratorModel(CSharpAuthor.ITypeDefinition ServiceType, CSharpAuthor.ITypeDefinition DecoratorType, int Order, CSharpAuthor.ITypeDefinition? Realm, System.Collections.Generic.IReadOnlyList? Conditions = null, DependencyModules.SourceGenerator.Impl.Models.ConstructorInfoModel? Constructor = null, int InnerParameterIndex = -1, bool TypeParametersMatchService = true) { } + public DecoratorModel(CSharpAuthor.ITypeDefinition ServiceType, CSharpAuthor.ITypeDefinition DecoratorType, int Order, CSharpAuthor.ITypeDefinition? Realm, System.Collections.Generic.IReadOnlyList? Conditions = null, DependencyModules.SourceGenerator.Impl.Models.ConstructorInfoModel? Constructor = null, int InnerParameterIndex = -1, bool TypeParametersMatchService = true, DependencyModules.SourceGenerator.Impl.Models.LocationModel? Location = null) { } public bool CanMonomorphise { get; } public System.Collections.Generic.IReadOnlyList? Conditions { get; init; } public DependencyModules.SourceGenerator.Impl.Models.ConstructorInfoModel? Constructor { get; init; } @@ -1271,6 +1259,7 @@ namespace DependencyModules.SourceGenerator.Impl.Models public int InnerParameterIndex { get; init; } public bool IsIgnored { get; } public bool IsOpenGeneric { get; } + public DependencyModules.SourceGenerator.Impl.Models.LocationModel? Location { get; init; } public int Order { get; init; } public CSharpAuthor.ITypeDefinition? Realm { get; init; } public CSharpAuthor.ITypeDefinition ServiceType { get; init; } @@ -1374,18 +1363,20 @@ namespace DependencyModules.SourceGenerator.Impl.Models public class InterceptorModel : System.IEquatable { public static readonly DependencyModules.SourceGenerator.Impl.Models.InterceptorModel Ignore; - public InterceptorModel(CSharpAuthor.ITypeDefinition ServiceType, CSharpAuthor.ITypeDefinition ImplementationType, System.Collections.Generic.IReadOnlyList Interceptors, System.Collections.Generic.IReadOnlyList Members, System.Collections.Generic.IReadOnlyList Declarations, int Order, DependencyModules.SourceGenerator.Impl.Models.InterceptionRefusal? Refusal = null, System.Collections.Generic.IReadOnlyList? TypeParameters = null) { } + public InterceptorModel(CSharpAuthor.ITypeDefinition ServiceType, CSharpAuthor.ITypeDefinition ImplementationType, System.Collections.Generic.IReadOnlyList Interceptors, System.Collections.Generic.IReadOnlyList Members, System.Collections.Generic.IReadOnlyList Declarations, int Order, DependencyModules.SourceGenerator.Impl.Models.InterceptionRefusal? Refusal = null, System.Collections.Generic.IReadOnlyList? TypeParameters = null, CSharpAuthor.ITypeDefinition? Realm = null, DependencyModules.SourceGenerator.Impl.Models.LocationModel? Location = null) { } public System.Collections.Generic.IReadOnlyList Declarations { get; init; } public CSharpAuthor.ITypeDefinition ImplementationType { get; init; } public System.Collections.Generic.IReadOnlyList Interceptors { get; init; } public bool IsIgnored { get; } public bool IsOpenGeneric { get; } + public DependencyModules.SourceGenerator.Impl.Models.LocationModel? Location { get; init; } public System.Collections.Generic.IReadOnlyList Members { get; init; } public int Order { get; init; } + public CSharpAuthor.ITypeDefinition? Realm { get; init; } public DependencyModules.SourceGenerator.Impl.Models.InterceptionRefusal? Refusal { get; init; } public CSharpAuthor.ITypeDefinition ServiceType { get; init; } public System.Collections.Generic.IReadOnlyList? TypeParameters { get; init; } - public static DependencyModules.SourceGenerator.Impl.Models.InterceptorModel Refused(string message) { } + public static DependencyModules.SourceGenerator.Impl.Models.InterceptorModel Refused(string message, DependencyModules.SourceGenerator.Impl.Models.LocationModel? location = null) { } } public class InterceptorModelComparer : System.Collections.Generic.IEqualityComparer { @@ -1402,6 +1393,21 @@ namespace DependencyModules.SourceGenerator.Impl.Models public CSharpAuthor.ITypeDefinition Type { get; init; } public bool CanServe(DependencyModules.SourceGenerator.Impl.Models.InterceptorKind kind) { } } + public class LocationModel : System.IEquatable + { + public static readonly DependencyModules.SourceGenerator.Impl.Models.LocationModel None; + public LocationModel(string FilePath, int SpanStart, int SpanLength, int StartLine, int StartCharacter, int EndLine, int EndCharacter) { } + public int EndCharacter { get; init; } + public int EndLine { get; init; } + public string FilePath { get; init; } + public int SpanLength { get; init; } + public int SpanStart { get; init; } + public int StartCharacter { get; init; } + public int StartLine { get; init; } + public Microsoft.CodeAnalysis.Location ToLocation() { } + public Microsoft.CodeAnalysis.Location ToLocationOrNone() { } + public static DependencyModules.SourceGenerator.Impl.Models.LocationModel From(Microsoft.CodeAnalysis.SyntaxNode node) { } + } public enum LogOutputLevel { Debug = 1, @@ -1428,10 +1434,11 @@ namespace DependencyModules.SourceGenerator.Impl.Models ShouldImplementEquals = 4, IsRecord = 8, NotPartial = 16, + NestedInType = 32, } public class ModuleEntryPointModel : DependencyModules.SourceGenerator.Impl.Models.IClassModel, System.IEquatable { - public ModuleEntryPointModel(DependencyModules.SourceGenerator.Impl.Models.ModuleEntryPointFeatures ModuleFeatures, string FileLocation, CSharpAuthor.ITypeDefinition EntryPointType, DependencyModules.SourceGenerator.Impl.Models.RegistrationType? RegistrationType, bool? GenerateAttribute, bool? RegisterJsonSerializers, string? UseMethod, bool? GenerateFactories, System.Collections.Generic.IReadOnlyList Parameters, System.Collections.Generic.IReadOnlyList PropertyInfoModels, System.Collections.Generic.IReadOnlyList AttributeModels, System.Collections.Generic.IReadOnlyList AdditionalModules, System.Collections.Generic.IReadOnlyList Features) { } + public ModuleEntryPointModel(DependencyModules.SourceGenerator.Impl.Models.ModuleEntryPointFeatures ModuleFeatures, string FileLocation, DependencyModules.SourceGenerator.Impl.Models.LocationModel Location, CSharpAuthor.ITypeDefinition EntryPointType, DependencyModules.SourceGenerator.Impl.Models.RegistrationType? RegistrationType, bool? GenerateAttribute, bool? RegisterJsonSerializers, string? UseMethod, bool? GenerateFactories, System.Collections.Generic.IReadOnlyList Parameters, System.Collections.Generic.IReadOnlyList PropertyInfoModels, System.Collections.Generic.IReadOnlyList AttributeModels, System.Collections.Generic.IReadOnlyList AdditionalModules, System.Collections.Generic.IReadOnlyList Features) { } public System.Collections.Generic.IReadOnlyList AdditionalModules { get; init; } public System.Collections.Generic.IReadOnlyList AttributeModels { get; init; } public CSharpAuthor.ITypeDefinition ClassType { get; } @@ -1440,6 +1447,7 @@ namespace DependencyModules.SourceGenerator.Impl.Models public string FileLocation { get; init; } public bool? GenerateAttribute { get; init; } public bool? GenerateFactories { get; init; } + public DependencyModules.SourceGenerator.Impl.Models.LocationModel Location { get; init; } public DependencyModules.SourceGenerator.Impl.Models.ModuleEntryPointFeatures ModuleFeatures { get; init; } public System.Collections.Generic.IReadOnlyList Parameters { get; init; } public System.Collections.Generic.IReadOnlyList PropertyInfoModels { get; init; } @@ -1518,13 +1526,14 @@ namespace DependencyModules.SourceGenerator.Impl.Models public class ServiceModel : System.IEquatable { public static DependencyModules.SourceGenerator.Impl.Models.ServiceModel Ignore; - public ServiceModel(CSharpAuthor.ITypeDefinition ImplementationType, DependencyModules.SourceGenerator.Impl.Models.ConstructorInfoModel? Constructor, DependencyModules.SourceGenerator.Impl.Models.ServiceFactoryModel? Factory, DependencyModules.SourceGenerator.Impl.Models.FactoryOutputDelegate? FactoryOutput, System.Collections.Generic.IReadOnlyList Registrations, DependencyModules.SourceGenerator.Impl.Models.RegistrationFeature Features, System.Collections.Generic.IReadOnlyList? Conditions = null) { } + public ServiceModel(CSharpAuthor.ITypeDefinition ImplementationType, DependencyModules.SourceGenerator.Impl.Models.ConstructorInfoModel? Constructor, DependencyModules.SourceGenerator.Impl.Models.ServiceFactoryModel? Factory, DependencyModules.SourceGenerator.Impl.Models.FactoryOutputDelegate? FactoryOutput, System.Collections.Generic.IReadOnlyList Registrations, DependencyModules.SourceGenerator.Impl.Models.RegistrationFeature Features, System.Collections.Generic.IReadOnlyList? Conditions = null, DependencyModules.SourceGenerator.Impl.Models.LocationModel? Location = null) { } public System.Collections.Generic.IReadOnlyList? Conditions { get; init; } public DependencyModules.SourceGenerator.Impl.Models.ConstructorInfoModel? Constructor { get; init; } public DependencyModules.SourceGenerator.Impl.Models.ServiceFactoryModel? Factory { get; init; } public DependencyModules.SourceGenerator.Impl.Models.FactoryOutputDelegate? FactoryOutput { get; init; } public DependencyModules.SourceGenerator.Impl.Models.RegistrationFeature Features { get; init; } public CSharpAuthor.ITypeDefinition ImplementationType { get; init; } + public DependencyModules.SourceGenerator.Impl.Models.LocationModel? Location { get; init; } public System.Collections.Generic.IReadOnlyList Registrations { get; init; } } public class ServiceModelComparer : System.Collections.Generic.IEqualityComparer