Skip to content

Commit 11e7acf

Browse files
authored
Introduction of Compiler Command class (Buildalyzer#243)
1 parent 5e89b0b commit 11e7acf

19 files changed

+703
-694
lines changed

.editorconfig

Lines changed: 133 additions & 119 deletions
Large diffs are not rendered by default.

src/Buildalyzer/AnalyzerResult.cs

Lines changed: 15 additions & 347 deletions
Large diffs are not rendered by default.

src/Buildalyzer/Buildalyzer.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" AllowedVersion="[4,)" />
11+
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.0.1" AllowedVersion="[4,)" />
1012
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
1113
<PackageReference Include="MsBuildPipeLogger.Server" Version="1.1.6" />
1214
<PackageReference Include="Microsoft.Build" Version="17.0.1" />
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#nullable enable
2+
3+
using Microsoft.CodeAnalysis.CSharp;
4+
5+
namespace Buildalyzer;
6+
7+
public sealed record CSharpCompilerCommand : RoslynBasedCompilerCommand<CSharpCommandLineArguments>
8+
{
9+
/// <inheritdoc />
10+
public override CompilerLanguage Language => CompilerLanguage.CSharp;
11+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#nullable enable
2+
3+
using System.IO;
4+
using Buildalyzer.IO;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.VisualBasic;
8+
9+
namespace Buildalyzer;
10+
11+
public static class Compiler
12+
{
13+
public static class CommandLine
14+
{
15+
[Pure]
16+
public static string[]? SplitCommandLineIntoArguments(string? commandLine, CompilerLanguage language) => language switch
17+
{
18+
CompilerLanguage.CSharp => RoslynCommandLineParser.SplitCommandLineIntoArguments(commandLine, "csc.dll", "csc.exe"),
19+
CompilerLanguage.VisualBasic => RoslynCommandLineParser.SplitCommandLineIntoArguments(commandLine, "vbc.dll", "vbc.exe"),
20+
CompilerLanguage.FSharp => FSharpCommandLineParser.SplitCommandLineIntoArguments(commandLine),
21+
_ => throw new NotSupportedException($"The {language} language is not supported."),
22+
};
23+
24+
[Pure]
25+
public static CompilerCommand Parse(DirectoryInfo? baseDir, string commandLine, CompilerLanguage language)
26+
{
27+
var tokens = SplitCommandLineIntoArguments(commandLine, language) ?? throw new FormatException("Commandline could not be parsed.");
28+
var location = new FileInfo(tokens[0]);
29+
var args = tokens[1..];
30+
31+
return Parse(baseDir?.ToString(), location.Directory?.ToString(), args, language) with
32+
{
33+
Text = commandLine,
34+
CompilerLocation = location,
35+
Arguments = args.ToImmutableArray(),
36+
};
37+
38+
CompilerCommand Parse(string? baseDir, string? root, string[] args, CompilerLanguage language)
39+
{
40+
return language switch
41+
{
42+
CompilerLanguage.CSharp => CSharpParser.Parse(args, baseDir, root),
43+
CompilerLanguage.VisualBasic => VisualBasicParser.Parse(args, baseDir, root),
44+
CompilerLanguage.FSharp => FSharpParser.Parse(args),
45+
_ => throw new NotSupportedException($"The {language} language is not supported."),
46+
};
47+
}
48+
}
49+
}
50+
51+
private static class CSharpParser
52+
{
53+
[Pure]
54+
public static CSharpCompilerCommand Parse(string[] args, string? baseDir, string? root)
55+
{
56+
var arguments = CSharpCommandLineParser.Default.Parse(args, baseDir, root);
57+
var command = new CSharpCompilerCommand()
58+
{
59+
CommandLineArguments = arguments,
60+
};
61+
return RoslynParser.Enrich(command, arguments);
62+
}
63+
}
64+
65+
private static class VisualBasicParser
66+
{
67+
[Pure]
68+
public static VisualBasicCompilerCommand Parse(string[] args, string? baseDir, string? root)
69+
{
70+
var arguments = VisualBasicCommandLineParser.Default.Parse(args, baseDir, root);
71+
var command = new VisualBasicCompilerCommand()
72+
{
73+
CommandLineArguments = arguments,
74+
PreprocessorSymbols = arguments.ParseOptions.PreprocessorSymbols.ToImmutableDictionary(),
75+
};
76+
return RoslynParser.Enrich(command, arguments);
77+
}
78+
}
79+
80+
private static class FSharpParser
81+
{
82+
[Pure]
83+
public static FSharpCompilerCommand Parse(string[] args)
84+
{
85+
var sourceFiles = args.Where(a => a[0] != '-').Select(IOPath.Parse);
86+
var preprocessorSymbolNames = args.Where(a => a.StartsWith("--define:")).Select(a => a[9..]);
87+
var metadataReferences = args.Where(a => a.StartsWith("-r:")).Select(a => a[3..]);
88+
89+
return new()
90+
{
91+
MetadataReferences = metadataReferences.ToImmutableArray(),
92+
PreprocessorSymbolNames = preprocessorSymbolNames.ToImmutableArray(),
93+
SourceFiles = sourceFiles.ToImmutableArray(),
94+
};
95+
}
96+
}
97+
98+
private static class RoslynParser
99+
{
100+
public static TCommand Enrich<TCommand>(TCommand command, CommandLineArguments arguments)
101+
where TCommand : CompilerCommand
102+
103+
=> command with
104+
{
105+
AnalyzerReferences = arguments.AnalyzerReferences.Select(AsIOPath).ToImmutableArray(),
106+
AnalyzerConfigPaths = arguments.AnalyzerConfigPaths.Select(IOPath.Parse).ToImmutableArray(),
107+
MetadataReferences = arguments.MetadataReferences.Select(m => m.Reference).ToImmutableArray(),
108+
PreprocessorSymbolNames = arguments.ParseOptions.PreprocessorSymbolNames.ToImmutableArray(),
109+
110+
SourceFiles = arguments.SourceFiles.Select(AsIOPath).ToImmutableArray(),
111+
AdditionalFiles = arguments.AdditionalFiles.Select(AsIOPath).ToImmutableArray(),
112+
EmbeddedFiles = arguments.EmbeddedFiles.Select(AsIOPath).ToImmutableArray(),
113+
};
114+
}
115+
116+
[Pure]
117+
internal static IOPath AsIOPath(CommandLineAnalyzerReference file) => IOPath.Parse(file.FilePath);
118+
119+
[Pure]
120+
internal static IOPath AsIOPath(CommandLineSourceFile file) => IOPath.Parse(file.Path);
121+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#nullable enable
2+
3+
using System.IO;
4+
using Buildalyzer.IO;
5+
using Microsoft.CodeAnalysis;
6+
7+
namespace Buildalyzer;
8+
9+
[DebuggerDisplay("{Language.Display()}: {Text}")]
10+
public abstract record CompilerCommand
11+
{
12+
/// <summary>The compiler lanuague.</summary>
13+
public abstract CompilerLanguage Language { get; }
14+
15+
/// <summary>The original text of the compiler command.</summary>
16+
public string Text { get; init; } = string.Empty;
17+
18+
/// <summary>The parsed command line arguments.</summary>
19+
public ImmutableArray<string> Arguments { get; init; }
20+
21+
/// <summary>The location of the used compiler.</summary>
22+
public FileInfo? CompilerLocation { get; init; }
23+
24+
/// <inheritdoc cref="CommandLineArguments.Errors" />
25+
public ImmutableArray<Diagnostic> Errors { get; init; }
26+
27+
/// <inheritdoc cref="CommandLineArguments.SourceFiles" />
28+
public ImmutableArray<IOPath> SourceFiles { get; init; }
29+
30+
/// <inheritdoc cref="CommandLineArguments.AdditionalFiles" />
31+
public ImmutableArray<IOPath> AdditionalFiles { get; init; }
32+
33+
/// <inheritdoc cref="CommandLineArguments.EmbeddedFiles" />
34+
public ImmutableArray<IOPath> EmbeddedFiles { get; init; }
35+
36+
/// <inheritdoc cref="CommandLineArguments.AnalyzerReferences" />
37+
public ImmutableArray<IOPath> AnalyzerReferences { get; init; }
38+
39+
/// <inheritdoc cref="CommandLineArguments.AnalyzerConfigPaths" />
40+
public ImmutableArray<IOPath> AnalyzerConfigPaths { get; init; }
41+
42+
/// <inheritdoc cref="ParseOptions.PreprocessorSymbolNames" />
43+
public ImmutableArray<string> PreprocessorSymbolNames { get; init; }
44+
45+
/// <inheritdoc cref="CommandLineArguments.MetadataReferences" />
46+
public ImmutableArray<string> MetadataReferences { get; init; }
47+
48+
/// <inheritdoc />
49+
[Pure]
50+
public override string ToString() => Text ?? string.Empty;
51+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Buildalyzer;
2+
3+
/// <summary>The compiler language.</summary>
4+
public enum CompilerLanguage
5+
{
6+
/// <summary>None.</summary>
7+
None = 0,
8+
9+
/// <summary>C#.</summary>
10+
CSharp = 1,
11+
12+
/// <summary>VB.NET.</summary>
13+
VisualBasic = 2,
14+
15+
/// <summary>F#.</summary>
16+
FSharp = 3,
17+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#nullable enable
2+
3+
namespace Buildalyzer;
4+
5+
internal static class FSharpCommandLineParser
6+
{
7+
[Pure]
8+
public static string[]? SplitCommandLineIntoArguments(string? commandLine)
9+
=> commandLine?.Split(Splitters, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) is { Length: > 0 } args
10+
&& First(args[0]).ToArray() is { Length: >= 1 } first
11+
? first.Concat(args[1..]).ToArray()
12+
: null;
13+
14+
[Pure]
15+
private static IEnumerable<string> First(string arg)
16+
=> Tokenize(arg)
17+
.SkipWhile(NotCompilerLocation)
18+
.Select(a => a.Trim())
19+
.Where(a => a.Length > 0);
20+
21+
[Pure]
22+
private static IEnumerable<string> Tokenize(string arg)
23+
{
24+
var first = 0;
25+
var cursor = 0;
26+
var quote = false;
27+
28+
foreach (var ch in arg)
29+
{
30+
if (ch == '"')
31+
{
32+
if (quote)
33+
{
34+
quote = false;
35+
yield return arg[first..cursor];
36+
}
37+
else
38+
{
39+
quote = true;
40+
}
41+
first = cursor + 1;
42+
}
43+
else if (ch == ' ' && cursor >= first && !quote)
44+
{
45+
yield return arg[first..cursor];
46+
first = cursor + 1;
47+
}
48+
cursor++;
49+
}
50+
yield return arg[first..];
51+
}
52+
53+
[Pure]
54+
public static bool NotCompilerLocation(string s)
55+
=> !s.EndsWith("fsc.dll", StringComparison.OrdinalIgnoreCase)
56+
&& !s.EndsWith("fsc.exe", StringComparison.OrdinalIgnoreCase);
57+
58+
private static readonly char[] Splitters = ['\r', '\n'];
59+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Buildalyzer.IO;
2+
using Microsoft.CodeAnalysis;
3+
4+
namespace Buildalyzer;
5+
6+
public sealed record FSharpCompilerCommand : CompilerCommand
7+
{
8+
/// <inheritdoc />
9+
public override CompilerLanguage Language => CompilerLanguage.FSharp;
10+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#nullable enable
2+
3+
using Microsoft.CodeAnalysis;
4+
5+
namespace Buildalyzer;
6+
7+
public abstract record RoslynBasedCompilerCommand<TArguments> : CompilerCommand
8+
where TArguments : CommandLineArguments
9+
{
10+
/// <summary>The Roslyn comppiler arguments.</summary>
11+
public TArguments? CommandLineArguments { get; init; }
12+
}

0 commit comments

Comments
 (0)