Skip to content

Guards: Improve support for wrapped guards #20121

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions java/ql/lib/change-notes/2025-07-28-guardwrappers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Guard implication logic involving wrapper methods has been improved. In particular, this means fewer false positives for `java/dereferenced-value-may-be-null`.
150 changes: 73 additions & 77 deletions java/ql/lib/semmle/code/java/controlflow/Guards.qll
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ private module GuardsInput implements SharedGuards::InputSig<Location> {

class ControlFlowNode = J::ControlFlowNode;

class NormalExitNode = ControlFlow::NormalExitNode;

class BasicBlock = J::BasicBlock;

predicate dominatingEdge(BasicBlock bb1, BasicBlock bb2) { J::dominatingEdge(bb1, bb2) }
Expand Down Expand Up @@ -322,6 +324,55 @@ private module GuardsInput implements SharedGuards::InputSig<Location> {

Expr getElse() { result = super.getFalseExpr() }
}

class Parameter = J::Parameter;

private int parameterPosition() { result in [-1, any(Parameter p).getPosition()] }

/** A parameter position represented by an integer. */
class ParameterPosition extends int {
ParameterPosition() { this = parameterPosition() }
}

/** An argument position represented by an integer. */
class ArgumentPosition extends int {
ArgumentPosition() { this = parameterPosition() }
}

/** Holds if arguments at position `apos` match parameters at position `ppos`. */
overlay[caller?]
pragma[inline]
predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) { ppos = apos }

final private class FinalMethod = Method;

class NonOverridableMethod extends FinalMethod {
NonOverridableMethod() { not super.isOverridable() }

Parameter getParameter(ParameterPosition ppos) {
super.getParameter(ppos) = result and
not result.isVarargs()
}

GuardsInput::Expr getAReturnExpr() {
exists(ReturnStmt ret |
this = ret.getEnclosingCallable() and
ret.getResult() = result
)
}
}

private predicate nonOverridableMethodCall(MethodCall call, NonOverridableMethod m) {
call.getMethod().getSourceDeclaration() = m
}

class NonOverridableMethodCall extends GuardsInput::Expr instanceof MethodCall {
NonOverridableMethodCall() { nonOverridableMethodCall(this, _) }

NonOverridableMethod getMethod() { nonOverridableMethodCall(this, result) }

GuardsInput::Expr getArgument(ArgumentPosition apos) { result = super.getArgument(apos) }
}
}

private module GuardsImpl = SharedGuards::Make<Location, GuardsInput>;
Expand All @@ -340,6 +391,17 @@ private module LogicInputCommon {
NullGuards::nullCheckMethod(call.getMethod(), val.asBooleanValue(), isNull)
)
}

predicate additionalImpliesStep(
GuardsImpl::PreGuard g1, GuardValue v1, GuardsImpl::PreGuard g2, GuardValue v2
) {
exists(MethodCall check, int argIndex |
g1 = check and
v1.getDualValue().isThrowsException() and
conditionCheckArgument(check, argIndex, v2.asBooleanValue()) and
g2 = check.getArgument(argIndex)
)
}
}

private module LogicInput_v1 implements GuardsImpl::LogicInputSig {
Expand All @@ -364,18 +426,13 @@ private module LogicInput_v1 implements GuardsImpl::LogicInputSig {
}
}

predicate parameterDefinition(Parameter p, SsaDefinition def) {
def.(BaseSsaImplicitInit).isParameterDefinition(p)
}

predicate additionalNullCheck = LogicInputCommon::additionalNullCheck/4;

predicate additionalImpliesStep(
GuardsImpl::PreGuard g1, GuardValue v1, GuardsImpl::PreGuard g2, GuardValue v2
) {
exists(MethodCall check, int argIndex |
g1 = check and
v1.getDualValue().isThrowsException() and
conditionCheckArgument(check, argIndex, v2.asBooleanValue()) and
g2 = check.getArgument(argIndex)
)
}
predicate additionalImpliesStep = LogicInputCommon::additionalImpliesStep/4;
}

private module LogicInput_v2 implements GuardsImpl::LogicInputSig {
Expand All @@ -400,15 +457,13 @@ private module LogicInput_v2 implements GuardsImpl::LogicInputSig {
}
}

predicate parameterDefinition(Parameter p, SsaDefinition def) {
def.(SSA::SsaImplicitInit).isParameterDefinition(p)
}

predicate additionalNullCheck = LogicInputCommon::additionalNullCheck/4;

predicate additionalImpliesStep(
GuardsImpl::PreGuard g1, GuardValue v1, GuardsImpl::PreGuard g2, GuardValue v2
) {
LogicInput_v1::additionalImpliesStep(g1, v1, g2, v2)
or
CustomGuard::additionalImpliesStep(g1, v1, g2, v2)
}
predicate additionalImpliesStep = LogicInputCommon::additionalImpliesStep/4;
}

private module LogicInput_v3 implements GuardsImpl::LogicInputSig {
Expand All @@ -421,70 +476,11 @@ private module LogicInput_v3 implements GuardsImpl::LogicInputSig {

predicate additionalNullCheck = LogicInputCommon::additionalNullCheck/4;

predicate additionalImpliesStep = LogicInput_v2::additionalImpliesStep/4;
}

private module CustomGuardInput implements Guards_v2::CustomGuardInputSig {
private import semmle.code.java.dataflow.SSA

private int parameterPosition() { result in [-1, any(Parameter p).getPosition()] }

/** A parameter position represented by an integer. */
class ParameterPosition extends int {
ParameterPosition() { this = parameterPosition() }
}

/** An argument position represented by an integer. */
class ArgumentPosition extends int {
ArgumentPosition() { this = parameterPosition() }
}

/** Holds if arguments at position `apos` match parameters at position `ppos`. */
overlay[caller?]
pragma[inline]
predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) { ppos = apos }

final private class FinalMethod = Method;

class BooleanMethod extends FinalMethod {
BooleanMethod() {
super.getReturnType().(PrimitiveType).hasName("boolean") and
not super.isOverridable()
}

LogicInput_v2::SsaDefinition getParameter(ParameterPosition ppos) {
exists(Parameter p |
super.getParameter(ppos) = p and
not p.isVarargs() and
result.(SsaImplicitInit).isParameterDefinition(p)
)
}

GuardsInput::Expr getAReturnExpr() {
exists(ReturnStmt ret |
this = ret.getEnclosingCallable() and
ret.getResult() = result
)
}
}

private predicate booleanMethodCall(MethodCall call, BooleanMethod m) {
call.getMethod().getSourceDeclaration() = m
}

class BooleanMethodCall extends GuardsInput::Expr instanceof MethodCall {
BooleanMethodCall() { booleanMethodCall(this, _) }

BooleanMethod getMethod() { booleanMethodCall(this, result) }

GuardsInput::Expr getArgument(ArgumentPosition apos) { result = super.getArgument(apos) }
}
predicate additionalImpliesStep = LogicInputCommon::additionalImpliesStep/4;
}

class GuardValue = GuardsImpl::GuardValue;

private module CustomGuard = Guards_v2::CustomGuard<CustomGuardInput>;

/** INTERNAL: Don't use. */
module Guards_v1 = GuardsImpl::Logic<LogicInput_v1>;

Expand Down
69 changes: 69 additions & 0 deletions java/ql/test/library-tests/guards/Guards.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,73 @@ void t7(int[] a) {
chk(); // $ guarded=found:true guarded='i < a.length:false'
}
}

public static boolean testNotNull1(String input) {
return input != null && input.length() > 0;
}

public static boolean testNotNull2(String input) {
if (input == null) return false;
return input.length() > 0;
}

public static int getNumOrDefault(Integer number) {
return number == null ? 0 : number;
}

public static String concatNonNull(String s1, String s2) {
if (s1 == null || s2 == null) return null;
return s1 + s2;
}

public static Status testEnumWrapper(boolean flag) {
return flag ? Status.SUCCESS : Status.FAILURE;
}

enum Status { SUCCESS, FAILURE }

void testWrappers(String s, Integer i) {
if (testNotNull1(s)) {
chk(); // $ guarded='s:not null' guarded=testNotNull1(...):true
} else {
chk(); // $ guarded=testNotNull1(...):false
}

if (testNotNull2(s)) {
chk(); // $ guarded='s:not null' guarded=testNotNull2(...):true
} else {
chk(); // $ guarded=testNotNull2(...):false
}

if (0 == getNumOrDefault(i)) {
chk(); // $ guarded='0 == getNumOrDefault(...):true' guarded='getNumOrDefault(...):0'
} else {
chk(); // $ guarded='0 == getNumOrDefault(...):false' guarded='getNumOrDefault(...):not 0' guarded='i:not 0' guarded='i:not null'
}

if (null == concatNonNull(s, "suffix")) {
chk(); // $ guarded='concatNonNull(...):null' guarded='null == concatNonNull(...):true'
} else {
chk(); // $ guarded='concatNonNull(...):not null' guarded='null == concatNonNull(...):false' guarded='s:not null'
}

switch (testEnumWrapper(g(1))) {
case SUCCESS:
chk(); // $ guarded='testEnumWrapper(...):SUCCESS' guarded='testEnumWrapper(...):match SUCCESS' guarded=g(1):true
break;
case FAILURE:
chk(); // $ guarded='testEnumWrapper(...):FAILURE' guarded='testEnumWrapper(...):match FAILURE' guarded=g(1):false
break;
}
}

static void ensureNotNull(Object o) throws Exception {
if (o == null) throw new Exception();
}

