Skip to content

Commit a0f0b03

Browse files
authored
Merge pull request #236 from CommunityToolkit/dev/observable-validator-partial-declarations
Fix ObservableValidator duplicate generated partial declarations
2 parents f003c4f + b5b0045 commit a0f0b03

File tree

8 files changed

+86
-19
lines changed

8 files changed

+86
-19
lines changed

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
88
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
99
using Microsoft.CodeAnalysis;
10-
using Microsoft.CodeAnalysis.CSharp;
1110
using Microsoft.CodeAnalysis.CSharp.Syntax;
1211
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
1312

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
using System.Collections.Generic;
66
using System.Collections.Immutable;
77
using System.Linq;
8-
using System.Text;
98
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
109
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
1110
using CommunityToolkit.Mvvm.SourceGenerators.Models;
1211
using Microsoft.CodeAnalysis;
1312
using Microsoft.CodeAnalysis.CSharp;
1413
using Microsoft.CodeAnalysis.CSharp.Syntax;
15-
using Microsoft.CodeAnalysis.Text;
1614
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
1715

1816
namespace CommunityToolkit.Mvvm.SourceGenerators;

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Linq;
6-
using System.Text;
76
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
87
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
98
using Microsoft.CodeAnalysis;
109
using Microsoft.CodeAnalysis.CSharp;
1110
using Microsoft.CodeAnalysis.CSharp.Syntax;
12-
using Microsoft.CodeAnalysis.Text;
1311

1412
namespace CommunityToolkit.Mvvm.SourceGenerators;
1513

@@ -27,7 +25,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
2725
context.SyntaxProvider
2826
.CreateSyntaxProvider(
2927
static (node, _) => node is ClassDeclarationSyntax,
30-
static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
28+
static (context, _) => (context.Node, Symbol: (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!))
29+
.Where(static item => item.Node.IsFirstSyntaxDeclarationForSymbol(item.Symbol))
30+
.Select(static (item, _) => item.Symbol);
3131

3232
// Get the types that inherit from ObservableValidator and gather their info
3333
IncrementalValuesProvider<ValidationInfo> validationInfo =

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@
55
using System.Collections.Generic;
66
using System.Collections.Immutable;
77
using System.Linq;
8-
using System.Text;
9-
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
108
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
119
using CommunityToolkit.Mvvm.SourceGenerators.Models;
1210
using Microsoft.CodeAnalysis;
1311
using Microsoft.CodeAnalysis.CSharp;
1412
using Microsoft.CodeAnalysis.CSharp.Syntax;
15-
using Microsoft.CodeAnalysis.Text;
1613
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
1714

1815
namespace CommunityToolkit.Mvvm.SourceGenerators;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis;
6+
7+
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
8+
9+
/// <summary>
10+
/// Extension methods for the <see cref="SyntaxNode"/> type.
11+
/// </summary>
12+
internal static class SyntaxNodeExtensions
13+
{
14+
/// <summary>
15+
/// Checks whether a given <see cref="SyntaxNode"/> represents the first (partial) declaration of a given symbol.
16+
/// </summary>
17+
/// <param name="syntaxNode">The input <see cref="SyntaxNode"/> instance.</param>
18+
/// <param name="symbol">The target <see cref="ISymbol"/> instance to check the syntax declaration for.</param>
19+
/// <returns>Whether <paramref name="syntaxNode"/> is the first (partial) declaration for <paramref name="symbol"/>.</returns>
20+
/// <remarks>
21+
/// This extension can be used to avoid accidentally generating repeated members for types that have multiple partial declarations.
22+
/// In order to keep this check efficient and without the need to collect all items and build some sort of hashset from them to
23+
/// remove duplicates, each syntax node is symply compared against the available declaring syntax references for the target symbol.
24+
/// If the syntax node matches the first syntax reference for the symbol, it is kept, otherwise it is considered a duplicate.
25+
/// </remarks>
26+
public static bool IsFirstSyntaxDeclarationForSymbol(this SyntaxNode syntaxNode, ISymbol symbol)
27+
{
28+
return
29+
symbol.DeclaringSyntaxReferences.Length > 0 &&
30+
symbol.DeclaringSyntaxReferences[0] is SyntaxReference syntaxReference &&
31+
syntaxReference.SyntaxTree == syntaxNode.SyntaxTree &&
32+
syntaxReference.Span == syntaxNode.Span;
33+
}
34+
}

CommunityToolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@
44

55
using System.Collections.Immutable;
66
using System.Linq;
7-
using System.Text;
8-
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
97
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
108
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
119
using CommunityToolkit.Mvvm.SourceGenerators.Models;
1210
using Microsoft.CodeAnalysis;
1311
using Microsoft.CodeAnalysis.CSharp;
1412
using Microsoft.CodeAnalysis.CSharp.Syntax;
15-
using Microsoft.CodeAnalysis.Text;
1613
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
1714

1815
namespace CommunityToolkit.Mvvm.SourceGenerators;

CommunityToolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44

55
using System.Collections.Immutable;
66
using System.Linq;
7-
using System.Text;
87
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
98
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
109
using Microsoft.CodeAnalysis;
1110
using Microsoft.CodeAnalysis.CSharp;
1211
using Microsoft.CodeAnalysis.CSharp.Syntax;
13-
using Microsoft.CodeAnalysis.Text;
1412

1513
namespace CommunityToolkit.Mvvm.SourceGenerators;
1614

@@ -32,11 +30,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3230
.CreateSyntaxProvider(
3331
static (node, _) => node is ClassDeclarationSyntax,
3432
static (context, _) => (context.Node, Symbol: (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!))
35-
.Where(static item =>
36-
item.Symbol.DeclaringSyntaxReferences.Length > 0 &&
37-
item.Symbol.DeclaringSyntaxReferences[0] is SyntaxReference syntaxReference &&
38-
syntaxReference.SyntaxTree == item.Node.SyntaxTree &&
39-
syntaxReference.Span == item.Node.Span)
33+
.Where(static item => item.Node.IsFirstSyntaxDeclarationForSymbol(item.Symbol))
4034
.Select(static (item, _) => item.Symbol);
4135

4236
// Get the target IRecipient<TMessage> interfaces and filter out other types

tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableValidator.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,36 @@ public void Test_ObservableValidator_ValidateAllProperties_WithFallback()
382382
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(Person.Age)));
383383
}
384384

