Skip to content

Commit

Permalink
Add support for C# 11 and newer versions
Browse files Browse the repository at this point in the history
If a langversion is explicitly provided, use it verbatim. The
compiler will check validity and emit and error if needed.
Otherwise, use the C# language version known to be supported by
the current runtime, as we may be using csc/Roslyn from a newer
runtime where the "latest" language features depend on new APIs
that aren't available on the current runtime.

If we do not have a language version mapping for the runtime, use
"latest" so all its language features are available until we add
the version mapping.

Fix #166 - Support for C# 11
  • Loading branch information
mhutch committed Oct 12, 2023
1 parent 821b31a commit 51a5db5
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 95 deletions.
24 changes: 21 additions & 3 deletions Mono.TextTemplating.Roslyn/RoslynCodeCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ CodeCompilerResult CompileFileInternal (
CancellationToken token)
{
CSharpCommandLineArguments args = null;
bool hasLangVersionArg = false;
if (arguments.AdditionalArguments != null) {
var splitArgs = CommandLineParser.SplitCommandLineIntoArguments (arguments.AdditionalArguments, false);
if (splitArgs.Any ()) {
args = CSharpCommandLineParser.Default.Parse (splitArgs, arguments.TempDirectory, null, null);
}
hasLangVersionArg = splitArgs.Any (CSharpLangVersionHelper.IsLangVersionArg);
}

var references = new List<MetadataReference> ();
Expand All @@ -52,15 +54,31 @@ CodeCompilerResult CompileFileInternal (

var parseOptions = args?.ParseOptions ?? new CSharpParseOptions();

// arguments.LangVersion takes precedence over any langversion arg in arguments.AdditionalArguments
// This behavior should match that of CscCodeCompiler.CompileFile and CSharpLangVersionHelper.GetLangVersionArg
if (arguments.LangVersion != null) {
if (LanguageVersionFacts.TryParse(arguments.LangVersion, out var langVersion)) {
parseOptions = parseOptions.WithLanguageVersion (langVersion);
hasLangVersionArg = true;
} else {
throw new RoslynCodeCompilerException ($"Unknown value '{arguments.LangVersion}' for langversion");
}
} else {
// need to update this when updating referenced roslyn binaries
CSharpLangVersionHelper.GetBestSupportedLangVersion (runtime, CSharpLangVersion.v9_0);
}

if (!hasLangVersionArg) {
// Default to the highest language version supported by the runtime
// as we may be using a version of Roslyn where "latest" language
// features depend on new APIs that aren't available on the current runtime.
// If the runtime is an unknown version, its MaxSupportedLangVersion will default
// to "latest" so new runtime versions will work before we explicitly add support for them.
if (LanguageVersionFacts.TryParse (CSharpLangVersionHelper.ToString (runtime.MaxSupportedLangVersion), out var runtimeSupportedLangVersion)) {
parseOptions = parseOptions.WithLanguageVersion (runtimeSupportedLangVersion);
} else {
// if Roslyn did not recognize the runtime's default lang version, it's newer than
// this version of Roslyn supports, so default to the latest supported version
parseOptions = parseOptions.WithLanguageVersion (LanguageVersion.Latest);
}

}

var syntaxTrees = new List<SyntaxTree> ();
Expand Down
21 changes: 20 additions & 1 deletion Mono.TextTemplating.Tests/ProcessingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,26 @@ public async Task CSharp9Records ()
#endif
}

#if !NET472
[Fact]
public async Task CSharp11StructRecords ()
{
string template = "<#+ public record struct Foo(string bar); #>";
var gen = new TemplateGenerator ();
string outputName = null;
await gen.ProcessTemplateAsync (null, template, outputName);

CompilerError firstError = gen.Errors.OfType<CompilerError> ().FirstOrDefault ();

// note: when running on netsdk we use the highest available csc regardless of runtime version,
// so struct records will always be available on our test environments
#if NETFRAMEWORK
Assert.NotNull (firstError);
#else
Assert.Null (firstError);
#endif
}

#if !NETFRAMEWORK
[Fact]
public async Task SetLangVersionViaAttribute ()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if NETFRAMEWORK
#nullable enable annotations
#else
#nullable enable
#endif

namespace Mono.TextTemplating.CodeCompilation;

enum CSharpLangVersion
{
v5_0,
v6_0,
v7_0,
v7_1,
v7_2,
v7_3,
v8_0,
v9_0,
v10_0,
v11_0,
v12_0,
Latest = 1024 // make sure value doesn't change as we add new C# versions
}
Original file line number Diff line number Diff line change
@@ -1,90 +1,79 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if NETFRAMEWORK
#nullable enable annotations
#else
#nullable enable
#endif

using System;
using System.Collections.Generic;
using System.Linq;

namespace Mono.TextTemplating.CodeCompilation
{
enum CSharpLangVersion
{
v5_0,
v6_0,
v7_0,
v7_1,
v7_2,
v7_3,
v8_0,
v9_0,
Latest
}

static class CSharpLangVersionHelper
{
public static CSharpLangVersion GetBestSupportedLangVersion (RuntimeInfo runtime, CSharpLangVersion? compilerLangVersion = null)
=> (CSharpLangVersion)Math.Min ((int)(compilerLangVersion ?? runtime.MaxSupportedLangVersion), (int) (runtime switch {
{ Kind: RuntimeKind.NetCore, Version.Major: > 5 } => CSharpLangVersion.Latest,
{ Kind: RuntimeKind.NetCore, Version.Major: 5 } => CSharpLangVersion.v9_0,
{ Kind: RuntimeKind.NetCore, Version.Major: 3 } => CSharpLangVersion.v8_0,
_ => CSharpLangVersion.v7_3,
}));

static bool HasLangVersionArg (string args) =>
!string.IsNullOrEmpty(args)
&& (args.IndexOf ("langversion", StringComparison.OrdinalIgnoreCase) > -1)
&& ProcessArgumentBuilder.TryParse (args, out var parsedArgs)
&& parsedArgs.Any (a => a.IndexOf ("langversion", StringComparison.OrdinalIgnoreCase) == 1);
namespace Mono.TextTemplating.CodeCompilation;

static string ToString (CSharpLangVersion version) => version switch {
CSharpLangVersion.v5_0 => "5",
CSharpLangVersion.v6_0 => "6",
CSharpLangVersion.v7_0 => "7",
CSharpLangVersion.v7_1 => "7.1",
CSharpLangVersion.v7_2 => "7.2",
CSharpLangVersion.v7_3 => "7.3",
CSharpLangVersion.v8_0 => "8.0",
CSharpLangVersion.v9_0 => "9.0",
CSharpLangVersion.Latest => "latest",
_ => throw new ArgumentException ($"Not a valid value: '{version}'", nameof (version))
};

public static string GetLangVersionArg (CodeCompilerArguments arguments, RuntimeInfo runtime)
{
if (!string.IsNullOrWhiteSpace (arguments.LangVersion)) {
return $"-langversion:{arguments.LangVersion}";
}
static class CSharpLangVersionHelper
{
public static bool HasLangVersionArg (string args) =>
!string.IsNullOrEmpty(args)
&& (args.IndexOf ("langversion", StringComparison.OrdinalIgnoreCase) > -1)
&& ProcessArgumentBuilder.TryParse (args, out var parsedArgs)
&& parsedArgs.Any (IsLangVersionArg);

if (HasLangVersionArg (arguments.AdditionalArguments)) {
return null;
}
public static bool IsLangVersionArg (string arg) =>
(arg[0] == '-' || arg[0] == '/')
&& arg.IndexOf ("langversion", StringComparison.OrdinalIgnoreCase) == 1;

return $"-langversion:{ToString(GetBestSupportedLangVersion(runtime))}";
public static string? GetLangVersionArg (CodeCompilerArguments arguments, RuntimeInfo runtime)
{
// Arguments.LangVersion takes precedence over -langversion in arguments.AdditionalArguments.
// This behavior should match that of CscCodeCompiler.CompileFile and RoslynCodeCompiler.CompileFileInternal
if (!string.IsNullOrWhiteSpace (arguments.LangVersion)) {
return $"-langversion:{arguments.LangVersion}";
}

public static CSharpLangVersion? FromRoslynPackageVersion (string roslynPackageVersion)
=> SemVersion.TryParse (roslynPackageVersion, out var version)
? version switch {
{ Major: > 3 } => CSharpLangVersion.v9_0,
{ Major: 3, Minor: >= 8 } => CSharpLangVersion.v9_0,
{ Major: 3, Minor: >= 3 } => CSharpLangVersion.v8_0,
// ignore 8.0 preview support in 3.0-3.2
{ Major: 2, Minor: >= 8 } => CSharpLangVersion.v7_3,
{ Major: 2, Minor: >= 6 } => CSharpLangVersion.v7_2,
{ Major: 2, Minor: >= 3 } => CSharpLangVersion.v7_1,
{ Major: 2 } => CSharpLangVersion.v7_0,
_ => CSharpLangVersion.v6_0
}
: null;
if (HasLangVersionArg (arguments.AdditionalArguments)) {
return null;
}

//https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history
public static CSharpLangVersion FromNetCoreSdkVersion (SemVersion sdkVersion)
=> sdkVersion switch {
{ Major: >= 5 } => CSharpLangVersion.v9_0,
{ Major: 3 } => CSharpLangVersion.v8_0,
{ Major: 2, Minor: >= 1 } => CSharpLangVersion.v7_3,
{ Major: 2, Minor: >= 0 } => CSharpLangVersion.v7_1,
_ => CSharpLangVersion.v7_0
};
// Default to the highest language version supported by the runtime
// as we may be using a csc from a newer runtime where "latest" language
// features depend on new APIs that aren't available on the current runtime.
// If we were unable to determine the supported language version for the runtime,
// its MaxSupportedLangVersion will default to "Latest" so its language features
// are available before we add a language version mapping for that runtime version.
return $"-langversion:{ToString (runtime.MaxSupportedLangVersion)}";
}

//https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history
public static CSharpLangVersion FromNetCoreSdkVersion (SemVersion sdkVersion)
=> sdkVersion switch {
{ Major: 8 } => CSharpLangVersion.v12_0,
{ Major: 7 } => CSharpLangVersion.v11_0,
{ Major: 6 } => CSharpLangVersion.v10_0,
{ Major: 5 } => CSharpLangVersion.v9_0,
{ Major: 3 } => CSharpLangVersion.v8_0,
{ Major: 2, Minor: >= 1 } => CSharpLangVersion.v7_3,
{ Major: 2, Minor: >= 0 } => CSharpLangVersion.v7_1,
{ Major: 1 } => CSharpLangVersion.v7_1,
// for unknown versions, always fall through to "Latest" so we don't break the
// ability to use new C# versions as they are released
_ => CSharpLangVersion.Latest
};

public static string ToString (CSharpLangVersion version) => version switch {
CSharpLangVersion.v5_0 => "5",
CSharpLangVersion.v6_0 => "6",
CSharpLangVersion.v7_0 => "7",
CSharpLangVersion.v7_1 => "7.1",
CSharpLangVersion.v7_2 => "7.2",
CSharpLangVersion.v7_3 => "7.3",
CSharpLangVersion.v8_0 => "8.0",
CSharpLangVersion.v9_0 => "9.0",
CSharpLangVersion.v10_0 => "10.0",
CSharpLangVersion.v11_0 => "11.0",
CSharpLangVersion.v12_0 => "12.0",
CSharpLangVersion.Latest => "latest",
_ => throw new ArgumentException ($"Not a valid value: '{version}'", nameof (version))
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

#if NETFRAMEWORK
#nullable enable annotations
#else
#nullable enable
#endif

using System;
using System.Collections.Generic;
Expand All @@ -45,21 +50,19 @@ public CscCodeCompiler (RuntimeInfo runtime)

static StreamWriter CreateTempTextFile (string extension, out string path)
{
path = null;
Exception ex = null;
try {
var tempDir = Path.GetTempPath ();
Directory.CreateDirectory (tempDir);

//this is how msbuild does it...
path = Path.Combine (tempDir, $"tmp{Guid.NewGuid ():N}{extension}");
path = Path.Combine (tempDir, $"tmp{Guid.NewGuid ():N}{extension}")!;
if (!File.Exists (path)) {
return File.CreateText (path);
}
} catch (Exception e) {
ex = e;
} catch (Exception ex) {
throw new TemplatingEngineException ("Failed to create temp file", ex);
}
throw new TemplatingEngineException ("Failed to create temp file", ex);
throw new TemplatingEngineException ("Failed to create temp file");
}

/// <summary>
Expand All @@ -70,7 +73,7 @@ static StreamWriter CreateTempTextFile (string extension, out string path)
/// <param name="token">Token.</param>
public override async Task<CodeCompilerResult> CompileFile (CodeCompilerArguments arguments, TextWriter log, CancellationToken token)
{
string rspPath;
string? rspPath;
StreamWriter rsp;
if (arguments.TempDirectory != null) {
rspPath = Path.Combine (arguments.TempDirectory, "response.rsp");
Expand All @@ -86,11 +89,6 @@ public override async Task<CodeCompilerResult> CompileFile (CodeCompilerArgument
rsp.WriteLine ("-debug");
}

var langVersionArg = CSharpLangVersionHelper.GetLangVersionArg (arguments, runtime);
if (langVersionArg != null) {
rsp.WriteLine (langVersionArg);
}

foreach (var reference in AssemblyResolver.GetResolvedReferences (runtime, arguments.AssemblyReferences)) {
rsp.Write ("-r:");
rsp.Write ("\"");
Expand All @@ -107,6 +105,15 @@ public override async Task<CodeCompilerResult> CompileFile (CodeCompilerArgument
rsp.WriteLine (arguments.AdditionalArguments);
}

// This comes after AdditionalArguments so arguments.LangVersion will take precedence
// over any langversion arg in AdditionalArguments.
// This behavior should match that of CSharpLangVersionHelper.GetLangVersionArg and
// RoslynCodeCompiler.CompileFileInternal
var langVersionArg = CSharpLangVersionHelper.GetLangVersionArg (arguments, runtime);
if (langVersionArg != null) {
rsp.WriteLine (langVersionArg);
}

//in older versions of csc, these must come last
foreach (var file in arguments.SourceFiles) {
rsp.Write ("\"");
Expand Down Expand Up @@ -151,8 +158,7 @@ public override async Task<CodeCompilerResult> CompileFile (CodeCompilerArgument
void ConsumeOutput (string s)
{
using var sw = new StringReader (s);
string line;
while ((line = sw.ReadLine ()) != null) {
while (sw.ReadLine () is string line) {
outputList.Add (line);
var err = MSBuildErrorParser.TryParseLine (line);
if (err != null) {
Expand Down Expand Up @@ -197,7 +203,7 @@ public override void WriteLine ()
b.WriteLine ();
}

public override void Write (string value)
public override void Write (string? value)
{
a.Write (value);
b.Write (value);
Expand Down

0 comments on commit 51a5db5

Please sign in to comment.