Skip to content

Commit c3d8145

Browse files
committed
feat(Corpus): render source line and caret in warning output
Warnings now include a snippet of the source file similar to GCC and Clang diagnostics. Each warning prints the file path, line, and column, followed by the line of source text and a caret marker pointing to the column. This makes it easier to identify and fix issues directly in context without having to open the file manually.
1 parent c7219fa commit c3d8145

File tree

2 files changed

+160
-18
lines changed

2 files changed

+160
-18
lines changed

src/lib/Metadata/Finalizers/JavadocFinalizer.cpp

Lines changed: 155 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <lib/Metadata/Finalizers/JavadocFinalizer.hpp>
1515
#include <mrdocs/Support/Algorithm.hpp>
1616
#include <mrdocs/Support/ScopeExit.hpp>
17+
#include <mrdocs/Support/Path.hpp>
1718
#include <algorithm>
1819
#include <format>
1920

@@ -1672,9 +1673,64 @@ checkExists(SymbolID const& id) const
16721673
MRDOCS_ASSERT(corpus_.info_.contains(id));
16731674
}
16741675

1676+
namespace {
1677+
// Expand tabs to spaces using a tab stop of 8 (common in toolchains)
1678+
inline
1679+
std::string
1680+
expand_tabs(std::string_view s, std::size_t tabw = 8)
1681+
{
1682+
std::string out;
1683+
out.reserve(s.size());
1684+
std::size_t col = 0;
1685+
for (char ch: s)
1686+
{
1687+
if (ch == '\t')
1688+
{
1689+
std::size_t spaces = tabw - (col % tabw);
1690+
out.append(spaces, ' ');
1691+
col += spaces;
1692+
} else
1693+
{
1694+
out.push_back(ch);
1695+
// naive column advance;
1696+
// good enough for ASCII/byte-based columns
1697+
++col;
1698+
}
1699+
}
1700+
return out;
1701+
}
1702+
1703+
// Split into lines; tolerates \n, \r\n, and final line w/o newline
1704+
inline
1705+
std::vector<std::string_view>
1706+
split_lines(std::string const& text)
1707+
{
1708+
std::vector<std::string_view> lines;
1709+
std::size_t start = 0;
1710+
while (start <= text.size())
1711+
{
1712+
auto nl = text.find('\n', start);
1713+
if (nl == std::string::npos)
1714+
{
1715+
// last line (may be empty)
1716+
lines.emplace_back(text.data() + start, text.size() - start);
1717+
break;
1718+
}
1719+
// trim a preceding '\r' if present
1720+
std::size_t len = nl - start;
1721+
if (len > 0 && text[nl - 1] == '\r')
1722+
{
1723+
--len;
1724+
}
1725+
lines.emplace_back(text.data() + start, len);
1726+
start = nl + 1;
1727+
}
1728+
return lines;
1729+
}
1730+
} // namespace
1731+
16751732
void
1676-
JavadocFinalizer::
1677-
emitWarnings()
1733+
JavadocFinalizer::emitWarnings()
16781734
{
16791735
MRDOCS_CHECK_OR(corpus_.config->warnings);
16801736
warnUndocumented();
@@ -1683,37 +1739,119 @@ emitWarnings()
16831739
warnUndocEnumValues();
16841740
warnUnnamedParams();
16851741

1686-
// Print to the console
1687-
auto const level = !corpus_.config->warnAsError ? report::Level::warn : report::Level::error;
1688-
for (auto const& [loc, msgs] : warnings_)
1742+
auto const level = !corpus_.config->warnAsError ?
1743+
report::Level::warn :
1744+
report::Level::error;
1745+
1746+
// Simple cache for the last file we touched
1747+
std::string_view lastPath;
1748+
std::string fileContents;
1749+
std::vector<std::string_view> fileLines;
1750+
1751+
for (auto const& [loc, msgs]: warnings_)
16891752
{
1690-
std::string locWarning =
1691-
std::format("{}:{}\n", loc.FullPath, loc.LineNumber);
1692-
int i = 1;
1693-
for (auto const &msg : msgs) {
1694-
locWarning += std::format(" {}) {}\n", i++, msg);
1695-
}
1696-
report::log(level, locWarning);
1753+
// Build the location header
1754+
std::string out;
1755+
out += std::format("{}:{}:{}:\n", loc.FullPath, loc.LineNumber, loc.ColumnNumber);
1756+
1757+
// Append grouped messages for this location
1758+
{
1759+
int i = 1;
1760+
for (auto const& msg: msgs)
1761+
{
1762+
out += std::format(" {}) {}\n", i++, msg);
1763+
}
1764+
}
1765+
1766+
// Render the source snippet if possible
1767+
// Load file if path changed
1768+
if (loc.FullPath != lastPath)
1769+
{
1770+
lastPath = loc.FullPath;
1771+
fileContents.clear();
1772+
fileLines.clear();
1773+
1774+
if (auto expFileContents = files::getFileText(loc.FullPath);
1775+
expFileContents)
1776+
{
1777+
fileContents = std::move(*expFileContents);
1778+
fileLines = split_lines(fileContents);
1779+
}
1780+
else
1781+
{
1782+
fileLines.clear();
1783+
}
1784+
}
1785+
1786+
if (loc.LineNumber < fileLines.size() &&
1787+
loc.LineNumber > 0)
1788+
{
1789+
std::string_view rawLine = fileLines[loc.LineNumber - 1];
1790+
std::size_t caretCol =
1791+
loc.ColumnNumber < rawLine.size() &&
1792+
loc.ColumnNumber > 0
1793+
? loc.ColumnNumber - 1
1794+
: std::size_t(-1);
1795+
std::string lineExpanded = expand_tabs(rawLine, 8);
1796+
1797+
// Compute width for the line number gutter
1798+
std::string gutter = std::format(" {} | ", loc.LineNumber);
1799+
out += gutter;
1800+
1801+
// Line text
1802+
out += lineExpanded;
1803+
out += "\n";
1804+
1805+
// Create gutter for the caret line
1806+
std::size_t const gutterWidth = gutter.size();
1807+
gutter = std::string(gutterWidth - 2, ' ') + "| ";
1808+
out += gutter;
1809+
1810+
if (caretCol != std::size_t(-1) && caretCol < rawLine.size())
1811+
{
1812+
std::size_t expandedCaretCol = 0;
1813+
for (std::size_t i = 0; i < caretCol; ++i)
1814+
{
1815+
if (rawLine[i] == '\t')
1816+
{
1817+
expandedCaretCol += 8;
1818+
}
1819+
else
1820+
{
1821+
++expandedCaretCol;
1822+
}
1823+
}
1824+
MRDOCS_ASSERT(expandedCaretCol <= lineExpanded.size());
1825+
1826+
out += std::string(expandedCaretCol, ' ');
1827+
out += "^";
1828+
1829+
out += std::string(lineExpanded.size() - expandedCaretCol - 1, '~');
1830+
out += "\n";
1831+
}
1832+
}
1833+
1834+
report::log(level, out);
16971835
}
16981836
}
16991837

