Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
AWT103 | Awaiten | Error | An implementation type is abstract or an interface
AWT104 | Awaiten | Error | An implementation type has no accessible constructor
AWT105 | Awaiten | Error | A singleton captures a shorter-lived scoped dependency
AWT106 | Awaiten | Warning | A synchronous factory's body provably produces an IAsyncInitializable concrete type its declared return type hides
AWT107 | Awaiten | Error | An implementation is registered with conflicting lifetimes
AWT108 | Awaiten | Error | A Factory registration names a member that is not a usable factory method
AWT109 | Awaiten | Error | An Instance registration names a member that is not a usable instance member
Expand Down
212 changes: 180 additions & 32 deletions Source/Awaiten.SourceGenerators/AwaitenGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -419,39 +419,11 @@ private static Dictionary<int, List<int>> BuildDependencyGraph(
}

// Select the producer: a container method (Factory) or the implementation's constructor (the
// default). A factory produces the instance, so the registered type may be an interface and is
// not subject to the not-instantiable check that a constructed type is.
IMethodSymbol? producer;
if (info.Production == ProductionKind.Factory)
// default). A null result means the registration is unusable and a diagnostic was already reported.
IMethodSymbol? producer = SelectProducer(info, containerSymbol, compilation, serviceToImpl, diagnostics);
if (producer is null)
{
producer = ResolveFactory(containerSymbol, info, compilation, diagnostics);
if (producer is null)
{
return null;
}
}
else
{
// An abstract type or interface cannot be constructed; reject it instead of emitting a 'new'
// against it (which would fail to compile in the generated source).
if (info.Symbol.IsAbstract || info.Symbol.TypeKind == TypeKind.Interface)
{
diagnostics.Add(new DiagnosticInfo(
Diagnostics.NotInstantiable,
info.Location,
new EquatableArray<string>([Display(info.ImplementationType),])));
return null;
}

producer = SelectConstructor(info.Symbol, containerSymbol, serviceToImpl.Keys.Select(k => k.Service));
if (producer is null)
{
diagnostics.Add(new DiagnosticInfo(
Diagnostics.NoAccessibleConstructor,
info.Location,
new EquatableArray<string>([Display(info.ImplementationType),])));
return null;
}
return null;
}

