]> git.kaiwu.me - njs.git/commitdiff
XML: preserve node methods with matching children
authorDmitry Volyntsev <xeioex@nginx.com>
Tue, 29 Sep 2026 05:23:45 +0000 (22:23 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Tue, 29 Sep 2026 20:48:28 +0000 (13:48 -0700)
QuickJS exposes child elements as own properties, so a child named
like an XMLNode method hides the method on its prototype. Let methods
take precedence over bare child names, as on the njs engine. Keep the
child accessible through $tag$name and avoid deleting it when deleting
the method name.

Cover collisions for all XMLNode methods and prototype changes.

external/qjs_xml_module.c
test/xml/xml.t.mjs
ts/njs_modules/xml.d.ts

index 9afb44de103ad96b90b8e64d0c973024525f7126..29dd14919d651af6fae61c157f5dead5ffa3882a 100644 (file)
@@ -72,6 +72,8 @@ static void qjs_xml_doc_free(JSRuntime *rt, qjs_xml_doc_t *current);
 
 static JSValue qjs_xml_node_make(JSContext *cx, qjs_xml_doc_t *doc,
     xmlNode *node);
+static int qjs_xml_node_has_method(JSContext *cx, JSValueConst obj,
+    JSAtom prop, njs_str_t *name);
 static int qjs_xml_node_get_own_property(JSContext *cx,
     JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
 static int qjs_xml_node_get_own_property_names(JSContext *cx,
@@ -1006,10 +1008,49 @@ qjs_xml_node_text_handler(JSContext *cx, JSValue current, JSValue setval)
 }
 
 
+static int
+qjs_xml_node_has_method(JSContext *cx, JSValueConst obj, JSAtom prop,
+    njs_str_t *name)
+{
+    int                         rc;
+    size_t                      i, length;
+    JSValue                     proto;
+    const JSCFunctionListEntry  *entry;
+
+    for (i = 0; i < njs_nitems(qjs_xml_node_proto); i++) {
+        entry = &qjs_xml_node_proto[i];
+
+        if (entry->def_type != JS_DEF_CFUNC) {
+            continue;
+        }
+
+        length = njs_strlen(entry->name);
+
+        if (name->length == length
+            && njs_strncmp(name->start, entry->name, length) == 0)
+        {
+            proto = JS_GetPrototype(cx, obj);
+            if (JS_IsException(proto)) {
+                return -1;
+            }
+
+            rc = JS_IsObject(proto) ? JS_GetOwnProperty(cx, NULL, proto, prop)
+                                    : 0;
+            JS_FreeValue(cx, proto);
+
+            return rc;
+        }
+    }
+
+    return 0;
+}
+
+
 static int
 qjs_xml_node_get_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
     JSValueConst obj, JSAtom prop)
 {
+    int             rc;
     u_char          *text;
     JSValue         value;
     xmlNode         *node;
@@ -1241,6 +1282,12 @@ qjs_xml_node_get_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
 
     nm = name;
 
+    rc = qjs_xml_node_has_method(cx, obj, prop, &name);
+    if (rc != 0) {
+        JS_FreeCString(cx, (char *) name.start);
+        return rc < 0 ? -1 : 0;
+    }
+
 tag:
 
     value = qjs_xml_node_tag_handler(cx, current, &nm);
@@ -1436,6 +1483,14 @@ qjs_xml_node_property_modify(JSContext *cx, JSValueConst obj, JSAtom atom,
         }
     }
 
+    if (delete) {
+        rc = qjs_xml_node_has_method(cx, obj, atom, &name);
+        if (rc != 0) {
+            JS_FreeCString(cx, (char *) name.start);
+            return rc < 0 ? -1 : 1;
+        }
+    }
+
     rc = qjs_xml_node_tag_modify(cx, obj, &name, delete);
     JS_FreeCString(cx, (char *) name.start);
 
index 6c129b880538288ad1ca17dbf451c766a6fa99a1..ed6078c88944872e841da48e268793471806c2f7 100644 (file)
@@ -81,6 +81,54 @@ let parse_tsuite = {
         { doc: `<r><!--c--><?p?>text<c/></r>`,
           get: (doc) => doc.r.getChildNS(null, 'c').$name,
           expected: 'c' },
+        { doc: `<r ID="ok"><addChild/><getAttributeNS/><getChildNS/>`
+               + `<removeChildren/><setAttribute/><removeAttribute/>`
+               + `<removeAllAttributes/><setText/><removeText/>`
+               + `<Issuer>trusted</Issuer></r>`,
+          get: (doc) => {
+              const node = doc.r;
+              const methods = ['addChild', 'getAttributeNS', 'getChildNS',
+                               'removeChildren', 'setAttribute',
+                               'removeAttribute', 'removeAllAttributes',
+                               'setText', 'removeText'];
+
+              return methods.every(name => typeof node[name] == 'function'
+                             && node[`$tag$${name}`].$name == name)
+                     && node.getAttributeNS(null, 'ID') == 'ok'
+                     && node.getChildNS(null, 'Issuer').$text == 'trusted';
+          },
+          expected: true },
+        { doc: `<r><getChildNS/></r>`,
+          get: (doc) => {
+              delete doc.r.$tag$getChildNS;
+              return typeof doc.r.getChildNS == 'function'
+                     && doc.r.$tag$getChildNS === undefined;
+          },
+          expected: true },
+        { doc: `<r><setAttribute/></r>`,
+          skip: () => !has_quickjs(),
+          get: (doc) => {
+              delete doc.r.setAttribute;
+              return typeof doc.r.setAttribute == 'function'
+                     && doc.r.$tag$setAttribute.$name == 'setAttribute';
+          },
+          expected: true },
+        { doc: `<r><getChildNS/></r>`,
+          skip: () => !has_quickjs(),
+          get: (doc) => {
+              const proto = Object.getPrototypeOf(doc.r);
+              const method = proto.getChildNS;
+
+              delete proto.getChildNS;
+
+              try {
+                  return doc.r.getChildNS.$name == 'getChildNS';
+
+              } finally {
+                  proto.getChildNS = method;
+              }
+          },
+          expected: true },
         { doc: `<!DOCTYPE r [<!ENTITY e "complete">]>`
                + `<r xmlns:x="urn:other" x:ID="pre&e;post"/>`,
           get: (doc) => doc.r.getAttributeNS('urn:other', 'ID'),
index 4cd7431e110f554d3d1285b0df9503664a9e5909..558df08f87df2511c572238e8b20d4ee9fe48e3e 100644 (file)
@@ -103,6 +103,7 @@ declare module "xml" {
 
         /**
          * node.$tag$xxx - the node's first child tag named "xxx".
+         * Use this form when the tag name matches an XMLNode method.
          * Assigning a value throws TypeError at runtime. Deleting the property
          * removes matching child tags.
          */