1use brokk_bifrost_core::analyzer::Range;
14use brokk_bifrost_core::analyzer::semantic_diagnostics::contains_node as covers;
15use brokk_bifrost_core::analyzer::structural::spec::StructuralSpec;
16use std::collections::{HashMap, HashSet};
17use tree_sitter::Node;
18
19use crate::bindings::{
20 PythonDirectScopeBindingKind, PythonLexicalNameResolution, PythonLexicalScopeInventory,
21 python_direct_scope_bindings_bounded, python_executable_scope_children,
22 python_name_resolution_at,
23};
24use crate::imports::python_import_bindings_from_tree;
25use crate::syntax::{python_parameter_name, python_plain_string_literal};
26
27const PROCESS_INPUT_CONTAINERS: [(&str, &str); 2] = [("os", "environ"), ("sys", "argv")];
35
36#[derive(Clone, Debug, Eq, PartialEq)]
41pub enum PythonRuntimeAccessKey {
42 Property(String),
44 Index(u128),
46 Dynamic,
48 Unsupported,
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum PythonRuntimeRootResolution {
56 ImportedModule,
59 LexicallyBound,
62 Unknown,
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum PythonRuntimeMutationEvidence {
69 NoKnownWrite,
70 KnownWrite,
71 KnownWriteAndUnknownEffects,
72 UnknownEffects,
73}
74
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum PythonRuntimeAccessorCoverage {
79 NoKnownAccessorEffects,
80 UnknownAccessorEffects,
81}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub enum PythonRuntimeExecutionContext {
86 Module,
87 DeferredFunction(Range),
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum PythonRuntimeWriteTarget {
93 Container,
95 Key,
97 ContainerEscape,
100 ModuleEscape,
103}
104
105#[derive(Clone, Debug, Eq, PartialEq)]
107pub struct PythonRuntimeWrite {
108 pub root_name: String,
110 pub container: Option<String>,
113 pub range: Range,
114 pub target: PythonRuntimeWriteTarget,
115}
116
117#[derive(Clone, Debug, Eq, PartialEq)]
119pub struct PythonRuntimeRead {
120 pub binding_name: String,
122 pub root_name: String,
124 pub container: String,
125 pub access: PythonRuntimeAccessKey,
126 pub range: Range,
128 pub root_range: Range,
129 pub container_range: Range,
133 pub candidate_anchor: Range,
134 pub key_range: Range,
135 pub lexical_resolution: PythonRuntimeRootResolution,
136 pub mutation: PythonRuntimeMutationEvidence,
137 pub execution: PythonRuntimeExecutionContext,
138 pub accessor: PythonRuntimeAccessorCoverage,
142}
143
144#[derive(Clone, Debug, Eq, PartialEq)]
146pub struct PythonRuntimeReadFacts {
147 pub reads: Vec<PythonRuntimeRead>,
148 pub writes: Vec<PythonRuntimeWrite>,
149 pub visited_nodes: usize,
151 pub complete: bool,
154}
155
156#[derive(Debug)]
158struct PythonRuntimeBudget {
159 remaining: usize,
160 exhausted: bool,
161 visited_nodes: usize,
162}
163
164impl PythonRuntimeBudget {
165 fn for_facts(max_facts: usize) -> Self {
168 Self {
169 remaining: max_facts.saturating_mul(64).max(4096),
170 exhausted: false,
171 visited_nodes: 0,
172 }
173 }
174
175 fn visit(&mut self) -> bool {
176 if self.remaining == 0 {
177 self.exhausted = true;
178 return false;
179 }
180 self.remaining -= 1;
181 self.visited_nodes += 1;
182 true
183 }
184}
185
186struct ModuleBinding {
188 local_name: String,
189 root_module: String,
191 scope_start_byte: usize,
192 scope_end_byte: usize,
193 function_scoped: bool,
194}
195
196pub fn extract_python_runtime_reads(
202 root: Node<'_>,
203 source: &str,
204 max_facts: usize,
205) -> PythonRuntimeReadFacts {
206 let mut budget = PythonRuntimeBudget::for_facts(max_facts);
207 let bindings = module_bindings(root, source);
208 let mut reads = Vec::new();
209 let mut writes = Vec::new();
210 let mut stack = vec![root];
211 while let Some(node) = stack.pop() {
212 if !budget.visit() {
213 break;
214 }
215 if node.kind() == "attribute"
216 && reads.len() + writes.len() < max_facts
217 && let Some(path) = reviewed_attribute_path(node, source)
218 {
219 let resolution = root_resolution(
220 path.root,
221 &path.root_module,
222 &bindings,
223 root,
224 source,
225 &mut budget,
226 );
227 if resolution == PythonRuntimeRootResolution::ImportedModule {
228 classify_container_occurrence(
229 node,
230 &path,
231 source,
232 root,
233 &mut budget,
234 &mut reads,
235 &mut writes,
236 );
237 }
238 }
239 if node.kind() == "identifier"
240 && writes.len() + reads.len() < max_facts
241 && let Some(module) =
242 escaping_module_identifier(node, &bindings, root, source, &mut budget)
243 {
244 writes.push(PythonRuntimeWrite {
245 root_name: module,
246 container: None,
247 range: node_source_range(node),
248 target: PythonRuntimeWriteTarget::ModuleEscape,
249 });
250 }
251 let mut cursor = node.walk();
252 stack.extend(node.named_children(&mut cursor));
253 }
254 for read in &mut reads {
257 let known_write = writes.iter().any(|write| {
258 write.root_name == read.root_name
259 && write
260 .container
261 .as_ref()
262 .is_none_or(|container| *container == read.container)
263 });
264 read.mutation = match (known_write, read.accessor) {
265 (false, PythonRuntimeAccessorCoverage::NoKnownAccessorEffects) => {
266 PythonRuntimeMutationEvidence::NoKnownWrite
267 }
268 (false, PythonRuntimeAccessorCoverage::UnknownAccessorEffects) => {
269 PythonRuntimeMutationEvidence::UnknownEffects
270 }
271 (true, PythonRuntimeAccessorCoverage::NoKnownAccessorEffects) => {
272 PythonRuntimeMutationEvidence::KnownWrite
273 }
274 (true, PythonRuntimeAccessorCoverage::UnknownAccessorEffects) => {
275 PythonRuntimeMutationEvidence::KnownWriteAndUnknownEffects
276 }
277 };
278 }
279 reads.sort_by_key(|read| read.range.start_byte);
280 writes.sort_by_key(|write| write.range.start_byte);
281 PythonRuntimeReadFacts {
282 complete: !budget.exhausted && reads.len() + writes.len() < max_facts,
283 visited_nodes: budget.visited_nodes,
284 reads,
285 writes,
286 }
287}
288
289pub fn python_runtime_keyed_access_seed<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
297 if node.kind() != "subscript" {
298 return None;
299 }
300 let base = node.child_by_field_name("value")?;
301 if base.kind() != "attribute" {
302 return None;
303 }
304 let attribute = base.child_by_field_name("attribute")?;
305 let object = base.child_by_field_name("object")?;
306 if object.kind() != "identifier" || attribute.kind() != "identifier" {
307 return None;
308 }
309 Some(base)
310}
311
312struct ReviewedPath<'tree> {
314 root: Node<'tree>,
315 root_module: String,
316 container: String,
317}
318
319fn reviewed_attribute_path<'tree>(node: Node<'tree>, source: &str) -> Option<ReviewedPath<'tree>> {
322 let object = node.child_by_field_name("object")?;
323 let attribute = node.child_by_field_name("attribute")?;
324 if object.kind() != "identifier" || attribute.kind() != "identifier" {
325 return None;
326 }
327 let container = node_text(attribute, source);
328 let (root_module, _) = PROCESS_INPUT_CONTAINERS
329 .iter()
330 .find(|(_, reviewed)| *reviewed == container)?;
331 Some(ReviewedPath {
332 root: object,
333 root_module: (*root_module).to_owned(),
334 container: container.to_owned(),
335 })
336}
337
338fn classify_container_occurrence<'tree>(
341 attribute: Node<'tree>,
342 path: &ReviewedPath<'tree>,
343 source: &str,
344 module_root: Node<'tree>,
345 budget: &mut PythonRuntimeBudget,
346 reads: &mut Vec<PythonRuntimeRead>,
347 writes: &mut Vec<PythonRuntimeWrite>,
348) {
349 let write = |target| PythonRuntimeWrite {
350 root_name: path.root_module.clone(),
351 container: Some(path.container.clone()),
352 range: node_source_range(attribute),
353 target,
354 };
355 let Some(parent) = attribute.parent() else {
356 writes.push(write(PythonRuntimeWriteTarget::ContainerEscape));
357 return;
358 };
359 if parent.kind() != "subscript"
360 || parent
361 .child_by_field_name("value")
362 .is_none_or(|value| value.id() != attribute.id())
363 {
364 writes.push(write(if is_binding_target(attribute) {
369 PythonRuntimeWriteTarget::Container
370 } else {
371 PythonRuntimeWriteTarget::ContainerEscape
372 }));
373 return;
374 }
375 if is_binding_target(parent) {
376 writes.push(write(PythonRuntimeWriteTarget::Key));
377 return;
378 }
379 let Some(subscript) = parent.child_by_field_name("subscript") else {
380 writes.push(write(PythonRuntimeWriteTarget::ContainerEscape));
381 return;
382 };
383 let mut cursor = parent.walk();
386 let access = if parent
387 .children(&mut cursor)
388 .any(|child| child.kind() == ",")
389 {
390 PythonRuntimeAccessKey::Unsupported
391 } else {
392 access_key(subscript, source)
393 };
394 let read_range = node_source_range(parent);
395 let execution = match enclosing_execution_context(attribute) {
396 Some(function) => {
397 PythonRuntimeExecutionContext::DeferredFunction(node_source_range(function))
398 }
399 None => PythonRuntimeExecutionContext::Module,
400 };
401 let accessor = execution_context_accessor_coverage(attribute, module_root, read_range, budget);
402 reads.push(PythonRuntimeRead {
403 binding_name: node_text(path.root, source).to_owned(),
404 root_name: path.root_module.clone(),
405 container: path.container.clone(),
406 access,
407 range: read_range,
408 root_range: node_source_range(path.root),
409 container_range: node_source_range(attribute),
410 candidate_anchor: node_source_range(attribute),
411 key_range: node_source_range(subscript),
412 lexical_resolution: PythonRuntimeRootResolution::ImportedModule,
413 mutation: PythonRuntimeMutationEvidence::UnknownEffects,
415 execution,
416 accessor,
417 });
418}
419
420fn access_key(subscript: Node<'_>, source: &str) -> PythonRuntimeAccessKey {
422 if let Some(literal) = python_plain_string_literal(subscript, source) {
423 return PythonRuntimeAccessKey::Property(literal.to_owned());
424 }
425 match subscript.kind() {
426 "integer" => crate::structural::PYTHON_STRUCTURAL_SPEC
427 .integer_literal_value(subscript, source)
428 .and_then(|value| value.decimal)
429 .and_then(|decimal| decimal.parse::<u128>().ok())
430 .map_or(PythonRuntimeAccessKey::Unsupported, |index| {
431 PythonRuntimeAccessKey::Index(index)
432 }),
433 "slice" | "tuple" | "list_splat" => PythonRuntimeAccessKey::Unsupported,
434 "string" | "concatenated_string" => PythonRuntimeAccessKey::Unsupported,
435 _ => PythonRuntimeAccessKey::Dynamic,
436 }
437}
438
439fn is_binding_target(node: Node<'_>) -> bool {
442 let mut current = node;
443 while let Some(parent) = current.parent() {
444 match parent.kind() {
445 "assignment" | "augmented_assignment" | "for_statement" | "for_in_clause" => {
446 return parent
447 .child_by_field_name("left")
448 .is_some_and(|left| left.id() == current.id());
449 }
450 "delete_statement" => return true,
451 "pattern_list" | "tuple_pattern" | "list_pattern" | "list_splat_pattern" => {
452 current = parent;
453 }
454 _ => return false,
455 }
456 }
457 false
458}
459
460pub(crate) fn enclosing_execution_context<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
463 let mut current = node;
464 while let Some(parent) = current.parent() {
465 if matches!(parent.kind(), "function_definition" | "lambda")
466 && parent
467 .child_by_field_name("body")
468 .is_some_and(|body| covers(body, current))
469 {
470 return Some(parent);
471 }
472 current = parent;
473 }
474 None
475}
476
477fn execution_context_accessor_coverage(
484 attribute: Node<'_>,
485 module_root: Node<'_>,
486 read_range: Range,
487 budget: &mut PythonRuntimeBudget,
488) -> PythonRuntimeAccessorCoverage {
489 let scope = enclosing_execution_context(attribute)
490 .and_then(|function| function.child_by_field_name("body"))
491 .unwrap_or(module_root);
492 let mut stack = vec![scope];
493 while let Some(node) = stack.pop() {
494 if !budget.visit() {
495 return PythonRuntimeAccessorCoverage::UnknownAccessorEffects;
496 }
497 if matches!(node.kind(), "function_definition" | "lambda") {
498 let mut cursor = node.walk();
501 stack.extend(node.named_children(&mut cursor).filter(|child| {
502 Some(child.id()) != node.child_by_field_name("body").map(|body| body.id())
503 }));
504 continue;
505 }
506 if node.kind() == "call"
507 && python_hazard_can_precede_bounded(node_source_range(node), read_range, node, || {
508 budget.visit()
509 }) != Some(false)
510 {
511 return PythonRuntimeAccessorCoverage::UnknownAccessorEffects;
512 }
513 let mut cursor = node.walk();
514 stack.extend(node.named_children(&mut cursor));
515 }
516 PythonRuntimeAccessorCoverage::NoKnownAccessorEffects
517}
518
519pub(crate) fn python_hazard_can_precede_bounded(
529 hazard: Range,
530 read_range: Range,
531 hazard_node: Node<'_>,
532 mut scope_step: impl FnMut() -> bool,
533) -> Option<bool> {
534 if !scope_step() {
535 return None;
536 }
537 if hazard.end_byte <= read_range.start_byte {
538 return Some(true);
539 }
540 let mut current = hazard_node;
541 while let Some(parent) = current.parent() {
542 if !scope_step() {
543 return None;
544 }
545 if matches!(parent.kind(), "for_statement" | "while_statement")
546 && parent.start_byte() <= read_range.start_byte
547 && read_range.end_byte <= parent.end_byte()
548 {
549 return Some(true);
550 }
551 current = parent;
552 }
553 Some(false)
554}
555
556fn escaping_module_identifier(
562 node: Node<'_>,
563 bindings: &[ModuleBinding],
564 module_root: Node<'_>,
565 source: &str,
566 budget: &mut PythonRuntimeBudget,
567) -> Option<String> {
568 let name = node_text(node, source);
569 let module = binding_root_module(node, name, bindings)?;
570 if root_resolution(node, &module, bindings, module_root, source, budget)
574 != PythonRuntimeRootResolution::ImportedModule
575 {
576 return None;
577 }
578 let parent = node.parent()?;
579 if parent.kind() == "attribute"
580 && parent
581 .child_by_field_name("object")
582 .is_some_and(|object| object.id() == node.id())
583 {
584 return None;
585 }
586 if matches!(
587 parent.kind(),
588 "import_statement" | "import_from_statement" | "aliased_import" | "dotted_name"
589 ) {
590 return None;
591 }
592 Some(module)
593}
594
595fn binding_root_module(
598 reference: Node<'_>,
599 name: &str,
600 bindings: &[ModuleBinding],
601) -> Option<String> {
602 let offset = reference.start_byte();
603 let mut module = None;
604 for binding in bindings {
605 if binding.local_name != name
606 || offset < binding.scope_start_byte
607 || offset > binding.scope_end_byte
608 {
609 continue;
610 }
611 match &module {
612 None => module = Some(binding.root_module.clone()),
613 Some(existing) if *existing == binding.root_module => {}
614 Some(_) => return None,
615 }
616 }
617 module.filter(|module| {
618 PROCESS_INPUT_CONTAINERS
619 .iter()
620 .any(|(reviewed, _)| reviewed == module)
621 })
622}
623
624fn root_resolution(
631 root: Node<'_>,
632 expected_module: &str,
633 bindings: &[ModuleBinding],
634 module_root: Node<'_>,
635 source: &str,
636 budget: &mut PythonRuntimeBudget,
637) -> PythonRuntimeRootResolution {
638 let name = node_text(root, source);
639 match python_name_resolution_at(name, root, source) {
640 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal => {
641 return if bindings
645 .iter()
646 .any(|binding| binding.local_name == name && binding.function_scoped)
647 {
648 PythonRuntimeRootResolution::Unknown
649 } else {
650 PythonRuntimeRootResolution::LexicallyBound
651 };
652 }
653 PythonLexicalNameResolution::Global | PythonLexicalNameResolution::Unbound => {}
654 }
655 match module_scope_binds_name_through_imports(module_root, name, source, budget) {
656 Some(true) => {}
657 Some(false) => return PythonRuntimeRootResolution::LexicallyBound,
658 None => return PythonRuntimeRootResolution::Unknown,
659 }
660 match binding_root_module(root, name, bindings) {
661 Some(module) if module == expected_module => PythonRuntimeRootResolution::ImportedModule,
662 Some(_) => PythonRuntimeRootResolution::LexicallyBound,
663 None => PythonRuntimeRootResolution::Unknown,
664 }
665}
666
667fn module_scope_binds_name_through_imports(
673 module_root: Node<'_>,
674 name: &str,
675 source: &str,
676 budget: &mut PythonRuntimeBudget,
677) -> Option<bool> {
678 let mut imported = false;
679 let mut stack = vec![module_root];
680 while let Some(node) = stack.pop() {
681 if !budget.visit() {
682 return None;
683 }
684 if node.kind() == "wildcard_import" {
685 return None;
686 }
687 for binding in python_direct_scope_bindings_bounded(node, source, || budget.visit())? {
688 if node_text(binding.declaration, source) != name {
689 continue;
690 }
691 match binding.kind {
692 PythonDirectScopeBindingKind::Import => imported = true,
693 _ => return Some(false),
694 }
695 }
696 stack.extend(python_executable_scope_children(node));
699 }
700 Some(imported)
701}
702
703fn module_bindings(root: Node<'_>, source: &str) -> Vec<ModuleBinding> {
710 let mut bindings = Vec::new();
711 for binding in python_import_bindings_from_tree(root, source) {
712 let Some(root_module) = binding.qualified_name.split('.').next() else {
713 continue;
714 };
715 if !PROCESS_INPUT_CONTAINERS
716 .iter()
717 .any(|(reviewed, _)| *reviewed == root_module)
718 {
719 continue;
720 }
721 if binding.qualified_name != root_module && binding.consumed_attributes == 0 {
723 continue;
724 }
725 bindings.push(ModuleBinding {
726 local_name: binding.local_name.clone(),
727 root_module: root_module.to_owned(),
728 scope_start_byte: binding.scope_start_byte,
729 scope_end_byte: binding.scope_end_byte,
730 function_scoped: binding.is_function_scoped(),
731 });
732 }
733 bindings
734}
735
736use brokk_bifrost_core::analyzer::usages::reference_site::node_range as node_source_range;
737
738fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
739 brokk_bifrost_core::analyzer::common::try_node_source_text(node, source)
740 .expect("a tree-sitter node range is valid UTF-8 source")
741}
742
743#[derive(Clone, Debug, Eq, PartialEq)]
745pub enum PythonModuleMemberMutation {
746 Write(Range),
748 Escape(Range),
751 Budget,
753}
754
755pub fn python_module_member_mutation(
780 root: Node<'_>,
781 source: &str,
782 source_module: &str,
783 module: &str,
784 member: &str,
785 max_nodes: usize,
786) -> Option<PythonModuleMemberMutation> {
787 let mut budget = PythonRuntimeBudget {
788 remaining: max_nodes,
789 exhausted: false,
790 visited_nodes: 0,
791 };
792 let bindings = python_import_bindings_from_tree(root, source);
793 let Some(facts) =
794 python_sys_modules_aliases(root, source, source_module, module, &bindings, &mut budget)
795 else {
796 return Some(PythonModuleMemberMutation::Budget);
797 };
798 let table_aliases = &facts.aliases;
799 let locals: Vec<String> = bindings
800 .iter()
801 .filter(|binding| {
802 binding.qualified_name == module
803 || binding
804 .qualified_name
805 .split('.')
806 .next()
807 .is_some_and(|root_module| {
808 root_module == module && binding.consumed_attributes > 0
809 })
810 })
811 .map(|binding| binding.local_name.clone())
812 .collect();
813 let mut stack = vec![root];
814 while let Some(node) = stack.pop() {
815 if !budget.visit() {
816 return Some(PythonModuleMemberMutation::Budget);
817 }
818 if node.kind() == "call"
819 && python_mock_patch_may_replace_module_member(node, source, &bindings, module, member)
820 {
821 return Some(PythonModuleMemberMutation::Write(node_source_range(node)));
822 }
823 if node.kind() == "identifier" {
824 let name = node_text(node, source);
825 if let Some(table) = python_sys_modules_table(node, source) {
826 if python_imported_path(table, source, &bindings)
827 .is_some_and(|path| path == ["sys", "modules"])
828 {
829 match python_sys_modules_lookup(
830 table,
831 source,
832 source_module,
833 module,
834 &bindings,
835 &facts,
836 &mut budget,
837 ) {
838 PythonSysModulesLookup::Unrelated => {}
839 PythonSysModulesLookup::Escape(range) => {
840 return Some(PythonModuleMemberMutation::Escape(range));
841 }
842 PythonSysModulesLookup::Target(read) => {
843 if let Some(mutation) =
844 python_sys_modules_read_use(read, source, member, table_aliases)
845 {
846 return Some(mutation);
847 }
848 }
849 }
850 } else {
851 return Some(PythonModuleMemberMutation::Escape(node_source_range(node)));
852 }
853 }
854 let table_alias = table_aliases.contains(name);
855 if table_alias && python_module_table_alias_binding_use(node, source, table_aliases) {
856 } else if (table_alias
858 || (locals.iter().any(|local| local == name)
859 && !matches!(
860 python_name_resolution_at(name, node, source),
861 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal
862 )))
863 && let Some(mutation) =
864 module_binding_use(node, source, &bindings, table_aliases, module, member)
865 {
866 return Some(mutation);
867 }
868 }
869 let mut cursor = node.walk();
870 stack.extend(node.named_children(&mut cursor));
871 }
872 None
873}
874
875enum PythonSysModulesLookup<'tree> {
876 Unrelated,
877 Target(Node<'tree>),
878 Escape(Range),
879}
880
881fn python_sys_modules_table<'tree>(node: Node<'tree>, source: &str) -> Option<Node<'tree>> {
883 let table = node.parent()?;
884 (table.kind() == "attribute"
885 && table
886 .child_by_field_name("object")
887 .is_some_and(|object| object.id() == node.id())
888 && table
889 .child_by_field_name("attribute")
890 .is_some_and(|attribute| node_text(attribute, source) == "modules"))
891 .then_some(table)
892}
893
894fn python_sys_modules_read_table<'tree>(read: Node<'tree>, source: &str) -> Option<Node<'tree>> {
895 if read.kind() == "subscript" {
896 let table = read.child_by_field_name("value")?;
897 return (table.kind() == "attribute"
898 && table
899 .child_by_field_name("attribute")
900 .is_some_and(|attribute| node_text(attribute, source) == "modules"))
901 .then_some(table);
902 }
903 if read.kind() == "call" {
904 let function = read.child_by_field_name("function")?;
905 if function.kind() != "attribute"
906 || function
907 .child_by_field_name("attribute")
908 .is_none_or(|attribute| {
909 !matches!(node_text(attribute, source), "get" | "pop" | "setdefault")
910 })
911 {
912 return None;
913 }
914 let table = function.child_by_field_name("object")?;
915 return (table.kind() == "attribute"
916 && table
917 .child_by_field_name("attribute")
918 .is_some_and(|attribute| node_text(attribute, source) == "modules"))
919 .then_some(table);
920 }
921 None
922}
923
924fn python_sys_modules_lookup<'tree>(
925 table: Node<'tree>,
926 source: &str,
927 source_module: &str,
928 module: &str,
929 bindings: &[crate::imports::PythonImportBinding],
930 facts: &PythonSysModulesFacts<'tree>,
931 budget: &mut PythonRuntimeBudget,
932) -> PythonSysModulesLookup<'tree> {
933 let escaped = || PythonSysModulesLookup::Escape(node_source_range(table));
937 if python_imported_path(table, source, bindings).is_none_or(|path| path != ["sys", "modules"]) {
938 return escaped();
939 }
940 let Some(parent) = table.parent() else {
941 return escaped();
942 };
943 let proof = PythonSysModulesProofContext {
944 source,
945 source_module,
946 module,
947 bindings,
948 facts,
949 };
950 if parent.kind() == "subscript"
951 && parent
952 .child_by_field_name("value")
953 .is_some_and(|value| value.id() == table.id())
954 {
955 if is_binding_target(parent) {
956 if parent
957 .parent()
958 .is_some_and(|parent| parent.kind() == "delete_statement")
959 {
960 return PythonSysModulesLookup::Unrelated;
961 }
962 let Some(value) = parent
963 .parent()
964 .filter(|assignment| assignment.kind() == "assignment")
965 .and_then(|assignment| assignment.child_by_field_name("right"))
966 else {
967 return escaped();
968 };
969 return match python_sys_modules_stored_value(
970 value,
971 &proof,
972 budget,
973 &mut HashSet::default(),
974 ) {
975 PythonSysModulesStoredValue::Unrelated => PythonSysModulesLookup::Unrelated,
976 PythonSysModulesStoredValue::Target | PythonSysModulesStoredValue::Unknown => {
977 escaped()
978 }
979 };
980 }
981 return python_sys_modules_read_identity(
982 parent.child_by_field_name("subscript"),
983 parent,
984 &proof,
985 budget,
986 );
987 }
988 if parent.kind() == "attribute"
989 && parent
990 .child_by_field_name("object")
991 .is_some_and(|object| object.id() == table.id())
992 {
993 let Some(call) = parent.parent().filter(|call| {
994 call.kind() == "call"
995 && call
996 .child_by_field_name("function")
997 .is_some_and(|function| function.id() == parent.id())
998 }) else {
999 return escaped();
1000 };
1001 let method = parent
1002 .child_by_field_name("attribute")
1003 .map(|attribute| node_text(attribute, source));
1004 match method {
1005 Some("get" | "pop") => {
1006 let args = python_sys_modules_arguments(call);
1007 return python_sys_modules_read_identity(
1008 args.first().copied(),
1009 call,
1010 &proof,
1011 budget,
1012 );
1013 }
1014 Some("setdefault") => {
1015 let args = python_sys_modules_arguments(call);
1016 let default = args
1017 .get(1)
1018 .copied()
1019 .or_else(|| python_sys_modules_keyword_argument(call, source, "default"));
1020 if let Some(default) = default
1021 && python_sys_modules_stored_value(
1022 default,
1023 &proof,
1024 budget,
1025 &mut HashSet::default(),
1026 ) != PythonSysModulesStoredValue::Unrelated
1027 {
1028 return escaped();
1029 }
1030 return python_sys_modules_read_identity(
1031 args.first().copied(),
1032 call,
1033 &proof,
1034 budget,
1035 );
1036 }
1037 Some("update") => {
1038 let Some(arguments) = call.child_by_field_name("arguments") else {
1039 return escaped();
1040 };
1041 let mut cursor = arguments.walk();
1042 for argument in arguments.named_children(&mut cursor) {
1043 let identity = if argument.kind() == "keyword_argument" {
1044 argument
1045 .child_by_field_name("value")
1046 .map(|value| {
1047 python_sys_modules_stored_value(
1048 value,
1049 &proof,
1050 budget,
1051 &mut HashSet::default(),
1052 )
1053 })
1054 .unwrap_or(PythonSysModulesStoredValue::Unknown)
1055 } else if argument.kind() == "dictionary_splat" {
1056 PythonSysModulesStoredValue::Unknown
1057 } else {
1058 python_sys_modules_update_values(
1059 argument,
1060 &proof,
1061 budget,
1062 &mut HashSet::default(),
1063 )
1064 };
1065 if identity != PythonSysModulesStoredValue::Unrelated {
1066 return escaped();
1067 }
1068 }
1069 return PythonSysModulesLookup::Unrelated;
1070 }
1071 Some("clear" | "keys") => return PythonSysModulesLookup::Unrelated,
1072 _ => return escaped(),
1073 }
1074 }
1075 escaped()
1076}
1077
1078fn python_sys_modules_arguments(call: Node<'_>) -> Vec<Node<'_>> {
1079 let Some(arguments) = call.child_by_field_name("arguments") else {
1080 return Vec::new();
1081 };
1082 let mut cursor = arguments.walk();
1083 arguments
1084 .named_children(&mut cursor)
1085 .filter(|argument| argument.kind() != "keyword_argument")
1086 .collect()
1087}
1088
1089fn python_sys_modules_keyword_argument<'tree>(
1090 call: Node<'tree>,
1091 source: &str,
1092 name: &str,
1093) -> Option<Node<'tree>> {
1094 let arguments = call.child_by_field_name("arguments")?;
1095 let mut cursor = arguments.walk();
1096 arguments
1097 .named_children(&mut cursor)
1098 .find(|argument| {
1099 argument.kind() == "keyword_argument"
1100 && argument
1101 .child_by_field_name("name")
1102 .is_some_and(|key| node_text(key, source) == name)
1103 })
1104 .and_then(|argument| argument.child_by_field_name("value"))
1105}
1106
1107fn python_sys_modules_read_identity<'tree>(
1108 key: Option<Node<'tree>>,
1109 read: Node<'tree>,
1110 proof: &PythonSysModulesProofContext<'_, 'tree>,
1111 budget: &mut PythonRuntimeBudget,
1112) -> PythonSysModulesLookup<'tree> {
1113 let Some(key) = key else {
1114 return PythonSysModulesLookup::Escape(node_source_range(read));
1115 };
1116 if key.kind() == "identifier" && node_text(key, proof.source) == "__name__" {
1117 return if proof.source_module == proof.module {
1118 PythonSysModulesLookup::Target(read)
1119 } else {
1120 PythonSysModulesLookup::Unrelated
1121 };
1122 }
1123 let Some(key_value) = crate::decode_python_string_value(key, proof.source).ok() else {
1124 if python_sys_modules_guard_excludes_module(key, read, proof, budget) {
1125 return PythonSysModulesLookup::Unrelated;
1126 }
1127 return PythonSysModulesLookup::Target(read);
1128 };
1129 if key_value == proof.module {
1130 PythonSysModulesLookup::Target(read)
1131 } else {
1132 PythonSysModulesLookup::Unrelated
1133 }
1134}
1135
1136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1137enum PythonGuardTruth {
1138 True,
1139 False,
1140 Unknown,
1141}
1142
1143fn python_sys_modules_guard_excludes_module(
1146 key: Node<'_>,
1147 read: Node<'_>,
1148 proof: &PythonSysModulesProofContext<'_, '_>,
1149 budget: &mut PythonRuntimeBudget,
1150) -> bool {
1151 if key.kind() != "identifier" {
1152 return false;
1153 }
1154 let key_name = node_text(key, proof.source);
1155 let mut child = read;
1156 let mut ancestor = read.parent();
1157 while let Some(node) = ancestor {
1158 if !budget.visit() {
1159 return false;
1160 }
1161 if matches!(node.kind(), "if_statement" | "elif_clause")
1162 && let (Some(condition), Some(consequence)) = (
1163 node.child_by_field_name("condition"),
1164 node.child_by_field_name("consequence"),
1165 )
1166 && consequence.start_byte() <= read.start_byte()
1167 && read.end_byte() <= consequence.end_byte()
1168 && python_sys_modules_key_binding_unchanged(key, condition, read, proof, budget)
1169 && python_sys_modules_guard_truth(
1170 condition,
1171 key_name,
1172 proof.module,
1173 proof.source,
1174 budget,
1175 ) == PythonGuardTruth::False
1176 {
1177 return true;
1178 }
1179 if node.kind() == "block"
1180 && let Some(statement) = python_sys_modules_direct_child_containing(node, child)
1181 && python_sys_modules_prior_continue_guard_excludes(
1182 node, statement, key, read, key_name, proof, budget,
1183 )
1184 {
1185 return true;
1186 }
1187 child = node;
1188 ancestor = node.parent();
1189 }
1190 false
1191}
1192
1193fn python_sys_modules_key_binding_unchanged(
1198 key: Node<'_>,
1199 guard: Node<'_>,
1200 read: Node<'_>,
1201 proof: &PythonSysModulesProofContext<'_, '_>,
1202 budget: &mut PythonRuntimeBudget,
1203) -> bool {
1204 let name = node_text(key, proof.source);
1205 let guard_reference = python_sys_modules_guard_key_reference(guard, name, proof.source, budget);
1206 let Some(guard_reference) = guard_reference else {
1207 return false;
1208 };
1209 let scope = python_alias_scope(key);
1210 let key_resolution = python_name_resolution_at(name, key, proof.source);
1211 let guard_resolution = python_name_resolution_at(name, guard_reference, proof.source);
1212 let same_lexical_binding = key_resolution == guard_resolution
1213 && (key_resolution == PythonLexicalNameResolution::Local
1214 || (scope.kind() == "module"
1215 && key_resolution == PythonLexicalNameResolution::Unbound));
1216 if python_alias_scope(guard_reference).id() != scope.id()
1217 || python_alias_scope(read).id() != scope.id()
1218 || !same_lexical_binding
1219 {
1220 return false;
1221 }
1222 if !python_sys_modules_has_one_binding(proof, scope, name) {
1223 return false;
1224 }
1225 let start = guard.start_byte();
1226 let guard_end = guard.end_byte();
1227 let end = read.start_byte();
1228 let mut stack = vec![scope.child_by_field_name("body").unwrap_or(scope)];
1229 while let Some(node) = stack.pop() {
1230 if !budget.visit() {
1231 return false;
1232 }
1233 if node.start_byte() >= end || node.end_byte() <= start {
1234 continue;
1235 }
1236 if python_direct_scope_bindings_bounded(node, proof.source, || budget.visit()).is_none_or(
1237 |bindings| {
1238 bindings.into_iter().any(|binding| {
1239 node_text(binding.declaration, proof.source) == name
1240 && start <= binding.declaration.start_byte()
1241 && binding.declaration.start_byte() < end
1242 })
1243 },
1244 ) {
1245 return false;
1246 }
1247 let mut cursor = node.walk();
1248 for child in node.named_children(&mut cursor) {
1249 if matches!(child.kind(), "call")
1250 && guard_end <= child.start_byte()
1251 && child.end_byte() < end
1252 {
1253 return false;
1254 }
1255 }
1256 stack.extend(python_executable_scope_children(node));
1257 }
1258 true
1259}
1260
1261fn python_sys_modules_guard_key_reference<'tree>(
1262 condition: Node<'tree>,
1263 name: &str,
1264 source: &str,
1265 budget: &mut PythonRuntimeBudget,
1266) -> Option<Node<'tree>> {
1267 let mut stack = vec![condition];
1268 while let Some(node) = stack.pop() {
1269 if !budget.visit() {
1270 return None;
1271 }
1272 if node.kind() == "identifier" && node_text(node, source) == name {
1273 return Some(node);
1274 }
1275 let mut cursor = node.walk();
1276 stack.extend(node.named_children(&mut cursor));
1277 }
1278 None
1279}
1280
1281fn python_sys_modules_direct_child_containing<'tree>(
1282 block: Node<'tree>,
1283 child: Node<'tree>,
1284) -> Option<Node<'tree>> {
1285 let mut direct = child;
1286 while direct
1287 .parent()
1288 .is_some_and(|parent| parent.id() != block.id())
1289 {
1290 direct = direct.parent()?;
1291 }
1292 direct
1293 .parent()
1294 .is_some_and(|parent| parent.id() == block.id())
1295 .then_some(direct)
1296}
1297
1298fn python_sys_modules_prior_continue_guard_excludes(
1299 block: Node<'_>,
1300 read_statement: Node<'_>,
1301 key: Node<'_>,
1302 read: Node<'_>,
1303 key_name: &str,
1304 proof: &PythonSysModulesProofContext<'_, '_>,
1305 budget: &mut PythonRuntimeBudget,
1306) -> bool {
1307 let mut cursor = block.walk();
1308 for statement in block.named_children(&mut cursor) {
1309 if statement.id() == read_statement.id() {
1310 break;
1311 }
1312 if statement.kind() != "if_statement" {
1313 continue;
1314 }
1315 let Some(condition) = statement.child_by_field_name("condition") else {
1316 continue;
1317 };
1318 if condition.kind() != "not_operator"
1319 || !python_sys_modules_guard_has_direct_abrupt_consequence(statement)
1320 || !python_sys_modules_key_binding_unchanged(key, condition, read, proof, budget)
1321 {
1322 continue;
1323 }
1324 let Some(guard_condition) = condition.named_child(0) else {
1325 continue;
1326 };
1327 if python_sys_modules_guard_truth(
1328 guard_condition,
1329 key_name,
1330 proof.module,
1331 proof.source,
1332 budget,
1333 ) == PythonGuardTruth::False
1334 {
1335 return true;
1336 }
1337 }
1338 false
1339}
1340
1341fn python_sys_modules_guard_has_direct_abrupt_consequence(statement: Node<'_>) -> bool {
1342 let Some(consequence) = statement.child_by_field_name("consequence") else {
1343 return false;
1344 };
1345 let mut cursor = consequence.walk();
1346 let mut statements = consequence.named_children(&mut cursor);
1347 let Some(only) = statements.next() else {
1348 return false;
1349 };
1350 statements.next().is_none()
1351 && matches!(
1352 only.kind(),
1353 "break_statement" | "continue_statement" | "return_statement" | "raise_statement"
1354 )
1355}
1356
1357fn python_sys_modules_guard_truth(
1358 condition: Node<'_>,
1359 key_name: &str,
1360 module: &str,
1361 source: &str,
1362 budget: &mut PythonRuntimeBudget,
1363) -> PythonGuardTruth {
1364 let mut pending = vec![(condition, false)];
1365 let mut results = HashMap::<usize, PythonGuardTruth>::default();
1366 while let Some((node, visited)) = pending.pop() {
1367 if !budget.visit() {
1368 return PythonGuardTruth::Unknown;
1369 }
1370 if !visited
1371 && matches!(
1372 node.kind(),
1373 "boolean_operator" | "not_operator" | "parenthesized_expression"
1374 )
1375 {
1376 pending.push((node, true));
1377 let mut cursor = node.walk();
1378 for child in node.named_children(&mut cursor) {
1379 pending.push((child, false));
1380 }
1381 continue;
1382 }
1383 let value = match node.kind() {
1384 "boolean_operator" => {
1385 let Some(left) = node.child_by_field_name("left") else {
1386 return PythonGuardTruth::Unknown;
1387 };
1388 let Some(right) = node.child_by_field_name("right") else {
1389 return PythonGuardTruth::Unknown;
1390 };
1391 let Some(operator) = node.child_by_field_name("operator") else {
1392 return PythonGuardTruth::Unknown;
1393 };
1394 let (Some(left), Some(right)) = (results.get(&left.id()), results.get(&right.id()))
1395 else {
1396 return PythonGuardTruth::Unknown;
1397 };
1398 match node_text(operator, source) {
1399 "and" => python_guard_and(*left, *right),
1400 "or" => python_guard_or(*left, *right),
1401 _ => PythonGuardTruth::Unknown,
1402 }
1403 }
1404 "not_operator" => node
1405 .named_child(0)
1406 .and_then(|child| results.get(&child.id()).copied())
1407 .map(python_guard_not)
1408 .unwrap_or(PythonGuardTruth::Unknown),
1409 "parenthesized_expression" => node
1410 .named_child(0)
1411 .and_then(|child| results.get(&child.id()).copied())
1412 .unwrap_or(PythonGuardTruth::Unknown),
1413 _ => python_sys_modules_guard_atom(node, key_name, module, source),
1414 };
1415 results.insert(node.id(), value);
1416 }
1417 results
1418 .get(&condition.id())
1419 .copied()
1420 .unwrap_or(PythonGuardTruth::Unknown)
1421}
1422
1423fn python_guard_and(left: PythonGuardTruth, right: PythonGuardTruth) -> PythonGuardTruth {
1424 match (left, right) {
1425 (PythonGuardTruth::False, _) | (_, PythonGuardTruth::False) => PythonGuardTruth::False,
1426 (PythonGuardTruth::True, PythonGuardTruth::True) => PythonGuardTruth::True,
1427 _ => PythonGuardTruth::Unknown,
1428 }
1429}
1430
1431fn python_guard_or(left: PythonGuardTruth, right: PythonGuardTruth) -> PythonGuardTruth {
1432 match (left, right) {
1433 (PythonGuardTruth::True, _) | (_, PythonGuardTruth::True) => PythonGuardTruth::True,
1434 (PythonGuardTruth::False, PythonGuardTruth::False) => PythonGuardTruth::False,
1435 _ => PythonGuardTruth::Unknown,
1436 }
1437}
1438
1439fn python_guard_not(value: PythonGuardTruth) -> PythonGuardTruth {
1440 match value {
1441 PythonGuardTruth::True => PythonGuardTruth::False,
1442 PythonGuardTruth::False => PythonGuardTruth::True,
1443 PythonGuardTruth::Unknown => PythonGuardTruth::Unknown,
1444 }
1445}
1446
1447fn python_sys_modules_guard_atom(
1448 node: Node<'_>,
1449 key_name: &str,
1450 module: &str,
1451 source: &str,
1452) -> PythonGuardTruth {
1453 if node.kind() == "comparison_operator" {
1454 return python_sys_modules_guard_comparison(node, key_name, module, source);
1455 }
1456 if node.kind() == "call" {
1457 return python_sys_modules_guard_string_method(node, key_name, module, source);
1458 }
1459 PythonGuardTruth::Unknown
1460}
1461
1462fn python_sys_modules_guard_comparison(
1463 node: Node<'_>,
1464 key_name: &str,
1465 module: &str,
1466 source: &str,
1467) -> PythonGuardTruth {
1468 let mut cursor = node.walk();
1469 let operands = node.named_children(&mut cursor).collect::<Vec<_>>();
1470 let mut cursor = node.walk();
1471 let operators = node
1472 .children_by_field_name("operators", &mut cursor)
1473 .collect::<Vec<_>>();
1474 let ([left, right], [operator]) = (operands.as_slice(), operators.as_slice()) else {
1475 return PythonGuardTruth::Unknown;
1476 };
1477 let operator = node_text(*operator, source);
1478 let key_left = left.kind() == "identifier" && node_text(*left, source) == key_name;
1479 let key_right = right.kind() == "identifier" && node_text(*right, source) == key_name;
1480 if key_left || key_right {
1481 let literal = if key_left {
1482 crate::decode_python_string_value(*right, source)
1483 } else {
1484 crate::decode_python_string_value(*left, source)
1485 };
1486 if matches!(operator, "==" | "!=")
1487 && let Ok(literal) = literal
1488 {
1489 let matches = literal == module;
1490 return python_guard_bool(if operator == "==" { matches } else { !matches });
1491 }
1492 if operator == "in" && key_left {
1493 return python_sys_modules_literal_membership(*right, module, source);
1494 }
1495 if operator == "in"
1496 && key_right
1497 && let Ok(literal) = literal
1498 {
1499 return python_guard_bool(module.contains(&literal));
1500 }
1501 }
1502 PythonGuardTruth::Unknown
1503}
1504
1505fn python_sys_modules_literal_membership(
1506 collection: Node<'_>,
1507 module: &str,
1508 source: &str,
1509) -> PythonGuardTruth {
1510 if !matches!(collection.kind(), "tuple" | "list" | "set") {
1511 return PythonGuardTruth::Unknown;
1512 }
1513 let mut cursor = collection.walk();
1514 let mut contains = false;
1515 for value in collection.named_children(&mut cursor) {
1516 let Ok(literal) = crate::decode_python_string_value(value, source) else {
1517 return PythonGuardTruth::Unknown;
1518 };
1519 contains |= literal == module;
1520 }
1521 python_guard_bool(contains)
1522}
1523
1524fn python_sys_modules_guard_string_method(
1525 call: Node<'_>,
1526 key_name: &str,
1527 module: &str,
1528 source: &str,
1529) -> PythonGuardTruth {
1530 let Some(function) = call.child_by_field_name("function") else {
1531 return PythonGuardTruth::Unknown;
1532 };
1533 if function.kind() != "attribute"
1534 || !function
1535 .child_by_field_name("object")
1536 .is_some_and(|object| {
1537 object.kind() == "identifier" && node_text(object, source) == key_name
1538 })
1539 {
1540 return PythonGuardTruth::Unknown;
1541 }
1542 let Some(method) = function
1543 .child_by_field_name("attribute")
1544 .map(|name| node_text(name, source))
1545 else {
1546 return PythonGuardTruth::Unknown;
1547 };
1548 let Some(arguments) = call.child_by_field_name("arguments") else {
1549 return PythonGuardTruth::Unknown;
1550 };
1551 let mut cursor = arguments.walk();
1552 let mut values = arguments.named_children(&mut cursor);
1553 let Some(first) = values.next() else {
1554 return PythonGuardTruth::Unknown;
1555 };
1556 if values.next().is_some() || first.kind() == "keyword_argument" {
1557 return PythonGuardTruth::Unknown;
1558 }
1559 let matches = if let Ok(literal) = crate::decode_python_string_value(first, source) {
1560 match method {
1561 "startswith" => module.starts_with(&literal),
1562 "endswith" => module.ends_with(&literal),
1563 _ => return PythonGuardTruth::Unknown,
1564 }
1565 } else if matches!(first.kind(), "tuple" | "list" | "set") {
1566 let mut cursor = first.walk();
1567 let mut matched = false;
1568 for value in first.named_children(&mut cursor) {
1569 let Ok(literal) = crate::decode_python_string_value(value, source) else {
1570 return PythonGuardTruth::Unknown;
1571 };
1572 matched |= match method {
1573 "startswith" => module.starts_with(&literal),
1574 "endswith" => module.ends_with(&literal),
1575 _ => return PythonGuardTruth::Unknown,
1576 };
1577 }
1578 matched
1579 } else {
1580 return PythonGuardTruth::Unknown;
1581 };
1582 python_guard_bool(matches)
1583}
1584
1585fn python_guard_bool(value: bool) -> PythonGuardTruth {
1586 if value {
1587 PythonGuardTruth::True
1588 } else {
1589 PythonGuardTruth::False
1590 }
1591}
1592
1593#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1594enum PythonSysModulesStoredValue {
1595 Target,
1596 Unrelated,
1597 Unknown,
1598}
1599
1600struct PythonSysModulesFacts<'tree> {
1601 aliases: HashSet<String>,
1602 assignments: HashMap<(usize, usize, String), Vec<Node<'tree>>>,
1603 binding_counts: HashMap<(usize, usize, String), usize>,
1604 classes: Vec<PythonExternalClassAlias>,
1605}
1606
1607struct PythonSysModulesProofContext<'a, 'tree> {
1608 source: &'a str,
1609 source_module: &'a str,
1610 module: &'a str,
1611 bindings: &'a [crate::imports::PythonImportBinding],
1612 facts: &'a PythonSysModulesFacts<'tree>,
1613}
1614
1615fn python_sys_modules_has_one_binding(
1616 proof: &PythonSysModulesProofContext<'_, '_>,
1617 scope: Node<'_>,
1618 name: &str,
1619) -> bool {
1620 let count = proof
1621 .facts
1622 .binding_counts
1623 .get(&(scope.start_byte(), scope.end_byte(), name.to_owned()))
1624 .copied()
1625 .unwrap_or_default()
1626 + proof
1627 .bindings
1628 .iter()
1629 .filter(|binding| {
1630 binding.local_name == name
1631 && binding.scope_start_byte == scope.start_byte()
1632 && binding.scope_end_byte == scope.end_byte()
1633 })
1634 .count();
1635 count == 1
1636}
1637
1638fn python_sys_modules_value_scope<'tree>(node: Node<'tree>, source: &str) -> Node<'tree> {
1639 let nearest = python_alias_scope(node);
1640 let name = python_static_path(node, source)
1641 .and_then(|(root, _)| (root.kind() == "identifier").then_some(node_text(root, source)))
1642 .unwrap_or_default();
1643 match python_name_resolution_at(name, node, source) {
1644 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal => nearest,
1645 PythonLexicalNameResolution::Global => {
1646 let mut scope = nearest;
1647 while scope.kind() != "module" {
1648 scope = python_alias_scope(scope.parent().expect("lexical scope has a parent"));
1649 }
1650 scope
1651 }
1652 PythonLexicalNameResolution::Unbound if nearest.kind() == "class_definition" => nearest,
1653 PythonLexicalNameResolution::Unbound => {
1654 let mut scope = nearest;
1655 while scope.kind() != "module" {
1656 scope = python_alias_scope(scope.parent().expect("lexical scope has a parent"));
1657 }
1658 scope
1659 }
1660 }
1661}
1662
1663fn python_sys_modules_stored_value<'tree>(
1664 value: Node<'tree>,
1665 context: &PythonSysModulesProofContext<'_, 'tree>,
1666 budget: &mut PythonRuntimeBudget,
1667 seen: &mut HashSet<(usize, usize, String)>,
1668) -> PythonSysModulesStoredValue {
1669 let source = context.source;
1670 let module = context.module;
1671 let bindings = context.bindings;
1672 let facts = context.facts;
1673 if !budget.visit() {
1674 return PythonSysModulesStoredValue::Unknown;
1675 }
1676 if python_sys_modules_table(value, source).is_some() {
1677 return PythonSysModulesStoredValue::Unknown;
1678 }
1679 if let Some(table) = python_sys_modules_read_table(value, source) {
1680 if python_imported_path(table, source, bindings)
1681 .is_none_or(|path| path != ["sys", "modules"])
1682 {
1683 return PythonSysModulesStoredValue::Unknown;
1684 }
1685 return match python_sys_modules_read_identity(
1686 if value.kind() == "subscript" {
1687 value.child_by_field_name("subscript")
1688 } else {
1689 python_sys_modules_arguments(value).first().copied()
1690 },
1691 value,
1692 context,
1693 budget,
1694 ) {
1695 PythonSysModulesLookup::Target(_) => PythonSysModulesStoredValue::Target,
1696 PythonSysModulesLookup::Unrelated => PythonSysModulesStoredValue::Unrelated,
1697 PythonSysModulesLookup::Escape(_) => PythonSysModulesStoredValue::Unknown,
1698 };
1699 }
1700 if matches!(
1701 value.kind(),
1702 "string"
1703 | "concatenated_string"
1704 | "integer"
1705 | "float"
1706 | "true"
1707 | "false"
1708 | "none"
1709 | "list"
1710 | "tuple"
1711 | "set"
1712 | "dictionary"
1713 | "list_comprehension"
1714 | "set_comprehension"
1715 | "dictionary_comprehension"
1716 | "generator_expression"
1717 ) {
1718 return PythonSysModulesStoredValue::Unrelated;
1719 }
1720 if let Some(path) = python_imported_path(value, source, bindings) {
1721 let root_name = python_static_path(value, source)
1722 .map(|(root, _)| node_text(root, source))
1723 .unwrap_or_default();
1724 let scope = python_sys_modules_value_scope(value, source);
1725 if facts.binding_counts.contains_key(&(
1726 scope.start_byte(),
1727 scope.end_byte(),
1728 root_name.to_owned(),
1729 )) {
1730 return PythonSysModulesStoredValue::Unknown;
1731 }
1732 return if path.join(".") == module {
1733 PythonSysModulesStoredValue::Target
1734 } else {
1735 PythonSysModulesStoredValue::Unrelated
1736 };
1737 }
1738 if value.kind() == "call" {
1739 let Some(function) = value.child_by_field_name("function") else {
1740 return PythonSysModulesStoredValue::Unknown;
1741 };
1742 if python_imported_path(function, source, bindings)
1743 .is_some_and(|path| path == ["types", "ModuleType"])
1744 {
1745 return PythonSysModulesStoredValue::Unrelated;
1746 }
1747 if let Some((root, path)) = python_static_path(function, source)
1748 && path.len() == 1
1749 && python_class_alias_visible_at(node_text(root, source), root, source, &facts.classes)
1750 {
1751 return PythonSysModulesStoredValue::Unrelated;
1752 }
1753 return PythonSysModulesStoredValue::Unknown;
1754 }
1755 let Some((root, path)) = python_static_path(value, source) else {
1756 return PythonSysModulesStoredValue::Unknown;
1757 };
1758 if path.len() == 1 && facts.aliases.contains(node_text(root, source)) {
1759 return PythonSysModulesStoredValue::Target;
1760 }
1761 if path.len() == 1
1762 && python_class_alias_visible_at(node_text(root, source), root, source, &facts.classes)
1763 {
1764 return PythonSysModulesStoredValue::Unrelated;
1765 }
1766 let scope = python_sys_modules_value_scope(value, source);
1767 if scope.kind() == "function_definition"
1768 && scope
1769 .child_by_field_name("parameters")
1770 .is_some_and(|parameters| {
1771 let mut cursor = parameters.walk();
1772 parameters
1773 .named_children(&mut cursor)
1774 .filter_map(|parameter| python_parameter_name(parameter, source))
1775 .any(|parameter| parameter == node_text(root, source))
1776 })
1777 {
1778 return PythonSysModulesStoredValue::Unknown;
1779 }
1780 if !python_sys_modules_has_one_binding(context, scope, node_text(root, source)) {
1781 return PythonSysModulesStoredValue::Unknown;
1782 }
1783 let key = (scope.start_byte(), scope.end_byte(), path.join("."));
1784 if !seen.insert(key.clone()) {
1785 return PythonSysModulesStoredValue::Unknown;
1786 }
1787 let Some([assignment]) = facts.assignments.get(&key).map(Vec::as_slice) else {
1788 return PythonSysModulesStoredValue::Unknown;
1789 };
1790 python_sys_modules_stored_value(*assignment, context, budget, seen)
1791}
1792
1793fn python_sys_modules_update_values<'tree>(
1794 mapping: Node<'tree>,
1795 context: &PythonSysModulesProofContext<'_, 'tree>,
1796 budget: &mut PythonRuntimeBudget,
1797 seen: &mut HashSet<(usize, usize, String)>,
1798) -> PythonSysModulesStoredValue {
1799 let source = context.source;
1800 let facts = context.facts;
1801 if mapping.kind() == "dictionary" {
1802 let mut cursor = mapping.walk();
1803 for pair in mapping.named_children(&mut cursor) {
1804 let Some(value) = pair.child_by_field_name("value") else {
1805 return PythonSysModulesStoredValue::Unknown;
1806 };
1807 let identity = python_sys_modules_stored_value(value, context, budget, seen);
1808 if identity != PythonSysModulesStoredValue::Unrelated {
1809 return identity;
1810 }
1811 }
1812 return PythonSysModulesStoredValue::Unrelated;
1813 }
1814 let Some((root, path)) = python_static_path(mapping, source) else {
1815 return PythonSysModulesStoredValue::Unknown;
1816 };
1817 let scope = python_sys_modules_value_scope(mapping, source);
1818 let key = (scope.start_byte(), scope.end_byte(), path.join("."));
1819 if !seen.insert(key.clone()) {
1820 return PythonSysModulesStoredValue::Unknown;
1821 }
1822 if !python_sys_modules_has_one_binding(context, scope, node_text(root, source)) {
1823 return PythonSysModulesStoredValue::Unknown;
1824 }
1825 let Some([assigned_value]) = facts.assignments.get(&key).map(Vec::as_slice) else {
1826 return PythonSysModulesStoredValue::Unknown;
1827 };
1828 python_sys_modules_update_values(*assigned_value, context, budget, seen)
1829}
1830
1831fn python_sys_modules_aliases<'tree>(
1835 root: Node<'tree>,
1836 source: &str,
1837 source_module: &str,
1838 module: &str,
1839 bindings: &[crate::imports::PythonImportBinding],
1840 budget: &mut PythonRuntimeBudget,
1841) -> Option<PythonSysModulesFacts<'tree>> {
1842 let mut aliases_from = HashMap::<String, Vec<String>>::default();
1843 let mut assignments = HashMap::<(usize, usize, String), Vec<Node<'_>>>::default();
1844 let mut binding_counts = HashMap::<(usize, usize, String), usize>::default();
1845 let mut classes = Vec::new();
1846 let mut pending_table_aliases = Vec::<(Vec<String>, Option<Node<'_>>, Node<'_>)>::new();
1847 let mut stack = vec![root];
1848 while let Some(node) = stack.pop() {
1849 if !budget.visit() {
1850 return None;
1851 }
1852 if node.kind() == "class_definition"
1853 && let Some(name) = node.child_by_field_name("name")
1854 {
1855 let scope = python_alias_scope(node);
1856 classes.push(PythonExternalClassAlias {
1857 name: node_text(name, source).to_owned(),
1858 scope_start_byte: scope.start_byte(),
1859 scope_end_byte: scope.end_byte(),
1860 });
1861 }
1862 for binding in python_direct_scope_bindings_bounded(node, source, || budget.visit())? {
1863 if binding.kind != PythonDirectScopeBindingKind::Import {
1864 let scope = python_alias_scope(binding.declaration);
1865 let key = (
1866 scope.start_byte(),
1867 scope.end_byte(),
1868 node_text(binding.declaration, source).to_owned(),
1869 );
1870 *binding_counts.entry(key).or_default() += 1;
1871 }
1872 }
1873 if matches!(node.kind(), "function_definition" | "lambda")
1874 && let Some(parameters) = node.child_by_field_name("parameters")
1875 {
1876 let scope = node;
1877 let mut cursor = parameters.walk();
1878 for parameter in parameters.named_children(&mut cursor) {
1879 if let Some(name) = python_parameter_name(parameter, source) {
1880 *binding_counts
1881 .entry((scope.start_byte(), scope.end_byte(), name))
1882 .or_default() += 1;
1883 }
1884 }
1885 }
1886 if matches!(node.kind(), "assignment" | "annotated_assignment") {
1887 let (targets, value) = if node.kind() == "assignment" {
1888 crate::imports::python_assignment_chain(node)
1889 } else {
1890 (
1891 node.child_by_field_name("left").into_iter().collect(),
1892 node.child_by_field_name("right"),
1893 )
1894 };
1895 if let Some(value) = value {
1896 for target in targets {
1897 if let Some((_, path)) = python_static_path(target, source) {
1898 let scope = python_alias_scope(target);
1899 let key = (scope.start_byte(), scope.end_byte(), path.join("."));
1900 assignments.entry(key).or_default().push(value);
1901 }
1902 }
1903 }
1904 }
1905 if node.kind() == "assignment" {
1906 let (targets, value) = crate::imports::python_assignment_chain(node);
1907 if let Some(value) = value
1908 && !targets.is_empty()
1909 && targets.iter().all(|target| target.kind() == "identifier")
1910 {
1911 let target_names = targets
1912 .iter()
1913 .map(|target| node_text(*target, source).to_owned())
1914 .collect::<Vec<_>>();
1915 if python_sys_modules_read_table(value, source).is_some_and(|table| {
1916 python_imported_path(table, source, bindings)
1917 .is_some_and(|path| path == ["sys", "modules"])
1918 }) {
1919 pending_table_aliases.push((
1920 target_names,
1921 if value.kind() == "subscript" {
1922 value.child_by_field_name("subscript")
1923 } else {
1924 python_sys_modules_arguments(value).first().copied()
1925 },
1926 value,
1927 ));
1928 } else if value.kind() == "identifier" {
1929 aliases_from
1930 .entry(node_text(value, source).to_owned())
1931 .or_default()
1932 .extend(target_names);
1933 }
1934 }
1935 }
1936 let mut cursor = node.walk();
1937 stack.extend(node.named_children(&mut cursor));
1938 }
1939
1940 let mut facts = PythonSysModulesFacts {
1941 aliases: HashSet::default(),
1942 assignments,
1943 binding_counts,
1944 classes,
1945 };
1946 let mut target_aliases = Vec::new();
1947 {
1948 let proof = PythonSysModulesProofContext {
1949 source,
1950 source_module,
1951 module,
1952 bindings,
1953 facts: &facts,
1954 };
1955 for (targets, key, read) in pending_table_aliases {
1956 if matches!(
1957 python_sys_modules_read_identity(key, read, &proof, budget),
1958 PythonSysModulesLookup::Target(_)
1959 ) {
1960 target_aliases.extend(targets);
1961 }
1962 }
1963 }
1964 facts.aliases.extend(target_aliases);
1965
1966 let mut pending = facts.aliases.iter().cloned().collect::<Vec<_>>();
1967 while let Some(source_name) = pending.pop() {
1968 if let Some(targets) = aliases_from.get(&source_name) {
1969 for target in targets {
1970 if facts.aliases.insert(target.clone()) {
1971 pending.push(target.clone());
1972 }
1973 }
1974 }
1975 }
1976 Some(facts)
1977}
1978
1979fn python_sys_modules_read_use(
1980 read: Node<'_>,
1981 source: &str,
1982 member: &str,
1983 aliases: &HashSet<String>,
1984) -> Option<PythonModuleMemberMutation> {
1985 let mut expression = read;
1986 let mut direct_attribute = None;
1987 while let Some(parent) = expression.parent() {
1988 if parent.kind() != "attribute"
1989 || parent
1990 .child_by_field_name("object")
1991 .is_none_or(|object| object.id() != expression.id())
1992 {
1993 break;
1994 }
1995 if direct_attribute.is_none() {
1996 direct_attribute = Some(parent);
1997 }
1998 expression = parent;
1999 }
2000 if let Some(attribute) = direct_attribute {
2001 let name = attribute
2002 .child_by_field_name("attribute")
2003 .map(|name| node_text(name, source));
2004 if name == Some("__dict__") {
2005 return Some(PythonModuleMemberMutation::Escape(node_source_range(
2006 attribute,
2007 )));
2008 }
2009 if name == Some(member) && is_binding_target(attribute) {
2010 return Some(PythonModuleMemberMutation::Write(node_source_range(
2011 attribute,
2012 )));
2013 }
2014 return None;
2015 }
2016 if python_builtin_module_read(expression, source) {
2017 return None;
2018 }
2019 if read.parent().is_some_and(|parent| {
2020 parent.kind() == "assignment"
2021 && parent
2022 .child_by_field_name("right")
2023 .is_some_and(|right| right.id() == read.id())
2024 && crate::imports::python_assignment_chain(parent)
2025 .0
2026 .iter()
2027 .all(|target| {
2028 target.kind() == "identifier" && aliases.contains(node_text(*target, source))
2029 })
2030 }) || read.parent().is_some_and(|parent| {
2031 parent.kind() == "expression_statement"
2032 && parent
2033 .named_child(0)
2034 .is_some_and(|expression| expression.id() == read.id())
2035 }) {
2036 return None;
2037 }
2038 Some(PythonModuleMemberMutation::Escape(node_source_range(read)))
2039}
2040
2041fn python_builtin_module_read(expression: Node<'_>, source: &str) -> bool {
2042 let Some(arguments) = expression
2043 .parent()
2044 .filter(|node| node.kind() == "argument_list")
2045 else {
2046 return false;
2047 };
2048 let Some(call) = arguments.parent().filter(|node| node.kind() == "call") else {
2049 return false;
2050 };
2051 let Some(function) = call.child_by_field_name("function") else {
2052 return false;
2053 };
2054 if function.kind() != "identifier"
2055 || !matches!(node_text(function, source), "getattr" | "hasattr")
2056 || matches!(
2057 python_name_resolution_at(node_text(function, source), function, source),
2058 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal
2059 )
2060 {
2061 return false;
2062 }
2063 let mut cursor = arguments.walk();
2064 arguments
2065 .named_children(&mut cursor)
2066 .find(|argument| argument.kind() != "keyword_argument")
2067 .is_some_and(|first| first.id() == expression.id())
2068}
2069
2070fn python_module_table_alias_binding_use(
2071 node: Node<'_>,
2072 source: &str,
2073 aliases: &HashSet<String>,
2074) -> bool {
2075 if node.kind() != "identifier" {
2076 return false;
2077 }
2078 let name = node_text(node, source);
2079 if !aliases.contains(name) {
2080 return false;
2081 }
2082 if is_binding_target(node) {
2083 return true;
2084 }
2085 node.parent().is_some_and(|assignment| {
2086 assignment.kind() == "assignment"
2087 && assignment
2088 .child_by_field_name("right")
2089 .is_some_and(|right| right.id() == node.id())
2090 && crate::imports::python_assignment_chain(assignment)
2091 .0
2092 .iter()
2093 .all(|target| {
2094 target.kind() == "identifier" && aliases.contains(node_text(*target, source))
2095 })
2096 })
2097}
2098
2099pub fn python_external_class_member_mutation(
2106 root: Node<'_>,
2107 source: &str,
2108 class_identities: &[&str],
2109 member: &str,
2110 max_nodes: usize,
2111) -> Option<PythonModuleMemberMutation> {
2112 let bindings = python_import_bindings_from_tree(root, source);
2113 let Some(aliases) =
2114 python_external_class_aliases(root, source, &bindings, class_identities, max_nodes)
2115 else {
2116 return Some(PythonModuleMemberMutation::Budget);
2117 };
2118 let mut budget = PythonRuntimeBudget {
2119 remaining: max_nodes,
2120 exhausted: false,
2121 visited_nodes: 0,
2122 };
2123 let mut stack = vec![root];
2124 while let Some(node) = stack.pop() {
2125 if !budget.visit() {
2126 return Some(PythonModuleMemberMutation::Budget);
2127 }
2128 if node.kind() == "attribute"
2129 && node
2130 .child_by_field_name("attribute")
2131 .is_some_and(|attribute| node_text(attribute, source) == member)
2132 && is_binding_target(node)
2133 && let Some(object) = node.child_by_field_name("object")
2134 && python_external_class_expression(
2135 object,
2136 source,
2137 &bindings,
2138 class_identities,
2139 &aliases,
2140 )
2141 {
2142 return Some(PythonModuleMemberMutation::Write(node_source_range(node)));
2143 }
2144 if node.kind() == "call"
2145 && python_setattr_may_replace_class_member(
2146 node,
2147 source,
2148 &bindings,
2149 class_identities,
2150 member,
2151 &aliases,
2152 )
2153 {
2154 return Some(PythonModuleMemberMutation::Write(node_source_range(node)));
2155 }
2156 if node.kind() == "call"
2157 && python_mock_patch_may_replace_class_member(
2158 node,
2159 source,
2160 &bindings,
2161 class_identities,
2162 member,
2163 &aliases,
2164 )
2165 {
2166 return Some(PythonModuleMemberMutation::Write(node_source_range(node)));
2167 }
2168 if node.kind() == "identifier"
2169 && python_class_alias_visible_at(node_text(node, source), node, source, &aliases)
2170 && python_class_alias_escapes_at(node, source, &aliases)
2171 {
2172 return Some(PythonModuleMemberMutation::Escape(node_source_range(node)));
2173 }
2174 let mut cursor = node.walk();
2175 stack.extend(node.named_children(&mut cursor));
2176 }
2177 None
2178}
2179
2180#[derive(Clone)]
2181struct PythonExternalClassAlias {
2182 name: String,
2183 scope_start_byte: usize,
2184 scope_end_byte: usize,
2185}
2186
2187fn python_external_class_aliases(
2188 root: Node<'_>,
2189 source: &str,
2190 bindings: &[crate::imports::PythonImportBinding],
2191 class_identities: &[&str],
2192 max_nodes: usize,
2193) -> Option<Vec<PythonExternalClassAlias>> {
2194 let mut budget = PythonRuntimeBudget {
2195 remaining: max_nodes,
2196 exhausted: false,
2197 visited_nodes: 0,
2198 };
2199 let mut assignments = Vec::new();
2200 let mut stack = vec![root];
2201 while let Some(node) = stack.pop() {
2202 if !budget.visit() {
2203 return None;
2204 }
2205 if matches!(node.kind(), "assignment" | "annotated_assignment")
2206 && let Some(target) = node.child_by_field_name("left")
2207 && target.kind() == "identifier"
2208 && let Some(value) = node.child_by_field_name("right")
2209 {
2210 let scope = python_alias_scope(node);
2211 assignments.push((
2212 node_text(target, source).to_owned(),
2213 value,
2214 scope.start_byte(),
2215 scope.end_byte(),
2216 ));
2217 }
2218 let mut cursor = node.walk();
2219 stack.extend(node.named_children(&mut cursor));
2220 }
2221
2222 let mut aliases = Vec::<PythonExternalClassAlias>::new();
2223 loop {
2224 let before = aliases.len();
2225 for (name, value, scope_start_byte, scope_end_byte) in &assignments {
2226 if python_external_class_expression(
2227 *value,
2228 source,
2229 bindings,
2230 class_identities,
2231 &aliases,
2232 ) && !aliases.iter().any(|alias| {
2233 alias.name == *name
2234 && alias.scope_start_byte == *scope_start_byte
2235 && alias.scope_end_byte == *scope_end_byte
2236 }) {
2237 aliases.push(PythonExternalClassAlias {
2238 name: name.clone(),
2239 scope_start_byte: *scope_start_byte,
2240 scope_end_byte: *scope_end_byte,
2241 });
2242 }
2243 }
2244 if aliases.len() == before {
2245 return Some(aliases);
2246 }
2247 }
2248}
2249
2250fn python_alias_scope(mut node: Node<'_>) -> Node<'_> {
2251 while let Some(parent) = node.parent() {
2252 if matches!(
2253 parent.kind(),
2254 "module" | "class_definition" | "function_definition" | "lambda"
2255 ) {
2256 return parent;
2257 }
2258 node = parent;
2259 }
2260 node
2261}
2262
2263fn python_external_class_expression(
2264 expression: Node<'_>,
2265 source: &str,
2266 bindings: &[crate::imports::PythonImportBinding],
2267 class_identities: &[&str],
2268 aliases: &[PythonExternalClassAlias],
2269) -> bool {
2270 if python_imported_class_expression(expression, source, bindings, class_identities) {
2271 return true;
2272 }
2273 let Some((root, path)) = python_static_path(expression, source) else {
2274 return false;
2275 };
2276 path.len() == 1 && python_class_alias_visible_at(node_text(root, source), root, source, aliases)
2277}
2278
2279fn python_class_alias_visible_at(
2280 name: &str,
2281 reference: Node<'_>,
2282 source: &str,
2283 aliases: &[PythonExternalClassAlias],
2284) -> bool {
2285 if !aliases.iter().any(|alias| alias.name == name) {
2286 return false;
2287 }
2288 let mut current = reference;
2289 while let Some(scope) = nearest_scope(current) {
2290 if aliases.iter().any(|alias| {
2291 alias.name == name
2292 && alias.scope_start_byte == scope.start_byte()
2293 && alias.scope_end_byte == scope.end_byte()
2294 }) {
2295 return true;
2296 }
2297 match scope.kind() {
2298 "function_definition" | "lambda" => {
2299 let parameters = scope
2300 .child_by_field_name("parameters")
2301 .into_iter()
2302 .flat_map(|parameters| {
2303 let mut cursor = parameters.walk();
2304 parameters
2305 .named_children(&mut cursor)
2306 .filter_map(|parameter| python_parameter_name(parameter, source))
2307 .collect::<Vec<_>>()
2308 });
2309 let Some(inventory) =
2310 PythonLexicalScopeInventory::collect_bounded(scope, source, parameters, || {
2311 true
2312 })
2313 else {
2314 return true;
2315 };
2316 match inventory.name_resolution_at(name, reference) {
2317 PythonLexicalNameResolution::Local => return false,
2318 PythonLexicalNameResolution::Global => break,
2319 PythonLexicalNameResolution::Nonlocal
2320 | PythonLexicalNameResolution::Unbound => {}
2321 }
2322 }
2323 "module" => return false,
2324 "class_definition" => {}
2327 _ => unreachable!("nearest_scope returns only Python lexical scopes"),
2328 }
2329 current = scope;
2330 }
2331 aliases.iter().any(|alias| {
2332 alias.name == name
2333 && alias.scope_start_byte == 0
2334 && reference.start_byte() < alias.scope_end_byte
2335 })
2336}
2337
2338fn nearest_scope(mut node: Node<'_>) -> Option<Node<'_>> {
2339 while let Some(parent) = node.parent() {
2340 if matches!(
2341 parent.kind(),
2342 "module" | "class_definition" | "function_definition" | "lambda"
2343 ) {
2344 return Some(parent);
2345 }
2346 node = parent;
2347 }
2348 None
2349}
2350
2351fn python_class_alias_escapes_at(
2352 node: Node<'_>,
2353 source: &str,
2354 aliases: &[PythonExternalClassAlias],
2355) -> bool {
2356 let mut current = node;
2357 while let Some(parent) = current.parent() {
2358 if matches!(
2359 parent.kind(),
2360 "function_definition" | "lambda" | "class_definition" | "module"
2361 ) {
2362 return false;
2363 }
2364 if parent.kind() == "argument_list"
2365 && parent.parent().is_some_and(|call| call.kind() == "call")
2366 {
2367 return true;
2368 }
2369 if matches!(parent.kind(), "return_statement" | "yield") {
2370 return true;
2371 }
2372 if matches!(parent.kind(), "list" | "tuple" | "set" | "dictionary") {
2373 return true;
2374 }
2375 if matches!(parent.kind(), "assignment" | "annotated_assignment")
2376 && parent.child_by_field_name("right").is_some_and(|right| {
2377 right.start_byte() <= node.start_byte() && node.end_byte() <= right.end_byte()
2378 })
2379 {
2380 let Some(target) = parent.child_by_field_name("left") else {
2381 return true;
2382 };
2383 if target.kind() == "identifier"
2384 && python_class_alias_visible_at(node_text(target, source), target, source, aliases)
2385 {
2386 return false;
2387 }
2388 return true;
2389 }
2390 current = parent;
2391 }
2392 false
2393}
2394
2395fn python_setattr_may_replace_class_member(
2396 call: Node<'_>,
2397 source: &str,
2398 bindings: &[crate::imports::PythonImportBinding],
2399 class_identities: &[&str],
2400 member: &str,
2401 aliases: &[PythonExternalClassAlias],
2402) -> bool {
2403 let Some(function) = call.child_by_field_name("function") else {
2404 return false;
2405 };
2406 let imported_setattr = python_imported_path(function, source, bindings)
2407 .is_some_and(|path| path == ["builtins", "setattr"]);
2408 let builtin_setattr = python_static_path(function, source).is_some_and(|(root, path)| {
2409 path == ["setattr"]
2410 && !matches!(
2411 python_name_resolution_at(node_text(root, source), root, source),
2412 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal
2413 )
2414 });
2415 if !imported_setattr && !builtin_setattr {
2416 return false;
2417 }
2418 let Some(arguments) = call.child_by_field_name("arguments") else {
2419 return false;
2420 };
2421 let mut cursor = arguments.walk();
2422 let positional = arguments
2423 .named_children(&mut cursor)
2424 .filter(|argument| argument.kind() != "keyword_argument")
2425 .collect::<Vec<_>>();
2426 let [object, name, ..] = positional.as_slice() else {
2427 return false;
2428 };
2429 if !python_external_class_expression(*object, source, bindings, class_identities, aliases) {
2430 return false;
2431 }
2432 python_plain_string_literal(*name, source).is_none_or(|name| name == member)
2433}
2434
2435fn python_mock_patch_may_replace_class_member(
2436 call: Node<'_>,
2437 source: &str,
2438 bindings: &[crate::imports::PythonImportBinding],
2439 class_identities: &[&str],
2440 member: &str,
2441 aliases: &[PythonExternalClassAlias],
2442) -> bool {
2443 let Some(function) = call.child_by_field_name("function") else {
2444 return false;
2445 };
2446 let Some(function_path) = python_imported_path(function, source, bindings) else {
2447 return false;
2448 };
2449 let patch_object = function_path == ["unittest", "mock", "patch", "object"];
2450 let patch_class_path = function_path == ["unittest", "mock", "patch"];
2451 if !patch_object && !patch_class_path {
2452 return false;
2453 }
2454 let Some(arguments) = call.child_by_field_name("arguments") else {
2455 return false;
2456 };
2457 let mut cursor = arguments.walk();
2458 let positional = arguments
2459 .named_children(&mut cursor)
2460 .filter(|argument| argument.kind() != "keyword_argument")
2461 .collect::<Vec<_>>();
2462 let Some(target) = positional.first().copied() else {
2463 return false;
2464 };
2465 if patch_object {
2466 let Some(name) = positional.get(1).copied() else {
2467 return false;
2468 };
2469 return python_external_class_expression(
2470 target,
2471 source,
2472 bindings,
2473 class_identities,
2474 aliases,
2475 ) && python_plain_string_literal(name, source).is_none_or(|name| name == member);
2476 }
2477 let Some(target_path) = python_plain_string_literal(target, source) else {
2478 return false;
2479 };
2480 class_identities
2481 .iter()
2482 .any(|class| target_path == *class || target_path == format!("{class}.{member}"))
2483}
2484
2485fn python_imported_path(
2486 expression: Node<'_>,
2487 source: &str,
2488 bindings: &[crate::imports::PythonImportBinding],
2489) -> Option<Vec<String>> {
2490 let (root, expression_path) = python_static_path(expression, source)?;
2491 let root_name = node_text(root, source);
2492 if matches!(
2493 python_name_resolution_at(root_name, root, source),
2494 PythonLexicalNameResolution::Local | PythonLexicalNameResolution::Nonlocal
2495 ) {
2496 return None;
2497 }
2498 let latest_import = bindings
2499 .iter()
2500 .filter(|binding| {
2501 binding.local_name == root_name
2502 && binding.start_byte <= root.start_byte()
2503 && binding.scope_start_byte <= root.start_byte()
2504 && root.end_byte() <= binding.scope_end_byte
2505 })
2506 .map(|binding| binding.start_byte)
2507 .max()?;
2508 let binding = bindings.iter().find(|binding| {
2509 binding.local_name == root_name
2510 && binding.start_byte == latest_import
2511 && binding.scope_start_byte <= root.start_byte()
2512 && root.end_byte() <= binding.scope_end_byte
2513 })?;
2514 let qualified_prefix = if binding.consumed_attributes > 0 {
2515 binding.qualified_name.split('.').next()?
2516 } else {
2517 binding.qualified_name.as_str()
2518 };
2519 let mut qualified_path = qualified_prefix
2520 .split('.')
2521 .map(str::to_owned)
2522 .collect::<Vec<_>>();
2523 qualified_path.extend(expression_path.into_iter().skip(1));
2524 Some(qualified_path)
2525}
2526
2527fn python_imported_class_expression(
2528 expression: Node<'_>,
2529 source: &str,
2530 bindings: &[crate::imports::PythonImportBinding],
2531 class_identities: &[&str],
2532) -> bool {
2533 let Some(path) = python_imported_path(expression, source, bindings) else {
2534 return false;
2535 };
2536 class_identities.iter().any(|identity| {
2537 identity
2538 .split('.')
2539 .map(str::to_owned)
2540 .eq(path.iter().cloned())
2541 })
2542}
2543
2544fn python_static_path<'tree>(
2545 node: Node<'tree>,
2546 source: &str,
2547) -> Option<(Node<'tree>, Vec<String>)> {
2548 let mut current = node;
2549 let mut attributes = Vec::new();
2550 loop {
2551 if current.kind() == "identifier" {
2552 attributes.reverse();
2553 let mut path = Vec::with_capacity(attributes.len() + 1);
2554 path.push(node_text(current, source).to_owned());
2555 path.extend(attributes);
2556 return Some((current, path));
2557 }
2558 if current.kind() != "attribute" {
2559 return None;
2560 }
2561 let attribute = current.child_by_field_name("attribute")?;
2562 attributes.push(node_text(attribute, source).to_owned());
2563 current = current.child_by_field_name("object")?;
2564 }
2565}
2566
2567fn module_binding_use(
2569 node: Node<'_>,
2570 source: &str,
2571 bindings: &[crate::imports::PythonImportBinding],
2572 table_aliases: &HashSet<String>,
2573 module: &str,
2574 member: &str,
2575) -> Option<PythonModuleMemberMutation> {
2576 let Some(parent) = node.parent() else {
2577 return Some(PythonModuleMemberMutation::Escape(node_source_range(node)));
2578 };
2579 if matches!(
2580 parent.kind(),
2581 "import_statement" | "import_from_statement" | "aliased_import" | "dotted_name"
2582 ) {
2583 return None;
2584 }
2585 let mut expression = node;
2586 while let Some(parent) = expression.parent() {
2587 if parent.kind() != "attribute"
2588 || parent
2589 .child_by_field_name("object")
2590 .is_none_or(|object| object.id() != expression.id())
2591 {
2592 break;
2593 }
2594 expression = parent;
2595 }
2596 if python_builtin_module_read(expression, source) {
2597 return None;
2598 }
2599 let imported_path = python_imported_path(expression, source, bindings);
2600 let table_alias_path = python_static_path(expression, source).and_then(|(root, suffix)| {
2601 let root_name = node_text(root, source);
2602 table_aliases.contains(root_name).then(|| {
2603 let mut resolved = vec![module.to_owned()];
2604 resolved.extend(suffix.into_iter().skip(1));
2605 resolved
2606 })
2607 });
2608 let Some(path) = imported_path.or(table_alias_path) else {
2609 return Some(PythonModuleMemberMutation::Escape(node_source_range(node)));
2610 };
2611 let path = path.join(".");
2612 let target = format!("{module}.{member}");
2613 if path == target {
2614 return is_binding_target(expression)
2615 .then(|| PythonModuleMemberMutation::Write(node_source_range(expression)));
2616 }
2617 if path == format!("{module}.__dict__") || path == module {
2618 return Some(PythonModuleMemberMutation::Escape(node_source_range(
2619 expression,
2620 )));
2621 }
2622 if path.starts_with(&format!("{module}.")) {
2623 None
2624 } else {
2625 Some(PythonModuleMemberMutation::Escape(node_source_range(node)))
2626 }
2627}
2628
2629fn python_mock_patch_may_replace_module_member(
2630 call: Node<'_>,
2631 source: &str,
2632 bindings: &[crate::imports::PythonImportBinding],
2633 module: &str,
2634 member: &str,
2635) -> bool {
2636 let Some(function) = call.child_by_field_name("function") else {
2637 return false;
2638 };
2639 let Some(path) = python_imported_path(function, source, bindings) else {
2640 return false;
2641 };
2642 if path.len() < 2 || path[path.len() - 2] != "mock" || path[path.len() - 1] != "patch" {
2643 return false;
2644 }
2645 let Some(arguments) = call.child_by_field_name("arguments") else {
2646 return false;
2647 };
2648 let mut cursor = arguments.walk();
2649 let Some(target) = arguments.named_children(&mut cursor).next() else {
2650 return false;
2651 };
2652 let expected = format!("{module}.{member}");
2653 python_plain_string_literal(target, source) == Some(expected.as_str())
2654}
2655
2656#[cfg(test)]
2657mod tests {
2658 use super::*;
2659
2660 fn facts(source: &str) -> PythonRuntimeReadFacts {
2661 let mut parser = tree_sitter::Parser::new();
2662 parser
2663 .set_language(&tree_sitter_python::LANGUAGE.into())
2664 .expect("python grammar loads");
2665 let tree = parser.parse(source, None).expect("fixture parses");
2666 extract_python_runtime_reads(tree.root_node(), source, 64)
2667 }
2668
2669 #[test]
2670 #[brokk_bifrost_test_macros::hardwon(
2671 "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: module alias exhaustion certified mutation-free"
2672 )]
2673 fn qs6_module_mutation_budget_is_not_absence() {
2674 let source = "import os\nos.system = fake\n";
2675 let tree = crate::declarations::parse_python_tree(source).unwrap();
2676 for budget in [0, 1, 2] {
2677 assert_eq!(
2678 python_module_member_mutation(
2679 tree.root_node(),
2680 source,
2681 "test",
2682 "os",
2683 "system",
2684 budget
2685 ),
2686 Some(PythonModuleMemberMutation::Budget),
2687 "budget={budget}"
2688 );
2689 }
2690 assert!(
2691 python_module_member_mutation(tree.root_node(), source, "test", "os", "system", 4096)
2692 .is_some()
2693 );
2694 }
2695
2696 #[test]
2699 #[brokk_bifrost_test_macros::hardwon(
2700 "6b123df7cc68: quicksilver round 6 on brokk-bifrost-python: runtime indices lost radix and separator literals"
2701 )]
2702 fn qs6_runtime_key_shapes() {
2703 use PythonRuntimeAccessKey::{Dynamic, Index, Property, Unsupported};
2704 for (key, expected) in [
2705 ("0", Index(0)),
2706 ("0x1", Index(1)),
2707 ("0b1", Index(1)),
2708 ("0o7", Index(7)),
2709 ("1_0", Index(10)),
2710 ("340282366920938463463374607431768211455", Index(u128::MAX)),
2711 ("340282366920938463463374607431768211456", Unsupported),
2712 ("1:3", Unsupported),
2713 ("1, 2", Unsupported),
2714 ("1,", Unsupported),
2715 ("(1, 2)", Unsupported),
2716 ("name", Dynamic),
2717 ("\"HOME\"", Property("HOME".into())),
2718 ] {
2719 let source = format!("import sys\nvalue = sys.argv[{key}]\n");
2720 let facts = facts(&source);
2721 assert!(facts.complete, "{facts:?}");
2722 assert_eq!(facts.reads.len(), 1, "{facts:?}");
2723 assert_eq!(facts.reads[0].access, expected, "{key}");
2724 }
2725 }
2726
2727 #[test]
2728 fn qs6_runtime_extraction_bounds() {
2729 let source = "pass\n".repeat(100);
2730 let tree = crate::declarations::parse_python_tree(&source).unwrap();
2731 let empty = extract_python_runtime_reads(tree.root_node(), &source, 1);
2732 assert!(empty.complete, "{empty:?}");
2733 assert!(empty.visited_nodes > 64, "{empty:?}");
2734 let source = "import sys\nx = sys.argv[1]\ny = sys.argv[2]\n";
2735 let tree = crate::declarations::parse_python_tree(source).unwrap();
2736 for cap in [0, 1, 2, 3] {
2737 let facts = extract_python_runtime_reads(tree.root_node(), source, cap);
2738 assert_eq!(facts.reads.len(), cap.min(2), "{facts:?}");
2739 assert_eq!(facts.complete, cap > 2, "{facts:?}");
2740 }
2741 }
2742
2743 #[test]
2745 fn imported_module_environment_subscript_is_a_pristine_keyed_read() {
2746 let source = "import os\n\n\ndef run():\n command = os.environ[\"DFB\"]\n os.system(command)\n";
2747 let facts = facts(source);
2748 assert!(facts.complete, "{facts:#?}");
2749 let [read] = facts.reads.as_slice() else {
2750 panic!("exactly one keyed read: {facts:#?}");
2751 };
2752 assert_eq!(read.root_name, "os");
2753 assert_eq!(read.container, "environ");
2754 assert_eq!(
2755 read.access,
2756 PythonRuntimeAccessKey::Property("DFB".to_owned())
2757 );
2758 assert_eq!(
2759 read.lexical_resolution,
2760 PythonRuntimeRootResolution::ImportedModule
2761 );
2762 assert_eq!(read.mutation, PythonRuntimeMutationEvidence::NoKnownWrite);
2763 assert_eq!(
2764 read.accessor,
2765 PythonRuntimeAccessorCoverage::NoKnownAccessorEffects,
2766 "the only call runs after the read"
2767 );
2768 assert!(facts.writes.is_empty(), "{facts:#?}");
2769 assert_eq!(
2772 &source[read.candidate_anchor.start_byte..read.candidate_anchor.end_byte],
2773 "os.environ"
2774 );
2775 assert_eq!(
2776 &source[read.range.start_byte..read.range.end_byte],
2777 "os.environ[\"DFB\"]"
2778 );
2779 }
2780
2781 #[test]
2782 fn imported_module_argv_index_is_a_keyed_read() {
2783 let facts = facts("import sys\n\n\ndef run():\n return sys.argv[1]\n");
2784 let [read] = facts.reads.as_slice() else {
2785 panic!("exactly one keyed read: {facts:#?}");
2786 };
2787 assert_eq!(read.root_name, "sys");
2788 assert_eq!(read.container, "argv");
2789 assert_eq!(read.access, PythonRuntimeAccessKey::Index(1));
2790 }
2791
2792 #[test]
2795 fn a_shadowed_module_name_is_not_a_read() {
2796 let facts = facts("import os\n\n\ndef run(os):\n return os.environ[\"DFB\"]\n");
2797 assert!(facts.reads.is_empty(), "{facts:#?}");
2798 }
2799
2800 #[test]
2801 fn a_local_container_name_is_not_a_read() {
2802 let facts =
2803 facts("def run():\n environ = {\"DFB\": \"x\"}\n return environ[\"DFB\"]\n");
2804 assert!(facts.reads.is_empty(), "{facts:#?}");
2805 assert!(facts.writes.is_empty(), "{facts:#?}");
2806 }
2807
2808 #[test]
2809 fn an_unrelated_module_spelled_os_is_not_a_read() {
2810 let facts = facts("from mypkg import os\n\n\ndef run():\n return os.environ[\"DFB\"]\n");
2811 assert!(facts.reads.is_empty(), "{facts:#?}");
2812 }
2813
2814 #[test]
2815 fn a_dynamic_key_stays_a_read_with_no_static_key() {
2816 let facts = facts("import os\n\n\ndef run(name):\n return os.environ[name]\n");
2817 let [read] = facts.reads.as_slice() else {
2818 panic!("one read: {facts:#?}");
2819 };
2820 assert_eq!(read.access, PythonRuntimeAccessKey::Dynamic);
2821 }
2822
2823 #[test]
2824 fn a_container_write_refutes_every_read_of_that_container() {
2825 let facts = facts(
2826 "import os\n\n\ndef run():\n os.environ[\"A\"] = \"1\"\n return os.environ[\"B\"]\n",
2827 );
2828 let [read] = facts.reads.as_slice() else {
2829 panic!("one read: {facts:#?}");
2830 };
2831 assert_eq!(read.mutation, PythonRuntimeMutationEvidence::KnownWrite);
2832 let [write] = facts.writes.as_slice() else {
2833 panic!("one write: {facts:#?}");
2834 };
2835 assert_eq!(write.target, PythonRuntimeWriteTarget::Key);
2836 assert_eq!(write.container.as_deref(), Some("environ"));
2837 }
2838
2839 #[test]
2840 fn a_container_method_receiver_refutes_the_pristine_claim() {
2841 let facts = facts(
2842 "import os\n\n\ndef run():\n os.environ.update({})\n return os.environ[\"B\"]\n",
2843 );
2844 let [read] = facts.reads.as_slice() else {
2845 panic!("one read: {facts:#?}");
2846 };
2847 assert_eq!(
2848 read.mutation,
2849 PythonRuntimeMutationEvidence::KnownWriteAndUnknownEffects
2850 );
2851 }
2852
2853 #[test]
2854 fn a_module_object_escape_refutes_every_container() {
2855 let facts =
2856 facts("import os\n\n\ndef run(sink):\n sink(os)\n return os.environ[\"B\"]\n");
2857 let [read] = facts.reads.as_slice() else {
2858 panic!("one read: {facts:#?}");
2859 };
2860 assert_eq!(
2861 read.mutation,
2862 PythonRuntimeMutationEvidence::KnownWriteAndUnknownEffects
2863 );
2864 assert!(
2865 facts
2866 .writes
2867 .iter()
2868 .any(|write| write.target == PythonRuntimeWriteTarget::ModuleEscape),
2869 "{facts:#?}"
2870 );
2871 }
2872
2873 #[test]
2874 fn a_call_before_the_read_leaves_accessor_coverage_open() {
2875 let facts = facts(
2876 "import os\n\n\ndef run(prepare):\n prepare()\n return os.environ[\"B\"]\n",
2877 );
2878 let [read] = facts.reads.as_slice() else {
2879 panic!("one read: {facts:#?}");
2880 };
2881 assert_eq!(
2882 read.accessor,
2883 PythonRuntimeAccessorCoverage::UnknownAccessorEffects
2884 );
2885 }
2886
2887 #[test]
2890 fn a_call_in_another_function_does_not_poison_the_read() {
2891 let source = "import os\n\n\ndef other(prepare):\n prepare()\n\n\ndef run():\n return os.environ[\"B\"]\n";
2892 let facts = facts(source);
2893 let [read] = facts.reads.as_slice() else {
2894 panic!("one read: {facts:#?}");
2895 };
2896 assert_eq!(
2897 read.accessor,
2898 PythonRuntimeAccessorCoverage::NoKnownAccessorEffects
2899 );
2900 }
2901
2902 #[test]
2903 fn a_call_later_in_the_same_loop_can_precede_the_read() {
2904 let source = "import os\n\n\ndef run(items, prepare):\n for item in items:\n value = os.environ[\"B\"]\n prepare(value)\n";
2905 let facts = facts(source);
2906 let [read] = facts.reads.as_slice() else {
2907 panic!("one read: {facts:#?}");
2908 };
2909 assert_eq!(
2910 read.accessor,
2911 PythonRuntimeAccessorCoverage::UnknownAccessorEffects
2912 );
2913 }
2914
2915 #[test]
2916 fn an_aliased_import_keeps_the_absolute_module_identity() {
2917 let facts =
2918 facts("import os as operating\n\n\ndef run():\n return operating.environ[\"B\"]\n");
2919 let [read] = facts.reads.as_slice() else {
2920 panic!("one read: {facts:#?}");
2921 };
2922 assert_eq!(read.binding_name, "operating");
2923 assert_eq!(read.root_name, "os");
2924 }
2925
2926 #[test]
2929 fn a_submodule_import_agrees_with_its_package_binding() {
2930 let facts =
2931 facts("import os\nimport os.path\n\n\ndef run():\n return os.environ[\"B\"]\n");
2932 let [read] = facts.reads.as_slice() else {
2933 panic!("one read: {facts:#?}");
2934 };
2935 assert_eq!(read.root_name, "os");
2936 }
2937
2938 #[test]
2939 fn a_module_scope_rebinding_of_the_import_name_is_not_a_read() {
2940 let facts = facts("import os\n\nos = None\n\n\ndef run():\n return os.environ[\"B\"]\n");
2941 assert!(facts.reads.is_empty(), "{facts:#?}");
2942 }
2943}
2944
2945#[cfg(test)]
2946mod module_member_tests {
2947 use super::*;
2948
2949 fn mutation(source: &str, member: &str) -> Option<PythonModuleMemberMutation> {
2950 let mut parser = tree_sitter::Parser::new();
2951 parser
2952 .set_language(&tree_sitter_python::LANGUAGE.into())
2953 .expect("python grammar loads");
2954 let tree = parser.parse(source, None).expect("fixture parses");
2955 python_module_member_mutation(tree.root_node(), source, "test", "os", member, 4096)
2956 }
2957
2958 #[test]
2959 fn a_member_assignment_refuses_that_member() {
2960 let found = mutation(
2961 "import os\n\n\ndef patch(fake):\n os.system = fake\n",
2962 "system",
2963 );
2964 assert!(
2965 matches!(found, Some(PythonModuleMemberMutation::Write(_))),
2966 "{found:?}"
2967 );
2968 assert_eq!(
2970 mutation(
2971 "import os\n\n\ndef patch(fake):\n os.getcwd = fake\n",
2972 "system"
2973 ),
2974 None
2975 );
2976 }
2977
2978 #[test]
2979 fn a_module_object_escape_refuses_every_member() {
2980 for source in [
2981 "import os\n\n\ndef leak(sink):\n sink(os)\n",
2982 "import os\n\n\nalias = os\n",
2983 "import os\nimport functools\n\n\ndef patch():\n setattr(os, \"system\", None)\n",
2984 ] {
2985 let found = mutation(source, "system");
2986 assert!(
2987 matches!(found, Some(PythonModuleMemberMutation::Escape(_))),
2988 "{source}: {found:?}"
2989 );
2990 }
2991 }
2992
2993 #[test]
2994 fn a_matching_module_table_read_counts_its_attribute_write() {
2995 let found = mutation(
2996 "import sys\n\n\ndef patch(fake):\n sys.modules[\"os\"].system = fake\n",
2997 "system",
2998 );
2999 assert!(
3000 matches!(found, Some(PythonModuleMemberMutation::Write(_))),
3001 "{found:?}"
3002 );
3003 }
3004
3005 #[test]
3006 fn a_file_that_never_binds_the_module_proves_the_absence() {
3007 assert_eq!(mutation("def run():\n return 1\n", "system"), None);
3008 }
3009}
3010
3011#[cfg(test)]
3012mod shadowing_tests {
3013 use super::*;
3014
3015 fn facts(source: &str) -> PythonRuntimeReadFacts {
3016 let mut parser = tree_sitter::Parser::new();
3017 parser
3018 .set_language(&tree_sitter_python::LANGUAGE.into())
3019 .expect("python grammar loads");
3020 let tree = parser.parse(source, None).expect("fixture parses");
3021 extract_python_runtime_reads(tree.root_node(), source, 64)
3022 }
3023
3024 #[test]
3027 fn a_parameter_spelling_the_module_name_is_not_an_escape() {
3028 let source = "import os\n\n\ndef read_shadowed(os):\n return os.environ[\"A\"]\n\n\ndef read_real():\n return os.environ[\"B\"]\n";
3029 let facts = facts(source);
3030 assert!(facts.writes.is_empty(), "{facts:#?}");
3031 let [read] = facts.reads.as_slice() else {
3032 panic!("only the unshadowed read is a candidate: {facts:#?}");
3033 };
3034 assert_eq!(read.mutation, PythonRuntimeMutationEvidence::NoKnownWrite);
3035 assert_eq!(
3036 &source[read.key_range.start_byte..read.key_range.end_byte],
3037 "\"B\""
3038 );
3039 }
3040
3041 #[test]
3042 fn a_parameter_spelling_the_module_name_does_not_refuse_a_member() {
3043 let source =
3044 "import os\n\n\ndef helper(os):\n return os\n\n\ndef run(c):\n os.system(c)\n";
3045 let mut parser = tree_sitter::Parser::new();
3046 parser
3047 .set_language(&tree_sitter_python::LANGUAGE.into())
3048 .expect("python grammar loads");
3049 let tree = parser.parse(source, None).expect("fixture parses");
3050 assert_eq!(
3051 python_module_member_mutation(tree.root_node(), source, "test", "os", "system", 4096),
3052 None
3053 );
3054 }
3055}
3056
3057#[cfg(test)]
3058mod external_class_mutation_tests {
3059 use super::*;
3060 use brokk_bifrost_test_macros::hardwon;
3061
3062 fn mutation(source: &str) -> Option<PythonModuleMemberMutation> {
3063 let mut parser = tree_sitter::Parser::new();
3064 parser
3065 .set_language(&tree_sitter_python::LANGUAGE.into())
3066 .expect("python grammar loads");
3067 let tree = parser.parse(source, None).expect("fixture parses");
3068 python_external_class_member_mutation(
3069 tree.root_node(),
3070 source,
3071 &["paramiko.client.SSHClient"],
3072 "set_missing_host_key_policy",
3073 4096,
3074 )
3075 }
3076
3077 #[test]
3078 fn exact_imported_class_member_writes_are_found() {
3079 for source in [
3080 "from paramiko.client import SSHClient as Client\n\nClient.set_missing_host_key_policy = fake\n",
3081 "import paramiko.client as client\n\nclient.SSHClient.set_missing_host_key_policy = fake\n",
3082 "from paramiko.client import SSHClient as Client\n\nsetattr(Client, 'set_missing_host_key_policy', fake)\n",
3083 "from paramiko.client import SSHClient as Client\n\nsetattr(Client, name, fake)\n",
3084 ] {
3085 assert!(
3086 matches!(mutation(source), Some(PythonModuleMemberMutation::Write(_))),
3087 "{source}"
3088 );
3089 }
3090 }
3091
3092 #[test]
3093 #[hardwon("#4411: class aliases and mock patch APIs left external method dispatch exact")]
3094 fn class_aliases_and_mock_patch_apis_refute_method_closure() {
3095 for source in [
3096 "from paramiko.client import SSHClient as C\nAlias = C\nAlias.set_missing_host_key_policy = fake\n",
3097 "from paramiko.client import SSHClient as C\nAlias = C\nsetattr(Alias, 'set_missing_host_key_policy', fake)\n",
3098 "from unittest import mock\nfrom paramiko.client import SSHClient as C\nmock.patch.object(C, 'set_missing_host_key_policy', fake)\n",
3099 "from unittest.mock import patch\nfrom paramiko.client import SSHClient as C\npatch.object(C, 'set_missing_host_key_policy', fake)\n",
3100 "from unittest import mock\nmock.patch('paramiko.client.SSHClient.set_missing_host_key_policy', fake)\n",
3101 ] {
3102 assert!(
3103 matches!(mutation(source), Some(PythonModuleMemberMutation::Write(_))),
3104 "{source}"
3105 );
3106 }
3107 }
3108
3109 #[test]
3110 #[hardwon("#4411: an escaped class alias left external method dispatch exact")]
3111 fn an_escaped_class_alias_refutes_method_closure() {
3112 assert!(matches!(
3113 mutation("from paramiko.client import SSHClient as C\nAlias = C\nconsume(Alias)\n"),
3114 Some(PythonModuleMemberMutation::Escape(_))
3115 ));
3116 }
3117
3118 #[test]
3119 fn constructor_uses_and_same_named_workspace_classes_are_not_writes() {
3120 assert_eq!(
3121 mutation(
3122 "from paramiko.client import SSHClient as Client\n\n\ndef connect():\n client = Client()\n client.set_missing_host_key_policy(policy)\n"
3123 ),
3124 None
3125 );
3126 assert_eq!(
3127 mutation(
3128 "class SSHClient:\n pass\n\nSSHClient.set_missing_host_key_policy = fake\n"
3129 ),
3130 None
3131 );
3132 }
3133}
3134
3135#[cfg(test)]
3136mod module_member_mutation_tests {
3137 use super::*;
3138 use brokk_bifrost_test_macros::hardwon;
3139
3140 fn mutation(source: &str) -> Option<PythonModuleMemberMutation> {
3141 mutation_in(source, "pkg.M")
3142 }
3143
3144 fn mutation_in(source: &str, source_module: &str) -> Option<PythonModuleMemberMutation> {
3145 mutation_for_module(source, source_module, "pkg.M")
3146 }
3147
3148 fn mutation_for_module(
3149 source: &str,
3150 source_module: &str,
3151 module: &str,
3152 ) -> Option<PythonModuleMemberMutation> {
3153 let mut parser = tree_sitter::Parser::new();
3154 parser
3155 .set_language(&tree_sitter_python::LANGUAGE.into())
3156 .expect("python grammar loads");
3157 let tree = parser.parse(source, None).expect("fixture parses");
3158 python_module_member_mutation(
3159 tree.root_node(),
3160 source,
3161 source_module,
3162 module,
3163 "NAME",
3164 4096,
3165 )
3166 }
3167
3168 #[test]
3169 #[hardwon(
3170 "#4622: writes through imported module aliases and mock patch APIs must keep a global class unknown"
3171 )]
3172 fn imported_module_member_mutation_covers_dynamic_write_forms() {
3173 for source in [
3174 "import pkg.M as target\ntarget.NAME = fake\n",
3175 "import pkg.M as target\ndel target.NAME\n",
3176 "import pkg.M as target\nsetattr(target, 'NAME', fake)\n",
3177 "import pkg.M as target\nsetattr(target, name, fake)\n",
3178 "import pkg.M as target\ntarget.__dict__['NAME'] = fake\n",
3179 "from unittest import mock\nmock.patch('pkg.M.NAME', fake)\n",
3180 "from unittest.mock import patch\npatch('pkg.M.NAME', fake)\n",
3181 "import pkg.M as target\nfrom unittest import mock\nmock.patch.object(target, 'NAME', fake)\n",
3182 ] {
3183 assert!(mutation(source).is_some(), "{source}");
3184 }
3185 assert_eq!(
3186 None,
3187 mutation("from unittest import mock\nmock.patch('pkg.Other.NAME', fake)\n")
3188 );
3189 }
3190
3191 #[test]
3192 #[hardwon("#4622: unrelated sys.modules reads blocked exact module-global class proofs")]
3193 fn sys_modules_reads_are_scoped_by_literal_module_identity() {
3194 assert_eq!(
3195 None,
3196 mutation("import sys\nother = sys.modules.get('torch')\n")
3197 );
3198 assert_eq!(None, mutation("import sys\nother = sys.modules['torch']\n"));
3199 assert!(matches!(
3200 mutation("import sys\nmodule = sys.modules.get('pkg.M')\nmodule.NAME = fake\n"),
3201 Some(PythonModuleMemberMutation::Write(_))
3202 ));
3203 assert!(matches!(
3204 mutation("import sys\nmodule = sys.modules['pkg.M']\nmodule.NAME = fake\n"),
3205 Some(PythonModuleMemberMutation::Write(_))
3206 ));
3207 assert_eq!(
3208 None,
3209 mutation("import sys\nmodule = sys.modules['pkg.M']\nconsume(module.NAME)\n")
3210 );
3211 assert!(matches!(
3212 mutation("import sys\nmodule = sys.modules[__name__]\nmodule.NAME = fake\n"),
3213 Some(PythonModuleMemberMutation::Write(_))
3214 ));
3215 assert_eq!(
3216 None,
3217 mutation_in("import sys\nmodule = sys.modules[__name__]\n", "pkg.Other")
3218 );
3219 for source in [
3220 "import sys\nsys.modules['pkg.M'] = fake\n",
3221 "import sys\nsys.modules.setdefault('pkg.M', fake)\n",
3222 "import sys\nsys.modules.update({'pkg.M': fake})\n",
3223 "import sys\nconsume(sys.modules)\n",
3224 ] {
3225 assert!(
3226 matches!(
3227 mutation(source),
3228 Some(PythonModuleMemberMutation::Escape(_))
3229 ),
3230 "{source}"
3231 );
3232 }
3233 for source in [
3234 "import sys\nmodule = sys.modules[module_name]\n",
3235 "import sys\ndel sys.modules['torch']\n",
3236 "import sys\nsys.modules.pop('torch')\n",
3237 "import sys\nsys.modules.clear()\n",
3238 "class Shim: pass\nimport sys\nsys.modules['shim'] = Shim()\n",
3239 "class Shim: pass\nimport sys\nsys.modules.update({'shim': Shim()})\n",
3240 "import sys\nimport types\nsys.modules['shim'] = types.ModuleType('shim')\n",
3241 "import sys\nsys.modules['literal'] = None\n",
3242 "import pkg.Other as shim\nimport sys\nsys.modules['shim'] = shim\n",
3243 ] {
3244 assert_eq!(mutation(source), None, "{source}");
3245 }
3246 }
3247
3248 #[test]
3249 #[hardwon("#4622: sys.modules values must be followed by module identity")]
3250 fn dynamic_module_table_reads_follow_only_writes_through_the_possible_alias() {
3251 assert!(matches!(
3252 mutation("import pkg.M as target\nimport sys\nsys.modules['shim'] = target\n"),
3253 Some(PythonModuleMemberMutation::Escape(_))
3254 ));
3255 assert_eq!(
3256 mutation("import sys\ntarget = sys.modules[module_name]\nconsume(target.NAME)\n"),
3257 None
3258 );
3259 assert_eq!(
3260 mutation(
3261 "import sys\ntarget = sys.modules[module_name]\nconsume(getattr(target, 'NAME'))\nconsume(hasattr(target, 'NAME'))\n"
3262 ),
3263 None
3264 );
3265 assert_eq!(
3266 mutation("import sys\nconsume(sys.modules[module_name].NAME)\n"),
3267 None
3268 );
3269 assert!(matches!(
3270 mutation("import sys\ntarget = sys.modules[module_name]\ntarget.NAME = fake\n"),
3271 Some(PythonModuleMemberMutation::Write(_))
3272 ));
3273 }
3274
3275 #[test]
3276 #[hardwon("#4622: a dominating string filter excludes unrelated dynamic sys.modules aliases")]
3277 fn dynamic_sys_modules_keys_respect_dominating_string_filters() {
3278 assert_eq!(
3279 mutation_for_module(
3280 "import sys\ndef patch():\n for module_name in module_names:\n if 'huggingface' in module_name or module_name.startswith('tqdm'):\n target = sys.modules[module_name]\n target.NAME = fake\n",
3281 "pkg.M",
3282 "backend.backends",
3283 ),
3284 None
3285 );
3286 assert!(matches!(
3287 mutation_for_module(
3288 "import sys\ndef patch():\n for module_name in module_names:\n if module_name.startswith('backend'):\n target = sys.modules[module_name]\n target.NAME = fake\n",
3289 "pkg.M",
3290 "backend.backends",
3291 ),
3292 Some(PythonModuleMemberMutation::Write(_))
3293 ));
3294 assert!(matches!(
3295 mutation_for_module(
3296 "import sys\ndef patch(allowed):\n for module_name in module_names:\n if module_name in allowed:\n target = sys.modules[module_name]\n target.NAME = fake\n",
3297 "pkg.M",
3298 "backend.backends",
3299 ),
3300 Some(PythonModuleMemberMutation::Write(_))
3301 ));
3302 assert!(matches!(
3303 mutation_for_module(
3304 "import sys\ndef patch():\n for module_name in module_names:\n if 'huggingface' in module_name:\n module_name = 'backend.backends'\n target = sys.modules[module_name]\n target.NAME = fake\n",
3305 "pkg.M",
3306 "backend.backends",
3307 ),
3308 Some(PythonModuleMemberMutation::Write(_))
3309 ));
3310 assert_eq!(
3311 mutation_for_module(
3312 "import sys\ndef patch():\n for module_name in module_names:\n if not ('huggingface' in module_name):\n continue\n target = sys.modules[module_name]\n target.NAME = fake\n",
3313 "pkg.M",
3314 "backend.backends",
3315 ),
3316 None
3317 );
3318 }
3319}