File size: 4,427 Bytes
7e2f74d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import os
import re
from typing import Dict, List, Any

# Simple regexes to find imports
PYTHON_IMPORT_RE = re.compile(r'^\s*(?:import\s+([\w\.,\s]+)|from\s+([\w\.]+)\s+import\s+([\w\.,\s\*]+))')
JS_IMPORT_RE = re.compile(r'(?:import\s+(?:[\w\s\{\}\*\,]+from\s+)?[\'"]([^\'"]+)[\'"]|require\([\'"]([^\'"]+)[\'"]\))')

def clean_python_import(imp_str: str) -> List[str]:
    """
    Cleans Python import strings. E.g. 'sys, os' -> ['sys', 'os']
    """
    if not imp_str:
        return []
    return [i.strip().split('.')[0] for i in imp_str.split(',') if i.strip()]

def extract_python_imports(content: str) -> List[str]:
    """
    Finds Python imports in code content.
    """
    imports = []
    for line in content.splitlines():
        match = PYTHON_IMPORT_RE.match(line)
        if match:
            group1, group2, _ = match.groups()
            if group1:
                # import X, Y
                imports.extend(clean_python_import(group1))
            if group2:
                # from X import Y
                # extract X
                parts = group2.split('.')
                if parts:
                    imports.append(parts[0])
    return list(set(imports))

def extract_js_imports(content: str) -> List[str]:
    """
    Finds JavaScript/TypeScript imports in code content.
    """
    imports = []
    matches = JS_IMPORT_RE.findall(content)
    for match in matches:
        group1, group2 = match
        imported = group1 or group2
        if imported:
            # Clean up paths (e.g. "./utils" or "lodash")
            # If it starts with . or .., it is local, otherwise it's a library/module
            imports.append(imported)
    return list(set(imports))

def build_initial_graph(files: List[Dict[str, Any]]) -> Dict[str, Any]:
    """
    Statically analyzes files to generate nodes (files) and edges (imports/references).
    """
    nodes = []
    edges = []
    
    # 1. Add file nodes
    for f in files:
        path = f["path"]
        _, ext = os.path.splitext(path.lower())
        
        # Determine node type based on extension
        node_type = "file"
        if ext in [".js", ".ts", ".jsx", ".tsx", ".py", ".go", ".rs", ".java"]:
            node_type = "module"
        elif ext in [".json", ".yaml", ".yml"]:
            node_type = "config"
        elif ext in [".html", ".css", ".scss"]:
            node_type = "ui"
            
        nodes.append({
            "id": path,
            "label": os.path.basename(path),
            "type": node_type,
            "properties": {
                "path": path,
                "size": f["size"]
            }
        })
        
    # 2. Extract dependencies (edges)
    for f in files:
        path = f["path"]
        content = f.get("content", "")
        _, ext = os.path.splitext(path.lower())
        
        imports = []
        if ext == ".py":
            imports = extract_python_imports(content)
        elif ext in [".js", ".jsx", ".ts", ".tsx"]:
            imports = extract_js_imports(content)
            
        for imp in imports:
            # Check if this import resolves to a local file in the scanned repository
            # Simple matching: see if the import string is in any file path or matches file basenames
            target_path = None
            
            # Case 1: Import matches local file name directly or path-wise
            for other_f in files:
                other_path = other_f["path"]
                other_base, _ = os.path.splitext(os.path.basename(other_path))
                
                # Check relative match or direct name match
                if imp == other_base or imp.endswith(other_base) or other_path.endswith(imp):
                    target_path = other_path
                    break
                    
            if target_path and target_path != path:
                edges.append({
                    "source": path,
                    "target": target_path,
                    "type": "imports",
                    "label": "imports"
                })
            else:
                # Case 2: Package/Library import (not in local files)
                # We can add a node for the external library if it's important,
                # but for Phase 1 skeleton we just skip external imports or let LLM do the mapping.
                pass
                
    return {
        "nodes": nodes,
        "edges": edges
    }