diff --git a/src/ZeroC.Slice.Symbols/BasicEnum.cs b/src/ZeroC.Slice.Symbols/BasicEnum.cs index dd77a3034..f307e91a0 100644 --- a/src/ZeroC.Slice.Symbols/BasicEnum.cs +++ b/src/ZeroC.Slice.Symbols/BasicEnum.cs @@ -15,6 +15,9 @@ public abstract class BasicEnum : Entity, ISymbol, IType /// Finds an enumerator by its Slice identifier. public abstract Entity? FindEnumeratorByIdentifier(string identifier); + + /// Gets the enumerators as a sequence of entities (without their typed values). + internal abstract IEnumerable EnumeratorEntities { get; } } /// Represents a Slice basic enumeration with a typed underlying value. @@ -27,6 +30,8 @@ public sealed class BasicEnum : BasicEnum where T : struct, System.Numerics.I public override Entity? FindEnumeratorByIdentifier(string identifier) => Enumerators.FirstOrDefault(e => e.Identifier == identifier); + internal override IEnumerable EnumeratorEntities => Enumerators; + /// Represents an enumerator in a Slice basic enumeration. public sealed class Enumerator : Entity { diff --git a/src/ZeroC.Slice.Symbols/Entity.cs b/src/ZeroC.Slice.Symbols/Entity.cs index aa725fa6d..f6c465091 100644 --- a/src/ZeroC.Slice.Symbols/Entity.cs +++ b/src/ZeroC.Slice.Symbols/Entity.cs @@ -20,7 +20,7 @@ public class Entity public Entity? Parent { get; internal set; } /// Gets the doc comment associated with this entity, if any. - public Comment? Comment { get; init; } + public Comment? Comment { get; internal set; } /// Gets the fully scoped Slice identifier (e.g. "MyModule::MyType" or /// "MyModule::MyInterface::MyOperation"). diff --git a/src/ZeroC.Slice.Symbols/SymbolConverter.cs b/src/ZeroC.Slice.Symbols/SymbolConverter.cs index 81e59a9e8..9916116ab 100644 --- a/src/ZeroC.Slice.Symbols/SymbolConverter.cs +++ b/src/ZeroC.Slice.Symbols/SymbolConverter.cs @@ -14,7 +14,7 @@ internal static ImmutableList ConvertFiles( IEnumerable referenceFiles) { var converter = new SymbolConverter(sourceFiles.Concat(referenceFiles)); - return sourceFiles.Select(converter.ConvertFile).ToImmutableList(); + return [.. sourceFiles.Select(converter.ConvertFile)]; } private static readonly Dictionary _builtins = new(StringComparer.Ordinal) @@ -60,13 +60,25 @@ private SymbolConverter(IEnumerable allFiles) } } - // Convert all named symbols in dependency order to fully populate _cache. + // Phase 1: convert all named symbols in dependency order. Doc-comment links are recorded as + // UnresolvedCommentLink and resolved in phase 2 — eager resolution here would miss any reference + // that targets a symbol not yet in _cache (forward references, self-references, or cycles between + // entities that aren't related by type dependencies). foreach (string typeId in TopologicalSort()) { (Compiler.SliceFile file, Compiler.Symbol symbol) = _named[typeId]; Module module = ConvertModule(file.ModuleDeclaration); _cache[typeId] = ConvertSymbol(symbol, file, module); } + + // Phase 2: resolve doc-comment links now that the symbol table is fully populated. + foreach (ISymbol symbol in _cache.Values) + { + if (symbol is Entity entity) + { + ResolveEntityComments(entity); + } + } } private SliceFile ConvertFile(Compiler.SliceFile file) @@ -74,7 +86,7 @@ private SliceFile ConvertFile(Compiler.SliceFile file) string moduleScope = file.ModuleDeclaration.Identifier; Module module = ConvertModule(file.ModuleDeclaration); - var contents = ImmutableList.CreateBuilder(); + ImmutableList.Builder contents = ImmutableList.CreateBuilder(); for (int i = 0; i < file.Contents.Count; i++) { Compiler.Symbol raw = file.Contents[i]; @@ -144,7 +156,7 @@ private List TopologicalSort() foreach ((string typeId, (Compiler.SliceFile file, Compiler.Symbol symbol)) in _named) { var deps = new HashSet(StringComparer.Ordinal); - CollectNamedTypeIds(symbol, file, deps); + CollectDependencies(symbol, file, deps); pending[typeId] = deps; } @@ -180,7 +192,7 @@ private List TopologicalSort() return sorted; - void CollectNamedTypeIds(Compiler.Symbol symbol, Compiler.SliceFile file, HashSet result) + void CollectDependencies(Compiler.Symbol symbol, Compiler.SliceFile file, HashSet result) { IEnumerable typeIds = symbol switch { @@ -204,7 +216,7 @@ void CollectNamedTypeIds(Compiler.Symbol symbol, Compiler.SliceFile file, HashSe else if (int.TryParse(typeId, out int index)) { // Anonymous type — recurse to collect any named types it references. - CollectNamedTypeIds(file.Contents[index], file, result); + CollectDependencies(file.Contents[index], file, result); } // else: builtin, ignore. } @@ -310,7 +322,7 @@ private Struct ConvertStruct(Compiler.Struct raw, Compiler.SliceFile file, Modul return result; } - private ISymbol ConvertBasicEnum(Compiler.BasicEnum raw, Module module) + private static ISymbol ConvertBasicEnum(Compiler.BasicEnum raw, Module module) { Builtin builtin = _builtins[raw.Underlying]; return builtin.Kind switch @@ -360,8 +372,7 @@ private ISymbol ConvertBasicEnum(Compiler.BasicEnum raw, Module module) module, builtin, (abs, _) => abs), - _ => throw new InvalidOperationException( - $"Unsupported enum underlying type: {builtin.Kind}"), + _ => throw new InvalidOperationException($"Unsupported enum underlying type: {builtin.Kind}"), }; } @@ -375,15 +386,17 @@ private ISymbol ConvertVariantEnum(Compiler.VariantEnum raw, Compiler.SliceFile Module = module, IsCompact = raw.IsCompact, IsUnchecked = raw.IsUnchecked, - Variants = raw.Variants.Select(v => new VariantEnum.Variant - { - Identifier = v.EntityInfo.Identifier, - Attributes = ConvertAttributes(v.EntityInfo.Attributes), - Comment = ConvertComment(v.EntityInfo.Comment), - Module = module, - Discriminant = v.Discriminant, - Fields = v.Fields.Select(f => ConvertField(f, file, module)).ToImmutableList(), - }).ToImmutableList(), + Variants = [ + .. raw.Variants.Select(v => new VariantEnum.Variant + { + Identifier = v.EntityInfo.Identifier, + Attributes = ConvertAttributes(v.EntityInfo.Attributes), + Comment = ConvertComment(v.EntityInfo.Comment), + Module = module, + Discriminant = v.Discriminant, + Fields = v.Fields.Select(f => ConvertField(f, file, module)).ToImmutableList(), + }) + ], }; SetParent(result, result.Variants); foreach (VariantEnum.Variant variant in result.Variants) @@ -401,10 +414,7 @@ private Interface ConvertInterface(Compiler.Interface raw, Compiler.SliceFile fi Attributes = ConvertAttributes(raw.EntityInfo.Attributes), Comment = ConvertComment(raw.EntityInfo.Comment), Module = module, - Bases = raw.Bases - .Select(baseId => ResolveNamedSymbol(baseId)) - .OfType() - .ToImmutableList(), + Bases = [.. raw.Bases.Select(ResolveNamedSymbol).OfType()], Operations = raw.Operations.Select(op => ConvertOperation(op, file, module)).ToImmutableList(), }; SetParent(result, result.Operations); @@ -433,7 +443,7 @@ private Operation ConvertOperation( return result; } - private BasicEnum CreateBasicEnum( + private static BasicEnum CreateBasicEnum( Compiler.BasicEnum raw, Module module, Builtin builtin, @@ -447,14 +457,16 @@ private BasicEnum CreateBasicEnum( Module = module, IsUnchecked = raw.IsUnchecked, Underlying = builtin, - Enumerators = raw.Enumerators.Select(e => new BasicEnum.Enumerator - { - Identifier = e.EntityInfo.Identifier, - Attributes = ConvertAttributes(e.EntityInfo.Attributes), - Comment = ConvertComment(e.EntityInfo.Comment), - Module = module, - Value = toValue(e.AbsoluteValue, e.HasNegativeValue), - }).ToImmutableList(), + Enumerators = [ + .. raw.Enumerators.Select(e => new BasicEnum.Enumerator + { + Identifier = e.EntityInfo.Identifier, + Attributes = ConvertAttributes(e.EntityInfo.Attributes), + Comment = ConvertComment(e.EntityInfo.Comment), + Module = module, + Value = toValue(e.AbsoluteValue, e.HasNegativeValue), + }) + ], }; SetParent(result, result.Enumerators); return result; @@ -492,13 +504,15 @@ private static void SetParent(Entity parent, IEnumerable children) } private static ImmutableList ConvertAttributes(IList raw) => - raw.Select(a => new Attribute - { - Directive = a.Directive, - Args = [.. a.Args], - }).ToImmutableList(); + [ + .. raw.Select(a => new Attribute + { + Directive = a.Directive, + Args = [.. a.Args], + }) + ]; - private Comment? ConvertComment(Compiler.DocComment? raw) + private static Comment? ConvertComment(Compiler.DocComment? raw) { if (raw is null) { @@ -508,18 +522,86 @@ private static ImmutableList ConvertAttributes(IList(c => c switch { Compiler.MessageComponent.Text t => new CommentText(t.V), - Compiler.MessageComponent.Link l => new CommentInlineLink(ResolveLink(l.V)), + Compiler.MessageComponent.Link l => new CommentInlineLink(new UnresolvedCommentLink(l.V)), _ => throw new InvalidOperationException($"Unknown MessageComponent kind: {c.GetType().FullName}") }).ToImmutableList(); - var seeTags = raw.Value.SeeTags.Select(ResolveLink).ToImmutableList(); + var seeTags = raw.Value.SeeTags + .Select(id => (CommentLink)new UnresolvedCommentLink(id)) + .ToImmutableList(); + + return new Comment { Overview = overview, SeeTags = seeTags }; + } + + private void ResolveEntityComments(Entity entity) + { + if (entity.Comment is Comment comment) + { + entity.Comment = ResolveComment(comment); + } + + switch (entity) + { + case Interface i: + foreach (Operation op in i.Operations) + { + ResolveEntityComments(op); + } + break; + case Operation op: + foreach (Field p in op.Parameters) + { + ResolveEntityComments(p); + } + foreach (Field r in op.ReturnType) + { + ResolveEntityComments(r); + } + break; + case Struct s: + foreach (Field f in s.Fields) + { + ResolveEntityComments(f); + } + break; + case VariantEnum v: + foreach (VariantEnum.Variant variant in v.Variants) + { + ResolveEntityComments(variant); + } + break; + case VariantEnum.Variant variant: + foreach (Field f in variant.Fields) + { + ResolveEntityComments(f); + } + break; + case BasicEnum basicEnum: + foreach (Entity enumerator in basicEnum.EnumeratorEntities) + { + ResolveEntityComments(enumerator); + } + break; + } + } + + private Comment ResolveComment(Comment comment) + { + var overview = comment.Overview.Select(c => c switch + { + CommentInlineLink l => new CommentInlineLink(ResolveOrKeep(l.Target)), + _ => c, + }) + .ToImmutableList(); + + var seeTags = comment.SeeTags.Select(ResolveOrKeep).ToImmutableList(); return new Comment { Overview = overview, SeeTags = seeTags }; - CommentLink ResolveLink(string entityId) => - ResolveEntityById(entityId) is Entity entity + CommentLink ResolveOrKeep(CommentLink link) => + link is UnresolvedCommentLink unresolved && ResolveEntityById(unresolved.Identifier) is Entity entity ? new ResolvedCommentLink(entity) - : new UnresolvedCommentLink(entityId); + : link; } private Entity? ResolveEntityById(string entityId)