Skip to content

Commit 0813dfc

Browse files
committed
Error when init accessor is used for a property with a default value but runtime doesn't support unsafe accessors and a little bug fix
1 parent 7431abc commit 0813dfc

File tree

6 files changed

+115
-85
lines changed

6 files changed

+115
-85
lines changed

src/ArgumentParsing.CodeFixes/ChangeToInitAccessorCodeFixProvider.cs renamed to src/ArgumentParsing.CodeFixes/ChangeAccessorKindCodeFixProvider.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
namespace ArgumentParsing.CodeFixes;
1111

1212
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
13-
public sealed class ChangeToInitAccessorCodeFixProvider : CodeFixProvider
13+
public sealed class ChangeAccessorKindCodeFixProvider : CodeFixProvider
1414
{
15-
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticDescriptors.PreferInitPropertyAccessor.Id);
15+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
16+
DiagnosticDescriptors.PreferInitPropertyAccessor.Id,
17+
DiagnosticDescriptors.CannotHaveInitAccessorWithADefaultValue.Id);
1618

1719
public override FixAllProvider? GetFixAllProvider()
1820
=> WellKnownFixAllProviders.BatchFixer;
@@ -29,22 +31,25 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
2931
return;
3032
}
3133

34+
var diagnostic = context.Diagnostics[0];
35+
var newAccessorKeywordKind = diagnostic.Descriptor.Id == DiagnosticDescriptors.PreferInitPropertyAccessor.Id ? SyntaxKind.InitKeyword : SyntaxKind.SetKeyword;
36+
3237
context.RegisterCodeFix(
3338
CodeAction.Create(
34-
"Change to 'init' accessor",
35-
_ => ChangeToInitAccessor(document, root, accessor),
36-
nameof(ChangeToInitAccessorCodeFixProvider)),
39+
$"Change to '{SyntaxFacts.GetText(newAccessorKeywordKind)}' accessor",
40+
_ => ChangeAccessorKind(document, root, accessor, newAccessorKeywordKind),
41+
nameof(ChangeAccessorKindCodeFixProvider)),
3742
context.Diagnostics[0]);
3843
}
3944

40-
private static Task<Document> ChangeToInitAccessor(Document document, SyntaxNode root, AccessorDeclarationSyntax accessor)
45+
private static Task<Document> ChangeAccessorKind(Document document, SyntaxNode root, AccessorDeclarationSyntax accessor, SyntaxKind newAccessorKeywordKind)
4146
{
4247
var fixedAccessor = SyntaxFactory
4348
.AccessorDeclaration(
44-
SyntaxKind.InitAccessorDeclaration,
49+
SyntaxFacts.GetAccessorDeclarationKind(newAccessorKeywordKind),
4550
accessor.AttributeLists,
4651
accessor.Modifiers,
47-
SyntaxFactory.Token(accessor.Keyword.LeadingTrivia, SyntaxKind.InitKeyword, accessor.Keyword.TrailingTrivia),
52+
SyntaxFactory.Token(accessor.Keyword.LeadingTrivia, newAccessorKeywordKind, accessor.Keyword.TrailingTrivia),
4853
accessor.Body,
4954
accessor.ExpressionBody,
5055
accessor.SemicolonToken);

src/ArgumentParsing.Generators/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
Rule ID | Category | Severity | Notes
44
--------|----------|----------|-------
55
ARGP0054 | ArgumentParsing | Info |
6+
ARGP0055 | ArgumentParsing | Error |

src/ArgumentParsing.Generators/Diagnostics/Analyzers/OptionsTypeAnalyzer.cs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ public sealed class OptionsTypeAnalyzer : DiagnosticAnalyzer
4545
DiagnosticDescriptors.InvalidHelpTextGeneratorTypeSpecifier,
4646
DiagnosticDescriptors.InvalidIdentifierName,
4747
DiagnosticDescriptors.CannotFindHelpTextGeneratorMethod,
48-
DiagnosticDescriptors.ImplementISpanParsable);
48+
DiagnosticDescriptors.ImplementISpanParsable,
49+
DiagnosticDescriptors.CannotHaveInitAccessorWithADefaultValue);
4950

