Skip to content

Commit 28a6905

Browse files
authored
Merge pull request #2712 from DeusData/fix/py-namespace-string-future-facts
fix(python): docstrings and __future__ imports are no namespace barriers
2 parents d211526 + 93724ba commit 28a6905

4 files changed

Lines changed: 200 additions & 20 deletions

File tree

‎internal/cbm/cbm.h‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,12 @@ static inline bool cbm_import_binds_runtime_name(const CBMImport *imp) {
412412
return imp && !imp->type_checking_only;
413413
}
414414

415-
/* Content-only ordered Python namespace facts; no resolved target identities. */
416-
#define CBM_PY_NAMESPACE_FACTS_VERSION 1u
415+
/* Content-only ordered Python namespace facts; no resolved target identities.
416+
* The version travels in the lsp_surface "py" payload, and a stored payload of
417+
* another version does not decode: an incremental run then re-extracts in full,
418+
* so stored facts of an older extraction never mix with fresh ones.
419+
* 2: string-literal statements are no effect; `from __future__` imports bind. */
420+
#define CBM_PY_NAMESPACE_FACTS_VERSION 2u
417421

418422
typedef enum {
419423
CBM_PY_NS_NOT_CAPTURED = 0,

‎internal/cbm/extract_imports.c‎

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,37 @@ static bool py_ns_plain_string(PyNamespaceBuilder *b, TSNode node, const char **
612612
return true;
613613
}
614614

615+
/* One string of a literal: any quoting or prefix (r, b, u, rb), unless it is an
616+
* f-string with a replacement field, whose __format__ / __str__ may run code. */
617+
static bool py_ns_string_without_field(TSNode node) {
618+
if (strcmp(ts_node_type(node), "string") != 0)
619+
return false;
620+
uint32_t count = ts_node_named_child_count(node);
621+
for (uint32_t i = 0; i < count; i++) {
622+
if (strcmp(ts_node_type(ts_node_named_child(node, i)), "interpolation") == 0)
623+
return false;
624+
}
625+
return true;
626+
}
627+
628+
/* A string literal, implicit concatenations included, evaluates nothing: a
629+
* docstring (module, class or attribute) is no namespace effect. */
630+
static bool py_ns_literal_string(TSNode node) {
631+
if (strcmp(ts_node_type(node), "concatenated_string") != 0)
632+
return py_ns_string_without_field(node);
633+
uint32_t count = ts_node_named_child_count(node);
634+
bool parts = false;
635+
for (uint32_t i = 0; i < count; i++) {
636+
TSNode part = ts_node_named_child(node, i);
637+
if (strcmp(ts_node_type(part), "comment") == 0)
638+
continue;
639+
if (!py_ns_string_without_field(part))
640+
return false;
641+
parts = true;
642+
}
643+
return parts;
644+
}
645+
615646
/* Bounded syntactic proof only. Unsupported or deep expressions become an
616647
* unknown-effect barrier; no evaluator or source execution is involved. */
617648
static bool py_ns_safe_expr(PyNamespaceBuilder *b, TSNode node, int depth) {
@@ -624,11 +655,8 @@ static bool py_ns_safe_expr(PyNamespaceBuilder *b, TSNode node, int depth) {
624655
strcmp(kind, "float") == 0 || strcmp(kind, "true") == 0 || strcmp(kind, "false") == 0 ||
625656
strcmp(kind, "none") == 0 || strcmp(kind, "comment") == 0)
626657
return true;
627-
if (strcmp(kind, "string") == 0) {
628-
const char *text = NULL;
629-
size_t len = 0;
630-
return py_ns_plain_string(b, node, &text, &len);
631-
}
658+
if (strcmp(kind, "string") == 0 || strcmp(kind, "concatenated_string") == 0)
659+
return py_ns_literal_string(node);
632660
bool container =
633661
strcmp(kind, "tuple") == 0 || strcmp(kind, "list") == 0 ||
634662
strcmp(kind, "parenthesized_expression") == 0 || strcmp(kind, "parameters") == 0 ||
@@ -677,8 +705,7 @@ static bool py_ns_class_safe(PyNamespaceBuilder *b, TSNode node) {
677705
continue;
678706
if (strcmp(kind, "expression_statement") == 0 &&
679707
ts_node_named_child_count(child) == 1) {
680-
TSNode expr = ts_node_named_child(child, 0);
681-
if (strcmp(ts_node_type(expr), "string") == 0 && py_ns_safe_expr(b, expr, 0))
708+
if (py_ns_literal_string(ts_node_named_child(child, 0)))
682709
continue;
683710
/* Even an identifier assignment can install a descriptor whose
684711
* __set_name__ hook executes during class creation. */
@@ -968,6 +995,35 @@ static bool py_ns_type_checking_block(PyNamespaceBuilder *b, TSNode node) {
968995
return !b->failure;
969996
}
970997

998+
/* `from __future__ import x [as y]` is its own node type, without a module
999+
* field: a compiler directive that also binds x (or y) to the feature object,
1000+
* like a from-import of module `__future__`. */
1001+
static bool py_ns_future_import(PyNamespaceBuilder *b, TSNode node) {
1002+
const char *module_name = py_ns_copy(b, "__future__", strlen("__future__"));
1003+
if (!module_name)
1004+
return false;
1005+
TSTreeCursor cursor = ts_tree_cursor_new(node);
1006+
bool ok = true;
1007+
int emitted = 0;
1008+
if (ts_tree_cursor_goto_first_child(&cursor)) {
1009+
do {
1010+
const char *field = ts_tree_cursor_current_field_name(&cursor);
1011+
if (!field || strcmp(field, "name") != 0)
1012+
continue;
1013+
CBMPyNamespaceFact f;
1014+
if (!py_ns_import_fact(b, ts_tree_cursor_current_node(&cursor), true, module_name, 0,
1015+
&f) ||
1016+
!py_ns_push(b, f)) {
1017+
ok = false;
1018+
break;
1019+
}
1020+
emitted++;
1021+
} while (ts_tree_cursor_goto_next_sibling(&cursor));
1022+
}
1023+
ts_tree_cursor_delete(&cursor);
1024+
return ok && emitted > 0;
1025+
}
1026+
9711027
static bool py_ns_statement(PyNamespaceBuilder *b, TSNode node) {
9721028
const char *kind = ts_node_type(node);
9731029
if (strcmp(kind, "comment") == 0 || strcmp(kind, "pass_statement") == 0)
@@ -976,6 +1032,8 @@ static bool py_ns_statement(PyNamespaceBuilder *b, TSNode node) {
9761032
return py_ns_import(b, node, false);
9771033
if (strcmp(kind, "import_from_statement") == 0)
9781034
return py_ns_import(b, node, true);
1035+
if (strcmp(kind, "future_import_statement") == 0)
1036+
return py_ns_future_import(b, node);
9791037
bool decorated = strcmp(kind, "decorated_definition") == 0;
9801038
TSNode def = decorated ? ts_node_child_by_field_name(node, TS_FIELD("definition")) : node;
9811039
if (!ts_node_is_null(def)) {

‎tests/test_extraction.c‎

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12318,6 +12318,96 @@ TEST(python_namespace_facts_type_checking_alternatives) {
1231812318
PASS();
1231912319
}
1232012320

12321+
/* A string-literal statement (a module, class or attribute docstring) has no
12322+
* effect, and `from __future__ import x` binds x like any from-import. Neither
12323+
* may withdraw the names bound before it: sqlalchemy's sql/expression.py lost
12324+
* its 149 re-exports to `nullsfirst = nulls_first` followed by a triple-quoted
12325+
* docstring. An f-string with a replacement field can run code
12326+
* (__format__ / __str__) and stays an effect. */
12327+
static bool py_facts_any_unknown(const CBMFileResult *r, CBMPyNamespaceUnknownReason reason) {
12328+
for (int i = 0; r && i < r->py_namespace.count; i++) {
12329+
if (r->py_namespace.items[i].kind == CBM_PY_NS_UNKNOWN &&
12330+
r->py_namespace.items[i].reason == reason)
12331+
return true;
12332+
}
12333+
return false;
12334+
}
12335+
12336+
TEST(python_namespace_facts_string_statements_and_future_imports) {
12337+
static const char source[] = "\"\"\"Module docstring.\"\"\"\n"
12338+
"from __future__ import annotations\n"
12339+
"from __future__ import (division as div, generator_stop)\n"
12340+
"from .impl import a\n"
12341+
"\"\"\"A triple-quoted string after an import.\"\"\"\n"
12342+
"'''Single-quoted triple.'''\n"
12343+
"r\"raw \\d\"\n"
12344+
"b\"bytes\"\n"
12345+
"rb'raw bytes'\n"
12346+
"u\"unicode\"\n"
12347+
"\"implicit\" 'concatenation'\n"
12348+
"f\"no replacement field {{escaped}}\"\n"
12349+
"from .impl import b\n"
12350+
"class Documented:\n"
12351+
" \"\"\"Class docstring.\"\"\"\n"
12352+
" def method(self):\n"
12353+
" pass\n"
12354+
"x = a\n"
12355+
"\"\"\"Attribute docstring.\"\"\"\n";
12356+
CBMFileResult *r = extract(source, CBM_LANG_PYTHON, "t", "pkg/__init__.py");
12357+
bool ok = r && !r->has_error && r->py_namespace.status == CBM_PY_NS_COMPLETE &&
12358+
cbm_py_namespace_facts_valid(&r->py_namespace) && r->py_namespace.count == 7;
12359+
if (ok) {
12360+
const CBMPyNamespaceFact *f = r->py_namespace.items;
12361+
ok = f[0].kind == CBM_PY_NS_IMPORT_NAME && strcmp(f[0].module_name, "__future__") == 0 &&
12362+
f[0].relative_level == 0 && strcmp(f[0].member_name, "annotations") == 0 &&
12363+
strcmp(f[0].local_name, "annotations") == 0 && f[1].kind == CBM_PY_NS_IMPORT_NAME &&
12364+
strcmp(f[1].member_name, "division") == 0 && strcmp(f[1].local_name, "div") == 0 &&
12365+
f[2].kind == CBM_PY_NS_IMPORT_NAME && strcmp(f[2].local_name, "generator_stop") == 0 &&
12366+
f[3].kind == CBM_PY_NS_IMPORT_NAME && strcmp(f[3].local_name, "a") == 0 &&
12367+
f[4].kind == CBM_PY_NS_IMPORT_NAME && strcmp(f[4].local_name, "b") == 0 &&
12368+
f[5].kind == CBM_PY_NS_OWN_DEF && f[5].def_kind == CBM_PY_NS_DEF_CLASS &&
12369+
strcmp(f[5].local_name, "Documented") == 0 && f[6].kind == CBM_PY_NS_SHADOW &&
12370+
strcmp(f[6].local_name, "x") == 0;
12371+
}
12372+
if (!ok && r) {
12373+
for (int i = 0; i < r->py_namespace.count; i++) {
12374+
const CBMPyNamespaceFact *f = &r->py_namespace.items[i];
12375+
printf(" string/future fact %d: kind %d reason %d name %s\n", i, (int)f->kind,
12376+
(int)f->reason, f->local_name ? f->local_name : "-");
12377+
}
12378+
}
12379+
if (r)
12380+
cbm_free_result(r);
12381+
ASSERT_TRUE(ok); /* RED while a docstring is an effect and __future__ is unsupported */
12382+
12383+
/* Controls: each stays a namespace barrier. */
12384+
static const struct {
12385+
const char *name;
12386+
const char *source;
12387+
CBMPyNamespaceUnknownReason reason;
12388+
} barriers[] = {
12389+
{"f-string field", "from .impl import a\nf\"{a}\"\n", CBM_PY_NS_REASON_EFFECT},
12390+
{"concatenated f-string field", "from .impl import a\n\"x\" f\"{a!r}\"\n",
12391+
CBM_PY_NS_REASON_EFFECT},
12392+
{"call", "from .impl import a\nprint(a)\n", CBM_PY_NS_REASON_EFFECT},
12393+
/* Python rejects it, and so does the grammar: the whole file is unknown */
12394+
{"star from __future__", "from .impl import a\nfrom __future__ import *\n",
12395+
CBM_PY_NS_REASON_PARSE},
12396+
};
12397+
for (size_t i = 0; i < sizeof(barriers) / sizeof(barriers[0]); i++) {
12398+
r = extract(barriers[i].source, CBM_LANG_PYTHON, "t", "pkg/__init__.py");
12399+
ok = r && r->py_namespace.status == CBM_PY_NS_COMPLETE &&
12400+
cbm_py_namespace_facts_valid(&r->py_namespace) &&
12401+
py_facts_any_unknown(r, barriers[i].reason);
12402+
if (!ok)
12403+
printf(" control '%s' is no namespace barrier\n", barriers[i].name);
12404+
if (r)
12405+
cbm_free_result(r);
12406+
ASSERT_TRUE(ok);
12407+
}
12408+
PASS();
12409+
}
12410+
1232112411
TEST(python_namespace_facts_distinguish_all_operations_and_unknowns) {
1232212412
static const char source[] = "__all__ = ['old']; __all__ = []\n"
1232312413
"__all__ = dynamic\n"
@@ -12665,7 +12755,7 @@ TEST(python_namespace_facts_multiple_bindings_and_unsupported_effects) {
1266512755
"__all__ = ['escaped\\n']\n";
1266612756
CBMFileResult *r = extract(source, CBM_LANG_PYTHON, "t", "main.py");
1266712757
bool ok = r && !r->has_error && !r->parse_incomplete &&
12668-
r->py_namespace.status == CBM_PY_NS_COMPLETE && r->py_namespace.count == 14 &&
12758+
r->py_namespace.status == CBM_PY_NS_COMPLETE && r->py_namespace.count == 13 &&
1266912759
cbm_py_namespace_facts_valid(&r->py_namespace) && has_def(r, "Function", "later") &&
1267012760
has_def(r, "Class", "HasDescriptor");
1267112761
if (ok) {
@@ -12681,8 +12771,10 @@ TEST(python_namespace_facts_multiple_bindings_and_unsupported_effects) {
1268112771
strcmp(f[7].local_name, "__all__") == 0 && f[8].kind == CBM_PY_NS_UNKNOWN &&
1268212772
f[9].kind == CBM_PY_NS_SHADOW && strcmp(f[9].local_name, "values") == 0 &&
1268312773
f[10].kind == CBM_PY_NS_UNKNOWN && f[11].kind == CBM_PY_NS_SHADOW &&
12684-
f[11].reason == CBM_PY_NS_REASON_UNPROVEN_CLASS && f[12].kind == CBM_PY_NS_UNKNOWN &&
12685-
f[13].kind == CBM_PY_NS_ALL_UNKNOWN;
12774+
f[11].reason == CBM_PY_NS_REASON_UNPROVEN_CLASS &&
12775+
/* a string with an escape evaluates nothing: no effect, but no
12776+
* literal export name either */
12777+
f[12].kind == CBM_PY_NS_ALL_UNKNOWN;
1268612778
}
1268712779
if (r)
1268812780
cbm_free_result(r);
@@ -12705,6 +12797,7 @@ SUITE(extraction) {
1270512797
RUN_TEST(python_namespace_facts_unknown_effects_and_body_stability);
1270612798
RUN_TEST(python_namespace_facts_parse_errors_are_complete_unknown);
1270712799
RUN_TEST(python_namespace_facts_type_checking_alternatives);
12800+
RUN_TEST(python_namespace_facts_string_statements_and_future_imports);
1270812801
RUN_TEST(python_namespace_facts_capture_and_copy_fail_atomically);
1270912802
RUN_TEST(python_namespace_facts_validation_rejects_partial_and_unknown_schema);
1271012803
RUN_TEST(python_namespace_facts_compact_spill_and_copy_keep_owned_payload);

‎tests/test_pipeline.c‎

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14624,6 +14624,7 @@ static const char *const py_reexport_calls[][2] = {
1462414624
{"ctl_from_cls", "direct.impl.Direct"},
1462514625
{"ctl_from_fn", "direct.impl.direct_fn"},
1462614626
{"lazy_tc", "lz.lazy_mod.Lazy"},
14627+
{"doc_reexport", "docp.impl.Doc"},
1462714628
};
1462814629
enum { PY_REEXPORT_N_CALLS = sizeof(py_reexport_calls) / sizeof(py_reexport_calls[0]) };
1462914630

@@ -14633,7 +14634,7 @@ enum { PY_REEXPORT_N_CALLS = sizeof(py_reexport_calls) / sizeof(py_reexport_call
1463314634
static const char *const py_reexport_excluded[][2] = {
1463414635
{"excl_unlisted", "pkg.all_mod.Unlisted"}, {"excl_private", "pkg.star_mod._private_fn"},
1463514636
{"excl_shadowed", "shad.impl.Gone"}, {"excl_barrier", "barr.impl.Before"},
14636-
{"excl_compat", "cmp.types_only.Shim"},
14637+
{"excl_compat", "cmp.types_only.Shim"}, {"excl_fstring", "fsp.impl.Kept"},
1463714638
};
1463814639
enum { PY_REEXPORT_N_EXCLUDED = sizeof(py_reexport_excluded) / sizeof(py_reexport_excluded[0]) };
1463914640

@@ -14693,13 +14694,28 @@ static int py_reexport_run(bool parallel, bool incremental, bool *lsp, bool *exc
1469314694
" except ImportError:\n Shim = None\n");
1469414695
write_temp_file(tmp, "app/cmp/types_only.py", "class Shim:\n pass\n");
1469514696
write_temp_file(tmp, "app/cmp/runtime_impl.py", "class Shim:\n pass\n");
14697+
/* The sql/expression.py shape of sqlalchemy: docstrings and a __future__
14698+
* import around the re-exports; an f-string with a field stays a barrier. */
14699+
write_temp_file(tmp, "app/docp/__init__.py",
14700+
"\"\"\"Package docstring.\"\"\"\n"
14701+
"from __future__ import annotations\n"
14702+
"from .impl import Doc\n"
14703+
"alias = Doc\n"
14704+
"\"\"\"Attribute docstring.\"\"\"\n");
14705+
write_temp_file(tmp, "app/docp/impl.py", "class Doc:\n pass\n");
14706+
write_temp_file(tmp, "app/fsp/__init__.py",
14707+
"from .impl import Kept\n"
14708+
"f\"{Kept}\"\n");
14709+
write_temp_file(tmp, "app/fsp/impl.py", "class Kept:\n pass\n");
1469614710
write_temp_file(tmp, "app/main.py",
1469714711
"from app import pkg\n"
1469814712
"from app import cyc\n"
1469914713
"from app import shad\n"
1470014714
"from app import barr\n"
1470114715
"from app import lz\n"
1470214716
"from app import cmp\n"
14717+
"from app import docp\n"
14718+
"from app import fsp\n"
1470314719
"from app.db import models\n"
1470414720
"from app.pkg import Explicit, Listed, Deep, star_fn, Renamed\n"
1470514721
"from app.direct.impl import Direct, direct_fn\n\n\n"
@@ -14725,6 +14741,8 @@ static int py_reexport_run(bool parallel, bool incremental, bool *lsp, bool *exc
1472514741
"def excl_barrier():\n barr.Before()\n\n\n"
1472614742
"def excl_compat():\n cmp.Shim()\n\n\n"
1472714743
"def lazy_tc():\n lz.Lazy()\n\n\n"
14744+
"def doc_reexport():\n docp.Doc()\n\n\n"
14745+
"def excl_fstring():\n fsp.Kept()\n\n\n"
1472814746
"def ctl_from_cls():\n Direct()\n\n\n"
1472914747
"def ctl_from_fn():\n direct_fn()\n");
1473014748
for (int i = 0; parallel && i < 52; i++) {
@@ -23459,26 +23477,32 @@ TEST(pipeline_python_namespace_codec_owns_ordered_facts) {
2345923477
}
2346023478

2346123479
TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows) {
23462-
/* Numeric values below are the frozen version-1 namespace wire values.
23463-
* Even an empty LSP array must validate its namespace envelope. */
23480+
/* Numeric values below are the frozen namespace wire values (version 2
23481+
* changed which statements extraction proves harmless, not the wire
23482+
* shape). Even an empty LSP array must validate its namespace envelope,
23483+
* and a payload of an older facts version is no authority: its facts
23484+
* were extracted under other rules, so the incremental run that reads
23485+
* it re-extracts in full. */
2346423486
const char *bad[] = {
2346523487
"{\"v\":1,\"lsp\":[]}",
2346623488
"{\"v\":3,\"lang\":1,\"lsp\":[]}",
2346723489
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":null}",
2346823490
"{\"v\":3,\"lang\":1.0,\"lsp\":[],\"py\":null}",
2346923491
"{\"v\":3,\"v\":3,\"lang\":0,\"lsp\":[],\"py\":null}",
23470-
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1,"
23492+
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1,"
2347123493
"\"module\":\"p.m\",\"path\":\"m.py\",\"status\":3,\"failure\":2,\"events\":[]}}",
23494+
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1,"
23495+
"\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[]}}",
2347223496
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":99,\"lang\":1,"
2347323497
"\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[]}}",
23474-
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1,"
23498+
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1,"
2347523499
"\"module\":\"p.m\",\"path\":\"m.py\\u0000suffix\",\"status\":2,"
2347623500
"\"failure\":0,\"events\":[]}}",
23477-
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1,"
23501+
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1,"
2347823502
"\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":["
2347923503
"{\"k\":999,\"d\":0,\"s\":0,\"r\":0,\"f\":0,\"l\":0,"
2348023504
"\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}",
23481-
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1,"
23505+
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1,"
2348223506
"\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":["
2348323507
"{\"k\":8,\"d\":0,\"s\":1,\"r\":0,\"f\":0,\"l\":4294967296,"
2348423508
"\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}",
@@ -23498,7 +23522,7 @@ TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows) {
2349823522
cbm_arena_destroy(&arena);
2349923523
}
2350023524
const char *unknown =
23501-
"{\"v\":3,\"lang\":1,\"lsp\":[],\"reg\":[],\"py\":{\"v\":1,\"lang\":1,"
23525+
"{\"v\":3,\"lang\":1,\"lsp\":[],\"reg\":[],\"py\":{\"v\":2,\"lang\":1,"
2350223526
"\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":["
2350323527
"{\"k\":7,\"d\":0,\"s\":0,\"r\":7,\"f\":0,\"l\":0,"
2350423528
"\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}";
@@ -23511,6 +23535,7 @@ TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows) {
2351123535
facts.facts.items[0].kind == CBM_PY_NS_UNKNOWN &&
2351223536
facts.facts.items[0].reason == CBM_PY_NS_REASON_PARSE;
2351323537
cbm_arena_destroy(&arena);
23538+
ASSERT_EQ(CBM_PY_NAMESPACE_FACTS_VERSION, 2u); /* the rows above spell it */
2351423539
ASSERT_TRUE(rejected);
2351523540
ASSERT_EQ(unknown_rc, 1);
2351623541
ASSERT_EQ(equal_rc, 0);

0 commit comments

Comments
 (0)