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

Commit

Permalink
Merge pull request #67 from Pzixel/feature/global_attributes_generation
Browse files Browse the repository at this point in the history
Assembly attributes support
  • Loading branch information
AArnott authored May 2, 2018
2 parents b889381 + 7383a10 commit 078476a
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 35 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ If you build with `dotnet build`, you need to take a couple extra steps. First,
Second, add this item to an `<ItemGroup>` in the project that will execute the code generator as part of your build:

```xml
<DotNetCliToolReference Include="dotnet-codegen" Version="0.4.11" />
<DotNetCliToolReference Include="dotnet-codegen" Version="0.4.12" />
```

You should adjust the version in the above xml to match the version of this tool you are using.
Expand All @@ -187,10 +187,10 @@ to immediately see the effects of your changes on the generated code.
You can also package up your code generator as a NuGet package for others to install
and use. Your NuGet package should include a dependency on the `CodeGeneration.Roslyn.BuildTime`
that matches the version of `CodeGeneration.Roslyn` that you used to produce your generator.
For example, if you used version 0.4.11 of this project, your .nuspec file would include this tag:
For example, if you used version 0.4.12 of this project, your .nuspec file would include this tag:

```xml
<dependency id="CodeGeneration.Roslyn.BuildTime" version="0.4.11" />
<dependency id="CodeGeneration.Roslyn.BuildTime" version="0.4.12" />
```

In addition to this dependency, your NuGet package should include a `build` folder with an
Expand Down Expand Up @@ -219,7 +219,7 @@ so that the MSBuild Task can invoke the `dotnet codegen` command line tool:
```xml
<ItemGroup>
<PackageReference Include="YourCodeGenPackage" Version="1.2.3" PrivateAssets="all" />
<DotNetCliToolReference Include="dotnet-codegen" Version="0.4.11" />
<DotNetCliToolReference Include="dotnet-codegen" Version="0.4.12" />
</ItemGroup>
```

Expand Down
6 changes: 6 additions & 0 deletions src/CodeGeneration.Roslyn.Tasks/GenerateCodeFromAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public class GenerateCodeFromAttributes : ToolTask
[Required]
public ITaskItem[] GeneratorAssemblySearchPaths { get; set; }

[Required]
public string ProjectDirectory { get; set; }

[Required]
public string IntermediateOutputDirectory { get; set; }

Expand Down Expand Up @@ -80,6 +83,9 @@ protected override string GenerateResponseFileCommands()
argBuilder.AppendLine("--out");
argBuilder.AppendLine(this.IntermediateOutputDirectory);

argBuilder.AppendLine("--projectDir");
argBuilder.AppendLine(this.ProjectDirectory);

this.generatedCompileItemsFilePath = Path.Combine(this.IntermediateOutputDirectory, Path.GetRandomFileName());
argBuilder.AppendLine("--generatedFilesList");
argBuilder.AppendLine(this.generatedCompileItemsFilePath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Target Name="GenerateCodeFromAttributes" DependsOnTargets="ResolveReferences" BeforeTargets="CoreCompile">
<GenerateCodeFromAttributes
ToolLocationOverride="$(GenerateCodeFromAttributesToolPathOverride)"
ProjectDirectory="$(MSBuildProjectDirectory)"
Compile="@(Compile)"
ReferencePath="@(ReferencePath)"
GeneratorAssemblySearchPaths="@(GeneratorAssemblySearchPaths)"
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 MIT 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.Assembly)]
[CodeGenerationAttribute(typeof(DirectoryPathGenerator))]
[Conditional("CodeGeneration")]
public class DirectoryPathAttribute : Attribute
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.

namespace CodeGeneration.Roslyn.Tests.Generators
{
using System;
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 DirectoryPathGenerator : ICodeGenerator
{
public DirectoryPathGenerator(AttributeData attributeData)
{
Requires.NotNull(attributeData, nameof(attributeData));
}

public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationContext context, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
{
var member = ClassDeclaration("DirectoryPathTest")
.AddMembers(
FieldDeclaration(
VariableDeclaration(
PredefinedType(Token(SyntaxKind.StringKeyword)))
.AddVariables(
VariableDeclarator(Identifier("Path"))
.WithInitializer(
EqualsValueClause(
LiteralExpression(
SyntaxKind.StringLiteralExpression,
Literal(context.ProjectDirectory))))))
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.ConstKeyword))));

return Task.FromResult(List<MemberDeclarationSyntax>(new []{member}));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationCon
var results = SyntaxFactory.List<MemberDeclarationSyntax>();

