Skip to content
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
23 changes: 18 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,57 @@ 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`.

# [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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,92 @@ 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> 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");
}
}
Original file line number Diff line number Diff line change
@@ -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<Source> 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;");
}
}
23 changes: 23 additions & 0 deletions src/Monify/Model/Encapsulated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,25 @@ internal sealed partial class Encapsulated
/// <summary>
/// Gets or sets a value indicating whether a conversion from the subject to <see cref="Type"/> already exists.
/// </summary>
/// <value>
/// A value indicating whether a conversion from the subject to <see cref="Type"/> already exists.
/// </value>
public bool HasConversionFrom { get; set; }

/// <summary>
/// Gets or sets a value indicating whether a conversion from <see cref="Type"/> to the subject already exists.
/// </summary>
/// <value>
/// A value indicating whether a conversion from <see cref="Type"/> to the subject already exists.
/// </value>
public bool HasConversionTo { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the subject already defines an equality operator for <see cref="Type"/>.
/// </summary>
/// <value>
/// A value indicating whether the subject already defines an equality operator for <see cref="Type"/>.
/// </value>
public bool HasEqualityOperator { get; set; }

/// <summary>
Expand All @@ -51,6 +60,9 @@ internal sealed partial class Encapsulated
/// <summary>
/// Gets or sets a value indicating whether the subject already defines an inequality operator for <see cref="Type"/>.
/// </summary>
/// <value>
/// A value indicating whether the subject already defines an inequality operator for <see cref="Type"/>.
/// </value>
public bool HasInequalityOperator { get; set; }

/// <summary>
Expand All @@ -72,5 +84,16 @@ internal sealed partial class Encapsulated
/// <summary>
/// Gets or sets the fully qualified name of the related type these operator checks apply to.
/// </summary>
/// <value>
/// The fully qualified name of the related type these operator checks apply to.
/// </value>
public string Type { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the unary operators supported by the encapsulated type.
/// </summary>
/// <value>
/// The unary operators supported by the encapsulated type.
/// </value>
public ImmutableArray<UnaryOperator> UnaryOperators { get; set; } = ImmutableArray<UnaryOperator>.Empty;
}
42 changes: 42 additions & 0 deletions src/Monify/Model/UnaryOperator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Monify.Model;

using Valuify;

/// <summary>
/// Represents a unary operator to be forwarded from the encapsulated type.
/// </summary>
[Valuify]
internal sealed partial class UnaryOperator
{
/// <summary>
/// Gets or sets a value indicating whether the unary operator should return the subject instead of the encapsulated type.
/// </summary>
/// <value>
/// A value indicating whether the unary operator should return the subject instead of the encapsulated type.
/// </value>
public bool IsReturnSubject { get; set; }

/// <summary>
/// Gets or sets the name of the operator being forwarded.
/// </summary>
/// <value>
/// The name of the operator being forwarded.
/// </value>
public string Operator { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the unary operator token used in the generated source.
/// </summary>
/// <value>
/// The unary operator token used in the generated source.
/// </value>
public string Symbol { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the converted return type.
/// </summary>
/// <value>
/// The converted return type.
/// </value>
public string Return { get; set; } = string.Empty;
}
9 changes: 9 additions & 0 deletions src/Monify/Monify.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,20 @@
<AutoGen>True</AutoGen>
<DependentUpon>AttributeAnalyzer.Resources.resx</DependentUpon>
</Compile>
<Compile Update="Strategies\UnaryOperatorStrategy.Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>UnaryOperatorStrategy.Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="AttributeAnalyzer.Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>AttributeAnalyzer.Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Strategies\UnaryOperatorStrategy.Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>UnaryOperatorStrategy.Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ public static ImmutableArray<Encapsulated> GetEncapsulated(this INamedTypeSymbol
private static Encapsulated Catalog(IMethodSymbol[] constructors, Compilation compilation, INamedTypeSymbol subject, ITypeSymbol value)
{
ImmutableArray<Conversion> conversions = ImmutableArray<Conversion>.Empty;
ImmutableArray<UnaryOperator> unaryOperators = ImmutableArray<UnaryOperator>.Empty;

if (value is INamedTypeSymbol encapsulated)
{
conversions = encapsulated.GetConversions(subject);
unaryOperators = encapsulated.GetUnaryOperators(subject);
}

return new Encapsulated
Expand All @@ -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,
};
}

Expand Down
Loading