Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduction of Compiler Command class #243

Merged
merged 15 commits into from
Apr 2, 2024
252 changes: 133 additions & 119 deletions .editorconfig

Large diffs are not rendered by default.

362 changes: 15 additions & 347 deletions src/Buildalyzer/AnalyzerResult.cs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/Buildalyzer/Buildalyzer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" AllowedVersion="[4,)" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.0.1" AllowedVersion="[4,)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="MsBuildPipeLogger.Server" Version="1.1.6" />
<PackageReference Include="Microsoft.Build" Version="17.0.1" />
Expand Down
11 changes: 11 additions & 0 deletions src/Buildalyzer/Compiler/CSharpCompilerCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable enable

using Microsoft.CodeAnalysis.CSharp;

namespace Buildalyzer;

public sealed record CSharpCompilerCommand : RoslynBasedCompilerCommand<CSharpCommandLineArguments>
{
/// <inheritdoc />
public override CompilerLanguage Language => CompilerLanguage.CSharp;
}
121 changes: 121 additions & 0 deletions src/Buildalyzer/Compiler/Compiler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#nullable enable

using System.IO;
using Buildalyzer.IO;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.VisualBasic;

namespace Buildalyzer;

public static class Compiler
{
public static class CommandLine
{
[Pure]
public static string[]? SplitCommandLineIntoArguments(string? commandLine, CompilerLanguage language) => language switch
{
CompilerLanguage.CSharp => RoslynCommandLineParser.SplitCommandLineIntoArguments(commandLine, "csc.dll", "csc.exe"),
CompilerLanguage.VisualBasic => RoslynCommandLineParser.SplitCommandLineIntoArguments(commandLine, "vbc.dll", "vbc.exe"),
CompilerLanguage.FSharp => FSharpCommandLineParser.SplitCommandLineIntoArguments(commandLine),
_ => throw new NotSupportedException($"The {language} language is not supported."),
};

[Pure]
public static CompilerCommand Parse(DirectoryInfo? baseDir, string commandLine, CompilerLanguage language)
{
var tokens = SplitCommandLineIntoArguments(commandLine, language) ?? throw new FormatException("Commandline could not be parsed.");
var location = new FileInfo(tokens[0]);

Check warning on line 28 in src/Buildalyzer/Compiler/Compiler.cs

View workflow job for this annotation

GitHub Actions / Build (windows-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1012)

Check warning on line 28 in src/Buildalyzer/Compiler/Compiler.cs

View workflow job for this annotation

GitHub Actions / Build (macos-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1012)
var args = tokens[1..];

return Parse(baseDir?.ToString(), location.Directory?.ToString(), args, language) with
{
Text = commandLine,
CompilerLocation = location,
Arguments = args.ToImmutableArray(),
};

CompilerCommand Parse(string? baseDir, string? root, string[] args, CompilerLanguage language)
{
return language switch
{
CompilerLanguage.CSharp => CSharpParser.Parse(args, baseDir, root),
CompilerLanguage.VisualBasic => VisualBasicParser.Parse(args, baseDir, root),
CompilerLanguage.FSharp => FSharpParser.Parse(args),
_ => throw new NotSupportedException($"The {language} language is not supported."),
};
}
}
}

private static class CSharpParser
{
[Pure]
public static CSharpCompilerCommand Parse(string[] args, string? baseDir, string? root)
{
var arguments = CSharpCommandLineParser.Default.Parse(args, baseDir, root);
var command = new CSharpCompilerCommand()

Check warning on line 57 in src/Buildalyzer/Compiler/Compiler.cs

View workflow job for this annotation

GitHub Actions / Build (windows-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1012)

Check warning on line 57 in src/Buildalyzer/Compiler/Compiler.cs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1012)

Check warning on line 57 in src/Buildalyzer/Compiler/Compiler.cs

View workflow job for this annotation

GitHub Actions / Build (macos-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1012)
{
CommandLineArguments = arguments,
};
return RoslynParser.Enrich(command, arguments);
}
}

