Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0f022d5
C#: Extractor support for extension blocks.
michaelnebel Jan 26, 2026
95e08a8
C#: Implicit extension parameters should be fresh entities.
michaelnebel Jan 26, 2026
c5af880
C#: Add extension types to the db scheme.
michaelnebel Jan 26, 2026
359934c
C#: Static extension methods in extension types should also have thei…
michaelnebel Jan 26, 2026
7b74200
C#: The call to an extension operator should be considered an operato…
michaelnebel Jan 27, 2026
9d4d2b7
C#: Minor improvements.
michaelnebel Jan 27, 2026
4e63de3
C#: Introduce a new expr kind for when accessors are invoked directly.
michaelnebel Jan 28, 2026
e4b0fa8
C#: Add various extension related classes to the QL library.
michaelnebel Jan 28, 2026
146d172
C#: Add extensions tests.
michaelnebel Jan 28, 2026
bda6dc1
C#: Extract the receiver type for extension types.
michaelnebel Jan 29, 2026
ca5e18e
C#: Add a extension receiver type table to the DB scheme.
michaelnebel Jan 29, 2026
791a7db
C#: Use the relation in the ExtensionType relation in the QL library.
michaelnebel Jan 29, 2026
5f35a62
C#: Add some QL tests for extension types.
michaelnebel Jan 29, 2026
a8eccea
C#: Remove some TODO reminder comments.
michaelnebel Jan 29, 2026
bfd4318
C#: Synthesize the implicit extension parameters for generics.
michaelnebel Jan 29, 2026
ec5b3ee
C#: Add generic test.
michaelnebel Jan 29, 2026
240c637
C#: Replace calls to constructed generic extensions methods with call…
michaelnebel Jan 30, 2026
c0b70af
C#: Update test expected output.
michaelnebel Jan 30, 2026
5f3c3bf
C#: Add some more generic types and methods.
michaelnebel Jan 30, 2026
fa769aa
C#: Add unbound parameter for constructed generic extensions.
michaelnebel Jan 30, 2026
7f32d50
C#: Update extensionType expected output.
michaelnebel Jan 30, 2026
81814ca
C#: Support replacing invocations of generic methods in generic exten…
michaelnebel Jan 30, 2026
570748c
C#: Add some more tests and update the expected test output.
michaelnebel Jan 30, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Semmle.Util;
using Semmle.Extraction.CSharp.Entities;

namespace Semmle.Extraction.CSharp
Expand Down Expand Up @@ -164,6 +165,7 @@
case TypeKind.Enum:
case TypeKind.Delegate:
case TypeKind.Error:
case TypeKind.Extension:
var named = (INamedTypeSymbol)type;
named.BuildNamedTypeId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType);
return;
Expand Down Expand Up @@ -275,6 +277,16 @@
public static IEnumerable<IFieldSymbol?> GetTupleElementsMaybeNull(this INamedTypeSymbol type) =>
type.TupleElements;

private static string GetExtensionTypeName(this INamedTypeSymbol named, Context cx)
{
var type = named.ExtensionParameter?.Type.Name;
if (type is null)
{
cx.ModelError(named, "Failed to get extension method type.");
}
return $"extension({type ?? "unknown"})";
}

private static void BuildQualifierAndName(INamedTypeSymbol named, Context cx, EscapingTextWriter trapFile, ISymbol symbolBeingDefined)
{
if (named.ContainingType is not null)
Expand All @@ -289,8 +301,19 @@
named.ContainingNamespace.BuildNamespace(cx, trapFile);
}

var name = named.IsFileLocal ? named.MetadataName : named.Name;
trapFile.Write(name);
if (named.IsFileLocal)
{
trapFile.Write(named.MetadataName);
}
else if (named.IsExtension)
{
var name = GetExtensionTypeName(named, cx);
trapFile.Write(name);
}
else
{
trapFile.Write(named.Name);
}
}

