diff --git a/.vscode/settings.json b/.vscode/settings.json index fd80bb6e..804aa441 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,9 @@ } ], "cSpell.words": [ + "langversion", "Preprocess", - "reimplementation" + "reimplementation", + "Xunit" ] } diff --git a/Directory.Build.props b/Directory.Build.props index 7162a681..33b96ee4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,25 +17,13 @@ + - - - true - true - embedded - - - true - - - - - true $(MSBuildThisFileDirectory)TextTemplating.snk diff --git a/Mono.TextTemplating.Roslyn/RoslynCodeCompiler.cs b/Mono.TextTemplating.Roslyn/RoslynCodeCompiler.cs index 6e875b71..b3323d55 100644 --- a/Mono.TextTemplating.Roslyn/RoslynCodeCompiler.cs +++ b/Mono.TextTemplating.Roslyn/RoslynCodeCompiler.cs @@ -1,6 +1,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -37,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 (); @@ -51,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 (); @@ -102,7 +121,7 @@ CodeCompilerResult CompileFileInternal ( var startLinePosition = location.StartLinePosition; var endLinePosition = location.EndLinePosition; return new CodeCompilerError { - Message = x.GetMessage (), + Message = x.GetMessage (CultureInfo.CurrentCulture), Column = startLinePosition.Character, Line = startLinePosition.Line, EndLine = endLinePosition.Line, diff --git a/Mono.TextTemplating.Tests/ProcessingTests.cs b/Mono.TextTemplating.Tests/ProcessingTests.cs index 58b5a4f1..2cb8b752 100644 --- a/Mono.TextTemplating.Tests/ProcessingTests.cs +++ b/Mono.TextTemplating.Tests/ProcessingTests.cs @@ -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 ().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 () { diff --git a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersion.cs b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersion.cs new file mode 100644 index 00000000..36b3c00d --- /dev/null +++ b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersion.cs @@ -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 +} diff --git a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersionHelper.cs b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersionHelper.cs index 20582dad..03395756 100644 --- a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersionHelper.cs +++ b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CSharpLangVersionHelper.cs @@ -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)) + }; } diff --git a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CscCodeCompiler.cs b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CscCodeCompiler.cs index 1abecd17..00858b2a 100644 --- a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CscCodeCompiler.cs +++ b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CscCodeCompiler.cs @@ -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; @@ -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"); } /// @@ -70,7 +73,7 @@ static StreamWriter CreateTempTextFile (string extension, out string path) /// Token. public override async Task CompileFile (CodeCompilerArguments arguments, TextWriter log, CancellationToken token) { - string rspPath; + string? rspPath; StreamWriter rsp; if (arguments.TempDirectory != null) { rspPath = Path.Combine (arguments.TempDirectory, "response.rsp"); @@ -86,11 +89,6 @@ public override async Task 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 ("\""); @@ -107,6 +105,15 @@ public override async Task 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 ("\""); @@ -151,8 +158,7 @@ public override async Task 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) { @@ -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); diff --git a/Mono.TextTemplating/Mono.TextTemplating.csproj b/Mono.TextTemplating/Mono.TextTemplating.csproj index a47b47a0..25aab184 100644 --- a/Mono.TextTemplating/Mono.TextTemplating.csproj +++ b/Mono.TextTemplating/Mono.TextTemplating.csproj @@ -16,6 +16,8 @@ 2.2.1 readme.md en-US + + $(NoWarn);NU5129 @@ -23,9 +25,9 @@ - + - +