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+
16751732void
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
17001838void
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}
0 commit comments