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
12 changes: 12 additions & 0 deletions lsp_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
CancelRequest,
ClientCapabilities,
ClientInfo,
CodeDescription,
CompletionRequest,
ContentChange,
DefinitionRequest,
Diagnostic,
DiagnosticRelatedInformation,
DiagnosticSeverity,
DiagnosticTag,
ErrorCodes,
ExitNotification,
GeneralClientCapabilities,
Expand All @@ -16,6 +21,7 @@
InitializeRequest,
InitializedNotification,
LanguageKind,
Location,
LSPErrorCodes,
Message,
NotificationMessage,
Expand Down Expand Up @@ -59,9 +65,14 @@
"CancelRequest",
"ClientCapabilities",
"ClientInfo",
"CodeDescription",
"CompletionRequest",
"ContentChange",
"DefinitionRequest",
"Diagnostic",
"DiagnosticRelatedInformation",
"DiagnosticSeverity",
"DiagnosticTag",
"ErrorCodes",
"ExitNotification",
"GeneralClientCapabilities",
Expand All @@ -72,6 +83,7 @@
"LSPClient",
"LSPErrorCodes",
"LanguageKind",
"Location",
"Message",
"NotificationMessage",
"PositionEncodingKind",
Expand Down
79 changes: 79 additions & 0 deletions lsp_client/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,85 @@ class Range(BaseModel):
end: Position


# Location
# See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#location # noqa: E501


class Location(BaseModel):
"""A range inside a text document, identified by its URI."""

uri: str
range: Range


# Diagnostic
# See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic # noqa: E501


class DiagnosticSeverity(IntEnum):
"""How severe a diagnostic is."""

Error = 1
Warning = 2
Information = 3
Hint = 4


class DiagnosticTag(IntEnum):
"""Additional metadata about a diagnostic. @since 3.15.0"""

#: Unused or unnecessary code — clients may render this faded out.
Unnecessary = 1
#: Deprecated or obsolete code — clients may render this struck through.
Deprecated = 2


class CodeDescription(BaseModel):
"""A structure describing a diagnostic's error code. @since 3.16.0"""

#: A URI to open with more information about the diagnostic error.
href: str


class DiagnosticRelatedInformation(BaseModel):
"""A related message and source location for a diagnostic.

Used e.g. when symbol names within a scope collide, to point at every
colliding definition.
"""

location: Location
message: str


class Diagnostic(BaseModel):
"""A diagnostic, such as a compiler error or warning.

Diagnostic objects are only valid in the scope of a resource.
"""

#: The range at which the message applies.
range: Range
#: The diagnostic's severity. If omitted the client interprets it.
severity: DiagnosticSeverity | None = None
#: The diagnostic's code, which may appear in the user interface.
code: int | str | None = None
#: An optional structure describing the error code. @since 3.16.0
codeDescription: CodeDescription | None = None
#: A human-readable description of the diagnostic's source, e.g.
#: ``"typescript"`` or ``"super lint"``.
source: str | None = None
#: The diagnostic's message.
message: str
#: Additional metadata about the diagnostic. @since 3.15.0
tags: list[DiagnosticTag] | None = None
#: An array of related diagnostic information.
relatedInformation: list[DiagnosticRelatedInformation] | None = None
#: A data entry preserved between a ``textDocument/publishDiagnostics``
#: notification and a ``textDocument/codeAction`` request. @since 3.16.0
data: Any | None = None


class ContentChange(BaseModel):
text: str
range: Optional[Range] = None
Expand Down
79 changes: 79 additions & 0 deletions tests/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@
CancelRequest,
ClientCapabilities,
ClientInfo,
CodeDescription,
CompletionRequest,
ContentChange,
DefinitionRequest,
Diagnostic,
DiagnosticRelatedInformation,
DiagnosticSeverity,
DiagnosticTag,
ErrorCodes,
ExitNotification,
GeneralClientCapabilities,
Expand All @@ -18,6 +23,7 @@
InitializeRequest,
InitializedNotification,
LanguageKind,
Location,
LSPErrorCodes,
Message,
NotificationMessage,
Expand Down Expand Up @@ -456,6 +462,79 @@ def test_server_capabilities_position_encoding_optional():
assert ServerCapabilities().model_dump(exclude_none=True) == {}


def _range() -> Range:
return Range(start=Position(line=1, character=0), end=Position(line=1, character=8))


def test_diagnostic_severity_values():
assert DiagnosticSeverity.Error == 1
assert DiagnosticSeverity.Warning == 2
assert DiagnosticSeverity.Information == 3
assert DiagnosticSeverity.Hint == 4


def test_diagnostic_tag_values():
assert DiagnosticTag.Unnecessary == 1
assert DiagnosticTag.Deprecated == 2


def test_location_structure():
loc = Location(uri="file:///tmp/a.py", range=_range())
assert loc.model_dump() == {
"uri": "file:///tmp/a.py",
"range": {
"start": {"line": 1, "character": 0},
"end": {"line": 1, "character": 8},
},
}


def test_diagnostic_minimal():
diag = Diagnostic(range=_range(), message="undefined name 'x'")
data = diag.model_dump(exclude_none=True)
# Only range and message are required; optional fields are omitted.
assert set(data) == {"range", "message"}
assert data["message"] == "undefined name 'x'"


def test_diagnostic_full():
diag = Diagnostic(
range=_range(),
severity=DiagnosticSeverity.Warning,
code="F821",
codeDescription=CodeDescription(href="https://example.com/F821"),
source="flake8",
message="undefined name 'x'",
tags=[DiagnosticTag.Unnecessary],
relatedInformation=[
DiagnosticRelatedInformation(
location=Location(uri="file:///tmp/b.py", range=_range()),
message="first defined here",
)
],
data={"fixable": True},
)
data = diag.model_dump(exclude_none=True)
assert data["severity"] == 2
assert data["code"] == "F821"
assert data["codeDescription"] == {"href": "https://example.com/F821"}
assert data["source"] == "flake8"
assert data["tags"] == [1]
assert data["relatedInformation"][0]["location"]["uri"] == "file:///tmp/b.py"
assert data["relatedInformation"][0]["message"] == "first defined here"
assert data["data"] == {"fixable": True}


def test_diagnostic_integer_code():
diag = Diagnostic(range=_range(), message="boom", code=42)
assert diag.model_dump(exclude_none=True)["code"] == 42


def test_diagnostic_rejects_invalid_severity():
with pytest.raises(ValidationError):
Diagnostic(range=_range(), message="boom", severity=5)


def test_error_codes_values():
# ErrorCodes carries only the JSON-RPC defined codes and reserved markers.
assert ErrorCodes.ParseError == -32700
Expand Down
Loading