Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>

<PropertyGroup>
<LangVersion>9.0</LangVersion>
<LangVersion>latest</LangVersion>
<Nullable>Enable</Nullable>
<DebugType>embedded</DebugType>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/ErrorProne.NET.Core/CompilationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static class CompilationExtensions

public static INamedTypeSymbol? TaskOfTType(this Compilation compilation)
=> compilation.GetTypeByFullName(typeof(Task<>).FullName);

public static INamedTypeSymbol? ValueTaskOfTType(this Compilation compilation)
=> compilation.GetTypeByFullName("System.Threading.Tasks.ValueTask`1");

Expand Down
8 changes: 1 addition & 7 deletions src/ErrorProne.NET.Core/SymbolAnalysisContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
// --------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// --------------------------------------------------------------------

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Diagnostics.CodeAnalysis;

Expand Down
180 changes: 180 additions & 0 deletions src/ErrorProne.NET.Core/SymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.FlowAnalysis;
using Microsoft.CodeAnalysis.Operations;

namespace ErrorProne.NET.Core
{
Expand All @@ -20,6 +23,11 @@ public enum SymbolVisibility
/// <nodoc />
public static class SymbolExtensions
{
public static bool HasAttributeWithName(this ISymbol? symbol, string attributeName)
{
return symbol?.GetAttributes().Any(a => a.AttributeClass.Name == attributeName) == true;
}

public static bool IsConstructor(this ISymbol symbol)
{
return (symbol is IMethodSymbol methodSymbol && methodSymbol.MethodKind == MethodKind.Constructor);
Expand Down Expand Up @@ -264,5 +272,177 @@ public static bool ExceptionFromCatchBlock(this ISymbol symbol)
// Use following code if the trick with DeclaredSyntaxReferences would not work properly!
// return (bool?)(symbol.GetType().GetRuntimeProperty("IsCatch")?.GetValue(symbol)) == true;
}

public static bool IsPrivate(this ISymbol symbol)
{
return symbol.DeclaredAccessibility == Accessibility.Private;
}
}

public static class MethodSymbolExtensions
{
/// <summary>
/// Checks if the given method matches Dispose method convention and can be recognized by "using".
/// </summary>
public static bool HasDisposeSignatureByConvention(this IMethodSymbol method)
{
return method.HasDisposeMethodSignature()
&& !method.IsStatic
&& !method.IsPrivate();
}

/// <summary>
/// Checks if the given method has the signature "void Dispose()".
/// </summary>
private static bool HasDisposeMethodSignature(this IMethodSymbol method)
{
return method.Name == "Dispose" && method.MethodKind == MethodKind.Ordinary &&
method.ReturnsVoid && method.Parameters.IsEmpty;
}
}

// TODO: move to another file
internal static class ControlFlowGraphExtensions
{
public static BasicBlock GetEntry(this ControlFlowGraph cfg) => cfg.Blocks.Single(b => b.Kind == BasicBlockKind.Entry);
public static BasicBlock GetExit(this ControlFlowGraph cfg) => cfg.Blocks.Single(b => b.Kind == BasicBlockKind.Exit);
public static IEnumerable<IOperation> DescendantOperations(this ControlFlowGraph cfg)
{
foreach (BasicBlock block in cfg.Blocks)
{
foreach (IOperation operation in block.DescendantOperations())
{
yield return operation;
}
}
}

public static bool IsFinallyBlock(this BasicBlock block)
{
return block.Kind == BasicBlockKind.Block && block.EnclosingRegion.Kind == ControlFlowRegionKind.Finally;
}

public static bool IsTryBlock(this BasicBlock block)
{
return block.Kind == BasicBlockKind.Block && block.EnclosingRegion.Kind == ControlFlowRegionKind.Try;
}

public static bool IsCatchBlock(this BasicBlock block)
{
return block.Kind == BasicBlockKind.Block && block.EnclosingRegion.Kind == ControlFlowRegionKind.Catch;
}

public static IEnumerable<IOperation> DescendantOperations(this BasicBlock basicBlock)
{
foreach (var statement in basicBlock.Operations)
{
foreach (var operation in statement.DescendantsAndSelf())
{
yield return operation;
}
}

if (basicBlock.BranchValue != null)
{
foreach (var operation in basicBlock.BranchValue.DescendantsAndSelf())
{
yield return operation;
}
}
}

public static IEnumerable<T> DescendantOperations<T>(this ControlFlowGraph cfg, OperationKind operationKind)
where T : IOperation
{
foreach (var descendant in cfg.DescendantOperations())
{
if (descendant?.Kind == operationKind)
{
yield return (T)descendant;
}
}
}

internal static bool SupportsFlowAnalysis(this ControlFlowGraph cfg)
{
// Skip flow analysis for following root operation blocks:
// 1. Null root operation (error case)
// 2. OperationKindEx.Attribute or OperationKind.None (used for attributes before IAttributeOperation support).
// 3. OperationKind.ParameterInitialzer (default parameter values).
if (cfg.OriginalOperation == null ||
cfg.OriginalOperation.Kind is OperationKindEx.Attribute or OperationKind.None or OperationKind.ParameterInitializer)
{
return false;
}

// Skip flow analysis for code with syntax/semantic errors
if (cfg.OriginalOperation.Syntax.GetDiagnostics().Any(d => d.DefaultSeverity == DiagnosticSeverity.Error) ||
cfg.OriginalOperation.HasAnyOperationDescendant(o => o is IInvalidOperation))
{
return false;
}

return true;
}

public static bool HasAnyOperationDescendant(this ImmutableArray<IOperation> operationBlocks, Func<IOperation, bool> predicate)
{
foreach (var operationBlock in operationBlocks)
{
if (operationBlock.HasAnyOperationDescendant(predicate))
{
return true;
}
}

return false;
}

public static bool HasAnyOperationDescendant(this ImmutableArray<IOperation> operationBlocks, Func<IOperation, bool> predicate, [NotNullWhen(returnValue: true)] out IOperation? foundOperation)
{
foreach (var operationBlock in operationBlocks)
{
if (operationBlock.HasAnyOperationDescendant(predicate, out foundOperation))
{
return true;
}
}

foundOperation = null;
return false;
}

public static bool HasAnyOperationDescendant(this ImmutableArray<IOperation> operationBlocks, OperationKind kind)
{
return operationBlocks.HasAnyOperationDescendant(predicate: operation => operation.Kind == kind);
}

public static bool HasAnyOperationDescendant(this IOperation operationBlock, Func<IOperation, bool> predicate)
{
return operationBlock.HasAnyOperationDescendant(predicate, out _);
}

public static bool HasAnyOperationDescendant(this IOperation operationBlock, Func<IOperation, bool> predicate, [NotNullWhen(returnValue: true)] out IOperation? foundOperation)
{
foreach (var descendant in operationBlock.DescendantsAndSelf())
{
if (predicate(descendant))
{
foundOperation = descendant;
return true;
}
}

foundOperation = null;
return false;
}
}

internal static class OperationKindEx
{
public const OperationKind FunctionPointerInvocation = (OperationKind)0x78;
public const OperationKind ImplicitIndexerReference = (OperationKind)0x7b;
public const OperationKind Utf8String = (OperationKind)0x7c;
public const OperationKind Attribute = (OperationKind)0x7d;
}
}
82 changes: 82 additions & 0 deletions src/ErrorProne.NET.Core/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,87 @@ public static bool OverridesToString(this ITypeSymbol type)
.Any(t => t.GetMembers(nameof(ToString)).Any(m => m.IsOverride));
}

