Skip to content

Commit 4c94be2

Browse files
Reintegrate existing logic
1 parent ce78a88 commit 4c94be2

1 file changed

Lines changed: 78 additions & 95 deletions

File tree

CommandLine/CodeConv.Shared/MSBuildWorkspaceConverter.cs

Lines changed: 78 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Collections.Immutable;
4+
using System.ComponentModel.DataAnnotations;
45
using System.Diagnostics;
56
using System.IO;
67
using System.Linq;
8+
using System.Runtime.CompilerServices;
79
using System.Text;
810
using System.Threading;
911
using System.Threading.Tasks;
10-
using Microsoft.CodeAnalysis;
11-
using Microsoft.CodeAnalysis.MSBuild;
12-
using Microsoft.CodeAnalysis.Diagnostics;
1312
using ICSharpCode.CodeConverter;
1413
using ICSharpCode.CodeConverter.Common;
14+
using ICSharpCode.CodeConverter.CSharp;
15+
using ICSharpCode.CodeConverter.Util;
16+
using ICSharpCode.CodeConverter.VB;
17+
using Microsoft.CodeAnalysis;
18+
using Microsoft.CodeAnalysis.Diagnostics;
19+
using Microsoft.CodeAnalysis.MSBuild;
20+
using Microsoft.VisualStudio.Threading;
1521

1622
namespace ICSharpCode.CodeConverter.CommandLine;
1723

