From f018b4baf676db7f72735b38b6bac45d1b6030e4 Mon Sep 17 00:00:00 2001 From: Darin Morrison Date: Fri, 24 Apr 2026 13:05:09 -0600 Subject: [PATCH] Improve cpp log level classification --- .../clangd_language_server.py | 33 ++++++++++++++ test/solidlsp/cpp/test_clangd_logging.py | 43 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 test/solidlsp/cpp/test_clangd_logging.py diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index 0dc5ed9c1..7b7d6839c 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -34,6 +34,39 @@ class ClangdLanguageServer(SolidLanguageServer): (default: the bundled Serena version). """ + @staticmethod + def _determine_log_level(line: str) -> int: + """ + Classify a clangd stderr line using clangd's explicit level prefix. + + See `clang::clangd::Logger::indicator` for details: + https://clang.llvm.org/extra/doxygen/classclang_1_1clangd_1_1Logger.html + + Clangd emits each log record prefixed by a single indicator character + followed by a timestamp in square brackets, e.g. ``I[12:27:16.234]``. + + The indicators are ``D`` (Debug), ``I`` (Info), ``E`` (Error) and + ``F`` (Fatal). Continuation lines of multi-line records carry no + prefix and are treated as informational. + + Without this override, the base implementation scans the line for + the substrings ``error`` and ``exception``, which produces false + positives on clangd's reconstructed compile commands in some cases + (e.g. ``-DNO_EXCEPTIONS``, ``-fno-exceptions``). + """ + # classify by clangd's level indicator character + if len(line) >= 2 and line[1] == "[": + indicator = line[0] + if indicator in ("E", "F"): + return logging.ERROR + if indicator == "I": + return logging.INFO + if indicator == "D": + return logging.DEBUG + + # continuation line or non-prefixed output: default to INFO, do not keyword-scan + return logging.INFO + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): """ Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. diff --git a/test/solidlsp/cpp/test_clangd_logging.py b/test/solidlsp/cpp/test_clangd_logging.py new file mode 100644 index 000000000..8cef777ef --- /dev/null +++ b/test/solidlsp/cpp/test_clangd_logging.py @@ -0,0 +1,43 @@ +import logging + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.language_servers.clangd_language_server import ClangdLanguageServer + + +@pytest.mark.cpp +class TestClangdLogging: + @pytest.mark.parametrize( + "expected, line", + [ + # Parametrize names instead of codes for human-readable output in test logs. + (logging.getLevelName(logging.INFO), "I[12:00:00.000] clangd version 22.1.3"), + (logging.getLevelName(logging.INFO), "V[12:00:00.000] verbose detail"), + (logging.getLevelName(logging.DEBUG), "D[12:00:00.000] debug detail"), + (logging.getLevelName(logging.ERROR), "E[12:00:00.000] something bad"), + (logging.getLevelName(logging.ERROR), "F[12:00:00.000] fatal"), + ], + ) + def test_log_record_detection(self, expected: str, line: str) -> None: + """ + Verifies clangd's stderr log classifier maps each level prefix to the + expected Python logging level and avoids misclassifying compile-command + payloads as ERROR. + """ + assert ClangdLanguageServer._determine_log_level(line) == logging.getLevelNamesMapping()[expected] + + def test_log_record_info_payload_mentions_fno_exceptions(self) -> None: + line = """clang++ -fno-exceptions -c no_exceptions.cpp""" + # Basic classifier incorrectly detects line as an error log. + assert SolidLanguageServer._determine_log_level(line) == logging.ERROR + # Clangd classifier correctly detects line as an info log. + assert ClangdLanguageServer._determine_log_level(line) == logging.INFO + + def test_log_record_info_payload_without_prefix(self) -> None: + # Multi-line records: first line carries the I[...] prefix, subsequent + # lines do not. Treat lack of prefix as continuation. + assert ClangdLanguageServer._determine_log_level("[/home/user/project/build]") == logging.INFO + + def test_log_record_info_payload_empty(self) -> None: + assert ClangdLanguageServer._determine_log_level("") == logging.INFO