Danger, Will Robinson!

Danger, Will Robinson!

Rate This
  • Comments 47

As long-time readers of this blog know, I am often asked why a particular hunk of bad-smelling code does not produce a compiler warning.

"Why not?" questions are inherently hard to answer because they turn causation on its head; normally we ask what caused a particular thing to happen, not what caused a particular thing to not happen. Therefore, rather than attack that question directly I like to rephrase the question into questions about the proposed feature. (A warning is of course a feature like any other.) By answering these questions we can see whether the compiler team is likely to spend its limited budget on the feature rather than prioritizing something else. Some questions I think about when considering "why did you not implement this warning?" are:

Did someone think of it?

If no one on the compiler team ever thinks of the possibility of a particular warning then obviously it won't happen. As an example of that, I was recently asked why:

using(null) statement;

does not produce a warning. That code is legal; it is equivalent to

IDisposable temp = null;
try
{
  statement;
}
finally
{
  if (temp != null) temp.Dispose();
}

Of course the code is completely pointless; all it does is introduce a try-finally block that adds nothing useful to the program. Why don't we give a warning for this useless code? Well, the primary reason is that (to my knowledge) no one on the compiler team ever thought that a customer would do such a thing, and therefore, never thought to design, implement, test and document a compiler feature to deal with the situation.

Now that we have thought of it, we can see if it meets any of our other criteria, like, is it plausible that a user would type this accidentally thinking that it does something sensible? But I'm getting ahead of myself.

Is the proposed warning even possible? Is it really expensive?

Sometimes people propose warnings that would require solving the Halting Problem (which is unsolvable in general) or solving an NP-hard problem (which is in practice unsolvable in a reasonable amount of time). Proposed warnings like "warn me if there is no possible way for this overload to be chosen" or "warn me if there is an input which guarantees that this recursive method has unbounded recursion" or "warn me if this argument can ever be null in this program" require a level of analysis that is either in principle impossible, or possible but better performed by tools custom made for that purpose, like the Code Contracts engine. Warning scenarios should be cheap for the compiler to disambiguate from non-warning scenarios.

Is the code being warned about both plausible and likely to be wrong? Is the potentially "wrong" code actually sensible in some scenario?

There are an infinite number of ways to write completely crazy code; the point of warnings is not to identify all possible crazy code, but rather to identify code that is plausible but likely to be wrong. We don't want to waste time and effort implementing features warning about ridiculous scenarios that in practice never arise.

Sometimes code seems to be both plausible and wrong, but is intentional for a reason that is not immediately obvious. The best example of this is our suppression of warnings about write-only locals; a local might be read by the developer during debugging even if it is never read in the program.

Machine-generated code is also often crazy-seeming but correct and intentional. It can be hard enough to write code generators that generate legal code; writing code generators that change their output when they determine that they're generating code that could produce warnings is burdensome. (For example, consider the machine-generated code that you get when you create a new project in Visual Studio. It had better compile without warnings!) However, we are much more concerned with catching errors in code written by humans than we are about making machines happy.

Obviously we only want to warn about code that has a high likelihood of actually being wrong. If we warn about correct code then we are encouraging users to change correct code, probably by changing it into incorrect code. A warning should be a bug-preventing feature, not a bug-encouraging feature. This leads me to my next point:

Is there a clear way to rewrite correct code that gets an unwanted warning into correct code that does not get a warning?

Warnings ideally ought to be easily turn-off-able. For example:

bool x = N();
...
if (x == null ) Q();

That gives a warning that == null on a non-nullable value type is always false. Clearly you can rewrite the code to eliminate the warning; if you intended it to always be false then get rid of the whole statement, or you can turn the variable into a nullable bool, or whatever.

A warning where there is no way to write the code so that the warning disappears is very irritating to people who actually do mean the seemingly-wrong thing. You can always turn a warning off with a #pragma of course, but that's a horribly ugly thing to force someone to do.

Will a new warning turn large amounts of existing code into errors?

Lots of people compile with "warnings as errors" turned on. Every time we add a warning that causes a lot of existing code to produce warnings, we potentially break a lot of people. Even if the warnings are good ones, breaking a lot of people is points against doing the feature. Such warnings are perhaps best handled in another tool, which brings me to:

Does the compiler team have to do this work? Or can another tool produce the warning?

Microsoft provides tools like FxCop, StyleCop and Code Contracts to do far more extensive analysis of code than the C# compiler performs. Similarly with third-party tools like ReSharper. If a warning can be implemented by one of those tools just as easily or more easily than it can be implemented in the compiler then that's less work for me, and that's awesome. Also, since FxCop examines compiled state, a warning in FxCop can catch problems in C# and VB, without having to change either compiler.

Will the user discover the error immediately upon testing anyway?

The earlier a bug is discovered, the cheaper it is to fix. The opportunities to find bugs are, in order of exponentially increasing cost of fixing the bug:

* When you type in the code
* When you compile the code
* When you test the code
* When your customer runs the code

Warnings about potential bugs that pop up while you are typing the code or compiling the code are bugs that never make it to the customer. However, a potential warning that points out a bug that would always be caught by testing is less compelling than a warning for a bug that could make it through testing to the customer.

For example, I was recently asked why the compiler does not provide a warning for this common typing error:

private decimal cost;
public decimal Cost { get { return this.Cost; } }

Whoops, that's a stack overflow exception (or, on machines with tail recursion optimizations, an infinite loop) waiting to happen right there. The compiler can in principle determine cheaply that property getter does nothing other than call itself, so why doesn't the compiler warn about it? We could do all the work of identifying code that has this pattern, but why bother warning about a bug that will be caught anyway? The instant you test this code you will immediately discover the problem and it will be clear how to fix it. This is unlike our earlier example of "if (x == null)", where the fact that there is unreachable code might go completely unnoticed during testing, particularly if the common scenario is for x to be false.

Summing up:

Since that is quite the gauntlet for a feature to run, we find that usually the right thing to do for a proposed warning is "don't add the warning". The most likely situation for lots of new warnings to be added is when the warnings are surrounding a new feature that has new ways of being misused; we added quite a few warnings in C# 4.0 for misuses of "dynamic", "no PIA", and "named and optional arguments". Warnings for new features have no existing code using them that would break.

