Skip to content

Commit 63504e5

Browse files
committed
[CoreLib] String.splitMap
Introduced a new public method splitMap to convert a string into a generic iterable, inspired by splitMapJoin, this new method uses the same parameters and returns Iterable<T> Closes dart-lang#40072
1 parent e87c93b commit 63504e5

File tree

14 files changed

+591
-1
lines changed

14 files changed

+591
-1
lines changed

pkg/dev_compiler/test/nullable_inference_test.dart

+1
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ void main() {
243243
s.replaceRange(1, 2, s);
244244
s.split(s);
245245
s.splitMapJoin(s, onMatch: (_) => s, onNonMatch: (_) => s);
246+
s.splitMap(s, onMatch: (_) => s, onNonMatch: (_) => s);
246247
s.startsWith(s);
247248
s.substring(1);
248249
s.toLowerCase();

pkg/linter/lib/src/rules/null_closures.dart

+4
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ final Map<String, Set<NonNullableFunction>>
9191
NonNullableFunction('dart.core', 'String', 'splitMapJoin',
9292
named: ['onMatch', 'onNonMatch']),
9393
},
94+
'splitMap': {
95+
NonNullableFunction('dart.core', 'Iterable', 'splitMap',
96+
named: ['onMatch', 'onNonMatch']),
97+
},
9498
'takeWhile': {
9599
NonNullableFunction('dart.core', 'Iterable', 'takeWhile', positional: [0]),
96100
},

