private static void EvaluateDefaultValueChanges(
            ItemMatch <IParameterDefinition> match,
            ComparerOptions options,
            IChangeResultAggregator aggregator)
        {
            var oldItem = match.OldItem;
            var newItem = match.NewItem;

            if (string.IsNullOrWhiteSpace(oldItem.DefaultValue) &&
                string.IsNullOrWhiteSpace(newItem.DefaultValue) == false)
            {
                // A default value has been added, this is technically a feature because the consuming assembly can treat it like an overload
                var args = new FormatArguments(
                    "{DefinitionType} {Identifier} has added the default value {NewValue}",
                    match.NewItem.FullName,
                    null,
                    newItem.DefaultValue);

                aggregator.AddElementChangedResult(SemVerChangeType.Feature, match, options.MessageFormatter, args);
            }
            else if (string.IsNullOrWhiteSpace(oldItem.DefaultValue) == false &&
                     string.IsNullOrWhiteSpace(newItem.DefaultValue))
            {
                // A default value has been removed
                // This will not be a breaking change for existing applications that happen to use a new binary without recompilation
                // however it does cause a breaking change for compiling existing applications against this API
                var args = new FormatArguments(
                    "{DefinitionType} {Identifier} has removed the default value {OldValue}",
                    match.NewItem.FullName,
                    oldItem.DefaultValue,
                    null);

                aggregator.AddElementChangedResult(SemVerChangeType.Breaking, match, options.MessageFormatter, args);
            }
        }
Example #2
0
        private static bool EvaluateArgumentCount(ItemMatch <IAttributeDefinition> match, ComparerOptions options,
                                                  IChangeResultAggregator aggregator)
        {
            var oldArguments = match.OldItem.Arguments.Count;
            var newArguments = match.NewItem.Arguments.Count;

            var argumentShift = oldArguments - newArguments;

            if (argumentShift != 0)
            {
                // One or more arguments have been added or removed
                var changeLabel = argumentShift > 0 ? "removed" : "added";
                var shiftAmount = Math.Abs(argumentShift);

                var suffix = shiftAmount == 1 ? "" : "s";
                var args   = new FormatArguments(
                    $"{{DefinitionType}} {{Identifier}} has {changeLabel} {shiftAmount} argument{suffix}",
                    match.NewItem.Name, null, null);

                aggregator.AddElementChangedResult(SemVerChangeType.Breaking, match, options.MessageFormatter, args);

                // No need to look into how the attribute has changed
                return(true);
            }

            return(false);
        }
Example #3
0
        private static bool EvaluateOrdinalArgument(ItemMatch <IAttributeDefinition> match, ComparerOptions options,
                                                    IChangeResultAggregator aggregator)
        {
            var oldArguments = match.OldItem.Arguments.Where(x => x.ArgumentType == ArgumentType.Ordinal).ToList();
            var newArguments = match.NewItem.Arguments.Where(x => x.ArgumentType == ArgumentType.Ordinal).ToList();

            for (var index = 0; index < oldArguments.Count; index++)
            {
                var oldArgument = oldArguments[index];
                var newArgument = newArguments[index];

                if (oldArgument.Value != newArgument.Value)
                {
                    var args = new FormatArguments(
                        "{DefinitionType} {Identifier} has changed value from {OldValue} to {NewValue}",
                        (index + 1).ToString(), oldArgument.Value, newArgument.Value);

                    // Create a match for the arguments
                    var argumentMatch = new ItemMatch <IArgumentDefinition>(oldArgument, newArgument);

                    aggregator.AddElementChangedResult(SemVerChangeType.Breaking, argumentMatch,
                                                       options.MessageFormatter,
                                                       args);

                    return(true);
                }
            }

            return(false);
        }
Example #4
0
        public IEnumerable <ComparisonResult> CompareMatch(ItemMatch <ITypeDefinition> match, ComparerOptions options)
        {
            match   = match ?? throw new ArgumentNullException(nameof(match));
            options = options ?? throw new ArgumentNullException(nameof(options));

            // Check for a change in type
            if (match.OldItem.GetType() != match.NewItem.GetType())
            {
                var newType = DetermineTypeChangeDescription(match.NewItem);

                var args = new FormatArguments(
                    "{DefinitionType} {Identifier} has changed to {NewValue}",
                    match.OldItem.FullName,
                    null,
                    newType);

                var message = options.MessageFormatter.FormatItemChangedMessage(match, args);

                var result = new ComparisonResult(
                    SemVerChangeType.Breaking,
                    match.OldItem, match.NewItem,
                    message);

                // We are not going to process any other changes
                return(new[]
                {
                    result
                });
            }

            if (match.OldItem is IClassDefinition oldClass &&
                match.NewItem is IClassDefinition newClass)
            {
                var itemMatch = new ItemMatch <IClassDefinition>(oldClass, newClass);

                return(_classComparer.CompareMatch(itemMatch, options));
            }

            if (match.OldItem is IStructDefinition oldStruct &&
                match.NewItem is IStructDefinition newStruct)
            {
                var itemMatch = new ItemMatch <IStructDefinition>(oldStruct, newStruct);

                return(_structComparer.CompareMatch(itemMatch, options));
            }

            if (match.OldItem is IInterfaceDefinition oldInterface &&
                match.NewItem is IInterfaceDefinition newInterface)
            {
                var itemMatch = new ItemMatch <IInterfaceDefinition>(oldInterface, newInterface);

                return(_interfaceComparer.CompareMatch(itemMatch, options));
            }

            throw new NotSupportedException(
                      $"There is no {nameof(ITypeComparer<ITypeDefinition>)} implementation for {match.OldItem.GetType()}");
        }
        public IEnumerable <ComparisonResult> CompareMatch(ItemMatch <IGenericTypeElement> match, ComparerOptions options)
        {
            match   = match ?? throw new ArgumentNullException(nameof(match));
            options = options ?? throw new ArgumentNullException(nameof(options));

            var oldTypeParameters = match.OldItem.GenericTypeParameters.FastToList();
            var newTypeParameters = match.NewItem.GenericTypeParameters.FastToList();

            if (oldTypeParameters.Count == 0 &&
                newTypeParameters.Count == 0)
            {
                return(Array.Empty <ComparisonResult>());
            }

            var typeParameterShift = oldTypeParameters.Count - newTypeParameters.Count;

            var aggregator = new ChangeResultAggregator();

            if (typeParameterShift != 0)
            {
                var changeLabel = typeParameterShift > 0 ? "removed" : "added";
                var shiftAmount = Math.Abs(typeParameterShift);

                // One or more generic type parameters have been removed
                var suffix = shiftAmount == 1 ? "" : "s";
                var args   = new FormatArguments(
                    $"{{DefinitionType}} {{Identifier}} has {changeLabel} {shiftAmount} generic type parameter{suffix}",
                    match.NewItem.FullName,
                    null,
                    null);

                aggregator.AddElementChangedResult(SemVerChangeType.Breaking, match, options.MessageFormatter, args);

                // No need to look into how the generic type has changed
                return(aggregator.Results);
            }

            // We have the same number of generic types, evaluate the constraints
            for (var index = 0; index < oldTypeParameters.Count; index++)
            {
                var oldName = oldTypeParameters[index];
                var newName = newTypeParameters[index];

                var oldConstraints = match.OldItem.GenericConstraints.FirstOrDefault(x => x.Name == oldName);
                var newConstraints = match.NewItem.GenericConstraints.FirstOrDefault(x => x.Name == newName);

                EvaluateGenericConstraints(match, oldConstraints, newConstraints, options, aggregator);
            }

            return(aggregator.Results);
        }
