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

Add analyzer/fixer that notifies people about explicit (but hidden) casts the compiler generates for foreach-statements. #60120

Merged
merged 31 commits into from
Mar 23, 2022

Conversation

CyrusNajmabadi
Copy link
Member

Fixes #54712

This is gated behind a codestyle option called:

dotnet_style_prefer_foreach_explicit_cast_in_source

It has three potential values:

  1. always: the cast must always be explicitly written in source if this casting was actually intentional.
  2. never: it's fine to not have the cast be explicitly written in source.
  3. non_legacy (default): for modern APIs the cast must be in source. for legacy APIs it's acceptable for ergonomic reasons to leave the cast off. A legacy API is one that doesn't implement IEnumerable<T> and returns object values. These are pre-2.0 APIs in the .net framework that have never been updated. The language feature here of implicitly emitted explicit casts was intentionally created to make using these APIs not as painful.

In general, for modern APIs, having this explicit cast be emitted is likely a bug indicating the user used the wrong foreach-type which will fail at runtime, but the compiler allows at compile time because it cannot prove failure. The user should either switch to a known safe (implicit) conversion which will not have any risk at runtime, or they should use an explicit cast int eh code to make it clear waht's going to happen.

A fixer comes with this that will insert that explicit cast for the user.

Tests graciously provided by @maxkoshevoi! Thank you very much. I didn't realize you already did the fixer too when i started on this on our end :'(

namespace Microsoft.CodeAnalysis.CSharp.Analyzers.ForEachCast
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpForEachCastDiagnosticAnalyzer : AbstractForEachCastDiagnosticAnalyzer<
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've only done the C# side for now. but the VB side wuold be trivial for someone to add as well.

@maxkoshevoi
Copy link

maxkoshevoi commented Mar 11, 2022

Hm, yes, original PR links got broken when issue moved around.

  • So, should CA2259: Use implicitly assignable foreach variable type roslyn-analyzers#4868 be closed? It was reviewed and ready to go a year ago (but your configurable three value solution is better)
  • Apart from the tests, diagnostic message wording might be useful for this PR as well
  • General question. New analyzers are merged into roslyn repo now? Is roslyn-analyzers still used for something?

context.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
node.GetFirstToken().GetLocation(),
Descriptor.GetEffectiveSeverity(options),
Copy link
Member

Choose a reason for hiding this comment

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

I recall @mavasani mentioned that option.Notification.Severity should be used to support the option = value:severity syntax. Not sure if that applies here.

Copy link
Member Author

Choose a reason for hiding this comment

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

wfm.

KeyValuePairUtil.Create("non_legacy", ForEachExplicitCastInSourcePreference.NonLegacy),
});

internal static readonly PerLanguageOption2<CodeStyleOption2<ForEachExplicitCastInSourcePreference>> ForEachExplicitCastInSource =
Copy link
Member

Choose a reason for hiding this comment

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

AutomationObject may need an update.

Copy link
Member Author

Choose a reason for hiding this comment

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

enh... but does it really?

Copy link
Member

Choose a reason for hiding this comment

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

@CyrusNajmabadi It affects importing and exporting VS settings. I recall @jmarolf added some missing options few years ago, so he might have a better idea. Usually I find myself adding options there.

Copy link
Member

Choose a reason for hiding this comment

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

#16512 That was the PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

I added an option the other day. I added code to AutomationObject in the same PR. I missed seeing you comment on my PR @Youssef1313 😁

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, if the expectation is that this option is round-trip-able in Visual Studio then support for it needs to be in the AutomationObject since that is what allows us to serialize VS Settings xml

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 wasn't intending to add a VS option for this. This seemed like a very niche thing to have a user visible option on. I think this is more for users who run into this issue and decide to opt in at the editorconfig level. But we can def discuss this on monday if people feel differently :)

Copy link
Member

@Youssef1313 Youssef1313 Mar 12, 2022

Choose a reason for hiding this comment

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

