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: add NotNullIfNotNull attribute on generated methods, support it on user implemented methods #1718

Merged
merged 1 commit into from
Feb 17, 2025
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
26 changes: 22 additions & 4 deletions src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,41 @@ public bool HasAttribute<TAttribute>(ISymbol symbol)
public IEnumerable<TAttribute> Access<TAttribute>(ISymbol symbol)
where TAttribute : Attribute => Access<TAttribute, TAttribute>(symbol);

public IEnumerable<TAttribute> TryAccess<TAttribute>(IEnumerable<AttributeData> data)
where TAttribute : Attribute => TryAccess<TAttribute, TAttribute>(data);

public IEnumerable<TData> Access<TAttribute, TData>(ISymbol symbol)
where TAttribute : Attribute
where TData : notnull
{
var attrDatas = symbolAccessor.GetAttributes<TAttribute>(symbol);
return Access<TAttribute, TData>(attrDatas);
}

public IEnumerable<TData> TryAccess<TAttribute, TData>(IEnumerable<AttributeData> attributes)
where TAttribute : Attribute
where TData : notnull
{
var attrDatas = symbolAccessor.TryGetAttributes<TAttribute>(attributes);
return attrDatas.Select(a => Access<TAttribute, TData>(a));
}

/// <summary>
/// Reads the attribute data and sets it on a newly created instance of <see cref="TData"/>.
/// If <see cref="TAttribute"/> has n type parameters,
/// <see cref="TData"/> needs to have an accessible ctor with the parameters 0 to n-1 to be of type <see cref="ITypeSymbol"/>.
/// <see cref="TData"/> needs to have exactly the same constructors as <see cref="TAttribute"/> with additional type arguments.
/// </summary>
/// <param name="symbol">The symbol on which the attributes should be read.</param>
/// <param name="attributes">The attributes data.</param>
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
/// <typeparam name="TData">The type of the data class. If no type parameters are involved, this is usually the same as <see cref="TAttribute"/>.</typeparam>
/// <returns>The attribute data.</returns>
/// <exception cref="InvalidOperationException">If a property or ctor argument of <see cref="TData"/> could not be read on the attribute.</exception>
public IEnumerable<TData> Access<TAttribute, TData>(ISymbol symbol)
public IEnumerable<TData> Access<TAttribute, TData>(IEnumerable<AttributeData> attributes)
where TAttribute : Attribute
where TData : notnull
{
var attrDatas = symbolAccessor.GetAttributes<TAttribute>(symbol);
foreach (var attrData in attrDatas)
foreach (var attrData in symbolAccessor.GetAttributes<TAttribute>(attributes))
{
yield return Access<TAttribute, TData>(attrData, symbolAccessor);
}
Expand Down
9 changes: 6 additions & 3 deletions src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public MapperConfigurationReader(
AttributeDataAccessor dataAccessor,
WellKnownTypes types,
ISymbol mapperSymbol,
MapperConfiguration defaultMapperConfiguration
MapperConfiguration defaultMapperConfiguration,
SupportedFeatures supportedFeatures
)
{
_dataAccessor = dataAccessor;
Expand All @@ -38,7 +39,8 @@ MapperConfiguration defaultMapperConfiguration
),
new MembersMappingConfiguration([], [], [], [], [], mapper.IgnoreObsoleteMembersStrategy, mapper.RequiredMappingStrategy),
[],
mapper.UseDeepCloning
mapper.UseDeepCloning,
supportedFeatures
);
}

Expand All @@ -61,7 +63,8 @@ DiagnosticCollection diagnostics
enumConfig,
membersConfig,
derivedTypesConfig,
supportsDeepCloning && MapperConfiguration.Mapper.UseDeepCloning
supportsDeepCloning && MapperConfiguration.Mapper.UseDeepCloning,
MapperConfiguration.SupportedFeatures
);
}

Expand Down
4 changes: 3 additions & 1 deletion src/Riok.Mapperly/Configuration/MappingConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors;

namespace Riok.Mapperly.Configuration;

