text: //===--- TestSupport.cpp - Clang-based refactoring tool -------------------===//
  //
  //                     The LLVM Compiler Infrastructure
  //
  // This file is distributed under the University of Illinois Open Source
  // License. See LICENSE.TXT for details.
  //
  //===----------------------------------------------------------------------===//
  ///
  /// \file
  /// \brief This file implements routines that provide refactoring testing
  /// utilities.
  ///
  //===----------------------------------------------------------------------===//
  
  #include "TestSupport.h"
  #include "clang/Basic/DiagnosticError.h"
  #include "clang/Basic/SourceManager.h"
  #include "clang/Lex/Lexer.h"
  #include "llvm/ADT/STLExtras.h"
  #include "llvm/Support/Error.h"
  #include "llvm/Support/ErrorOr.h"
  #include "llvm/Support/LineIterator.h"
  #include "llvm/Support/MemoryBuffer.h"
  #include "llvm/Support/Regex.h"
  #include "llvm/Support/raw_ostream.h"
  
  using namespace llvm;
  
  namespace clang {
  namespace refactor {
  
  void TestSelectionRangesInFile::dump(raw_ostream &OS) const {
    for (const auto &Group : GroupedRanges) {
      OS << "Test selection group '" << Group.Name << "':\n";
      for (const auto &Range : Group.Ranges) {
        OS << "  " << Range.Begin << "-" << Range.End << "\n";
      }
    }
  }
  
  bool TestSelectionRangesInFile::foreachRange(
      const SourceManager &SM,
      llvm::function_ref<void(SourceRange)> Callback) const {
    const FileEntry *FE = SM.getFileManager().getFile(Filename);
    FileID FID = FE ? SM.translateFile(FE) : FileID();
    if (!FE || FID.isInvalid()) {
      llvm::errs() << "error: -selection=test:" << Filename
                   << " : given file is not in the target TU";
      return true;
    }
    SourceLocation FileLoc = SM.getLocForStartOfFile(FID);
    for (const auto &Group : GroupedRanges) {
      for (const TestSelectionRange &Range : Group.Ranges) {
        // Translate the offset pair to a true source range.
        SourceLocation Start =
            SM.getMacroArgExpandedLocation(FileLoc.getLocWithOffset(Range.Begin));
        SourceLocation End =
            SM.getMacroArgExpandedLocation(FileLoc.getLocWithOffset(Range.End));
        assert(Start.isValid() && End.isValid() && "unexpected invalid range");
        Callback(SourceRange(Start, End));
      }
    }
    return false;
  }
  
  namespace {
  
  void dumpChanges(const tooling::AtomicChanges &Changes, raw_ostream &OS) {
    for (const auto &Change : Changes)
      OS << const_cast<tooling::AtomicChange &>(Change).toYAMLString() << "\n";
  }
  
  bool areChangesSame(const tooling::AtomicChanges &LHS,
                      const tooling::AtomicChanges &RHS) {
    if (LHS.size() != RHS.size())
      return false;
    for (const auto &I : llvm::zip(LHS, RHS)) {
      if (!(std::get<0>(I) == std::get<1>(I)))
        return false;
    }
    return true;
  }
  
  bool printRewrittenSources(const tooling::AtomicChanges &Changes,
                             raw_ostream &OS) {
    std::set<std::string> Files;
    for (const auto &Change : Changes)
      Files.insert(Change.getFilePath());
    tooling::ApplyChangesSpec Spec;
    Spec.Cleanup = false;
    for (const auto &File : Files) {
      llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> BufferErr =
          llvm::MemoryBuffer::getFile(File);
      if (!BufferErr) {
        llvm::errs() << "failed to open" << File << "\n";
        return true;
      }
      auto Result = tooling::applyAtomicChanges(File, (*BufferErr)->getBuffer(),
                                                Changes, Spec);
      if (!Result) {
        llvm::errs() << toString(Result.takeError());
        return true;
      }
      OS << *Result;
    }
    return false;
  }
  
  class TestRefactoringResultConsumer final
      : public ClangRefactorToolConsumerInterface {
  public:
    TestRefactoringResultConsumer(const TestSelectionRangesInFile &TestRanges)
        : TestRanges(TestRanges) {
      Results.push_back({});
    }
  
    ~TestRefactoringResultConsumer() {
      // Ensure all results are checked.
      for (auto &Group : Results) {
        for (auto &Result : Group) {
          if (!Result) {
            (void)llvm::toString(Result.takeError());
          }
        }
      }
    }
  
    void handleError(llvm::Error Err) override { handleResult(std::move(Err)); }
  
    void handle(tooling::AtomicChanges Changes) override {
      handleResult(std::move(Changes));
    }
  
    void handle(tooling::SymbolOccurrences Occurrences) override {
      tooling::RefactoringResultConsumer::handle(std::move(Occurrences));
    }
  
  private:
    bool handleAllResults();
  
    void handleResult(Expected<tooling::AtomicChanges> Result) {
      Results.back().push_back(std::move(Result));
      size_t GroupIndex = Results.size() - 1;
      if (Results.back().size() >=
          TestRanges.GroupedRanges[GroupIndex].Ranges.size()) {
        ++GroupIndex;
        if (GroupIndex >= TestRanges.GroupedRanges.size()) {
          if (handleAllResults())
            exit(1); // error has occurred.
          return;
        }
        Results.push_back({});
      }
    }
  
    const TestSelectionRangesInFile &TestRanges;
    std::vector<std::vector<Expected<tooling::AtomicChanges>>> Results;
  };
  
  std::pair<unsigned, unsigned> getLineColumn(StringRef Filename,
                                              unsigned Offset) {
    ErrorOr<std::unique_ptr<MemoryBuffer>> ErrOrFile =
        MemoryBuffer::getFile(Filename);
    if (!ErrOrFile)
      return {0, 0};
    StringRef Source = ErrOrFile.get()->getBuffer();
    Source = Source.take_front(Offset);
    size_t LastLine = Source.find_last_of("\r\n");
    return {Source.count('\n') + 1,
            (LastLine == StringRef::npos ? Offset : Offset - LastLine) + 1};
  }
  
  } // end anonymous namespace
  
  bool TestRefactoringResultConsumer::handleAllResults() {
    bool Failed = false;
    for (auto &Group : llvm::enumerate(Results)) {
      // All ranges in the group must produce the same result.
      Optional<tooling::AtomicChanges> CanonicalResult;
      Optional<std::string> CanonicalErrorMessage;
      for (auto &I : llvm::enumerate(Group.value())) {
        Expected<tooling::AtomicChanges> &Result = I.value();
        std::string ErrorMessage;
        bool HasResult = !!Result;
        if (!HasResult) {
          handleAllErrors(
              Result.takeError(),
              [&](StringError &Err) { ErrorMessage = Err.getMessage(); },
              [&](DiagnosticError &Err) {
                const PartialDiagnosticAt &Diag = Err.getDiagnostic();
                llvm::SmallString<100> DiagText;
                Diag.second.EmitToString(getDiags(), DiagText);
                ErrorMessage = DiagText.str().str();
              });
        }
        if (!CanonicalResult && !CanonicalErrorMessage) {
          if (HasResult)
            CanonicalResult = std::move(*Result);
          else
            CanonicalErrorMessage = std::move(ErrorMessage);
          continue;
        }
  
        // Verify that this result corresponds to the canonical result.
        if (CanonicalErrorMessage) {
          // The error messages must match.
          if (!HasResult && ErrorMessage == *CanonicalErrorMessage)
            continue;
        } else {
          assert(CanonicalResult && "missing canonical result");
          // The results must match.
          if (HasResult && areChangesSame(*Result, *CanonicalResult))
            continue;
        }
        Failed = true;
        // Report the mismatch.
        std::pair<unsigned, unsigned> LineColumn = getLineColumn(
            TestRanges.Filename,
            TestRanges.GroupedRanges[Group.index()].Ranges[I.index()].Begin);
        llvm::errs()
            << "error: unexpected refactoring result for range starting at "
            << LineColumn.first << ':' << LineColumn.second << " in group '"
            << TestRanges.GroupedRanges[Group.index()].Name << "':\n  ";
        if (HasResult)
          llvm::errs() << "valid result";
        else
          llvm::errs() << "error '" << ErrorMessage << "'";
        llvm::errs() << " does not match initial ";
        if (CanonicalErrorMessage)
          llvm::errs() << "error '" << *CanonicalErrorMessage << "'\n";
        else
          llvm::errs() << "valid result\n";
        if (HasResult && !CanonicalErrorMessage) {
          llvm::errs() << "  Expected to Produce:\n";
          dumpChanges(*CanonicalResult, llvm::errs());
          llvm::errs() << "  Produced:\n";
          dumpChanges(*Result, llvm::errs());
        }
      }
  
      // Dump the results:
      const auto &TestGroup = TestRanges.GroupedRanges[Group.index()];
      if (!CanonicalResult) {
        llvm::outs() << TestGroup.Ranges.size() << " '" << TestGroup.Name
                     << "' results:\n";
        llvm::outs() << *CanonicalErrorMessage << "\n";
      } else {
        llvm::outs() << TestGroup.Ranges.size() << " '" << TestGroup.Name
                     << "' results:\n";
        if (printRewrittenSources(*CanonicalResult, llvm::outs()))
          return true;
      }
    }
    return Failed;
  }
  
  std::unique_ptr<ClangRefactorToolConsumerInterface>
  TestSelectionRangesInFile::createConsumer() const {
    return llvm::make_unique<TestRefactoringResultConsumer>(*this);
  }
  
  /// Adds the \p ColumnOffset to file offset \p Offset, without going past a
  /// newline.
  static unsigned addColumnOffset(StringRef Source, unsigned Offset,
                                  unsigned ColumnOffset) {
    if (!ColumnOffset)
      return Offset;
    StringRef Substr = Source.drop_front(Offset).take_front(ColumnOffset);
    size_t NewlinePos = Substr.find_first_of("\r\n");
    return Offset +
           (NewlinePos == StringRef::npos ? ColumnOffset : (unsigned)NewlinePos);
  }
  
  static unsigned addEndLineOffsetAndEndColumn(StringRef Source, unsigned Offset,
                                               unsigned LineNumberOffset,
                                               unsigned Column) {
    StringRef Line = Source.drop_front(Offset);
    unsigned LineOffset = 0;
    for (; LineNumberOffset != 0; --LineNumberOffset) {
      size_t NewlinePos = Line.find_first_of("\r\n");
      // Line offset goes out of bounds.
      if (NewlinePos == StringRef::npos)
        break;
      LineOffset += NewlinePos + 1;
      Line = Line.drop_front(NewlinePos + 1);
    }
    // Source now points to the line at +lineOffset;
    size_t LineStart = Source.find_last_of("\r\n", /*From=*/Offset + LineOffset);
    return addColumnOffset(
        Source, LineStart == StringRef::npos ? 0 : LineStart + 1, Column - 1);
  }
  
  Optional<TestSelectionRangesInFile>
  findTestSelectionRanges(StringRef Filename) {
    ErrorOr<std::unique_ptr<MemoryBuffer>> ErrOrFile =
        MemoryBuffer::getFile(Filename);
    if (!ErrOrFile) {
      llvm::errs() << "error: -selection=test:" << Filename
                   << " : could not open the given file";
      return None;
    }
    StringRef Source = ErrOrFile.get()->getBuffer();
  
    // See the doc comment for this function for the explanation of this
    // syntax.
    static Regex RangeRegex("range[[:blank:]]*([[:alpha:]_]*)?[[:blank:]]*=[[:"
                            "blank:]]*(\\+[[:digit:]]+)?[[:blank:]]*(->[[:blank:]"
                            "]*[\\+\\:[:digit:]]+)?");
  
    std::map<std::string, SmallVector<TestSelectionRange, 8>> GroupedRanges;
  
    LangOptions LangOpts;
    LangOpts.CPlusPlus = 1;
    LangOpts.CPlusPlus11 = 1;
    Lexer Lex(SourceLocation::getFromRawEncoding(0), LangOpts, Source.begin(),
              Source.begin(), Source.end());
    Lex.SetCommentRetentionState(true);
    Token Tok;
    for (Lex.LexFromRawLexer(Tok); Tok.isNot(tok::eof);
         Lex.LexFromRawLexer(Tok)) {
      if (Tok.isNot(tok::comment))
        continue;
      StringRef Comment =
          Source.substr(Tok.getLocation().getRawEncoding(), Tok.getLength());
      SmallVector<StringRef, 4> Matches;
      // Try to detect mistyped 'range:' comments to ensure tests don't miss
      // anything.
      auto DetectMistypedCommand = [&]() -> bool {
        if (Comment.contains_lower("range") && Comment.contains("=") &&
            !Comment.contains_lower("run") && !Comment.contains("CHECK")) {
          llvm::errs() << "error: suspicious comment '" << Comment
                       << "' that "
                          "resembles the range command found\n";
          llvm::errs() << "note: please reword if this isn't a range command\n";
        }
        return false;
      };
      // Allow CHECK: comments to contain range= commands.
      if (!RangeRegex.match(Comment, &Matches) || Comment.contains("CHECK")) {
        if (DetectMistypedCommand())
          return None;
        continue;
      }
      unsigned Offset = Tok.getEndLoc().getRawEncoding();
      unsigned ColumnOffset = 0;
      if (!Matches[2].empty()) {
        // Don't forget to drop the '+'!
        if (Matches[2].drop_front().getAsInteger(10, ColumnOffset))
          assert(false && "regex should have produced a number");
      }
      Offset = addColumnOffset(Source, Offset, ColumnOffset);
      unsigned EndOffset;
  
      if (!Matches[3].empty()) {
        static Regex EndLocRegex(
            "->[[:blank:]]*(\\+[[:digit:]]+):([[:digit:]]+)");
        SmallVector<StringRef, 4> EndLocMatches;
        if (!EndLocRegex.match(Matches[3], &EndLocMatches)) {
          if (DetectMistypedCommand())
            return None;
          continue;
        }
        unsigned EndLineOffset = 0, EndColumn = 0;
        if (EndLocMatches[1].drop_front().getAsInteger(10, EndLineOffset) ||
            EndLocMatches[2].getAsInteger(10, EndColumn))
          assert(false && "regex should have produced a number");
        EndOffset = addEndLineOffsetAndEndColumn(Source, Offset, EndLineOffset,
                                                 EndColumn);
      } else {
        EndOffset = Offset;
      }
      TestSelectionRange Range = {Offset, EndOffset};
      auto It = GroupedRanges.insert(std::make_pair(
          Matches[1].str(), SmallVector<TestSelectionRange, 8>{Range}));
      if (!It.second)
        It.first->second.push_back(Range);
    }
    if (GroupedRanges.empty()) {
      llvm::errs() << "error: -selection=test:" << Filename
                   << ": no 'range' commands";
      return None;
    }
  
    TestSelectionRangesInFile TestRanges = {Filename.str(), {}};
    for (auto &Group : GroupedRanges)
      TestRanges.GroupedRanges.push_back({Group.first, std::move(Group.second)});
    return std::move(TestRanges);
  }
  
  } // end namespace refactor
  } // end namespace clang
  