C#: Exclude base type extraction of recursive generics

This commit is contained in:
Tamas Vajk
2023-08-31 13:12:55 +02:00
parent 3476437bfe
commit c1d8091891
3 changed files with 253 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -82,8 +83,15 @@ namespace Semmle.Extraction.CSharp.Entities
var baseTypes = GetBaseTypeDeclarations();
var hasExpandingCycle = GenericsRecursionGraph.HasExpandingCycle(Symbol);
if (hasExpandingCycle)
{
Context.ExtractionError("Found recursive generic inheritance hierarchy. Base class of type is not extracted", Symbol.ToDisplayString(), Context.CreateLocation(ReportingLocation), severity: Util.Logging.Severity.Warning);
}
// Visit base types
if (Symbol.GetNonObjectBaseType(Context) is INamedTypeSymbol @base)
if (!hasExpandingCycle
&& Symbol.GetNonObjectBaseType(Context) is INamedTypeSymbol @base)
{
var bts = GetBaseTypeDeclarations(baseTypes, @base);
@@ -347,6 +355,211 @@ namespace Semmle.Extraction.CSharp.Entities
}
public override int GetHashCode() => SymbolEqualityComparer.Default.GetHashCode(Symbol);
/// <summary>
/// Class to detect recursive generic inheritance hierarchies.
///
/// Details can be found in https://www.ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf Chapter II.9.2 Generics and recursive inheritance graphs
/// The dotnet runtime already implements this check as a runtime validation: https://github.com/dotnet/runtime/blob/e48e88d0fe9c2e494c0e6fd0c7c1fb54e7ddbdb1/src/coreclr/vm/generics.cpp#L748
/// </summary>
public class GenericsRecursionGraph
{
private static readonly ConcurrentDictionary<INamedTypeSymbol, bool> resultCache = new(SymbolEqualityComparer.Default);
/// <summary>
/// Checks whether the given type has a recursive generic inheritance hierarchy. The result is cached.
/// </summary>
public static bool HasExpandingCycle(ITypeSymbol start)
{
if (start.OriginalDefinition is not INamedTypeSymbol namedTypeDefinition ||
!namedTypeDefinition.IsGenericType)
{
return false;
}
if (resultCache.TryGetValue(namedTypeDefinition, out var result))
{
return result;
}
result = new GenericsRecursionGraph(namedTypeDefinition).HasExpandingCycle();
resultCache.TryAdd(namedTypeDefinition, result);
return result;
}
private readonly INamedTypeSymbol startSymbol;
private readonly HashSet<INamedTypeSymbol> instantiationClosure = new(SymbolEqualityComparer.Default);
private readonly Dictionary<ITypeParameterSymbol, List<(ITypeParameterSymbol To, bool IsExpanding)>> edges = new(SymbolEqualityComparer.Default);
private GenericsRecursionGraph(INamedTypeSymbol startSymbol)
{
this.startSymbol = startSymbol;
ComputeInstantiationClosure();
ComputeGraphEdges();
}
private void ComputeGraphEdges()
{
foreach (var reference in instantiationClosure)
{
var definition = reference.OriginalDefinition;
if (SymbolEqualityComparer.Default.Equals(reference, definition))
{
// It's a definition, so no edges
continue;
}
for (var i = 0; i < reference.TypeArguments.Length; i++)
{
var target = definition.TypeParameters[i];
if (reference.TypeArguments[i] is ITypeParameterSymbol source)
{
// non-expanding
if (!edges.TryGetValue(source, out var targets))
{
targets = new List<(ITypeParameterSymbol, bool)>();
edges.Add(source, targets);
}
targets.Add((target, false));
}
else if (reference.TypeArguments[i] is INamedTypeSymbol namedType)
{
// expanding
var sources = GetAllNestedTypeParameters(namedType);
foreach (var s in sources)
{
if (!edges.TryGetValue(s, out var targets))
{
targets = new List<(ITypeParameterSymbol, bool)>();
edges.Add(s, targets);
}
targets.Add((target, true));
}
}
}
}
}
private List<ITypeParameterSymbol> GetAllNestedTypeParameters(INamedTypeSymbol symbol)
{
var res = new List<ITypeParameterSymbol>();
foreach (var typeArgument in symbol.TypeArguments)
{
if (typeArgument is ITypeParameterSymbol typeParameter)
{
res.Add(typeParameter);
}
else if (typeArgument is INamedTypeSymbol namedType)
{
res.AddRange(GetAllNestedTypeParameters(namedType));
}
}
return res;
}
private void ComputeInstantiationClosure()
{
var workQueue = new Queue<INamedTypeSymbol>();
workQueue.Enqueue(startSymbol);
while (workQueue.Count > 0)
{
var current = workQueue.Dequeue();
if (instantiationClosure.Contains(current) ||
!current.IsGenericType)
{
continue;
}
instantiationClosure.Add(current);
if (SymbolEqualityComparer.Default.Equals(current, current.OriginalDefinition))
{
// Definition, so enqueue all base types and interfaces
if (current.BaseType != null)
{
workQueue.Enqueue(current.BaseType);
}
foreach (var i in current.Interfaces)
{
workQueue.Enqueue(i);
}
}
else
{
// Reference, so enqueue all type arguments and their original definitions:
foreach (var namedTypeArgument in current.TypeArguments.OfType<INamedTypeSymbol>())
{
workQueue.Enqueue(namedTypeArgument);
workQueue.Enqueue(namedTypeArgument.OriginalDefinition);
}
}
}
}
private bool HasExpandingCycle()
{
return startSymbol.TypeParameters.Any(HasExpandingCycle);
}
private bool HasExpandingCycle(ITypeParameterSymbol start)
{
var visited = new HashSet<ITypeParameterSymbol>(SymbolEqualityComparer.Default);
var recStack = new HashSet<ITypeParameterSymbol>(SymbolEqualityComparer.Default);
var hasExpandingCycle = HasExpandingCycle(start, visited, recStack, start, hasSeenExpandingEdge: false);
return hasExpandingCycle;
}
private List<(ITypeParameterSymbol To, bool IsExpanding)> GetOutgoingEdges(ITypeParameterSymbol typeParameter)
{
return edges.TryGetValue(typeParameter, out var outgoingEdges)
? outgoingEdges
: new List<(ITypeParameterSymbol, bool)>();
}
/// <summary>
/// A modified cycle detection algorithm based on DFS.
/// </summary>
/// <param name="current">The current node that is being visited</param>
/// <param name="visited">The nodes that have already been visited by any path.</param>
/// <param name="currentPath">The nodes already visited on the current path. Could be a List<> if the order was important.</param>
/// <param name="start">The start and end of the cycle. We're not looking for any cycle, but a cycle that goes back to the start.</param>
/// <param name="hasSeenExpandingEdge">Whether an expanding edge was already seen in this path. We're looking for a cycle that has at least one expanding edge.</param>
/// <returns></returns>
private bool HasExpandingCycle(ITypeParameterSymbol current, HashSet<ITypeParameterSymbol> visited, HashSet<ITypeParameterSymbol> currentPath, ITypeParameterSymbol start, bool hasSeenExpandingEdge)
{
if (currentPath.Count > 0 && SymbolEqualityComparer.Default.Equals(current, start))
{
return hasSeenExpandingEdge;
}
if (visited.Contains(current))
{
return false;
}
visited.Add(current);
currentPath.Add(current);
var outgoingEdges = GetOutgoingEdges(current);
foreach (var outgoingEdge in outgoingEdges)
{
if (HasExpandingCycle(outgoingEdge.To, visited, currentPath, start, hasSeenExpandingEdge: hasSeenExpandingEdge || outgoingEdge.IsExpanding))
{
return true;
}
}
currentPath.Remove(current);
return false;
}
}
}
internal abstract class Type<T> : Type where T : ITypeSymbol

View File

@@ -0,0 +1,4 @@
| test.cs:2:14:2:20 | Found recursive generic inheritance hierarchy. Base class of type is not extracted | 4 | GenB<GenB<T>> |
| test.cs:2:14:2:20 | Found recursive generic inheritance hierarchy. Base class of type is not extracted | 4 | GenB<GenB<string>> |
| test.cs:2:14:2:20 | Found recursive generic inheritance hierarchy. Base class of type is not extracted | 4 | GenB<T> |
| test.cs:2:14:2:20 | Found recursive generic inheritance hierarchy. Base class of type is not extracted | 4 | GenB<string> |

View File

@@ -0,0 +1,35 @@
| test.cs:1:14:1:20 | GenA<> | System.Object |
| test.cs:1:14:1:20 | GenA<GenB<GenB<>>> | System.Object |
| test.cs:1:14:1:20 | GenA<GenB<GenB<String>>> | System.Object |
| test.cs:2:14:2:20 | GenB<> | System.Object |
| test.cs:2:14:2:20 | GenB<GenB<>> | System.Object |
| test.cs:2:14:2:20 | GenB<GenB<String>> | System.Object |
| test.cs:2:14:2:20 | GenB<String> | System.Object |
| test.cs:4:7:4:10 | P<> | System.Object |
| test.cs:4:7:4:10 | P<C<,>> | System.Object |
| test.cs:4:7:4:10 | P<C<Int32,String>> | System.Object |
| test.cs:4:7:4:10 | P<C<String,Int32>> | System.Object |
| test.cs:4:7:4:10 | P<C<V,U>> | System.Object |
| test.cs:4:7:4:10 | P<C<W,X>> | System.Object |
| test.cs:4:7:4:10 | P<C<X,W>> | System.Object |
| test.cs:4:7:4:10 | P<D<,>> | System.Object |
| test.cs:4:7:4:10 | P<D<Int32,String>> | System.Object |
| test.cs:4:7:4:10 | P<D<String,Int32>> | System.Object |
| test.cs:4:7:4:10 | P<D<U,V>> | System.Object |
| test.cs:4:7:4:10 | P<D<V,U>> | System.Object |
| test.cs:4:7:4:10 | P<D<X,W>> | System.Object |
| test.cs:5:7:5:13 | C<,> | P<D<V,U>> |
| test.cs:5:7:5:13 | C<Int32,String> | P<D<System.String,System.Int32>> |
| test.cs:5:7:5:13 | C<String,Int32> | P<D<System.Int32,System.String>> |
| test.cs:5:7:5:13 | C<V,U> | P<D<U,V>> |
| test.cs:5:7:5:13 | C<W,X> | P<D<X,W>> |
| test.cs:5:7:5:13 | C<X,W> | P<D<,>> |
| test.cs:6:7:6:13 | D<,> | P<C<W,X>> |
| test.cs:6:7:6:13 | D<Int32,String> | P<C<System.Int32,System.String>> |
| test.cs:6:7:6:13 | D<String,Int32> | P<C<System.String,System.Int32>> |
| test.cs:6:7:6:13 | D<U,V> | P<C<,>> |
| test.cs:6:7:6:13 | D<V,U> | P<C<V,U>> |
| test.cs:6:7:6:13 | D<X,W> | P<C<X,W>> |
| test.cs:8:7:8:10 | A<> | System.Object |
| test.cs:8:7:8:10 | A<String> | System.Object |
| test.cs:13:14:13:18 | Class | System.Object |