Skip to content

[APICompat - PackageValidation] Enable baseline assembly references #32930

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

Merged
merged 3 commits into from
Jun 1, 2023

Conversation

ViktorHofer
Copy link
Member

@ViktorHofer ViktorHofer commented May 31, 2023

Fixes #32922, #18676, #31271

If assembly references aren't provided for the baseline package via the MSBuild task or the CLI frontend, use the current package's assembly references as an approximation.

As current assembly references are grouped by TFMs, use the nearest compatible TFM for which assembly references are provided. This guarantees that assembly references from current are used even if TFMs don't match.

@ericstj what do you think about the assembly reference change related to picking the nearest compatible TFM? Do we want a baseline package with a lib/net7.0/a.dll asset to use the assembly references from the current package lib/net8.0/a.dll asset? Note that the current assembly symbol loader ignored versions.

@ViktorHofer ViktorHofer requested review from ericstj, a team and joperezr as code owners May 31, 2023 13:45
@ViktorHofer ViktorHofer self-assigned this May 31, 2023
@ghost ghost added the untriaged Request triage from a team member label May 31, 2023
@ghost
Copy link

ghost commented May 31, 2023

I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label.

@ericstj
Copy link
Member

ericstj commented May 31, 2023

@ericstj what do you think about the assembly reference change related to picking the nearest compatible TFM?

What I was imagining we'd do was use the right-hand-side dependencies when the left-hand-side dependencies were missing. This was what we did previously with API Compat.

Nearest might be OK - so long as you're going forward and not back (I always get mixed up with NuGet's terms around this), since the nearest forward should be compatible and should be a parallel operation to the compatibility tuples we're creating between frameworks.

Copy link
Member

@ericstj ericstj left a comment

Choose a reason for hiding this comment

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

Can we add a test case for this somehow?

@ViktorHofer
Copy link
Member Author

ViktorHofer commented May 31, 2023

What I was imagining we'd do was use the right-hand-side dependencies when the left-hand-side dependencies were missing. This was what we did previously with API Compat.

Yes, that's what this PR is doing.

Nearest might be OK - so long as you're going forward and not back (I always get mixed up with NuGet's terms around this), since the nearest forward should be compatible and should be a parallel operation to the compatibility tuples we're creating between frameworks.

I'm slightly confused what you refer to with going back. The current package (right hand side) usually uses the same or a newer TFM and presumably rarely an older TFM. In my example above, would we want to allow the following behavior or not (?):

Do we want a baseline package with a lib/net7.0/a.dll asset to use the assembly references from the current package lib/net8.0/a.dll asset?

...

Can we add a test case for this somehow?

Non trivial as many of our package validation integration tests are currently disabled because of #23533. I'm unsure if I can come up with a new test without deeply looking and with that fixing the test issue. I did run my changes on top off the msbuild change and the error went away. I also verified in the debugger that the assembly references are considered on the left hand side - as expected.

@ericstj
Copy link
Member

ericstj commented May 31, 2023

Yes, that's what this PR is doing.

Not quite, it's computing which references to use independently based on the TFM of each side. I meant something more specific. Given a tuple comparing {A} to {B, b-references} use b-references for A -- without any calculation involving TFM at all. Its a bit mechanical and leaves less up to chance. For example, in this method:
https://github.com/dotnet/sdk/blob/e0a643139aa4ec61f8953f36ff3cd0988bbbce51/src/ApiCompat/Microsoft.DotNet.PackageValidation/ApiCompatRunnerExtensions.cs#LL20C28-L20C57
All the content items on the right would have the same TFM and you could pass that down to GetMetadataInformation and use it in case the left-side TFM didn't exist in package.AssemblyReferences. Maybe as a "fallbackReferenceFramework" parameter?

would we want to allow the following behavior or not

Yeah, we'd want that behavior. I'm agreeing with you -- I'm just asking to double check that NuGetFrameworkUtility.GetNearest is doing the right thing here. I can never remember which direction that API searches. Have a look at this tool to illustrate what we don't want to do: https://nugettools.azurewebsites.net/6.4.0/get-nearest-framework?project=net7.0&package=net6.0%0D%0Anet8.0. Regardless, if you can avoid doing an extra calculation here I think that would be better.

@ViktorHofer
Copy link
Member Author

ViktorHofer commented Jun 1, 2023

Not quite, it's computing which references to use independently based on the TFM of each side. I meant something more specific. Given a tuple comparing {A} to {B, b-references} use b-references for A -- without any calculation involving TFM at all. Its a bit mechanical and leaves less up to chance.

What makes this hard with the current architecture is that the component that loads symbols and compares left against rights does have a 1:n relationship. A single left is often compared against multiple rights. As an example:

Package TFMs
Baseline netstandard2.0
Current netstandard2.0, net7.0

APICompat / PackageValidation validates the [Baseline] netstandard2.0 asset against the netstandard2.0 and net7.0 assets. That design is implemented in all ApiCompatibility.dll components: ApiCompatRunner, ApiComparer, RuleRunner except for the rules themselves which operate on symbols via a 1:1 relationship.
While in this example, it's clear which reference assemblies to pick (as there's a direct match), it's less clear in the following case:

Package TFMs
Baseline netstandard2.0
Current net6.0

In that scenario, the customer decides to drop the netstandard2.0 asset but still expects APICompat to validate that the API between the netstandard2.0 and the net6.0 isn't changed in an incompatible way. It's not entirely clear to me if we want to pass the net6.0 reference assemblies from the current asset to the [Baseline] netstandard2.0 asset. My change doesn't do that as the net6.0 reference assembly group can't be referenced from the netstandard2.0 baseline TFM from a NuGet compatibility standpoint.

