Skip to content

Release 1.2.0: Self-descriptors #15

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

Merged
merged 9 commits into from
Sep 10, 2024
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Please provide a brief description of the changes you have made, including the p

Please ensure the following are true for your PR:

- [ ] I have followed the project's [coding style guidelines](CONTRIBUTING.md).
- [ ] I have followed the project's [coding style guidelines](/.github/CONTRIBUTING.md).
- [ ] I have added tests that prove my fix is effective or that my feature works.
- [ ] I have verified that new and existing unit tests pass locally with my changes.
- [ ] I have updated the documentation (if applicable).
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.2.0] - 2024-09-10

## Added

- FLTFY07 is now raised if the usage of the `Descriptor` attribute results in the same value that Fluentify would selected by default.

## Changed

- The Descriptor attribute can now direct Fluentify to use the delcared name as the Descriptor without having to repeat the declared name (#9).

## [1.1.1] - 2024-08-15

## Fixed
Expand Down
41 changes: 26 additions & 15 deletions Fluentify.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Analyzers", "Analyzers", "{
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EDFD8FBD-CDAB-4EAE-986C-538E457515D0}"
ProjectSection(SolutionItems) = preProject
.filenesting.json = .filenesting.json
.gitignore = .gitignore
.runsettings = .runsettings
CHANGELOG.md = CHANGELOG.md
codecov.yml = codecov.yml
.github\CODEOWNERS = .github\CODEOWNERS
CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md
CONTRIBUTING.md = CONTRIBUTING.md
.github\dependabot.yml = .github\dependabot.yml
LICENSE = LICENSE
nuget.config = nuget.config
.github\pull_request_template.md = .github\pull_request_template.md
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fluentify.Tests", "src\Fluentify.Tests\Fluentify.Tests.csproj", "{25FAA0FE-0B5A-489F-8145-DF74C9BADD81}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fluentify.Console.Tests", "src\Fluentify.Console.Tests\Fluentify.Console.Tests.csproj", "{4CAAA913-C6DD-4FD2-AF51-BCB89E6DD44F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentation", "{E9DE67A5-339C-4BA3-BD91-F9E0937B3BFF}"
ProjectSection(SolutionItems) = preProject
CHANGELOG.md = CHANGELOG.md
.github\CODE_OF_CONDUCT.md = .github\CODE_OF_CONDUCT.md
.github\CONTRIBUTING.md = .github\CONTRIBUTING.md
LICENSE = LICENSE
README.md = README.md
.github\SECURITY.md = .github\SECURITY.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rules", "Rules", "{14CECDE5-92C5-4ADC-A1B7-8BC089588D0B}"
ProjectSection(SolutionItems) = preProject
Expand All @@ -61,6 +54,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rules", "Rules", "{14CECDE5
docs\rules\FLTFY04.md = docs\rules\FLTFY04.md
docs\rules\FLTFY05.md = docs\rules\FLTFY05.md
docs\rules\FLTFY06.md = docs\rules\FLTFY06.md
docs\rules\FLTFY07.md = docs\rules\FLTFY07.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{62844182-B1F0-4244-AE6A-C7272BE585F5}"
ProjectSection(SolutionItems) = preProject
.filenesting.json = .filenesting.json
.gitignore = .gitignore
.runsettings = .runsettings
codecov.yml = codecov.yml
.github\CODEOWNERS = .github\CODEOWNERS
nuget.config = nuget.config
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Templates", "Templates", "{69C82B4D-CAE6-4E96-805F-7BA9E581D65D}"
ProjectSection(SolutionItems) = preProject
.github\ISSUE_TEMPLATE\bug_report.md = .github\ISSUE_TEMPLATE\bug_report.md
.github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md
.github\pull_request_template.md = .github\pull_request_template.md
EndProjectSection
EndProject
Global
Expand Down
23 changes: 10 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ If you are unfamiliar with Fluent Builder pattern, please review [Building Compl
```csharp
var movie = new Movie
{
Genre = Genre.SciFi,
Title = "Star Trek: First Contact",
ReleasedOn = new DateOnly(1996, 12, 13),
Actors =
[
new Actor
Expand All @@ -16,9 +19,6 @@ var movie = new Movie
Surname = "Stewart",
},
],
Genre = Genre.SciFi,
ReleasedOn = new DateOnly(1996, 12, 13),
Title = "Star Trek: First Contact",
};
```

Expand Down Expand Up @@ -130,7 +130,9 @@ Console.WriteLine(@new.Actors.Length); // Displays 2

## Custom Descriptors

The name of the generated extension method(s) can be customized via the `Descriptor` attribute.
By default, Fluentify will generate an extension method for each property using the `With{Property Name}` pattern for all types, with the exception of `bool`, which defaults to the declared name of the property.

The name used can be customized via the `Descriptor` attribute. When a descriptor is provided that is deemed acceptable as a method name to the compiler, it is applied to the extension method. When no descriptor is provided, the declared name of the property used.

### Record Type Usage

Expand All @@ -145,7 +147,7 @@ public partial record Actor(
public partial record Movie(
Actor[] Actors,
[Descriptor("OfGenre")] Genre Genre,
[Descriptor("ReleasedOn")] DateOnly ReleasedOn,
[Descriptor] DateOnly ReleasedOn,
string Title);
```

Expand All @@ -171,7 +173,7 @@ public class Movie
[Descriptor("OfGenre")]
public Genre Genre { get; init; }

[Descriptor("ReleasedOn")]
[Descriptor]
public DateOnly ReleasedOn { get; init; }

public string Title { get; init; }
Expand All @@ -191,12 +193,6 @@ var movie = new Movie()
.BornIn(1940));
```

When no custom descriptor is specified, the extension method(s) will use the following pattern for all property types, except `bool`:

`With{PropertyName}`

For `bool`, the extension method will utilize the same name as the property.

## Property Exclusion

Specific properties can be excluded from generating Fluentify extension method(s) using the `Ignore` attribute:
Expand Down Expand Up @@ -242,6 +238,7 @@ Rule ID | Category | Severity | Notes
[FLTFY04](docs/rules/FLTFY04.md) | Naming | Warning | Descriptor must adhere to the naming conventions for Methods
[FLTFY05](docs/rules/FLTFY05.md) | Usage | Info | Type does not utilize Fluentify
[FLTFY06](docs/rules/FLTFY06.md) | Usage | Info | Property is already disregarded from consideration by Fluentify
[FLTFY07](docs/rules/FLTFY07.md) | Usage | Info | Specified descriptor is already the default used by Fluentify

## Building a Service

Expand Down Expand Up @@ -290,7 +287,7 @@ MyService service = MyServiceBuilder

## Contributing

Contributions are welcome - see the [CONTRIBUTING.md](CONTRIBUTING.md) file for details.
Contributions are welcome - see the [CONTRIBUTING.md](/.github/CONTRIBUTING.md) file for details.

## License

Expand Down
111 changes: 111 additions & 0 deletions docs/rules/FLTFY07.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# FLTFY07: Specified descriptor is already the default used by Fluentify

<table>
<tr>
<td>Type Name</td>
<td>FLTFY07_DescriptorAttributeAnalyzer</td>
</tr>
<tr>
<td>Diagnostic Id</td>
<td>FLTFY07</td>
</tr>
<tr>
<td>Category</td>
<td>Usage</td>
</tr>
<tr>
<td>Severity</td>
<td>Info</td>
</tr>
<tr>
<td>Is Enabled By Default</td>
<td>Yes</td>
</tr>
</table>

## Cause

The specified descriptor is already the default used by Fluentify, so its usage is redundant.

## Rule Description

A violation of this rule occurs when the descriptor specified for a given property results in the same value that Fluentify will select by default if no `Descriptor` attribute is specified, making the usage of the `Descriptor` attribute redundant.

For example:

```csharp
[Fluentify]
public class Example
{
[Descriptor]
public bool IsActive { get; init; }

[Descriptor("WithName")]
public string Name { get; init; }
}
```

In this example, `IsActive` is a boolean, and by default, Fluentify will select its declared name as the descriptor, so the usage of the `Descriptor` attribute is redundant. Likewise, Fluentify will use the `With{PropertyName}` pattern for non-boolean properties, so the default descriptor for the `Name` property would be `WithName`.

## How to Fix Violations

Remove the redundant `Descriptor` attribute from properties that are already given the matching descriptor by default.

For example:

```csharp
[Fluentify]
public class Example
{
public bool IsActive { get; init; }
public string Name { get; init; }
}
```

## When to Suppress Warnings

Warnings from this rule should be suppressed only if there is a strong justification for the redundant use of the `Descriptor` attribute.

If suppression is desired, one of the following approaches can be used:

```csharp
[Fluentify]
public class Example
{
#pragma warning disable FLTFY07 // Specified descriptor is already the default used by Fluentify

[Descriptor]
public bool IsActive { get; init; }

[Descriptor("WithName")]
public string Name { get; init; }

#pragma warning restore FLTFY07 // Specified descriptor is already the default used by Fluentify
}
```

or alternatively:

```csharp
[Fluentify]
public class Example
{
[Descriptor]
[SuppressMessage("Usage", "FLTFY07:Specified descriptor is already the default used by Fluentify", Justification = "Explanation for suppression")]
public bool IsActive { get; init; }

[Descriptor("WithName")]
[SuppressMessage("Usage", "FLTFY07:Specified descriptor is already the default used by Fluentify", Justification = "Explanation for suppression")]
public string Name { get; init; }
}
```

## How to Disable FLTFY06

It is not recommended to disable the rule, as its presence suggests a misunderstanding by the engineer as to its intended usage.

```ini
# Disable FLTFY07: Specified descriptor is already the default used by Fluentify
[*.cs]
dotnet_diagnostic.FLTYF07.severity = none
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace Fluentify.Console.Class.SelfDescriptor.OnIgnoredTests;

public sealed class WhenWithAgeIsCalled
{
[Fact]
public void GivenNullSubjectThenArgumentNullExceptionIsThrown()
{
// Arrange
OnIgnored? subject = default;

// Act
Func<OnIgnored> act = () => subject!.WithAge(1);

// Assert
_ = act.Should().Throw<ArgumentNullException>()
.WithParameterName(nameof(subject));
}

[Theory]
[InlineData(1)]
[InlineData(-1)]
[InlineData(0)]
[InlineData(int.MinValue)]
[InlineData(int.MaxValue)]
public void GivenAnAgeThenTheValueIsApplied(int age)
{
// Arrange
var original = new OnIgnored
{
Age = Random.Shared.Next(),
Attributes = [new(), new()],
Name = "Avery Brooks",
};

// Act
OnIgnored actual = original.WithAge(age);

// Assert
_ = actual.Should().NotBeSameAs(original);
_ = actual.Age.Should().Be(age);
_ = actual.Attributes.Should().BeEquivalentTo(original.Attributes);
_ = actual.Name.Should().BeEquivalentTo(original.Name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Fluentify.Console.Class.SelfDescriptor.OnIgnoredTests;

public sealed class WhenWithAttributesIsCalled
{
[Fact]
public void GivenNullSubjectThenArgumentNullExceptionIsThrown()
{
// Arrange
OnIgnored? subject = default;

// Act
Func<OnIgnored> act = () => subject!.WithAttributes(new object());

// Assert
_ = act.Should().Throw<ArgumentNullException>()
.WithParameterName(nameof(subject));
}

[Fact]
public void GivenAttributesThenTheValueIsApplied()
{
// Arrange
object[] attributes = [new()];

var original = new OnIgnored
{
Age = Random.Shared.Next(),
Attributes = [],
Name = "Avery Brooks",
};

// Act
OnIgnored actual = original.WithAttributes(attributes);

// Assert
_ = actual.Should().NotBeSameAs(original);
_ = actual.Age.Should().Be(original.Age);
_ = actual.Attributes.Should().BeEquivalentTo(attributes);
_ = actual.Name.Should().Be(original.Name);
}
}
Loading