Class
Sealed
public sealed class AssemblyAnalyzer
Namespace: Moka.Docs.CSharp.Metadata
Analyzes C# source files via Roslyn to extract a complete API model. Uses CSharpCompilation to build a semantic model without requiring MSBuild.
Constructors
| Name | Description |
|---|---|
AssemblyAnalyzer(AssemblyAnalyzer> logger) |
Analyzes C# source files via Roslyn to extract a complete API model. Uses CSharpCompilation to build a semantic model without requiring MSBuild. |
AssemblyAnalyzer(AssemblyAnalyzer> logger)
AssemblyAnalyzer.AssemblyAnalyzer(ILogger<AssemblyAnalyzer> logger)
Analyzes C# source files via Roslyn to extract a complete API model. Uses CSharpCompilation to build a semantic model without requiring MSBuild.
Methods
| Name | Description |
|---|---|
AnalyzeCompilation(…) |
Analyzes a Roslyn compilation to extract the API model. |
AnalyzeDirectory(…) |
Analyzes all C# source files in a directory to build an API model. |
AnalyzeSyntaxTrees(…) |
Analyzes syntax trees to build an API model. |
AnalyzeCompilation(CSharpCompilation compilation, string assemblyName, bool includeInternals)
ApiReference AssemblyAnalyzer.AnalyzeCompilation(CSharpCompilation compilation, string assemblyName, bool includeInternals = false)
Analyzes a Roslyn compilation to extract the API model.
AnalyzeDirectory(string sourceDirectory, string assemblyName, bool includeInternals)
ApiReference AssemblyAnalyzer.AnalyzeDirectory(string sourceDirectory, string assemblyName, bool includeInternals = false)
Analyzes all C# source files in a directory to build an API model.
Parameters
| Name | Type | Description |
|---|---|---|
sourceDirectory | string | The directory containing C# source files. |
assemblyName | string | The assembly name (used for display). |
includeInternals | bool | Whether to include internal types. |
Returns: The extracted API reference model.
AnalyzeSyntaxTrees(IReadOnlyList syntaxTrees, string assemblyName, bool includeInternals)
ApiReference AssemblyAnalyzer.AnalyzeSyntaxTrees(IReadOnlyList<SyntaxTree> syntaxTrees, string assemblyName, bool includeInternals = false)
Analyzes syntax trees to build an API model.
View Source
/// <summary>
/// Analyzes C# source files via Roslyn to extract a complete API model.
/// Uses <see cref = "CSharpCompilation"/> to build a semantic model without requiring MSBuild.
/// </summary>
public sealed class AssemblyAnalyzer(ILogger<AssemblyAnalyzer> logger)
{
/// <summary>
/// Analyzes all C# source files in a directory to build an API model.
/// </summary>
/// <param name = "sourceDirectory">The directory containing C# source files.</param>
/// <param name = "assemblyName">The assembly name (used for display).</param>
/// <param name = "includeInternals">Whether to include internal types.</param>
/// <returns>The extracted API reference model.</returns>
public ApiReference AnalyzeDirectory(string sourceDirectory, string assemblyName, bool includeInternals = false)
{
var csFiles = Directory.GetFiles(sourceDirectory, "*.cs", SearchOption.AllDirectories).Where(f => !f.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}")).Where(f => !f.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")).ToList();
if (csFiles.Count == 0)
{
logger.LogWarning("No C# source files found in {Directory}", sourceDirectory);
return new ApiReference
{
Assemblies = [assemblyName]
};
}
logger.LogInformation("Analyzing {Count} source files in {Directory}", csFiles.Count, sourceDirectory);
var syntaxTrees = csFiles.Select(f => CSharpSyntaxTree.ParseText(File.ReadAllText(f), path: f)).ToList();
return AnalyzeSyntaxTrees(syntaxTrees, assemblyName, includeInternals);
}
/// <summary>
/// Analyzes syntax trees to build an API model.
/// </summary>
public ApiReference AnalyzeSyntaxTrees(IReadOnlyList<SyntaxTree> syntaxTrees, string assemblyName, bool includeInternals = false)
{
PortableExecutableReference[] references = new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)
};
// Add runtime assemblies
string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
IEnumerable<PortableExecutableReference> runtimeRefs = new[]
{
"System.Runtime.dll",
"System.Collections.dll",
"netstandard.dll"
}.Select(f => Path.Combine(runtimeDir, f)).Where(File.Exists).Select(f => MetadataReference.CreateFromFile(f));
var compilation = CSharpCompilation.Create(assemblyName, syntaxTrees, references.Concat(runtimeRefs), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
return AnalyzeCompilation(compilation, assemblyName, includeInternals);
}
/// <summary>
/// Analyzes a Roslyn compilation to extract the API model.
/// </summary>
public ApiReference AnalyzeCompilation(CSharpCompilation compilation, string assemblyName, bool includeInternals = false)
{
var namespaceMap = new Dictionary<string, List<ApiType>>(StringComparer.Ordinal);
foreach (SyntaxTree tree in compilation.SyntaxTrees)
{
SemanticModel semanticModel = compilation.GetSemanticModel(tree);
SyntaxNode root = tree.GetRoot();
foreach (TypeDeclarationSyntax typeDecl in root.DescendantNodes().OfType<TypeDeclarationSyntax>())
{
INamedTypeSymbol? symbol = semanticModel.GetDeclaredSymbol(typeDecl);
if (symbol is null)
{
continue;
}
if (!ShouldInclude(symbol, includeInternals))
{
continue;
}
ApiType? apiType = ExtractType(symbol);
if (apiType is null)
{
continue;
}
// Capture the source code from the syntax node
apiType = apiType with
{
SourceCode = typeDecl.NormalizeWhitespace().ToFullString()
};
string ns = symbol.ContainingNamespace?.ToDisplayString() ?? "(global)";
if (!namespaceMap.TryGetValue(ns, out List<ApiType>? types))
{
types = [];
namespaceMap[ns] = types;
}
types.Add(apiType);
}
// Also extract top-level enum declarations
foreach (EnumDeclarationSyntax enumDecl in root.DescendantNodes().OfType<EnumDeclarationSyntax>())
{
INamedTypeSymbol? symbol = semanticModel.GetDeclaredSymbol(enumDecl);
if (symbol is null)
{
continue;
}
if (!ShouldInclude(symbol, includeInternals))
{
continue;
}
ApiType apiType = ExtractEnumType(symbol);
// Capture the source code from the syntax node
apiType = apiType with
{
SourceCode = enumDecl.NormalizeWhitespace().ToFullString()
};
string ns = symbol.ContainingNamespace?.ToDisplayString() ?? "(global)";
if (!namespaceMap.TryGetValue(ns, out List<ApiType>? types))
{
types = [];
namespaceMap[ns] = types;
}
types.Add(apiType);
}
// Delegate declarations
foreach (DelegateDeclarationSyntax delegateDecl in root.DescendantNodes().OfType<DelegateDeclarationSyntax>())
{
INamedTypeSymbol? symbol = semanticModel.GetDeclaredSymbol(delegateDecl);
if (symbol is null)
{
continue;
}
if (!ShouldInclude(symbol, includeInternals))
{
continue;
}
ApiType apiType = ExtractDelegateType(symbol);
// Capture the source code from the syntax node
apiType = apiType with
{
SourceCode = delegateDecl.NormalizeWhitespace().ToFullString()
};
string ns = symbol.ContainingNamespace?.ToDisplayString() ?? "(global)";
if (!namespaceMap.TryGetValue(ns, out List<ApiType>? types))
{
types = [];
namespaceMap[ns] = types;
}
types.Add(apiType);
}
}
var namespaces = namespaceMap.OrderBy(kv => kv.Key, StringComparer.Ordinal).Select(kv => new ApiNamespace { Name = kv.Key, Types = kv.Value.OrderBy(t => t.Name).ToList() }).ToList();
logger.LogInformation("Extracted {TypeCount} types in {NsCount} namespaces from {Assembly}", namespaces.Sum(n => n.Types.Count), namespaces.Count, assemblyName);
return new ApiReference
{
Assemblies = [assemblyName],
Namespaces = namespaces
};
}
#region Type Extraction
private static ApiType? ExtractType(INamedTypeSymbol symbol)
{
ApiTypeKind? kind = symbol.TypeKind switch
{
TypeKind.Class => symbol.IsRecord ? ApiTypeKind.Record : ApiTypeKind.Class,
TypeKind.Struct => symbol.IsRecord ? ApiTypeKind.Record : ApiTypeKind.Struct,
TypeKind.Interface => ApiTypeKind.Interface,
_ => null
};
if (kind is null)
{
return null;
}
return new ApiType
{
Name = symbol.Name,
FullName = symbol.ToDisplayString(),
Kind = kind.Value,
Accessibility = MapAccessibility(symbol.DeclaredAccessibility),
IsStatic = symbol.IsStatic,
IsAbstract = symbol.IsAbstract && kind != ApiTypeKind.Interface,
IsSealed = symbol.IsSealed,
IsRecord = symbol.IsRecord,
BaseType = symbol.BaseType?.ToDisplayString()is { } bt && bt != "object" ? bt : null,
ImplementedInterfaces = symbol.Interfaces.Select(i => i.ToDisplayString()).OrderBy(i => i).ToList(),
TypeParameters = ExtractTypeParameters(symbol.TypeParameters),
Members = ExtractMembers(symbol),
Namespace = symbol.ContainingNamespace?.ToDisplayString(),
Assembly = symbol.ContainingAssembly?.Name,
IsObsolete = HasAttribute(symbol, "ObsoleteAttribute"),
ObsoleteMessage = GetObsoleteMessage(symbol),
Attributes = ExtractAttributes(symbol),
Documentation = ExtractXmlDoc(symbol)
};
}
private static ApiType ExtractEnumType(INamedTypeSymbol symbol)
{
var members = symbol.GetMembers().OfType<IFieldSymbol>().Where(f => f.HasConstantValue).Select(f => new ApiMember { Name = f.Name, Kind = ApiMemberKind.Field, Signature = $"{f.Name} = {f.ConstantValue}", IsStatic = true, Documentation = ExtractXmlDoc(f) }).ToList();
return new ApiType
{
Name = symbol.Name,
FullName = symbol.ToDisplayString(),
Kind = ApiTypeKind.Enum,
Accessibility = MapAccessibility(symbol.DeclaredAccessibility),
Members = members,
Namespace = symbol.ContainingNamespace?.ToDisplayString(),
Assembly = symbol.ContainingAssembly?.Name,
IsObsolete = HasAttribute(symbol, "ObsoleteAttribute"),
ObsoleteMessage = GetObsoleteMessage(symbol),
Attributes = ExtractAttributes(symbol),
Documentation = ExtractXmlDoc(symbol)
};
}
private static ApiType ExtractDelegateType(INamedTypeSymbol symbol)
{
IMethodSymbol? invokeMethod = symbol.DelegateInvokeMethod;
List<ApiParameter> parameters = invokeMethod?.Parameters.Select(ExtractParameter).ToList() ?? [];
return new ApiType
{
Name = symbol.Name,
FullName = symbol.ToDisplayString(),
Kind = ApiTypeKind.Delegate,
Accessibility = MapAccessibility(symbol.DeclaredAccessibility),
TypeParameters = ExtractTypeParameters(symbol.TypeParameters),
Namespace = symbol.ContainingNamespace?.ToDisplayString(),
Assembly = symbol.ContainingAssembly?.Name,
Members = [new ApiMember
{
Name = "Invoke",
Kind = ApiMemberKind.Method,
Signature = symbol.ToDisplayString(),
ReturnType = invokeMethod?.ReturnType.ToDisplayString(),
Parameters = parameters
}
],
IsObsolete = HasAttribute(symbol, "ObsoleteAttribute"),
Documentation = ExtractXmlDoc(symbol)
};
}
#endregion
#region Member Extraction
private static List<ApiMember> ExtractMembers(INamedTypeSymbol typeSymbol)
{
var members = new List<ApiMember>();
foreach (ISymbol member in typeSymbol.GetMembers())
{
// Skip compiler-generated members
if (member.IsImplicitlyDeclared)
{
continue;
}
// Skip private members
if (member.DeclaredAccessibility == Accessibility.Private)
{
continue;
}
ApiMember? apiMember = member switch
{
IMethodSymbol method => ExtractMethod(method),
IPropertySymbol property => ExtractProperty(property),
IFieldSymbol field => ExtractField(field),
IEventSymbol @event => ExtractEvent(@event),
_ => null
};
if (apiMember is not null)
{
members.Add(apiMember);
}
}
return members;
}
private static ApiMember? ExtractMethod(IMethodSymbol method)
{
// Skip property accessors, event accessors, etc.
if (method.MethodKind is MethodKind.PropertyGet or MethodKind.PropertySet or MethodKind.EventAdd or MethodKind.EventRemove or MethodKind.EventRaise)
{
return null;
}
ApiMemberKind kind = method.MethodKind switch
{
MethodKind.Constructor or MethodKind.StaticConstructor => ApiMemberKind.Constructor,
MethodKind.UserDefinedOperator or MethodKind.Conversion => ApiMemberKind.Operator,
_ => ApiMemberKind.Method
};
string name = method.MethodKind switch
{
MethodKind.Constructor or MethodKind.StaticConstructor => method.ContainingType.Name,
_ => method.Name
};
return new ApiMember
{
Name = name,
Kind = kind,
Signature = method.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
ReturnType = method.ReturnType.ToDisplayString(),
Accessibility = MapAccessibility(method.DeclaredAccessibility),
IsStatic = method.IsStatic,
IsVirtual = method.IsVirtual,
IsAbstract = method.IsAbstract,
IsOverride = method.IsOverride,
IsSealed = method.IsSealed,
IsExtensionMethod = method.IsExtensionMethod,
Parameters = method.Parameters.Select(ExtractParameter).ToList(),
TypeParameters = ExtractTypeParameters(method.TypeParameters),
IsObsolete = HasAttribute(method, "ObsoleteAttribute"),
ObsoleteMessage = GetObsoleteMessage(method),
Attributes = ExtractAttributes(method),
Documentation = ExtractXmlDoc(method)
};
}
private static ApiMember ExtractProperty(IPropertySymbol property)
{
string signature = property.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
// Add get/set/init accessors info
var accessors = new List<string>();
if (property.GetMethod is not null)
{
accessors.Add("get");
}
if (property.SetMethod is not null)
{
accessors.Add(property.SetMethod.IsInitOnly ? "init" : "set");
}
if (accessors.Count > 0)
{
signature += $" {{ {string.Join("; ", accessors)}; }}";
}
return new ApiMember
{
Name = property.Name,
Kind = property.IsIndexer ? ApiMemberKind.Indexer : ApiMemberKind.Property,
Signature = signature,
ReturnType = property.Type.ToDisplayString(),
Accessibility = MapAccessibility(property.DeclaredAccessibility),
IsStatic = property.IsStatic,
IsVirtual = property.IsVirtual,
IsAbstract = property.IsAbstract,
IsOverride = property.IsOverride,
IsSealed = property.IsSealed,
Parameters = property.IsIndexer ? property.Parameters.Select(ExtractParameter).ToList() : [],
IsObsolete = HasAttribute(property, "ObsoleteAttribute"),
ObsoleteMessage = GetObsoleteMessage(property),
Attributes = ExtractAttributes(property),
Documentation = ExtractXmlDoc(property)
};
}
private static ApiMember? ExtractField(IFieldSymbol field)
{
// Skip backing fields
if (field.AssociatedSymbol is not null)
{
return null;
}
return new ApiMember
{
Name = field.Name,
Kind = ApiMemberKind.Field,
Signature = $"{field.Type.ToDisplayString()} {field.Name}",
ReturnType = field.Type.ToDisplayString(),
Accessibility = MapAccessibility(field.DeclaredAccessibility),
IsStatic = field.IsStatic,
IsObsolete = HasAttribute(field, "ObsoleteAttribute"),
Attributes = ExtractAttributes(field),
Documentation = ExtractXmlDoc(field)
};
}
private static ApiMember ExtractEvent(IEventSymbol @event)
{
return new ApiMember
{
Name = @event.Name,
Kind = ApiMemberKind.Event,
Signature = $"event {@event.Type.ToDisplayString()} {@event.Name}",
ReturnType = @event.Type.ToDisplayString(),
Accessibility = MapAccessibility(@event.DeclaredAccessibility),
IsStatic = @event.IsStatic,
IsVirtual = @event.IsVirtual,
IsAbstract = @event.IsAbstract,
IsOverride = @event.IsOverride,
IsSealed = @event.IsSealed,
IsObsolete = HasAttribute(@event, "ObsoleteAttribute"),
Attributes = ExtractAttributes(@event),
Documentation = ExtractXmlDoc(@event)
};
}
#endregion
#region Helpers
private static ApiParameter ExtractParameter(IParameterSymbol param)
{
return new ApiParameter
{
Name = param.Name,
Type = param.Type.ToDisplayString(),
HasDefaultValue = param.HasExplicitDefaultValue,
DefaultValue = param.HasExplicitDefaultValue ? param.ExplicitDefaultValue?.ToString() : null,
IsParams = param.IsParams,
IsRef = param.RefKind == RefKind.Ref,
IsOut = param.RefKind == RefKind.Out,
IsIn = param.RefKind == RefKind.In,
IsNullable = param.NullableAnnotation == NullableAnnotation.Annotated
};
}
private static List<ApiTypeParameter> ExtractTypeParameters(ImmutableArray<ITypeParameterSymbol> typeParams)
{
return typeParams.Select(tp => new ApiTypeParameter { Name = tp.Name, Constraints = GetConstraints(tp) }).ToList();
}
private static List<string> GetConstraints(ITypeParameterSymbol tp)
{
var constraints = new List<string>();
if (tp.HasReferenceTypeConstraint)
{
constraints.Add("class");
}
if (tp.HasValueTypeConstraint)
{
constraints.Add("struct");
}
if (tp.HasNotNullConstraint)
{
constraints.Add("notnull");
}
if (tp.HasUnmanagedTypeConstraint)
{
constraints.Add("unmanaged");
}
foreach (ITypeSymbol c in tp.ConstraintTypes)
{
constraints.Add(c.ToDisplayString());
}
if (tp.HasConstructorConstraint)
{
constraints.Add("new()");
}
return constraints;
}
private static List<ApiAttribute> ExtractAttributes(ISymbol symbol)
{
return symbol.GetAttributes().Where(a => a.AttributeClass is not null).Where(a => !IsCompilerAttribute(a.AttributeClass!.Name)).Select(a => new ApiAttribute { Name = a.AttributeClass!.Name.Replace("Attribute", ""), Arguments = a.ConstructorArguments.Select(arg => arg.Value?.ToString() ?? "").ToList() }).ToList();
}
private static XmlDocBlock? ExtractXmlDoc(ISymbol symbol)
{
string? xml = symbol.GetDocumentationCommentXml();
if (string.IsNullOrWhiteSpace(xml))
{
return null;
}
try
{
var doc = XDocument.Parse(xml);
XElement? root = doc.Root;
if (root is null)
{
return null;
}
return new XmlDocBlock
{
Summary = XmlDocParser.RenderInnerXml(root.Element("summary")),
Remarks = XmlDocParser.RenderInnerXml(root.Element("remarks")),
Returns = XmlDocParser.RenderInnerXml(root.Element("returns")),
Value = XmlDocParser.RenderInnerXml(root.Element("value")),
Parameters = ParseDocParams(root, "param"),
TypeParameters = ParseDocParams(root, "typeparam"),
Exceptions = root.Elements("exception").Select(e => new ExceptionDoc { Type = (e.Attribute("cref")?.Value ?? "").TrimStart('T', ':'), Description = XmlDocParser.RenderInnerXml(e) }).ToList(),
Examples = root.Elements("example").Select(e => XmlDocParser.RenderInnerXml(e)).Where(s => !string.IsNullOrWhiteSpace(s)).ToList(),
SeeAlso = root.Elements("seealso").Select(e => e.Attribute("cref")?.Value ?? e.Value).Where(s => !string.IsNullOrWhiteSpace(s)).ToList()
};
}
catch
{
return null;
}
}
private static Dictionary<string, string> ParseDocParams(XElement root, string elementName)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (XElement p in root.Elements(elementName))
{
string? name = p.Attribute("name")?.Value;
if (!string.IsNullOrEmpty(name))
{
result[name] = XmlDocParser.RenderInnerXml(p);
}
}
return result;
}
private static bool ShouldInclude(INamedTypeSymbol symbol, bool includeInternals)
{
if (symbol.DeclaredAccessibility == Accessibility.Public)
{
return true;
}
if (includeInternals && symbol.DeclaredAccessibility == Accessibility.Internal)
{
return true;
}
if (symbol.DeclaredAccessibility == Accessibility.Protected)
{
return true;
}
if (symbol.DeclaredAccessibility == Accessibility.ProtectedOrInternal)
{
return true;
}
return false;
}
private static ApiAccessibility MapAccessibility(Accessibility a)
{
return a switch
{
Accessibility.Public => ApiAccessibility.Public,
Accessibility.Protected => ApiAccessibility.Protected,
Accessibility.Internal => ApiAccessibility.Internal,
Accessibility.ProtectedOrInternal => ApiAccessibility.ProtectedInternal,
Accessibility.ProtectedAndInternal => ApiAccessibility.PrivateProtected,
Accessibility.Private => ApiAccessibility.Private,
_ => ApiAccessibility.Public
};
}
private static bool HasAttribute(ISymbol symbol, string attributeName) => symbol.GetAttributes().Any(a => a.AttributeClass?.Name == attributeName);
private static string? GetObsoleteMessage(ISymbol symbol)
{
return symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == "ObsoleteAttribute")?.ConstructorArguments.FirstOrDefault().Value?.ToString();
}
private static bool IsCompilerAttribute(string name)
{
return name is "CompilerGeneratedAttribute" or "NullableAttribute" or "NullableContextAttribute" or "AsyncStateMachineAttribute" or "DebuggerStepThroughAttribute" or "IteratorStateMachineAttribute" or "IsReadOnlyAttribute" or "ParamArrayAttribute" or "TupleElementNamesAttribute" or "DynamicAttribute" or "IsUnmanagedAttribute" or "ExtensionAttribute";
}
#endregion
}