diff-code-change-range 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ """Main reference extractor orchestrating the analysis pipeline."""
2
+
3
+ import sys
4
+ from typing import Dict, List, Optional
5
+
6
+ from .models import (
7
+ Reference,
8
+ ReferenceResult,
9
+ AffectedScope,
10
+ AffectedNode,
11
+ QualifiedNode,
12
+ NodeType,
13
+ )
14
+ from .scope_parser import ScopeParser
15
+ from .analyzer import ReferenceAnalyzer
16
+ from .differ import ReferenceDiffer
17
+
18
+
19
+ def extract_references(
20
+ before_code: Dict[str, str],
21
+ after_code: Dict[str, str],
22
+ affected_scope: AffectedScope
23
+ ) -> ReferenceResult:
24
+ """
25
+ Extract references between affected nodes in before and after code versions.
26
+
27
+ Args:
28
+ before_code: Map of file path -> source code for before version
29
+ after_code: Map of file path -> source code for after version
30
+ affected_scope: Affected scope trees for before and after
31
+
32
+ Returns:
33
+ ReferenceResult containing before/after references and their differences
34
+ """
35
+ # Parse scope to get qualified nodes
36
+ parser = ScopeParser(affected_scope)
37
+ before_nodes, after_nodes = parser.get_all_qualified_nodes()
38
+
39
+ # Filter to only leaf nodes (methods, functions, members) that can contain references
40
+ before_leaf_nodes = _filter_leaf_nodes(before_nodes)
41
+ after_leaf_nodes = _filter_leaf_nodes(after_nodes)
42
+
43
+ # Analyze before version
44
+ before_refs = _analyze_version(
45
+ before_code,
46
+ before_leaf_nodes,
47
+ before_nodes, # All nodes as potential targets
48
+ "before"
49
+ )
50
+
51
+ # Analyze after version
52
+ after_refs = _analyze_version(
53
+ after_code,
54
+ after_leaf_nodes,
55
+ after_nodes, # All nodes as potential targets
56
+ "after"
57
+ )
58
+
59
+ # Compute differences
60
+ result = ReferenceDiffer.compute_diff(before_refs, after_refs)
61
+
62
+ return result
63
+
64
+
65
+ def _filter_leaf_nodes(nodes: List[QualifiedNode]) -> List[QualifiedNode]:
66
+ """Filter to only leaf nodes that can contain references (methods, functions, members)."""
67
+ leaf_types = {
68
+ NodeType.METHOD,
69
+ NodeType.FUNCTION,
70
+ NodeType.MEMBER,
71
+ NodeType.VARIABLE,
72
+ }
73
+ return [n for n in nodes if n.node_type in leaf_types]
74
+
75
+
76
+ def _analyze_version(
77
+ code_map: Dict[str, str],
78
+ source_nodes: List[QualifiedNode],
79
+ all_nodes: List[QualifiedNode],
80
+ version: str
81
+ ) -> List[Reference]:
82
+ """
83
+ Analyze one version (before or after) and extract references.
84
+
85
+ Args:
86
+ code_map: Map of file path -> source code
87
+ source_nodes: List of nodes to analyze as sources (leaf nodes)
88
+ all_nodes: List of all nodes as potential targets
89
+ version: "before" or "after" (for logging)
90
+
91
+ Returns:
92
+ List of references found
93
+ """
94
+ all_references = []
95
+
96
+ # Group nodes by file
97
+ nodes_by_file: Dict[str, List[QualifiedNode]] = {}
98
+ for node in source_nodes:
99
+ if node.file_path not in nodes_by_file:
100
+ nodes_by_file[node.file_path] = []
101
+ nodes_by_file[node.file_path].append(node)
102
+
103
+ # Analyze each file
104
+ for file_path, nodes in nodes_by_file.items():
105
+ if file_path not in code_map:
106
+ continue
107
+
108
+ source_code = code_map[file_path]
109
+ if not source_code:
110
+ continue
111
+
112
+ # Create analyzer with all nodes as potential targets
113
+ # (within the same version)
114
+ analyzer = ReferenceAnalyzer(all_nodes, file_path)
115
+
116
+ # Analyze each node in the file
117
+ for node in nodes:
118
+ try:
119
+ refs = analyzer.analyze(source_code, node)
120
+ # Filter out self-references (source == target)
121
+ refs = [r for r in refs if r.source != r.target]
122
+ all_references.extend(refs)
123
+ except Exception as e:
124
+ print(
125
+ f"Warning: Failed to analyze {node.qualified_path} in {version}: {e}",
126
+ file=sys.stderr
127
+ )
128
+ continue
129
+
130
+ return all_references
@@ -0,0 +1,85 @@
1
+ """Data models for reference extraction."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import List, Optional, Tuple
6
+
7
+
8
+ class ReferenceType(Enum):
9
+ """Types of references between code nodes."""
10
+ METHOD_CALL = "method_call"
11
+ FIELD_ACCESS = "field_access"
12
+ TYPE_REFERENCE = "type_reference"
13
+ INSTANTIATION = "instantiation"
14
+ ANNOTATION = "annotation"
15
+ INHERITANCE = "inheritance"
16
+ IMPLEMENTATION = "implementation"
17
+
18
+
19
+ class NodeType(Enum):
20
+ """Types of code nodes."""
21
+ FILE = "file"
22
+ CLASS = "class"
23
+ INTERFACE = "interface"
24
+ OBJECT = "object"
25
+ ENUM = "enum"
26
+ FUNCTION = "function"
27
+ METHOD = "method"
28
+ MEMBER = "member"
29
+ VARIABLE = "variable"
30
+
31
+
32
+ @dataclass
33
+ class Reference:
34
+ """Represents a reference from one code node to another."""
35
+ source: str # Fully qualified path of source node
36
+ target: str # Fully qualified path of target node
37
+ type: ReferenceType
38
+ line: Optional[int] = None # Line number where reference occurs
39
+
40
+ def __hash__(self):
41
+ # For diffing purposes, ignore line number
42
+ return hash((self.source, self.target, self.type))
43
+
44
+ def __eq__(self, other):
45
+ if not isinstance(other, Reference):
46
+ return False
47
+ return (self.source == other.source and
48
+ self.target == other.target and
49
+ self.type == other.type)
50
+
51
+
52
+ @dataclass
53
+ class ReferenceResult:
54
+ """Result of reference extraction containing before/after references."""
55
+ before_references: List[Reference] = field(default_factory=list)
56
+ after_references: List[Reference] = field(default_factory=list)
57
+ added_references: List[Reference] = field(default_factory=list)
58
+ removed_references: List[Reference] = field(default_factory=list)
59
+
60
+
61
+ @dataclass
62
+ class AffectedNode:
63
+ """Represents a node in the affected scope tree."""
64
+ name: str
65
+ type: NodeType
66
+ line_range: Optional[Tuple[int, int]] = None
67
+ children: Optional[List['AffectedNode']] = None
68
+
69
+
70
+ @dataclass
71
+ class AffectedScope:
72
+ """Container for before and after affected scope trees."""
73
+ before: List[AffectedNode]
74
+ after: List[AffectedNode]
75
+
76
+
77
+ @dataclass
78
+ class QualifiedNode:
79
+ """Flattened representation of an affected node with its qualified path."""
80
+ qualified_path: str
81
+ name: str
82
+ node_type: NodeType
83
+ line_range: Tuple[int, int]
84
+ file_path: str
85
+ container_path: Optional[str] = None # Path of parent class/object
@@ -0,0 +1,79 @@
1
+ """Parser for flattening affected scope tree into qualified node list."""
2
+
3
+ from typing import List, Optional, Tuple
4
+ from .models import AffectedNode, AffectedScope, NodeType, QualifiedNode
5
+
6
+
7
+ class ScopeParser:
8
+ """Parses affected scope and generates qualified paths for nodes."""
9
+
10
+ def __init__(self, scope: AffectedScope):
11
+ self.scope = scope
12
+
13
+ def get_all_qualified_nodes(self) -> Tuple[List[QualifiedNode], List[QualifiedNode]]:
14
+ """Get all qualified nodes for before and after scopes."""
15
+ before_nodes = self._flatten_nodes(self.scope.before)
16
+ after_nodes = self._flatten_nodes(self.scope.after)
17
+ return before_nodes, after_nodes
18
+
19
+ def _flatten_nodes(
20
+ self,
21
+ nodes: List[AffectedNode],
22
+ parent_path: str = "",
23
+ file_path: str = ""
24
+ ) -> List[QualifiedNode]:
25
+ """Recursively flatten node tree into qualified nodes."""
26
+ result = []
27
+
28
+ for node in nodes:
29
+ current_path, current_file = self._build_path(node, parent_path, file_path)
30
+
31
+ # Create qualified node if it has a line range
32
+ if node.line_range:
33
+ qualified = QualifiedNode(
34
+ qualified_path=current_path,
35
+ name=node.name,
36
+ node_type=node.type,
37
+ line_range=node.line_range,
38
+ file_path=current_file,
39
+ container_path=parent_path if parent_path else None
40
+ )
41
+ result.append(qualified)
42
+
43
+ # Process children
44
+ if node.children:
45
+ result.extend(self._flatten_nodes(node.children, current_path, current_file))
46
+
47
+ return result
48
+
49
+ def _build_path(
50
+ self,
51
+ node: AffectedNode,
52
+ parent_path: str,
53
+ file_path: str
54
+ ) -> Tuple[str, str]:
55
+ """Build qualified path for a node."""
56
+ if node.type == NodeType.FILE:
57
+ # File node: path is just the file path
58
+ return node.name, node.name
59
+
60
+ if not parent_path:
61
+ # Top-level node (function, class, etc.)
62
+ return f"{file_path}#{node.name}", file_path
63
+
64
+ # Check if parent_path already contains # or $
65
+ if '#' in parent_path:
66
+ # Nested node: append to parent path with $ separator
67
+ return f"{parent_path}${node.name}", file_path
68
+ else:
69
+ # parent_path is just a file path
70
+ return f"{parent_path}#{node.name}", file_path
71
+
72
+ def get_nodes_by_file(self, nodes: List[QualifiedNode]) -> dict:
73
+ """Group qualified nodes by file path."""
74
+ result = {}
75
+ for node in nodes:
76
+ if node.file_path not in result:
77
+ result[node.file_path] = []
78
+ result[node.file_path].append(node)
79
+ return result