11using System ;
22using System . Collections . Generic ;
33using System . Collections . Immutable ;
4+ using System . ComponentModel . DataAnnotations ;
45using System . Diagnostics ;
56using System . IO ;
67using System . Linq ;
8+ using System . Runtime . CompilerServices ;
79using System . Text ;
810using System . Threading ;
911using System . Threading . Tasks ;
10- using Microsoft . CodeAnalysis ;
11- using Microsoft . CodeAnalysis . MSBuild ;
12- using Microsoft . CodeAnalysis . Diagnostics ;
1312using ICSharpCode . CodeConverter ;
1413using 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
1622namespace ICSharpCode . CodeConverter . CommandLine ;
1723
@@ -20,81 +26,62 @@ namespace ICSharpCode.CodeConverter.CommandLine;
2026/// </summary>
2127public 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