Skip to content

Commit

Permalink
🎅 Fix the Incremental behavior of our Source Generator (#815)
Browse files Browse the repository at this point in the history
* Fix that puppy, tomorrow I make the code looks nicer

* 🎅 Fixed the SG and everyone should be happy again

* fix typo

Co-authored-by: Vladislav Antonyuk <33021114+VladislavAntonyuk@users.noreply.github.com>

* Replace `record` `ValueTuple`, Remove `IsExternalInit`, `SourceStringExtensions` -> `SourceStringService`

* Support Nullable

* Add `static` modifier

* Re-add `record`

* Remove `internal`

* Remove `Debugger` logic

* Remove duplicate null check

* `dotnet format`

* Update TextColorToGenerator.cs

* Update DockLayoutTests.cs

Co-authored-by: Vladislav Antonyuk <33021114+VladislavAntonyuk@users.noreply.github.com>
Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 9, 2022
1 parent 0540501 commit 06c2b7c
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@

namespace CommunityToolkit.Maui.SourceGenerators.Generators;

[Generator(LanguageNames.CSharp)]
// IF you want to perform any change in the pipeline or in the generated code
// add this line right before the `#nullable enable` line
// // Final version: {DateTime.Now}
// Use this as a check, if the DateTime value changes when you change a code
// that has not to do with the generator (changing a code in another class, e.g.)
// then you broke the Incremental behavior of it and it need to be fixed before submit a PR

[Generator]
class TextColorToGenerator : IIncrementalGenerator
{
const string iTextStyleInterface = "Microsoft.Maui.ITextStyle";
Expand All @@ -21,91 +28,66 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// Get All Classes in User Library
var userGeneratedClassesProvider = context.SyntaxProvider.CreateSyntaxProvider(
static (syntaxNode, cancellationToken) => syntaxNode is ClassDeclarationSyntax { BaseList: not null },
static (context, cancellationToken) => (ClassDeclarationSyntax)context.Node);

// Get Microsoft.Maui.Controls Assembly Symbol
var mauiControlsAssemblySymbolProvider = context.CompilationProvider.Select(
static (compilation, token) => compilation.SourceModule.ReferencedAssemblySymbols.Single(q => q.Name == mauiControlsAssembly));

var inputs = userGeneratedClassesProvider.Collect()
.Combine(mauiControlsAssemblySymbolProvider)
.Select(static (combined, cancellationToken) => (UserGeneratedClassesProvider: combined.Left, MauiControlsAssemblySymbolProvider: combined.Right))
.Combine(context.CompilationProvider)
.Select(static (combined, cancellationToken) => (combined.Left.UserGeneratedClassesProvider, combined.Left.MauiControlsAssemblySymbolProvider, Compilation: combined.Right));
static (context, cancellationToken) =>
{
var compilation = context.SemanticModel.Compilation;

context.RegisterSourceOutput(inputs, (context, collectedValues) =>
Execute(context, collectedValues.Compilation, collectedValues.UserGeneratedClassesProvider, collectedValues.MauiControlsAssemblySymbolProvider));
}
var iTextStyleInterfaceSymbol = compilation.GetTypeByMetadataName(iTextStyleInterface);
var iAnimatableInterfaceSymbol = compilation.GetTypeByMetadataName(iAnimatableInterface);

static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray<ClassDeclarationSyntax> userGeneratedClassesProvider, IAssemblySymbol mauiControlsAssemblySymbolProvider)
{
var textStyleSymbol = compilation.GetTypeByMetadataName(iTextStyleInterface);
var iAnimatableSymbol = compilation.GetTypeByMetadataName(iAnimatableInterface);

if (textStyleSymbol is null || iAnimatableSymbol is null)
{
var diag = Diagnostic.Create(TextColorToDiagnostic.MauiReferenceIsMissing, Location.None);
context.ReportDiagnostic(diag);
return;
}
if (iTextStyleInterfaceSymbol is null || iAnimatableInterfaceSymbol is null)
{
throw new Exception("There's no .NET MAUI referenced in the project.");
}

var textStyleClassList = new List<(string ClassName, string ClassAcessModifier, string Namespace, string GenericArguments, string GenericConstraints)>();
var classSymbol = (INamedTypeSymbol?)context.SemanticModel.GetDeclaredSymbol(context.Node);

// Collect Microsoft.Maui.Controls that Implement ITextStyle
var mauiTextStyleImplementors = mauiControlsAssemblySymbolProvider.GlobalNamespace.GetNamedTypeSymbols().Where(x => x.AllInterfaces.Contains(textStyleSymbol, SymbolEqualityComparer.Default)
&& x.AllInterfaces.Contains(iAnimatableSymbol, SymbolEqualityComparer.Default));
// If the ClassDlecarationSyntax doesn't implements those interfaces we just return null
if (classSymbol is null
|| !(classSymbol.AllInterfaces.Contains(iAnimatableInterfaceSymbol, SymbolEqualityComparer.Default)
&& classSymbol.AllInterfaces.Contains(iTextStyleInterfaceSymbol, SymbolEqualityComparer.Default)))
{
return null;
}

foreach (var namedTypeSymbol in mauiTextStyleImplementors)
{
textStyleClassList.Add((namedTypeSymbol.Name, "internal", namedTypeSymbol.ContainingNamespace.ToDisplayString(), namedTypeSymbol.TypeArguments.GetGenericTypeArgumentsString(), namedTypeSymbol.GetGenericTypeConstraintsAsString()));
}
return classSymbol;
});

// Collect All Classes in User Library that Implement ITextStyle
foreach (var classDeclarationSyntax in userGeneratedClassesProvider)
{
var declarationSymbol = compilation.GetSymbol<INamedTypeSymbol>(classDeclarationSyntax);
if (declarationSymbol is null)
// Get Microsoft.Maui.Controls Symbols that implements the desired interfaces
var mauiControlsAssemblySymbolProvider = context.CompilationProvider.Select(
static (compilation, token) =>
{
var diag = Diagnostic.Create(TextColorToDiagnostic.InvalidClassDeclarationSyntax, Location.None, classDeclarationSyntax.Identifier.Text);
context.ReportDiagnostic(diag);
continue;
}

// If the control is inherit from a Maui control that implements ITextStyle
// We don't need to generate a extension method for it.
// We just generate a method if the Control is a new implementation of ITextStyle and IAnimatable
var doesContainSymbolBaseType = mauiTextStyleImplementors.ContainsSymbolBaseType(declarationSymbol);
var iTextStyleInterfaceSymbol = compilation.GetTypeByMetadataName(iTextStyleInterface);
var iAnimatableInterfaceSymbol = compilation.GetTypeByMetadataName(iAnimatableInterface);

if (!doesContainSymbolBaseType
&& declarationSymbol.AllInterfaces.Contains(textStyleSymbol, SymbolEqualityComparer.Default)
&& declarationSymbol.AllInterfaces.Contains(iAnimatableSymbol, SymbolEqualityComparer.Default))
{
if (declarationSymbol.ContainingNamespace.IsGlobalNamespace)
if (iTextStyleInterfaceSymbol is null || iAnimatableInterfaceSymbol is null)
{
var diag = Diagnostic.Create(TextColorToDiagnostic.GlobalNamespace, Location.None, declarationSymbol.Name);
context.ReportDiagnostic(diag);
continue;
throw new Exception("There's no .NET MAUI referenced in the project.");
}

var nameSpace = declarationSymbol.ContainingNamespace.ToDisplayString();
var mauiAssembly = compilation.SourceModule.ReferencedAssemblySymbols.Single(q => q.Name == mauiControlsAssembly);
var symbols = GetMauiInterfaceImplementors(mauiAssembly, iAnimatableInterfaceSymbol, iTextStyleInterfaceSymbol).Where(static x => x is not null);

var accessModifier = GetClassAccessModifier(declarationSymbol);
return symbols;
});

if (accessModifier == string.Empty)
{
var diag = Diagnostic.Create(TextColorToDiagnostic.InvalidModifierAccess, Location.None, declarationSymbol.Name);
context.ReportDiagnostic(diag);
continue;
}

textStyleClassList.Add((declarationSymbol.Name, accessModifier, nameSpace, declarationSymbol.TypeArguments.GetGenericTypeArgumentsString(), declarationSymbol.GetGenericTypeConstraintsAsString()));
}
}
// Here we Collect all the Classes candidates from the first pipeline
// Then we merge them with the Maui.Controls that implements the desired interfaces
// Then we make sure they are unique and the user control doesn't inherit from any Maui control that implements the desired interface already
// Then we transform the ISymbol to be a type that we can compare and preserve the Incremental behavior of this Source Generator
var inputs = userGeneratedClassesProvider.Collect()
.Combine(mauiControlsAssemblySymbolProvider)
.SelectMany(static (x, _) => Deduplicate(x.Left, x.Right).ToImmutableArray())
.Select(static (x, _) => GenerateMetadata(x));

var options = ((CSharpCompilation)compilation).SyntaxTrees[0].Options as CSharpParseOptions;
foreach (var textStyleClass in textStyleClassList)
{
var textColorToBuilder = @"
context.RegisterSourceOutput(inputs, Execution);
}

