using Microsoft.CodeAnalysis; using Semmle.Extraction.CSharp.Entities; using System; using System.Collections.Generic; using System.Linq; namespace Semmle.Extraction.CSharp { /// /// An ITypeSymbol with nullability annotations. /// Although a similar class has been implemented in Rolsyn, /// 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; public NullableAnnotation Nullability; public AnnotatedTypeSymbol(ITypeSymbol symbol, NullableAnnotation nullability) { Symbol = symbol; Nullability = nullability; } } 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. */ var errorType = type as IErrorTypeSymbol; return errorType != null && errorType.CandidateSymbols.Any() ? errorType.CandidateSymbols.First() as ITypeSymbol : type; } /// /// Gets the name of this symbol. /// /// If the symbol implements an explicit interface, only the /// name of the member being implemented is included, not the /// explicit prefix. /// public static string GetName(this ISymbol symbol, bool useMetadataName = false) { var name = useMetadataName ? symbol.MetadataName : symbol.Name; return symbol.CanBeReferencedByName ? name : name.Substring(symbol.Name.LastIndexOf('.') + 1); } /// /// Gets the source-level modifiers belonging to this symbol, if any. /// public static IEnumerable GetSourceLevelModifiers(this ISymbol symbol) { var methodModifiers = symbol.DeclaringSyntaxReferences. Select(r => r.GetSyntax()). OfType(). SelectMany(md => md.Modifiers); var typeModifers = symbol.DeclaringSyntaxReferences. Select(r => r.GetSyntax()). OfType(). SelectMany(cd => cd.Modifiers); return methodModifiers.Concat(typeModifers).Select(m => m.Text); } /// /// Holds if this type symbol contains a type parameter from the /// declaring generic . /// public static bool ContainsTypeParameters(this ITypeSymbol type, Context cx, ISymbol declaringGeneric) { using (cx.StackGuard) { switch (type.TypeKind) { case TypeKind.Array: var array = (IArrayTypeSymbol)type; return array.ElementType.ContainsTypeParameters(cx, declaringGeneric); 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 = named.TupleUnderlyingType; if (named.ContainingType != null && named.ContainingType.ContainsTypeParameters(cx, declaringGeneric)) return true; return named.TypeArguments.Any(arg => arg.ContainsTypeParameters(cx, declaringGeneric)); case TypeKind.Pointer: var ptr = (IPointerTypeSymbol)type; return ptr.PointedAtType.ContainsTypeParameters(cx, declaringGeneric); case TypeKind.TypeParameter: var tp = (ITypeParameterSymbol)type; var declaringGen = tp.TypeParameterKind == TypeParameterKind.Method ? tp.DeclaringMethod : (ISymbol)tp.DeclaringType; return Equals(declaringGen, declaringGeneric); default: return false; } } } /// /// Constructs a unique string for this type symbol. /// /// The supplied action is applied to the /// syntactic sub terms of this type (if any). /// /// The extraction context. /// The trap builder used to store the result. /// The action to apply to syntactic sub terms of this type. public static void BuildTypeId(this ITypeSymbol type, Context cx, ITrapBuilder tb, Action subTermAction) { if (type.SpecialType != SpecialType.None) { /* * Use the keyword ("int" etc) for the built-in types. * This makes the IDs shorter and means that all built-in types map to * the same entities (even when using multiple versions of mscorlib). */ tb.Append(type.ToDisplayString()); return; } using (cx.StackGuard) { switch (type.TypeKind) { case TypeKind.Array: var array = (IArrayTypeSymbol)type; subTermAction(cx, tb, array.ElementType); array.BuildArraySuffix(tb); 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, tb, subTermAction); return; case TypeKind.Pointer: var ptr = (IPointerTypeSymbol)type; subTermAction(cx, tb, ptr.PointedAtType); tb.Append("*"); return; case TypeKind.TypeParameter: var tp = (ITypeParameterSymbol)type; tb.Append(tp.Name); return; case TypeKind.Dynamic: tb.Append("dynamic"); return; default: throw new InternalError(type, $"Unhandled type kind '{type.TypeKind}'"); } } } /// /// 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, ITrapBuilder tb) { tb.Append("["); for (int i = 0; i < array.Rank - 1; i++) tb.Append(","); tb.Append("]"); } static void BuildNamedTypeId(this INamedTypeSymbol named, Context cx, ITrapBuilder tb, Action subTermAction) { if (named.IsTupleType) { tb.Append("("); tb.BuildList(",", named.TupleElements, (f, tb0) => { tb.Append(f.Name).Append(":"); subTermAction(cx, tb0, f.Type); } ); tb.Append(")"); return; } if (named.ContainingType != null) { subTermAction(cx, tb, named.ContainingType); tb.Append("."); } else if (named.ContainingNamespace != null) { named.ContainingNamespace.BuildNamespace(cx, tb); } if (named.IsAnonymousType) named.BuildAnonymousName(cx, tb, subTermAction, true); else if (named.TypeParameters.IsEmpty) tb.Append(named.Name); else if (IsReallyUnbound(named)) tb.Append(named.Name).Append("`").Append(named.TypeParameters.Length); else { subTermAction(cx, tb, named.ConstructedFrom); tb.Append("<"); // 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. tb.BuildList(",", named.GetAnnotatedTypeArguments(), (ta, tb0) => { subTermAction(cx, tb0, ta.Symbol); tb.Append((int)ta.Nullability); }); tb.Append(">"); } } static void BuildNamespace(this INamespaceSymbol ns, Context cx, ITrapBuilder tb) { // Only include the assembly information in each type ID // for normal extractions. This is because standalone extractions // lack assembly information or may be ambiguous. bool prependAssemblyToTypeId = !cx.Extractor.Standalone && ns.ContainingAssembly != null; if (prependAssemblyToTypeId) { // Note that we exclude the revision number as this has // been observed to be unstable. var assembly = ns.ContainingAssembly.Identity; tb.Append(assembly.Name).Append("_"). Append(assembly.Version.Major).Append("."). Append(assembly.Version.Minor).Append("."). Append(assembly.Version.Build).Append("::"); } tb.Append(Namespace.Create(cx, ns)).Append("."); } static void BuildAnonymousName(this ITypeSymbol type, Context cx, ITrapBuilder tb, Action subTermAction, bool includeParamName) { var buildParam = includeParamName ? (prop, tb0) => { tb0.Append(prop.Name).Append(" "); subTermAction(cx, tb0, prop.Type); } : (Action)((prop, tb0) => subTermAction(cx, tb0, prop.Type)); int memberCount = type.GetMembers().OfType().Count(); int hackTypeNumber = memberCount == 1 ? 1 : 0; tb.Append("<>__AnonType"); tb.Append(hackTypeNumber); tb.Append("<"); tb.BuildList(",", type.GetMembers().OfType(), buildParam); tb.Append(">"); } /// /// 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, ITrapBuilder tb) { using (cx.StackGuard) { switch (type.TypeKind) { case TypeKind.Array: var array = (IArrayTypeSymbol)type; var elementType = array.ElementType; if (elementType.MetadataName.IndexOf("`") >= 0) { tb.Append(elementType.Name); return; } elementType.BuildDisplayName(cx, tb); array.BuildArraySuffix(tb); 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, tb); return; case TypeKind.Pointer: var ptr = (IPointerTypeSymbol)type; ptr.PointedAtType.BuildDisplayName(cx, tb); tb.Append("*"); return; case TypeKind.TypeParameter: tb.Append(type.Name); return; case TypeKind.Dynamic: tb.Append("dynamic"); return; default: throw new InternalError(type, $"Unhandled type kind '{type.TypeKind}'"); } } } public static void BuildNamedTypeDisplayName(this INamedTypeSymbol namedType, Context cx, ITrapBuilder tb) { if (namedType.IsTupleType) { tb.Append("("); tb.BuildList(",", namedType.TupleElements.Select(f => f.Type), (t, tb0) => t.BuildDisplayName(cx, tb0) ); tb.Append(")"); return; } if (namedType.IsAnonymousType) { namedType.BuildAnonymousName(cx, tb, (cx0, tb0, sub) => sub.BuildDisplayName(cx0, tb0), false); } tb.Append(namedType.Name); if (namedType.IsGenericType && namedType.TypeKind != TypeKind.Error && namedType.TypeArguments.Any()) { tb.Append("<"); tb.BuildList(",", namedType.TypeArguments, (p, tb0) => { if (IsReallyBound(namedType)) p.BuildDisplayName(cx, tb0); }); tb.Append(">"); } } public static bool IsReallyUnbound(this INamedTypeSymbol type) => 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. /// public static bool IsBoundNullable(this ITypeSymbol type) => type.SpecialType == SpecialType.None && type.OriginalDefinition.IsUnboundNullable(); /// /// Holds if this type is System.Nullable. /// public static bool IsUnboundNullable(this ITypeSymbol type) => type.SpecialType == SpecialType.System_Nullable_T; /// /// 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) return ((IMethodSymbol)parameterizable).Parameters; if (parameterizable is IPropertySymbol) return ((IPropertySymbol)parameterizable).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) => Equals(symbol, symbol.OriginalDefinition); /// /// Holds if this method is a source declaration. /// public static bool IsSourceDeclaration(this IMethodSymbol method) => IsSourceDeclaration((ISymbol)method) && Equals(method, method.ConstructedFrom) && method.ReducedFrom == null; /// /// Holds if this parameter is a source declaration. /// public static bool IsSourceDeclaration(this IParameterSymbol parameter) { var method = parameter.ContainingSymbol as IMethodSymbol; if (method != null) return method.IsSourceDeclaration(); var property = parameter.ContainingSymbol as IPropertySymbol; if (property != null && property.IsIndexer) return property.IsSourceDeclaration(); return true; } public static IEntity CreateEntity(this Context cx, ISymbol symbol) { if (symbol == null) return null; using (cx.StackGuard) { try { return symbol.Accept(new Populators.Symbols(cx)); } catch (Exception ex) // lgtm[cs/catch-of-all-exceptions] { cx.ModelError(symbol, $"Exception processing symbol '{symbol.Kind}' of type '{ex}': {symbol}"); return null; } } } public static TypeInfo GetTypeInfo(this Context cx, Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode node) => cx.Model(node).GetTypeInfo(node); public static SymbolInfo GetSymbolInfo(this Context cx, Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode node) => cx.Model(node).GetSymbolInfo(node); /// /// Gets the symbol for a particular syntax node. /// Throws an exception if the symbol is not found. /// /// /// /// This gives a nicer message than a "null pointer exception", /// and should be used where we require a symbol to be resolved. /// /// /// The extraction context. /// The syntax node. /// The resolved symbol. public static ISymbol GetSymbol(this Context cx, Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode node) { var info = GetSymbolInfo(cx, node); if (info.Symbol == null) { throw new InternalError(node, "Could not resolve symbol"); } return info.Symbol; } /// /// 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 of an ILocalSymbol. /// This has not yet been exposed on the public API. /// public static AnnotatedTypeSymbol GetAnnotatedType(this ILocalSymbol symbol) => new AnnotatedTypeSymbol(symbol.Type, symbol.NullableAnnotation); /// /// Gets the annotated type of an IPropertySymbol. /// This has not yet been exposed on the public API. /// public static AnnotatedTypeSymbol GetAnnotatedType(this IPropertySymbol symbol) => new AnnotatedTypeSymbol(symbol.Type, symbol.NullableAnnotation); /// /// Gets the annotated type of an IFieldSymbol. /// This has not yet been exposed on the public API. /// public static AnnotatedTypeSymbol GetAnnotatedType(this IFieldSymbol symbol) => new AnnotatedTypeSymbol(symbol.Type, symbol.NullableAnnotation); /// /// Gets the annotated return type of an IMethodSymbol. /// This has not yet been exposed on the public API. /// public static AnnotatedTypeSymbol GetAnnotatedReturnType(this IMethodSymbol symbol) => new AnnotatedTypeSymbol(symbol.ReturnType, symbol.ReturnNullableAnnotation); /// /// Gets the type annotation for a NullableAnnotation. /// public static Kinds.TypeAnnotation GetTypeAnnotation(this NullableAnnotation na) { switch(na) { case NullableAnnotation.Annotated: return Kinds.TypeAnnotation.Annotated; case NullableAnnotation.NotAnnotated: return Kinds.TypeAnnotation.NotAnnotated; default: return Kinds.TypeAnnotation.None; } } /// /// Gets the annotated element type of an IArrayTypeSymbol. /// This has not yet been exposed on the public API. /// public static AnnotatedTypeSymbol GetAnnotatedElementType(this IArrayTypeSymbol symbol) => new AnnotatedTypeSymbol(symbol.ElementType, symbol.ElementNullableAnnotation); /// /// 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.TypeArgumentsNullableAnnotations, (t, a) => new AnnotatedTypeSymbol(t, a)); /// /// Gets the annotated type arguments of an IMethodSymbol. /// This has not yet been exposed on the public API. /// public static IEnumerable GetAnnotatedTypeArguments(this IMethodSymbol symbol) => symbol.TypeArguments.Zip(symbol.TypeArgumentsNullableAnnotations, (t, a) => new AnnotatedTypeSymbol(t, a)); /// /// Gets the annotated type constraints of an ITypeParameterSymbol. /// This has not yet been exposed on the public API. /// public static IEnumerable GetAnnotatedTypeConstraints(this ITypeParameterSymbol symbol) => symbol.ConstraintTypes.Zip(symbol.ConstraintNullableAnnotations, (t, a) => new AnnotatedTypeSymbol(t, a)); /// /// Creates an AnnotatedTypeSymbol from an ITypeSymbol. /// public static AnnotatedTypeSymbol WithAnnotation(this ITypeSymbol symbol, NullableAnnotation annotation) => new AnnotatedTypeSymbol(symbol, annotation); } }