Skip to content

Commit ce58fc8

Browse files
Add csproj file support to Documentation Source Generator
This will allow us to pass them into the generator and get additional info we can use in the Sample App for pointing folks towards Source code and Packages (for now).
1 parent de5e0fb commit ce58fc8

File tree

6 files changed

+92
-18
lines changed

6 files changed

+92
-18
lines changed

CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,18 @@ internal static GeneratorDriver WithMarkdown(this GeneratorDriver driver, params
5757

5858
return driver;
5959
}
60+
61+
internal static GeneratorDriver WithCsproj(this GeneratorDriver driver, params string[] csprojFilesToCreate)
62+
{
63+
foreach (var proj in csprojFilesToCreate)
64+
{
65+
if (!string.IsNullOrWhiteSpace(proj))
66+
{
67+
var text = new InMemoryAdditionalText(@"C:\pathtorepo\components\experiment\src\componentname.csproj", proj);
68+
driver = driver.AddAdditionalTexts(ImmutableArray.Create<AdditionalText>(text));
69+
}
70+
}
71+
72+
return driver;
73+
}
6074
}

CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ internal static SourceGeneratorRunResult RunSourceGenerator<TGenerator>(this Syn
1919
return RunSourceGenerator<TGenerator>(compilation, markdown);
2020
}
2121

22-
internal static SourceGeneratorRunResult RunSourceGenerator<TGenerator>(this Compilation compilation, string markdown = "")
22+
internal static SourceGeneratorRunResult RunSourceGenerator<TGenerator>(this Compilation compilation, string markdown = "", string csproj = "")
2323
where TGenerator : class, IIncrementalGenerator, new()
2424
{
2525
// Create a driver for the source generator
2626
var driver = compilation
2727
.CreateSourceGeneratorDriver(new TGenerator())
28-
.WithMarkdown(markdown);
28+
.WithMarkdown(markdown)
29+
.WithCsproj(csproj);
2930

3031
// Update the original compilation using the source generator
3132
_ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation generatorCompilation, out ImmutableArray<Diagnostic> postGeneratorCompilationDiagnostics);

CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,14 @@ public void DocumentationValidWithRegistry()
270270
Which is valid.
271271
> [!SAMPLE Sample]";
272272

273+
string csproj = """
274+
<Project Sdk="MSBuild.Sdk.Extras/3.0.23">
275+
<PropertyGroup>
276+
<ToolkitComponentName>Primitives</ToolkitComponentName>
277+
</PropertyGroup>
278+
</Project>
279+
""";
280+
273281
var sampleProjectAssembly = SimpleSource.ToSyntaxTree()
274282
.CreateCompilation("MyApp.Samples")
275283
.ToMetadataReference();
@@ -279,7 +287,7 @@ Which is valid.
279287
.CreateCompilation("MyApp.Head")
280288
.AddReferences(sampleProjectAssembly);
281289

282-
var result = headCompilation.RunSourceGenerator<ToolkitSampleMetadataGenerator>(markdown);
290+
var result = headCompilation.RunSourceGenerator<ToolkitSampleMetadataGenerator>(markdown, csproj);
283291

284292
result.AssertNoCompilationErrors();
285293

