Skip to content

Commit

Permalink
Merge pull request #860 from unoplatform/dev/dr/selection
Browse files Browse the repository at this point in the history
feat(selection): Add support for selection in Feeds
  • Loading branch information
dr1rrb authored Oct 28, 2022
2 parents df51544 + e14ffcf commit c439932
Show file tree
Hide file tree
Showing 57 changed files with 2,724 additions and 267 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ KE0002 | Usage | Error | A record that implements GetKeyHashCode should also i
KE0003 | Usage | Error | A record that implements KeyEquals should also implement GetKeyHashCode.
KE0004 | Usage | Warning | A record flagged with [ImplicitKeyEquality] attribute must have an eligible key property.
KE0005 | Usage | Warning | A record should have only one matching key property for implicit IKeyEquatable generation.
KE0006 | Usage | Warning | A record that implements IKeyEquatable should also implement IKeyed.
PS0001 | Usage | Error | A property selector can only use property members.
PS0002 | Usage | Error | A property selector cannot have any closure.
PS0003 | Usage | Error | A property selector must be a lambda.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ namespace Uno.Extensions.Generators;

internal static class RoslynExtensions
{
public static string ToFullString(this ISymbol symbol)
=> symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

public static IEnumerable<INamedTypeSymbol> GetNamespaceTypes(this INamespaceSymbol sym)
{
foreach (var child in sym.GetTypeMembers())
Expand Down Expand Up @@ -152,6 +155,18 @@ bool IsExpectedType(INamedTypeSymbol named)
|| SymbolEqualityComparer.Default.Equals(named.ConstructedFrom, typeOrInterface);
}

public static IPropertySymbol? FindImplementationOf(this INamedTypeSymbol type, IPropertySymbol boundedInterfaceProperty, SymbolEqualityComparer? comparer = null)
=> type.GetAllProperties().FirstOrDefault(p => p.IsImplementationOf(boundedInterfaceProperty, comparer));

public static IPropertySymbol? FindLocalImplementationOf(this INamedTypeSymbol type, IPropertySymbol boundedInterfaceProperty, SymbolEqualityComparer? comparer = null)
=> type.GetProperties().FirstOrDefault(p => p.IsImplementationOf(boundedInterfaceProperty, comparer));

public static bool IsImplementationOf(this IPropertySymbol property, IPropertySymbol boundedInterfaceProperty, SymbolEqualityComparer? comparer = null)
=> property is { OverriddenProperty: not null }
? SymbolEqualityComparer.IncludeNullability.Equals(property.OverriddenProperty, boundedInterfaceProperty)
: property.Name.Equals(boundedInterfaceProperty.Name, StringComparison.Ordinal)
&& (comparer ?? SymbolEqualityComparer.IncludeNullability).Equals(property.Type, boundedInterfaceProperty.Type);

public static IMethodSymbol? FindLocalImplementationOf(this INamedTypeSymbol type, IMethodSymbol boundedInterfaceMethod, SymbolEqualityComparer? comparer = null)
=> type.GetMethods().FirstOrDefault(m => m.IsImplementationOf(boundedInterfaceMethod, comparer));

Expand Down Expand Up @@ -396,6 +411,9 @@ private static object ToTypedArray(Type type, IEnumerable<object?> values)
=> type.GetMembers().OfType<IMethodSymbol>().FirstOrDefault(method => method.Name.Equals(methodName, comparison))
?? (allowBaseTypes && type.BaseType is { } @base? @base.FindMethod(methodName, allowBaseTypes, comparison) : null);

public static IPropertySymbol GetProperty(this INamedTypeSymbol type, string methodName, bool allowBaseTypes = true, StringComparison comparison = StringComparison.Ordinal)
=> type.FindProperty(methodName, allowBaseTypes, comparison) ?? throw new InvalidOperationException($"Property {methodName} not found on {type.Name}.");

public static IMethodSymbol GetMethod(this INamedTypeSymbol type, string methodName, bool allowBaseTypes = true, StringComparison comparison = StringComparison.Ordinal)
=> type.FindMethod(methodName, allowBaseTypes, comparison) ?? throw new InvalidOperationException($"Method {methodName} not found on {type.Name}.");
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ public static bool IsDisabled(this GeneratorExecutionContext context, string dis
isOptional: x.attribute.IsOptional || x.parameter.GetCustomAttributesData().Any(attr => attr.AttributeType.FullName.Equals("System.Runtime.CompilerServices.NullableAttribute")),
symbol: compilation
.GetTypesByMetadataName(x.attribute.Type)
.OrderBy(t =>
SymbolEqualityComparer.Default.Equals(t.ContainingAssembly, compilation.Assembly) ? 0
: t.IsAccessibleTo(compilation.Assembly) ? 1
: 2)
.OrderBy(t => t switch
{
_ when SymbolEqualityComparer.Default.Equals(t.ContainingAssembly, compilation.Assembly) => 0,
_ when t.IsAccessibleTo(compilation.Assembly) => 1,
_ => 2
})
.FirstOrDefault()
))
.ToList();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Uno.Extensions.Equality;