5051
public override void Initialize(AnalysisContext context)
5152
{
@@ -302,16 +303,34 @@ property.SetMethod is not null &&
302303
DiagnosticDescriptors.PropertyMustHaveAccessibleSetter,
303304
property.SetMethod?.Locations.First() ?? propertyLocation));
304305
}
305-
else if (property is { SetMethod: { IsInitOnly: false } setMethod } &&
306-
languageVersion >= LanguageVersion.CSharp9 &&
307-
knownTypes.IsExternalInitType is not null)
306+
else
308307
{
309-
if (propertySyntax is not PropertyDeclarationSyntax { Initializer: not null } ||
310-
knownTypes.UnsafeAccessorAttributeType is not null)
308+
var setMethod = property.SetMethod;
309+
310+
if (setMethod.IsInitOnly)
311311
{
312-
context.ReportDiagnostic(
313-
Diagnostic.Create(
314-
DiagnosticDescriptors.PreferInitPropertyAccessor, setMethod.Locations.First()));
312+
if (propertySyntax is PropertyDeclarationSyntax { Initializer: not null } &&
313+
knownTypes.UnsafeAccessorAttributeType is null)
314+
{
315+
context.ReportDiagnostic(
316+
Diagnostic.Create(
317+
DiagnosticDescriptors.CannotHaveInitAccessorWithADefaultValue, setMethod.Locations.First(),
318+
property.Name));
319+
}
320+
}
321+
else
322+
{
323+
if (knownTypes.IsExternalInitType is not null && languageVersion >= LanguageVersion.CSharp9)
324+
{
325+
if (propertySyntax is not PropertyDeclarationSyntax { Initializer: not null } ||
326+
knownTypes.UnsafeAccessorAttributeType is not null)
327+
{
328+
context.ReportDiagnostic(
329+
Diagnostic.Create(
330+
DiagnosticDescriptors.PreferInitPropertyAccessor, setMethod.Locations.First(),
331+
property.Name));
332+
}
333+
}
315334
}
316335
}
317336

@@ -605,7 +624,7 @@ knownTypes.SystemRuntimeCompilerServicesRequiredMemberAttributeType is not null
605624
DiagnosticDescriptors.InvalidRemainingParametersPropertyType, locationForTypeRelatedDiagnostics));
606625
}
607626
}
608-
else if (sequenceType != SequenceType.ImmutableArray)
627+
else if (sequenceType != SequenceType.ImmutableArray && knownTypes.ImmutableArrayOfTType is not null)
609628
{
610629
context.ReportDiagnostic(
611630
Diagnostic.Create(

src/ArgumentParsing.Generators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public static class DiagnosticDescriptors
9090
public static readonly DiagnosticDescriptor PreferInitPropertyAccessor = new(
9191
id: "ARGP0011",
9292
title: "Prefer 'init' property accessor",
93-
messageFormat: "Prefer 'init' property accessor for parser-related properties",
93+
messageFormat: "Prefer 'init' accessor for parser-related property '{0}'",
9494
category: ArgumentParsingCategoryName,
9595
defaultSeverity: DiagnosticSeverity.Info,
9696
isEnabledByDefault: true);
@@ -454,4 +454,13 @@ public static class DiagnosticDescriptors
454454
category: ArgumentParsingCategoryName,
455455
defaultSeverity: DiagnosticSeverity.Info,
456456
isEnabledByDefault: true);
457+
458+
public static readonly DiagnosticDescriptor CannotHaveInitAccessorWithADefaultValue = new(
459+
id: "ARGP0055",
460+
title: "'init' accessor cannot be used here",
461+
messageFormat: "'init' accessor cannot be used for parser-related property '{0}', use 'set' accessor instead",
462+
description: "Parser uses unsafe accessors to conditionally assign an init-only property with a default value. Current runtime doesn't support unsafe accessors therefore 'init' cannot be used.",
463+
category: ArgumentParsingCategoryName,
464+
defaultSeverity: DiagnosticSeverity.Error,
465+
isEnabledByDefault: true);
457466
}

tests/ArgumentParsing.Tests.Unit/ArgumentParserGeneratorTests.cs

Lines changed: 8 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,70 +2297,13 @@ partial class C
22972297
await VerifyGeneratorAsync(source);
22982298
}
22992299

