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

IDE0001: False Positive for default primary constructor #75026

Closed
AliveDevil opened this issue Sep 9, 2024 · 12 comments · Fixed by #75546
Closed

IDE0001: False Positive for default primary constructor #75026

AliveDevil opened this issue Sep 9, 2024 · 12 comments · Fixed by #75546
Labels
Area-IDE Bug help wanted The issue is "up for grabs" - add a comment if you are interested in working on it
Milestone

Comments

@AliveDevil
Copy link

AliveDevil commented Sep 9, 2024

Version used: dotnet --info

.NET SDK:
 Version:           8.0.400
 Commit:            36fe6dda56
 Workload version:  8.0.400-manifests.56cd0383
 MSBuild version:   17.11.3+0c8610977

Laufzeitumgebung:
 OS Name:     Windows
 OS Version:  10.0.22631
 OS Platform: Windows
 RID:         win-x64
 Base Path:   C:\Program Files\dotnet\sdk\8.0.400\

Installierte .NET-Workloads:
Konfiguriert für die Verwendung loose manifests beim Installieren neuer Manifeste.
Es sind keine installierten Workloads zum Anzeigen vorhanden.

Host:
  Version:      8.0.8
  Architecture: x64
  Commit:       08338fcaa5

.NET SDKs installed:
  3.1.426 [C:\Program Files\dotnet\sdk]
  5.0.408 [C:\Program Files\dotnet\sdk]
  6.0.202 [C:\Program Files\dotnet\sdk]
  6.0.321 [C:\Program Files\dotnet\sdk]
  8.0.400 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 3.1.25 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.26 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.33 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 8.0.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 3.1.25 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.15 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.26 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.33 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.1.25 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.5 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.12 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.15 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.26 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.33 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 8.0.8 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

Steps to Reproduce:
Class, with default constructor:

// A
public abstract class Base<TOther>;
public sealed class Concrete() : Base<Concrete.Impl>() // IDE0001: Simplify Name
{
	public class Impl;
}

// B
public abstract class Base<TOther>();
public sealed class Concrete() : Base<Concrete.Impl>() // IDE0001: Simplify Name
{
	public class Impl;
}

Record class, with argument:

public abstract record class Base<TOther>(object A);
public sealed record class Concrete() : Base<Concrete.Impl>(default!) // IDE0001: Simplify Name
{
	public class Impl;
}

Diagnostic Id: IDE0001: Simplify Name

Expected Behavior: No false-positive, just like this does not produce a false-positive:

public abstract class Base<TOther>();

public sealed class Concrete() : Base<Concrete.Impl>
{
	public class Impl;
}

Actual Behavior: IDE0001 false-positive is emitted for : Base<Concrete.Impl>(), where it shouldn't.

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Sep 9, 2024
@CyrusNajmabadi CyrusNajmabadi added Bug help wanted The issue is "up for grabs" - add a comment if you are interested in working on it and removed untriaged Issues and PRs which have not yet been triaged by a lead labels Sep 9, 2024
@CyrusNajmabadi CyrusNajmabadi added this to the Backlog milestone Sep 9, 2024
@github-project-automation github-project-automation bot moved this to InQueue in Small Fixes Sep 9, 2024
@Orachor
Copy link

Orachor commented Sep 9, 2024

I'll try fixing this :)

@CyrusNajmabadi
Copy link
Member

Awesome. Thanks!

@Orachor
Copy link

Orachor commented Sep 14, 2024

This one turned out more complicated than I expected. The issue is with the following code:

public abstract class C<T>;

public sealed class D() : C<E>()
{
    public class E;
}

Where the generic parameter E inside the base list is getting incorrectly bound even though members of D are not accessible in this context.

From what I have seen this case should be taken into account (according to the comments here) however, in the end, the binder that is used is one that contains an InContainerBinder (the container being type D) which obviously binds the symbol.

I am still not 100% sure about some of the details as well as not knowing which part of the code needs to be changed (guessing it's the binder lookup).

@CyrusNajmabadi any chance you (or anyone else) might be able to provide some insight?

@CyrusNajmabadi
Copy link
Member

I don't have a suggestion for you. It sounds like a binding issue in teh compiler. You might want to talk to @dotnet/roslyn-compiler to see if htey ahve any ideas. If you make a small repro test that demonstrates the problem when calling into the compiler apis, they may be amenable to fixing it.

@333fred
Copy link
Member

333fred commented Oct 15, 2024

I'm not sure what you mean by "Where the generic parameter E inside the base list is getting incorrectly bound even though members of D are not accessible in this context." This isn't happening from what I can tell. As an example, this test passes:

        [Fact]
        public void QuickTest()
        {
            var source = """
                public abstract class C<T>;

                public sealed class D() : C<E>()
                {
                    public class E;
                }
                """;

            var comp = CreateCompilation(source);
            comp.VerifyDiagnostics(
                // (3,29): error CS0246: The type or namespace name 'E' could not be found (are you missing a using directive or an assembly reference?)
                // public sealed class D() : C<E>()
                Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "E").WithArguments("E").WithLocation(3, 29)
            );

            var tree = comp.SyntaxTrees[0];
            var model = comp.GetSemanticModel(tree);
            var classD = tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Skip(1).First();
            var cBaseList = classD.BaseList.Types[0].Type;
            var eArg = ((GenericNameSyntax)cBaseList).TypeArgumentList.Arguments[0];
            var e = model.GetSymbolInfo(eArg).Symbol;
            Assert.Null(e);
        }