static void Execution(SourceProductionContext context, TextStyleClassMetadata textStyleClassMetadata)
{

var textColorToBuilder = $@"
// <auto-generated>
// See: CommunityToolkit.Maui.SourceGenerators.TextColorToGenerator
Expand All @@ -118,9 +100,9 @@ static void Execute(SourceProductionContext context, Compilation compilation, Im
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
namespace " + textStyleClass.Namespace + @";
namespace " + textStyleClassMetadata.Namespace + @";
" + textStyleClass.ClassAcessModifier + @" static partial class ColorAnimationExtensions_" + textStyleClass.ClassName + @"
" + textStyleClassMetadata.ClassAcessModifier + @" static partial class ColorAnimationExtensions_" + textStyleClassMetadata.ClassName + @"
{
/// <summary>
/// Animates the TextColor of an <see cref=""Microsoft.Maui.ITextStyle""/> to the given color
Expand All @@ -131,8 +113,8 @@ namespace " + textStyleClass.Namespace + @";
/// <param name=""length"">The duration, in milliseconds, of the animation</param>
/// <param name=""easing"">The easing function to be used in the animation</param>
/// <returns>Value indicating if the animation completed successfully or not</returns>
public static Task<bool> TextColorTo" + textStyleClass.GenericArguments + "(this " + textStyleClass.Namespace + "." + textStyleClass.ClassName + textStyleClass.GenericArguments + @" element, Color color, uint rate = 16u, uint length = 250u, Easing? easing = null)
" + textStyleClass.GenericConstraints + @"
public static Task<bool> TextColorTo" + textStyleClassMetadata.GenericArguments + "(this " + textStyleClassMetadata.Namespace + "." + textStyleClassMetadata.ClassName + textStyleClassMetadata.GenericArguments + @" element, Color color, uint rate = 16u, uint length = 250u, Easing? easing = null)
" + textStyleClassMetadata.GenericConstraints + @"
{
ArgumentNullException.ThrowIfNull(element);
ArgumentNullException.ThrowIfNull(color);
Expand Down Expand Up @@ -161,36 +143,74 @@ namespace " + textStyleClass.Namespace + @";
{
//When creating an Animation too early in the lifecycle of the Page, i.e. in the OnAppearing method,
//the Page might not have an 'IAnimationManager' yet, resulting in an ArgumentException.
System.Diagnostics.Debug.WriteLine($""{aex.GetType().Name} thrown in {typeof(ColorAnimationExtensions_" + textStyleClass.ClassName + @").FullName}: {aex.Message}"");
System.Diagnostics.Debug.WriteLine($""{aex.GetType().Name} thrown in {typeof(ColorAnimationExtensions_" + textStyleClassMetadata.ClassName + @").FullName}: {aex.Message}"");
animationCompletionSource.SetResult(false);
}
return animationCompletionSource.Task;
static Animation GetRedTransformAnimation(" + textStyleClass.Namespace + "." + textStyleClass.ClassName + textStyleClass.GenericArguments + @" element, float targetRed) =>
static Animation GetRedTransformAnimation(" + textStyleClassMetadata.Namespace + "." + textStyleClassMetadata.ClassName + textStyleClassMetadata.GenericArguments + @" element, float targetRed) =>
new(v => element.TextColor = element.TextColor.WithRed(v), element.TextColor.Red, targetRed);
static Animation GetGreenTransformAnimation(" + textStyleClass.Namespace + "." + textStyleClass.ClassName + textStyleClass.GenericArguments + @" element, float targetGreen) =>
static Animation GetGreenTransformAnimation(" + textStyleClassMetadata.Namespace + "." + textStyleClassMetadata.ClassName + textStyleClassMetadata.GenericArguments + @" element, float targetGreen) =>
new(v => element.TextColor = element.TextColor.WithGreen(v), element.TextColor.Green, targetGreen);
static Animation GetBlueTransformAnimation(" + textStyleClass.Namespace + "." + textStyleClass.ClassName + textStyleClass.GenericArguments + @" element, float targetBlue) =>
static Animation GetBlueTransformAnimation(" + textStyleClassMetadata.Namespace + "." + textStyleClassMetadata.ClassName + textStyleClassMetadata.GenericArguments + @" element, float targetBlue) =>
new(v => element.TextColor = element.TextColor.WithBlue(v), element.TextColor.Blue, targetBlue);
static Animation GetAlphaTransformAnimation(" + textStyleClass.Namespace + "." + textStyleClass.ClassName + textStyleClass.GenericArguments + @" element, float targetAlpha) =>
static Animation GetAlphaTransformAnimation(" + textStyleClassMetadata.Namespace + "." + textStyleClassMetadata.ClassName + textStyleClassMetadata.GenericArguments + @" element, float targetAlpha) =>
new(v => element.TextColor = element.TextColor.WithAlpha((float)v), element.TextColor.Alpha, targetAlpha);
}
}";
var source = textColorToBuilder.ToString();
SourceStringExtensions.FormatText(ref source, options);
context.AddSource($"{textStyleClass.ClassName}TextColorTo.g.shared.cs", SourceText.From(source, Encoding.UTF8));
var source = textColorToBuilder.ToString();
SourceStringService.FormatText(ref source);
context.AddSource($"{textStyleClassMetadata.ClassName}TextColorTo.g.shared.cs", SourceText.From(source, Encoding.UTF8));
}

static TextStyleClassMetadata GenerateMetadata(INamedTypeSymbol namedTypeSymbol)
{
var accessModifier = mauiControlsAssembly == namedTypeSymbol.ContainingNamespace.ToDisplayString()
? "internal"
: GetClassAccessModifier(namedTypeSymbol);

return new(namedTypeSymbol.Name, accessModifier, namedTypeSymbol.ContainingNamespace.ToDisplayString(), namedTypeSymbol.TypeArguments.GetGenericTypeArgumentsString(), namedTypeSymbol.GetGenericTypeConstraintsAsString());
}

static IEnumerable<INamedTypeSymbol> Deduplicate(ImmutableArray<INamedTypeSymbol?> left, IEnumerable<INamedTypeSymbol> right)
{
foreach (var leftItem in left)
{
if (leftItem is null)
{
continue;
}

var result = right.ContainsSymbolBaseType(leftItem);
if (!result)
{
yield return leftItem;
}
}

foreach (var rightItem in right)
{
yield return rightItem;
}
}

static IEnumerable<INamedTypeSymbol> GetMauiInterfaceImplementors(IAssemblySymbol mauiControlsAssemblySymbolProvider, INamedTypeSymbol iAnimatableSymbol, INamedTypeSymbol itextStyleSymbol)
{
return mauiControlsAssemblySymbolProvider.GlobalNamespace.GetNamedTypeSymbols().Where(x => x.AllInterfaces.Contains(itextStyleSymbol, SymbolEqualityComparer.Default)
&& x.AllInterfaces.Contains(iAnimatableSymbol, SymbolEqualityComparer.Default));
}

static string GetClassAccessModifier(INamedTypeSymbol namedTypeSymbol) => namedTypeSymbol.DeclaredAccessibility switch
{
Accessibility.Public => "public",
Accessibility.Internal => "internal",
_ => string.Empty
};

record TextStyleClassMetadata(string ClassName, string ClassAcessModifier, string Namespace, string GenericArguments, string GenericConstraints);
}
66 changes: 66 additions & 0 deletions src/CommunityToolkit.Maui.SourceGenerators/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// <auto-generated>
// This code file has automatically been added by the "IsExternalInit" NuGet package (https://www.nuget.org/packages/IsExternalInit).
// Please see https://github.com/manuelroemer/IsExternalInit for more information.
//
// IMPORTANT:
// DO NOT DELETE THIS FILE if you are using a "packages.config" file to manage your NuGet references.
// Consider migrating to PackageReferences instead:
// https://docs.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference
// Migrating brings the following benefits:
// * The "IsExternalInit" folder and the "IsExternalInit.cs" file don't appear in your project.
// * The added file is immutable and can therefore not be modified by coincidence.
// * Updating/Uninstalling the package will work flawlessly.
// </auto-generated>