Expand All @@ -10,9 +13,10 @@ internal record KeyEqualityGenerationContext(

// Types
[ContextType($"{NS.Equality}.IKeyEquatable`1")] INamedTypeSymbol IKeyEquatable,
[ContextType($"{NS.Equality}.IKeyed`1")] INamedTypeSymbol IKeyed,

// Config
[ContextType(typeof(ImplicitKeyEqualityAttribute))] INamedTypeSymbol ImplicitKeyAttribute,
[ContextType(typeof(ImplicitKeysAttribute))] INamedTypeSymbol ImplicitKeyAttribute,
[ContextType(typeof(KeyAttribute))] INamedTypeSymbol KeyAttribute,
[ContextType("System.ComponentModel.DataAnnotations.KeyAttribute?")] INamedTypeSymbol? DataAnnotationsKeyAttribute
)
Expand All @@ -22,4 +26,29 @@ internal record KeyEqualityGenerationContext(

private IMethodSymbol? _keyEquals;
public IMethodSymbol KeyEquals => _keyEquals ??= IKeyEquatable.GetMethod(nameof(KeyEquals));

public ITypeSymbol ConstructTupleOrSingle(params ITypeSymbol[] typeArguments)
=> typeArguments switch
{
null or { Length: 0 } => throw new InvalidOperationException("No types to create tuple."),
{ Length: 1 } => typeArguments[0],
_ when Context.Compilation.GetTypeByMetadataName($"System.ValueTuple`{typeArguments.Length}") is { } tuple => tuple.Construct(typeArguments),
_ => throw new InvalidOperationException("Failed to construct tuple."),
};

public ImmutableArray<ITypeSymbol> DeconstructTupleOrSingle(ITypeSymbol? maybeTuple)
=> maybeTuple switch
{
null => ImmutableArray<ITypeSymbol>.Empty,
{IsTupleType: true} => ((INamedTypeSymbol)maybeTuple).TypeArguments,
_ => ImmutableArray.Create(maybeTuple)
};

public INamedTypeSymbol? GetLocalIKeyed(INamedTypeSymbol? baseIKeyed, ICollection<IPropertySymbol> additionalLocalKeysTypes)
=> (baseIKeyed, additionalLocalKeysTypes) switch
{
(_, null or { Count: 0 }) => null, // If no local keys, then no local implementation of IKeyed
(null, _) => IKeyed.Construct(ConstructTupleOrSingle(additionalLocalKeysTypes.Select(k => k.Type).ToArray())),
_ => IKeyed.Construct(ConstructTupleOrSingle(DeconstructTupleOrSingle(baseIKeyed.TypeArguments[0]).Concat(additionalLocalKeysTypes.Select(k => k.Type)).ToArray()))
};
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Uno.Extensions.Equality;
Expand Down Expand Up @@ -51,15 +52,15 @@ public KeyEqualityGenerationTool(KeyEqualityGenerationContext context)

private void LoadConfigs()
{
var assemblyImplicit = _assembly.FindAttribute<ImplicitKeyEqualityAttribute>() ?? new ImplicitKeyEqualityAttribute();
var assemblyImplicit = _assembly.FindAttribute<ImplicitKeysAttribute>() ?? new ImplicitKeysAttribute();
var assemblyTypes = from module in _assembly.Modules from type in module.GetNamespaceTypes() select type;
foreach (var type in assemblyTypes)
{
GetOrCreateConfig(type, assemblyImplicit);
}
}

private Config? GetOrCreateConfig(INamedTypeSymbol? type, ImplicitKeyEqualityAttribute assemblyImplicit)
private Config? GetOrCreateConfig(INamedTypeSymbol? type, ImplicitKeysAttribute assemblyImplicit)
{
if (type is null)
{
Expand All @@ -74,20 +75,32 @@ private void LoadConfigs()
if (!type.IsRecord)
{
// If the type is a class that implement IKeyEquatable, we still add it to the configs (with code gen disabled) for the registry generation.
return _configs[type] = type.IsOrImplements(_ctx.IKeyEquatable, allowBaseTypes: true, out var implementation)
? new Config(type, implementation, null, new (0), NeedsCodeGen: false, HasGetKeyHashCode: true, HasKeyEquals: true)
: null;
var nonRecordIsIKeyEquatable = type.IsOrImplements(_ctx.IKeyEquatable, allowBaseTypes: true, out var nonRecordIKeyEquatable);
var nonRecordIsIKeyed = type.IsOrImplements(_ctx.IKeyed, allowBaseTypes: true, out var nonRecordIKeyed);

if (nonRecordIsIKeyEquatable || nonRecordIsIKeyed)
{
return _configs[type] = new Config(type, nonRecordIKeyEquatable, null, nonRecordIKeyed, null, new(0), NeedsCodeGen: false, IsCustomImplementation: true);
}
else
{
return null;
}
}

var iKeyEquatable = _ctx.IKeyEquatable.Construct(type);
var iKeyEquatable_GetKeyHashCode = iKeyEquatable.GetMethod(GetKeyHashCode);
var iKeyEquatable_KeyEquals = iKeyEquatable.GetMethod(KeyEquals);
var baseIKeyEquatable = type.BaseType is { } baseType ? GetBaseIKeyEquatable(baseType) : null;
var (baseIKeyEquatable, baseIKeyed) = type.BaseType is { } baseType ? GetBaseImplementations(baseType) : default;

var hasIKeyEquatableDeclared = type.IsOrImplements(iKeyEquatable, allowBaseTypes: false, out _);
var hasIKeyedDeclared = type.IsOrImplements(_ctx.IKeyed, allowBaseTypes: false, out var declaredIKeyed);
var getKeyHashCode = type.FindLocalImplementationOf(iKeyEquatable_GetKeyHashCode, SymbolEqualityComparer.Default);
var keyEquals = type.FindLocalImplementationOf(iKeyEquatable_KeyEquals, SymbolEqualityComparer.Default);
var keys = SearchKeys(type, assemblyImplicit);
var isCustomImplementation = getKeyHashCode is not null || keyEquals is not null;

var keys = isCustomImplementation ? new (0) : SearchKeys(type, assemblyImplicit);
var iKeyed = isCustomImplementation ? declaredIKeyed : _ctx.GetLocalIKeyed(baseIKeyed, keys);

var isIKeyEquatable = keys is { Count: > 0 } || hasIKeyEquatableDeclared || getKeyHashCode is not null || keyEquals is not null;
var needsCodeGen = isIKeyEquatable && (!hasIKeyEquatableDeclared || getKeyHashCode is null || keyEquals is null);
Expand All @@ -97,7 +110,7 @@ private void LoadConfigs()
{
// If the type is not IKeyEquatable but inherits from type that is, we still add it to the configs (with code gen disabled) for the registry generation.
return _configs[type] = baseIKeyEquatable is not null
? new Config(type, null, baseIKeyEquatable, new(0), NeedsCodeGen: false, HasGetKeyHashCode: false, HasKeyEquals: false)
? new Config(type, null, baseIKeyEquatable, null, baseIKeyed, new(0), NeedsCodeGen: false, IsCustomImplementation: false)
: null;
}

Expand All @@ -109,6 +122,7 @@ private void LoadConfigs()
return _configs[type] = null; // Reduce number of errors by considering type as not IKeyEquatable
}

// Make sure that if user partially implemented IKeyEquatable, he fully implements it, and he also provide IKeyed implementation
if (getKeyHashCode is not null && keyEquals is null)
{
_ctx.Context.ReportDiagnostic(Rules.KE0002.GetDiagnostic(type, getKeyHashCode));
Expand All @@ -117,16 +131,32 @@ private void LoadConfigs()
{
_ctx.Context.ReportDiagnostic(Rules.KE0003.GetDiagnostic(type, keyEquals));
}
if (isCustomImplementation && !hasIKeyedDeclared)
{
_ctx.Context.ReportDiagnostic(Rules.KE0006.GetDiagnostic(type, getKeyHashCode ?? keyEquals!));
}

return _configs[type] = new Config(type, iKeyEquatable, baseIKeyEquatable, iKeyed, baseIKeyed, keys, needsCodeGen, isCustomImplementation);

return _configs[type] = new Config(type, iKeyEquatable, baseIKeyEquatable, keys, needsCodeGen, getKeyHashCode is not null, keyEquals is not null);
(INamedTypeSymbol? iKeyEquatable, INamedTypeSymbol? iKeyed) GetBaseImplementations(INamedTypeSymbol baseType)
{
if (SymbolEqualityComparer.Default.Equals(baseType.ContainingAssembly, _assembly))
{
return GetOrCreateConfig(baseType, assemblyImplicit) is { } config
? (config.IKeyEquatable, config.IKeyed ?? config.BaseIKeyed)
: default;
}
else
{
baseType.IsOrImplements(_ctx.IKeyEquatable, out var baseKeyEquatable);
baseType.IsOrImplements(_ctx.IKeyEquatable, out var baseKeyed);

INamedTypeSymbol? GetBaseIKeyEquatable(INamedTypeSymbol baseType)
=> SymbolEqualityComparer.Default.Equals(baseType.ContainingAssembly, _assembly)
? GetOrCreateConfig(baseType, assemblyImplicit)?.IKeyEquatable
: (baseType.IsOrImplements(_ctx.IKeyEquatable, out var baseKeyEquatable) ? baseKeyEquatable : null);
return (baseKeyEquatable, baseKeyed);
}
}
}

private List<IPropertySymbol> SearchKeys(INamedTypeSymbol type, ImplicitKeyEqualityAttribute assemblyImplicit)
private List<IPropertySymbol> SearchKeys(INamedTypeSymbol type, ImplicitKeysAttribute assemblyImplicit)
{
// Search for properties flagged with [Key] attribute (Using Uno's attribute or System.ComponentModel.DataAnnotations.KeyAttribute)
var keys = type
Expand All @@ -138,7 +168,7 @@ private List<IPropertySymbol> SearchKeys(INamedTypeSymbol type, ImplicitKeyEqual
// If none found, search for implicit key properties
if (keys is { Count: 0 })
{
var typeImplicit = type.FindAttribute<ImplicitKeyEqualityAttribute>();
var typeImplicit = type.FindAttribute<ImplicitKeysAttribute>();
var @implicit = typeImplicit ?? assemblyImplicit;
if (@implicit.IsEnabled)
{
Expand Down Expand Up @@ -179,10 +209,34 @@ private string GenerateKeyEquatable(INamedTypeSymbol type, Config config)

return this.AsPartialOf(
type,
$"{NS.Equality}.IKeyEquatable<{type}>", // i.e. config.IKeyEquatable
$"{NS.Equality}.IKeyEquatable<{type}>{(config.IKeyed is null ? "" : ", " + config.IKeyed.ToFullString())}", // i.e. config.IKeyEquatable
$@"
{(config.HasGetKeyHashCode
? $"// Skipping {GetKeyHashCode} as it has already been implemented in user's code"
{(config.IsCustomImplementation
? "// Skipping IKeyed.Key as user is providing a custom implementation of IKeyEquatable"
: $@"/// <inheritdoc cref=""{{NS.Equality}}.IKeyed{{T}}"" />
{this.GetCodeGenAttribute()}
{config.IKeyed!.TypeArguments[0].ToFullString()} {config.IKeyed.ToFullString()}.Key
{{
get
{{
{config switch
{
{ BaseIKeyed: null, Keys.Count: 1 } => $"return {config.Keys[0].Name};",
{ BaseIKeyed: null } => $"return ({config.GetKeyNames()});",
_ => $@"
var baseKey = (({config.BaseIKeyed.ToFullString()})this).Key;
return ({_ctx.DeconstructTupleOrSingle(config.BaseIKeyed!.TypeArguments[0]).Length switch
{
1 => "baseKey",
var baseKeys => Enumerable.Range(1, baseKeys).Select(i => $"baseKey.Item{i}").Align(9, ",")
}},
{config.GetKeyNames()});".Align(8)
}}
}}
}}".Align(4))}
{(config.IsCustomImplementation
? $"// Skipping {GetKeyHashCode} as user is providing a custom implementation of IKeyEquatable"
: $@"/// <inheritdoc cref=""{NS.Equality}.IKeyEquatable{{T}}"" />
{this.GetCodeGenAttribute()}
public {modifiers} int {GetKeyHashCode}()
Expand All @@ -205,8 +259,8 @@ private string GenerateKeyEquatable(INamedTypeSymbol type, Config config)
}}
}}".Align(4))}
{(config.HasKeyEquals
? $"// Skipping {KeyEquals} as it has already been implemented in user's code"
{(config.IsCustomImplementation
? $"// Skipping {KeyEquals} as user is providing a custom implementation of IKeyEquatable"
: $@"/// <inheritdoc cref=""{NS.Equality}.IKeyEquatable{{T}}"" />
{this.GetCodeGenAttribute()}
public bool {KeyEquals}({type}{(type.IsValueType ? "" : "?")} other)
Expand Down Expand Up @@ -309,18 +363,24 @@ private __KeyEqualityProvider()
///
/// </summary>
/// <param name="Type"></param>
/// <param name="IKeyEquatable">The bounded implementation of IKeyEquatable.</param>
/// <param name="IKeyEquatable">The bounded local implementation of IKeyEquatable.</param>
/// <param name="BaseIKeyEquatable">The bounded base implementation of IKeyEquatable if any.</param>
/// <param name="IKeyed">The bounded local implementation of IKeyed if any.</param>
/// <param name="BaseIKeyed">The bounded base implementation of IKeyed if any.</param>
/// <param name="Keys">The keys to use to generation.</param>
/// <param name="NeedsCodeGen">Indicates is a partial class is needed.</param>
/// <param name="HasGetKeyHashCode">Indicates if the GetKeyHashCode has already been implemented.</param>
/// <param name="HasKeyEquals">Indicates if the KeyEquals has already been implemented.</param>
/// <param name="IsCustomImplementation">Indicates the user is providing it own implementation of IKeyEquatable.</param>
private record Config(
INamedTypeSymbol Type,
INamedTypeSymbol? IKeyEquatable,
INamedTypeSymbol? BaseIKeyEquatable,
INamedTypeSymbol? IKeyed,
INamedTypeSymbol? BaseIKeyed,
List<IPropertySymbol> Keys,
bool NeedsCodeGen,
bool HasGetKeyHashCode,
bool HasKeyEquals);
[property: MemberNotNullWhen(true, nameof(Config.IKeyed))] bool IsCustomImplementation)
{
public string GetKeyNames(string separator = ", ")
=> Keys.Select(k => k.Name).JoinBy(separator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal static partial class Rules
public static class KE0001
{
private const string message = "The record '{0}' is eligible to IKeyEquatable generation (due to {1}) but is not partial."
+ " Either make it partial (recommended), either disable implicit IKeyEquality generation using [ImplicitKeyEquality(IsEnabled = false)]"
+ " Either make it partial (recommended), either disable implicit IKeyEquality generation using [ImplicitKeys(IsEnabled = false)]"
+ " on the record itself or on the whole assembly (not recommended).";

public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
Expand Down Expand Up @@ -96,13 +96,13 @@ public static Diagnostic GetDiagnostic(INamedTypeSymbol @class, IMethodSymbol ke

public static class KE0004
{
private const string message = "The record '{0}' is flagged with [ImplicitKeyEquality] attribute, but no property match any of the defined implicit keys."
private const string message = "The record '{0}' is flagged with [ImplicitKeys] attribute, but no property match any of the defined implicit keys."
+ " The IKeyEquatable implementation cannot be generated."
+ " You should either remove the [ImplicitKeyEquality] attribute, either add property named {1}.";
+ " You should either remove the [ImplicitKeys] attribute, either add property named {1}.";

public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
nameof(KE0004),
"Records flags with [ImplicitKeyEquality] attribute should have a matching key",
"Records flags with [ImplicitKeys] attribute should have a matching key",
message,
Category.Usage,
DiagnosticSeverity.Warning,
Expand Down Expand Up @@ -166,6 +166,31 @@ public static Diagnostic GetDiagnostic(INamedTypeSymbol @class, ICollection<IPro
FormatKeys(allPossibleKeys.Except(new[] { usedKey }).ToList(), " and "));
}

public static class KE0006
{
private const string message = "The record '{0}' provides custom implemenation of IKeyEquatable but does not implements IKeyed";

public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
nameof(KE0006),
"A record that implements IKeyEquatable should also implement IKeyed",
message,
Category.Usage,
DiagnosticSeverity.Warning,
helpLinkUri: "https://platform.uno/docs/articles/external/uno.extensions/doc/Overview/KeyEquality/rules.html#KE0006",
isEnabledByDefault: true);

public static string GetMessage(INamedTypeSymbol @class)
=> string.Format(CultureInfo.InvariantCulture, message, @class.Name);

public static Diagnostic GetDiagnostic(INamedTypeSymbol @class, IMethodSymbol equatableImpl)
=> Diagnostic.Create(
Descriptor,
equatableImpl.DeclaringSyntaxReferences.FirstOrDefault() is { } syntax
? Location.Create(syntax.SyntaxTree, syntax.Span)
: Location.None,
@class.Name);
}

private static string FormatKeys(ICollection<string> keys, string separator)
=> keys?.Count switch
{
Expand Down
Loading

0 comments on commit c439932

Please sign in to comment.