Could you please clarify what you're seeing? If you're calling GetTypeInfo on that location, I indeed suspect that could give you E back, since there's no bound node information and it will likely do a fallback error binding.

@AlekseyTs
Copy link
Contributor

Is this problem really specific to primary constructors?

@AliveDevil
Copy link
Author

Yes:

namespace Roslyn75026;

public abstract class Base<TOther>;

public sealed class PrimaryToBasePrimary() : Base<PrimaryToBasePrimary.Impl>()
{
    public class Impl;
}

public sealed class PrimaryToBaseDefault() : Base<PrimaryToBaseDefault.Impl>
{
    public class Impl;
}

public sealed class DefaultToBaseDefault : Base<DefaultToBaseDefault.Impl>
{
    public DefaultToBaseDefault()
    {
    }

    public class Impl;
}

public sealed class DefaultToBase : Base<DefaultToBase.Impl>
{
    public DefaultToBase() : base()
    {
    }

    public class Impl;
}

public sealed class ImplicitToBaseDefault : Base<ImplicitToBaseDefault.Impl>
{
    public class Impl;
}

Only the PrimaryToBasePrimary creats the IDE0001 suggestion.

@AliveDevil
Copy link
Author

AliveDevil commented Oct 15, 2024

This btw doesn't affect default-primaries only, but all primaries, i.e.:

public abstract record class Base<TOther>(string A);
public sealed record class Concrete() : Base<Concrete.Impl>("A")
{
  public class Impl;
}

also has the IDE0001 annotation, but can only be worked around by discarding the primary constructor altogether.
Edit: Updated entry-post to reflect this case as well.

@AlekseyTs
Copy link
Contributor

It is not obvious why IDE is getting confused. If it gets an unexpected result from Compiler's API, we (Compiler team) need more details:

  • what API
  • under what conditions
  • etc.

@Orachor
Copy link

Orachor commented Oct 16, 2024

The issue can be reproduced with this test:

  [Fact]
  public void Test()
  {
      var source = """
              public abstract class C<T>;

              public sealed class D() : C<D.E>() // Starting with working code
              {
                  public class E;
              }
              """;

      var comp = CSharpTestBase.CreateCompilation(source);

      var tree = comp.SyntaxTrees[0];
      var model = comp.GetSemanticModel(tree);
      var classD = tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Skip(1).First();
      var cBaseList = classD.BaseList!.Types[0].Type;
      var deArg = ((GenericNameSyntax)cBaseList).TypeArgumentList.Arguments[0];

      var ceArg = SyntaxFactory.GenericName(SyntaxFactory.Identifier("C"), SyntaxFactory.TypeArgumentList([SyntaxFactory.IdentifierName("E")]));
      var simplified = SyntaxFactory.PrimaryConstructorBaseType(ceArg);

      model.TryGetSpeculativeSemanticModel(deArg.SpanStart, simplified, out var speculativeModel); // Replace C<D.E>() with C<E>()

      var eArg = ((GenericNameSyntax)simplified.Type).TypeArgumentList.Arguments[0];
      var e = speculativeModel!.GetSymbolInfo(eArg).Symbol;
      Assert.Null(e); // throws, e is D.E here
  }

I am not 100% sure but I'm assuming that the speculative semantic model is on the IDE side of things, so perhaps there are no issues with GetSymbolInfo and it's just a matter of incorrectly using the API (since the original example works correctly).

@AlekseyTs
Copy link
Contributor

The used overload of TryGetSpeculativeSemanticModel is documented as "Get a SemanticModel object that is associated with a constructor initializer ..."
What this meant to say - it is about the executable part of the node, i.e. the argument list. The type part is not even part of the binding process, any changes to the base type portion of the node are completely ignored. No matter what that part say, the initializer part is bound in context of the same constructor declared by the original syntax. For the purpose of that speculative semantic model, the base type doesn't change and a possibility of it changing isn't even considered as something to pay attention to.
When the symbol info is requested, we determine that the node is not bound as part of the initializer. It is bound in an error recovery mode, as a stand-alone node in context of the initializer, where the nested type is in scope. I guess, we could tighten behavior of the speculative semantic model so that it doesn't return any semantic information for nodes outside of the argument list. However, that probably won't help the goal that IDE is trying to achieve in this scenario. I think IDE should use a "regular" way of speculating on base types within base list. Something like:

model.GetSpeculativeSymbolInfo(deArg.SpanStart, SyntaxFactory.IdentifierName("E"), SpeculativeBindingOption.BindAsTypeOrNamespace)

Or whatever mechanism is used when argument list is not present.

@Orachor
Copy link

Orachor commented Oct 17, 2024

Thanks for the in depth explanation, now I understand 🙂

@github-project-automation github-project-automation bot moved this from InQueue to Completed in Small Fixes Oct 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE Bug help wanted The issue is "up for grabs" - add a comment if you are interested in working on it
Projects
Status: Completed
Development

Successfully merging a pull request may close this issue.

5 participants