private static class VisualBasicParser
{
[Pure]
public static VisualBasicCompilerCommand Parse(string[] args, string? baseDir, string? root)
{
var arguments = VisualBasicCommandLineParser.Default.Parse(args, baseDir, root);
var command = new VisualBasicCompilerCommand()

Check warning on line 71 in src/Buildalyzer/Compiler/Compiler.cs

View workflow job for this annotation

GitHub Actions / Build (windows-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1012)

Check warning on line 71 in src/Buildalyzer/Compiler/Compiler.cs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1012)

Check warning on line 71 in src/Buildalyzer/Compiler/Compiler.cs

View workflow job for this annotation

GitHub Actions / Build (macos-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1012)
{
CommandLineArguments = arguments,
PreprocessorSymbols = arguments.ParseOptions.PreprocessorSymbols.ToImmutableDictionary(),
};
return RoslynParser.Enrich(command, arguments);
}
}

private static class FSharpParser
{
[Pure]
public static FSharpCompilerCommand Parse(string[] args)
{
var sourceFiles = args.Where(a => a[0] != '-').Select(IOPath.Parse);
var preprocessorSymbolNames = args.Where(a => a.StartsWith("--define:")).Select(a => a[9..]);
var metadataReferences = args.Where(a => a.StartsWith("-r:")).Select(a => a[3..]);

return new()
{
MetadataReferences = metadataReferences.ToImmutableArray(),
PreprocessorSymbolNames = preprocessorSymbolNames.ToImmutableArray(),
SourceFiles = sourceFiles.ToImmutableArray(),
};
}
}

private static class RoslynParser
{
public static TCommand Enrich<TCommand>(TCommand command, CommandLineArguments arguments)
where TCommand : CompilerCommand

=> command with
{
AnalyzerReferences = arguments.AnalyzerReferences.Select(AsIOPath).ToImmutableArray(),
AnalyzerConfigPaths = arguments.AnalyzerConfigPaths.Select(IOPath.Parse).ToImmutableArray(),
MetadataReferences = arguments.MetadataReferences.Select(m => m.Reference).ToImmutableArray(),
PreprocessorSymbolNames = arguments.ParseOptions.PreprocessorSymbolNames.ToImmutableArray(),

SourceFiles = arguments.SourceFiles.Select(AsIOPath).ToImmutableArray(),
AdditionalFiles = arguments.AdditionalFiles.Select(AsIOPath).ToImmutableArray(),
EmbeddedFiles = arguments.EmbeddedFiles.Select(AsIOPath).ToImmutableArray(),
};
}

[Pure]
internal static IOPath AsIOPath(CommandLineAnalyzerReference file) => IOPath.Parse(file.FilePath);

[Pure]
internal static IOPath AsIOPath(CommandLineSourceFile file) => IOPath.Parse(file.Path);
}
51 changes: 51 additions & 0 deletions src/Buildalyzer/Compiler/CompilerCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#nullable enable

using System.IO;
using Buildalyzer.IO;
using Microsoft.CodeAnalysis;

namespace Buildalyzer;

[DebuggerDisplay("{Language.Display()}: {Text}")]
public abstract record CompilerCommand
{
/// <summary>The compiler lanuague.</summary>
public abstract CompilerLanguage Language { get; }

/// <summary>The original text of the compiler command.</summary>
public string Text { get; init; } = string.Empty;

/// <summary>The parsed command line arguments.</summary>
public ImmutableArray<string> Arguments { get; init; }

/// <summary>The location of the used compiler.</summary>
public FileInfo? CompilerLocation { get; init; }

/// <inheritdoc cref="CommandLineArguments.Errors" />
public ImmutableArray<Diagnostic> Errors { get; init; }

/// <inheritdoc cref="CommandLineArguments.SourceFiles" />
public ImmutableArray<IOPath> SourceFiles { get; init; }

/// <inheritdoc cref="CommandLineArguments.AdditionalFiles" />
public ImmutableArray<IOPath> AdditionalFiles { get; init; }

/// <inheritdoc cref="CommandLineArguments.EmbeddedFiles" />
public ImmutableArray<IOPath> EmbeddedFiles { get; init; }

/// <inheritdoc cref="CommandLineArguments.AnalyzerReferences" />
public ImmutableArray<IOPath> AnalyzerReferences { get; init; }

/// <inheritdoc cref="CommandLineArguments.AnalyzerConfigPaths" />
public ImmutableArray<IOPath> AnalyzerConfigPaths { get; init; }

/// <inheritdoc cref="ParseOptions.PreprocessorSymbolNames" />
public ImmutableArray<string> PreprocessorSymbolNames { get; init; }

/// <inheritdoc cref="CommandLineArguments.MetadataReferences" />
public ImmutableArray<string> MetadataReferences { get; init; }

/// <inheritdoc />
[Pure]
public override string ToString() => Text ?? string.Empty;
}
17 changes: 17 additions & 0 deletions src/Buildalyzer/Compiler/CompilerLanguage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Buildalyzer;

