From e300780e94155a270ade374b6271fe7fa16d31b2 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Wed, 2 Sep 2026 10:13:29 +0200 Subject: [PATCH 1/7] Fix more complog-related issues --- .../CompilerLogAssemblyNameTests.cs | 54 +++- .../CompilerLogWebAccessTests.cs | 30 +- .../Pass1-Generation/SolutionGenerator.cs | 294 +++++++++++++++--- 3 files changed, 332 insertions(+), 46 deletions(-) diff --git a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs index ec22090..5bccc97 100644 --- a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs +++ b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs @@ -71,6 +71,58 @@ 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 }); + + 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] + public void Compiler_log_project_score_excludes_reference_assemblies() + { + SolutionGenerator.CalculateCompilerLogProjectScore( + @"D:\repo\src\libraries\Foo\ref\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] + public void Compiler_log_project_score_prefers_newer_and_platform_specific_frameworks() + { + var older = SolutionGenerator.CalculateCompilerLogProjectScore( + @"D:\repo\Foo.csproj", "net10.0", sourceFileCount: 100); + var current = SolutionGenerator.CalculateCompilerLogProjectScore( + @"D:\repo\Foo.csproj", "net11.0", sourceFileCount: 100); + var linux = SolutionGenerator.CalculateCompilerLogProjectScore( + @"D:\repo\Foo.csproj", "net11.0-linux", sourceFileCount: 100); + + current.ShouldBeGreaterThan(older); + linux.ShouldBeGreaterThan(current); + } + [TestMethod] public void Compiler_log_document_folders_match_BinLogToSln_link_behavior() { @@ -99,7 +151,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" }); diff --git a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs index 2ba627d..d798d05 100644 --- a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs +++ b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs @@ -153,9 +153,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 +169,30 @@ public void Vmr_subrepo_paths_are_aliased_under_the_original_compiler_root() "dotnet/dotnet") .ShouldBe("dotnet/sdk"); } + + [TestMethod] + public void Vmr_server_path_is_aliased_from_the_original_outer_root() + { + 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 = 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\"); + } } diff --git a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs index 76e6fba..e75d529 100644 --- a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs +++ b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs @@ -86,8 +86,8 @@ 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 compilerLogProjectScores = new List(); private SolutionGenerator( string solutionFilePath, @@ -249,30 +249,42 @@ internal static MSBuildWorkspace CreateWorkspace(ImmutableDictionary repoPathMappings = null, - IEnumerable compilerLogRepositoryRoots = null) + IEnumerable compilerLogDocumentRoots = null, + IReadOnlyList projectScores = null) { var projectInfos = solutionInfo.Projects - .Select(p => NormalizeCompilerLogProject(p, repoPathMappings, compilerLogRepositoryRoots)) + .Select((project, index) => ( + Project: NormalizeCompilerLogProject(project, repoPathMappings, compilerLogDocumentRoots), + Score: projectScores != null && index < projectScores.Count + ? 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)); } private static ProjectInfo NormalizeCompilerLogProject( ProjectInfo projectInfo, IReadOnlyDictionary repoPathMappings, - IEnumerable compilerLogRepositoryRoots) + IEnumerable compilerLogDocumentRoots) { var normalized = projectInfo.WithAssemblyName(Path.GetFileNameWithoutExtension(projectInfo.AssemblyName)); var projectDirectory = Path.GetDirectoryName(projectInfo.FilePath); @@ -281,7 +293,7 @@ private static ProjectInfo NormalizeCompilerLogProject( return normalized; } - var repositoryRoot = (compilerLogRepositoryRoots ?? Enumerable.Empty()) + var repositoryRoot = (compilerLogDocumentRoots ?? Enumerable.Empty()) .Where(path => Paths.IsOrContains(path, projectInfo.FilePath)) .OrderBy(path => path.Length) .FirstOrDefault(); @@ -345,41 +357,38 @@ 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) - { - if (diagnostic.Severity == DiagnosticSeverity.Warning || - diagnostic.Severity == DiagnosticSeverity.Error) - { - Log.Message($"Compiler log '{compilerLogFilePath}' diagnostic for '{compilerCall.GetDiagnosticName()}': {diagnostic}"); - } - } - + var projectScore = CalculateCompilerLogProjectScore( + compilerCall.ProjectFilePath, + compilerCall.TargetFramework, + sourceFileCount: 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; + 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 +398,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 +436,8 @@ private void ReadCompilerLogMetadata(string compilerLogFilePath) internal static IReadOnlyDictionary AddCompilerLogServerPathMapping( IReadOnlyDictionary serverPathMappings, string compilerLogFilePath, - IEnumerable> compilerPathMappings) + IEnumerable> compilerPathMappings, + IReadOnlyDictionary repoPathMappings = null) { var normalizedServerPathMappings = CopyServerPathMappings(serverPathMappings); var compilerLogDirectory = Path.GetDirectoryName(Path.GetFullPath(compilerLogFilePath)); @@ -431,7 +463,11 @@ internal static IReadOnlyDictionary AddCompilerLogServerPathMapp return serverPathMappings; } - normalizedServerPathMappings[repositoryRoot] = configuredMapping.Value; + var originalRepositoryRoot = GetCompilerLogDocumentRoot( + repoPathMappings, + compilerLogFilePath, + repositoryRoot); + normalizedServerPathMappings[originalRepositoryRoot] = configuredMapping.Value; return normalizedServerPathMappings; } @@ -457,6 +493,10 @@ internal static IReadOnlyDictionary AddCompilerLogRepoPathMappin { return repoPathMappings; } + var originalRepositoryRoot = GetCompilerLogDocumentRoot( + repoPathMappings, + compilerLogFilePath, + repositoryRoot); foreach (var mapping in repoPathMappings ?? Enumerable.Empty>()) { @@ -468,14 +508,179 @@ 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 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) + { + var projectDirectory = Path.GetDirectoryName(projectFilePath); + var projectFolder = Path.GetFileName(projectDirectory); + if (string.Equals(projectFolder, "ref", StringComparison.OrdinalIgnoreCase) || + string.Equals(projectFolder, "stubs", StringComparison.OrdinalIgnoreCase) || + string.Equals(Path.GetFileName(Path.GetDirectoryName(projectDirectory)), "cycle-breakers", StringComparison.OrdinalIgnoreCase)) + { + return int.MinValue; + } + + var score = sourceFileCount; + if (TryGetTargetFrameworkVersion(targetFramework, out var major, out var minor, out var platform)) + { + score += major * 1000 + minor * 100; + + if (!string.IsNullOrEmpty(platform)) + { + score += 500; + if (platform.Equals("linux", StringComparison.OrdinalIgnoreCase)) + { + score += 100; + } + else if (platform.Equals("unix", StringComparison.OrdinalIgnoreCase)) + { + score += 50; + } + } + } + + return score; + } + + private static bool TryGetTargetFrameworkVersion( + string targetFramework, + out int major, + out int minor, + out string platform) + { + major = 0; + minor = 0; + platform = string.Empty; + if (string.IsNullOrEmpty(targetFramework)) + { + return false; + } + + var dash = targetFramework.IndexOf('-'); + var framework = dash >= 0 ? targetFramework.Substring(0, dash) : targetFramework; + if (dash >= 0) + { + var platformPart = targetFramework.Substring(dash + 1); + var platformLength = platformPart.TakeWhile(char.IsLetter).Count(); + platform = platformPart.Substring(0, platformLength); + } + + string version; + if (framework.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase)) + { + version = framework.Substring("netstandard".Length); + } + else if (framework.StartsWith("netcoreapp", StringComparison.OrdinalIgnoreCase)) + { + version = framework.Substring("netcoreapp".Length); + } + else if (framework.StartsWith("net", StringComparison.OrdinalIgnoreCase)) + { + version = framework.Substring("net".Length); + } + else + { + return false; + } + + var parts = version.Split('.'); + if (parts.Length > 1) + { + return int.TryParse(parts[0], out major) && + int.TryParse(parts[1], out minor); + } + + if (version.Length >= 2 && + version[0] is >= '1' and <= '4' && + char.IsDigit(version[1])) + { + major = version[0] - '0'; + minor = version[1] - '0'; + return true; + } + + return int.TryParse(version, out major); + } + private static string GetCompilerLogRepositoryRoot(IEnumerable> compilerPathMappings) { var repositoryRoot = compilerPathMappings @@ -901,7 +1106,8 @@ private async Task CreateSolutionAsync(string solutionFilePath, Cancel var solutionInfo = NormalizeCompilerLogAssemblyNames( reader.ReadSolutionInfo(), RepoPathMappings, - compilerLogRepositoryRoots); + compilerLogDocumentRoots, + compilerLogProjectScores); var adhocWorkspace = new AdhocWorkspace(); adhocWorkspace.AddSolution(solutionInfo); From 86614d9b6ea9ded2c79e4542e4b407c7b7fefa8b Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Wed, 2 Sep 2026 13:34:51 +0200 Subject: [PATCH 2/7] Run smoke test on branch builds too --- azure-pipelines.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 From 65bcee1ad6f7ad6f6ceeec3c3c0b385c3d517dde Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Fri, 4 Sep 2026 12:55:49 +0200 Subject: [PATCH 3/7] Fix more complog-related issues --- .../CompilerLogAssemblyNameTests.cs | 41 ++++++ .../CompilerLogWebAccessTests.cs | 66 ++++++++++ .../Pass1-Generation/ProjectGenerator.cs | 4 +- .../SolutionGenerator.SolutionExplorer.cs | 2 +- .../Pass1-Generation/SolutionGenerator.cs | 119 ++++++++++++++++-- 5 files changed, 220 insertions(+), 12 deletions(-) diff --git a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs index 5bccc97..5578b32 100644 --- a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs +++ b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogAssemblyNameTests.cs @@ -182,6 +182,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: 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 d798d05..200e469 100644 --- a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs +++ b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs @@ -1,3 +1,4 @@ +using Microsoft.CodeAnalysis; using Microsoft.SourceBrowser.HtmlGenerator; using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; @@ -195,4 +196,69 @@ public void Vmr_server_path_is_aliased_from_the_original_outer_root() result[@"D:\a\_work\1\s\"].ShouldBe("https://github.com/dotnet/dotnet/tree/abc/"); result.ShouldNotContainKey(@"D:\a\_work\1\s\src\runtime\"); } + + [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(); + } } 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 e75d529..da52c1f 100644 --- a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs +++ b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs @@ -54,12 +54,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 @@ -87,6 +93,9 @@ public IReadOnlyList ResolveRepoChain(string projectFilePath) // so it must not be disposed until Pass1 generation has finished reading every document. private SolutionReader compilerLogReader; 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( @@ -242,6 +251,8 @@ internal static MSBuildWorkspace CreateWorkspace(ImmutableDictionary repoPathMappings = null, IEnumerable compilerLogDocumentRoots = null, - IReadOnlyList projectScores = null) + IReadOnlyList projectScores = null, + IReadOnlyList projectFilePaths = null) { + var alignedProjectFilePaths = projectFilePaths != null && + projectFilePaths.Count == solutionInfo.Projects.Count() + ? projectFilePaths + : null; var projectInfos = solutionInfo.Projects .Select((project, index) => ( - Project: NormalizeCompilerLogProject(project, repoPathMappings, compilerLogDocumentRoots), + Project: NormalizeCompilerLogProject( + project, + repoPathMappings, + compilerLogDocumentRoots, + alignedProjectFilePaths != null + ? alignedProjectFilePaths[index] + : null), Score: projectScores != null && index < projectScores.Count ? projectScores[index] : project.Documents.Count)) @@ -281,24 +303,92 @@ public static Microsoft.CodeAnalysis.SolutionInfo NormalizeCompilerLogAssemblyNa 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) + => 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 compilerLogDocumentRoots) + IEnumerable compilerLogDocumentRoots, + string originalProjectFilePath) { var normalized = projectInfo.WithAssemblyName(Path.GetFileNameWithoutExtension(projectInfo.AssemblyName)); - var projectDirectory = Path.GetDirectoryName(projectInfo.FilePath); + var projectFilePath = !string.IsNullOrEmpty(originalProjectFilePath) + ? originalProjectFilePath + : projectInfo.FilePath; + var projectDirectory = Path.GetDirectoryName(projectFilePath); if (string.IsNullOrEmpty(projectDirectory)) { return normalized; } var repositoryRoot = (compilerLogDocumentRoots ?? Enumerable.Empty()) - .Where(path => Paths.IsOrContains(path, projectInfo.FilePath)) + .Where(path => Paths.IsOrContains(path, projectFilePath)) .OrderBy(path => path.Length) .FirstOrDefault(); repositoryRoot ??= (repoPathMappings?.Keys ?? Enumerable.Empty()) - .Where(path => Paths.IsOrContains(path, projectInfo.FilePath)) + .Where(path => Paths.IsOrContains(path, projectFilePath)) .OrderBy(path => path.Length) .FirstOrDefault(); @@ -311,7 +401,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)) @@ -360,6 +451,7 @@ private void ReadCompilerLogMetadata(string compilerLogFilePath) var compilerCalls = reader.ReadAllCompilerCalls(cc => cc.Kind == CompilerCallKind.Regular); foreach (var compilerCall in compilerCalls) { + compilerLogProjectPaths.Add(compilerCall.ProjectFilePath); var projectScore = CalculateCompilerLogProjectScore( compilerCall.ProjectFilePath, compilerCall.TargetFramework, @@ -1103,11 +1195,20 @@ private async Task CreateSolutionAsync(string solutionFilePath, Cancel solutionFilePath, BasicAnalyzerKind.None); this.compilerLogReader = reader; + var rawSolutionInfo = reader.ReadSolutionInfo(); + compilerLogProjectPathsById = CreateCompilerLogProjectPathMap( + rawSolutionInfo, + compilerLogProjectPaths); + var projectFilePathsForNormalization = + compilerLogProjectPathsById.Count == rawSolutionInfo.Projects.Count() + ? compilerLogProjectPaths + : null; var solutionInfo = NormalizeCompilerLogAssemblyNames( - reader.ReadSolutionInfo(), + rawSolutionInfo, RepoPathMappings, compilerLogDocumentRoots, - compilerLogProjectScores); + compilerLogProjectScores, + projectFilePathsForNormalization); var adhocWorkspace = new AdhocWorkspace(); adhocWorkspace.AddSolution(solutionInfo); From e0abc54feeb03542987358f0cf1bd99c6e2deb9e Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Sat, 5 Sep 2026 11:05:34 +0200 Subject: [PATCH 4/7] Fix mapping --- .../CompilerLogWebAccessTests.cs | 30 ++++ .../Pass1-Generation/SolutionGenerator.cs | 140 +++++++++++++++++- 2 files changed, 164 insertions(+), 6 deletions(-) diff --git a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs index 200e469..fc6684c 100644 --- a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs +++ b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs @@ -261,4 +261,34 @@ public void Compiler_log_original_project_paths_are_mapped_by_project_identity() }) .ShouldBeEmpty(); } + + [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/Pass1-Generation/SolutionGenerator.cs b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs index da52c1f..92e4771 100644 --- a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs +++ b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs @@ -346,10 +346,17 @@ internal static IReadOnlyDictionary CreateCompilerLogProjectP } private string GetRepoResolutionPath(Project project) - => GetRepoResolutionPath( + { + if (Path.IsPathRooted(project?.FilePath)) + { + return project.FilePath; + } + + return GetRepoResolutionPath( project?.Id, project?.FilePath ?? ProjectFilePath, compilerLogProjectPathsById); + } internal static string GetRepoResolutionPath( ProjectId projectId, @@ -374,20 +381,20 @@ private static ProjectInfo NormalizeCompilerLogProject( string originalProjectFilePath) { var normalized = projectInfo.WithAssemblyName(Path.GetFileNameWithoutExtension(projectInfo.AssemblyName)); - var projectFilePath = !string.IsNullOrEmpty(originalProjectFilePath) - ? originalProjectFilePath - : 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 = (compilerLogDocumentRoots ?? Enumerable.Empty()) + var repositoryRoot = (repoPathMappings?.Keys ?? Enumerable.Empty()) .Where(path => Paths.IsOrContains(path, projectFilePath)) .OrderBy(path => path.Length) .FirstOrDefault(); - repositoryRoot ??= (repoPathMappings?.Keys ?? Enumerable.Empty()) + repositoryRoot ??= (compilerLogDocumentRoots ?? Enumerable.Empty()) .Where(path => Paths.IsOrContains(path, projectFilePath)) .OrderBy(path => path.Length) .FirstOrDefault(); @@ -452,6 +459,25 @@ private void ReadCompilerLogMetadata(string compilerLogFilePath) foreach (var compilerCall in compilerCalls) { compilerLogProjectPaths.Add(compilerCall.ProjectFilePath); + if (TryGetCompilerLogOriginalRoot( + RepoPathMappings, + compilerLogFilePath, + compilerCall.ProjectFilePath, + out var originalRoot) && + compilerLogDocumentRoots.Add(originalRoot)) + { + Log.Message( + $"Mapped compiler log '{compilerLogFilePath}' to original repository root '{originalRoot}'."); + ServerPathMappings = AddCompilerLogServerPathMapping( + ServerPathMappings, + compilerLogFilePath, + originalRoot); + RepoPathMappings = AddCompilerLogRepoPathMappings( + RepoPathMappings, + compilerLogFilePath, + originalRoot); + } + var projectScore = CalculateCompilerLogProjectScore( compilerCall.ProjectFilePath, compilerCall.TargetFramework, @@ -563,6 +589,24 @@ internal static IReadOnlyDictionary AddCompilerLogServerPathMapp return normalizedServerPathMappings; } + private static IReadOnlyDictionary AddCompilerLogServerPathMapping( + IReadOnlyDictionary serverPathMappings, + string compilerLogFilePath, + string originalRepositoryRoot) + { + var normalizedServerPathMappings = CopyServerPathMappings(serverPathMappings); + var configuredMapping = normalizedServerPathMappings + .Where(mapping => Paths.IsOrContains(mapping.Key, compilerLogFilePath)) + .OrderByDescending(mapping => mapping.Key.Length) + .FirstOrDefault(); + if (configuredMapping.Key != null) + { + normalizedServerPathMappings[originalRepositoryRoot] = configuredMapping.Value; + } + + return normalizedServerPathMappings; + } + internal static IReadOnlyDictionary AddCompilerLogRepoPathMappings( IReadOnlyDictionary repoPathMappings, string compilerLogFilePath, @@ -608,6 +652,90 @@ internal static IReadOnlyDictionary AddCompilerLogRepoPathMappin 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.LastIndexOf(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, From 0bae827859a2cbea08138a3b0686b87fdc0d1ef8 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 7 Sep 2026 13:21:25 +0200 Subject: [PATCH 5/7] Simplify --- .../src/BinLogParser/BinLogParser.csproj | 1 + .../BinLogParser/CompilerInvocationScoring.cs | 47 ++++++ .../src/BinLogToSln/BinLogToSln.csproj | 1 - src/SourceBrowser/src/BinLogToSln/Program.cs | 60 ++------ .../CompilerLogAssemblyNameTests.cs | 72 ++++++++-- .../HtmlGenerator.Tests.csproj | 1 + .../src/HtmlGenerator/HtmlGenerator.csproj | 1 + .../Pass1-Generation/SolutionGenerator.cs | 136 ++++-------------- 8 files changed, 145 insertions(+), 174 deletions(-) create mode 100644 src/SourceBrowser/src/BinLogParser/CompilerInvocationScoring.cs 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 5578b32..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 => @@ -83,7 +90,9 @@ public void Compiler_source_count_takes_priority_over_injected_generated_documen var normalized = SolutionGenerator.NormalizeCompilerLogAssemblyNames( original, - projectScores: new[] { 1, 10 }); + 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 }); @@ -95,10 +104,43 @@ public void Compiler_source_count_takes_priority_over_injected_generated_documen } [TestMethod] - public void Compiler_log_project_score_excludes_reference_assemblies() + [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( - @"D:\repo\src\libraries\Foo\ref\Foo.csproj", + Path.Combine(@"D:\repo", projectFolder, "Foo.csproj"), "net11.0", sourceFileCount: 10_000) .ShouldBe(int.MinValue); @@ -110,17 +152,19 @@ public void Compiler_log_project_score_excludes_reference_assemblies() } [TestMethod] - public void Compiler_log_project_score_prefers_newer_and_platform_specific_frameworks() + [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) { - var older = SolutionGenerator.CalculateCompilerLogProjectScore( - @"D:\repo\Foo.csproj", "net10.0", sourceFileCount: 100); - var current = SolutionGenerator.CalculateCompilerLogProjectScore( - @"D:\repo\Foo.csproj", "net11.0", sourceFileCount: 100); - var linux = SolutionGenerator.CalculateCompilerLogProjectScore( - @"D:\repo\Foo.csproj", "net11.0-linux", sourceFileCount: 100); - - current.ShouldBeGreaterThan(older); - linux.ShouldBeGreaterThan(current); + SolutionGenerator.CalculateCompilerLogProjectScore( + @"D:\repo\Foo.csproj", targetFramework, sourceFileCount: 100).ShouldBe(expected); } [TestMethod] @@ -210,7 +254,7 @@ public void Compiler_log_parent_traversal_is_normalized_to_repository_relative_p var normalized = SolutionGenerator.NormalizeCompilerLogAssemblyNames( solution, compilerLogDocumentRoots: new[] { repositoryRoot }, - projectFilePaths: new[] { projectFilePath }); + projectFilePaths: SolutionGenerator.CreateCompilerLogProjectPathMap(solution, new[] { projectFilePath })); var normalizedDocument = normalized.Projects.Single().Documents.Single(); normalizedDocument.Folders.ShouldBe( 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/SolutionGenerator.cs b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs index 92e4771..7363ea6 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 @@ -253,31 +254,36 @@ internal static MSBuildWorkspace CreateWorkspace(ImmutableDictionary public static Microsoft.CodeAnalysis.SolutionInfo NormalizeCompilerLogAssemblyNames( Microsoft.CodeAnalysis.SolutionInfo solutionInfo, IReadOnlyDictionary repoPathMappings = null, IEnumerable compilerLogDocumentRoots = null, IReadOnlyList projectScores = null, - IReadOnlyList projectFilePaths = null) + IReadOnlyDictionary projectFilePaths = null) { - var alignedProjectFilePaths = projectFilePaths != null && - projectFilePaths.Count == solutionInfo.Projects.Count() - ? 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((project, index) => ( Project: NormalizeCompilerLogProject( project, repoPathMappings, compilerLogDocumentRoots, - alignedProjectFilePaths != null - ? alignedProjectFilePaths[index] + projectFilePaths != null && projectFilePaths.TryGetValue(project.Id, out var projectFilePath) + ? projectFilePath : null), - Score: projectScores != null && index < projectScores.Count + Score: useProjectScores ? projectScores[index] : project.Documents.Count)) .ToList(); @@ -478,12 +484,13 @@ private void ReadCompilerLogMetadata(string compilerLogFilePath) originalRoot); } - var projectScore = CalculateCompilerLogProjectScore( - compilerCall.ProjectFilePath, - compilerCall.TargetFramework, - sourceFileCount: 0); + var projectScore = 0; try { + 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) @@ -810,95 +817,10 @@ internal static int CalculateCompilerLogProjectScore( string targetFramework, int sourceFileCount) { - var projectDirectory = Path.GetDirectoryName(projectFilePath); - var projectFolder = Path.GetFileName(projectDirectory); - if (string.Equals(projectFolder, "ref", StringComparison.OrdinalIgnoreCase) || - string.Equals(projectFolder, "stubs", StringComparison.OrdinalIgnoreCase) || - string.Equals(Path.GetFileName(Path.GetDirectoryName(projectDirectory)), "cycle-breakers", StringComparison.OrdinalIgnoreCase)) - { - return int.MinValue; - } - - var score = sourceFileCount; - if (TryGetTargetFrameworkVersion(targetFramework, out var major, out var minor, out var platform)) - { - score += major * 1000 + minor * 100; - - if (!string.IsNullOrEmpty(platform)) - { - score += 500; - if (platform.Equals("linux", StringComparison.OrdinalIgnoreCase)) - { - score += 100; - } - else if (platform.Equals("unix", StringComparison.OrdinalIgnoreCase)) - { - score += 50; - } - } - } - - return score; - } - - private static bool TryGetTargetFrameworkVersion( - string targetFramework, - out int major, - out int minor, - out string platform) - { - major = 0; - minor = 0; - platform = string.Empty; - if (string.IsNullOrEmpty(targetFramework)) - { - return false; - } - - var dash = targetFramework.IndexOf('-'); - var framework = dash >= 0 ? targetFramework.Substring(0, dash) : targetFramework; - if (dash >= 0) - { - var platformPart = targetFramework.Substring(dash + 1); - var platformLength = platformPart.TakeWhile(char.IsLetter).Count(); - platform = platformPart.Substring(0, platformLength); - } - - string version; - if (framework.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase)) - { - version = framework.Substring("netstandard".Length); - } - else if (framework.StartsWith("netcoreapp", StringComparison.OrdinalIgnoreCase)) - { - version = framework.Substring("netcoreapp".Length); - } - else if (framework.StartsWith("net", StringComparison.OrdinalIgnoreCase)) - { - version = framework.Substring("net".Length); - } - else - { - return false; - } - - var parts = version.Split('.'); - if (parts.Length > 1) - { - return int.TryParse(parts[0], out major) && - int.TryParse(parts[1], out minor); - } - - if (version.Length >= 2 && - version[0] is >= '1' and <= '4' && - char.IsDigit(version[1])) - { - major = version[0] - '0'; - minor = version[1] - '0'; - return true; - } - - return int.TryParse(version, out major); + return CompilerInvocationScoring.IsReferenceAssembly( + Path.GetDirectoryName(projectFilePath), StringComparison.OrdinalIgnoreCase) + ? int.MinValue + : CompilerInvocationScoring.CalculateScore(targetFramework, sourceFileCount); } private static string GetCompilerLogRepositoryRoot(IEnumerable> compilerPathMappings) @@ -1327,16 +1249,12 @@ private async Task CreateSolutionAsync(string solutionFilePath, Cancel compilerLogProjectPathsById = CreateCompilerLogProjectPathMap( rawSolutionInfo, compilerLogProjectPaths); - var projectFilePathsForNormalization = - compilerLogProjectPathsById.Count == rawSolutionInfo.Projects.Count() - ? compilerLogProjectPaths - : null; var solutionInfo = NormalizeCompilerLogAssemblyNames( rawSolutionInfo, RepoPathMappings, compilerLogDocumentRoots, compilerLogProjectScores, - projectFilePathsForNormalization); + compilerLogProjectPathsById); var adhocWorkspace = new AdhocWorkspace(); adhocWorkspace.AddSolution(solutionInfo); From 54261c35489b1ac63a57fda53f407284da2409ba Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 7 Sep 2026 17:45:36 +0200 Subject: [PATCH 6/7] Handle duplicated segments --- .../CompilerLogWebAccessTests.cs | 49 +++++++++++++++++++ .../Pass1-Generation/SolutionGenerator.cs | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs index fc6684c..330e3e9 100644 --- a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs +++ b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs @@ -3,6 +3,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; using System.Collections.Generic; +using System.Linq; namespace HtmlGenerator.Tests; @@ -262,6 +263,54 @@ public void Compiler_log_original_project_paths_are_mapped_by_project_identity() .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() { diff --git a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs index 7363ea6..8f1b8a3 100644 --- a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs +++ b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs @@ -730,7 +730,7 @@ internal static bool TryGetCompilerLogOriginalRoot( var relativePath = Path.GetRelativePath(configuredRepositoryRoot, mappingPath) .Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var needle = Path.DirectorySeparatorChar + relativePath + Path.DirectorySeparatorChar; - var matchIndex = projectDirectory.LastIndexOf(needle, StringComparison.OrdinalIgnoreCase); + var matchIndex = projectDirectory.IndexOf(needle, StringComparison.OrdinalIgnoreCase); if (matchIndex < 0 || relativePath.Length <= bestRelativePathLength) { continue; From d5c8b22fb0641480551c5a4a033a9ee12b07d36e Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Wed, 9 Sep 2026 17:47:42 +0200 Subject: [PATCH 7/7] Improve source mapping of nested repos --- .../CompilerLogWebAccessTests.cs | 70 ++++++++++++++++--- .../Pass1-Generation/SolutionGenerator.cs | 61 +++++++++------- 2 files changed, 94 insertions(+), 37 deletions(-) diff --git a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs index 330e3e9..6cf10a2 100644 --- a/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs +++ b/src/SourceBrowser/src/HtmlGenerator.Tests/CompilerLogWebAccessTests.cs @@ -34,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 { @@ -47,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/"); @@ -173,7 +180,9 @@ public void Vmr_subrepo_paths_are_aliased_under_the_original_compiler_root() } [TestMethod] - public void Vmr_server_path_is_aliased_from_the_original_outer_root() + [DataRow(false)] + [DataRow(true)] + public void Vmr_server_path_is_aliased_from_the_original_outer_root(bool useOriginalRoot) { var repoPathMappings = new Dictionary { @@ -185,19 +194,58 @@ public void Vmr_server_path_is_aliased_from_the_original_outer_root() [@"C:\index\dotnet\"] = "https://github.com/dotnet/dotnet/tree/abc/", }; - var result = SolutionGenerator.AddCompilerLogServerPathMapping( - serverPathMappings, - @"C:\index\dotnet\runtime.complog", - new Dictionary - { - [@"D:\a\_work\1\s\src\runtime"] = @"/_/", - }, - repoPathMappings); + 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() { diff --git a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs index 8f1b8a3..feaf905 100644 --- a/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs +++ b/src/SourceBrowser/src/HtmlGenerator/Pass1-Generation/SolutionGenerator.cs @@ -477,7 +477,8 @@ private void ReadCompilerLogMetadata(string compilerLogFilePath) ServerPathMappings = AddCompilerLogServerPathMapping( ServerPathMappings, compilerLogFilePath, - originalRoot); + originalRoot, + RepoPathMappings); RepoPathMappings = AddCompilerLogRepoPathMappings( RepoPathMappings, compilerLogFilePath, @@ -564,24 +565,6 @@ internal static IReadOnlyDictionary AddCompilerLogServerPathMapp IEnumerable> compilerPathMappings, IReadOnlyDictionary repoPathMappings = null) { - var normalizedServerPathMappings = CopyServerPathMappings(serverPathMappings); - var compilerLogDirectory = Path.GetDirectoryName(Path.GetFullPath(compilerLogFilePath)); - var standaloneStageOneSourceDirectory = Paths.EnsureTrailingSlash( - Path.Combine(compilerLogDirectory, "src")); - var configuredMapping = normalizedServerPathMappings - .Where(mapping => - Paths.IsOrContains(mapping.Key, compilerLogFilePath) || - string.Equals( - Paths.EnsureTrailingSlash(Path.GetFullPath(mapping.Key)), - standaloneStageOneSourceDirectory, - StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(mapping => mapping.Key.Length) - .FirstOrDefault(); - if (configuredMapping.Key == null) - { - return serverPathMappings; - } - var repositoryRoot = GetCompilerLogRepositoryRoot(compilerPathMappings); if (repositoryRoot == null) { @@ -592,25 +575,51 @@ internal static IReadOnlyDictionary AddCompilerLogServerPathMapp repoPathMappings, compilerLogFilePath, repositoryRoot); - normalizedServerPathMappings[originalRepositoryRoot] = configuredMapping.Value; - return normalizedServerPathMappings; + return AddCompilerLogServerPathMapping( + serverPathMappings, compilerLogFilePath, originalRepositoryRoot, repoPathMappings); } - private static IReadOnlyDictionary AddCompilerLogServerPathMapping( + internal static IReadOnlyDictionary AddCompilerLogServerPathMapping( IReadOnlyDictionary serverPathMappings, string compilerLogFilePath, - string originalRepositoryRoot) + string originalRepositoryRoot, + IReadOnlyDictionary repoPathMappings = null) { var normalizedServerPathMappings = CopyServerPathMappings(serverPathMappings); + var compilerLogDirectory = Path.GetDirectoryName(Path.GetFullPath(compilerLogFilePath)); + var standaloneStageOneSourceDirectory = Paths.EnsureTrailingSlash( + Path.Combine(compilerLogDirectory, "src")); var configuredMapping = normalizedServerPathMappings - .Where(mapping => Paths.IsOrContains(mapping.Key, compilerLogFilePath)) + .Where(mapping => + Paths.IsOrContains(mapping.Key, compilerLogFilePath) || + string.Equals( + Paths.EnsureTrailingSlash(Path.GetFullPath(mapping.Key)), + standaloneStageOneSourceDirectory, + StringComparison.OrdinalIgnoreCase)) .OrderByDescending(mapping => mapping.Key.Length) .FirstOrDefault(); - if (configuredMapping.Key != null) + if (configuredMapping.Key == null) + { + return serverPathMappings; + } + + var configuredRepositoryRoot = (repoPathMappings?.Keys ?? Enumerable.Empty()) + .Where(mapping => Paths.IsOrContains(mapping, compilerLogFilePath)) + .OrderBy(mapping => mapping.Length) + .FirstOrDefault(); + var originalServerRoot = originalRepositoryRoot; + if (configuredRepositoryRoot != null) { - normalizedServerPathMappings[originalRepositoryRoot] = configuredMapping.Value; + // 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[Paths.EnsureTrailingSlash(Path.GetFullPath(originalServerRoot))] = configuredMapping.Value; return normalizedServerPathMappings; }