118 lines
4.0 KiB
C#
118 lines
4.0 KiB
C#
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
|
|
namespace gregExtractor.Services;
|
|
|
|
public sealed class RoslynCoverageScanner : ICoverageScanner
|
|
{
|
|
private readonly Dictionary<string, HashSet<string>> _matches = new(StringComparer.Ordinal);
|
|
|
|
public IReadOnlyDictionary<string, IReadOnlyCollection<string>> LastScanMatches => _matches
|
|
.ToDictionary(static x => x.Key, static x => (IReadOnlyCollection<string>)x.Value.ToArray(), StringComparer.Ordinal);
|
|
|
|
public HashSet<string> ScanImplementedPatches(string[] sourceDirs, IProgress<string>? progress)
|
|
{
|
|
_matches.Clear();
|
|
|
|
var keys = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (string file in EnumerateSourceFiles(sourceDirs))
|
|
{
|
|
progress?.Report($"Roslyn scanning {file}");
|
|
|
|
string code = File.ReadAllText(file);
|
|
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
|
|
SyntaxNode root = tree.GetRoot();
|
|
|
|
IEnumerable<AttributeSyntax> attributes = root.DescendantNodes().OfType<AttributeSyntax>();
|
|
foreach (AttributeSyntax attribute in attributes)
|
|
{
|
|
if (!IsHarmonyPatchAttribute(attribute))
|
|
continue;
|
|
|
|
if (!TryExtractPatchKey(attribute, out string? key))
|
|
continue;
|
|
|
|
key = NormalizeKey(key!);
|
|
keys.Add(key);
|
|
|
|
if (!_matches.TryGetValue(key, out HashSet<string>? files))
|
|
{
|
|
files = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
_matches[key] = files;
|
|
}
|
|
|
|
files.Add(file);
|
|
}
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
private static IEnumerable<string> EnumerateSourceFiles(IEnumerable<string> sourceDirs)
|
|
{
|
|
foreach (string dir in sourceDirs.Where(Directory.Exists))
|
|
{
|
|
foreach (string file in Directory.EnumerateFiles(dir, "*.cs", SearchOption.AllDirectories))
|
|
yield return file;
|
|
}
|
|
}
|
|
|
|
private static bool IsHarmonyPatchAttribute(AttributeSyntax attribute)
|
|
{
|
|
string name = attribute.Name.ToString();
|
|
return name.Contains("HarmonyPatch", StringComparison.Ordinal);
|
|
}
|
|
|
|
private static bool TryExtractPatchKey(AttributeSyntax attribute, out string? key)
|
|
{
|
|
key = null;
|
|
|
|
if (attribute.ArgumentList is null)
|
|
return false;
|
|
|
|
SeparatedSyntaxList<AttributeArgumentSyntax> args = attribute.ArgumentList.Arguments;
|
|
if (args.Count < 2)
|
|
return false;
|
|
|
|
if (args[0].Expression is not TypeOfExpressionSyntax typeOfExpression)
|
|
return false;
|
|
|
|
string typeName = typeOfExpression.Type.ToString().Replace("global::", string.Empty).Trim();
|
|
if (string.IsNullOrWhiteSpace(typeName))
|
|
return false;
|
|
|
|
if (!TryExtractMethodName(args[1].Expression, out string? methodName))
|
|
return false;
|
|
|
|
key = $"{typeName}::{methodName}";
|
|
return true;
|
|
}
|
|
|
|
private static bool TryExtractMethodName(ExpressionSyntax expression, out string? methodName)
|
|
{
|
|
methodName = null;
|
|
|
|
if (expression is InvocationExpressionSyntax invocation
|
|
&& invocation.Expression is IdentifierNameSyntax nameofIdentifier
|
|
&& nameofIdentifier.Identifier.Text == "nameof"
|
|
&& invocation.ArgumentList.Arguments.Count == 1)
|
|
{
|
|
ExpressionSyntax argExpression = invocation.ArgumentList.Arguments[0].Expression;
|
|
methodName = argExpression switch
|
|
{
|
|
MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
|
|
IdentifierNameSyntax identifierName => identifierName.Identifier.Text,
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
return !string.IsNullOrWhiteSpace(methodName);
|
|
}
|
|
|
|
private static string NormalizeKey(string key)
|
|
{
|
|
return key.Replace(" ", string.Empty);
|
|
}
|
|
}
|