Skip to main content

sqry_lang_python/relations/
graph_builder.rs

1use std::{collections::HashMap, path::Path};
2
3use sqry_core::graph::unified::StagingGraph;
4use sqry_core::graph::unified::build::GraphBuildHelper;
5use sqry_core::graph::unified::edge::FfiConvention;
6use sqry_core::graph::unified::node::NodeId as UnifiedNodeId;
7use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Span};
8use tree_sitter::{Node, Tree};
9
10use super::local_scopes;
11
12const DEFAULT_SCOPE_DEPTH: usize = 4;
13const STD_C_MODULES: &[&str] = &[
14    "_ctypes",
15    "_socket",
16    "_ssl",
17    "_hashlib",
18    "_json",
19    "_pickle",
20    "_struct",
21    "_sqlite3",
22    "_decimal",
23    "_lzma",
24    "_bz2",
25    "_zlib",
26    "_elementtree",
27    "_csv",
28    "_datetime",
29    "_heapq",
30    "_bisect",
31    "_random",
32    "_collections",
33    "_functools",
34    "_itertools",
35    "_operator",
36    "_io",
37    "_thread",
38    "_multiprocessing",
39    "_posixsubprocess",
40    "_asyncio",
41    "array",
42    "math",
43    "cmath",
44];
45const THIRD_PARTY_C_PACKAGES: &[&str] = &[
46    "numpy",
47    "pandas",
48    "scipy",
49    "sklearn",
50    "cv2",
51    "PIL",
52    "torch",
53    "tensorflow",
54    "lxml",
55    "psycopg2",
56    "MySQLdb",
57    "sqlite3",
58    "cryptography",
59    "bcrypt",
60    "regex",
61    "ujson",
62    "orjson",
63    "msgpack",
64    "greenlet",
65    "gevent",
66    "uvloop",
67];
68
69/// Graph builder for Python files using unified `CodeGraph` architecture.
70#[derive(Debug, Clone, Copy)]
71pub struct PythonGraphBuilder {
72    max_scope_depth: usize,
73}
74
75impl Default for PythonGraphBuilder {
76    fn default() -> Self {
77        Self {
78            max_scope_depth: DEFAULT_SCOPE_DEPTH,
79        }
80    }
81}
82
83impl PythonGraphBuilder {
84    #[must_use]
85    pub fn new(max_scope_depth: usize) -> Self {
86        Self { max_scope_depth }
87    }
88}
89
90impl GraphBuilder for PythonGraphBuilder {
91    fn build_graph(
92        &self,
93        tree: &Tree,
94        content: &[u8],
95        file: &Path,
96        staging: &mut StagingGraph,
97    ) -> GraphResult<()> {
98        // Create helper for staging graph population
99        let mut helper = GraphBuildHelper::new(staging, file, Language::Python);
100
101        // Build AST graph for call context tracking
102        let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
103            GraphBuilderError::ParseError {
104                span: Span::default(),
105                reason: e,
106            }
107        })?;
108
109        // Check if __all__ is defined in the module
110        let has_all = has_all_assignment(tree.root_node(), content);
111
112        // Build local variable scope tree
113        let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
114
115        // Create recursion guard for tree walking
116        let recursion_limits =
117            sqry_core::config::RecursionLimits::load_or_default().map_err(|e| {
118                GraphBuilderError::ParseError {
119                    span: Span::default(),
120                    reason: format!("Failed to load recursion limits: {e}"),
121                }
122            })?;
123        let file_ops_depth = recursion_limits.effective_file_ops_depth().map_err(|e| {
124            GraphBuilderError::ParseError {
125                span: Span::default(),
126                reason: format!("Invalid file_ops_depth configuration: {e}"),
127            }
128        })?;
129        let mut guard =
130            sqry_core::query::security::RecursionGuard::new(file_ops_depth).map_err(|e| {
131                GraphBuilderError::ParseError {
132                    span: Span::default(),
133                    reason: format!("Failed to create recursion guard: {e}"),
134                }
135            })?;
136
137        // Walk tree to find functions, classes, methods, calls, and imports
138        walk_tree_for_graph(
139            tree.root_node(),
140            content,
141            &ast_graph,
142            &mut helper,
143            has_all,
144            &mut guard,
145            &mut scope_tree,
146        )?;
147
148        Ok(())
149    }
150
151    fn language(&self) -> Language {
152        Language::Python
153    }
154}
155
156/// Check if the module defines `__all__`.
157fn has_all_assignment(node: Node, content: &[u8]) -> bool {
158    let mut cursor = node.walk();
159    for child in node.children(&mut cursor) {
160        if child.kind() == "expression_statement" {
161            // Check for __all__ assignment
162            let assignment = child
163                .children(&mut child.walk())
164                .find(|c| c.kind() == "assignment" || c.kind() == "augmented_assignment");
165
166            if let Some(assignment) = assignment
167                && let Some(left) = assignment.child_by_field_name("left")
168                && let Ok(left_text) = left.utf8_text(content)
169                && left_text.trim() == "__all__"
170            {
171                return true;
172            }
173        }
174    }
175    false
176}
177
178/// Walk the tree and populate the staging graph.
179/// # Errors
180///
181/// Returns [`GraphBuilderError`] if graph operations fail or recursion depth exceeds the guard's limit.
182#[allow(clippy::too_many_lines)]
183fn walk_tree_for_graph(
184    node: Node,
185    content: &[u8],
186    ast_graph: &ASTGraph,
187    helper: &mut GraphBuildHelper,
188    has_all: bool,
189    guard: &mut sqry_core::query::security::RecursionGuard,
190    scope_tree: &mut local_scopes::PythonScopeTree,
191) -> GraphResult<()> {
192    guard.enter().map_err(|e| GraphBuilderError::ParseError {
193        span: Span::default(),
194        reason: format!("Recursion limit exceeded: {e}"),
195    })?;
196
197    match node.kind() {
198        "class_definition" => {
199            // Extract class name
200            if let Some(name_node) = node.child_by_field_name("name")
201                && let Ok(class_name) = name_node.utf8_text(content)
202            {
203                let span = span_from_node(node);
204
205                // Build qualified class name from scope
206                let qualified_name = class_name.to_string();
207
208                // Add class node
209                let class_id = helper.add_class(&qualified_name, Some(span));
210
211                // Process inheritance (base classes)
212                process_class_inheritance(node, content, class_id, helper);
213
214                // Note: Class body annotations are processed via normal recursion in walk_tree_for_graph
215
216                // Export public classes at module level (only if __all__ is not defined)
217                if !has_all && is_module_level(node) && is_public_name(class_name) {
218                    export_from_file_module(helper, class_id);
219                }
220            }
221        }
222        "expression_statement" => {
223            // Check for __all__ assignment (exports)
224            process_all_assignment(node, content, helper);
225
226            // Check for annotated assignments (type hints on variables)
227            process_annotated_assignment(node, content, ast_graph, helper);
228        }
229        "function_definition" => {
230            // Extract function context from AST graph
231            if let Some(call_context) = ast_graph.get_callable_context(node.id()) {
232                let span = span_from_node(node);
233
234                // Extract visibility from function name
235                let func_name = node
236                    .child_by_field_name("name")
237                    .and_then(|n| n.utf8_text(content).ok())
238                    .unwrap_or("");
239                let visibility = extract_visibility_from_name(func_name);
240
241                // Check if this is a property (has @property decorator)
242                let is_property = has_property_decorator(node, content);
243
244                // Extract return type annotation for signature
245                let return_type = extract_return_type_annotation(node, content);
246
247                // Add function/method/property node
248                let function_id = if is_property && call_context.is_method {
249                    // Property node
250                    helper.add_node_with_visibility(
251                        &call_context.qualified_name,
252                        Some(span),
253                        sqry_core::graph::unified::node::NodeKind::Property,
254                        Some(visibility),
255                    )
256                } else if call_context.is_method {
257                    // Regular method with signature
258                    if return_type.is_some() {
259                        helper.add_method_with_signature(
260                            &call_context.qualified_name,
261                            Some(span),
262                            call_context.is_async,
263                            false, // Python doesn't have static methods in the same way
264                            Some(visibility),
265                            return_type.as_deref(),
266                        )
267                    } else {
268                        helper.add_method_with_visibility(
269                            &call_context.qualified_name,
270                            Some(span),
271                            call_context.is_async,
272                            false,
273                            Some(visibility),
274                        )
275                    }
276                } else {
277                    // Regular function with signature
278                    if return_type.is_some() {
279                        helper.add_function_with_signature(
280                            &call_context.qualified_name,
281                            Some(span),
282                            call_context.is_async,
283                            false, // Python doesn't have unsafe
284                            Some(visibility),
285                            return_type.as_deref(),
286                        )
287                    } else {
288                        helper.add_function_with_visibility(
289                            &call_context.qualified_name,
290                            Some(span),
291                            call_context.is_async,
292                            false,
293                            Some(visibility),
294                        )
295                    }
296                };
297
298                // Check for HTTP route decorators (Flask/FastAPI)
299                if let Some((http_method, route_path)) = extract_route_decorator_info(node, content)
300                {
301                    let endpoint_name = format!("route::{http_method}::{route_path}");
302                    let endpoint_id = helper.add_endpoint(&endpoint_name, Some(span));
303                    helper.add_contains_edge(endpoint_id, function_id);
304                }
305
306                // Process parameters to create TypeOf and Reference edges for type hints
307                process_function_parameters(node, content, ast_graph, helper);
308
309                // Export public functions at module level (not methods, only if __all__ is not defined)
310                if !has_all
311                    && !call_context.is_method
312                    && is_module_level(node)
313                    && let Some(name_node) = node.child_by_field_name("name")
314                    && let Ok(func_name) = name_node.utf8_text(content)
315                    && is_public_name(func_name)
316                {
317                    export_from_file_module(helper, function_id);
318                }
319            }
320        }
321        "call" => {
322            // Check for FFI patterns first (ctypes, cffi)
323            let is_ffi = build_ffi_call_edge(ast_graph, node, content, helper)?;
324            if !is_ffi {
325                // Not an FFI call - build regular call edge
326                if let Ok(Some((caller_qname, callee_qname, argument_count, is_awaited))) =
327                    build_call_for_staging(ast_graph, node, content)
328                {
329                    // Ensure both nodes exist
330                    let call_context = ast_graph.get_callable_context(node.id());
331                    let is_async = call_context.is_some_and(|c| c.is_async);
332
333                    let source_id = helper.ensure_function(&caller_qname, None, is_async, false);
334                    let target_id = helper.ensure_function(&callee_qname, None, false, false);
335
336                    // Add call edge
337                    let call_span = span_from_node(node);
338                    let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
339                    helper.add_call_edge_full_with_span(
340                        source_id,
341                        target_id,
342                        argument_count,
343                        is_awaited,
344                        vec![call_span],
345                    );
346                }
347            }
348        }
349        "import_statement" | "import_from_statement" => {
350            // Build import edge
351            if let Ok(Some((from_qname, to_qname))) =
352                build_import_for_staging(node, content, helper)
353            {
354                // Ensure both module nodes exist
355                let from_id = helper.add_import(&from_qname, None);
356                let to_id = helper.add_import(&to_qname, Some(span_from_node(node)));
357
358                // Add import edge
359                helper.add_import_edge(from_id, to_id);
360
361                // Check if this imports a known native C extension module
362                if is_native_extension_import(&to_qname) {
363                    build_native_import_ffi_edge(&to_qname, node, helper);
364                }
365            }
366        }
367        "identifier" => {
368            // Local variable reference tracking
369            local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
370        }
371        _ => {}
372    }
373
374    // Recurse into children
375    let mut cursor = node.walk();
376    for child in node.children(&mut cursor) {
377        walk_tree_for_graph(
378            child, content, ast_graph, helper, has_all, guard, scope_tree,
379        )?;
380    }
381
382    guard.exit();
383    Ok(())
384}
385
386/// Build call edge information for the staging graph.
387fn build_call_for_staging(
388    ast_graph: &ASTGraph,
389    call_node: Node<'_>,
390    content: &[u8],
391) -> GraphResult<Option<(String, String, usize, bool)>> {
392    // Get or create module-level context for top-level calls
393    let module_context;
394    let call_context = if let Some(ctx) = ast_graph.get_callable_context(call_node.id()) {
395        ctx
396    } else {
397        // Create synthetic module-level context for top-level calls
398        module_context = CallContext {
399            qualified_name: "<module>".to_string(),
400            span: (0, content.len()),
401            is_async: false,
402            is_method: false,
403            class_name: None,
404        };
405        &module_context
406    };
407
408    let Some(callee_expr) = call_node.child_by_field_name("function") else {
409        return Ok(None);
410    };
411
412    let callee_text = callee_expr
413        .utf8_text(content)
414        .map_err(|_| GraphBuilderError::ParseError {
415            span: span_from_node(call_node),
416            reason: "failed to read call expression".to_string(),
417        })?
418        .trim()
419        .to_string();
420
421    if callee_text.is_empty() {
422        return Ok(None);
423    }
424
425    let callee_simple = simple_name(&callee_text);
426    if callee_simple.is_empty() {
427        return Ok(None);
428    }
429
430    // Derive qualified callee name with proper self resolution
431    let caller_qname = call_context.qualified_name();
432    let target_qname = if let Some(method_name) = callee_text.strip_prefix("self.") {
433        // Resolve self.method() to ClassName.method()
434        if let Some(class_name) = &call_context.class_name {
435            format!("{}.{}", class_name, simple_name(method_name))
436        } else {
437            callee_simple.to_string()
438        }
439    } else {
440        callee_simple.to_string()
441    };
442
443    let argument_count = count_arguments(call_node);
444    let is_awaited = is_awaited_call(call_node);
445    Ok(Some((
446        caller_qname,
447        target_qname,
448        argument_count,
449        is_awaited,
450    )))
451}
452
453/// Build import edge information for the staging graph.
454fn build_import_for_staging(
455    import_node: Node<'_>,
456    content: &[u8],
457    helper: &GraphBuildHelper,
458) -> GraphResult<Option<(String, String)>> {
459    // Extract the raw module name from the AST
460    let raw_module_name = if import_node.kind() == "import_statement" {
461        import_node
462            .child_by_field_name("name")
463            .and_then(|n| extract_module_name(n, content))
464    } else if import_node.kind() == "import_from_statement" {
465        import_node
466            .child_by_field_name("module_name")
467            .and_then(|n| extract_module_name(n, content))
468    } else {
469        None
470    };
471
472    // Handle relative imports with no module name
473    let module_name = if raw_module_name.is_none() && import_node.kind() == "import_from_statement"
474    {
475        if let Ok(import_text) = import_node.utf8_text(content) {
476            if let Some(from_idx) = import_text.find("from") {
477                if let Some(import_idx) = import_text.find("import") {
478                    let between = import_text[from_idx + 4..import_idx].trim();
479                    if between.starts_with('.') {
480                        Some(between.to_string())
481                    } else {
482                        None
483                    }
484                } else {
485                    None
486                }
487            } else {
488                None
489            }
490        } else {
491            None
492        }
493    } else {
494        raw_module_name
495    };
496
497    let Some(module_name) = module_name else {
498        return Ok(None);
499    };
500
501    if module_name.is_empty() {
502        return Ok(None);
503    }
504
505    // Resolve the import path to a canonical module identifier
506    let resolved_path = sqry_core::graph::resolve_python_import(
507        std::path::Path::new(helper.file_path()),
508        &module_name,
509        import_node.kind() == "import_from_statement",
510    )?;
511
512    // Return from/to qualified names
513    Ok(Some((helper.file_path().to_string(), resolved_path)))
514}
515
516fn span_from_node(node: Node<'_>) -> Span {
517    let start = node.start_position();
518    let end = node.end_position();
519    Span::new(
520        sqry_core::graph::node::Position::new(start.row, start.column),
521        sqry_core::graph::node::Position::new(end.row, end.column),
522    )
523}
524
525fn count_arguments(call_node: Node<'_>) -> usize {
526    call_node
527        .child_by_field_name("arguments")
528        .map_or(0, |args| {
529            args.named_children(&mut args.walk())
530                .filter(|child| {
531                    // Count actual arguments, not commas or parentheses
532                    !matches!(child.kind(), "," | "(" | ")")
533                })
534                .count()
535        })
536}
537
538fn is_awaited_call(call_node: Node<'_>) -> bool {
539    let mut current = call_node.parent();
540    while let Some(node) = current {
541        let kind = node.kind();
542        if kind == "await" || kind == "await_expression" {
543            return true;
544        }
545        current = node.parent();
546    }
547    false
548}
549
550/// Extract the simple name from a dotted identifier (for general call targets).
551///
552/// Takes the last component after splitting by dots.
553/// Used for qualified names like "module.func" → "func" or "obj.method" → "method".
554fn simple_name(qualified: &str) -> &str {
555    qualified.split('.').next_back().unwrap_or(qualified)
556}
557
558/// Extract a simple library name from an FFI library path.
559///
560/// For library paths with file extensions, extracts the base name before the extension.
561/// This prevents different libraries with the same extension (lib1.so, lib2.so) from
562/// colliding as duplicate "so" targets.
563///
564/// Handles:
565/// - Full paths: "/opt/v1.2/libfoo.so" → "libfoo"
566/// - Relative paths: "libs/lib1.so" → "lib1"
567/// - Versioned libs: "libc.so.6" → "libc"
568/// - Simple names: "kernel32" → "kernel32"
569/// - Variable refs: "$libname" → "$libname"
570fn ffi_library_simple_name(library_path: &str) -> String {
571    use std::path::Path;
572
573    // Strip directory components first (handles /opt/v1.2/libfoo.so)
574    let filename = Path::new(library_path)
575        .file_name()
576        .and_then(|f| f.to_str())
577        .unwrap_or(library_path);
578
579    // Handle versioned .so files first (libc.so.6 → libc)
580    if let Some(so_pos) = filename.find(".so.") {
581        return filename[..so_pos].to_string();
582    }
583
584    // Handle standard library extensions
585    if let Some(dot_pos) = filename.find('.') {
586        let extension = &filename[dot_pos + 1..];
587
588        // Check for known library extensions
589        if extension == "so" || extension == "dll" || extension == "dylib" {
590            // Extract base name before extension
591            return filename[..dot_pos].to_string();
592        }
593    }
594
595    // No library extension found - return filename as-is
596    filename.to_string()
597}
598
599/// Check if a name is public (does not start with underscore).
600///
601/// In Python, names starting with a single underscore are considered private by convention.
602/// Names starting with double underscores trigger name mangling in classes.
603/// Public names do not start with an underscore.
604fn is_public_name(name: &str) -> bool {
605    !name.starts_with('_')
606}
607
608/// Check if a node is at module level (direct child of the module body).
609///
610/// In tree-sitter Python AST, module-level items are direct children of the root "module" node.
611/// We check if the parent is "module" to determine module-level scope.
612fn is_module_level(node: Node<'_>) -> bool {
613    // Walk up the tree to find the immediate container
614    let mut current = node.parent();
615    while let Some(parent) = current {
616        match parent.kind() {
617            "module" => return true,
618            "function_definition" | "class_definition" => return false,
619            _ => current = parent.parent(),
620        }
621    }
622    false
623}
624
625/// Export a symbol from the file module.
626///
627/// File-level module name for exports/imports.
628/// Distinct from `<module>` to avoid conflicts with top-level call context.
629const FILE_MODULE_NAME: &str = "<file_module>";
630
631fn export_from_file_module(
632    helper: &mut GraphBuildHelper,
633    exported: sqry_core::graph::unified::node::NodeId,
634) {
635    let module_id = helper.add_module(FILE_MODULE_NAME, None);
636    helper.add_export_edge(module_id, exported);
637}
638
639/// Extract module name from a `dotted_name`, `aliased_import`, or `relative_import` node
640///
641/// For `import numpy as np`, the "name" field is an `aliased_import` node with structure:
642/// `aliased_import { name: dotted_name("numpy"), alias: identifier("np") }`
643/// We need to extract just "numpy", not "numpy as np".
644fn extract_module_name(node: Node<'_>, content: &[u8]) -> Option<String> {
645    // Handle aliased imports: `import numpy as np` -> extract "numpy"
646    if node.kind() == "aliased_import" {
647        // The "name" field of aliased_import contains the actual module name
648        return node
649            .child_by_field_name("name")
650            .and_then(|name_node| name_node.utf8_text(content).ok())
651            .map(std::string::ToString::to_string);
652    }
653
654    // Regular dotted_name or identifier
655    node.utf8_text(content)
656        .ok()
657        .map(std::string::ToString::to_string)
658}
659
660// ============================================================================
661// Exports - __all__ assignment handling
662// ============================================================================
663
664/// Process `__all__ = ['name1', 'name2']` assignments to create export edges.
665///
666/// Python's `__all__` list explicitly defines the public API of a module.
667/// Each name in the list gets an Export edge from the module to the exported symbol.
668fn process_all_assignment(node: Node<'_>, content: &[u8], helper: &mut GraphBuildHelper) {
669    // expression_statement contains an assignment child
670    let assignment = node
671        .children(&mut node.walk())
672        .find(|child| child.kind() == "assignment" || child.kind() == "augmented_assignment");
673
674    let Some(assignment) = assignment else {
675        return;
676    };
677
678    // Check if left side is __all__
679    let left = assignment.child_by_field_name("left");
680    let Some(left) = left else {
681        return;
682    };
683
684    let Ok(left_text) = left.utf8_text(content) else {
685        return;
686    };
687
688    if left_text.trim() != "__all__" {
689        return;
690    }
691
692    // Get the right side (should be a list)
693    let right = assignment.child_by_field_name("right");
694    let Some(right) = right else {
695        return;
696    };
697
698    // Handle list or tuple literal (both valid for __all__)
699    if right.kind() == "list" || right.kind() == "tuple" {
700        process_all_list(right, content, helper);
701    }
702}
703
704/// Process a list/tuple of exported names from __all__.
705fn process_all_list(list_node: Node<'_>, content: &[u8], helper: &mut GraphBuildHelper) {
706    for child in list_node.children(&mut list_node.walk()) {
707        // Look for string literals
708        if child.kind() == "string"
709            && let Some(export_name) = extract_string_content(child, content)
710            && !export_name.is_empty()
711        {
712            // Create a node for the exported symbol
713            // We use add_function here as a generic symbol; the actual type
714            // will be resolved later by cross-file analysis
715            let span = span_from_node(child);
716            let export_id = helper.add_function(&export_name, Some(span), false, false);
717
718            // Add export edge (Direct export, no alias for Python __all__)
719            export_from_file_module(helper, export_id);
720        }
721    }
722}
723
724/// Extract the content of a string literal node (removing quotes).
725fn extract_string_content(string_node: Node<'_>, content: &[u8]) -> Option<String> {
726    // String nodes contain string_content or string_start/string_content/string_end
727    // Try to get the full text and strip quotes
728    let Ok(text) = string_node.utf8_text(content) else {
729        return None;
730    };
731
732    let text = text.trim();
733
734    // Handle various Python string formats: 'x', "x", '''x''', """x""", r'x', etc.
735    let stripped = text
736        .trim_start_matches(|c: char| {
737            c == 'r'
738                || c == 'b'
739                || c == 'f'
740                || c == 'u'
741                || c == 'R'
742                || c == 'B'
743                || c == 'F'
744                || c == 'U'
745        })
746        .trim_start_matches("'''")
747        .trim_end_matches("'''")
748        .trim_start_matches("\"\"\"")
749        .trim_end_matches("\"\"\"")
750        .trim_start_matches('\'')
751        .trim_end_matches('\'')
752        .trim_start_matches('"')
753        .trim_end_matches('"');
754
755    Some(stripped.to_string())
756}
757
758// ============================================================================
759// OOP - Inheritance handling
760// ============================================================================
761
762/// Process class inheritance to create Inherits edges.
763///
764/// Python supports multiple inheritance: `class Child(Parent1, Parent2):`
765/// Each base class gets an Inherits edge from the child class.
766fn process_class_inheritance(
767    class_node: Node<'_>,
768    content: &[u8],
769    class_id: UnifiedNodeId,
770    helper: &mut GraphBuildHelper,
771) {
772    // In Python AST, base classes are in the superclasses field (argument_list)
773    // class_definition has a "superclasses" field containing argument_list
774    let superclasses = class_node.child_by_field_name("superclasses");
775
776    let Some(superclasses) = superclasses else {
777        return;
778    };
779
780    // argument_list contains the base classes
781    for child in superclasses.children(&mut superclasses.walk()) {
782        if child.kind() == "keyword_argument" {
783            // Skip keyword arguments like metaclass=ABCMeta.
784            continue;
785        }
786
787        match child.kind() {
788            "identifier" => {
789                // Simple base class: class Child(Parent):
790                if let Ok(base_name) = child.utf8_text(content) {
791                    let base_name = base_name.trim();
792                    if !base_name.is_empty() {
793                        let span = span_from_node(child);
794                        let base_id = helper.add_class(base_name, Some(span));
795                        helper.add_inherits_edge(class_id, base_id);
796                    }
797                }
798            }
799            "attribute" => {
800                // Qualified base class: class Child(module.Parent):
801                if let Ok(base_name) = child.utf8_text(content) {
802                    let base_name = base_name.trim();
803                    if !base_name.is_empty() {
804                        let span = span_from_node(child);
805                        let base_id = helper.add_class(base_name, Some(span));
806                        helper.add_inherits_edge(class_id, base_id);
807                    }
808                }
809            }
810            "call" => {
811                // Parameterized base class with call syntax: class Child(SomeBase(arg)):
812                // Extract the function being called
813                if let Some(func) = child.child_by_field_name("function")
814                    && let Ok(base_name) = func.utf8_text(content)
815                {
816                    let base_name = base_name.trim();
817                    if !base_name.is_empty() {
818                        let span = span_from_node(child);
819                        let base_id = helper.add_class(base_name, Some(span));
820                        helper.add_inherits_edge(class_id, base_id);
821                    }
822                }
823            }
824            "subscript" => {
825                // Generic base class: class Child(Generic[T]): or class Child(List[int]):
826                // Extract the base type from the subscript (value field)
827                if let Some(value) = child.child_by_field_name("value")
828                    && let Ok(base_name) = value.utf8_text(content)
829                {
830                    let base_name = base_name.trim();
831                    if !base_name.is_empty() {
832                        let span = span_from_node(child);
833                        let base_id = helper.add_class(base_name, Some(span));
834                        helper.add_inherits_edge(class_id, base_id);
835                    }
836                }
837            }
838            _ => {}
839        }
840    }
841}
842
843// ============================================================================
844// AST Graph - tracks callable contexts (functions, methods, classes)
845// ============================================================================
846
847#[derive(Debug, Clone)]
848struct CallContext {
849    qualified_name: String,
850    #[allow(dead_code)] // Reserved for scope analysis
851    span: (usize, usize),
852    is_async: bool,
853    is_method: bool,
854    class_name: Option<String>,
855}
856
857impl CallContext {
858    fn qualified_name(&self) -> String {
859        self.qualified_name.clone()
860    }
861}
862
863struct ASTGraph {
864    contexts: Vec<CallContext>,
865    node_to_context: HashMap<usize, usize>,
866}
867
868impl ASTGraph {
869    fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Result<Self, String> {
870        let mut contexts = Vec::new();
871        let mut node_to_context = HashMap::new();
872        let mut scope_stack: Vec<String> = Vec::new();
873        let mut class_stack: Vec<String> = Vec::new();
874
875        walk_ast(
876            tree.root_node(),
877            content,
878            &mut contexts,
879            &mut node_to_context,
880            &mut scope_stack,
881            &mut class_stack,
882            max_depth,
883        )?;
884
885        Ok(Self {
886            contexts,
887            node_to_context,
888        })
889    }
890
891    #[allow(dead_code)] // Reserved for future context queries
892    fn contexts(&self) -> &[CallContext] {
893        &self.contexts
894    }
895
896    fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
897        self.node_to_context
898            .get(&node_id)
899            .and_then(|idx| self.contexts.get(*idx))
900    }
901}
902
903fn walk_ast(
904    node: Node,
905    content: &[u8],
906    contexts: &mut Vec<CallContext>,
907    node_to_context: &mut HashMap<usize, usize>,
908    scope_stack: &mut Vec<String>,
909    class_stack: &mut Vec<String>,
910    max_depth: usize,
911) -> Result<(), String> {
912    if scope_stack.len() > max_depth {
913        return Ok(());
914    }
915
916    match node.kind() {
917        "class_definition" => {
918            let name_node = node
919                .child_by_field_name("name")
920                .ok_or_else(|| "class_definition missing name".to_string())?;
921            let class_name = name_node
922                .utf8_text(content)
923                .map_err(|_| "failed to read class name".to_string())?;
924
925            // Build qualified class name
926            let qualified_class = if scope_stack.is_empty() {
927                class_name.to_string()
928            } else {
929                format!("{}.{}", scope_stack.join("."), class_name)
930            };
931
932            class_stack.push(qualified_class.clone());
933            scope_stack.push(class_name.to_string());
934
935            // Recurse into class body
936            if let Some(body) = node.child_by_field_name("body") {
937                let mut cursor = body.walk();
938                for child in body.children(&mut cursor) {
939                    walk_ast(
940                        child,
941                        content,
942                        contexts,
943                        node_to_context,
944                        scope_stack,
945                        class_stack,
946                        max_depth,
947                    )?;
948                }
949            }
950
951            class_stack.pop();
952            scope_stack.pop();
953        }
954        "function_definition" => {
955            let name_node = node
956                .child_by_field_name("name")
957                .ok_or_else(|| "function_definition missing name".to_string())?;
958            let func_name = name_node
959                .utf8_text(content)
960                .map_err(|_| "failed to read function name".to_string())?;
961
962            // Check if async
963            let is_async = node
964                .children(&mut node.walk())
965                .any(|child| child.kind() == "async");
966
967            // Build qualified function name
968            let qualified_func = if scope_stack.is_empty() {
969                func_name.to_string()
970            } else {
971                format!("{}.{}", scope_stack.join("."), func_name)
972            };
973
974            // Determine if this is a method (inside a class)
975            let is_method = !class_stack.is_empty();
976            let class_name = class_stack.last().cloned();
977
978            let context_idx = contexts.len();
979            contexts.push(CallContext {
980                qualified_name: qualified_func.clone(),
981                span: (node.start_byte(), node.end_byte()),
982                is_async,
983                is_method,
984                class_name,
985            });
986
987            // Associate the function definition node itself with this context
988            // This is required so walk_tree_for_graph can find the context
989            node_to_context.insert(node.id(), context_idx);
990
991            // Associate all descendants with this context
992            if let Some(body) = node.child_by_field_name("body") {
993                associate_descendants(body, context_idx, node_to_context);
994            }
995
996            scope_stack.push(func_name.to_string());
997
998            // Recurse into function body to find nested functions
999            if let Some(body) = node.child_by_field_name("body") {
1000                let mut cursor = body.walk();
1001                for child in body.children(&mut cursor) {
1002                    walk_ast(
1003                        child,
1004                        content,
1005                        contexts,
1006                        node_to_context,
1007                        scope_stack,
1008                        class_stack,
1009                        max_depth,
1010                    )?;
1011                }
1012            }
1013
1014            scope_stack.pop();
1015        }
1016        _ => {
1017            // Recurse into children for other node types
1018            let mut cursor = node.walk();
1019            for child in node.children(&mut cursor) {
1020                walk_ast(
1021                    child,
1022                    content,
1023                    contexts,
1024                    node_to_context,
1025                    scope_stack,
1026                    class_stack,
1027                    max_depth,
1028                )?;
1029            }
1030        }
1031    }
1032
1033    Ok(())
1034}
1035
1036fn associate_descendants(
1037    node: Node,
1038    context_idx: usize,
1039    node_to_context: &mut HashMap<usize, usize>,
1040) {
1041    node_to_context.insert(node.id(), context_idx);
1042
1043    let mut stack = vec![node];
1044    while let Some(current) = stack.pop() {
1045        node_to_context.insert(current.id(), context_idx);
1046
1047        let mut cursor = current.walk();
1048        for child in current.children(&mut cursor) {
1049            stack.push(child);
1050        }
1051    }
1052}
1053
1054// ============================================================================
1055// FFI Detection - ctypes, cffi, and C extensions
1056// ============================================================================
1057
1058/// Build FFI edges for call expressions.
1059///
1060/// Detects Python FFI patterns:
1061/// - `ctypes.CDLL('libfoo.so')` / `ctypes.cdll.LoadLibrary('libfoo.so')`
1062/// - `ctypes.WinDLL('kernel32')` / `ctypes.windll.kernel32`
1063/// - `ctypes.PyDLL('libpython.so')`
1064/// - `cffi.FFI().dlopen('libfoo.so')`
1065/// - `ffi.dlopen('libfoo.so')`
1066///
1067/// Returns true if an FFI edge was created, false otherwise.
1068fn build_ffi_call_edge(
1069    ast_graph: &ASTGraph,
1070    call_node: Node<'_>,
1071    content: &[u8],
1072    helper: &mut GraphBuildHelper,
1073) -> GraphResult<bool> {
1074    let Some(callee_expr) = call_node.child_by_field_name("function") else {
1075        return Ok(false);
1076    };
1077
1078    let callee_text = callee_expr
1079        .utf8_text(content)
1080        .map_err(|_| GraphBuilderError::ParseError {
1081            span: span_from_node(call_node),
1082            reason: "failed to read call expression".to_string(),
1083        })?
1084        .trim();
1085
1086    // Check for ctypes library loading patterns
1087    if is_ctypes_load_call(callee_text) {
1088        return Ok(build_ctypes_ffi_edge(
1089            ast_graph,
1090            call_node,
1091            content,
1092            callee_text,
1093            helper,
1094        ));
1095    }
1096
1097    // Check for cffi dlopen patterns
1098    if is_cffi_dlopen_call(callee_text) {
1099        return Ok(build_cffi_ffi_edge(ast_graph, call_node, content, helper));
1100    }
1101
1102    Ok(false)
1103}
1104
1105/// Check if the callee is a ctypes library loading function.
1106///
1107/// Narrowed patterns to reduce false positives - only match explicit ctypes paths.
1108/// Previous: `callee_text.ends_with(".LoadLibrary")` matched too broadly.
1109///
1110/// Note: `ctypes.cdll.kernel32` style attribute access patterns are not detected
1111/// because they're attribute access (not function calls). We only detect explicit
1112/// library loading function calls like CDLL('lib.so').
1113fn is_ctypes_load_call(callee_text: &str) -> bool {
1114    // Direct ctypes constructors (fully qualified)
1115    callee_text == "ctypes.CDLL"
1116        || callee_text == "ctypes.WinDLL"
1117        || callee_text == "ctypes.OleDLL"
1118        || callee_text == "ctypes.PyDLL"
1119        // ctypes.cdll/windll LoadLibrary (fully qualified)
1120        || callee_text == "ctypes.cdll.LoadLibrary"
1121        || callee_text == "ctypes.windll.LoadLibrary"
1122        || callee_text == "ctypes.oledll.LoadLibrary"
1123        // After `from ctypes import *` or `from ctypes import CDLL, etc.`
1124        || callee_text == "CDLL"
1125        || callee_text == "WinDLL"
1126        || callee_text == "OleDLL"
1127        || callee_text == "PyDLL"
1128        // After `from ctypes import cdll` or similar
1129        || callee_text == "cdll.LoadLibrary"
1130        || callee_text == "windll.LoadLibrary"
1131        || callee_text == "oledll.LoadLibrary"
1132}
1133
1134/// Check if the callee is a cffi dlopen function.
1135///
1136/// Narrowed patterns to reduce false positives - only match known cffi patterns.
1137/// Previous: `callee_text.ends_with(".dlopen")` matched too broadly.
1138fn is_cffi_dlopen_call(callee_text: &str) -> bool {
1139    // Common cffi FFI variable names followed by dlopen
1140    callee_text == "ffi.dlopen"
1141        || callee_text == "cffi.dlopen"
1142        || callee_text == "_ffi.dlopen"
1143        // FFI() constructor followed by dlopen (chained call)
1144        // This pattern typically appears as: FFI().dlopen('lib.so')
1145        // In tree-sitter, the callee text would be the method access part
1146        // After `from cffi import FFI`
1147        || callee_text == "FFI().dlopen"
1148}
1149
1150/// Build FFI edge for ctypes library loading.
1151fn build_ctypes_ffi_edge(
1152    ast_graph: &ASTGraph,
1153    call_node: Node<'_>,
1154    content: &[u8],
1155    callee_text: &str,
1156    helper: &mut GraphBuildHelper,
1157) -> bool {
1158    // Get caller context
1159    let caller_id = get_ffi_caller_node_id(ast_graph, call_node, content, helper);
1160
1161    // Determine FFI convention based on the ctypes type
1162    let convention = if callee_text.contains("WinDLL")
1163        || callee_text.contains("windll")
1164        || callee_text.contains("OleDLL")
1165    {
1166        FfiConvention::Stdcall
1167    } else {
1168        FfiConvention::C
1169    };
1170
1171    // Try to extract library name from first argument
1172    let library_name = extract_ffi_library_name(call_node, content)
1173        .unwrap_or_else(|| "ctypes::unknown".to_string());
1174
1175    let ffi_name = format!("native::{}", ffi_library_simple_name(&library_name));
1176    let ffi_node_id = helper.add_module(&ffi_name, Some(span_from_node(call_node)));
1177
1178    // Add FFI edge
1179    helper.add_ffi_edge(caller_id, ffi_node_id, convention);
1180
1181    true
1182}
1183
1184/// Build FFI edge for cffi dlopen.
1185fn build_cffi_ffi_edge(
1186    ast_graph: &ASTGraph,
1187    call_node: Node<'_>,
1188    content: &[u8],
1189    helper: &mut GraphBuildHelper,
1190) -> bool {
1191    // Get caller context
1192    let caller_id = get_ffi_caller_node_id(ast_graph, call_node, content, helper);
1193
1194    // Try to extract library name from first argument
1195    let library_name =
1196        extract_ffi_library_name(call_node, content).unwrap_or_else(|| "cffi::unknown".to_string());
1197
1198    let ffi_name = format!("native::{}", ffi_library_simple_name(&library_name));
1199    let ffi_node_id = helper.add_module(&ffi_name, Some(span_from_node(call_node)));
1200
1201    // cffi uses C calling convention
1202    helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
1203
1204    true
1205}
1206
1207/// Get the caller node ID for FFI edges.
1208fn get_ffi_caller_node_id(
1209    ast_graph: &ASTGraph,
1210    node: Node<'_>,
1211    content: &[u8],
1212    helper: &mut GraphBuildHelper,
1213) -> UnifiedNodeId {
1214    let module_context;
1215    let call_context = if let Some(ctx) = ast_graph.get_callable_context(node.id()) {
1216        ctx
1217    } else {
1218        module_context = CallContext {
1219            qualified_name: "<module>".to_string(),
1220            span: (0, content.len()),
1221            is_async: false,
1222            is_method: false,
1223            class_name: None,
1224        };
1225        &module_context
1226    };
1227
1228    let caller_span = Some(Span::from_bytes(call_context.span.0, call_context.span.1));
1229    helper.ensure_function(
1230        &call_context.qualified_name(),
1231        caller_span,
1232        call_context.is_async,
1233        false,
1234    )
1235}
1236
1237/// Extract the library name from the first argument of a call.
1238fn extract_ffi_library_name(call_node: Node<'_>, content: &[u8]) -> Option<String> {
1239    let args = call_node.child_by_field_name("arguments")?;
1240
1241    let mut cursor = args.walk();
1242    let first_arg = args
1243        .children(&mut cursor)
1244        .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
1245
1246    // Handle string literals
1247    if first_arg.kind() == "string" {
1248        return extract_string_content(first_arg, content);
1249    }
1250
1251    // Handle identifiers (variable names) - we can't resolve them statically
1252    if first_arg.kind() == "identifier" {
1253        let text = first_arg.utf8_text(content).ok()?;
1254        return Some(format!("${}", text.trim())); // Mark as variable reference
1255    }
1256
1257    None
1258}
1259
1260/// Check if an import statement imports a known native extension module.
1261///
1262/// This detects patterns like:
1263/// - `import numpy` (known C extension)
1264/// - `from numpy import array` (known C extension)
1265/// - `import _sqlite3` (private C module)
1266fn is_native_extension_import(module_name: &str) -> bool {
1267    // Private C modules (underscore prefix)
1268    if module_name.starts_with('_') && !module_name.starts_with("__") {
1269        return true;
1270    }
1271
1272    // Check against known modules
1273    let base_module = module_name.split('.').next().unwrap_or(module_name);
1274
1275    STD_C_MODULES.contains(&base_module) || THIRD_PARTY_C_PACKAGES.contains(&base_module)
1276}
1277
1278/// Build FFI edge for native extension import.
1279fn build_native_import_ffi_edge(
1280    module_name: &str,
1281    import_node: Node<'_>,
1282    helper: &mut GraphBuildHelper,
1283) {
1284    // Create module node for the importing file
1285    let file_path = helper.file_path().to_string();
1286    let importer_id = helper.add_module(&file_path, None);
1287
1288    // Create node for the native module
1289    let ffi_name = format!("native::{}", simple_name(module_name));
1290    let ffi_node_id = helper.add_module(&ffi_name, Some(span_from_node(import_node)));
1291
1292    // Add FFI edge (C convention for Python C extensions)
1293    helper.add_ffi_edge(importer_id, ffi_node_id, FfiConvention::C);
1294}
1295
1296// ============================================================================
1297// HTTP Route Endpoint Detection - Flask/FastAPI decorators
1298// ============================================================================
1299
1300/// HTTP methods recognized in route decorators.
1301const ROUTE_METHOD_NAMES: &[&str] = &["get", "post", "put", "delete", "patch"];
1302
1303/// Receiver names recognized as route-capable objects.
1304///
1305/// `Flask` uses `app` or `blueprint`, `FastAPI` uses `app` or `router`.
1306const ROUTE_RECEIVER_NAMES: &[&str] = &["app", "router", "blueprint"];
1307
1308/// Extract HTTP route information from Flask/FastAPI-style decorators on a function.
1309///
1310/// Checks whether the given `function_definition` node is wrapped in a `decorated_definition`
1311/// and whether any of its decorators match known route patterns:
1312///
1313/// - `@app.route('/path')` or `@app.route('/path', methods=['GET'])` -- GET by default
1314/// - `@app.get('/path')` / `@app.post('/path')` / `@app.put('/path')` / etc.
1315/// - `@router.get('/path')` (`FastAPI`)
1316/// - `@blueprint.route('/path')` (Flask blueprints)
1317///
1318/// Returns `Some((method, path))` where `method` is the uppercased HTTP method and
1319/// `path` is the route path string, or `None` if no route decorator is found.
1320fn extract_route_decorator_info(func_node: Node<'_>, content: &[u8]) -> Option<(String, String)> {
1321    // The function_definition must be a child of decorated_definition
1322    let parent = func_node.parent()?;
1323    if parent.kind() != "decorated_definition" {
1324        return None;
1325    }
1326
1327    // Iterate through decorator children of the decorated_definition
1328    let mut cursor = parent.walk();
1329    for child in parent.children(&mut cursor) {
1330        if child.kind() != "decorator" {
1331            continue;
1332        }
1333
1334        let Ok(decorator_text) = child.utf8_text(content) else {
1335            continue;
1336        };
1337        let decorator_text = decorator_text.trim();
1338
1339        // Strip the leading '@'
1340        let without_at = decorator_text.strip_prefix('@')?;
1341
1342        // Try to parse as a route decorator
1343        if let Some(result) = parse_route_decorator_text(without_at) {
1344            return Some(result);
1345        }
1346    }
1347
1348    None
1349}
1350
1351/// Parse a single decorator text (without the leading `@`) to extract route information.
1352///
1353/// Recognized patterns:
1354/// - `app.route('/path')` or `app.route('/path', methods=['POST'])`
1355/// - `app.get('/path')` / `router.post('/path')` / `blueprint.delete('/path')`
1356///
1357/// Returns `Some((HTTP_METHOD, path))` or `None`.
1358fn parse_route_decorator_text(text: &str) -> Option<(String, String)> {
1359    // Split into receiver.method and argument portion
1360    // e.g. "app.route('/api/users')" -> ("app.route", "'/api/users')")
1361    let paren_pos = text.find('(')?;
1362    let accessor = &text[..paren_pos];
1363    let args_text = &text[paren_pos + 1..];
1364
1365    // Split accessor into receiver and method_name
1366    let dot_pos = accessor.rfind('.')?;
1367    let receiver = &accessor[..dot_pos];
1368    let method_name = &accessor[dot_pos + 1..];
1369
1370    // Check that the receiver is a known route-capable object.
1371    // Allow dotted receivers (e.g., "api.v1") as long as the final segment matches.
1372    let receiver_base = receiver.rsplit('.').next().unwrap_or(receiver);
1373    if !ROUTE_RECEIVER_NAMES.contains(&receiver_base) {
1374        return None;
1375    }
1376
1377    // Extract the route path from the first argument (string literal)
1378    let path = extract_path_from_decorator_args(args_text)?;
1379
1380    // Determine the HTTP method
1381    let method_lower = method_name.to_ascii_lowercase();
1382    if ROUTE_METHOD_NAMES.contains(&method_lower.as_str()) {
1383        // Direct method decorator: @app.get('/path') -> GET
1384        return Some((method_lower.to_ascii_uppercase(), path));
1385    }
1386
1387    if method_lower == "route" {
1388        // Generic route decorator: @app.route('/path', methods=['POST'])
1389        let http_method = extract_method_from_route_args(args_text);
1390        return Some((http_method, path));
1391    }
1392
1393    None
1394}
1395
1396/// Extract the route path string from decorator arguments text.
1397///
1398/// The `args_text` parameter is everything after the opening parenthesis of the decorator call,
1399/// e.g. `'/api/users', methods=['GET'])` or `"/api/items")`.
1400///
1401/// Returns the path string with quotes stripped, or `None` if no path is found.
1402fn extract_path_from_decorator_args(args_text: &str) -> Option<String> {
1403    let trimmed = args_text.trim();
1404
1405    // Find the first string literal (single or double quoted)
1406    let (quote_char, start_pos) = {
1407        let single_pos = trimmed.find('\'');
1408        let double_pos = trimmed.find('"');
1409        match (single_pos, double_pos) {
1410            (Some(s), Some(d)) => {
1411                if s < d {
1412                    ('\'', s)
1413                } else {
1414                    ('"', d)
1415                }
1416            }
1417            (Some(s), None) => ('\'', s),
1418            (None, Some(d)) => ('"', d),
1419            (None, None) => return None,
1420        }
1421    };
1422
1423    // Find the closing quote
1424    let after_open = start_pos + 1;
1425    let close_pos = trimmed[after_open..].find(quote_char)?;
1426    let path = &trimmed[after_open..after_open + close_pos];
1427
1428    if path.is_empty() {
1429        return None;
1430    }
1431
1432    Some(path.to_string())
1433}
1434
1435/// Extract the HTTP method from `@app.route('/path', methods=['POST'])` style arguments.
1436///
1437/// Looks for a `methods=` keyword argument containing a list of method strings.
1438/// If found, returns the first method in uppercase. Otherwise defaults to `"GET"`.
1439fn extract_method_from_route_args(args_text: &str) -> String {
1440    // Look for 'methods' keyword in the arguments
1441    let Some(methods_pos) = args_text.find("methods") else {
1442        return "GET".to_string();
1443    };
1444
1445    // Find the opening bracket after 'methods='
1446    let after_methods = &args_text[methods_pos..];
1447    let Some(bracket_pos) = after_methods.find('[') else {
1448        return "GET".to_string();
1449    };
1450
1451    let after_bracket = &after_methods[bracket_pos + 1..];
1452
1453    // Find the first string literal inside the bracket
1454    let method_str = extract_first_string_literal(after_bracket);
1455    match method_str {
1456        Some(m) => m.to_ascii_uppercase(),
1457        None => "GET".to_string(),
1458    }
1459}
1460
1461/// Extract the first single- or double-quoted string literal from the given text.
1462fn extract_first_string_literal(text: &str) -> Option<String> {
1463    let trimmed = text.trim();
1464
1465    let (quote_char, start_pos) = {
1466        let single_pos = trimmed.find('\'');
1467        let double_pos = trimmed.find('"');
1468        match (single_pos, double_pos) {
1469            (Some(s), Some(d)) => {
1470                if s < d {
1471                    ('\'', s)
1472                } else {
1473                    ('"', d)
1474                }
1475            }
1476            (Some(s), None) => ('\'', s),
1477            (None, Some(d)) => ('"', d),
1478            (None, None) => return None,
1479        }
1480    };
1481
1482    let after_open = start_pos + 1;
1483    let close_pos = trimmed[after_open..].find(quote_char)?;
1484    let literal = &trimmed[after_open..after_open + close_pos];
1485
1486    if literal.is_empty() {
1487        return None;
1488    }
1489
1490    Some(literal.to_string())
1491}
1492
1493// ============================================================================
1494// Property Detection - @property decorator
1495// ============================================================================
1496
1497/// Check if a function definition has a `@property` decorator.
1498///
1499/// Python AST structure for decorated functions:
1500/// ```python
1501/// @property
1502/// def name(self):
1503///     return self._name
1504/// ```
1505///
1506/// The tree-sitter AST wraps the `function_definition` in a `decorated_definition` node:
1507/// ```text
1508/// (block
1509///   (decorated_definition
1510///     decorator: (decorator "@property")
1511///     definition: (function_definition)))
1512/// ```
1513fn has_property_decorator(func_node: Node<'_>, content: &[u8]) -> bool {
1514    // The function_definition is a child of decorated_definition
1515    let Some(parent) = func_node.parent() else {
1516        return false;
1517    };
1518
1519    // Check if parent is decorated_definition
1520    if parent.kind() != "decorated_definition" {
1521        return false;
1522    }
1523
1524    // Look for @property decorator in the decorated_definition
1525    let mut cursor = parent.walk();
1526    for child in parent.children(&mut cursor) {
1527        if child.kind() == "decorator" {
1528            // Extract decorator text
1529            if let Ok(decorator_text) = child.utf8_text(content) {
1530                let decorator_text = decorator_text.trim();
1531                // Match @property or @property()
1532                if decorator_text == "@property"
1533                    || decorator_text.starts_with("@property(")
1534                    || decorator_text.starts_with("@property (")
1535                {
1536                    return true;
1537                }
1538            }
1539        }
1540    }
1541
1542    false
1543}
1544
1545/// Extract visibility from Python identifier based on naming convention.
1546///
1547/// Python uses naming conventions for visibility:
1548/// - `__name` (dunder) -> private (name mangling)
1549/// - `_name` (single underscore) -> protected/internal
1550/// - `name` -> public
1551fn extract_visibility_from_name(name: &str) -> &'static str {
1552    if name.starts_with("__") && !name.ends_with("__") {
1553        "private"
1554    } else if name.starts_with('_') {
1555        "protected"
1556    } else {
1557        "public"
1558    }
1559}
1560
1561// ============================================================================
1562// Type Hint Processing - TypeOf and Reference Edges
1563// ============================================================================
1564
1565/// Find the containing scope (function/class) for a node to create scope-qualified names.
1566///
1567/// This walks up the AST to find the nearest enclosing function or class definition.
1568/// Returns:
1569/// - Empty string for module-level
1570/// - Class name for class-level (e.g., "`MyClass`")
1571/// - Function qualified name for function-level (e.g., "MyClass.method" or "process")
1572fn find_containing_scope(node: Node<'_>, content: &[u8], ast_graph: &ASTGraph) -> String {
1573    let mut current = node;
1574    let mut found_class_name: Option<String> = None;
1575
1576    // Walk up the tree to find enclosing function or class
1577    while let Some(parent) = current.parent() {
1578        match parent.kind() {
1579            "function_definition" => {
1580                // Found enclosing function - get its qualified name
1581                if let Some(ctx) = ast_graph.get_callable_context(parent.id()) {
1582                    return ctx.qualified_name.clone();
1583                }
1584            }
1585            "class_definition" => {
1586                // Remember the class name but continue walking up
1587                // to check if we're inside a function within this class
1588                if found_class_name.is_none() {
1589                    // Extract class name directly from node
1590                    if let Some(name_node) = parent.child_by_field_name("name")
1591                        && let Ok(class_name) = name_node.utf8_text(content)
1592                    {
1593                        found_class_name = Some(class_name.to_string());
1594                    }
1595                }
1596            }
1597            _ => {}
1598        }
1599        current = parent;
1600    }
1601
1602    // If we found a class but no enclosing function, it's a class attribute
1603    found_class_name.unwrap_or_default()
1604}
1605
1606/// Extract return type annotation from a function definition.
1607///
1608/// Python AST structure:
1609/// ```python
1610/// def foo() -> int:  # return_type field contains type annotation
1611/// ```
1612fn extract_return_type_annotation(func_node: Node<'_>, content: &[u8]) -> Option<String> {
1613    let return_type_node = func_node.child_by_field_name("return_type")?;
1614    extract_type_from_node(return_type_node, content)
1615}
1616
1617/// Process function parameters to create `TypeOf` and Reference edges for type hints.
1618///
1619/// Handles:
1620/// - `def foo(x: int, y: str):` - typed parameters
1621/// - `def foo(self, x: int):` - skips self/cls
1622/// - `def foo(x: List[int]):` - extracts base type from generics
1623fn process_function_parameters(
1624    func_node: Node<'_>,
1625    content: &[u8],
1626    ast_graph: &ASTGraph,
1627    helper: &mut GraphBuildHelper,
1628) {
1629    let Some(params_node) = func_node.child_by_field_name("parameters") else {
1630        return;
1631    };
1632
1633    // Get the qualified name of the containing function/method for scope qualification
1634    let scope_prefix = ast_graph
1635        .get_callable_context(func_node.id())
1636        .map_or("", |ctx| ctx.qualified_name.as_str());
1637
1638    // Iterate through parameters in the parameter_list
1639    for param in params_node.children(&mut params_node.walk()) {
1640        // Python tree-sitter uses "typed_parameter" and "typed_default_parameter"
1641        // but we need to handle the actual structure
1642        match param.kind() {
1643            "typed_parameter" | "typed_default_parameter" => {
1644                process_typed_parameter(param, content, scope_prefix, helper);
1645            }
1646            // Untyped parameter - check if it has a type annotation in parent context
1647            // For now, skip (no type hint)
1648            // Default parameter without type - skip
1649            "identifier" | "default_parameter" => {}
1650            _ => {
1651                // Other parameter types - try to process if they have type annotations
1652                // This handles various parameter structures
1653                if param.child_by_field_name("type").is_some() {
1654                    process_typed_parameter(param, content, scope_prefix, helper);
1655                }
1656            }
1657        }
1658    }
1659}
1660
1661/// Process a single typed parameter node.
1662///
1663/// Creates scope-qualified variable names to prevent cross-scope type contamination.
1664/// Format: `<scope_prefix>:<param_name>` (e.g., `MyClass.method:x` or `process:x`)
1665fn process_typed_parameter(
1666    param: Node<'_>,
1667    content: &[u8],
1668    scope_prefix: &str,
1669    helper: &mut GraphBuildHelper,
1670) {
1671    // Extract parameter name (could be in "name" field or as identifier child)
1672    let param_name = if let Some(name_node) = param.child_by_field_name("name") {
1673        name_node.utf8_text(content).ok()
1674    } else {
1675        // Fallback: look for identifier child
1676        param
1677            .children(&mut param.walk())
1678            .find(|c| c.kind() == "identifier")
1679            .and_then(|n| n.utf8_text(content).ok())
1680    };
1681
1682    let Some(param_name) = param_name else {
1683        return;
1684    };
1685
1686    // Skip self and cls (special method parameters)
1687    if param_name == "self" || param_name == "cls" {
1688        return;
1689    }
1690
1691    // Extract type annotation
1692    let Some(type_node) = param.child_by_field_name("type") else {
1693        return;
1694    };
1695
1696    let Some(type_name) = extract_type_from_node(type_node, content) else {
1697        return;
1698    };
1699
1700    // Create scope-qualified parameter name to prevent cross-scope contamination
1701    // Format: <scope_prefix>:<param_name> (e.g., "MyClass.method:x" or "process:x")
1702    let qualified_param_name = if scope_prefix.is_empty() {
1703        // Top-level function parameter
1704        format!(":{param_name}")
1705    } else {
1706        format!("{scope_prefix}:{param_name}")
1707    };
1708
1709    // Create parameter variable node with qualified name
1710    let param_id = helper.add_variable(&qualified_param_name, Some(span_from_node(param)));
1711
1712    // Create type node
1713    let type_id = helper.add_type(&type_name, None);
1714
1715    // Add TypeOf and Reference edges
1716    helper.add_typeof_edge(param_id, type_id);
1717    helper.add_reference_edge(param_id, type_id);
1718}
1719
1720/// Process annotated assignments to create `TypeOf` and Reference edges.
1721///
1722/// Handles:
1723/// - `user: User = get_user()` - annotated assignment with value
1724/// - `count: int` - annotated assignment without value
1725/// - `items: List[str] = []` - generic types
1726fn process_annotated_assignment(
1727    expr_stmt_node: Node<'_>,
1728    content: &[u8],
1729    ast_graph: &ASTGraph,
1730    helper: &mut GraphBuildHelper,
1731) {
1732    // Get the containing scope for scope qualification
1733    // For assignments, we need to find the enclosing function/class
1734    let scope_prefix = find_containing_scope(expr_stmt_node, content, ast_graph);
1735
1736    // Look for expression_statement containing an assignment
1737    for child in expr_stmt_node.children(&mut expr_stmt_node.walk()) {
1738        if child.kind() == "assignment" {
1739            process_typed_assignment(child, content, &scope_prefix, helper);
1740        }
1741    }
1742}
1743
1744/// Process a typed assignment node (shared logic for variables and class attributes).
1745///
1746/// Creates scope-qualified variable names to prevent cross-scope type contamination.
1747fn process_typed_assignment(
1748    assignment_node: Node<'_>,
1749    content: &[u8],
1750    scope_prefix: &str,
1751    helper: &mut GraphBuildHelper,
1752) {
1753    // Check if this is a typed assignment by looking for type annotation
1754    // In Python, annotated assignments look like: name: type = value
1755    // The AST structure is: assignment { left: identifier, type: type, right: expression }
1756
1757    let Some(left) = assignment_node.child_by_field_name("left") else {
1758        return;
1759    };
1760
1761    let Some(type_node) = assignment_node.child_by_field_name("type") else {
1762        return;
1763    };
1764
1765    // Extract variable name
1766    let Ok(var_name) = left.utf8_text(content) else {
1767        return;
1768    };
1769
1770    // Extract type
1771    let Some(type_name) = extract_type_from_node(type_node, content) else {
1772        return;
1773    };
1774
1775    // Create scope-qualified variable name to prevent cross-scope contamination
1776    // For class attributes (module-level or class-level), use simple name
1777    // For function-local variables, use qualified name
1778    let qualified_var_name = if scope_prefix.is_empty() {
1779        // Module-level variable
1780        var_name.to_string()
1781    } else if scope_prefix.contains('.') && !scope_prefix.contains(':') {
1782        // Class attribute (scope_prefix is class name without function)
1783        format!("{scope_prefix}.{var_name}")
1784    } else {
1785        // Function-local variable
1786        format!("{scope_prefix}:{var_name}")
1787    };
1788
1789    // Create variable node with qualified name
1790    let var_id = helper.add_variable(&qualified_var_name, Some(span_from_node(assignment_node)));
1791
1792    // Create type node
1793    let type_id = helper.add_type(&type_name, None);
1794
1795    // Add TypeOf and Reference edges
1796    helper.add_typeof_edge(var_id, type_id);
1797    helper.add_reference_edge(var_id, type_id);
1798}
1799
1800/// Extract type name from a type annotation node.
1801///
1802/// Handles:
1803/// - Simple types: `int`, `str`, `bool`
1804/// - Generic types: `List[int]` → extract base type `List`
1805/// - Optional types: `Optional[User]` → extract base type `Optional`
1806/// - Qualified types: `module.Type` → extract full qualified name
1807/// - Forward references: `"User"` → `User` (strips quotes)
1808/// - PEP 604 unions: `User | None` → `User` (extracts left-most base type)
1809fn extract_type_from_node(type_node: Node<'_>, content: &[u8]) -> Option<String> {
1810    match type_node.kind() {
1811        "type" => {
1812            // The "type" node wraps the actual type - recurse into first child
1813            type_node
1814                .named_child(0)
1815                .and_then(|child| extract_type_from_node(child, content))
1816        }
1817        "identifier" => {
1818            // Simple type: int, str, User
1819            type_node.utf8_text(content).ok().map(String::from)
1820        }
1821        "string" => {
1822            // Forward reference: "User" -> User
1823            // Strip surrounding quotes from string literal annotations
1824            let text = type_node.utf8_text(content).ok()?;
1825            let trimmed = text.trim();
1826
1827            // Remove quotes: "User" or 'User' -> User
1828            if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1829                || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
1830            {
1831                let unquoted = &trimmed[1..trimmed.len() - 1];
1832                // Handle potential unions inside string: "User | None" -> "User"
1833                Some(normalize_union_type(unquoted))
1834            } else {
1835                Some(trimmed.to_string())
1836            }
1837        }
1838        "binary_operator" => {
1839            // PEP 604 union: User | None -> User
1840            // Extract left operand as the primary type
1841            if let Some(left) = type_node.child_by_field_name("left") {
1842                extract_type_from_node(left, content)
1843            } else {
1844                // Fallback: extract text and normalize
1845                type_node
1846                    .utf8_text(content)
1847                    .ok()
1848                    .map(|text| normalize_union_type(text.trim()))
1849            }
1850        }
1851        "generic_type" | "subscript" => {
1852            // Generic type: List[int], Dict[str, int], Optional[User]
1853            // Extract base type (before the brackets)
1854            // Structure: subscript { value: identifier, subscript: [...] }
1855            if let Some(value_node) = type_node.child_by_field_name("value") {
1856                extract_type_from_node(value_node, content)
1857            } else {
1858                // Fallback: try first named child
1859                type_node
1860                    .named_child(0)
1861                    .and_then(|child| extract_type_from_node(child, content))
1862                    .or_else(|| {
1863                        // Last resort: extract the full text and take the base type
1864                        type_node.utf8_text(content).ok().and_then(|text| {
1865                            // Extract base type from "List[str]" -> "List"
1866                            text.split('[').next().map(|s| s.trim().to_string())
1867                        })
1868                    })
1869            }
1870        }
1871        "attribute" => {
1872            // Qualified type: module.Type or package.module.Type
1873            type_node.utf8_text(content).ok().map(String::from)
1874        }
1875        "list" | "tuple" | "set" => {
1876            // Collection literals (though rare in type annotations)
1877            type_node.utf8_text(content).ok().map(String::from)
1878        }
1879        _ => {
1880            // Fallback: try to extract text from any other node
1881            // For unknown node types, try to extract intelligently
1882            let text = type_node.utf8_text(content).ok()?;
1883            let trimmed = text.trim();
1884
1885            // If it looks like a generic type, extract base type
1886            if trimmed.contains('[') {
1887                trimmed.split('[').next().map(|s| s.trim().to_string())
1888            } else {
1889                // Check for union syntax
1890                Some(normalize_union_type(trimmed))
1891            }
1892        }
1893    }
1894}
1895
1896/// Normalize union types by extracting the left-most/primary type.
1897///
1898/// Examples:
1899/// - `User | None` → `User`
1900/// - `str | int` → `str`
1901/// - `Optional[User]` → `Optional[User]` (unchanged, not a union)
1902fn normalize_union_type(type_str: &str) -> String {
1903    if let Some(pipe_pos) = type_str.find('|') {
1904        // Extract left side of union and trim
1905        type_str[..pipe_pos].trim().to_string()
1906    } else {
1907        type_str.to_string()
1908    }
1909}
1910
1911#[cfg(test)]
1912mod tests {
1913    use super::*;
1914
1915    #[test]
1916    fn test_simple_name_extracts_dotted_identifiers() {
1917        // General dotted identifier handling (for call targets)
1918        assert_eq!(simple_name("module.func"), "func");
1919        assert_eq!(simple_name("obj.method"), "method");
1920        assert_eq!(simple_name("package.module.func"), "func");
1921        assert_eq!(simple_name("self.helper"), "helper");
1922
1923        // No dots - return as-is
1924        assert_eq!(simple_name("function"), "function");
1925        assert_eq!(simple_name(""), "");
1926    }
1927
1928    #[test]
1929    fn test_ffi_library_simple_name_extracts_library_base_names() {
1930        // Standard shared library names
1931        assert_eq!(ffi_library_simple_name("libfoo.so"), "libfoo");
1932        assert_eq!(ffi_library_simple_name("lib1.so"), "lib1");
1933        assert_eq!(ffi_library_simple_name("lib2.so"), "lib2");
1934
1935        // Different extensions
1936        assert_eq!(ffi_library_simple_name("kernel32.dll"), "kernel32");
1937        assert_eq!(ffi_library_simple_name("libSystem.dylib"), "libSystem");
1938
1939        // Versioned shared libraries (libc.so.6)
1940        assert_eq!(ffi_library_simple_name("libc.so.6"), "libc");
1941
1942        // No extension - return as-is
1943        assert_eq!(ffi_library_simple_name("kernel32"), "kernel32");
1944        assert_eq!(ffi_library_simple_name("numpy"), "numpy");
1945
1946        // Variable references (prefixed with $)
1947        assert_eq!(ffi_library_simple_name("$libname"), "$libname");
1948
1949        // Edge cases
1950        assert_eq!(ffi_library_simple_name(""), "");
1951        assert_eq!(ffi_library_simple_name("lib.so"), "lib");
1952    }
1953
1954    #[test]
1955    fn test_ffi_library_simple_name_prevents_duplicate_edges() {
1956        // This was the bug: lib1.so and lib2.so both became "so"
1957        let name1 = ffi_library_simple_name("lib1.so");
1958        let name2 = ffi_library_simple_name("lib2.so");
1959
1960        // They should be different
1961        assert_ne!(
1962            name1, name2,
1963            "lib1.so and lib2.so must produce different simple names"
1964        );
1965        assert_eq!(name1, "lib1");
1966        assert_eq!(name2, "lib2");
1967    }
1968
1969    #[test]
1970    fn test_ffi_library_simple_name_handles_directory_paths() {
1971        // Full paths with directories containing dots (Codex finding)
1972        assert_eq!(ffi_library_simple_name("/opt/v1.2/libfoo.so"), "libfoo");
1973        assert_eq!(
1974            ffi_library_simple_name("/usr/lib/x86_64-linux-gnu/libc.so.6"),
1975            "libc"
1976        );
1977        assert_eq!(ffi_library_simple_name("libs/lib1.so"), "lib1");
1978
1979        // Relative paths
1980        assert_eq!(ffi_library_simple_name("./libs/kernel32.dll"), "kernel32");
1981        assert_eq!(
1982            ffi_library_simple_name("../lib/libSystem.dylib"),
1983            "libSystem"
1984        );
1985    }
1986
1987    // ====================================================================
1988    // Route decorator parsing unit tests
1989    // ====================================================================
1990
1991    #[test]
1992    fn test_parse_route_decorator_app_route_default_get() {
1993        let result = parse_route_decorator_text("app.route('/api/users')");
1994        assert_eq!(result, Some(("GET".to_string(), "/api/users".to_string())));
1995    }
1996
1997    #[test]
1998    fn test_parse_route_decorator_app_route_with_methods_post() {
1999        let result = parse_route_decorator_text("app.route('/api/users', methods=['POST'])");
2000        assert_eq!(result, Some(("POST".to_string(), "/api/users".to_string())));
2001    }
2002
2003    #[test]
2004    fn test_parse_route_decorator_app_route_with_methods_put_double_quotes() {
2005        let result = parse_route_decorator_text("app.route(\"/api/items\", methods=[\"PUT\"])");
2006        assert_eq!(result, Some(("PUT".to_string(), "/api/items".to_string())));
2007    }
2008
2009    #[test]
2010    fn test_parse_route_decorator_app_get() {
2011        let result = parse_route_decorator_text("app.get('/api/users')");
2012        assert_eq!(result, Some(("GET".to_string(), "/api/users".to_string())));
2013    }
2014
2015    #[test]
2016    fn test_parse_route_decorator_app_post() {
2017        let result = parse_route_decorator_text("app.post('/api/items')");
2018        assert_eq!(result, Some(("POST".to_string(), "/api/items".to_string())));
2019    }
2020
2021    #[test]
2022    fn test_parse_route_decorator_app_put() {
2023        let result = parse_route_decorator_text("app.put('/api/items/1')");
2024        assert_eq!(
2025            result,
2026            Some(("PUT".to_string(), "/api/items/1".to_string()))
2027        );
2028    }
2029
2030    #[test]
2031    fn test_parse_route_decorator_app_delete() {
2032        let result = parse_route_decorator_text("app.delete('/api/items/1')");
2033        assert_eq!(
2034            result,
2035            Some(("DELETE".to_string(), "/api/items/1".to_string()))
2036        );
2037    }
2038
2039    #[test]
2040    fn test_parse_route_decorator_app_patch() {
2041        let result = parse_route_decorator_text("app.patch('/api/items/1')");
2042        assert_eq!(
2043            result,
2044            Some(("PATCH".to_string(), "/api/items/1".to_string()))
2045        );
2046    }
2047
2048    #[test]
2049    fn test_parse_route_decorator_router_get_fastapi() {
2050        let result = parse_route_decorator_text("router.get('/api/users')");
2051        assert_eq!(result, Some(("GET".to_string(), "/api/users".to_string())));
2052    }
2053
2054    #[test]
2055    fn test_parse_route_decorator_router_post_fastapi() {
2056        let result = parse_route_decorator_text("router.post('/api/items')");
2057        assert_eq!(result, Some(("POST".to_string(), "/api/items".to_string())));
2058    }
2059
2060    #[test]
2061    fn test_parse_route_decorator_blueprint_route() {
2062        let result = parse_route_decorator_text("blueprint.route('/health')");
2063        assert_eq!(result, Some(("GET".to_string(), "/health".to_string())));
2064    }
2065
2066    #[test]
2067    fn test_parse_route_decorator_unknown_receiver_returns_none() {
2068        // "server" is not a recognized receiver
2069        let result = parse_route_decorator_text("server.get('/api/users')");
2070        assert_eq!(result, None);
2071    }
2072
2073    #[test]
2074    fn test_parse_route_decorator_unknown_method_returns_none() {
2075        // "options" is not in the ROUTE_METHOD_NAMES list and is not "route"
2076        let result = parse_route_decorator_text("app.options('/api/users')");
2077        assert_eq!(result, None);
2078    }
2079
2080    #[test]
2081    fn test_parse_route_decorator_no_parens_returns_none() {
2082        let result = parse_route_decorator_text("app.route");
2083        assert_eq!(result, None);
2084    }
2085
2086    #[test]
2087    fn test_parse_route_decorator_no_dot_returns_none() {
2088        let result = parse_route_decorator_text("route('/api/users')");
2089        assert_eq!(result, None);
2090    }
2091
2092    #[test]
2093    fn test_extract_path_from_decorator_args_single_quotes() {
2094        let result = extract_path_from_decorator_args("'/api/users')");
2095        assert_eq!(result, Some("/api/users".to_string()));
2096    }
2097
2098    #[test]
2099    fn test_extract_path_from_decorator_args_double_quotes() {
2100        let result = extract_path_from_decorator_args("\"/api/items\")");
2101        assert_eq!(result, Some("/api/items".to_string()));
2102    }
2103
2104    #[test]
2105    fn test_extract_path_from_decorator_args_empty_returns_none() {
2106        let result = extract_path_from_decorator_args("'')");
2107        assert_eq!(result, None);
2108    }
2109
2110    #[test]
2111    fn test_extract_path_from_decorator_args_no_string_returns_none() {
2112        let result = extract_path_from_decorator_args("some_var)");
2113        assert_eq!(result, None);
2114    }
2115
2116    #[test]
2117    fn test_extract_method_from_route_args_with_methods_keyword() {
2118        let result = extract_method_from_route_args("'/api/users', methods=['POST'])");
2119        assert_eq!(result, "POST");
2120    }
2121
2122    #[test]
2123    fn test_extract_method_from_route_args_without_methods_keyword() {
2124        let result = extract_method_from_route_args("'/api/users')");
2125        assert_eq!(result, "GET");
2126    }
2127
2128    #[test]
2129    fn test_extract_method_from_route_args_delete() {
2130        let result = extract_method_from_route_args("'/api/items', methods=['DELETE'])");
2131        assert_eq!(result, "DELETE");
2132    }
2133
2134    #[test]
2135    fn test_extract_method_from_route_args_lowercase_normalizes() {
2136        let result = extract_method_from_route_args("'/x', methods=['put'])");
2137        assert_eq!(result, "PUT");
2138    }
2139
2140    #[test]
2141    fn test_extract_first_string_literal_single_quotes() {
2142        let result = extract_first_string_literal("'POST']");
2143        assert_eq!(result, Some("POST".to_string()));
2144    }
2145
2146    #[test]
2147    fn test_extract_first_string_literal_double_quotes() {
2148        let result = extract_first_string_literal("\"DELETE\"]");
2149        assert_eq!(result, Some("DELETE".to_string()));
2150    }
2151
2152    #[test]
2153    fn test_extract_first_string_literal_empty_returns_none() {
2154        let result = extract_first_string_literal("no quotes here");
2155        assert_eq!(result, None);
2156    }
2157}