src/StructuredLogViewer.Core/PreprocessedFileManager.cs (280 lines of code) (raw):

using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Logging.StructuredLogger; using Bucket = System.Collections.Generic.HashSet<StructuredLogViewer.ProjectImport>; namespace StructuredLogViewer { public class PreprocessedFileManager { private readonly Build build; private readonly SourceFileResolver sourceFileResolver; private readonly Dictionary<string, Dictionary<string, Bucket>> importMapsPerEvaluation = new Dictionary<string, Dictionary<string, Bucket>>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, string> preprocessedFileCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); public PreprocessedFileManager(Build build, SourceFileResolver sourceFileResolver) { this.build = build; this.sourceFileResolver = sourceFileResolver; BuildImportMap(); } public event Action<string> DisplayFile; public void BuildImportMap() { var evaluation = build.FindChild<NamedNode>(Strings.Evaluation); if (evaluation == null) { return; } foreach (var projectEvaluation in evaluation.Children.OfType<ProjectEvaluation>()) { var imports = projectEvaluation.FindChild<NamedNode>(Strings.Imports); if (imports == null) { continue; } // under which circumstances can this happen? // https://github.com/KirillOsenkov/MSBuildStructuredLog/issues/274 if (projectEvaluation.ProjectFile == null) { continue; } imports.VisitAllChildren<Import>(import => VisitImport(import, projectEvaluation)); } } private void VisitImport(Import import, ProjectEvaluation projectEvaluation) { if (sourceFileResolver.HasFile(import.ProjectFilePath) && sourceFileResolver.HasFile(import.ImportedProjectFilePath) && !string.IsNullOrEmpty(projectEvaluation.ProjectFile)) { var importMap = GetOrCreateImportMap(GetEvaluationKey(projectEvaluation), import.ProjectFilePath); AddImport(importMap, import.ProjectFilePath, import.ImportedProjectFilePath, import.Line, import.Column); } } public static string GetEvaluationKey(ProjectEvaluation evaluation) => evaluation == null ? null : evaluation.ProjectFile + evaluation.Id.ToString(); public static string GetEvaluationKey(Project project) { if (project == null) { return null; } if (project.EvaluationId == BuildEventContext.InvalidEvaluationId) { return project.ProjectFile; } return project.ProjectFile + project.EvaluationId.ToString(); } private Dictionary<string, Bucket> GetOrCreateImportMap(string key, string projectFilePath) { if (!importMapsPerEvaluation.TryGetValue(key, out var importMap)) { importMap = new Dictionary<string, Bucket>(StringComparer.OrdinalIgnoreCase); importMapsPerEvaluation[key] = importMap; // we want to have a "default" import map for each project, without specifying an evaluation id // this is when we click on a project and want to preprocess (we currently don't know which // evaluation id is associated with this project). // TODO: improve this when https://github.com/dotnet/msbuild/issues/4926 is fixed. importMapsPerEvaluation[projectFilePath] = importMap; } return importMap; } private Dictionary<string, Bucket> GetImportMap(string projectEvaluationKey) { if (projectEvaluationKey != null && importMapsPerEvaluation.TryGetValue(projectEvaluationKey, out var importMap)) { return importMap; } return null; } private int CorrectForMultilineTag(SourceText text, int lineNumber, string startText = "<Import", string endText = "/>") { // can happen for corrupt binlogs where some files have no text // see https://github.com/KirillOsenkov/MSBuildStructuredLog/issues/258 if (lineNumber < 0 || lineNumber >= text.Lines.Count) { return 0; } var lineText = text.GetLineText(lineNumber); if (lineText.Contains(startText)) { while (!lineText.Contains(endText) && lineNumber < text.Lines.Count - 1) { lineNumber++; lineText = text.GetLineText(lineNumber); } } return lineNumber; } private static void AddImport(Dictionary<string, Bucket> importMap, string project, string importedProject, int line, int column) { if (line > 0) { // convert to 0-based from 1-based line--; } if (!importMap.TryGetValue(project, out var bucket)) { bucket = new Bucket(); importMap[project] = bucket; } bucket.Add(new ProjectImport(importedProject, line, column)); } public Action GetPreprocessAction(string sourceFilePath, string preprocessEvaluationContext) { if (!CanPreprocess(sourceFilePath, preprocessEvaluationContext)) { return null; } return () => ShowPreprocessed(sourceFilePath, preprocessEvaluationContext); } public string GetPreprocessedText(string sourceFilePath, string projectEvaluationContext) { string preprocessedFileCacheKey = projectEvaluationContext + sourceFilePath; if (preprocessedFileCache.TryGetValue(preprocessedFileCacheKey, out var result)) { return result; } var sourceText = sourceFileResolver.GetSourceFileText(sourceFilePath); var importMap = GetImportMap(projectEvaluationContext); if (importMap == null) { return string.Empty; } if (importMap.TryGetValue(sourceFilePath, out var imports) && imports.Count > 0 && !string.IsNullOrWhiteSpace(sourceText.Text)) { result = GetPreprocessedTextCore(projectEvaluationContext, sourceText, imports); } else { result = sourceText.Text; if (sourceText.GetLineText(0).Contains("<?xml")) { result = result.Substring(sourceText.Lines[0].Length); } } preprocessedFileCache[preprocessedFileCacheKey] = result; return result; } private string GetPreprocessedTextCore(string projectEvaluationContext, SourceText sourceText, Bucket imports) { string result; var sb = new StringBuilder(); int line = 0; if (sourceText.GetLineText(line).Contains("<?xml")) { line++; } var importsList = imports.OrderBy(i => i.Line).ToList(); var sdkProps = importsList.FirstOrDefault(i => i.ProjectPath.EndsWith("Sdk.props", StringComparison.OrdinalIgnoreCase) && i.Line == 0 && i.Column == 0); if (sdkProps != default) { while (sourceText.GetLineText(line) is string firstLine && !firstLine.Contains("<Project")) { sb.AppendLine(firstLine); line++; } line = SkipTag(sourceText, sb, line, line, "<Project", ">"); InjectImportedProject(projectEvaluationContext, sb, sdkProps); importsList.Remove(sdkProps); } var sdkTargets = importsList.FirstOrDefault(i => i.ProjectPath.EndsWith("Sdk.targets", StringComparison.OrdinalIgnoreCase) && i.Line == 0 && i.Column == 0); if (sdkTargets != default) { importsList.Remove(sdkTargets); } foreach (var import in importsList) { line = SkipTag(sourceText, sb, line, import.Line); InjectImportedProject(projectEvaluationContext, sb, import); } int count = sourceText.Lines.Count; for (; line < count; line++) { var lastLineText = sourceText.GetLineText(line); if (lastLineText.Contains("</Project>")) { if (sdkTargets != default) { InjectImportedProject(projectEvaluationContext, sb, sdkTargets); sdkTargets = default; } } if (line < count - 1 || lastLineText.Length > 0) { sb.AppendLine(lastLineText); } } result = sb.ToString(); return result; } private void InjectImportedProject(string projectEvaluationContext, StringBuilder sb, ProjectImport import) { string projectPath = import.ProjectPath; var importText = GetPreprocessedText(projectPath, projectEvaluationContext); sb.AppendLine($"<!-- ======== {projectPath} ======= -->"); sb.Append(importText); if (!importText.EndsWith("\n")) { sb.AppendLine(); } sb.AppendLine($"<!-- ======== END OF {projectPath} ======= -->"); } private int SkipTag(SourceText sourceText, StringBuilder sb, int line, int lineNumber, string startText = "<Import", string endText = "/>") { var elementEndLine = CorrectForMultilineTag(sourceText, lineNumber, startText, endText); for (; line <= elementEndLine; line++) { sb.AppendLine(sourceText.GetLineText(line)); } return line; } public static string GetNodeEvaluationKey(object node) { if (node is TreeNode treeNode) { var project = treeNode.GetNearestParentOrSelf<Project>(); if (project != null) { return GetEvaluationKey(project); } var evaluation = treeNode.GetNearestParentOrSelf<ProjectEvaluation>(); if (evaluation != null) { return GetEvaluationKey(evaluation); } } return null; } public void ShowPreprocessed(IPreprocessable preprocessable) { if (preprocessable == null) { return; } ShowPreprocessed(preprocessable.RootFilePath, GetNodeEvaluationKey(preprocessable)); } public void ShowPreprocessed(string sourceFilePath, string projectContext) { if (sourceFilePath == null || projectContext == null) { return; } var preprocessedText = GetPreprocessedText(sourceFilePath, projectContext); if (preprocessedText == null) { return; } var filePath = SettingsService.WriteContentToTempFileAndGetPath(preprocessedText, ".xml"); DisplayFile?.Invoke(filePath); } public bool CanPreprocess(IPreprocessable preprocessable) { string sourceFilePath = preprocessable.RootFilePath; string projectContext = GetNodeEvaluationKey(preprocessable); return CanPreprocess(sourceFilePath, projectContext); } public bool CanPreprocess(string sourceFilePath, string projectEvaluationKey) { return sourceFilePath != null && sourceFileResolver.HasFile(sourceFilePath) && GetImportMap(projectEvaluationKey) is Dictionary<string, Bucket> importMap && importMap.TryGetValue(sourceFilePath, out var bucket) && bucket.Count > 0; } } }