Skip to content

Fix XPath variable handling and invalid value contamination#342

Open
tompng wants to merge 5 commits into
ruby:masterfrom
tompng:fix_xpath_nil_contamination
Open

Fix XPath variable handling and invalid value contamination#342
tompng wants to merge 5 commits into
ruby:masterfrom
tompng:fix_xpath_nil_contamination

Conversation

@tompng

@tompng tompng commented Jun 28, 2026

Copy link
Copy Markdown
Member

Fixes #15

Fix nil and other invalid value contamination. Nil can be injected through variables and through id function.
Unimplemented function id() is fixed to return [] instead of nil.
Applies predicates/path evaluation if it exists after variable.
Add variable validation (TypeError if invalid) and variable existence check (NameError if undefined variable referenced)

Minor behavior changes for an invalid XPath match:

doc = REXML::Document.new("<root/>")
REXML::XPath.match(doc, '($x)[1<2]', {}, {'x'=>42})
#=> [42] → []
REXML::XPath.match(doc, '$x[1<2]', {}, {'x'=>42})
#=> [42] → []

It may raise TypeError (because $x wasn't evaluated to a nodeset), though, it shoudn't return [42]

Note

Although nodeset as a variable has been accepted before, the code added in pull request explicitly permits it.
Nokogiri only accepts string value as a variable, so there's an option to limit the value types here.
Accepting nodeset may cause a problem if the passed nodes doesn't belong to a single document root. (or just consider such case as unsupported)

Copilot AI review requested due to automatic review settings June 28, 2026 08:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adjusts REXML’s XPath evaluation to prevent nil/invalid values from “contaminating” results, primarily by making id() return an empty node-set and by coercing variable values + applying remaining predicates/steps after variable/group evaluation.

Changes:

  • Make unimplemented XPath function id() return [] (empty node-set) instead of nil.
  • Add variable coercion and ensure remaining predicates/steps are applied after variable and grouped expressions.
  • Add/extend tests covering id() and variable handling (including invalid predicate application on non-node-set variables).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
test/xpath/test_base.rb Adds regression tests for id() and variable coercion/predicate behavior.
lib/rexml/xpath_parser.rb Implements variable coercion and applies remaining predicates after variable/group evaluation.
lib/rexml/functions.rb Changes id() to return an empty node-set ([]).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rexml/xpath_parser.rb
Comment thread test/xpath/test_base.rb Outdated
@tompng tompng force-pushed the fix_xpath_nil_contamination branch from 3f20d0b to f343f5b Compare June 28, 2026 09:23
Copilot AI review requested due to automatic review settings July 7, 2026 03:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

tompng added 2 commits July 12, 2026 12:02
Fix nil and other invalid value contamination. Nil can be injected through variables and through id function.
Unimplemented function `id()` is fixed to return `[]` instead of `nil`.
Add variable coerce and predicates after variables.
Comment thread lib/rexml/xpath_parser.rb
Comment on lines +337 to +338
# If evaluated value is not a nodeset, treat it as an empty nodeset.
# TODO: Decide whether REXML should raise type error or keep this behavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If evaluated value is not a nodeset, treat it as an empty nodeset.
TODO: Decide whether REXML should raise type error or keep this behavior.

According to the XPath specification, anything other than a node set results in an error.

https://www.w3.org/TR/1999/REC-xpath-19991116/#node-sets

It is an error if the expression to be filtered does not evaluate to a node-set.

It is an error if the expression does not evaluate to a node-set.

I think it would be better to raise an error, as that would make the processing clearer.
What do you think? @kou @tompng

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think it's better to align with the spec, but it requires much more changes
In my opinion, it must out of scope of this PR, must be done in a separated PR on top of this and on #343
For example, these xpath that requires requires intermediate value to be a nodeset should be changed to raise error.

# Function argument that requires nodeset
local-name(1)
sum("string")
count(1 < 2)

# Path after function
string(/a)[predicate]
string(/a)/path

# Path after non-nodeset variable
$x[predicate]
$x/path

# path after literal
"string"[predicate]
42/path

# path after neg
-number(/a)[predicate]
-number(/a)/path

Comment thread lib/rexml/xpath_parser.rb Outdated
Comment on lines +346 to +347
# Coerces a variable value to a type that can be used in XPath expressions.
# TODO: Decide whether REXML should warn, raise, or ignore when a variable value is invalid.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Coerces a variable value to a type that can be used in XPath expressions.
TODO: Decide whether REXML should warn, raise, or ignore when a variable value is invalid.

According to the XPath specification, if a variable name is not bound to any value, an error occurs.

https://www.w3.org/TR/1999/REC-xpath-19991116/#section-Basics

A VariableReference evaluates to the value to which the variable name is bound in the set of variable bindings in the context.

I think it would be better to raise an error, as that would make the processing clearer.
What do you think? @kou @tompng

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think referencing unbound variable should raise error. Concern: when to do that, and what kind of error should it raise.

when :variable
  var_name = path_stack.shift
  # Handle unbound variables
  raise unless @variables.key?(var_name)

  # Handle conversion and validation of variable value
  # @variables[var_name]==nil case is handled here
  value = coerce_variable(@variables[var_name])
  ...

Even if we reject unbound variables, this TODO comment for bound variables (reject bound invalid-value variable or convert it to a fallback value "") still remains.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Concern: when to do that, and what kind of error should it raise.

I think it would be better to check immediately after parsing and before evaluation begins, rather than raising an exception when the variable statement is evaluated.
I think NameError is the appropriate error type to raise.

Reasons:

  • If we raise only when the reference is actually evaluated, the error becomes document-dependent. For example, //foo[@bar=$typo] does not raise when no foo reaches the predicate, and or/and short-circuits can also skip the reference. So the typo silently returns [] in exactly the case we most want to detect.

Even if we reject unbound variables, this TODO comment for bound variables (reject bound invalid-value variable or convert it to a fallback value "") still remains.

I think the preferred design is to fall back to "" for compatibility purposes only when the value is nil, and to reject the input in all other cases.
In this case, I think TypeError is the appropriate type of error to raise.

@tompng tompng Jul 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think it would be better to check immediately after parsing and before evaluation

It makes sense. Applied in 7cdffee
The added check is worth enabled for normal use, so it is applied even if strict is false (default)

Copilot AI review requested due to automatic review settings July 13, 2026 19:02
@tompng tompng force-pushed the fix_xpath_nil_contamination branch from a31f5ce to 06bafbe Compare July 13, 2026 19:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread lib/rexml/xpath_parser.rb
Comment thread lib/rexml/xpath_parser.rb
Comment on lines +357 to +360
unless value.all?(REXML::Node)
raise TypeError, 'Array variable must contain only REXML::Node objects'
end
value.uniq

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ensured to work correctly with test added in d19849a

Copilot AI review requested due to automatic review settings July 13, 2026 19:16
@tompng tompng force-pushed the fix_xpath_nil_contamination branch from 06bafbe to d19849a Compare July 13, 2026 19:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread lib/rexml/xpath_parser.rb
Comment on lines +75 to 79
def variables=(vars)
vars = vars.transform_values { |v| coerce_variable(v) }
@functions.variables = vars
@variables = vars
end
Comment thread lib/rexml/xpath_parser.rb
Comment on lines +356 to +360
when Array
unless value.all?(REXML::Node)
raise TypeError, 'Array variable must contain only REXML::Node objects'
end
value.uniq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XPath: fix default node detection in functions

3 participants