diff --git a/tasks/docs/generate/test_xml_to_markdown.py b/tasks/docs/generate/test_xml_to_markdown.py index c547c9703..4516f2089 100755 --- a/tasks/docs/generate/test_xml_to_markdown.py +++ b/tasks/docs/generate/test_xml_to_markdown.py @@ -199,6 +199,49 @@ def memberdef(schema, fn): print("✓ Internal-schema private detection test passed") +def test_schema_name_misparse_is_skipped(): + """Name-dropped CREATE FUNCTION mis-parses are skipped, operators are kept. + + Doxygen drops the real name of `CREATE FUNCTION .(... a + . ...)`, leaving the schema as . These internal + "Unsupported operator blocker" helpers carry the word "operator" in their + brief, so the skip must key on (CREATE FUNCTION), not the + brief — otherwise they'd be mis-remapped to a junk operator name and + mislabeled public. A genuine CREATE OPERATOR is still recovered. + """ + from xml.etree import ElementTree as ET + + process_function = _load_process_function() + + # Mis-parsed CREATE FUNCTION (schema as name, "operator" in the brief). + misfn = ET.fromstring(''' + + eql_v3_internal + CREATE FUNCTION eql_v3_internal + CREATE FUNCTION + (a jsonb, b public.text_ord) RETURNS jsonb + Unsupported operator blocker for public.text_ord. + + + ''') + assert process_function(misfn) is None, "name-dropped CREATE FUNCTION must be skipped" + + # Genuine CREATE OPERATOR — recover the symbol from the brief, keep it. + op = ET.fromstring(''' + + public + CREATE OPERATOR public.->> + CREATE OPERATOR + (public.json, text) + ->> operator with text selector. + + + ''') + res = process_function(op) + assert res is not None and res["name"] == "->>", "CREATE OPERATOR symbol must be recovered" + + print("✓ Schema-name mis-parse skip test passed") + if __name__ == '__main__': print("Running xml-to-markdown tests...\n") @@ -208,6 +251,7 @@ def memberdef(schema, fn): test_param_name_type_swap() test_schema_qualified_type() test_internal_schema_is_private() + test_schema_name_misparse_is_skipped() print("\n✅ All tests passed!") sys.exit(0) diff --git a/tasks/docs/generate/xml-to-markdown.py b/tasks/docs/generate/xml-to-markdown.py index f838547fc..76afc9281 100755 --- a/tasks/docs/generate/xml-to-markdown.py +++ b/tasks/docs/generate/xml-to-markdown.py @@ -191,17 +191,34 @@ def process_function(memberdef): if func_name.upper() in sql_intrinsics: return None - # For SQL operators, Doxygen uses schema name as function name - # Extract actual operator from brief description + # Doxygen puts a SCHEMA where the function name should be in two cases, + # told apart by the (CREATE OPERATOR vs CREATE FUNCTION): + # 1. CREATE OPERATOR — Doxygen names it by schema and puts the operator + # symbol in the brief ("->> operator with ..."); recover it. + # 2. CREATE FUNCTION .(... . ...) where a + # schema-qualified operand type derails the C++ parser: it drops the + # real name, leaving the schema as . Unrecoverable, and these are + # the internal "Unsupported operator blocker" helpers — skip them. + # NB their brief contains the word "operator" ("Unsupported operator + # blocker for ..."), so the skip must key on , NOT the + # brief, or they'd be mis-remapped to a junk operator name. Left in, + # ~hundreds surface as bogus `eql_v3_internal` functions mislabeled + # public. brief_elem = memberdef.find('briefdescription') - if func_name in ['eql_v2', 'eql_v3', 'public'] and brief_elem is not None: - brief_para = brief_elem.find('para') - if brief_para is not None and brief_para.text: - # Check if brief starts with an operator (like "->>" or "->") - import re - op_match = re.match(r'^([^\s]+)\s+operator', brief_para.text.strip()) - if op_match: - func_name = op_match.group(1) # Use operator as function name + if func_name in ['eql_v2', 'eql_v3', 'eql_v3_internal', 'public']: + definition = extract_para_text(memberdef.find('definition')) + if definition.upper().startswith('CREATE FUNCTION'): + return None # name-dropped CREATE FUNCTION mis-parse + brief_para = brief_elem.find('para') if brief_elem is not None else None + op_match = ( + re.match(r'^([^\s]+)\s+operator', brief_para.text.strip()) + if brief_para is not None and brief_para.text + else None + ) + if op_match: + func_name = op_match.group(1) # CREATE OPERATOR: use the operator symbol + else: + return None # unrecoverable schema-named mis-parse # Check if this is a private/internal function. # Internal functions live in the `eql_v3_internal` schema. Doxygen puts the @@ -286,7 +303,6 @@ def process_function(memberdef): if argsstring is not None and argsstring.text: # Look for RETURNS keyword in argsstring - import re returns_match = re.search(r'RETURNS\s+([^\s]+)', argsstring.text) if returns_match: return_type_text = returns_match.group(1)