| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "FindBadConstructsConsumer.h" | 5 #include "FindBadConstructsConsumer.h" |
| 6 | 6 |
| 7 #include "clang/Frontend/CompilerInstance.h" | 7 #include "clang/Frontend/CompilerInstance.h" |
| 8 #include "clang/AST/Attr.h" | 8 #include "clang/AST/Attr.h" |
| 9 #include "clang/Lex/Lexer.h" | 9 #include "clang/Lex/Lexer.h" |
| 10 #include "llvm/Support/raw_ostream.h" | 10 #include "llvm/Support/raw_ostream.h" |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 71 } | 71 } |
| 72 | 72 |
| 73 // Generates a fixit hint to remove the 'virtual' keyword. | 73 // Generates a fixit hint to remove the 'virtual' keyword. |
| 74 // Unfortunately, there doesn't seem to be a good way to determine the source | 74 // Unfortunately, there doesn't seem to be a good way to determine the source |
| 75 // location of the 'virtual' keyword. It's available in Declarator, but that | 75 // location of the 'virtual' keyword. It's available in Declarator, but that |
| 76 // isn't accessible from the AST. So instead, make an educated guess that the | 76 // isn't accessible from the AST. So instead, make an educated guess that the |
| 77 // first token is probably the virtual keyword. Strictly speaking, this doesn't | 77 // first token is probably the virtual keyword. Strictly speaking, this doesn't |
| 78 // have to be true, but it probably will be. | 78 // have to be true, but it probably will be. |
| 79 // TODO(dcheng): Add a warning to force virtual to always appear first ;-) | 79 // TODO(dcheng): Add a warning to force virtual to always appear first ;-) |
| 80 FixItHint FixItRemovalForVirtual(const SourceManager& manager, | 80 FixItHint FixItRemovalForVirtual(const SourceManager& manager, |
| 81 const LangOptions& lang_opts, |
| 81 const CXXMethodDecl* method) { | 82 const CXXMethodDecl* method) { |
| 82 SourceRange range(method->getLocStart()); | 83 SourceRange range(method->getLocStart()); |
| 83 // Get the spelling loc just in case it was expanded from a macro. | 84 // Get the spelling loc just in case it was expanded from a macro. |
| 84 SourceRange spelling_range(manager.getSpellingLoc(range.getBegin())); | 85 SourceRange spelling_range(manager.getSpellingLoc(range.getBegin())); |
| 85 // Sanity check that the text looks like virtual. | 86 // Sanity check that the text looks like virtual. |
| 86 StringRef text = clang::Lexer::getSourceText( | 87 StringRef text = clang::Lexer::getSourceText( |
| 87 CharSourceRange::getTokenRange(spelling_range), manager, LangOptions()); | 88 CharSourceRange::getTokenRange(spelling_range), manager, lang_opts); |
| 88 if (text.trim() != "virtual") | 89 if (text.trim() != "virtual") |
| 89 return FixItHint(); | 90 return FixItHint(); |
| 90 return FixItHint::CreateRemoval(range); | 91 return FixItHint::CreateRemoval(range); |
| 91 } | 92 } |
| 92 | 93 |
| 93 bool IsPodOrTemplateType(const CXXRecordDecl& record) { | 94 bool IsPodOrTemplateType(const CXXRecordDecl& record) { |
| 94 return record.isPOD() || | 95 return record.isPOD() || |
| 95 record.getDescribedClassTemplate() || | 96 record.getDescribedClassTemplate() || |
| 96 record.getTemplateSpecializationKind() || | 97 record.getTemplateSpecializationKind() || |
| 97 record.isDependentType(); | 98 record.isDependentType(); |
| (...skipping 185 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 283 // find one that tries to be inline. | 284 // find one that tries to be inline. |
| 284 for (CXXRecordDecl::ctor_iterator it = record->ctor_begin(); | 285 for (CXXRecordDecl::ctor_iterator it = record->ctor_begin(); |
| 285 it != record->ctor_end(); | 286 it != record->ctor_end(); |
| 286 ++it) { | 287 ++it) { |
| 287 // The current check is buggy. An implicit copy constructor does not | 288 // The current check is buggy. An implicit copy constructor does not |
| 288 // have an inline body, so this check never fires for classes with a | 289 // have an inline body, so this check never fires for classes with a |
| 289 // user-declared out-of-line constructor. | 290 // user-declared out-of-line constructor. |
| 290 if (it->hasInlineBody()) { | 291 if (it->hasInlineBody()) { |
| 291 if (it->isCopyConstructor() && | 292 if (it->isCopyConstructor() && |
| 292 !record->hasUserDeclaredCopyConstructor()) { | 293 !record->hasUserDeclaredCopyConstructor()) { |
| 293 emitWarning(record_location, | 294 // In general, implicit constructors are generated on demand. But |
| 294 "Complex class/struct needs an explicit out-of-line " | 295 // in the Windows component build, dllexport causes instantiation of |
| 295 "copy constructor."); | 296 // the copy constructor which means that this fires on many more |
| 297 // classes. For now, suppress this on dllexported classes. |
| 298 // (This does mean that windows component builds will not emit this |
| 299 // warning in some cases where it is emitted in other configs, but |
| 300 // that's the better tradeoff at this point). |
| 301 // TODO(dcheng): With the RecursiveASTVisitor, these warnings might |
| 302 // be emitted on other platforms too, reevaluate if we want to keep |
| 303 // surpressing this then http://crbug.com/467288 |
| 304 if (!record->hasAttr<DLLExportAttr>()) |
| 305 emitWarning(record_location, |
| 306 "Complex class/struct needs an explicit out-of-line " |
| 307 "copy constructor."); |
| 296 } else { | 308 } else { |
| 297 emitWarning(it->getInnerLocStart(), | 309 // See the comment in the previous branch about copy constructors. |
| 298 "Complex constructor has an inlined body."); | 310 // This does the same for implicit move constructors. |
| 311 bool is_likely_compiler_generated_dllexport_move_ctor = |
| 312 it->isMoveConstructor() && |
| 313 !record->hasUserDeclaredMoveConstructor() && |
| 314 record->hasAttr<DLLExportAttr>(); |
| 315 if (!is_likely_compiler_generated_dllexport_move_ctor) |
| 316 emitWarning(it->getInnerLocStart(), |
| 317 "Complex constructor has an inlined body."); |
| 299 } | 318 } |
| 300 } else if (it->isInlined() && !it->isInlineSpecified() && | 319 } else if (it->isInlined() && !it->isInlineSpecified() && |
| 301 !it->isDeleted() && (!it->isCopyOrMoveConstructor() || | 320 !it->isDeleted() && (!it->isCopyOrMoveConstructor() || |
| 302 it->isExplicitlyDefaulted())) { | 321 it->isExplicitlyDefaulted())) { |
| 303 // isInlined() is a more reliable check than hasInlineBody(), but | 322 // isInlined() is a more reliable check than hasInlineBody(), but |
| 304 // unfortunately, it results in warnings for implicit copy/move | 323 // unfortunately, it results in warnings for implicit copy/move |
| 305 // constructors in the previously mentioned situation. To preserve | 324 // constructors in the previously mentioned situation. To preserve |
| 306 // compatibility with existing Chromium code, only warn if it's an | 325 // compatibility with existing Chromium code, only warn if it's an |
| 307 // explicitly defaulted copy or move constructor. | 326 // explicitly defaulted copy or move constructor. |
| 308 emitWarning(it->getInnerLocStart(), | 327 emitWarning(it->getInnerLocStart(), |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 347 // incorrectly, but having the plugin enforce override is also nice. | 366 // incorrectly, but having the plugin enforce override is also nice. |
| 348 (InTestingNamespace(overridden) && | 367 (InTestingNamespace(overridden) && |
| 349 !IsGtestTestFixture(overridden->getParent()))) { | 368 !IsGtestTestFixture(overridden->getParent()))) { |
| 350 return true; | 369 return true; |
| 351 } | 370 } |
| 352 } | 371 } |
| 353 | 372 |
| 354 return false; | 373 return false; |
| 355 } | 374 } |
| 356 | 375 |
| 376 SuppressibleDiagnosticBuilder |
| 377 FindBadConstructsConsumer::ReportIfSpellingLocNotIgnored( |
| 378 SourceLocation loc, |
| 379 unsigned diagnostic_id) { |
| 380 return SuppressibleDiagnosticBuilder( |
| 381 &diagnostic(), loc, diagnostic_id, |
| 382 InBannedDirectory(instance().getSourceManager().getSpellingLoc(loc))); |
| 383 } |
| 384 |
| 357 // Checks that virtual methods are correctly annotated, and have no body in a | 385 // Checks that virtual methods are correctly annotated, and have no body in a |
| 358 // header file. | 386 // header file. |
| 359 void FindBadConstructsConsumer::CheckVirtualMethods( | 387 void FindBadConstructsConsumer::CheckVirtualMethods( |
| 360 SourceLocation record_location, | 388 SourceLocation record_location, |
| 361 CXXRecordDecl* record, | 389 CXXRecordDecl* record, |
| 362 bool warn_on_inline_bodies) { | 390 bool warn_on_inline_bodies) { |
| 363 // Gmock objects trigger these for each MOCK_BLAH() macro used. So we have a | 391 // Gmock objects trigger these for each MOCK_BLAH() macro used. So we have a |
| 364 // trick to get around that. If a class has member variables whose types are | 392 // trick to get around that. If a class has member variables whose types are |
| 365 // in the "testing" namespace (which is how gmock works behind the scenes), | 393 // in the "testing" namespace (which is how gmock works behind the scenes), |
| 366 // there's a really high chance we won't care about these errors | 394 // there's a really high chance we won't care about these errors |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 404 const CXXMethodDecl* method) { | 432 const CXXMethodDecl* method) { |
| 405 bool is_override = method->size_overridden_methods() > 0; | 433 bool is_override = method->size_overridden_methods() > 0; |
| 406 bool has_virtual = method->isVirtualAsWritten(); | 434 bool has_virtual = method->isVirtualAsWritten(); |
| 407 OverrideAttr* override_attr = method->getAttr<OverrideAttr>(); | 435 OverrideAttr* override_attr = method->getAttr<OverrideAttr>(); |
| 408 FinalAttr* final_attr = method->getAttr<FinalAttr>(); | 436 FinalAttr* final_attr = method->getAttr<FinalAttr>(); |
| 409 | 437 |
| 410 if (IsMethodInBannedOrTestingNamespace(method)) | 438 if (IsMethodInBannedOrTestingNamespace(method)) |
| 411 return; | 439 return; |
| 412 | 440 |
| 413 SourceManager& manager = instance().getSourceManager(); | 441 SourceManager& manager = instance().getSourceManager(); |
| 442 const LangOptions& lang_opts = instance().getLangOpts(); |
| 414 | 443 |
| 415 // Complain if a method is annotated virtual && (override || final). | 444 // Complain if a method is annotated virtual && (override || final). |
| 416 if (has_virtual && (override_attr || final_attr)) { | 445 if (has_virtual && (override_attr || final_attr)) { |
| 417 // ... but only if virtual does not originate in a macro from a banned file. | 446 // ... but only if virtual does not originate in a macro from a banned file. |
| 418 // Note this is just an educated guess: the assumption here is that any | 447 // Note this is just an educated guess: the assumption here is that any |
| 419 // macro for declaring methods will probably be at the start of the method's | 448 // macro for declaring methods will probably be at the start of the method's |
| 420 // source range. | 449 // source range. |
| 421 if (!InBannedDirectory(manager.getSpellingLoc(method->getLocStart()))) { | 450 ReportIfSpellingLocNotIgnored(method->getLocStart(), |
| 422 diagnostic().Report(method->getLocStart(), | 451 diag_redundant_virtual_specifier_) |
| 423 diag_redundant_virtual_specifier_) | 452 << "'virtual'" |
| 424 << "'virtual'" | 453 << (override_attr ? static_cast<Attr*>(override_attr) : final_attr) |
| 425 << (override_attr ? static_cast<Attr*>(override_attr) : final_attr) | 454 << FixItRemovalForVirtual(manager, lang_opts, method); |
| 426 << FixItRemovalForVirtual(manager, method); | |
| 427 } | |
| 428 } | 455 } |
| 429 | 456 |
| 430 // Complain if a method is an override and is not annotated with override or | 457 // Complain if a method is an override and is not annotated with override or |
| 431 // final. | 458 // final. |
| 432 if (is_override && !override_attr && !final_attr) { | 459 if (is_override && !override_attr && !final_attr) { |
| 433 SourceRange range = method->getSourceRange(); | 460 SourceRange range = method->getSourceRange(); |
| 434 SourceLocation loc; | 461 SourceLocation loc; |
| 435 if (method->hasInlineBody()) { | 462 if (method->hasInlineBody()) { |
| 436 loc = method->getBody()->getSourceRange().getBegin(); | 463 loc = method->getBody()->getSourceRange().getBegin(); |
| 437 } else { | 464 } else { |
| 438 // TODO(dcheng): We should probably use ASTContext's LangOptions here. | 465 loc = Lexer::getLocForEndOfToken(manager.getSpellingLoc(range.getEnd()), |
| 439 LangOptions lang_options; | 466 0, manager, lang_opts); |
| 440 loc = Lexer::getLocForEndOfToken( | |
| 441 manager.getSpellingLoc(range.getEnd()), 0, | |
| 442 manager, lang_options); | |
| 443 // The original code used the ending source loc of TypeSourceInfo's | 467 // The original code used the ending source loc of TypeSourceInfo's |
| 444 // TypeLoc. Unfortunately, this breaks down in the presence of attributes. | 468 // TypeLoc. Unfortunately, this breaks down in the presence of attributes. |
| 445 // Attributes often appear at the end of a TypeLoc, e.g. | 469 // Attributes often appear at the end of a TypeLoc, e.g. |
| 446 // virtual ULONG __stdcall AddRef() | 470 // virtual ULONG __stdcall AddRef() |
| 447 // has a TypeSourceInfo that looks something like: | 471 // has a TypeSourceInfo that looks something like: |
| 448 // ULONG AddRef() __attribute(stdcall) | 472 // ULONG AddRef() __attribute(stdcall) |
| 449 // so a fix-it insertion would be generated to insert 'override' after | 473 // so a fix-it insertion would be generated to insert 'override' after |
| 450 // __stdcall in the code as written. | 474 // __stdcall in the code as written. |
| 451 // While using the spelling loc of the CXXMethodDecl fixes attribute | 475 // While using the spelling loc of the CXXMethodDecl fixes attribute |
| 452 // handling, it breaks handling of "= 0" and similar constructs.. To work | 476 // handling, it breaks handling of "= 0" and similar constructs.. To work |
| 453 // around this, scan backwards in the source text for a '=' or ')' token | 477 // around this, scan backwards in the source text for a '=' or ')' token |
| 454 // and adjust the location as needed... | 478 // and adjust the location as needed... |
| 455 for (SourceLocation l = loc.getLocWithOffset(-1); | 479 for (SourceLocation l = loc.getLocWithOffset(-1); |
| 456 l != manager.getLocForStartOfFile(manager.getFileID(loc)); | 480 l != manager.getLocForStartOfFile(manager.getFileID(loc)); |
| 457 l = l.getLocWithOffset(-1)) { | 481 l = l.getLocWithOffset(-1)) { |
| 458 l = Lexer::GetBeginningOfToken(l, manager, lang_options); | 482 l = Lexer::GetBeginningOfToken(l, manager, lang_opts); |
| 459 Token token; | 483 Token token; |
| 460 // getRawToken() returns *true* on failure. In that case, just give up | 484 // getRawToken() returns *true* on failure. In that case, just give up |
| 461 // and don't bother generating a possibly incorrect fix-it. | 485 // and don't bother generating a possibly incorrect fix-it. |
| 462 if (Lexer::getRawToken(l, token, manager, lang_options, true)) { | 486 if (Lexer::getRawToken(l, token, manager, lang_opts, true)) { |
| 463 loc = SourceLocation(); | 487 loc = SourceLocation(); |
| 464 break; | 488 break; |
| 465 } | 489 } |
| 466 if (token.is(tok::r_paren)) { | 490 if (token.is(tok::r_paren)) { |
| 467 break; | 491 break; |
| 468 } else if (token.is(tok::equal)) { | 492 } else if (token.is(tok::equal)) { |
| 469 loc = l; | 493 loc = l; |
| 470 break; | 494 break; |
| 471 } | 495 } |
| 472 } | 496 } |
| 473 } | 497 } |
| 498 // Again, only emit the warning if it doesn't originate from a macro in |
| 499 // a system header. |
| 474 if (loc.isValid()) { | 500 if (loc.isValid()) { |
| 475 diagnostic().Report(loc, diag_method_requires_override_) | 501 ReportIfSpellingLocNotIgnored(loc, diag_method_requires_override_) |
| 476 << FixItHint::CreateInsertion(loc, " override"); | 502 << FixItHint::CreateInsertion(loc, " override"); |
| 477 } else { | 503 } else { |
| 478 diagnostic().Report(range.getBegin(), diag_method_requires_override_); | 504 ReportIfSpellingLocNotIgnored(range.getBegin(), |
| 505 diag_method_requires_override_); |
| 479 } | 506 } |
| 480 } | 507 } |
| 481 | 508 |
| 482 if (final_attr && override_attr) { | 509 if (final_attr && override_attr) { |
| 483 diagnostic().Report(override_attr->getLocation(), | 510 ReportIfSpellingLocNotIgnored(override_attr->getLocation(), |
| 484 diag_redundant_virtual_specifier_) | 511 diag_redundant_virtual_specifier_) |
| 485 << override_attr << final_attr | 512 << override_attr << final_attr |
| 486 << FixItHint::CreateRemoval(override_attr->getRange()); | 513 << FixItHint::CreateRemoval(override_attr->getRange()); |
| 487 } | 514 } |
| 488 | 515 |
| 489 if (final_attr && !is_override) { | 516 if (final_attr && !is_override) { |
| 490 diagnostic().Report(method->getLocStart(), | 517 ReportIfSpellingLocNotIgnored(method->getLocStart(), |
| 491 diag_base_method_virtual_and_final_) | 518 diag_base_method_virtual_and_final_) |
| 492 << FixItRemovalForVirtual(manager, method) | 519 << FixItRemovalForVirtual(manager, lang_opts, method) |
| 493 << FixItHint::CreateRemoval(final_attr->getRange()); | 520 << FixItHint::CreateRemoval(final_attr->getRange()); |
| 494 } | 521 } |
| 495 } | 522 } |
| 496 | 523 |
| 497 void FindBadConstructsConsumer::CheckVirtualBodies( | 524 void FindBadConstructsConsumer::CheckVirtualBodies( |
| 498 const CXXMethodDecl* method) { | 525 const CXXMethodDecl* method) { |
| 499 // Virtual methods should not have inline definitions beyond "{}". This | 526 // Virtual methods should not have inline definitions beyond "{}". This |
| 500 // only matters for header files. | 527 // only matters for header files. |
| 501 if (method->hasBody() && method->hasInlineBody()) { | 528 if (method->hasBody() && method->hasInlineBody()) { |
| 502 if (CompoundStmt* cs = dyn_cast<CompoundStmt>(method->getBody())) { | 529 if (CompoundStmt* cs = dyn_cast<CompoundStmt>(method->getBody())) { |
| 503 if (cs->size()) { | 530 if (cs->size()) { |
| 504 emitWarning(cs->getLBracLoc(), | 531 SourceLocation loc = cs->getLBracLoc(); |
| 505 "virtual methods with non-empty bodies shouldn't be " | 532 // CR_BEGIN_MSG_MAP_EX and BEGIN_SAFE_MSG_MAP_EX try to be compatible |
| 506 "declared inline."); | 533 // to BEGIN_MSG_MAP(_EX). So even though they are in chrome code, |
| 534 // we can't easily fix them, so explicitly whitelist them here. |
| 535 bool emit = true; |
| 536 if (loc.isMacroID()) { |
| 537 SourceManager& manager = instance().getSourceManager(); |
| 538 if (InBannedDirectory(manager.getSpellingLoc(loc))) |
| 539 emit = false; |
| 540 else { |
| 541 StringRef name = Lexer::getImmediateMacroName( |
| 542 loc, manager, instance().getLangOpts()); |
| 543 if (name == "CR_BEGIN_MSG_MAP_EX" || |
| 544 name == "BEGIN_SAFE_MSG_MAP_EX") |
| 545 emit = false; |
| 546 } |
| 547 } |
| 548 if (emit) |
| 549 emitWarning(loc, |
| 550 "virtual methods with non-empty bodies shouldn't be " |
| 551 "declared inline."); |
| 507 } | 552 } |
| 508 } | 553 } |
| 509 } | 554 } |
| 510 } | 555 } |
| 511 | 556 |
| 512 void FindBadConstructsConsumer::CountType(const Type* type, | 557 void FindBadConstructsConsumer::CountType(const Type* type, |
| 513 int* trivial_member, | 558 int* trivial_member, |
| 514 int* non_trivial_member, | 559 int* non_trivial_member, |
| 515 int* templated_non_trivial_member) { | 560 int* templated_non_trivial_member) { |
| 516 switch (type->getTypeClass()) { | 561 switch (type->getTypeClass()) { |
| 517 case Type::Record: { | 562 case Type::Record: { |
| 518 // Simplifying; the whole class isn't trivial if the dtor is, but | 563 // Simplifying; the whole class isn't trivial if the dtor is, but |
| 519 // we use this as a signal about complexity. | 564 // we use this as a signal about complexity. |
| 520 if (TypeHasNonTrivialDtor(type)) | 565 if (TypeHasNonTrivialDtor(type)) |
| 521 (*trivial_member)++; | 566 (*trivial_member)++; |
| 522 else | 567 else |
| 523 (*non_trivial_member)++; | 568 (*non_trivial_member)++; |
| 524 break; | 569 break; |
| 525 } | 570 } |
| 526 case Type::TemplateSpecialization: { | 571 case Type::TemplateSpecialization: { |
| 527 TemplateName name = | 572 TemplateName name = |
| 528 dyn_cast<TemplateSpecializationType>(type)->getTemplateName(); | 573 dyn_cast<TemplateSpecializationType>(type)->getTemplateName(); |
| 529 bool whitelisted_template = false; | 574 bool whitelisted_template = false; |
| 530 | 575 |
| 531 // HACK: I'm at a loss about how to get the syntax checker to get | 576 // HACK: I'm at a loss about how to get the syntax checker to get |
| 532 // whether a template is exterened or not. For the first pass here, | 577 // whether a template is externed or not. For the first pass here, |
| 533 // just do retarded string comparisons. | 578 // just do retarded string comparisons. |
| 534 if (TemplateDecl* decl = name.getAsTemplateDecl()) { | 579 if (TemplateDecl* decl = name.getAsTemplateDecl()) { |
| 535 std::string base_name = decl->getNameAsString(); | 580 std::string base_name = decl->getNameAsString(); |
| 536 if (base_name == "basic_string") | 581 if (base_name == "basic_string") |
| 537 whitelisted_template = true; | 582 whitelisted_template = true; |
| 538 } | 583 } |
| 539 | 584 |
| 540 if (whitelisted_template) | 585 if (whitelisted_template) |
| 541 (*non_trivial_member)++; | 586 (*non_trivial_member)++; |
| 542 else | 587 else |
| (...skipping 274 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 817 // one of those, it means there is at least one member after a factory. | 862 // one of those, it means there is at least one member after a factory. |
| 818 if (weak_ptr_factory_location.isValid() && | 863 if (weak_ptr_factory_location.isValid() && |
| 819 !param_is_weak_ptr_factory_to_self) { | 864 !param_is_weak_ptr_factory_to_self) { |
| 820 diagnostic().Report(weak_ptr_factory_location, | 865 diagnostic().Report(weak_ptr_factory_location, |
| 821 diag_weak_ptr_factory_order_); | 866 diag_weak_ptr_factory_order_); |
| 822 } | 867 } |
| 823 } | 868 } |
| 824 } | 869 } |
| 825 | 870 |
| 826 } // namespace chrome_checker | 871 } // namespace chrome_checker |
| OLD | NEW |