Skip to content
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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

safern
Copy link
Member

@safern safern commented Feb 10, 2021

Design doc for: dotnet/core#5701
Contributes to: dotnet/core#5700

Relates to: dotnet/roslyn-analyzers#3901

cc: @terrajobst @danmoseley @ericstj @Anipik @sharwell @ViktorHofer @stephentoub

Feel free to loop in other people that might be interested on this.


## Compatibility Rules

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonpryor @spouliot could be interested in adopting this if the rules are working for them

Copy link
Member

Choose a reason for hiding this comment

The 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. 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.
Copy link
Member

@omajid omajid Feb 10, 2021

Choose a reason for hiding this comment

The 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 .editorconfig file (which also seems a bit surprising because this isn't about configuring the editor, it's for analyzers).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the analyzer scenario .editorconfig is the best way to set this rules as project configurations don't make it to the analyzer because of how the analyzers are invoked as part of the build. So if you already have an .editorconfig with these rules and want to run the tool as a dotnet tool for some reason, I thought it might be useful to accept that .editorconfig as an input rather than having people learn about multiple command line arguments to flow those rule settings.

However, it seems like something that is not critical. We could leave the .editorconfig support only for the analyzer scenario.

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

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`.
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

### 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.
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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.


## Open questions

1. Where should these tools live? Maybe [dotnet/roslyn-analyzers](https://github.com/dotnet/roslyn-analyzers)?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the distribution mechanism? Are the tools part of the .NET SDK? Distributed separately as a nuget package?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add that as an open question, but I think it should be distributed separately as a nuget package.


### 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roslyn analyzer a dotnet tool and potentially an MSBuild Task,

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this goes together with your list of different inputs.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

- Constructor
- Generic Parameters

Should we check for assembly attributes maybe as opt-in?
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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).


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).
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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's release/3.x branch that we decided just to be extra careful when reviewing 2.1 and 3.1 code changes.


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.

## Compatibility Rules
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, sounds good.


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
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

@safern safern Feb 10, 2021

Choose a reason for hiding this comment

The 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.

cc: @ericstj @Anipik


### 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain why it might be helpful (to customers?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll elaborate more on each input.

@tmds
Copy link
Member

tmds commented Feb 11, 2021

I was wondering today whether I could reuse some of the tooling I know from dotnet repos in my own libraries. @ViktorHofer pointed me to this design.

I see many things I'd like in the design doc. This is how I imagine using them:

After adding something in csproj, dotnet build produces a text file that describes the public API and compares it with a text file that describes the previous API.
When they differ on my development machine, I get a warning during the build. When they differ in the CI build, it is an error.
When I think the changes are good, I can copy them over, and include them in the commit.

@drewnoakes
Copy link
Member

See also dotnet/roslyn-analyzers#3047 for some places where the Roslyn PublicApiAnalyzer could have been extended.


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).
Copy link
Member

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's release/3.x branch that we decided just to be extra careful when reviewing 2.1 and 3.1 code changes.


`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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI dotnet/aspnetcore uses Microsoft.CodeAnalysis.PublicApiAnalyzers in our release/5.0 and main branches

I assume you mean

Suggested change
`Microsoft.CodeAnalysis.PublicAnalyzers` is used in `dotnet/roslyn` and it is a pure roslyn analyzer.
`Microsoft.CodeAnalysis.PublicApiAnalyzers` is used in `dotnet/roslyn` and it is a pure roslyn analyzer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. So, a Roslyn analyzer for in-proc use and a command-line tool to wrap it❔
  2. What about fixers usable from command line, VS, VS for Mac, and VS Code❔

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

however, we think that can come afterwards

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.

Copy link
Member Author

@safern safern Feb 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

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❔


`Microsoft.CodeAnalysis.PublicAnalyzers` is used in `dotnet/roslyn` and it is a pure roslyn analyzer.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
The biggest difference in between these is the inputs they take for the contract to know if the current implementation is a breaking change. `PublicApiAnalyzers` 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. Whereas `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.


### 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
Just as `Microsoft.CodeAnalysis.PublicApiAnalyzers` 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.

### 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding an abstract member is also a breaking change

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.
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this should cover the interfaces from the base types.


## Compatibility Rules

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.
Copy link
Member

Choose a reason for hiding this comment

The 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.

@safern
Copy link
Member Author

safern commented Feb 25, 2021

I see many things I'd like in the design doc. This is how I imagine using them:

After adding something in csproj, dotnet build produces a text file that describes the public API and compares it with a text file that describes the previous API.
When they differ on my development machine, I get a warning during the build. When they differ in the CI build, it is an error.
When I think the changes are good, I can copy them over, and include them in the commit.

Thanks @tmds for your input and interest on this. I will push an updated doc after the feedback and your input and will describe more extensively the scenarios and how customers would use it.

See also dotnet/roslyn-analyzers#3047 for some places where the Roslyn PublicApiAnalyzer could have been extended.

Thanks, @drewnoakes, will make sure to include all of them in the list of rules. I believe most of them are already covered, but I need to confirm.

Adding [Obsolete] to a member, I think that should be configurable by excluding/including the attribute from the attributes list so that each customer can tweak about the attributes they care... maybe for a major release they want to obsolete some members and want to exclude that attribute from the errors in the compatibility checks from one release to the other.

@ericstj
Copy link
Member

ericstj commented Jun 17, 2021

@safern, what do we need to do to move this forward?

@safern
Copy link
Member Author

safern commented Jun 17, 2021

I'll update it to what we decided at the end and clean it up. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.