| OLD | NEW |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /// Defines static information collected by the type checker and used later by | 5 /// Defines static information collected by the type checker and used later by |
| 6 /// emitters to generate code. | 6 /// emitters to generate code. |
| 7 library dev_compiler.src.info; | 7 library dev_compiler.src.info; |
| 8 | 8 |
| 9 import 'package:analyzer/src/generated/ast.dart'; | 9 import 'package:analyzer/src/generated/ast.dart'; |
| 10 import 'package:analyzer/src/generated/element.dart'; | 10 import 'package:analyzer/src/generated/element.dart'; |
| 11 import 'package:analyzer/src/generated/error.dart'; | |
| 12 import 'package:analyzer/src/generated/parser.dart'; | 11 import 'package:analyzer/src/generated/parser.dart'; |
| 13 | 12 |
| 14 import 'checker/rules.dart'; | |
| 15 import 'utils.dart' as utils; | 13 import 'utils.dart' as utils; |
| 14 import 'package:analyzer/src/task/strong/info.dart'; |
| 15 export 'package:analyzer/src/task/strong/info.dart'; |
| 16 | 16 |
| 17 /// Represents a summary of the results collected by running the program | 17 /// Represents a summary of the results collected by running the program |
| 18 /// checker. | 18 /// checker. |
| 19 class CheckerResults { | 19 class CheckerResults { |
| 20 final List<LibraryInfo> libraries; | 20 final List<LibraryInfo> libraries; |
| 21 final TypeRules rules; | |
| 22 final bool failure; | 21 final bool failure; |
| 23 | 22 |
| 24 CheckerResults(this.libraries, this.rules, this.failure); | 23 CheckerResults(this.libraries, this.failure); |
| 25 } | 24 } |
| 26 | 25 |
| 27 /// Computed information about each library. | 26 /// Computed information about each library. |
| 28 class LibraryInfo { | 27 class LibraryInfo { |
| 29 /// Canonical name of the library. This is unfortunately not derived from the | 28 /// Canonical name of the library. This is unfortunately not derived from the |
| 30 /// library directive as it doesn't have any meaningful rules enforced. | 29 /// library directive as it doesn't have any meaningful rules enforced. |
| 31 /// Instead, this is inferred from the path to the file defining the library. | 30 /// Instead, this is inferred from the path to the file defining the library. |
| 32 final String name; | 31 final String name; |
| 33 | 32 |
| 34 /// Corresponding analyzer element. | 33 /// Corresponding analyzer element. |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 95 // TODO(jmesserly): as a workaround for analyzer <0.26.0-alpha.1. | 94 // TODO(jmesserly): as a workaround for analyzer <0.26.0-alpha.1. |
| 96 // ResolutionCopier won't copy the type, so we do it here. | 95 // ResolutionCopier won't copy the type, so we do it here. |
| 97 @override | 96 @override |
| 98 AwaitExpression visitAwaitExpression(AwaitExpression node) { | 97 AwaitExpression visitAwaitExpression(AwaitExpression node) { |
| 99 var clone = super.visitAwaitExpression(node); | 98 var clone = super.visitAwaitExpression(node); |
| 100 clone.staticType = node.staticType; | 99 clone.staticType = node.staticType; |
| 101 clone.propagatedType = node.propagatedType; | 100 clone.propagatedType = node.propagatedType; |
| 102 return clone; | 101 return clone; |
| 103 } | 102 } |
| 104 } | 103 } |
| 105 | |
| 106 // The abstract type of coercions mapping one type to another. | |
| 107 // This class also exposes static builder functions which | |
| 108 // check for errors and reduce redundant coercions to the identity. | |
| 109 abstract class Coercion { | |
| 110 final DartType fromType; | |
| 111 final DartType toType; | |
| 112 Coercion(this.fromType, this.toType); | |
| 113 static Coercion cast(DartType fromT, DartType toT) => new Cast(fromT, toT); | |
| 114 static Coercion identity(DartType type) => new Identity(type); | |
| 115 static Coercion error() => new CoercionError(); | |
| 116 } | |
| 117 | |
| 118 // Coercion which casts one type to another | |
| 119 class Cast extends Coercion { | |
| 120 Cast(DartType fromType, DartType toType) : super(fromType, toType); | |
| 121 } | |
| 122 | |
| 123 // The identity coercion | |
| 124 class Identity extends Coercion { | |
| 125 Identity(DartType fromType) : super(fromType, fromType); | |
| 126 } | |
| 127 | |
| 128 // The error coercion. This coercion signals that a coercion | |
| 129 // could not be generated. The code generator should not see | |
| 130 // these. | |
| 131 class CoercionError extends Coercion { | |
| 132 CoercionError() : super(null, null); | |
| 133 } | |
| 134 | |
| 135 // TODO(jmesserly): this could use some refactoring. These are essentially | |
| 136 // like ErrorCodes in analyzer, but we're including some details in our message. | |
| 137 // Analyzer instead has template strings, and replaces '{0}' with the first | |
| 138 // argument. | |
| 139 abstract class StaticInfo { | |
| 140 /// AST Node this info is attached to. | |
| 141 AstNode get node; | |
| 142 | |
| 143 // TODO(jmesserly): review the usage of error codes. We probably want our own, | |
| 144 // as well as some DDC specific [ErrorType]s. | |
| 145 ErrorCode toErrorCode(); | |
| 146 | |
| 147 // TODO(jmesserly): what convention to use here? | |
| 148 String get name => 'dev_compiler.$runtimeType'; | |
| 149 | |
| 150 List<Object> get arguments => [node]; | |
| 151 | |
| 152 AnalysisError toAnalysisError() { | |
| 153 int begin = node is AnnotatedNode | |
| 154 ? (node as AnnotatedNode).firstTokenAfterCommentAndMetadata.offset | |
| 155 : node.offset; | |
| 156 int length = node.end - begin; | |
| 157 var source = (node.root as CompilationUnit).element.source; | |
| 158 return new AnalysisError(source, begin, length, toErrorCode(), arguments); | |
| 159 } | |
| 160 } | |
| 161 | |
| 162 /// Implicitly injected expression conversion. | |
| 163 abstract class CoercionInfo extends StaticInfo { | |
| 164 final TypeRules rules; | |
| 165 | |
| 166 final Expression node; | |
| 167 | |
| 168 DartType get convertedType; | |
| 169 | |
| 170 CoercionInfo(this.rules, this.node); | |
| 171 | |
| 172 DartType get baseType => rules.getStaticType(node); | |
| 173 DartType get staticType => convertedType; | |
| 174 | |
| 175 String get message; | |
| 176 toErrorCode() => new HintCode(name, message); | |
| 177 | |
| 178 static const String _propertyName = 'dev_compiler.src.info.CoercionInfo'; | |
| 179 | |
| 180 /// Gets the coercion info associated with this node. | |
| 181 static CoercionInfo get(AstNode node) => node.getProperty(_propertyName); | |
| 182 | |
| 183 /// Sets the coercion info associated with this node. | |
| 184 static CoercionInfo set(AstNode node, CoercionInfo info) { | |
| 185 node.setProperty(_propertyName, info); | |
| 186 return info; | |
| 187 } | |
| 188 } | |
| 189 | |
| 190 // Base class for all casts from base type to sub type. | |
| 191 abstract class DownCast extends CoercionInfo { | |
| 192 Cast _cast; | |
| 193 | |
| 194 DownCast._internal(TypeRules rules, Expression expression, this._cast) | |
| 195 : super(rules, expression) { | |
| 196 assert(_cast.toType != baseType && | |
| 197 _cast.fromType == baseType && | |
| 198 (baseType.isDynamic || | |
| 199 // Call methods make the following non-redundant | |
| 200 _cast.toType.isSubtypeOf(baseType) || | |
| 201 baseType.isAssignableTo(_cast.toType))); | |
| 202 } | |
| 203 | |
| 204 Cast get cast => _cast; | |
| 205 | |
| 206 DartType get convertedType => _cast.toType; | |
| 207 | |
| 208 @override List<Object> get arguments => [node, baseType, convertedType]; | |
| 209 @override String get message => '{0} ({1}) will need runtime check ' | |
| 210 'to cast to type {2}'; | |
| 211 | |
| 212 // Factory to create correct DownCast variant. | |
| 213 static StaticInfo create(TypeRules rules, Expression expression, Cast cast, | |
| 214 {String reason}) { | |
| 215 final fromT = cast.fromType; | |
| 216 final toT = cast.toType; | |
| 217 | |
| 218 // toT <:_R fromT => to <: fromT | |
| 219 // NB: classes with call methods are subtypes of function | |
| 220 // types, but the function type is not assignable to the class | |
| 221 assert(toT.isSubtypeOf(fromT) || fromT.isAssignableTo(toT)); | |
| 222 | |
| 223 // Handle null call specially. | |
| 224 if (expression is NullLiteral) { | |
| 225 // TODO(vsm): Create a NullCast for this once we revisit nonnullability. | |
| 226 return new DownCastImplicit(rules, expression, cast); | |
| 227 } | |
| 228 | |
| 229 // Inference "casts": | |
| 230 if (expression is Literal) { | |
| 231 // fromT should be an exact type - this will almost certainly fail at | |
| 232 // runtime. | |
| 233 return new StaticTypeError(rules, expression, toT, reason: reason); | |
| 234 } | |
| 235 if (expression is FunctionExpression) { | |
| 236 // fromT should be an exact type - this will almost certainly fail at | |
| 237 // runtime. | |
| 238 return new UninferredClosure(rules, expression, cast); | |
| 239 } | |
| 240 if (expression is InstanceCreationExpression) { | |
| 241 // fromT should be an exact type - this will almost certainly fail at | |
| 242 // runtime. | |
| 243 return new StaticTypeError(rules, expression, toT, reason: reason); | |
| 244 } | |
| 245 | |
| 246 // Composite cast: these are more likely to fail. | |
| 247 if (!rules.isGroundType(toT)) { | |
| 248 // This cast is (probably) due to our different treatment of dynamic. | |
| 249 // It may be more likely to fail at runtime. | |
| 250 return new DownCastComposite(rules, expression, cast); | |
| 251 } | |
| 252 | |
| 253 // Dynamic cast | |
| 254 if (fromT.isDynamic) { | |
| 255 return new DynamicCast(rules, expression, cast); | |
| 256 } | |
| 257 | |
| 258 // Assignment cast | |
| 259 var parent = expression.parent; | |
| 260 if (parent is VariableDeclaration && (parent.initializer == expression)) { | |
| 261 return new AssignmentCast(rules, expression, cast); | |
| 262 } | |
| 263 | |
| 264 // Other casts | |
| 265 return new DownCastImplicit(rules, expression, cast); | |
| 266 } | |
| 267 } | |
| 268 | |
| 269 // | |
| 270 // Standard down casts. These casts are implicitly injected by the compiler. | |
| 271 // | |
| 272 | |
| 273 // A down cast from dynamic to T. | |
| 274 class DynamicCast extends DownCast { | |
| 275 DynamicCast(TypeRules rules, Expression expression, Cast cast) | |
| 276 : super._internal(rules, expression, cast); | |
| 277 | |
| 278 toErrorCode() => new HintCode(name, message); | |
| 279 } | |
| 280 | |
| 281 // A down cast due to a variable declaration to a ground type. E.g., | |
| 282 // T x = expr; | |
| 283 // where T is ground. We exclude non-ground types as these behave differently | |
| 284 // compared to standard Dart. | |
| 285 class AssignmentCast extends DownCast { | |
| 286 AssignmentCast(TypeRules rules, Expression expression, Cast cast) | |
| 287 : super._internal(rules, expression, cast); | |
| 288 | |
| 289 toErrorCode() => new HintCode(name, message); | |
| 290 } | |
| 291 | |
| 292 // | |
| 293 // Temporary "casts" of allocation sites - literals, constructor invocations, | |
| 294 // and closures. These should be handled by contextual inference. In most | |
| 295 // cases, inference will be sufficient, though in some it may unmask an actual | |
| 296 // error: e.g., | |
| 297 // List<int> l = [1, 2, 3]; // Inference succeeds | |
| 298 // List<String> l = [1, 2, 3]; // Inference reveals static type error | |
| 299 // We're marking all as warnings for now. | |
| 300 // | |
| 301 // TODO(vsm,leafp): Remove this. | |
| 302 class UninferredClosure extends DownCast { | |
| 303 UninferredClosure(TypeRules rules, FunctionExpression expression, Cast cast) | |
| 304 : super._internal(rules, expression, cast); | |
| 305 | |
| 306 toErrorCode() => new StaticTypeWarningCode(name, message); | |
| 307 } | |
| 308 | |
| 309 // | |
| 310 // Implicit down casts. These are only injected by the compiler by flag. | |
| 311 // | |
| 312 | |
| 313 // A down cast to a non-ground type. These behave differently from standard | |
| 314 // Dart and may be more likely to fail at runtime. | |
| 315 class DownCastComposite extends DownCast { | |
| 316 DownCastComposite(TypeRules rules, Expression expression, Cast cast) | |
| 317 : super._internal(rules, expression, cast); | |
| 318 | |
| 319 toErrorCode() => new StaticTypeWarningCode(name, message); | |
| 320 } | |
| 321 | |
| 322 // A down cast to a non-ground type. These behave differently from standard | |
| 323 // Dart and may be more likely to fail at runtime. | |
| 324 class DownCastImplicit extends DownCast { | |
| 325 DownCastImplicit(TypeRules rules, Expression expression, Cast cast) | |
| 326 : super._internal(rules, expression, cast); | |
| 327 | |
| 328 toErrorCode() => new HintCode(name, message); | |
| 329 } | |
| 330 | |
| 331 // An inferred type for the wrapped expression, which may need to be | |
| 332 // reified into the term | |
| 333 abstract class InferredTypeBase extends CoercionInfo { | |
| 334 final DartType _type; | |
| 335 | |
| 336 InferredTypeBase._internal(TypeRules rules, Expression expression, this._type) | |
| 337 : super(rules, expression); | |
| 338 | |
| 339 DartType get type => _type; | |
| 340 DartType get convertedType => type; | |
| 341 @override String get message => '{0} has inferred type {1}'; | |
| 342 @override List get arguments => [node, type]; | |
| 343 | |
| 344 toErrorCode() => new HintCode(name, message); | |
| 345 } | |
| 346 | |
| 347 // Standard / unspecialized inferred type | |
| 348 class InferredType extends InferredTypeBase { | |
| 349 InferredType(TypeRules rules, Expression expression, DartType type) | |
| 350 : super._internal(rules, expression, type); | |
| 351 | |
| 352 // Factory to create correct InferredType variant. | |
| 353 static InferredTypeBase create( | |
| 354 TypeRules rules, Expression expression, DartType type) { | |
| 355 // Specialized inference: | |
| 356 if (expression is Literal) { | |
| 357 return new InferredTypeLiteral(rules, expression, type); | |
| 358 } | |
| 359 if (expression is InstanceCreationExpression) { | |
| 360 return new InferredTypeAllocation(rules, expression, type); | |
| 361 } | |
| 362 if (expression is FunctionExpression) { | |
| 363 return new InferredTypeClosure(rules, expression, type); | |
| 364 } | |
| 365 return new InferredType(rules, expression, type); | |
| 366 } | |
| 367 } | |
| 368 | |
| 369 // An infered type for a literal expression. | |
| 370 class InferredTypeLiteral extends InferredTypeBase { | |
| 371 InferredTypeLiteral(TypeRules rules, Expression expression, DartType type) | |
| 372 : super._internal(rules, expression, type); | |
| 373 } | |
| 374 | |
| 375 // An inferred type for a non-literal allocation site. | |
| 376 class InferredTypeAllocation extends InferredTypeBase { | |
| 377 InferredTypeAllocation(TypeRules rules, Expression expression, DartType type) | |
| 378 : super._internal(rules, expression, type); | |
| 379 } | |
| 380 | |
| 381 // An inferred type for a closure expression | |
| 382 class InferredTypeClosure extends InferredTypeBase { | |
| 383 InferredTypeClosure(TypeRules rules, Expression expression, DartType type) | |
| 384 : super._internal(rules, expression, type); | |
| 385 } | |
| 386 | |
| 387 class DynamicInvoke extends CoercionInfo { | |
| 388 DynamicInvoke(TypeRules rules, Expression expression) | |
| 389 : super(rules, expression); | |
| 390 | |
| 391 DartType get convertedType => rules.provider.dynamicType; | |
| 392 String get message => '{0} requires dynamic invoke'; | |
| 393 toErrorCode() => new HintCode(name, message); | |
| 394 | |
| 395 static const String _propertyName = 'dev_compiler.src.info.DynamicInvoke'; | |
| 396 | |
| 397 /// Whether this [node] is the target of a dynamic operation. | |
| 398 static bool get(AstNode node) { | |
| 399 var value = node.getProperty(_propertyName); | |
| 400 return value != null ? value : false; | |
| 401 } | |
| 402 | |
| 403 /// Sets whether this node is the target of a dynamic operation. | |
| 404 static bool set(AstNode node, bool value) { | |
| 405 // Free the storage for things that aren't dynamic. | |
| 406 if (value == false) value = null; | |
| 407 node.setProperty(_propertyName, value); | |
| 408 return value; | |
| 409 } | |
| 410 } | |
| 411 | |
| 412 abstract class StaticError extends StaticInfo { | |
| 413 final AstNode node; | |
| 414 | |
| 415 StaticError(this.node); | |
| 416 | |
| 417 String get message; | |
| 418 | |
| 419 toErrorCode() => new CompileTimeErrorCode(name, message); | |
| 420 } | |
| 421 | |
| 422 class StaticTypeError extends StaticError { | |
| 423 final DartType baseType; | |
| 424 final DartType expectedType; | |
| 425 String reason = null; | |
| 426 | |
| 427 StaticTypeError(TypeRules rules, Expression expression, this.expectedType, | |
| 428 {this.reason}) | |
| 429 : baseType = rules.getStaticType(expression), | |
| 430 super(expression); | |
| 431 | |
| 432 @override List<Object> get arguments => [node, baseType, expectedType]; | |
| 433 @override String get message => | |
| 434 'Type check failed: {0} ({1}) is not of type {2}' + | |
| 435 ((reason == null) ? '' : ' because $reason'); | |
| 436 } | |
| 437 | |
| 438 class InvalidVariableDeclaration extends StaticError { | |
| 439 final DartType expectedType; | |
| 440 | |
| 441 InvalidVariableDeclaration( | |
| 442 TypeRules rules, AstNode declaration, this.expectedType) | |
| 443 : super(declaration); | |
| 444 | |
| 445 @override List<Object> get arguments => [expectedType]; | |
| 446 @override String get message => 'Type check failed: null is not of type {0}'; | |
| 447 } | |
| 448 | |
| 449 class InvalidParameterDeclaration extends StaticError { | |
| 450 final DartType expectedType; | |
| 451 | |
| 452 InvalidParameterDeclaration( | |
| 453 TypeRules rules, FormalParameter declaration, this.expectedType) | |
| 454 : super(declaration); | |
| 455 | |
| 456 @override List<Object> get arguments => [node, expectedType]; | |
| 457 @override String get message => 'Type check failed: {0} is not of type {1}'; | |
| 458 } | |
| 459 | |
| 460 class NonGroundTypeCheckInfo extends StaticInfo { | |
| 461 final DartType type; | |
| 462 final AstNode node; | |
| 463 | |
| 464 NonGroundTypeCheckInfo(this.node, this.type) { | |
| 465 assert(node is IsExpression || node is AsExpression); | |
| 466 } | |
| 467 | |
| 468 @override List<Object> get arguments => [type]; | |
| 469 String get message => | |
| 470 "Runtime check on non-ground type {0} may throw StrongModeError"; | |
| 471 | |
| 472 toErrorCode() => new HintCode(name, message); | |
| 473 } | |
| 474 | |
| 475 // Invalid override of an instance member of a class. | |
| 476 abstract class InvalidOverride extends StaticError { | |
| 477 /// Member declaration with the invalid override. | |
| 478 final ExecutableElement element; | |
| 479 | |
| 480 /// Type (class or interface) that provides the base declaration. | |
| 481 final InterfaceType base; | |
| 482 | |
| 483 /// Actual type of the overridden member. | |
| 484 final DartType subType; | |
| 485 | |
| 486 /// Actual type of the base member. | |
| 487 final DartType baseType; | |
| 488 | |
| 489 /// Whether the error comes from combining a base class and an interface | |
| 490 final bool fromBaseClass; | |
| 491 | |
| 492 /// Whether the error comes from a mixin (either overriding a base class or an | |
| 493 /// interface declaration). | |
| 494 final bool fromMixin; | |
| 495 | |
| 496 InvalidOverride( | |
| 497 AstNode node, this.element, this.base, this.subType, this.baseType) | |
| 498 : fromBaseClass = node is ExtendsClause, | |
| 499 fromMixin = node.parent is WithClause, | |
| 500 super(node); | |
| 501 | |
| 502 ClassElement get parent => element.enclosingElement; | |
| 503 | |
| 504 @override List<Object> get arguments => | |
| 505 [parent.name, element.name, subType, base, baseType]; | |
| 506 | |
| 507 String _messageHelper(String errorName) { | |
| 508 var lcErrorName = errorName.toLowerCase(); | |
| 509 var intro = fromBaseClass | |
| 510 ? 'Base class introduces an $lcErrorName' | |
| 511 : (fromMixin ? 'Mixin introduces an $lcErrorName' : errorName); | |
| 512 return '$intro. The type of {0}.{1} ({2}) is not a ' | |
| 513 'subtype of {3}.{1} ({4}).'; | |
| 514 } | |
| 515 } | |
| 516 | |
| 517 // Invalid override due to incompatible type. I.e., the overridden signature | |
| 518 // is not compatible with the original. | |
| 519 class InvalidMethodOverride extends InvalidOverride { | |
| 520 InvalidMethodOverride(AstNode node, ExecutableElement element, | |
| 521 InterfaceType base, FunctionType subType, FunctionType baseType) | |
| 522 : super(node, element, base, subType, baseType); | |
| 523 | |
| 524 String get message => _messageHelper('Invalid override'); | |
| 525 } | |
| 526 | |
| 527 /// Used to mark unexpected situations in our compiler were we couldn't compute | |
| 528 /// the type of an expression. | |
| 529 // TODO(sigmund): This is normally a result of another error that is caught by | |
| 530 // the analyzer, so this should likely be removed in the future. | |
| 531 class MissingTypeError extends StaticInfo { | |
| 532 final AstNode node; | |
| 533 toErrorCode() => new StaticTypeWarningCode(name, message); | |
| 534 | |
| 535 MissingTypeError(this.node); | |
| 536 | |
| 537 @override List<Object> get arguments => [node, node.runtimeType]; | |
| 538 String get message => "type analysis didn't compute the type of: {0} {1}"; | |
| 539 } | |
| 540 | |
| 541 /// Dart constructors have one weird quirk, illustrated with this example: | |
| 542 /// | |
| 543 /// class Base { | |
| 544 /// var x; | |
| 545 /// Base() : x = print('Base.1') { | |
| 546 /// print('Base.2'); | |
| 547 /// } | |
| 548 /// } | |
| 549 /// | |
| 550 /// class Derived extends Base { | |
| 551 /// var y, z; | |
| 552 /// Derived() | |
| 553 /// : y = print('Derived.1'), | |
| 554 /// super(), | |
| 555 /// z = print('Derived.2') { | |
| 556 /// print('Derived.3'); | |
| 557 /// } | |
| 558 /// } | |
| 559 /// | |
| 560 /// The order will be Derived.1, Base.1, Derived.2, Base.2, Derived.3; this | |
| 561 /// ordering preserves the invariant that code can't observe uninitialized | |
| 562 /// state, however it results in super constructor body not being run | |
| 563 /// immediately after super initializers. Normally this isn't observable, but it | |
| 564 /// could be if initializers have side effects. | |
| 565 /// | |
| 566 /// Better to have `super` at the end, as required by the Dart style guide: | |
| 567 /// <http://goo.gl/q1T4BB> | |
| 568 /// | |
| 569 /// For now this is the only pattern we support. | |
| 570 class InvalidSuperInvocation extends StaticError { | |
| 571 InvalidSuperInvocation(SuperConstructorInvocation node) : super(node); | |
| 572 | |
| 573 @override String get message => "super call must be last in an initializer " | |
| 574 "list (see http://goo.gl/q1T4BB): {0}"; | |
| 575 } | |
| OLD | NEW |