diff --git a/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs b/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs index 72f78f160598..e90c3d13891b 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs @@ -164,6 +164,7 @@ public static void BuildTypeId(this ITypeSymbol type, Context cx, EscapingTextWr case TypeKind.Enum: case TypeKind.Delegate: case TypeKind.Error: + case TypeKind.Extension: var named = (INamedTypeSymbol)type; named.BuildNamedTypeId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType); return; @@ -275,6 +276,16 @@ private static void BuildFunctionPointerTypeId(this IFunctionPointerTypeSymbol f public static IEnumerable 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) @@ -289,8 +300,19 @@ private static void BuildQualifierAndName(INamedTypeSymbol named, Context cx, Es 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) @@ -391,6 +413,7 @@ public static void BuildDisplayName(this ITypeSymbol type, Context cx, TextWrite case TypeKind.Enum: case TypeKind.Delegate: case TypeKind.Error: + case TypeKind.Extension: var named = (INamedTypeSymbol)type; named.BuildNamedTypeDisplayName(cx, trapFile, constructUnderlyingTupleType); return; @@ -484,6 +507,13 @@ private static void BuildNamedTypeDisplayName(this INamedTypeSymbol namedType, C return; } + if (namedType.IsExtension) + { + var name = GetExtensionTypeName(namedType, cx); + trapFile.Write(name); + return; + } + if (namedType.IsAnonymousType) { namedType.BuildAnonymousName(cx, trapFile); @@ -596,6 +626,56 @@ public static bool IsSourceDeclaration(this IParameterSymbol parameter) return true; } + /// + /// Return true if this method is a compiler-generated extension method. + /// + public static bool IsCompilerGeneratedExtensionMethod(this IMethodSymbol method) => + method.TryGetExtensionMethod(out _); + + /// + /// Returns true if this method is a compiler-generated extension method, + /// and outputs the original extension method declaration. + /// + 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() + .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() + .FirstOrDefault(m => SymbolEqualityComparer.Default.Equals(m.AssociatedExtensionImplementation, method.ConstructedFrom)); + + var isFullyConstructed = method.IsBoundGenericMethod(); + if (isFullyConstructed && unboundDeclaration?.ContainingType is INamedTypeSymbol extensionType && extensionType.IsGenericType) + { + // Use the type arguments from the constructed extension method to construct the extension type. + var arguments = method.TypeArguments.ToArray(); + var boundExtensionType = extensionType.Construct(arguments); + var boundDeclaration = boundExtensionType.GetMembers() + .OfType() + .FirstOrDefault(c => SymbolEqualityComparer.Default.Equals(c.OriginalDefinition, unboundDeclaration)); + declaration = boundDeclaration; + } + 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); + /// /// Gets the base type of `symbol`. Unlike `symbol.BaseType`, this excludes effective base /// types of type parameters as well as `object` base types. diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/CachedEntity.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/CachedEntity.cs index 2002fe0f1d7a..39d0f886b813 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/CachedEntity.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/CachedEntity.cs @@ -54,22 +54,6 @@ public string DebugContents } } - protected static void WriteLocationToTrap(Action writeAction, T1 entity, Location l) - { - if (l is not EmptyLocation) - { - writeAction(entity, l); - } - } - - protected static void WriteLocationsToTrap(Action writeAction, T1 entity, IEnumerable 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(); diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/CachedSymbol.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/CachedSymbol.cs index 92861e97fdd8..1a69b0e08b2b 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/CachedSymbol.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/CachedSymbol.cs @@ -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) diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/Entity.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/Entity.cs index ca1887b3be9c..94b2e16e9e6b 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/Entity.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Base/Entity.cs @@ -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 { @@ -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; } @@ -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(Action writeAction, T1 entity, Entities.Location l) + { + if (l is not EmptyLocation) + { + writeAction(entity, l); + } + } + + protected static void WriteLocationsToTrap(Action writeAction, T1 entity, IEnumerable locations) + { + foreach (var loc in locations) + { + WriteLocationToTrap(writeAction, entity, loc); + } + } + public override string ToString() => Label.ToString(); } } diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Invocation.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Invocation.cs index a6272974c22b..a2fb9387a7af 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Invocation.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Invocation.cs @@ -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)) @@ -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()` @@ -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) { @@ -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) diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Method.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Method.cs index c92c561f31b6..ecf1e1f0ee93 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Method.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Method.cs @@ -14,9 +14,28 @@ internal abstract class Method : CachedSymbol, IExpressionParentE protected Method(Context cx, IMethodSymbol init) : base(cx, init) { } + private IParameter? SyntheticParameter { get; set; } + + private int SynthesizeExtensionParameter() + { + // Synthesize implicit parameter for extension methods declared using extension(...) syntax. + if (Symbol.ContainingSymbol is INamedTypeSymbol type && + type.IsExtension && type.ExtensionParameter is IParameterSymbol parameter && + !string.IsNullOrEmpty(parameter.Name) && !Symbol.IsStatic) + { + var originalSyntheticParam = OriginalDefinition.SyntheticParameter; + SyntheticParameter = ImplicitExtensionParameter.Create(Context, this, parameter, originalSyntheticParam); + return 1; + } + + return 0; + } + protected void PopulateParameters() { var originalMethod = OriginalDefinition; + var positionOffset = SynthesizeExtensionParameter(); + IEnumerable parameters = Symbol.Parameters; IEnumerable originalParameters = originalMethod.Symbol.Parameters; @@ -24,8 +43,8 @@ protected void PopulateParameters() { var original = SymbolEqualityComparer.Default.Equals(p.paramSymbol, p.originalParam) ? null - : Parameter.Create(Context, p.originalParam, originalMethod); - Parameter.Create(Context, p.paramSymbol, this, original); + : Parameter.Create(Context, p.originalParam, originalMethod, null, positionOffset); + Parameter.Create(Context, p.paramSymbol, this, original, positionOffset); } if (Symbol.IsVararg) @@ -302,9 +321,9 @@ public static void AddExplicitInterfaceQualifierToId(Context cx, EscapingTextWri /// /// Whether this method has unbound type parameters. /// - public bool IsUnboundGeneric => IsGeneric && SymbolEqualityComparer.Default.Equals(Symbol.ConstructedFrom, Symbol); + public bool IsUnboundGeneric => Symbol.IsUnboundGenericMethod(); - public bool IsBoundGeneric => IsGeneric && !IsUnboundGeneric; + public bool IsBoundGeneric => Symbol.IsBoundGenericMethod(); protected IMethodSymbol ConstructedFromSymbol => Symbol.ConstructedFrom; diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/OrdinaryMethod.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/OrdinaryMethod.cs index 22bcd1dce2c8..2fb148358e8c 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/OrdinaryMethod.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/OrdinaryMethod.cs @@ -23,7 +23,11 @@ protected OrdinaryMethod(Context cx, IMethodSymbol init) ? Symbol.ContainingType.GetSymbolLocation() : BodyDeclaringSymbol.GetSymbolLocation(); - public override bool NeedsPopulation => base.NeedsPopulation || IsCompilerGeneratedDelegate(); + public override bool NeedsPopulation => + (base.NeedsPopulation || IsCompilerGeneratedDelegate()) && + // Exclude compiler-generated extension methods. A call to such a method + // is replaced by a call to the defining extension method. + !Symbol.IsCompilerGeneratedExtensionMethod(); public override void Populate(TextWriter trapFile) { diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Parameter.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Parameter.cs index 49ef9a4a6e9a..540de3ad61bd 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Parameter.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Parameter.cs @@ -7,16 +7,25 @@ namespace Semmle.Extraction.CSharp.Entities { - internal class Parameter : CachedSymbol, IExpressionParentEntity + // Marker interface for parameter entities. + public interface IParameter : IEntity { } + internal class Parameter : CachedSymbol, IExpressionParentEntity, IParameter { protected IEntity? Parent { get; set; } protected Parameter Original { get; } + private int PositionOffset { get; set; } - protected Parameter(Context cx, IParameterSymbol init, IEntity? parent, Parameter? original) + private Parameter(Context cx, IParameterSymbol init, IEntity? parent, Parameter? original, int positionOffset) : base(cx, init) { Parent = parent; Original = original ?? this; + PositionOffset = positionOffset; + } + + protected Parameter(Context cx, IParameterSymbol init, IEntity? parent, Parameter? original) + : this(cx, init, parent, original, 0) + { } public override Microsoft.CodeAnalysis.Location ReportingLocation => Symbol.GetSymbolLocation(); @@ -32,7 +41,7 @@ public enum Kind RefReadOnly = 6 } - protected virtual int Ordinal => Symbol.Ordinal; + protected virtual int Ordinal => Symbol.Ordinal + PositionOffset; private Kind ParamKind { @@ -55,23 +64,25 @@ private Kind ParamKind if (Ordinal == 0) { if (Symbol.ContainingSymbol is IMethodSymbol method && method.IsExtensionMethod) + { return Kind.This; + } } return Kind.None; } } } - public static Parameter Create(Context cx, IParameterSymbol param, IEntity parent, Parameter? original = null) + public static Parameter Create(Context cx, IParameterSymbol param, IEntity parent, Parameter? original = null, int positionOffset = 0) { var cachedSymbol = cx.GetPossiblyCachedParameterSymbol(param); - return ParameterFactory.Instance.CreateEntity(cx, cachedSymbol, (cachedSymbol, parent, original)); + return ParameterFactory.Instance.CreateEntity(cx, cachedSymbol, (cachedSymbol, parent, original, positionOffset)); } public static Parameter Create(Context cx, IParameterSymbol param) { var cachedSymbol = cx.GetPossiblyCachedParameterSymbol(param); - return ParameterFactory.Instance.CreateEntity(cx, cachedSymbol, (cachedSymbol, null, null)); + return ParameterFactory.Instance.CreateEntity(cx, cachedSymbol, (cachedSymbol, null, null, 0)); } public override void WriteId(EscapingTextWriter trapFile) @@ -79,6 +90,9 @@ public override void WriteId(EscapingTextWriter trapFile) if (Parent is null) Parent = Method.Create(Context, Symbol.ContainingSymbol as IMethodSymbol); + if (Parent is null && Symbol.ContainingSymbol is INamedTypeSymbol type && type.IsExtension) + Parent = Type.Create(Context, type); + if (Parent is null) throw new InternalError(Symbol, "Couldn't get parent of symbol."); @@ -194,11 +208,11 @@ Symbol.ContainingSymbol is IMethodSymbol ms && return syntax?.Default; } - private class ParameterFactory : CachedEntityFactory<(IParameterSymbol, IEntity?, Parameter?), Parameter> + private class ParameterFactory : CachedEntityFactory<(IParameterSymbol, IEntity?, Parameter?, int), Parameter> { public static ParameterFactory Instance { get; } = new ParameterFactory(); - public override Parameter Create(Context cx, (IParameterSymbol, IEntity?, Parameter?) init) => new Parameter(cx, init.Item1, init.Item2, init.Item3); + public override Parameter Create(Context cx, (IParameterSymbol, IEntity?, Parameter?, int) init) => new Parameter(cx, init.Item1, init.Item2, init.Item3, init.Item4); } public override TrapStackBehaviour TrapStackBehaviour => TrapStackBehaviour.OptionalLabel; @@ -243,6 +257,115 @@ private class VarargsTypeFactory : CachedEntityFactory } } + /// + /// Synthetic parameter for extension methods declared using the extensions syntax. + /// That is, we add a synthetic parameter s to IsValid in the following example: + /// extension(string s) { + /// public bool IsValid() { ... } + /// } + /// + /// Note, that we use the characteristics of the parameter of the associated (compiler generated) extension method + /// to populate the database. + /// + internal class ImplicitExtensionParameter : FreshEntity, IParameter + { + private Method ExtensionMethod { get; } + private IParameterSymbol ExtensionParameter { get; } + private IParameter Original { get; } + + private ImplicitExtensionParameter(Context cx, Method method, IParameterSymbol parameter, IParameter? original) : base(cx) + { + ExtensionMethod = method; + ExtensionParameter = parameter; + Original = original ?? this; + } + + private static int Ordinal => 0; + + private Parameter.Kind ParamKind + { + get + { + switch (ExtensionParameter.RefKind) + { + case RefKind.Ref: + return Parameter.Kind.Ref; + case RefKind.In: + return Parameter.Kind.In; + case RefKind.RefReadOnlyParameter: + return Parameter.Kind.RefReadOnly; + default: + return Parameter.Kind.None; + } + } + } + + private string Name => ExtensionParameter.Name; + + private bool IsSourceDeclaration => ExtensionMethod.Symbol.IsSourceDeclaration(); + + private void PopulateAttributes() + { + // Only extract attributes for source declarations + if (ReferenceEquals(ExtensionParameter, ExtensionParameter.OriginalDefinition)) + Attribute.ExtractAttributes(Context, ExtensionParameter, this); + } + + /// + /// Bind comments to this symbol. + /// Comments are only bound to source declarations. + /// + private void BindComments() + { + if (IsSourceDeclaration && ExtensionParameter.FromSource()) + Context.BindComments(this, ExtensionParameter.Locations.BestOrDefault()); + } + + protected override void Populate(TextWriter trapFile) + { + PopulateAttributes(); + PopulateNullability(trapFile, ExtensionParameter.GetAnnotatedType()); + PopulateRefKind(trapFile, ExtensionParameter.RefKind); + + var type = Type.Create(Context, ExtensionParameter.Type); + trapFile.@params(this, Name, type.TypeRef, Ordinal, ParamKind, ExtensionMethod, Original); + + if (Context.OnlyScaffold) + { + return; + } + + if (Context.ExtractLocation(ExtensionParameter)) + { + var locations = Context.GetLocations(ExtensionParameter); + WriteLocationsToTrap(trapFile.param_location, this, locations); + } + + if (!IsSourceDeclaration || !ExtensionParameter.FromSource()) + return; + + BindComments(); + + if (IsSourceDeclaration) + { + foreach (var syntax in ExtensionParameter.DeclaringSyntaxReferences + .Select(d => d.GetSyntax()) + .OfType() + .Where(s => s.Type is not null)) + { + TypeMention.Create(Context, syntax.Type!, this, type); + } + } + } + + public static ImplicitExtensionParameter Create(Context cx, Method method, IParameterSymbol parameter, IParameter? original) + { + var p = new ImplicitExtensionParameter(cx, method, parameter, original); + p.TryPopulate(); + return p; + } + } + internal class VarargsParam : Parameter { #nullable disable warnings diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Property.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Property.cs index d48d778cb75a..4010142655cd 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Property.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Property.cs @@ -103,6 +103,7 @@ public override void Populate(TextWriter trapFile) trapFile.expr_access(access, this); if (!Symbol.IsStatic) { + // TODO: What about the containing type for extensions? This.CreateImplicit(Context, Symbol.ContainingType, Location, access, -1); } }); diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Types/NamedType.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Types/NamedType.cs index dcf2bffe095f..d89f3db11e64 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Types/NamedType.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Types/NamedType.cs @@ -20,6 +20,8 @@ private NamedType(Context cx, INamedTypeSymbol init, bool constructUnderlyingTup public static NamedType Create(Context cx, INamedTypeSymbol type) => NamedTypeFactory.Instance.CreateEntityFromSymbol(cx, type); + public NamedType OriginalDefinition => Create(Context, Symbol.OriginalDefinition); + /// /// Creates a named type entity from a tuple type. Unlike , this /// will create an entity for the underlying `System.ValueTuple` struct. @@ -90,6 +92,25 @@ public override void Populate(TextWriter trapFile) { trapFile.anonymous_types(this); } + + if (Symbol.IsExtension && Symbol.ExtensionParameter is IParameterSymbol parameter) + { + // For some reason an extension type has a receiver parameter with an empty name + // even when there is no parameter. + if (!string.IsNullOrEmpty(parameter.Name)) + { + var originalType = OriginalDefinition; + // In case the this is constructed generic, we also need to create the unbound parameter. + var originalParameter = SymbolEqualityComparer.Default.Equals(Symbol, originalType.Symbol.ExtensionParameter) || originalType.Symbol.ExtensionParameter is null + ? null + : Parameter.Create(Context, originalType.Symbol.ExtensionParameter, originalType); + Parameter.Create(Context, parameter, this, originalParameter); + } + + // Use the parameter type as the receiver type. + var receiverType = Type.Create(Context, parameter.Type).TypeRef; + trapFile.extension_receiver_type(this, receiverType); + } } private readonly Lazy typeArgumentsLazy; diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Types/Type.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Types/Type.cs index 3e79a8f81018..0f28a1153e22 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Types/Type.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Types/Type.cs @@ -105,6 +105,7 @@ public Kinds.TypeKind GetTypeKind(Context cx, bool constructUnderlyingTupleType) case TypeKind.Pointer: return Kinds.TypeKind.POINTER; case TypeKind.FunctionPointer: return Kinds.TypeKind.FUNCTION_POINTER; case TypeKind.Error: return Kinds.TypeKind.UNKNOWN; + case TypeKind.Extension: return Kinds.TypeKind.EXTENSION; default: cx.ModelError(Symbol, $"Unhandled type kind '{Symbol.TypeKind}'"); return Kinds.TypeKind.UNKNOWN; @@ -366,7 +367,7 @@ private class DelegateTypeParameter : Parameter private DelegateTypeParameter(Context cx, IParameterSymbol init, IEntity parent, Parameter? original) : base(cx, init, parent, original) { } - public static new DelegateTypeParameter Create(Context cx, IParameterSymbol param, IEntity parent, Parameter? original = null) => + public static DelegateTypeParameter Create(Context cx, IParameterSymbol param, IEntity parent, Parameter? original = null) => // We need to use a different cache key than `param` to avoid mixing up // `DelegateTypeParameter`s and `Parameter`s DelegateTypeParameterFactory.Instance.CreateEntity(cx, (typeof(DelegateTypeParameter), new SymbolEqualityWrapper(param)), (param, parent, original)); diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/UserOperator.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/UserOperator.cs index fc9358ffc2dc..b87bf9e10d08 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/UserOperator.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/UserOperator.cs @@ -68,6 +68,7 @@ public override Type ContainingType /// private bool IsImplicitOperator(out ITypeSymbol containingType) { + // TODO: Do we need to handle extension operators here? containingType = Symbol.ContainingType; if (containingType is not null) { diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Kinds/ExprKind.cs b/csharp/extractor/Semmle.Extraction.CSharp/Kinds/ExprKind.cs index 46a694192842..8b7edc1f208e 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Kinds/ExprKind.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Kinds/ExprKind.cs @@ -133,6 +133,7 @@ public enum ExprKind COLLECTION = 136, SPREAD_ELEMENT = 137, INTERPOLATED_STRING_INSERT = 138, + ACCESSOR_INVOCATION = 139, DEFINE_SYMBOL = 999, } } diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Kinds/TypeKind.cs b/csharp/extractor/Semmle.Extraction.CSharp/Kinds/TypeKind.cs index a35f25c7596c..9088a11da61f 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Kinds/TypeKind.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Kinds/TypeKind.cs @@ -38,5 +38,6 @@ public enum TypeKind TUPLE = 32, FUNCTION_POINTER = 33, INLINE_ARRAY = 34, + EXTENSION = 35 } } diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Trap/Tuples.cs b/csharp/extractor/Semmle.Extraction.CSharp/Trap/Tuples.cs index b789eaa2e9c7..1a25da058bd8 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Trap/Tuples.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Trap/Tuples.cs @@ -202,6 +202,9 @@ internal static void exprorstmt_name(this TextWriter trapFile, IEntity expr, str internal static void extend(this TextWriter trapFile, Type type, Type super) => trapFile.WriteTuple("extend", type, super); + internal static void extension_receiver_type(this TextWriter trapFile, Type @extension, Type receiverType) => + trapFile.WriteTuple("extension_receiver_type", extension, receiverType); + internal static void anonymous_types(this TextWriter trapFile, Type type) => trapFile.WriteTuple("anonymous_types", type); @@ -292,10 +295,10 @@ internal static void operators(this TextWriter trapFile, UserOperator method, st internal static void overrides(this TextWriter trapFile, Method overriding, Method overridden) => trapFile.WriteTuple("overrides", overriding, overridden); - internal static void param_location(this TextWriter trapFile, Parameter param, Location location) => + internal static void param_location(this TextWriter trapFile, IParameter param, Location location) => trapFile.WriteTuple("param_location", param, location); - internal static void @params(this TextWriter trapFile, Parameter param, string name, Type type, int child, Parameter.Kind mode, IEntity method, Parameter originalDefinition) => + internal static void @params(this TextWriter trapFile, IParameter param, string name, Type type, int child, Parameter.Kind mode, IEntity method, IParameter originalDefinition) => trapFile.WriteTuple("params", param, name, type, child, (int)mode, method, originalDefinition); internal static void parent_namespace(this TextWriter trapFile, IEntity type, Namespace parent) => diff --git a/csharp/ql/lib/semmle/code/csharp/Callable.qll b/csharp/ql/lib/semmle/code/csharp/Callable.qll index 49a2271b27c8..985cdb8f1b26 100644 --- a/csharp/ql/lib/semmle/code/csharp/Callable.qll +++ b/csharp/ql/lib/semmle/code/csharp/Callable.qll @@ -221,6 +221,23 @@ class Callable extends Parameterizable, ExprOrStmtParent, @callable { /** Gets a `Call` that has this callable as a target. */ Call getACall() { this = result.getTarget() } + + /** Holds if this callable is declared in an extension type. */ + predicate isInExtension() { this.getDeclaringType() instanceof ExtensionType } +} + +/** + * A callable that is declared as an extension. + * + * Either an extension method (`ExtensionMethod`), an extension operator + * (`ExtensionOperator`) or an extension accessor (`ExtensionAccessor`). + */ +abstract class ExtensionCallable extends Callable { + /** Gets the type being extended by this method. */ + pragma[noinline] + Type getExtendedType() { result = this.getDeclaringType().(ExtensionType).getExtendedType() } + + override string getAPrimaryQlClass() { result = "ExtensionCallable" } } /** @@ -267,8 +284,11 @@ class Method extends Callable, Virtualizable, Attributable, @method { override Location getALocation() { method_location(this.getUnboundDeclaration(), result) } + /** Holds if this method is a classic extension method. */ + predicate isClassicExtensionMethod() { this.getParameter(0).hasExtensionMethodModifier() } + /** Holds if this method is an extension method. */ - predicate isExtensionMethod() { this.getParameter(0).hasExtensionMethodModifier() } + predicate isExtensionMethod() { this.isClassicExtensionMethod() or this.isInExtension() } /** Gets the type of the `params` parameter of this method, if any. */ Type getParamsType() { @@ -296,7 +316,17 @@ class Method extends Callable, Virtualizable, Attributable, @method { } /** - * An extension method, for example + * An extension method. + * + * Either a classic extension method (`ClassicExtensionMethod`) or an extension + * type extension method (`ExtensionTypeExtensionMethod`). + */ +abstract class ExtensionMethod extends ExtensionCallable, Method { + override string getAPrimaryQlClass() { result = "ExtensionMethod" } +} + +/** + * An extension method, for example * * ```csharp * static bool IsDefined(this Widget w) { @@ -304,16 +334,28 @@ class Method extends Callable, Virtualizable, Attributable, @method { * } * ``` */ -class ExtensionMethod extends Method { - ExtensionMethod() { this.isExtensionMethod() } - - override predicate isStatic() { any() } +class ClassicExtensionMethod extends ExtensionMethod { + ClassicExtensionMethod() { this.isClassicExtensionMethod() } - /** Gets the type being extended by this method. */ pragma[noinline] - Type getExtendedType() { result = this.getParameter(0).getType() } + override Type getExtendedType() { result = this.getParameter(0).getType() } - override string getAPrimaryQlClass() { result = "ExtensionMethod" } + override predicate isStatic() { any() } +} + +/** + * An extension method declared in an extension type, for example `IsNullOrEmpty` in + * + * ```csharp + * static class MyExtensions { + * extension(string s) { + * public bool IsNullOrEmpty() { ... } + * } + * } + * ``` + */ +class ExtensionTypeExtensionMethod extends ExtensionMethod { + ExtensionTypeExtensionMethod() { this.isInExtension() } } /** @@ -536,6 +578,20 @@ class RecordCloneMethod extends Method { } } +/** + * An extension operator, for example `*` in + * ```csharp + * static class MyExtensions { + * extension(string s) { + * public static string operator *(int s1, string s2) { ... } + * } + * } + * ``` + */ +class ExtensionOperator extends ExtensionCallable, Operator { + ExtensionOperator() { this.isInExtension() } +} + /** * A user-defined unary operator - an operator taking one operand. * diff --git a/csharp/ql/lib/semmle/code/csharp/Member.qll b/csharp/ql/lib/semmle/code/csharp/Member.qll index a196d3b3fc70..d4be2eaae972 100644 --- a/csharp/ql/lib/semmle/code/csharp/Member.qll +++ b/csharp/ql/lib/semmle/code/csharp/Member.qll @@ -469,7 +469,7 @@ class Virtualizable extends Overridable, Member, @virtualizable { /** * A parameterizable declaration. Either a callable (`Callable`), a delegate - * type (`DelegateType`), or an indexer (`Indexer`). + * type (`DelegateType`), an indexer (`Indexer`) or an extension block (`ExtensionType`). */ class Parameterizable extends Declaration, @parameterizable { /** Gets raw parameter `i`, including the `this` parameter at index 0. */ diff --git a/csharp/ql/lib/semmle/code/csharp/Property.qll b/csharp/ql/lib/semmle/code/csharp/Property.qll index e651639b6313..37156929b9cc 100644 --- a/csharp/ql/lib/semmle/code/csharp/Property.qll +++ b/csharp/ql/lib/semmle/code/csharp/Property.qll @@ -260,6 +260,20 @@ class Property extends DeclarationWithGetSetAccessors, @property { override string getAPrimaryQlClass() { result = "Property" } } +/** + * An extension property, for example `FirstChar` in + * ```csharp + * static class MyExtensions { + * extension(string s) { + * public char FirstChar { get { ... } } + * } + * } + * ``` + */ +class ExtensionProperty extends Property { + ExtensionProperty() { this.getDeclaringType() instanceof ExtensionType } +} + /** * An indexer, for example `string this[int i]` on line 2 in * @@ -413,6 +427,21 @@ class Accessor extends Callable, Modifiable, Attributable, Overridable, @callabl override string toString() { result = this.getName() } } +/** + * An extension accessor. Either a getter (`Getter`), a setter (`Setter`) of an + * extension property, for example `get` in + * ```csharp + * static class MyExtensions { + * extension(string s) { + * public char FirstChar { get { ... } } + * } + * } + * ``` + */ +class ExtensionAccessor extends ExtensionCallable, Accessor { + ExtensionAccessor() { this.getDeclaringType() instanceof ExtensionType } +} + /** * A `get` accessor, for example `get { return p; }` in * diff --git a/csharp/ql/lib/semmle/code/csharp/Type.qll b/csharp/ql/lib/semmle/code/csharp/Type.qll index 1efb1aa93bff..12fce3cb7f93 100644 --- a/csharp/ql/lib/semmle/code/csharp/Type.qll +++ b/csharp/ql/lib/semmle/code/csharp/Type.qll @@ -17,7 +17,8 @@ private import semmle.code.csharp.frameworks.system.runtime.CompilerServices * * Either a value or reference type (`ValueOrRefType`), the `void` type (`VoidType`), * a pointer type (`PointerType`), the arglist type (`ArglistType`), an unknown - * type (`UnknownType`), or a type parameter (`TypeParameter`). + * type (`UnknownType`), a type parameter (`TypeParameter`) or + * an extension type (`ExtensionType`). */ class Type extends Member, TypeContainer, @type { /** Gets the name of this type without additional syntax such as `[]` or `*`. */ @@ -1326,3 +1327,34 @@ class TypeMention extends @type_mention { /** Gets the location of this type mention. */ Location getLocation() { type_mention_location(this, result) } } + +/** + * A type extension declaration, for example `extensions(string s) { ... }` in + * ```csharp + * static class MyExtensions { + * extensions(string s) { ... } + * ``` + */ +class ExtensionType extends Parameterizable, @extension_type { + /** + * Gets the receiver parameter of this extension type, if any. + */ + Parameter getReceiverParameter() { result = this.getParameter(0) } + + /** + * Holds if this extension type has a receiver parameter. + */ + predicate hasReceiverParameter() { exists(this.getReceiverParameter()) } + + /** + * Gets the type being extended by this extension type. + */ + Type getExtendedType() { + extension_receiver_type(this, result) + or + not extension_receiver_type(this, any(Type t)) and + extension_receiver_type(this, getTypeRef(result)) + } + + override string getAPrimaryQlClass() { result = "ExtensionType" } +} diff --git a/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll b/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll index eecbc35900aa..ce8391c0f29b 100644 --- a/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll +++ b/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll @@ -479,6 +479,29 @@ class OperatorCall extends Call, LateBindableExpr, @operator_invocation_expr { override string getAPrimaryQlClass() { result = "OperatorCall" } } +/** + * A call to an extension operator, for example `3 * s` on + * line 9 in + * ```csharp + * static class MyExtensions { + * extension(string s) { + * public static string operator *(int i, string s) { ... } + * } + * } + * + * class A { + * string M(string s) { + * return 3 * s; + * } + * } + * ``` + */ +class ExtensionOperatorCall extends OperatorCall { + ExtensionOperatorCall() { this.getTarget() instanceof ExtensionOperator } + + override string getAPrimaryQlClass() { result = "ExtensionOperatorCall" } +} + /** * A call to a user-defined mutator operator, for example `a++` on * line 7 in @@ -577,8 +600,8 @@ class FunctionPointerCall extends DelegateLikeCall, @function_pointer_invocation /** * A call to an accessor. Either a property accessor call (`PropertyCall`), - * an indexer accessor call (`IndexerCall`), or an event accessor call - * (`EventCall`). + * an indexer accessor call (`IndexerCall`), an event accessor call + * (`EventCall`) or an extension accessor call (`ExtensionAccessorCall`). */ class AccessorCall extends Call, QualifiableExpr, @call_access_expr { override Accessor getTarget() { none() } @@ -588,6 +611,35 @@ class AccessorCall extends Call, QualifiableExpr, @call_access_expr { override Accessor getARuntimeTarget() { result = Call.super.getARuntimeTarget() } } +/** + * A direct call to an extension accessor, for example `MyExtensions.get_FirstChar(s)` + * in + * ```csharp + * static class MyExtensions { + * extension(string s) { + * public char FirstChar { get { ... } } + * } + * } + * + * class A { + * char M(string s) { + * return MyExtensions.get_FirstChar(s); + * } + * } + * ``` + */ +class ExtensionAccessorCall extends AccessorCall, @accessor_invocation_expr { + override Accessor getTarget() { expr_call(this, result) } + + override Expr getArgument(int i) { result = this.getChild(i) and i >= 0 } + + override string toString() { + result = "call to extension accessor " + concat(this.getTarget().getName()) + } + + override string getAPrimaryQlClass() { result = "ExtensionAccessorCall" } +} + /** * A call to a property accessor, for example the call to `get_P` on * line 5 in @@ -658,6 +710,29 @@ class IndexerCall extends AccessorCall, IndexerAccessExpr { override string getAPrimaryQlClass() { result = "IndexerCall" } } +/** + * A call to an extension property accessor (via the property), for example + * `s.FirstChar` on line 9 in + * ```csharp + * static class MyExtensions { + * extension(string s) { + * public char FirstChar { get { ... } } + * } + * } + * + * class A { + * char M(string s) { + * return s.FirstChar; + * } + * } + * ``` + */ +class ExtensionPropertyCall extends PropertyCall { + ExtensionPropertyCall() { this.getProperty() instanceof ExtensionProperty } + + override string getAPrimaryQlClass() { result = "ExtensionPropertyCall" } +} + /** * A call to an event accessor, for example the call to `add_Click` * (defined on line 5) on line 12 in diff --git a/csharp/ql/lib/semmlecode.csharp.dbscheme b/csharp/ql/lib/semmlecode.csharp.dbscheme index 68b5aec54e50..ec6a1dfb9f69 100644 --- a/csharp/ql/lib/semmlecode.csharp.dbscheme +++ b/csharp/ql/lib/semmlecode.csharp.dbscheme @@ -222,7 +222,7 @@ overlayChangedFiles( | @using_directive | @type_parameter_constraints | @externalDataElement | @xmllocatable | @asp_element | @namespace | @preprocessor_directive; -@declaration = @callable | @generic | @assignable | @namespace; +@declaration = @callable | @generic | @assignable | @namespace | @extension_type; @named_element = @namespace | @declaration; @@ -492,6 +492,7 @@ case @type.kind of | 32 = @tuple_type | 33 = @function_pointer_type | 34 = @inline_array_type +| 35 = @extension_type ; @simple_type = @bool_type | @char_type | @integral_type | @floating_point_type | @decimal_type; @@ -502,7 +503,7 @@ case @type.kind of @value_type = @simple_type | @enum_type | @struct_type | @nullable_type | @int_ptr_type | @uint_ptr_type | @tuple_type | @void_type | @inline_array_type; @ref_type = @class_type | @interface_type | @array_type | @delegate_type | @null_type - | @dynamic_type; + | @dynamic_type | @extension_type; @value_or_ref_type = @value_type | @ref_type; typerefs( @@ -541,6 +542,10 @@ function_pointer_return_type( unique int function_pointer_id: @function_pointer_type ref, int return_type_id: @type_or_ref ref); +extension_receiver_type( + unique int extension: @extension_type ref, + int receiver_type_id: @type_or_ref ref); + extend( int sub: @type ref, int super: @type_or_ref ref); @@ -903,7 +908,7 @@ localvar_location( unique int id: @local_variable ref, int loc: @location ref); -@parameterizable = @callable | @delegate_type | @indexer | @function_pointer_type; +@parameterizable = @callable | @delegate_type | @indexer | @function_pointer_type | @extension_type; #keyset[name, parent_id] #keyset[index, parent_id] @@ -1193,6 +1198,8 @@ case @expr.kind of | 136 = @collection_expr | 137 = @spread_element_expr | 138 = @interpolated_string_insert_expr +/* C# 14.0 */ +| 139 = @accessor_invocation_expr /* Preprocessor */ | 999 = @define_symbol_expr ; @@ -1267,9 +1274,9 @@ case @expr.kind of @call = @method_invocation_expr | @constructor_init_expr | @operator_invocation_expr | @delegate_invocation_expr | @object_creation_expr | @call_access_expr - | @local_function_invocation_expr | @function_pointer_invocation_expr; + | @local_function_invocation_expr | @function_pointer_invocation_expr | @accessor_invocation_expr; -@call_access_expr = @property_access_expr | @event_access_expr | @indexer_access_expr; +@call_access_expr = @property_access_expr | @event_access_expr | @indexer_access_expr | @accessor_invocation_expr; @late_bindable_expr = @dynamic_element_access_expr | @dynamic_member_access_expr | @object_creation_expr | @method_invocation_expr | @operator_invocation_expr; @@ -1318,7 +1325,8 @@ dynamic_member_name( @qualifiable_expr = @member_access_expr | @method_invocation_expr - | @element_access_expr; + | @element_access_expr + | @accessor_invocation_expr; conditional_access( unique int id: @qualifiable_expr ref); diff --git a/csharp/ql/test/library-tests/extension/extensionType.expected b/csharp/ql/test/library-tests/extension/extensionType.expected new file mode 100644 index 000000000000..1714a5db2524 --- /dev/null +++ b/csharp/ql/test/library-tests/extension/extensionType.expected @@ -0,0 +1,11 @@ +extensionTypeReceiverParameter +| extensions.cs:6:5:16:5 | extension(String) | extensions.cs:6:22:6:22 | s | +| extensions.cs:25:5:34:5 | extension(Object) | extensions.cs:25:20:25:20 | t | +| extensions.cs:25:5:34:5 | extension(String) | extensions.cs:25:20:25:20 | t | +| extensions.cs:25:5:34:5 | extension(T)`1 | extensions.cs:25:20:25:20 | t | +extensionTypeExtendedType +| extensions.cs:6:5:16:5 | extension(String) | string | +| extensions.cs:18:5:23:5 | extension(Object) | object | +| extensions.cs:25:5:34:5 | extension(Object) | object | +| extensions.cs:25:5:34:5 | extension(String) | string | +| extensions.cs:25:5:34:5 | extension(T)`1 | T | diff --git a/csharp/ql/test/library-tests/extension/extensionType.ql b/csharp/ql/test/library-tests/extension/extensionType.ql new file mode 100644 index 000000000000..c584252c4bd3 --- /dev/null +++ b/csharp/ql/test/library-tests/extension/extensionType.ql @@ -0,0 +1,11 @@ +import csharp + +query predicate extensionTypeReceiverParameter(ExtensionType et, Parameter p) { + et.getFile().getBaseName() = "extensions.cs" and + p = et.getReceiverParameter() +} + +query predicate extensionTypeExtendedType(ExtensionType et, string t) { + et.getFile().getBaseName() = "extensions.cs" and + t = et.getExtendedType().toStringWithTypes() +} diff --git a/csharp/ql/test/library-tests/extension/extensions.cs b/csharp/ql/test/library-tests/extension/extensions.cs new file mode 100644 index 000000000000..37c648fd2c0f --- /dev/null +++ b/csharp/ql/test/library-tests/extension/extensions.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; + +public static class MyExtensions +{ + extension(string s) + { + public bool Prop1 => s.Length > 0; + public bool Prop2 { get { return true; } set { } } + public static bool StaticProp1 { get { return false; } } + public bool M1() => s is not null; + public string M2(string other) { return s + other; } + public static int StaticM1() { return 0; } + public static int StaticM2(string x) { return x.Length; } + public static string operator *(int a, string b) { return ""; } + } + + extension(object) + { + public static int StaticObjectM1() { return 0; } + public static int StaticObjectM2(string s) { return s.Length; } + public static bool StaticProp => true; + } + + extension(T t) where T : class + { + public bool GenericProp1 => t is not null; + public bool GenericProp2 { get { return true; } set { } } + public bool GenericM1() => t is not null; + public void GenericM2(S other) { } + public void GenericStaticM1() { } + public static void GenericStaticM2(S other) { } + public static T operator +(T a, T b) { return null; } + } +} + +public static class ClassicExtensions +{ + public static bool M3(this string s) => s is not null; +} + +public class C +{ + public static void CallingExtensions() + { + var s = "Hello World."; + + // Calling the extensions properties + var x11 = s.Prop1; + var x12 = s.Prop2; + s.Prop2 = true; + var x13 = string.StaticProp1; + var x14 = object.StaticProp; + + // Calling the extensions methods. + var x21 = s.M1(); + var x22 = s.M2("!!!"); + var x23 = string.StaticM1(); + var x24 = string.StaticM2(s); + var x25 = object.StaticObjectM1(); + var x26 = object.StaticObjectM2(s); + + // Calling the extension operator. + var x30 = 3 * s; + + // Calling the classic extension method. + var y = s.M3(); + + // Calling the compiler generated static extension methods. + MyExtensions.M1(s); + MyExtensions.M2(s, "!!!"); + MyExtensions.StaticM1(); + MyExtensions.StaticM2(s); + MyExtensions.StaticObjectM1(); + MyExtensions.StaticObjectM2(s); + + // Calling the compiler generated operator method. + MyExtensions.op_Multiply(3, s); + + // Calling the compiler generated methods used by the extension property accessors. + MyExtensions.get_Prop1(s); + MyExtensions.get_Prop2(s); + MyExtensions.set_Prop2(s, false); + MyExtensions.get_StaticProp(); + } + + public static void CallingGenericExtensions() + { + var s = "Hello Generic World."; + var o = new object(); + + // Calling generic extension method + o.GenericM1(); + s.GenericM1(); + + // Calling the compiler generated static extension methods. + MyExtensions.GenericM1(o); + MyExtensions.GenericM1(s); + } +} diff --git a/csharp/ql/test/library-tests/extension/extensions.expected b/csharp/ql/test/library-tests/extension/extensions.expected new file mode 100644 index 000000000000..6582e1953fe9 --- /dev/null +++ b/csharp/ql/test/library-tests/extension/extensions.expected @@ -0,0 +1,86 @@ +extensionMethodCallArgument +| extensions.cs:56:19:56:24 | call to method M1 | extensions.cs:11:21:11:22 | M1 | extensions.cs:6:22:6:22 | s | 0 | extensions.cs:56:19:56:19 | access to local variable s | +| extensions.cs:57:19:57:29 | call to method M2 | extensions.cs:12:23:12:24 | M2 | extensions.cs:6:22:6:22 | s | 0 | extensions.cs:57:19:57:19 | access to local variable s | +| extensions.cs:57:19:57:29 | call to method M2 | extensions.cs:12:23:12:24 | M2 | extensions.cs:12:33:12:37 | other | 1 | extensions.cs:57:24:57:28 | "!!!" | +| extensions.cs:59:19:59:36 | call to method StaticM2 | extensions.cs:14:27:14:34 | StaticM2 | extensions.cs:14:43:14:43 | x | 0 | extensions.cs:59:35:59:35 | access to local variable s | +| extensions.cs:61:19:61:42 | call to method StaticObjectM2 | extensions.cs:21:27:21:40 | StaticObjectM2 | extensions.cs:21:49:21:49 | s | 0 | extensions.cs:61:41:61:41 | access to local variable s | +| extensions.cs:67:17:67:22 | call to method M3 | extensions.cs:39:24:39:25 | M3 | extensions.cs:39:39:39:39 | s | 0 | extensions.cs:67:17:67:17 | access to local variable s | +| extensions.cs:70:9:70:26 | call to method M1 | extensions.cs:11:21:11:22 | M1 | extensions.cs:6:22:6:22 | s | 0 | extensions.cs:70:25:70:25 | access to local variable s | +| extensions.cs:71:9:71:33 | call to method M2 | extensions.cs:12:23:12:24 | M2 | extensions.cs:6:22:6:22 | s | 0 | extensions.cs:71:25:71:25 | access to local variable s | +| extensions.cs:71:9:71:33 | call to method M2 | extensions.cs:12:23:12:24 | M2 | extensions.cs:12:33:12:37 | other | 1 | extensions.cs:71:28:71:32 | "!!!" | +| extensions.cs:73:9:73:32 | call to method StaticM2 | extensions.cs:14:27:14:34 | StaticM2 | extensions.cs:14:43:14:43 | x | 0 | extensions.cs:73:31:73:31 | access to local variable s | +| extensions.cs:75:9:75:38 | call to method StaticObjectM2 | extensions.cs:21:27:21:40 | StaticObjectM2 | extensions.cs:21:49:21:49 | s | 0 | extensions.cs:75:37:75:37 | access to local variable s | +| extensions.cs:93:9:93:21 | call to method GenericM1 | extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:20:25:20 | t | 0 | extensions.cs:93:9:93:9 | access to local variable o | +| extensions.cs:94:9:94:21 | call to method GenericM1 | extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:20:25:20 | t | 0 | extensions.cs:94:9:94:9 | access to local variable s | +| extensions.cs:97:9:97:33 | call to method GenericM1 | extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:20:25:20 | t | 0 | extensions.cs:97:32:97:32 | access to local variable o | +| extensions.cs:98:9:98:33 | call to method GenericM1 | extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:20:25:20 | t | 0 | extensions.cs:98:32:98:32 | access to local variable s | +extensionMethodCalls +| extensions.cs:56:19:56:24 | call to method M1 | extensions.cs:11:21:11:22 | M1 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).M1 | +| extensions.cs:57:19:57:29 | call to method M2 | extensions.cs:12:23:12:24 | M2 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).M2 | +| extensions.cs:58:19:58:35 | call to method StaticM1 | extensions.cs:13:27:13:34 | StaticM1 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).StaticM1 | +| extensions.cs:59:19:59:36 | call to method StaticM2 | extensions.cs:14:27:14:34 | StaticM2 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).StaticM2 | +| extensions.cs:60:19:60:41 | call to method StaticObjectM1 | extensions.cs:20:27:20:40 | StaticObjectM1 | extensions.cs:18:5:23:5 | extension(Object) | MyExtensions+extension(Object).StaticObjectM1 | +| extensions.cs:61:19:61:42 | call to method StaticObjectM2 | extensions.cs:21:27:21:40 | StaticObjectM2 | extensions.cs:18:5:23:5 | extension(Object) | MyExtensions+extension(Object).StaticObjectM2 | +| extensions.cs:67:17:67:22 | call to method M3 | extensions.cs:39:24:39:25 | M3 | extensions.cs:37:21:37:37 | ClassicExtensions | ClassicExtensions.M3 | +| extensions.cs:70:9:70:26 | call to method M1 | extensions.cs:11:21:11:22 | M1 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).M1 | +| extensions.cs:71:9:71:33 | call to method M2 | extensions.cs:12:23:12:24 | M2 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).M2 | +| extensions.cs:72:9:72:31 | call to method StaticM1 | extensions.cs:13:27:13:34 | StaticM1 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).StaticM1 | +| extensions.cs:73:9:73:32 | call to method StaticM2 | extensions.cs:14:27:14:34 | StaticM2 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).StaticM2 | +| extensions.cs:74:9:74:37 | call to method StaticObjectM1 | extensions.cs:20:27:20:40 | StaticObjectM1 | extensions.cs:18:5:23:5 | extension(Object) | MyExtensions+extension(Object).StaticObjectM1 | +| extensions.cs:75:9:75:38 | call to method StaticObjectM2 | extensions.cs:21:27:21:40 | StaticObjectM2 | extensions.cs:18:5:23:5 | extension(Object) | MyExtensions+extension(Object).StaticObjectM2 | +| extensions.cs:93:9:93:21 | call to method GenericM1 | extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:5:34:5 | extension(Object) | MyExtensions+extension(Object).GenericM1 | +| extensions.cs:94:9:94:21 | call to method GenericM1 | extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:5:34:5 | extension(String) | MyExtensions+extension(String).GenericM1 | +| extensions.cs:97:9:97:33 | call to method GenericM1 | extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:5:34:5 | extension(Object) | MyExtensions+extension(Object).GenericM1 | +| extensions.cs:98:9:98:33 | call to method GenericM1 | extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:5:34:5 | extension(String) | MyExtensions+extension(String).GenericM1 | +extensionParameter +| extensions.cs:11:21:11:22 | M1 | extensions.cs:6:22:6:22 | s | 0 | string | extensions.cs:6:22:6:22 | s | +| extensions.cs:12:23:12:24 | M2 | extensions.cs:6:22:6:22 | s | 0 | string | extensions.cs:6:22:6:22 | s | +| extensions.cs:12:23:12:24 | M2 | extensions.cs:12:33:12:37 | other | 1 | string | extensions.cs:12:33:12:37 | other | +| extensions.cs:14:27:14:34 | StaticM2 | extensions.cs:14:43:14:43 | x | 0 | string | extensions.cs:14:43:14:43 | x | +| extensions.cs:21:27:21:40 | StaticObjectM2 | extensions.cs:21:49:21:49 | s | 0 | string | extensions.cs:21:49:21:49 | s | +| extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:20:25:20 | t | 0 | T | extensions.cs:25:20:25:20 | t | +| extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:20:25:20 | t | 0 | object | extensions.cs:25:20:25:20 | t | +| extensions.cs:29:21:29:29 | GenericM1 | extensions.cs:25:20:25:20 | t | 0 | string | extensions.cs:25:20:25:20 | t | +| extensions.cs:30:21:30:32 | GenericM2`1 | extensions.cs:25:20:25:20 | t | 0 | T | extensions.cs:25:20:25:20 | t | +| extensions.cs:30:21:30:32 | GenericM2`1 | extensions.cs:25:20:25:20 | t | 0 | object | extensions.cs:25:20:25:20 | t | +| extensions.cs:30:21:30:32 | GenericM2`1 | extensions.cs:25:20:25:20 | t | 0 | string | extensions.cs:25:20:25:20 | t | +| extensions.cs:30:21:30:32 | GenericM2`1 | extensions.cs:30:36:30:40 | other | 1 | S | extensions.cs:30:36:30:40 | other | +| extensions.cs:30:21:30:32 | GenericM2`1 | extensions.cs:30:36:30:40 | other | 1 | S | extensions.cs:30:36:30:40 | other | +| extensions.cs:30:21:30:32 | GenericM2`1 | extensions.cs:30:36:30:40 | other | 1 | S | extensions.cs:30:36:30:40 | other | +| extensions.cs:31:21:31:35 | GenericStaticM1 | extensions.cs:25:20:25:20 | t | 0 | T | extensions.cs:25:20:25:20 | t | +| extensions.cs:31:21:31:35 | GenericStaticM1 | extensions.cs:25:20:25:20 | t | 0 | object | extensions.cs:25:20:25:20 | t | +| extensions.cs:31:21:31:35 | GenericStaticM1 | extensions.cs:25:20:25:20 | t | 0 | string | extensions.cs:25:20:25:20 | t | +| extensions.cs:32:28:32:45 | GenericStaticM2`1 | extensions.cs:32:49:32:53 | other | 0 | S | extensions.cs:32:49:32:53 | other | +| extensions.cs:32:28:32:45 | GenericStaticM2`1 | extensions.cs:32:49:32:53 | other | 0 | S | extensions.cs:32:49:32:53 | other | +| extensions.cs:32:28:32:45 | GenericStaticM2`1 | extensions.cs:32:49:32:53 | other | 0 | S | extensions.cs:32:49:32:53 | other | +| extensions.cs:39:24:39:25 | M3 | extensions.cs:39:39:39:39 | s | 0 | string | extensions.cs:39:39:39:39 | s | +extensionOperatorCallArgument +| extensions.cs:15:39:15:39 | * | extensions.cs:64:19:64:23 | call to operator * | extensions.cs:15:45:15:45 | a | 0 | extensions.cs:64:19:64:19 | 3 | +| extensions.cs:15:39:15:39 | * | extensions.cs:64:19:64:23 | call to operator * | extensions.cs:15:55:15:55 | b | 1 | extensions.cs:64:23:64:23 | access to local variable s | +| extensions.cs:15:39:15:39 | * | extensions.cs:78:9:78:38 | call to operator * | extensions.cs:15:45:15:45 | a | 0 | extensions.cs:78:34:78:34 | 3 | +| extensions.cs:15:39:15:39 | * | extensions.cs:78:9:78:38 | call to operator * | extensions.cs:15:55:15:55 | b | 1 | extensions.cs:78:37:78:37 | access to local variable s | +extensionOperatorCalls +| extensions.cs:64:19:64:23 | call to operator * | extensions.cs:15:39:15:39 | * | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).op_Multiply | +| extensions.cs:78:9:78:38 | call to operator * | extensions.cs:15:39:15:39 | * | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).op_Multiply | +extensionProperty +| extensions.cs:8:21:8:25 | Prop1 | extensions.cs:6:5:16:5 | extension(String) | +| extensions.cs:9:21:9:25 | Prop2 | extensions.cs:6:5:16:5 | extension(String) | +| extensions.cs:10:28:10:38 | StaticProp1 | extensions.cs:6:5:16:5 | extension(String) | +| extensions.cs:22:28:22:37 | StaticProp | extensions.cs:18:5:23:5 | extension(Object) | +| extensions.cs:27:21:27:32 | GenericProp1 | extensions.cs:25:5:34:5 | extension(Object) | +| extensions.cs:27:21:27:32 | GenericProp1 | extensions.cs:25:5:34:5 | extension(String) | +| extensions.cs:27:21:27:32 | GenericProp1 | extensions.cs:25:5:34:5 | extension(T)`1 | +| extensions.cs:28:21:28:32 | GenericProp2 | extensions.cs:25:5:34:5 | extension(Object) | +| extensions.cs:28:21:28:32 | GenericProp2 | extensions.cs:25:5:34:5 | extension(String) | +| extensions.cs:28:21:28:32 | GenericProp2 | extensions.cs:25:5:34:5 | extension(T)`1 | +extensionPropertyCall +| extensions.cs:49:19:49:25 | access to property Prop1 | extensions.cs:8:21:8:25 | Prop1 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).Prop1 | +| extensions.cs:50:19:50:25 | access to property Prop2 | extensions.cs:9:21:9:25 | Prop2 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).Prop2 | +| extensions.cs:51:9:51:15 | access to property Prop2 | extensions.cs:9:21:9:25 | Prop2 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).Prop2 | +| extensions.cs:52:19:52:36 | access to property StaticProp1 | extensions.cs:10:28:10:38 | StaticProp1 | extensions.cs:6:5:16:5 | extension(String) | MyExtensions+extension(String).StaticProp1 | +| extensions.cs:53:19:53:35 | access to property StaticProp | extensions.cs:22:28:22:37 | StaticProp | extensions.cs:18:5:23:5 | extension(Object) | MyExtensions+extension(Object).StaticProp | +extensionAccessorCall +| extensions.cs:81:9:81:33 | call to extension accessor get_Prop1 | extensions.cs:8:30:8:41 | get_Prop1 | extensions.cs:8:21:8:25 | Prop1 | MyExtensions+extension(String).get_Prop1 | +| extensions.cs:82:9:82:33 | call to extension accessor get_Prop2 | extensions.cs:9:29:9:31 | get_Prop2 | extensions.cs:9:21:9:25 | Prop2 | MyExtensions+extension(String).get_Prop2 | +| extensions.cs:83:9:83:40 | call to extension accessor set_Prop2 | extensions.cs:9:50:9:52 | set_Prop2 | extensions.cs:9:21:9:25 | Prop2 | MyExtensions+extension(String).set_Prop2 | +| extensions.cs:84:9:84:37 | call to extension accessor get_StaticProp | extensions.cs:22:42:22:45 | get_StaticProp | extensions.cs:22:28:22:37 | StaticProp | MyExtensions+extension(Object).get_StaticProp | diff --git a/csharp/ql/test/library-tests/extension/extensions.ql b/csharp/ql/test/library-tests/extension/extensions.ql new file mode 100644 index 000000000000..23d01d4848c3 --- /dev/null +++ b/csharp/ql/test/library-tests/extension/extensions.ql @@ -0,0 +1,69 @@ +import csharp + +query predicate extensionMethodCallArgument( + ExtensionMethodCall emc, ExtensionMethod em, Parameter p, int i, Expr e +) { + em.getFile().getBaseName() = "extensions.cs" and + emc.getTarget() = em and + em.getParameter(i) = p and + emc.getArgument(i) = e +} + +query predicate extensionMethodCalls( + ExtensionMethodCall emc, ExtensionMethod em, Type t, string type +) { + em.getFile().getBaseName() = "extensions.cs" and + emc.getTarget() = em and + em.getDeclaringType() = t and + em.getFullyQualifiedNameDebug() = type +} + +query predicate extensionParameter( + ExtensionMethod em, Parameter p, int i, string type, Parameter unbound +) { + em.getFile().getBaseName() = "extensions.cs" and + p = em.getParameter(i) and + type = p.getType().toStringWithTypes() and + unbound = p.getUnboundDeclaration() +} + +query predicate extensionOperatorCallArgument( + ExtensionOperator op, ExtensionOperatorCall opc, Parameter p, int pos, Expr e +) { + opc.getTarget() = op and + op.getFile().getBaseName() = "extensions.cs" and + p = op.getParameter(pos) and + e = opc.getArgument(pos) +} + +query predicate extensionOperatorCalls( + ExtensionOperatorCall opc, ExtensionOperator op, Type t, string type +) { + op.getFile().getBaseName() = "extensions.cs" and + opc.getTarget() = op and + op.getDeclaringType() = t and + op.getFullyQualifiedNameDebug() = type +} + +query predicate extensionProperty(ExtensionProperty p, Type t) { + p.getFile().getBaseName() = "extensions.cs" and + p.getDeclaringType() = t +} + +query predicate extensionPropertyCall( + ExtensionPropertyCall pc, ExtensionProperty p, Type t, string type +) { + p.getFile().getBaseName() = "extensions.cs" and + pc.getProperty() = p and + p.getDeclaringType() = t and + p.getFullyQualifiedNameDebug() = type +} + +query predicate extensionAccessorCall( + ExtensionAccessorCall ac, ExtensionAccessor a, ExtensionProperty p, string type +) { + p.getFile().getBaseName() = "extensions.cs" and + (a.(Getter).getDeclaration() = p or a.(Setter).getDeclaration() = p) and + ac.getTarget() = a and + a.getFullyQualifiedNameDebug() = type +} diff --git a/csharp/ql/test/library-tests/extension/options b/csharp/ql/test/library-tests/extension/options new file mode 100644 index 000000000000..77b22963f5c8 --- /dev/null +++ b/csharp/ql/test/library-tests/extension/options @@ -0,0 +1,2 @@ +semmle-extractor-options: /nostdlib /noconfig +semmle-extractor-options: --load-sources-from-project:${testdir}/../../resources/stubs/_frameworks/Microsoft.NETCore.App/Microsoft.NETCore.App.csproj