|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace SuperFileCheck
{
internal readonly record struct MethodDeclarationInfo(MethodDeclarationSyntax Syntax, string FullyQualifiedName);
internal readonly record struct FileCheckResult(int ExitCode, string StandardOutput, string StandardError);
internal class SuperFileCheckException : Exception
{
public SuperFileCheckException(string message): base(message) { }
}
internal class Program
{
const string CommandLineArgumentCSharp = "--csharp";
const string CommandLineArgumentCSharpListMethodNames = "--csharp-list-method-names";
const string CommandLineCheckPrefixes = "--check-prefixes";
const string CommandLineCheckPrefixesEqual = "--check-prefixes=";
const string CommandLineInputFile = "--input-file";
const string SyntaxDirectiveFullLine = "-FULL-LINE:";
const string SyntaxDirectiveFullLineNext = "-FULL-LINE-NEXT:";
static string FileCheckPath;
static Program()
{
// Determine the location of LLVM FileCheck.
// We first look through the "runtimes" directory relative to
// the location of SuperFileCheck to find FileCheck.
// If it cannot find it, then we assume FileCheck
// is in the same directory as SuperFileCheck.
var superFileCheckPath = typeof(Program).Assembly.Location;
if (String.IsNullOrEmpty(superFileCheckPath))
{
throw new SuperFileCheckException("Invalid SuperFileCheck path.");
}
var superFileCheckDir = Path.GetDirectoryName(superFileCheckPath);
if (superFileCheckDir != null)
{
var fileCheckPath =
Directory.EnumerateFiles(Path.Combine(superFileCheckDir, "runtimes/"), "FileCheck*", SearchOption.AllDirectories)
.FirstOrDefault();
if (fileCheckPath != null)
{
FileCheckPath = fileCheckPath;
}
else
{
FileCheckPath = Path.Combine(superFileCheckDir, "FileCheck");
}
}
else
{
FileCheckPath = "FileCheck";
}
}
/// <summary>
/// Checks if the given string contains LLVM "<prefix>" directives, such as "<prefix>:", "<prefix>-LABEL:", etc..
/// </summary>
static bool ContainsCheckPrefixes(string str, string[] checkPrefixes)
{
// LABEL, NOT, SAME, etc. are from LLVM FileCheck https://llvm.org/docs/CommandGuide/FileCheck.html
// FULL-LINE and FULL-LINE-NEXT are not part of LLVM FileCheck - they are new syntax directives for SuperFileCheck to be able to
// match a single full-line, similar to that of LLVM FileCheck's --match-full-lines option.
var pattern = $"({String.Join('|', checkPrefixes)})+?({{LITERAL}})?(:|-LABEL:|-NEXT:|-NOT:|-SAME:|-EMPTY:|-COUNT:|-DAG:|{SyntaxDirectiveFullLine}|{SyntaxDirectiveFullLineNext})";
var regex = new System.Text.RegularExpressions.Regex(pattern);
return regex.Count(str) > 0;
}
/// <summary>
/// Verifies LLVM "<prefix>" directives, such as "<prefix>:", "<prefix>-LABEL:", etc.. are valid.
/// Currently only checks to see if the user is using '-NEXT-FULL-LINE:' instead of '-FULL-LINE-NEXT:'.
/// </summary>
static void VerifyCheckPrefixes(string str, string[] checkPrefixes)
{
var invalidFullLinePattern = $"({String.Join('|', checkPrefixes)})+?({{LITERAL}})?(-NEXT-FULL-LINE:)";
var invalidRegex = new System.Text.RegularExpressions.Regex(invalidFullLinePattern);
if (invalidRegex.Count(str) > 0)
{
throw new SuperFileCheckException("'NEXT-FULL-LINE' is an invalid directive. Use 'FULL-LINE-NEXT'.");
}
}
/// <summary>
/// Runs LLVM's FileCheck executable.
/// Will always redirect standard error and output.
/// </summary>
static async Task<FileCheckResult> RunLLVMFileCheckAsync(string[] args)
{
var startInfo = new ProcessStartInfo();
startInfo.FileName = FileCheckPath;
startInfo.Arguments = String.Join(' ', args);
startInfo.CreateNoWindow = true;
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
try
{
using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
using (var proc = Process.Start(startInfo))
{
if (proc == null)
{
return new FileCheckResult(1, String.Empty, String.Empty);
}
var stdOut = new StringBuilder();
var stdErr = new StringBuilder();
proc.OutputDataReceived += (_, e) =>
{
if (e.Data == null)
{
outputWaitHandle.Set();
}
else
{
stdOut.AppendLine(e.Data);
}
};
proc.ErrorDataReceived += (_, e) =>
{
if (e.Data == null)
{
errorWaitHandle.Set();
}
else
{
stdErr.AppendLine(e.Data);
}
};
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
await proc.WaitForExitAsync();
outputWaitHandle.WaitOne();
errorWaitHandle.WaitOne();
var exitCode = proc.ExitCode;
return new FileCheckResult(exitCode, stdOut.ToString(), stdErr.ToString());
}
}
catch (Exception ex)
{
return new FileCheckResult(1, String.Empty, ex.Message);
}
}
/// <summary>
/// Get the method name from the method declaration.
/// </summary>
static string GetMethodName(MethodDeclarationSyntax methodDecl)
{
var methodName = methodDecl.Identifier.ValueText;
var typeArity = methodDecl.TypeParameterList?.ChildNodes().Count();
if (typeArity > 0)
{
methodName = $"{methodName}[*]";
}
return $"{methodName}(*)";
}
/// <summary>
/// Get the enclosing type declaration syntax of the given node.
/// Errors if it cannot find one.
/// </summary>
static TypeDeclarationSyntax GetEnclosingTypeDeclaration(SyntaxNode node)
{
var typeDecl = node.Ancestors().OfType<TypeDeclarationSyntax>().FirstOrDefault();
if (typeDecl == null)
{
throw new SuperFileCheckException($"Unable to find enclosing type declaration on: {node.Span}");
}
return typeDecl;
}
/// <summary>
/// Get all the acestoral enclosing type declaration syntaxes of the given node.
/// </summary>
static TypeDeclarationSyntax[] GetEnclosingTypeDeclarations(SyntaxNode node)
{
return node.Ancestors().OfType<TypeDeclarationSyntax>().ToArray();
}
/// <summary>
/// Try to get an enclosing type name from the given syntax node.
/// </summary>
static string GetTypeName(TypeDeclarationSyntax typeDecl)
{
var typeName = typeDecl.Identifier.ValueText;
var typeArity = typeDecl.TypeParameterList?.ChildNodes().Count();
if (typeArity > 0)
{
typeName = $"{typeName}`{typeArity}[*]";
}
return typeName;
}
/// <summary>
/// Get the method's fully qualified enclosing namespace and type name.
/// </summary>
static string GetFullyQualifiedEnclosingTypeName(MethodDeclarationSyntax methodDecl)
{
var qualifiedTypeName = String.Empty;
var typeDecl = GetEnclosingTypeDeclaration(methodDecl);
qualifiedTypeName = GetTypeName(typeDecl);
var typeDecls = GetEnclosingTypeDeclarations(typeDecl);
for (var i = 0; i < typeDecls.Length; i++)
{
typeDecl = typeDecls[i];
qualifiedTypeName = $"{GetTypeName(typeDecl)}+{qualifiedTypeName}";
}
var namespaceDecl = typeDecl.Ancestors().OfType<NamespaceDeclarationSyntax>().FirstOrDefault();
if (namespaceDecl != null)
{
var identifiers =
namespaceDecl.Name.DescendantTokens().Where(x => x.IsKind(SyntaxKind.IdentifierToken)).Select(x => x.ValueText);
return $"{String.Join(".", identifiers)}.{qualifiedTypeName}";
}
return qualifiedTypeName;
}
/// <summary>
/// Get all the descendant single line comment trivia items.
/// </summary>
static IEnumerable<SyntaxTrivia> GetDescendantSingleLineCommentTrivia(SyntaxNode node)
{
return
node
.DescendantTrivia()
.Where(x => x.IsKind(SyntaxKind.SingleLineCommentTrivia));
}
/// <summary>
/// Gather all syntactical method declarations whose body contains
/// FileCheck syntax.
/// </summary>
static MethodDeclarationInfo[] FindMethodsByFile(string filePath, string[] checkPrefixes)
{
var syntaxTree = CSharpSyntaxTree.ParseText(SourceText.From(File.ReadAllText(filePath)));
var root = syntaxTree.GetRoot();
var trivia =
GetDescendantSingleLineCommentTrivia(root)
.Where(x =>
{
if (x.Token.Parent == null)
{
return true;
}
// A comment before the method declaration is considered a child of the method
// declaration. In this example:
//
// // trivia1
// public void M()
// {
// // trivia2
// }
//
// Both // trivia1 and // trivia2 are descendants of MethodDeclarationSyntax.
//
// We are only allowing checks to occur in 'trivia2'. The 'Contains' check is
// used to find 'trivia1'.
return !x.Token.Parent.Ancestors().Any(p => p.IsKind(SyntaxKind.MethodDeclaration) && p.Span.Contains(x.Span));
})
.Where(x => ContainsCheckPrefixes(x.ToString(), checkPrefixes))
.ToArray();
if (trivia.Length > 0)
{
throw new SuperFileCheckException("FileCheck syntax not allowed outside of a method.");
}
return
root
.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(x => {
var str = x.ToString();
VerifyCheckPrefixes(str, checkPrefixes);
return ContainsCheckPrefixes(str, checkPrefixes);
})
.Select(x => new MethodDeclarationInfo(x, $"{GetFullyQualifiedEnclosingTypeName(x)}:{GetMethodName(x)}"))
.ToArray();
}
/// <summary>
/// Helper to expand FileCheck syntax.
/// </summary>
static string? TryTransformDirective(string lineStr, string[] checkPrefixes, string syntaxDirective, string transformSuffix)
{
var index = lineStr.IndexOf(syntaxDirective);
if (index == -1)
{
return null;
}
var prefix = lineStr.Substring(0, index);
// Do not transform if the prefix is not part of --check-prefixes.
if (!checkPrefixes.Any(x => prefix.EndsWith(x)))
{
return null;
}
return lineStr.Substring(0, index) + $"{transformSuffix}: {{{{^ *}}}}" + lineStr.Substring(index + syntaxDirective.Length) + "{{$}}";
}
/// <summary>
/// Will try to transform a line containing custom SuperFileCheck syntax, e.g. "CHECK-FULL-LINE:"
/// to the appropriate FileCheck syntax.
/// </summary>
static string TransformLine(TextLine line, string[] checkPrefixes)
{
var text = line.Text;
if (text == null)
{
throw new InvalidOperationException("SourceText is null.");
}
var lineStr = text.ToString(line.Span);
var result = TryTransformDirective(lineStr, checkPrefixes, SyntaxDirectiveFullLine, String.Empty);
if (result != null)
{
return result;
}
result = TryTransformDirective(lineStr, checkPrefixes, SyntaxDirectiveFullLineNext, "-NEXT");
return result ?? lineStr;
}
/// <summary>
/// Will try to transform a method containing custom SuperFileCheck syntax, e.g. "CHECK-FULL-LINE:"
/// to the appropriate FileCheck syntax.
/// </summary>
static string TransformMethod(MethodDeclarationSyntax methodDecl, string[] checkPrefixes)
{
return String.Join(Environment.NewLine, methodDecl.GetText().Lines.Select(x => TransformLine(x, checkPrefixes)));
}
/// <summary>
/// Gets the starting line number of the method declaration.
/// </summary>
static int GetMethodStartingLineNumber(MethodDeclarationSyntax methodDecl)
{
var leadingTrivia = methodDecl.GetLeadingTrivia();
if (leadingTrivia.Count == 0)
{
return methodDecl.GetLocation().GetLineSpan().StartLinePosition.Line;
}
else
{
return leadingTrivia[0].GetLocation().GetLineSpan().StartLinePosition.Line;
}
}
/// <summary>
/// Returns only the method declaration text along with any SuperFileCheck transformations.
/// </summary>
static string PreProcessMethod(MethodDeclarationInfo methodDeclInfo, string[] checkPrefixes)
{
var methodDecl = methodDeclInfo.Syntax;
var methodName = methodDeclInfo.FullyQualifiedName.Replace("*", "{{.*}}"); // Change wild-card to FileCheck wild-card syntax.
// Create anchors from the first prefix.
var beginAnchorText = $"// {checkPrefixes[0]}-LABEL: BEGIN METHOD {methodName}";
var endAnchorText = $"// {checkPrefixes[0]}: END METHOD {methodName}";
// Create temp source file based on the source text of the method.
// Newlines are added to pad the text so FileCheck's error messages will correspond
// to the correct line and column of the original source file.
// This is not perfect but will work for most cases.
var lineNumber = GetMethodStartingLineNumber(methodDecl);
var tmpSrc = new StringBuilder();
for (var i = 1; i < lineNumber; i++)
{
tmpSrc.AppendLine(String.Empty);
}
tmpSrc.AppendLine(beginAnchorText);
tmpSrc.AppendLine(TransformMethod(methodDecl, checkPrefixes));
tmpSrc.AppendLine(endAnchorText);
return tmpSrc.ToString();
}
/// <summary>
/// Runs SuperFileCheck logic.
/// </summary>
static async Task<FileCheckResult> RunSuperFileCheckAsync(MethodDeclarationInfo methodDeclInfo, string[] args, string[] checkPrefixes, string tmpFilePath)
{
File.WriteAllText(tmpFilePath, PreProcessMethod(methodDeclInfo, checkPrefixes));
try
{
args[0] = tmpFilePath;
return await RunLLVMFileCheckAsync(args);
}
finally
{
try { File.Delete(tmpFilePath); } catch { }
}
}
/// <summary>
/// Checks if the argument is --csharp.
/// </summary>
static bool IsArgumentCSharp(string arg)
{
return arg.Equals(CommandLineArgumentCSharp);
}
/// <summary>
/// Checks if the argument is --csharp-list-method-names.
/// </summary>
static bool IsArgumentCSharpListMethodNames(string arg)
{
return arg.Equals(CommandLineArgumentCSharpListMethodNames);
}
/// <summary>
/// Checks if the argument contains -h.
/// </summary>
static bool ArgumentsContainHelp(string[] args)
{
return args.Any(x => x.Contains("-h"));
}
/// <summary>
/// From the given arguments, find the first --check-prefixes argument and parse its value
/// in the form of an array.
/// </summary>
static string[] ParseCheckPrefixes(string[] args)
{
var checkPrefixesArg = args.FirstOrDefault(x => x.StartsWith(CommandLineCheckPrefixesEqual));
if (checkPrefixesArg == null)
{
return new string[] { };
}
return
checkPrefixesArg
.Replace(CommandLineCheckPrefixesEqual, "")
.Split(",")
.Where(x => !String.IsNullOrWhiteSpace(x))
.ToArray();
}
/// <summary>
/// Will always return one or more prefixes.
/// </summary>
static string[] DetermineCheckPrefixes(string[] args)
{
var checkPrefixes = ParseCheckPrefixes(args);
if (checkPrefixes.Length == 0)
{
// FileCheck's default.
return new string[] { "CHECK" };
}
return checkPrefixes;
}
/// <summary>
/// Prints error expecting a CSharp file.
/// </summary>
static void PrintErrorExpectedCSharpFile()
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine("Expected C# file.");
Console.ResetColor();
}
/// <summary>
/// Prints error indicating a duplicate method name was found.
/// </summary>
static void PrintErrorDuplicateMethodName(string methodName)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine($"Duplicate method name found: {methodName}");
Console.ResetColor();
}
/// <summary>
/// Prints error indicating the method was not marked with attribute 'MethodImpl(MethodImplOptions.NoInlining)'.
/// </summary>
static void PrintErrorMethodNoInlining(string methodName)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine($"'{methodName}' is not marked with attribute 'MethodImpl(MethodImplOptions.NoInlining)'.");
Console.ResetColor();
}
/// <summary>
/// Prints error indicating that no methods were found to have any FileCheck syntax
/// of the given --check-prefixes.
/// </summary>
static void PrintErrorNoMethodsFound(string[] checkPrefixes)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine("No methods were found. Check if any method bodies are using one or more of the following FileCheck prefixes:");
foreach (var prefix in checkPrefixes)
{
Console.Error.WriteLine($" {prefix}");
}
Console.ResetColor();
}
/// <summary>
/// Prints error indicating that a --input-file was not found.
/// </summary>
static void PrintErrorNoInputFileFound()
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine($"{CommandLineInputFile} is required.");
Console.ResetColor();
}
/// <summary>
/// Prints command line help.
/// </summary>
static void PrintHelp()
{
Console.Write(Environment.NewLine);
Console.WriteLine("USAGE: SuperFileCheck [options] <check-file>");
Console.WriteLine("USAGE: SuperFileCheck <super-option> <check-file> [options]");
Console.Write(Environment.NewLine);
Console.WriteLine("SUPER OPTIONS:");
Console.Write(Environment.NewLine);
Console.WriteLine($" --csharp - A {CommandLineInputFile} is required.");
Console.WriteLine($" <check-file> must be a C# source file.");
Console.WriteLine($" Methods must not have duplicate names.");
Console.WriteLine($" Methods must be marked as not inlining.");
Console.WriteLine($" One or more methods are required.");
Console.WriteLine($" Prefixes are determined by {CommandLineCheckPrefixes}.");
Console.WriteLine($" --csharp-list-method-names - Print a space-delimited list of method names to be");
Console.WriteLine($" supplied to environment variable DOTNET_JitDisasm.");
Console.WriteLine($" <check-file> must be a C# source file.");
Console.WriteLine($" Methods must not have duplicate names.");
Console.WriteLine($" Methods must be marked as not inlining.");
Console.WriteLine($" Prints nothing if no methods are found.");
Console.WriteLine($" Prefixes are determined by {CommandLineCheckPrefixes}.");
}
/// <summary>
/// Try to find the first duplicate method name of the given method declarations.
/// </summary>
static string? TryFindDuplicateMethodName(MethodDeclarationInfo[] methodDeclInfos)
{
var set = new HashSet<string>();
var duplicateMethodDeclInfo =
methodDeclInfos.FirstOrDefault(x => !set.Add(x.FullyQualifiedName));
return duplicateMethodDeclInfo.FullyQualifiedName;
}
/// <summary>
/// Is the method marked with MethodImpl(MethodImplOptions.NoInlining)?
/// </summary>
static bool MethodHasNoInlining(MethodDeclarationSyntax methodDecl)
{
return methodDecl.AttributeLists.ToString().Contains("MethodImplOptions.NoInlining");
}
/// <summary>
/// Will print an error if any duplicate method names are found.
/// </summary>
static bool CheckDuplicateMethodNames(MethodDeclarationInfo[] methodDeclInfos)
{
var duplicateMethodName = TryFindDuplicateMethodName(methodDeclInfos);
if (duplicateMethodName != null)
{
PrintErrorDuplicateMethodName(duplicateMethodName);
return false;
}
return true;
}
/// <summary>
/// Will print an error if the methods containing FileCheck syntax
/// is not marked with the attribute 'MethodImpl(MethodImplOptions.NoInlining)'.
/// </summary>
static bool CheckMethodsHaveNoInlining(MethodDeclarationInfo[] methodDeclInfos)
{
return
methodDeclInfos
.All(methodDeclInfo =>
{
if (!MethodHasNoInlining(methodDeclInfo.Syntax))
{
PrintErrorMethodNoInlining(methodDeclInfo.FullyQualifiedName);
return false;
}
return true;
});
}
/// <summary>
/// The goal of SuperFileCheck is to make writing LLVM FileCheck tests against the
/// NET Core Runtime easier in C#.
/// </summary>
static async Task<int> Main(string[] args)
{
if (args.Length >= 1)
{
if (IsArgumentCSharpListMethodNames(args[0]))
{
if (args.Length == 1)
{
PrintErrorExpectedCSharpFile();
return 1;
}
var checkPrefixes = DetermineCheckPrefixes(args);
try
{
var methodDeclInfos = FindMethodsByFile(args[1], checkPrefixes);
if (methodDeclInfos.Length == 0)
{
return 0;
}
if (!CheckDuplicateMethodNames(methodDeclInfos))
{
return 1;
}
Console.Write(String.Join(' ', methodDeclInfos.Select(x => x.FullyQualifiedName)));
return 0;
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
Console.ResetColor();
return 1;
}
}
if (IsArgumentCSharp(args[0]))
{
if (args.Length == 1)
{
PrintErrorExpectedCSharpFile();
return 1;
}
var checkFilePath = args[1];
var checkFileNameNoExt = Path.GetFileNameWithoutExtension(checkFilePath);
var hasInputFile = args.Any(x => x.Equals(CommandLineInputFile));
if (!hasInputFile)
{
PrintErrorNoInputFileFound();
return 1;
}
var checkPrefixes = DetermineCheckPrefixes(args);
try
{
var methodDeclInfos = FindMethodsByFile(checkFilePath, checkPrefixes);
if (!CheckDuplicateMethodNames(methodDeclInfos))
{
return 1;
}
if (!CheckMethodsHaveNoInlining(methodDeclInfos))
{
return 1;
}
if (methodDeclInfos.Length > 0)
{
var didSucceed = true;
var tasks = new Task<FileCheckResult>[methodDeclInfos.Length];
// Remove the first 'csharp' argument so we can pass the rest of the args
// to LLVM FileCheck.
var argsToCopy = args.AsSpan(1).ToArray();
for (int i = 0; i < methodDeclInfos.Length; i++)
{
var index = i;
var tmpFileName = $"__tmp{index}_{checkFileNameNoExt}.cs";
var tmpDirName = Path.GetDirectoryName(checkFilePath);
string tmpFilePath;
if (String.IsNullOrWhiteSpace(tmpDirName))
{
tmpFilePath = tmpFileName;
}
else
{
tmpFilePath = Path.Combine(tmpDirName, tmpFileName);
}
tasks[i] = Task.Run(() => RunSuperFileCheckAsync(methodDeclInfos[index], argsToCopy.ToArray(), checkPrefixes, tmpFilePath));
}
await Task.WhenAll(tasks);
foreach (var x in tasks)
{
if (x.Result.ExitCode != 0)
{
didSucceed = false;
}
Console.Write(x.Result.StandardOutput);
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.Write(x.Result.StandardError);
Console.ResetColor();
}
return didSucceed ? 0 : 1;
}
else
{
PrintErrorNoMethodsFound(checkPrefixes);
return 1;
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
Console.ResetColor();
return 1;
}
}
}
var result = await RunLLVMFileCheckAsync(args);
Console.Write(result.StandardOutput);
if (ArgumentsContainHelp(args))
{
PrintHelp();
}
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.Write(result.StandardError);
Console.ResetColor();
return result.ExitCode;
}
}
}
|