From 3de4625e583d9dd234c0d2c09b050aa147ada493 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 29 Aug 2022 21:38:17 -0400 Subject: [PATCH] deprecate: non-namespaced custom XPath handler functions See #2147 for more context. --- ext/nokogiri/nokogiri.h | 4 ++-- ext/nokogiri/xml_xpath_context.c | 7 +++++++ lib/nokogiri/xml/searchable.rb | 21 +++++++++++++-------- test/xml/test_xpath.rb | 16 ++++++++++++++++ 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/ext/nokogiri/nokogiri.h b/ext/nokogiri/nokogiri.h index 3313961ca93..ffc1749b91d 100644 --- a/ext/nokogiri/nokogiri.h +++ b/ext/nokogiri/nokogiri.h @@ -218,9 +218,9 @@ xmlParserCtxtPtr noko_xml_sax_parser_context_unwrap(VALUE rb_context); #define DISCARD_CONST_QUAL_XMLCHAR(v) DISCARD_CONST_QUAL(xmlChar *, v) #if HAVE_RB_CATEGORY_WARNING -# define NOKO_WARN_DEPRECATION(message) rb_category_warning(RB_WARN_CATEGORY_DEPRECATED, message) +# define NOKO_WARN_DEPRECATION(message...) rb_category_warning(RB_WARN_CATEGORY_DEPRECATED, message) #else -# define NOKO_WARN_DEPRECATION(message) rb_warning(message) +# define NOKO_WARN_DEPRECATION(message...) rb_warning(message) #endif void Nokogiri_structured_error_func_save(libxmlStructuredErrorHandlerState *handler_state); diff --git a/ext/nokogiri/xml_xpath_context.c b/ext/nokogiri/xml_xpath_context.c index d7df29636a4..175f5a2e26a 100644 --- a/ext/nokogiri/xml_xpath_context.c +++ b/ext/nokogiri/xml_xpath_context.c @@ -320,6 +320,13 @@ handler_lookup(void *data, const xmlChar *c_name, const xmlChar *c_ns_uri) { VALUE rb_handler = (VALUE)data; if (rb_respond_to(rb_handler, rb_intern((const char *)c_name))) { + if (c_ns_uri == NULL) { + NOKO_WARN_DEPRECATION( + "A custom XPath or CSS handler function named '%s' is being invoked without a namespace." + " Please update your query to reference this function as 'nokogiri:%s'." + " Invoking custom handler functions without a namespace is deprecated and support will be removed in a future release of Nokogiri.", + c_name, c_name); + } return method_caller; } diff --git a/lib/nokogiri/xml/searchable.rb b/lib/nokogiri/xml/searchable.rb index d7674ec3da5..52536291f8c 100644 --- a/lib/nokogiri/xml/searchable.rb +++ b/lib/nokogiri/xml/searchable.rb @@ -36,16 +36,19 @@ module Searchable # node.search('.//address[@domestic=$value]', nil, {:value => 'Yes'}) # # 💡 Custom XPath functions and CSS pseudo-selectors may also be defined. To define custom - # functions create a class and implement the function you want to define. The first argument - # to the method will be the current matching NodeSet. Any other arguments are ones that you - # pass in. Note that this class may appear anywhere in the argument list. For example: + # functions create a class and implement the function you want to define, which will be in the + # `nokogiri` namespace in XPath queries. + # + # The first argument to the method will be the current matching NodeSet. Any other arguments + # are ones that you pass in. Note that this class may appear anywhere in the argument + # list. For example: # # handler = Class.new { # def regex node_set, regex # node_set.find_all { |node| node['some_attribute'] =~ /#{regex}/ } # end # }.new - # node.search('.//title[regex(., "\w+")]', 'div.employee:regex("[0-9]+")', handler) + # node.search('.//title[nokogiri:regex(., "\w+")]', 'div.employee:regex("[0-9]+")', handler) # # See Searchable#xpath and Searchable#css for further usage help. def search(*args) @@ -160,16 +163,18 @@ def at_css(*args) # node.xpath('.//address[@domestic=$value]', nil, {:value => 'Yes'}) # # 💡 Custom XPath functions may also be defined. To define custom functions create a class and - # implement the function you want to define. The first argument to the method will be the - # current matching NodeSet. Any other arguments are ones that you pass in. Note that this - # class may appear anywhere in the argument list. For example: + # implement the function you want to define, which will be in the `nokogiri` namespace. + # + # The first argument to the method will be the current matching NodeSet. Any other arguments + # are ones that you pass in. Note that this class may appear anywhere in the argument + # list. For example: # # handler = Class.new { # def regex(node_set, regex) # node_set.find_all { |node| node['some_attribute'] =~ /#{regex}/ } # end # }.new - # node.xpath('.//title[regex(., "\w+")]', handler) + # node.xpath('.//title[nokogiri:regex(., "\w+")]', handler) # def xpath(*args) paths, handler, ns, binds = extract_params(args) diff --git a/test/xml/test_xpath.rb b/test/xml/test_xpath.rb index becca2dc9e2..eba2e843a29 100644 --- a/test/xml/test_xpath.rb +++ b/test/xml/test_xpath.rb @@ -121,6 +121,22 @@ def test_css_search_with_ambiguous_integer_or_string_attributes refute_nil(doc.at_css("img[width=200]")) end + def test_xpath_with_nonnamespaced_custom_function_is_deprecated_but_works + result = nil + assert_output("", /Invoking custom handler functions without a namespace is deprecated/) do + result = @xml.xpath("anint()", @handler) + end + assert_equal(1230456, result) + end + + def test_xpath_with_namespaced_custom_function_is_not_deprecated + result = nil + assert_silent do + result = @xml.xpath("nokogiri:anint()", @handler) + end + assert_equal(1230456, result) + end + def test_css_search_uses_custom_selectors_with_arguments set = @xml.css('employee > address:my_filter("domestic", "Yes")', @handler) refute_empty(set)