Skip to content

Conversation

@jcouv
Copy link
Member

@jcouv jcouv commented Jul 28, 2025

Docs on extension blocks are attached to the corresponding extension marker type (docID is <ContainingType>.<GroupingType>.<MarkerType>).
Docs on equivalent extensions blocks (ie. same marker name) are merged silently.

Closes #79043 (fixed by new metadata design change, which uses marker names as grouping keys)
Relates to test plan #76130

@jcouv jcouv self-assigned this Jul 28, 2025
@jcouv jcouv added Area-Compilers Feature - Extension Everything The extension everything feature labels Jul 28, 2025
@jcouv jcouv force-pushed the extensions-xmldocs branch from f78dea1 to dc61bd5 Compare July 28, 2025 22:54
@jcouv jcouv force-pushed the extensions-xmldocs branch from dc61bd5 to e5261d8 Compare July 28, 2025 23:15
@jcouv jcouv mentioned this pull request Jul 24, 2025
28 tasks
@jcouv jcouv marked this pull request as ready for review July 29, 2025 01:50
@jcouv jcouv requested a review from a team as a code owner July 29, 2025 01:50
@jcouv jcouv requested a review from jjonescz July 29, 2025 01:50
@jcouv
Copy link
Member Author

jcouv commented Jul 29, 2025

@jjonescz @dotnet/roslyn-compiler for review. Thanks

throw ExceptionUtilities.Unreachable();
}

internal ImmutableArray<SourceNamedTypeSymbol> GetMatchingExtensions(SourceNamedTypeSymbol extension)
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetMatchingExtensions

Minor, perhaps the name is not very specific. #Closed

ArrayBuilder<SourceNamedTypeSymbol> extensions = sortSourceNamedTypeSymbols(markerMap[markerName]);
yield return extensions;

extensions.Free();
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extensions.Free();

This is very fragile, we don't know whether the consumer is done with the builder. Should find other ways to manage builders. Perhaps this method shouldn't allocate new builder (if it is done for sorting, that could be consumer's responsibility), or it should return a builder of builders and the consumer will be responsible to free them once done. #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, consumer can pass a delegate that will be executed for each builder and then the builder will be freed.

get
{
Debug.Assert(ExtensionMarkerTypes[0].UnderlyingExtensions[0].ContainingType is not null);
return ExtensionMarkerTypes[0].UnderlyingExtensions[0].ContainingType!.GetCciAdapter();
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!

Is the suppression still needed? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because of the array access

firstExtension = extension;
}

if (!TryGetDocumentationCommentNodes(extension, out var maxDocumentationMode, out var foundDocCommentNodes))
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var

Consider spelling out the type #Closed


if (documentedTypeParameters != null)
{
PooledHashSet<string> documentedTypeParameterNames = PooledHashSet<string>.GetInstance();
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentedTypeParameterNames

Does it make sense to build this set from the start, or there are other consumers that should use the original set? #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you provide details why the switch to hashset of names was reverted?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It caused a regression. I captured the scenario in XmlDoc_16 where we would have spurious WRN_DuplicateTypeParamTag. I'm punting on resolving this for now (added tracking issue to XmlDoc_05)

Debug.Assert(extension.IsExtension);
if (firstExtension is null)
{
firstExtension = extension;
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

firstExtension = extension;

It looks like the input might not be sorted, therefore the "first" might not be deterministic. #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this comment because GetMatchingExtensions doesn't explicitly sort the array before returning, but it looks like the array was sorted as part of its creation. It feels confusing though that two APIs return the "same" sequence of types as different collections types.

if (symbol.IsExtension && (SourceNamedTypeSymbol)symbol.ContainingType is { } containingType)
{
ImmutableArray<SourceNamedTypeSymbol> extensions = containingType.GetExtensionGroupingInfo().GetMatchingExtensions((SourceNamedTypeSymbol)symbol);
appendMergedExtensionBlocks(extensions);
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appendMergedExtensionBlocks

It would be good to add a comment about scenario when this code path is reachable. #Closed


foreach (var markerName in markerNames)
{
ArrayBuilder<SourceNamedTypeSymbol> extensions = sortSourceNamedTypeSymbols(markerMap[markerName]);
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sortSourceNamedTypeSymbols(markerMap[markerName]);

Note that GetMatchingExtensions effectively gets the same set as an immutable array, which is already sorted in lexical declaration order. #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, note that _lazyGroupingTypes already gives grouping types in deterministic, sorted order, and each of them provides ExtensionMarkerTypes in deterministic, sorted order, and each of them has UnderlyingExtensions in the desired sorted order. BTW, GetMatchingExtensions uses them instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Simplified

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Jul 30, 2025

            return _lazyUncommonProperties.extensionInfo.GroupingTypeSymbol.MetadataName; // PROTOTYPE: is this the right value to return?

It looks like the comment can be removed now. #Closed


Refers to: src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs:2707 in e5261d8. [](commit_id = e5261d8, deletion_comment = False)

}

internal override string ExtensionName
internal override string ExtensionGroupingName
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal override string ExtensionGroupingName

I think it will be better to not override these APIs in this class. #Closed


var e = comp.GetMember<NamedTypeSymbol>("E");

var extensions = e.GetTypeMembers().ToArray();
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extensions

Is this local used? #Closed

AssertEx.Equal("T:E.<Extension>$C43E2675C7BBF9284AF22FB8A9BF0280.<Extension>$8048A6C8BE30A622530249B904B537EB.<Marker>$D1693D81A12E8DED4ED68FE22D9E856F",
nestedExtension.GetDocumentationCommentId());

// PROTOTYPE should we be merging these nested extension blocks instead?
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// PROTOTYPE should we be merging these nested extension blocks instead?

I do not think this error scenario worth the effort #Closed

}

/// <typeparam name="T2">Description for T2</typeparam>
extension<T2, U>(C<T2, U> c)
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T2

Do we test scenario where this type parameter ins named "T1" (to force merging with the previous extension)? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XmlDoc_05. Note: it was fixed by tracking type parameters by name (it now reports a warning for documenting type parameter twice)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we test scenario where this type parameter ins named "T1" (to force merging with the previous extension)?

XmlDoc_05. Note: it was fixed by tracking type parameters by name (it now reports a warning for documenting type parameter twice)

I assume the answer is "No" then. It appears this test is focusing on a different aspect, ErrorCode.WRN_MissingTypeParamTag.

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Jul 30, 2025

Done with review pass (commit 1) #Closed

@jcouv
Copy link
Member Author

jcouv commented Jul 30, 2025

            return _lazyUncommonProperties.extensionInfo.GroupingTypeSymbol.MetadataName; // PROTOTYPE: is this the right value to return?

It's removed in next PR where I also added explicit test for this scenario/path


In reply to: 3137046726


Refers to: src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs:2707 in e5261d8. [](commit_id = e5261d8, deletion_comment = False)

}

internal override string ExtensionGroupingName
=> _underlyingType.ExtensionGroupingName;
Copy link
Member Author

@jcouv jcouv Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like the concept of grouping and marker name should only be applicable to definitions. Maybe we could avoid implementation here by adjusting callers (symbol display and docID). What do you think? #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the implementation is simple enough to not introduce additional complexity on consumer's side.

@jcouv jcouv requested a review from AlekseyTs July 30, 2025 19:02
@jcouv
Copy link
Member Author

jcouv commented Jul 30, 2025

@jjonescz for second review. Thanks

@AlekseyTs
Copy link
Contributor

@jcouv It looks like there are legitimate test failures

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Jul 31, 2025

    var extensions = e.GetTypeMembers().ToArray();

Isn't this an array already? #Closed


Refers to: src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs:48220 in c556788. [](commit_id = c556788, deletion_comment = False)

throw ExceptionUtilities.Unreachable();
}

// Given an extension block, returns all the extensions that are grouped together with it
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

//

Consider using standard doc comment #Closed

get { throw ExceptionUtilities.Unreachable(); }
}

