Skip to content

Commit

Permalink
Refine the generator (#6)
Browse files Browse the repository at this point in the history
Update the generator to use attribute fullName match
Update the generator to get typeName to be used for type query
  • Loading branch information
WeihanLi authored Oct 13, 2021
1 parent 458316b commit 713c482
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

// TODO: Get this working...

using Microsoft.JSInterop.Attributes;

namespace Microsoft.JSInterop.Extensions;

[JavaScriptInterop(TypeName = "IdleDeadline", Url = "https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API")]
public static partial class BackgroundTasksExtensions
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Blazor.SourceGenerators\Blazor.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Blazor.SourceGenerators\Blazor.SourceGenerators.csproj" OutputItemType="Analyzer" />
</ItemGroup>

</Project>
22 changes: 9 additions & 13 deletions src/Blazor.JavaScriptInterop.Extensions/GeolocationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,12 @@

// TODO: Get this working...

//using Microsoft.JSInterop.Attributes;
//
//namespace Microsoft.JSInterop.Extensions;
//
//[
// JavaScriptInterop(
// "Geolocation",
// Url = "https://developer.mozilla.org/en-US/docs/Web/API/Geolocation")
//]
//public static partial class GeolocationExtensions
//{
//}
//
using Microsoft.JSInterop.Attributes;

namespace Microsoft.JSInterop.Extensions;

[JavaScriptInterop(
Url = "https://developer.mozilla.org/en-US/docs/Web/API/Geolocation")]
public static partial class GeolocationExtensions
{
}
14 changes: 14 additions & 0 deletions src/Blazor.SourceGenerators/JavaScriptInteropAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Microsoft.JSInterop.Attributes;

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class JavaScriptInteropAttribute : Attribute
{
public string? TypeName { get; set; }

public string? Url { get; set; }
}
132 changes: 93 additions & 39 deletions src/Blazor.SourceGenerators/JavaScriptInteropGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand All @@ -11,6 +14,8 @@ namespace Blazor.SourceGenerators
[Generator]
public class JavaScriptInteropGenerator : ISourceGenerator
{
private const string JavaScriptInteropAttributeFullName = "Microsoft.JSInterop.Attributes.JavaScriptInteropAttribute";

private const string attributeText = @"
using System;
Expand All @@ -21,71 +26,120 @@ namespace Microsoft.JSInterop.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class JavaScriptInteropAttribute : Attribute
{
public string TypeName { get; }
public string? Url { get; set; } = default!;
public string? TypeName { get; set; }
public JavaScriptInteropAttribute(string typeName)
{
ArgumentNullException.ThrowIfNull(nameof(typeName));
TypeName = typeName;
}
public string? Url { get; set; }
}
";

public void Initialize(GeneratorInitializationContext context)
{
#if DEBUG
// For debugging
if (!Debugger.IsAttached) Debugger.Launch();
#endif

// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
context.RegisterForSyntaxNotifications(SyntaxContextReceiver.Create);
}

public void Execute(GeneratorExecutionContext context)
{
// Add the attribute text
context.AddSource("JavaScriptInteropAttribute", SourceText.From(attributeText, Encoding.UTF8));
// context.AddSource("JavaScriptInteropAttribute", SourceText.From(attributeText, Encoding.UTF8));

if (context.SyntaxReceiver is not SyntaxReceiver receiver)
if (context.SyntaxContextReceiver is not SyntaxContextReceiver receiver
|| receiver.ClassDeclarations.Count == 0)
return;

_ = receiver;
foreach (var classDeclaration in receiver.ClassDeclarations)
{
// TODO:

// 1. Parse corresponding type:
// a. Class name, less the "Extensions" suffix.
// - or -
// b. The TypeName as defined within the JavaScriptInterop itself.
var model = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree);
var symbol = model.GetDeclaredSymbol(classDeclaration);
if (!(symbol is ITypeSymbol typeSymbol
&& typeSymbol.IsStatic))
{
continue;
}
var assemblyName = GetType().Assembly.GetName().Name;
var attributes = typeSymbol.GetAttributes();
var attribute = attributes.First(c => c.AttributeClass.ContainingAssembly.Name == assemblyName
&& c.AttributeClass.ToDisplayString() == JavaScriptInteropAttributeFullName);
var attributeTypeName = attribute.NamedArguments.FirstOrDefault(a => a.Key == "TypeName").Value.Value?.ToString();
var classTypeName = typeSymbol.Name;

// The final type name to be used
var typeName = string.IsNullOrEmpty(attributeTypeName) ? classTypeName : attributeTypeName;
if (typeName.EndsWith("Extensions"))
{
typeName = typeName[..^"Extensions".Length];
}

// 2. Ask cache for API descriptors
// a. If not found, request raw from values from
// https://github.com/microsoft/TypeScript-DOM-lib-generator/tree/main/inputfiles
// and populate cache.
// - or -
// b. If found, return it.

// TODO:
// 3. Source generate records, classes, structs, and interfaces that define the object surface area.
// 4. Source generate the extension methods.

// 1. Parse corresponding type:
// a. Class name, less the "Extensions" suffix.
// - or -
// b. The TypeName as defined within the JavaScriptInterop itself.
// maybe consider for a generate template
var generatedExtensions = new StringBuilder();
generatedExtensions.Append($@"// This file is generated
using Microsoft.JSInterop;
// 2. Ask cache for API descriptors
// a. If not found, request raw from values from
// https://github.com/microsoft/TypeScript-DOM-lib-generator/tree/main/inputfiles
// and populate cache.
// - or -
// b. If found, return it.
namespace {typeSymbol.ContainingNamespace.ToDisplayString()};
// 3. Source generate records, classes, structs, and interfaces that define the object surface area.
// 4. Source generate the extension methods.
// 5. Source generate the JavaScript, if necessary.
public static partial class {typeSymbol.Name}
{{
private static void TestMethod(this IJSRuntime jsRuntime){{}}
}}
");
var generatedText = generatedExtensions.ToString();
context.AddSource($"{typeSymbol.Name}.generated.cs", generatedText);

// 5. Source generate the JavaScript, if necessary.
}
}

internal sealed class SyntaxReceiver : ISyntaxReceiver
private sealed class SyntaxContextReceiver : ISyntaxContextReceiver
{
internal HashSet<TypeDeclarationSyntax> TypeDeclarationSyntaxSet { get; } = new();
internal static ISyntaxContextReceiver Create()
{
return new SyntaxContextReceiver();
}

public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
public HashSet<ClassDeclarationSyntax> ClassDeclarations { get; } = new();

public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax)
if(context.Node is ClassDeclarationSyntax classDeclaration
&& classDeclaration.AttributeLists.Count > 0)
{
var attribute =
typeDeclarationSyntax.AttributeLists.SelectMany(
list => list.Attributes.Where(
attr => attr.Name.ToString().Contains("JavaScriptInterop")))
.FirstOrDefault();

if (attribute is not null)
foreach (var attributeListSyntax in classDeclaration.AttributeLists)
{
TypeDeclarationSyntaxSet.Add(typeDeclarationSyntax);
foreach (var attributeSyntax in attributeListSyntax.Attributes)
{
var symbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol;
if (symbol is not IMethodSymbol attributeSymbol)
{
continue;
}
var attributeContainingTypeSymbol = attributeSymbol.ContainingType;
var fullName = attributeContainingTypeSymbol.ToDisplayString();
if (fullName == JavaScriptInteropAttributeFullName)
{
ClassDeclarations.Add(classDeclaration);
}
}
}
}
}
Expand Down

0 comments on commit 713c482

Please sign in to comment.