using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using System.IO; using System.Linq; using System.Collections.Generic; using Semmle.Util.Logging; using Semmle.Util; namespace Semmle.Extraction.CSharp { public class TracingAnalyser : Analyser, IDisposable { private Entities.Compilation? compilationEntity; private IDisposable? compilationTrapFile; private bool init; public TracingAnalyser(IProgressMonitor pm, ILogger logger, bool addAssemblyTrapPrefix, PathTransformer pathTransformer) : base(pm, logger, addAssemblyTrapPrefix, pathTransformer) { } /// /// Start initialization of the analyser. /// /// The arguments passed to Roslyn. /// A Boolean indicating whether to proceed with extraction. public bool BeginInitialize(IEnumerable roslynArgs) { return init = LogRoslynArgs(roslynArgs, Extraction.Extractor.Version); } /// /// End initialization of the analyser. /// /// Arguments passed to csc. /// Extractor options. /// The Roslyn compilation. /// A Boolean indicating whether to proceed with extraction. public void EndInitialize( CSharpCommandLineArguments commandLineArguments, CommonOptions options, CSharpCompilation compilation) { if (!init) throw new InternalError("EndInitialize called without BeginInitialize returning true"); this.layout = new Layout(); this.options = options; this.compilation = compilation; this.extractor = new TracingExtractor(GetOutputName(compilation, commandLineArguments), Logger, PathTransformer); LogDiagnostics(); SetReferencePaths(); CompilationErrors += FilteredDiagnostics.Count(); } public override void Dispose() { compilationTrapFile?.Dispose(); base.Dispose(); } /// /// Extracts compilation-wide entities, such as compilations and compiler diagnostics. /// public void AnalyseCompilation() { extractionTasks.Add(() => DoAnalyseCompilation()); } /// /// Logs information about the extractor, as well as the arguments to Roslyn. /// /// The arguments passed to Roslyn. /// A Boolean indicating whether the same arguments have been logged previously. private bool LogRoslynArgs(IEnumerable roslynArgs, string extractorVersion) { LogExtractorInfo(extractorVersion); Logger.Log(Severity.Info, $" Arguments to Roslyn: {string.Join(' ', roslynArgs)}"); var tempFile = Extractor.GetCSharpArgsLogPath(Path.GetRandomFileName()); bool argsWritten; using (var streamWriter = new StreamWriter(new FileStream(tempFile, FileMode.Append, FileAccess.Write))) { streamWriter.WriteLine($"# Arguments to Roslyn: {string.Join(' ', roslynArgs.Where(arg => !arg.StartsWith('@')))}"); argsWritten = roslynArgs.WriteCommandLine(streamWriter); } var hash = FileUtils.ComputeFileHash(tempFile); var argsFile = Extractor.GetCSharpArgsLogPath(hash); if (argsWritten) Logger.Log(Severity.Info, $" Arguments have been written to {argsFile}"); if (File.Exists(argsFile)) { try { File.Delete(tempFile); } catch (IOException e) { Logger.Log(Severity.Warning, $" Failed to remove {tempFile}: {e.Message}"); } return false; } try { File.Move(tempFile, argsFile); } catch (IOException e) { Logger.Log(Severity.Warning, $" Failed to move {tempFile} to {argsFile}: {e.Message}"); } return true; } /// /// Determine the path of the output dll/exe. /// /// Information about the compilation. /// Cancellation token required. /// The filename. private static string GetOutputName(CSharpCompilation compilation, CSharpCommandLineArguments commandLineArguments) { // There's no apparent way to access the output filename from the compilation, // so we need to re-parse the command line arguments. if (commandLineArguments.OutputFileName is null) { // No output specified: Use name based on first filename var entry = compilation.GetEntryPoint(System.Threading.CancellationToken.None); if (entry is null) { if (compilation.SyntaxTrees.Length == 0) throw new InvalidOperationException("No source files seen"); // Probably invalid, but have a go anyway. var entryPointFile = compilation.SyntaxTrees.First().FilePath; return Path.ChangeExtension(entryPointFile, ".exe"); } var entryPointFilename = entry.Locations.First().SourceTree!.FilePath; return Path.ChangeExtension(entryPointFilename, ".exe"); } return Path.Combine(commandLineArguments.OutputDirectory, commandLineArguments.OutputFileName); } #nullable disable warnings /// /// Logs detailed information about this invocation, /// in the event that errors were detected. /// /// A Boolean indicating whether to proceed with extraction. private void LogDiagnostics() { foreach (var error in FilteredDiagnostics) { Logger.Log(Severity.Error, " Compilation error: {0}", error); } if (FilteredDiagnostics.Any()) { foreach (var reference in compilation.References) { Logger.Log(Severity.Info, " Resolved reference {0}", reference.Display); } } } private static readonly HashSet errorsToIgnore = new HashSet { "CS7027", // Code signing failure "CS1589", // XML referencing not supported "CS1569" // Error writing XML documentation }; private IEnumerable FilteredDiagnostics { get { return extractor is null || extractor.Standalone || compilation is null ? Enumerable.Empty() : compilation. GetDiagnostics(). Where(e => e.Severity >= DiagnosticSeverity.Error && !errorsToIgnore.Contains(e.Id)); } } private void DoAnalyseCompilation() { try { var assemblyPath = ((TracingExtractor?)extractor).OutputPath; var transformedAssemblyPath = PathTransformer.Transform(assemblyPath); var assembly = compilation.Assembly; var projectLayout = layout.LookupProjectOrDefault(transformedAssemblyPath); var trapWriter = projectLayout.CreateTrapWriter(Logger, transformedAssemblyPath, options.TrapCompression, discardDuplicates: false); compilationTrapFile = trapWriter; // Dispose later var cx = new Context(extractor, compilation.Clone(), trapWriter, new AssemblyScope(assembly, assemblyPath), addAssemblyTrapPrefix); compilationEntity = Entities.Compilation.Create(cx); } catch (Exception ex) // lgtm[cs/catch-of-all-exceptions] { Logger.Log(Severity.Error, " Unhandled exception analyzing {0}: {1}", "compilation", ex); } } public void LogPerformance(Entities.PerformanceMetrics p) => compilationEntity.PopulatePerformance(p); #nullable restore warnings } }