pkg/linter/messages.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -7037,6 +7037,7 @@ LintCode:
70377037
* `String.replaceAllMapped` at the 1st positional parameter
70387038
* `String.replaceFirstMapped` at the 1st positional parameter
70397039
* `String.splitMapJoin` at the named parameters `onMatch` and `onNonMatch`
7040+
* `String.splitMap` at the named parameter `onMatch` and `onNonMatch`
70407041
70417042
**BAD:**
70427043
```dart

pkg/linter/tool/machine/rules.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1532,7 +1532,7 @@
15321532
"incompatible": [],
15331533
"sets": [],
15341534
"fixStatus": "hasFix",
1535-
"details": "**DON'T** pass `null` as an argument where a closure is expected.\n\nOften a closure that is passed to a method will only be called conditionally,\nso that tests and \"happy path\" production calls do not reveal that `null` will\nresult in an exception being thrown.\n\nThis rule only catches null literals being passed where closures are expected\nin the following locations:\n\n#### Constructors\n\n* From `dart:async`\n * `Future` at the 0th positional parameter\n * `Future.microtask` at the 0th positional parameter\n * `Future.sync` at the 0th positional parameter\n * `Timer` at the 0th positional parameter\n * `Timer.periodic` at the 1st positional parameter\n* From `dart:core`\n * `List.generate` at the 1st positional parameter\n\n#### Static functions\n\n* From `dart:async`\n * `scheduleMicrotask` at the 0th positional parameter\n * `Future.doWhile` at the 0th positional parameter\n * `Future.forEach` at the 0th positional parameter\n * `Future.wait` at the named parameter `cleanup`\n * `Timer.run` at the 0th positional parameter\n\n#### Instance methods\n\n* From `dart:async`\n * `Future.then` at the 0th positional parameter\n * `Future.complete` at the 0th positional parameter\n* From `dart:collection`\n * `Queue.removeWhere` at the 0th positional parameter\n * `Queue.retain\n * `Iterable.firstWhere` at the 0th positional parameter, and the named\n parameter `orElse`\n * `Iterable.forEach` at the 0th positional parameter\n * `Iterable.fold` at the 1st positional parameter\n * `Iterable.lastWhere` at the 0th positional parameter, and the named\n parameter `orElse`\n * `Iterable.map` at the 0th positional parameter\n * `Iterable.reduce` at the 0th positional parameter\n * `Iterable.singleWhere` at the 0th positional parameter, and the named\n parameter `orElse`\n * `Iterable.skipWhile` at the 0th positional parameter\n * `Iterable.takeWhile` at the 0th positional parameter\n * `Iterable.where` at the 0th positional parameter\n * `List.removeWhere` at the 0th positional parameter\n * `List.retainWhere` at the 0th positional parameter\n * `String.replaceAllMapped` at the 1st positional parameter\n * `String.replaceFirstMapped` at the 1st positional parameter\n * `String.splitMapJoin` at the named parameters `onMatch` and `onNonMatch`\n\n**BAD:**\n```dart\n[1, 3, 5].firstWhere((e) => e.isOdd, orElse: null);\n```\n\n**GOOD:**\n```dart\n[1, 3, 5].firstWhere((e) => e.isOdd, orElse: () => null);\n```",
1535+
"details": "**DON'T** pass `null` as an argument where a closure is expected.\n\nOften a closure that is passed to a method will only be called conditionally,\nso that tests and \"happy path\" production calls do not reveal that `null` will\nresult in an exception being thrown.\n\nThis rule only catches null literals being passed where closures are expected\nin the following locations:\n\n#### Constructors\n\n* From `dart:async`\n * `Future` at the 0th positional parameter\n * `Future.microtask` at the 0th positional parameter\n * `Future.sync` at the 0th positional parameter\n * `Timer` at the 0th positional parameter\n * `Timer.periodic` at the 1st positional parameter\n* From `dart:core`\n * `List.generate` at the 1st positional parameter\n\n#### Static functions\n\n* From `dart:async`\n * `scheduleMicrotask` at the 0th positional parameter\n * `Future.doWhile` at the 0th positional parameter\n * `Future.forEach` at the 0th positional parameter\n * `Future.wait` at the named parameter `cleanup`\n * `Timer.run` at the 0th positional parameter\n\n#### Instance methods\n\n* From `dart:async`\n * `Future.then` at the 0th positional parameter\n * `Future.complete` at the 0th positional parameter\n* From `dart:collection`\n * `Queue.removeWhere` at the 0th positional parameter\n * `Queue.retain\n * `Iterable.firstWhere` at the 0th positional parameter, and the named\n parameter `orElse`\n * `Iterable.forEach` at the 0th positional parameter\n * `Iterable.fold` at the 1st positional parameter\n * `Iterable.lastWhere` at the 0th positional parameter, and the named\n parameter `orElse`\n * `Iterable.map` at the 0th positional parameter\n * `Iterable.reduce` at the 0th positional parameter\n * `Iterable.singleWhere` at the 0th positional parameter, and the named\n parameter `orElse`\n * `Iterable.skipWhile` at the 0th positional parameter\n * `Iterable.takeWhile` at the 0th positional parameter\n * `Iterable.where` at the 0th positional parameter\n * `List.removeWhere` at the 0th positional parameter\n * `List.retainWhere` at the 0th positional parameter\n * `String.replaceAllMapped` at the 1st positional parameter\n * `String.replaceFirstMapped` at the 1st positional parameter\n * `String.splitMapJoin` at the named parameters `onMatch` and `onNonMatch` * `String.splitMap` at the named parameters `onMatch` and `onNonMatch`\n\n**BAD:**\n```dart\n[1, 3, 5].firstWhere((e) => e.isOdd, orElse: null);\n```\n\n**GOOD:**\n```dart\n[1, 3, 5].firstWhere((e) => e.isOdd, orElse: () => null);\n```",
15361536
"sinceDartSdk": "2.0"
15371537
},
15381538
{

sdk/lib/_internal/js_dev_runtime/private/js_string.dart

+97
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,103 @@ final class JSString extends Interceptor
8383
return stringReplaceAllFuncUnchecked(this, from, onMatch, onNonMatch);
8484
}
8585

