Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/solidlsp/language_servers/clangd_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions test/solidlsp/cpp/test_clangd_logging.py
Original file line number Diff line number Diff line change
@@ -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
Loading