-
Notifications
You must be signed in to change notification settings - Fork 40
Disallow new abstract
type members
#259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
|
@@ -17,11 +17,10 @@ public class ApiListingComparerTests | |
public Assembly V1Assembly => typeof(ApiCheckApiListingV1).GetTypeInfo().Assembly; | ||
public Assembly V2Assembly => typeof(ApiCheckApiListingV2).GetTypeInfo().Assembly; | ||
|
||
public IEnumerable<Func<MemberInfo, bool>> TestFilters | ||
=> new Func<MemberInfo, bool> [] | ||
{ | ||
ti => (ti as TypeInfo)?.Namespace?.StartsWith("ComparisonScenarios") == false | ||
}; | ||
public IEnumerable<Func<MemberInfo, bool>> TestFilters => new Func<MemberInfo, bool> [] | ||
{ | ||
ti => (ti as TypeInfo)?.Namespace?.StartsWith("ComparisonScenarios") == false | ||
}; | ||
|
||
[Fact] | ||
public void Compare_Detects_ChangesInTypeVisibility_as_removal() | ||
|
@@ -80,6 +79,68 @@ public void Compare_Detects_TypeGenericAritybreakingChanges_as_removal() | |
Assert.Contains(expected, breakingChanges); | ||
} | ||
|
||
[Theory] | ||
[InlineData("public class ComparisonScenarios.ClassToRemoveFieldsFrom")] | ||
[InlineData("public struct ComparisonScenarios.StructToRemoveFieldsFrom")] | ||
public void Compare_DetectsAllFieldRemovals(string typeToCheck) | ||
{ | ||
// Arrange | ||
var v1ApiListing = CreateApiListingDocument(V1Assembly); | ||
var v2ApiListing = CreateApiListingDocument(V2Assembly); | ||
var comparer = new ApiListingComparer(v1ApiListing, v2ApiListing); | ||
|
||
// Oops. The NewInternalProperty addition is a breaking change; makes it impossible to subclass type in | ||
// another assembly. | ||
var expected = new List<BreakingChange> | ||
{ | ||
// Changing a const's value doesn't cause a binary incompatibility but often causes problems. | ||
new BreakingChange( | ||
typeToCheck, | ||
"public const System.Int32 ConstToChangeValue = 1", | ||
ChangeKind.Removal), | ||
// Removing a const doesn't cause a binary incompatibilty but often causes problems. | ||
new BreakingChange( | ||
typeToCheck, | ||
"public const System.Int32 ConstToMakeField = 2", | ||
ChangeKind.Removal), | ||
// Oops. Making a field writable is not technically a breaking change. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And the oops here is that API Check is disallowing a non-breaking change to a field. This one is also a corner case and it's also easy to work around (with entries in the breaking change files). |
||
new BreakingChange( | ||
typeToCheck, | ||
"public readonly System.Int32 FieldToMakeWritable", | ||
ChangeKind.Removal), | ||
new BreakingChange( | ||
typeToCheck, | ||
"public static readonly System.Int32 StaticFieldToMakeConst", | ||
ChangeKind.Removal), | ||
// Oops. Making a field writable is not technically a breaking change. | ||
new BreakingChange( | ||
typeToCheck, | ||
"public static readonly System.Int32 StaticFieldToMakeWritable", | ||
ChangeKind.Removal), | ||
new BreakingChange( | ||
typeToCheck, | ||
"public static System.Int32 StaticFieldToMakeReadonly", | ||
ChangeKind.Removal), | ||
new BreakingChange( | ||
typeToCheck, | ||
"public System.Int32 FieldToMakeReadonly", | ||
ChangeKind.Removal), | ||
new BreakingChange( | ||
typeToCheck, | ||
"public System.Int32 FieldToRemove", | ||
ChangeKind.Removal), | ||
}; | ||
|
||
// Act | ||
var breakingChanges = comparer.GetDifferences(); | ||
|
||
// Assert | ||
var breakingChanginesInType = breakingChanges | ||
.Where(change => string.Equals(change.TypeId, typeToCheck, StringComparison.Ordinal)) | ||
.OrderBy(change => change.MemberId); | ||
Assert.Equal(expected, breakingChanginesInType); | ||
} | ||
|
||
[Fact] | ||
public void Compare_Detects_NamespacebreakingChanges_as_removal() | ||
{ | ||
|
@@ -191,6 +252,56 @@ public void Compare_DoesNotFailForTypeAddingAnInterface() | |
bc => bc.TypeId == "public ComparisonScenarios.TypeWithExtraInterface")); | ||
} | ||
|
||
[Fact] | ||
public void Compare_DetectsAbstractMethodAdditions() | ||
{ | ||
// Arrange | ||
var v1ApiListing = CreateApiListingDocument(V1Assembly); | ||
var v2ApiListing = CreateApiListingDocument(V2Assembly); | ||
var comparer = new ApiListingComparer(v1ApiListing, v2ApiListing); | ||
var typeToCheck = "public abstract class ComparisonScenarios.AbstractClassToAddMethodsTo"; | ||
|
||
// Act | ||
var breakingChanges = comparer.GetDifferences(); | ||
|
||
// Assert | ||
var breakingChangesInType = breakingChanges | ||
.Where(change => string.Equals(change.TypeId, typeToCheck, StringComparison.Ordinal)); | ||
var breakingChange = Assert.Single(breakingChangesInType); | ||
Assert.Equal(ChangeKind.Addition, breakingChange.Kind); | ||
Assert.Equal("public abstract System.Void NewAbstractMethod()", breakingChange.MemberId); | ||
} | ||
|
||
[Fact] | ||
public void Compare_DetectsAbstractPropertyAdditions() | ||
{ | ||
// Arrange | ||
var v1ApiListing = CreateApiListingDocument(V1Assembly); | ||
var v2ApiListing = CreateApiListingDocument(V2Assembly); | ||
var comparer = new ApiListingComparer(v1ApiListing, v2ApiListing); | ||
var typeToCheck = "public abstract class ComparisonScenarios.AbstractClassToAddPropertiesTo"; | ||
var expected = new List<BreakingChange> | ||
{ | ||
new BreakingChange( | ||
typeToCheck, | ||
"public abstract System.Int32 get_NewAbstractProperty()", | ||
ChangeKind.Addition), | ||
new BreakingChange( | ||
typeToCheck, | ||
"public abstract System.Void set_PropertyToAddSetterTo(System.Int32 value)", | ||
ChangeKind.Addition), | ||
}; | ||
|
||
// Act | ||
var breakingChanges = comparer.GetDifferences(); | ||
|
||
// Assert | ||
var breakingChangesInType = breakingChanges | ||
.Where(change => string.Equals(change.TypeId, typeToCheck, StringComparison.Ordinal)) | ||
.OrderBy(change => change.MemberId); | ||
Assert.Equal(expected, breakingChangesInType); | ||
} | ||
|
||
[Fact] | ||
public void Compare_DetectsNewMembersBeingAddedToAnInterface_as_addition() | ||
{ | ||
|
@@ -203,9 +314,8 @@ public void Compare_DetectsNewMembersBeingAddedToAnInterface_as_addition() | |
var breakingChanges = comparer.GetDifferences(); | ||
|
||
// Assert | ||
var interfaceBreakingChanges = breakingChanges.Where( | ||
b => b.TypeId == | ||
"public interface ComparisonScenarios.IInterfaceToAddMembersTo") | ||
var interfaceBreakingChanges = breakingChanges | ||
.Where( b => b.TypeId == "public interface ComparisonScenarios.IInterfaceToAddMembersTo") | ||
.ToList(); | ||
Assert.Single(interfaceBreakingChanges, | ||
b => b.MemberId == "System.Int32 get_NewMember()" && b.Kind == ChangeKind.Addition); | ||
|
@@ -222,23 +332,24 @@ public void Compare_AllowsExclusionsOnNewInterfaceMembers() | |
var comparer = new ApiListingComparer(v1ApiListing, v2ApiListing); | ||
|
||
var knownBreakingChanges = new List<BreakingChange> | ||
{ | ||
new BreakingChange( | ||
"public interface ComparisonScenarios.IInterfaceToAddMembersTo", | ||
"System.Int32 get_NewMember()", | ||
ChangeKind.Addition), | ||
new BreakingChange( | ||
"public interface ComparisonScenarios.IInterfaceToAddMembersTo", | ||
"System.Void set_NewMember(System.Int32 value)", | ||
ChangeKind.Addition) | ||
}; | ||
{ | ||
new BreakingChange( | ||
"public interface ComparisonScenarios.IInterfaceToAddMembersTo", | ||
"System.Int32 get_NewMember()", | ||
ChangeKind.Addition), | ||
new BreakingChange( | ||
"public interface ComparisonScenarios.IInterfaceToAddMembersTo", | ||
"System.Void set_NewMember(System.Int32 value)", | ||
ChangeKind.Addition) | ||
}; | ||
|
||
// Act | ||
var breakingChanges = comparer.GetDifferences().Except(knownBreakingChanges); | ||
|
||
// Assert | ||
Assert.Null(breakingChanges.FirstOrDefault( | ||
bc => bc.TypeId == "public interface ComparisonScenarios.IInterfaceToAddMembersTo")); | ||
Assert.DoesNotContain( | ||
breakingChanges, | ||
bc => bc.TypeId == "public interface ComparisonScenarios.IInterfaceToAddMembersTo"); | ||
} | ||
|
||
[Fact] | ||
|
@@ -253,9 +364,8 @@ public void Compare_DetectsNewMembersInThePresenceOfRenamedAndRemovedMembers() | |
var breakingChanges = comparer.GetDifferences(); | ||
|
||
// Assert | ||
var interfaceBreakingChanges = breakingChanges.Where( | ||
b => b.TypeId == | ||
"public interface ComparisonScenarios.IInterfaceWithMembersThatWillGetRenamedRemovedAndAdded") | ||
var interfaceBreakingChanges = breakingChanges | ||
.Where( b => b.TypeId == "public interface ComparisonScenarios.IInterfaceWithMembersThatWillGetRenamedRemovedAndAdded") | ||
.ToList(); | ||
Assert.Single(interfaceBreakingChanges, | ||
b => b.MemberId == "System.Void MemberToBeRenamed()" && b.Kind == ChangeKind.Removal); | ||
|
@@ -279,9 +389,8 @@ public void Compare_DetectsNewMembersInThePresenceOfTheSameNumberOfRemovedAndAdd | |
var breakingChanges = comparer.GetDifferences(); | ||
|
||
// Assert | ||
var interfaceBreakingChanges = breakingChanges.Where( | ||
b => b.TypeId == | ||
"public interface ComparisonScenarios.IInterfaceWithSameNumberOfRemovedAndAddedMembers") | ||
var interfaceBreakingChanges = breakingChanges | ||
.Where(b => b.TypeId == "public interface ComparisonScenarios.IInterfaceWithSameNumberOfRemovedAndAddedMembers") | ||
.ToList(); | ||
Assert.Single(interfaceBreakingChanges, | ||
b => b.MemberId == "System.Void FirstMemberToRemove()" && b.Kind == ChangeKind.Removal); | ||
|
@@ -297,11 +406,9 @@ public void Compare_DetectsNewMembersInThePresenceOfTheSameNumberOfRemovedAndAdd | |
b => b.MemberId == "System.Void ThirdAddedMember()" && b.Kind == ChangeKind.Addition); | ||
} | ||
|
||
private ApiListing CreateApiListingDocument(Assembly assembly, | ||
IEnumerable<Func<MemberInfo, bool>> additionalFilters = null) | ||
private ApiListing CreateApiListingDocument(Assembly assembly) | ||
{ | ||
additionalFilters = additionalFilters ?? Enumerable.Empty<Func<MemberInfo, bool>>(); | ||
var generator = new ApiListingGenerator(assembly, TestFilters.Concat(additionalFilters)); | ||
var generator = new ApiListingGenerator(assembly, TestFilters); | ||
|
||
return generator.GenerateApiListing(); | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Eilon these "oops" comments are about corner cases. Worth filing bugs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I don't understand the comment in the code. What is "oops" about it? And what is the issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API check doesn't complain when an
internal abstract
member is added though that is a significant breaking change -- prevents creation of a subclass outside the defining assembly. As I said, it's a corner case but "oops", something is slipping though…