MemberDeclarationSyntax copy = null;
var applyToClass = context.ProcessingMember as ClassDeclarationSyntax;
var applyToClass = context.ProcessingNode as ClassDeclarationSyntax;
if (applyToClass != null)
{
copy = applyToClass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationCon
var results = SyntaxFactory.List<MemberDeclarationSyntax>();

MemberDeclarationSyntax copy = null;
var applyToClass = context.ProcessingMember as ClassDeclarationSyntax;
var applyToClass = context.ProcessingNode as ClassDeclarationSyntax;
if (applyToClass != null)
{
var properties = applyToClass.Members.OfType<PropertyDeclarationSyntax>()
Expand Down
6 changes: 3 additions & 3 deletions src/CodeGeneration.Roslyn.Tests/CodeGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CodeGeneration.Roslyn.Tests.Generators;
using Xunit;

[assembly: DirectoryPath]

public partial class CodeGenerationTests
{
/// <summary>
Expand All @@ -21,6 +20,7 @@ public void SimpleGenerationWorks()
var fooB = new CodeGenerationTests.FooB();
var multiplied = new MultipliedBar();
multiplied.ValueSuff1020();
Assert.EndsWith(@"src\CodeGeneration.Roslyn.Tests", DirectoryPathTest.Path, StringComparison.OrdinalIgnoreCase);
}

[DuplicateWithSuffixByName("A")]
Expand Down
3 changes: 3 additions & 0 deletions src/CodeGeneration.Roslyn.Tool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ static int Main(string[] args)
IReadOnlyList<string> generatorSearchPaths = Array.Empty<string>();
string generatedCompileItemFile = null;
string outputDirectory = null;
string projectDir = null;
ArgumentSyntax.Parse(args, syntax =>
{
syntax.DefineOptionList("r|reference", ref refs, "Paths to assemblies being referenced");
syntax.DefineOptionList("generatorSearchPath", ref generatorSearchPaths, "Paths to folders that may contain generator assemblies");
syntax.DefineOption("out", ref outputDirectory, true, "The directory to write generated source files to");
syntax.DefineOption("projectDir", ref projectDir, true, "The absolute path of the directory where the project file is located");
syntax.DefineOption("generatedFilesList", ref generatedCompileItemFile, "The path to the file to create with a list of generated source files");
syntax.DefineParameterList("compile", ref compile, "Source files included in compilation");
});
Expand All @@ -41,6 +43,7 @@ static int Main(string[] args)

var generator = new CompilationGenerator
{
ProjectDirectory = projectDir,
Compile = compile,
ReferencePath = refs,
GeneratorAssemblySearchPaths = generatorSearchPaths,
Expand Down
3 changes: 3 additions & 0 deletions src/CodeGeneration.Roslyn/CompilationGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public class CompilationGenerator
/// </summary>
public IEnumerable<string> EmptyGeneratedFiles => this.emptyGeneratedFiles;

public string ProjectDirectory { get; set; }

public void Generate(IProgress<Diagnostic> progress = null, CancellationToken cancellationToken = default(CancellationToken))
{
Verify.Operation(this.Compile != null, $"{nameof(Compile)} must be set first.");
Expand Down Expand Up @@ -104,6 +106,7 @@ public class CompilationGenerator
var generatedSyntaxTree = DocumentTransform.TransformAsync(
compilation,
inputSyntaxTree,
this.ProjectDirectory,
this.LoadAssembly,
progress).GetAwaiter().GetResult();

Expand Down
44 changes: 25 additions & 19 deletions src/CodeGeneration.Roslyn/DocumentTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ namespace CodeGeneration.Roslyn
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -40,7 +39,7 @@ public static class DocumentTransform
/// <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>
/// <returns>A task whose result is the generated document.</returns>
public static async Task<SyntaxTree> TransformAsync(CSharpCompilation compilation, SyntaxTree inputDocument, Func<AssemblyName, Assembly> assemblyLoader, IProgress<Diagnostic> progress)
public static async Task<SyntaxTree> TransformAsync(CSharpCompilation compilation, SyntaxTree inputDocument, string projectDirectory, Func<AssemblyName, Assembly> assemblyLoader, IProgress<Diagnostic> progress)
{
Requires.NotNull(compilation, nameof(compilation));
Requires.NotNull(inputDocument, nameof(inputDocument));
Expand All @@ -51,16 +50,16 @@ public static async Task<SyntaxTree> TransformAsync(CSharpCompilation compilatio

var inputFileLevelUsingDirectives = inputSyntaxTree.GetRoot().ChildNodes().OfType<UsingDirectiveSyntax>();

var memberNodes = from syntax in inputSyntaxTree.GetRoot().DescendantNodes(n => n is CompilationUnitSyntax || n is NamespaceDeclarationSyntax || n is TypeDeclarationSyntax).OfType<MemberDeclarationSyntax>()
select syntax;
var memberNodes = inputSyntaxTree.GetRoot().DescendantNodesAndSelf(n => n is CompilationUnitSyntax || n is NamespaceDeclarationSyntax || n is TypeDeclarationSyntax).OfType<CSharpSyntaxNode>();

var emittedMembers = SyntaxFactory.List<MemberDeclarationSyntax>();
foreach (var memberNode in memberNodes)
{
var generators = FindCodeGenerators(inputSemanticModel, memberNode, assemblyLoader);
var attributeData = GetAttributeData(compilation, inputSemanticModel, memberNode);
var generators = FindCodeGenerators(attributeData, assemblyLoader);
foreach (var generator in generators)
{
var context = new TransformationContext(memberNode, inputSemanticModel, compilation);
var context = new TransformationContext(memberNode, inputSemanticModel, compilation, projectDirectory);
var generatedTypes = await generator.GenerateAsync(context, progress, CancellationToken.None);

// Figure out ancestry for the generated type, including nesting types and namespaces.
Expand Down Expand Up @@ -112,22 +111,29 @@ public static async Task<SyntaxTree> TransformAsync(CSharpCompilation compilatio
return compilationUnit.SyntaxTree;
}

private static IEnumerable<ICodeGenerator> FindCodeGenerators(SemanticModel document, SyntaxNode nodeWithAttributesApplied, Func<AssemblyName, Assembly> assemblyLoader)
private static ImmutableArray<AttributeData> GetAttributeData(Compilation compilation, SemanticModel document, SyntaxNode syntaxNode)
{
Requires.NotNull(document, "document");
Requires.NotNull(nodeWithAttributesApplied, "nodeWithAttributesApplied");
Requires.NotNull(document, nameof(document));
Requires.NotNull(syntaxNode, nameof(syntaxNode));

var symbol = document.GetDeclaredSymbol(nodeWithAttributesApplied);
if (symbol != null)
switch (syntaxNode)
{
foreach (var attributeData in symbol.GetAttributes())
case CompilationUnitSyntax syntax:
return compilation.Assembly.GetAttributes().Where(x => x.ApplicationSyntaxReference.SyntaxTree == syntax.SyntaxTree).ToImmutableArray();
default:
return document.GetDeclaredSymbol(syntaxNode)?.GetAttributes() ?? ImmutableArray<AttributeData>.Empty;
}
}

private static IEnumerable<ICodeGenerator> FindCodeGenerators(ImmutableArray<AttributeData> nodeAttributes, Func<AssemblyName, Assembly> assemblyLoader)
{
foreach (var attributeData in nodeAttributes)
{
Type generatorType = GetCodeGeneratorTypeForAttribute(attributeData.AttributeClass, assemblyLoader);
if (generatorType != null)
{
Type generatorType = GetCodeGeneratorTypeForAttribute(attributeData.AttributeClass, assemblyLoader);
if (generatorType != null)
{
ICodeGenerator generator = (ICodeGenerator)Activator.CreateInstance(generatorType, attributeData);
yield return generator;
}
ICodeGenerator generator = (ICodeGenerator)Activator.CreateInstance(generatorType, attributeData);
yield return generator;
}
}
}
Expand Down
20 changes: 13 additions & 7 deletions src/CodeGeneration.Roslyn/TransformationContext.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace CodeGeneration.Roslyn
{
Expand All @@ -10,23 +8,31 @@ public class TransformationContext
/// <summary>
/// Initializes a new instance of the <see cref="TransformationContext" /> class.
/// </summary>
/// <param name="processingMember">The syntax node the generator attribute is found on.</param>
/// <param name="processingNode">The syntax node the generator attribute is found on.</param>
/// <param name="semanticModel">The semantic model.</param>
/// <param name="compilation">The overall compilation being generated for.</param>
public TransformationContext(MemberDeclarationSyntax processingMember, SemanticModel semanticModel, CSharpCompilation compilation)
/// <param name="projectDirectory">The absolute path of the directory where the project file is located</param>
public TransformationContext(CSharpSyntaxNode processingNode, SemanticModel semanticModel, CSharpCompilation compilation,
string projectDirectory)
{
ProcessingMember = processingMember;
ProcessingNode = processingNode;
SemanticModel = semanticModel;
Compilation = compilation;
ProjectDirectory = projectDirectory;
}

/// <summary>Gets the syntax node the generator attribute is found on.</summary>
public MemberDeclarationSyntax ProcessingMember { get; }
public CSharpSyntaxNode ProcessingNode { get; }

/// <summary>Gets the semantic model for the <see cref="Compilation" />.</summary>
public SemanticModel SemanticModel { get; }

/// <summary>Gets the overall compilation being generated for.</summary>
public CSharpCompilation Compilation { get; }

/// <summary>
/// Gets the absolute path of the directory where the project file is located
/// </summary>
public string ProjectDirectory { get; }
}
}

0 comments on commit 078476a

Please sign in to comment.