-
Notifications
You must be signed in to change notification settings - Fork 424
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
NPE on OptionSpec.getValue() #1767
Comments
@remkop . Rsenden and myself enountered another issue that we wanted to get your opinion on. We tried to implement a simple null check, as rsenden has already mentioned. But in doing so, this caused the unit test "testIssue1300BooleanInitialization" (for isue #1300 ) to fail. On investigation, the unit test is failing because the code in ArgSpec#initialValue() expects the get() method to throw an exception for uninitalized arggroups. I did make an attempt to also update ArgSpec#initialValue() (Link), but I have doubts that this update is appropiate. So because of this issue, do you have a recommendation on how to proceed? |
@remkop Any suggestions on how to fix this issue? Now, whenever I want to call Ideally, we should implement proper null checks in
However, we'd need to add those methods to the IGetter (and ISetter) interface (or introduce a new interface), which will affect various other classes that implement those interfaces. So, unless we're willing to do some refactoring, I think we're left with the following options:
The first option results in a cleaner interface, but could potentially break existing code which expects this method to throw an exception for non-initialized ArgGroups or other error situations. And although easy to implement, neither of these options are ideal; I think it's against best practices to throw and catch exception in non-exceptional situations (I would consider a non-initialized ArgGroup as a common, non-exceptional situation). What's your opinion on this? |
I can try giving a more elaborate answer tomorrow but what comes to mind now is to work with what’s available; maybe something like this: public void run() {
spec.options().stream()
.map(this::safeGetValue)
.forEach(this::printOptionValue);
}
Object safeGetValue(OptionSpec o) {
try {
return o.getValue();
} catch (NullPointerException npe) {
return null;
}
} |
Another way to avoid the NullPointerException might be to check if the OptionSpec has a group, and if it does, check if the group is null. |
@remkop The In general, I think people would expect this option to simply return null if an option hasn't been given on the command line and no default value is defined, without having to take into consideration that the ArgGroup in which the option has been defined may not have been initialized (just because none of the options in the ArgGroup have been specified on the command line). So, I think it would be better to either change the behavior of |
I am not a fan of the I am not considering modifying the IGetter/ISetter interfaces. One idea would be to add a method to ArgSpec (that returns a boolean) that can be used to detect if I don't oppose modifying Looking at the code, there is no obvious clean way to do this. Perhaps one idea would be to introduce a new interface interface IScoped {
IScope getScope();
} Then, FieldBinding and MethodBinding can implement the // TODO javadoc
public boolean isValueGettable() {
if (getter instanceof IScoped) {
IScoped scoped = (IScoped) getter;
IScope scope = scoped.getScope();
Object obj = scope.get();
return obj != null;
}
return true;
}
/** Returns the current value of this argument.
Returns {@code null} if the getter's scope is null.
Delegates to the current {@link #getter()}. */
public <T> T getValue() throws PicocliException {
if (!isValueGettable()) { return null; }
try {
return getter.<T>get();
} catch (PicocliException ex) { throw ex;
} catch (Exception ex) { throw new PicocliException("Could not get value for " + this + ": " + ex, ex);
}
} Thoughts? |
@remkop Sorry for the late reply, I was occupied with other things. I guess your suggestion should work, but it feels like this exposes too many implementation details from the Binding classes (i.e. the fact that a value is not gettable if the scoped object is null). Why not combine the two ideas? Create a new interface IAccessible: interface IAccessible {
boolean isAccessible();
} Have public isAccessible() {
return scope.get() != null;
} And finally have public <T> T getValue() throws PicocliException {
if ( getter instanceof IAccessible && !((IAccessible)getter).isAccessible() ) { return null; }
try {
return getter.<T>get();
} catch (PicocliException ex) { throw ex;
} catch (Exception ex) { throw new PicocliException("Could not get value for " + this + ": " + ex, ex);
}
} This way, the implementation for determining whether a value is accessible is nicely encapsulated in the Thoughts? |
Thanks for helping think this through! I see the benefits of the Another thing I noticed is that in your comment you mention encapsulating all logic inside the Thoughts? |
@remkop From an encapsulation perspective, I think the logic for determining whether the getter can be called belongs in classes that implement the I've actually already implemented the approach that I described, was just about to commit ;). Using slightly different naming though, as eventually you may want to to have separate As for the |
The semantics of the We introduced this method because we’re thinking of changing the behavior of the About the new interface naming and semantics, |
As for the semantics of the Similarly, deciding whether the value is gettable based on whether the scoped object is null or not, implicitly assumes that the This is all theory though, in practice the |
@remkop I had already implemented the |
Okay great thank you! 🙏 |
For options defined in an ArgGroup, a call to OptionSpec.getValue() will fail due to a NullPointerException if none of the options contained in the ArgGroup have been specified on the command line.
The following command class demonstrates this issue:
This command will fail with an exception on
OptionSpec::getValue
if neither--arg2
nor--arg3
have been specified on the command line.A simple null-check in the
FieldBinding#get()
method fixes this issue; we will be submitting a PR for this:Note that the
FieldBinding#set()
method likely exhibits the same problem, however this may be more difficult to fix; if an ArgGroup is null, then there's no object to set the option value on. As such, we won't be including any fix for theset()
method in the PR.It might make sense to always initialize ArgGroups with a non-null value, as that would avoid these kind of issues, and would also reduce the number of null checks in command implementations when trying to access options defined in an ArgGroup. Not sure though whether there are any command implementations that rely on the fact that an ArgGroup is null if none of the options in that ArgGroup have been specified on the command line, so this would potentially be a breaking change.
The text was updated successfully, but these errors were encountered: