-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Analyzer Proposal: Seal internal/private types #49944
Comments
Can the JIT not perform a similar thought process? |
It's the JIT that's doing these aforementioned optimizations based on the type being sealed, but the JIT today can't assume that non-sealed types won't ever have derived types, e.g. if a derived type were to be reflection emitted. |
Full AOT that can do global analysis of all code that will ever run in the process can do this optimization. JIT can do this optimization speculatively by assuming that the type is sealed, and undoing/rejiting all code that depends on this assumption when it is proven false. We have stayed away from speculative optimizitions like this so far. We have actually tried to build a similar speculative optimization for dictionaries of shared generic types, but we ended up rolling it back because it was hard to stabilize and because it had questionable performance characteristics. |
Sealing a type might cause warning CS0628 or CA1047 if the type has protected members. If the type is being sealed to help the JIT compiler, not for design reasons, then the developer might want to keep protected members protected so that the type can be easily unsealed again if a derived class is added. If there is going to be a code fix that seals types, then perhaps it could also suppress those warnings. |
It's quite common for a lib project to have InternalsVisibleTo for its corresponding test project. |
Various optimizations are disabled by InternalsVisibleTo, e.g. the linker can't remove unused internals when building in a library marked InternalsVisibleTo. I'm fine with that being a limitation for this analyzer, as it is for other analyzers, like CA1812 (avoid uninstantiated internal classes). |
Yes, that's why I wrote "We could also optionally factor in whether the type declares any new virtual methods, a protected ctor, or anything else that suggests the type is intended for derivation... upon detecting such things, we could either choose not to warn, or we could warn with a different diagnostic id." |
Yes, though my understanding from previous discussions with @AndyAyersMS on the topic is we have no intention of doing that sort of work any time soon, even though his OSR work starts to open the door to that. |
Might be able to expand the analyzer scope even further:
That last point allowed us to seal some public System.Globalization types in a previous release. See #31761. |
Have we ever considered a flag on I always use IVT for testing and never for sharing runtime code between assemblies. |
What would the analyzer do differently if that was set? Ignore the attribute and then it's up to you to either change your tests or suppress the warning if the tests were dependent on the type being unsealed? |
From personal experience actually having implemented an analyzer to warn if classes with no new virtual members and no inheritors aren't sealed and to offer a fix, Roslyn has a design issue that means that you can't offer fixes for any diagnostics reported by your analyzer during the compilation end action. And the compilation end action is the most (the only?) reasonable place to report everything, since you need to wait until all symbols have been analyzed in order to know whether any classes inherit from the ones you might report the diagnostic for. Reported issue: dotnet/roslyn#51653 |
I've been sealing stuff across dotnet/runtime. Another case has come up that we might want to factor in to whether the analyzer raises a notification (or maybe a separate diagnostic ID for these less clear cases), or potentially it would just impact a fixer. Today, you can write code like this: using System;
class C { }
class Consumer
{
public static IDisposable Bar(C obj) => obj as IDisposable;
} and that will compile fine. However, if you seal C: using System;
sealed class C { }
class Consumer
{
public static IDisposable Bar(C obj) => obj as IDisposable;
} that cast will now fail to compile:
because the compiler can now see that C definitely doesn't implement the interface (whereas before a type derived from C may have). This can also happen even without derived types in the picture, where casts are used to go from a concrete generic instantiation to an open generic, e.g. this compiles fine: interface IMyInterface<T> {}
class C<T> : IMyInterface<T> {}
class Int32C : C<int>
{
public static Int32C Instance { get; } = new Int32C();
}
class Consumer
{
static IMyInterface<T> Bar<T>()
{
if (typeof(T) == typeof(int))
{
return (IMyInterface<T>)Int32C.Instance;
}
return null;
}
} Int32C implements interface IMyInterface<T> {}
class C<T> : IMyInterface<T> {}
sealed class Int32C : C<int>
{
public static Int32C Instance { get; } = new Int32C();
}
class Consumer
{
static IMyInterface<T> Bar<T>()
{
if (typeof(T) == typeof(int))
{
return (IMyInterface<T>)Int32C.Instance;
}
return null;
}
} now you get:
Both of these cases can be solved by inserting an |
@stephentoub I made a small modification to your sample code and wanted to get your thoughts. using System;
public class C {
public static IDisposable M(MyClass c) {
return c as IDisposable; // CS0039
}
}
public sealed class MyClass : System.Collections.Generic.List<int>
{
} This feels wrong to me. The C# compiler in theory has no way of knowing whether that cast will succeed or fail at runtime, since it has no way of knowing the shape of the actual runtime superclass type. It only knows about what's exposed via the reference assembly. Obviously we're not going to add IDisposable to |
@GrabYourPitchforks, to make sure I understand, you're questioning the C# compiler's warnings/errors in these cases and suggesting it shouldn't validate interface casts at all? |
Correct. Or at most, a warning ("are you sure you intended to write this? it probably won't work at runtime."), not an error ("it definitely won't work at runtime."). That would hopefully reduce any complexity in your proposed analyzer and fixer. |
@stephentoub Does it make sense to extend this to |
If it doesn't have any public, protected, or protected internal constructors, then we could also factor it in. |
@stephentoub I'm sorry I've not catch is it https://rules.sonarsource.com/csharp/RSPEC-3260 about the same issue? Did I miss something important? |
@askazakov, that's in an unrelated static analysis library rather than the analyzers that ship as part of the .NET SDK, and at least from the description it's only about private rather than non-public, e.g. it doesn't seem to cover internal types (at least not from the short description you linked to). |
I think the analyzer should exclude classes declared as using System.Runtime.InteropServices;
_ = (IFileSaveDialog)new FileSaveDialogRCW(); // If FileSaveDialogRCW is sealed: error CS0030: Cannot convert type 'FileSaveDialogRCW' to 'IFileSaveDialog'
[ComImport]
[Guid("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[CoClass(typeof(FileSaveDialogRCW))]
internal interface IFileSaveDialog
{
}
[ComImport]
[ClassInterface(ClassInterfaceType.None)]
[Guid("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B")]
class FileSaveDialogRCW // 👈 sealed is not valid here because of `(IFileSaveDialog)new NativeFileSaveDialog()`
{
} |
@meziantou -- it looks like this is currently under PR here dotnet/roslyn-analyzers#5594 Would you mind adding your feedback there? |
There are numerous performance benefits to sealing types:
results in:
is
/as
type checks for the type can be done more efficiently, as it only needs to compare the type itself rather than account for a potential hierarchy.results in:
results in:
results in:
We should add an analyzer, at either hidden or info level but that we’d look to turn on as a warning in dotnet/runtime, that flags:
and flag that they should be sealed. A fixer would seal the type. We could also make it configurable on the visibility, in case someone wanted to e.g. also opt-in public types (we wouldn't/couldn't in dotnet/runtime). We could also optionally factor in whether the type declares any new virtual methods, a protected ctor, or anything else that suggests the type is intended for derivation... upon detecting such things, we could either choose not to warn, or we could warn with a different diagnostic id.
If a developer working in the library ever wants to derive from such a type, they can remove the sealed when they add the derivation.
The text was updated successfully, but these errors were encountered: