Skip to content

Commit ea481b1

Browse files
committed
Make KVC syntax more robust
Allow `foo.%(...)` so that keys can be supplied via objects or as the result of a function or method call. Additionally, replacement is done with ranges of text instead of regular expressions based on what to replace. This is much less error-prone. Also fixes some critical bugs that were revealed by these additions.
1 parent 9852528 commit ea481b1

File tree

1 file changed

+101
-13
lines changed

1 file changed

+101
-13
lines changed

bin/lib/Logos/KVC.pm

+101-13
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ sub scanStringLiteral {
5555

5656
# Case: has escaped quotes
5757
while (distanceToPattern('\\"') != -1 &&
58-
distanceToPattern('\\"') < distanceToPattern('[^\\]"')) {
58+
distanceToPattern('\\"') < distanceToPattern('[^\\]]"')) {
5959
# Scan escaped quote
6060
scanUpToPattern('\\"');
6161
scanPattern('\\"');
@@ -67,6 +67,22 @@ sub scanStringLiteral {
6767
return 1;
6868
}
6969

70+
sub insideString {
71+
my $_start = $pos;
72+
$pos = 0;
73+
74+
# While we're behind our spot...
75+
while ($pos++ < $_start) {
76+
# Can we scan a string and moved past our spot?
77+
if (scanStringLiteral() && $pos >= $_start) {
78+
$pos = $_start;
79+
return 1;
80+
}
81+
}
82+
83+
{ $pos = $_start; return 0; };
84+
}
85+
7086
sub scanBracePair {
7187
my $_start = $pos;
7288

@@ -80,7 +96,8 @@ sub scanBracePair {
8096
while (distanceToPattern($open) != -1 &&
8197
distanceToPattern($open) < distanceToPattern($close)) {
8298
# Scan inner pair of brackets
83-
scanMethodCall();
99+
scanUpToPattern($open);
100+
scanBracePair($open, $close);
84101
}
85102

86103
# Scan closing bracket
@@ -179,7 +196,7 @@ sub scanObjectBehindCursor() {
179196
# We will first find `foo.bar` out of `[foo.bar baz].%whatever`.
180197
# We would need to keep going back until we hit `[foo.bar baz]` entirely.
181198
my $preScanPos = $pos;
182-
if (scanObjectToken() && $pos == $oldPos) {
199+
if ((scanObjectToken() || scanParenthesis()) && $pos == $oldPos) {
183200
my $obj = substr($_line, $preScanPos, $pos - $preScanPos);
184201

185202
# Skip over leading whitespace
@@ -203,23 +220,94 @@ sub scanObjectBehindCursor() {
203220
{ $pos = $_start; return 0; };
204221
}
205222

223+
# Leaves cursor just before the .%
224+
# Returns (key, object, range.loc, range.length)
225+
sub scanKVCKeyAndObjectAndLengths() {
226+
my $postKVCPos = $pos;
227+
my $preKVCPos = $pos - 2;
228+
my $key = undef;
229+
my $keyLength = undef;
230+
231+
# Case: foo.%bar
232+
if (scanIdentifier()) {
233+
# Scan the "key" and surround it in @"" quotes
234+
$key = substr($_line, $postKVCPos, $pos - $postKVCPos);
235+
$keyLength = length($key); # for range to replace
236+
$key = '@"' . $key . '"';
237+
}
238+
# Case: foo.%(...)
239+
elsif (scanParenthesis()) {
240+
# Scan the "key" which could be anything
241+
$key = substr($_line, $postKVCPos, $pos - $postKVCPos);
242+
$keyLength = length($key);
243+
}
244+
245+
# Case: setter syntax: ' = (stuff);'
246+
my $setterPattern = '\s*=\s*([^;]+);';
247+
my $val = undef;
248+
my $setterLength = 0;
249+
if (scanUpToPattern($setterPattern)) {
250+
if (substr($_line, $pos) =~ /^($setterPattern)/) {
251+
$val = $2;
252+
$setterLength = length($1) - 1; # we don't want the ';'
253+
}
254+
}
255+
256+
# Back up to before the key
257+
$pos = $preKVCPos;
258+
259+
# Scan the "object"
260+
my $object = scanObjectBehindCursor();
261+
my $objLength = length($object);
262+
263+
# Compute range to replace (loc, len)
264+
# Length is target.length + len(".%") + key.length
265+
my $loc = $pos - $objLength;
266+
my $len = $objLength + 2 + $keyLength + $setterLength;
267+
268+
# Scan past the .%
269+
$pos = $postKVCPos;
270+
271+
return ($key, $object, $val, $loc, $len);
272+
}
273+
206274
sub replaceSetters {
207275
$pos = 0;
208-
my $setterRegex = '\.%([\w\d]+) = ([^;]+);';
209-
while (scanUpToPattern($setterRegex)) {
210-
$scanned =~ s/\s*(.*)/$1/;
211-
$_line =~ s/($scanned)$setterRegex/[$1 setValue:$3 forKey:@"$2"];/g;
276+
277+
while (scanUpToPattern('\.%') && scanPattern('\.%')) {
278+
if (!insideString()) {
279+
# Get associated variables
280+
my ($key, $object, $val, $loc, $len) = scanKVCKeyAndObjectAndLengths();
281+
282+
if ($val) {
283+
# Replace the range with the KVC setter
284+
substr($_line, $loc, $len) = "[(id)$object setValue:$val forKey:$key]";
285+
286+
# Continue scanning from the start of the new code,
287+
# as the key may contain more .% calls inside it
288+
$pos = $loc;
289+
} else {
290+
291+
}
292+
}
212293
}
213294
}
214295

215296
sub replaceGetters {
216297
$pos = 0;
217-
while (scanUpToPattern('\.%([\w\d]+)')) {
218-
# Todo: regex escape $object and use it
219-
# instead of using .{$length} in the pattern
220-
my $object = scanObjectBehindCursor();
221-
my $length = length($object);
222-
$_line =~ s/(.{$length})\.%([\w\d]+)/[(id)$1 valueForKey:@"$2"]/;
298+
299+
while (scanUpToPattern('\.%') && scanPattern('\.%')) {
300+
if (!insideString()) {
301+
# Get associated variables
302+
my ($key, $object, $val, $loc, $len) = scanKVCKeyAndObjectAndLengths();
303+
304+
# Replace the range with the KVC getter
305+
substr($_line, $loc, $len) = "[(id)$object valueForKey:$key]";
306+
307+
# Continue scanning from the start of the new code,
308+
# as the key may contain more .% calls inside it
309+
$pos = $loc;
310+
}
223311
}
224312
}
225313

0 commit comments

Comments
 (0)