Example #6
0
        public void FormatItemAddedMessageMapsKnownDefinitionTypes(Type definitionType)
        {
            const string?messageFormat = "{DefinitionType} {Identifier}";
            var          identifier    = Guid.NewGuid().ToString();
            var          arguments     = new FormatArguments(messageFormat, identifier, null, null);

            var definition = (IItemDefinition)Substitute.For(new[] { definitionType }, Array.Empty <object>());

            var sut = new DefaultMessageFormatter();

            var actual = sut.FormatItemAddedMessage(definition, arguments !);

            actual.Should().NotStartWith("Element ");
        }
Example #7
0
        private static void EvaluateNamedArgument(ItemMatch <IAttributeDefinition> match, ComparerOptions options,
                                                  IChangeResultAggregator aggregator)
        {
            var oldArguments = match.OldItem.Arguments.Where(x => x.ArgumentType == ArgumentType.Named).ToList();
            var newArguments = match.NewItem.Arguments.Where(x => x.ArgumentType == ArgumentType.Named).ToList();

            // At this point we have the same number of named arguments
            // Assuming there is a change, it can either be
            // All parameter names match but a value has changed, or
            // There is a different in parameter names which can only mean at least one has changed (old removed, new added)
            // The later will just be recorded as the old named parameter has been removed
            for (var index = 0; index < oldArguments.Count; index++)
            {
                var oldArgument = oldArguments[index];
                var newArgument = newArguments.FirstOrDefault(x => x.ParameterName == oldArgument.ParameterName);

                if (newArgument == null)
                {
                    // This argument has been removed
                    var args = new FormatArguments(
                        "{DefinitionType} {Identifier} has been removed",
                        oldArgument.ParameterName, null, null);

                    var message = options.MessageFormatter.FormatItemRemovedMessage(oldArgument, args);

                    var result = new ComparisonResult(SemVerChangeType.Breaking, oldArgument, null, message);

                    aggregator.AddResult(result);

                    return;
                }

                if (oldArgument.Value != newArgument.Value)
                {
                    // There is a match on the parameter names but the value has changed
                    var args = new FormatArguments(
                        "{DefinitionType} {Identifier} has changed value from {OldValue} to {NewValue}",
                        oldArgument.ParameterName, oldArgument.Value, newArgument.Value);

                    // Create a match for the arguments
                    var argumentMatch = new ItemMatch <IArgumentDefinition>(oldArgument, newArgument);

                    aggregator.AddElementChangedResult(SemVerChangeType.Breaking, argumentMatch,
                                                       options.MessageFormatter,
                                                       args);

                    return;
                }
            }
        }
        public void PropertiesReturnConstructorParameters()
        {
            var messageFormat = Guid.NewGuid().ToString();
            var identifier    = Guid.NewGuid().ToString();
            var oldValue      = Guid.NewGuid().ToString();
            var newValue      = Guid.NewGuid().ToString();

            var sut = new FormatArguments(messageFormat, identifier, oldValue, newValue);

            sut.MessageFormat.Should().Be(messageFormat);
            sut.Identifier.Should().Be(identifier);
            sut.OldValue.Should().Be(oldValue);
            sut.NewValue.Should().Be(newValue);
        }
Example #9
0
        public void FormatItemAddedMessageDeterminesDefinitionTypeBasedOnDefinitionType(Type definitionType,
                                                                                        string expected)
        {
            const string?messageFormat = "{DefinitionType} {Identifier}";
            var          identifier    = Guid.NewGuid().ToString();
            var          arguments     = new FormatArguments(messageFormat, identifier, null, null);

            var definition = (IItemDefinition)Substitute.For(new[] { definitionType }, Array.Empty <object>());

            var sut = new DefaultMessageFormatter();

            var actual = sut.FormatItemAddedMessage(definition, arguments !);

            actual.Should().Be($"{expected} {identifier}");
        }
