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