Files
gregExtractor/Services/RoslynCoverageScanner.cs
T

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);
}
}