void testExceptionWrapper(String s) throws Exception {
chk(); // nothing guards here
ensureNotNull(s);
chk(); // $ guarded='ensureNotNull(...):no exception' guarded='s:not null'
}
}
25 changes: 25 additions & 0 deletions java/ql/test/library-tests/guards/GuardsInline.expected
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,28 @@
| Guards.java:139:9:139:13 | chk(...) | found:true |
| Guards.java:143:7:143:11 | chk(...) | 'i < a.length:false' |
| Guards.java:143:7:143:11 | chk(...) | found:true |
| Guards.java:173:7:173:11 | chk(...) | 's:not null' |
| Guards.java:173:7:173:11 | chk(...) | testNotNull1(...):true |
| Guards.java:175:7:175:11 | chk(...) | testNotNull1(...):false |
| Guards.java:179:7:179:11 | chk(...) | 's:not null' |
| Guards.java:179:7:179:11 | chk(...) | testNotNull2(...):true |
| Guards.java:181:7:181:11 | chk(...) | testNotNull2(...):false |
| Guards.java:185:7:185:11 | chk(...) | '0 == getNumOrDefault(...):true' |
| Guards.java:185:7:185:11 | chk(...) | 'getNumOrDefault(...):0' |
| Guards.java:187:7:187:11 | chk(...) | '0 == getNumOrDefault(...):false' |
| Guards.java:187:7:187:11 | chk(...) | 'getNumOrDefault(...):not 0' |
| Guards.java:187:7:187:11 | chk(...) | 'i:not 0' |
| Guards.java:187:7:187:11 | chk(...) | 'i:not null' |
| Guards.java:191:7:191:11 | chk(...) | 'concatNonNull(...):null' |
| Guards.java:191:7:191:11 | chk(...) | 'null == concatNonNull(...):true' |
| Guards.java:193:7:193:11 | chk(...) | 'concatNonNull(...):not null' |
| Guards.java:193:7:193:11 | chk(...) | 'null == concatNonNull(...):false' |
| Guards.java:193:7:193:11 | chk(...) | 's:not null' |
| Guards.java:198:9:198:13 | chk(...) | 'testEnumWrapper(...):SUCCESS' |
| Guards.java:198:9:198:13 | chk(...) | 'testEnumWrapper(...):match SUCCESS' |
| Guards.java:198:9:198:13 | chk(...) | g(1):true |
| Guards.java:201:9:201:13 | chk(...) | 'testEnumWrapper(...):FAILURE' |
| Guards.java:201:9:201:13 | chk(...) | 'testEnumWrapper(...):match FAILURE' |
| Guards.java:201:9:201:13 | chk(...) | g(1):false |
| Guards.java:213:5:213:9 | chk(...) | 'ensureNotNull(...):no exception' |
| Guards.java:213:5:213:9 | chk(...) | 's:not null' |
2 changes: 1 addition & 1 deletion java/ql/test/query-tests/Nullness/C.java
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public void ex14(int[] a) {
obj = new Object();
} else if (a[i] == 2) {
verifyNotNull(obj);
obj.hashCode(); // NPE - false positive
obj.hashCode(); // OK
}
}
}
Expand Down
1 change: 0 additions & 1 deletion java/ql/test/query-tests/Nullness/NullMaybe.expected
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
| C.java:137:7:137:10 | obj2 | Variable $@ may be null at this access as suggested by $@ null guard. | C.java:131:5:131:23 | Object obj2 | obj2 | C.java:132:9:132:20 | ... != ... | this |
| C.java:144:15:144:15 | a | Variable $@ may be null at this access as suggested by $@ null guard. | C.java:141:20:141:26 | a | a | C.java:142:13:142:21 | ... == ... | this |
| C.java:188:9:188:11 | obj | Variable $@ may be null at this access because of $@ assignment. | C.java:181:5:181:22 | Object obj | obj | C.java:181:12:181:21 | obj | this |
| C.java:207:9:207:11 | obj | Variable $@ may be null at this access because of $@ assignment. | C.java:201:5:201:22 | Object obj | obj | C.java:201:12:201:21 | obj | this |
| C.java:219:9:219:10 | o1 | Variable $@ may be null at this access as suggested by $@ null guard. | C.java:212:20:212:28 | o1 | o1 | C.java:213:9:213:18 | ... == ... | this |
| C.java:233:7:233:8 | xs | Variable $@ may be null at this access because of $@ assignment. | C.java:231:5:231:56 | int[] xs | xs | C.java:231:11:231:55 | xs | this |
| F.java:11:5:11:7 | obj | Variable $@ may be null at this access as suggested by $@ null guard. | F.java:8:18:8:27 | obj | obj | F.java:9:9:9:19 | ... == ... | this |
Expand Down
Loading
Loading