1use crate::bindings::PythonLexicalScopeInventory;
4use brokk_bifrost_core::analyzer::ProjectFile;
5use brokk_bifrost_core::analyzer::Range;
6use brokk_bifrost_core::analyzer::python_facts::PythonSourceFacts;
7use brokk_bifrost_core::analyzer::query_token::QueryToken;
8use brokk_bifrost_core::analyzer::source_facts::SourceFactRows;
9use std::collections::{HashMap, HashSet};
10use std::mem::size_of;
11use std::sync::Arc;
12use tree_sitter::Node;
13
14#[derive(Debug, Clone, Default)]
22pub struct PythonModuleGlobalSourceFacts {
23 bindings: HashMap<Box<str>, PythonModuleGlobalBindingFact>,
24 global_declarations: HashSet<Box<str>>,
25 wildcard_import_offsets: Vec<usize>,
26 has_dynamic_write: bool,
27 has_exec: bool,
28}
29
30#[derive(Debug, Clone, Default)]
31struct PythonModuleGlobalBindingFact {
32 statements: u32,
33 initializer: Option<Range>,
34 initializer_statement_end: usize,
35}
36
37impl PythonModuleGlobalSourceFacts {
38 pub fn module_binds_name(&self, name: &str) -> bool {
40 self.bindings.contains_key(name)
41 }
42
43 pub fn initializer_range(&self, name: &str) -> Option<Range> {
45 if self.has_dynamic_write || self.has_exec || self.global_declarations.contains(name) {
46 return None;
47 }
48 let binding = self.bindings.get(name)?;
49 if binding.statements != 1 {
50 return None;
51 }
52 if self
53 .wildcard_import_offsets
54 .iter()
55 .any(|offset| *offset > binding.initializer_statement_end)
56 {
57 return None;
58 }
59 binding.initializer
60 }
61
62 pub fn estimated_retained_bytes(&self) -> usize {
64 size_of::<Self>()
65 + self
66 .bindings
67 .keys()
68 .map(|name| name.len() + size_of::<PythonModuleGlobalBindingFact>())
69 .sum::<usize>()
70 + self
71 .global_declarations
72 .iter()
73 .map(|name| name.len() + size_of::<Box<str>>())
74 .sum::<usize>()
75 + self.wildcard_import_offsets.len() * size_of::<usize>()
76 }
77}
78
79#[derive(Debug)]
80enum PythonModuleMutationHazard {
81 Global(Vec<Box<str>>),
82 Exec,
83 ReflectiveWrite,
84}
85
86fn python_module_mutation_hazard_bounded(
89 node: Node<'_>,
90 source: &str,
91 mut scope_step: impl FnMut() -> bool,
92) -> Option<Option<PythonModuleMutationHazard>> {
93 if !scope_step() {
94 return None;
95 }
96 if node.kind() == "global_statement" {
97 let mut names = Vec::new();
98 let mut cursor = node.walk();
99 for child in node.named_children(&mut cursor) {
100 if !scope_step() {
101 return None;
102 }
103 if child.kind() == "identifier" {
104 names.push(child.utf8_text(source.as_bytes()).ok()?.into());
105 }
106 }
107 return Some(Some(PythonModuleMutationHazard::Global(names)));
108 }
109 if node.kind() != "call" {
110 return Some(None);
111 }
112 if node
113 .child_by_field_name("function")
114 .is_some_and(|function| {
115 function.kind() == "identifier"
116 && function.utf8_text(source.as_bytes()).ok() == Some("exec")
117 })
118 {
119 return Some(Some(PythonModuleMutationHazard::Exec));
120 }
121 Some(
122 module_globals_write_call_bounded(node, source, &mut scope_step)?
123 .then_some(PythonModuleMutationHazard::ReflectiveWrite),
124 )
125}
126
127#[derive(Debug)]
133pub struct PythonModuleMutationFacts {
134 global_binding_events: HashSet<Box<str>>,
135 dynamic_hazards: Vec<Range>,
136}
137
138impl PythonModuleMutationFacts {
139 pub fn collect_bounded(
142 root: Node<'_>,
143 source: &str,
144 mut scope_step: impl FnMut() -> bool,
145 ) -> Option<Self> {
146 use brokk_bifrost_core::analyzer::tree_walk::{
147 WalkControl, node_range, try_walk_named_tree_preorder,
148 };
149 if !scope_step() || root.kind() != "module" || root.has_error() {
150 return None;
151 }
152 let mut facts = Self {
153 global_binding_events: HashSet::new(),
154 dynamic_hazards: Vec::new(),
155 };
156 let mut checked_global_scopes = HashSet::new();
157 try_walk_named_tree_preorder(root, true, |node| {
158 if !scope_step() {
159 return Err(());
160 }
161 match python_module_mutation_hazard_bounded(node, source, &mut scope_step).ok_or(())? {
162 Some(PythonModuleMutationHazard::Global(_)) => {
163 let mut current = node.parent();
166 while let Some(scope) = current {
167 if !scope_step() {
168 return Err(());
169 }
170 if scope.kind() == "module" {
171 break;
174 }
175 if matches!(
176 scope.kind(),
177 "function_definition" | "lambda" | "class_definition"
178 ) {
179 if !checked_global_scopes.contains(&scope.id()) {
180 let bindings = PythonLexicalScopeInventory::collect_bounded(
183 scope,
184 source,
185 std::iter::empty::<String>(),
186 &mut scope_step,
187 )
188 .ok_or(())?
189 .into_bindings();
190 for name in bindings.global_binding_events() {
193 if !scope_step() {
194 return Err(());
195 }
196 facts.global_binding_events.insert(name.into());
197 }
198 checked_global_scopes.insert(scope.id());
199 }
200 break;
201 }
202 current = scope.parent();
203 }
204 }
205 Some(
206 PythonModuleMutationHazard::Exec | PythonModuleMutationHazard::ReflectiveWrite,
207 ) => {
208 facts.dynamic_hazards.push(node_range(node));
209 }
210 None => {}
211 }
212 Ok(WalkControl::Continue)
213 })
214 .ok()?;
215 scope_step().then_some(facts)
216 }
217
218 pub fn binding_unmutated_at_bounded(
224 &self,
225 root: Node<'_>,
226 name: &str,
227 reference: Node<'_>,
228 mut scope_step: impl FnMut() -> bool,
229 ) -> Option<bool> {
230 use brokk_bifrost_core::analyzer::tree_walk::node_range;
231 use brokk_bifrost_core::analyzer::usages::reference_site::smallest_named_node_covering_with_scope;
232 if !scope_step() {
233 return None;
234 }
235 if root.kind() != "module" || root.has_error() {
236 return scope_step().then_some(false);
237 }
238 let deferred = crate::syntax::python_reference_is_deferred_function_body_bounded(
239 reference,
240 &mut scope_step,
241 )?;
242 let read_range = node_range(reference);
243 let mut unmutated = !self.global_binding_events.contains(name);
244 if unmutated {
245 for hazard in &self.dynamic_hazards {
246 if !scope_step() {
247 return None;
248 }
249 let node = smallest_named_node_covering_with_scope(
250 root,
251 hazard.start_byte,
252 hazard.end_byte,
253 &mut scope_step,
254 )?;
255 if node.start_byte() != hazard.start_byte || node.end_byte() != hazard.end_byte {
256 return None;
257 }
258 if deferred
261 || crate::runtime_values::python_hazard_can_precede_bounded(
262 *hazard,
263 read_range,
264 node,
265 &mut scope_step,
266 )?
267 {
268 unmutated = false;
269 break;
270 }
271 }
272 }
273 scope_step().then_some(unmutated)
276 }
277
278 pub fn export_unmutated_bounded(
281 &self,
282 name: &str,
283 mut scope_step: impl FnMut() -> bool,
284 ) -> Option<bool> {
285 if !scope_step() {
286 return None;
287 }
288 let unmutated =
289 !self.global_binding_events.contains(name) && self.dynamic_hazards.is_empty();
290 scope_step().then_some(unmutated)
291 }
292}
293
294pub fn python_module_binding_unmutated_at_bounded(
296 root: Node<'_>,
297 source: &str,
298 name: &str,
299 reference: Node<'_>,
300 mut scope_step: impl FnMut() -> bool,
301) -> Option<bool> {
302 PythonModuleMutationFacts::collect_bounded(root, source, &mut scope_step)?
303 .binding_unmutated_at_bounded(root, name, reference, scope_step)
304}
305
306pub fn python_module_export_unmutated_bounded(
308 root: Node<'_>,
309 source: &str,
310 name: &str,
311 mut scope_step: impl FnMut() -> bool,
312) -> Option<bool> {
313 PythonModuleMutationFacts::collect_bounded(root, source, &mut scope_step)?
314 .export_unmutated_bounded(name, scope_step)
315}
316
317pub fn python_module_global_source_facts(
324 root: Node<'_>,
325 source: &str,
326 max_nodes: usize,
327 mut keep_going: impl FnMut() -> bool,
328) -> Option<PythonModuleGlobalSourceFacts> {
329 if root.kind() != "module" || root.has_error() {
330 return None;
331 }
332 let mut remaining = max_nodes;
333 let mut facts = PythonModuleGlobalSourceFacts::default();
334 let mut stack = vec![(root, true)];
335 while let Some((node, initializer_path)) = stack.pop() {
336 if remaining == 0 || !keep_going() {
337 return None;
338 }
339 remaining -= 1;
340
341 if node.kind() == "wildcard_import" {
342 facts.wildcard_import_offsets.push(node.start_byte());
343 }
344
345 let mut bound_names = HashSet::<String>::new();
346 if matches!(
347 node.kind(),
348 "function_definition"
349 | "class_definition"
350 | "import_statement"
351 | "import_from_statement"
352 | "assignment"
353 | "augmented_assignment"
354 | "annotated_assignment"
355 | "named_expression"
356 | "for_statement"
357 | "delete_statement"
358 | "as_pattern"
359 | "except_clause"
360 | "case_clause"
361 ) {
362 let mut steps = || {
363 if remaining == 0 || !keep_going() {
364 false
365 } else {
366 remaining -= 1;
367 true
368 }
369 };
370 for binding in
371 crate::bindings::python_direct_scope_bindings_bounded(node, source, &mut steps)?
372 {
373 if binding.declaration.kind() == "identifier" {
374 bound_names.insert(
375 binding
376 .declaration
377 .utf8_text(source.as_bytes())
378 .ok()?
379 .to_owned(),
380 );
381 }
382 }
383 }
384 if node.kind() == "annotated_assignment"
385 && let Some(target) = node.child_by_field_name("left")
386 {
387 module_target_names(
388 target,
389 source,
390 &mut bound_names,
391 &mut remaining,
392 &mut keep_going,
393 )?;
394 }
395 if node.kind() == "with_statement" {
396 let mut cursor = node.walk();
397 for child in node.named_children(&mut cursor) {
398 if child.kind() == "with_clause" {
399 let mut with_stack = vec![child];
400 while let Some(item) = with_stack.pop() {
401 if remaining == 0 || !keep_going() {
402 return None;
403 }
404 remaining -= 1;
405 if item.kind() == "as_pattern"
406 && let Some(alias) = item.child_by_field_name("alias")
407 {
408 module_target_names(
409 alias,
410 source,
411 &mut bound_names,
412 &mut remaining,
413 &mut keep_going,
414 )?;
415 }
416 with_stack.extend(item.named_children(&mut item.walk()));
417 }
418 }
419 }
420 }
421
422 if !bound_names.is_empty() {
423 let assignment_initializer = (node.kind() == "assignment"
424 && initializer_path
425 && node.parent().is_some_and(|parent| {
426 matches!(parent.kind(), "module" | "block" | "expression_statement")
427 })
428 && node.child_by_field_name("type").is_none()
429 && node.child_by_field_name("annotation").is_none()
430 && node
431 .child_by_field_name("left")
432 .is_some_and(|left| left.kind() == "identifier")
433 && bound_names.len() == 1)
434 .then(|| node.child_by_field_name("right"))
435 .flatten();
436 for name in bound_names {
437 let binding = facts.bindings.entry(name.into_boxed_str()).or_default();
438 binding.statements = binding.statements.saturating_add(1);
439 if let Some(value) = assignment_initializer {
440 binding.initializer = Some(node_range(value, source));
441 binding.initializer_statement_end = node.end_byte();
442 } else {
443 binding.initializer = None;
444 binding.initializer_statement_end = node.end_byte();
445 }
446 }
447 }
448
449 let allowed_block = matches!(
450 node.kind(),
451 "if_statement"
452 | "try_statement"
453 | "with_statement"
454 | "for_statement"
455 | "block"
456 | "except_clause"
457 | "else_clause"
458 | "finally_clause"
459 | "expression_statement"
460 );
461 let scope_body = matches!(
462 node.kind(),
463 "function_definition" | "class_definition" | "lambda"
464 )
465 .then(|| node.child_by_field_name("body").map(|body| body.id()))
466 .flatten();
467 let name_node = matches!(node.kind(), "function_definition" | "class_definition")
468 .then(|| node.child_by_field_name("name").map(|name| name.id()))
469 .flatten();
470 let mut cursor = node.walk();
471 let children = node.named_children(&mut cursor).collect::<Vec<_>>();
472 for child in children.into_iter().rev() {
473 if Some(child.id()) == scope_body || Some(child.id()) == name_node {
474 continue;
475 }
476 let child_initializer_path =
477 if matches!(node.kind(), "module" | "block" | "expression_statement") {
478 initializer_path
479 } else if matches!(
480 node.kind(),
481 "except_clause" | "else_clause" | "finally_clause"
482 ) {
483 initializer_path && child.kind() == "block"
484 } else if allowed_block {
485 if node.kind() == "try_statement" {
486 initializer_path
487 && matches!(
488 child.kind(),
489 "block" | "except_clause" | "else_clause" | "finally_clause"
490 )
491 } else if matches!(node.kind(), "if_statement" | "for_statement")
492 && child.kind() == "else_clause"
493 {
494 initializer_path
495 } else {
496 initializer_path
497 && child.kind() == "block"
498 && (node
499 .child_by_field_name("body")
500 .is_some_and(|body| body.id() == child.id())
501 || node
502 .child_by_field_name("consequence")
503 .is_some_and(|body| body.id() == child.id())
504 || node
505 .child_by_field_name("alternative")
506 .is_some_and(|body| body.id() == child.id()))
507 }
508 } else {
509 false
510 };
511 if matches!(
512 node.kind(),
513 "list_comprehension"
514 | "set_comprehension"
515 | "dictionary_comprehension"
516 | "generator_expression"
517 ) && child.kind() == "for_in_clause"
518 {
519 let target = child.child_by_field_name("left");
520 let mut clause_cursor = child.walk();
521 for clause_child in child.named_children(&mut clause_cursor) {
522 if target.is_some_and(|target| target.id() == clause_child.id()) {
523 continue;
524 }
525 stack.push((clause_child, false));
526 }
527 continue;
528 }
529 stack.push((child, child_initializer_path));
530 }
531 }
532
533 remaining = max_nodes;
534 let mut stack = vec![root];
535 while let Some(node) = stack.pop() {
536 if remaining == 0 || !keep_going() {
537 return None;
538 }
539 remaining -= 1;
540 match python_module_mutation_hazard_bounded(node, source, || true)? {
543 Some(PythonModuleMutationHazard::Global(names)) => {
544 facts.global_declarations.extend(names);
545 }
546 Some(PythonModuleMutationHazard::Exec) => facts.has_exec = true,
547 Some(PythonModuleMutationHazard::ReflectiveWrite) => facts.has_dynamic_write = true,
548 None => {}
549 }
550 let mut cursor = node.walk();
551 stack.extend(node.named_children(&mut cursor));
552 }
553 facts.wildcard_import_offsets.sort_unstable();
554 Some(facts)
555}
556
557fn node_range(node: Node<'_>, source: &str) -> Range {
558 let start_byte = node.start_byte();
559 let end_byte = node.end_byte();
560 let start_line = source.as_bytes()[..start_byte]
561 .iter()
562 .filter(|byte| **byte == b'\n')
563 .count()
564 + 1;
565 let end_line = source.as_bytes()[..end_byte]
566 .iter()
567 .filter(|byte| **byte == b'\n')
568 .count()
569 + 1;
570 Range {
571 start_byte,
572 end_byte,
573 start_line,
574 end_line,
575 }
576}
577
578fn module_target_names(
579 root: Node<'_>,
580 source: &str,
581 names: &mut HashSet<String>,
582 remaining: &mut usize,
583 keep_going: &mut impl FnMut() -> bool,
584) -> Option<()> {
585 let mut stack = vec![root];
586 while let Some(node) = stack.pop() {
587 if *remaining == 0 || !keep_going() {
588 return None;
589 }
590 *remaining -= 1;
591 match node.kind() {
592 "identifier" => {
593 names.insert(node.utf8_text(source.as_bytes()).ok()?.to_owned());
594 }
595 "attribute" | "subscript" | "call" | "string" | "integer" => {}
596 _ => {
597 let mut cursor = node.walk();
598 stack.extend(node.named_children(&mut cursor));
599 }
600 }
601 }
602 Some(())
603}
604
605fn module_globals_write_call_bounded(
606 call: Node<'_>,
607 source: &str,
608 mut scope_step: impl FnMut() -> bool,
609) -> Option<bool> {
610 if !scope_step() {
611 return None;
612 }
613 let function = call.child_by_field_name("function");
614 let Some(function) = function else {
615 return Some(false);
616 };
617 if function.kind() == "attribute" {
618 let method = function.child_by_field_name("attribute");
619 let object = function.child_by_field_name("object");
620 if method.is_some_and(|method| {
621 method
622 .utf8_text(source.as_bytes())
623 .ok()
624 .is_some_and(|name| matches!(name, "update" | "setdefault" | "pop" | "clear"))
625 }) && object.is_some_and(|object| is_module_mapping_call(object, source))
626 {
627 return Some(true);
628 }
629 }
630 if !is_module_mapping_call(call, source) {
631 return Some(false);
632 }
633 let mut current = call;
634 while let Some(parent) = current.parent() {
635 if !scope_step() {
636 return None;
637 }
638 match parent.kind() {
639 "assignment" | "augmented_assignment" | "annotated_assignment" => {
640 return Some(parent.child_by_field_name("left").is_some_and(|left| {
641 left.start_byte() <= call.start_byte() && call.end_byte() <= left.end_byte()
642 }));
643 }
644 "delete_statement" => return Some(true),
645 "subscript" | "attribute" => current = parent,
646 _ => return Some(false),
647 }
648 }
649 Some(false)
650}
651
652fn is_module_mapping_call(node: Node<'_>, source: &str) -> bool {
653 if node.kind() != "call"
654 || !node
655 .child_by_field_name("arguments")
656 .is_some_and(|arguments| {
657 let mut cursor = arguments.walk();
658 arguments.named_children(&mut cursor).next().is_none()
659 })
660 {
661 return false;
662 }
663 node.child_by_field_name("function")
664 .is_some_and(|function| {
665 function.kind() == "identifier"
666 && function
667 .utf8_text(source.as_bytes())
668 .ok()
669 .is_some_and(|name| matches!(name, "globals" | "vars"))
670 })
671}
672
673#[derive(Debug, Clone)]
674pub struct PythonFileSourceFacts {
675 pub occurrences: SourceFactRows,
676 pub facts: PythonSourceFacts,
677}
678
679impl PythonFileSourceFacts {
680 pub fn callable_return_for_range(
682 &self,
683 start: usize,
684 end: usize,
685 keep_going: &dyn Fn() -> bool,
686 ) -> Option<&brokk_bifrost_core::analyzer::python_facts::PythonCallableReturnFact> {
687 for fact in &self.facts.callable_returns {
688 if !keep_going() {
689 return None;
690 }
691 let declaration = self.occurrences.declaration(fact.declaration);
692 let range = self.occurrences.occurrence(declaration.occurrence).range;
693 if range.start_byte == start && range.end_byte == end {
694 return Some(fact);
695 }
696 }
697 None
698 }
699
700 pub fn estimated_retained_bytes(&self) -> usize {
701 self.occurrences
702 .estimated_bytes()
703 .saturating_add(self.facts.estimated_retained_bytes())
704 }
705}
706
707pub trait PythonSourceFactProvider: Send + Sync {
708 fn python_source_facts(
711 &self,
712 token: QueryToken<'_>,
713 file: &ProjectFile,
714 keep_going: &dyn Fn() -> bool,
715 ) -> Option<Arc<PythonFileSourceFacts>>;
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721 use brokk_bifrost_test_macros::hardwon;
722
723 fn module_facts(source: &str) -> PythonModuleGlobalSourceFacts {
724 let mut parser = tree_sitter::Parser::new();
725 parser
726 .set_language(&tree_sitter_python::LANGUAGE.into())
727 .expect("Python grammar");
728 let tree = parser.parse(source, None).expect("parse fixture");
729 python_module_global_source_facts(tree.root_node(), source, 100_000, || true)
730 .expect("complete Python module facts")
731 }
732
733 fn initializer<'a>(
734 facts: &PythonModuleGlobalSourceFacts,
735 source: &'a str,
736 name: &str,
737 ) -> Option<&'a str> {
738 let range = facts.initializer_range(name)?;
739 source.get(range.start_byte..range.end_byte)
740 }
741
742 #[test]
743 #[hardwon(
744 "#4622+#5745: module mutation and stopped scans cannot prove unchanged initializer classes"
745 )]
746 fn module_global_source_fact_requires_a_single_safe_assignment() {
747 let source = "class Target: pass\n\
748safe = Target()\n\
749items = [safe for safe in ()]\n\
750if True:\n nested_safe = Target()\n";
751 let facts = module_facts(source);
752 assert_eq!(
753 Some("Target()"),
754 initializer(&facts, source, "safe"),
755 "{facts:#?}"
756 );
757 assert_eq!(Some("Target()"), initializer(&facts, source, "nested_safe"));
758
759 let source = "class Target: pass\n\
760global_write = Target()\n\
761def change():\n global global_write\n global_write = None\n";
762 assert_eq!(
763 None,
764 initializer(&module_facts(source), source, "global_write")
765 );
766
767 let source = "class Target: pass\n\
768twice = Target()\n\
769twice = Target()\n";
770 assert_eq!(None, initializer(&module_facts(source), source, "twice"));
771
772 let source = "class Target: pass\n\
773dynamic = Target()\n\
774globals()[\"dynamic\"] = None\n";
775 assert_eq!(None, initializer(&module_facts(source), source, "dynamic"));
776
777 let source = "class Target: pass\n\
778dynamic = Target()\n\
779vars().update(dynamic=None)\n";
780 assert_eq!(None, initializer(&module_facts(source), source, "dynamic"));
781
782 let source = "class Target: pass\n\
783dynamic = Target()\n\
784exec(\"dynamic = None\")\n";
785 assert_eq!(None, initializer(&module_facts(source), source, "dynamic"));
786
787 let deferred_exec =
788 "class Target: pass\nvalue = Target()\ndef later():\n exec('value = None')\n";
789 assert_eq!(
790 None,
791 initializer(&module_facts(deferred_exec), deferred_exec, "value")
792 );
793
794 let bounded_source =
797 "class Base: pass\nclass D(Base): pass\ndef later():\n exec('Base = object')\n";
798 let mut parser = tree_sitter::Parser::new();
799 parser
800 .set_language(&tree_sitter_python::LANGUAGE.into())
801 .expect("Python grammar");
802 let tree = parser.parse(bounded_source, None).expect("parse fixture");
803 let root = tree.root_node();
804 let reference = root
805 .named_child(1)
806 .unwrap()
807 .child_by_field_name("superclasses")
808 .unwrap()
809 .named_child(0)
810 .unwrap();
811 let mut visits = 0_usize;
812 assert_eq!(
813 Some(true),
814 python_module_binding_unmutated_at_bounded(
815 root,
816 bounded_source,
817 "Base",
818 reference,
819 || {
820 visits += 1;
821 true
822 },
823 )
824 );
825 for allowance in 0..visits {
826 let mut remaining = allowance;
827 assert_eq!(
828 None,
829 python_module_binding_unmutated_at_bounded(
830 root,
831 bounded_source,
832 "Base",
833 reference,
834 || {
835 let allowed = remaining > 0;
836 remaining = remaining.saturating_sub(1);
837 allowed
838 },
839 ),
840 "stopping after {allowance} of {visits} visits cannot admit Base"
841 );
842 }
843 assert_eq!(
844 Some(false),
845 python_module_export_unmutated_bounded(root, bounded_source, "Base", || true)
846 );
847
848 let multi_source = "class A: pass\nclass B: pass\nclass C: pass\nclass ReadOnly: pass\nclass Sibling: pass\nclass D(A, B, C, ReadOnly, Sibling): pass\ndef writer():\n global A\n global B, C\n global ReadOnly\n A = None\n class B(B): pass\n del C\n return ReadOnly\ndef sibling():\n global Sibling\n from elsewhere import Sibling\n";
851 let tree = parser
852 .parse(multi_source, None)
853 .expect("parse all-name events");
854 let root = tree.root_node();
855 assert!(!root.has_error());
856 let arguments = root
857 .named_child(5)
858 .unwrap()
859 .child_by_field_name("superclasses")
860 .unwrap();
861 let mut collection_visits = 0_usize;
862 let mutation = PythonModuleMutationFacts::collect_bounded(root, multi_source, || {
863 collection_visits += 1;
864 true
865 })
866 .expect("complete mutation inventory");
867 for allowance in 0..collection_visits {
868 let mut remaining = allowance;
869 assert!(
870 PythonModuleMutationFacts::collect_bounded(root, multi_source, || {
871 let allowed = remaining > 0;
872 remaining = remaining.saturating_sub(1);
873 allowed
874 })
875 .is_none(),
876 "partial collection at {allowance} must not publish facts"
877 );
878 }
879 for (index, name, expected) in [
880 (0, "A", false),
881 (1, "B", false),
882 (2, "C", false),
883 (3, "ReadOnly", true),
884 (4, "Sibling", false),
885 ] {
886 let reference = arguments.named_child(index).unwrap();
887 let mut read_visits = 0_usize;
888 assert_eq!(
889 Some(expected),
890 mutation.binding_unmutated_at_bounded(root, name, reference, || {
891 read_visits += 1;
892 true
893 }),
894 "all-name inventory: {name}"
895 );
896 assert!(
897 read_visits < collection_visits,
898 "a reused name read must not recollect the module"
899 );
900 assert_eq!(
901 None,
902 mutation.binding_unmutated_at_bounded(root, name, reference, || false),
903 "completed facts cannot override a stopped reader"
904 );
905 for allowance in 0..read_visits {
906 let mut remaining = allowance;
907 assert_eq!(
908 None,
909 mutation.binding_unmutated_at_bounded(root, name, reference, || {
910 let allowed = remaining > 0;
911 remaining = remaining.saturating_sub(1);
912 allowed
913 }),
914 "{name}: stopped read including final check at {allowance}"
915 );
916 }
917 let mut export_visits = 0_usize;
918 assert_eq!(
919 Some(expected),
920 mutation.export_unmutated_bounded(name, || {
921 export_visits += 1;
922 true
923 })
924 );
925 for allowance in 0..export_visits {
926 let mut remaining = allowance;
927 assert_eq!(
928 None,
929 mutation.export_unmutated_bounded(name, || {
930 let allowed = remaining > 0;
931 remaining = remaining.saturating_sub(1);
932 allowed
933 }),
934 "{name}: stopped export including final check at {allowance}"
935 );
936 }
937 }
938
939 for (label, source, container_index, expected, export_expected) in [
940 (
941 "empty inventory still checks the final stop",
942 "class Base: pass\nclass D(Base): pass\n",
943 1,
944 true,
945 true,
946 ),
947 (
948 "later exec is irrelevant to immediate read",
949 bounded_source,
950 1,
951 true,
952 false,
953 ),
954 (
955 "deferred read includes later exec",
956 "class Base: pass\ndef load():\n class D(Base): pass\ndef later():\n exec('Base = object')\n",
957 1,
958 false,
959 false,
960 ),
961 (
962 "later loop hazard can precede next read",
963 "class Base: pass\nfor item in (1, 2):\n class D(Base): pass\n exec('Base = object')\n",
964 1,
965 false,
966 false,
967 ),
968 (
969 "earlier callable remains a possible writer",
970 "class Base: pass\ndef earlier():\n exec('Base = object')\nclass D(Base): pass\n",
971 2,
972 false,
973 false,
974 ),
975 (
976 "later reflective call is irrelevant to immediate read",
977 "class Base: pass\nclass D(Base): pass\nglobals().update(Base=object)\n",
978 1,
979 true,
980 false,
981 ),
982 ] {
983 let tree = parser.parse(source, None).expect("parse hazard ordering");
984 let root = tree.root_node();
985 assert!(!root.has_error(), "{label}");
986 let container = root.named_child(container_index).unwrap();
987 let class = if container.kind() == "class_definition" {
988 container
989 } else {
990 container
991 .child_by_field_name("body")
992 .unwrap()
993 .named_child(0)
994 .unwrap()
995 };
996 assert_eq!(class.kind(), "class_definition");
997 let reference = class
998 .child_by_field_name("superclasses")
999 .unwrap()
1000 .named_child(0)
1001 .unwrap();
1002 let mutation =
1003 PythonModuleMutationFacts::collect_bounded(root, source, || true).unwrap();
1004 let mut read_visits = 0_usize;
1005 assert_eq!(
1006 Some(expected),
1007 mutation.binding_unmutated_at_bounded(root, "Base", reference, || {
1008 read_visits += 1;
1009 true
1010 }),
1011 "{label}"
1012 );
1013 assert_eq!(
1014 Some(export_expected),
1015 mutation.export_unmutated_bounded("Base", || true),
1016 "{label}: completed exports include all hazards"
1017 );
1018 for allowance in 0..read_visits {
1019 let mut remaining = allowance;
1020 assert_eq!(
1021 None,
1022 mutation.binding_unmutated_at_bounded(root, "Base", reference, || {
1023 let allowed = remaining > 0;
1024 remaining = remaining.saturating_sub(1);
1025 allowed
1026 }),
1027 "{label}: recovery/ancestry/final stop at {allowance}"
1028 );
1029 }
1030 }
1031
1032 let original_tree = parser.parse(bounded_source, None).unwrap();
1036 let mutation = PythonModuleMutationFacts::collect_bounded(
1037 original_tree.root_node(),
1038 bounded_source,
1039 || true,
1040 )
1041 .unwrap();
1042 let changed_source = "class Base: pass\nclass D(Base): pass\ndef later():\n longer_call('a longer structured expression at the previous hazard site')\n";
1043 let changed_tree = parser.parse(changed_source, None).unwrap();
1044 let changed_root = changed_tree.root_node();
1045 let changed_reference = changed_root
1046 .named_child(1)
1047 .unwrap()
1048 .child_by_field_name("superclasses")
1049 .unwrap()
1050 .named_child(0)
1051 .unwrap();
1052 assert_eq!(
1053 None,
1054 mutation
1055 .binding_unmutated_at_bounded(changed_root, "Base", changed_reference, || true,)
1056 );
1057
1058 let read_only_initializer = "class Target: pass\nvalue = Target()\ndef read_only():\n global value\n return value\n";
1061 assert_eq!(
1062 None,
1063 initializer(
1064 &module_facts(read_only_initializer),
1065 read_only_initializer,
1066 "value"
1067 )
1068 );
1069 for (label, suffix, expected) in [
1070 (
1071 "read-only directives and unrelated local writer",
1072 "def read_only():\n global Base\n global Base\n return Base\ndef local():\n Base = 0\n",
1073 true,
1074 ),
1075 ("module directive is inert", "", true),
1076 (
1077 "sibling global assignment",
1078 "def read_only():\n global Base\n return Base\ndef change():\n global Base\n Base = 0\n",
1079 false,
1080 ),
1081 (
1082 "pending global class binding",
1083 "class Outer:\n global Base\n class Base(Base):\n pass\n",
1084 false,
1085 ),
1086 (
1087 "global function declaration",
1088 "def change():\n global Base\n def Base():\n pass\n",
1089 false,
1090 ),
1091 (
1092 "global import binding",
1093 "def change():\n global Base\n from elsewhere import Base\n",
1094 false,
1095 ),
1096 (
1097 "global delete binding",
1098 "def change():\n global Base\n del Base\n",
1099 false,
1100 ),
1101 (
1102 "nested directive uses nearest class owner",
1103 "def outer():\n Base = 0\n class ReadOnly:\n global Base\n value = Base\n",
1104 true,
1105 ),
1106 ] {
1107 let source = format!("global Base\nclass Base: pass\nclass D(Base): pass\n{suffix}");
1110 let tree = parser
1111 .parse(&source, None)
1112 .expect("parse global event fixture");
1113 let root = tree.root_node();
1114 assert!(!root.has_error(), "{label}: {source}");
1115 let reference = root
1116 .named_child(2)
1117 .unwrap()
1118 .child_by_field_name("superclasses")
1119 .unwrap()
1120 .named_child(0)
1121 .unwrap();
1122 let mut visits = 0_usize;
1123 assert_eq!(
1124 Some(expected),
1125 python_module_binding_unmutated_at_bounded(
1126 root,
1127 &source,
1128 "Base",
1129 reference,
1130 || {
1131 visits += 1;
1132 true
1133 }
1134 ),
1135 "{label}"
1136 );
1137 assert_eq!(
1138 Some(expected),
1139 python_module_export_unmutated_bounded(root, &source, "Base", || true),
1140 "{label}: complete-module export uses the same event proof"
1141 );
1142 for allowance in 0..visits {
1143 let mut remaining = allowance;
1144 assert_eq!(
1145 None,
1146 python_module_binding_unmutated_at_bounded(
1147 root,
1148 &source,
1149 "Base",
1150 reference,
1151 || {
1152 let allowed = remaining > 0;
1153 remaining = remaining.saturating_sub(1);
1154 allowed
1155 },
1156 ),
1157 "{label}: stop after {allowance} of {visits} visits cannot prove no writer"
1158 );
1159 }
1160 }
1161
1162 let source = "class Target: pass\n\
1163wild = Target()\n\
1164from elsewhere import *\n";
1165 assert_eq!(None, initializer(&module_facts(source), source, "wild"));
1166
1167 let source = "class Target: pass\n\
1168annotated: object = Target()\n\
1169augmented = Target()\n\
1170augmented += Target()\n\
1171deleted = Target()\n\
1172del deleted\n\
1173for loop_target in ():\n pass\n\
1174with manager as with_target:\n pass\n\
1175walrus = None\n\
1176(walrus := Target())\n\
1177import pathlib as imported\n\
1178from os import path as from_imported\n\
1179def declared():\n pass\n\
1180class Declared: pass\n";
1181 let facts = module_facts(source);
1182 for name in [
1183 "annotated",
1184 "augmented",
1185 "deleted",
1186 "loop_target",
1187 "with_target",
1188 "walrus",
1189 "imported",
1190 "from_imported",
1191 "declared",
1192 "Declared",
1193 ] {
1194 assert_eq!(
1195 None,
1196 initializer(&facts, source, name),
1197 "{name}: {facts:#?}"
1198 );
1199 }
1200 }
1201}