Unchecked Optional Access
ID |
java.no_access_empty_optional |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Java |
Tags |
reliability |
Description
Reports Optional.get() where nothing in reach ever checks the value is there.
A method counts as guarded, and nothing in it is reported, when its body calls isPresent() or
isEmpty() anywhere — on any Optional — or when it makes an unqualified call to a method
declared in the same compilation unit whose own body does. Developers routinely factor the check
into a one-line sibling predicate, and the check is not forgotten there, just named:
if (shouldCleanProjectHomepage(projectOptional, branchOptional)) { // not reported: the
cleanUserHomepageInDb(dbSession, user); // predicate below checks
return empty(); // both Optionals
}
homepage.setComponent(projectOptional.get().getKey());
private boolean shouldCleanProjectHomepage(Optional<ProjectDto> p, Optional<BranchDto> b) {
return !p.isPresent() || !b.isPresent() || !userSession.hasEntityPermission(USER, p.get());
}
Rationale
get() throws a bare NoSuchElementException when the Optional is empty, naming neither the value that was missing nor why it was expected. Calling it without the method ever checking presence is usually a forgotten guard rather than a deliberate choice:
Optional<User> user = repository.findById(id);
return user.get().getName(); // throws NoSuchElementException if the id does not exist
Exceptions
orElseThrow() is not reported, although it also throws on an empty Optional. It is the idiom for
"an absent value is a contract violation here", which is the opposite of a forgotten check — and in
practice it usually carries a supplier naming a domain exception, so the failure explains itself:
User user = repository.findById(id).orElseThrow(() -> new UserNotFoundException(id)); // not reported
Remediation
Check presence first, or use one of the accessors that does not require it:
Optional<User> user = repository.findById(id);
if (user.isPresent()) {
return user.get().getName();
}
return "unknown";
What is not reported
Beyond a guarded method, a single access is accepted when the Optional is handed to a throwing
validator first — a check/require/ensure/validate-prefixed call taking that same
Optional as an argument, as in checkFoundWithOptional(group, "…") before group.get() — or when the access sits lexically inside a map, flatMap, filter or ifPresent lambda taken
on that same Optional, since such a lambda runs only when the value is present. orElseGet and
or are pointedly not in that list: their lambda runs only when the value is absent, so a
get() in there is the defect, not an exception to it.
The analysis is coarse on purpose. A guard is never tied to the access it covers — a method
checking a different Optional, or checking on a path that does not reach the access, is still
accepted. Only method bodies are scanned, not constructors or initializer blocks. A var-declared
local whose type cannot be inferred is not recognized as Optional and is silently not reported.
Three guard shapes are still missed and produce known false positives, because closing them needs
real flow analysis: a check on the collection rather than the Optional
(if (m.size() == 1) { … m.keySet().stream().findFirst().get(); }), a non-Optional presence
predicate on the same key (if (!entry.hasField(f)) return; then entry.getField(f).get()), and
a precondition established in a different method.