#region License
// MIT License
//
// Copyright (c) Manuel Römer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#endregion

#if !ISEXTERNALINIT_DISABLE
#nullable enable
#pragma warning disable

namespace System.Runtime.CompilerServices
{
using global::System.Diagnostics;
using global::System.Diagnostics.CodeAnalysis;

/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
/// <remarks>
/// This definition is provided by the <i>IsExternalInit</i> NuGet package (https://www.nuget.org/packages/IsExternalInit).
/// Please see https://github.com/manuelroemer/IsExternalInit for more information.
/// </remarks>
#if !ISEXTERNALINIT_INCLUDE_IN_CODE_COVERAGE
[ExcludeFromCodeCoverage, DebuggerNonUserCode]
#endif
internal static class IsExternalInit
{
}
}

#pragma warning restore
#nullable restore
#endif // ISEXTERNALINIT_DISABLE
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

namespace CommunityToolkit.Maui.SourceGenerators.Helpers;

static class SourceStringExtensions
static class SourceStringService
{
public static void FormatText(ref string classSource, CSharpParseOptions? options)
public static void FormatText(ref string classSource, CSharpParseOptions? options = null)
{
var mysource = CSharpSyntaxTree.ParseText(SourceText.From(classSource, Encoding.UTF8), options);
var formattedRoot = (CSharpSyntaxNode)mysource.GetRoot().NormalizeWhitespace();
options ??= CSharpParseOptions.Default;

var sourceCode = CSharpSyntaxTree.ParseText(SourceText.From(classSource, Encoding.UTF8), options);
var formattedRoot = (CSharpSyntaxNode)sourceCode.GetRoot().NormalizeWhitespace();
classSource = CSharpSyntaxTree.Create(formattedRoot).ToString();
}
}

0 comments on commit 06c2b7c

Please sign in to comment.