internal override string ExtensionGroupingName
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal override string ExtensionGroupingName

I think the implementation in SubstitutedNestedTypeSymbol should be sealed instead. #Closed


private int _nextSourceIncludeElementIndex;
private HashSet<Location> _inProgressIncludeElementNodes;
private HashSet<ParameterSymbol> _documentedParameters;
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_documentedParameters

Is it possible that we might want to make the same change for this hashset? Consider opening a follow-up issue if so. #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible, but I'd rather not spend time to track this through now and if we open an issue to look at it later I doubt we'll get to it in MQ. So I prefer to let it be.

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Jul 31, 2025

Done with review pass (commit 2) #Closed

}

return _lazyUncommonProperties.extensionInfo.GroupingTypeSymbol.MetadataName; // PROTOTYPE: is this the right value to return?
}
Copy link
Member

@jjonescz jjonescz Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the prototype comment still valid? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's removed in next PR where I also added explicit test for this scenario/path

return GetCorrespondingMarkerType(extension).UnderlyingExtensions;
}

// Returns all the extension blocks but grouped/merged by equivalency (ie. same marker name)
Copy link
Member

@jjonescz jjonescz Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider turning this into a doc comment. #Resolved

{
get
{
Debug.Assert(ExtensionMarkerTypes[0].UnderlyingExtensions[0].ContainingType is not null);
Copy link
Member

@jjonescz jjonescz Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting ExtensionMarkerTypes[0].UnderlyingExtensions[0].ContainingType into a variable and removing the suppression below. #Resolved

}
if (_lazyExtensionInfo.LazyExtensionMarkerName is null)
{
_lazyExtensionInfo.LazyExtensionMarkerName = "<Marker>$" + RawNameToHashString(ComputeExtensionMarkerRawName()); // PROTOTYPE: Add a constant for this prefix.
Copy link
Member

@jjonescz jjonescz Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether we should choose shorter metadata prefixes like <E>$ and <M>$ (I'm not sure if even all the special characters are necessary, perhaps keep only the $) to avoid bloating up metadata sizes. #ByDesign

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable. Please reply to the email thread I started on the naming convention topic if you want to suggest a change

@jcouv jcouv requested review from AlekseyTs and jjonescz July 31, 2025 16:33
@AlekseyTs
Copy link
Contributor

AlekseyTs commented Jul 31, 2025

        //// (13,20): warning CS1710: XML comment has a duplicate typeparam tag for 'T'

Is there a similar warning for parameters? If so, perhaps one should be expected in this scenario too. #Resolved


Refers to: src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs:48213 in 7f20a26. [](commit_id = 7f20a26, deletion_comment = False)

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (commit 6)

@jcouv jcouv merged commit 21dede8 into dotnet:features/extensions Jul 31, 2025
24 of 28 checks passed
@jcouv jcouv deleted the extensions-xmldocs branch July 31, 2025 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers Feature - Extension Everything The extension everything feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants