Skip to content

Commit

Permalink
Merge pull request #868 from unoplatform/dev/dr/vmToModel
Browse files Browse the repository at this point in the history
feat(feeds): Add ability to generate [Bindable]VM from Model instead of ViewModel
  • Loading branch information
dr1rrb authored Oct 28, 2022
2 parents c439932 + 6903244 commit 958c420
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 67 deletions.
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

0 comments on commit 958c420

Please sign in to comment.