|
1 | 1 | // Copyright (c) Microsoft Corporation. |
2 | 2 | // Licensed under the MIT License. |
3 | 3 |
|
| 4 | +using System.Collections.Generic; |
| 5 | +using System.Collections.Immutable; |
| 6 | +using System.Linq; |
| 7 | +using Microsoft.CodeAnalysis; |
| 8 | +using Microsoft.CodeAnalysis.CSharp; |
4 | 9 | using Microsoft.Macios.Generator; |
| 10 | +using Microsoft.Macios.Generator.Context; |
| 11 | +using Microsoft.Macios.Generator.DataModel; |
| 12 | +using static Microsoft.Macios.Generator.RgenDiagnostics; |
5 | 13 |
|
6 | 14 | namespace Microsoft.Macios.Bindings.Analyzer.Validators; |
7 | 15 |
|
8 | 16 | /// <summary> |
9 | 17 | /// Validator for class bindings. |
10 | 18 | /// </summary> |
11 | 19 | sealed class ClassValidator : BindingValidator { |
| 20 | + |
| 21 | + readonly ArrayValidator<Property> propertiesValidator = new (new PropertyOrFieldValidator ()); |
| 22 | + |
| 23 | + /// <summary> |
| 24 | + /// Validates that strong delegate names are unique across all properties. |
| 25 | + /// </summary> |
| 26 | + /// <param name="properties">The properties to validate.</param> |
| 27 | + /// <param name="context">The root context for validation.</param> |
| 28 | + /// <param name="diagnostics">When this method returns, contains diagnostics for any duplicate strong delegate names; otherwise, an empty array.</param> |
| 29 | + /// <param name="location">The code location to be used for the diagnostics.</param> |
| 30 | + /// <returns><c>true</c> if all strong delegate names are unique; otherwise, <c>false</c>.</returns> |
| 31 | + bool StrongDelegatesAreUnique (ImmutableArray<Property> properties, RootContext context, |
| 32 | + out ImmutableArray<Diagnostic> diagnostics, Location? location = null) |
| 33 | + { |
| 34 | + diagnostics = ImmutableArray<Diagnostic>.Empty; |
| 35 | + // use a dictionary to track all the strong names and the properties that use them |
| 36 | + var strongNames = new Dictionary<string, List<Property>> (); |
| 37 | + foreach (var p in properties) { |
| 38 | + var strongDelegate = p.ToStrongDelegate (); |
| 39 | + if (strongNames.TryGetValue (strongDelegate.Name, out var list)) { |
| 40 | + list.Add (p); |
| 41 | + } else { |
| 42 | + // add list with the current property since we want to use is as a ref |
| 43 | + strongNames.Add (strongDelegate.Name, [p]); |
| 44 | + } |
| 45 | + } |
| 46 | + // get all the strong names that have more than one property using them |
| 47 | + var duplicates = strongNames.Where (x => x.Value.Count > 1).ToImmutableArray (); |
| 48 | + if (duplicates.Length == 0) { |
| 49 | + // no duplicates, we are good |
| 50 | + return true; |
| 51 | + } |
| 52 | + // build the diagnostics |
| 53 | + var builder = ImmutableArray.CreateBuilder<Diagnostic> (); |
| 54 | + foreach (var duplicate in duplicates) { |
| 55 | + // add a diagnostic for each duplicate strong delegate using the first one as a reference and the second |
| 56 | + // one as the location of the error. We use the first one as a reference because we have to choose one and |
| 57 | + // is the one on top of the file |
| 58 | + var firstProperty = duplicate.Value.First (); |
| 59 | + for (var index = 1; index < duplicate.Value.Count; index++) { |
| 60 | + var dupProperty = duplicate.Value [index]; // used for the msg and the location |
| 61 | + builder.Add (Diagnostic.Create ( |
| 62 | + descriptor: RBI0033, |
| 63 | + location: dupProperty.Location, |
| 64 | + messageArgs: [ |
| 65 | + dupProperty.Name, |
| 66 | + duplicate.Key, |
| 67 | + firstProperty.Name |
| 68 | + ])); |
| 69 | + } |
| 70 | + } |
| 71 | + diagnostics = builder.ToImmutable (); |
| 72 | + return diagnostics.Length == 0; |
| 73 | + } |
| 74 | + |
| 75 | + /// <summary> |
| 76 | + /// Validates that selectors are unique across all properties and methods in a binding. |
| 77 | + /// </summary> |
| 78 | + /// <param name="binding">The binding to validate.</param> |
| 79 | + /// <param name="context">The root context for validation.</param> |
| 80 | + /// <param name="diagnostics">When this method returns, contains diagnostics for any duplicate selectors; otherwise, an empty array.</param> |
| 81 | + /// <param name="location">The code location to be used for the diagnostics.</param> |
| 82 | + /// <returns><c>true</c> if all selectors are unique; otherwise, <c>false</c>.</returns> |
| 83 | + bool SelectorsAreUnique (Binding binding, RootContext context, |
| 84 | + out ImmutableArray<Diagnostic> diagnostics, Location? location = null) |
| 85 | + { |
| 86 | + diagnostics = ImmutableArray<Diagnostic>.Empty; |
| 87 | + var builder = ImmutableArray.CreateBuilder<Diagnostic> (); |
| 88 | + |
| 89 | + // the logic is as follows: |
| 90 | + // 1. Collect all selectors that we have decided to register. Those are the ones in properties and methods that |
| 91 | + // do not have the SkipRegister attribute. |
| 92 | + // 2. Collect the selectors based on them being static or instance selectors. We can have the same selector |
| 93 | + // for static and instance methods, but not for two static or two instance methods. |
| 94 | + |
| 95 | + var instanceSelectors = new Dictionary<string, List<(string SymbolName, Location? Location)>> (); |
| 96 | + var staticSelectors = new Dictionary<string, List<(string SymbolName, Location? Location)>> (); |
| 97 | + // collect property selectors |
| 98 | + foreach (var property in binding.Properties) { |
| 99 | + if (string.IsNullOrEmpty (property.Selector)) |
| 100 | + continue; |
| 101 | + if (property.SkipRegistration) |
| 102 | + // user has decided to skip registration for this property, so we don't need to validate it |
| 103 | + continue; |
| 104 | + // decide which dictionary to use based on the property being static or instance |
| 105 | + var selectors = property.IsStatic ? staticSelectors : instanceSelectors; |
| 106 | + if (selectors.TryGetValue (property.Selector, out var list)) { |
| 107 | + list.Add ((property.Name, property.Location)); |
| 108 | + } else { |
| 109 | + // add a new list with the current property |
| 110 | + selectors.Add (property.Selector, [(property.Name, property.Location)]); |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + // collect method selectors |
| 115 | + foreach (var method in binding.Methods) { |
| 116 | + if (string.IsNullOrEmpty (method.Selector)) |
| 117 | + continue; |
| 118 | + if (method.SkipRegistration) |
| 119 | + // user has decided to skip registration for this method, so we don't need to validate it |
| 120 | + continue; |
| 121 | + var selectors = method.IsStatic ? staticSelectors : instanceSelectors; |
| 122 | + if (selectors.TryGetValue (method.Selector, out var list)) { |
| 123 | + list.Add ((method.Name, method.Location)); |
| 124 | + } else { |
| 125 | + // add a new list with the current property |
| 126 | + selectors.Add (method.Selector, [(method.Name, method.Location)]); |
| 127 | + } |
| 128 | + } |
| 129 | + // get all the selectors that have more than one property or method |
| 130 | + var instanceDuplicates = instanceSelectors.Where (x => x.Value.Count > 1).ToImmutableArray (); |
| 131 | + var staticDuplicates = staticSelectors.Where (x => x.Value.Count > 1).ToImmutableArray (); |
| 132 | + |
| 133 | + if (instanceDuplicates.Length == 0 && staticDuplicates.Length == 0) { |
| 134 | + // no duplicates, we are good |
| 135 | + return true; |
| 136 | + } |
| 137 | + // loop over each of the duplicates and create diagnostics for them, we do this separately for instance and |
| 138 | + // static selectors to make it easier to read the code and to avoid mixing selectors and getting confused about |
| 139 | + // which one is which. |
| 140 | + BuildDiagnostics (instanceDuplicates, builder); |
| 141 | + BuildDiagnostics (staticDuplicates, builder); |
| 142 | + |
| 143 | + diagnostics = builder.ToImmutable (); |
| 144 | + return diagnostics.Length == 0; |
| 145 | + |
| 146 | + void BuildDiagnostics (ImmutableArray<KeyValuePair<string, List<(string SymbolName, Location? Location)>>> keyValuePairs, |
| 147 | + ImmutableArray<Diagnostic>.Builder builder1) |
| 148 | + { |
| 149 | + foreach (var duplicate in keyValuePairs) { |
| 150 | + var firstSymbol = duplicate.Value.First (); |
| 151 | + for (var index = 1; index < duplicate.Value.Count; index++) { |
| 152 | + var dupSymbol = duplicate.Value [index]; // used for the msg and the location |
| 153 | + builder1.Add (Diagnostic.Create ( |
| 154 | + descriptor: RBI0034, |
| 155 | + location: dupSymbol.Location, |
| 156 | + messageArgs: [ |
| 157 | + duplicate.Key, |
| 158 | + dupSymbol.SymbolName, |
| 159 | + firstSymbol.SymbolName |
| 160 | + ])); |
| 161 | + } |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + |
12 | 166 | /// <summary> |
13 | 167 | /// Initializes a new instance of the <see cref="ClassValidator"/> class. |
14 | 168 | /// </summary> |
15 | 169 | public ClassValidator () |
16 | 170 | { |
17 | 171 | // class bindings must be partial |
18 | | - AddGlobalStrategy (RgenDiagnostics.RBI0001, IsPartial); |
| 172 | + AddGlobalStrategy (RBI0001, IsPartial); |
| 173 | + |
| 174 | + // use a nested validator to validate the properties and fields individually |
| 175 | + AddNestedValidator (b => b.Properties, propertiesValidator); |
| 176 | + |
| 177 | + // validate that the selectors are not duplicated, this includes properties and methods |
| 178 | + AddGlobalStrategy ([RBI0034], SelectorsAreUnique); |
| 179 | + |
| 180 | + // validate that strong delegates are not duplicated, this is only for weak properties |
| 181 | + AddStrategy ( |
| 182 | + b => b.Properties.Where (p => p.IsWeakDelegate).ToImmutableArray (), |
| 183 | + [RBI0033], |
| 184 | + StrongDelegatesAreUnique, "WeakDelegates"); |
19 | 185 | } |
20 | 186 | } |
0 commit comments