That said, we can always improve; if you have ideas for warnings do keep them coming.

  • I'd like to assume that people test what they write, but I've learned the hard way that some developers will just write the code and deploy it without ever running it. (Deploy could mean to production or to whatever the next acceptance level is, the common theme being said developer doesn't actually run the code on his or her machine.) So I certainly do appreciate it when the compiler catches those things that *should* be caught in testing. However, I do of course realize that your time is finite and you can't possibly cover everything. Just keep fighting the good fight.

    Developers who are so unprofessional as to deploy code without running it also ignore compiler warnings. -- Eric

  • I can think of legitimate use cases for 'using(null)' - a conditional. e.g.

    using( ispathRemote ? new TemporaryPathPermission(path) : null)
    {
     // perform operation on path
    }

    Without 'null' being a legitimate argument, this kind of conditional logic becomes much more difficult to express.

    Sure, that's a perfectly reasonable expression to put in a using block. But just plain "using(null)" is pointless. -- Eric

  • The "stack overflow on a self-referential property" isn't always quite as easy to debug as you might like. I once spent a non-trivial amount of time trying to work out why my Windows Phone 7 test app was bombing out. The piece of code in question was in a section which was hard/impossible to unit test, and the actual app just aborted - the process was killed with no debugger interruption at all. There's just a chance that the property was being used in a type initializer, which obviously made it even nastier.

    I would suggest that this is a case where the consequences can be harder to cope with than a normal bug, precisely because StackOverflowException brings down the process. Maybe this would have been easier if it had been a desktop application, but there certainly *are* scenarios where "easily controlled" testing is tricky, and where debugging isn't ideal.

    Personally I think that for *this particular case*, it would be worth the compiler team's time to issue a warning. Don't bother trying to gold-plate it of course: only a property member (either get or set) which does nothing but directly call itself would fine.

    I hadn't thought of scenarios where you can write code but not debug it; I'll take your word for it that there are such scenarios. That is indeed points in favour of issuing a warning. -- Eric

  • "Developers who are so unprofessional as to deploy code without running it also ignore compiler warnings. -- Eric"

    Sadly, yes, that's an excellent point.

  • "Will the user discover the error immediately upon testing anyway?"

    I'd rather find out about the warning before I start testing.  It takes more work to fix a warning if it makes it into testing.  So, warnings that save me testing effort do have value, albeit far less value than warnings which prevent my users from experiencing issues.

  • I agree with Jon on this case. there are other scenarios where you can't debug your code. Perhaps you don't have Visual Studio in your current environment and you're trying to get a piece of code quickly to hack around a critical bug on a server before going back to your office to write a proper fix?

    Also, I'd guess the particular case of a method whose entire body is calling itself with the same parameters is easy to catch, although I obviously have no experience with your compile-time data structures.

    Your point about ReSharper is something I take issue with. Do you actually expect me to buy a piece of software from a different vendor to make full use of a development environment? If it can be part of Visual Studio (but not the compiler), it *should* be. I don't like ReSharper. I don't think it's a good piece of software. I don't want to use it. Please don't make me.

    That said, I can't think of a single warning other than self-referencing properties that the compiler should provide and it doesn't. The C# compiler is an excellent tool; I think its warnings are invaluable. I don't compile with warnings as errors, but I would try to never commit code with warnings.

  • >>>

    bool x = N();

    if (x = M()) Q();

    That gives a warning that you probably meant "==", not "=".

    <<<

    Since when?

    In fact, one of the things I thought was nice about the lack of implicit conversions for integer types to bool was that it was much less likely to have the classic "assignment in 'if'" bug. So the compiler keeps quiet on assignments in 'if' statements.

    Is there some scenario in which your statement is actually true?

    I was misremembering; serves me right for not running the code through the compiler. -- Eric

  • I know this might sound silly, but one such program that might use this is a compiler unit test. Let's say I want to create a new C# compiler, called (with reverence to Mono), "Poly". If I write a unit test to verify that my compiler doesn't produce crashy code for using(null), my unit test might be: try { using(null) { } } catch { Assert.Fail(); }

    That's just about the only use I can think of.

  • "I hadn't thought of scenarios where you can write code but not debug it; I'll take your word for it that there are such scenarios. That is indeed points in favour of issuing a warning."

    I think it's a good idea to keep such scenarios in mind, of course. But I also think your other points about cost of implementing and maintaining such features in the compiler are important. It seems to me that, just as some warnings are better handled in external tools rather than the compiler, some debugging features are better handled in making the _debugging_ tools better, rather than relying on a compiler warning.

    Fact is, having to write the code in a certain way just to make it easier to debug (e.g. the "write-only locals" post) or having the compiler warn about things that _should_ be trivial to debug (e.g. the recursive property getter) points to a failing in the debugging tools. If it's dirt-cheap for the compiler to catch an issue, fine…include that as a feature. But I'd expect in most cases, even seemingly easy-to-implement warnings will involve a significant overhead: spec, implementation, testing, maintenance, all of this adds up.

    If it's harder to debug code when it's written a particular way, then the real solution is for the debugging tools to be better. Give me a way to inspect the return value a method is about to return. And for scenarios like debugging mobile apps, let's put pressure on those environments to provide better debugging tools. It's not the C#'s compiler to waste time generally, just to address semi-niche scenarios such as this.

    There is also of course the question of performance. One of the things I love about C# is how fast compilation is. There are no doubt lots of reasons for this, but surely every warning that's added to the analysis is going to slow compilation down by some amount. Again, if the detection of the warned-about code costs basically nothing at run-time (i.e. it falls naturally out of analysis the compiler is already doing and requires no additional work), then maybe it's worth putting in. But how many warnings are really like that? Even the recursive property getter seems likely to involve at least one special-case check for a call to a specific method (i.e. the getter itself) that would not have had to be done otherwise.

    So, please…just keep doing the excellent job being done so far. I find that the C# compiler currently has a very good balance of useful warnings and performance. Do listen to us customers of course, but please also continue to resist the temptation to put something into the compiler just because one of us asked for it. :)

  • @pete.d: That gives a warning because x is a bool, so there is no implicit conversion anywhere.

  • Might I +1 the recursive property warning? I've been bitten by it often enough, although it was always discovered fast enough that it caused no problem. It just feels like something the compiler should detect.

  • Why on earth do you still let users "Continue" when an application throws an unhandled exception at runtime? By definition, and Unhandled exception means that the application is in an unknown state.

  • I'd be interested to know what percentage of proposed warnings don't make the cut solely due to the "warnings as errors" criterion. The translation of foreach into an enumerator leaves some nasty holes I'd love to catch at compile time, but I fear that warning was defeated by this test.

  • You forgot the cheapest, step 0:

    *When you think about the code

    Designing a language feature that causes developers to think along successful patterns (async/await comes to mind) is probably the best way to prevent bugs from getting to the customer (along with hiring disciplined developers).

  • Hi Eric, I got blind-sided very recently with the issue that the compiler does not check/warn for duplicate values in an enum definition, which can lead to totally break a program even though unit tests are working if you do not explicitly test for duplicates. This is especially bad if it is just a forgotten value assignment.

    See this simple example:

    class Program {

    static void Main(string[] args) {

    Console.WriteLine("A:"+Test.A);

    Console.WriteLine("B:" + Test.B);

    }

    enum Test { A,

    B=0 } }

    Unfortunately the email function on your blog does not work....

Page 1 of 4 (47 items) 1234