17001838
void
1701-
JavadocFinalizer::
1702-
warnUndocumented()
1839+
JavadocFinalizer::warnUndocumented()
17031840
{
17041841
MRDOCS_CHECK_OR(corpus_.config->warnIfUndocumented);
1705-
for (auto& undocI : corpus_.undocumented_)
1842+
for (auto& undocI: corpus_.undocumented_)
17061843
{
17071844
if (Info const* I = corpus_.find(undocI.id))
17081845
{
17091846
MRDOCS_CHECK_OR(
17101847
!I->javadoc || I->Extraction == ExtractionMode::Regular);
17111848
}
17121849
bool const prefer_definition = undocI.kind == InfoKind::Record
1713-
|| undocI.kind == InfoKind::Enum;
1850+
|| undocI.kind == InfoKind::Enum;
17141851
this->warn(
17151852
*getPrimaryLocation(undocI.Loc, prefer_definition),
1716-
"{}: Symbol is undocumented", undocI.name);
1853+
"{}: Symbol is undocumented",
1854+
undocI.name);
17171855
}
17181856
corpus_.undocumented_.clear();
17191857
}

src/lib/Metadata/Finalizers/JavadocFinalizer.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ class JavadocFinalizer
7171
{
7272
return lhs.FullPath < rhs.FullPath;
7373
}
74-
return lhs.LineNumber > rhs.LineNumber;
74+
if (lhs.LineNumber != rhs.LineNumber)
75+
{
76+
return lhs.LineNumber > rhs.LineNumber;
77+
}
78+
return lhs.ColumnNumber > rhs.ColumnNumber;
7579
}
7680
};
7781

0 commit comments

Comments
 (0)