Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(feeds): Add ability to generate [Bindable]VM from Model instead of ViewModel #868

Merged
merged 2 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,60 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Uno.Extensions.Generators;
using Uno.Extensions.Reactive.Config;
using Uno.RoslynHelpers;
using static Microsoft.CodeAnalysis.Accessibility;

namespace Uno.Extensions.Reactive.Generator;

internal class BindableViewModelGenerator
internal class BindableViewModelGenerator : ICodeGenTool
{
private const string _version = "1";
/// <inheritdoc />
public string Version => "1";

private readonly BindableGenerationContext _ctx;
private readonly BindableGenerator _bindables;
private readonly BindableViewModelMappingGenerator _viewModelsMapping;
private readonly IAssemblySymbol _assembly;

public BindableViewModelGenerator(BindableGenerationContext ctx)
{
_ctx = ctx;
_bindables = new BindableGenerator(ctx);
_viewModelsMapping = new BindableViewModelMappingGenerator(ctx);
_assembly = ctx.Context.Compilation.Assembly;
}

private bool IsSupported(INamedTypeSymbol? type)
=> type is not null
&& (_ctx.IsGenerationEnabled(type)
?? type.Name.EndsWith("ViewModel", StringComparison.Ordinal) && type.IsPartial());
{
if (type is null)
{
return false;
}

if (_ctx.IsGenerationEnabled(type) is {} isEnabled)
{
// If the attribute is set, we don't check for the `partial`: the build as to fail if not
return isEnabled;
}

public IEnumerable<(string fileName, string code)> Generate(IAssemblySymbol assembly)
if (type.IsPartial()
&& (type.ContainingAssembly.FindAttribute<ImplicitBindablesAttribute>() ?? new ()) is { IsEnabled: true } @implicit // Note: the type might be from another assembly than current
&& @implicit.Patterns.Any(pattern => Regex.IsMatch(type.ToString(), pattern)))
{
return true;
}

return false;
}

public IEnumerable<(string fileName, string code)> Generate()
{
var viewModels = from module in assembly.Modules
var viewModels = from module in _assembly.Modules
from type in module.GetNamespaceTypes()
where IsSupported(type)
select type;
Expand All @@ -50,23 +73,35 @@ where IsSupported(type)
yield return _viewModelsMapping.Generate();
}

private string Generate(INamedTypeSymbol vm)
private static string GetViewModelName(INamedTypeSymbol type)
{
// Note: the type might be from another assembly than current
var isLegacyMode = type.ContainingAssembly.FindAttribute<ImplicitBindablesAttribute>() is { Patterns.Length: 1 } config
&& config.Patterns[0] is ImplicitBindablesAttribute.LegacyPattern;

return !isLegacyMode && type.Name.IndexOf("ViewModel", StringComparison.OrdinalIgnoreCase) < 0
? $"{type.Name}ViewModel"
: $"Bindable{type.Name}";
}

private string Generate(INamedTypeSymbol model)
{
var hasBaseType = IsSupported(vm.BaseType);
var vmName = GetViewModelName(model);
var hasBaseType = IsSupported(model.BaseType);
var baseType = hasBaseType
? $"{vm.BaseType}.Bindable{vm.BaseType!.Name}"
? $"{model.BaseType}.{GetViewModelName(model.BaseType!)}"
: $"{NS.Bindings}.BindableViewModelBase";

List<IInputInfo> inputs;
IEnumerable<string> inputsErrors;
if (hasBaseType || vm.IsAbstract)
if (hasBaseType || model.IsAbstract)
{
inputs = new List<IInputInfo>(0);
inputsErrors = Enumerable.Empty<string>();
}
else
{
inputs = GetInputs(vm).ToList();
inputs = GetInputs(model).ToList();
inputsErrors = inputs
.GroupBy(input => input.Parameter.Name)
.Where(group => group.Distinct().Count() > 1)
Expand All @@ -82,7 +117,7 @@ private string Generate(INamedTypeSymbol vm)
inputs = inputs.Distinct().ToList();
}

var mappedMembers = GetMembers(vm).ToList();
var mappedMembers = GetMembers(model).ToList();
var mappedMembersConflictingWithInputs = mappedMembers
.Where(member => inputs.Any(input => input.Property?.Name.Equals(member.Name, StringComparison.Ordinal) ?? false))
.ToList();
Expand All @@ -92,16 +127,16 @@ private string Generate(INamedTypeSymbol vm)
mappedMembers = mappedMembers.Except(mappedMembersConflictingWithInputs).ToList();

var bindableVmCode = $@"
[global::System.CodeDom.Compiler.GeneratedCode(""{nameof(BindableViewModelGenerator)}"", ""{_version}"")]
public partial class Bindable{vm.Name} : {baseType}
{this.GetCodeGenAttribute()}
public partial class {vmName} : {baseType}
{{
{inputsErrors.Align(4)}
{inputs.Select(input => input.GetBackingField()).Align(4)}
{mappedMembers.Select(member => member.GetBackingField()).Align(4)}

{vm
{model
.Constructors
.Where(ctor => !ctor.IsCloneCtor(vm)
.Where(ctor => !ctor.IsCloneCtor(model)
// we do not support inheritance of ctor, inheritance always goes through the same BindableVM(vm) ctor.
&& ctor.DeclaredAccessibility is not Protected and not ProtectedAndFriend and not ProtectedAndInternal)
.Where(_ctx.IsGenerationNotDisable)
Expand All @@ -115,8 +150,8 @@ public partial class Bindable{vm.Name} : {baseType}
.ToList();

return $@"
{GetCtorAccessibility(ctor)} Bindable{vm.Name}({parameters.Select(p => p.GetCtorParameter().code).JoinBy(", ")})
: base(new {vm}({parameters.Select(p => p.GetVMCtorParameter()).JoinBy(", ")}))
{GetCtorAccessibility(ctor)} {vmName}({parameters.Select(p => p.GetCtorParameter().code).JoinBy(", ")})
: base(new {model}({parameters.Select(p => p.GetVMCtorParameter()).JoinBy(", ")}))
{{
var {N.Ctor.Model} = {N.Model};
var {N.Ctor.Ctx} = {NS.Core}.SourceContext.GetOrCreate({N.Ctor.Model});
Expand All @@ -142,11 +177,11 @@ public partial class Bindable{vm.Name} : {baseType}
.JoinBy(", ");

return $@"
{GetCtorAccessibility(ctor)} Bindable{vm.Name}({bindableVmParameters})
{GetCtorAccessibility(ctor)} {vmName}({bindableVmParameters})
{{
{inputs.Select(input => input.GetCtorInit(parameters.Contains(input))).Align(9)}

var {N.Ctor.Model} = new {vm}({vmParameters});
var {N.Ctor.Model} = new {model}({vmParameters});
var {N.Ctor.Ctx} = {NS.Core}.SourceContext.GetOrCreate({N.Ctor.Model});
{NS.Core}.SourceContext.Set(this, {N.Ctor.Ctx});
base.RegisterDisposable({N.Ctor.Model});
Expand All @@ -159,61 +194,40 @@ public partial class Bindable{vm.Name} : {baseType}
})
.Align(4)}

protected Bindable{vm.Name}({vm} {N.Ctor.Model}){(hasBaseType ? $" : base({N.Ctor.Model})" : "")}
protected {vmName}({model} {N.Ctor.Model}){(hasBaseType ? $" : base({N.Ctor.Model})" : "")}
{{
var {N.Ctor.Ctx} = {NS.Core}.SourceContext.GetOrCreate({N.Ctor.Model});
{(hasBaseType ? "" : $"{NS.Core}.SourceContext.Set(this, {N.Ctor.Ctx});")}

{mappedMembers.Select(member => member.GetInitialization()).Align(5)}
}}

public {(hasBaseType ? $"new {vm} {N.Model} => ({vm}) base.{N.Model};" : $"{vm} {N.Model} {{ get; }}")}
public {(hasBaseType ? $"new {model} {N.Model} => ({model}) base.{N.Model};" : $"{model} {N.Model} {{ get; }}")}

{inputs.Select(input => input.Property?.ToString()).Align(4)}

{mappedMembersErrors.Align(4)}
{mappedMembers.Select(member => member.GetDeclaration()).Align(4)}
}}";

// We make the bindbale VM a nested class of the VM itself
var vmCode = $@"partial {(vm.IsRecord ? "record" : "class")} {vm.Name} : global::System.IAsyncDisposable, {NS.Core}.ISourceContextAware
{{
var fileCode = this.AsPartialOf(
model,
$"global::System.IAsyncDisposable, {NS.Core}.ISourceContextAware",
$@"
{bindableVmCode.Align(4)}

/// <inheritdoc />
[global::System.CodeDom.Compiler.GeneratedCode(""{nameof(BindableViewModelGenerator)}"", ""{_version}"")]
{this.GetCodeGenAttribute()}
public global::System.Threading.Tasks.ValueTask DisposeAsync()
=> {NS.Core}.SourceContext.Find(this)?.DisposeAsync() ?? default;
}}";

// Inject usings and declare the full namespace, including class nesting
var fileCode = $@"//----------------------
// <auto-generated>
// Generated by the {nameof(BindableViewModelGenerator)} v{_version}. DO NOT EDIT!
// Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//----------------------
#nullable enable
#pragma warning disable

using System;
using System.Linq;
using System.Threading.Tasks;

namespace {vm.ContainingNamespace}
{{
{vm.GetContainingTypes().Select(type => $"partial {(type.IsRecord ? "record" : "class")} {type.Name}\r\n{{").Align(4)}
{vmCode.Align(4).Indent(vm.GetContainingTypes().Count())}
{vm.GetContainingTypes().Select(_ => "}").Align(4)}
}}
";
");

// If type is at least internally accessible, add it to a mapping from the VM type to it's bindable counterpart to ease usage in navigation engine.
// (Private types are almost only a test case which is not supported by nav anyway)
if (vm.DeclaredAccessibility is not Accessibility.Private
&& vm.GetContainingTypes().All(type => type.DeclaredAccessibility is not Accessibility.Private))
if (model.DeclaredAccessibility is not Accessibility.Private
&& model.GetContainingTypes().All(type => type.DeclaredAccessibility is not Accessibility.Private))
{
_viewModelsMapping.Register(vm, $"{vm.ContainingNamespace}.{vm.GetContainingTypes().Select(type => type.Name + '.').JoinBy("")}{vm.Name}.Bindable{vm.Name}");
_viewModelsMapping.Register(model, $"{model.ContainingNamespace}.{model.GetContainingTypes().Select(type => type.Name + '.').JoinBy("")}{model.Name}.{vmName}");
}

return fileCode.Align(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

namespace Uno.Extensions.Reactive.Generator;

internal class BindableViewModelMappingGenerator
internal class BindableViewModelMappingGenerator : ICodeGenTool
{
private const string _version = "1";
/// <inheritdoc />
public string Version => "1";

#pragma warning disable RS1024 // Compare symbols correctly => False positive
private readonly Dictionary<INamedTypeSymbol, string> _bindableVMs = new(SymbolEqualityComparer.Default);
Expand All @@ -25,18 +26,11 @@ public void Register(INamedTypeSymbol viewModelType, string bindableViewModelTyp

public (string file, string code) Generate()
{
var code = $@"//----------------------
// <auto-generated>
// Generated by the {nameof(BindableViewModelMappingGenerator)} v{_version}. DO NOT EDIT!
// Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//----------------------
#nullable enable
#pragma warning disable
var code = $@"{this.GetFileHeader(3)}

namespace {_ctx.Context.Compilation.Assembly.Name}
{{
[global::System.CodeDom.Compiler.GeneratedCode(""{nameof(BindableViewModelMappingGenerator)}"", ""{_version}"")]
{this.GetCodeGenAttribute()}
public static partial class ReactiveViewModelMappings
{{
/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Uno.Extensions.Reactive.Generator/FeedsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void Execute(GeneratorExecutionContext context)

if (GenerationContext.TryGet<BindableGenerationContext>(context, out var error) is {} bindableContext)
{
foreach (var generated in new BindableViewModelGenerator(bindableContext).Generate(context.Compilation.Assembly))
foreach (var generated in new BindableViewModelGenerator(bindableContext).Generate())
{
context.AddSource(PathHelper.SanitizeFileName(generated.fileName) + ".g.cs", generated.code);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public void NestedInheritingViewModel()
=> Assert.IsNotNull(GetBindable(typeof(NestedSubViewModel)));

private Type? GetBindable(Type vmType)
=> vmType.GetNestedType($"Bindable{vmType.Name}");
=> vmType.GetNestedType(vmType.Name.Contains("ViewModel") ? $"Bindable{vmType.Name}" : $"{vmType.Name}ViewModel");

private Type? GetBindable(string vmType)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Linq;

namespace Uno.Extensions.Reactive.Config;

/// <summary>
/// Indicates if types that matches defined suffixes should be automatically exposed as bindable friendly view models.
/// </summary>
/// <remarks>If disabled, you can still generates bindable friendly view model by flagging a class with the <see cref="ReactiveBindableAttribute"/>.</remarks>
[AttributeUsage(AttributeTargets.Assembly)]
public class ImplicitBindablesAttribute : Attribute
{
/// <summary>
/// Gets the legacy pattern that was used in versions prior to 2.3.
/// </summary>
public const string LegacyPattern = "ViewModel$";

/// <summary>
/// Gets or sets a bool which indicates if the generation of view models based on class names is enabled of not.
/// </summary>
public bool IsEnabled { get; init; } = true;

/// <summary>
/// The patterns that the class FullName has to match to implicitly trigger view model generation.
/// </summary>
public string[] Patterns { get; } = { "Model$" };

/// <summary>
/// Create a new instance using default values.
/// </summary>
public ImplicitBindablesAttribute()
{
}

/// <summary>
/// Creates a new instance specifying the <see cref="Patterns"/>.
/// </summary>
/// <param name="patterns">The patterns that the class FullName has to match to implicitly trigger view model generation.</param>
public ImplicitBindablesAttribute(params string[] patterns)
{
Patterns = patterns;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace Uno.Extensions.Reactive;

/// <summary>
/// Indicates that the input should be considered as a simple value. It can be get/set/ through bindings, but it won't be de-normalized.
/// Indicates that the input should be considered as a simple value. It can be get/set through bindings, but it won't be de-normalized.
/// </summary>
/// <remarks>This should be used for records inputs for which we want to disable the default de-normalization.</remarks>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
Expand Down