86+
@notNull
87+
Iterable<T> splitMap<T>(
88+
Pattern pattern, {
89+
T Function(Match match)? onMatch,
90+
T Function(String nonMatch)? onNonMatch,
91+
}) {
92+
return stringSplitMapUnchecked(this, pattern, onMatch, onNonMatch);
93+
}
94+
95+
Iterable<T> stringSplitMapUnchecked<T>(String receiver, Pattern pattern,
96+
T Function(Match)? onMatch, T Function(String)? onNonMatch) {
97+
onMatch ??= (match) => match[0]! as T;
98+
onNonMatch ??= (string) => string as T;
99+
if (pattern is String) {
100+
return stringSplitStringMapUnchecked(receiver, pattern, onMatch, onNonMatch);
101+
}
102+
if (pattern is JSSyntaxRegExp) {
103+
return stringSplitJSRegExpMapUnchecked(receiver, pattern, onMatch, onNonMatch);
104+
}
105+
return stringSplitGeneralMapUnchecked(receiver, pattern, onMatch, onNonMatch);
106+
}
107+
108+
Iterable<T> stringSplitStringMapUnchecked<T>(String receiver, String pattern,
109+
T Function(Match) onMatch, T Function(String) onNonMatch) {
110+
if (pattern.isEmpty) {
111+
return stringSplitEmptyMapUnchecked(receiver, onMatch, onNonMatch);
112+
}
113+
List<T> result = <T>[];
114+
int startIndex = 0;
115+
int patternLength = pattern.length;
116+
while (true) {
117+
int position = stringIndexOfStringUnchecked(receiver, pattern, startIndex);
118+
if (position == -1) {
119+
break;
120+
}
121+
result.add(onNonMatch(receiver.substring(startIndex, position)));
122+
result.add(onMatch(StringMatch(position, receiver, pattern)));
123+
startIndex = position + patternLength;
124+
}
125+
result.add(onNonMatch(receiver.substring(startIndex)));
126+
return result;
127+
}
128+
129+
Iterable<T> stringSplitEmptyMapUnchecked<T>(
130+
String receiver, T Function(Match) onMatch, T Function(String) onNonMatch) {
131+
List<T> result = <T>[];
132+
int length = receiver.length;
133+
int i = 0;
134+
result.add(onNonMatch(""));
135+
while (i < length) {
136+
result.add(onMatch(StringMatch(i, receiver, "")));
137+
// Special case to avoid splitting a surrogate pair.
138+
int code = receiver.codeUnitAt(i);
139+
if ((code & ~0x3FF) == 0xD800 && length > i + 1) {
140+
// Leading surrogate;
141+
code = receiver.codeUnitAt(i + 1);
142+
if ((code & ~0x3FF) == 0xDC00) {
143+
// Matching trailing surrogate.
144+
result.add(onNonMatch(receiver.substring(i, i + 2)));
145+
i += 2;
146+
continue;
147+
}
148+
}
149+
result.add(onNonMatch(receiver[i]));
150+
i++;
151+
}
152+
result.add(onMatch(StringMatch(i, receiver, "")));
153+
result.add(onNonMatch(""));
154+
return result;
155+
}
156+
157+
Iterable<T> stringSplitJSRegExpMapUnchecked<T>(String receiver,
158+
JSSyntaxRegExp pattern, T Function(Match) onMatch, T Function(String) onNonMatch) {
159+
List<T> result = <T>[];
160+
int startIndex = 0;
161+
for (Match match in pattern.allMatches(receiver)) {
162+
result.add(onNonMatch(receiver.substring(startIndex, match.start)));
163+
result.add(onMatch(match));
164+
startIndex = match.end;
165+
}
166+
result.add(onNonMatch(receiver.substring(startIndex)));
167+
return result;
168+
}
169+
170+
Iterable<T> stringSplitGeneralMapUnchecked<T>(String receiver, Pattern pattern,
171+
T Function(Match) onMatch, T Function(String) onNonMatch) {
172+
List<T> result = <T>[];
173+
int startIndex = 0;
174+
for (Match match in pattern.allMatches(receiver)) {
175+
result.add(onNonMatch(receiver.substring(startIndex, match.start)));
176+
result.add(onMatch(match));
177+
startIndex = match.end;
178+
}
179+
result.add(onNonMatch(receiver.substring(startIndex)));
180+
return result;
181+
}
182+
86183
@notNull
87184
String replaceFirst(
88185
Pattern from,

sdk/lib/_internal/js_runtime/lib/interceptors.dart

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import 'dart:_js_helper'
4242
stringIndexOfStringUnchecked,
4343
stringLastIndexOfUnchecked,
4444
stringReplaceAllFuncUnchecked,
45+
stringSplitMapUnchecked,
4546
stringReplaceAllUnchecked,
4647
stringReplaceFirstUnchecked,
4748
stringReplaceFirstMappedUnchecked,

sdk/lib/_internal/js_runtime/lib/js_string.dart

+93
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,106 @@ final class JSString extends Interceptor
7272
return stringReplaceAllFuncUnchecked(this, from, onMatch, onNonMatch);
7373
}
7474

75+
Iterable<T> splitMap<T>(Pattern pattern,
76+
{T Function(Match match)? onMatch, T Function(String nonMatch)? onNonMatch}) {
77+
return stringSplitMapUnchecked(this, pattern, onMatch, onNonMatch);
78+
}
79+
7580
String replaceFirst(Pattern from, String to, [int startIndex = 0]) {
7681
checkString(to);
7782
checkInt(startIndex);
7883
RangeError.checkValueInInterval(startIndex, 0, this.length, "startIndex");
7984
return stringReplaceFirstUnchecked(this, from, to, startIndex);
8085
}
8186