private static void BuildTupleId(INamedTypeSymbol named, Context cx, EscapingTextWriter trapFile, ISymbol symbolBeingDefined)
Expand Down Expand Up @@ -391,6 +414,7 @@
case TypeKind.Enum:
case TypeKind.Delegate:
case TypeKind.Error:
case TypeKind.Extension:
var named = (INamedTypeSymbol)type;
named.BuildNamedTypeDisplayName(cx, trapFile, constructUnderlyingTupleType);
return;
Expand Down Expand Up @@ -484,6 +508,13 @@
return;
}

if (namedType.IsExtension)
{
var name = GetExtensionTypeName(namedType, cx);
trapFile.Write(name);
return;
}

if (namedType.IsAnonymousType)
{
namedType.BuildAnonymousName(cx, trapFile);
Expand Down Expand Up @@ -596,6 +627,74 @@
return true;
}

/// <summary>
/// Return true if this method is a compiler-generated extension method.
/// </summary>
public static bool IsCompilerGeneratedExtensionMethod(this IMethodSymbol method) =>
method.TryGetExtensionMethod(out _);

/// <summary>
/// Returns true if this method is a compiler-generated extension method,
/// and outputs the original extension method declaration.
/// </summary>
public static bool TryGetExtensionMethod(this IMethodSymbol method, out IMethodSymbol? declaration)
{
declaration = null;
if (method.IsImplicitlyDeclared && method.ContainingSymbol is INamedTypeSymbol containingType)
{
// Extension types are declared within the same type as the generated
// extension method implementation.
var extensions = containingType.GetMembers()
.OfType<INamedTypeSymbol>()
.Where(t => t.IsExtension);
// Find the (possibly unbound) original extension method that maps to this implementation (if any).
var unboundDeclaration = extensions.SelectMany(e => e.GetMembers())
.OfType<IMethodSymbol>()
.FirstOrDefault(m => SymbolEqualityComparer.Default.Equals(m.AssociatedExtensionImplementation, method.ConstructedFrom));

var isFullyConstructed = method.IsBoundGenericMethod();
// TODO: We also need to handle generic methods in non-generic extension types.
if (isFullyConstructed && unboundDeclaration?.ContainingType is INamedTypeSymbol extensionType && extensionType.IsGenericType)
{
try
{
// Use the type arguments from the constructed extension method to construct the extension type.
var arguments = method.TypeArguments.ToArray();
var (extensionTypeArguments, extensionMethodArguments) = arguments.SplitAt(extensionType.TypeParameters.Length);

// Construct the extension type.
var boundExtensionType = extensionType.Construct(extensionTypeArguments.ToArray());

// Find the extension method declaration within the constructed extension type.
var extensionDeclaration = boundExtensionType.GetMembers()
.OfType<IMethodSymbol>()
.First(c => SymbolEqualityComparer.Default.Equals(c.OriginalDefinition, unboundDeclaration));

// If the extension declaration is unbound apply the remaning type arguments and construct it.
declaration = extensionDeclaration.IsUnboundGenericMethod()
? extensionDeclaration.Construct(extensionMethodArguments.ToArray())
: extensionDeclaration;
}
catch (Exception)
{
// If anything goes wrong, fall back to the unbound declaration.
declaration = unboundDeclaration;
}
Comment on lines +678 to +682

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
}
else
{
declaration = unboundDeclaration;
}

}
return declaration is not null;
}

public static bool IsUnboundGenericMethod(this IMethodSymbol method) =>
method.IsGenericMethod && SymbolEqualityComparer.Default.Equals(method.ConstructedFrom, method);

public static bool IsBoundGenericMethod(this IMethodSymbol method) => method.IsGenericMethod && !IsUnboundGenericMethod(method);

/// <summary>
/// Gets the base type of `symbol`. Unlike `symbol.BaseType`, this excludes effective base
/// types of type parameters as well as `object` base types.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,6 @@ public string DebugContents
}
}

