From 871b9c01c808915a0825226987bb40bfa2d7ed8e Mon Sep 17 00:00:00 2001 From: Vlada Shubina Date: Wed, 14 Jul 2021 17:12:52 +0300 Subject: [PATCH] added conditional processing for C# language features based on languageVersion / framework --- .../.template.config/template.json | 30 ++ .../content/ClassLibrary-CSharp/Class1.cs | 7 +- .../Company.ClassLibrary1.csproj | 3 +- .../.template.config/template.json | 39 ++ .../Company.ConsoleApplication1.csproj | 4 +- .../ConsoleApplication-CSharp/Program.cs | 21 +- .../CommonTemplatesTests.cs | 353 ++++++++++++++++-- 7 files changed, 429 insertions(+), 28 deletions(-) diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/.template.config/template.json b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/.template.config/template.json index 144d4d9de37..39c4cd89897 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/.template.config/template.json +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/.template.config/template.json @@ -84,6 +84,36 @@ "description": "If specified, skips the automatic restore of the project on create.", "defaultValue": "false", "displayName": "Skip restore" + }, + "csharp10orLater": { + "type": "generated", + "generator": "regexMatch", + "datatype": "bool", + "parameters": { + "pattern": "^(|10\\.0|10|preview|latest|default|latestMajor)$", + "source": "langVersion" + } + }, + "csharp8orLater": { + "type": "generated", + "generator": "regexMatch", + "datatype": "bool", + "parameters": { + "pattern": "^(|8|8\\.0|9|9\\.0|10\\.0|10|preview|latest|default|latestMajor)$", + "source": "langVersion" + } + }, + "csharpFeature_ImplicitUsings": { + "type": "computed", + "value": "Framework == \"net6.0\" && csharp10orLater == \"true\"" + }, + "csharpFeature_FileScopedNamespaces": { + "type": "computed", + "value": "(Framework == \"net6.0\" || langVersion != \"\") && csharp10orLater == \"true\"" + }, + "csharpFeature_Nullable": { + "type": "computed", + "value": "(Framework != \"netstandard2.0\" || langVersion != \"\") && csharp8orLater == \"true\"" } }, "primaryOutputs": [ diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/Class1.cs b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/Class1.cs index a6325f3fb1b..2dcafee5a35 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/Class1.cs +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/Class1.cs @@ -1,6 +1,11 @@ -namespace Company.ClassLibrary1 +#if (!csharpFeature_ImplicitUsings) +using System; + +#endif +namespace Company.ClassLibrary1 { public class Class1 { + } } diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/Company.ClassLibrary1.csproj b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/Company.ClassLibrary1.csproj index 892876e76de..46784087ed7 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/Company.ClassLibrary1.csproj +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ClassLibrary-CSharp/Company.ClassLibrary1.csproj @@ -5,7 +5,8 @@ TargetFrameworkOverride Company.ClassLibrary1 $(ProjectLanguageVersion) - enable + true + enable diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/.template.config/template.json b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/.template.config/template.json index 0d33dafa0e6..1a08e0a395f 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/.template.config/template.json +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/.template.config/template.json @@ -60,6 +60,45 @@ "description": "If specified, skips the automatic restore of the project on create.", "defaultValue": "false", "displayName": "Skip restore" + }, + "csharp10orLater": { + "type": "generated", + "generator": "regexMatch", + "datatype": "bool", + "parameters": { + "pattern": "^(|10\\.0|10|preview|latest|default|latestMajor)$", + "source": "langVersion" + } + }, + "csharp9orLater": { + "type": "generated", + "generator": "regexMatch", + "datatype": "bool", + "parameters": { + "pattern": "^(|9|9\\.0|10\\.0|10|preview|latest|default|latestMajor)$", + "source": "langVersion" + } + }, + "csharp8orLater": { + "type": "generated", + "generator": "regexMatch", + "datatype": "bool", + "parameters": { + "pattern": "^(|8|8\\.0|9|9\\.0|10\\.0|10|preview|latest|default|latestMajor)$", + "source": "langVersion" + } + }, + "csharpFeature_ImplicitUsings": { + "type": "computed", + "value": "csharp10orLater == \"true\"" + }, + "csharpFeature_Nullable": { + "type": "computed", + "value": "csharp8orLater == \"true\"" + }, + "csharpFeature_TopLevelProgram": { + "type": "computed", + "value": "csharp9orLater == \"true\"" } }, "primaryOutputs": [ diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/Company.ConsoleApplication1.csproj b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/Company.ConsoleApplication1.csproj index 66e0f74129c..ced2071aa9f 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/Company.ConsoleApplication1.csproj +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/Company.ConsoleApplication1.csproj @@ -4,8 +4,10 @@ Exe net6.0 TargetFrameworkOverride + Company.ConsoleApplication1 $(ProjectLanguageVersion) - enable + true + enable diff --git a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/Program.cs b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/Program.cs index 68c6516d215..bdeb5843bdb 100644 --- a/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/Program.cs +++ b/template_feed/Microsoft.DotNet.Common.ProjectTemplates.6.0/content/ConsoleApplication-CSharp/Program.cs @@ -1,2 +1,21 @@ -Console.WriteLine("Hello, World!"); +#if (csharpFeature_TopLevelProgram) // See https://aka.ms/new-console-template for more information +#endif +#if (!csharpFeature_ImplicitUsings) +using System; + +#endif +#if (csharpFeature_TopLevelProgram) +Console.WriteLine("Hello, World!"); +#else +namespace Company.ConsoleApplication1 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello, World!"); + } + } +} +#endif diff --git a/test/dotnet-new3.UnitTests/CommonTemplatesTests.cs b/test/dotnet-new3.UnitTests/CommonTemplatesTests.cs index 88132affd06..ccc22fb5218 100644 --- a/test/dotnet-new3.UnitTests/CommonTemplatesTests.cs +++ b/test/dotnet-new3.UnitTests/CommonTemplatesTests.cs @@ -66,21 +66,6 @@ public CommonTemplatesTests(SharedHomeDirectory fixture, ITestOutputHelper log) [InlineData("Class Library", "classlib", "VB", "netstandard2.0")] [InlineData("Class Library", "classlib", "F#", "netstandard2.0")] - [InlineData("Class Library", "classlib", "C#", "net6.0", "10.0")] - [InlineData("Class Library", "classlib", "C#", "net6.0", "9.0", Skip = "https://github.com/dotnet/templating/issues/3449")] - [InlineData("Class Library", "classlib", "C#", "net6.0", "8.0", Skip = "https://github.com/dotnet/templating/issues/3449")] - [InlineData("Class Library", "classlib", "C#", "net6.0", "7.3", Skip = "https://github.com/dotnet/templating/issues/3449")] - [InlineData("Class Library", "classlib", "C#", "net6.0", "7.2", Skip = "https://github.com/dotnet/templating/issues/3449")] - [InlineData("Class Library", "classlib", "C#", "net6.0", "7.1", Skip = "https://github.com/dotnet/templating/issues/3449")] - [InlineData("Class Library", "classlib", "C#", "net6.0", "7", Skip = "https://github.com/dotnet/templating/issues/3449")] - - [InlineData("Console Application", "console", "C#", "net6.0", "10.0")] - [InlineData("Console Application", "console", "C#", "net6.0", "9.0", Skip = "https://github.com/dotnet/templating/issues/3449")] - [InlineData("Console Application", "console", "C#", "net6.0", "8.0", Skip = "https://github.com/dotnet/templating/issues/3449")] - [InlineData("Console Application", "console", "C#", "net6.0", "7.3", Skip = "https://github.com/dotnet/templating/issues/3449")] - [InlineData("Console Application", "console", "C#", "net6.0", "7.2", Skip = "https://github.com/dotnet/templating/issues/3449")] - [InlineData("Console Application", "console", "C#", "net6.0", "7.1", Skip = "https://github.com/dotnet/templating/issues/3449")] - [InlineData("Console Application", "console", "C#", "net6.0", "7", Skip = "https://github.com/dotnet/templating/issues/3449")] public void AllCommonProjectsCreateRestoreAndBuild(string expectedTemplateName, string templateShortName, string? language = null, string? framework = null, string? langVersion = null) { string workingDir = TestUtils.CreateTemporaryFolder(); @@ -243,6 +228,334 @@ public void AllCommonItemsCreate(string expectedTemplateName, string templateSho Directory.Delete(workingDir, true); } + #region Project templates language features tests + + /// + /// Creates all possible combinations for supported templates, language versions and frameworks. + /// + public static IEnumerable TopLevelProgramSupport_Data() + { + var templatesToTest = new[] + { + new { Name = "console", Frameworks = new[] { null, "net6.0" } } + }; + + string[] unsupportedLanguageVersions = { "1", "ISO-1" }; + string?[] supportedLanguageVersions = { null, "ISO-2", "2", "3", "4", "5", "6", "7", "7.1", "7.2", "7.3", "8.0", "9.0", "10.0", "latest", "latestMajor", "default", "preview" }; + + string?[] topLevelStatementSupport = { null, "9.0", "10.0", "latest", "latestMajor", "default", "preview" }; + + foreach (var template in templatesToTest) + { + foreach (var langVersion in unsupportedLanguageVersions) + { + foreach (var framework in template.Frameworks) + { + yield return new object?[] + { + template.Name, + false, //dotnet build should fail + framework, + langVersion, + topLevelStatementSupport.Contains(langVersion) + }; + } + } + foreach (var langVersion in supportedLanguageVersions) + { + foreach (var framework in template.Frameworks) + { + yield return new object?[] + { + template.Name, + true, //dotnet build should pass + framework, + langVersion, + topLevelStatementSupport.Contains(langVersion) + }; + } + } + } + } + + [Theory] + //creates all possible combinations for supported templates, language versions and frameworks + [MemberData(nameof(TopLevelProgramSupport_Data))] + public void TopLevelProgramSupport(string name, bool buildPass, string? framework, string? langVersion, bool supportsFeature) + { + string workingDir = TestUtils.CreateTemporaryFolder(); + + List args = new List() { name, "-o", "MyProject" }; + if (!string.IsNullOrWhiteSpace(framework)) + { + args.Add("--framework"); + args.Add(framework); + } + if (!string.IsNullOrWhiteSpace(langVersion)) + { + args.Add("--langVersion"); + args.Add(langVersion); + } + + new DotnetNewCommand(_log, args.ToArray()) + .WithCustomHive(_fixture.HomeDirectory) + .WithWorkingDirectory(workingDir) + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr(); + + var buildResult = new DotnetCommand(_log, "build", "MyProject") + .WithWorkingDirectory(workingDir) + .Execute(); + + if (buildPass) + { + buildResult.Should().ExitWith(0).And.NotHaveStdErr(); + } + else + { + buildResult.Should().Fail(); + return; + } + + string programFileContent = File.ReadAllText(Path.Combine(workingDir, "MyProject", "Program.cs")); + string unexpectedTopLevelContent = +@"namespace MyProject +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine(""Hello, World!""); + } + } +} +"; + if (supportsFeature) + { + Assert.Contains("Console.WriteLine(\"Hello, World!\")", programFileContent); + Assert.Contains("// See https://aka.ms/new-console-template for more information", programFileContent); + Assert.DoesNotContain(unexpectedTopLevelContent, programFileContent); + } + else + { + Assert.DoesNotContain("// See https://aka.ms/new-console-template for more information", programFileContent); + Assert.Contains(unexpectedTopLevelContent, programFileContent); + } + } + + /// + /// Creates all possible combinations for supported templates, language versions and frameworks. + /// + public static IEnumerable NullableSupport_Data() + { + var templatesToTest = new[] + { + new { Template = "console", Frameworks = new[] { null, "net6.0" } }, + new { Template = "classlib", Frameworks = new[] { null, "net6.0", "netstandard2.0", "netstandard2.1" } } + }; + + string[] unsupportedLanguageVersions = { "1", "ISO-1" }; + string?[] supportedLanguageVersions = { null, "ISO-2", "2", "3", "4", "5", "6", "7", "7.1", "7.2", "7.3", "8.0", "9.0", "10.0", "latest", "latestMajor", "default", "preview" }; + + string?[] supportedInFrameworkByDefault = { null, "net6.0", "netstandard2.1" }; + string?[] supportedInLanguageVersion = { "8.0", "9.0", "10.0", "latest", "latestMajor", "default", "preview" }; + + foreach (var template in templatesToTest) + { + foreach (var langVersion in unsupportedLanguageVersions) + { + foreach (var framework in template.Frameworks) + { + yield return new object?[] + { + template.Template, + false, //dotnet build should fail + framework, + langVersion, + supportedInLanguageVersion.Contains(langVersion) + || langVersion == null && supportedInFrameworkByDefault.Contains(framework) + }; + } + } + foreach (var langVersion in supportedLanguageVersions) + { + foreach (var framework in template.Frameworks) + { + yield return new object?[] + { + template.Template, + true, //dotnet build should pass + framework, + langVersion, + supportedInLanguageVersion.Contains(langVersion) + || langVersion == null && supportedInFrameworkByDefault.Contains(framework) + }; + } + } + } + } + + [Theory] + //creates all possible combinations for supported templates, language versions and frameworks + [MemberData(nameof(NullableSupport_Data))] + public void NullableSupport(string name, bool buildPass, string? framework, string? langVersion, bool supportsFeature) + { + string workingDir = TestUtils.CreateTemporaryFolder(); + + List args = new List() { name, "-o", "MyProject" }; + if (!string.IsNullOrWhiteSpace(framework)) + { + args.Add("--framework"); + args.Add(framework); + } + if (!string.IsNullOrWhiteSpace(langVersion)) + { + args.Add("--langVersion"); + args.Add(langVersion); + } + + new DotnetNewCommand(_log, args.ToArray()) + .WithCustomHive(_fixture.HomeDirectory) + .WithWorkingDirectory(workingDir) + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr(); + + var buildResult = new DotnetCommand(_log, "build", "MyProject") + .WithWorkingDirectory(workingDir) + .Execute(); + + if (buildPass) + { + buildResult.Should().ExitWith(0).And.NotHaveStdErr(); + } + else + { + buildResult.Should().Fail(); + return; + } + + XDocument projectXml = XDocument.Load(Path.Combine(workingDir, "MyProject", "MyProject.csproj")); + XNamespace ns = projectXml.Root?.Name.Namespace ?? throw new Exception("Unexpected project file format"); + if (supportsFeature) + { + Assert.Equal("enable", projectXml.Root?.Element(ns + "PropertyGroup")?.Element(ns + "Nullable")?.Value); + } + else + { + Assert.Null(projectXml.Root?.Element(ns + "PropertyGroup")?.Element(ns + "Nullable")); + } + } + + /// + /// Creates all possible combinations for supported templates, language versions and frameworks. + /// + public static IEnumerable ImplicitUsingsSupport_Data() + { + var templatesToTest = new[] + { + new { Template = "console", Frameworks = new[] { null, "net6.0" } }, + new { Template = "classlib", Frameworks = new[] { null, "net6.0", "netstandard2.0", "netstandard2.1" } } + }; + string[] unsupportedLanguageVersions = { "1", "ISO-1" }; + string?[] supportedLanguageVersions = { null, "ISO-2", "2", "3", "4", "5", "6", "7", "7.1", "7.2", "7.3", "8.0", "9.0", "10.0", "latest", "latestMajor", "default", "preview" }; + + string?[] supportedInFramework = { null, "net6.0" }; + string?[] supportedInLangVersion = { null, "10.0", "latest", "latestMajor", "default", "preview" }; + + foreach (var template in templatesToTest) + { + foreach (var langVersion in unsupportedLanguageVersions) + { + foreach (var framework in template.Frameworks) + { + yield return new object?[] + { + template.Template, + false, //dotnet build should fail + framework, + langVersion, + supportedInLangVersion.Contains(langVersion) && supportedInFramework.Contains(framework) + }; + } + } + foreach (var langVersion in supportedLanguageVersions) + { + foreach (var framework in template.Frameworks) + { + yield return new object?[] + { + template.Template, + true, //dotnet build should pass + framework, + langVersion, + supportedInLangVersion.Contains(langVersion) && supportedInFramework.Contains(framework) + }; + } + } + } + } + + [Theory] + //creates all possible combinations for supported templates, language versions and frameworks + [MemberData(nameof(ImplicitUsingsSupport_Data))] + public void ImplicitUsingsSupport(string name, bool buildPass, string? framework, string? langVersion, bool supportsFeature) + { + string workingDir = TestUtils.CreateTemporaryFolder(); + + List args = new List() { name, "-o", "MyProject" }; + if (!string.IsNullOrWhiteSpace(framework)) + { + args.Add("--framework"); + args.Add(framework); + } + if (!string.IsNullOrWhiteSpace(langVersion)) + { + args.Add("--langVersion"); + args.Add(langVersion); + } + + new DotnetNewCommand(_log, args.ToArray()) + .WithCustomHive(_fixture.HomeDirectory) + .WithWorkingDirectory(workingDir) + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr(); + + var buildResult = new DotnetCommand(_log, "build", "MyProject") + .WithWorkingDirectory(workingDir) + .Execute(); + + if (buildPass) + { + buildResult.Should().ExitWith(0).And.NotHaveStdErr(); + } + else + { + buildResult.Should().Fail(); + return; + } + string codeFileName = name == "console" ? "Program.cs" : "Class1.cs"; + string programFileContent = File.ReadAllText(Path.Combine(workingDir, "MyProject", codeFileName)); + XDocument projectXml = XDocument.Load(Path.Combine(workingDir, "MyProject", "MyProject.csproj")); + XNamespace ns = projectXml.Root?.Name.Namespace ?? throw new Exception("Unexpected project file format"); + if (supportsFeature) + { + Assert.DoesNotContain("using System;", programFileContent); + Assert.Null(projectXml.Root?.Element(ns + "PropertyGroup")?.Element(ns + "DisableImplicitNamespaceImports")); + } + else + { + Assert.Contains("using System;", programFileContent); + Assert.Equal("true", projectXml.Root?.Element(ns + "PropertyGroup")?.Element(ns + "DisableImplicitNamespaceImports")?.Value); + } + } + #endregion + [Theory] [InlineData("Nullable", "enable", "Console Application", "console", null, null)] [InlineData("CheckForOverflowUnderflow", null, "Console Application", "console", null, null)] @@ -251,9 +564,6 @@ public void AllCommonItemsCreate(string expectedTemplateName, string templateSho [InlineData("Nullable", null, "Console Application", "console", null, "net5.0")] [InlineData("Nullable", null, "Console Application", "console", null, "netcoreapp3.1")] [InlineData("Nullable", null, "Console Application", "console", null, "netcoreapp2.1")] - [InlineData("Nullable", "enable", "Console Application", "console", null, null, "9.0")] - [InlineData("Nullable", "enable", "Console Application", "console", null, null, "8.0")] - [InlineData("Nullable", "enable", "Console Application", "console", null, null, "7.3")] [InlineData("Nullable", null, "Console Application", "console", "F#", null)] [InlineData("CheckForOverflowUnderflow", null, "Console Application", "console", "F#", null)] @@ -286,7 +596,7 @@ public void AllCommonItemsCreate(string expectedTemplateName, string templateSho [InlineData("TargetFramework", "net6.0", "Class Library", "classlib", "VB", null)] [InlineData("Nullable", null, "Class Library", "classlib", "VB", "netstandard2.0")] - public void SetPropertiesByDefault(string propertyName, string? propertyValue, string expectedTemplateName, string templateShortName, string? language, string? framework, string? langVersion = null) + public void SetPropertiesByDefault(string propertyName, string? propertyValue, string expectedTemplateName, string templateShortName, string? language, string? framework) { string workingDir = TestUtils.CreateTemporaryFolder(); List args = new List() { templateShortName, "--no-restore" }; @@ -300,11 +610,6 @@ public void SetPropertiesByDefault(string propertyName, string? propertyValue, s args.Add("--framework"); args.Add(framework); } - if (!string.IsNullOrWhiteSpace(langVersion)) - { - args.Add("--langVersion"); - args.Add(langVersion); - } new DotnetNewCommand(_log, args.ToArray()) .WithCustomHive(_fixture.HomeDirectory)