Summary
NodeSet#attr(key, value) uses truthiness to decide whether a value was supplied and whether to invoke a block. Explicit false and nil values therefore do not follow the setter contract.
Reproduction
doc = Nokogiri::XML('<root><item/></root>')
nodes = doc.xpath('//item')
nodes.attr('enabled', false)
p nodes.first['enabled']
The positional form returns an attribute/getter result instead of the NodeSet. Hash values also fall through to yield when false/nil, raising LocalJumpError without a block.
Candidate and verification
Use a private sentinel to distinguish an omitted argument from explicit false/nil, and invoke the block only when supplied. Source-only candidate f28a177d0d43a2d0a92fe5871d977e1d584b8700 is based on current main. Current passes 2,841 runs /10,329 assertions with 24 skips, plus focused positional/hash false/nil models. Release reproduces each failure.
This is issue-first because a compliant Nokogiri change requires behavioral tests and changelog material, while the consumer audit does not modify upstream tests.
Summary
NodeSet#attr(key, value)uses truthiness to decide whether a value was supplied and whether to invoke a block. Explicitfalseandnilvalues therefore do not follow the setter contract.Reproduction
The positional form returns an attribute/getter result instead of the NodeSet. Hash values also fall through to
yieldwhen false/nil, raisingLocalJumpErrorwithout a block.Candidate and verification
Use a private sentinel to distinguish an omitted argument from explicit false/nil, and invoke the block only when supplied. Source-only candidate
f28a177d0d43a2d0a92fe5871d977e1d584b8700is based on current main. Current passes 2,841 runs /10,329 assertions with 24 skips, plus focused positional/hash false/nil models. Release reproduces each failure.This is issue-first because a compliant Nokogiri change requires behavioral tests and changelog material, while the consumer audit does not modify upstream tests.