using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Microsoft.CodeAnalysis; using Semmle.Extraction.CSharp.Entities; namespace Semmle.Extraction.CSharp { /// /// An ITypeSymbol with nullability annotations. /// Although a similar class has been implemented in Roslyn, /// https://github.com/dotnet/roslyn/blob/090e52e27c38ad8f1ea4d033114c2a107604ddaa/src/Compilers/CSharp/Portable/Symbols/TypeWithAnnotations.cs /// it is an internal struct that has not yet been exposed on the public interface. /// public struct AnnotatedTypeSymbol { public ITypeSymbol? Symbol { get; set; } public NullableAnnotation Nullability { get; } public AnnotatedTypeSymbol(ITypeSymbol? symbol, NullableAnnotation nullability) { Symbol = symbol; Nullability = nullability; } public static AnnotatedTypeSymbol? CreateNotAnnotated(ITypeSymbol? symbol) => symbol is null ? (AnnotatedTypeSymbol?)null : new AnnotatedTypeSymbol(symbol, NullableAnnotation.None); } internal static class SymbolExtensions { /// /// Tries to recover from an ErrorType. /// /// /// The type to disambiguate. /// public static ITypeSymbol? DisambiguateType(this ITypeSymbol? type) { /* A type could not be determined. * Sometimes this happens due to a missing reference, * or sometimes because the same type is defined in multiple places. * * In the case that a symbol is multiply-defined, Roslyn tells you which * symbols are candidates. It usually resolves to the same DB entity, * so it's reasonably safe to just pick a candidate. * * The conservative option would be to resolve all error types as null. */ return type is IErrorTypeSymbol errorType && errorType.CandidateSymbols.Any() ? errorType.CandidateSymbols.First() as ITypeSymbol : type; } private static IEnumerable GetModifiers(this ISymbol symbol, Func> getModifierTokens) => symbol.DeclaringSyntaxReferences .Select(r => r.GetSyntax()) .OfType() .SelectMany(getModifierTokens); /// /// Gets the source-level modifiers belonging to this symbol, if any. /// public static IEnumerable GetSourceLevelModifiers(this ISymbol symbol) => symbol.GetModifiers(md => md.Modifiers).Select(m => m.Text); /// /// Holds if the ID generated for `dependant` will contain a reference to /// the ID for `symbol`. If this is the case, then the ID for `symbol` must /// not contain a reference back to `dependant`. /// public static bool IdDependsOn(this ITypeSymbol dependant, Context cx, ISymbol symbol) { var seen = new HashSet(SymbolEqualityComparer.Default); bool IdDependsOnImpl(ITypeSymbol? type) { if (SymbolEqualityComparer.Default.Equals(type, symbol)) return true; if (type is null || seen.Contains(type)) return false; seen.Add(type); using (cx.StackGuard) { switch (type.TypeKind) { case TypeKind.Array: var array = (IArrayTypeSymbol)type; return IdDependsOnImpl(array.ElementType); case TypeKind.Class: case TypeKind.Interface: case TypeKind.Struct: case TypeKind.Enum: case TypeKind.Delegate: case TypeKind.Error: var named = (INamedTypeSymbol)type; if (named.IsTupleType && named.TupleUnderlyingType is not null) named = named.TupleUnderlyingType; if (IdDependsOnImpl(named.ContainingType)) return true; if (IdDependsOnImpl(named.ConstructedFrom)) return true; return named.TypeArguments.Any(IdDependsOnImpl); case TypeKind.Pointer: var ptr = (IPointerTypeSymbol)type; return IdDependsOnImpl(ptr.PointedAtType); case TypeKind.TypeParameter: var tp = (ITypeParameterSymbol)type; return tp.ContainingSymbol is ITypeSymbol cont ? IdDependsOnImpl(cont) : SymbolEqualityComparer.Default.Equals(tp.ContainingSymbol, symbol); case TypeKind.FunctionPointer: var funptr = (IFunctionPointerTypeSymbol)type; if (funptr.Signature.Parameters.Any(p => IdDependsOnImpl(p.Type))) { return true; } return IdDependsOnImpl(funptr.Signature.ReturnType); default: return false; } } } return IdDependsOnImpl(dependant); } /// /// Constructs a unique string for this type symbol. /// /// The extraction context. /// The trap builder used to store the result. /// The outer symbol being defined (to avoid recursive ids). /// Whether to build a type ID for the underlying `System.ValueTuple` struct in the case of tuple types. public static void BuildTypeId(this ITypeSymbol type, Context cx, EscapingTextWriter trapFile, ISymbol symbolBeingDefined, bool constructUnderlyingTupleType) { using (cx.StackGuard) { switch (type.TypeKind) { case TypeKind.Array: var array = (IArrayTypeSymbol)type; array.ElementType.BuildOrWriteId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType: false); array.BuildArraySuffix(trapFile); return; case TypeKind.Class: case TypeKind.Interface: case TypeKind.Struct: case TypeKind.Enum: case TypeKind.Delegate: case TypeKind.Error: var named = (INamedTypeSymbol)type; named.BuildNamedTypeId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType); return; case TypeKind.Pointer: var ptr = (IPointerTypeSymbol)type; ptr.PointedAtType.BuildOrWriteId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType: false); trapFile.Write('*'); return; case TypeKind.TypeParameter: var tp = (ITypeParameterSymbol)type; tp.ContainingSymbol.BuildOrWriteId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType: false); trapFile.Write('_'); trapFile.Write(tp.Name); return; case TypeKind.Dynamic: trapFile.Write("dynamic"); return; case TypeKind.FunctionPointer: var funptr = (IFunctionPointerTypeSymbol)type; funptr.BuildFunctionPointerTypeId(cx, trapFile, symbolBeingDefined); return; default: throw new InternalError(type, $"Unhandled type kind '{type.TypeKind}'"); } } } private static void BuildOrWriteId(this ISymbol? symbol, Context cx, EscapingTextWriter trapFile, ISymbol symbolBeingDefined, bool constructUnderlyingTupleType) { if (symbol is null) { cx.ModelError(symbolBeingDefined, "Missing symbol. Couldn't build some part of the ID."); return; } // We need to keep track of the symbol being defined in order to avoid cyclic labels. // For example, in // // ```csharp // class C : IEnumerable { } // ``` // // when we generate the label for ``C`1``, the base class `IEnumerable` has `T` as a type // argument, which will be qualified with `__self__` instead of the label we are defining. // In effect, the label will (simplified) look like // // ``` // #123 = @"C`1 : IEnumerable<__self___T>" // ``` if (SymbolEqualityComparer.Default.Equals(symbol, symbolBeingDefined)) trapFile.Write("__self__"); else if (symbol is ITypeSymbol type && type.IdDependsOn(cx, symbolBeingDefined)) type.BuildTypeId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType); else if (symbol is INamedTypeSymbol namedType && namedType.IsTupleType && constructUnderlyingTupleType) trapFile.WriteSubId(NamedType.CreateNamedTypeFromTupleType(cx, namedType)); else trapFile.WriteSubId(CreateEntity(cx, symbol)); } /// /// Adds an appropriate ID to the trap builder /// for the symbol belonging to /// . /// /// This will either write a reference to the ID of the entity belonging to /// (`{#label}`), or if that will lead to cyclic IDs, /// it will generate an appropriate ID that encodes the signature of /// . /// public static void BuildOrWriteId(this ISymbol? symbol, Context cx, EscapingTextWriter trapFile, ISymbol symbolBeingDefined) => symbol.BuildOrWriteId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType: false); /// /// Constructs an array suffix string for this array type symbol. /// /// The trap builder used to store the result. public static void BuildArraySuffix(this IArrayTypeSymbol array, TextWriter trapFile) { trapFile.Write('['); for (var i = 0; i < array.Rank - 1; i++) trapFile.Write(','); trapFile.Write(']'); } private static void BuildAssembly(IAssemblySymbol asm, EscapingTextWriter trapFile, bool extraPrecise = false) { var assembly = asm.Identity; trapFile.Write(assembly.Name); trapFile.Write('_'); trapFile.Write(assembly.Version.Major); trapFile.Write('.'); trapFile.Write(assembly.Version.Minor); trapFile.Write('.'); trapFile.Write(assembly.Version.Build); if (extraPrecise) { trapFile.Write('.'); trapFile.Write(assembly.Version.Revision); } trapFile.Write("::"); } private static void BuildFunctionPointerTypeId(this IFunctionPointerTypeSymbol funptr, Context cx, EscapingTextWriter trapFile, ISymbol symbolBeingDefined) => BuildFunctionPointerSignature(funptr, trapFile, s => s.BuildOrWriteId(cx, trapFile, symbolBeingDefined)); /// /// Workaround for a Roslyn bug: https://github.com/dotnet/roslyn/issues/53943 /// public static IEnumerable GetTupleElementsMaybeNull(this INamedTypeSymbol type) => type.TupleElements; private static void BuildQualifierAndName(INamedTypeSymbol named, Context cx, EscapingTextWriter trapFile, ISymbol symbolBeingDefined) { if (named.ContainingType is not null) { named.ContainingType.BuildOrWriteId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType: false); trapFile.Write('.'); } else if (named.ContainingNamespace is not null) { if (cx.ShouldAddAssemblyTrapPrefix && named.ContainingAssembly is not null) BuildAssembly(named.ContainingAssembly, trapFile); named.ContainingNamespace.BuildNamespace(cx, trapFile); } var name = named.IsFileLocal ? named.MetadataName : named.Name; trapFile.Write(name); } private static void BuildTupleId(INamedTypeSymbol named, Context cx, EscapingTextWriter trapFile, ISymbol symbolBeingDefined) { trapFile.Write('('); trapFile.BuildList(",", named.GetTupleElementsMaybeNull(), (i, f) => { if (f is null) { trapFile.Write($"null({i})"); } else { trapFile.Write((f.CorrespondingTupleField ?? f).Name); trapFile.Write(":"); f.Type.BuildOrWriteId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType: false); } } ); trapFile.Write(")"); } private static void BuildNamedTypeId(this INamedTypeSymbol named, Context cx, EscapingTextWriter trapFile, ISymbol symbolBeingDefined, bool constructUnderlyingTupleType) { if (!constructUnderlyingTupleType && named.IsTupleType) { BuildTupleId(named, cx, trapFile, symbolBeingDefined); return; } if (named.TypeParameters.IsEmpty) { BuildQualifierAndName(named, cx, trapFile, symbolBeingDefined); } else if (named.IsReallyUnbound()) { BuildQualifierAndName(named, cx, trapFile, symbolBeingDefined); trapFile.Write("`"); trapFile.Write(named.TypeParameters.Length); } else { named.ConstructedFrom.BuildOrWriteId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType); trapFile.Write('<'); // Encode the nullability of the type arguments in the label. // Type arguments with different nullability can result in // a constructed type with different nullability of its members and methods, // so we need to create a distinct database entity for it. trapFile.BuildList(",", named.GetAnnotatedTypeArguments(), ta => ta.Symbol.BuildOrWriteId(cx, trapFile, symbolBeingDefined, constructUnderlyingTupleType: false) ); trapFile.Write('>'); } } private static void BuildNamespace(this INamespaceSymbol ns, Context cx, EscapingTextWriter trapFile) { trapFile.WriteSubId(Namespace.Create(cx, ns)); trapFile.Write('.'); } private static void BuildAnonymousName(this INamedTypeSymbol type, Context cx, TextWriter trapFile) { var memberCount = type.GetMembers().OfType().Count(); var hackTypeNumber = memberCount == 1 ? 1 : 0; trapFile.Write("<>__AnonType"); trapFile.Write(hackTypeNumber); trapFile.Write('<'); trapFile.BuildList(",", type.GetMembers().OfType(), prop => BuildDisplayName(prop.Type, cx, trapFile)); trapFile.Write('>'); } /// /// Constructs a display name string for this type symbol. /// /// The trap builder used to store the result. public static void BuildDisplayName(this ITypeSymbol type, Context cx, TextWriter trapFile, bool constructUnderlyingTupleType = false) { using (cx.StackGuard) { switch (type.TypeKind) { case TypeKind.Array: var array = (IArrayTypeSymbol)type; var elementType = array.ElementType; if (elementType.MetadataName.Contains("`")) { trapFile.Write(TrapExtensions.EncodeString(elementType.Name)); return; } elementType.BuildDisplayName(cx, trapFile); array.BuildArraySuffix(trapFile); return; case TypeKind.Class: case TypeKind.Interface: case TypeKind.Struct: case TypeKind.Enum: case TypeKind.Delegate: case TypeKind.Error: var named = (INamedTypeSymbol)type; named.BuildNamedTypeDisplayName(cx, trapFile, constructUnderlyingTupleType); return; case TypeKind.Pointer: var ptr = (IPointerTypeSymbol)type; ptr.PointedAtType.BuildDisplayName(cx, trapFile); trapFile.Write('*'); return; case TypeKind.FunctionPointer: var funptr = (IFunctionPointerTypeSymbol)type; funptr.BuildFunctionPointerTypeDisplayName(cx, trapFile); return; case TypeKind.TypeParameter: trapFile.Write(type.Name); return; case TypeKind.Dynamic: trapFile.Write("dynamic"); return; default: throw new InternalError(type, $"Unhandled type kind '{type.TypeKind}'"); } } } public static void BuildFunctionPointerSignature(IFunctionPointerTypeSymbol funptr, TextWriter trapFile, Action buildNested) { trapFile.Write("delegate* "); trapFile.Write(funptr.Signature.CallingConvention.ToString().ToLowerInvariant()); if (funptr.Signature.UnmanagedCallingConventionTypes.Any()) { trapFile.Write('['); trapFile.BuildList(",", funptr.Signature.UnmanagedCallingConventionTypes, buildNested); trapFile.Write("]"); } trapFile.Write('<'); trapFile.BuildList(",", funptr.Signature.Parameters, p => { buildNested(p.Type); switch (p.RefKind) { case RefKind.Out: trapFile.Write(" out"); break; case RefKind.In: trapFile.Write(" in"); break; case RefKind.Ref: trapFile.Write(" ref"); break; } }); if (funptr.Signature.Parameters.Any()) { trapFile.Write(","); } buildNested(funptr.Signature.ReturnType); if (funptr.Signature.ReturnsByRef) trapFile.Write(" ref"); if (funptr.Signature.ReturnsByRefReadonly) trapFile.Write(" ref readonly"); trapFile.Write('>'); } private static void BuildFunctionPointerTypeDisplayName(this IFunctionPointerTypeSymbol funptr, Context cx, TextWriter trapFile) => BuildFunctionPointerSignature(funptr, trapFile, s => s.BuildDisplayName(cx, trapFile)); private static void BuildNamedTypeDisplayName(this INamedTypeSymbol namedType, Context cx, TextWriter trapFile, bool constructUnderlyingTupleType) { if (!constructUnderlyingTupleType && namedType.IsTupleType) { trapFile.Write('('); trapFile.BuildList( ",", namedType.GetTupleElementsMaybeNull(), (i, f) => { if (f is null) trapFile.Write($"null({i})"); else f.Type.BuildDisplayName(cx, trapFile); }); trapFile.Write(")"); return; } if (namedType.IsAnonymousType) { namedType.BuildAnonymousName(cx, trapFile); } else { trapFile.Write(TrapExtensions.EncodeString(namedType.Name)); } } public static bool IsReallyUnbound(this INamedTypeSymbol type) => SymbolEqualityComparer.Default.Equals(type.ConstructedFrom, type) || type.IsUnboundGenericType; public static bool IsReallyBound(this INamedTypeSymbol type) => !IsReallyUnbound(type); /// /// Holds if this type is of the form int? or /// System.Nullable<int>. /// public static bool IsBoundNullable(this ITypeSymbol type) => type.SpecialType == SpecialType.None && type.OriginalDefinition.IsUnboundNullable(); /// /// Holds if this type is System.Nullable<T>. /// public static bool IsUnboundNullable(this ITypeSymbol type) => type.SpecialType == SpecialType.System_Nullable_T; /// /// Holds if this type is System.Span<T>. /// public static bool IsUnboundSpan(this ITypeSymbol type) => type.ToString() == "System.Span"; /// /// Holds if this type is of the form System.Span<byte>. /// public static bool IsBoundSpan(this ITypeSymbol type) => type.SpecialType == SpecialType.None && type.OriginalDefinition.IsUnboundSpan(); /// /// Holds if this type is System.ReadOnlySpan<T>. /// public static bool IsUnboundReadOnlySpan(this ITypeSymbol type) => type.ToString() == "System.ReadOnlySpan"; public static bool IsInlineArray(this ITypeSymbol type) { var attributes = type.GetAttributes(); var isInline = attributes.Any(attribute => attribute.AttributeClass is INamedTypeSymbol nt && nt.Name == "InlineArrayAttribute" && nt.ContainingNamespace.ToString() == "System.Runtime.CompilerServices" ); return isInline; } /// /// Holds if this type is of the form System.ReadOnlySpan<byte>. /// public static bool IsBoundReadOnlySpan(this ITypeSymbol type) => type.SpecialType == SpecialType.None && type.OriginalDefinition.IsUnboundReadOnlySpan(); /// /// Gets the parameters of a method or property. /// /// The list of parameters, or an empty list. public static IEnumerable GetParameters(this ISymbol parameterizable) { if (parameterizable is IMethodSymbol meth) return meth.Parameters; if (parameterizable is IPropertySymbol prop) return prop.Parameters; return Enumerable.Empty(); } /// /// Holds if this symbol is defined in a source code file. /// public static bool FromSource(this ISymbol symbol) => symbol.Locations.Any(l => l.IsInSource); /// /// Holds if this symbol is a source declaration. /// public static bool IsSourceDeclaration(this ISymbol symbol) => SymbolEqualityComparer.Default.Equals(symbol, symbol.OriginalDefinition); /// /// Holds if this method is a source declaration. /// public static bool IsSourceDeclaration(this IMethodSymbol method) => IsSourceDeclaration((ISymbol)method) && SymbolEqualityComparer.Default.Equals(method, method.ConstructedFrom) && method.ReducedFrom is null; /// /// Holds if this parameter is a source declaration. /// public static bool IsSourceDeclaration(this IParameterSymbol parameter) { if (parameter.ContainingSymbol is IMethodSymbol method) return method.IsSourceDeclaration(); if (parameter.ContainingSymbol is IPropertySymbol property && property.IsIndexer) return property.IsSourceDeclaration(); return true; } /// /// Gets the base type of `symbol`. Unlike `symbol.BaseType`, this excludes effective base /// types of type parameters as well as `object` base types. /// public static INamedTypeSymbol? GetNonObjectBaseType(this ITypeSymbol symbol, Context cx) => symbol is ITypeParameterSymbol || SymbolEqualityComparer.Default.Equals(symbol.BaseType, cx.Compilation.ObjectType) ? null : symbol.BaseType; [return: NotNullIfNotNull(nameof(symbol))] public static IEntity? CreateEntity(this Context cx, ISymbol symbol) { if (symbol is null) return null; using (cx.StackGuard) { try { var entity = symbol.Accept(new Populators.Symbols(cx)); if (entity is null) { cx.ModelError(symbol, $"Symbol visitor returned null entity on symbol: {symbol}"); } #nullable disable warnings return entity; #nullable restore warnings } catch (Exception ex) // lgtm[cs/catch-of-all-exceptions] { cx.ModelError(symbol, $"Exception processing symbol '{symbol.Kind}' of type '{ex}': {symbol}"); #nullable disable warnings return null; #nullable restore warnings } } } public static TypeInfo GetTypeInfo(this Context cx, Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode node) => cx.GetModel(node).GetTypeInfo(node); public static SymbolInfo GetSymbolInfo(this Context cx, Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode node) => cx.GetModel(node).GetSymbolInfo(node); /// /// Determines the type of a node, or default /// if the type could not be determined. /// /// Extractor context. /// The node to determine. /// The type symbol of the node, or default. public static AnnotatedTypeSymbol GetType(this Context cx, Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode node) { var info = GetTypeInfo(cx, node); return new AnnotatedTypeSymbol(info.Type.DisambiguateType(), info.Nullability.Annotation); } /// /// Gets the annotated type arguments of an INamedTypeSymbol. /// This has not yet been exposed on the public API. /// public static IEnumerable GetAnnotatedTypeArguments(this INamedTypeSymbol symbol) => symbol.TypeArguments.Zip(symbol.TypeArgumentNullableAnnotations, (t, a) => new AnnotatedTypeSymbol(t, a)); /// /// Returns true if the symbol is public, protected or protected internal. /// public static bool IsPublicOrProtected(this ISymbol symbol) => symbol.DeclaredAccessibility == Accessibility.Public || symbol.DeclaredAccessibility == Accessibility.Protected || symbol.DeclaredAccessibility == Accessibility.ProtectedOrInternal; /// /// Returns true if the given symbol should be extracted. /// public static bool ShouldExtractSymbol(this ISymbol symbol) { // Extract all source symbols and public/protected metadata symbols. if (symbol.Locations.Any(x => !x.IsInMetadata) || symbol.IsPublicOrProtected()) { return true; } if (symbol is IMethodSymbol method) { return method.ExplicitInterfaceImplementations.Any(m => m.ContainingType.ShouldExtractSymbol()); } if (symbol is IPropertySymbol property) { return property.ExplicitInterfaceImplementations.Any(m => m.ContainingType.ShouldExtractSymbol()); } return false; } /// /// Returns the symbols that should be extracted. /// public static IEnumerable ExtractionCandidates(this IEnumerable symbols) where T : ISymbol => symbols.Where(symbol => symbol.ShouldExtractSymbol()); } }