Nearest might be OK - so long as you're going forward and not back (I always get mixed up with NuGet's terms around this), since the nearest forward should be compatible and should be a parallel operation to the compatibility tuples we're creating between frameworks.

OK, I now understand what you mean by that. Yes, right now we are going backwards instead of forward. Will see if I can change that and avoid the additional compatibility check.

If assembly references aren't provided for the baseline package via the
MSBuild task or the CLI frontend, use the current package's assembly
references as an approximation.

As current assembly references are grouped by TFMs, use the nearest
compatible TFM for which assembly references are provided. This
guarantees that assembly references from current are used even if TFMs
don't match.
@ViktorHofer ViktorHofer force-pushed the PackageValidationBaselineUseAssemblyReferences branch from a4eb4b8 to c1172a2 Compare June 1, 2023 13:34
@ViktorHofer
Copy link
Member Author

I found a place that enqueues package validation assemblies with an 1:1 mapping (TFM to TFM) which allows to re-use the assembly references from the right. Let's see what CI says. I re-ran the tests on top of the msbuild PR and the errors disappeared.

@ericstj
Copy link
Member

ericstj commented Jun 1, 2023

It's not entirely clear to me if we want to pass the net6.0 reference assemblies from the current asset to the [Baseline] netstandard2.0 asset.

Think about API Compat baseline check as doing an experiment on framework supported by the latest package. It's doing a what if check between the previous version and the new version on a supported framework. Imagine all the references as the "control" in that experiment. So API Compat is answering the question of what will break if I updated the baseline package to the latest package in a project targeting a specific framework? The references in that case are not the variable under test, so treating them like a control is reasonable -- it does mean we might miss breaking changes in those references but we're already excluding them from the comparison rules so IMO it's an acceptable heuristic.

@ViktorHofer
Copy link
Member Author

ViktorHofer commented Jun 1, 2023

With these changes, I do see quite a lot of errors in a runtime allconfigurations build. I just verified that at least one of them is legit and due to dotnet/runtime#75235.

Looking at the other ones now.

Copy link
Member

@ericstj ericstj left a comment

Choose a reason for hiding this comment

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

This looks better, just one small suggestion.

leftContentItems[leftIndex],
displayString: options.IsBaselineComparison ? Resources.Baseline + " " + leftContentItems[leftIndex].Path : null,
// Use the assembly references from the right package if the left package doesn't provide them.
assemblyReferences: rightPackage is not null ? right[right.Length > leftIndex ? leftIndex : 0].References : null);
Copy link
Member

Choose a reason for hiding this comment

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

Are the references actually specific to some index here? I thought this index was iterating through multiple content items from the package, multiple assemblies in the same folder, so I would expect all the references to be the same for a given call of this method.

Ultimately it looks like a work item loads all assemblies in the same loader (CSharpCompilation), so associating references with individual content items is probably unnecessary overhead:
https://github.com/dotnet/sdk/blob/5f6b9be62ba8952410d2b181fe5a2ac3fee4c03a/src/ApiCompat/Microsoft.DotNet.ApiCompatibility/Runner/ApiCompatRunner.cs#LL123C31-L123C31

Copy link
Member Author

Choose a reason for hiding this comment

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

It's unnecessary, yes but I felt like keeping the index in sync (as in most cases left and right count is in sync) makes more sense than just always defaulting to the first item.

Copy link
Member

Choose a reason for hiding this comment

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

Fair, but it was confusing to me trying to understand the relationship between the indexes of these two lists. I was trying to understand why we would want to align them but would also be OK with just substituting the first. Perhaps a future refactoring might move references up to the work-item instead of associate with the individual files.

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 will remove that indexing as you are right that it's unnecessary overhead and adds complexity.

Ultimately it looks like a work item loads all assemblies in the same loader (CSharpCompilation), so associating references with individual content items is probably unnecessary overhead:

Just to clarify, a workitem doesn't share the compilation context as this would otherwise result in reference assembly clashes. The individual elements (lefts and rights) each create a compilation context and load the assemblies into that container. Not relevant for this discussion but this domain has some perf optimization potentials. I.e. the assembly symbol loader could respect assembly versions (and public key tokens) and cache the loaded assembly symbols.

Copy link
Member

@ericstj ericstj left a comment

Choose a reason for hiding this comment

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

Change seems OK. My feedback is more around a future refactoring. Thank you for fixing this, by the way. I think this fix will help quite a bit.

@ViktorHofer ViktorHofer merged commit c47af12 into main Jun 1, 2023
@ViktorHofer ViktorHofer deleted the PackageValidationBaselineUseAssemblyReferences branch June 1, 2023 20:30
@ViktorHofer
Copy link
Member Author

All the errors that appeared in runtime with this change are correct. There is one odd case though:

  • APIs switching from the public ObsoleteAttribute to the internal polyfill one (for netstandard2.0). That is flagged as an attribute removal (as the public attribute is removed and APICompat filters out the internal one).

ViktorHofer added a commit to dotnet/runtime that referenced this pull request Jun 2, 2023
... and suppress the new errors that are flagged because of breaking changes introduced in .NET 8. See dotnet/sdk#32930 for more details on the added validation in APICompat.
ViktorHofer added a commit to dotnet/runtime that referenced this pull request Jun 2, 2023
* Update APICompat.Task to latest version

... and suppress the new errors that are flagged because of breaking changes introduced in .NET 8. See dotnet/sdk#32930 for more details on the added validation in APICompat.

* Suppress new errors that were intentionally made

* Update Version.Details.xml

* Update CompatibilitySuppressions.xml

* Update CompatibilitySuppressions.xml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-ApiCompat untriaged Request triage from a team member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ApiCompat flags introduction of a base exception type for exceptions previously inheriting from System.Exception
2 participants