Skip to content

Commit fc22ad4

Browse files
committed
feat: Parent chain matching - Add support for dot notation in identifier names to specify parent relationships
1 parent 08271cf commit fc22ad4

File tree

6 files changed

+112
-20
lines changed

6 files changed

+112
-20
lines changed

src/cedarscript_editor/cedarscript_editor.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,20 +106,17 @@ def _update_command(self, cmd: UpdateCommand):
106106
lines = src.splitlines()
107107

108108
identifier_finder = IdentifierFinder(file_path, src, RangeSpec.EMPTY)
109-
109+
110110
search_range = RangeSpec.EMPTY
111+
move_src_range = None
111112
match action:
112113
case MoveClause():
113114
# READ + DELETE region : action.region (PARENT RESTRICTION: target.as_marker)
114115
move_src_range = restrict_search_range(action.region, target, identifier_finder, lines)
115116
# WRITE region: action.insert_position
116117
search_range = restrict_search_range(action.insert_position, None, identifier_finder, lines)
117-
case _:
118-
move_src_range = None
119-
# Set range_spec to cover the identifier
120-
match action:
121-
case RegionClause(region=region) | InsertClause(insert_position=region):
122-
search_range = restrict_search_range(region, target, identifier_finder, lines)
118+
case RegionClause(region=region) | InsertClause(insert_position=region):
119+
search_range = restrict_search_range(region, target, identifier_finder, lines)
123120

124121
# UPDATE FUNCTION "_check_raw_id_fields_item"
125122
# FROM FILE "refactor-benchmark/checks_BaseModelAdminChecks__check_raw_id_fields_item/checks.py"

src/cedarscript_editor/tree_sitter_identifier_finder.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,40 @@ def _find_identifier(self,
7979
or None if not found
8080
"""
8181
query_info_key = marker.type
82+
identifier_name = marker.value
8283
match marker.type:
8384
case 'method':
8485
query_info_key = 'function'
8586
try:
86-
candidates = (
87-
self.language.query(self.query_info[query_info_key].format(name=marker.value))
87+
all_restrictions: list[ParentRestriction] = [parent_restriction]
88+
# Extract parent name if using dot notation
89+
if '.' in identifier_name:
90+
*parent_parts, identifier_name = identifier_name.split('.')
91+
all_restrictions.append("." + '.'.join(reversed(parent_parts)))
92+
93+
# Get all node candidates first
94+
candidate_nodes = (
95+
self.language.query(self.query_info[query_info_key].format(name=identifier_name))
8896
.captures(self.tree.root_node)
8997
)
90-
# TODO discard candidates that aren't of type 'marker.type'
91-
candidates: list[IdentifierBoundaries] = [ib for ib in capture2identifier_boundaries(
92-
candidates,
93-
self.lines
94-
) if ib.match_parent(parent_restriction)]
98+
if not candidate_nodes:
99+
return None
100+
101+
# Convert captures to boundaries and filter by parent
102+
candidates: list[IdentifierBoundaries] = []
103+
for ib in capture2identifier_boundaries(candidate_nodes, self.lines):
104+
# For methods, verify the immediate parent is a class
105+
if marker.type == 'method':
106+
if not ib.parents or not ib.parents[0].parent_type.startswith('class'):
107+
continue
108+
# Check parent restriction (e.g., specific class name)
109+
candidate_matched_all_restrictions = True
110+
for pr in all_restrictions:
111+
if not ib.match_parent(pr):
112+
candidate_matched_all_restrictions = False
113+
break
114+
if candidate_matched_all_restrictions:
115+
candidates.append(ib)
95116
except Exception as e:
96117
raise ValueError(f"Unable to capture nodes for {marker}: {e}") from e
97118

@@ -100,13 +121,13 @@ def _find_identifier(self,
100121
return None
101122
if candidate_count > 1 and marker.offset is None:
102123
raise ValueError(
103-
f"The {marker.type} identifier named `{marker.value}` is ambiguous (found {candidate_count} matches). "
124+
f"The {marker.type} identifier named `{identifier_name}` is ambiguous (found {candidate_count} matches). "
104125
f"Choose an `OFFSET` between 0 and {candidate_count - 1} to determine how many to skip. "
105-
f"Example to reference the *last* `{marker.value}`: `OFFSET {candidate_count - 1}`"
126+
f"Example to reference the *last* `{identifier_name}`: `OFFSET {candidate_count - 1}`"
106127
)
107128
if marker.offset and marker.offset >= candidate_count:
108129
raise ValueError(
109-
f"There are only {candidate_count} {marker.type} identifiers named `{marker.value}`, "
130+
f"There are only {candidate_count} {marker.type} identifiers named `{identifier_name}`, "
110131
f"but 'OFFSET' was set to {marker.offset} (you can skip at most {candidate_count - 1} of those)"
111132
)
112133
candidates.sort(key=lambda x: x.whole.start)

src/text_manipulation/range_spec.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,23 @@ def match_parent(self, parent_restriction: ParentRestriction) -> bool:
372372
return True
373373
case RangeSpec():
374374
return self.whole in parent_restriction
375-
case str() as parent_name:
376-
# TODO Implement advanced query syntax
377-
return parent_name in [p.parent_name for p in self.parents]
375+
case str() as parent_spec:
376+
# Parent chain matching: Handle dot notation for parent relationships
377+
parent_chain = parent_spec.split('.')
378+
if len(parent_chain) == 1:
379+
# Simple case - just check if name is any of the parents
380+
return parent_spec in [p.parent_name for p in self.parents]
381+
parent_chain = [p for p in parent_chain if p]
382+
if len(parent_chain) > len(self.parents):
383+
return False
384+
# len(parent_chain) <= len(self.parents)
385+
# Check parent chain partially matches (
386+
# sub-chain match when there are fewer items in 'parent_chain' than in 'self.parents'
387+
# )
388+
return all(
389+
expected == actual.parent_name
390+
for expected, actual in zip(parent_chain, self.parents)
391+
)
378392
case _:
379393
raise ValueError(f'Invalid parent restriction: {parent_restriction}')
380394

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
class A:
2+
def a(self):
3+
pass
4+
def calculate(self,
5+
a,
6+
b,
7+
c,
8+
d,
9+
e
10+
):
11+
pass
12+
class Parent1OfB:
13+
class Parent2OfB:
14+
class B:
15+
def a(self):
16+
pass
17+
def calculate(self,
18+
a,
19+
b,
20+
c,
21+
d,
22+
e
23+
):
24+
pass
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<no-train>
2+
```CEDARScript
3+
UPDATE METHOD "Parent2OfB.B.calculate"
4+
FROM FILE "1.py"
5+
REPLACE LINE 1
6+
WITH CONTENT '''
7+
@0:def calculate(self, line_1,
8+
@1:line_2,
9+
''';
10+
```
11+
</no-train>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class A:
2+
def a(self):
3+
pass
4+
def calculate(self,
5+
a,
6+
b,
7+
c,
8+
d,
9+
e
10+
):
11+
pass
12+
class Parent1OfB:
13+
class Parent2OfB:
14+
class B:
15+
def a(self):
16+
pass
17+
def calculate(self, line_1,
18+
line_2,
19+
a,
20+
b,
21+
c,
22+
d,
23+
e
24+
):
25+
pass

0 commit comments

Comments
 (0)