AnalysisBasedWarnings.cpp revision af13d5b25b360e698cc1cf1055ad7d14e008e505
1//=- AnalysisBasedWarnings.cpp - Sema warnings based on libAnalysis -*- C++ -*-=// 2// 3// The LLVM Compiler Infrastructure 4// 5// This file is distributed under the University of Illinois Open Source 6// License. See LICENSE.TXT for details. 7// 8//===----------------------------------------------------------------------===// 9// 10// This file defines analysis_warnings::[Policy,Executor]. 11// Together they are used by Sema to issue warnings based on inexpensive 12// static analysis algorithms in libAnalysis. 13// 14//===----------------------------------------------------------------------===// 15 16#include "clang/Sema/AnalysisBasedWarnings.h" 17#include "clang/Sema/SemaInternal.h" 18#include "clang/Sema/ScopeInfo.h" 19#include "clang/Basic/SourceManager.h" 20#include "clang/Lex/Preprocessor.h" 21#include "clang/AST/DeclObjC.h" 22#include "clang/AST/DeclCXX.h" 23#include "clang/AST/ExprObjC.h" 24#include "clang/AST/ExprCXX.h" 25#include "clang/AST/StmtObjC.h" 26#include "clang/AST/StmtCXX.h" 27#include "clang/Analysis/AnalysisContext.h" 28#include "clang/Analysis/CFG.h" 29#include "clang/Analysis/Analyses/ReachableCode.h" 30#include "clang/Analysis/Analyses/CFGReachabilityAnalysis.h" 31#include "clang/Analysis/CFGStmtMap.h" 32#include "clang/Analysis/Analyses/UninitializedValues.h" 33#include "llvm/ADT/BitVector.h" 34#include "llvm/Support/Casting.h" 35 36using namespace clang; 37 38//===----------------------------------------------------------------------===// 39// Unreachable code analysis. 40//===----------------------------------------------------------------------===// 41 42namespace { 43 class UnreachableCodeHandler : public reachable_code::Callback { 44 Sema &S; 45 public: 46 UnreachableCodeHandler(Sema &s) : S(s) {} 47 48 void HandleUnreachable(SourceLocation L, SourceRange R1, SourceRange R2) { 49 S.Diag(L, diag::warn_unreachable) << R1 << R2; 50 } 51 }; 52} 53 54/// CheckUnreachable - Check for unreachable code. 55static void CheckUnreachable(Sema &S, AnalysisContext &AC) { 56 UnreachableCodeHandler UC(S); 57 reachable_code::FindUnreachableCode(AC, UC); 58} 59 60//===----------------------------------------------------------------------===// 61// Check for missing return value. 62//===----------------------------------------------------------------------===// 63 64enum ControlFlowKind { 65 UnknownFallThrough, 66 NeverFallThrough, 67 MaybeFallThrough, 68 AlwaysFallThrough, 69 NeverFallThroughOrReturn 70}; 71 72/// CheckFallThrough - Check that we don't fall off the end of a 73/// Statement that should return a value. 74/// 75/// \returns AlwaysFallThrough iff we always fall off the end of the statement, 76/// MaybeFallThrough iff we might or might not fall off the end, 77/// NeverFallThroughOrReturn iff we never fall off the end of the statement or 78/// return. We assume NeverFallThrough iff we never fall off the end of the 79/// statement but we may return. We assume that functions not marked noreturn 80/// will return. 81static ControlFlowKind CheckFallThrough(AnalysisContext &AC) { 82 CFG *cfg = AC.getCFG(); 83 if (cfg == 0) return UnknownFallThrough; 84 85 // The CFG leaves in dead things, and we don't want the dead code paths to 86 // confuse us, so we mark all live things first. 87 llvm::BitVector live(cfg->getNumBlockIDs()); 88 unsigned count = reachable_code::ScanReachableFromBlock(cfg->getEntry(), 89 live); 90 91 bool AddEHEdges = AC.getAddEHEdges(); 92 if (!AddEHEdges && count != cfg->getNumBlockIDs()) 93 // When there are things remaining dead, and we didn't add EH edges 94 // from CallExprs to the catch clauses, we have to go back and 95 // mark them as live. 96 for (CFG::iterator I = cfg->begin(), E = cfg->end(); I != E; ++I) { 97 CFGBlock &b = **I; 98 if (!live[b.getBlockID()]) { 99 if (b.pred_begin() == b.pred_end()) { 100 if (b.getTerminator() && isa<CXXTryStmt>(b.getTerminator())) 101 // When not adding EH edges from calls, catch clauses 102 // can otherwise seem dead. Avoid noting them as dead. 103 count += reachable_code::ScanReachableFromBlock(b, live); 104 continue; 105 } 106 } 107 } 108 109 // Now we know what is live, we check the live precessors of the exit block 110 // and look for fall through paths, being careful to ignore normal returns, 111 // and exceptional paths. 112 bool HasLiveReturn = false; 113 bool HasFakeEdge = false; 114 bool HasPlainEdge = false; 115 bool HasAbnormalEdge = false; 116 117 // Ignore default cases that aren't likely to be reachable because all 118 // enums in a switch(X) have explicit case statements. 119 CFGBlock::FilterOptions FO; 120 FO.IgnoreDefaultsWithCoveredEnums = 1; 121 122 for (CFGBlock::filtered_pred_iterator 123 I = cfg->getExit().filtered_pred_start_end(FO); I.hasMore(); ++I) { 124 const CFGBlock& B = **I; 125 if (!live[B.getBlockID()]) 126 continue; 127 128 // Destructors can appear after the 'return' in the CFG. This is 129 // normal. We need to look pass the destructors for the return 130 // statement (if it exists). 131 CFGBlock::const_reverse_iterator ri = B.rbegin(), re = B.rend(); 132 bool hasNoReturnDtor = false; 133 134 for ( ; ri != re ; ++ri) { 135 CFGElement CE = *ri; 136 137 // FIXME: The right solution is to just sever the edges in the 138 // CFG itself. 139 if (const CFGImplicitDtor *iDtor = ri->getAs<CFGImplicitDtor>()) 140 if (iDtor->isNoReturn(AC.getASTContext())) { 141 hasNoReturnDtor = true; 142 HasFakeEdge = true; 143 break; 144 } 145 146 if (isa<CFGStmt>(CE)) 147 break; 148 } 149 150 if (hasNoReturnDtor) 151 continue; 152 153 // No more CFGElements in the block? 154 if (ri == re) { 155 if (B.getTerminator() && isa<CXXTryStmt>(B.getTerminator())) { 156 HasAbnormalEdge = true; 157 continue; 158 } 159 // A labeled empty statement, or the entry block... 160 HasPlainEdge = true; 161 continue; 162 } 163 164 CFGStmt CS = cast<CFGStmt>(*ri); 165 Stmt *S = CS.getStmt(); 166 if (isa<ReturnStmt>(S)) { 167 HasLiveReturn = true; 168 continue; 169 } 170 if (isa<ObjCAtThrowStmt>(S)) { 171 HasFakeEdge = true; 172 continue; 173 } 174 if (isa<CXXThrowExpr>(S)) { 175 HasFakeEdge = true; 176 continue; 177 } 178 if (const AsmStmt *AS = dyn_cast<AsmStmt>(S)) { 179 if (AS->isMSAsm()) { 180 HasFakeEdge = true; 181 HasLiveReturn = true; 182 continue; 183 } 184 } 185 if (isa<CXXTryStmt>(S)) { 186 HasAbnormalEdge = true; 187 continue; 188 } 189 190 bool NoReturnEdge = false; 191 if (CallExpr *C = dyn_cast<CallExpr>(S)) { 192 if (std::find(B.succ_begin(), B.succ_end(), &cfg->getExit()) 193 == B.succ_end()) { 194 HasAbnormalEdge = true; 195 continue; 196 } 197 Expr *CEE = C->getCallee()->IgnoreParenCasts(); 198 if (getFunctionExtInfo(CEE->getType()).getNoReturn()) { 199 NoReturnEdge = true; 200 HasFakeEdge = true; 201 } else if (DeclRefExpr *DRE = dyn_cast<DeclRefExpr>(CEE)) { 202 ValueDecl *VD = DRE->getDecl(); 203 if (VD->hasAttr<NoReturnAttr>()) { 204 NoReturnEdge = true; 205 HasFakeEdge = true; 206 } 207 } 208 } 209 // FIXME: Add noreturn message sends. 210 if (NoReturnEdge == false) 211 HasPlainEdge = true; 212 } 213 if (!HasPlainEdge) { 214 if (HasLiveReturn) 215 return NeverFallThrough; 216 return NeverFallThroughOrReturn; 217 } 218 if (HasAbnormalEdge || HasFakeEdge || HasLiveReturn) 219 return MaybeFallThrough; 220 // This says AlwaysFallThrough for calls to functions that are not marked 221 // noreturn, that don't return. If people would like this warning to be more 222 // accurate, such functions should be marked as noreturn. 223 return AlwaysFallThrough; 224} 225 226namespace { 227 228struct CheckFallThroughDiagnostics { 229 unsigned diag_MaybeFallThrough_HasNoReturn; 230 unsigned diag_MaybeFallThrough_ReturnsNonVoid; 231 unsigned diag_AlwaysFallThrough_HasNoReturn; 232 unsigned diag_AlwaysFallThrough_ReturnsNonVoid; 233 unsigned diag_NeverFallThroughOrReturn; 234 bool funMode; 235 SourceLocation FuncLoc; 236 237 static CheckFallThroughDiagnostics MakeForFunction(const Decl *Func) { 238 CheckFallThroughDiagnostics D; 239 D.FuncLoc = Func->getLocation(); 240 D.diag_MaybeFallThrough_HasNoReturn = 241 diag::warn_falloff_noreturn_function; 242 D.diag_MaybeFallThrough_ReturnsNonVoid = 243 diag::warn_maybe_falloff_nonvoid_function; 244 D.diag_AlwaysFallThrough_HasNoReturn = 245 diag::warn_falloff_noreturn_function; 246 D.diag_AlwaysFallThrough_ReturnsNonVoid = 247 diag::warn_falloff_nonvoid_function; 248 249 // Don't suggest that virtual functions be marked "noreturn", since they 250 // might be overridden by non-noreturn functions. 251 bool isVirtualMethod = false; 252 if (const CXXMethodDecl *Method = dyn_cast<CXXMethodDecl>(Func)) 253 isVirtualMethod = Method->isVirtual(); 254 255 if (!isVirtualMethod) 256 D.diag_NeverFallThroughOrReturn = 257 diag::warn_suggest_noreturn_function; 258 else 259 D.diag_NeverFallThroughOrReturn = 0; 260 261 D.funMode = true; 262 return D; 263 } 264 265 static CheckFallThroughDiagnostics MakeForBlock() { 266 CheckFallThroughDiagnostics D; 267 D.diag_MaybeFallThrough_HasNoReturn = 268 diag::err_noreturn_block_has_return_expr; 269 D.diag_MaybeFallThrough_ReturnsNonVoid = 270 diag::err_maybe_falloff_nonvoid_block; 271 D.diag_AlwaysFallThrough_HasNoReturn = 272 diag::err_noreturn_block_has_return_expr; 273 D.diag_AlwaysFallThrough_ReturnsNonVoid = 274 diag::err_falloff_nonvoid_block; 275 D.diag_NeverFallThroughOrReturn = 276 diag::warn_suggest_noreturn_block; 277 D.funMode = false; 278 return D; 279 } 280 281 bool checkDiagnostics(Diagnostic &D, bool ReturnsVoid, 282 bool HasNoReturn) const { 283 if (funMode) { 284 return (ReturnsVoid || 285 D.getDiagnosticLevel(diag::warn_maybe_falloff_nonvoid_function, 286 FuncLoc) == Diagnostic::Ignored) 287 && (!HasNoReturn || 288 D.getDiagnosticLevel(diag::warn_noreturn_function_has_return_expr, 289 FuncLoc) == Diagnostic::Ignored) 290 && (!ReturnsVoid || 291 D.getDiagnosticLevel(diag::warn_suggest_noreturn_block, FuncLoc) 292 == Diagnostic::Ignored); 293 } 294 295 // For blocks. 296 return ReturnsVoid && !HasNoReturn 297 && (!ReturnsVoid || 298 D.getDiagnosticLevel(diag::warn_suggest_noreturn_block, FuncLoc) 299 == Diagnostic::Ignored); 300 } 301}; 302 303} 304 305/// CheckFallThroughForFunctionDef - Check that we don't fall off the end of a 306/// function that should return a value. Check that we don't fall off the end 307/// of a noreturn function. We assume that functions and blocks not marked 308/// noreturn will return. 309static void CheckFallThroughForBody(Sema &S, const Decl *D, const Stmt *Body, 310 const BlockExpr *blkExpr, 311 const CheckFallThroughDiagnostics& CD, 312 AnalysisContext &AC) { 313 314 bool ReturnsVoid = false; 315 bool HasNoReturn = false; 316 317 if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(D)) { 318 ReturnsVoid = FD->getResultType()->isVoidType(); 319 HasNoReturn = FD->hasAttr<NoReturnAttr>() || 320 FD->getType()->getAs<FunctionType>()->getNoReturnAttr(); 321 } 322 else if (const ObjCMethodDecl *MD = dyn_cast<ObjCMethodDecl>(D)) { 323 ReturnsVoid = MD->getResultType()->isVoidType(); 324 HasNoReturn = MD->hasAttr<NoReturnAttr>(); 325 } 326 else if (isa<BlockDecl>(D)) { 327 QualType BlockTy = blkExpr->getType(); 328 if (const FunctionType *FT = 329 BlockTy->getPointeeType()->getAs<FunctionType>()) { 330 if (FT->getResultType()->isVoidType()) 331 ReturnsVoid = true; 332 if (FT->getNoReturnAttr()) 333 HasNoReturn = true; 334 } 335 } 336 337 Diagnostic &Diags = S.getDiagnostics(); 338 339 // Short circuit for compilation speed. 340 if (CD.checkDiagnostics(Diags, ReturnsVoid, HasNoReturn)) 341 return; 342 343 // FIXME: Function try block 344 if (const CompoundStmt *Compound = dyn_cast<CompoundStmt>(Body)) { 345 switch (CheckFallThrough(AC)) { 346 case UnknownFallThrough: 347 break; 348 349 case MaybeFallThrough: 350 if (HasNoReturn) 351 S.Diag(Compound->getRBracLoc(), 352 CD.diag_MaybeFallThrough_HasNoReturn); 353 else if (!ReturnsVoid) 354 S.Diag(Compound->getRBracLoc(), 355 CD.diag_MaybeFallThrough_ReturnsNonVoid); 356 break; 357 case AlwaysFallThrough: 358 if (HasNoReturn) 359 S.Diag(Compound->getRBracLoc(), 360 CD.diag_AlwaysFallThrough_HasNoReturn); 361 else if (!ReturnsVoid) 362 S.Diag(Compound->getRBracLoc(), 363 CD.diag_AlwaysFallThrough_ReturnsNonVoid); 364 break; 365 case NeverFallThroughOrReturn: 366 if (ReturnsVoid && !HasNoReturn && CD.diag_NeverFallThroughOrReturn) 367 S.Diag(Compound->getLBracLoc(), 368 CD.diag_NeverFallThroughOrReturn); 369 break; 370 case NeverFallThrough: 371 break; 372 } 373 } 374} 375 376//===----------------------------------------------------------------------===// 377// -Wuninitialized 378//===----------------------------------------------------------------------===// 379 380typedef std::pair<const Expr*, bool> UninitUse; 381 382namespace { 383struct SLocSort { 384 bool operator()(const UninitUse &a, const UninitUse &b) { 385 SourceLocation aLoc = a.first->getLocStart(); 386 SourceLocation bLoc = b.first->getLocStart(); 387 return aLoc.getRawEncoding() < bLoc.getRawEncoding(); 388 } 389}; 390 391class UninitValsDiagReporter : public UninitVariablesHandler { 392 Sema &S; 393 typedef llvm::SmallVector<UninitUse, 2> UsesVec; 394 typedef llvm::DenseMap<const VarDecl *, UsesVec*> UsesMap; 395 UsesMap *uses; 396 397public: 398 UninitValsDiagReporter(Sema &S) : S(S), uses(0) {} 399 ~UninitValsDiagReporter() { 400 flushDiagnostics(); 401 } 402 403 void handleUseOfUninitVariable(const Expr *ex, const VarDecl *vd, 404 bool isAlwaysUninit) { 405 if (!uses) 406 uses = new UsesMap(); 407 408 UsesVec *&vec = (*uses)[vd]; 409 if (!vec) 410 vec = new UsesVec(); 411 412 vec->push_back(std::make_pair(ex, isAlwaysUninit)); 413 } 414 415 void flushDiagnostics() { 416 if (!uses) 417 return; 418 419 for (UsesMap::iterator i = uses->begin(), e = uses->end(); i != e; ++i) { 420 const VarDecl *vd = i->first; 421 UsesVec *vec = i->second; 422 423 bool fixitIssued = false; 424 425 // Sort the uses by their SourceLocations. While not strictly 426 // guaranteed to produce them in line/column order, this will provide 427 // a stable ordering. 428 std::sort(vec->begin(), vec->end(), SLocSort()); 429 430 for (UsesVec::iterator vi = vec->begin(), ve = vec->end(); vi != ve; ++vi) 431 { 432 const bool isAlwaysUninit = vi->second; 433 if (const DeclRefExpr *dr = dyn_cast<DeclRefExpr>(vi->first)) { 434 S.Diag(dr->getLocStart(), 435 isAlwaysUninit ? diag::warn_uninit_var 436 : diag::warn_maybe_uninit_var) 437 << vd->getDeclName() << dr->getSourceRange(); 438 } 439 else { 440 const BlockExpr *be = cast<BlockExpr>(vi->first); 441 S.Diag(be->getLocStart(), 442 isAlwaysUninit ? diag::warn_uninit_var_captured_by_block 443 : diag::warn_maybe_uninit_var_captured_by_block) 444 << vd->getDeclName(); 445 } 446 447 // Report where the variable was declared. 448 S.Diag(vd->getLocStart(), diag::note_uninit_var_def) 449 << vd->getDeclName(); 450 451 // Only report the fixit once. 452 if (fixitIssued) 453 continue; 454 455 fixitIssued = true; 456 457 // Suggest possible initialization (if any). 458 const char *initialization = 0; 459 QualType vdTy = vd->getType().getCanonicalType(); 460 461 if (vdTy->getAs<ObjCObjectPointerType>()) { 462 // Check if 'nil' is defined. 463 if (S.PP.getMacroInfo(&S.getASTContext().Idents.get("nil"))) 464 initialization = " = nil"; 465 else 466 initialization = " = 0"; 467 } 468 else if (vdTy->isRealFloatingType()) 469 initialization = " = 0.0"; 470 else if (vdTy->isBooleanType() && S.Context.getLangOptions().CPlusPlus) 471 initialization = " = false"; 472 else if (vdTy->isEnumeralType()) 473 continue; 474 else if (vdTy->isScalarType()) 475 initialization = " = 0"; 476 477 if (initialization) { 478 SourceLocation loc = S.PP.getLocForEndOfToken(vd->getLocEnd()); 479 S.Diag(loc, diag::note_var_fixit_add_initialization) 480 << FixItHint::CreateInsertion(loc, initialization); 481 } 482 } 483 delete vec; 484 } 485 delete uses; 486 } 487}; 488} 489 490//===----------------------------------------------------------------------===// 491// AnalysisBasedWarnings - Worker object used by Sema to execute analysis-based 492// warnings on a function, method, or block. 493//===----------------------------------------------------------------------===// 494 495clang::sema::AnalysisBasedWarnings::Policy::Policy() { 496 enableCheckFallThrough = 1; 497 enableCheckUnreachable = 0; 498} 499 500clang::sema::AnalysisBasedWarnings::AnalysisBasedWarnings(Sema &s) : S(s) { 501 Diagnostic &D = S.getDiagnostics(); 502 DefaultPolicy.enableCheckUnreachable = (unsigned) 503 (D.getDiagnosticLevel(diag::warn_unreachable, SourceLocation()) != 504 Diagnostic::Ignored); 505} 506 507static void flushDiagnostics(Sema &S, sema::FunctionScopeInfo *fscope) { 508 for (llvm::SmallVectorImpl<sema::PossiblyUnreachableDiag>::iterator 509 i = fscope->PossiblyUnreachableDiags.begin(), 510 e = fscope->PossiblyUnreachableDiags.end(); 511 i != e; ++i) { 512 const sema::PossiblyUnreachableDiag &D = *i; 513 S.Diag(D.Loc, D.PD); 514 } 515} 516 517void clang::sema:: 518AnalysisBasedWarnings::IssueWarnings(sema::AnalysisBasedWarnings::Policy P, 519 sema::FunctionScopeInfo *fscope, 520 const Decl *D, const BlockExpr *blkExpr) { 521 522 // We avoid doing analysis-based warnings when there are errors for 523 // two reasons: 524 // (1) The CFGs often can't be constructed (if the body is invalid), so 525 // don't bother trying. 526 // (2) The code already has problems; running the analysis just takes more 527 // time. 528 Diagnostic &Diags = S.getDiagnostics(); 529 530 // Do not do any analysis for declarations in system headers if we are 531 // going to just ignore them. 532 if (Diags.getSuppressSystemWarnings() && 533 S.SourceMgr.isInSystemHeader(D->getLocation())) 534 return; 535 536 // For code in dependent contexts, we'll do this at instantiation time. 537 if (cast<DeclContext>(D)->isDependentContext()) 538 return; 539 540 if (Diags.hasErrorOccurred() || Diags.hasFatalErrorOccurred()) { 541 // Flush out any possibly unreachable diagnostics. 542 flushDiagnostics(S, fscope); 543 return; 544 } 545 546 const Stmt *Body = D->getBody(); 547 assert(Body); 548 549 // Don't generate EH edges for CallExprs as we'd like to avoid the n^2 550 // explosion for destrutors that can result and the compile time hit. 551 AnalysisContext AC(D, 0, /*useUnoptimizedCFG=*/false, /*addehedges=*/false, 552 /*addImplicitDtors=*/true, /*addInitializers=*/true); 553 554 // Emit delayed diagnostics. 555 if (!fscope->PossiblyUnreachableDiags.empty()) { 556 bool analyzed = false; 557 558 // Register the expressions with the CFGBuilder. 559 for (llvm::SmallVectorImpl<sema::PossiblyUnreachableDiag>::iterator 560 i = fscope->PossiblyUnreachableDiags.begin(), 561 e = fscope->PossiblyUnreachableDiags.end(); 562 i != e; ++i) { 563 if (const Stmt *stmt = i->stmt) 564 AC.registerForcedBlockExpression(stmt); 565 } 566 567 if (AC.getCFG()) { 568 analyzed = true; 569 for (llvm::SmallVectorImpl<sema::PossiblyUnreachableDiag>::iterator 570 i = fscope->PossiblyUnreachableDiags.begin(), 571 e = fscope->PossiblyUnreachableDiags.end(); 572 i != e; ++i) 573 { 574 const sema::PossiblyUnreachableDiag &D = *i; 575 bool processed = false; 576 if (const Stmt *stmt = i->stmt) { 577 const CFGBlock *block = AC.getBlockForRegisteredExpression(stmt); 578 assert(block); 579 if (CFGReverseBlockReachabilityAnalysis *cra = AC.getCFGReachablityAnalysis()) { 580 // Can this block be reached from the entrance? 581 if (cra->isReachable(&AC.getCFG()->getEntry(), block)) 582 S.Diag(D.Loc, D.PD); 583 processed = true; 584 } 585 } 586 if (!processed) { 587 // Emit the warning anyway if we cannot map to a basic block. 588 S.Diag(D.Loc, D.PD); 589 } 590 } 591 } 592 593 if (!analyzed) 594 flushDiagnostics(S, fscope); 595 } 596 597 598 // Warning: check missing 'return' 599 if (P.enableCheckFallThrough) { 600 const CheckFallThroughDiagnostics &CD = 601 (isa<BlockDecl>(D) ? CheckFallThroughDiagnostics::MakeForBlock() 602 : CheckFallThroughDiagnostics::MakeForFunction(D)); 603 CheckFallThroughForBody(S, D, Body, blkExpr, CD, AC); 604 } 605 606 // Warning: check for unreachable code 607 if (P.enableCheckUnreachable) 608 CheckUnreachable(S, AC); 609 610 if (Diags.getDiagnosticLevel(diag::warn_uninit_var, D->getLocStart()) 611 != Diagnostic::Ignored || 612 Diags.getDiagnosticLevel(diag::warn_maybe_uninit_var, D->getLocStart()) 613 != Diagnostic::Ignored) { 614 if (CFG *cfg = AC.getCFG()) { 615 UninitValsDiagReporter reporter(S); 616 runUninitializedVariablesAnalysis(*cast<DeclContext>(D), *cfg, AC, 617 reporter); 618 } 619 } 620} 621