Skip to content

Commit 08aa35d

Browse files
Restore support for validation of record parameters (#217)
* Restore support for validation of record parameters * Add support for `[element: ]` attributes on record parameters
1 parent 7142ac8 commit 08aa35d

File tree

5 files changed

+290
-20
lines changed

5 files changed

+290
-20
lines changed

src/Common/ITypeSymbolExtensions.cs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -303,22 +303,47 @@ typeSymbol is INamedTypeSymbol
303303
SemanticModel semanticModel
304304
)
305305
{
306-
var propertySyntax = (PropertyDeclarationSyntax)propertySymbol.DeclaringSyntaxReferences.First().GetSyntax();
306+
switch (propertySymbol.DeclaringSyntaxReferences.First().GetSyntax())
307+
{
308+
case PropertyDeclarationSyntax propertySyntax:
309+
{
310+
var list = new List<(string? Target, IObjectCreationOperation AttributeOperation)>();
307311

308-
var list = new List<(string? Target, IObjectCreationOperation AttributeOperation)>();
312+
foreach (var attributeList in propertySyntax.AttributeLists)
313+
{
314+
var target = attributeList.Target?.Identifier.ValueText;
309315

310-
foreach (var attributeList in propertySyntax.AttributeLists)
311-
{
312-
var target = attributeList.Target?.Identifier.ValueText;
316+
foreach (var attribute in attributeList.Attributes)
317+
{
318+
if (semanticModel.GetOperation(attribute) is IAttributeOperation { Operation: IObjectCreationOperation operation })
319+
list.Add((target, operation));
320+
}
321+
}
322+
323+
return list;
324+
}
313325

314-
foreach (var attribute in attributeList.Attributes)
326+
case ParameterSyntax parameterSyntax:
315327
{
316-
if (semanticModel.GetOperation(attribute) is IAttributeOperation { Operation: IObjectCreationOperation operation })
317-
list.Add((target, operation));
328+
var list = new List<(string? Target, IObjectCreationOperation AttributeOperation)>();
329+
330+
foreach (var attributeList in parameterSyntax.AttributeLists)
331+
{
332+
var target = attributeList.Target?.Identifier.ValueText.NullIf("property");
333+
334+
foreach (var attribute in attributeList.Attributes)
335+
{
336+
if (semanticModel.GetOperation(attribute) is IAttributeOperation { Operation: IObjectCreationOperation operation })
337+
list.Add((target, operation));
338+
}
339+
}
340+
341+
return list;
318342
}
319-
}
320343

321-
return list;
344+
case var syntax:
345+
throw new InvalidOperationException($"Property declared using a `{syntax.GetType().FullName}`.");
346+
}
322347
}
323348

324349
public static bool IsTargetTypeSymbol(this ISymbol symbol) =>

src/Immediate.Validations.Analyzers/InvalidAttributeTargetSuppressor.cs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,31 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
2929

3030
if (syntaxTree
3131
?.GetRoot(token)
32-
.FindNode(diagnostic.Location.SourceSpan) is
33-
not AttributeTargetSpecifierSyntax
32+
.FindNode(diagnostic.Location.SourceSpan) is not AttributeTargetSpecifierSyntax
3433
{
3534
Identifier.ValueText: "element",
36-
Parent.Parent: PropertyDeclarationSyntax propertyDeclarationSyntax
35+
Parent.Parent: SyntaxNode declarationSyntax
3736
})
3837
{
3938
continue;
4039
}
4140

42-
if (context.GetSemanticModel(syntaxTree)
43-
.GetDeclaredSymbol(propertyDeclarationSyntax, token) is
44-
not IPropertySymbol
45-
{
46-
ContainingType: INamedTypeSymbol containerSymbol,
47-
Type: ITypeSymbol propertyTypeSymbol
48-
})
41+
if (context.GetSemanticModel(syntaxTree).GetDeclaredSymbol(declarationSyntax, token) switch
42+
{
43+
IPropertySymbol
44+
{
45+
ContainingType: INamedTypeSymbol ct1,
46+
Type: ITypeSymbol pt1
47+
} => (true, ct1, pt1),
48+
49+
IParameterSymbol
50+
{
51+
ContainingType: INamedTypeSymbol ct1,
52+
Type: ITypeSymbol pt1
53+
} => (true, ct1, pt1),
54+
55+
_ => (false, null, null),
56+
} is not (true, { } containerSymbol, { } propertyTypeSymbol))
4957
{
5058
continue;
5159
}
@@ -79,6 +87,8 @@ not IPropertySymbol
7987
diagnostic
8088
)
8189
);
90+
91+
break;
8292
}
8393
}
8494
}

tests/Immediate.Validations.Tests/AnalyzerTests/InvalidAttributeTargetSuppressorTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,36 @@ public sealed class InvalidAttributeTargetSuppressorTests
88
public static readonly DiagnosticResult CS0658 =
99
DiagnosticResult.CompilerWarning("CS0658");
1010

11+
[Fact]
12+
public async Task ElementAttributeInValidatorListForParameterIsSuppressed() =>
13+
await AnalyzerTestHelpers
14+
.CreateSuppressorTest<InvalidAttributeTargetSuppressor>(
15+
"""
16+
#nullable enable
17+
18+
using System.Collections.Generic;
19+
using Immediate.Validations.Shared;
20+
21+
[Validate]
22+
public record Target(
23+
[{|#0:element|}: MaxLength(3)]
24+
List<string> Strings
25+
): IValidationTarget<Target>
26+
{
27+
28+
public ValidationResult Validate() => [];
29+
public ValidationResult Validate(ValidationResult errors) => [];
30+
public static ValidationResult Validate(Target target) => [];
31+
public static ValidationResult Validate(Target target, ValidationResult errors) => [];
32+
}
33+
"""
34+
)
35+
.WithSpecificDiagnostics([CS0658])
36+
.WithExpectedDiagnosticsResults([
37+
CS0658.WithLocation(0).WithIsSuppressed(true),
38+
])
39+
.RunAsync(TestContext.Current.CancellationToken);
40+
1141
[Fact]
1242
public async Task ElementAttributeInValidatorListIsSuppressed() =>
1343
await AnalyzerTestHelpers
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//HintName: IV...ValidateClass.g.cs
2+
using System.Collections.Generic;
3+
using Immediate.Validations.Shared;
4+
5+
#nullable enable
6+
#pragma warning disable CS1591
7+
8+
9+
partial record ValidateClass : IValidationTarget
10+
{
11+
ValidationResult IValidationTarget.Validate() =>
12+
Validate(this, []);
13+
14+
ValidationResult IValidationTarget.Validate(ValidationResult errors) =>
15+
Validate(this, errors);
16+
17+
static ValidationResult IValidationTarget<ValidateClass>.Validate(ValidateClass? target) =>
18+
Validate(target, []);
19+
20+
static ValidationResult IValidationTarget<ValidateClass>.Validate(ValidateClass? target, ValidationResult errors) =>
21+
Validate(target, errors);
22+
23+
public static ValidationResult Validate(ValidateClass? target) =>
24+
Validate(target, []);
25+
26+
public static ValidationResult Validate(ValidateClass? target, ValidationResult errors)
27+
{
28+
if (target is not { } t)
29+
{
30+
return new()
31+
{
32+
{ ".self", "`target` must not be `null`." },
33+
};
34+
}
35+
36+
if (!errors.VisitType(typeof(ValidateClass)))
37+
return errors;
38+
39+
40+
__ValidateTesting1(errors, t, t.Testing1);
41+
__Validatedata(errors, t, t.data);
42+
43+
44+
return errors;
45+
}
46+
47+
48+
49+
private static void __ValidateTesting1(
50+
ValidationResult errors, ValidateClass instance, string target
51+
)
52+
{
53+
54+
if (target is not { } t)
55+
{
56+
errors.Add(
57+
$"Testing1",
58+
global::Immediate.Validations.Shared.NotNullAttribute.DefaultMessage,
59+
new()
60+
{
61+
["PropertyName"] = $"Testing1",
62+
["PropertyValue"] = null,
63+
}
64+
);
65+
66+
return;
67+
}
68+
69+
70+
71+
{
72+
if (!global::Immediate.Validations.Shared.MaxLengthAttribute.ValidateProperty(
73+
t
74+
, maxLength: 3
75+
)
76+
)
77+
{
78+
errors.Add(
79+
$"Testing1",
80+
global::Immediate.Validations.Shared.MaxLengthAttribute.DefaultMessage,
81+
new()
82+
{
83+
["PropertyName"] = $"Testing1",
84+
["PropertyValue"] = t,
85+
["MaxLengthName"] = "",
86+
["MaxLengthValue"] = 3,
87+
}
88+
);
89+
}
90+
}
91+
}
92+
93+
private static void __Validatedata0(
94+
ValidationResult errors, ValidateClass instance, int target, int counter0
95+
)
96+
{
97+
98+
var t = target;
99+
100+
101+
102+
{
103+
if (!global::Immediate.Validations.Shared.GreaterThanAttribute.ValidateProperty(
104+
t
105+
, comparison: 0
106+
)
107+
)
108+
{
109+
errors.Add(
110+
$"data[{counter0}]",
111+
global::Immediate.Validations.Shared.GreaterThanAttribute.DefaultMessage,
112+
new()
113+
{
114+
["PropertyName"] = $"data[{counter0}]",
115+
["PropertyValue"] = t,
116+
["ComparisonName"] = "",
117+
["ComparisonValue"] = 0,
118+
}
119+
);
120+
}
121+
}
122+
}
123+
124+
private static void __Validatedata(
125+
ValidationResult errors, ValidateClass instance, global::System.Collections.Generic.List<int> target
126+
)
127+
{
128+
129+
if (target is not { } t)
130+
{
131+
errors.Add(
132+
$"data",
133+
global::Immediate.Validations.Shared.NotNullAttribute.DefaultMessage,
134+
new()
135+
{
136+
["PropertyName"] = $"data",
137+
["PropertyValue"] = null,
138+
}
139+
);
140+
141+
return;
142+
}
143+
144+
145+
var counter0 = 0;
146+
foreach (var item0 in t)
147+
{
148+
__Validatedata0(
149+
errors, instance, item0, counter0
150+
);
151+
counter0++;
152+
}
153+
154+
{
155+
if (!global::Immediate.Validations.Shared.NotEmptyAttribute.ValidateProperty(
156+
t
157+
)
158+
)
159+
{
160+
errors.Add(
161+
$"data",
162+
global::Immediate.Validations.Shared.NotEmptyAttribute.DefaultMessage,
163+
new()
164+
{
165+
["PropertyName"] = $"data",
166+
["PropertyValue"] = t,
167+
}
168+
);
169+
}
170+
}
171+
}
172+
173+
}
174+

tests/Immediate.Validations.Tests/GeneratorTests/MiscellaneousTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,37 @@ namespace Immediate.Validations.Tests.GeneratorTests;
22

33
public sealed class MiscellaneousTests
44
{
5+
[Fact]
6+
public async Task PropertyValidationOnRecord()
7+
{
8+
var result = GeneratorTestHelper.RunGenerator(
9+
"""
10+
using System.ComponentModel;
11+
using System.Collections.Generic;
12+
using Immediate.Validations.Shared;
13+
14+
[Validate]
15+
public sealed partial record ValidateClass(
16+
[property: MaxLength(3)]
17+
string Testing1,
18+
19+
[property: NotEmpty]
20+
[element: GreaterThan(0)]
21+
List<int> data
22+
): IValidationTarget<ValidateClass>;
23+
"""
24+
);
25+
26+
Assert.Equal(
27+
[
28+
@"Immediate.Validations.Generators/Immediate.Validations.Generators.ImmediateValidationsGenerator/IV...ValidateClass.g.cs",
29+
],
30+
result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/'))
31+
);
32+
33+
_ = await Verify(result);
34+
}
35+
536
[Fact]
637
public async Task FilledDescriptionChangesName()
738
{

0 commit comments

Comments
 (0)