Index: llvm/docs/CommandGuide/FileCheck.rst =================================================================== --- llvm/docs/CommandGuide/FileCheck.rst +++ llvm/docs/CommandGuide/FileCheck.rst @@ -80,9 +80,14 @@ -verify``. With this option FileCheck will verify that input does not contain warnings not covered by any ``CHECK:`` patterns. +.. option:: --dump-input + + Dump annotated original input either 'always', on 'fail', or 'never'. + .. option:: --dump-input-on-failure - When the check fails, dump all of the original input. + When the check fails, dump all of the original input. This option is + deprecated in favor of `--dump-input`. .. option:: --enable-var-scope Index: llvm/include/llvm/Support/FileCheck.h =================================================================== --- llvm/include/llvm/Support/FileCheck.h +++ llvm/include/llvm/Support/FileCheck.h @@ -18,6 +18,7 @@ #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Regex.h" #include "llvm/Support/SourceMgr.h" +#include #include #include @@ -62,6 +63,8 @@ }; } +struct FileCheckDiag; + class FileCheckPattern { SMLoc PatternLoc; @@ -123,6 +126,31 @@ size_t FindRegexVarEnd(StringRef Str, SourceMgr &SM); }; +//===----------------------------------------------------------------------===// +/// Summary of a FileCheck diagnostic. +//===----------------------------------------------------------------------===// + +struct FileCheckDiag { + /// What is the FileCheck directive for this diagnostic? + Check::FileCheckType CheckTy; + /// Where is the FileCheck directive for this diagnostic? + unsigned CheckLine, CheckCol; + /// What kind of match result does this diagnostic describe? + /// + /// We iterate these types, so they must have contiguous values in + /// [0, MatchTypeCount). + enum MatchType { + // TODO: More members will appear with later patches in this series. + MatchNoneButExpected, //< no match for an expected pattern + MatchTypeFirst = MatchNoneButExpected, + MatchTypeCount, + } MatchTy; + /// The search range. + unsigned InputStartLine, InputStartCol, InputEndLine, InputEndCol; + FileCheckDiag(const SourceMgr &SM, const Check::FileCheckType &CheckTy, + SMLoc CheckLoc, MatchType MatchTy, SMRange InputRange); +}; + //===----------------------------------------------------------------------===// // Check Strings. //===----------------------------------------------------------------------===// @@ -147,7 +175,7 @@ size_t Check(const SourceMgr &SM, StringRef Buffer, bool IsLabelScanMode, size_t &MatchLen, StringMap &VariableTable, - FileCheckRequest &Req) const; + FileCheckRequest &Req, std::list *Diags) const; bool CheckNext(const SourceMgr &SM, StringRef Buffer) const; bool CheckSame(const SourceMgr &SM, StringRef Buffer) const; @@ -158,7 +186,8 @@ size_t CheckDag(const SourceMgr &SM, StringRef Buffer, std::vector &NotStrings, StringMap &VariableTable, - const FileCheckRequest &Req) const; + const FileCheckRequest &Req, + std::list *Diags) const; }; /// FileCheck class takes the request and exposes various methods that @@ -195,7 +224,8 @@ /// /// Returns false if the input fails to satisfy the checks. bool CheckInput(SourceMgr &SM, StringRef Buffer, - ArrayRef CheckStrings); + ArrayRef CheckStrings, + std::list *Diags = nullptr); }; } // namespace llvm #endif Index: llvm/lib/Support/FileCheck.cpp =================================================================== --- llvm/lib/Support/FileCheck.cpp +++ llvm/lib/Support/FileCheck.cpp @@ -408,6 +408,21 @@ } } +static SMRange ProcessMatchResult(FileCheckDiag::MatchType MatchTy, + const SourceMgr &SM, SMLoc Loc, + Check::FileCheckType CheckTy, + StringRef Buffer, size_t Pos, size_t Len, + std::list *Diags) { + SMLoc Start = SMLoc::getFromPointer(Buffer.data() + Pos); + SMLoc End = SMLoc::getFromPointer(Buffer.data() + Pos + Len); + SMRange Range(Start, End); + // TODO: The second condition will disappear when we extend this to handle + // more match types. + if (Diags && MatchTy != FileCheckDiag::MatchTypeCount) + Diags->emplace_back(SM, CheckTy, Loc, MatchTy, Range); + return Range; +} + void FileCheckPattern::PrintFuzzyMatch( const SourceMgr &SM, StringRef Buffer, const StringMap &VariableTable) const { @@ -527,6 +542,22 @@ return StringRef(OutputBuffer.data(), OutputBuffer.size() - 1); } +FileCheckDiag::FileCheckDiag(const SourceMgr &SM, + const Check::FileCheckType &CheckTy, + SMLoc CheckLoc, MatchType MatchTy, + SMRange InputRange) + : CheckTy(CheckTy), MatchTy(MatchTy) { + auto Start = SM.getLineAndColumn(InputRange.Start); + auto End = SM.getLineAndColumn(InputRange.End); + InputStartLine = Start.first; + InputStartCol = Start.second; + InputEndLine = End.first; + InputEndCol = End.second; + Start = SM.getLineAndColumn(CheckLoc); + CheckLine = Start.first; + CheckCol = Start.second; +} + static bool IsPartOfWord(char c) { return (isalnum(c) || c == '-' || c == '_'); } @@ -875,10 +906,11 @@ } static void PrintNoMatch(bool ExpectedMatch, const SourceMgr &SM, - StringRef Prefix, SMLoc Loc, const FileCheckPattern &Pat, - StringRef Buffer, + StringRef Prefix, SMLoc Loc, + const FileCheckPattern &Pat, StringRef Buffer, StringMap &VariableTable, - bool VerboseVerbose) { + bool VerboseVerbose, + std::list *Diags) { if (!ExpectedMatch && !VerboseVerbose) return; @@ -892,9 +924,11 @@ // Print the "scanning from here" line. If the current position is at the // end of a line, advance to the start of the next line. Buffer = Buffer.substr(Buffer.find_first_not_of(" \t\n\r")); - - SM.PrintMessage(SMLoc::getFromPointer(Buffer.data()), SourceMgr::DK_Note, - "scanning from here"); + SMRange SearchRange = ProcessMatchResult( + ExpectedMatch ? FileCheckDiag::MatchNoneButExpected + : FileCheckDiag::MatchTypeCount, + SM, Loc, Pat.getCheckTy(), Buffer, 0, Buffer.size(), Diags); + SM.PrintMessage(SearchRange.Start, SourceMgr::DK_Note, "scanning from here"); // Allow the pattern to print additional information if desired. Pat.PrintVariableUses(SM, Buffer, VariableTable); @@ -905,9 +939,10 @@ static void PrintNoMatch(bool ExpectedMatch, const SourceMgr &SM, const FileCheckString &CheckStr, StringRef Buffer, StringMap &VariableTable, - bool VerboseVerbose) { + bool VerboseVerbose, + std::list *Diags) { PrintNoMatch(ExpectedMatch, SM, CheckStr.Prefix, CheckStr.Loc, CheckStr.Pat, - Buffer, VariableTable, VerboseVerbose); + Buffer, VariableTable, VerboseVerbose, Diags); } /// Count the number of newlines in the specified range. @@ -935,9 +970,10 @@ /// Match check string and its "not strings" and/or "dag strings". size_t FileCheckString::Check(const SourceMgr &SM, StringRef Buffer, - bool IsLabelScanMode, size_t &MatchLen, - StringMap &VariableTable, - FileCheckRequest &Req) const { + bool IsLabelScanMode, size_t &MatchLen, + StringMap &VariableTable, + FileCheckRequest &Req, + std::list *Diags) const { size_t LastPos = 0; std::vector NotStrings; @@ -947,7 +983,7 @@ // over the block again (including the last CHECK-LABEL) in normal mode. if (!IsLabelScanMode) { // Match "dag strings" (with mixed "not strings" if any). - LastPos = CheckDag(SM, Buffer, NotStrings, VariableTable, Req); + LastPos = CheckDag(SM, Buffer, NotStrings, VariableTable, Req, Diags); if (LastPos == StringRef::npos) return StringRef::npos; } @@ -956,7 +992,8 @@ StringRef MatchBuffer = Buffer.substr(LastPos); size_t MatchPos = Pat.Match(MatchBuffer, MatchLen, VariableTable); if (MatchPos == StringRef::npos) { - PrintNoMatch(true, SM, *this, MatchBuffer, VariableTable, Req.VerboseVerbose); + PrintNoMatch(true, SM, *this, MatchBuffer, VariableTable, + Req.VerboseVerbose, Diags); return StringRef::npos; } PrintMatch(true, SM, *this, MatchBuffer, VariableTable, MatchPos, MatchLen, Req); @@ -1073,7 +1110,7 @@ if (Pos == StringRef::npos) { PrintNoMatch(false, SM, Prefix, Pat->getLoc(), *Pat, Buffer, - VariableTable, Req.VerboseVerbose); + VariableTable, Req.VerboseVerbose, nullptr); continue; } @@ -1087,10 +1124,12 @@ } /// Match "dag strings" and their mixed "not strings". -size_t FileCheckString::CheckDag(const SourceMgr &SM, StringRef Buffer, - std::vector &NotStrings, - StringMap &VariableTable, - const FileCheckRequest &Req) const { +size_t +FileCheckString::CheckDag(const SourceMgr &SM, StringRef Buffer, + std::vector &NotStrings, + StringMap &VariableTable, + const FileCheckRequest &Req, + std::list *Diags) const { if (DagNotStrings.empty()) return 0; @@ -1134,7 +1173,7 @@ // that group of CHECK-DAGs fails immediately. if (MatchPosBuf == StringRef::npos) { PrintNoMatch(true, SM, Prefix, Pat.getLoc(), Pat, MatchBuffer, - VariableTable, Req.VerboseVerbose); + VariableTable, Req.VerboseVerbose, Diags); return StringRef::npos; } // Re-calc it as the offset relative to the start of the original string. @@ -1275,7 +1314,8 @@ /// /// Returns false if the input fails to satisfy the checks. bool llvm::FileCheck::CheckInput(SourceMgr &SM, StringRef Buffer, - ArrayRef CheckStrings) { + ArrayRef CheckStrings, + std::list *Diags) { bool ChecksFailed = false; /// VariableTable - This holds all the current filecheck variables. @@ -1298,9 +1338,8 @@ // Scan to next CHECK-LABEL match, ignoring CHECK-NOT and CHECK-DAG size_t MatchLabelLen = 0; - size_t MatchLabelPos = - CheckLabelStr.Check(SM, Buffer, true, MatchLabelLen, VariableTable, - Req); + size_t MatchLabelPos = CheckLabelStr.Check( + SM, Buffer, true, MatchLabelLen, VariableTable, Req, Diags); if (MatchLabelPos == StringRef::npos) // Immediately bail of CHECK-LABEL fails, nothing else we can do. return false; @@ -1319,8 +1358,8 @@ // Check each string within the scanned region, including a second check // of any final CHECK-LABEL (to verify CHECK-NOT and CHECK-DAG) size_t MatchLen = 0; - size_t MatchPos = - CheckStr.Check(SM, CheckRegion, false, MatchLen, VariableTable, Req); + size_t MatchPos = CheckStr.Check(SM, CheckRegion, false, MatchLen, + VariableTable, Req, Diags); if (MatchPos == StringRef::npos) { ChecksFailed = true; Index: llvm/test/FileCheck/dump-input-annotations.txt =================================================================== --- /dev/null +++ llvm/test/FileCheck/dump-input-annotations.txt @@ -0,0 +1,214 @@ +;-------------------------------------------------- +; Use -strict-whitespace to check marker alignment here. +; (Also check multiline marker where start/end columns vary across lines.) +; +; In the remaining checks, don't use -strict-whitespace and thus check just the +; presence, order, and lengths of markers. That way, if we ever change padding +; within line labels, we don't have to adjust so many tests. +;-------------------------------------------------- + +; RUN: echo 'hello world' > %t.in +; RUN: echo 'goodbye' >> %t.in +; RUN: echo 'world' >> %t.in + +; RUN: echo 'CHECK: hello' > %t.chk +; RUN: echo 'CHECK: universe' >> %t.chk + +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -v 2>&1 \ +; RUN: | FileCheck -strict-whitespace -match-full-lines -check-prefix=ALIGN %s + +; ALIGN:Full input was: +; ALIGN-NEXT:<<<<<< +; ALIGN-NEXT: 1: hello world +; ALIGN-NEXT:check:2 X~~~~ +; ALIGN-NEXT: 2: goodbye +; ALIGN-NEXT:check:2 ~~~~~~~ +; ALIGN-NEXT: 3: world +; ALIGN-NEXT:check:2 ~~~~~ +; ALIGN-NEXT:>>>>>> +; ALIGN-NOT:{{.}} + +;-------------------------------------------------- +; CHECK (also: multi-line search range) +;-------------------------------------------------- + +; Good match and no match. + +; RUN: echo 'hello' > %t.in +; RUN: echo 'again' >> %t.in +; RUN: echo 'whirled' >> %t.in + +; RUN: echo 'CHECK: hello' > %t.chk +; RUN: echo 'CHECK: world' >> %t.chk + +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefix=CHK +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -v 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=CHK,CHK-V +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -vv 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=CHK,CHK-V + +; CHK: <<<<<< +; CHK-NEXT: 1: hello +; CHK-NEXT: 2: again +; CHK-NEXT: check:2 X~~~~ +; CHK-NEXT: 3: whirled +; CHK-NEXT: check:2 ~~~~~~~ +; CHK-NEXT: >>>>>> +; CHK-NOT: {{.}} + +;-------------------------------------------------- +; CHECK-NEXT (also: EOF search-range) +;-------------------------------------------------- + +; Good match and no match. + +; RUN: echo 'hello' > %t.in +; RUN: echo 'again' >> %t.in + +; RUN: echo 'CHECK: hello' > %t.chk +; RUN: echo 'CHECK-NEXT: again' >> %t.chk +; RUN: echo 'CHECK-NEXT: world' >> %t.chk + +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefix=NXT +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -v 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=NXT,NXT-V +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -vv 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=NXT,NXT-V,NXT-VV + +; NXT: <<<<<< +; NXT-NEXT: 1: hello +; NXT-NEXT: 2: again +; NXT-NEXT: 3: +; NXT-NEXT: next:3 X +; NXT-NEXT: >>>>>> +; NXT-NOT: {{.}} + +;-------------------------------------------------- +; CHECK-SAME (also: single-char search range) +;-------------------------------------------------- + +; Good match and no match. + +; RUN: echo 'hello world!' > %t.in + +; RUN: echo 'CHECK: hello' > %t.chk +; RUN: echo 'CHECK-SAME: world' >> %t.chk +; RUN: echo 'CHECK-SAME: again' >> %t.chk + +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefix=SAM +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -v 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=SAM,SAM-V +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -vv 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=SAM,SAM-V,SAM-VV + +; SAM: <<<<<< +; SAM-NEXT: 1: hello world! +; SAM-NEXT: same:3 X +; SAM-NEXT: >>>>>> +; SAM-NOT: {{.}} + +;-------------------------------------------------- +; CHECK-EMPTY (also: search range ends at label) +;-------------------------------------------------- + +; Good match and no match. +; +; CHECK-EMPTY always seems to match an empty line at EOF (illegally when it's +; not the next line) unless either (1) the last line is non-empty and has no +; newline or (2) there's a CHECK-LABEL to end the search range before EOF. We +; choose scenario 2 to check the case of no match. + +; RUN: echo 'hello' > %t.in +; RUN: echo '' >> %t.in +; RUN: echo 'world' >> %t.in +; RUN: echo 'label' >> %t.in + +; RUN: echo 'CHECK: hello' > %t.chk +; RUN: echo 'CHECK-EMPTY:' >> %t.chk +; RUN: echo 'CHECK-EMPTY:' >> %t.chk +; RUN: echo 'CHECK-LABEL: label' >> %t.chk + +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefix=EMP +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -v 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=EMP,EMP-V +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -vv 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=EMP,EMP-V,EMP-VV + +; EMP: <<<<<< +; EMP-NEXT: 1: hello +; EMP-NEXT: 2: +; EMP-NEXT: 3: world +; EMP-NEXT: empty:3 X~~~~ +; EMP-NEXT: 4: label +; EMP-NEXT: empty:3 ~~~~~ +; EMP-NEXT: >>>>>> +; EMP-NOT: {{.}} + +;-------------------------------------------------- +; CHECK-DAG +;-------------------------------------------------- + +; Good match, discarded match plus good match, and no match. + +; RUN: echo 'abc' > %t.in +; RUN: echo 'def' >> %t.in +; RUN: echo 'abc' >> %t.in + +; RUN: echo 'CHECK-DAG: def' > %t.chk +; RUN: echo 'CHECK-DAG: abc' >> %t.chk +; RUN: echo 'CHECK-DAG: abc' >> %t.chk +; RUN: echo 'CHECK-DAG: def' >> %t.chk + +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=DAG +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -v 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=DAG +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -vv 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=DAG + +; DAG: <<<<<< +; DAG-NEXT: 1: abc +; DAG-NEXT: 2: def +; DAG-NEXT: 3: abc +; DAG-NEXT: dag:4 X~~ +; DAG-NEXT: >>>>>> +; DAG-NOT: {{.}} + +;-------------------------------------------------- +; CHECK-LABEL +;-------------------------------------------------- + +; Good match and no match. + +; RUN: echo 'lab0' > %t.in +; RUN: echo 'foo' >> %t.in +; RUN: echo 'lab1' >> %t.in +; RUN: echo 'bar' >> %t.in + +; RUN: echo 'CHECK-LABEL: lab0' > %t.chk +; RUN: echo 'CHECK: foo' >> %t.chk +; RUN: echo 'CHECK-LABEL: lab2' >> %t.chk + +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=LAB +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -v 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=LAB,LAB-V +; RUN: not FileCheck -dump-input=always -input-file %t.in %t.chk -vv 2>&1 \ +; RUN: | FileCheck -match-full-lines %s -check-prefixes=LAB,LAB-V,LAB-VV + +; LAB: <<<<<< +; LAB-NEXT: 1: lab0 +; LAB-NEXT: 2: foo +; LAB-NEXT: label:3 X~~ +; LAB-NEXT: 3: lab1 +; LAB-NEXT: label:3 ~~~~ +; LAB-NEXT: 4: bar +; LAB-NEXT: label:3 ~~~ +; LAB-NEXT: >>>>>> +; LAB-NOT: {{.}} + + Index: llvm/test/FileCheck/dump-input-enable.txt =================================================================== --- /dev/null +++ llvm/test/FileCheck/dump-input-enable.txt @@ -0,0 +1,72 @@ +; RUN: echo ciao > %t.good +; RUN: echo world >> %t.good + +; RUN: echo hello > %t.err +; RUN: echo world >> %t.err + +; RUN: echo 'CHECK: ciao' > %t.check +; RUN: echo 'CHECK-NEXT: world' >> %t.check + +;-------------------------------------------------- +; unknown value +;-------------------------------------------------- + +; RUN: not FileCheck -input-file %t.good %t.check -check-prefix=CHECK \ +; RUN: -match-full-lines -dump-input=foobar 2>&1 \ +; RUN: | FileCheck %s -match-full-lines -check-prefix=BADVAL + +BADVAL: Unrecognized value for dump-input option: foobar + +;-------------------------------------------------- +; never +;-------------------------------------------------- + +; RUN: FileCheck -input-file %t.good %t.check -check-prefix=CHECK \ +; RUN: -match-full-lines -dump-input=never 2>&1 \ +; RUN: | FileCheck %s -match-full-lines -check-prefix=CHECK-NODUMP -allow-empty + +; RUN: not FileCheck -input-file %t.err %t.check -check-prefix=CHECK \ +; RUN: -match-full-lines -dump-input=never 2>&1 \ +; RUN: | FileCheck %s -match-full-lines -check-prefix=CHECK-NODUMP + +;-------------------------------------------------- +; fail +;-------------------------------------------------- + +; RUN: FileCheck -input-file %t.good %t.check -check-prefix=CHECK \ +; RUN: -match-full-lines -dump-input=fail 2>&1 \ +; RUN: | FileCheck %s -match-full-lines -check-prefix=CHECK-NODUMP -allow-empty + +; RUN: not FileCheck -input-file %t.err %t.check -check-prefix=CHECK \ +; RUN: -match-full-lines -dump-input=fail 2>&1 \ +; RUN: | FileCheck %s -match-full-lines -check-prefix=CHECK-ERR + +;-------------------------------------------------- +; always +;-------------------------------------------------- + +; RUN: FileCheck -input-file %t.good %t.check -check-prefix=CHECK \ +; RUN: -match-full-lines -dump-input=always -v 2>&1 \ +; RUN: | FileCheck %s -match-full-lines -check-prefix=CHECK-GOOD + +; RUN: not FileCheck -input-file %t.err %t.check -check-prefix=CHECK \ +; RUN: -match-full-lines -dump-input=always 2>&1 \ +; RUN: | FileCheck %s -match-full-lines -check-prefix=CHECK-ERR + +; END. + +; CHECK-GOOD: Full input was: +; CHECK-GOOD-NEXT: <<<<<< +; CHECK-GOOD-NEXT: 1: ciao +; CHECK-GOOD-NEXT: 2: world +; CHECK-GOOD-NEXT: >>>>>> + +; CHECK-ERR: Full input was: +; CHECK-ERR-NEXT: <<<<<< +; CHECK-ERR-NEXT: 1: hello +; CHECK-ERR-NEXT: check:1 X~~~~ +; CHECK-ERR-NEXT: 2: world +; CHECK-ERR-NEXT: check:1 ~~~~~ +; CHECK-ERR-NEXT: >>>>>> + +; CHECK-NODUMP-NOT: <<<<<< Index: llvm/utils/FileCheck/FileCheck.cpp =================================================================== --- llvm/utils/FileCheck/FileCheck.cpp +++ llvm/utils/FileCheck/FileCheck.cpp @@ -19,6 +19,7 @@ #include "llvm/Support/CommandLine.h" #include "llvm/Support/InitLLVM.h" #include "llvm/Support/Process.h" +#include "llvm/Support/WithColor.h" #include "llvm/Support/raw_ostream.h" #include "llvm/Support/FileCheck.h" using namespace llvm; @@ -91,7 +92,14 @@ "dump-input-on-failure", cl::init(std::getenv(DumpInputEnv)), cl::desc("Dump original input to stderr before failing.\n" "The value can be also controlled using\n" - "FILECHECK_DUMP_INPUT_ON_FAILURE environment variable.\n")); + "FILECHECK_DUMP_INPUT_ON_FAILURE environment variable.\n" + "This option is deprecated in favor of -dump-input.\n")); + +static cl::opt DumpInput( + "dump-input", cl::init("never"), + cl::desc("Dump annotated input to stderr either 'always', on 'fail',\n" + "or 'never'.\n"), + cl::value_desc("mode")); typedef cl::list::const_iterator prefix_iterator; @@ -108,6 +116,262 @@ errs() << "\n"; } +struct MatchTypeStyle { + char Mark; + raw_ostream::Colors Color; + const char *What; + MatchTypeStyle(char Mark, raw_ostream::Colors Color, const char *What) + : Mark(Mark), Color(Color), What(What) {} +}; + +static MatchTypeStyle GetMatchTypeStyle(unsigned MatchTy) { + switch (MatchTy) { + case FileCheckDiag::MatchNoneButExpected: + return MatchTypeStyle('X', raw_ostream::RED, + "the search range for an unmatched expected " + "pattern (e.g., CHECK)"); + case FileCheckDiag::MatchTypeCount: + llvm_unreachable_internal("unexpected match type"); + } + llvm_unreachable_internal("unexpected match type"); +} + +static void DumpInputAnnotationExplanation(raw_ostream &OS, + const FileCheckRequest &Req) { + OS << "\nFormat for input dump annotations:\n\n"; + + // Labels for input lines. + OS << " - "; + WithColor(OS, raw_ostream::SAVEDCOLOR, true) << "L:"; + OS << "S labels line number L of the input file, where S is a " + << "single space\n"; + + // Labels for annotation lines. + OS << " - "; + WithColor(OS, raw_ostream::SAVEDCOLOR, true) << "T:L"; + OS << " labels the match result for a pattern of type T from " + << "line L of\n" + << " the check file\n"; + + // Markers on annotation lines. + OS << " - "; + WithColor(OS, raw_ostream::SAVEDCOLOR, true) << "X~~"; + OS << " marks search range when no match is found\n"; + + // Colors. + if (WithColor(OS).colorsEnabled()) { + OS << " - color "; + WithColor(OS, raw_ostream::RED, true) << "error"; + OS << '\n'; + } + + // Files. + OS << "\nInput file: "; + if (InputFilename == "-") + OS << ""; + else + OS << InputFilename; + OS << "\nCheck file: " << CheckFilename << "\n"; +} + +/// An annotation for a single input line. +struct InputAnnotation { + /// The check file line (one-origin indexing) where the directive that + /// produced this annotation is located. + unsigned CheckLine; + /// The label for this annotation. + std::string Label; + /// What input line (one-origin indexing) this annotation marks. This might + /// be different from the starting line of the original diagnostic if this is + /// a non-initial fragment of a diagnostic that has been broken across + /// multiple lines. + unsigned InputLine; + /// The column range (one-origin indexing, open end) in which to to mark the + /// input line. If InputEndCol is UINT_MAX, treat it as the last column + /// before the newline. + unsigned InputStartCol, InputEndCol; + /// The starting char (before tildes) for marking the line. + char Mark; + /// What color to use for this annotation. + raw_ostream::Colors Color; +}; + +/// Get an abbreviation for the check type. +std::string GetCheckTypeAbbreviation(Check::FileCheckType Ty) { + switch (Ty) { + case Check::CheckPlain: + return "check"; + case Check::CheckNext: + return "next"; + case Check::CheckSame: + return "same"; + case Check::CheckNot: + return "not"; + case Check::CheckDAG: + return "dag"; + case Check::CheckLabel: + return "label"; + case Check::CheckEmpty: + return "empty"; + case Check::CheckEOF: + return "eof"; + case Check::CheckBadNot: + return "bad-not"; + case Check::CheckNone: + llvm_unreachable("invalid FileCheckType"); + } + llvm_unreachable("unknown FileCheckType"); +} + +static void BuildInputAnnotations(const std::list &DiagList, + std::list &AnnotationList, + unsigned &LabelWidth) { + // What's the widest label? + LabelWidth = 0; + for (auto DiagItr = DiagList.begin(), DiagEnd = DiagList.end(); + DiagItr != DiagEnd; ++DiagItr) { + AnnotationList.emplace_back(); + InputAnnotation &A = AnnotationList.back(); + + // Build label, which uniquely identifies this check result. + A.CheckLine = DiagItr->CheckLine; + llvm::raw_string_ostream Label(A.Label); + Label << GetCheckTypeAbbreviation(DiagItr->CheckTy) << ":" + << DiagItr->CheckLine; + Label.flush(); + LabelWidth = std::max((std::string::size_type)LabelWidth, A.Label.size()); + + MatchTypeStyle MatchTyStyle = GetMatchTypeStyle(DiagItr->MatchTy); + A.Mark = MatchTyStyle.Mark; + A.Color = MatchTyStyle.Color; + + // Compute the mark location, and break annotation into multiple + // annotations if it spans multiple lines. + A.InputLine = DiagItr->InputStartLine; + A.InputStartCol = DiagItr->InputStartCol; + if (DiagItr->InputStartLine == DiagItr->InputEndLine) { + // Sometimes ranges are empty in order to indicate a specific point, but + // that would mean nothing would be marked, so adjust the range to + // include the following character. + A.InputEndCol = + std::max(DiagItr->InputStartCol + 1, DiagItr->InputEndCol); + } else { + assert(DiagItr->InputStartLine < DiagItr->InputEndLine && + "expected input range not to be inverted"); + A.InputEndCol = UINT_MAX; + for (unsigned L = DiagItr->InputStartLine + 1, E = DiagItr->InputEndLine; + L <= E; ++L) { + // If a range ends before the first column on a line, then it has no + // characters on that line, so there's nothing to render. + if (DiagItr->InputEndCol == 1 && L == E) + break; + AnnotationList.emplace_back(); + InputAnnotation &B = AnnotationList.back(); + B.CheckLine = A.CheckLine; + B.Label = A.Label; + B.InputLine = L; + B.Mark = '~'; + B.InputStartCol = 1; + if (L != E) + B.InputEndCol = UINT_MAX; + else + B.InputEndCol = DiagItr->InputEndCol; + B.Color = A.Color; + } + } + } +} + +static void DumpAnnotatedInput( + raw_ostream &OS, StringRef InputFileText, + const std::list &AnnotationList, unsigned LabelWidth) { + OS << "Full input was:\n<<<<<<\n"; + + // Sort annotations. + // + // First, sort in the order of input lines to make it easier to find relevant + // annotations while iterating input lines in the implementation below. + // FileCheck diagnostics are not always reported and recorded in the order of + // input lines due to, for example, CHECK-DAG and CHECK-NOT. + // + // Second, for annotations for the same input line, sort in the order of the + // FileCheck directive's line in the check file (where there's at most one + // directive per line). The rationale of this choice is that, for any input + // line, this sort establishes a total order of annotations that, with + // respect to match results, is consistent across multiple lines, thus + // making match results easier to track from one line to the next when they + // span multiple lines. + std::vector Annotations(AnnotationList.begin(), + AnnotationList.end()); + std::sort(Annotations.begin(), Annotations.end(), + [](const InputAnnotation &A, const InputAnnotation &B) { + if (A.InputLine != B.InputLine) + return A.InputLine < B.InputLine; + return A.CheckLine < B.CheckLine; + }); + + // Compute the width of the label column. + const unsigned char *InputFilePtr = InputFileText.bytes_begin(), + *InputFileEnd = InputFileText.bytes_end(); + unsigned LineCount = InputFileText.count('\n'); + if (InputFileEnd[-1] != '\n') + ++LineCount; + unsigned LineNoWidth = log10(LineCount) + 1; + // +3 below adds spaces (1) to the left of the (right-aligned) line numbers + // on input lines and (2) to the right of the (left-aligned) labels on + // annotation lines so that input lines and annotation lines are more + // visually distinct. For example, the spaces on the annotation lines ensure + // that input line numbers and check directive line numbers never align + // horizontally. Those line numbers might not even be for the same file. + // One space would be enough to achieve that, but more makes it even easier + // to see. + LabelWidth = std::max(LabelWidth, LineNoWidth) + 3; + + // Print annotated input lines. + auto AnnotationItr = Annotations.begin(), AnnotationEnd = Annotations.end(); + for (unsigned Line = 1; + InputFilePtr != InputFileEnd || AnnotationItr != AnnotationEnd; + ++Line) { + const unsigned char *InputFileLine = InputFilePtr; + + // Print right-aligned line number. + WithColor(OS, raw_ostream::BLACK, true) + << format_decimal(Line, LabelWidth) << ": "; + + // Print numbered line. + bool Newline = false; + while (InputFilePtr != InputFileEnd && !Newline) { + if (*InputFilePtr == '\n') + Newline = true; + else + OS << *InputFilePtr; + ++InputFilePtr; + } + OS << '\n'; + unsigned InputLineWidth = InputFilePtr - InputFileLine - Newline; + + // Print any annotations. + while (AnnotationItr != AnnotationEnd && + AnnotationItr->InputLine == Line) { + WithColor COS(OS, AnnotationItr->Color, true); + // The two spaces below are where the ": " appears on input lines. + COS << left_justify(AnnotationItr->Label, LabelWidth) << " "; + unsigned Col; + for (Col = 1; Col < AnnotationItr->InputStartCol; ++Col) + COS << ' '; + COS << AnnotationItr->Mark; + // If InputEndCol=UINT_MAX, stop at InputLineWidth. + for (++Col; Col < AnnotationItr->InputEndCol && Col <= InputLineWidth; + ++Col) + COS << '~'; + COS << '\n'; + ++AnnotationItr; + } + } + + OS << ">>>>>>\n"; +} + int main(int argc, char **argv) { // Enable use of ANSI color codes because FileCheck is using them to // highlight text. @@ -157,6 +421,10 @@ return 2; } + if (DumpInput != "never" && DumpInput != "fail" && DumpInput != "always") { + errs() << "Unrecognized value for dump-input option: " << DumpInput << '\n'; + return 2; + } SourceMgr SM; @@ -204,10 +472,21 @@ InputFileText, InputFile.getBufferIdentifier()), SMLoc()); - int ExitCode = - FC.CheckInput(SM, InputFileText, CheckStrings) ? EXIT_SUCCESS : 1; + std::list DiagList; + int ExitCode = FC.CheckInput(SM, InputFileText, CheckStrings, &DiagList) + ? EXIT_SUCCESS + : 1; if (ExitCode == 1 && DumpInputOnFailure) errs() << "Full input was:\n<<<<<<\n" << InputFileText << "\n>>>>>>\n"; + if (DumpInput == "always" || (ExitCode == 1 && DumpInput == "fail")) { + errs() << '\n'; + DumpInputAnnotationExplanation(errs(), Req); + std::list AnnotationList; + unsigned LabelWidth; + BuildInputAnnotations(DiagList, AnnotationList, LabelWidth); + errs() << '\n'; + DumpAnnotatedInput(errs(), InputFileText, AnnotationList, LabelWidth); + } return ExitCode; }