Example #10
0
        public void FormatItemChangedMessageFormatsMessageWithMarkup()
        {
            var identifier = Guid.NewGuid().ToString();
            var oldValue   = Guid.NewGuid().ToString();
            var newValue   = Guid.NewGuid().ToString();
            var match      = new ItemMatch <IPropertyDefinition>(new TestPropertyDefinition(), new TestPropertyDefinition());
            var arguments  = new FormatArguments("{DefinitionType} {Identifier} {OldValue} {NewValue}", identifier,
                                                 oldValue, newValue);

            var sut = new GitHubMarkdownMessageFormatter();

            var actual = sut.FormatItemChangedMessage(match, arguments);

            actual.Should().Be($"Property `{identifier}` `{oldValue}` `{newValue}`");
        }
Example #11
0
        public void FormatItemAddedMessageFormatsMessageWithProvidedArguments()
        {
            const string?messageFormat = "{DefinitionType} - {Identifier} - {OldValue} - {NewValue}";
            var          identifier    = Guid.NewGuid().ToString();
            var          oldValue      = Guid.NewGuid().ToString();
            var          newValue      = Guid.NewGuid().ToString();
            var          arguments     = new FormatArguments(messageFormat, identifier, oldValue, newValue);

            var definition = Substitute.For <IClassDefinition>();

            var sut = new DefaultMessageFormatter();

            var actual = sut.FormatItemAddedMessage(definition, arguments !);

            actual.Should().Be($"Class - {identifier} - {oldValue} - {newValue}");
        }
        public void AddElementChangedResultThrowsExceptionWithNullMessageFormatter()
        {
            var oldItem   = new TestClassDefinition();
            var newItem   = new TestClassDefinition();
            var match     = new ItemMatch <IClassDefinition>(oldItem, newItem);
            var arguments = new FormatArguments(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), oldItem.Name,
                                                newItem.Name);

            var aggregator = Substitute.For <IChangeResultAggregator>();

            Action action = () =>
                            aggregator.AddElementChangedResult(SemVerChangeType.Breaking, match,
                                                               null !, arguments);

            action.Should().Throw <ArgumentNullException>();
        }
Example #13
0
        private void EvaluateParameterChanges(
            ItemMatch <IMethodDefinition> match,
            ComparerOptions options,
            IChangeResultAggregator aggregator)
        {
            var oldParameters = match.OldItem.Parameters.FastToList();
            var newParameters = match.NewItem.Parameters.FastToList();

            if (oldParameters.Count == 0 &&
                newParameters.Count == 0)
            {
                return;
            }

            var parameterShift = oldParameters.Count - newParameters.Count;

            if (parameterShift != 0)
            {
                var changeLabel = parameterShift > 0 ? "removed" : "added";
                var shiftAmount = Math.Abs(parameterShift);

                // One or more generic type parameters have been added or removed
                var suffix = shiftAmount == 1 ? "" : "s";
                var args   = new FormatArguments(
                    $"{{DefinitionType}} {{Identifier}} has {changeLabel} {shiftAmount} parameter{suffix}",
                    match.NewItem.FullName,
                    null,
                    null);

                aggregator.AddElementChangedResult(SemVerChangeType.Breaking, match, options.MessageFormatter, args);

                // No need to look into how the generic type has changed
                return;
            }

            // We have the same number of parameters, compare them
            for (var index = 0; index < oldParameters.Count; index++)
            {
                var oldParameter   = oldParameters[index];
                var newParameter   = newParameters[index];
                var parameterMatch = new ItemMatch <IParameterDefinition>(oldParameter, newParameter);

                var parameterChanges = _parameterComparer.CompareMatch(parameterMatch, options);

                aggregator.AddResults(parameterChanges);
            }
        }
