From 381de2d2a5370776f1414322f3c7fb0573ea4b3f Mon Sep 17 00:00:00 2001 From: T-Ernst <119951944+T-Ernst@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:06:41 +0100 Subject: [PATCH] fix(T4.BuildTools): evaluate T4Transform/T4Preprocess Metadata --- .../MSBuildExecutionTests.cs | 192 +++++++++++++++--- .../Nested/Template/Folder/foo.tt | 2 + .../PreprocessTemplateFromRelativePath.csproj | 14 ++ .../OutputDirectory.tt | 2 + .../OutputDirectoryAndOutputFileName.tt | 2 + .../OutputFileName.tt | 3 + .../OutputFilePath.tt | 2 + .../PreprocessTemplateMetadata.csproj | 26 +++ .../PreprocessTemplateWithExtension.csproj | 14 ++ .../PreprocessTemplateWithExtension/foo.tt | 3 + .../Nested/Template/Folder/foo.tt | 4 + .../TransformTemplateFromRelativePath.csproj | 15 ++ .../OutputDirectory.tt | 4 + .../OutputDirectoryAndOutputFileName.tt | 4 + .../OutputFileName.tt | 5 + .../OutputFilePath.tt | 4 + .../TransformTemplateMetadata.csproj | 28 +++ .../TransformTemplateWithExtension.csproj | 15 ++ .../TransformTemplateWithExtension/foo.tt | 5 + .../T4.BuildTools.targets | 2 + .../T4.BuildTools.targets.buildschema.json | 20 +- .../TemplateBuildState.cs | 17 +- Mono.TextTemplating.Build/TextTransform.cs | 75 ++++++- .../TextTransformProcessor.cs | 41 ++-- Mono.TextTemplating.Build/readme.md | 2 +- Mono.TextTemplating.Tests/TestDataPath.cs | 15 ++ 26 files changed, 461 insertions(+), 55 deletions(-) create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/Nested/Template/Folder/foo.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/PreprocessTemplateFromRelativePath.csproj create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectory.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectoryAndOutputFileName.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFileName.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFilePath.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/PreprocessTemplateMetadata.csproj create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/PreprocessTemplateWithExtension.csproj create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/foo.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateFromRelativePath/Nested/Template/Folder/foo.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateFromRelativePath/TransformTemplateFromRelativePath.csproj create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputDirectory.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputDirectoryAndOutputFileName.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputFileName.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputFilePath.tt create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/TransformTemplateMetadata.csproj create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateWithExtension/TransformTemplateWithExtension.csproj create mode 100644 Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateWithExtension/foo.tt diff --git a/Mono.TextTemplating.Build.Tests/MSBuildExecutionTests.cs b/Mono.TextTemplating.Build.Tests/MSBuildExecutionTests.cs index b3740a8e..b846c8d6 100644 --- a/Mono.TextTemplating.Build.Tests/MSBuildExecutionTests.cs +++ b/Mono.TextTemplating.Build.Tests/MSBuildExecutionTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.Linq; using Xunit; @@ -10,94 +11,185 @@ namespace Mono.TextTemplating.Tests [CollectionDefinition (nameof (MSBuildExecutionTests), DisableParallelization = true)] public class MSBuildExecutionTests : IClassFixture { - [Fact] - public void TransformExplicitWithArguments () + [Theory] + [InlineData ("TransformTemplates", "foo.txt", "Hello 2019!")] + [InlineData ("TransformTemplateFromRelativePath", "Nested/Template/Folder/foo.txt", "Hello 2024!")] + [InlineData ("TransformTemplateWithExtension", "foo.html", "

Hello 2024!

")] + public void TransformExplicitWithArguments (string projectName, string expectedFilePath, string expectedText) { using var ctx = new MSBuildTestContext (); - var project = ctx.LoadTestProject ("TransformTemplates"); + var project = ctx.LoadTestProject (projectName); var instance = project.Build ("TransformTemplates"); - var generated = project.DirectoryPath["foo.txt"].AssertTextStartsWith ("Hello 2019!"); + var generated = project.DirectoryPath[expectedFilePath].AssertTextStartsWith (expectedText); instance.AssertSingleItem ("GeneratedTemplates", withFullPath: generated); instance.AssertNoItems ("PreprocessedTemplates"); } - [Fact] - public void TransformOnBuild () + [Theory] + [InlineData ("TransformTemplates", "foo.txt", "Hello 2019!")] + [InlineData ("TransformTemplateFromRelativePath", "Nested/Template/Folder/foo.txt", "Hello 2024!")] + [InlineData ("TransformTemplateWithExtension", "foo.html", "

Hello 2024!

")] + public void TransformOnBuild (string projectName, string expectedFilePath, string expectedText) { using var ctx = new MSBuildTestContext (); - var project = ctx.LoadTestProject ("TransformTemplates") + var project = ctx.LoadTestProject (projectName) .WithProperty ("TransformOnBuild", "true"); project.Restore (); var instance = project.Build ("Build"); - var generatedFilePath = project.DirectoryPath["foo.txt"].AssertTextStartsWith ("Hello 2019!"); + var generatedFilePath = project.DirectoryPath[expectedFilePath].AssertTextStartsWith (expectedText); instance.AssertSingleItem ("GeneratedTemplates", withFullPath: generatedFilePath); instance.AssertNoItems ("PreprocessedTemplates"); } - [Fact] - public void TransformOnBuildDisabled () + [Theory] + [InlineData ("TransformTemplates", "foo.txt")] + [InlineData ("TransformTemplateFromRelativePath", "Nested/Template/Folder/foo.txt")] + [InlineData ("TransformTemplateWithExtension", "foo.html")] + public void TransformOnBuildDisabled (string projectName, string expectedFilePath) { using var ctx = new MSBuildTestContext (); - var project = ctx.LoadTestProject ("TransformTemplates"); + var project = ctx.LoadTestProject (projectName); project.Restore (); var instance = project.Build ("Build"); - project.DirectoryPath["foo.txt"].AssertFileExists (false); + project.DirectoryPath[expectedFilePath].AssertFileExists (false); instance.AssertNoItems ("GeneratedTemplates", "PreprocessedTemplates"); } [Fact] - public void PreprocessLegacy () + public void TransformMetadata () + { + // Arrange + using var ctx = new MSBuildTestContext (); + var project = ctx.LoadTestProject ("TransformTemplateMetadata"); + + var outputDirectory = project.DirectoryPath["Demo/Output/OutputDirectory.txt"]; + var outputFilePath = project.DirectoryPath["Demo/LegacyOutput/OutputFilePath.txt"]; + var outputFileName = project.DirectoryPath["Demo/OutputFileNameTest"]; + var outputDirectoryAndOutputFileName = project.DirectoryPath["Demo/Output/OutputDirectoryAndFileNameTest.log"]; + + // Act + var instance = project.Build ("TransformTemplates"); + + // Assert + Assert.Multiple (() => { + outputDirectory.AssertTextStartsWith ("Hello Metadata OutputDirectory 2024!"); + outputFilePath.AssertTextStartsWith ("Hello Metadata OutputFilePath 2024!"); + outputFileName.AssertTextStartsWith ("Hello Metadata OutputFileName 2024!"); + outputDirectoryAndOutputFileName.AssertTextStartsWith ("Hello Metadata OutputDirectory and OutputFileName 2024!"); + }); + + instance.AssertNoItems ("PreprocessedTemplates"); + } + + [Theory] + [InlineData ( + "PreprocessTemplate", + "foo.cs", + new string[] { + "namespace PreprocessTemplate {", + "public partial class foo : fooBase {" + } + )] + [InlineData ( + "PreprocessTemplateFromRelativePath", + "Nested/Template/Folder/foo.cs", + new string[] { + "namespace PreprocessTemplateFromRelativePath.Nested.Template.Folder {", + "public partial class foo : fooBase {" + } + )] + [InlineData ( + "PreprocessTemplateWithExtension", + "foo.g.cs", + new string[] { + "namespace PreprocessTemplateWithExtension {", + "public partial class foo : fooBase {" + } + )] + public void PreprocessLegacy (string projectName, string expectedFilePath, string[] expectedContents) { using var ctx = new MSBuildTestContext (); - var project = ctx.LoadTestProject ("PreprocessTemplate") + var project = ctx.LoadTestProject (projectName) .WithProperty ("UseLegacyT4Preprocessing", "true"); var instance = project.Build ("TransformTemplates"); - var generatedFilePath = project.DirectoryPath["foo.cs"].AssertTextStartsWith ("//--------"); + var generatedFilePath = project.DirectoryPath[expectedFilePath] + .AssertContainsText + ( + StringComparison.Ordinal, + expectedContents + ); instance.AssertSingleItem ("PreprocessedTemplates", generatedFilePath); instance.AssertNoItems ("GeneratedTemplates"); } - [Fact] - public void PreprocessOnBuild () + [Theory] + [InlineData ( + "PreprocessTemplate", + "TextTransform/foo.cs", + "PreprocessTemplate.foo" + )] + [InlineData ( + "PreprocessTemplateFromRelativePath", + "TextTransform/Nested/Template/Folder/foo.cs", + "PreprocessTemplateFromRelativePath.Nested.Template.Folder.foo" + )] + [InlineData ( + "PreprocessTemplateWithExtension", + "TextTransform/foo.g.cs", + "PreprocessTemplateWithExtension.foo" + )] + public void PreprocessOnBuild (string projectName, string expectedFilePath, string expectedType) { using var ctx = new MSBuildTestContext (); - var project = ctx.LoadTestProject ("PreprocessTemplate"); + var project = ctx.LoadTestProject (projectName); project.Restore (); var instance = project.Build ("Build"); var objDir = project.DirectoryPath["obj", "Debug", "netstandard2.0"]; - var generatedFilePath = instance.GetIntermediateDirFile ("TextTransform", "foo.cs") + var generatedFilePath = instance.GetIntermediateDirFile (expectedFilePath) .AssertTextStartsWith ("//--------"); instance.AssertSingleItem ("PreprocessedTemplates", generatedFilePath); instance.AssertNoItems ("GeneratedTemplates"); instance.GetTargetPath () - .AssertFileName ("PreprocessTemplate.dll") - .AssertAssemblyContainsType ("PreprocessTemplate.foo"); + .AssertFileName ($"{projectName}.dll") + .AssertAssemblyContainsType (expectedType); } - [Fact] - public void PreprocessOnDesignTimeBuild () + [Theory] + [InlineData ( + "PreprocessTemplate", + "TextTransform/foo.cs" + )] + [InlineData ( + "PreprocessTemplateFromRelativePath", + "TextTransform/Nested/Template/Folder/foo.cs" + )] + [InlineData ( + "PreprocessTemplateWithExtension", + "TextTransform/foo.g.cs" + )] + public void PreprocessOnDesignTimeBuild (string projectName, string expectedFilePath) { using var ctx = new MSBuildTestContext (); - var project = ctx.LoadTestProject ("PreprocessTemplate") + var project = ctx.LoadTestProject (projectName) .WithProperty ("DesignTimeBuild", "true") .WithProperty ("SkipCompilerExecution", "true"); @@ -105,13 +197,63 @@ public void PreprocessOnDesignTimeBuild () var instance = project.Build ("CoreCompile"); - var generatedFilePath = instance.GetIntermediateDirFile ("TextTransform", "foo.cs") + var generatedFilePath = instance.GetIntermediateDirFile (expectedFilePath) .AssertTextStartsWith ("//--------"); instance.AssertSingleItem ("PreprocessedTemplates", generatedFilePath); instance.AssertNoItems ("GeneratedTemplates"); } + [Fact] + public void PreprocessLegacyMetadata () + { + // Arrange + using var ctx = new MSBuildTestContext (); + var project = ctx.LoadTestProject ("PreprocessTemplateMetadata") + .WithProperty ("UseLegacyT4Preprocessing", "true"); + + var outputDirectory = project.DirectoryPath["Demo/Output/OutputDirectory.cs"]; + var outputFilePath = project.DirectoryPath["Demo/LegacyOutput/OutputFilePath.cs"]; + var outputFileName = project.DirectoryPath["Demo/OutputFileNameTest.cs"]; + var outputFileNameAndOutputDirectory = project.DirectoryPath["Demo/Output/OutputDirectoryAndFileNameTest.g.cs"]; + + // Act + var instance = project.Build ("TransformTemplates"); + + // Assert + Assert.Multiple (() => { + outputDirectory.AssertContainsText + ( + StringComparison.Ordinal, + "namespace PreprocessTemplateMetadata.Demo.Output {", + "partial class OutputDirectory" + ); + + outputFilePath.AssertContainsText + ( + StringComparison.Ordinal, + "namespace PreprocessTemplateMetadata.Demo.LegacyOutput {", + "partial class OutputFilePath" + ); + + outputFileName.AssertContainsText + ( + StringComparison.Ordinal, + "namespace PreprocessTemplateMetadata.Demo {", + "partial class OutputFileNameTest" + ); + + outputFileNameAndOutputDirectory.AssertContainsText + ( + StringComparison.Ordinal, + "namespace PreprocessTemplateMetadata.Demo.Output {", + "partial class OutputDirectoryAndFileNameTest" + ); + }); + + instance.AssertNoItems ("GeneratedTemplates"); + } + [Fact] public void IncrementalTransform () { diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/Nested/Template/Folder/foo.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/Nested/Template/Folder/foo.tt new file mode 100644 index 00000000..b608e036 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/Nested/Template/Folder/foo.tt @@ -0,0 +1,2 @@ +<#@ template language="C#" #> +Hello World diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/PreprocessTemplateFromRelativePath.csproj b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/PreprocessTemplateFromRelativePath.csproj new file mode 100644 index 00000000..d69d1c8d --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateFromRelativePath/PreprocessTemplateFromRelativePath.csproj @@ -0,0 +1,14 @@ + + + + + netstandard2.0 + + + + + + + + + \ No newline at end of file diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectory.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectory.tt new file mode 100644 index 00000000..5cf62310 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectory.tt @@ -0,0 +1,2 @@ +<#@ template language="C#" #> +Hello Item Metadata OutputDirectory diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectoryAndOutputFileName.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectoryAndOutputFileName.tt new file mode 100644 index 00000000..8811756b --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputDirectoryAndOutputFileName.tt @@ -0,0 +1,2 @@ +<#@ template language="C#" #> +Hello Item Metadata OutputDirectory And OutputDFileName diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFileName.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFileName.tt new file mode 100644 index 00000000..09a68f65 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFileName.tt @@ -0,0 +1,3 @@ +<#@ template language="C#" #> +<#@ output extension=".generated.cs" #> +Hello Item Metadata OutputDFileName diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFilePath.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFilePath.tt new file mode 100644 index 00000000..30021ab1 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/OutputFilePath.tt @@ -0,0 +1,2 @@ +<#@ template language="C#" #> +Hello Item Metadata OutputFilePath diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/PreprocessTemplateMetadata.csproj b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/PreprocessTemplateMetadata.csproj new file mode 100644 index 00000000..e50d1a3f --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateMetadata/PreprocessTemplateMetadata.csproj @@ -0,0 +1,26 @@ + + + + + netstandard2.0 + + + + + Demo/Output + + + Demo/LegacyOutput + + + Demo/OutputFileNameTest.cs + + + Demo/Output + OutputDirectoryAndFileNameTest.g.cs + + + + + + \ No newline at end of file diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/PreprocessTemplateWithExtension.csproj b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/PreprocessTemplateWithExtension.csproj new file mode 100644 index 00000000..4cf25532 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/PreprocessTemplateWithExtension.csproj @@ -0,0 +1,14 @@ + + + + + netstandard2.0 + + + + + + + + + \ No newline at end of file diff --git a/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/foo.tt b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/foo.tt new file mode 100644 index 00000000..6e129f92 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/PreprocessTemplateWithExtension/foo.tt @@ -0,0 +1,3 @@ +<#@ template language="C#" #> +<#@ output extension=".g.cs" #> +Hello World diff --git a/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateFromRelativePath/Nested/Template/Folder/foo.tt b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateFromRelativePath/Nested/Template/Folder/foo.tt new file mode 100644 index 00000000..61aee478 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateFromRelativePath/Nested/Template/Folder/foo.tt @@ -0,0 +1,4 @@ +<#@ template language="C#" #> +<#@ parameter name="Year" type="int" #> +<#@ parameter name="Greeting" #> +<#=Greeting#> <#=Year#>! diff --git a/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateFromRelativePath/TransformTemplateFromRelativePath.csproj b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateFromRelativePath/TransformTemplateFromRelativePath.csproj new file mode 100644 index 00000000..12d161e0 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateFromRelativePath/TransformTemplateFromRelativePath.csproj @@ -0,0 +1,15 @@ + + + + + netstandard2.0 + + + + + + + + + + \ No newline at end of file diff --git a/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputDirectory.tt b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputDirectory.tt new file mode 100644 index 00000000..e75991fa --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputDirectory.tt @@ -0,0 +1,4 @@ +<#@ template language="C#" #> +<#@ parameter name="Year" type="int" #> +<#@ parameter name="Greeting" #> +<#=Greeting#> OutputDirectory <#=Year#>! diff --git a/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputDirectoryAndOutputFileName.tt b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputDirectoryAndOutputFileName.tt new file mode 100644 index 00000000..aa367f37 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputDirectoryAndOutputFileName.tt @@ -0,0 +1,4 @@ +<#@ template language="C#" #> +<#@ parameter name="Year" type="int" #> +<#@ parameter name="Greeting" #> +<#=Greeting#> OutputDirectory and OutputFileName <#=Year#>! diff --git a/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputFileName.tt b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputFileName.tt new file mode 100644 index 00000000..082675a6 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputFileName.tt @@ -0,0 +1,5 @@ +<#@ template language="C#" #> +<#@ output extension=".log" #> +<#@ parameter name="Year" type="int" #> +<#@ parameter name="Greeting" #> +<#=Greeting#> OutputFileName <#=Year#>! diff --git a/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputFilePath.tt b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputFilePath.tt new file mode 100644 index 00000000..e67cba92 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/OutputFilePath.tt @@ -0,0 +1,4 @@ +<#@ template language="C#" #> +<#@ parameter name="Year" type="int" #> +<#@ parameter name="Greeting" #> +<#=Greeting#> OutputFilePath <#=Year#>! diff --git a/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/TransformTemplateMetadata.csproj b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/TransformTemplateMetadata.csproj new file mode 100644 index 00000000..5248e463 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateMetadata/TransformTemplateMetadata.csproj @@ -0,0 +1,28 @@ + + + + + netstandard2.0 + + + + + + + + Demo/Output + + + Demo/LegacyOutput + + + Demo/OutputFileNameTest + + + Demo/Output + OutputDirectoryAndFileNameTest.log + + + + + \ No newline at end of file diff --git a/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateWithExtension/TransformTemplateWithExtension.csproj b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateWithExtension/TransformTemplateWithExtension.csproj new file mode 100644 index 00000000..7a81efa4 --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateWithExtension/TransformTemplateWithExtension.csproj @@ -0,0 +1,15 @@ + + + + + netstandard2.0 + + + + + + + + + + \ No newline at end of file diff --git a/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateWithExtension/foo.tt b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateWithExtension/foo.tt new file mode 100644 index 00000000..2499ef4c --- /dev/null +++ b/Mono.TextTemplating.Build.Tests/TestCases/TransformTemplateWithExtension/foo.tt @@ -0,0 +1,5 @@ +<#@ template language="C#" #> +<#@ output extension=".html" #> +<#@ parameter name="Year" type="int" #> +<#@ parameter name="Greeting" #> +

<#=Greeting#> <#=Year#>!

diff --git a/Mono.TextTemplating.Build/T4.BuildTools.targets b/Mono.TextTemplating.Build/T4.BuildTools.targets index 3839b1ba..fd21a097 100644 --- a/Mono.TextTemplating.Build/T4.BuildTools.targets +++ b/Mono.TextTemplating.Build/T4.BuildTools.targets @@ -76,6 +76,7 @@ <_T4PreprocessOnly>False <_T4PreprocessOnly Condition="'$(_T4TransformKind)'=='DesignTime' Or ('$(_T4TransformKind)'=='OnBuild' And '$(TransformOnBuild)'=='False')">True <_T4IntermediateTemplateOutputDir Condition="'$(_T4IntermediateTemplateOutputDir)'==''">$(IntermediateOutputPath)TextTransform\ + <_T4ProjectDirectory Condition="'$(_T4ProjectDirectory)'==''">$(MSBuildProjectDirectory) diff --git a/Mono.TextTemplating.Build/T4.BuildTools.targets.buildschema.json b/Mono.TextTemplating.Build/T4.BuildTools.targets.buildschema.json index cc8b4adf..c9868802 100644 --- a/Mono.TextTemplating.Build/T4.BuildTools.targets.buildschema.json +++ b/Mono.TextTemplating.Build/T4.BuildTools.targets.buildschema.json @@ -69,14 +69,24 @@ }, "metadata": [ { - // Metadata that controls how a specific template is processed - "$appliesTo": [ "T4Transform", "T4Preprocess" ], - "OutputFilePath": { - "description": "Overrides the output folder for a T4 template. Note that Visual Studio does not respect this.", + "$appliesTo": [ "T4Transform" ], + "OutputDirectory": { + "description": "Overrides the output folder for a transformed T4 template. Note that the Visual Studio T4 generator does not respect this.", "type": "folder" }, "OutputFileName": { - "description": "Overrides the output filename for a T4 template. Note that Visual Studio does not respect this.", + "description": "Overrides the output filename for a transformed T4 template. Note that the Visual Studio T4 generator does not respect this.", + "type": "file" + } + }, + { + "$appliesTo": [ "T4Preprocess" ], + "OutputDirectory": { + "description": "Legacy option to override the output folder for a preprocessed T4 template. Ignored unless `UseLegacyT4Preprocessing` is `true`. Note that the Visual Studio T4 generator does not respect this.", + "type": "folder" + }, + "OutputFileName": { + "description": "Legacy option to override the output filename for a preprocessed T4 template. Ignored unless `UseLegacyT4Preprocessing` is `true`. Note that the Visual Studio T4 generator does not respect this.", "type": "file" } }, diff --git a/Mono.TextTemplating.Build/TemplateBuildState.cs b/Mono.TextTemplating.Build/TemplateBuildState.cs index 93f48dda..e14fc77b 100644 --- a/Mono.TextTemplating.Build/TemplateBuildState.cs +++ b/Mono.TextTemplating.Build/TemplateBuildState.cs @@ -220,7 +220,7 @@ public bool Equals (Parameter other) // TODO: cache warnings [MessagePackObject] - public class TransformTemplate + public class TransformTemplate : ITemplateState { [Key (0)] public string InputFile { get; set; } @@ -228,6 +228,8 @@ public class TransformTemplate public string OutputFile { get; set; } [Key (2)] public List Dependencies { get; set; } + [Key (3)] + public string ExtensionOverride { get; set; } public bool IsStale (Func getFileWriteTime, TaskLoggingHelper logger) { @@ -256,7 +258,7 @@ public bool IsStale (Func getFileWriteTime, TaskLoggingHelper // TODO: cache warnings [MessagePackObject] - public class PreprocessedTemplate + public class PreprocessedTemplate : ITemplateState { [Key (0)] public string InputFile { get; set; } @@ -266,6 +268,10 @@ public class PreprocessedTemplate public List Dependencies { get; set; } [Key (3)] public List References { get; set; } + [Key (4)] + public string Namespace { get; set; } + [Key (5)] + public string ExtensionOverride { get; set; } public bool IsStale (Func getFileWriteTime, TaskLoggingHelper logger) { @@ -294,6 +300,13 @@ public bool IsStale (Func getFileWriteTime, TaskLoggingHelper } } + public interface ITemplateState + { + string OutputFile { get; set; } + + string ExtensionOverride { get; set; } + } + #if !NETCOREAPP2_1_OR_GREATER struct HashCode { diff --git a/Mono.TextTemplating.Build/TextTransform.cs b/Mono.TextTemplating.Build/TextTransform.cs index 1d9827ea..093a0372 100644 --- a/Mono.TextTemplating.Build/TextTransform.cs +++ b/Mono.TextTemplating.Build/TextTransform.cs @@ -40,6 +40,9 @@ public TextTransform () : base (Messages.ResourceManager) { } [Required] public string IntermediateDirectory { get; set; } + [Required] + public string ProjectDirectory { get; set; } + [Output] public ITaskItem[] RequiredAssemblies { get; set; } @@ -54,6 +57,7 @@ public override bool Execute () bool success = true; Directory.CreateDirectory (IntermediateDirectory); + Directory.CreateDirectory (ProjectDirectory); string buildStateFilename = Path.Combine (IntermediateDirectory, "t4-build-state.msgpack"); @@ -101,16 +105,20 @@ public override bool Execute () foreach (var ppt in PreprocessTemplates) { string inputFile = ppt.ItemSpec; string outputFile; + + // Metadata only supported for legacy processing. + string extensionOverride = null; if (UseLegacyPreprocessingMode) { - //TODO: OutputFilePath, OutputFileName - outputFile = Path.ChangeExtension (inputFile, ".cs"); + outputFile = GetOutputPathViaMetadata (ppt, inputFile, ".cs", out extensionOverride); } else { - //FIXME: this could cause collisions. generate a path based on relative path and link metadata outputFile = Path.Combine (IntermediateDirectory, Path.ChangeExtension (inputFile, ".cs")); } + buildState.PreprocessTemplates.Add (new TemplateBuildState.PreprocessedTemplate { InputFile = inputFile, - OutputFile = outputFile + OutputFile = outputFile, + Namespace = CalculateNamespace (outputFile), + ExtensionOverride = extensionOverride }); } } @@ -118,14 +126,14 @@ public override bool Execute () if (TransformTemplates != null) { buildState.TransformTemplates = new List (); foreach (var tt in TransformTemplates) { - //TODO: OutputFilePath, OutputFileName - //var outputFilePathMetadata = tt.TryGetMetadata("OutputFilePath"); - //var outputFileNameMetadata = tt.TryGetMetadata("OutputFileName"); string inputFile = tt.ItemSpec; - string outputFile = Path.ChangeExtension (inputFile, ".txt"); + + var outputFile = GetOutputPathViaMetadata (tt, inputFile, ".txt", out var extensionOverride); + buildState.TransformTemplates.Add (new TemplateBuildState.TransformTemplate { InputFile = inputFile, - OutputFile = outputFile + OutputFile = outputFile, + ExtensionOverride = extensionOverride }); } } @@ -152,7 +160,6 @@ public override bool Execute () //RequiredAssemblies //settings.Debug //settings.Log - //metadata to override output name, class name and namespace SaveBuildState (buildState, buildStateFilename, msgPackOptions); @@ -161,6 +168,29 @@ public override bool Execute () return success; } + string GetOutputPathViaMetadata (ITaskItem taskItem, string inputFile, string extensionDefault, out string extensionOverride) + { + extensionOverride = null; + if (!taskItem.TryGetMetadata ("OutputFileName", out var outputFile)) { + var name = Path.GetFileNameWithoutExtension (inputFile); + outputFile = Path.ChangeExtension (name, extensionDefault); + } else { + extensionOverride = Path.GetExtension (outputFile); + } + + // If set, it is relative to the ProjectDirectory. + if (taskItem.TryGetMetadata ("OutputDirectory", out var outputDirectory)) { + outputFile = Path.Combine (ProjectDirectory, outputDirectory, outputFile); + } else if (taskItem.TryGetMetadata ("OutputFilePath", out outputDirectory)) { + outputFile = Path.Combine (ProjectDirectory, outputDirectory, outputFile); + } else { // otherwise use the same directory as the template. + var parentDir = GetDirectoryFullPath (inputFile); + outputFile = Path.Combine (parentDir, outputFile); + } + + return outputFile; + } + static TaskItem ConstructOutputItem (string outputFile, string inputFile, List itemDependencies) { var item = new TaskItem (outputFile); @@ -173,6 +203,24 @@ static TaskItem ConstructOutputItem (string outputFile, string inputFile, List { string inputFile = transform.InputFile; - string outputFile = Path.ChangeExtension (inputFile, ".txt"); var pt = LoadTemplate (generator, inputFile, out var inputContent); TemplateSettings settings = TemplatingEngine.GetSettings (generator, pt); + UpdateOutputExtensionWithSettings (transform, settings); if (parameterMap != null) { AddCoercedSessionParameters (generator, pt, parameterMap); @@ -49,14 +49,12 @@ public static bool Process (TaskLoggingHelper taskLog, TemplateBuildState previo return; } - string outputContent; - (outputFile, outputContent) = generator.ProcessTemplateAsync (pt, inputFile, inputContent, outputFile, settings).Result; + (var outputFile, var outputContent) = generator.ProcessTemplateAsync (pt, inputFile, inputContent, transform.OutputFile, settings).Result; if (generator.Errors.HasErrors) { return; } - transform.OutputFile = outputFile; transform.Dependencies = new List (generator.IncludedFiles); transform.Dependencies.AddRange (generator.CapturedReferences); @@ -79,26 +77,21 @@ public static bool Process (TaskLoggingHelper taskLog, TemplateBuildState previo var pt = LoadTemplate (generator, inputFile, out var inputContent); TemplateSettings settings = TemplatingEngine.GetSettings (generator, pt); + UpdateOutputExtensionWithSettings (preprocess, settings); settings.RelativeLinePragmas = true; settings.RelativeLinePragmasBaseDirectory = Path.GetDirectoryName (preprocess.OutputFile); settings.CodeGenerationOptions.UseRemotingCallContext = buildState.PreprocessTargetRuntimeIdentifier == ".NETFramework"; - // FIXME: make these configurable, take relative path into account - settings.Namespace = buildState.DefaultNamespace; - settings.Name = Path.GetFileNameWithoutExtension (preprocess.InputFile); + settings.Namespace = preprocess.Namespace; + settings.Name = GetFileNameWithoutExtension (preprocess.OutputFile); generator.Errors.AddRange (pt.Errors); if (generator.Errors.HasErrors) { return; } - //FIXME: escaping - //FIXME: namespace name based on relative path and link metadata - string preprocessClassName = Path.GetFileNameWithoutExtension (inputFile); - settings.Name = preprocessClassName; - var outputContent = generator.PreprocessTemplate (pt, inputFile, inputContent, settings, out var references); if (generator.Errors.HasErrors) { @@ -142,6 +135,19 @@ public static bool Process (TaskLoggingHelper taskLog, TemplateBuildState previo return !hasErrors; } + static void UpdateOutputExtensionWithSettings (TemplateBuildState.ITemplateState state, TemplateSettings settings) + { + // If extension is an empty string, the returned path from Path.ChangeExtension contains + // the contents of path with any characters following the last period removed. + // Therefore we must trim the trailing period. + if (state.ExtensionOverride != null) { + state.OutputFile = Path.ChangeExtension (state.OutputFile, state.ExtensionOverride).TrimEnd ('.'); + settings.Extension = state.ExtensionOverride; + } else if (settings.Extension != null) { + state.OutputFile = Path.ChangeExtension (state.OutputFile, settings.Extension).TrimEnd ('.'); + } + } + static bool LogErrors (TaskLoggingHelper taskLog, string filename, CompilerErrorCollection errors) { bool hasErrors = false; @@ -178,9 +184,20 @@ static ParsedTemplate LoadTemplate (MSBuildTemplateGenerator generator, string f return generator.ParseTemplate (filename, inputContent); } + static string GetFileNameWithoutExtension (string path) + { + var fileName = Path.GetFileName (path); + var firstDotIndex = fileName.IndexOf ('.'); + return firstDotIndex != -1 + ? fileName.Substring (0, firstDotIndex) + : fileName; + } + static void WriteOutput (MSBuildTemplateGenerator generator, string outputFile, string outputContent, Encoding encoding) { try { + var parentDir = Path.GetDirectoryName (outputFile); + Directory.CreateDirectory (parentDir); File.WriteAllText (outputFile, outputContent, encoding ?? new UTF8Encoding (encoderShouldEmitUTF8Identifier: false)); } catch (IOException ex) { diff --git a/Mono.TextTemplating.Build/readme.md b/Mono.TextTemplating.Build/readme.md index 6a7304e4..f844a898 100644 --- a/Mono.TextTemplating.Build/readme.md +++ b/Mono.TextTemplating.Build/readme.md @@ -74,7 +74,7 @@ Similarly, the following `DirectiveProcessor` items are equivalent: | Metadata | Description | -- | -- -| `OutputDirectory`| Set an output directory for the file generated by the template. If this is not set, it defaults to the directory containing the template file. It is evaluated relative to the project directory, not relative to the template file. If the directory does not exist, it wil; be created. +| `OutputDirectory`| Set an output directory for the file generated by the template. If this is not set, it defaults to the directory containing the template file. It is evaluated relative to the project directory, not relative to the template file. If the directory does not exist, it will be created. | `OutputFileName`| Set a filename to be used for the template output instead of deriving one from the template filename. If this is set, it will be the exact name used for the generated file. Any `<#@extension..#>` directive present in the template file will be ignored, and no other extension will be added. This filename may include directory components, and is evaluated relative to the template directory, which defaults to the directory containing the template file. ### CLI Properties diff --git a/Mono.TextTemplating.Tests/TestDataPath.cs b/Mono.TextTemplating.Tests/TestDataPath.cs index 971e33cb..6878abcd 100644 --- a/Mono.TextTemplating.Tests/TestDataPath.cs +++ b/Mono.TextTemplating.Tests/TestDataPath.cs @@ -85,6 +85,21 @@ public TestDataPath AssertTextStartsWith (string value, StringComparison compari return this; } + /// + /// Assert that the file exists and contains the given values + /// + public TestDataPath AssertContainsText (StringComparison comparison, params string[] values) + { + AssertFileExists (); + var text = File.ReadAllText (path); + Assert.Multiple(() => { + foreach (var value in values) { + Assert.Contains (value, text, comparison); + } + }); + return this; + } + static string TrimEndingDirectorySeparator (string path) #if NETCOREAPP3_0_OR_GREATER => Path.TrimEndingDirectorySeparator (path);