Skip to content

Generate a call of a base class constructor if the default constructor does not exist.#30264

Merged
andriipatsula merged 7 commits intodotnet:mainfrom
andriipatsula:task/apatsula/arcade_11254
Feb 6, 2023
Merged

Generate a call of a base class constructor if the default constructor does not exist.#30264
andriipatsula merged 7 commits intodotnet:mainfrom
andriipatsula:task/apatsula/arcade_11254

Conversation

@andriipatsula
Copy link
Member

Copy link
Contributor

@smasher164 smasher164 left a comment

Choose a reason for hiding this comment

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

LGTM

{
ConstructorInitializerSyntax constructorInitializer = SyntaxFactory.ConstructorInitializer(SyntaxKind.BaseConstructorInitializer);

IMethodSymbol baseConstructor = baseTypeConstructors.OrderBy(c => c.Parameters.Count()).First();
Copy link
Member

@ViktorHofer ViktorHofer Feb 2, 2023

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

MemberExtensions.IsObsoleteWithUsageTreatedAsCompilationError even returns false for [Obsolete(null, true)], just like Roslyn does for legacy compatibility. https://github.com/dotnet/roslyn/blob/15b43b33901c88f68ef43f8314b5a2457716780d/src/Compilers/CSharp/Portable/Symbols/ObsoleteAttributeHelpers.cs#L159-L165

Copy link
Member Author

Choose a reason for hiding this comment

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

@ViktorHofer

Scenario 1:
namespace Foo
{
    public class B
    {
        [System.Obsolete("Constructor is deprecated.", true)]
        public B(int p1) { }
    }

    public class C : B
    {
        /// CS7036: there is no argument given that corresponds to the required parameter p1
        public C() { }
    }
}

Scenario 2:

namespace Foo
{
    public class B
    {
        [System.Obsolete("Constructor is deprecated.", true)]
        public B(int p1) { }
    }

    public class C : B
    {
        /// CS0619: B.B(int) is obsolete.
        public C() : base(1) { }
    }
}

What is the best way to handle such scenario?

Copy link
Member

Choose a reason for hiding this comment

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

Unsure but can we just mimic the existing legacy GenAPI behavior? cc @ericstj

Copy link
Member

Choose a reason for hiding this comment

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

I'd start by saying that GenAPI doesn't need to handle a case that's impossible to represent in the product. It also shouldn't go out of its way to support something that's extremely unlikely (eg: we Obsolete(true) an API that is in active use by some up-stack component).

Worst case, if we somehow had a case of this scenario, then I would expect that we'd need to workaround it by having an internal constructor on the base type. Manual exclusion could also allow a user to exclude the upstack constructor if they needed to redefine it to call a non-default internal constructor. I would recommend that GenAPI omit the base call to an obsolete(true) constructor. This is what the old GenAPI did.

TL;DR

  1. Omit all calls to base obsolete(true) constructors.
  2. Developers can author an internal constructor on the base type manually (and a call it, if it's not default).

if (parameter.Type.IsValueType)
identifier = SyntaxFactory.IdentifierName("default");
else
identifier = SyntaxFactory.IdentifierName("default!");
Copy link
Member

Choose a reason for hiding this comment

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

This comment suggests that there should be a better way to figure out if a parameter type accepts null or not: https://github.com/dotnet/arcade/blob/608f3c7aed68dec1314d75601c6f143747e78cc6/src/Microsoft.Cci.Extensions/Writers/CSharp/CSDeclarationWriter.Methods.cs#L456-L463

cc @eerhardt

@dotnet dotnet deleted a comment from azure-pipelines bot Feb 2, 2023
@dotnet dotnet deleted a comment from azure-pipelines bot Feb 2, 2023
ConstructorDeclarationSyntax declaration = (ConstructorDeclarationSyntax)syntaxGenerator.Declaration(method);
return declaration.WithInitializer(GenerateBaseConstructorInitializer(baseType.Constructors));
IOrderedEnumerable<IMethodSymbol> baseTypeConstructors = baseType.Constructors
.Where(c => !c.GetAttributes().Any(a => a.IsObsoleteWithUsageTreatedAsCompilationError()))
Copy link
Member

Choose a reason for hiding this comment

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

same here: Use All instead of !Any for readability:

Suggested change
.Where(c => !c.GetAttributes().Any(a => a.IsObsoleteWithUsageTreatedAsCompilationError()))
.Where(c => c.GetAttributes().All(a => !a.IsObsoleteWithUsageTreatedAsCompilationError()))

Copy link
Member Author

Choose a reason for hiding this comment

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

My expectation is that Any returns false when the first element found as a result in best case you don't need to traverse the whole list. On the other hand All will always traverse the list.

Copy link
Member

Choose a reason for hiding this comment

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

On the other hand All will always traverse the list.

That would be a very inefficient implementation of All. It returns as soon as it finds an element that doesn't match: https://source.dot.net/#System.Linq/System/Linq/AnyAll.cs,62

All(a => !a.IsObsoleteWithUsageTreatedAsCompilationError()) is the just the more readable statement in comparison to !Any(a => a.IsObsoleteWithUsageTreatedAsCompilationError()).

Copy link
Member Author

Choose a reason for hiding this comment

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

updated

INamedTypeSymbol? baseType = method.ContainingType.BaseType;
// If the base type does not have default constructor.
if (baseType != null && !baseType.Constructors.IsEmpty && baseType.Constructors.Where(c => c.Parameters.IsEmpty).Count() == 0)
if (baseType != null && !baseType.Constructors.IsEmpty && baseType.Constructors.All(c => !c.Parameters.IsEmpty))
Copy link
Member

@ViktorHofer ViktorHofer Feb 2, 2023

Choose a reason for hiding this comment

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

We are missing a filter check here to only include constructors that are publicly accessible (i.e. filter out effectively private ones): https://github.com/dotnet/arcade/blob/608f3c7aed68dec1314d75601c6f143747e78cc6/src/Microsoft.Cci.Extensions/Writers/CSharp/CSDeclarationWriter.Methods.cs#L444-L445

Copy link
Member Author

Choose a reason for hiding this comment

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

members are filter out before we generate SyntaxNode out of ISymbol.

IEnumerable<ISymbol> members = namedType.GetMembers().Where(_symbolFilter.Include);

Copy link
Member

Choose a reason for hiding this comment

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

The filtering makes sure that we only traverse symbols that are included. Here we are looking at the base type constructors of the included constructor. Nothing guarantees at that point that the base type constructors are included.

You can test that with a type that has a base type with an internal ctor. This should result in a compilation error because the internal ctor of the base type isn't emitted (assuming that the IncludeVisibleOutsideOfAssembly setting is false).

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, updated.

Copy link
Member

@ViktorHofer ViktorHofer left a comment

Choose a reason for hiding this comment

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

The filter includes effectively sealed constructors, doesn't it?

includeEffectivelyPrivateSymbols: true,

We need to find a non effectively sealed ctor.

Also, please add a test with an effectively sealed ctor to guard this path. Effectively sealed means i.e. when the ctor is protected (= publicly exposed) but the type is sealed (so nobody can derive from it).

Copy link
Member

@ViktorHofer ViktorHofer left a comment

Choose a reason for hiding this comment

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

Chatted offline. Effectively sealed ctors don't matter in this case as other types can't inherit from such types anyway.

@andriipatsula andriipatsula merged commit c5fca2d into dotnet:main Feb 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate a call of a base class constructor if the default constructor does not exist.

5 participants