Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/dotnet-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
run: dotnet pack --configuration Release -p:Version=$RELEASE_VERSION --no-build --no-restore src/ObjectAssertions/ObjectAssertions.csproj
- name: Create the package
run: dotnet pack --configuration Release -p:Version=$RELEASE_VERSION --no-build --no-restore src/ObjectAssertions.Abstractions/ObjectAssertions.Abstractions.csproj
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: ObjectAssertions
path: |
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/dotnet-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
# - name: Test
# run: dotnet test --no-build --verbosity normal
- name: Test
run: dotnet test --configuration Release --no-build --no-restore --verbosity detailed
- name: Create the package
run: dotnet pack --configuration Release --no-build --no-restore src/ObjectAssertions/ObjectAssertions.csproj
- name: Create the package
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,5 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
/samples/ObjectAssertions.Sample/GeneratedFiles/
/.idea/
/tests/ObjectAssertions.Generator.Tests/GeneratedFiles/
9 changes: 9 additions & 0 deletions ObjectAssertions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
global.json = global.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E00904EA-5E71-463A-8968-DF604409A57F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObjectAssertions.Generator.Tests", "tests\ObjectAssertions.Generator.Tests\ObjectAssertions.Generator.Tests.csproj", "{C7CD5C18-0585-4CB6-8502-EA76968F9E9E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -38,6 +42,10 @@ Global
{B363E0E8-B785-4BB2-8ED0-2F6FD6266CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B363E0E8-B785-4BB2-8ED0-2F6FD6266CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B363E0E8-B785-4BB2-8ED0-2F6FD6266CAE}.Release|Any CPU.Build.0 = Release|Any CPU
{C7CD5C18-0585-4CB6-8502-EA76968F9E9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C7CD5C18-0585-4CB6-8502-EA76968F9E9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C7CD5C18-0585-4CB6-8502-EA76968F9E9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C7CD5C18-0585-4CB6-8502-EA76968F9E9E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -46,6 +54,7 @@ Global
{330EAF7F-15DD-4DDB-8908-E51532814B74} = {C9BA6D2C-1F31-42B2-8805-0CAC4FDC35EB}
{AA60BB21-5D3B-410D-A460-83417AA001E8} = {4CCE88B3-F55E-4804-BF29-D4DB88A09694}
{B363E0E8-B785-4BB2-8ED0-2F6FD6266CAE} = {C9BA6D2C-1F31-42B2-8805-0CAC4FDC35EB}
{C7CD5C18-0585-4CB6-8502-EA76968F9E9E} = {E00904EA-5E71-463A-8968-DF604409A57F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {08EFB033-621F-4496-8414-AF0CEEEE58E6}
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.405",
"version": "10.0.300",
"rollForward": "latestFeature"
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
Expand All @@ -23,9 +23,9 @@


<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
41 changes: 41 additions & 0 deletions samples/ObjectAssertions.Sample/Tests/AbstractClasses.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using ObjectAssertions.Abstractions;
using ObjectAssertions.Sample;
using System;
using Xunit;
using static ObjectAssertions.Sample.ObjectToAssert;

namespace ObjectAssertions.Sample
{
public abstract class AbstractClass
{
public abstract string Property { get; }
}

public class ImplementationClass : AbstractClass
{
public override string Property => "value";
}


public class ObjectAssertionsBugRepro
{
[Fact]
public void ImplementationClass_NotRequires_AssertionOfAbstractProperty()
{
var derivedClass = new ImplementationClass();

var assertions = new DerivedClassAssertions(derivedClass)
{
Property = a => Assert.NotNull(a)
};

assertions.Assert();
}
}



public partial class DerivedClassAssertions : IAssertsAllPropertiesOf<ImplementationClass>
{
}
}
3 changes: 2 additions & 1 deletion src/ObjectAssertions/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ OBJASS0001 | Object Assertions | Error | Diagnostics
OBJASS0002 | Object Assertions | Error | Diagnostics
OBJASS0003 | Object Assertions | Error | Diagnostics
OBJASS0004 | Object Assertions | Error | Diagnostics
OBJASS0005 | Object Assertions | Error | Diagnostics
OBJASS0005 | Object Assertions | Error | Diagnostics
OBJASS0006 | Object Assertions | Error | Diagnostics
34 changes: 28 additions & 6 deletions src/ObjectAssertions/Configuration/ConfigurationCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,41 @@ public ConfigurationCollector(GeneratorExecutionContext context, SemanticModel s

private ObjectAssertionsConfiguration? Collect()
{
var assertedType = (INamedTypeSymbol)_assertAllPropertiesOfInterface.TypeArguments[0];
if (_assertAllPropertiesOfInterface.TypeArguments[0] is not INamedTypeSymbol assertedType)
{
return null;
}

if (assertedType.TypeKind == TypeKind.Error)
{
// This doesn't really matter in editor, but we want tests to behave nicely
return null;
}

var publicMembers = assertedType.GetBaseTypesAndThis().SelectMany(n => n.GetMembers()).Where(n => n.DeclaredAccessibility == Accessibility.Public);
var assertedTypePublicProperties = assertedType.GetBaseTypesAndThis().SelectMany(n => n.GetMembers()).Where(n => n.DeclaredAccessibility == Accessibility.Public);

var properties = publicMembers.OfType<IPropertySymbol>();
var properties = assertedTypePublicProperties.OfType<IPropertySymbol>();

var fields = publicMembers.OfType<IFieldSymbol>()
var fields = assertedTypePublicProperties.OfType<IFieldSymbol>()
.Where(n => n.AssociatedSymbol == null && n.DeclaredAccessibility == Accessibility.Public)
.ToList();

bool memberFilter(ISymbol symbol)
{
if (symbol.IsAbstract)
{
// We do not supprot abstract classes due to complexity of handling them
// We could have a feature to assert abstract members, but then abstract class can override member and it's hard to detect which is top level
// If abstract classes are not supported, we can just exclude all abstract properties
return false;
}
return true;
}


var members = new List<ISymbol>();
members.AddRange(properties);
members.AddRange(fields);
members.AddRange(properties.Where(memberFilter));
members.AddRange(fields.Where(memberFilter));

string assertionFieldName = "_objectToAssert";

Expand Down
14 changes: 10 additions & 4 deletions src/ObjectAssertions/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,26 @@ public static class Diagnostics

public static readonly DiagnosticDescriptor MultipleInterfaceDeclarations = new(
"OBJASS0004",
"Multiple interface decalrations",
"{0} can implement " + typeof(IAssertsAllPropertiesOf<>).Name + " only once.",
"Multiple interface declarations",
"{0} can implement IAssertsAllPropertiesOf<TObject> only once",
"Object Assertions",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor UnknownTypeName = new(
"OBJASS0005",
"Unsupported type",
"Argument of " + typeof(IAssertsAllPropertiesOf<>).Name + " in not regular type.",
"Argument of IAssertsAllPropertiesOf<TObject> is not a regular type",
"Object Assertions",
DiagnosticSeverity.Error,
true);


public static readonly DiagnosticDescriptor AbstractClassesAreNotSupported = new(
"OBJASS0006",
"Unsupported type",
"Argument of " + typeof(IAssertsAllPropertiesOf<>).Name + " in abstract class. Library does not support abstract classes, use concrete type instead.",
"Object Assertions",
DiagnosticSeverity.Error,
true);
}
}
11 changes: 10 additions & 1 deletion src/ObjectAssertions/Generator/GenerationOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public void Generate()

private ObjectAssertionsConfiguration? IsAssertionsClass(TypeDeclarationSyntax classDeclaration, SemanticModel semanticModel)
{
var markerInterface = semanticModel.Compilation.GetTypeByMetadataName(typeof(IAssertsAllPropertiesOf<>).FullName)?.ConstructUnboundGenericType();
var markerInterface = semanticModel.Compilation.GetTypeByMetadataName("ObjectAssertions.Abstractions.IAssertsAllPropertiesOf`1")?.ConstructUnboundGenericType();
var assertAllPropertiesOfInterfaces = classDeclaration.BaseList?.Types.Select(baseType =>
{
if (baseType is not SimpleBaseTypeSyntax simpleBaseTypeSyntax)
Expand Down Expand Up @@ -117,6 +117,15 @@ public void Generate()
return null;
}

if (namedTypeSymbol.IsAbstract)
{
var abstractClassDiagnostic =
Diagnostic.Create(Diagnostics.AbstractClassesAreNotSupported, classDeclaration.GetLocation(), classDeclaration.Identifier.Text);
_context.ReportDiagnostic(abstractClassDiagnostic);
return null;
}


var isPartial = classDeclaration.Modifiers.Any(n => n.IsKeyword() && n.ToString() == "partial");
if (!isPartial)
{
Expand Down
34 changes: 20 additions & 14 deletions src/ObjectAssertions/Generator/MemberGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,27 @@ internal static MemberDeclarationSyntax GenerateFromField(SemanticModel semantic
{
bool isObsolete = ObsoleteMemberHandler.IsObsolete(fieldSymbol, out string obsoleteMessage);
var property = GenerateProperty(semanticModel, (INamedTypeSymbol)fieldSymbol.Type, fieldSymbol.Name);

if (isObsolete)
{
var obsoleteAttribute = ObsoleteMemberHandler.GenerateObsoleteAttribute(obsoleteMessage);
return property.AddAttributeLists(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(obsoleteAttribute)));
}

return property;
}

internal static MemberDeclarationSyntax GenerateFromProperty(SemanticModel semanticModel, IPropertySymbol propertySymbol)
{
bool isObsolete = ObsoleteMemberHandler.IsObsolete(propertySymbol, out string obsoleteMessage);
var property = GenerateProperty(semanticModel, propertySymbol.Type, propertySymbol.Name);

if (isObsolete)
{
var obsoleteAttribute = ObsoleteMemberHandler.GenerateObsoleteAttribute(obsoleteMessage);
return property.AddAttributeLists(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(obsoleteAttribute)));
}

return property;
}

Expand Down Expand Up @@ -91,15 +91,21 @@ internal static MethodDeclarationSyntax GenerateAssertMethod(SemanticModel seman

internal static MethodDeclarationSyntax GenerateCollectAssertionsMethod(SemanticModel semanticModel, string methodName, string assertionFieldName, IReadOnlyCollection<MemberInfo> members)
{
var memberExpressions = members.Select(m => SyntaxFactory.ParseExpression("() => " + m.Symbol.Name + "(" + assertionFieldName + "." + m.Symbol.Name + ")"));

var arrayOfActionType = SyntaxFactory.ArrayType(SyntaxFactory.ParseName("System.Action[]"));
var commas = Enumerable.Repeat(SyntaxFactory.Token(SyntaxKind.CommaToken), members.Count - 1);

var initializer = SyntaxFactory
.InitializerExpression(SyntaxKind.ArrayInitializerExpression);

if (members.Count > 0)
{
var memberExpressions = members.Select(m => SyntaxFactory.ParseExpression("() => " + m.Symbol.Name + "(" + assertionFieldName + "." + m.Symbol.Name + ")"));
var commas = Enumerable.Repeat(SyntaxFactory.Token(SyntaxKind.CommaToken), members.Count - 1);
initializer = initializer
.WithExpressions(SyntaxFactory.SeparatedList(memberExpressions, commas));
}

var arrayInitializationSyntax = SyntaxFactory
.ArrayCreationExpression(SyntaxFactory.Token(SyntaxKind.NewKeyword), arrayOfActionType, SyntaxFactory
.InitializerExpression(SyntaxKind.ArrayInitializerExpression)
.WithExpressions(SyntaxFactory.SeparatedList(memberExpressions, commas)));
.ArrayCreationExpression(SyntaxFactory.Token(SyntaxKind.NewKeyword), arrayOfActionType, initializer);

var returnArraySyntax = SyntaxFactory.ReturnStatement(arrayInitializationSyntax);

Expand All @@ -112,7 +118,7 @@ internal static MemberDeclarationSyntax GenerateFromMemberInfo(SemanticModel sem
{
var symbol = memberInfo.Symbol;
MemberDeclarationSyntax property;

if (symbol is IFieldSymbol fieldSymbol)
{
property = GenerateProperty(semanticModel, (INamedTypeSymbol)fieldSymbol.Type, fieldSymbol.Name);
Expand All @@ -125,16 +131,16 @@ internal static MemberDeclarationSyntax GenerateFromMemberInfo(SemanticModel sem
{
throw new NotSupportedException($"Unsupported symbol type: {symbol.GetType().Name}");
}

if (memberInfo.ObsoleteInfo != null)
{
var obsoleteAttribute = ObsoleteMemberHandler.GenerateObsoleteAttribute(
memberInfo.ObsoleteInfo.Message);

return property.AddAttributeLists(
SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(obsoleteAttribute)));
}

return property;
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/ObjectAssertions/ObjectAssertions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<Title>Object Assertions</Title>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\ObjectAssertions.Abstractions\IAssertsAllPropertiesOf.cs" Link="IAssertsAllPropertiesOf.cs" />
</ItemGroup>


<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ObjectAssertions.Abstractions\ObjectAssertions.Abstractions.csproj" />
</ItemGroup>


<ItemGroup>
<None Include="..\..\LICENSE" Pack="true" PackagePath="" />
Expand Down
Loading