|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace Microsoft.CodeAnalysis.Test.Utilities
{
public abstract class TrackingDiagnosticAnalyzer<TLanguageKindEnum> : TestDiagnosticAnalyzer<TLanguageKindEnum> where TLanguageKindEnum : struct
{
#region Tracking
public class Entry
{
public readonly string AbstractMemberName;
public readonly string CallerName;
public readonly TLanguageKindEnum SyntaxKind;
public readonly SymbolKind? SymbolKind;
public readonly MethodKind? MethodKind;
public readonly bool ReturnsVoid;
public Entry(string abstractMemberName, string callerName, SyntaxNode node, ISymbol symbol)
{
AbstractMemberName = abstractMemberName;
CallerName = callerName;
SyntaxKind = node == null ? default(TLanguageKindEnum) : (TLanguageKindEnum)(object)(ushort)node.RawKind;
SymbolKind = symbol == null ? (SymbolKind?)null : symbol.Kind;
MethodKind = symbol is IMethodSymbol ? ((IMethodSymbol)symbol).MethodKind : (MethodKind?)null;
ReturnsVoid = symbol is IMethodSymbol ? ((IMethodSymbol)symbol).ReturnsVoid : false;
}
public override string ToString()
{
return CallerName + "(" + string.Join(", ", SymbolKind, MethodKind, SyntaxKind) + ")";
}
}
private readonly ConcurrentQueue<Entry> _callLog = new ConcurrentQueue<Entry>();
protected override void OnAbstractMember(string abstractMemberName, SyntaxNode node, ISymbol symbol, string callerName)
{
_callLog.Enqueue(new Entry(abstractMemberName, callerName, node, symbol));
}
public IEnumerable<Entry> CallLog
{
get { return _callLog; }
}
#endregion
#region Analysis
private static readonly Regex s_omittedSyntaxKindRegex =
new Regex(@"None|Trivia|Token|Keyword|List|Xml|Cref|Compilation|Namespace|Class|Struct|Enum|Interface|Delegate|Field|Property|Indexer|Event|Operator|Constructor|Access|Incomplete|Attribute|Filter|InterpolatedString");
private bool FilterByAbstractName(Entry entry, string abstractMemberName)
{
return abstractMemberName == entry.AbstractMemberName;
}
public void VerifyAllAnalyzerMembersWereCalled()
{
var actualMembers = _callLog.Select(e => e.CallerName).Distinct();
AssertSequenceEqual(AllAnalyzerMemberNames, actualMembers);
}
public void VerifyAnalyzeSymbolCalledForAllSymbolKinds()
{
var expectedSymbolKinds = new[]
{
SymbolKind.Event, SymbolKind.Field, SymbolKind.Method, SymbolKind.NamedType, SymbolKind.Namespace, SymbolKind.Parameter, SymbolKind.Property
};
var actualSymbolKinds = _callLog.Where(a => FilterByAbstractName(a, "Symbol")).Where(e => e.SymbolKind.HasValue).Select(e => e.SymbolKind.Value).Distinct();
AssertSequenceEqual(expectedSymbolKinds, actualSymbolKinds);
}
protected virtual bool IsAnalyzeNodeSupported(TLanguageKindEnum syntaxKind)
{
return !s_omittedSyntaxKindRegex.IsMatch(syntaxKind.ToString());
}
public void VerifyAnalyzeNodeCalledForAllSyntaxKinds(HashSet<TLanguageKindEnum> expectedMissingSyntaxKinds)
{
var expectedSyntaxKinds = AllSyntaxKinds.Where(IsAnalyzeNodeSupported);
var actualSyntaxKinds = new HashSet<TLanguageKindEnum>(_callLog.Where(a => FilterByAbstractName(a, "SyntaxNode")).Select(e => e.SyntaxKind));
var savedSyntaxKindsPatterns = new HashSet<TLanguageKindEnum>(expectedMissingSyntaxKinds);
expectedMissingSyntaxKinds.IntersectWith(actualSyntaxKinds);
Assert.True(expectedMissingSyntaxKinds.Count == 0, "AllInOne test contains ignored SyntaxKinds: " + string.Join(", ", expectedMissingSyntaxKinds));
actualSyntaxKinds.UnionWith(savedSyntaxKindsPatterns);
AssertIsSuperset(expectedSyntaxKinds, actualSyntaxKinds);
}
protected virtual bool IsOnCodeBlockSupported(SymbolKind symbolKind, MethodKind methodKind, bool returnsVoid)
{
return true;
}
public void VerifyOnCodeBlockCalledForAllSymbolAndMethodKinds(HashSet<SymbolKind> symbolKindsWithNoCodeBlocks = null, bool allowUnexpectedCalls = false)
{
const MethodKind InvalidMethodKind = (MethodKind)(-1);
var expectedArguments = new[]
{
new { SymbolKind = SymbolKind.Event, MethodKind = InvalidMethodKind, ReturnsVoid = false },
new { SymbolKind = SymbolKind.Field, MethodKind = InvalidMethodKind, ReturnsVoid = false },
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.Constructor, ReturnsVoid = true },
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.Conversion, ReturnsVoid = false },
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.Destructor, ReturnsVoid = true }, // C# only
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.EventAdd, ReturnsVoid = true },
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.EventRemove, ReturnsVoid = true },
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.EventRaise, ReturnsVoid = true }, // VB only
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.ExplicitInterfaceImplementation, ReturnsVoid = true }, // C# only
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.Ordinary, ReturnsVoid = false },
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.Ordinary, ReturnsVoid = true },
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.PropertyGet, ReturnsVoid = false },
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.PropertySet, ReturnsVoid = true },
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.StaticConstructor, ReturnsVoid = true },
new { SymbolKind = SymbolKind.Method, MethodKind = MethodKind.UserDefinedOperator, ReturnsVoid = false },
new { SymbolKind = SymbolKind.Property, MethodKind = InvalidMethodKind, ReturnsVoid = false },
new { SymbolKind = SymbolKind.NamedType, MethodKind = InvalidMethodKind, ReturnsVoid = false },
new { SymbolKind = SymbolKind.Namespace, MethodKind = InvalidMethodKind, ReturnsVoid = false }
}.AsEnumerable();
if (symbolKindsWithNoCodeBlocks != null)
{
expectedArguments = expectedArguments.Where(a => !symbolKindsWithNoCodeBlocks.Contains(a.SymbolKind));
}
expectedArguments = expectedArguments.Where(a => IsOnCodeBlockSupported(a.SymbolKind, a.MethodKind, a.ReturnsVoid));
var actualOnCodeBlockStartedArguments = _callLog.Where(a => FilterByAbstractName(a, "CodeBlockStart"))
.Select(e => new { SymbolKind = e.SymbolKind.Value, MethodKind = e.MethodKind ?? InvalidMethodKind, e.ReturnsVoid }).Distinct();
var actualOnCodeBlockEndedArguments = _callLog.Where(a => FilterByAbstractName(a, "CodeBlock"))
.Select(e => new { SymbolKind = e.SymbolKind.Value, MethodKind = e.MethodKind ?? InvalidMethodKind, e.ReturnsVoid }).Distinct();
if (!allowUnexpectedCalls)
{
AssertSequenceEqual(expectedArguments, actualOnCodeBlockStartedArguments, items => items.OrderBy(p => p.SymbolKind).ThenBy(p => p.MethodKind).ThenBy(p => p.ReturnsVoid));
AssertSequenceEqual(expectedArguments, actualOnCodeBlockEndedArguments, items => items.OrderBy(p => p.SymbolKind).ThenBy(p => p.MethodKind).ThenBy(p => p.ReturnsVoid));
}
else
{
AssertIsSuperset(expectedArguments, actualOnCodeBlockStartedArguments);
AssertIsSuperset(expectedArguments, actualOnCodeBlockEndedArguments);
}
}
private void AssertSequenceEqual<T>(IEnumerable<T> expected, IEnumerable<T> actual, Func<IEnumerable<T>, IOrderedEnumerable<T>> sorter = null)
{
sorter = sorter ?? new Func<IEnumerable<T>, IOrderedEnumerable<T>>(items => items.OrderBy(i => i));
expected = sorter(expected);
actual = sorter(actual);
Assert.True(expected.SequenceEqual(actual),
Environment.NewLine + "Expected: " + string.Join(", ", expected) +
Environment.NewLine + "Actual: " + string.Join(", ", actual));
}
private void AssertIsSuperset<T>(IEnumerable<T> expectedSubset, IEnumerable<T> actualSuperset)
{
var missingElements = expectedSubset.GroupJoin(actualSuperset, e => e, a => a, (e, a) => new { Element = e, IsMissing = !a.Any() })
.Where(p => p.IsMissing).Select(p => p.Element).ToList();
var presentElements = expectedSubset.Except(missingElements);
Assert.True(missingElements.Count == 0,
Environment.NewLine + "Missing: " + string.Join(", ", missingElements) +
Environment.NewLine + "Present: " + string.Join(", ", presentElements));
}
#endregion
}
}
|