From b26ee03ce31faf43cfe65cdc226386b3efabea3b Mon Sep 17 00:00:00 2001 From: Paul Martins <50200071+MooVC@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:28:42 +0000 Subject: [PATCH 1/4] Add unary operator forwarding support --- .../WhenGetEncapsulatedIsCalled.cs | 86 +++++++++++++++++ .../WhenGenerateIsCalled.cs | 75 +++++++++++++++ src/Monify/Model/Encapsulated.cs | 8 ++ src/Monify/Model/UnaryOperator.cs | 42 +++++++++ ...medTypeSymbolExtensions.GetEncapsulated.cs | 3 + ...dTypeSymbolExtensions.GetUnaryOperators.cs | 74 +++++++++++++++ ...edTypeSymbolExtensions.HasUnaryOperator.cs | 36 +++++++ .../Strategies/UnaryOperatorStrategy.cs | 94 +++++++++++++++++++ src/Monify/TypeGenerator.cs | 1 + 9 files changed, 419 insertions(+) create mode 100644 src/Monify.Tests/Strategies/UnaryOperatorStrategyTests/WhenGenerateIsCalled.cs create mode 100644 src/Monify/Model/UnaryOperator.cs create mode 100644 src/Monify/Semantics/INamedTypeSymbolExtensions.GetUnaryOperators.cs create mode 100644 src/Monify/Semantics/INamedTypeSymbolExtensions.HasUnaryOperator.cs create mode 100644 src/Monify/Strategies/UnaryOperatorStrategy.cs diff --git a/src/Monify.Tests/Semantics/INamedTypeSymbolExtensionsTests/WhenGetEncapsulatedIsCalled.cs b/src/Monify.Tests/Semantics/INamedTypeSymbolExtensionsTests/WhenGetEncapsulatedIsCalled.cs index 1dd7046..a582354 100644 --- a/src/Monify.Tests/Semantics/INamedTypeSymbolExtensionsTests/WhenGetEncapsulatedIsCalled.cs +++ b/src/Monify.Tests/Semantics/INamedTypeSymbolExtensionsTests/WhenGetEncapsulatedIsCalled.cs @@ -145,4 +145,90 @@ public sealed class Value encapsulated[0].Conversions[1].Parameter.ShouldBe("string"); encapsulated[0].Conversions[1].Return.ShouldBe("global::Sample.Wrapper"); } + + [Fact] + public void GivenEncapsulatedTypeWithUnaryOperatorsThenTheyAreCaptured() + { + // Arrange + const string attribute = """ + namespace Monify + { + using System; + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] + internal sealed class MonifyAttribute : Attribute + { + public Type? Type { get; set; } + } + } + """; + + const string declarations = """ + using Monify; + + namespace Sample; + + [Monify(Type = typeof(Value))] + public sealed partial class Wrapper + { + } + + public sealed class Value + { + public static Value operator +(Value value) => value; + + public static Value operator -(Value value) => value; + + public static Value operator !(Value value) => value; + + public static Value operator ~(Value value) => value; + + public static Value operator ++(Value value) => value; + + public static Value operator --(Value value) => value; + + public static bool operator true(Value value) => true; + + public static bool operator false(Value value) => false; + } + """; + + CSharpParseOptions options = new(LanguageVersion.CSharp11); + SyntaxTree[] trees = + [ + CSharpSyntaxTree.ParseText(attribute, options), + CSharpSyntaxTree.ParseText(declarations, options), + ]; + + MetadataReference[] references = + [ + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + ]; + + var compilation = CSharpCompilation.Create( + "Sample", + trees, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + SemanticModel model = compilation.GetSemanticModel(trees[1]); + INamedTypeSymbol? wrapper = compilation.GetTypeByMetadataName("Sample.Wrapper"); + + _ = wrapper.ShouldNotBeNull(); + wrapper.HasMonify(model, out ITypeSymbol value).ShouldBeTrue(); + + // Act + ImmutableArray encapsulated = wrapper.GetEncapsulated(compilation, value); + + // Assert + encapsulated[0].UnaryOperators.Length.ShouldBe(8); + encapsulated[0].UnaryOperators.ShouldContain(operator => operator.Operator == "op_UnaryPlus" + && operator.IsReturnSubject + && operator.Return == "global::Sample.Wrapper" + && operator.Symbol == "+"); + encapsulated[0].UnaryOperators.ShouldContain(operator => operator.Operator == "op_True" + && !operator.IsReturnSubject + && operator.Return == "bool" + && operator.Symbol == "true"); + } } \ No newline at end of file diff --git a/src/Monify.Tests/Strategies/UnaryOperatorStrategyTests/WhenGenerateIsCalled.cs b/src/Monify.Tests/Strategies/UnaryOperatorStrategyTests/WhenGenerateIsCalled.cs new file mode 100644 index 0000000..91d0452 --- /dev/null +++ b/src/Monify.Tests/Strategies/UnaryOperatorStrategyTests/WhenGenerateIsCalled.cs @@ -0,0 +1,75 @@ +namespace Monify.Strategies.UnaryOperatorStrategyTests; + +using System.Linq; +using Monify.Model; +using Monify.Strategies; + +public sealed class WhenGenerateIsCalled +{ + [Fact] + public void GivenNoUnaryOperatorsThenNoSourceIsGenerated() + { + // Arrange + Subject subject = TestSubject.Create(); + subject.Encapsulated = [new Encapsulated { Type = "int", UnaryOperators = [] }]; + var strategy = new UnaryOperatorStrategy(); + + // Act + IEnumerable result = strategy.Generate(subject); + + // Assert + result.ShouldBeEmpty(); + } + + [Fact] + public void GivenUnaryOperatorsThenSourceIsGenerated() + { + // Arrange + Subject subject = TestSubject.Create(); + subject.Encapsulated = + [ + new Encapsulated + { + Type = "int", + UnaryOperators = + [ + new UnaryOperator + { + IsReturnSubject = true, + Operator = "op_UnaryNegation", + Return = subject.Qualification, + Symbol = "-", + }, + new UnaryOperator + { + IsReturnSubject = true, + Operator = "op_Increment", + Return = subject.Qualification, + Symbol = "++", + }, + new UnaryOperator + { + IsReturnSubject = false, + Operator = "op_True", + Return = "bool", + Symbol = "true", + }, + ], + }, + ]; + var strategy = new UnaryOperatorStrategy(); + + // Act + Source[] sources = strategy.Generate(subject).ToArray(); + + // Assert + sources.Length.ShouldBe(3); + sources[0].Hint.ShouldBe("UnaryOperators.00"); + sources[0].Code.ShouldContain("operator -"); + sources[0].Code.ShouldContain("new Sample(-subject._value)"); + sources[1].Code.ShouldContain("value = subject._value;"); + sources[1].Code.ShouldContain("new Sample(++value)"); + sources[2].Code.ShouldContain("operator true"); + sources[2].Code.ShouldContain("return (bool)subject._value ? true : false;"); + } +} diff --git a/src/Monify/Model/Encapsulated.cs b/src/Monify/Model/Encapsulated.cs index 8957e3f..1cfc266 100644 --- a/src/Monify/Model/Encapsulated.cs +++ b/src/Monify/Model/Encapsulated.cs @@ -25,6 +25,14 @@ internal sealed partial class Encapsulated /// public bool HasConstructor { get; set; } + /// + /// Gets or sets the unary operators supported by the encapsulated type. + /// + /// + /// The unary operators supported by the encapsulated type. + /// + public ImmutableArray UnaryOperators { get; set; } = ImmutableArray.Empty; + /// /// Gets or sets a value indicating whether a conversion from the subject to already exists. /// diff --git a/src/Monify/Model/UnaryOperator.cs b/src/Monify/Model/UnaryOperator.cs new file mode 100644 index 0000000..3f0b39f --- /dev/null +++ b/src/Monify/Model/UnaryOperator.cs @@ -0,0 +1,42 @@ +namespace Monify.Model; + +using Valuify; + +/// +/// Represents a unary operator to be forwarded from the encapsulated type. +/// +[Valuify] +internal sealed partial class UnaryOperator +{ + /// + /// Gets or sets a value indicating whether the unary operator should return the subject instead of the encapsulated type. + /// + /// + /// A value indicating whether the unary operator should return the subject instead of the encapsulated type. + /// + public bool IsReturnSubject { get; set; } + + /// + /// Gets or sets the name of the operator being forwarded. + /// + /// + /// The name of the operator being forwarded. + /// + public string Operator { get; set; } = string.Empty; + + /// + /// Gets or sets the unary operator token used in the generated source. + /// + /// + /// The unary operator token used in the generated source. + /// + public string Symbol { get; set; } = string.Empty; + + /// + /// Gets or sets the converted return type. + /// + /// + /// The converted return type. + /// + public string Return { get; set; } = string.Empty; +} diff --git a/src/Monify/Semantics/INamedTypeSymbolExtensions.GetEncapsulated.cs b/src/Monify/Semantics/INamedTypeSymbolExtensions.GetEncapsulated.cs index 6c4ae39..c8e46aa 100644 --- a/src/Monify/Semantics/INamedTypeSymbolExtensions.GetEncapsulated.cs +++ b/src/Monify/Semantics/INamedTypeSymbolExtensions.GetEncapsulated.cs @@ -37,10 +37,12 @@ public static ImmutableArray GetEncapsulated(this INamedTypeSymbol private static Encapsulated Catalog(IMethodSymbol[] constructors, Compilation compilation, INamedTypeSymbol subject, ITypeSymbol value) { ImmutableArray conversions = ImmutableArray.Empty; + ImmutableArray unaryOperators = ImmutableArray.Empty; if (value is INamedTypeSymbol encapsulated) { conversions = encapsulated.GetConversions(subject); + unaryOperators = encapsulated.GetUnaryOperators(subject); } return new Encapsulated @@ -55,6 +57,7 @@ private static Encapsulated Catalog(IMethodSymbol[] constructors, Compilation co IsEquatable = subject.IsEquatable(compilation, type: value), IsSequence = value.IsSequence(), Type = value.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + UnaryOperators = unaryOperators, }; } diff --git a/src/Monify/Semantics/INamedTypeSymbolExtensions.GetUnaryOperators.cs b/src/Monify/Semantics/INamedTypeSymbolExtensions.GetUnaryOperators.cs new file mode 100644 index 0000000..434c34d --- /dev/null +++ b/src/Monify/Semantics/INamedTypeSymbolExtensions.GetUnaryOperators.cs @@ -0,0 +1,74 @@ +namespace Monify.Semantics; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Monify.Model; + +/// +/// Provides extensions relating to . +/// +internal static partial class INamedTypeSymbolExtensions +{ + private static readonly IReadOnlyDictionary SupportedUnaryOperators = new Dictionary + { + ["op_Decrement"] = "--", + ["op_Increment"] = "++", + ["op_LogicalNot"] = "!", + ["op_OnesComplement"] = "~", + ["op_True"] = "true", + ["op_False"] = "false", + ["op_UnaryNegation"] = "-", + ["op_UnaryPlus"] = "+", + }; + + private const int ExpectedParametersForUnaryOperator = 1; + + /// + /// Identifies the unary operators declared by the type. + /// + /// The encapsulated type whose operators are to be inspected. + /// The subject type being generated. + /// The unary operators that should be forwarded to the subject. + public static ImmutableArray GetUnaryOperators(this INamedTypeSymbol encapsulated, INamedTypeSymbol subject) + { + ImmutableArray.Builder unaryOperators = ImmutableArray.CreateBuilder(); + + foreach (IMethodSymbol method in encapsulated.GetMembers().OfType()) + { + if (method.MethodKind != MethodKind.UserDefinedOperator || method.Parameters.Length != ExpectedParametersForUnaryOperator) + { + continue; + } + + if (!SupportedUnaryOperators.TryGetValue(method.Name, out string symbol)) + { + continue; + } + + if (!method.Parameters[0].Type.Equals(encapsulated, SymbolEqualityComparer.IncludeNullability)) + { + continue; + } + + bool isReturnSubject = method.ReturnType.Equals(encapsulated, SymbolEqualityComparer.IncludeNullability); + ITypeSymbol returnType = isReturnSubject ? subject : method.ReturnType; + + if (subject.HasUnaryOperator(method.Name, returnType)) + { + continue; + } + + unaryOperators.Add(new UnaryOperator + { + IsReturnSubject = isReturnSubject, + Operator = method.Name, + Return = returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + Symbol = symbol, + }); + } + + return unaryOperators.ToImmutable(); + } +} diff --git a/src/Monify/Semantics/INamedTypeSymbolExtensions.HasUnaryOperator.cs b/src/Monify/Semantics/INamedTypeSymbolExtensions.HasUnaryOperator.cs new file mode 100644 index 0000000..1426a20 --- /dev/null +++ b/src/Monify/Semantics/INamedTypeSymbolExtensions.HasUnaryOperator.cs @@ -0,0 +1,36 @@ +namespace Monify.Semantics; + +using Microsoft.CodeAnalysis; + +/// +/// Provides extensions relating to . +/// +internal static partial class INamedTypeSymbolExtensions +{ + /// + /// Determines whether or not the declares its own unary operator. + /// + /// + /// The to be checked. + /// + /// + /// The name of the operator to check. + /// + /// + /// The return type associated with the operator. + /// + /// + /// if the declares the operator, otherwise . + /// + public static bool HasUnaryOperator(this INamedTypeSymbol subject, string @operator, ITypeSymbol returnType) + { + return subject + .GetMembers() + .OfType() + .Any(method => method.MethodKind == MethodKind.UserDefinedOperator + && method.Name == @operator + && method.Parameters.Length == ExpectedParametersForUnaryOperator + && method.Parameters[0].Type.Equals(subject, SymbolEqualityComparer.IncludeNullability) + && method.ReturnType.Equals(returnType, SymbolEqualityComparer.IncludeNullability)); + } +} diff --git a/src/Monify/Strategies/UnaryOperatorStrategy.cs b/src/Monify/Strategies/UnaryOperatorStrategy.cs new file mode 100644 index 0000000..ace3510 --- /dev/null +++ b/src/Monify/Strategies/UnaryOperatorStrategy.cs @@ -0,0 +1,94 @@ +namespace Monify.Strategies; + +using System; +using Monify.Model; +using static Monify.Model.Subject; + +/// +/// Generates operators to forward unary operators supported by the encapsulated type. +/// +internal sealed class UnaryOperatorStrategy + : IStrategy +{ + private const string Indentation = " "; + + /// + public IEnumerable Generate(Subject subject) + { + for (int index = 0; index < subject.Encapsulated.Length; index++) + { + Encapsulated encapsulated = subject.Encapsulated[index]; + + if (encapsulated.UnaryOperators.IsDefaultOrEmpty) + { + continue; + } + + string hintPrefix = index == IndexForEncapsulatedValue + ? "UnaryOperators" + : $"UnaryOperators.Passthrough.Level{index:D2}"; + + for (int unaryIndex = 0; unaryIndex < encapsulated.UnaryOperators.Length; unaryIndex++) + { + UnaryOperator unary = encapsulated.UnaryOperators[unaryIndex]; + + string hint = $"{hintPrefix}.{unaryIndex:D2}"; + string code = CreateOperator(subject, encapsulated, unary); + + yield return new Source(code, hint); + } + } + } + + private static string CreateOperator(Subject subject, Encapsulated encapsulated, UnaryOperator unary) + { + (string operand, bool requiresValueCopy) = GetOperand(unary.Symbol); + string operation = ApplyOperator(unary.Symbol, operand); + string returnType = unary.IsReturnSubject ? subject.Qualification : unary.Return; + string valueDeclaration = requiresValueCopy + ? $"{encapsulated.Type} value = subject._value;{Environment.NewLine}{Environment.NewLine}{Indentation}" + : string.Empty; + string result = unary.IsReturnSubject + ? $"return new {subject.Qualification}({operation});" + : $"return ({returnType}){operation};"; + + return $$""" + {{subject.Declaration}} {{subject.Qualification}} + { + public static {{returnType}} operator {{unary.Symbol}}({{subject.Qualification}} subject) + { + if (ReferenceEquals(subject, null)) + { + throw new ArgumentNullException("subject"); + } + + {{valueDeclaration}}{{result}} + } + } + """; + } + + private static string ApplyOperator(string symbol, string operand) + { + return symbol switch + { + "-" => $"-{operand}", + "!" => $"!{operand}", + "++" => $"++{operand}", + "--" => $"--{operand}", + "~" => $"~{operand}", + "+" => $"+{operand}", + "true" => $"{operand} ? true : false", + "false" => $"{operand} ? false : true", + _ => throw new NotSupportedException($"Unsupported unary operator: {symbol}"), + }; + } + + private static (string Operand, bool RequiresValueCopy) GetOperand(string symbol) + { + bool requiresValueCopy = symbol is "++" or "--"; + string operand = requiresValueCopy ? "value" : "subject._value"; + + return (operand, requiresValueCopy); + } +} diff --git a/src/Monify/TypeGenerator.cs b/src/Monify/TypeGenerator.cs index 91a62f5..07fb9b5 100644 --- a/src/Monify/TypeGenerator.cs +++ b/src/Monify/TypeGenerator.cs @@ -26,6 +26,7 @@ public sealed class TypeGenerator new FieldStrategy(), new GetHashCodeStrategy(), new InequalityStrategy(), + new UnaryOperatorStrategy(), new ToStringStrategy(), }; From 9acee003699cc32047dd1cc3e050338ff6151212 Mon Sep 17 00:00:00 2001 From: Paul Martins <50200071+MooVC@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:36:29 +0000 Subject: [PATCH 2/4] Refine unary operator generation formatting --- .../Strategies/UnaryOperatorStrategy.cs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Monify/Strategies/UnaryOperatorStrategy.cs b/src/Monify/Strategies/UnaryOperatorStrategy.cs index ace3510..45cde57 100644 --- a/src/Monify/Strategies/UnaryOperatorStrategy.cs +++ b/src/Monify/Strategies/UnaryOperatorStrategy.cs @@ -10,7 +10,6 @@ namespace Monify.Strategies; internal sealed class UnaryOperatorStrategy : IStrategy { - private const string Indentation = " "; /// public IEnumerable Generate(Subject subject) @@ -45,13 +44,30 @@ private static string CreateOperator(Subject subject, Encapsulated encapsulated, (string operand, bool requiresValueCopy) = GetOperand(unary.Symbol); string operation = ApplyOperator(unary.Symbol, operand); string returnType = unary.IsReturnSubject ? subject.Qualification : unary.Return; - string valueDeclaration = requiresValueCopy - ? $"{encapsulated.Type} value = subject._value;{Environment.NewLine}{Environment.NewLine}{Indentation}" - : string.Empty; string result = unary.IsReturnSubject ? $"return new {subject.Qualification}({operation});" : $"return ({returnType}){operation};"; + if (requiresValueCopy) + { + return $$""" + {{subject.Declaration}} {{subject.Qualification}} + { + public static {{returnType}} operator {{unary.Symbol}}({{subject.Qualification}} subject) + { + if (ReferenceEquals(subject, null)) + { + throw new ArgumentNullException("subject"); + } + + {{encapsulated.Type}} value = subject._value; + + {{result}} + } + } + """; + } + return $$""" {{subject.Declaration}} {{subject.Qualification}} { @@ -62,7 +78,7 @@ private static string CreateOperator(Subject subject, Encapsulated encapsulated, throw new ArgumentNullException("subject"); } - {{valueDeclaration}}{{result}} + {{result}} } } """; From e8e195e086aedda0b512064242735fe5c24c43a8 Mon Sep 17 00:00:00 2001 From: Paul Martins Date: Fri, 28 Nov 2025 18:54:40 +0000 Subject: [PATCH 3/4] Whitespace and symbol cleanup --- CHANGELOG.md | 23 +++- .../WhenGetEncapsulatedIsCalled.cs | 18 +-- .../WhenGenerateIsCalled.cs | 2 +- src/Monify/Model/Encapsulated.cs | 29 ++++- src/Monify/Model/UnaryOperator.cs | 2 +- src/Monify/Monify.csproj | 9 ++ ...dTypeSymbolExtensions.GetUnaryOperators.cs | 11 +- ...edTypeSymbolExtensions.HasUnaryOperator.cs | 2 +- ...naryOperatorStrategy.Resources.Designer.cs | 72 ++++++++++ .../UnaryOperatorStrategy.Resources.resx | 123 ++++++++++++++++++ .../Strategies/UnaryOperatorStrategy.cs | 13 +- src/Monify/TypeGenerator.cs | 4 +- 12 files changed, 272 insertions(+), 36 deletions(-) create mode 100644 src/Monify/Strategies/UnaryOperatorStrategy.Resources.Designer.cs create mode 100644 src/Monify/Strategies/UnaryOperatorStrategy.Resources.resx diff --git a/CHANGELOG.md b/CHANGELOG.md index adb21db..0e5431e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,27 +4,37 @@ All notable changes to Monify will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +# [Unreleased] + +## Added + +- Conversions supported by the encapsulated type are now propagated to the `Monify` type (#44). +- Unary operators supported by the encapsulated type are now propagated to the `Monify` type (#44). + +## Fixed -### Fixed - MONFY03 should no longer be raised for types that do not explicitly capture state (#22). -## [1.2.0] - 2025-11-16 +# [1.2.0] - 2025-11-16 + +## Added -### Added - When the encapsulated type is for a type that also uses `Monify`, constructors, equality checks and implicit conversion operators to enable conversion directly to the nested encapsulated type (#34). -### Fixed +## Fixed + - Equality checks now correctly handle null values for reference types. # [1.1.4] - 2025-11-03 ## Fixed + - Instances of `ImmutableArray<>` are now explicitly checked for default as part of the equality checks (#29). # [1.1.3] - 2025-10-31 ## Fixed + - MONFY03 is no longer raised if the encapsulated type cannot be determined (#22). - Equality checks involing an encapsulated array containing identical values now yield true (#19). - `ToString` no longer throws a `FormatException`. @@ -32,16 +42,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # [1.1.2] - 2025-10-20 ## Fixed + - ConvertFrom now uses `ReferenceEquals` instead of `==` to avoid ambiguity when the encapsulated type is a reference type (#17). # [1.1.1] - 2025-10-17 ## Fixed + - Set **Valuify** to Version **1.7.0** instead of **1.7.0-rc.1**. # [1.1.0] - 2025-10-17 ## Changed + - Reverted **Microsoft.CodeAnalysis.Analyzers** to Version **3.11.0** to maximize compatibility with Visual Studio 2022. - Reverted **Microsoft.CodeAnalysis.CSharp** to Version **4.4.0** to maximize compatibility with Visual Studio 2022. - Reverted **Microsoft.CodeAnalysis.CSharp.Workspaces** Version **4.4.0** to maximize compatibility with Visual Studio 2022. diff --git a/src/Monify.Tests/Semantics/INamedTypeSymbolExtensionsTests/WhenGetEncapsulatedIsCalled.cs b/src/Monify.Tests/Semantics/INamedTypeSymbolExtensionsTests/WhenGetEncapsulatedIsCalled.cs index a582354..910140b 100644 --- a/src/Monify.Tests/Semantics/INamedTypeSymbolExtensionsTests/WhenGetEncapsulatedIsCalled.cs +++ b/src/Monify.Tests/Semantics/INamedTypeSymbolExtensionsTests/WhenGetEncapsulatedIsCalled.cs @@ -222,13 +222,15 @@ public sealed class Value // Assert encapsulated[0].UnaryOperators.Length.ShouldBe(8); - encapsulated[0].UnaryOperators.ShouldContain(operator => operator.Operator == "op_UnaryPlus" - && operator.IsReturnSubject - && operator.Return == "global::Sample.Wrapper" - && operator.Symbol == "+"); - encapsulated[0].UnaryOperators.ShouldContain(operator => operator.Operator == "op_True" - && !operator.IsReturnSubject - && operator.Return == "bool" - && operator.Symbol == "true"); + + encapsulated[0].UnaryOperators.ShouldContain(@operator => @operator.Operator == "op_UnaryPlus" + && @operator.IsReturnSubject + && @operator.Return == "global::Sample.Wrapper" + && @operator.Symbol == "+"); + + encapsulated[0].UnaryOperators.ShouldContain(@operator => @operator.Operator == "op_True" + && !@operator.IsReturnSubject + && @operator.Return == "bool" + && @operator.Symbol == "true"); } } \ No newline at end of file diff --git a/src/Monify.Tests/Strategies/UnaryOperatorStrategyTests/WhenGenerateIsCalled.cs b/src/Monify.Tests/Strategies/UnaryOperatorStrategyTests/WhenGenerateIsCalled.cs index 91d0452..39f5f16 100644 --- a/src/Monify.Tests/Strategies/UnaryOperatorStrategyTests/WhenGenerateIsCalled.cs +++ b/src/Monify.Tests/Strategies/UnaryOperatorStrategyTests/WhenGenerateIsCalled.cs @@ -72,4 +72,4 @@ public void GivenUnaryOperatorsThenSourceIsGenerated() sources[2].Code.ShouldContain("operator true"); sources[2].Code.ShouldContain("return (bool)subject._value ? true : false;"); } -} +} \ No newline at end of file diff --git a/src/Monify/Model/Encapsulated.cs b/src/Monify/Model/Encapsulated.cs index 1cfc266..91ac6c4 100644 --- a/src/Monify/Model/Encapsulated.cs +++ b/src/Monify/Model/Encapsulated.cs @@ -26,26 +26,27 @@ internal sealed partial class Encapsulated public bool HasConstructor { get; set; } /// - /// Gets or sets the unary operators supported by the encapsulated type. + /// Gets or sets a value indicating whether a conversion from the subject to already exists. /// /// - /// The unary operators supported by the encapsulated type. + /// A value indicating whether a conversion from the subject to already exists. /// - public ImmutableArray UnaryOperators { get; set; } = ImmutableArray.Empty; - - /// - /// Gets or sets a value indicating whether a conversion from the subject to already exists. - /// public bool HasConversionFrom { get; set; } /// /// Gets or sets a value indicating whether a conversion from to the subject already exists. /// + /// + /// A value indicating whether a conversion from to the subject already exists. + /// public bool HasConversionTo { get; set; } /// /// Gets or sets a value indicating whether the subject already defines an equality operator for . /// + /// + /// A value indicating whether the subject already defines an equality operator for . + /// public bool HasEqualityOperator { get; set; } /// @@ -59,6 +60,9 @@ internal sealed partial class Encapsulated /// /// Gets or sets a value indicating whether the subject already defines an inequality operator for . /// + /// + /// A value indicating whether the subject already defines an inequality operator for . + /// public bool HasInequalityOperator { get; set; } /// @@ -80,5 +84,16 @@ internal sealed partial class Encapsulated /// /// Gets or sets the fully qualified name of the related type these operator checks apply to. /// + /// + /// The fully qualified name of the related type these operator checks apply to. + /// public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets the unary operators supported by the encapsulated type. + /// + /// + /// The unary operators supported by the encapsulated type. + /// + public ImmutableArray UnaryOperators { get; set; } = ImmutableArray.Empty; } \ No newline at end of file diff --git a/src/Monify/Model/UnaryOperator.cs b/src/Monify/Model/UnaryOperator.cs index 3f0b39f..16ff8c8 100644 --- a/src/Monify/Model/UnaryOperator.cs +++ b/src/Monify/Model/UnaryOperator.cs @@ -39,4 +39,4 @@ internal sealed partial class UnaryOperator /// The converted return type. /// public string Return { get; set; } = string.Empty; -} +} \ No newline at end of file diff --git a/src/Monify/Monify.csproj b/src/Monify/Monify.csproj index f844c68..e018cd8 100644 --- a/src/Monify/Monify.csproj +++ b/src/Monify/Monify.csproj @@ -15,11 +15,20 @@ True AttributeAnalyzer.Resources.resx + + True + True + UnaryOperatorStrategy.Resources.resx + ResXFileCodeGenerator AttributeAnalyzer.Resources.Designer.cs + + ResXFileCodeGenerator + UnaryOperatorStrategy.Resources.Designer.cs + \ No newline at end of file diff --git a/src/Monify/Semantics/INamedTypeSymbolExtensions.GetUnaryOperators.cs b/src/Monify/Semantics/INamedTypeSymbolExtensions.GetUnaryOperators.cs index 434c34d..eb5ed68 100644 --- a/src/Monify/Semantics/INamedTypeSymbolExtensions.GetUnaryOperators.cs +++ b/src/Monify/Semantics/INamedTypeSymbolExtensions.GetUnaryOperators.cs @@ -1,6 +1,5 @@ namespace Monify.Semantics; -using System; using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis; @@ -11,7 +10,9 @@ namespace Monify.Semantics; /// internal static partial class INamedTypeSymbolExtensions { - private static readonly IReadOnlyDictionary SupportedUnaryOperators = new Dictionary + private const int ExpectedParametersForUnaryOperator = 1; + + private static readonly IReadOnlyDictionary _supported = new Dictionary { ["op_Decrement"] = "--", ["op_Increment"] = "++", @@ -23,8 +24,6 @@ internal static partial class INamedTypeSymbolExtensions ["op_UnaryPlus"] = "+", }; - private const int ExpectedParametersForUnaryOperator = 1; - /// /// Identifies the unary operators declared by the type. /// @@ -42,7 +41,7 @@ public static ImmutableArray GetUnaryOperators(this INamedTypeSym continue; } - if (!SupportedUnaryOperators.TryGetValue(method.Name, out string symbol)) + if (!_supported.TryGetValue(method.Name, out string symbol)) { continue; } @@ -71,4 +70,4 @@ public static ImmutableArray GetUnaryOperators(this INamedTypeSym return unaryOperators.ToImmutable(); } -} +} \ No newline at end of file diff --git a/src/Monify/Semantics/INamedTypeSymbolExtensions.HasUnaryOperator.cs b/src/Monify/Semantics/INamedTypeSymbolExtensions.HasUnaryOperator.cs index 1426a20..cc37d9d 100644 --- a/src/Monify/Semantics/INamedTypeSymbolExtensions.HasUnaryOperator.cs +++ b/src/Monify/Semantics/INamedTypeSymbolExtensions.HasUnaryOperator.cs @@ -33,4 +33,4 @@ public static bool HasUnaryOperator(this INamedTypeSymbol subject, string @opera && method.Parameters[0].Type.Equals(subject, SymbolEqualityComparer.IncludeNullability) && method.ReturnType.Equals(returnType, SymbolEqualityComparer.IncludeNullability)); } -} +} \ No newline at end of file diff --git a/src/Monify/Strategies/UnaryOperatorStrategy.Resources.Designer.cs b/src/Monify/Strategies/UnaryOperatorStrategy.Resources.Designer.cs new file mode 100644 index 0000000..9f1b079 --- /dev/null +++ b/src/Monify/Strategies/UnaryOperatorStrategy.Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Monify.Strategies { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UnaryOperatorStrategy_Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UnaryOperatorStrategy_Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Monify.Strategies.UnaryOperatorStrategy.Resources", typeof(UnaryOperatorStrategy_Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Unary operator of type `{0}` is not supported.. + /// + internal static string ApplyOperatorNotSupported { + get { + return ResourceManager.GetString("ApplyOperatorNotSupported", resourceCulture); + } + } + } +} diff --git a/src/Monify/Strategies/UnaryOperatorStrategy.Resources.resx b/src/Monify/Strategies/UnaryOperatorStrategy.Resources.resx new file mode 100644 index 0000000..7265ad9 --- /dev/null +++ b/src/Monify/Strategies/UnaryOperatorStrategy.Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unary operator of type `{0}` is not supported. + + \ No newline at end of file diff --git a/src/Monify/Strategies/UnaryOperatorStrategy.cs b/src/Monify/Strategies/UnaryOperatorStrategy.cs index 45cde57..4f1071d 100644 --- a/src/Monify/Strategies/UnaryOperatorStrategy.cs +++ b/src/Monify/Strategies/UnaryOperatorStrategy.cs @@ -3,14 +3,13 @@ namespace Monify.Strategies; using System; using Monify.Model; using static Monify.Model.Subject; - +using static Monify.Strategies.UnaryOperatorStrategy_Resources; /// /// Generates operators to forward unary operators supported by the encapsulated type. /// internal sealed class UnaryOperatorStrategy : IStrategy { - /// public IEnumerable Generate(Subject subject) { @@ -43,7 +42,11 @@ private static string CreateOperator(Subject subject, Encapsulated encapsulated, { (string operand, bool requiresValueCopy) = GetOperand(unary.Symbol); string operation = ApplyOperator(unary.Symbol, operand); - string returnType = unary.IsReturnSubject ? subject.Qualification : unary.Return; + + string returnType = unary.IsReturnSubject + ? subject.Qualification + : unary.Return; + string result = unary.IsReturnSubject ? $"return new {subject.Qualification}({operation});" : $"return ({returnType}){operation};"; @@ -96,7 +99,7 @@ private static string ApplyOperator(string symbol, string operand) "+" => $"+{operand}", "true" => $"{operand} ? true : false", "false" => $"{operand} ? false : true", - _ => throw new NotSupportedException($"Unsupported unary operator: {symbol}"), + _ => throw new NotSupportedException(string.Format(ApplyOperatorNotSupported, symbol)), }; } @@ -107,4 +110,4 @@ private static (string Operand, bool RequiresValueCopy) GetOperand(string symbol return (operand, requiresValueCopy); } -} +} \ No newline at end of file diff --git a/src/Monify/TypeGenerator.cs b/src/Monify/TypeGenerator.cs index 07fb9b5..6948a4f 100644 --- a/src/Monify/TypeGenerator.cs +++ b/src/Monify/TypeGenerator.cs @@ -17,17 +17,17 @@ public sealed class TypeGenerator private static readonly IStrategy[] _strategies = new IStrategy[] { new ConstructorStrategy(), + new ConversionOperatorStrategy(), new ConvertFromStrategy(), new ConvertToStrategy(), - new ConversionOperatorStrategy(), new EqualityStrategy(), new EqualsStrategy(), new EquatableStrategy(), new FieldStrategy(), new GetHashCodeStrategy(), new InequalityStrategy(), - new UnaryOperatorStrategy(), new ToStringStrategy(), + new UnaryOperatorStrategy(), }; /// From dd1d3e17f2ce03c83ef8e5fb67fe00be693aac80 Mon Sep 17 00:00:00 2001 From: Paul Martins Date: Fri, 28 Nov 2025 18:58:05 +0000 Subject: [PATCH 4/4] Fixed comment spacing --- src/Monify/Strategies/UnaryOperatorStrategy.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Monify/Strategies/UnaryOperatorStrategy.cs b/src/Monify/Strategies/UnaryOperatorStrategy.cs index 4f1071d..c5fcece 100644 --- a/src/Monify/Strategies/UnaryOperatorStrategy.cs +++ b/src/Monify/Strategies/UnaryOperatorStrategy.cs @@ -4,6 +4,7 @@ namespace Monify.Strategies; using Monify.Model; using static Monify.Model.Subject; using static Monify.Strategies.UnaryOperatorStrategy_Resources; + /// /// Generates operators to forward unary operators supported by the encapsulated type. ///