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
2 changes: 2 additions & 0 deletions docs/architecture/source-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Source Generator 是 AspectCore 的编译时代理引擎,基于 Roslyn 增量
- `IsProxyableClassProperty`(`:470`):同上(属性版)。
- 自动发现(程序集级)额外跳过含事件成员的类型、以及已显式标注的类型。

程序集级自动发现的**接口**没有可推断的实现类型,默认生成**无目标 stub 代理**(成员返回 `default`,见下方「接口 stub」)。需要带实现的完整接口代理时,仍须在接口或实现类型上显式标注 `[AspectCoreGenerateProxy(typeof(Impl))]`。

## 3. 诊断(ACSGxxx)

生成器在遇到不支持的情况时报告诊断(`Emit/GeneratorDiagnostics.cs`,类别 `AspectCore.SourceGenerator`):
Expand Down
235 changes: 131 additions & 104 deletions src/AspectCore.SourceGenerator/AspectCoreProxyGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
static (node, _) => node is TypeDeclarationSyntax tds && tds.AttributeLists.Count > 0,
static (ctx, _) => GetCandidate(ctx))
.Where(static x => x is not null)
.Select(static (x, _) => x!);
.Select(static (x, _) => new Candidate(x!, isExplicit: true));

// Also discover candidates from referenced assemblies (multi-assembly support)
var referencedAssemblyCandidates = context.CompilationProvider
Expand All @@ -38,16 +38,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}

/// <summary>
/// Discovers types decorated with [AspectCoreGenerateProxy] in referenced assemblies.
/// This enables multi-assembly scenarios where the attribute is placed in a referenced library.
/// Discovers proxy candidates in referenced assemblies. This enables multi-assembly
/// scenarios where the attribute is placed in a referenced library:
/// - assembly-level attribute there → auto-discover all eligible types (Explicit = false)
/// - type-level attribute on individual types → explicit candidates (Explicit = true)
/// </summary>
private static ImmutableArray<INamedTypeSymbol> GetReferencedAssemblyCandidates(Compilation compilation)
private static ImmutableArray<Candidate> GetReferencedAssemblyCandidates(Compilation compilation)
{
var attrSymbol = compilation.GetTypeByMetadataName(GenerateProxyAttributeMetadataName);
if (attrSymbol is null)
return ImmutableArray<INamedTypeSymbol>.Empty;
return ImmutableArray<Candidate>.Empty;

var results = new List<INamedTypeSymbol>();
var results = new List<Candidate>();

// Scan all referenced assemblies
foreach (var referencedAssembly in compilation.References)
Expand All @@ -62,58 +64,81 @@ private static ImmutableArray<INamedTypeSymbol> GetReferencedAssemblyCandidates(

if (hasAssemblyAttr)
{
// Assembly-level: discover all eligible types
foreach (var type in EnumerateAssemblyTypes(assemblySymbol.GlobalNamespace))
// Assembly-level: auto-discover all eligible types
foreach (var type in EnumerateTypes(assemblySymbol.GlobalNamespace))
{
if (IsEligibleForAutoProxy(type))
results.Add(type);
if (IsEligibleForAutoProxy(type, attrSymbol))
results.Add(new Candidate(type, isExplicit: false));
}
}
else
{
// Type-level: only discover types that explicitly carry the attribute
foreach (var type in EnumerateAssemblyTypes(assemblySymbol.GlobalNamespace))
foreach (var type in EnumerateTypes(assemblySymbol.GlobalNamespace))
{
if (type.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol)))
results.Add(type);
results.Add(new Candidate(type, isExplicit: true));
}
}
}

return results.ToImmutableArray();
}

private static IEnumerable<INamedTypeSymbol> EnumerateAssemblyTypes(INamespaceSymbol ns)
private static IEnumerable<INamedTypeSymbol> EnumerateTypes(INamespaceSymbol ns)
{
foreach (var type in ns.GetTypeMembers())
yield return type;
foreach (var childNs in ns.GetNamespaceMembers())
{
foreach (var type in EnumerateAssemblyTypes(childNs))
foreach (var type in EnumerateTypes(childNs))
yield return type;
}
}

