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