// An asynchronous factory returns Task<T> / ValueTask<T>: the container awaits it, so the type it
Expand Down Expand Up @@ -494,6 +466,16 @@ private static Dictionary<int, List<int>> BuildDependencyGraph(
// IAsyncInitializable, the container additionally awaits its InitializeAsync after the factory completes.
bool asyncInit = asyncInitializableSymbol is not null && ImplementsInterface(disposalType, asyncInitializableSymbol);

// Best-effort lint (AWT106): a synchronous factory whose declared return type hides the asynchronous
// initialization its body provably produces. The container reads async-init taint off producer.ReturnType
// (above), so a concrete IAsyncInitializable returned behind a plainer interface is never initialized.
// An async Task<T>/ValueTask<T> factory owns its own initialization (the container awaits the factory),
// and a hidden IDisposable is disposed at runtime via RuntimeDisposalCheck - neither is reported.
if (info.Production == ProductionKind.Factory && !asyncFactory)
{
ReportFactoryHidingAsyncInitialization(producer, compilation, asyncInitializableSymbol, diagnostics);
}

return new InstanceModel(
info.ImplementationType,
info.Symbol.Name,
Expand Down Expand Up @@ -524,6 +506,172 @@ static bool CouldHideDisposable(ITypeSymbol type)
|| (type.TypeKind == TypeKind.Class && !type.IsSealed);
}

/// <summary>
/// Selects the method that produces an implementation: a container method for a <c>Factory</c>
/// registration, or the implementation's own constructor otherwise. Returns <see langword="null" />
/// when the registration is unusable - an unresolved factory (AWT108), a non-instantiable abstract or
/// interface type (AWT103), or a type with no accessible constructor (AWT104) - having already appended
/// the corresponding diagnostic. A factory produces the instance, so the registered type may be an
/// interface and is not subject to the not-instantiable check a constructed type is.
/// </summary>
private static IMethodSymbol? SelectProducer(
ImplInfo info,
INamedTypeSymbol containerSymbol,
Compilation compilation,
Dictionary<ServiceKey, string> serviceToImpl,
List<DiagnosticInfo> diagnostics)
{
if (info.Production == ProductionKind.Factory)
{
return ResolveFactory(containerSymbol, info, compilation, diagnostics);
}

// An abstract type or interface cannot be constructed; reject it instead of emitting a 'new'
// against it (which would fail to compile in the generated source).
if (info.Symbol.IsAbstract || info.Symbol.TypeKind == TypeKind.Interface)
{
diagnostics.Add(new DiagnosticInfo(
Diagnostics.NotInstantiable,
info.Location,
new EquatableArray<string>([Display(info.ImplementationType),])));
return null;
}

IMethodSymbol? constructor = SelectConstructor(info.Symbol, containerSymbol, serviceToImpl.Keys.Select(k => k.Service));
if (constructor is null)
{
diagnostics.Add(new DiagnosticInfo(
Diagnostics.NoAccessibleConstructor,
info.Location,
new EquatableArray<string>([Display(info.ImplementationType),])));
}

return constructor;
}

/// <summary>
/// Reports <see cref="Diagnostics.FactoryHidesAsyncInitialization">AWT106</see> when a synchronous
/// factory method's body provably returns a concrete type that implements <c>IAsyncInitializable</c>
/// while the method's declared return type does not - so the initialization is invisible to the
/// container and never runs.
/// </summary>
/// <remarks>
/// Conservative by design: it inspects only the producer's own <c>return</c> expressions (both
/// expression-bodied and block-bodied), never descending into nested lambdas or local functions, and
/// fires only when the statically determined type of the returned expression is a non-abstract,
/// non-interface named type that is async-initializable. A metadata-only factory (no syntax) or an
/// unresolved/unanalyzable return type yields no diagnostic. False negatives (helper-returned or
/// runtime-selected implementations) are accepted; false positives are not. A hidden <c>IDisposable</c>
/// is not reported (the container disposes factory outputs behind a runtime check); an asynchronous
/// factory is excluded by the caller (it owns its own initialization).
/// </remarks>
private static void ReportFactoryHidingAsyncInitialization(
IMethodSymbol producer,
Compilation compilation,
INamedTypeSymbol? asyncInitializableSymbol,
List<DiagnosticInfo> diagnostics)
{
if (asyncInitializableSymbol is null)
{
return;
}

ITypeSymbol declaredReturnType = producer.ReturnType;

// The container already sees the initialization when the declared return type is itself
// async-initializable, so nothing it hides could be missed - there is no diagnostic to report.
if (Implements(declaredReturnType, asyncInitializableSymbol))
{
return;
}

// Already-reported concrete types: a factory with several returns of the same hidden type should
// surface a single diagnostic, not one per return.
HashSet<ITypeSymbol> reported = new(SymbolEqualityComparer.Default);

foreach (SyntaxReference reference in producer.DeclaringSyntaxReferences)
{
// A factory must be a method on the container; anything else (or metadata-only, no syntax) is
// not analyzable here and is left silent.
if (reference.GetSyntax() is not MethodDeclarationSyntax method)
{
continue;
}

SemanticModel model = compilation.GetSemanticModel(method.SyntaxTree);

foreach (ExpressionSyntax returnExpression in CollectReturnExpressions(method))
{
ITypeSymbol? returnedType = model.GetTypeInfo(returnExpression).Type;
if (returnedType is not INamedTypeSymbol concrete
|| concrete.TypeKind == TypeKind.Interface
|| concrete.IsAbstract
|| concrete.TypeKind == TypeKind.Error
|| !reported.Add(concrete)
|| !Implements(concrete, asyncInitializableSymbol))
{
continue;
}

diagnostics.Add(new DiagnosticInfo(
Diagnostics.FactoryHidesAsyncInitialization,
LocationInfo.From(returnExpression.GetLocation()),
new EquatableArray<string>([
producer.Name,
Display(concrete.ToDisplayString(FullyQualified)),
Display(declaredReturnType.ToDisplayString(FullyQualified)),
])));
}
}

static bool Implements(ITypeSymbol type, INamedTypeSymbol @interface)
{
return SymbolEqualityComparer.Default.Equals(type, @interface)
|| type.AllInterfaces.Any(implemented => SymbolEqualityComparer.Default.Equals(implemented, @interface));
}
}

/// <summary>
/// The expressions a method directly returns: the arrow expression of an expression-bodied method, or
/// every <c>return x;</c> in a block body. Nested lambdas and local functions are not descended into,
/// so their returns are never attributed to the enclosing factory.
/// </summary>
private static IEnumerable<ExpressionSyntax> CollectReturnExpressions(MethodDeclarationSyntax method)
{
if (method.ExpressionBody?.Expression is { } arrow)
{
yield return arrow;
yield break;
}

if (method.Body is null)
{
yield break;
}

Stack<SyntaxNode> pending = new();
pending.Push(method.Body);
while (pending.Count > 0)
{
SyntaxNode node = pending.Pop();
foreach (SyntaxNode child in node.ChildNodes())
{
// Do not cross into a nested function: its returns belong to it, not to the factory.
if (child is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax)
{
continue;
}

if (child is ReturnStatementSyntax { Expression: { } returned })
{
yield return returned;
}

pending.Push(child);
}
}
}

/// <summary>
/// Classifies the producer's parameters (a constructor's or a factory method's) and reports
/// <see cref="Diagnostics.MissingDependency">AWT101</see> for any non-<c>[Arg]</c> parameter whose
Expand Down
21 changes: 21 additions & 0 deletions Source/Awaiten.SourceGenerators/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ internal static class Diagnostics
DiagnosticSeverity.Error,
isEnabledByDefault: true);

/// <summary>
/// A synchronous <c>Factory</c> method's body provably constructs (or returns a local of) a concrete
/// type that implements <c>IAsyncInitializable</c>, while its declared return type does not expose it.
/// The container reads async-initialization taint off the declared return type, so it cannot see that
/// the produced instance needs initialization - the hidden <c>InitializeAsync</c> never runs and the
/// instance is handed out uninitialized. A hidden <c>IDisposable</c> is <em>not</em> reported: the
/// container disposes factory outputs behind a runtime check, so it does not leak. An asynchronous
/// <c>Task&lt;T&gt;</c> / <c>ValueTask&lt;T&gt;</c> factory is <em>not</em> reported either: it owns its
/// own initialization (the container awaits the factory itself). This is a best-effort lint (Warning):
/// it fires only when the concrete returned type is statically provable from the body, so it has false
/// negatives by design (a helper-returned or runtime-selected implementation is not reported), but is
/// intended to have no false positives.
/// </summary>
public static readonly DiagnosticDescriptor FactoryHidesAsyncInitialization = new(
"AWT106",
"Factory hides asynchronous initialization behind its declared return type",
"factory '{0}' constructs '{1}', which is async-initialized, but declares return type '{2}'; the container cannot see that it needs initialization, so its InitializeAsync never runs. Return '{1}', or make the factory 'async Task<{2}>' and handle initialization inside it.",
"Awaiten",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

/// <summary>
/// The same implementation is registered with more than one lifetime; coalescing into a single
/// instance would silently drop one of the declared lifetimes.
Expand Down
Loading
Loading