private static bool IsEligibleForAutoProxy(INamedTypeSymbol type)
/// <summary>
/// Determines whether <paramref name="type"/> is eligible for assembly-level auto-proxy
/// generation when its assembly declares <c>[assembly: AspectCoreGenerateProxy]</c>.
/// Types that explicitly carry the type-level attribute are excluded here — they flow
/// through the explicit path and must keep their attribute metadata.
/// </summary>
private static bool IsEligibleForAutoProxy(INamedTypeSymbol type, INamedTypeSymbol attrSymbol)
{
if (type.ContainingType is not null) return false; // skip nested
if (type.IsStatic) return false;
if (type.IsRefLikeType) return false; // skip ref structs (cannot be boxed/interfaced/class fields)
if (type.DeclaredAccessibility is not (Accessibility.Public or Accessibility.Internal)) return false;
// Skip types that already have explicit type-level attribute (handled separately)
if (type.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol)))
return false;

// Skip nested types
if (type.ContainingType is not null)
return false;

// Skip static types (no instance members to intercept)
if (type.IsStatic)
return false;

// Skip ref structs (cannot be boxed / used as interface impl / class field)
if (type.IsRefLikeType)
return false;

// The generated proxy class is always public (ProxyEmitter hardcodes it), so auto-proxying
// is only possible for public source types. Internal types — even within the current
// compilation — would produce CS0060 (inconsistent accessibility): a public proxy cannot
// derive from or implement a less-accessible type. Referenced assemblies additionally only
// expose their public types to the consumer's generated code. The explicit path shares this
// limitation.
if (type.DeclaredAccessibility != Accessibility.Public)
return false;

// Skip types with events (event proxying is not supported by either engine)
if (type.GetMembers().OfType<IEventSymbol>().Any())
return false;

if (type.TypeKind == TypeKind.Class)
{
if (type.IsSealed && !type.IsAbstract) return false;
if (type.IsSealed && !type.IsAbstract)
return false;
// Must have at least one overridable member
var isRecord = RecordTypeUtils.IsRecord(type);
foreach (var member in type.GetMembers())
{
if (member is IMethodSymbol m && IsProxyableClassMethod(type, m, isRecord))
return true;
if (member is IPropertySymbol p && IsProxyableClassProperty(type, p, isRecord))
return true;
}
return false;
return HasAnyOverridableMember(type);
}

if (type.TypeKind == TypeKind.Interface)
Expand Down Expand Up @@ -148,7 +173,7 @@ private static bool IsEligibleForAutoProxy(INamedTypeSymbol type)
return null;
}

private static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray<INamedTypeSymbol> candidates)
private static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray<Candidate> candidates)
{
// Supports:
// - type-level [AspectCoreGenerateProxy]: explicit per-type proxy generation
Expand All @@ -167,23 +192,40 @@ private static void Execute(SourceProductionContext context, Compilation compila
var hasAssemblyLevelAttr = compilation.Assembly.GetAttributes()
.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol));

// Collect all candidate types: explicit type-level + auto-discovered from assembly-level
var allCandidates = new HashSet<INamedTypeSymbol>(NamedTypeSymbolEqualityComparer.Instance);
foreach (var t in candidates)
allCandidates.Add(t);

// Explicit candidates carry the type-level attribute and keep their attribute metadata
// (e.g. the declared implementation type). Auto-discovered candidates from assembly-level
// auto-discovery carry none, so they are created with default proxy semantics below.
var explicitTypes = candidates
.Where(c => c.Explicit)
.Select(c => c.Type)
.Distinct(NamedTypeSymbolEqualityComparer.Instance)
.ToList();

var autoDiscovered = candidates
.Where(c => !c.Explicit)
.Select(c => c.Type)
.ToList();

// Auto-discover eligible types in the current assembly when it declares the assembly-level attribute.
// Note: iterate Assembly.GlobalNamespace (source-declared types only) — Compilation.GlobalNamespace
// also surfaces metadata types from referenced assemblies.
if (hasAssemblyLevelAttr)
{
foreach (var t in GetAssemblyEligibleTypes(compilation, attrSymbol))
allCandidates.Add(t);
foreach (var type in EnumerateTypes(compilation.Assembly.GlobalNamespace))
{
if (IsEligibleForAutoProxy(type, attrSymbol))
autoDiscovered.Add(type);
}
}

var entries = new List<ProxyEntry>();
foreach (var type in allCandidates.Distinct(NamedTypeSymbolEqualityComparer.Instance))
foreach (var type in explicitTypes)
{
var attrData = type.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol));
if (attrData is null)
{
// Defensive: explicit candidates always carry the attribute; keep the guard so
// the explicit path never treats an auto-discovered type as explicit.
continue;
}

Expand Down Expand Up @@ -282,6 +324,8 @@ private static void Execute(SourceProductionContext context, Compilation compila
}
}