Expand All @@ -7,5 +8,6 @@ public record MappingConfiguration(
EnumMappingConfiguration Enum,
MembersMappingConfiguration Members,
IReadOnlyCollection<DerivedTypeMappingConfiguration> DerivedTypes,
bool UseDeepCloning
bool UseDeepCloning,
SupportedFeatures SupportedFeatures
);
6 changes: 4 additions & 2 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public DescriptorBuilder(
MapperConfiguration defaultMapperConfiguration
)
{
_mapperDescriptor = new MapperDescriptor(mapperDeclaration, _methodNameBuilder);
var supportedFeatures = SupportedFeatures.Build(compilationContext.Types, symbolAccessor, compilationContext.ParseLanguageVersion);
_mapperDescriptor = new MapperDescriptor(mapperDeclaration, _methodNameBuilder, supportedFeatures);
_symbolAccessor = symbolAccessor;
_types = compilationContext.Types;
_mappingBodyBuilder = new MappingBodyBuilder(_mappings);
Expand All @@ -51,7 +52,8 @@ MapperConfiguration defaultMapperConfiguration
attributeAccessor,
_types,
mapperDeclaration.Symbol,
defaultMapperConfiguration
defaultMapperConfiguration,
supportedFeatures
);
_diagnostics = new DiagnosticCollection(mapperDeclaration.Syntax.GetLocation());

Expand Down
5 changes: 4 additions & 1 deletion src/Riok.Mapperly/Descriptors/MapperDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ public class MapperDescriptor

public bool Static { get; set; }

public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder)
public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder, SupportedFeatures supportedFeatures)
{
_declaration = declaration;
NameBuilder = nameBuilder;
SupportedFeatures = supportedFeatures;
Name = BuildName(declaration.Symbol);
UnsafeAccessorName = nameBuilder.New(AccessorClassName);

Expand All @@ -31,6 +32,8 @@ public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBui
}
}

public SupportedFeatures SupportedFeatures { get; }

public string Name { get; }

public string? Namespace { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,12 @@ public static class ConvertStaticMethodMappingBuilder
allTargetMethods,
GetTargetStaticMethodNames(ctx),
ctx.Source,
ctx.Target,
nonNullableTarget,
targetIsNullable
);

if (mapping is not null)
{
return mapping;
}

var allSourceMethods = ctx.SymbolAccessor.GetAllMethods(ctx.Source);

Expand All @@ -44,7 +41,6 @@ public static class ConvertStaticMethodMappingBuilder
allSourceMethods.ToList(),
GetSourceStaticMethodNames(ctx),
ctx.Source,
ctx.Target,
nonNullableTarget,
targetIsNullable
);
Expand Down Expand Up @@ -81,7 +77,6 @@ private static bool IsDateTimeToTimeOnlyConversion(MappingBuilderContext ctx)
List<IMethodSymbol> allMethods,
IEnumerable<string> methodNames,
ITypeSymbol sourceType,
ITypeSymbol targetType,
ITypeSymbol nonNullableTargetType,
bool targetIsNullable
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ public static class NullableMappingBuilder
return null;

var delegateMapping = ctx.BuildMapping(mappingKey, MappingBuildingOptions.KeepUserSymbol);

return delegateMapping == null ? null : BuildNullDelegateMapping(ctx, delegateMapping);
}

Expand Down Expand Up @@ -52,7 +51,13 @@ private static INewInstanceMapping BuildNullDelegateMapping(MappingBuilderContex

return mapping switch
{
NewInstanceMethodMapping methodMapping => new NullDelegateMethodMapping(ctx.Source, ctx.Target, methodMapping, nullFallback),
NewInstanceMethodMapping methodMapping => new NullDelegateMethodMapping(
ctx.Source,
ctx.Target,
methodMapping,
nullFallback,
ctx.Configuration.SupportedFeatures.NullableAttributes
),
_ => new NullDelegateMapping(ctx.Source, ctx.Target, mapping, nullFallback),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static class QueryableMappingBuilder
ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingsDoNotSupportReferenceHandling);
}

return new QueryableProjectionMapping(ctx.Source, ctx.Target, mapping);
return new QueryableProjectionMapping(ctx.Source, ctx.Target, mapping, ctx.Configuration.SupportedFeatures.NullableAttributes);
}

private static TypeMappingKey TryBuildMappingKey(MappingBuilderContext ctx, ITypeSymbol sourceType, ITypeSymbol targetType)
Expand Down
13 changes: 12 additions & 1 deletion src/Riok.Mapperly/Descriptors/MappingCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,11 @@ public void AddNamedUserMapping(string? name, TUserMapping mapping)
public MappingCollectionAddResult TryAddAsDefault(T mapping, TypeMappingConfiguration config)
{
var mappingKey = new TypeMappingKey(mapping, config);
return _defaultMappings.TryAdd(mappingKey, mapping)
var result = _defaultMappings.TryAdd(mappingKey, mapping)
? MappingCollectionAddResult.Added
: MappingCollectionAddResult.NotAddedDuplicated;
AddAdditionalMappings(mapping, config);
return result;
}