/// <summary>
/// Return true if a given <paramref name="symbol"/> derives from <paramref name="candidateBaseType"/>.
/// </summary>
public static bool DerivesFrom([NotNullWhen(returnValue: true)] this ITypeSymbol? symbol, [NotNullWhen(returnValue: true)] ITypeSymbol? candidateBaseType, bool baseTypesOnly = false, bool checkTypeParameterConstraints = true)
{
if (candidateBaseType == null || symbol == null)
{
return false;
}

if (!baseTypesOnly && candidateBaseType.TypeKind == TypeKind.Interface)
{
var allInterfaces = symbol.AllInterfaces.OfType<ITypeSymbol>();
if (SymbolEqualityComparer.Default.Equals(candidateBaseType.OriginalDefinition, candidateBaseType))
{
// Candidate base type is not a constructed generic type, so use original definition for interfaces.
allInterfaces = allInterfaces.Select(i => i.OriginalDefinition);
}

if (allInterfaces.Contains(candidateBaseType))
{
return true;
}
}

if (checkTypeParameterConstraints && symbol.TypeKind == TypeKind.TypeParameter)
{
var typeParameterSymbol = (ITypeParameterSymbol)symbol;
foreach (var constraintType in typeParameterSymbol.ConstraintTypes)
{
if (constraintType.DerivesFrom(candidateBaseType, baseTypesOnly, checkTypeParameterConstraints))
{
return true;
}
}
}

while (symbol != null)
{
if (SymbolEqualityComparer.Default.Equals(symbol, candidateBaseType))
{
return true;
}

symbol = symbol.BaseType;
}

return false;
}

/// <summary>
/// Indicates if the given <paramref name="type"/> is disposable,
/// and thus can be used in a <code>using</code> or <code>await using</code> statement.
/// </summary>
public static bool IsDisposable(this ITypeSymbol type,
INamedTypeSymbol? iDisposable,
INamedTypeSymbol? iAsyncDisposable,
INamedTypeSymbol? configuredAsyncDisposable)
{
if (type.IsReferenceType)
{
return IsInterfaceOrImplementsInterface(type, iDisposable)
|| IsInterfaceOrImplementsInterface(type, iAsyncDisposable);
}
else if (SymbolEqualityComparer.Default.Equals(type, configuredAsyncDisposable))
{
return true;
}

if (type.IsRefLikeType)
{
return type.GetMembers("Dispose").OfType<IMethodSymbol>()
.Any(method => method.HasDisposeSignatureByConvention());
}

return false;

static bool IsInterfaceOrImplementsInterface(ITypeSymbol type, INamedTypeSymbol? interfaceType)
=> interfaceType != null &&
(SymbolEqualityComparer.Default.Equals(type, interfaceType) || type.AllInterfaces.Contains(interfaceType));
}

}
}
6 changes: 6 additions & 0 deletions src/ErrorProne.NET.Core/WellKnownTypesProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,10 @@ public static WellKnownTypeProvider GetOrCreate(Compilation compilation)
v => Compilation.GetBestTypeByMetadataName(v));
}
}

public static class WellKnownTypeNames
{
public const string SystemIAsyncDisposable = "System.IAsyncDisposable";
public const string SystemIDisposable = "System.IDisposable";
}
}
Loading