Skip to content

Commit

Permalink
Add addParameterless option to generate parameterless constructor (#120)
Browse files Browse the repository at this point in the history
Fixes #120
  • Loading branch information
DomasM authored May 23, 2024
1 parent 1b6f5d9 commit 8e50e68
Show file tree
Hide file tree
Showing 21 changed files with 884 additions and 135 deletions.
1 change: 1 addition & 0 deletions AutoConstructor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.globalconfig = .globalconfig
CHANGELOG.md = CHANGELOG.md
.github\workflows\ci.yml = .github\workflows\ci.yml
Directory.Build.props = Directory.Build.props
Directory.Packages.props = Directory.Packages.props
Expand Down
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

- Fix call to static initializer method
- Fix generation when a reserved keyword is used (directly or indirectly)
- Fix edge cases on MistmatchTypesRule diagnostic
- Fix edge cases on MismatchTypesRule diagnostic

## [5.0.0] - 2023-11-07

Expand Down Expand Up @@ -283,7 +283,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Changed

- Change parameters of `AutoConstructorInjectAttribute` to be optionals
- Change parameters of `AutoConstructorInjectAttribute` to be optional
- Change ACONS05 to be reported on each attribute

### Fixed
Expand Down
52 changes: 47 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ and even use a parameter from another field not annotated with `AutoConstructorI

### Constructor accessibility

Constructor accessibility can be changed using the optionnal parameter `accessibility` on `AutoConstructorAttribute` (like `[AutoConstructor("internal")]`).
Constructor accessibility can be changed using the optional parameter `accessibility` on `AutoConstructorAttribute` (like `[AutoConstructor("internal")]`).
The default is `public` and it can be set to one of the following values:
- `public`
- `private`
Expand Down Expand Up @@ -173,7 +173,7 @@ partial class Test
### Properties injection

Get-only properties (`public int Property { get; }`) are injected by the generator by default.
Non get-only properties (`public int Property { get; set;}`) are injected only if marked with (`[field: AutoConstructorInject]`) attributte.
Non get-only properties (`public int Property { get; set;}`) are injected only if marked with (`[field: AutoConstructorInject]`) attribute.
The behavior of the injection can be modified using auto-implemented property field-targeted attributes on its backing field. The following code show an injected get-only property with a custom injecter:

```csharp
Expand Down Expand Up @@ -203,8 +203,8 @@ To enable this behavior, set `AutoConstructor_GenerateArgumentNullExceptionCheck

### Generating `this()` calls

By default, if a parameterless constructor is available on the class (other than the implicit one), a call
to `this()` is generated with the generated constructor
By default, if a non-generated parameterless constructor is available on the class (other than the implicit one), a call
to `this()` is generated with the generated constructor.
To disable this behavior, set `AutoConstructor_GenerateThisCalls` to `false` in the project file:

``` xml
Expand Down Expand Up @@ -367,6 +367,44 @@ will generate
}
```

## Parameterless constructor for serialization use

Consider the scenario when you want to expose constructor with parameters for DTO, but serializer requires type to have an empty constructor.
You can generate such constructor with `addParameterless` attribute option.

``` csharp
[AutoConstructor(addParameterless:true)]
public partial class Test
{
public int Id { get; set; }
public string Name {get; set;}
}
```

will generate

```csharp
partial class Test
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor.
[global::System.ObsoleteAttribute("For serialization only", true)]
public Test()
{
}
}
```

If you type has any fields/properties to inject, two constructors will be generated: one with arguments and one parameterless.
In any case, to generate parameterless constructor, base type of the target type must have parameterless constructor itself.

Whether this constructor is marked with `[Obsolete]` attribute and obsolete message can be customized by properties in project file:

``` xml
<AutoConstructor_MarkParameterlessConstructorAsObsolete>false</AutoConstructor_MarkParameterlessConstructorAsObsolete>
<AutoConstructor_ParameterlessConstructorObsoleteMessage>Custom obsolete message</AutoConstructor_ParameterlessConstructorObsoleteMessage>
```


## Diagnostics

### ACONS01
Expand All @@ -375,7 +413,7 @@ The `AutoConstructor` attribute is used on a class that is not partial.

### ACONS02

The `AutoConstructor` attribute is used on a class without fields to inject.
The `AutoConstructor` attribute is used on a class without fields to inject without `addParameterless` option.

### ACONS03

Expand Down Expand Up @@ -425,6 +463,10 @@ The accessibility defined in the `AutoConstructor` attribute is not an allowed v

`AutoConstructorDefaultBase` attribute used on multiple constructors inside type.

### ACONS12

`addParameterless` option used on type whose base type does not have a parameterless constructor.

### ACONS99

`AutoConstructor_DisableNullChecking` is obsolete.
6 changes: 6 additions & 0 deletions src/AutoConstructor.Generator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
ACONS12 | Usage | Error | TypeWithoutBaseParameterlessConstructorAnalyzer, [Documentation](https://github.com/k94ll13nn3/AutoConstructor#ACONS12)
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context)
&& symbol.GetAttribute(Source.AttributeFullName) is AttributeData attribute
&& attribute.AttributeConstructor?.Parameters.Length > 0
&& attribute.GetParameterValue<string>("accessibility") is string { Length: > 0 } accessibilityValue
&& !AutoConstructorGenerator.ConstuctorAccessibilities.Contains(accessibilityValue))
&& !AutoConstructorGenerator.ConstructorAccessibilities.Contains(accessibilityValue))
{
var diagnostic = Diagnostic.Create(DiagnosticDescriptors.TypeWithWrongConstructorAccessibilityRule, typeDeclarationSyntax.Identifier.GetLocation());
context.ReportDiagnostic(diagnostic);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Collections.Immutable;
using AutoConstructor.Generator.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace AutoConstructor.Generator.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class TypeWithoutBaseParameterlessConstructorAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.TypeWithoutBaseParameterlessConstructorRule);

public override void Initialize(AnalysisContext context)
{
_ = context ?? throw new ArgumentNullException(nameof(context));

context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
}

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
var symbol = (INamedTypeSymbol)context.Symbol;

if (symbol.GetAttribute(Source.AttributeFullName) is AttributeData attr)
{
bool addParameterLess = symbol.DeclaringSyntaxReferences[0].GetSyntax() is TypeDeclarationSyntax
&& symbol.GetAttribute(Source.AttributeFullName) is AttributeData attribute
&& attribute.AttributeConstructor?.Parameters.Length > 0
&& attribute.GetBoolParameterValue("addParameterless");
if (addParameterLess)
{
bool baseHasAccessibleParameterlessConstructor = BaseHasAccessibleParameterlessConstructor(symbol);
if (!baseHasAccessibleParameterlessConstructor)
{
SyntaxReference? propertyTypeIdentifier = attr.ApplicationSyntaxReference;
if (propertyTypeIdentifier is not null)
{
var location = Location.Create(propertyTypeIdentifier.SyntaxTree, propertyTypeIdentifier.Span);
var diagnostic = Diagnostic.Create(DiagnosticDescriptors.TypeWithoutBaseParameterlessConstructorRule, location);
context.ReportDiagnostic(diagnostic);
}
}
}
}
}

private static bool BaseHasAccessibleParameterlessConstructor(INamedTypeSymbol symbol)
{
INamedTypeSymbol? baseType = symbol.BaseType;
if (baseType?.BaseType is null)
{
return true;
}
IMethodSymbol? acceptableConstructor = baseType.Constructors.FirstOrDefault(d =>
!d.IsStatic && d.DeclaredAccessibility != Accessibility.Private && d.Parameters.Length == 0);
return acceptableConstructor != null;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using AutoConstructor.Generator.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace AutoConstructor.Generator.Analyzers;
Expand All @@ -24,17 +25,23 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
var symbol = (INamedTypeSymbol)context.Symbol;

if (symbol.GetAttribute(Source.AttributeFullName) is AttributeData attr)
if (symbol.GetAttribute(Source.AttributeFullName) is AttributeData attribute)
{
bool hasFields = SymbolHasFields(symbol) || ParentHasFields(context.Compilation, symbol);
if (!hasFields)
bool addParameterLess = symbol.DeclaringSyntaxReferences[0].GetSyntax() is TypeDeclarationSyntax
&& attribute.AttributeConstructor?.Parameters.Length > 0
&& attribute.GetBoolParameterValue("addParameterless");
if (!addParameterLess)
{
SyntaxReference? propertyTypeIdentifier = attr.ApplicationSyntaxReference;
if (propertyTypeIdentifier is not null)
bool hasFields = SymbolHasFields(symbol) || ParentHasFields(context.Compilation, symbol);
if (!hasFields)
{
var location = Location.Create(propertyTypeIdentifier.SyntaxTree, propertyTypeIdentifier.Span);
var diagnostic = Diagnostic.Create(DiagnosticDescriptors.TypeWithoutFieldsToInjectRule, location);
context.ReportDiagnostic(diagnostic);
SyntaxReference? propertyTypeIdentifier = attribute.ApplicationSyntaxReference;
if (propertyTypeIdentifier is not null)
{
var location = Location.Create(propertyTypeIdentifier.SyntaxTree, propertyTypeIdentifier.Span);
var diagnostic = Diagnostic.Create(DiagnosticDescriptors.TypeWithoutFieldsToInjectRule, location);
context.ReportDiagnostic(diagnostic);
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/AutoConstructor.Generator/AutoConstructor.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<CompilerVisibleProperty Include="AutoConstructor_ConstructorDocumentationComment" />
<CompilerVisibleProperty Include="AutoConstructor_GenerateThisCalls" />
<CompilerVisibleProperty Include="AutoConstructor_GenerateArgumentNullExceptionChecks" />
<CompilerVisibleProperty Include="AutoConstructor_MarkParameterlessConstructorAsObsolete" />
<CompilerVisibleProperty Include="AutoConstructor_ParameterlessConstructorObsoleteMessage" />
</ItemGroup>

</Project>
Loading

0 comments on commit 8e50e68

Please sign in to comment.