public MappingCollectionAddResult AddUserMapping(TUserMapping mapping, bool? isDefault, string? name)
Expand Down Expand Up @@ -291,7 +293,16 @@ private MappingCollectionAddResult AddDefaultUserMapping(T mapping)

_duplicatedNonDefaultUserMappings.Remove(mappingKey);
_defaultMappings[mappingKey] = mapping;
AddAdditionalMappings(mapping, TypeMappingConfiguration.Default);
return MappingCollectionAddResult.Added;
}

private void AddAdditionalMappings(T mapping, TypeMappingConfiguration config)
{
foreach (var additionalKey in mapping.BuildAdditionalMappingKeys(config))
{
_defaultMappings.TryAdd(additionalKey, mapping);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Emit.Syntax;
using Riok.Mapperly.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

Expand All @@ -11,11 +12,11 @@ namespace Riok.Mapperly.Descriptors.Mappings;
/// by implementing a type switch over known types and performs the provided mapping for each type.
/// </summary>
public class DerivedTypeSwitchMapping(ITypeSymbol sourceType, ITypeSymbol targetType, IReadOnlyCollection<INewInstanceMapping> typeMappings)
: NewInstanceMapping(sourceType, targetType)
: NewInstanceMethodMapping(sourceType, targetType)
{
private const string GetTypeMethodName = nameof(GetType);

public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
public override IEnumerable<StatementSyntax> BuildBody(TypeMappingBuildContext ctx)
{
// _ => throw new ArgumentException(msg, nameof(ctx.Source)),
var sourceTypeExpr = ctx.SyntaxFactory.Invocation(MemberAccess(ctx.Source, GetTypeMethodName));
Expand All @@ -32,14 +33,15 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
// source switch { A x => MapToADto(x), B x => MapToBDto(x) }
var (typeArmContext, typeArmVariableName) = ctx.WithNewSource();
var arms = typeMappings.Select(x => BuildSwitchArm(typeArmVariableName, x.SourceType, x.Build(typeArmContext))).Append(fallbackArm);
return ctx.SyntaxFactory.Switch(ctx.Source, arms);
var switchExpression = ctx.SyntaxFactory.Switch(ctx.Source, arms);
return [ctx.SyntaxFactory.Return(switchExpression)];
}

private SwitchExpressionArmSyntax BuildSwitchArm(string typeArmVariableName, ITypeSymbol type, ExpressionSyntax mapping)
{
// A x => MapToADto(x),
var declaration = DeclarationPattern(
FullyQualifiedIdentifier(type).AddTrailingSpace(),
FullyQualifiedIdentifier(type.NonNullable()).AddTrailingSpace(),
SingleVariableDesignation(Identifier(typeArmVariableName))
);
return SwitchArm(declaration, mapping);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@ protected ExistingTargetMapping(ITypeSymbol sourceType, ITypeSymbol targetType)

public virtual bool IsSynthetic => false;

public IEnumerable<TypeMappingKey> BuildAdditionalMappingKeys(TypeMappingConfiguration config) => [];

public abstract IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax target);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ public class ObjectMemberExistingTargetMapping(ITypeSymbol sourceType, ITypeSymb
public ITypeSymbol TargetType { get; } = targetType;

public bool IsSynthetic => false;

public IEnumerable<TypeMappingKey> BuildAdditionalMappingKeys(TypeMappingConfiguration config) => [];
}
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Descriptors/Mappings/ITypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface ITypeMapping : IMapping
/// Gets a value indicating whether this mapping produces any code or can be omitted completely (eg. direct assignments or delegate mappings).
/// </summary>
bool IsSynthetic { get; }

IEnumerable<TypeMappingKey> BuildAdditionalMappingKeys(TypeMappingConfiguration config);
}
11 changes: 8 additions & 3 deletions src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ ITypeSymbol targetType

public bool IsSynthetic => false;

public virtual IEnumerable<TypeMappingKey> BuildAdditionalMappingKeys(TypeMappingConfiguration config) => [];

public virtual ExpressionSyntax Build(TypeMappingBuildContext ctx) =>
ctx.SyntaxFactory.Invocation(
MethodName,
Expand All @@ -91,7 +93,7 @@ public virtual MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx)
SourceParameter.Name,
ReferenceHandlerParameter?.Name,
ctx.NameBuilder.NewScope(),
ctx.SyntaxFactory.AddIndentation()
ctx.SyntaxFactory
);

var parameters = BuildParameterList();
Expand All @@ -101,8 +103,8 @@ public virtual MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx)
return MethodDeclaration(returnType.AddTrailingSpace(), Identifier(MethodName))
.WithModifiers(TokenList(BuildModifiers(ctx.IsStatic)))
.WithParameterList(parameters)
.WithAttributeLists(ctx.SyntaxFactory.GeneratedCodeAttributeList())
.WithBody(ctx.SyntaxFactory.Block(BuildBody(typeMappingBuildContext)));
.WithAttributeLists(BuildAttributes(typeMappingBuildContext))
.WithBody(ctx.SyntaxFactory.Block(BuildBody(typeMappingBuildContext.AddIndentation())));
}

public abstract IEnumerable<StatementSyntax> BuildBody(TypeMappingBuildContext ctx);
Expand All @@ -121,6 +123,9 @@ internal virtual void EnableReferenceHandling(INamedTypeSymbol iReferenceHandler
);
}

protected internal virtual SyntaxList<AttributeListSyntax> BuildAttributes(TypeMappingBuildContext ctx) =>
SingletonList(ctx.SyntaxFactory.GeneratedCodeAttribute());

protected virtual ParameterListSyntax BuildParameterList() =>
ParameterList(IsExtensionMethod, [SourceParameter, ReferenceHandlerParameter, .. AdditionalSourceParameters]);

Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Descriptors/Mappings/NewInstanceMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ protected NewInstanceMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
/// <inheritdoc cref="INewInstanceMapping.IsSynthetic"/>
public virtual bool IsSynthetic => false;

public virtual IEnumerable<TypeMappingKey> BuildAdditionalMappingKeys(TypeMappingConfiguration config) => [];

public abstract ExpressionSyntax Build(TypeMappingBuildContext ctx);
}
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Descriptors/Mappings/NoOpMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ public class NoOpMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : IExis
public ITypeSymbol TargetType => targetType;
public bool IsSynthetic => true;

public IEnumerable<TypeMappingKey> BuildAdditionalMappingKeys(TypeMappingConfiguration config) => [];

public IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax target) => [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,38 @@ public class NullDelegateMethodMapping(
ITypeSymbol nullableSourceType,
ITypeSymbol nullableTargetType,
MethodMapping delegateMapping,
NullFallbackValue nullFallbackValue
NullFallbackValue nullFallbackValue,
bool nullableAttributesSupported
) : NewInstanceMethodMapping(nullableSourceType, nullableTargetType)
{
public override IEnumerable<TypeMappingKey> BuildAdditionalMappingKeys(TypeMappingConfiguration config)
{
// if the fallback value is not nullable,
// this mapping never returns null.
// add the following mapping keys:
// null => null (added by default)
// null => non-null
// non-null => non-null
if (!nullFallbackValue.IsNullable(TargetType))
{
yield return new TypeMappingKey(SourceType, TargetType.NonNullable(), config);
yield return new TypeMappingKey(SourceType.NonNullable(), TargetType.NonNullable(), config);
yield break;
}

// this mapping never returns null for non-null input values
// and is guarded with [return: NotNullIfNotNull]
// therefore this mapping can also be used as mapping for non-null values.
yield return new TypeMappingKey(delegateMapping, config);
}

protected internal override SyntaxList<AttributeListSyntax> BuildAttributes(TypeMappingBuildContext ctx)
{
return !nullableAttributesSupported || !TargetType.IsNullable() || !nullFallbackValue.IsNullable(TargetType)
? base.BuildAttributes(ctx)
: base.BuildAttributes(ctx).Add(ctx.SyntaxFactory.ReturnNotNullIfNotNullAttribute(ctx.Source));
}

public override IEnumerable<StatementSyntax> BuildBody(TypeMappingBuildContext ctx)
{
var body = delegateMapping.BuildBody(ctx);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.Mappings;

internal static class NullFallbackValueExtensions
{
public static bool IsNullable(this NullFallbackValue fallbackValue, ITypeSymbol targetType) =>
fallbackValue == NullFallbackValue.Default && targetType.IsNullable();
}
Loading