protected static void WriteLocationToTrap<T1>(Action<T1, Location> writeAction, T1 entity, Location l)
{
if (l is not EmptyLocation)
{
writeAction(entity, l);
}
}

protected static void WriteLocationsToTrap<T1>(Action<T1, Location> writeAction, T1 entity, IEnumerable<Location> locations)
{
foreach (var loc in locations)
{
WriteLocationToTrap(writeAction, entity, loc);
}
}

public override bool NeedsPopulation { get; }

public override int GetHashCode() => Symbol is null ? 0 : Symbol.GetHashCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,32 +32,6 @@ protected void PopulateAttributes()
Attribute.ExtractAttributes(Context, Symbol, this);
}

protected void PopulateNullability(TextWriter trapFile, AnnotatedTypeSymbol type)
{
var n = NullabilityEntity.Create(Context, Nullability.Create(type));
if (!type.HasObliviousNullability())
{
trapFile.type_nullability(this, n);
}
}

protected void PopulateRefKind(TextWriter trapFile, RefKind kind)
{
switch (kind)
{
case RefKind.Out:
trapFile.type_annotation(this, Kinds.TypeAnnotation.Out);
break;
case RefKind.Ref:
trapFile.type_annotation(this, Kinds.TypeAnnotation.Ref);
break;
case RefKind.RefReadOnly:
case RefKind.RefReadOnlyParameter:
trapFile.type_annotation(this, Kinds.TypeAnnotation.ReadonlyRef);
break;
}
}

protected void PopulateScopedKind(TextWriter trapFile, ScopedKind kind)
{
switch (kind)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.CodeAnalysis;
using Semmle.Extraction.CSharp.Entities;

namespace Semmle.Extraction.CSharp
{
Expand All @@ -24,7 +26,7 @@ public virtual void WriteQuotedId(EscapingTextWriter trapFile)
trapFile.WriteUnescaped('\"');
}

public abstract Location? ReportingLocation { get; }
public abstract Microsoft.CodeAnalysis.Location? ReportingLocation { get; }

public abstract TrapStackBehaviour TrapStackBehaviour { get; }

Expand Down Expand Up @@ -65,6 +67,48 @@ public string GetDebugLabel()
}
#endif

protected void PopulateRefKind(TextWriter trapFile, RefKind kind)
{
switch (kind)
{
case RefKind.Out:
trapFile.type_annotation(this, Kinds.TypeAnnotation.Out);
break;
case RefKind.Ref:
trapFile.type_annotation(this, Kinds.TypeAnnotation.Ref);
break;
case RefKind.RefReadOnly:
case RefKind.RefReadOnlyParameter:
trapFile.type_annotation(this, Kinds.TypeAnnotation.ReadonlyRef);
break;
}
}

protected void PopulateNullability(TextWriter trapFile, AnnotatedTypeSymbol type)
{
var n = NullabilityEntity.Create(Context, Nullability.Create(type));
if (!type.HasObliviousNullability())
{
trapFile.type_nullability(this, n);
}
}

protected static void WriteLocationToTrap<T1>(Action<T1, Entities.Location> writeAction, T1 entity, Entities.Location l)
{
if (l is not EmptyLocation)
{
writeAction(entity, l);
}
}

protected static void WriteLocationsToTrap<T1>(Action<T1, Entities.Location> writeAction, T1 entity, IEnumerable<Entities.Location> locations)
{
foreach (var loc in locations)
{
WriteLocationToTrap(writeAction, entity, loc);
}
}

public override string ToString() => Label.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ private Invocation(ExpressionNodeInfo info)

private bool IsExplicitDelegateInvokeCall() => Kind == ExprKind.DELEGATE_INVOCATION && Context.GetModel(Syntax.Expression).GetSymbolInfo(Syntax.Expression).Symbol is IMethodSymbol m && m.MethodKind == MethodKind.DelegateInvoke;

private bool IsOperatorCall() => Kind == ExprKind.OPERATOR_INVOCATION;