CollectAutoDiscoveredEntries(context, entries, autoDiscovered);

if (entries.Count == 0)
{
return;
Expand Down Expand Up @@ -310,6 +354,37 @@ private static void Execute(SourceProductionContext context, Compilation compila
}
}

/// <summary>
/// Creates proxy entries for types discovered via assembly-level auto-discovery.
/// These types carry no type-level attribute, so entries are built with default
/// semantics: classes become class proxies (service = implementation = the type),
/// interfaces become no-target stub proxies.
/// </summary>
private static void CollectAutoDiscoveredEntries(
SourceProductionContext context, List<ProxyEntry> entries, IEnumerable<INamedTypeSymbol> types)
{
foreach (var type in types.Distinct(NamedTypeSymbolEqualityComparer.Instance))
{
switch (type.TypeKind)
{
case TypeKind.Interface:
// No implementation type is known for auto-discovered interfaces;
// emit a no-target stub proxy (members return default).
entries.Add(ProxyEntry.CreateInterface(serviceType: type, implementationType: null));
break;

case TypeKind.Class:
if (!HasAccessibleConstructor(type))
{
context.ReportDiagnostic(GeneratorDiagnostics.NoAccessibleConstructor(type));
continue;
}
entries.Add(ProxyEntry.CreateClass(serviceType: type, implementationType: type));
break;
}
}
}

/// <summary>
/// 检查类型是否对生成器可见(考虑 internal 和 InternalsVisibleTo)
/// </summary>
Expand Down Expand Up @@ -374,72 +449,6 @@ private static bool HasAccessibleConstructor(INamedTypeSymbol type)
return false;
}

/// <summary>
/// Discovers all eligible types in the assembly for auto-proxy generation when
/// [assembly: AspectCoreGenerateProxy] is used. Eligible types are public classes
/// and interfaces that are not sealed (for classes), not nested, not abstract (for classes),
/// and have at least one overridable member.
/// </summary>
private static IEnumerable<INamedTypeSymbol> GetAssemblyEligibleTypes(Compilation compilation, INamedTypeSymbol attrSymbol)
{
var globalNamespace = compilation.GlobalNamespace;
foreach (var type in EnumerateAllTypes(globalNamespace))
{
// Skip types that already have explicit type-level attribute (they'll be handled separately)
if (type.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol)))
continue;

// Skip nested types
if (type.ContainingType is not null)
continue;

// Skip types that are not public or internal
if (type.DeclaredAccessibility is not (Accessibility.Public or Accessibility.Internal))
continue;

// For classes: must not be sealed (unless abstract), and must have at least one overridable member
if (type.TypeKind == TypeKind.Class)
{
if (type.IsSealed && !type.IsAbstract)
continue;
if (type.IsStatic)
continue;
// Check if there's at least one overridable method or property
if (!HasAnyOverridableMember(type))
continue;
}
else if (type.TypeKind == TypeKind.Interface)
{
// Interfaces are always eligible
}
else
{
continue;
}

// Skip types with events (not supported)
if (type.GetMembers().OfType<IEventSymbol>().Any())
continue;

yield return type;
}
}

private static IEnumerable<INamedTypeSymbol> EnumerateAllTypes(INamespaceSymbol ns)
{
foreach (var type in ns.GetTypeMembers())
{
yield return type;
}
foreach (var childNs in ns.GetNamespaceMembers())
{
foreach (var type in EnumerateAllTypes(childNs))
{
yield return type;
}
}
}

private static bool HasAnyOverridableMember(INamedTypeSymbol type)
{
var isRecord = RecordTypeUtils.IsRecord(type);
Expand Down Expand Up @@ -488,6 +497,24 @@ public int GetHashCode(INamedTypeSymbol obj)
=> SymbolEqualityComparer.Default.GetHashCode(obj);
}

/// <summary>
/// A proxy candidate discovered by the generator. <see cref="Explicit"/> distinguishes
/// types carrying the type-level <c>[AspectCoreGenerateProxy]</c> attribute from types
/// auto-discovered via the assembly-level attribute. Auto-discovered types carry no
/// attribute metadata, so their proxy entries are built with default semantics.
/// </summary>
internal readonly struct Candidate
{
public Candidate(INamedTypeSymbol type, bool isExplicit)
{
Type = type;
Explicit = isExplicit;
}

public INamedTypeSymbol Type { get; }
public bool Explicit { get; }
}

internal enum ProxyKind
{
Interface = 0,
Expand Down
Loading
Loading