| //===-- lib/Parser/message.cpp --------------------------------------------===// | |
| // | |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| // | |
| //===----------------------------------------------------------------------===// | |
| #include "flang/Parser/message.h" | |
| #include "flang/Common/idioms.h" | |
| #include "flang/Parser/char-set.h" | |
| #include "llvm/ADT/StringRef.h" | |
| #include "llvm/Support/raw_ostream.h" | |
| #include <algorithm> | |
| #include <cstdarg> | |
| #include <cstddef> | |
| #include <cstdio> | |
| #include <string> | |
| #include <vector> | |
| namespace Fortran::parser { | |
| // The nextCh parser emits this, and Message::GetProvenanceRange() looks for it. | |
| const MessageFixedText MessageFixedText::endOfFileMessage{ | |
| "end of file"_err_en_US}; | |
| llvm::raw_ostream &operator<<(llvm::raw_ostream &o, const MessageFixedText &t) { | |
| std::size_t n{t.text().size()}; | |
| for (std::size_t j{0}; j < n; ++j) { | |
| o << t.text()[j]; | |
| } | |
| return o; | |
| } | |
| void MessageFormattedText::Format(const MessageFixedText *text, ...) { | |
| const char *p{text->text().begin()}; | |
| std::string asString; | |
| if (*text->text().end() != '\0') { | |
| // not NUL-terminated | |
| asString = text->text().NULTerminatedToString(); | |
| p = asString.c_str(); | |
| } | |
| va_list ap; | |
| va_start(ap, text); | |
| #ifdef _MSC_VER | |
| // Microsoft has a separate function for "positional arguments", which is | |
| // used in some messages. | |
| int need{_vsprintf_p(nullptr, 0, p, ap)}; | |
| #else | |
| int need{vsnprintf(nullptr, 0, p, ap)}; | |
| #endif | |
| CHECK(need >= 0); | |
| char *buffer{ | |
| static_cast<char *>(std::malloc(static_cast<std::size_t>(need) + 1))}; | |
| CHECK(buffer); | |
| va_end(ap); | |
| va_start(ap, text); | |
| #ifdef _MSC_VER | |
| // Use positional argument variant of printf. | |
| int need2{_vsprintf_p(buffer, need + 1, p, ap)}; | |
| #else | |
| int need2{vsnprintf(buffer, need + 1, p, ap)}; | |
| #endif | |
| CHECK(need2 == need); | |
| va_end(ap); | |
| string_ = buffer; | |
| std::free(buffer); | |
| conversions_.clear(); | |
| } | |
| const char *MessageFormattedText::Convert(const std::string &s) { | |
| conversions_.emplace_front(s); | |
| return conversions_.front().c_str(); | |
| } | |
| const char *MessageFormattedText::Convert(std::string &&s) { | |
| conversions_.emplace_front(std::move(s)); | |
| return conversions_.front().c_str(); | |
| } | |
| const char *MessageFormattedText::Convert(const std::string_view &s) { | |
| conversions_.emplace_front(s); | |
| return conversions_.front().c_str(); | |
| } | |
| const char *MessageFormattedText::Convert(std::string_view &&s) { | |
| conversions_.emplace_front(s); | |
| return conversions_.front().c_str(); | |
| } | |
| const char *MessageFormattedText::Convert(CharBlock x) { | |
| return Convert(x.ToString()); | |
| } | |
| const char *MessageFormattedText::Convert(llvm::StringRef x) { | |
| return Convert(x.str()); | |
| } | |
| std::string MessageExpectedText::ToString() const { | |
| return common::visit( | |
| common::visitors{ | |
| [](CharBlock cb) { | |
| if (!cb.empty() && cb.back() == ' ') { | |
| // Omit any trailing blank in the expected token string. | |
| cb = CharBlock{cb.begin(), cb.size() - 1}; | |
| } | |
| return MessageFormattedText("expected '%s'"_err_en_US, cb) | |
| .MoveString(); | |
| }, | |
| [](const SetOfChars &set) { | |
| SetOfChars expect{set}; | |
| if (expect.Has('\n')) { | |
| expect = expect.Difference('\n'); | |
| if (expect.empty()) { | |
| return "expected end of line"_err_en_US.text().ToString(); | |
| } else { | |
| std::string s{expect.ToString()}; | |
| if (s.size() == 1) { | |
| return MessageFormattedText( | |
| "expected end of line or '%s'"_err_en_US, s) | |
| .MoveString(); | |
| } else { | |
| return MessageFormattedText( | |
| "expected end of line or one of '%s'"_err_en_US, s) | |
| .MoveString(); | |
| } | |
| } | |
| } | |
| std::string s{expect.ToString()}; | |
| if (s.size() != 1) { | |
| return MessageFormattedText("expected one of '%s'"_err_en_US, s) | |
| .MoveString(); | |
| } else { | |
| return MessageFormattedText("expected '%s'"_err_en_US, s) | |
| .MoveString(); | |
| } | |
| }, | |
| }, | |
| u_); | |
| } | |
| bool MessageExpectedText::Merge(const MessageExpectedText &that) { | |
| return common::visit(common::visitors{ | |
| [](SetOfChars &s1, const SetOfChars &s2) { | |
| s1 = s1.Union(s2); | |
| return true; | |
| }, | |
| [](const auto &, const auto &) { return false; }, | |
| }, | |
| u_, that.u_); | |
| } | |
| bool Message::SortBefore(const Message &that) const { | |
| // Messages from prescanning have ProvenanceRange values for their locations, | |
| // while messages from later phases have CharBlock values, since the | |
| // conversion of cooked source stream locations to provenances is not | |
| // free and needs to be deferred, and many messages created during parsing | |
| // are speculative. Messages with ProvenanceRange locations are ordered | |
| // before others for sorting. | |
| return common::visit( | |
| common::visitors{ | |
| [](CharBlock cb1, CharBlock cb2) { | |
| return cb1.begin() < cb2.begin(); | |
| }, | |
| [](CharBlock, const ProvenanceRange &) { return false; }, | |
| [](const ProvenanceRange &pr1, const ProvenanceRange &pr2) { | |
| return pr1.start() < pr2.start(); | |
| }, | |
| [](const ProvenanceRange &, CharBlock) { return true; }, | |
| }, | |
| location_, that.location_); | |
| } | |
| bool Message::IsFatal() const { return IsFatalSeverity(severity()); } | |
| Severity Message::severity() const { | |
| return common::visit( | |
| common::visitors{ | |
| [](const MessageExpectedText &) { return Severity::Error; }, | |
| [](const MessageFixedText &x) { return x.severity(); }, | |
| [](const MessageFormattedText &x) { return x.severity(); }, | |
| }, | |
| text_); | |
| } | |
| Message &Message::set_severity(Severity severity) { | |
| common::visit( | |
| common::visitors{ | |
| [](const MessageExpectedText &) {}, | |
| [severity](MessageFixedText &x) { x.set_severity(severity); }, | |
| [severity](MessageFormattedText &x) { x.set_severity(severity); }, | |
| }, | |
| text_); | |
| return *this; | |
| } | |
| std::optional<common::LanguageFeature> Message::languageFeature() const { | |
| return languageFeature_; | |
| } | |
| Message &Message::set_languageFeature(common::LanguageFeature feature) { | |
| languageFeature_ = feature; | |
| return *this; | |
| } | |
| std::optional<common::UsageWarning> Message::usageWarning() const { | |
| return usageWarning_; | |
| } | |
| Message &Message::set_usageWarning(common::UsageWarning warning) { | |
| usageWarning_ = warning; | |
| return *this; | |
| } | |
| std::string Message::ToString() const { | |
| return common::visit( | |
| common::visitors{ | |
| [](const MessageFixedText &t) { | |
| return t.text().NULTerminatedToString(); | |
| }, | |
| [](const MessageFormattedText &t) { return t.string(); }, | |
| [](const MessageExpectedText &e) { return e.ToString(); }, | |
| }, | |
| text_); | |
| } | |
| void Message::ResolveProvenances(const AllCookedSources &allCooked) { | |
| if (CharBlock * cb{std::get_if<CharBlock>(&location_)}) { | |
| if (std::optional<ProvenanceRange> resolved{ | |
| allCooked.GetProvenanceRange(*cb)}) { | |
| location_ = *resolved; | |
| } | |
| } | |
| if (Message * attachment{attachment_.get()}) { | |
| attachment->ResolveProvenances(allCooked); | |
| } | |
| } | |
| std::optional<ProvenanceRange> Message::GetProvenanceRange( | |
| const AllCookedSources &allCooked) const { | |
| return common::visit( | |
| common::visitors{ | |
| [&](CharBlock cb) -> std::optional<ProvenanceRange> { | |
| if (auto pr{allCooked.GetProvenanceRange(cb)}) { | |
| return pr; | |
| } else if (const auto *fixed{std::get_if<MessageFixedText>(&text_)}; | |
| fixed && | |
| fixed->text() == MessageFixedText::endOfFileMessage.text() && | |
| cb.begin() && cb.size() == 1) { | |
| // Failure from "nextCh" due to reaching EOF. Back up one byte | |
| // to the terminal newline so that the output looks better. | |
| return allCooked.GetProvenanceRange(CharBlock{cb.begin() - 1, 1}); | |
| } else { | |
| return std::nullopt; | |
| } | |
| }, | |
| [](const ProvenanceRange &pr) { return std::make_optional(pr); }, | |
| }, | |
| location_); | |
| } | |
| static std::string Prefix(Severity severity) { | |
| switch (severity) { | |
| case Severity::Error: | |
| case Severity::ErrorUnlessDeadCode: | |
| return "error: "; | |
| case Severity::Warning: | |
| return "warning: "; | |
| case Severity::Portability: | |
| return "portability: "; | |
| case Severity::Because: | |
| return "because: "; | |
| case Severity::Context: | |
| return "in the context: "; | |
| case Severity::Todo: | |
| return "error: not yet implemented: "; | |
| case Severity::None: | |
| break; | |
| } | |
| return ""; | |
| } | |
| static llvm::raw_ostream::Colors PrefixColor(Severity severity) { | |
| if (IsFatalSeverity(severity)) { | |
| return llvm::raw_ostream::RED; | |
| } else if (IsWarningSeverity(severity)) { | |
| return llvm::raw_ostream::MAGENTA; | |
| } else { | |
| return llvm::raw_ostream::SAVEDCOLOR; | |
| } | |
| } | |
| static std::string HintLanguageControlFlag( | |
| const common::LanguageFeatureControl *hintFlagPtr, | |
| std::optional<common::LanguageFeature> feature, | |
| std::optional<common::UsageWarning> warning) { | |
| if (hintFlagPtr) { | |
| std::string flag; | |
| if (warning) { | |
| flag = hintFlagPtr->getDefaultCliSpelling(*warning); | |
| } else if (feature) { | |
| flag = hintFlagPtr->getDefaultCliSpelling(*feature); | |
| } | |
| if (!flag.empty()) { | |
| return " [-W" + flag + "]"; | |
| } | |
| } | |
| return ""; | |
| } | |
| static constexpr int MAX_CONTEXTS_EMITTED{2}; | |
| static constexpr bool OMIT_SHARED_CONTEXTS{true}; | |
| void Message::Emit(llvm::raw_ostream &o, const AllCookedSources &allCooked, | |
| bool echoSourceLine, | |
| const common::LanguageFeatureControl *hintFlagPtr) const { | |
| std::optional<ProvenanceRange> provenanceRange{GetProvenanceRange(allCooked)}; | |
| const AllSources &sources{allCooked.allSources()}; | |
| const std::string text{ToString()}; | |
| const std::string hint{ | |
| HintLanguageControlFlag(hintFlagPtr, languageFeature_, usageWarning_)}; | |
| sources.EmitMessage(o, provenanceRange, text + hint, Prefix(severity()), | |
| PrefixColor(severity()), echoSourceLine); | |
| // Refers to whether the attachment in the loop below is a context, but can't | |
| // be declared inside the loop because the previous iteration's | |
| // attachment->attachmentIsContext_ indicates this. | |
| bool isContext{attachmentIsContext_}; | |
| int contextsEmitted{0}; | |
| // Emit attachments. | |
| for (const Message *attachment{attachment_.get()}; attachment; | |
| isContext = attachment->attachmentIsContext_, | |
| attachment = attachment->attachment_.get()) { | |
| Severity severity = isContext ? Severity::Context : attachment->severity(); | |
| auto emitAttachment = [&]() { | |
| sources.EmitMessage(o, attachment->GetProvenanceRange(allCooked), | |
| attachment->ToString(), Prefix(severity), PrefixColor(severity), | |
| echoSourceLine); | |
| }; | |
| if (isContext) { | |
| // Truncate the number of contexts emitted. | |
| if (contextsEmitted < MAX_CONTEXTS_EMITTED) { | |
| emitAttachment(); | |
| ++contextsEmitted; | |
| } | |
| if constexpr (OMIT_SHARED_CONTEXTS) { | |
| // Skip less specific contexts at the same location. | |
| for (const Message *next_attachment{attachment->attachment_.get()}; | |
| next_attachment && next_attachment->attachmentIsContext_ && | |
| next_attachment->AtSameLocation(*attachment); | |
| next_attachment = next_attachment->attachment_.get()) { | |
| attachment = next_attachment; | |
| } | |
| // NB, this loop increments `attachment` one more time after the | |
| // previous loop is done advancing it to the last context at the same | |
| // location. | |
| } | |
| } else { | |
| emitAttachment(); | |
| } | |
| } | |
| } | |
| // Messages are equal if they're for the same location and text, and the user | |
| // visible aspects of their attachments are the same | |
| bool Message::operator==(const Message &that) const { | |
| if (!AtSameLocation(that) || ToString() != that.ToString() || | |
| severity() != that.severity() || | |
| attachmentIsContext_ != that.attachmentIsContext_) { | |
| return false; | |
| } | |
| const Message *thatAttachment{that.attachment_.get()}; | |
| for (const Message *attachment{attachment_.get()}; attachment; | |
| attachment = attachment->attachment_.get()) { | |
| if (!thatAttachment || !attachment->AtSameLocation(*thatAttachment) || | |
| attachment->ToString() != thatAttachment->ToString() || | |
| attachment->severity() != thatAttachment->severity()) { | |
| return false; | |
| } | |
| thatAttachment = thatAttachment->attachment_.get(); | |
| } | |
| return !thatAttachment; | |
| } | |
| bool Message::Merge(const Message &that) { | |
| return AtSameLocation(that) && | |
| (!that.attachment_.get() || | |
| attachment_.get() == that.attachment_.get()) && | |
| common::visit( | |
| common::visitors{ | |
| [](MessageExpectedText &e1, const MessageExpectedText &e2) { | |
| return e1.Merge(e2); | |
| }, | |
| [](const auto &, const auto &) { return false; }, | |
| }, | |
| text_, that.text_); | |
| } | |
| Message &Message::Attach(Message *m) { | |
| if (!attachment_) { | |
| attachment_ = m; | |
| } else { | |
| if (attachment_->references() > 1) { | |
| // Don't attach to a shared context attachment; copy it first. | |
| attachment_ = new Message{*attachment_}; | |
| } | |
| attachment_->Attach(m); | |
| } | |
| return *this; | |
| } | |
| Message &Message::Attach(std::unique_ptr<Message> &&m) { | |
| return Attach(m.release()); | |
| } | |
| bool Message::AtSameLocation(const Message &that) const { | |
| return common::visit( | |
| common::visitors{ | |
| [](CharBlock cb1, CharBlock cb2) { | |
| return cb1.begin() == cb2.begin(); | |
| }, | |
| [](const ProvenanceRange &pr1, const ProvenanceRange &pr2) { | |
| return pr1.start() == pr2.start(); | |
| }, | |
| [](const auto &, const auto &) { return false; }, | |
| }, | |
| location_, that.location_); | |
| } | |
| bool Messages::Merge(const Message &msg) { | |
| if (msg.IsMergeable()) { | |
| for (auto &m : messages_) { | |
| if (m.Merge(msg)) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| void Messages::Merge(Messages &&that) { | |
| if (messages_.empty()) { | |
| *this = std::move(that); | |
| } else { | |
| while (!that.messages_.empty()) { | |
| if (Merge(that.messages_.front())) { | |
| that.messages_.pop_front(); | |
| } else { | |
| auto next{that.messages_.begin()}; | |
| ++next; | |
| messages_.splice( | |
| messages_.end(), that.messages_, that.messages_.begin(), next); | |
| } | |
| } | |
| } | |
| } | |
| void Messages::Copy(const Messages &that) { | |
| for (const Message &m : that.messages_) { | |
| Message copy{m}; | |
| Say(std::move(copy)); | |
| } | |
| } | |
| void Messages::ResolveProvenances(const AllCookedSources &allCooked) { | |
| for (Message &m : messages_) { | |
| m.ResolveProvenances(allCooked); | |
| } | |
| } | |
| void Messages::Emit(llvm::raw_ostream &o, const AllCookedSources &allCooked, | |
| bool echoSourceLines, const common::LanguageFeatureControl *hintFlagPtr, | |
| std::size_t maxErrorsToEmit, bool warningsAreErrors) const { | |
| std::vector<const Message *> sorted; | |
| for (const auto &msg : messages_) { | |
| sorted.push_back(&msg); | |
| } | |
| std::stable_sort(sorted.begin(), sorted.end(), | |
| [](const Message *x, const Message *y) { return x->SortBefore(*y); }); | |
| std::vector<const Message *> msgsWithLastLocation; | |
| std::size_t errorsEmitted{0}; | |
| for (const Message *msg : sorted) { | |
| bool shouldSkipMsg{false}; | |
| // Don't emit two identical messages for the same location. | |
| // At the same location, messages are sorted by the order they were | |
| // added to the Messages buffer, which is a decent proxy for the | |
| // causality of the messages. | |
| if (!msgsWithLastLocation.empty()) { | |
| if (msgsWithLastLocation[0]->AtSameLocation(*msg)) { | |
| for (const Message *msgAtThisLocation : msgsWithLastLocation) { | |
| if (*msg == *msgAtThisLocation) { | |
| shouldSkipMsg = true; // continue loop over sorted messages | |
| break; | |
| } | |
| } | |
| } else { | |
| msgsWithLastLocation.clear(); | |
| } | |
| } | |
| if (shouldSkipMsg) { | |
| continue; | |
| } | |
| msgsWithLastLocation.push_back(msg); | |
| msg->Emit(o, allCooked, echoSourceLines, hintFlagPtr); | |
| if (warningsAreErrors || msg->IsFatal()) { | |
| ++errorsEmitted; | |
| } | |
| // If maxErrorsToEmit is 0, emit all errors, otherwise break after | |
| // maxErrorsToEmit. | |
| if (maxErrorsToEmit > 0 && errorsEmitted >= maxErrorsToEmit) { | |
| break; | |
| } | |
| } | |
| } | |
| void Messages::AttachTo(Message &msg, std::optional<Severity> severity) { | |
| for (Message &m : messages_) { | |
| Message m2{std::move(m)}; | |
| if (severity) { | |
| m2.set_severity(*severity); | |
| } | |
| msg.Attach(std::move(m2)); | |
| } | |
| messages_.clear(); | |
| } | |
| bool Messages::AnyFatalError(bool warningsAreErrors) const { | |
| // Short-circuit in the most common case. | |
| if (messages_.empty()) { | |
| return false; | |
| } | |
| // If warnings are errors and there are warnings or errors, this is fatal. | |
| // This preserves the compiler's current behavior of treating any non-fatal | |
| // message as a warning. We may want to refine this in the future. | |
| if (warningsAreErrors) { | |
| return true; | |
| } | |
| // Otherwise, check the message buffer for fatal errors. | |
| for (const auto &msg : messages_) { | |
| if (msg.IsFatal()) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } // namespace Fortran::parser |