private bool IsAccessorInvocation() => Kind == ExprKind.ACCESSOR_INVOCATION;

private bool IsValidMemberAccessKind()
{
return Kind == ExprKind.METHOD_INVOCATION ||
IsEventDelegateCall() ||
IsExplicitDelegateInvokeCall() ||
IsOperatorCall() ||
IsAccessorInvocation();
}

protected override void PopulateExpression(TextWriter trapFile)
{
if (IsNameof(Syntax))
Expand All @@ -37,7 +50,7 @@ protected override void PopulateExpression(TextWriter trapFile)
var target = TargetSymbol;
switch (Syntax.Expression)
{
case MemberAccessExpressionSyntax memberAccess when Kind == ExprKind.METHOD_INVOCATION || IsEventDelegateCall() || IsExplicitDelegateInvokeCall():
case MemberAccessExpressionSyntax memberAccess when IsValidMemberAccessKind():
memberName = memberAccess.Name.Identifier.Text;
if (Syntax.Expression.Kind() == SyntaxKind.SimpleMemberAccessExpression)
// Qualified method call; `x.M()`
Expand Down Expand Up @@ -113,14 +126,39 @@ private static bool IsDynamicCall(ExpressionNodeInfo info)

public SymbolInfo SymbolInfo => info.SymbolInfo;

private static bool IsOperatorLikeCall(ExpressionNodeInfo info)
{
return info.SymbolInfo.Symbol is IMethodSymbol method &&
method.TryGetExtensionMethod(out var original) &&
original!.MethodKind == MethodKind.UserDefinedOperator;
}

private static bool IsAccessorLikeInvocation(ExpressionNodeInfo info)
{
return info.SymbolInfo.Symbol is IMethodSymbol method &&
method.TryGetExtensionMethod(out var original) &&
(original!.MethodKind == MethodKind.PropertyGet ||
original!.MethodKind == MethodKind.PropertySet);
}

public IMethodSymbol? TargetSymbol
{
get
{
var si = SymbolInfo;

if (si.Symbol is not null)
return si.Symbol as IMethodSymbol;
if (si.Symbol is ISymbol symbol)
{
var method = symbol as IMethodSymbol;
// Case for compiler-generated extension methods.
if (method is not null &&
method.TryGetExtensionMethod(out var original))
{
return original;
}

return method;
}

if (si.CandidateReason == CandidateReason.OverloadResolutionFailure)
{
Expand Down Expand Up @@ -196,15 +234,29 @@ private static bool IsLocalFunctionInvocation(ExpressionNodeInfo info)

private static ExprKind GetKind(ExpressionNodeInfo info)
{
return IsNameof((InvocationExpressionSyntax)info.Node)
? ExprKind.NAMEOF
: IsDelegateLikeCall(info)
? IsDelegateInvokeCall(info)
? ExprKind.DELEGATE_INVOCATION
: ExprKind.FUNCTION_POINTER_INVOCATION
: IsLocalFunctionInvocation(info)
? ExprKind.LOCAL_FUNCTION_INVOCATION
: ExprKind.METHOD_INVOCATION;
if (IsNameof((InvocationExpressionSyntax)info.Node))
{
return ExprKind.NAMEOF;
}
if (IsDelegateLikeCall(info))
{
return IsDelegateInvokeCall(info)
? ExprKind.DELEGATE_INVOCATION
: ExprKind.FUNCTION_POINTER_INVOCATION;
}
if (IsLocalFunctionInvocation(info))
{
return ExprKind.LOCAL_FUNCTION_INVOCATION;
}
if (IsOperatorLikeCall(info))
{
return ExprKind.OPERATOR_INVOCATION;
}
if (IsAccessorLikeInvocation(info))
{
return ExprKind.ACCESSOR_INVOCATION;
}
return ExprKind.METHOD_INVOCATION;
}

private static bool IsNameof(InvocationExpressionSyntax syntax)
Expand Down
Loading
Loading