385+
// See https://github.com/CommunityToolkit/dotnet/issues/235
386+
[TestMethod]
387+
public void Test_ObservableValidator_ValidateAllProperties_WithinPartialClassDeclaration()
388+
{
389+
PersonWithPartialDeclaration model = new();
390+
List<DataErrorsChangedEventArgs> events = new();
391+
392+
model.ErrorsChanged += (s, e) => events.Add(e);
393+
394+
model.ValidateAllProperties();
395+
396+
Assert.IsTrue(model.HasErrors);
397+
Assert.IsTrue(events.Count == 2);
398+
399+
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(PersonWithPartialDeclaration.Name)));
400+
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(PersonWithPartialDeclaration.Number)));
401+
402+
events.Clear();
403+
404+
model.Name = "Bob";
405+
model.Number = 42;
406+
407+
model.ValidateAllProperties();
408+
409+
Assert.IsFalse(model.HasErrors);
410+
Assert.IsTrue(events.Count == 2);
411+
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(PersonWithPartialDeclaration.Name)));
412+
Assert.IsTrue(events.Any(e => e.PropertyName == nameof(PersonWithPartialDeclaration.Number)));
413+
}
414+
385415
[TestMethod]
386416
public void Test_ObservableValidator_CustomValidation()
387417
{
@@ -729,4 +759,22 @@ public class ObservableValidatorDerived : ObservableValidatorBase
729759

730760
public int SomeRandomproperty { get; set; }
731761
}
762+
763+
public partial class PersonWithPartialDeclaration : ObservableValidator
764+
{
765+
[Required]
766+
[MinLength(1)]
767+
public string? Name { get; set; }
768+
769+
public new void ValidateAllProperties()
770+
{
771+
base.ValidateAllProperties();
772+
}
773+
}
774+
775+
public partial class PersonWithPartialDeclaration
776+
{
777+
[Range(10, 1000)]
778+
public int Number { get; set; }
779+
}
732780
}

0 commit comments

Comments
 (0)