2300-
[Fact]
2301-
public async Task DefaultValues_NoCodeGenForInitOnlyMemberWhenRuntimeDoesNotSupportUnsafeAccessor_Option()
2302-
{
2303-
var source = """
2304-
partial class C
2305-
{
2306-
[GeneratedArgumentParser]
2307-
public static partial ParseResult<MyOptions> {|CS8795:ParseArguments|}(string[] args);
2308-
}
2309-
2310-
[OptionsType]
2311-
class MyOptions
2312-
{
2313-
[Option]
2314-
public int InitOnlyOption { get; init; } = 3;
2315-
}
2316-
2317-
namespace System.Runtime.CompilerServices
2318-
{
2319-
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
2320-
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
2321-
internal static class IsExternalInit
2322-
{
2323-
}
2324-
}
2325-
""";
2326-
2327-
await VerifyGeneratorAsync(source, ReferenceAssemblies.NetStandard.NetStandard20, []);
2328-
}
2329-
2330-
[Fact]
2331-
public async Task DefaultValues_NoCodeGenForInitOnlyMemberWhenRuntimeDoesNotSupportUnsafeAccessor_Parameter()
2332-
{
2333-
var source = """
2334-
partial class C
2335-
{
2336-
[GeneratedArgumentParser]
2337-
public static partial ParseResult<MyOptions> {|CS8795:ParseArguments|}(string[] args);
2338-
}
2339-
2340-
[OptionsType]
2341-
class MyOptions
2342-
{
2343-
[Parameter(0)]
2344-
public string InitOnlyParam { get; init; } = "Test";
2345-
}
2346-
2347-
namespace System.Runtime.CompilerServices
2348-
{
2349-
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
2350-
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
2351-
internal static class IsExternalInit
2352-
{
2353-
}
2354-
}
2355-
""";
2356-
2357-
await VerifyGeneratorAsync(source, ReferenceAssemblies.NetStandard.NetStandard20, []);
2358-
}
2359-
2360-
[Fact]
2361-
public async Task DefaultValues_NoCodeGenForInitOnlyMemberWhenRuntimeDoesNotSupportUnsafeAccessor_RemainingParameters()
2300+
[Theory]
2301+
[InlineData("Option", "int", "3")]
2302+
[InlineData("Parameter(0)", "string", "\"Test\"")]
2303+
[InlineData("RemainingParameters", "System.Collections.Generic.IReadOnlyList<int>", "[1, 2, 3]")]
2304+
public async Task DefaultValues_NoCodeGenForInitOnlyMemberWhenRuntimeDoesNotSupportUnsafeAccessor(string attributeContent, string propertyType, string initializerSyntax)
23622305
{
2363-
var source = """
2306+
var source = $$"""
23642307
partial class C
23652308
{
23662309
[GeneratedArgumentParser]
@@ -2370,8 +2313,8 @@ partial class C
23702313
[OptionsType]
23712314
class MyOptions
23722315
{
2373-
[RemainingParameters]
2374-
public System.Collections.Generic.IReadOnlyList<int> RemainingParams { get; init; } = [1, 2, 3];
2316+
[{{attributeContent}}]
2317+
public {{propertyType}} ParserProp { get; init; } = {{initializerSyntax}};
23752318
}
23762319
23772320
namespace System.Runtime.CompilerServices

tests/ArgumentParsing.Tests.Unit/OptionsTypeAnalyzerTests.cs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ class MyOptions
305305
}
306306
""";
307307

308-
await VerifyAnalyzerWithCodeFixAsync<ChangeToInitAccessorCodeFixProvider>(source, fixedSource);
308+
await VerifyAnalyzerWithCodeFixAsync<ChangeAccessorKindCodeFixProvider>(source, fixedSource);
309309
}
310310

311311
[Fact]
@@ -2041,4 +2041,57 @@ await VerifyAnalyzerAsync(source,
20412041
.WithArguments("RemainingParameters", "MyOptions", "MyParsable")
20422042
], referenceAssemblies: ReferenceAssemblies.Net.Net80);
20432043
}
2044+
2045+
[Theory]
2046+
[InlineData("Option", "int", "3")]
2047+
[InlineData("Parameter(0)", "string", "\"Test\"")]
2048+
[InlineData("RemainingParameters", "System.Collections.Generic.IReadOnlyList<int>", "[1, 2, 3]")]
2049+
public async Task DefaultValues_InitOnlyMemberWhenRuntimeDoesNotSupportUnsafeAccessor(string attributeContent, string propertyType, string initializerSyntax)
2050+
{
2051+
await VerifyAnalyzerWithCodeFixAsync<ChangeAccessorKindCodeFixProvider>($$"""
2052+
partial class C
2053+
{
2054+
[GeneratedArgumentParser]
2055+
public static partial ParseResult<MyOptions> {|CS8795:ParseArguments|}(string[] args);
2056+
}
2057+
2058+
[OptionsType]
2059+
class MyOptions
2060+
{
2061+
[{{attributeContent}}]
2062+
public {{propertyType}} ParserProp { get; {|ARGP0055:init|}; } = {{initializerSyntax}};
2063+
}
2064+
2065+
namespace System.Runtime.CompilerServices
2066+
{
2067+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
2068+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
2069+
internal static class IsExternalInit
2070+
{
2071+
}
2072+
}
2073+
""", $$"""
2074+
partial class C
2075+
{
2076+
[GeneratedArgumentParser]
2077+
public static partial ParseResult<MyOptions> {|CS8795:ParseArguments|}(string[] args);
2078+
}
2079+
2080+
[OptionsType]
2081+
class MyOptions
2082+
{
2083+
[{{attributeContent}}]
2084+
public {{propertyType}} ParserProp { get; set; } = {{initializerSyntax}};
2085+
}
2086+
2087+
namespace System.Runtime.CompilerServices
2088+
{
2089+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
2090+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
2091+
internal static class IsExternalInit
2092+
{
2093+
}
2094+
}
2095+
""", referenceAssemblies: ReferenceAssemblies.NetStandard.NetStandard20);
2096+
}
20442097
}

0 commit comments

Comments
 (0)