-
Notifications
You must be signed in to change notification settings - Fork 235
Expand file tree
/
Copy pathCodeConvProgram.cs
More file actions
154 lines (129 loc) · 8.41 KB
/
CodeConvProgram.cs
File metadata and controls
154 lines (129 loc) · 8.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
using System;
using System.ComponentModel.DataAnnotations;
using McMaster.Extensions.CommandLineUtils;
using System.Threading;
using ICSharpCode.CodeConverter.Common;
using System.Threading.Tasks;
using ICSharpCode.CodeConverter.DotNetTool.Util;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using ICSharpCode.CodeConverter.CommandLine.Util;
using System.Reflection;
using System.Diagnostics;
using Microsoft.VisualStudio.Threading;
// ReSharper disable UnassignedGetOnlyAutoProperty - Command line framework initializes these by reflection
namespace ICSharpCode.CodeConverter.CommandLine;
[Command(Name = "codeconv", Description = "Convert code from VB.NET to C# or C# to VB.NET",
ExtendedHelpText = @"
Remarks:
Converts all projects in a solution from VB.NET to C#.
Please backup / commit your files to source control before use.
We recommend running the conversion in-place (i.e. not specifying an output directory) for best performance.
See https://github.com/icsharpcode/CodeConverter for the source code, issues, Visual Studio extension and other info.
")]
[HelpOption("-h|--help")]
public partial class CodeConvProgram
{
public const string CoreOptionDefinition = "--core-only";
/// <remarks>Calls <see cref="OnExecuteAsync(CommandLineApplication)"/> by reflection</remarks>
public static async Task<int> Main(string[] args) => await CommandLineApplication.ExecuteAsync<CodeConvProgram>(args);
// ReSharper disable once UnusedMember.Local - Used by reflection in CommandLineApplication.ExecuteAsync
#pragma warning disable IDE0052 // Remove unread private members - Used by reflection in CommandLineApplication.ExecuteAsync
// ReSharper disable once UnusedParameter.Local - Used by reflection in CommandLineApplication.ExecuteAsync
private async Task<int> OnExecuteAsync(CommandLineApplication _) => await ExecuteAsync();
#pragma warning restore IDE0052 // Remove unread private members
[FileExists]
[Required]
[Argument(0, "Source solution path", "The solution containing project(s) to be converted.")]
public string SolutionPath { get; } = "";
[Option("-i|--include", "Regex matching project file paths to convert. Can be used multiple times", CommandOptionType.MultipleValue)]
public string[] Include { get; } = Array.Empty<string>();
[Option("-e|--exclude", "Regex matching project file paths to exclude from conversion. Can be used multiple times", CommandOptionType.MultipleValue)]
public string[] Exclude { get; } = Array.Empty<string>();
[Option("-t|--target-language", "The language to convert to.", CommandOptionType.SingleValue, ValueName = nameof(Language.CS) + " | " + nameof(Language.VB))]
public Language? TargetLanguage { get; }
[Option("-f|--force", "Wipe the output directory before conversion", CommandOptionType.NoValue)]
public bool Force { get; }
[Option("-b|--best-effort", "Overrides warnings about compilation issues with input, and attempts a best effort conversion anyway", CommandOptionType.NoValue)]
public bool BestEffort { get; }
[FileNotExists]
[Option("-o|--output-directory", "Empty or non-existent directory to copy the solution directory to, then write the output.", CommandOptionType.SingleValue)]
public string? OutputDirectory { get; }
/// <remarks>
/// Also allows semicolon and comma splitting of build properties to be compatible with https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-command-line-reference?view=vs-2019#switches
/// </remarks>
[Option("-p|--build-property", "Set build properties in format: propertyName=propertyValue. Can be used multiple times", CommandOptionType.MultipleValue, ValueName = "Configuration=Release")]
public string[] BuildProperty { get; } = Array.Empty<string>();
private async Task<int> ExecuteAsync()
{
try {
Progress<ConversionProgress> progress = new Progress<ConversionProgress>(s => Console.Out.WriteLine(s.ToString()));
await ConvertAsync(progress, CancellationToken.None);
} catch (Exception ex) {
await Task.Delay(100); // Give any async progress updates a moment to flush so they don't clash with this:
if (!(ex is ValidationException)) {
await Console.Error.WriteLineAsync(Environment.NewLine + ex.GetType() + ex.StackTrace);
if (ex is ReflectionTypeLoadException rtle && rtle.LoaderExceptions is IEnumerable<Exception> loaderExceptions) {
foreach (var e in loaderExceptions) {
await Console.Error.WriteLineAsync(e.Message);
}
}
}
await Console.Error.WriteLineAsync(Environment.NewLine + ex.Message + Environment.NewLine +
"Please report issues at github.com/icsharpcode/CodeConverter"
);
return ProgramExitCodes.EX_SOFTWARE;
}
Console.WriteLine();
Console.WriteLine("Exiting successfully. Report any issues at github.com/icsharpcode/CodeConverter to help us improve the accuracy of future conversions");
return 0;
}
private async Task ConvertAsync(IProgress<ConversionProgress> progress, CancellationToken cancellationToken)
{
string finalSolutionPath = Path.IsPathRooted(SolutionPath)
? SolutionPath
: Path.Combine(Environment.CurrentDirectory, SolutionPath);
IProgress<string> strProgress = new Progress<string>(p => progress.Report(new ConversionProgress(p)));
var ext = Path.GetExtension(finalSolutionPath);
if (!string.Equals(ext, ".sln", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(ext, ".slnx", StringComparison.OrdinalIgnoreCase)) {
throw new ValidationException("Solution path must end in `.sln` or `.slnx`");
}
string? directoryName = string.IsNullOrWhiteSpace(OutputDirectory) ? Path.GetDirectoryName(finalSolutionPath) : OutputDirectory;
var outputDirectory = new DirectoryInfo(directoryName ?? throw new InvalidOperationException("Output directory could not be determined"));
if (await CouldOverwriteUncommittedFilesAsync(outputDirectory)) {
var action = string.IsNullOrWhiteSpace(OutputDirectory) ? "may be overwritten" : "will be deleted";
strProgress.Report($"WARNING: There are files in {outputDirectory.FullName} which {action}, and aren't committed to git");
if (Force) strProgress.Report("Continuing with possibility of data loss due to force option.");
else throw new ValidationException("Aborting to avoid data loss (see above warning). Commit the files to git, remove them, or use the --force option to override this check.");
}
var properties = ParsedProperties();
var joinableTaskFactory = new JoinableTaskFactory(new JoinableTaskContext());
var msbuildWorkspaceConverter = new MsBuildWorkspaceConverter(joinableTaskFactory, finalSolutionPath, BestEffort, properties);
var converterResultsEnumerable = msbuildWorkspaceConverter.ConvertProjectsWhereAsync(ShouldIncludeProject, TargetLanguage, progress, cancellationToken);
await ConversionResultWriter.WriteConvertedAsync(converterResultsEnumerable, finalSolutionPath, outputDirectory, Force, true, strProgress, cancellationToken);
}
private static async Task<bool> CouldOverwriteUncommittedFilesAsync(DirectoryInfo outputDirectory)
{
if (!outputDirectory.Exists || !outputDirectory.ContainsDataOtherThanGitDir()) return false;
return !await outputDirectory.IsGitDiffEmptyAsync();
}
private Dictionary<string, string> ParsedProperties()
{
var props = BuildProperty.SelectMany(bp => bp.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Split('=')));
return props.ToLookup(s => s[0], GetValidatedPropertyValue).ToDictionary();
}
private string GetValidatedPropertyValue(string[] s)
{
return s.Length == 2 ? s[1] : throw new ValidationException($"Build property {s[0]} must have exactly one value, e.g. `{s[0]}=1`");
}
private bool ShouldIncludeProject(Project project)
{
string projectFilePath = project.FilePath ?? "";
var isIncluded = !Include.Any() || Include.Any(regex => Regex.IsMatch(projectFilePath, regex));
return isIncluded && Exclude.All(regex => !Regex.IsMatch(projectFilePath, regex));
}
}