@@ -291,7 +299,7 @@ public static class ToolkitDocumentRegistry
291299
{
292300
public static System.Collections.Generic.IEnumerable<CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter> Execute()
293301
{
294-
yield return new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter() { Title = "Canvas Layout", Author = "mhawker", Description = "A canvas-like VirtualizingLayout for use in an ItemsRepeater", Keywords = "CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange", Category = ToolkitSampleCategory.Controls, Subcategory = ToolkitSampleSubcategory.Layout, DiscussionId = 0, IssueId = 0, Icon = @"experiment/samples/assets/icon.png", FilePath = @"experiment\samples\documentation.md", SampleIdReferences = new string[] { "Sample" }, IsExperimental = true };
302+
yield return new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter() { ComponentName = "Primitives", Title = "Canvas Layout", Author = "mhawker", Description = "A canvas-like VirtualizingLayout for use in an ItemsRepeater", Keywords = "CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange", Category = ToolkitSampleCategory.Controls, Subcategory = ToolkitSampleSubcategory.Layout, DiscussionId = 0, IssueId = 0, Icon = @"experiment/samples/assets/icon.png", FilePath = @"experiment\samples\documentation.md", SampleIdReferences = new string[] { "Sample" }, IsExperimental = true, CsProjName = @"componentname.csproj" };
295303
}
296304
}
297305
""", "Unexpected code generated");

CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitFrontMatter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ public sealed class ToolkitFrontMatter : DocsFrontMatter
2525
public string? FilePath { get; set; }
2626
public string[]? SampleIdReferences { get; set; }
2727
public string? Icon { get; set; }
28+
public string? ComponentName { get; set; }
29+
public string? CsProjName { get; set; }
2830
}

CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public partial class ToolkitSampleMetadataGenerator
4949
private const string FrontMatterRegexIsExperimentalExpression = @"^experimental:\s*(?<experimental>.*)$";
5050
private static readonly Regex FrontMatterRegexIsExperimental = new Regex(FrontMatterRegexIsExperimentalExpression, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
5151

52+
private const string CsProjRegexComponentNameExpression = @"^\s*<ToolkitComponentName>(?<ToolkitComponentName>.*)<\/ToolkitComponentName>\s*$";
53+
private static readonly Regex CsProjRegexComponentName = new Regex(CsProjRegexComponentNameExpression, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
54+
5255
private static void ReportDocumentDiagnostics(SourceProductionContext ctx, Dictionary<string, ToolkitSampleRecord> sampleMetadata, IEnumerable<AdditionalText> markdownFileData, IEnumerable<(ToolkitSampleAttribute Attribute, string AttachedQualifiedTypeName, ISymbol Symbol)> toolkitSampleAttributeData, ImmutableArray<ToolkitFrontMatter> docFrontMatter)
5356
{
5457
// Keep track of all sample ids and remove them as we reference them so we know if we have any unreferenced samples.
@@ -90,14 +93,16 @@ private static void ReportDocumentDiagnostics(SourceProductionContext ctx, Dicti
9093
}
9194
}
9295

93-
private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProductionContext ctx, IEnumerable<AdditionalText> data)
96+
private static ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProductionContext ctx, IEnumerable<(AdditionalText Document, AdditionalText? CsProj)> data)
9497
{
95-
return data.Select(file =>
98+
return data.Select(info =>
9699
{
100+
var file = info.Document;
101+
97102
// We have to manually parse the YAML here for now because of
98103
// https://github.com/dotnet/roslyn/issues/43903
99104

100-
var content = file.GetText()!.ToString();
105+
var content = info.Document.GetText()!.ToString();
101106
var matter = content.Split(new[] { "---" }, StringSplitOptions.RemoveEmptyEntries);
102107

103108
if (matter.Length <= 1)
@@ -221,6 +226,28 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu
221226
return null;
222227
}
223228

229+
string? componentName = null;
230+
string? csprojName = null;
231+
232+
// Get component name from csproj file (if we have one) and its filename, otherwise, use path data of doc file
233+
if (info.CsProj != null)
234+
{
235+
var text = info.CsProj.GetText()!.ToString();
236+
237+
var match = CsProjRegexComponentName.Match(text);
238+
239+
if (match.Success)
240+
{
241+
componentName = match.Groups["ToolkitComponentName"].Value.Trim();
242+
}
243+
244+
csprojName = info.CsProj.Path.Split(new char[] { '/', '\\' }).LastOrDefault();
245+
}
246+
else
247+
{
248+
componentName = filepath.Split(new char[] { '/', '\\' }).FirstOrDefault();
249+
}
250+
224251
// Finally, construct the complete object.
225252
return new ToolkitFrontMatter()
226253
{
@@ -236,12 +263,14 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu
236263
IssueId = issueId,
237264
Icon = iconpath,
238265
IsExperimental = isExperimental,
266+
ComponentName = componentName,
267+
CsProjName = csprojName,
239268
};
240269
}
241270
}).OfType<ToolkitFrontMatter>().ToImmutableArray();
242271
}
243272

244-
private string? ParseYamlField(ref SourceProductionContext ctx, string filepath, ref string content, Regex pattern, string captureGroupName, bool optional = false)
273+
private static string? ParseYamlField(ref SourceProductionContext ctx, string filepath, ref string content, Regex pattern, string captureGroupName, bool optional = false)
245274
{
246275
var match = pattern.Match(content);
247276

@@ -263,7 +292,7 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu
263292
return match.Groups[captureGroupName].Value.Trim();
264293
}
265294

266-
private void CreateDocumentRegistry(SourceProductionContext ctx, ImmutableArray<ToolkitFrontMatter> matter)
295+
private static void CreateDocumentRegistry(SourceProductionContext ctx, ImmutableArray<ToolkitFrontMatter> matter)
267296
{
268297
// TODO: Emit a better error that no documentation is here?
269298
if (matter.Length == 0)
@@ -292,6 +321,6 @@ private static string FrontMatterToRegistryCall(ToolkitFrontMatter metadata)
292321
var categoryParam = $"{nameof(ToolkitSampleCategory)}.{metadata.Category}";
293322
var subcategoryParam = $"{nameof(ToolkitSampleSubcategory)}.{metadata.Subcategory}";
294323

295-
return @$"yield return new {typeof(ToolkitFrontMatter).FullName}() {{ Title = ""{metadata.Title}"", Author = ""{metadata.Author}"", Description = ""{metadata.Description}"", Keywords = ""{metadata.Keywords}"", Category = {categoryParam}, Subcategory = {subcategoryParam}, DiscussionId = {metadata.DiscussionId}, IssueId = {metadata.IssueId}, Icon = @""{metadata.Icon}"", FilePath = @""{metadata.FilePath}"", SampleIdReferences = new string[] {{ ""{string.Join("\",\"", metadata.SampleIdReferences)}"" }}, IsExperimental = {metadata.IsExperimental.ToString().ToLowerInvariant()} }};"; // TODO: Add list of sample ids in document
324+
return @$"yield return new {typeof(ToolkitFrontMatter).FullName}() {{ ComponentName = ""{metadata.ComponentName}"", Title = ""{metadata.Title}"", Author = ""{metadata.Author}"", Description = ""{metadata.Description}"", Keywords = ""{metadata.Keywords}"", Category = {categoryParam}, Subcategory = {subcategoryParam}, DiscussionId = {metadata.DiscussionId}, IssueId = {metadata.IssueId}, Icon = @""{metadata.Icon}"", FilePath = @""{metadata.FilePath}"", SampleIdReferences = new string[] {{ ""{string.Join("\",\"", metadata.SampleIdReferences)}"" }}, IsExperimental = {metadata.IsExperimental.ToString().ToLowerInvariant()}, CsProjName = @""{metadata.CsProjName}"" }};"; // TODO: Add list of sample ids in document
296325
}
297326
}

CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.CodeAnalysis;
99
using Microsoft.CodeAnalysis.CSharp;
1010
using Microsoft.CodeAnalysis.CSharp.Syntax;
11+
using System.Linq;
1112

1213
namespace CommunityToolkit.Tooling.SampleGen;
1314

@@ -35,6 +36,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3536
.Where(static file => file.Path.EndsWith(".md"))
3637
.Collect();
3738

39+
var csprojFiles = context.AdditionalTextsProvider
40+
.Where(static file => file.Path.EndsWith(".csproj"))
41+
.Collect();
42+
3843
var assemblyName = context.CompilationProvider.Select((x, _) => x.Assembly.Name);
3944

4045
// Only generate diagnostics (sample projects)
@@ -106,15 +111,30 @@ void Execute(IncrementalValuesProvider<ISymbol> types, bool skipDiagnostics = fa
106111
.Combine(toolkitSampleAttributeData)
107112
.Combine(generatedPaneOptions)
108113
.Combine(markdownFiles)
114+
.Combine(csprojFiles)
109115
.Combine(assemblyName);
110116

117+
// TODO: We can make this static if we could pass in our two boolean values as context, no idea how to do that...
111118
context.RegisterSourceOutput(all, (ctx, data) =>
112119
{
113-
var toolkitSampleAttributeData = data.Left.Left.Left.Right.Where(x => x != default).Distinct();
114-
var optionsPaneAttribute = data.Left.Left.Left.Left.Where(x => x != default).Distinct();
115-
var generatedOptionPropertyData = data.Left.Left.Right.Where(x => x.Attribute is not null && x.Symbol is not null);
116-
var markdownFileData = data.Left.Right.Where(x => x != default).Distinct();
117-
var currentAssembly = data.Right;
120+
var (((((optionsPaneAttributes, toolkitSampleAttributes), generatedPaneOptions), markdownFiles), csprojFiles), currentAssembly) = data;
121+
122+
var toolkitSampleAttributeData = toolkitSampleAttributes.Where(x => x != default).Distinct();
123+
var optionsPaneAttributeData = optionsPaneAttributes.Where(x => x != default).Distinct();
124+
var generatedOptionPropertyData = generatedPaneOptions.Where(x => x.Attribute is not null && x.Symbol is not null);
125+
126+
var markdownFileData = markdownFiles.Where(x => x != default).Distinct();
127+
var csprojFileData = csprojFiles.Where(x => x != default).Distinct();
128+
129+
var markdownProjPairings = markdownFileData.Select<AdditionalText, (AdditionalText Document, AdditionalText? CsProj)>((docFile, _) =>
130+
{
131+
// TODO: We use these splits a lot to extra path info, so we should probably make a helper function?
132+
var rootPathFile = docFile.Path.Split(new string[] { @"\components\", "/components/", @"\tooling\", "/tooling/" }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()?.Split(new char[] { '/', '\\' }).FirstOrDefault();
133+
134+
var csproj = csprojFileData.FirstOrDefault(csProjFile => csProjFile.Path.Split(new string[] { @"\components\", "/components/", @"\tooling\", "/tooling/" }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()?.Split(new char[] { '/', '\\' }).FirstOrDefault() == rootPathFile);
135+
136+
return (docFile, csproj);
137+
});
118138

119139
var isExecutingInSampleProject = currentAssembly?.EndsWith(".Samples") ?? false;
120140

@@ -129,16 +149,16 @@ void Execute(IncrementalValuesProvider<ISymbol> types, bool skipDiagnostics = fa
129149
sample.Attribute.DisplayName,
130150
sample.Attribute.Description,
131151
sample.AttachedQualifiedTypeName,
132-
optionsPaneAttribute.FirstOrDefault(x => x.Item1?.SampleId == sample.Attribute.Id).Item2?.ToString(),
152+
optionsPaneAttributeData.FirstOrDefault(x => x.Item1?.SampleId == sample.Attribute.Id).Item2?.ToString(),
133153
generatedOptionPropertyData.Where(x => x.Symbol.Equals(sample.Symbol, SymbolEqualityComparer.Default)).Select(x => x.Item2)
134154
)
135155
);
136156

137-
var docFrontMatter = GatherDocumentFrontMatter(ctx, markdownFileData);
157+
var docFrontMatter = GatherDocumentFrontMatter(ctx, markdownProjPairings);
138158

139159
if (isExecutingInSampleProject && !skipDiagnostics)
140160
{
141-
ReportSampleDiagnostics(ctx, toolkitSampleAttributeData, optionsPaneAttribute, generatedOptionPropertyData);
161+
ReportSampleDiagnostics(ctx, toolkitSampleAttributeData, optionsPaneAttributeData, generatedOptionPropertyData);
142162
ReportDocumentDiagnostics(ctx, sampleMetadata, markdownFileData, toolkitSampleAttributeData, docFrontMatter);
143163
}
144164

0 commit comments

Comments
 (0)