Skip to content

Commit 345419e

Browse files
authored
Extensions: allow cref references to extension members (#78735)
1 parent 347786a commit 345419e

39 files changed

+3259
-204
lines changed

src/Compilers/CSharp/Portable/Binder/Binder_Crefs.cs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ private ImmutableArray<Symbol> BindCrefInternal(CrefSyntax syntax, out Symbol? a
3535
case SyntaxKind.IndexerMemberCref:
3636
case SyntaxKind.OperatorMemberCref:
3737
case SyntaxKind.ConversionOperatorMemberCref:
38+
case SyntaxKind.ExtensionMemberCref:
3839
return BindMemberCref((MemberCrefSyntax)syntax, containerOpt: null, ambiguityWinner: out ambiguityWinner, diagnostics: diagnostics);
3940
default:
4041
throw ExceptionUtilities.UnexpectedValue(syntax.Kind());
@@ -125,6 +126,9 @@ private ImmutableArray<Symbol> BindMemberCref(MemberCrefSyntax syntax, Namespace
125126
case SyntaxKind.ConversionOperatorMemberCref:
126127
result = BindConversionOperatorMemberCref((ConversionOperatorMemberCrefSyntax)syntax, containerOpt, out ambiguityWinner, diagnostics);
127128
break;
129+
case SyntaxKind.ExtensionMemberCref:
130+
result = BindExtensionMemberCref((ExtensionMemberCrefSyntax)syntax, containerOpt, out ambiguityWinner, diagnostics);
131+
break;
128132
default:
129133
throw ExceptionUtilities.UnexpectedValue(syntax.Kind());
130134
}
@@ -216,6 +220,142 @@ private ImmutableArray<Symbol> BindIndexerMemberCref(IndexerMemberCrefSyntax syn
216220
diagnostics: diagnostics);
217221
}
218222

223+
private ImmutableArray<Symbol> BindExtensionMemberCref(ExtensionMemberCrefSyntax syntax, NamespaceOrTypeSymbol? containerOpt, out Symbol? ambiguityWinner, BindingDiagnosticBag diagnostics)
224+
{
225+
// Tracked by https://github.com/dotnet/roslyn/issues/76130 : handle extension operators
226+
CheckFeatureAvailability(syntax, MessageID.IDS_FeatureExtensions, diagnostics);
227+
228+
if (containerOpt is not NamedTypeSymbol namedContainer)
229+
{
230+
ambiguityWinner = null;
231+
return ImmutableArray<Symbol>.Empty;
232+
}
233+
234+
ImmutableArray<Symbol> sortedSymbols = default;
235+
int arity = 0;
236+
TypeArgumentListSyntax? typeArgumentListSyntax = null;
237+
CrefParameterListSyntax? parameters = null;
238+
239+
if (syntax.Member is NameMemberCrefSyntax { Name: SimpleNameSyntax simpleName } nameMember)
240+
{
241+
arity = simpleName.Arity;
242+
typeArgumentListSyntax = simpleName is GenericNameSyntax genericName ? genericName.TypeArgumentList : null;
243+
parameters = nameMember.Parameters;
244+
245+
TypeArgumentListSyntax? extensionTypeArguments = syntax.TypeArgumentList;
246+
int extensionArity = extensionTypeArguments?.Arguments.Count ?? 0;
247+
sortedSymbols = computeSortedAndFilteredCrefExtensionMembers(namedContainer, simpleName.Identifier.ValueText, extensionArity, arity, extensionTypeArguments, diagnostics, syntax);
248+
}
249+
250+
if (sortedSymbols.IsDefaultOrEmpty)
251+
{
252+
ambiguityWinner = null;
253+
return [];
254+
}
255+
256+
Debug.Assert(sortedSymbols.All(s => s.GetIsNewExtensionMember()));
257+
258+
return ProcessCrefMemberLookupResults(sortedSymbols, arity, syntax, typeArgumentListSyntax, parameters, out ambiguityWinner, diagnostics);
259+
260+
ImmutableArray<Symbol> computeSortedAndFilteredCrefExtensionMembers(NamedTypeSymbol container, string name, int extensionArity, int arity, TypeArgumentListSyntax? extensionTypeArguments, BindingDiagnosticBag diagnostics, ExtensionMemberCrefSyntax syntax)
261+
{
262+
Debug.Assert(name is not null);
263+
264+
Debug.Assert(syntax.Parameters is not null);
265+
ImmutableArray<ParameterSymbol> extensionParameterSymbols = BindCrefParameters(syntax.Parameters, diagnostics);
266+
267+
// Use signature method symbols to match extension blocks
268+
var providedExtensionSignature = new SignatureOnlyMethodSymbol(
269+
methodKind: MethodKind.Ordinary,
270+
typeParameters: IndexedTypeParameterSymbol.TakeSymbols(extensionArity),
271+
parameters: extensionParameterSymbols,
272+
callingConvention: Cci.CallingConvention.Default,
273+
// These are ignored by this specific MemberSignatureComparer.
274+
containingType: null,
275+
name: null,
276+
refKind: RefKind.None,
277+
isInitOnly: false,
278+
isStatic: false,
279+
returnType: default,
280+
refCustomModifiers: [],
281+
explicitInterfaceImplementations: []);
282+
283+
LookupOptions options = LookupOptions.AllMethodsOnArityZero | LookupOptions.MustNotBeParameter;
284+
CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = this.GetNewCompoundUseSiteInfo(diagnostics);
285+
ArrayBuilder<Symbol>? sortedSymbolsBuilder = null;
286+
287+
foreach (var nested in container.GetTypeMembers())
288+
{
289+
if (!nested.IsExtension || nested.Arity != extensionArity || nested.ExtensionParameter is null)
290+
{
291+
continue;
292+
}
293+
294+
var constructedNested = (NamedTypeSymbol)ConstructWithCrefTypeParameters(extensionArity, extensionTypeArguments, nested);
295+
296+
var candidateExtensionSignature = new SignatureOnlyMethodSymbol(
297+
methodKind: MethodKind.Ordinary,
298+
typeParameters: IndexedTypeParameterSymbol.TakeSymbols(constructedNested.Arity),
299+
parameters: [constructedNested.ExtensionParameter],
300+
callingConvention: Cci.CallingConvention.Default,
301+
// These are ignored by this specific MemberSignatureComparer.
302+
containingType: null,
303+
name: null,
304+
refKind: RefKind.None,
305+
isInitOnly: false,
306+
isStatic: false,
307+
returnType: default,
308+
refCustomModifiers: [],
309+
explicitInterfaceImplementations: []);
310+
311+
if (!MemberSignatureComparer.CrefComparer.Equals(candidateExtensionSignature, providedExtensionSignature))
312+
{
313+
continue;
314+
}
315+
316+
var candidates = constructedNested.GetMembers(name);
317+
318+
foreach (var candidate in candidates)
319+
{
320+
if (!SourceMemberContainerTypeSymbol.IsAllowedExtensionMember(candidate))
321+
{
322+
continue;
323+
}
324+
325+
if (arity != 0 && candidate.GetArity() != arity)
326+
{
327+
continue;
328+
}
329+
330+
// Note: we bypass the arity check here, as it would check for total arity (extension + member arity)
331+
SingleLookupResult result = this.CheckViability(candidate, arity: 0, options, accessThroughType: null, diagnose: true, useSiteInfo: ref useSiteInfo);
332+
333+
if (result.Kind == LookupResultKind.Viable)
334+
{
335+
sortedSymbolsBuilder ??= ArrayBuilder<Symbol>.GetInstance();
336+
sortedSymbolsBuilder.Add(result.Symbol);
337+
}
338+
}
339+
}
340+
341+
diagnostics.Add(syntax, useSiteInfo);
342+
343+
if (sortedSymbolsBuilder is null)
344+
{
345+
return ImmutableArray<Symbol>.Empty;
346+
}
347+
348+
// Since we resolve ambiguities by just picking the first symbol we encounter,
349+
// the order of the symbols matters for repeatability.
350+
if (sortedSymbolsBuilder.Count > 1)
351+
{
352+
sortedSymbolsBuilder.Sort(ConsistentSymbolOrder.Instance);
353+
}
354+
355+
return sortedSymbolsBuilder.ToImmutableAndFree();
356+
}
357+
}
358+
219359
// NOTE: not guaranteed to be a method (e.g. class op_Addition)
220360
// NOTE: constructor fallback logic applies
221361
private ImmutableArray<Symbol> BindOperatorMemberCref(OperatorMemberCrefSyntax syntax, NamespaceOrTypeSymbol? containerOpt, out Symbol? ambiguityWinner, BindingDiagnosticBag diagnostics)

src/Compilers/CSharp/Portable/Binder/WithCrefTypeParametersBinder.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ private void AddTypeParameters(TypeSyntax typeSyntax, MultiDictionary<string, Ty
9090
AddTypeParameters(qualifiedNameSyntax.Left, map);
9191
break;
9292
case SyntaxKind.GenericName:
93-
AddTypeParameters((GenericNameSyntax)typeSyntax, map);
93+
AddTypeParameters(((GenericNameSyntax)typeSyntax).TypeArgumentList.Arguments, map);
9494
break;
9595
case SyntaxKind.IdentifierName:
9696
case SyntaxKind.PredefinedType:
@@ -103,20 +103,28 @@ private void AddTypeParameters(TypeSyntax typeSyntax, MultiDictionary<string, Ty
103103
private void AddTypeParameters(MemberCrefSyntax memberSyntax, MultiDictionary<string, TypeParameterSymbol> map)
104104
{
105105
// Other members have arity 0.
106-
if (memberSyntax.Kind() == SyntaxKind.NameMemberCref)
106+
if (memberSyntax is NameMemberCrefSyntax nameMemberCref)
107107
{
108-
AddTypeParameters(((NameMemberCrefSyntax)memberSyntax).Name, map);
108+
AddTypeParameters(nameMemberCref.Name, map);
109+
}
110+
else if (memberSyntax is ExtensionMemberCrefSyntax extensionCref)
111+
{
112+
if (extensionCref.TypeArgumentList is { } extensionTypeArguments)
113+
{
114+
AddTypeParameters(extensionTypeArguments.Arguments, map);
115+
}
116+
117+
AddTypeParameters(extensionCref.Member, map);
109118
}
110119
}
111120

112-
private static void AddTypeParameters(GenericNameSyntax genericNameSyntax, MultiDictionary<string, TypeParameterSymbol> map)
121+
private static void AddTypeParameters(SeparatedSyntaxList<TypeSyntax> typeArguments, MultiDictionary<string, TypeParameterSymbol> map)
113122
{
114123
// NOTE: Dev11 does not warn about duplication, it just matches parameter types to the
115124
// *last* type parameter with the same name. That's why we're iterating backwards and
116125
// skipping subsequent symbols with the same name. This can result in some surprising
117126
// behavior. For example, both 'T's in "A<T>.B<T>" bind to the second implicitly
118127
// declared type parameter.
119-
SeparatedSyntaxList<TypeSyntax> typeArguments = genericNameSyntax.TypeArgumentList.Arguments;
120128
for (int i = typeArguments.Count - 1; i >= 0; i--)
121129
{
122130
// Other types (non-identifiers) are allowed in error scenarios, but they do not introduce new

src/Compilers/CSharp/Portable/CSharpResources.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8096,7 +8096,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
80968096
<value>Extension declarations may not have a name.</value>
80978097
</data>
80988098
<data name="ERR_ExtensionDisallowsMember" xml:space="preserve">
8099-
<value>Extension declarations can include only methods or properties</value>
8099+
<value>This member is not allowed in an extension block</value>
81008100
</data>
81018101
<data name="ERR_BadExtensionContainingType" xml:space="preserve">
81028102
<value>Extensions must be declared in a top-level, non-generic, static class</value>
@@ -8185,4 +8185,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
81858185
<data name="ERR_PPShebangInProjectBasedProgram" xml:space="preserve">
81868186
<value>'#!' directives can be only used in scripts or file-based programs</value>
81878187
</data>
8188+
<data name="ERR_MisplacedExtension" xml:space="preserve">
8189+
<value>An extension member syntax is disallowed in nested position within an extension member syntax</value>
8190+
</data>
81888191
</root>

src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ internal static bool HasParameterList(CrefSyntax crefSyntax)
357357
return ((OperatorMemberCrefSyntax)crefSyntax).Parameters != null;
358358
case SyntaxKind.ConversionOperatorMemberCref:
359359
return ((ConversionOperatorMemberCrefSyntax)crefSyntax).Parameters != null;
360+
case SyntaxKind.ExtensionMemberCref:
361+
return HasParameterList(((ExtensionMemberCrefSyntax)crefSyntax).Member);
360362
}
361363

362364
return false;
@@ -379,7 +381,7 @@ private static SymbolInfo GetCrefSymbolInfo(OneOrMany<Symbol> symbols, SymbolInf
379381

380382
LookupResultKind resultKind = LookupResultKind.Ambiguous;
381383

382-
// The boundary between Ambiguous and OverloadResolutionFailure is let clear-cut for crefs.
384+
// The boundary between Ambiguous and OverloadResolutionFailure is less clear-cut for crefs.
383385
// We'll say that overload resolution failed if the syntax has a parameter list and if
384386
// all of the candidates have the same kind.
385387
SymbolKind firstCandidateKind = symbols[0].Kind;

src/Compilers/CSharp/Portable/Compiler/DocumentationCommentCompiler.DocumentationCommentWalker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public override void DefaultVisit(SyntaxNode node)
151151

152152
// Do this for the diagnostics, even if it won't be written.
153153
BindingDiagnosticBag diagnostics = diagnose ? _diagnostics : BindingDiagnosticBag.GetInstance(withDiagnostics: false, withDependencies: _diagnostics.AccumulatesDependencies);
154-
string docCommentId = GetDocumentationCommentId(cref, binder, diagnostics);
154+
string docCommentId = GetEscapedDocumentationCommentId(cref, binder, diagnostics);
155155

156156
if (!diagnose)
157157
{

src/Compilers/CSharp/Portable/Compiler/DocumentationCommentCompiler.IncludeElementExpander.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ private void BindAndReplaceCref(XAttribute attribute, CSharpSyntaxNode originati
516516
Binder binder = BinderFactory.MakeCrefBinder(crefSyntax, memberDeclSyntax, _compilation.GetBinderFactory(memberDeclSyntax.SyntaxTree));
517517

518518
var crefDiagnostics = BindingDiagnosticBag.GetInstance(_diagnostics);
519-
attribute.Value = GetDocumentationCommentId(crefSyntax, binder, crefDiagnostics); // NOTE: mutation (element must be a copy)
519+
attribute.Value = GetEscapedDocumentationCommentId(crefSyntax, binder, crefDiagnostics); // NOTE: mutation (element must be a copy)
520520
RecordBindingDiagnostics(crefDiagnostics, sourceLocation); // Respects DocumentationMode.
521521
crefDiagnostics.Free();
522522
}

src/Compilers/CSharp/Portable/Compiler/DocumentationCommentCompiler.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,9 @@ public override void VisitMethod(MethodSymbol symbol)
251251
return;
252252
}
253253

254-
WriteLine("<member name=\"{0}\">", symbol.GetDocumentationCommentId());
254+
WriteLine("<member name=\"{0}\">", symbol.GetEscapedDocumentationCommentId());
255255
Indent();
256-
WriteLine("<inheritdoc cref=\"{0}\"/>", symbolForDocComment.GetDocumentationCommentId());
256+
WriteLine("<inheritdoc cref=\"{0}\"/>", symbolForDocComment.GetEscapedDocumentationCommentId());
257257
Unindent();
258258
WriteLine("</member>");
259259
return;
@@ -331,7 +331,7 @@ public override void DefaultVisit(Symbol symbol)
331331
// If the XML in any of the doc comments is invalid, skip all further processing (for this symbol) and
332332
// just write a comment saying that info was lost for this symbol.
333333
string message = ErrorFacts.GetMessage(MessageID.IDS_XMLIGNORED, CultureInfo.CurrentUICulture);
334-
WriteLine(string.Format(CultureInfo.CurrentUICulture, message, symbol.GetDocumentationCommentId()));
334+
WriteLine(string.Format(CultureInfo.CurrentUICulture, message, symbol.GetEscapedDocumentationCommentId()));
335335
return;
336336
}
337337

@@ -381,7 +381,7 @@ public override void DefaultVisit(Symbol symbol)
381381
// If the XML in any of the doc comments is invalid, skip all further processing (for this symbol) and
382382
// just write a comment saying that info was lost for this symbol.
383383
string message = ErrorFacts.GetMessage(MessageID.IDS_XMLIGNORED, CultureInfo.CurrentUICulture);
384-
WriteLine(string.Format(CultureInfo.CurrentUICulture, message, symbol.GetDocumentationCommentId()));
384+
WriteLine(string.Format(CultureInfo.CurrentUICulture, message, symbol.GetEscapedDocumentationCommentId()));
385385
return;
386386
}
387387

@@ -467,7 +467,7 @@ private bool TryProcessRecordPropertyDocumentation(
467467
Debug.Assert(paramTags.Count > 0);
468468

469469
BeginTemporaryString();
470-
WriteLine("<member name=\"{0}\">", recordPropertySymbol.GetDocumentationCommentId());
470+
WriteLine("<member name=\"{0}\">", recordPropertySymbol.GetEscapedDocumentationCommentId());
471471
Indent();
472472
var substitutedTextBuilder = PooledStringBuilder.GetInstance();
473473
var includeElementNodesBuilder = _processIncludes ? ArrayBuilder<CSharpSyntaxNode>.GetInstance() : null;
@@ -573,7 +573,7 @@ private bool TryProcessDocumentationCommentTriviaNodes(
573573
// because we need to have XML to process.
574574
if (!shouldSkipPartialDefinitionComments || _processIncludes)
575575
{
576-
WriteLine("<member name=\"{0}\">", symbol.GetDocumentationCommentId());
576+
WriteLine("<member name=\"{0}\">", symbol.GetEscapedDocumentationCommentId());
577577
Indent();
578578
}
579579

@@ -1096,7 +1096,7 @@ private static string LongestCommonPrefix(string str1, string str2)
10961096
/// <remarks>
10971097
/// Does not respect DocumentationMode, so use a temporary bag if diagnostics are not desired.
10981098
/// </remarks>
1099-
private static string GetDocumentationCommentId(CrefSyntax crefSyntax, Binder binder, BindingDiagnosticBag diagnostics)
1099+
private static string GetEscapedDocumentationCommentId(CrefSyntax crefSyntax, Binder binder, BindingDiagnosticBag diagnostics)
11001100
{
11011101
if (crefSyntax.ContainsDiagnostics)
11021102
{
@@ -1135,7 +1135,7 @@ private static string GetDocumentationCommentId(CrefSyntax crefSyntax, Binder bi
11351135
diagnostics.AddDependencies(symbol as TypeSymbol ?? symbol.ContainingType);
11361136
}
11371137

1138-
return symbol.OriginalDefinition.GetDocumentationCommentId();
1138+
return symbol.OriginalDefinition.GetEscapedDocumentationCommentId();
11391139
}
11401140

11411141
/// <summary>

src/Compilers/CSharp/Portable/Errors/ErrorCode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2400,7 +2400,7 @@ internal enum ErrorCode
24002400
ERR_ExpressionTreeContainsNamedArgumentOutOfPosition = 9307,
24012401

24022402
ERR_OperatorsMustBePublic = 9308,
2403-
// available 9309,
2403+
ERR_MisplacedExtension = 9309,
24042404
ERR_OperatorMustReturnVoid = 9310,
24052405
ERR_CloseUnimplementedInterfaceMemberOperatorMismatch = 9311,
24062406
ERR_OperatorMismatchOnOverride = 9312,

src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,7 @@ or ErrorCode.ERR_PartialWrongTypeParams
830830
or ErrorCode.ERR_PartialWrongConstraints
831831
or ErrorCode.ERR_NoImplicitConvCast
832832
or ErrorCode.ERR_PartialMisplaced
833+
or ErrorCode.ERR_MisplacedExtension
833834
or ErrorCode.ERR_ImportedCircularBase
834835
or ErrorCode.ERR_UseDefViolationOut
835836
or ErrorCode.ERR_ArraySizeInDeclaration

src/Compilers/CSharp/Portable/Generated/CSharp.Generated.g4

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)