"Have you ever been in the situation". Yes. Basically, this happens whenever you look at a function and aren't sure what it's supposed to do.
"What are your best heuristics". Write tests. Document internal interfaces. Use good names. Comment code (some consider the last of these undesirable or a last resort. I disagree, but if they get by without it then all power to them). On encountering code written by someone who hasn't done those things, find them, torture a confession out of them as to what the code is supposed to do, and get the above things sorted out.
There's a good maxim that you should write code as if the maintenance programmer is a psychopath who knows where you live. Game theory predicts that a threat doesn't work unless there's a non-zero chance of it being carried out. So someone has to be that psychopath. I humbly volunteer.
As an absolute last resort, when you can't tell the intent of the code and you don't have enough tests to prove whether a change is harmful or not, the question comes down to "if I change this line of code, will the program work better or worse?". I'm not really aware of any good heuristics for answering that question, other than:
- look at some or all of the call sites and see what they seem to be expecting
- consider how the user experience is influenced by the code in question and whether that's good or bad in terms of the general approach the app takes
- just decide what you think it ought to do, declare it a feature if that's what it currently does and a bug otherwise, and add new requirements and tests to that component accordingly.
- [Edit: I think I've come up with one which is perhaps non-obvious and sometimes useful: look at what any parent or child classes do, or in general other implementations of the same interface, and see whether what this implementation does is consistent with them.]
If you decide it's a bug, you change the code and take a gamble whether you've saved the day, or broken someone else's calling code somewhere. But if the application currently works, then the presumption should be to leave behaviour it as it is, even if you want to improve the specific code. So you may end up simplifying the code in the common case, at the cost of having to add special cases that you aren't sure were ever intended. A null parameter to the current code for convoluted reasons causes IOException rather than NullPointer? OK, keep that behaviour unless you can prove that nobody relies on it. If a better way to write the function is to call some method on that parameter before starting the IO, then you'll need an explicit check-null-and-throw-IOException. Call it backward compatibility.