diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 088e658..cd916d9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -45,7 +45,7 @@ variables: value: 'staging' - name: storageAccountName value: 'netsourceindexprod' - - name: stagingHost + - name: deploymentHost value: 'staging.source.dot.net' - ${{ else }}: - name: poolName @@ -58,6 +58,8 @@ variables: value: 'validation' - name: storageAccountName value: 'netsourceindexvalidprod' + - name: deploymentHost + value: 'validation.source.dot.net' extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates @@ -221,26 +223,26 @@ extends: inlineScript: | az webapp restart --name $(webAppName) --slot $(temporaryDeploymentSlot) --resource-group $(resourceGroupName) - # FIXME: Health endpoints disabled till they can be audited: "https://$(stagingHost)/health", "https://$(stagingHost)/health/alive" + # FIXME: Health endpoints disabled till they can be audited: "https://$(deploymentHost)/health", "https://$(deploymentHost)/health/alive" - pwsh: | Start-Sleep 60 $urls = @( - "https://$(stagingHost)" - "https://$(stagingHost)/System.Private.CoreLib/src/runtime/src/libraries/System.Private.CoreLib/src/System/String.cs.html" + "https://$(deploymentHost)" + "https://$(deploymentHost)/System.Private.CoreLib/src/runtime/src/libraries/System.Private.CoreLib/src/System/String.cs.html" ) foreach ($url in $urls) { Write-Host "Testing URL: $url" try { $statusCode = Invoke-WebRequest $url -UseBasicParsing -SkipHttpErrorCheck | select -ExpandProperty StatusCode if ($statusCode -ne 200) { - Write-Error "##vso[task.logissue type=warning;]Deployed staging website returned unexpected status code $statusCode from url $url" + Write-Error "##vso[task.logissue type=warning;]Deployed website returned unexpected status code $statusCode from url $url" } } catch { Write-Error "##vso[task.logissue type=warning;]Failed to test URL $url : $_" } } displayName: 🟣Test Deployed WebApp - condition: and(succeeded(), eq(variables['isOfficialBuild'], 'True')) + condition: succeeded() - task: AzureCLI@2 displayName: 🟣Swap Staging Slot into Production diff --git a/src/SourceBrowser/src/BinLogParser/BinLogParser.csproj b/src/SourceBrowser/src/BinLogParser/BinLogParser.csproj index 9701513..70ad922 100644 --- a/src/SourceBrowser/src/BinLogParser/BinLogParser.csproj +++ b/src/SourceBrowser/src/BinLogParser/BinLogParser.csproj @@ -13,6 +13,7 @@ + diff --git a/src/SourceBrowser/src/BinLogParser/CompilerInvocationScoring.cs b/src/SourceBrowser/src/BinLogParser/CompilerInvocationScoring.cs new file mode 100644 index 0000000..6b55dcf --- /dev/null +++ b/src/SourceBrowser/src/BinLogParser/CompilerInvocationScoring.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using NuGet.Frameworks; + +namespace Microsoft.SourceBrowser.BinLogParser +{ + public static class CompilerInvocationScoring + { + public static int CalculateScore(string targetFramework, int sourceFileCount) + { + var score = sourceFileCount; + if (!string.IsNullOrEmpty(targetFramework)) + { + var framework = NuGetFramework.Parse(targetFramework); + if (framework.Version != null) + { + score += framework.Version.Major * 1000 + framework.Version.Minor * 100; + } + + if (framework.HasPlatform) + { + score += 500; + if (framework.Platform.Equals("linux", StringComparison.OrdinalIgnoreCase)) + { + score += 100; + } + else if (framework.Platform.Equals("unix", StringComparison.OrdinalIgnoreCase)) + { + score += 50; + } + } + } + + return score; + } + + public static bool IsReferenceAssembly( + string projectDirectory, + StringComparison comparison = StringComparison.Ordinal) + { + var projectFolder = Path.GetFileName(projectDirectory); + return string.Equals(projectFolder, "ref", comparison) || + string.Equals(projectFolder, "stubs", comparison) || + string.Equals(Path.GetFileName(Path.GetDirectoryName(projectDirectory)), "cycle-breakers", comparison); + } + } +} diff --git a/src/SourceBrowser/src/BinLogToSln/BinLogToSln.csproj b/src/SourceBrowser/src/BinLogToSln/BinLogToSln.csproj index 31dc560..90243dd 100644 --- a/src/SourceBrowser/src/BinLogToSln/BinLogToSln.csproj +++ b/src/SourceBrowser/src/BinLogToSln/BinLogToSln.csproj @@ -14,7 +14,6 @@ - diff --git a/src/SourceBrowser/src/BinLogToSln/Program.cs b/src/SourceBrowser/src/BinLogToSln/Program.cs index 560b311..8b14861 100644 --- a/src/SourceBrowser/src/BinLogToSln/Program.cs +++ b/src/SourceBrowser/src/BinLogToSln/Program.cs @@ -4,7 +4,6 @@ using Microsoft.CodeAnalysis.Emit; using Microsoft.SourceBrowser.BinLogParser; using Mono.Options; -using NuGet.Frameworks; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -69,49 +68,17 @@ internal static int CalculateInvocationScore(CompilerInvocation invocation) score -= 10000; // Heavy penalty for platform not supported assemblies } - // 3. Newest TargetFramework version (third priority) - if (invocation.ProjectProperties?.TryGetValue("TargetFramework", out var targetFramework) == true && - !string.IsNullOrEmpty(targetFramework)) + var sourceFileCount = invocation.Parsed?.SourceFiles.Length ?? 0; + string targetFramework = null; + invocation.ProjectProperties?.TryGetValue("TargetFramework", out targetFramework); + try { - try - { - var framework = NuGetFramework.Parse(targetFramework); - - // Prefer newer frameworks (high weight) - if (framework.Version != null) - { - score += (int)(framework.Version.Major * 1000 + framework.Version.Minor * 100); - } - - // 4. Has a platform (fourth priority) - // Prefer platform-specific frameworks - if (framework.HasPlatform) - { - score += 500; - - if (framework.Platform.Equals("linux", StringComparison.OrdinalIgnoreCase)) - { - score += 100; // Linux is preferred over other platforms - } - else if (framework.Platform.Equals("unix", StringComparison.OrdinalIgnoreCase)) - { - score += 50; // Unix is also preferred, but less than Linux - } - } - - } - catch (Exception ex) - { - Console.WriteLine($"Warning: Could not parse TargetFramework '{targetFramework}': {ex.Message}"); - } + score += CompilerInvocationScoring.CalculateScore(targetFramework, sourceFileCount); } - - // 5. More source files (lowest priority) - var sourceFiles = invocation.Parsed?.SourceFiles; - if (sourceFiles.HasValue) + catch (Exception ex) when (ex is ArgumentException || ex is NuGet.Frameworks.FrameworkException) { - int totalSourceFiles = sourceFiles.Value.Length; - score += totalSourceFiles; // Lower weight than other factors + Console.WriteLine($"Warning: Could not parse TargetFramework '{targetFramework}': {ex.Message}"); + score += sourceFileCount; } } catch (Exception ex) @@ -131,16 +98,9 @@ private static bool ShouldExcludeInvocation(CompilerInvocation invocation) return true; } - string projectFolder = Path.GetFileName(invocation.ProjectDirectory); - if (projectFolder == "ref" || projectFolder == "stubs") - { - Console.WriteLine($"Skipping Ref Assembly project {invocation.ProjectFilePath}"); - return true; - } - - if (Path.GetFileName(Path.GetDirectoryName(invocation.ProjectDirectory)) == "cycle-breakers") + if (CompilerInvocationScoring.IsReferenceAssembly(invocation.ProjectDirectory)) { - Console.WriteLine($"Skipping Wpf Cycle-Breaker project {invocation.ProjectFilePath}"); + Console.WriteLine($"Skipping reference assembly or cycle-breaker project {invocation.ProjectFilePath}"); return true; } diff --git a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs index ec22090..b751684 100644 --- a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs +++ b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs @@ -1,5 +1,6 @@ using System.IO; using System.Linq; +using Microsoft.Build.Locator; using Microsoft.CodeAnalysis; using Microsoft.SourceBrowser.HtmlGenerator; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -11,6 +12,12 @@ namespace HtmlGenerator.Tests [TestClass] public class CompilerLogAssemblyNameTests { + [ClassInitialize] + public static void Initialize(TestContext context) + { + MSBuildLocator.RegisterDefaults(); + } + private static SolutionInfo CreateSolutionInfo(params string[] assemblyNames) { var projects = assemblyNames.Select(name => @@ -71,6 +78,95 @@ public void Implementation_compilation_precedes_reference_assembly_with_the_same .ShouldBe(new[] { implementation.Id, unrelated.Id, referenceAssembly.Id }); } + [TestMethod] + public void Compiler_source_count_takes_priority_over_injected_generated_documents() + { + var generatedHeavyProject = CreateProjectInfo("A.dll", documentCount: 10); + var implementation = CreateProjectInfo("A.dll", documentCount: 1); + var original = SolutionInfo.Create( + SolutionId.CreateNewId(), + VersionStamp.Default, + projects: new[] { generatedHeavyProject, implementation }); + + var normalized = SolutionGenerator.NormalizeCompilerLogAssemblyNames( + original, + projectScores: new[] { 1, 10 }, + projectFilePaths: SolutionGenerator.CreateCompilerLogProjectPathMap( + original, new[] { @"D:\repo\Generated.csproj", @"D:\repo\Implementation.csproj" })); + + normalized.Projects.Select(p => p.Id) + .ShouldBe(new[] { implementation.Id, generatedHeavyProject.Id }); + + using var workspace = new AdhocWorkspace(); + workspace.AddSolution(normalized); + workspace.CurrentSolution.Projects.Select(p => p.Id) + .ShouldBe(new[] { implementation.Id, generatedHeavyProject.Id }); + } + + [TestMethod] + [DataRow(2, 2, false)] + [DataRow(2, 2, true)] + [DataRow(1, 2, false)] + [DataRow(0, 2, false)] + [DataRow(2, 1, false)] + [DataRow(2, 0, false)] + [DataRow(2, 3, false)] + public void Compiler_log_scores_require_complete_aligned_metadata(int pathCount, int scoreCount, bool reversePaths) + { + var first = CreateProjectInfo("A.dll", documentCount: 1).WithFilePath("First.csproj"); + var second = CreateProjectInfo("A.dll", documentCount: 10).WithFilePath("Second.csproj"); + var solution = SolutionInfo.Create( + SolutionId.CreateNewId(), VersionStamp.Default, projects: new[] { first, second }); + var paths = new[] { @"D:\repo\First.csproj", @"D:\repo\Second.csproj" }.Take(pathCount); + var pathMap = SolutionGenerator.CreateCompilerLogProjectPathMap( + solution, (reversePaths ? paths.Reverse() : paths).ToArray()); + + var normalized = SolutionGenerator.NormalizeCompilerLogAssemblyNames( + solution, + projectScores: new[] { 12_000, 11_000, 10_000 }.Take(scoreCount).ToArray(), + projectFilePaths: pathMap); + + normalized.Projects.Select(project => project.Id).ShouldBe( + pathCount == 2 && scoreCount == 2 && !reversePaths + ? new[] { first.Id, second.Id } + : new[] { second.Id, first.Id }); + } + + [TestMethod] + [DataRow("ref")] + [DataRow("REF")] + [DataRow("stubs")] + [DataRow(@"cycle-breakers\Foo")] + public void Compiler_log_project_score_deprioritizes_reference_assemblies(string projectFolder) + { + SolutionGenerator.CalculateCompilerLogProjectScore( + Path.Combine(@"D:\repo", projectFolder, "Foo.csproj"), + "net11.0", + sourceFileCount: 10_000) + .ShouldBe(int.MinValue); + SolutionGenerator.CalculateCompilerLogProjectScore( + @"D:\repo\src\libraries\Foo\src\Foo.csproj", + "net11.0", + sourceFileCount: 100) + .ShouldBeGreaterThan(int.MinValue); + } + + [TestMethod] + [DataRow("net10.0", 10_100)] + [DataRow("net11.0", 11_100)] + [DataRow("net11.0-linux", 11_700)] + [DataRow("net11.0-unix", 11_650)] + [DataRow("net11.0-windows10.0.19041.0", 11_600)] + [DataRow("net48", 4_900)] + [DataRow("netstandard2.1", 2_200)] + [DataRow(null, 100)] + [DataRow("invalid-framework-name", 100)] + public void Compiler_log_project_score_matches_binlog_weights(string targetFramework, int expected) + { + SolutionGenerator.CalculateCompilerLogProjectScore( + @"D:\repo\Foo.csproj", targetFramework, sourceFileCount: 100).ShouldBe(expected); + } + [TestMethod] public void Compiler_log_document_folders_match_BinLogToSln_link_behavior() { @@ -99,7 +195,7 @@ public void Compiler_log_document_folders_match_BinLogToSln_link_behavior() projects: new[] { project }); var normalized = SolutionGenerator.NormalizeCompilerLogAssemblyNames( solution, - compilerLogRepositoryRoots: new[] { repositoryRoot }); + compilerLogDocumentRoots: new[] { repositoryRoot }); var documents = normalized.Projects.Single().Documents.ToDictionary(d => d.Name); documents["Local.cs"].Folders.ShouldBe(new[] { "Internal" }); @@ -130,6 +226,47 @@ public void Existing_compiler_log_document_folders_are_preserved() normalized.ShouldBeSameAs(document); } + [TestMethod] + public void Compiler_log_parent_traversal_is_normalized_to_repository_relative_path() + { + const string repositoryRoot = @"D:\a\_work\1\s"; + const string projectFilePath = + repositoryRoot + @"\src\runtime\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj"; + var projectId = ProjectId.CreateNewId(); + var document = DocumentInfo.Create( + DocumentId.CreateNewId(projectId), + "String.cs", + folders: new[] { "..", "..", "..", "..", "libraries", "System.Private.CoreLib", "src", "System" }, + filePath: repositoryRoot + @"\src\runtime\src\libraries\System.Private.CoreLib\src\System\String.cs"); + var project = ProjectInfo.Create( + projectId, + VersionStamp.Default, + name: "System.Private.CoreLib", + assemblyName: "System.Private.CoreLib.dll", + language: LanguageNames.CSharp, + filePath: "System.Private.CoreLib.csproj", + documents: new[] { document }); + var solution = SolutionInfo.Create( + SolutionId.CreateNewId(), + VersionStamp.Default, + projects: new[] { project }); + + var normalized = SolutionGenerator.NormalizeCompilerLogAssemblyNames( + solution, + compilerLogDocumentRoots: new[] { repositoryRoot }, + projectFilePaths: SolutionGenerator.CreateCompilerLogProjectPathMap(solution, new[] { projectFilePath })); + var normalizedDocument = normalized.Projects.Single().Documents.Single(); + + normalizedDocument.Folders.ShouldBe( + new[] { "src", "runtime", "src", "libraries", "System.Private.CoreLib", "src", "System" }); + + using var workspace = new AdhocWorkspace(); + workspace.AddSolution(normalized); + var roslynDocument = workspace.CurrentSolution.Projects.Single().Documents.Single(); + Paths.GetRelativeFilePathInProject(roslynDocument, project.FilePath).ShouldBe( + @"src\runtime\src\libraries\System.Private.CoreLib\src\System\String.cs"); + } + private static ProjectInfo CreateProjectInfo(string assemblyName, int documentCount) { var projectId = ProjectId.CreateNewId(); diff --git a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs index 2ba627d..6cf10a2 100644 --- a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs +++ b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs @@ -1,7 +1,9 @@ +using Microsoft.CodeAnalysis; using Microsoft.SourceBrowser.HtmlGenerator; using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; using System.Collections.Generic; +using System.Linq; namespace HtmlGenerator.Tests; @@ -32,7 +34,9 @@ public void Compiler_log_repository_root_aliases_configured_server_path() } [TestMethod] - public void Standalone_stage_one_compiler_log_uses_sibling_src_server_path() + [DataRow(false)] + [DataRow(true)] + public void Standalone_stage_one_compiler_log_uses_sibling_src_server_path(bool includeRepoMappings) { var serverPathMappings = new Dictionary { @@ -45,7 +49,12 @@ public void Standalone_stage_one_compiler_log_uses_sibling_src_server_path() new Dictionary { [@"D:\a\_work\1\s\"] = @"/_/", - }); + }, + includeRepoMappings ? new Dictionary + { + [@"C:\index\extensions\"] = "dotnet/extensions", + [@"C:\index\extensions\src\"] = "dotnet/extensions", + } : null); result.Count.ShouldBe(2); result[@"D:\a\_work\1\s\"].ShouldBe("https://github.com/dotnet/extensions/tree/abc/"); @@ -153,9 +162,11 @@ public void Vmr_subrepo_paths_are_aliased_under_the_original_compiler_root() @"C:\index\dotnet\logs\runtime.complog", new Dictionary { - [@"D:\a\_work\1\s"] = @"/_/", + [@"D:\a\_work\1\s\src\runtime"] = @"/_/", }); + result[@"D:\a\_work\1\s\"].ShouldBe("dotnet/dotnet"); + result[@"D:\a\_work\1\s\src\runtime\"].ShouldBe("dotnet/runtime"); Program.ResolveRepoChain( @"D:\a\_work\1\s\src\runtime\src\libraries\System.Private.CoreLib\System.Private.CoreLib.csproj", result, @@ -167,4 +178,214 @@ public void Vmr_subrepo_paths_are_aliased_under_the_original_compiler_root() "dotnet/dotnet") .ShouldBe("dotnet/sdk"); } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void Vmr_server_path_is_aliased_from_the_original_outer_root(bool useOriginalRoot) + { + var repoPathMappings = new Dictionary + { + [@"C:\index\dotnet\"] = "dotnet/dotnet", + [@"C:\index\dotnet\src\runtime"] = "dotnet/runtime", + }; + var serverPathMappings = new Dictionary + { + [@"C:\index\dotnet\"] = "https://github.com/dotnet/dotnet/tree/abc/", + }; + + var result = useOriginalRoot + ? SolutionGenerator.AddCompilerLogServerPathMapping( + serverPathMappings, @"C:\index\dotnet\runtime.complog", @"D:\a\_work\1\s\", repoPathMappings) + : SolutionGenerator.AddCompilerLogServerPathMapping( + serverPathMappings, + @"C:\index\dotnet\runtime.complog", + new Dictionary + { + [@"D:\a\_work\1\s\src\runtime"] = @"/_/", + }, + repoPathMappings); + + result[@"D:\a\_work\1\s\"].ShouldBe("https://github.com/dotnet/dotnet/tree/abc/"); + result.ShouldNotContainKey(@"D:\a\_work\1\s\src\runtime\"); + } + + [TestMethod] + [DataRow(false, @"D:\build\src\runtime\", false)] + [DataRow(false, @"D:\build\", false)] + [DataRow(true, @"D:\build\", false)] + [DataRow(false, @"D:\build\src\runtime\", true)] + [DataRow(true, @"D:\build\", true)] + public void Nested_repository_server_path_preserves_its_relative_root( + bool useOriginalRoot, string compilerRoot, bool siblingSourceDirectory) + { + const string compilerLog = @"C:\index\dotnet\src\runtime\runtime.complog"; + const string outerUrl = "https://github.com/dotnet/dotnet/tree/outer/"; + const string runtimeUrl = "https://github.com/dotnet/runtime/tree/runtime/"; + var repoMappings = new Dictionary + { + [@"C:\index\dotnet\"] = "dotnet/dotnet", + [@"C:\index\dotnet\src\runtime\"] = "dotnet/runtime", + }; + var serverMappings = new Dictionary + { + [@"C:\index\dotnet\"] = outerUrl, + [siblingSourceDirectory ? @"C:\index\dotnet\src\runtime\src\" : @"C:\index\dotnet\src\runtime\"] = runtimeUrl, + }; + + var result = useOriginalRoot + ? SolutionGenerator.AddCompilerLogServerPathMapping( + serverMappings, compilerLog, compilerRoot, repoMappings) + : SolutionGenerator.AddCompilerLogServerPathMapping( + serverMappings, compilerLog, + new Dictionary { [compilerRoot] = "/_/" }, repoMappings); + + result[@"D:\build\src\runtime\"].ShouldBe(runtimeUrl); + result.ShouldNotContainKey(@"D:\build\"); + result[@"C:\index\dotnet\"].ShouldBe(outerUrl); + serverMappings.Count.ShouldBe(2); + } + + [TestMethod] + public void Compiler_log_original_project_paths_are_mapped_by_project_identity() + { + var runtimeId = ProjectId.CreateNewId(); + var sdkId = ProjectId.CreateNewId(); + var solutionInfo = Microsoft.CodeAnalysis.SolutionInfo.Create( + SolutionId.CreateNewId(), + VersionStamp.Default, + projects: new[] + { + ProjectInfo.Create( + runtimeId, + VersionStamp.Default, + "Runtime", + "Runtime", + LanguageNames.CSharp, + filePath: "Runtime.csproj"), + ProjectInfo.Create( + sdkId, + VersionStamp.Default, + "Sdk", + "Sdk", + LanguageNames.CSharp, + filePath: "Sdk.csproj"), + }); + + var result = SolutionGenerator.CreateCompilerLogProjectPathMap( + solutionInfo, + new[] + { + @"D:\a\_work\1\s\src\runtime\Runtime.csproj", + @"D:\a\_work\1\s\src\sdk\Sdk.csproj", + }); + + result[runtimeId].ShouldBe(@"D:\a\_work\1\s\src\runtime\Runtime.csproj"); + result[sdkId].ShouldBe(@"D:\a\_work\1\s\src\sdk\Sdk.csproj"); + + var normalized = SolutionGenerator.NormalizeCompilerLogAssemblyNames(solutionInfo); + normalized.Projects.ShouldContain(project => project.Id == runtimeId); + normalized.Projects.ShouldContain(project => project.Id == sdkId); + + var resolutionPath = SolutionGenerator.GetRepoResolutionPath( + runtimeId, + "Runtime.csproj", + result); + Program.ResolveRepoName( + resolutionPath, + new Dictionary + { + [@"D:\a\_work\1\s"] = "dotnet/dotnet", + [@"D:\a\_work\1\s\src\runtime"] = "dotnet/runtime", + }, + "dotnet/dotnet") + .ShouldBe("dotnet/runtime"); + + SolutionGenerator.CreateCompilerLogProjectPathMap( + solutionInfo, + new[] + { + @"D:\a\_work\1\s\src\sdk\Sdk.csproj", + @"D:\a\_work\1\s\src\runtime\Runtime.csproj", + }) + .ShouldBeEmpty(); + } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void Vmr_repeated_subrepo_path_keeps_projects_in_the_same_repo(bool executableFirst) + { + const string compilerLog = @"D:\index\dotnet\msbuild.complog"; + const string originalRoot = @"C:\code\__w\1\s\"; + IReadOnlyDictionary mappings = new Dictionary + { + [@"D:\index\dotnet"] = "dotnet/dotnet", + [@"D:\index\dotnet\src\msbuild"] = "dotnet/msbuild", + }; + IReadOnlyDictionary serverMappings = new Dictionary + { + [@"D:\index\dotnet"] = "https://github.com/dotnet/dotnet/tree/abc/", + }; + var projectPaths = new[] + { + originalRoot + @"src\msbuild\src\Build\Microsoft.Build.csproj", + originalRoot + @"src\msbuild\src\MSBuild\MSBuild.csproj", + }; + + foreach (var projectPath in executableFirst ? projectPaths.Reverse() : projectPaths) + { + SolutionGenerator.TryGetCompilerLogOriginalRoot(mappings, compilerLog, projectPath, out var root) + .ShouldBeTrue(); + root.ShouldBe(originalRoot); + mappings = SolutionGenerator.AddCompilerLogRepoPathMappings(mappings, compilerLog, root); + serverMappings = SolutionGenerator.AddCompilerLogServerPathMapping( + serverMappings, compilerLog, new Dictionary { [root] = "/_/" }, mappings); + } + + mappings[originalRoot + @"src\msbuild\"].ShouldBe("dotnet/msbuild"); + serverMappings[originalRoot].ShouldBe("https://github.com/dotnet/dotnet/tree/abc/"); + serverMappings.ShouldNotContainKey(originalRoot + @"src\msbuild\"); + + var explorer = new Folder(); + var solutionCounts = new Dictionary { ["dotnet/dotnet"] = 2 }; + foreach (var projectPath in projectPaths) + { + var chain = Program.ResolveRepoChain(projectPath, mappings, "dotnet/dotnet"); + chain.ShouldBe(new[] { "dotnet/dotnet", "dotnet/msbuild" }); + Program.GetSolutionExplorerGroupingFolder(explorer, chain, "msbuild", 2, solutionCounts); + } + + explorer.Folders["dotnet/dotnet"].Folders.Keys.ShouldBe(new[] { "dotnet/msbuild" }); + } + + [TestMethod] + public void Vmr_original_root_is_derived_from_project_path_and_configured_subrepo() + { + var mappings = new Dictionary + { + [@"D:\index\dotnet"] = "dotnet/dotnet", + [@"D:\index\dotnet\src\runtime"] = "dotnet/runtime", + [@"D:\index\dotnet\src\sdk"] = "dotnet/sdk", + }; + + SolutionGenerator.TryGetCompilerLogOriginalRoot( + mappings, + @"D:\index\dotnet\runtime.complog", + @"C:\code\__w\1\s\src\runtime\src\coreclr\nativeaot\System.Private.CoreLib\src\System.Private.CoreLib.csproj", + out var originalRoot) + .ShouldBeTrue(); + + originalRoot.ShouldBe(@"C:\code\__w\1\s\"); + + var aliases = SolutionGenerator.AddCompilerLogRepoPathMappings( + mappings, + @"D:\index\dotnet\runtime.complog", + originalRoot); + Program.ResolveRepoChain( + @"C:\code\__w\1\s\src\runtime\src\libraries\System.Private.CoreLib\System.Private.CoreLib.csproj", + aliases, + "dotnet/dotnet") + .ShouldBe(new[] { "dotnet/dotnet", "dotnet/runtime" }); + } } diff --git a/src/SourceBrowser/src/HtmlGenerator.Tests/HtmlGenerator.Tests.csproj b/src/SourceBrowser/src/HtmlGenerator.Tests/HtmlGenerator.Tests.csproj index cf61a09..0085e4e 100644 --- a/src/SourceBrowser/src/HtmlGenerator.Tests/HtmlGenerator.Tests.csproj +++ b/src/SourceBrowser/src/HtmlGenerator.Tests/HtmlGenerator.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/src/SourceBrowser/src/HtmlGenerator/HtmlGenerator.csproj b/src/SourceBrowser/src/HtmlGenerator/HtmlGenerator.csproj index 3ec663d..a967865 100644 --- a/src/SourceBrowser/src/HtmlGenerator/HtmlGenerator.csproj +++ b/src/SourceBrowser/src/HtmlGenerator/HtmlGenerator.csproj @@ -47,6 +47,7 @@ + diff --git a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/ProjectGenerator.cs b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/ProjectGenerator.cs index 059ddc2..6b27b2e 100644 --- a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/ProjectGenerator.cs +++ b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/ProjectGenerator.cs @@ -29,12 +29,12 @@ public partial class ProjectGenerator /// Repo tag resolved per project from the owning SolutionGenerator's /repoPath /// mappings (falling back to its per-input tag); solution tag is inherited as-is. - public string RepoName => SolutionGenerator?.ResolveRepoName(ProjectFilePath) ?? string.Empty; + public string RepoName => SolutionGenerator?.ResolveRepoName(Project) ?? string.Empty; /// Repo ancestry resolved per project (outermost repo first, own repo last), so a /// parent repo can include its nested sub-repos in filtering/grouping. See /// . - public IReadOnlyList RepoChain => SolutionGenerator?.ResolveRepoChain(ProjectFilePath) ?? (IReadOnlyList)System.Array.Empty(); + public IReadOnlyList RepoChain => SolutionGenerator?.ResolveRepoChain(Project) ?? (IReadOnlyList)System.Array.Empty(); public string SolutionName => SolutionGenerator?.SolutionName ?? string.Empty; public List OtherFiles { get; set; } public IEnumerable PluginSymbolVisitors { get; private set; } diff --git a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.SolutionExplorer.cs b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.SolutionExplorer.cs index 64d0833..52faf0b 100644 --- a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.SolutionExplorer.cs +++ b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.SolutionExplorer.cs @@ -54,7 +54,7 @@ private void AddProjectToFolder(Folder folder, Project project, IEnumerable sub-repo) before laying down the .sln folder chain underneath it. - var repoChain = ResolveRepoChain(project.FilePath); + var repoChain = ResolveRepoChain(project); var repoName = repoChain.Count > 0 ? repoChain[repoChain.Count - 1] : string.Empty; folder = Program.GetSolutionExplorerGroupingFolder(folder, repoChain, SolutionName, DistinctRepoCount, SolutionCountsByRepo); diff --git a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs index 76e6fba..feaf905 100644 --- a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs +++ b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.MSBuild; using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.SourceBrowser.BinLogParser; using Microsoft.SourceBrowser.Common; namespace Microsoft.SourceBrowser.HtmlGenerator @@ -54,12 +55,18 @@ public partial class SolutionGenerator : IDisposable public string ResolveRepoName(string projectFilePath) => Program.ResolveRepoName(projectFilePath, RepoPathMappings, RepoName); + public string ResolveRepoName(Project project) + => ResolveRepoName(GetRepoResolutionPath(project)); + /// Resolves a single project's full repo ancestry (outermost first, own repo last) /// from the /repoPath mappings, so a parent repo can group its nested sub-repos. See /// . public IReadOnlyList ResolveRepoChain(string projectFilePath) => Program.ResolveRepoChain(projectFilePath, RepoPathMappings, RepoName); + public IReadOnlyList ResolveRepoChain(Project project) + => ResolveRepoChain(GetRepoResolutionPath(project)); + /// /// Optional solution display name tag applied to every assembly generated from this /// solution, auto-derived from the top-level .sln/.slnx file name. Empty for standalone @@ -86,8 +93,11 @@ public IReadOnlyList ResolveRepoChain(string projectFilePath) // documents produced by SolutionReader load their source text lazily through this reader, // so it must not be disposed until Pass1 generation has finished reading every document. private SolutionReader compilerLogReader; - - private readonly HashSet compilerLogRepositoryRoots = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly HashSet compilerLogDocumentRoots = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly List compilerLogProjectPaths = new List(); + private IReadOnlyDictionary compilerLogProjectPathsById = + ImmutableDictionary.Empty; + private readonly List compilerLogProjectScores = new List(); private SolutionGenerator( string solutionFilePath, @@ -242,51 +252,156 @@ internal static MSBuildWorkspace CreateWorkspace(ImmutableDictionary public static Microsoft.CodeAnalysis.SolutionInfo NormalizeCompilerLogAssemblyNames( Microsoft.CodeAnalysis.SolutionInfo solutionInfo, IReadOnlyDictionary repoPathMappings = null, - IEnumerable compilerLogRepositoryRoots = null) + IEnumerable compilerLogDocumentRoots = null, + IReadOnlyList projectScores = null, + IReadOnlyDictionary projectFilePaths = null) { + var useProjectScores = projectScores?.Count == solutionInfo.Projects.Count && + projectFilePaths?.Count == solutionInfo.Projects.Count && + solutionInfo.Projects.All(project => projectFilePaths.ContainsKey(project.Id)); + if (projectScores != null && !useProjectScores) + { + Log.Message("Compiler log selection metadata is incomplete or unaligned; using document counts for all projects."); + } + var projectInfos = solutionInfo.Projects - .Select(p => NormalizeCompilerLogProject(p, repoPathMappings, compilerLogRepositoryRoots)) + .Select((project, index) => ( + Project: NormalizeCompilerLogProject( + project, + repoPathMappings, + compilerLogDocumentRoots, + projectFilePaths != null && projectFilePaths.TryGetValue(project.Id, out var projectFilePath) + ? projectFilePath + : null), + Score: useProjectScores + ? projectScores[index] + : project.Documents.Count)) .ToList(); - var projectsByAssembly = projectInfos - .GroupBy(p => p.AssemblyName, StringComparer.OrdinalIgnoreCase) - .ToDictionary( - g => g.Key, - g => new Queue(g.OrderByDescending(p => p.Documents.Count)), - StringComparer.OrdinalIgnoreCase); + var projectsByAssembly = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var group in projectInfos.GroupBy(p => p.Project.AssemblyName, StringComparer.OrdinalIgnoreCase)) + { + var ordered = group.OrderByDescending(p => p.Score).ToArray(); + if (ordered.Length > 1) + { + Log.Message( + $"Selecting compiler log project '{ordered[0].Project.FilePath}' for assembly " + + $"'{group.Key}' from {ordered.Length} candidates (score {ordered[0].Score})."); + } + + projectsByAssembly[group.Key] = new Queue(ordered.Select(p => p.Project)); + } projectInfos = projectInfos - .Select(p => projectsByAssembly[p.AssemblyName].Dequeue()) + .Select(p => (Project: projectsByAssembly[p.Project.AssemblyName].Dequeue(), p.Score)) .ToList(); return Microsoft.CodeAnalysis.SolutionInfo.Create( - solutionInfo.Id, solutionInfo.Version, solutionInfo.FilePath, projectInfos); + solutionInfo.Id, solutionInfo.Version, solutionInfo.FilePath, projectInfos.Select(p => p.Project)); + } + + internal static IReadOnlyDictionary CreateCompilerLogProjectPathMap( + Microsoft.CodeAnalysis.SolutionInfo solutionInfo, + IReadOnlyList projectFilePaths) + { + var result = new Dictionary(); + var projects = solutionInfo.Projects.ToArray(); + if (projects.Length != (projectFilePaths?.Count ?? 0)) + { + Log.Message( + $"Compiler log project metadata count mismatch: SolutionReader returned {projects.Length} project(s), " + + $"but the compiler log contained {projectFilePaths?.Count ?? 0} regular compiler call(s)."); + return result; + } + + // SolutionReader and CompilerLogReader both enumerate regular calls by the compiler-log + // call index. Keep the original paths keyed by ProjectId so later project reordering is safe. + for (var i = 0; i < projects.Length; i++) + { + var solutionProjectFileName = Path.GetFileName(projects[i].FilePath); + var compilerProjectFileName = Path.GetFileName(projectFilePaths[i]); + if (!string.IsNullOrEmpty(solutionProjectFileName) && + !string.IsNullOrEmpty(compilerProjectFileName) && + !string.Equals(solutionProjectFileName, compilerProjectFileName, StringComparison.OrdinalIgnoreCase)) + { + Log.Message( + $"Compiler log project metadata order mismatch at index {i}: SolutionReader returned " + + $"'{solutionProjectFileName}', but CompilerLogReader returned '{compilerProjectFileName}'."); + return new Dictionary(); + } + } + + for (var i = 0; i < projects.Length; i++) + { + if (!string.IsNullOrEmpty(projectFilePaths[i])) + { + result[projects[i].Id] = projectFilePaths[i]; + } + } + + return result; + } + + private string GetRepoResolutionPath(Project project) + { + if (Path.IsPathRooted(project?.FilePath)) + { + return project.FilePath; + } + + return GetRepoResolutionPath( + project?.Id, + project?.FilePath ?? ProjectFilePath, + compilerLogProjectPathsById); + } + + internal static string GetRepoResolutionPath( + ProjectId projectId, + string projectFilePath, + IReadOnlyDictionary compilerLogProjectPaths) + { + if (projectId != null && + compilerLogProjectPaths != null && + compilerLogProjectPaths.TryGetValue(projectId, out var originalProjectPath) && + !string.IsNullOrEmpty(originalProjectPath)) + { + return originalProjectPath; + } + + return projectFilePath ?? string.Empty; } private static ProjectInfo NormalizeCompilerLogProject( ProjectInfo projectInfo, IReadOnlyDictionary repoPathMappings, - IEnumerable compilerLogRepositoryRoots) + IEnumerable compilerLogDocumentRoots, + string originalProjectFilePath) { var normalized = projectInfo.WithAssemblyName(Path.GetFileNameWithoutExtension(projectInfo.AssemblyName)); - var projectDirectory = Path.GetDirectoryName(projectInfo.FilePath); + var projectFilePath = Path.IsPathRooted(projectInfo.FilePath) + ? projectInfo.FilePath + : (!string.IsNullOrEmpty(originalProjectFilePath) ? originalProjectFilePath : projectInfo.FilePath); + var projectDirectory = Path.GetDirectoryName(projectFilePath); if (string.IsNullOrEmpty(projectDirectory)) { return normalized; } - var repositoryRoot = (compilerLogRepositoryRoots ?? Enumerable.Empty()) - .Where(path => Paths.IsOrContains(path, projectInfo.FilePath)) + var repositoryRoot = (repoPathMappings?.Keys ?? Enumerable.Empty()) + .Where(path => Paths.IsOrContains(path, projectFilePath)) .OrderBy(path => path.Length) .FirstOrDefault(); - repositoryRoot ??= (repoPathMappings?.Keys ?? Enumerable.Empty()) - .Where(path => Paths.IsOrContains(path, projectInfo.FilePath)) + repositoryRoot ??= (compilerLogDocumentRoots ?? Enumerable.Empty()) + .Where(path => Paths.IsOrContains(path, projectFilePath)) .OrderBy(path => path.Length) .FirstOrDefault(); @@ -299,7 +414,8 @@ internal static DocumentInfo AddCompilerLogDocumentFolders( string projectDirectory, string repositoryRoot) { - if (document.Folders.Count > 0 || + var hasParentTraversal = document.Folders.Any(folder => folder == ".."); + if ((document.Folders.Count > 0 && !hasParentTraversal) || string.IsNullOrEmpty(document.FilePath) || !Path.IsPathRooted(document.FilePath) || !Path.IsPathRooted(projectDirectory)) @@ -345,41 +461,60 @@ private void ReadCompilerLogMetadata(string compilerLogFilePath) try { using var reader = CompilerLogReader.Create(compilerLogFilePath, BasicAnalyzerKind.None); - foreach (var compilerCall in reader.ReadAllCompilerCalls(cc => cc.Kind == CompilerCallKind.Regular)) + var compilerCalls = reader.ReadAllCompilerCalls(cc => cc.Kind == CompilerCallKind.Regular); + foreach (var compilerCall in compilerCalls) { - // With BasicAnalyzerKind.None the generated sources must already be present in the - // log; if they are not, the indexed output for this project will be missing those - // files, so surface it rather than failing silently. - if (!reader.HasAllGeneratedFileContent(compilerCall)) - { - Log.Message($"Compiler log '{compilerLogFilePath}' is missing generated source content for '{compilerCall.GetDiagnosticName()}'; generated files will not be indexed for that project."); - } - - var data = reader.ReadCompilationData(compilerCall); - foreach (var diagnostic in data.CreationDiagnostics) + compilerLogProjectPaths.Add(compilerCall.ProjectFilePath); + if (TryGetCompilerLogOriginalRoot( + RepoPathMappings, + compilerLogFilePath, + compilerCall.ProjectFilePath, + out var originalRoot) && + compilerLogDocumentRoots.Add(originalRoot)) { - if (diagnostic.Severity == DiagnosticSeverity.Warning || - diagnostic.Severity == DiagnosticSeverity.Error) - { - Log.Message($"Compiler log '{compilerLogFilePath}' diagnostic for '{compilerCall.GetDiagnosticName()}': {diagnostic}"); - } + Log.Message( + $"Mapped compiler log '{compilerLogFilePath}' to original repository root '{originalRoot}'."); + ServerPathMappings = AddCompilerLogServerPathMapping( + ServerPathMappings, + compilerLogFilePath, + originalRoot, + RepoPathMappings); + RepoPathMappings = AddCompilerLogRepoPathMappings( + RepoPathMappings, + compilerLogFilePath, + originalRoot); } + var projectScore = 0; try { - var pathMappings = compilerCall.IsCSharp - ? CSharpCommandLineParser.Default.Parse(reader.ReadRawArguments(compilerCall), compilerCall.ProjectDirectory, sdkDirectory: null).PathMap - : VisualBasicCommandLineParser.Default.Parse(reader.ReadRawArguments(compilerCall), compilerCall.ProjectDirectory, sdkDirectory: null).PathMap; + projectScore = CalculateCompilerLogProjectScore( + compilerCall.ProjectFilePath, + compilerCall.TargetFramework, + sourceFileCount: 0); + var rawArguments = reader.ReadRawArguments(compilerCall); + var parsedArguments = compilerCall.IsCSharp + ? (CommandLineArguments)CSharpCommandLineParser.Default.Parse(rawArguments, compilerCall.ProjectDirectory, sdkDirectory: null) + : VisualBasicCommandLineParser.Default.Parse(rawArguments, compilerCall.ProjectDirectory, sdkDirectory: null); + var pathMappings = parsedArguments.PathMap; var repositoryRoot = GetCompilerLogRepositoryRoot(pathMappings); if (repositoryRoot != null) { - compilerLogRepositoryRoots.Add(repositoryRoot); + compilerLogDocumentRoots.Add(GetCompilerLogDocumentRoot( + RepoPathMappings, + compilerLogFilePath, + repositoryRoot)); } + projectScore = CalculateCompilerLogProjectScore( + compilerCall.ProjectFilePath, + compilerCall.TargetFramework, + parsedArguments.SourceFiles.Length); ServerPathMappings = AddCompilerLogServerPathMapping( ServerPathMappings, compilerLogFilePath, - pathMappings); + pathMappings, + RepoPathMappings); RepoPathMappings = AddCompilerLogRepoPathMappings( RepoPathMappings, compilerLogFilePath, @@ -389,9 +524,31 @@ private void ReadCompilerLogMetadata(string compilerLogFilePath) { Log.Exception( ex, - $"Failed to read path mappings for '{compilerCall.ProjectFilePath}' from '{compilerLogFilePath}'.", + $"Failed to read path mappings and selection metadata for '{compilerCall.ProjectFilePath}' from '{compilerLogFilePath}'.", isSevere: false); } + compilerLogProjectScores.Add(projectScore); + } + + foreach (var compilerCall in compilerCalls) + { + // With BasicAnalyzerKind.None the generated sources must already be present in the + // log; if they are not, the indexed output for this project will be missing those + // files, so surface it rather than failing silently. + if (!reader.HasAllGeneratedFileContent(compilerCall)) + { + Log.Message($"Compiler log '{compilerLogFilePath}' is missing generated source content for '{compilerCall.GetDiagnosticName()}'; generated files will not be indexed for that project."); + } + + var data = reader.ReadCompilationData(compilerCall); + foreach (var diagnostic in data.CreationDiagnostics) + { + if (diagnostic.Severity == DiagnosticSeverity.Warning || + diagnostic.Severity == DiagnosticSeverity.Error) + { + Log.Message($"Compiler log '{compilerLogFilePath}' diagnostic for '{compilerCall.GetDiagnosticName()}': {diagnostic}"); + } + } } } catch (Exception ex) @@ -405,7 +562,28 @@ private void ReadCompilerLogMetadata(string compilerLogFilePath) internal static IReadOnlyDictionary AddCompilerLogServerPathMapping( IReadOnlyDictionary serverPathMappings, string compilerLogFilePath, - IEnumerable> compilerPathMappings) + IEnumerable> compilerPathMappings, + IReadOnlyDictionary repoPathMappings = null) + { + var repositoryRoot = GetCompilerLogRepositoryRoot(compilerPathMappings); + if (repositoryRoot == null) + { + return serverPathMappings; + } + + var originalRepositoryRoot = GetCompilerLogDocumentRoot( + repoPathMappings, + compilerLogFilePath, + repositoryRoot); + return AddCompilerLogServerPathMapping( + serverPathMappings, compilerLogFilePath, originalRepositoryRoot, repoPathMappings); + } + + internal static IReadOnlyDictionary AddCompilerLogServerPathMapping( + IReadOnlyDictionary serverPathMappings, + string compilerLogFilePath, + string originalRepositoryRoot, + IReadOnlyDictionary repoPathMappings = null) { var normalizedServerPathMappings = CopyServerPathMappings(serverPathMappings); var compilerLogDirectory = Path.GetDirectoryName(Path.GetFullPath(compilerLogFilePath)); @@ -425,13 +603,23 @@ internal static IReadOnlyDictionary AddCompilerLogServerPathMapp return serverPathMappings; } - var repositoryRoot = GetCompilerLogRepositoryRoot(compilerPathMappings); - if (repositoryRoot == null) + var configuredRepositoryRoot = (repoPathMappings?.Keys ?? Enumerable.Empty()) + .Where(mapping => Paths.IsOrContains(mapping, compilerLogFilePath)) + .OrderBy(mapping => mapping.Length) + .FirstOrDefault(); + var originalServerRoot = originalRepositoryRoot; + if (configuredRepositoryRoot != null) { - return serverPathMappings; + // Source URLs may belong to a nested repo, unlike the outer document/grouping root. + // The bundle-only sibling src/ does not exist in the original checkout layout. + var configuredServerRoot = Paths.IsOrContains(configuredMapping.Key, compilerLogFilePath) + ? configuredMapping.Key + : compilerLogDirectory; + var relativePath = Path.GetRelativePath(configuredRepositoryRoot, configuredServerRoot); + originalServerRoot = Path.Combine(originalRepositoryRoot, relativePath); } - normalizedServerPathMappings[repositoryRoot] = configuredMapping.Value; + normalizedServerPathMappings[Paths.EnsureTrailingSlash(Path.GetFullPath(originalServerRoot))] = configuredMapping.Value; return normalizedServerPathMappings; } @@ -457,6 +645,10 @@ internal static IReadOnlyDictionary AddCompilerLogRepoPathMappin { return repoPathMappings; } + var originalRepositoryRoot = GetCompilerLogDocumentRoot( + repoPathMappings, + compilerLogFilePath, + repositoryRoot); foreach (var mapping in repoPathMappings ?? Enumerable.Empty>()) { @@ -468,14 +660,178 @@ internal static IReadOnlyDictionary AddCompilerLogRepoPathMappin var relativePath = Path.GetRelativePath(configuredRepositoryRoot, mappingPath); var originalPath = relativePath == "." - ? repositoryRoot - : Path.Combine(repositoryRoot, relativePath); + ? originalRepositoryRoot + : Path.Combine(originalRepositoryRoot, relativePath); normalizedRepoPathMappings[Paths.EnsureTrailingSlash(Path.GetFullPath(originalPath))] = mapping.Value; } return normalizedRepoPathMappings; } + internal static IReadOnlyDictionary AddCompilerLogRepoPathMappings( + IReadOnlyDictionary repoPathMappings, + string compilerLogFilePath, + string originalRepositoryRoot) + { + var normalizedRepoPathMappings = CopyServerPathMappings(repoPathMappings); + var configuredRepositoryRoot = normalizedRepoPathMappings.Keys + .Where(mapping => Paths.IsOrContains(mapping, compilerLogFilePath)) + .OrderBy(mapping => mapping.Length) + .FirstOrDefault(); + if (configuredRepositoryRoot == null) + { + return repoPathMappings; + } + + configuredRepositoryRoot = Path.GetFullPath(configuredRepositoryRoot); + foreach (var mapping in repoPathMappings ?? Enumerable.Empty>()) + { + var mappingPath = Path.GetFullPath(mapping.Key); + if (!Paths.IsOrContains(configuredRepositoryRoot, mappingPath)) + { + continue; + } + + var relativePath = Path.GetRelativePath(configuredRepositoryRoot, mappingPath); + var originalPath = relativePath == "." + ? originalRepositoryRoot + : Path.Combine(originalRepositoryRoot, relativePath); + normalizedRepoPathMappings[Paths.EnsureTrailingSlash(Path.GetFullPath(originalPath))] = mapping.Value; + } + + return normalizedRepoPathMappings; + } + + internal static bool TryGetCompilerLogOriginalRoot( + IReadOnlyDictionary repoPathMappings, + string compilerLogFilePath, + string compilerProjectFilePath, + out string originalRoot) + { + originalRoot = null; + if (string.IsNullOrEmpty(compilerProjectFilePath) || !Path.IsPathRooted(compilerProjectFilePath)) + { + return false; + } + + var configuredRepositoryRoot = (repoPathMappings?.Keys ?? Enumerable.Empty()) + .Where(mapping => Paths.IsOrContains(mapping, compilerLogFilePath)) + .OrderBy(mapping => mapping.Length) + .FirstOrDefault(); + if (configuredRepositoryRoot == null) + { + return false; + } + + configuredRepositoryRoot = Path.GetFullPath(configuredRepositoryRoot); + var projectDirectory = Paths.EnsureTrailingSlash(Path.GetFullPath(Path.GetDirectoryName(compilerProjectFilePath))); + var bestRelativePathLength = -1; + + foreach (var mapping in repoPathMappings) + { + var mappingPath = Path.GetFullPath(mapping.Key); + if (!Paths.IsOrContains(configuredRepositoryRoot, mappingPath) || + string.Equals(mappingPath, configuredRepositoryRoot, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var relativePath = Path.GetRelativePath(configuredRepositoryRoot, mappingPath) + .Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var needle = Path.DirectorySeparatorChar + relativePath + Path.DirectorySeparatorChar; + var matchIndex = projectDirectory.IndexOf(needle, StringComparison.OrdinalIgnoreCase); + if (matchIndex < 0 || relativePath.Length <= bestRelativePathLength) + { + continue; + } + + originalRoot = Paths.EnsureTrailingSlash(projectDirectory.Substring(0, matchIndex)); + bestRelativePathLength = relativePath.Length; + } + + return originalRoot != null; + } + + internal static string GetCompilerLogDocumentRoot( + IReadOnlyDictionary repoPathMappings, + string compilerLogFilePath, + string compilerRepositoryRoot) + { + var compilerLogFullPath = Path.GetFullPath(compilerLogFilePath); + var configuredMapping = (repoPathMappings ?? Enumerable.Empty>()) + .Where(mapping => Paths.IsOrContains(mapping.Key, compilerLogFullPath)) + .OrderBy(mapping => mapping.Key.Length) + .FirstOrDefault(); + if (configuredMapping.Key == null) + { + return compilerRepositoryRoot; + } + + var configuredRepositoryRoot = Path.GetFullPath(configuredMapping.Key); + var outerRepoName = configuredMapping.Value; + string bestRoot = compilerRepositoryRoot; + var bestSegmentCount = 0; + + foreach (var mapping in repoPathMappings) + { + var mappingPath = Path.GetFullPath(mapping.Key); + if (!Paths.IsOrContains(configuredRepositoryRoot, mappingPath) || + string.Equals(mapping.Value, outerRepoName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var relativePath = Path.GetRelativePath(configuredRepositoryRoot, mappingPath); + if (TryRemovePathSuffix(compilerRepositoryRoot, relativePath, out var originalRoot, out var segmentCount) && + segmentCount > bestSegmentCount) + { + bestRoot = originalRoot; + bestSegmentCount = segmentCount; + } + } + + return bestRoot; + } + + private static bool TryRemovePathSuffix( + string path, + string suffix, + out string pathWithoutSuffix, + out int segmentCount) + { + var suffixSegments = suffix.Split( + new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, + StringSplitOptions.RemoveEmptyEntries); + var candidate = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + for (var i = suffixSegments.Length - 1; i >= 0; i--) + { + if (!string.Equals(Path.GetFileName(candidate), suffixSegments[i], StringComparison.OrdinalIgnoreCase)) + { + pathWithoutSuffix = null; + segmentCount = 0; + return false; + } + + candidate = Path.GetDirectoryName(candidate); + } + + pathWithoutSuffix = Paths.EnsureTrailingSlash(candidate); + segmentCount = suffixSegments.Length; + return true; + } + + internal static int CalculateCompilerLogProjectScore( + string projectFilePath, + string targetFramework, + int sourceFileCount) + { + return CompilerInvocationScoring.IsReferenceAssembly( + Path.GetDirectoryName(projectFilePath), StringComparison.OrdinalIgnoreCase) + ? int.MinValue + : CompilerInvocationScoring.CalculateScore(targetFramework, sourceFileCount); + } + private static string GetCompilerLogRepositoryRoot(IEnumerable> compilerPathMappings) { var repositoryRoot = compilerPathMappings @@ -898,10 +1254,16 @@ private async Task CreateSolutionAsync(string solutionFilePath, Cancel solutionFilePath, BasicAnalyzerKind.None); this.compilerLogReader = reader; + var rawSolutionInfo = reader.ReadSolutionInfo(); + compilerLogProjectPathsById = CreateCompilerLogProjectPathMap( + rawSolutionInfo, + compilerLogProjectPaths); var solutionInfo = NormalizeCompilerLogAssemblyNames( - reader.ReadSolutionInfo(), + rawSolutionInfo, RepoPathMappings, - compilerLogRepositoryRoots); + compilerLogDocumentRoots, + compilerLogProjectScores, + compilerLogProjectPathsById); var adhocWorkspace = new AdhocWorkspace(); adhocWorkspace.AddSolution(solutionInfo);