-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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 rule CA2255 is conceptually broken and needs to be removed. #62472
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
Tagging subscribers to this area: @dotnet/area-system-runtime-compilerservices Issue DetailsI opened this in the analyzers repo, only to have it promptly closed with a request to open it here. Diagnostic analyzer CA2255 escribes the Apparently this analyzer was created as a result of the discussion at #43268, which somehow fails to even consider the possibility that plugin systems exist and might want to use this. And... well... start from a bad premise and it's easy to reach the wrong conclusion. Upon migrating from .NET 5 to .NET 6, every single plugin project in my Solution is being harassed by this new analyzer. It needs to stop existing, as it is based upon a fundamental misunderstanding of the way
|
Note. Discussed this with Mason in gitter. This sounds be pretty trivial to disable with a single line in a root editorconfig. |
The point is not to hide the problem, but rather to fix it. |
tagging @stephentoub @jeffhandley . I'm not sure there's actually a problem here. The analyzer seems to work towards its intended purpose. And users who understand what the analyzer is saying but still would like to proceed have several avenues available to them to suppress it. As we talked about offline dirs.props and editorconfig are trivial ways for you to inform the system that you are ok with the issues around module-initializers and do want to use them anyways. |
That's the entire point: its intended purpose is wrong. It is based on an incorrect premise. "As designed" isn't particularly meaningful when the design itself is faulty. Frankly, this whole thing is a bit mystifying, given that I was active in the discussion surrounding the initial design of this feature and I mentioned plugin systems as a primary use case multiple times. So how did it get overlooked entirely in the discussion that led to the creation of this analyzer? |
I don't see what's incorrect. Reading the motivating thread, it seems sensible to me.
I don't think it was overlooked. I think this is exactly how most analyzers work. They detect potential problems, but are suppressible for the use cases where users can explicitly make a decision that for their domain it's not a problem. |
It was never mentioned once. What do you call that? |
I personally couldn't use Here's a sample: https://github.com/ericstj/sample-code/tree/ModuleInitializer/ModuleInitializer |
Yes, it has always been the behavior of module initializers. It matches the behavior of regular static constructors. Loading a type e.g. using As the warning says, module initializers are only for advanced scenarios. |
Yes, this is why, when the system was being set up, I advocated for a runtime change that would run module initializers eagerly rather than lazily. Unfortunately we didn't get that, but to be honest it's fairly easy to work around by incorporating a call to |
I guess in my case, if I would have to have the discoverer run |
It sounds like the analyzer is doing the right thing here then. It does seem like the docs should likely mention these cases though then so that users can know how to do things like "incorporating a call to RunModuleConstructor" as @masonwheeler mentioned. Given the advanced nature as @jkotas mentioned, keeping things working as they are today (while improving docs around this rule) seems the most appropriate path forward. |
@masonwheeler Can you clarify what the thumbs-down is for? It feels like the analyzer was appropriate here, calling out a concern here. As you yourself have mentioned, even your code needs to do additional stuff. So it feels like we should update our docs to call this out, and then allow you (and others) who have encountered this to take those steps, and then suppress the analyzer from that point using the simple approaches available. |
@ericstj What is this "discoverer" you're talking about? The stuff you say makes it sound like the plugin system is somehow groping about blindly through some swathe of the file system and checking every DLL it comes across, rather than the usual approach of having plugins set up in a well-defined-by-convention place that exists just for them. |
This comment has been minimized.
This comment has been minimized.
I understand that you disagree with the design, but I don't appreciate switching to bad faith argumentation. @CyrusNajmabadi is hardly trying to harass you when he keeps pointing out that the behavior of the analyzer is by-design. The anecdote from @ericstj provides evidence that our assumption holds: many people don't fully understand how module initializers work, because the semantics of the runtime isn't necessarily intuitive. As @jkotas pointed out static constructors have similar non-intuitive behaviors and over the years this has been a constant source of bug reports when subtle changes in user code cause them to no longer be called. We wanted to avoid people building complex architectures on top of module initializers because we know how fragile this can be. The point of the analyzer is to inform users that their mental model of how this feature works is very likely incorrect (or at least incomplete). The fact that the semantics of module initializers work for you isn't sufficient evidence that the analyzer is bogus; it just means that there are legitimate use cases for them, which we obviously don't dispute since we recently added support for them. I agree that asking people to suppress the analyzer when they are using them correctly is causing overhead but the cost of building a fragile architecture on top of them is much greater, so that trade off seems worth it to me. |
@terrajobst I can understand why it might appear to be "bad faith argumentation" in isolation. Suffice it to say, this is not an isolated incident, but a part of a much longer pattern of behavior stretching back years. He was well aware that he is not welcome in this discussion before he ever made the first comment, and yet he did it anyway. And continuing to insist that the behavior is by design, when I have already explained quite clearly that the design itself is flawed, is exactly the sort of passive-aggressive abuse that makes him unwelcome. With regards to your technical points:
I don't agree; it appears that he understands quite well how they work, and the problem is the unintuitive semantics actively getting in the way. As I noted above, this was something I pushed for fixing, by making module initializers run eagerly rather than lazily, and I still believe that would be an improvement that would cause significant gains with minimal, if any, tradeoff burden.
Again, I'd be willing to bet that 99% of those unintuitive, fragile behaviors can be laid directly at the feet of lazy initialization. (While I won't advocate changing static constructors to eager initialization -- there's a lot of existing code out there that depends on the current semantics -- in hindsight it was clearly a decision of questionable value!) But even without fixing module initialization semantics, I don't believe those same problems apply to module initializers for a few reasons.
|
@CyrusNajmabadi is welcome in all discussions in this repo. |
I agree with this. I'm still scratching my head over why module initializers are safer or more appropriate in app projects than in library projects. I'd rather https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2255 talked about the specific gotchas such as Assembly.GetTypes()/reflection not running module initializers or static constructors and suggesting RunModuleConstructor or alternative approaches like Eric would suggest. The part that I do think is excellent is this:
And even there, why can't trimmers foreseeably become smart enough to handle module initializers safely, adding additional rooting or whatever hurdles the design requires? Can this paragraph talk about the current state of trimming rather than making a for-all-time statement about it? |
There is no way for trimmers to know what depends on the module initializer side-effects. Trimmers have to unconditionally root the module initializers, there is no other reasonable alternative. |
Because the initializer is a root. Because for all the trimmer knows, this could be the entry point into a plugin and the entire point of the assembly is to provide the types that it's referring to. Honestly, I find the "it causes problems for trimming/linking" argument a little bit silly, particularly with the new emphasis .NET Core places on large numbers of small, highly specialized assemblies. If a module initializer is going to pull in code not relevant to the rest of the assembly, that kind of smells like an indication that this assembly could benefit from being broken up. |
I also don't see the problem with saying that the module initializer roots things. Good! The trimmer should tell you if you're rooting things unnecessarily and there is a better option (like static constructor). |
@jnm2 That's an interesting idea. Can you think of an algorithmic way to define "necessary/unnecessary" in this context? |
I guess not, now that I'm thinking about it, because state timing is important. I'm back to thinking that rooting things using module initializers is a feature not a bug, and it's not (or shouldn't be) that special compared to other things when it comes to diagnosing why linking isn't working the way you want. Why should it be handled differently than a library that contains a method that calls GetMethod on a dynamic combination of type and name, or any other thing a library can do to cause unwanted rooting? |
That's not how this works. You don't get to decide who is welcome to comment on issues in repos you don't own, the owners of that repo do. @CyrusNajmabadi is a key maintainer for Roslyn, a member of the C# design team, and thus has a lot of expertise with everything involving compilation and static code analysis. Not only are we dependent on getting his input, but we also actively seek it out. Please change your attitude towards people holding contrarian viewpoints or we're forced to block you. I disagree with your design proposal, but I also believe your technical feedback has merit and is worthwhile discussing. However, I'm not interested in talking to people who are acting hostile to members of our team. |
@terrajobst Are you on Gitter? That might be a better place to discuss this. Suffice it to say, I have no problem whatsoever with "people holding contrarian viewpoints." As you can see from this thread, I'm perfectly capable of discussing things reasonably with people who disagree with me. There's significantly more to this than that. |
Having been a witness for years to the same conversations between Cyrus and Mason where Mason claims trolling and abusive behavior, and talking extensively with both Cyrus and Mason on this topic in private, I don't think Mason is seeing clearly. In my opinion there's a hangup and Mason is projecting onto Cyrus things he's run into elsewhere. |
Mason, I have to stick to my morals knowing you won't like me sharing what I see. It doesn't bother me. |
This is also a topic of interest for me. And i find it very weird to have a position put forth that i should not engage with it. I was the original person discussing this topic with you @masonwheeler , on both roslyn-analzyers, and on gitter (where i shared useful information on how to disable these, as well as letting you know how you could stack editorconfig files to have both project and solution level suppressions take effect). However, that we've been discussing this together from the start is not even relevant here. Our beliefs and CoC are strongly oriented around the idea that constructive and respectful discussion are part and parcel of how our repos work. I have shared my views and opinions on the topic and have respectfully been receptive to yours. All we ask is that you do the same with everyone who participates here as well. Thank you. |
In my opinion, module initializers are a poor answer to the plugin initialization problem, so I don't think it's worthwhile to attempt to force them to fit a problem they're not really intended for. With the advent of generic attributes and public interface IPluginInitializer
{
abstract static void Initialize();
}
public abstract class RegisterPluginAttribute : Attribute
{
public Action InitializeMethod { get; }
private protected RegisterPluginAttribute(Action initializeMethod)
{
InitializeMethod = initializeMethod;
}
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class RegisterPluginAttribute<T> : RegisterPluginAttribute
where T : IPluginInitializer
{
public RegisterPluginAttribute()
: base(T.Initialize)
{
}
} Which is consumed as so: [assembly: RegisterPlugin<SomePluginClass>] sealed class SomePluginClass : IPluginInitializer
{
static void IPluginInitializer.Initialize()
{
// ...
}
} Your application need only call In addition, you have full control over the initializer method's signature when you take this approach. This has significant value because plugin initializers are typically going to require some kind of state (an interface exposing APIs, for example) to be passed in so that they can appropriately interact with the host program. |
@DaZombieKiller I'm afraid I don't follow. You're saying I should replace a registration system that's simple, straightforward, and statically verifiable with one that depends on reflection because it will work better? 😕 |
The above approach is simple, straightforward and statically verifiable as well. The only use of reflection is in |
The module initializer is how I avoid it. I picked that route specifically because I did not want to use reflection. |
Module constructor is similar to a DllMain - it's a special method executed under a lock. It's better not to have one. People could do plugins in the native world by just having the plugin register itself on load from DllMain, but I've not seen anyone do that - there's usually a well known entrypoint that the plugin host locates by name (one could say that's reflection too) and calls that, mostly so that the plugin can do anything it needs without having to worry about being under a special lock. |
There's also this interesting nugget of information in RunModuleConstructor comments. We might want to lift that into the docs if the comment is still true (I can see the reasons why it could be true): Lines 64 to 70 in b83fee9
|
That seems more like wordplay than anything. It doesn't have the problems that reflection has, namely, 1) it runs extremely slowly and 2) a developer can change things that make the code being reflected on invalid without giving you compile-time errors to notify you that you have to fix the reflection-based code.
That's interesting! I didn't know that. Now I'm curious: Why is this lock needed in order to run module initializers, and what restrictions does it impose that you need to know not to violate?
That's pretty much exactly how it's done in Delphi, except it's not DllMain; it's |
Neither of these issues apply to the assembly-level attribute approach when used as shown above, so if that's your reason for avoiding reflection you shouldn't run into problems with that. Using |
Module initializer must only run once. Nothing in the module is accessible from outside before module initializer runs. It's similar restrictions to class constructors, except one needs to be able to reason about everything in the module, not just a single type.
|
Thanks for sharing those ideas, @DaZombieKiller. I had not considered static abstracts in interfaces and generic attributes as a good pairing for this, but that looks very approachable. 💯 I appreciate the discussion here, and it seems a good understanding is in place now for why we're discouraging the module initializer approach for plugins. I'm closing the issue. |
I opened this in the analyzers repo, only to have it promptly closed with a request to open it here.
Diagnostic analyzer CA2255 escribes the
ModuleInitializer
attribute as being only for application initialization and "advanced code generator scenarios." But the most obvious use case for this feature is nothing of the sort: this was practically tailor-made for plugin registration.Apparently this analyzer was created as a result of the discussion at #43268, which somehow fails to even consider the possibility that plugin systems exist and might want to use this. And... well... start from a bad premise and it's easy to reach the wrong conclusion.
Upon migrating from .NET 5 to .NET 6, every single plugin project in my Solution is being harassed by this new analyzer. It needs to stop existing, as it is based upon a fundamental misunderstanding of the way
ModuleInitializer
is being used in real-world code.The text was updated successfully, but these errors were encountered: