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

Commit e19b09a

Browse files
committed
Replace AdhocWorkspace with CSharpCompilation
This is a breaking change to our extensibility API, but it is necessary to completely eliminate the MEF cost that we incur for every project build. And should not adversely impact what code generators can achieve. Fixes #18
1 parent 8a9e892 commit e19b09a

File tree

6 files changed

+39
-65
lines changed

6 files changed

+39
-65
lines changed

src/CodeGeneration.Roslyn.Tasks.Helper/Helper.cs

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ namespace CodeGeneration.Roslyn.Tasks
2121
using Microsoft.CodeAnalysis.Text;
2222
using Task = System.Threading.Tasks.Task;
2323
using Validation;
24+
using Microsoft.CodeAnalysis.CSharp.Syntax;
2425

2526
public class Helper
2627
#if NET46
@@ -76,7 +77,7 @@ public void Execute()
7677

7778
Task.Run(async delegate
7879
{
79-
var project = this.CreateProject();
80+
var compilation = this.CreateCompilation();
8081
var outputFiles = new List<ITaskItem>();
8182
var writtenFiles = new List<ITaskItem>();
8283

@@ -91,35 +92,36 @@ where string.Equals(item.GetMetadata("Generator"), $"MSBuild:{this.TargetName}",
9192
select item.ItemSpec,
9293
StringComparer.OrdinalIgnoreCase);
9394

94-
foreach (var inputDocument in project.Documents)
95+
foreach (var inputSyntaxTree in compilation.SyntaxTrees)
9596
{
9697
this.CancellationToken.ThrowIfCancellationRequested();
9798

9899
// Skip over documents that aren't on the prescribed list of files to scan.
99-
if (!explicitIncludeList.Contains(inputDocument.Name))
100+
if (!explicitIncludeList.Contains(inputSyntaxTree.FilePath))
100101
{
101102
continue;
102103
}
103104

104-
string sourceHash = inputDocument.Name.GetHashCode().ToString("x", CultureInfo.InvariantCulture);
105-
string outputFilePath = Path.Combine(this.IntermediateOutputDirectory, Path.GetFileNameWithoutExtension(inputDocument.Name) + $".{sourceHash}.generated.cs");
105+
string sourceHash = inputSyntaxTree.FilePath.GetHashCode().ToString("x", CultureInfo.InvariantCulture);
106+
string outputFilePath = Path.Combine(this.IntermediateOutputDirectory, Path.GetFileNameWithoutExtension(inputSyntaxTree.FilePath) + $".{sourceHash}.generated.cs");
106107

107108
// Code generation is relatively fast, but it's not free.
108109
// And when we run the Simplifier.ReduceAsync it's dog slow.
109110
// So skip files that haven't changed since we last generated them.
110111
bool generated = false;
111112
DateTime outputLastModified = File.GetLastWriteTime(outputFilePath);
112-
if (File.GetLastWriteTime(inputDocument.Name) > outputLastModified || assembliesLastModified > outputLastModified)
113+
if (File.GetLastWriteTime(inputSyntaxTree.FilePath) > outputLastModified || assembliesLastModified > outputLastModified)
113114
{
114-
var outputDocument = await DocumentTransform.TransformAsync(
115-
inputDocument,
116-
new ProgressLogger(this.Log, inputDocument.Name));
117-
118-
// Only produce a new file if the generated document is not empty.
119-
var semanticModel = await outputDocument.GetSemanticModelAsync(this.CancellationToken);
120-
if (!CSharpDeclarationComputer.GetDeclarationsInSpan(semanticModel, TextSpan.FromBounds(0, semanticModel.SyntaxTree.Length), false, this.CancellationToken).IsEmpty)
115+
var generatedSyntaxTree = await DocumentTransform.TransformAsync(
116+
compilation,
117+
inputSyntaxTree,
118+
new ProgressLogger(this.Log, inputSyntaxTree.FilePath));
119+
120+
// Only produce a new file if the generated document has generated a type.
121+
bool anyMembersGenerated = generatedSyntaxTree?.GetRoot(this.CancellationToken).DescendantNodes().OfType<TypeDeclarationSyntax>().Any() ?? false;
122+
if (anyMembersGenerated)
121123
{
122-
var outputText = await outputDocument.GetTextAsync(this.CancellationToken);
124+
var outputText = generatedSyntaxTree.GetText(this.CancellationToken);
123125
using (var outputFileStream = File.OpenWrite(outputFilePath))
124126
using (var outputWriter = new StreamWriter(outputFileStream))
125127
{
@@ -130,12 +132,12 @@ where string.Equals(item.GetMetadata("Generator"), $"MSBuild:{this.TargetName}",
130132
outputFileStream.SetLength(outputFileStream.Position);
131133
}
132134

133-
this.Log.LogMessage(MessageImportance.Normal, "{0} -> {1}", inputDocument.Name, outputFilePath);
135+
this.Log.LogMessage(MessageImportance.Normal, "{0} -> {1}", inputSyntaxTree.FilePath, outputFilePath);
134136
generated = true;
135137
}
136138
else
137139
{
138-
this.Log.LogMessage(MessageImportance.Low, "{0} used no code generation attributes.", inputDocument.Name);
140+
this.Log.LogMessage(MessageImportance.Low, "{0} used no code generation attributes.", inputSyntaxTree.FilePath);
139141
}
140142
}
141143
else
@@ -238,24 +240,22 @@ private Assembly TryLoadAssembly(AssemblyName assemblyName)
238240
return null;
239241
}
240242

241-
private Project CreateProject()
243+
private CSharpCompilation CreateCompilation()
242244
{
243-
var workspace = new AdhocWorkspace();
244-
var project = workspace.CurrentSolution.AddProject("codegen", "codegen", "C#")
245-
.WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
246-
.WithMetadataReferences(this.ReferencePath.Select(p => MetadataReference.CreateFromFile(p.ItemSpec)));
247-
245+
var compilation = CSharpCompilation.Create("codegen")
246+
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
247+
.WithReferences(this.ReferencePath.Select(p => MetadataReference.CreateFromFile(p.ItemSpec)));
248248
foreach (var sourceFile in this.Compile)
249249
{
250-
using (var stream = File.OpenRead(sourceFile.ItemSpec))
250+
using (var stream = File.OpenRead(sourceFile.GetMetadata("FullPath")))
251251
{
252252
this.CancellationToken.ThrowIfCancellationRequested();
253253
var text = SourceText.From(stream);
254-
project = project.AddDocument(sourceFile.ItemSpec, text).Project;
254+
compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(text, path: sourceFile.ItemSpec, cancellationToken: this.CancellationToken));
255255
}
256256
}
257257

258-
return project;
258+
return compilation;
259259
}
260260
}
261261

src/CodeGeneration.Roslyn.Tests.Generators/DuplicateWithSuffixGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public DuplicateWithSuffixGenerator(AttributeData attributeData)
3030
this.data = this.attributeData.NamedArguments.ToImmutableDictionary(kv => kv.Key, kv => kv.Value);
3131
}
3232

33-
public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(MemberDeclarationSyntax applyTo, Document document, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
33+
public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(MemberDeclarationSyntax applyTo, CSharpCompilation compilation, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
3434
{
3535
var results = SyntaxFactory.List<MemberDeclarationSyntax>();
3636

src/CodeGeneration.Roslyn/CodeGeneration.Roslyn.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
</ItemGroup>
1616

1717
<ItemGroup>
18-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="1.3.2" />
18+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="1.3.2" />
1919
<PackageReference Include="Validation" Version="2.4.13" />
2020
</ItemGroup>
2121

src/CodeGeneration.Roslyn/DocumentTransform.cs

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ namespace CodeGeneration.Roslyn
1515
using Microsoft.CodeAnalysis;
1616
using Microsoft.CodeAnalysis.CSharp;
1717
using Microsoft.CodeAnalysis.CSharp.Syntax;
18-
using Microsoft.CodeAnalysis.Formatting;
19-
using Microsoft.CodeAnalysis.Simplification;
2018
using Validation;
2119

2220
/// <summary>
@@ -27,7 +25,7 @@ public class DocumentTransform
2725
private static readonly string GeneratedByAToolPreamble = @"// ------------------------------------------------------------------------------
2826
// <auto-generated>
2927
// This code was generated by a tool.
30-
//
28+
//
3129
// Changes to this file may cause incorrect behavior and will be lost if
3230
// the code is regenerated.
3331
// </auto-generated>
@@ -41,19 +39,16 @@ private DocumentTransform()
4139
/// <summary>
4240
/// Produces a new document in response to any code generation attributes found in the specified document.
4341
/// </summary>
42+
/// <param name="compilation">The compilation to which the document belongs.</param>
4443
/// <param name="inputDocument">The document to scan for generator attributes.</param>
4544
/// <param name="progress">Reports warnings and errors in code generation.</param>
46-
/// <param name="simplify">
47-
/// A value indicating whether to simplify the generated document.
48-
/// This produces a more visually appealing code file, but costs significant time during code generation.
49-
/// </param>
5045
/// <returns>A task whose result is the generated document.</returns>
51-
public static async Task<Document> TransformAsync(Document inputDocument, IProgress<Diagnostic> progress, bool simplify = false)
46+
public static async Task<SyntaxTree> TransformAsync(CSharpCompilation compilation, SyntaxTree inputDocument, IProgress<Diagnostic> progress)
5247
{
48+
Requires.NotNull(compilation, nameof(compilation));
5349
Requires.NotNull(inputDocument, nameof(inputDocument));
5450

55-
var workspace = inputDocument.Project.Solution.Workspace;
56-
var inputSemanticModel = await inputDocument.GetSemanticModelAsync();
51+
var inputSemanticModel = compilation.GetSemanticModel(inputDocument);
5752
var inputSyntaxTree = inputSemanticModel.SyntaxTree;
5853

5954
var inputFileLevelUsingDirectives = inputSyntaxTree.GetRoot().ChildNodes().OfType<UsingDirectiveSyntax>();
@@ -67,7 +62,7 @@ public static async Task<Document> TransformAsync(Document inputDocument, IProgr
6762
var generators = FindCodeGenerators(inputSemanticModel, memberNode);
6863
foreach (var generator in generators)
6964
{
70-
var generatedTypes = await generator.GenerateAsync(memberNode, inputDocument, progress, CancellationToken.None);
65+
var generatedTypes = await generator.GenerateAsync(memberNode, compilation, progress, CancellationToken.None);
7166

7267
// Figure out ancestry for the generated type, including nesting types and namespaces.
7368
foreach (var ancestor in memberNode.Ancestors())
@@ -108,36 +103,14 @@ public static async Task<Document> TransformAsync(Document inputDocument, IProgr
108103
// By default, retain all the using directives that came from the input file.
109104
var resultFileLevelUsingDirectives = SyntaxFactory.List(inputFileLevelUsingDirectives);
110105

111-
// Add a using directive for the ImmutableObjectGraph if there isn't one.
112-
////var immutableObjectGraphName = nameof(ImmutableObjectGraph);
113-
////if (!resultFileLevelUsingDirectives.Any(u => u.Name.ToString() == immutableObjectGraphName))
114-
////{
115-
//// resultFileLevelUsingDirectives = resultFileLevelUsingDirectives.Add(
116-
//// SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName(immutableObjectGraphName)));
117-
////}
118-
119-
var emittedTree = SyntaxFactory.CompilationUnit()
106+
var compilationUnit = SyntaxFactory.CompilationUnit()
120107
.WithUsings(resultFileLevelUsingDirectives)
121108
.WithMembers(emittedMembers)
122109
.WithLeadingTrivia(SyntaxFactory.Comment(GeneratedByAToolPreamble))
123110
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)
124111
.NormalizeWhitespace();
125112

126-
// Format the tree to get reasonably good whitespace.
127-
var formattedTree = Formatter.Format(emittedTree, workspace, workspace.Options);
128-
129-
// Reduce the document to get rid of unnecessary fully-qualified type names that just hurt readability.
130-
var formattedText = formattedTree.GetText();
131-
var document = inputDocument.Project.AddDocument("generated.cs", formattedText);
132-
if (simplify)
133-
{
134-
var annotatedDocument = document
135-
.WithSyntaxRoot((await document.GetSyntaxRootAsync())
136-
.WithAdditionalAnnotations(Simplifier.Annotation)); // allow simplification of the entire document
137-
document = await Simplifier.ReduceAsync(annotatedDocument);
138-
}
139-
140-
return document;
113+
return compilationUnit.SyntaxTree;
141114
}
142115

143116
private static IEnumerable<ICodeGenerator> FindCodeGenerators(SemanticModel document, SyntaxNode nodeWithAttributesApplied)

src/CodeGeneration.Roslyn/ICodeGenerator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace CodeGeneration.Roslyn
99
using System.Threading.Tasks;
1010
using Microsoft.CodeAnalysis;
1111
using Microsoft.CodeAnalysis.CSharp.Syntax;
12+
using Microsoft.CodeAnalysis.CSharp;
1213

1314
/// <summary>
1415
/// Describes a code generator that responds to attributes on members to generate code.
@@ -19,10 +20,10 @@ public interface ICodeGenerator
1920
/// Create the syntax tree representing the expansion of some member to which this attribute is applied.
2021
/// </summary>
2122
/// <param name="applyTo">The syntax node this attribute is found on.</param>
22-
/// <param name="document">The document with the semantic model in which this attribute was found.</param>
23+
/// <param name="compilation">The overall compilation being generated for.</param>
2324
/// <param name="progress">A way to report diagnostic messages.</param>
2425
/// <param name="cancellationToken">A cancellation token.</param>
2526
/// <returns>The generated member syntax to be added to the project.</returns>
26-
Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(MemberDeclarationSyntax applyTo, Document document, IProgress<Diagnostic> progress, CancellationToken cancellationToken);
27+
Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(MemberDeclarationSyntax applyTo, CSharpCompilation compilation, IProgress<Diagnostic> progress, CancellationToken cancellationToken);
2728
}
2829
}

src/version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
3-
"version": "0.2",
3+
"version": "0.3",
44
"publicReleaseRefSpec": [
55
"^refs/heads/master$", // we release out of master
66
"^refs/tags/v\\d\\.\\d" // we also release tags starting with vN.N

0 commit comments

Comments
 (0)