-
Notifications
You must be signed in to change notification settings - Fork 162
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
Proposal to provide API Compat tools #177
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,143 @@ | ||||||
# API Compatibility | ||||||
|
||||||
In an effort to provide tools to help customers write long term compatible libraries there are different tools. Currently we have [`Microsoft.DotNet.ApiCompat`](https://github.com/dotnet/arcade/tree/0a67fb1cd008b0d30577f53d1633ed00db8bc1e6/src/Microsoft.DotNet.ApiCompat#microsoftdotnetapicompat) and [`Microsoft.CodeAnalysis.PublicApiAnalyzers`](https://github.com/dotnet/roslyn-analyzers/blob/master/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md). In between these two there is a huge difference on how these work, what rules they have and how to integrate it to libraries. | ||||||
|
||||||
`Microsoft.DotNet.ApiCompat` is heavily used in `ASP.NET`, `dotnet/runtime`, `ML.NET` and a few other products across the `dotnet` orgs. While `ApiCompat` has a lot of useful rules and functionality, it uses `CCI` as its metadata object model which brings some limitations on maintainibility and extensibility when new language features are added because `CCI` doesn't know about them and we have to implement those ourselves (i.e ref structs, readonly, nullable annotations, etc). | ||||||
|
||||||
`Microsoft.CodeAnalysis.PublicAnalyzers` is used in `dotnet/roslyn` and it is a pure roslyn analyzer. | ||||||
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. FYI dotnet/aspnetcore uses I assume you mean
Suggested 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. Thanks. |
||||||
|
||||||
The biggest difference in between these is the inputs they take for the contract to know if the current implementation is a breaking change. `PublicAnalyzers` looks for two files in the project, `PublicAPI.Shipped.txt` to check for compat against previous versions of the library and `PublicAPI.Unshipped.txt` to check for compat on the current release. Wheareas `ApiCompat` takes two binaries, one for the contract and the other for the implementation and checks for the differences in between those two. These two different inputs are useful in different ways and customers have their own preference. | ||||||
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.
Suggested change
|
||||||
|
||||||
The proposal in this document is to create a tool that will help customers build compatible libraries by helping them identify breaking changes as part of their builds. This tool, will make sure that the public APIs in between an implementation and a contract (which can be a previous version or a current version of the library) are compatible to avoid introducing an accidental breaking change. | ||||||
|
||||||
## How is this tool going to be consumed | ||||||
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. Does this tool understand anything about assembly versioning eg semver? I assume not. 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. I don't think that is in the scope of this tool but it might come into play when thinking on package validation which will land into the same area/tool as we want to make it less expensive so having different tools for binary compatibility and package compat would defeat that purpose. |
||||||
|
||||||
We are proposing a tool that can be consumed as a Roslyn analyzer and also providing a dotnet tool that people can integrate to their build systems as an out of proc tool or run directly from the console. | ||||||
|
||||||
Something to consider would be adding an MSBuild Task entrypoint so that libraries that need to run it in their build, they don't need to shell out a process to invoke it and just run in-proc. | ||||||
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.
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. We still need to figure out the fixer's scenario. From talking with @ericstj we think that running in-proc as an MSBuild Task is the most important scenario to tackle as that is what drives compat from one release to another. The analyzer scenario is very valuable in this case, specifically for the code fixers (I have a compat warning, help me fix it), however, we think that can come afterwards. 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.
Could you please expand on the reasoning here❔ My thought is that without automation to correct the API baselines, ASP.NET Core just has too much surface area to handle. That means we can't use new tooling without reliable and usable fixers. It's currently a pain point that the PublicAPIAnalyzers fixers work only in Visual Studio. Requiring manual API baseline generation and updates is a non-starter. 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.
Oh, we do plan to support baselining errors automatically regardless of having the analyzer scenario or not... I still need to discuss with @terrajobst the user experience for baselining errors. Cause with analyzers you can disable on the code or via .editorconfig, so we might want to be able to do similar stuff like a global "NoWarn" and also baseline errors via a baseline file. But we think that the MSBuild Task can generate this for you if you run it with a flag (which is essentially the same thing as running a codefixer). Does that make sense? By the way this is great input and that is why I wanted to publish the doc early to gather feedback, no I'm working tomorrow with @terrajobst to polish this and put it in a final state where we can say, this is how everything is going to work on v1 and this is what will come in future releases, etc. 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.
I'm not sure because my expectation is we need to fix both product code (possibly with suppressions) and the baselines. The tool you described only generates baselines, right❔ |
||||||
|
||||||
## Tool input scenarios | ||||||
|
||||||
### 1. Text files | ||||||
|
||||||
Just as `Microsoft.CodeAnalysis.PublicAnalyzers` currently accepts a `PublicAPI.Unshipped.txt` and `Public.Shipped.txt` files as inputs, we should preserve this behavior, but extend some of the rules and rule settings that we support so that all inputs have equal support. | ||||||
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.
Suggested change
|
||||||
|
||||||
### 2. Binary inputs | ||||||
|
||||||
Having binaries as inputs is really helpful for compatibility, as this allows you to run a reference assembly against multiple runtime implementations (i.e win-x64, linux-x64, etc) and make sure you are compatible across all runtimes with previous versions of your library and also with the reference assembly. | ||||||
|
||||||
### 3. Source inputs | ||||||
|
||||||
Instead of binaries or text files, it might be helpful to capture what the contract is as a C# minimal file (somewhat like a native header file) that contains the API definitions and metadata. For this we would provide a separate tool, aka `GenAPI` that given an binary input produces minimal C# code. This tool will also be Roslyn based, protype can be found here: https://github.com/safern/roslyn-genapi. This provides a contract that is "easier" to read and is also buildable and produces a minimal reference assembly, currently what `dotnet/runtime` uses as a contract. | ||||||
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. Could you explain why it might be helpful (to customers?) 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. Sure, I'll elaborate more on each input. |
||||||
|
||||||
## Compatibility Rules | ||||||
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. It might be easier to just pull out all these rules into a separate doc since that's really a separate set of decisions and then this doc can focus on how the tool will be used, how we will write it, etc. 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. Sure, sounds good. |
||||||
|
||||||
After looking at the existing tools, these is an overview of the proposed rules for the first version of the tool and that will run by default. | ||||||
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. 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. When in servicing, almost any change to public types and members is a breaking change. Hope narrowing errors or warnings to breaking changes can be disabled i.e. we have the ability to get the tool to scream about any change. |
||||||
|
||||||
### 1. Attribute Difference | ||||||
This rules checks attributes on public members and gives an error if there is a miss match on the attributes. This rule checks attributes on: | ||||||
- Types (Class, Struct, Enum, Interface, Record, Delegate) | ||||||
- Fields | ||||||
- Properties | ||||||
- Methods | ||||||
- Event | ||||||
- ReturnValue | ||||||
- Constructor | ||||||
- Generic Parameters | ||||||
|
||||||
Should we check for assembly attributes maybe as opt-in? | ||||||
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. The concern here is that most of them don't relate to compat? 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. Attributes are all use case specific if they impact compatibility or not. You need to understand the purpose of the attribute to know if it matters. This is not only limited to assembly attributes. IMHO a public tool should error on the side of minimizing false positives by default. We should have rules for attributes we know impact compatibility, and then define the comparisons for those attributes. We could allow a user to enable more checking if we think its important, perhaps by enabling user-specified attributes and assuming a default compatibility rule (exact match). |
||||||
|
||||||
It should check for the following: | ||||||
1. Attribute does not exist in the contract or implementation. | ||||||
2. Attribute arguments does not match in both the contract and implementation. | ||||||
2.1 For flag arguments (i.e AtrributeTargets) we should decide whether expanding or removing a flag is breaking or not. | ||||||
|
||||||
We should allow a Rule setting to disable attribute diffing for a list of attributes where libraries can add attributes they don't care about compat, i.e `EditorBrowsableAttribute`. | ||||||
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. Would we be opinionated by default, e.g. with a built-in default list of attributes we believe shouldn't impact compatibility? Or would we require every developer to build that list up themselves? 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. Yeah, I think we should have a default list of attributes that shouldn't impact compatibility, like compiler generated attributes, obsolete attributes, editor browsable, etc. Will add a note about that here. |
||||||
|
||||||
### 2. Cannot remove abstract members | ||||||
This rules check for abstract members in types. This rule validates that a type has the same abstract members in both the contract and implementation when the type can be used as base type: | ||||||
1. Is marked as sealed. | ||||||
2. Has no non static visible constructors outside the assembly. | ||||||
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. Adding an |
||||||
|
||||||
### 3. Cannot make member non virtual | ||||||
This rule checks that a member should be virtual on both the implementation and in the contract when the member is overridable: | ||||||
1. Is virtual. | ||||||
2. Is not sealed. | ||||||
3. Type is not sealed. | ||||||
|
||||||
### 4. Cannot remove base type or interface | ||||||
This rule should check that the base types and interfaces match on both the implementation and in the contract. | ||||||
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. Important to handle the special case of moving an interface declaration to the base type. That's not 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. Yeah, this should cover the interfaces from the base types. |
||||||
|
||||||
### 5. Cannot seal type | ||||||
This rule checks if the type is non inheritable on both the implementation and contract: | ||||||
1. Has the sealed modifier | ||||||
2. Has no non static visible constructors outside the assembly. | ||||||
|
||||||
### 6. Enum type must match | ||||||
Enum underlying type must match on both the implementation and contract. | ||||||
|
||||||
### 7. Enum values must match | ||||||
Enum values most match on both the implementation and contract. | ||||||
|
||||||
### 8. Interface should have same members | ||||||
Interface members must match on both the implementation and contract. | ||||||
|
||||||
We should have a rule setting that users can set to support allowing adding a new member to the interface if the member includes a default implementation. Adding a member to an interface on new releases is a breaking change unless the member has a default implementation, this requires C# 8.0. | ||||||
|
||||||
### 9. Members must exist | ||||||
This rules checks that all the public members should exist on both the implementation and contract. This applies to any member that can be accessible within outside of the assembly, excluding interface members as those are handled in a separate rule. | ||||||
|
||||||
1. This rules makes sure that the member exists on the type or any of it's base types. | ||||||
2. Parameter types are equal. | ||||||
3. Return types match. | ||||||
|
||||||
### 10. Modifiers cannot change | ||||||
This rule checks for differences in marshaling attributes like `in`, `out`, `ref`, `readonly` and custom modifiers like `const` and `volatile` in parameters and member return's type. | ||||||
|
||||||
1. Return's type modifiers can be ref, readonly or none. | ||||||
2. Parameter modifiers can be in, out, ref and custom modifiers like const and volatile can exist. | ||||||
|
||||||
### 11. Parameter names cannot change | ||||||
This rule makes sure that constructor, property or method parameter names match. | ||||||
|
||||||
### 12. Type cannot change classification | ||||||
This rule makes sure that type's classification doesn't change. Type classification refers to, `class`, `struct`, `ref struct`, `readonly`, `interface` or `delegate`. | ||||||
|
||||||
### 13. Nullable annotations should match | ||||||
This rule should make sure that nullable annotations match in between the implementation and the contract. | ||||||
|
||||||
Also, this rule should allow differences in nullable annotations that are not breaking, for example, relaxing a nullable reference type annotation on an in parameter (i.e `public string Foo(string a) -> public string Foo(string? a)`). However, this should be configurable via rule settings to disable this as in some cases where you are comparing a reference assembly vs it's implementation you want them to match in all cases. | ||||||
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. Does the above list cover everything relevant called out in the breaking change guidance we have in dotnet/runtime? 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. Also, are there any differences between this and what apicompat protects in dotnet/runtime? 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. Yes, APICompat doesn't yet support nullable. I think we should be calling out all the rules which APICompat has today as well as those which we deem are needed. 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. I doubled checked and this list covers everything we have in dotnet/runtime. It is a super set as @ericstj mentioned we don't yet support nullable in APICompat. |
||||||
|
||||||
### 14. Constructor makes base class instantiable. | ||||||
Rule that checks if a public API is a constructor that makes a class instantiable, even though the base class is not instantiable. That API pattern is not allowed, because it causes protected members of the base class, which are not considered public APIs, to be exposed to subclasses of this class. | ||||||
|
||||||
## Configurability | ||||||
|
||||||
Given that we are proposing a Roslyn analyzer a dotnet tool and potentially an MSBuild Task, we should support a friendly format for all three possible scenarios to baseline warnings/errors, configure rules and severity of rules. | ||||||
|
||||||
To baseline errors we could support: | ||||||
|
||||||
1. All analyzer's disable mechanism when using the tool as an analyzer. | ||||||
2. A txt file that lists the errors to baseline (maybe we could support this for the analyzer scenario as well). | ||||||
|
||||||
Configuring rules involves settings like, ignoring certain attributes as describe in the attribute compat rule, rules number 8 in the document, etc. I think these could be part of the `.editorconfig` with configuration for rules' severity. | ||||||
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. Sorry if this is a silly question, but why dont we use the project files that already contain all project configuration for this? Why a separate 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. For the analyzer scenario However, it seems like something that is not critical. We could leave the 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. After chatting with @ericstj extensibly we believe that the analyzer scenario is not the highest priority at the moment so we shouldn't design based on the analyzer scenario, the analyzer scenario can come after. |
||||||
|
||||||
### MSBuild support | ||||||
|
||||||
We should provide MSBuild first class configurability via properties so that costumers can just add a `PackageReference` and set properties on how they want to consume the tool (via MSBuild Task, Analyzer, etc), what the inputs are going to be, where the baseline errors file can be found, etc. This would make it very simple for customers to include in their libraries and configure without having to write complicated MSBuild targets to run the tool on an MSBuild process if that is their best way to hook it up into their build system. | ||||||
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.
Could you say a little more about the scenarios for each? I guess Roslyn analyzers don't get handed the list of references, or we wouldn't need an MSBuild task also? When would we use each of these -- and do we need all three in this release? 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. Maybe this goes together with your list of different inputs. 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. Not to belabor the point, -- but I think it would be helpful to walk through from a customer point of view -- they have an existing library they're building and shipping, and want to be sure they don't unintentionally break the surface -- they heard about this feature -- what do they do now? Other scenarios might include -- understanding whether their dependencies changed ? Baselining a known break? Baselines that differ according to target framework? Etc. There may be other scenarios. 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. Sure, makes sense. I will also add priority of each one, user scenarios, pros and cons. |
||||||
|
||||||
With this we could extend support to make it easier for packages to run validation by automatically download previous version(s) of the package and then run compatibility checks vs the current version by just setting a few MSBuild properties. | ||||||
|
||||||
## Diagnostics consistency | ||||||
|
||||||
When running as an analyzer's we are going to provide compiler diagnostics, so when running outside of an analyzer context, we should provide diagnostics that are consistent on all scenarios so that baselining these diagnostics and understanding them is clear within an editor, console or msbuild process. | ||||||
|
||||||
So something like: | ||||||
|
||||||
``` | ||||||
error RS0017: <helpful error description that doesn't depend of a squiggle to tell what's going on> | ||||||
``` | ||||||
|
||||||
## Open questions | ||||||
|
||||||
1. Where should these tools live? Maybe [dotnet/roslyn-analyzers](https://github.com/dotnet/roslyn-analyzers)? |
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.
With this effort, could we delete
Microsoft.DotNet.ApiCompat
everywhere? What work would be necessary in each repo to change over?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.
Yeah, I think with this work
Microsoft.DotNet.ApiCompat
should be obsoleted and each repo should change over.Changing over will be, consuming the new package/tool, deciding what's the best scenario for each repo and adjusting based on the new support, MSBuild properties/targets, etc.
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.
nit / FYI: dotnet/aspnetcore does not use
Microsoft.DotNet.ApiCompat
. We considered using it but enough bug fixes weren't back-ported to Arcade'srelease/3.x
branch that we decided just to be extra careful when reviewing 2.1 and 3.1 code changes.