-
Notifications
You must be signed in to change notification settings - Fork 387
/
map_helper.dart
190 lines (157 loc) · 5.78 KB
/
map_helper.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';
import 'package:source_helper/source_helper.dart';
import '../constants.dart';
import '../shared_checkers.dart';
import '../type_helper.dart';
import '../unsupported_type_error.dart';
import 'to_from_string.dart';
const _keyParam = 'k';
class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
const MapHelper();
@override
String? serialize(
DartType targetType,
String expression,
TypeHelperContextWithConfig context,
) {
if (!coreMapTypeChecker.isAssignableFromType(targetType)) {
return null;
}
final args = targetType.typeArgumentsOf(coreMapTypeChecker)!;
assert(args.length == 2);
final keyType = args[0];
final valueType = args[1];
_checkSafeKeyType(expression, keyType);
final subFieldValue = context.serialize(valueType, closureArg);
final subKeyValue =
_forType(keyType)?.serialize(keyType, _keyParam, false) ??
context.serialize(keyType, _keyParam);
if (closureArg == subFieldValue && _keyParam == subKeyValue) {
return expression;
}
final optionalQuestion = targetType.isNullableType ? '?' : '';
return '$expression$optionalQuestion'
'.map(($_keyParam, $closureArg) => '
'MapEntry($subKeyValue, $subFieldValue))';
}
@override
String? deserialize(
DartType targetType,
String expression,
TypeHelperContextWithConfig context,
bool defaultProvided,
) {
if (!coreMapTypeChecker.isExactlyType(targetType)) {
return null;
}
final typeArgs = targetType.typeArgumentsOf(coreMapTypeChecker)!;
assert(typeArgs.length == 2);
final keyArg = typeArgs.first;
final valueArg = typeArgs.last;
_checkSafeKeyType(expression, keyArg);
final valueArgIsAny = valueArg.isDynamic ||
(valueArg.isDartCoreObject && valueArg.isNullableType);
final isKeyStringable = _isKeyStringable(keyArg);
final targetTypeIsNullable = defaultProvided || targetType.isNullableType;
final optionalQuestion = targetTypeIsNullable ? '?' : '';
if (!isKeyStringable) {
if (valueArgIsAny) {
if (context.config.anyMap) {
if (keyArg.isLikeDynamic) {
return '$expression as Map$optionalQuestion';
}
} else {
// this is the trivial case. Do a runtime cast to the known type of
// JSON map values - `Map<String, dynamic>`
return '$expression as Map<String, dynamic>$optionalQuestion';
}
}
if (!targetTypeIsNullable &&
(valueArgIsAny ||
// explicitly exclude double since we need to do an explicit
// `toDouble` on input values
valueArg.isSimpleJsonTypeNotDouble)) {
// No mapping of the values or null check required!
final valueString = valueArg.getDisplayString(withNullability: true);
return 'Map<String, $valueString>.from($expression as Map)';
}
}
// In this case, we're going to create a new Map with matching reified
// types.
final itemSubVal = context.deserialize(valueArg, closureArg);
var mapCast = context.config.anyMap ? 'as Map' : 'as Map<String, dynamic>';
if (targetTypeIsNullable) {
mapCast += '?';
}
String keyUsage;
if (keyArg.isEnum) {
keyUsage = context.deserialize(keyArg, _keyParam).toString();
} else if (context.config.anyMap &&
!(keyArg.isDartCoreObject || keyArg.isDynamic)) {
keyUsage = '$_keyParam as String';
} else if (context.config.anyMap &&
keyArg.isDartCoreObject &&
!keyArg.isNullableType) {
keyUsage = '$_keyParam as Object';
} else {
keyUsage = _keyParam;
}
final toFromString = _forType(keyArg);
if (toFromString != null) {
keyUsage =
toFromString.deserialize(keyArg, keyUsage, false, true).toString();
}
return '($expression $mapCast)$optionalQuestion.map( '
'($_keyParam, $closureArg) => MapEntry($keyUsage, $itemSubVal),)';
}
}
final _intString = ToFromStringHelper('int.parse', 'toString()', 'int');
/// [ToFromStringHelper] instances representing non-String types that can
/// be used as [Map] keys.
final _instances = [
bigIntString,
dateTimeString,
_intString,
uriString,
];
ToFromStringHelper? _forType(DartType type) =>
_instances.singleWhereOrNull((i) => i.matches(type));
/// Returns `true` if [keyType] can be automatically converted to/from String –
/// and is therefor usable as a key in a [Map].
bool _isKeyStringable(DartType keyType) =>
keyType.isEnum || _instances.any((inst) => inst.matches(keyType));
void _checkSafeKeyType(String expression, DartType keyArg) {
// We're not going to handle converting key types at the moment
// So the only safe types for key are dynamic/Object/String/enum
if (keyArg.isDynamic ||
(!keyArg.isNullableType &&
(keyArg.isDartCoreObject ||
coreStringTypeChecker.isExactlyType(keyArg) ||
_isKeyStringable(keyArg)))) {
return;
}
throw UnsupportedTypeError(
keyArg,
expression,
'Map keys must be one of: ${allowedMapKeyTypes.join(', ')}.',
);
}
/// The names of types that can be used as [Map] keys.
///
/// Used in [_checkSafeKeyType] to provide a helpful error with unsupported
/// types.
List<String> get allowedMapKeyTypes => [
'Object',
'dynamic',
'enum',
'String',
..._instances.map((i) => i.coreTypeName)
];
extension on DartType {
bool get isSimpleJsonTypeNotDouble =>
!isDartCoreDouble && simpleJsonTypeChecker.isAssignableFromType(this);
}