Generate a call of a base class constructor if the default constructor does not exist.#30264
Conversation
…r does not exist.
| { | ||
| ConstructorInitializerSyntax constructorInitializer = SyntaxFactory.ConstructorInitializer(SyntaxKind.BaseConstructorInitializer); | ||
|
|
||
| IMethodSymbol baseConstructor = baseTypeConstructors.OrderBy(c => c.Parameters.Count()).First(); |
There was a problem hiding this comment.
The legacy GenAPI handled this case as well: https://github.com/dotnet/arcade/blob/608f3c7aed68dec1314d75601c6f143747e78cc6/src/Microsoft.Cci.Extensions/Writers/CSharp/CSDeclarationWriter.Methods.cs#L444-L445
I think we should port that over here as well (consider ObsoleteAttributes that result in compiler errors).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Unsure but can we just mimic the existing legacy GenAPI behavior? cc @ericstj
There was a problem hiding this comment.
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
- Omit all calls to base obsolete(true) constructors.
- 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!"); |
There was a problem hiding this comment.
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
src/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs
Outdated
Show resolved
Hide resolved
src/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs
Outdated
Show resolved
Hide resolved
src/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs
Outdated
Show resolved
Hide resolved
| 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())) |
There was a problem hiding this comment.
same here: Use All instead of !Any for readability:
| .Where(c => !c.GetAttributes().Any(a => a.IsObsoleteWithUsageTreatedAsCompilationError())) | |
| .Where(c => c.GetAttributes().All(a => !a.IsObsoleteWithUsageTreatedAsCompilationError())) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()).
| 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)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
members are filter out before we generate SyntaxNode out of ISymbol.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
The filter includes effectively sealed constructors, doesn't it?
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).
ViktorHofer
left a comment
There was a problem hiding this comment.
Chatted offline. Effectively sealed ctors don't matter in this case as other types can't inherit from such types anyway.
Fixes: dotnet/arcade#11254