mirror of
https://github.com/Kitware/CMake.git
synced 2026-01-02 03:39:43 -06:00
After !6954 got merged, it has become easier for tools to get full stack-traces for runtime traces of a CMake program. The trace information already included in the JSON objects (line number, source file path) allows tools that display these stack traces to print the CMake source code associated to them. However, CMake commands may spawn multiple lines, and the JSON information associated to a trace only contains the line in which the command started, but not the one in which it ended. If tools want to print stack traces along the relevant source code, and they want to print the whole command associated to the stack frame, they will have to implement their own CMake language parser to know where the command ends. In order to simplify the life of those who want to write tooling for CMake, this commit adds a `line_end` field to the json-v1 trace format. If a given command spans multiple lines, the `line_end` field will contain the line of the last line spanned by the command (that of the closing parenthesis associated to the command).
555 lines
16 KiB
C++
555 lines
16 KiB
C++
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
|
file Copyright.txt or https://cmake.org/licensing for details. */
|
|
#include "cmListFileCache.h"
|
|
|
|
#include <cassert>
|
|
#include <memory>
|
|
#include <sstream>
|
|
#include <utility>
|
|
|
|
#ifdef _WIN32
|
|
# include <cmsys/Encoding.hxx>
|
|
#endif
|
|
|
|
#include "cmListFileLexer.h"
|
|
#include "cmMessageType.h"
|
|
#include "cmMessenger.h"
|
|
#include "cmStringAlgorithms.h"
|
|
#include "cmSystemTools.h"
|
|
|
|
struct cmListFileParser
|
|
{
|
|
cmListFileParser(cmListFile* lf, cmListFileBacktrace lfbt,
|
|
cmMessenger* messenger);
|
|
~cmListFileParser();
|
|
cmListFileParser(const cmListFileParser&) = delete;
|
|
cmListFileParser& operator=(const cmListFileParser&) = delete;
|
|
void IssueFileOpenError(std::string const& text) const;
|
|
void IssueError(std::string const& text) const;
|
|
bool ParseFile(const char* filename);
|
|
bool ParseString(const char* str, const char* virtual_filename);
|
|
bool Parse();
|
|
bool ParseFunction(const char* name, long line);
|
|
bool AddArgument(cmListFileLexer_Token* token,
|
|
cmListFileArgument::Delimiter delim);
|
|
cm::optional<cmListFileContext> CheckNesting() const;
|
|
cmListFile* ListFile;
|
|
cmListFileBacktrace Backtrace;
|
|
cmMessenger* Messenger;
|
|
const char* FileName;
|
|
cmListFileLexer* Lexer;
|
|
std::string FunctionName;
|
|
long FunctionLine;
|
|
long FunctionLineEnd;
|
|
std::vector<cmListFileArgument> FunctionArguments;
|
|
enum
|
|
{
|
|
SeparationOkay,
|
|
SeparationWarning,
|
|
SeparationError
|
|
} Separation;
|
|
};
|
|
|
|
cmListFileParser::cmListFileParser(cmListFile* lf, cmListFileBacktrace lfbt,
|
|
cmMessenger* messenger)
|
|
: ListFile(lf)
|
|
, Backtrace(std::move(lfbt))
|
|
, Messenger(messenger)
|
|
, FileName(nullptr)
|
|
, Lexer(cmListFileLexer_New())
|
|
{
|
|
}
|
|
|
|
cmListFileParser::~cmListFileParser()
|
|
{
|
|
cmListFileLexer_Delete(this->Lexer);
|
|
}
|
|
|
|
void cmListFileParser::IssueFileOpenError(const std::string& text) const
|
|
{
|
|
this->Messenger->IssueMessage(MessageType::FATAL_ERROR, text,
|
|
this->Backtrace);
|
|
}
|
|
|
|
void cmListFileParser::IssueError(const std::string& text) const
|
|
{
|
|
cmListFileContext lfc;
|
|
lfc.FilePath = this->FileName;
|
|
lfc.Line = cmListFileLexer_GetCurrentLine(this->Lexer);
|
|
cmListFileBacktrace lfbt = this->Backtrace;
|
|
lfbt = lfbt.Push(lfc);
|
|
this->Messenger->IssueMessage(MessageType::FATAL_ERROR, text, lfbt);
|
|
cmSystemTools::SetFatalErrorOccured();
|
|
}
|
|
|
|
bool cmListFileParser::ParseFile(const char* filename)
|
|
{
|
|
this->FileName = filename;
|
|
|
|
#ifdef _WIN32
|
|
std::string expandedFileName = cmsys::Encoding::ToNarrow(
|
|
cmSystemTools::ConvertToWindowsExtendedPath(filename));
|
|
filename = expandedFileName.c_str();
|
|
#endif
|
|
|
|
// Open the file.
|
|
cmListFileLexer_BOM bom;
|
|
if (!cmListFileLexer_SetFileName(this->Lexer, filename, &bom)) {
|
|
this->IssueFileOpenError("cmListFileCache: error can not open file.");
|
|
return false;
|
|
}
|
|
|
|
if (bom == cmListFileLexer_BOM_Broken) {
|
|
cmListFileLexer_SetFileName(this->Lexer, nullptr, nullptr);
|
|
this->IssueFileOpenError("Error while reading Byte-Order-Mark. "
|
|
"File not seekable?");
|
|
return false;
|
|
}
|
|
|
|
// Verify the Byte-Order-Mark, if any.
|
|
if (bom != cmListFileLexer_BOM_None && bom != cmListFileLexer_BOM_UTF8) {
|
|
cmListFileLexer_SetFileName(this->Lexer, nullptr, nullptr);
|
|
this->IssueFileOpenError(
|
|
"File starts with a Byte-Order-Mark that is not UTF-8.");
|
|
return false;
|
|
}
|
|
|
|
return this->Parse();
|
|
}
|
|
|
|
bool cmListFileParser::ParseString(const char* str,
|
|
const char* virtual_filename)
|
|
{
|
|
this->FileName = virtual_filename;
|
|
|
|
if (!cmListFileLexer_SetString(this->Lexer, str)) {
|
|
this->IssueFileOpenError("cmListFileCache: cannot allocate buffer.");
|
|
return false;
|
|
}
|
|
|
|
return this->Parse();
|
|
}
|
|
|
|
bool cmListFileParser::Parse()
|
|
{
|
|
// Use a simple recursive-descent parser to process the token
|
|
// stream.
|
|
bool haveNewline = true;
|
|
while (cmListFileLexer_Token* token = cmListFileLexer_Scan(this->Lexer)) {
|
|
if (token->type == cmListFileLexer_Token_Space) {
|
|
} else if (token->type == cmListFileLexer_Token_Newline) {
|
|
haveNewline = true;
|
|
} else if (token->type == cmListFileLexer_Token_CommentBracket) {
|
|
haveNewline = false;
|
|
} else if (token->type == cmListFileLexer_Token_Identifier) {
|
|
if (haveNewline) {
|
|
haveNewline = false;
|
|
if (this->ParseFunction(token->text, token->line)) {
|
|
this->ListFile->Functions.emplace_back(
|
|
std::move(this->FunctionName), this->FunctionLine,
|
|
this->FunctionLineEnd, std::move(this->FunctionArguments));
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
std::ostringstream error;
|
|
error << "Parse error. Expected a newline, got "
|
|
<< cmListFileLexer_GetTypeAsString(this->Lexer, token->type)
|
|
<< " with text \"" << token->text << "\".";
|
|
this->IssueError(error.str());
|
|
return false;
|
|
}
|
|
} else {
|
|
std::ostringstream error;
|
|
error << "Parse error. Expected a command name, got "
|
|
<< cmListFileLexer_GetTypeAsString(this->Lexer, token->type)
|
|
<< " with text \"" << token->text << "\".";
|
|
this->IssueError(error.str());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check if all functions are nested properly.
|
|
if (auto badNesting = this->CheckNesting()) {
|
|
this->Messenger->IssueMessage(
|
|
MessageType::FATAL_ERROR,
|
|
"Flow control statements are not properly nested.",
|
|
this->Backtrace.Push(*badNesting));
|
|
cmSystemTools::SetFatalErrorOccured();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool cmListFile::ParseFile(const char* filename, cmMessenger* messenger,
|
|
cmListFileBacktrace const& lfbt)
|
|
{
|
|
if (!cmSystemTools::FileExists(filename) ||
|
|
cmSystemTools::FileIsDirectory(filename)) {
|
|
return false;
|
|
}
|
|
|
|
bool parseError = false;
|
|
|
|
{
|
|
cmListFileParser parser(this, lfbt, messenger);
|
|
parseError = !parser.ParseFile(filename);
|
|
}
|
|
|
|
return !parseError;
|
|
}
|
|
|
|
bool cmListFile::ParseString(const char* str, const char* virtual_filename,
|
|
cmMessenger* messenger,
|
|
const cmListFileBacktrace& lfbt)
|
|
{
|
|
bool parseError = false;
|
|
|
|
{
|
|
cmListFileParser parser(this, lfbt, messenger);
|
|
parseError = !parser.ParseString(str, virtual_filename);
|
|
}
|
|
|
|
return !parseError;
|
|
}
|
|
|
|
bool cmListFileParser::ParseFunction(const char* name, long line)
|
|
{
|
|
// Ininitialize a new function call.
|
|
this->FunctionName = name;
|
|
this->FunctionLine = line;
|
|
|
|
// Command name has already been parsed. Read the left paren.
|
|
cmListFileLexer_Token* token;
|
|
while ((token = cmListFileLexer_Scan(this->Lexer)) &&
|
|
token->type == cmListFileLexer_Token_Space) {
|
|
}
|
|
if (!token) {
|
|
std::ostringstream error;
|
|
/* clang-format off */
|
|
error << "Unexpected end of file.\n"
|
|
<< "Parse error. Function missing opening \"(\".";
|
|
/* clang-format on */
|
|
this->IssueError(error.str());
|
|
return false;
|
|
}
|
|
if (token->type != cmListFileLexer_Token_ParenLeft) {
|
|
std::ostringstream error;
|
|
error << "Parse error. Expected \"(\", got "
|
|
<< cmListFileLexer_GetTypeAsString(this->Lexer, token->type)
|
|
<< " with text \"" << token->text << "\".";
|
|
this->IssueError(error.str());
|
|
return false;
|
|
}
|
|
|
|
// Arguments.
|
|
unsigned long parenDepth = 0;
|
|
this->Separation = SeparationOkay;
|
|
while ((token = cmListFileLexer_Scan(this->Lexer))) {
|
|
if (token->type == cmListFileLexer_Token_Space ||
|
|
token->type == cmListFileLexer_Token_Newline) {
|
|
this->Separation = SeparationOkay;
|
|
continue;
|
|
}
|
|
if (token->type == cmListFileLexer_Token_ParenLeft) {
|
|
parenDepth++;
|
|
this->Separation = SeparationOkay;
|
|
if (!this->AddArgument(token, cmListFileArgument::Unquoted)) {
|
|
return false;
|
|
}
|
|
} else if (token->type == cmListFileLexer_Token_ParenRight) {
|
|
if (parenDepth == 0) {
|
|
this->FunctionLineEnd = token->line;
|
|
return true;
|
|
}
|
|
parenDepth--;
|
|
this->Separation = SeparationOkay;
|
|
if (!this->AddArgument(token, cmListFileArgument::Unquoted)) {
|
|
return false;
|
|
}
|
|
this->Separation = SeparationWarning;
|
|
} else if (token->type == cmListFileLexer_Token_Identifier ||
|
|
token->type == cmListFileLexer_Token_ArgumentUnquoted) {
|
|
if (!this->AddArgument(token, cmListFileArgument::Unquoted)) {
|
|
return false;
|
|
}
|
|
this->Separation = SeparationWarning;
|
|
} else if (token->type == cmListFileLexer_Token_ArgumentQuoted) {
|
|
if (!this->AddArgument(token, cmListFileArgument::Quoted)) {
|
|
return false;
|
|
}
|
|
this->Separation = SeparationWarning;
|
|
} else if (token->type == cmListFileLexer_Token_ArgumentBracket) {
|
|
if (!this->AddArgument(token, cmListFileArgument::Bracket)) {
|
|
return false;
|
|
}
|
|
this->Separation = SeparationError;
|
|
} else if (token->type == cmListFileLexer_Token_CommentBracket) {
|
|
this->Separation = SeparationError;
|
|
} else {
|
|
// Error.
|
|
std::ostringstream error;
|
|
error << "Parse error. Function missing ending \")\". "
|
|
<< "Instead found "
|
|
<< cmListFileLexer_GetTypeAsString(this->Lexer, token->type)
|
|
<< " with text \"" << token->text << "\".";
|
|
this->IssueError(error.str());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::ostringstream error;
|
|
cmListFileContext lfc;
|
|
lfc.FilePath = this->FileName;
|
|
lfc.Line = line;
|
|
cmListFileBacktrace lfbt = this->Backtrace;
|
|
lfbt = lfbt.Push(lfc);
|
|
error << "Parse error. Function missing ending \")\". "
|
|
<< "End of file reached.";
|
|
this->Messenger->IssueMessage(MessageType::FATAL_ERROR, error.str(), lfbt);
|
|
return false;
|
|
}
|
|
|
|
bool cmListFileParser::AddArgument(cmListFileLexer_Token* token,
|
|
cmListFileArgument::Delimiter delim)
|
|
{
|
|
this->FunctionArguments.emplace_back(token->text, delim, token->line);
|
|
if (this->Separation == SeparationOkay) {
|
|
return true;
|
|
}
|
|
bool isError = (this->Separation == SeparationError ||
|
|
delim == cmListFileArgument::Bracket);
|
|
std::ostringstream m;
|
|
cmListFileContext lfc;
|
|
lfc.FilePath = this->FileName;
|
|
lfc.Line = token->line;
|
|
cmListFileBacktrace lfbt = this->Backtrace;
|
|
lfbt = lfbt.Push(lfc);
|
|
|
|
m << "Syntax " << (isError ? "Error" : "Warning") << " in cmake code at "
|
|
<< "column " << token->column << "\n"
|
|
<< "Argument not separated from preceding token by whitespace.";
|
|
/* clang-format on */
|
|
if (isError) {
|
|
this->Messenger->IssueMessage(MessageType::FATAL_ERROR, m.str(), lfbt);
|
|
return false;
|
|
}
|
|
this->Messenger->IssueMessage(MessageType::AUTHOR_WARNING, m.str(), lfbt);
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
enum class NestingStateEnum
|
|
{
|
|
If,
|
|
Else,
|
|
While,
|
|
Foreach,
|
|
Function,
|
|
Macro,
|
|
};
|
|
|
|
struct NestingState
|
|
{
|
|
NestingStateEnum State;
|
|
cmListFileContext Context;
|
|
};
|
|
|
|
bool TopIs(std::vector<NestingState>& stack, NestingStateEnum state)
|
|
{
|
|
return !stack.empty() && stack.back().State == state;
|
|
}
|
|
}
|
|
|
|
cm::optional<cmListFileContext> cmListFileParser::CheckNesting() const
|
|
{
|
|
std::vector<NestingState> stack;
|
|
|
|
for (auto const& func : this->ListFile->Functions) {
|
|
auto const& name = func.LowerCaseName();
|
|
if (name == "if") {
|
|
stack.push_back({
|
|
NestingStateEnum::If,
|
|
cmListFileContext::FromListFileFunction(func, this->FileName),
|
|
});
|
|
} else if (name == "elseif") {
|
|
if (!TopIs(stack, NestingStateEnum::If)) {
|
|
return cmListFileContext::FromListFileFunction(func, this->FileName);
|
|
}
|
|
stack.back() = {
|
|
NestingStateEnum::If,
|
|
cmListFileContext::FromListFileFunction(func, this->FileName),
|
|
};
|
|
} else if (name == "else") {
|
|
if (!TopIs(stack, NestingStateEnum::If)) {
|
|
return cmListFileContext::FromListFileFunction(func, this->FileName);
|
|
}
|
|
stack.back() = {
|
|
NestingStateEnum::Else,
|
|
cmListFileContext::FromListFileFunction(func, this->FileName),
|
|
};
|
|
} else if (name == "endif") {
|
|
if (!TopIs(stack, NestingStateEnum::If) &&
|
|
!TopIs(stack, NestingStateEnum::Else)) {
|
|
return cmListFileContext::FromListFileFunction(func, this->FileName);
|
|
}
|
|
stack.pop_back();
|
|
} else if (name == "while") {
|
|
stack.push_back({
|
|
NestingStateEnum::While,
|
|
cmListFileContext::FromListFileFunction(func, this->FileName),
|
|
});
|
|
} else if (name == "endwhile") {
|
|
if (!TopIs(stack, NestingStateEnum::While)) {
|
|
return cmListFileContext::FromListFileFunction(func, this->FileName);
|
|
}
|
|
stack.pop_back();
|
|
} else if (name == "foreach") {
|
|
stack.push_back({
|
|
NestingStateEnum::Foreach,
|
|
cmListFileContext::FromListFileFunction(func, this->FileName),
|
|
});
|
|
} else if (name == "endforeach") {
|
|
if (!TopIs(stack, NestingStateEnum::Foreach)) {
|
|
return cmListFileContext::FromListFileFunction(func, this->FileName);
|
|
}
|
|
stack.pop_back();
|
|
} else if (name == "function") {
|
|
stack.push_back({
|
|
NestingStateEnum::Function,
|
|
cmListFileContext::FromListFileFunction(func, this->FileName),
|
|
});
|
|
} else if (name == "endfunction") {
|
|
if (!TopIs(stack, NestingStateEnum::Function)) {
|
|
return cmListFileContext::FromListFileFunction(func, this->FileName);
|
|
}
|
|
stack.pop_back();
|
|
} else if (name == "macro") {
|
|
stack.push_back({
|
|
NestingStateEnum::Macro,
|
|
cmListFileContext::FromListFileFunction(func, this->FileName),
|
|
});
|
|
} else if (name == "endmacro") {
|
|
if (!TopIs(stack, NestingStateEnum::Macro)) {
|
|
return cmListFileContext::FromListFileFunction(func, this->FileName);
|
|
}
|
|
stack.pop_back();
|
|
}
|
|
}
|
|
|
|
if (!stack.empty()) {
|
|
return stack.back().Context;
|
|
}
|
|
|
|
return cm::nullopt;
|
|
}
|
|
|
|
// We hold a call/file context.
|
|
struct cmListFileBacktrace::Entry
|
|
{
|
|
Entry(std::shared_ptr<Entry const> parent, cmListFileContext lfc)
|
|
: Context(std::move(lfc))
|
|
, Parent(std::move(parent))
|
|
{
|
|
}
|
|
|
|
cmListFileContext Context;
|
|
std::shared_ptr<Entry const> Parent;
|
|
};
|
|
|
|
/* NOLINTNEXTLINE(performance-unnecessary-value-param) */
|
|
cmListFileBacktrace::cmListFileBacktrace(std::shared_ptr<Entry const> parent,
|
|
cmListFileContext const& lfc)
|
|
: TopEntry(std::make_shared<Entry const>(std::move(parent), lfc))
|
|
{
|
|
}
|
|
|
|
cmListFileBacktrace::cmListFileBacktrace(std::shared_ptr<Entry const> top)
|
|
: TopEntry(std::move(top))
|
|
{
|
|
}
|
|
|
|
cmListFileBacktrace cmListFileBacktrace::Push(std::string const& file) const
|
|
{
|
|
// We are entering a file-level scope but have not yet reached
|
|
// any specific line or command invocation within it. This context
|
|
// is useful to print when it is at the top but otherwise can be
|
|
// skipped during call stack printing.
|
|
cmListFileContext lfc;
|
|
lfc.FilePath = file;
|
|
return this->Push(lfc);
|
|
}
|
|
|
|
cmListFileBacktrace cmListFileBacktrace::Push(
|
|
cmListFileContext const& lfc) const
|
|
{
|
|
return cmListFileBacktrace(this->TopEntry, lfc);
|
|
}
|
|
|
|
cmListFileBacktrace cmListFileBacktrace::Pop() const
|
|
{
|
|
assert(this->TopEntry);
|
|
return cmListFileBacktrace(this->TopEntry->Parent);
|
|
}
|
|
|
|
cmListFileContext const& cmListFileBacktrace::Top() const
|
|
{
|
|
assert(this->TopEntry);
|
|
return this->TopEntry->Context;
|
|
}
|
|
|
|
bool cmListFileBacktrace::Empty() const
|
|
{
|
|
return !this->TopEntry;
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& os, cmListFileContext const& lfc)
|
|
{
|
|
os << lfc.FilePath;
|
|
if (lfc.Line > 0) {
|
|
os << ":" << lfc.Line;
|
|
if (!lfc.Name.empty()) {
|
|
os << " (" << lfc.Name << ")";
|
|
}
|
|
} else if (lfc.Line == cmListFileContext::DeferPlaceholderLine) {
|
|
os << ":DEFERRED";
|
|
}
|
|
return os;
|
|
}
|
|
|
|
bool operator<(const cmListFileContext& lhs, const cmListFileContext& rhs)
|
|
{
|
|
if (lhs.Line != rhs.Line) {
|
|
return lhs.Line < rhs.Line;
|
|
}
|
|
return lhs.FilePath < rhs.FilePath;
|
|
}
|
|
|
|
bool operator==(const cmListFileContext& lhs, const cmListFileContext& rhs)
|
|
{
|
|
return lhs.Line == rhs.Line && lhs.FilePath == rhs.FilePath;
|
|
}
|
|
|
|
bool operator!=(const cmListFileContext& lhs, const cmListFileContext& rhs)
|
|
{
|
|
return !(lhs == rhs);
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& os, BT<std::string> const& s)
|
|
{
|
|
return os << s.Value;
|
|
}
|
|
|
|
std::vector<BT<std::string>> cmExpandListWithBacktrace(
|
|
std::string const& list, cmListFileBacktrace const& bt, bool emptyArgs)
|
|
{
|
|
std::vector<BT<std::string>> result;
|
|
std::vector<std::string> tmp = cmExpandedList(list, emptyArgs);
|
|
result.reserve(tmp.size());
|
|
for (std::string& i : tmp) {
|
|
result.emplace_back(std::move(i), bt);
|
|
}
|
|
return result;
|
|
}
|