@@ -20,81 +26,62 @@ namespace ICSharpCode.CodeConverter.CommandLine;
2026
/// </summary>
2127
public sealed class MsBuildWorkspaceConverter
2228
{
23-
private readonly string _solutionFilePath;
29+
private readonly SolutionLoader _solutionLoader;
30+
private AsyncLazy<Solution>? _cachedSolution;
31+
2432
// The other parameters are ignored for compatibility
25-
public MsBuildWorkspaceConverter(string solutionFilePath, bool isNetCore, object joinableTaskFactory, bool bestEffortConversion = false, Dictionary<string, string>? buildProps = null)
33+
public MsBuildWorkspaceConverter(string solutionFilePath, bool bestEffortConversion = false, Dictionary<string, string>? buildProps = null)
2634
{
27-
_solutionFilePath = solutionFilePath;
35+
_solutionLoader = new SolutionLoader(solutionFilePath, bestEffortConversion, buildProps);
2836
}
2937

30-
/// <summary>
31-
/// Maintains compatibility: yields a ConversionResult for each diagnostic in the solution.
32-
/// </summary>
33-
public async IAsyncEnumerable<ConversionResult> ConvertProjectsWhereAsync(
34-
Func<Project, bool> shouldConvertProject,
35-
Language? targetLanguage,
36-
IProgress<ConversionProgress> progress,
37-
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken token)
38+
39+
public async IAsyncEnumerable<ConversionResult> ConvertProjectsWhereAsync(Func<Project, bool> shouldConvertProject, Language? targetLanguage, IProgress<ConversionProgress> progress, [EnumeratorCancellation] CancellationToken token)
3840
{
39-
var analysis = await AnalyzeSolutionAsync(_solutionFilePath);
40-
foreach (var projectResult in analysis.ProjectResults)
41-
{
42-
if (!shouldConvertProject(projectResult.Project)) continue;
43-
foreach (var diag in projectResult.Diagnostics)
44-
{
45-
var result = new ConversionResult(new Exception(diag.ToString()))
46-
{
47-
SourcePathOrNull = projectResult.Project.FilePath
48-
};
49-
yield return result;
50-
}
41+
var strProgress = new Progress<string>(s => progress.Report(new ConversionProgress(s)));
42+
#pragma warning disable VSTHRD012 // Provide JoinableTaskFactory where allowed - Shouldn't need main thread, and I can't access ThreadHelper without referencing VS shell.
43+
_cachedSolution ??= new AsyncLazy<Solution>(async () => await _solutionLoader.AnalyzeSolutionAsync(strProgress));
44+
#pragma warning restore VSTHRD012 // Provide JoinableTaskFactory where allowed
45+
var solution = await _cachedSolution.GetValueAsync(token);
46+
47+
if (!targetLanguage.HasValue) {
48+
targetLanguage = solution.Projects.Any(p => p.Language == LanguageNames.VisualBasic) ? Language.CS : Language.VB;
5149
}
52-
}
5350

54-
/// <summary>
55-
/// Analyzes a complete .NET solution (.sln) file and returns diagnostics for all projects.
56-
/// </summary>
57-
/// <param name="solutionPath">The absolute path to the .sln file.</param>
58-
/// <param name="configuration">The build configuration (e.g., "Debug" or "Release").</param>
59-
/// <returns>A SolutionAnalysisResult containing all projects and diagnostics.</returns>
60-
public async Task<SolutionAnalysisResult> AnalyzeSolutionAsync(string solutionPath, string configuration = "Debug")
61-
{
62-
var analyzer = new SolutionAnalyzer();
63-
return await analyzer.AnalyzeSolutionAsync(solutionPath, configuration);
51+
var languageConversion = targetLanguage == Language.CS
52+
? (ILanguageConversion)new VBToCSConversion()
53+
: new CSToVBConversion();
54+
languageConversion.ConversionOptions = new ConversionOptions { AbandonOptionalTasksAfter = TimeSpan.FromHours(4) };
55+
var languageNameToConvert = targetLanguage == Language.CS
56+
? LanguageNames.VisualBasic
57+
: LanguageNames.CSharp;
58+
59+
var projectsToConvert = solution.Projects.Where(p => p.Language == languageNameToConvert && shouldConvertProject(p)).ToArray();
60+
var results = SolutionConverter.CreateFor(languageConversion, projectsToConvert, progress, token).ConvertAsync();
61+
await foreach (var r in results.WithCancellation(token)) yield return r;
6462
}
6563

66-
/// <summary>
67-
/// A container for the results of a full solution analysis.
68-
/// </summary>
69-
public class SolutionAnalysisResult
64+
private class SolutionLoader
7065
{
71-
public Solution Solution { get; set; } = null!;
72-
public List<ProjectAnalysisResult> ProjectResults { get; set; } = new List<ProjectAnalysisResult>();
73-
public IEnumerable<Diagnostic> AllDiagnostics => ProjectResults.SelectMany(p => p.Diagnostics);
74-
}
66+
private readonly string _solutionFilePath;
67+
private readonly bool _bestEffort;
68+
private readonly IDictionary<string, string> _buildProps;
7569

76-
/// <summary>
77-
/// A container for the results of a single project analysis.
78-
/// </summary>
79-
public class ProjectAnalysisResult
80-
{
81-
public Project Project { get; set; } = null!;
82-
public IReadOnlyList<Diagnostic> Diagnostics { get; set; } = Array.Empty<Diagnostic>();
83-
}
84-
85-
private class SolutionAnalyzer
86-
{
87-
private readonly List<Diagnostic> _loadDiagnostics = new();
70+
public SolutionLoader(string solutionFilePath, bool bestEffort, IDictionary<string, string>? buildProps)
71+
{
72+
_solutionFilePath = solutionFilePath;
73+
_bestEffort = bestEffort;
74+
_buildProps = buildProps ?? new Dictionary<string, string>();
75+
}
8876

89-
public async Task<SolutionAnalysisResult> AnalyzeSolutionAsync(string solutionPath, string configuration = "Debug")
77+
public async Task<Solution> AnalyzeSolutionAsync(IProgress<string> progress, string configuration = "Debug")
9078
{
79+
progress.Report($"Running dotnet restore on {_solutionFilePath}");
9180
// === PREREQUISITE: Run 'dotnet restore' ===
92-
await RunDotnetRestoreAsync(solutionPath);
93-
94-
_loadDiagnostics.Clear();
81+
await RunDotnetRestoreAsync(_solutionFilePath);
9582

9683
// === STEP 1: Create and Configure Workspace ===
97-
var properties = new Dictionary<string, string>
84+
var properties = new Dictionary<string, string>(_buildProps)
9885
{
9986
{ "Configuration", configuration },
10087
{ "RunAnalyzers", "true" },
@@ -107,49 +94,49 @@ public async Task<SolutionAnalysisResult> AnalyzeSolutionAsync(string solutionPa
10794
Solution solution;
10895
try
10996
{
110-
solution = await workspace.OpenSolutionAsync(solutionPath);
97+
solution = await workspace.OpenSolutionAsync(_solutionFilePath);
11198
}
11299
finally
113100
{
114101
workspace.WorkspaceFailed -= HandleWorkspaceFailure;
115102
}
116103

117-
// === STEP 2: Analyze Each Project ===
118-
var projectResults = new List<ProjectAnalysisResult>();
119-
foreach (var project in solution.Projects)
120-
{
121-
var projectResult = await AnalyzeProjectAsync(project);
122-
projectResults.Add(projectResult);
104+
var errorString = await GetCompilationErrorsAsync(workspace, solution.Projects);
105+
if (string.IsNullOrEmpty(errorString)) return solution;
106+
errorString = " " + errorString.Replace(Environment.NewLine, Environment.NewLine + " ");
107+
progress.Report($"Compilation errors found before conversion.:{Environment.NewLine}{errorString}");
108+
109+
if (_bestEffort) {
110+
progress.Report("Attempting best effort conversion on broken input due to override");
111+
} else {
112+
throw CreateException("Fix compilation errors before conversion for an accurate conversion, or as a last resort, use the best effort conversion option", errorString);
123113
}
124114

125-
// Include load diagnostics in the first project or create a dummy entry
126-
if (projectResults.Count > 0 && _loadDiagnostics.Any())
115+
return solution;
116+
117+
ValidationException CreateException(string mainMessage, string fullDetail)
127118
{
128-
var firstProject = projectResults[0];
129-
projectResults[0] = new ProjectAnalysisResult
130-
{
131-
Project = firstProject.Project,
132-
Diagnostics = _loadDiagnostics.Concat(firstProject.Diagnostics).ToList()
133-
};
119+
return new ValidationException($"{mainMessage}:{Environment.NewLine}{fullDetail}{Environment.NewLine}{mainMessage}");
134120
}
121+
}
135122

136-
return new SolutionAnalysisResult
137-
{
138-
Solution = solution,
139-
ProjectResults = projectResults
140-
};
123+
private async Task<string> GetCompilationErrorsAsync(MSBuildWorkspace workspace, IEnumerable<Project> projectsToConvert)
124+
{
125+
var workspaceErrors = workspace.Diagnostics.GetErrorString();
126+
var errors = await projectsToConvert.ParallelSelectAwaitAsync(async x => {
127+
var c = await x.GetCompilationAsync() ?? throw new InvalidOperationException($"Compilation could not be created for {x.Language}");
128+
return new[] { CompilationWarnings.WarningsForCompilation(c, c.AssemblyName) };
129+
}, Env.MaxDop).ToArrayAsync();
130+
var errorString = string.Join("\r\n", workspaceErrors.Yield().Concat(errors.SelectMany(w => w)).Where(w => w != null));
131+
return errorString;
141132
}
142133

143-
private async Task<ProjectAnalysisResult> AnalyzeProjectAsync(Project project)
134+
private async Task<List<Diagnostic>> GetDiagnosticsAsync(Project project)
144135
{
145136
Compilation? compilation = await project.GetCompilationAsync();
146-
if (compilation is null)
147-
{
148-
return new ProjectAnalysisResult
149-
{
150-
Project = project,
151-
Diagnostics = new List<Diagnostic>()
152-
};
137+
if (compilation is null) {
138+
var collection = Diagnostic.Create("FAIL", "Compilation", "Compilation is null", DiagnosticSeverity.Error, DiagnosticSeverity.Error, true, 3);
139+
return new List<Diagnostic> {collection};
153140
}
154141

155142
ImmutableArray<Diagnostic> compileDiagnostics = compilation.GetDiagnostics();
@@ -169,11 +156,7 @@ private async Task<ProjectAnalysisResult> AnalyzeProjectAsync(Project project)
169156
.Concat(analyzerDiagnostics)
170157
.ToList();
171158

172-
return new ProjectAnalysisResult
173-
{
174-
Project = project,
175-
Diagnostics = allDiagnostics
176-
};
159+
return allDiagnostics;
177160
}
178161

179162
private void HandleWorkspaceFailure(object? sender, WorkspaceDiagnosticEventArgs e)

0 commit comments

Comments
 (0)