Example #14
0
        public void FormatItemAddedMessageDeterminesDefinitionTypeBasedOnArgumentType(ArgumentType argumentType,
                                                                                      string expected)
        {
            const string?messageFormat = "{DefinitionType} {Identifier}";
            var          identifier    = Guid.NewGuid().ToString();
            var          arguments     = new FormatArguments(messageFormat, identifier, null, null);

            var definition = Substitute.For <IArgumentDefinition>();

            definition.ArgumentType.Returns(argumentType);

            var sut = new DefaultMessageFormatter();

            var actual = sut.FormatItemAddedMessage(definition, arguments !);

            actual.Should().Be($"{expected} {identifier}");
        }
Example #15
0
        private static void EvaluateMethodNameChanges(
            ItemMatch <IMethodDefinition> match,
            ComparerOptions options,
            IChangeResultAggregator aggregator)
        {
            if (match.OldItem.RawName == match.NewItem.RawName)
            {
                return;
            }

            var args = new FormatArguments(
                "{DefinitionType} {Identifier} has been renamed to {NewValue}",
                match.OldItem.FullName,
                null,
                match.NewItem.Name);

            aggregator.AddElementChangedResult(SemVerChangeType.Breaking, match, options.MessageFormatter, args);
        }
        private static void EvaluateParameterTypeChanges(
            ItemMatch <IParameterDefinition> match,
            ComparerOptions options,
            IChangeResultAggregator aggregator)
        {
            var oldType = match.OldItem.Type;
            var newType = match.NewItem.Type;

            var oldMappedType =
                match.OldItem.DeclaringMethod.GetMatchingGenericType(oldType, match.NewItem.DeclaringMethod);

            if (oldMappedType != newType)
            {
                var args = new FormatArguments(
                    "{DefinitionType} {Identifier} has change type from {OldValue} to {NewValue}",
                    match.NewItem.FullName,
                    oldType,
                    newType);

                aggregator.AddElementChangedResult(SemVerChangeType.Breaking, match, options.MessageFormatter, args);
            }
        }
        public void AddElementChangedResultAddsMessageResultToAggregator()
        {
            var oldItem   = new TestClassDefinition();
            var newItem   = new TestClassDefinition();
            var match     = new ItemMatch <IClassDefinition>(oldItem, newItem);
            var arguments = new FormatArguments(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), oldItem.Name,
                                                newItem.Name);
            var message    = Guid.NewGuid().ToString();
            var changeType = Model.UsingModule <ConfigurationModule>().Create <SemVerChangeType>();

            var aggregator = Substitute.For <IChangeResultAggregator>();
            var formatter  = Substitute.For <IMessageFormatter>();

            formatter.FormatItemChangedMessage(match, arguments).Returns(message);

            aggregator.AddElementChangedResult(changeType, match, formatter, arguments);

            aggregator.Received(1).AddResult(Arg.Any <ComparisonResult>());
            aggregator.Received().AddResult(Arg.Is <ComparisonResult>(x => x.OldItem == oldItem));
            aggregator.Received().AddResult(Arg.Is <ComparisonResult>(x => x.NewItem == newItem));
            aggregator.Received().AddResult(Arg.Is <ComparisonResult>(x => x.Message == message));
            aggregator.Received().AddResult(Arg.Is <ComparisonResult>(x => x.ChangeType == changeType));
        }
 /// <summary>
 /// just debugger string
 /// </summary>
 /// <returns>debugger string</returns>
 public string DebugDisplay()
 {
     return $"Number arguments {FormatArguments?.ToString()} ";
 }
 private void AddIfNotExists(string key, string value)
 {
     if (FormatArguments.ContainsKey(key))
         return;
     FormatArguments.Add(key, value);
 }
        protected virtual IEnumerable <ComparisonResult> CompareMatch(
            ItemMatch <IElementDefinition> match,
            T oldValue,
            T newValue,
            ComparerOptions options)
        {
            match = match ?? throw new ArgumentNullException(nameof(match));

            var change = _changeTable.CalculateChange(oldValue, newValue);

            if (change == SemVerChangeType.None)
            {
                yield break;
            }

            var newModifiers = GetDeclaredModifiers(match.NewItem);
            var oldModifiers = GetDeclaredModifiers(match.OldItem);

            if (string.IsNullOrWhiteSpace(oldModifiers))
            {
                // Modifiers have been added where there were previously none defined
                var suffix = string.Empty;

                if (newModifiers.Contains(" "))
                {
                    // There is more than one modifier
                    suffix = "s";
                }

                var args = new FormatArguments(
                    $"{{DefinitionType}} {{Identifier}} has added the {{NewValue}} {ModifierLabel}{suffix}",
                    match.NewItem.FullName,
                    null,
                    newModifiers);

                var message = options.MessageFormatter.FormatItemChangedMessage(match, args);

                var result = new ComparisonResult(
                    change,
                    match.OldItem, match.NewItem,
                    message);

                yield return(result);
            }
            else if (string.IsNullOrWhiteSpace(newModifiers))
            {
                // All previous modifiers have been removed
                var suffix = string.Empty;

                if (oldModifiers.Contains(" "))
                {
                    // There is more than one modifier
                    suffix = "s";
                }

                var args = new FormatArguments(
                    $"{{DefinitionType}} {{Identifier}} has removed the {{OldValue}} {ModifierLabel}{suffix}",
                    match.NewItem.FullName,
                    oldModifiers,
                    null);

                var message = options.MessageFormatter.FormatItemChangedMessage(match, args);

                var result = new ComparisonResult(
                    change,
                    match.OldItem, match.NewItem,
                    message);

                yield return(result);
            }
            else
            {
                // Modifiers have been changed
                var suffix = string.Empty;

                if (oldModifiers.Contains(" ") ||
                    newModifiers.Contains(" "))
                {
                    // There is more than one modifier
                    suffix = "s";
                }

                var args = new FormatArguments(
                    $"{{DefinitionType}} {{Identifier}} has changed the {ModifierLabel}{suffix} from {{OldValue}} to {{NewValue}}",
                    match.NewItem.FullName,
                    oldModifiers,
                    newModifiers);

                var message = options.MessageFormatter.FormatItemChangedMessage(match, args);

                var result = new ComparisonResult(
                    change,
                    match.OldItem, match.NewItem,
                    message);

                yield return(result);
            }
        }
        public virtual IEnumerable <ComparisonResult> CalculateChanges(
            IEnumerable <T> oldItems,
            IEnumerable <T> newItems,
            ComparerOptions options)
        {
            oldItems = oldItems ?? throw new ArgumentNullException(nameof(oldItems));
            newItems = newItems ?? throw new ArgumentNullException(nameof(newItems));
            options  = options ?? throw new ArgumentNullException(nameof(options));

            IMatchResults <T> matchingNodes = _evaluator.FindMatches(oldItems, newItems);

            // Record any visible items that have been added
            // Items added which are not publicly visible are ignored
            foreach (var memberAdded in matchingNodes.ItemsAdded)
            {
                var isVisible = IsVisible(memberAdded);
                var name      = memberAdded.Name;

                if (memberAdded is IElementDefinition element)
                {
                    name = element.FullName;
                }

                if (isVisible)
                {
                    var args = new FormatArguments("{DefinitionType} {Identifier} has been added", name, null, null);

                    var message = options.MessageFormatter.FormatItemAddedMessage(memberAdded, args);

                    var changeType = GetItemAddedChangeType(memberAdded);

                    var result = new ComparisonResult(changeType, null, memberAdded, message);

                    yield return(result);
                }
                else if (_logger != null &&
                         _logger.IsEnabled(LogLevel.Debug))
                {
                    var args = new FormatArguments("{DefinitionType} {Identifier} has been added but is not visible",
                                                   name, null, null);

                    var message = options.MessageFormatter.FormatItemAddedMessage(memberAdded, args);

                    _logger.LogDebug(message);
                }
            }

            // Record any visible items that have been removed
            // Items removed which are not publicly visible are ignored
            foreach (var memberRemoved in matchingNodes.ItemsRemoved)
            {
                var isVisible = IsVisible(memberRemoved);
                var name      = memberRemoved.Name;

                if (memberRemoved is IElementDefinition element)
                {
                    name = element.FullName;
                }

                if (isVisible)
                {
                    var args = new FormatArguments("{DefinitionType} {Identifier} has been removed", name, null, null);

                    var message = options.MessageFormatter.FormatItemRemovedMessage(memberRemoved, args);

                    var result = new ComparisonResult(SemVerChangeType.Breaking, memberRemoved, null, message);

                    yield return(result);
                }
                else if (_logger != null &&
                         _logger.IsEnabled(LogLevel.Debug))
                {
                    var args = new FormatArguments("{DefinitionType} {Identifier} has been removed but is not visible",
                                                   name, null, null);

                    var message = options.MessageFormatter.FormatItemAddedMessage(memberRemoved, args);

                    _logger.LogDebug(message);
                }
            }

            // Check all the matches for a breaking change or feature added
            foreach (var match in matchingNodes.MatchingItems)
            {
                var comparisonResults = CompareMatchingItems(match, options);

                foreach (var result in comparisonResults)
                {
                    yield return(result);
                }
            }
        }
        private static void EvaluateGenericConstraints(
            ItemMatch <IGenericTypeElement> match,
            IConstraintListDefinition?oldConstraintList,
            IConstraintListDefinition?newConstraintList,
            ComparerOptions options,
            IChangeResultAggregator aggregator)
        {
            var oldConstraintCount = oldConstraintList?.Constraints.Count ?? 0;
            var newConstraintCount = newConstraintList?.Constraints.Count ?? 0;

            if (oldConstraintCount == 0 &&
                newConstraintCount == 0)
            {
                // There are no generic constraints defined on either type
                return;
            }

            if (oldConstraintCount == 0)
            {
                var suffix = string.Empty;

                if (newConstraintCount != 1)
                {
                    // There is more than one modifier
                    suffix = "s";
                }

                var args = new FormatArguments(
                    $"{{DefinitionType}} {{Identifier}} has added {newConstraintCount} generic type constraint{suffix}",
                    match.NewItem.FullName,
                    null,
                    null);

                aggregator.AddElementChangedResult(SemVerChangeType.Breaking, match, options.MessageFormatter, args);

                // No need to look into the constraints themselves
                return;
            }

            if (newConstraintCount == 0)
            {
                var suffix = string.Empty;

                if (oldConstraintCount != 1)
                {
                    // There is more than one modifier
                    suffix = "s";
                }

                var args = new FormatArguments(
                    $"{{DefinitionType}} {{Identifier}} has removed {oldConstraintCount} generic type constraint{suffix}",
                    match.NewItem.FullName,
                    null,
                    null);

                aggregator.AddElementChangedResult(SemVerChangeType.Feature, match, options.MessageFormatter, args);

                // No need to look into the constraints themselves
                return;
            }

            // Find the old constraints that have been removed
            var removedConstraints = oldConstraintList !.Constraints.Except(newConstraintList !.Constraints);

            foreach (var constraint in removedConstraints)
            {
                var args = new FormatArguments(
                    "{DefinitionType} {Identifier} has removed the {OldValue} generic type constraint",
                    match.NewItem.FullName,
                    constraint,
                    null);

                aggregator.AddElementChangedResult(SemVerChangeType.Feature, match, options.MessageFormatter, args);
            }

            // Find the new constraints that have been added
            var addedConstraints = newConstraintList !.Constraints.Except(oldConstraintList !.Constraints);

            foreach (var constraint in addedConstraints)
            {
                var args = new FormatArguments(
                    "{DefinitionType} {Identifier} has added the {NewValue} generic type constraint",
                    match.NewItem.FullName,
                    null,
                    constraint);

                aggregator.AddElementChangedResult(SemVerChangeType.Breaking, match, options.MessageFormatter, args);
            }
        }