Skip to content
This repository has been archived by the owner on Dec 12, 2020. It is now read-only.

Commit

Permalink
feat: Give Generators Access to MSBuild Properties (#210)
Browse files Browse the repository at this point in the history
* Add abiilty to pass msbuild properties to the generator

* update readme

* revert to item approach

* escape semicolons when creating _CodeGenToolResponseFileLines

* add sample using TargetFrameworks property to show escaping works

* update doc comments in transformation context

* add link to sample projects in readme, update changelog


Co-authored-by: talenfisher <[email protected]>
  • Loading branch information
talenfisher authored Apr 15, 2020
1 parent 84f9d1e commit f3a03fc
Show file tree
Hide file tree
Showing 19 changed files with 285 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
* Support for accessing arbitrary MSBuild properties, see [Readme section](README.md#access-msbuild-properties) ([#210])


## [0.7.63] - 2020-04-08

Expand Down Expand Up @@ -43,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#178]: https://github.com/AArnott/CodeGeneration.Roslyn/pull/178
[#198]: https://github.com/AArnott/CodeGeneration.Roslyn/pull/198
[#205]: https://github.com/AArnott/CodeGeneration.Roslyn/pull/205
[#210]: https://github.com/AArnott/CodeGeneration.Roslyn/pull/210
[v0.7 migration guide]: https://github.com/AArnott/CodeGeneration.Roslyn/wiki/Migrations#v07


Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Instructions on development and using this project's source code are in [CONTRIB
- [Separate out the attribute](#separate-out-the-attribute)
- [Create the metapackage](#create-the-metapackage)
- [Add extra `build/` content in Plugin package](#add-extra-build-content-in-plugin-package)
- [Access MSBuild Properties](#access-msbuild-properties)

## How to write your own code generator

Expand Down Expand Up @@ -543,6 +544,30 @@ to add custom MSBuild props/targets into NuGet package's `build` folder (and hav
imported when package is referenced), you'll need to use `PackageBuildFolderProjectImport`
ItemGroup, as shown in `PackagedGenerator` sample.

### Accesss MSBuild Properties

You may access MSBuild property values of the project being generated for, by first adding the property
name to the `PluginRequestedProperty` item list. For example, if you want to access the TargetFramework build
property, you would do the following in your generator's .csproj file:

```xml
<ItemGroup>
<PluginRequestedProperty Include="ExampleBuildProperty" />
</ItemGroup>
```

Then, you can access its value like this:

```cs
public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationContext context, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
{
var targetFramework = context.BuildProperties["TargetFramework"];
// ...
}
```

> ℹ For a sample generator that accesses MSBuild properties, see [BuildPropsGenerator](samples/BuildPropsGenerator) and its consuming project, [BuildPropsConsumer](samples/BuildPropsConsumer)
[NuPkg]: https://nuget.org/packages/CodeGeneration.Roslyn
[AttrNuPkg]: https://nuget.org/packages/CodeGeneration.Roslyn.Attributes
[ToolNuPkg]: https://nuget.org/packages/CodeGeneration.Roslyn.Tool
Expand Down
16 changes: 16 additions & 0 deletions samples/BuildPropsConsumer/BuildPropsConsumer.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.2;netcoreapp3.1</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CodeGeneration.Roslyn.Attributes" Version="$(LocalNuGetVersion)" PrivateAssets="all" />
<PackageReference Include="CodeGeneration.Roslyn.Tool" Version="$(LocalNuGetVersion)" PrivateAssets="all" />
<PackageReference Include="BuildPropsGenerator" Version="1.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
27 changes: 27 additions & 0 deletions samples/BuildPropsConsumer/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using CodeGeneration.Roslyn;

namespace BuildPropsConsumer
{

[CodeGenerationAttribute("BuildPropsGenerator.FrameworkInfoProviderGenerator, BuildPropsGenerator")]
class FrameworkInfoProviderAttribute : Attribute { }

[FrameworkInfoProvider]
partial class Program
{
static void Main(string[] args)
{
var program = new Program();
var frameworks = program.TargetFrameworks;
var currentFramework = program.CurrentTargetFramework;

Console.WriteLine("This project is build for the following frameworks: ");

foreach(var framework in frameworks) {
var message = framework == currentFramework ? $"{framework} (current)" : framework;
Console.WriteLine(message);
}
}
}
}
17 changes: 17 additions & 0 deletions samples/BuildPropsGenerator/BuildPropsGenerator.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="$(CodeGenerationRoslynPluginSdkPath)Sdk.props" />

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>

<ItemGroup>
<PluginRequestedProperty Include="TargetFrameworks" />
<PluginRequestedProperty Include="TargetFramework" />
</ItemGroup>

<Import Project="$(CodeGenerationRoslynPluginSdkPath)Sdk.targets" />

</Project>
54 changes: 54 additions & 0 deletions samples/BuildPropsGenerator/FrameworkInfoProviderGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CodeGeneration.Roslyn;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace BuildPropsGenerator
{
public class FrameworkInfoProviderGenerator : ICodeGenerator
{
public FrameworkInfoProviderGenerator(AttributeData attributeData)
{
}

public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationContext context, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
{
var partialType = CreatePartialType();
return Task.FromResult(SyntaxFactory.List(partialType));

IEnumerable<MemberDeclarationSyntax> CreatePartialType()
{
var newPartialType =
context.ProcessingNode is ClassDeclarationSyntax classDeclaration
? SyntaxFactory.ClassDeclaration(classDeclaration.Identifier.ValueText)
: context.ProcessingNode is StructDeclarationSyntax structDeclaration
? SyntaxFactory.StructDeclaration(structDeclaration.Identifier.ValueText)
: default(TypeDeclarationSyntax);
if (newPartialType is null)
yield break;
yield return newPartialType
?.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword))
.AddMembers(CreateTargetFrameworkListProperty(), CreateCurrentTargetFrameworkProperty());
}
MemberDeclarationSyntax CreateTargetFrameworkListProperty()
{
var collectionType = "System.Collections.Generic.List<string>";
var frameworks = context.BuildProperties["TargetFrameworks"];
var quotedFrameworks = frameworks.Split(";").Select(framework => $"\"{framework}\"");
var commaDelimitedFrameworks = string.Join(',', quotedFrameworks.ToArray());

return SyntaxFactory.ParseMemberDeclaration($"public {collectionType} TargetFrameworks {{ get; }} = new {collectionType} {{ {commaDelimitedFrameworks} }};");
}
MemberDeclarationSyntax CreateCurrentTargetFrameworkProperty()
{
var framework = context.BuildProperties["TargetFramework"];
return SyntaxFactory.ParseMemberDeclaration($"public string CurrentTargetFramework {{ get; }} = \"{framework}\";");
}
}
}
}
11 changes: 11 additions & 0 deletions samples/BuildPropsGenerator/build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env pwsh

Write-Host "Running in $PSScriptRoot" -ForegroundColor Cyan
Push-Location $PSScriptRoot
try {
Write-Host "dotnet build" -ForegroundColor Green
dotnet build
}
finally {
Pop-Location
}
6 changes: 6 additions & 0 deletions src/CodeGeneration.Roslyn.Engine/CompilationGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public class CompilationGenerator
/// </summary>
public IReadOnlyList<string> PluginPaths { get; set; } = new List<string>();

/// <summary>
/// Gets or sets the build properties to expose to generators.
/// </summary>
public IReadOnlyDictionary<string, string> BuildProperties { get; set; }

/// <summary>
/// Gets or sets the path to the directory that contains generated source files.
/// </summary>
Expand Down Expand Up @@ -122,6 +127,7 @@ public async Task GenerateAsync(IProgress<Diagnostic> progress = null, Cancellat
compilation,
inputSyntaxTree,
this.ProjectDirectory,
this.BuildProperties,
this.LoadPlugin,
progress,
cancellationToken);
Expand Down
5 changes: 4 additions & 1 deletion src/CodeGeneration.Roslyn.Engine/DocumentTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public static class DocumentTransform
/// <param name="compilation">The compilation to which the document belongs.</param>
/// <param name="inputDocument">The document to scan for generator attributes.</param>
/// <param name="projectDirectory">The path of the <c>.csproj</c> project file.</param>
/// <param name="buildProperties">MSBuild properties to expose to the generator.</param>
/// <param name="assemblyLoader">A function that can load an assembly with the given name.</param>
/// <param name="progress">Reports warnings and errors in code generation.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
Expand All @@ -48,6 +49,7 @@ public static async Task<SyntaxTree> TransformAsync(
CSharpCompilation compilation,
SyntaxTree inputDocument,
string projectDirectory,
IReadOnlyDictionary<string, string> buildProperties,
Func<AssemblyName, Assembly> assemblyLoader,
IProgress<Diagnostic> progress,
CancellationToken cancellationToken)
Expand Down Expand Up @@ -91,7 +93,8 @@ public static async Task<SyntaxTree> TransformAsync(
compilation,
projectDirectory,
emittedUsings,
emittedExterns);
emittedExterns,
buildProperties);

var richGenerator = generator as IRichCodeGenerator ?? new EnrichingCodeGeneratorProxy(generator);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
@(_PackageBuildFolderProjectImport_props->' <Import Project="%(Identity)" />', '%0D%0A')
<ItemGroup>
<CodeGenerationRoslynPlugin Include="%24(MSBuildThisFileDirectory)../$(PluginPackagePath)/$(TargetFileName)" />
@(PluginRequestedProperty->' <CodeGenerationRoslynRequestedProperty Include="%(Identity)" />', '%0D%0A')
</ItemGroup>
</Project>
]]>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the MS-PL license. See LICENSE.txt file in the project root for full license information.

namespace CodeGeneration.Roslyn.Tests.Generators
{
using System;
using System.Diagnostics;
using Validation;

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
[CodeGenerationAttribute(typeof(AddExampleBuildPropertyGenerator))]
[Conditional("CodeGeneration")]
public class AddExampleBuildPropertyAttribute : Attribute
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the MS-PL license. See LICENSE.txt file in the project root for full license information.

namespace CodeGeneration.Roslyn.Tests.Generators
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Validation;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

public class AddExampleBuildPropertyGenerator : ICodeGenerator
{
public AddExampleBuildPropertyGenerator(AttributeData attributeData)
{
Requires.NotNull(attributeData, nameof(attributeData));
}

public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationContext context, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
{
var partialClass = GeneratePartialClass();
return Task.FromResult(SyntaxFactory.List(partialClass));

IEnumerable<MemberDeclarationSyntax> GeneratePartialClass()
{
var classDeclaration = context.ProcessingNode as ClassDeclarationSyntax;
yield return classDeclaration
.AddMembers(CreateExampleBuildProperty());
}

MemberDeclarationSyntax CreateExampleBuildProperty()
{
var value = context.BuildProperties["ExampleBuildProperty"];
return SyntaxFactory.ParseMemberDeclaration($"public string ExampleBuildProperty {{ get; }} = \"{value}\";");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
<!-- Ignore CGR1002 because it's expected to be raised by legacy Amadevus.RecordGenerator -->
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);CGR1002</MSBuildWarningsAsMessages>
<GenerateCodeFromAttributesDependsOn>OverrideCodeGenToolPath;$(GenerateCodeFromAttributesDependsOn)</GenerateCodeFromAttributesDependsOn>
<ExampleBuildProperty>c7189d5e-495c-4cab-8e18-ab8d7ab71a2e</ExampleBuildProperty>
</PropertyGroup>

<ItemGroup>
<CodeGenerationRoslynRequestedProperty Include="ExampleBuildProperty" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Amadevus.RecordGenerator.Attributes" Version="0.4.1" PrivateAssets="all" />
<PackageReference Include="Amadevus.RecordGenerator.Generators" Version="0.4.1" PrivateAssets="all" />
Expand Down
12 changes: 12 additions & 0 deletions src/CodeGeneration.Roslyn.Tests/CodeGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ public void NuGetRecordGeneratorWorks()
record.ToBuilder();
}

[Fact]
public void AccessingBuildPropertiesWorks()
{
var objWithBuildProp = new ClassWithExampleBuildProperty();
Assert.Equal("c7189d5e-495c-4cab-8e18-ab8d7ab71a2e", objWithBuildProp.ExampleBuildProperty);
}

public partial class Wrapper
{
[ExternalDuplicateWithSuffixByName("Suffix")]
Expand Down Expand Up @@ -66,4 +73,9 @@ public partial class MultipliedBar
[Test(X = 10, Y = 20)]
public string Value { get; set; }
}

[AddExampleBuildProperty]
public partial class ClassWithExampleBuildProperty
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ protected static async Task<SyntaxTree> GenerateAsync(string source)
var diagnostics = compilation.GetDiagnostics();
Assert.Empty(diagnostics.Where(x => x.Severity >= DiagnosticSeverity.Warning));
var progress = new Progress<Diagnostic>();
var result = await DocumentTransform.TransformAsync(compilation, tree, null, Assembly.Load, progress, CancellationToken.None);
var result = await DocumentTransform.TransformAsync(compilation, tree, null, null, Assembly.Load, progress, CancellationToken.None);
return result;
}

Expand Down
5 changes: 5 additions & 0 deletions src/CodeGeneration.Roslyn.Tool/CommandLine/Enumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public static T[] ToArray<T>(this IEnumerable<T> source)
return System.Linq.Enumerable.ToArray(source);
}

public static Dictionary<TKey, TElement> ToDictionary<T, TKey, TElement>(this IEnumerable<T> source, Func<T, TKey> keySelector, Func<T, TElement> elementSelector)
{
return System.Linq.Enumerable.ToDictionary(source, keySelector, elementSelector);
}

public static T Last<T>(this IEnumerable<T> source)
{
return System.Linq.Enumerable.Last(source);
Expand Down
Loading

0 comments on commit f3a03fc

Please sign in to comment.