/// <summary>The compiler language.</summary>
public enum CompilerLanguage
{
/// <summary>None.</summary>
None = 0,

/// <summary>C#.</summary>
CSharp = 1,

/// <summary>VB.NET.</summary>
VisualBasic = 2,

/// <summary>F#.</summary>
FSharp = 3,
}
59 changes: 59 additions & 0 deletions src/Buildalyzer/Compiler/FSharpCommandLineParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#nullable enable

namespace Buildalyzer;

internal static class FSharpCommandLineParser
{
[Pure]
public static string[]? SplitCommandLineIntoArguments(string? commandLine)
=> commandLine?.Split(Splitters, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) is { Length: > 0 } args
&& First(args[0]).ToArray() is { Length: >= 1 } first
? first.Concat(args[1..]).ToArray()
: null;

[Pure]
private static IEnumerable<string> First(string arg)
=> Tokenize(arg)
.SkipWhile(NotCompilerLocation)
.Select(a => a.Trim())
.Where(a => a.Length > 0);

[Pure]
private static IEnumerable<string> Tokenize(string arg)
{
var first = 0;
var cursor = 0;
var quote = false;

Check warning on line 26 in src/Buildalyzer/Compiler/FSharpCommandLineParser.cs

View workflow job for this annotation

GitHub Actions / Build (windows-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1012)

foreach (var ch in arg)

Check warning on line 28 in src/Buildalyzer/Compiler/FSharpCommandLineParser.cs

View workflow job for this annotation

GitHub Actions / Build (windows-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1009)
{
if (ch == '"')
{
if (quote)
{
quote = false;
yield return arg[first..cursor];
}
else
{
quote = true;
}
first = cursor + 1;
}
else if (ch == ' ' && cursor >= first && !quote)
{
yield return arg[first..cursor];
first = cursor + 1;
}
cursor++;
}
yield return arg[first..];
}

[Pure]
public static bool NotCompilerLocation(string s)
=> !s.EndsWith("fsc.dll", StringComparison.OrdinalIgnoreCase)
&& !s.EndsWith("fsc.exe", StringComparison.OrdinalIgnoreCase);

private static readonly char[] Splitters = ['\r', '\n'];
}
10 changes: 10 additions & 0 deletions src/Buildalyzer/Compiler/FSharpCompilerCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Buildalyzer.IO;
using Microsoft.CodeAnalysis;

namespace Buildalyzer;

public sealed record FSharpCompilerCommand : CompilerCommand
{
/// <inheritdoc />
public override CompilerLanguage Language => CompilerLanguage.FSharp;
}
12 changes: 12 additions & 0 deletions src/Buildalyzer/Compiler/RoslynBasedCompilerCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#nullable enable

using Microsoft.CodeAnalysis;

namespace Buildalyzer;

public abstract record RoslynBasedCompilerCommand<TArguments> : CompilerCommand
where TArguments : CommandLineArguments
{
/// <summary>The Roslyn comppiler arguments.</summary>
public TArguments? CommandLineArguments { get; init; }
}
28 changes: 28 additions & 0 deletions src/Buildalyzer/Compiler/RoslynCommandLineParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#nullable enable

using Microsoft.CodeAnalysis;

namespace Buildalyzer;

internal static class RoslynCommandLineParser
{
[Pure]
public static string[]? SplitCommandLineIntoArguments(string? commandLine, params string[] execs)
=> Split(CommandLineParser.SplitCommandLineIntoArguments(commandLine ?? string.Empty, removeHashComments: true).ToArray(), execs);

[Pure]
private static string[]? Split(string[] args, string[] execs)
{
foreach (var exec in execs)

Check warning on line 16 in src/Buildalyzer/Compiler/RoslynCommandLineParser.cs

View workflow job for this annotation

GitHub Actions / Build (windows-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1009)

Check warning on line 16 in src/Buildalyzer/Compiler/RoslynCommandLineParser.cs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest)

([deprecated] Use RCS1264 instead) Use explicit type instead of 'var' (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1009)
{
for (var i = 0; i < args.Length - 1; i++)
{
if (args[i].EndsWith(exec, StringComparison.OrdinalIgnoreCase))
{
return args[i..];
}
}
}
return null;
}
}
14 changes: 14 additions & 0 deletions src/Buildalyzer/Compiler/VisualBasicCompilerCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#nullable enable

using Microsoft.CodeAnalysis.VisualBasic;

namespace Buildalyzer;

public sealed record VisualBasicCompilerCommand : RoslynBasedCompilerCommand<VisualBasicCommandLineArguments>
{
/// <inheritdoc />
public override CompilerLanguage Language => CompilerLanguage.VisualBasic;

/// <inheritdoc cref="VisualBasicParseOptions.PreprocessorSymbols" />
public ImmutableDictionary<string, object>? PreprocessorSymbols { get; init; }
}
14 changes: 14 additions & 0 deletions src/Buildalyzer/Extensions/CompilerLanguageExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Buildalyzer;

internal static class CompilerLanguageExtensions
{
/// <summary>Represents the <see cref="CompilerLanguage"/> as (DEBUG) display string.</summary>
[Pure]
public static string Display(this CompilerLanguage language) => language switch
{
CompilerLanguage.CSharp => "C#",
CompilerLanguage.FSharp => "F#",
CompilerLanguage.VisualBasic => "VB.NET",
_ => language.ToString(),
};
}
42 changes: 42 additions & 0 deletions src/Buildalyzer/Guard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#nullable enable

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace Buildalyzer;

/// <summary>Supplies parameter guarding for methods and constructors.</summary>
/// <remarks>
/// Advised usage:
/// * Change the namespace to maximum shared namespace amongst the using projects
/// * Keep it internal and use [assembly: InternalsVisibleTo] to open up access
/// * Add specific Guard methods if you software needs them.
/// * Keep the checks cheap so that you also can run them in production code.
/// </remarks>
[ExcludeFromCodeCoverage]
internal static class Guard
{
/// <summary>Guards the parameter if not null, otherwise throws an argument (null) exception.</summary>
/// <typeparam name="T">The type to guard; cannot be a structure.</typeparam>
/// <param name="parameter">The parameter to guard.</param>
/// <param name="paramName">The name of the parameter.</param>
/// <returns>
/// The guarded parameter.
/// </returns>
[DebuggerStepThrough]
public static T NotNull<T>([ValidatedNotNull] T? parameter, [CallerArgumentExpression(nameof(parameter))] string? paramName = null)
where T : class
=> parameter ?? throw new ArgumentNullException(paramName);

/// <summary>Marks the NotNull argument as being validated for not being null, to satisfy the static code analysis.</summary>
/// <remarks>
/// Notice that it does not matter what this attribute does, as long as
/// it is named ValidatedNotNullAttribute.
///
/// It is marked as conditional, as does not add anything to have the attribute compiled.
/// </remarks>
[Conditional("Analysis")]
[AttributeUsage(AttributeTargets.Parameter)]
private sealed class ValidatedNotNullAttribute : Attribute { }

Check warning on line 41 in src/Buildalyzer/Guard.cs

View workflow job for this annotation

GitHub Actions / Build (windows-latest)

Check warning on line 41 in src/Buildalyzer/Guard.cs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest)

Check warning on line 41 in src/Buildalyzer/Guard.cs

View workflow job for this annotation

GitHub Actions / Build (macos-latest)

}
Loading
Loading