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.
- diff_code_change_range/__init__.py +17 -0
- diff_code_change_range/__main__.py +7 -0
- diff_code_change_range/affected_marker.py +173 -0
- diff_code_change_range/cli.py +167 -0
- diff_code_change_range/diff_parser.py +218 -0
- diff_code_change_range/reference/__init__.py +59 -0
- diff_code_change_range/reference/analyzer.py +555 -0
- diff_code_change_range/reference/code_slicer.py +58 -0
- diff_code_change_range/reference/differ.py +80 -0
- diff_code_change_range/reference/extractor.py +130 -0
- diff_code_change_range/reference/models.py +85 -0
- diff_code_change_range/reference/scope_parser.py +79 -0
- diff_code_change_range/structure_extractor.py +750 -0
- diff_code_change_range/yaml_reporter.py +89 -0
- diff_code_change_range-0.0.1.dist-info/METADATA +386 -0
- diff_code_change_range-0.0.1.dist-info/RECORD +19 -0
- diff_code_change_range-0.0.1.dist-info/WHEEL +5 -0
- diff_code_change_range-0.0.1.dist-info/entry_points.txt +2 -0
- diff_code_change_range-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -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
|