@CyrusNajmabadi In that case, I wonder why there is a RoamingProfileStorageLocation for this option? Shouldn't EditorConfigStorageLocation alone be sufficient?

Copy link
Member Author

Choose a reason for hiding this comment

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

yup. that makes sense. will fixup. thanks! :)

@RikkiGibson RikkiGibson self-assigned this Mar 11, 2022
@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
Copy link
Member

Choose a reason for hiding this comment

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

This is the only change in this file.

@RikkiGibson
Copy link
Contributor

Sorry, I know I just asked this offline, but do code style analyzers run during command line builds (say, when building with a recent .NET SDK)? I thought they do not.

@Youssef1313
Copy link
Member

Sorry, I know I just asked this offline, but do code style analyzers run during command line builds (say, when building with a recent .NET SDK)? I thought they do not.

They don't by default, but can be opted-in using EnforceCodeStyleInBuild

@davidwengier
Copy link
Contributor

Apologies if this is not the right time, but this is the first I've seen of this idea, and it strikes me as a little odd. An analyzer that says "this could throw at runtime" is great, but its strange to me that the "fix" changes the code to something that could still throw at runtime in exactly the same way.

Was there any alternatives suggested? Like also a fix to OfType (though granted it's a little "on error resume next"-y and definitely changes semantics)? Or fix to var and leave the semantic changing as an exercise for the user?

Apologies if this went through a design meeting I missed, I'm on my phone and have read all of the backstory.

CodeStyleOption2<ForEachExplicitCastInSourcePreference> option,
CodeStyleOption2<ForEachExplicitCastInSourcePreference> defaultValue)
{
Debug.Assert(s_forEachExplicitCastInSourcePreferencePreferenceMap.ContainsValue(option.Value));
Copy link
Member

Choose a reason for hiding this comment

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

What if the user provided an invalid value in editorconfig?

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 debug assert :) but it doesn't hit anyone at runtime. this helps us capture issues in internal debug builds that are more likely to be a code issue. but in release this doesn't happen since clearly it could happen with a user.

@CyrusNajmabadi
Copy link
Member Author

General question. New analyzers are merged into roslyn repo now? Is roslyn-analyzers still used for something?

It depends. If we feel this has a useful style/fix, then we are more ok to put here.

@JoeRobich
Copy link
Member

Design Notes 2022/03/21

Discussion

  • This is implemented as a CodeStyle analyzer, but feel more like CodeQuality. Is this something people would want failing their build?
    • This does feel like style and has a fixer that makes the implicit cast an explicit cast.
  • Don't like the non-legacy option value. Maybe something like strongly-typed or generic.
  • Potentially can drop the always option.

Proposal
Remove always option and rename the non-legacy option.

@CyrusNajmabadi
Copy link
Member Author

This is ready for review after the last design pass.

Comment on lines +117 to +119
void Main(object[] x)
{
[|foreach|] (string item in x)
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious about testing tuples

        void Main((object, object)[] x)
        {
            foreach ((string, string) item in x)
            {
            }
        }

Copy link
Member

Choose a reason for hiding this comment

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

This is interesting due to:

using System;
using System.Linq;

Program.M(new (object, object)[] { ("AB", "CD") });

public static class Program
{   
    public static void M((object, object)[] x)
    {
        foreach ((string, string) item in x
            //.Cast<(string, string)>() // Uncomment this. Crash!!
        )
        {
            Console.WriteLine(item.Item1);
            Console.WriteLine(item.Item2);
        }
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

tuple tests are at the bottom of teh file.

Copy link
Member

Choose a reason for hiding this comment

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

@CyrusNajmabadi I found them, but none seems to match the above case.

Copy link
Member Author

Choose a reason for hiding this comment

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

//.Cast<(string, string)>() // Uncomment this. Crash!!

Added test to show we do the right thing here (no crash) :)

@CyrusNajmabadi
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 4 pipeline(s).

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

Successfully merging this pull request may close these issues.

[New rule] Notify that foreach may throw InvalidCastException
8 participants