87+
Iterable<T> stringSplitMapUnchecked<T>(String receiver, Pattern pattern,
88+
T Function(Match)? onMatch, T Function(String)? onNonMatch) {
89+
onMatch ??= (match) => match[0]! as T;
90+
onNonMatch ??= (string) => string as T;
91+
if (pattern is String) {
92+
return stringSplitStringMapUnchecked(receiver, pattern, onMatch, onNonMatch);
93+
}
94+
if (pattern is JSSyntaxRegExp) {
95+
return stringSplitJSRegExpMapUnchecked(receiver, pattern, onMatch, onNonMatch);
96+
}
97+
return stringSplitGeneralMapUnchecked(receiver, pattern, onMatch, onNonMatch);
98+
}
99+
100+
Iterable<T> stringSplitStringMapUnchecked<T>(String receiver, String pattern,
101+
T Function(Match) onMatch, T Function(String) onNonMatch) {
102+
if (pattern.isEmpty) {
103+
return stringSplitEmptyMapUnchecked(receiver, onMatch, onNonMatch);
104+
}
105+
List<T> result = <T>[];
106+
int startIndex = 0;
107+
int patternLength = pattern.length;
108+
while (true) {
109+
int position = stringIndexOfStringUnchecked(receiver, pattern, startIndex);
110+
if (position == -1) {
111+
break;
112+
}
113+
result.add(onNonMatch(receiver.substring(startIndex, position)));
114+
result.add(onMatch(StringMatch(position, receiver, pattern)));
115+
startIndex = position + patternLength;
116+
}
117+
result.add(onNonMatch(receiver.substring(startIndex)));
118+
return result;
119+
}
120+
121+
Iterable<T> stringSplitEmptyMapUnchecked<T>(
122+
String receiver, T Function(Match) onMatch, T Function(String) onNonMatch) {
123+
List<T> result = <T>[];
124+
int length = receiver.length;
125+
int i = 0;
126+
result.add(onNonMatch(""));
127+
while (i < length) {
128+
result.add(onMatch(StringMatch(i, receiver, "")));
129+
// Special case to avoid splitting a surrogate pair.
130+
int code = receiver.codeUnitAt(i);
131+
if ((code & ~0x3FF) == 0xD800 && length > i + 1) {
132+
// Leading surrogate;
133+
code = receiver.codeUnitAt(i + 1);
134+
if ((code & ~0x3FF) == 0xDC00) {
135+
// Matching trailing surrogate.
136+
result.add(onNonMatch(receiver.substring(i, i + 2)));
137+
i += 2;
138+
continue;
139+
}
140+
}
141+
result.add(onNonMatch(receiver[i]));
142+
i++;
143+
}
144+
result.add(onMatch(StringMatch(i, receiver, "")));
145+
result.add(onNonMatch(""));
146+
return result;
147+
}
148+
149+
Iterable<T> stringSplitJSRegExpMapUnchecked<T>(String receiver,
150+
JSSyntaxRegExp pattern, T Function(Match) onMatch, T Function(String) onNonMatch) {
151+
List<T> result = <T>[];
152+
int startIndex = 0;
153+
for (Match match in pattern.allMatches(receiver)) {
154+
result.add(onNonMatch(receiver.substring(startIndex, match.start)));
155+
result.add(onMatch(match));
156+
startIndex = match.end;
157+
}
158+
result.add(onNonMatch(receiver.substring(startIndex)));
159+
return result;
160+
}
161+
162+
Iterable<T> stringSplitGeneralMapUnchecked<T>(String receiver, Pattern pattern,
163+
T Function(Match) onMatch, T Function(String) onNonMatch) {
164+
List<T> result = <T>[];
165+
int startIndex = 0;
166+
for (Match match in pattern.allMatches(receiver)) {
167+
result.add(onNonMatch(receiver.substring(startIndex, match.start)));
168+
result.add(onMatch(match));
169+
startIndex = match.end;
170+
}
171+
result.add(onNonMatch(receiver.substring(startIndex)));
172+
return result;
173+
}
174+
82175
String replaceFirstMapped(Pattern from, String replace(Match match),
83176
[int startIndex = 0]) {
84177
checkNull(replace);

sdk/lib/_internal/js_runtime/lib/string_helper.dart

+88
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,94 @@ String stringReplaceAllFuncUnchecked(String receiver, Pattern pattern,
264264
return buffer.toString();
265265
}
266266

267+
Iterable<T> stringSplitMapUnchecked<T>(String receiver, Pattern pattern,
268+
T Function(Match)? onMatch, T Function(String)? onNonMatch) {
269+
onMatch ??= (match) => match[0]! as T;
270+
onNonMatch ??= (string) => string as T;
271+
if (pattern is String) {
272+
return stringSplitStringMapUnchecked(receiver, pattern, onMatch, onNonMatch);
273+
}
274+
if (pattern is JSSyntaxRegExp) {
275+
return stringSplitJSRegExpMapUnchecked(receiver, pattern, onMatch, onNonMatch);
276+
}
277+
return stringSplitGeneralMapUnchecked(receiver, pattern, onMatch, onNonMatch);
278+
}
279+
280+
Iterable<T> stringSplitStringMapUnchecked<T>(String receiver, String pattern,
281+
T Function(Match) onMatch, T Function(String) onNonMatch) {
282+
if (pattern.isEmpty) {
283+
return stringSplitEmptyMapUnchecked(receiver, onMatch, onNonMatch);
284+
}
285+
List<T> result = <T>[];
286+
int startIndex = 0;
287+
int patternLength = pattern.length;
288+
while (true) {
289+
int position = stringIndexOfStringUnchecked(receiver, pattern, startIndex);
290+
if (position == -1) {
291+
break;
292+
}
293+
result.add(onNonMatch(receiver.substring(startIndex, position)));
294+
result.add(onMatch(StringMatch(position, receiver, pattern)));
295+
startIndex = position + patternLength;
296+
}
297+
result.add(onNonMatch(receiver.substring(startIndex)));
298+
return result;
299+
}
300+
301+
Iterable<T> stringSplitEmptyMapUnchecked<T>(
302+
String receiver, T Function(Match) onMatch, T Function(String) onNonMatch) {
303+
List<T> result = <T>[];
304+
int length = receiver.length;
305+
int i = 0;
306+
result.add(onNonMatch(""));
307+
while (i < length) {
308+
result.add(onMatch(StringMatch(i, receiver, "")));
309+
// Special case to avoid splitting a surrogate pair.
310+
int code = receiver.codeUnitAt(i);
311+
if ((code & ~0x3FF) == 0xD800 && length > i + 1) {
312+
// Leading surrogate;
313+
code = receiver.codeUnitAt(i + 1);
314+
if ((code & ~0x3FF) == 0xDC00) {
315+
// Matching trailing surrogate.
316+
result.add(onNonMatch(receiver.substring(i, i + 2)));
317+
i += 2;
318+
continue;
319+
}
320+
}
321+
result.add(onNonMatch(receiver[i]));
322+
i++;
323+
}
324+
result.add(onMatch(StringMatch(i, receiver, "")));
325+
result.add(onNonMatch(""));
326+
return result;
327+
}
328+
329+
Iterable<T> stringSplitJSRegExpMapUnchecked<T>(String receiver,
330+
JSSyntaxRegExp pattern, T Function(Match) onMatch, T Function(String) onNonMatch) {
331+
List<T> result = <T>[];
332+
int startIndex = 0;
333+
for (Match match in pattern.allMatches(receiver)) {
334+
result.add(onNonMatch(receiver.substring(startIndex, match.start)));
335+
result.add(onMatch(match));
336+
startIndex = match.end;
337+
}
338+
result.add(onNonMatch(receiver.substring(startIndex)));
339+
return result;
340+
}
341+
342+
Iterable<T> stringSplitGeneralMapUnchecked<T>(String receiver, Pattern pattern,
343+
T Function(Match) onMatch, T Function(String) onNonMatch) {
344+
List<T> result = <T>[];
345+
int startIndex = 0;
346+
for (Match match in pattern.allMatches(receiver)) {
347+
result.add(onNonMatch(receiver.substring(startIndex, match.start)));
348+
result.add(onMatch(match));
349+
startIndex = match.end;
350+
}
351+
result.add(onNonMatch(receiver.substring(startIndex)));
352+
return result;
353+
}
354+
267355
String stringReplaceAllEmptyFuncUnchecked(String receiver,
268356
String Function(Match) onMatch, String Function(String) onNonMatch) {
269357
// Pattern is the empty string.

0 commit comments

Comments
 (0)