Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* When a guard such as `isSafe(x)` is defined, we now also automatically handle `isSafe(x) == true` and `isSafe(x) != false`.
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change note currently documents only == true / != false, but the implementation and PR description also cover is / is not. Also, for Python-facing examples the boolean literals should be True/False (capitalized) to match Python syntax.

Suggested change
* When a guard such as `isSafe(x)` is defined, we now also automatically handle `isSafe(x) == true` and `isSafe(x) != false`.
* When a guard such as `isSafe(x)` is defined, we now also automatically handle comparisons to boolean literals such as `isSafe(x) is True`, `isSafe(x) == True`, `isSafe(x) is not False`, and `isSafe(x) != False`.

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
Expand Up @@ -553,14 +553,42 @@ ControlFlowNode guardNode(ConditionBlock conditionBlock, boolean flipped) {
result = conditionBlock.getLastNode() and
flipped = false
or
// Recursive case: if a guard node is a `not`-expression,
// Recursive cases:
// if a guard node is a `not`-expression,
// the operand is also a guard node, but with inverted polarity.
exists(UnaryExprNode notNode |
result = notNode.getOperand() and
notNode.getNode().getOp() instanceof Not
|
notNode = guardNode(conditionBlock, flipped.booleanNot())
)
or
// if a guard node is compared to a boolean literal,
// the other operand is also a guard node,
// but with polarity depending on the literal (and on the comparison).
exists(CompareNode cmpNode, Cmpop op, ControlFlowNode b, boolean bool |
(
cmpNode.operands(result, op, b) or
cmpNode.operands(b, op, result)
) and
not result.getNode() instanceof BooleanLiteral and
(
// comparing to the boolean
(op instanceof Eq or op instanceof Is) and
// `bool` is the value being compared against, here the value of `b`
b.getNode().(BooleanLiteral).booleanValue() = bool
or
// comparing to the negation of the boolean
(op instanceof NotEq or op instanceof IsNot) and
// again, `bool` is the value being compared against, but here it is the value of `not b`
b.getNode().(BooleanLiteral).booleanValue() = bool.booleanNot()
)
|
// if `bool` is true, we should preserve `flipped`, otherwise we should flip it
// `flipped xor (not bool)` achieves that.
flipped in [true, false] and
cmpNode = guardNode(conditionBlock, flipped.booleanXor(bool.booleanNot()))
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,12 @@ isSanitizer
| test_logical.py:176:24:176:24 | ControlFlowNode for s |
| test_logical.py:185:24:185:24 | ControlFlowNode for s |
| test_logical.py:193:24:193:24 | ControlFlowNode for s |
| test_logical.py:199:28:199:28 | ControlFlowNode for s |
| test_logical.py:206:28:206:28 | ControlFlowNode for s |
| test_logical.py:211:28:211:28 | ControlFlowNode for s |
| test_logical.py:214:28:214:28 | ControlFlowNode for s |
| test_logical.py:219:28:219:28 | ControlFlowNode for s |
| test_logical.py:226:28:226:28 | ControlFlowNode for s |
| test_logical.py:231:28:231:28 | ControlFlowNode for s |
| test_logical.py:234:28:234:28 | ControlFlowNode for s |
| test_reference.py:31:28:31:28 | ControlFlowNode for s |
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,49 @@ def test_with_exception_neg():

ensure_not_tainted(s)

def test_comparison_with_bool():
s = TAINTED_STRING

if is_safe(s) == True:
ensure_not_tainted(s)
else:
ensure_tainted(s) # $ tainted

if is_safe(s) == False:
ensure_tainted(s) # $ tainted
else:
ensure_not_tainted(s)

if is_safe(s) != True:
ensure_tainted(s) # $ tainted
else:
ensure_not_tainted(s)

if is_safe(s) != False:
ensure_not_tainted(s)
else:
ensure_tainted(s) # $ tainted

if is_safe(s) is True:
ensure_not_tainted(s)
else:
ensure_tainted(s) # $ tainted

if is_safe(s) is False:
ensure_tainted(s) # $ tainted
else:
ensure_not_tainted(s)

if is_safe(s) is not True:
ensure_tainted(s) # $ tainted
else:
ensure_not_tainted(s)

if is_safe(s) is not False:
ensure_not_tainted(s)
else:
ensure_tainted(s) # $ tainted
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line has trailing whitespace after the # $ tainted marker; please remove it to keep test files clean and avoid noisy diffs/linters.

Suggested change
ensure_tainted(s) # $ tainted
ensure_tainted(s) # $ tainted

Copilot uses AI. Check for mistakes.

# Make tests runable

test_basic()
Expand All @@ -211,3 +254,4 @@ def test_with_exception_neg():
test_with_exception_neg()
except:
pass
test_comparison_with_bool()
Loading