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
26 changes: 26 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,20 @@ across live-text eviction and are cleared by `didClose`, so an evicted newer
version cannot be replaced by a stale change. Other providers return empty
arrays or null when the database cannot answer safely instead of inventing
language-server analysis.
Before URI/path resolution, live-document access, or query-snapshot refresh,
one shared coordinate validator checks every supported position/range-bearing
method. `definition`, `declaration`, `references`, `hover`, `completion`, and
`documentHighlight` require one position; `inlayHint` requires an ordered range;
and each optional range supplied by `didChange` is validated before live text
can change. Lines and characters must be JSON integers in the LSP `uinteger`
domain (0 through 2,147,483,647), and range starts must not follow range ends.
Malformed, missing, negative, or overflowing request coordinates return
`-32602` (`Invalid params`); invalid notification coordinates are ignored
without mutating document state. Structural validation does not turn a valid
document no-match into a protocol error: provider lookup retains UTF-16
coordinates and its existing end-of-line/EOF policy, while missing, unreadable,
or out-of-range documents still produce the provider's conservative empty/null
result.
`LspServer` uses one lock-protected lifecycle state machine across ordinary
dispatch, the cancellation fast path, and queue-overload responses. Its phases
are before-initialize, initializing, running, shutdown, and exited. Only the
Expand Down Expand Up @@ -3720,6 +3734,18 @@ indexed symbol に fallback する。numeric document-version tombstone は live
後も上限付きで保持し、`didClose` で消去するため、evict 済みの新しい version を stale change が
置き換えることはない。それ以外の provider は database が安全に答えられない場合、
language-server analysis を作り上げず、空配列または null を返す。
URI / path 解決、live document へのアクセス、query snapshot の refresh より前に、1つの共通
coordinate validator ですべての対応済み position / range method を検証する。
`definition`、`declaration`、`references`、`hover`、`completion`、
`documentHighlight` は1つの position を必須とし、`inlayHint` は順序どおりの range を必須とする。
また、`didChange` に任意の range が指定された場合は、live text を変更する前に各 range を検証する。
line と character は LSP の `uinteger` 範囲(0〜2,147,483,647)の JSON integer でなければならず、
range の start が end より後であってはならない。request の coordinate が malformed、欠落、負数、
overflow の場合は `-32602`(`Invalid params`)を返す。notification の coordinate が不正な場合は
document state を変更せず無視する。構造検証によって、正当な document の no-match を protocol
error に変えてはならない。provider lookup は UTF-16 coordinate と既存の行末 / EOF policy を維持し、
document が欠落、読み取り不能、または範囲外の場合は、引き続き各 provider の保守的な空 / null
result を返す。
`LspServer` は通常 dispatch、cancellation fast path、queue-overload response の全経路で、
1 つの lock 保護された lifecycle state machine を使う。phase は before-initialize、
initializing、running、shutdown、exited である。最初の `initialize` request だけが初期化へ
Expand Down
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
keeps explicit constructors split across partial declarations in one fixture. Preserve the separate `definition` / `goto --kind function` assertions and the grouped `--kind class --group-partials` assertion so callable constructor results cannot merge back into logical type-family results.
- `LspServerTests.cs`
LSP JSON-RPC behavior and indexed editor semantics. Reference parity coverage must compare LSP locations with the matching CLI candidate bundle, including overload identity and both `includeDeclaration` states. Keep document/workspace identifier ranges and explicit-versus-inferred inlay hints in one source-semantics fixture so persisted-column anchoring, source confirmation, and hint suppression cannot drift independently.
Coordinate-validation coverage keeps every supported position provider in one negative/malformed/overflow matrix, with missing-document precedence and missing/reversed inlay ranges in the same read-only fixture. Keep UTF-16 and EOF success controls together, but retain invalid ranged-`didChange` as a separate mutable-state fixture so a rejected notification cannot replace accepted live text (#4869).
C# semantic-token coverage decodes LSP delta coordinates and pairs the same keyword/modifier, namespace/type, method/property/event/enum-member, parameter/variable/field, generic/record-struct, explicit-interface-member, attribute, using-declaration, accessor-local, numeric member-access, escaped/contextual-identifier, interpolation, comma-separated declaration, and declaration-kind fixture with `QueryCommandRunnerTests.RunExcerpt_JsonCSharpSemanticKindsMatchLspClassifier_Issue4852`. Keep both surfaces on the shared source classifier, retain ordering/non-overlap assertions, preserve the narrow excerpt check whose parameter/type-parameter declarations are outside the returned range, and retain `RunExcerpt_JsonCSharpSemanticTokensUseVisibleRangeBudget_Issue4852` so earlier tokens cannot empty a late excerpt while indexed-source reconstruction remains line/character bounded. `HandleMessage_SemanticTokens_FallsBackToIndexedCSharpSymbolsWhenSourceReadFails_Issue4852` keeps indexed declaration tokens available when bounded source loading fails.
Constructor-navigation coverage keeps explicit zero- and one-argument constructors in separate partial files, including a caller beside the zero-argument declaration, plus an implicit partial constructor, a positional record, and an ordinary partial-type reference in one fixture. The adversarial fixtures add static constructors, finalizers, primary-plus-secondary constructors, coexisting generic arities, same-arity overloads, value-type default construction beside explicit constructors, enums, and delegates. Assert both definition and declaration requests so source-site identity, invocation-arity filtering, stable implicit representatives, constructor-only ambiguity, preserved non-class construction, and intentional type-family expansion remain separate.
Document-symbol hierarchy coverage keeps positional and body record members in one same-line fixture, asserts the complete root-to-member shape and deterministic sibling order, and retains the separate same-range top-level controls so order-independent parent resolution cannot over-nest unrelated symbols. Keep a same-line duplicate-container fixture with distinct positional members so selection-column disambiguation cannot move an earlier member beneath a later same-named container.
Expand Down Expand Up @@ -1441,6 +1442,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
は partial declaration の別ファイルに分かれた明示 constructor を 1 つの fixture にまとめます。callable constructor result が logical type-family result に再び混在しないよう、`definition` / `goto --kind function` の検証と、`--kind class --group-partials` による grouped type の検証を分離したまま維持してください。
- `LspServerTests.cs`
LSP の JSON-RPC 挙動と indexed editor semantics のテスト。reference parity coverage では、overload identity と `includeDeclaration` の両状態を含め、LSP location と対応する CLI candidate bundle を比較してください。document/workspace の identifier range と explicit/inferred inlay hint は 1 つの source-semantics fixture にまとめ、保存済み column の anchoring、source 上の確認、hint 抑制が別々に drift しないようにします。
coordinate validation coverage は、対応済みの全 position provider を1つの負数 / malformed / overflow matrix にまとめ、missing document より検証を優先することと、inlay range の欠落 / 逆順も同じ read-only fixture で検証します。UTF-16 と EOF の成功 control は一緒に維持し、不正な range 付き `didChange` は mutable state の独立 fixture として残して、拒否された notification が受理済み live text を置き換えられないことを検証してください(#4869)。
C# semantic-token coverage は LSP の delta 座標を decode し、同じ keyword/modifier、namespace/type、method/property/event/enum-member、parameter/variable/field、generic/record struct、explicit interface member、attribute、using declaration、accessor local、numeric member access、escaped/contextual identifier、interpolation、comma-separated declaration、declaration kind の fixture を `QueryCommandRunnerTests.RunExcerpt_JsonCSharpSemanticKindsMatchLspClassifier_Issue4852` と対にします。両 surface が shared source classifier を使い続け、順序と非 overlap の assertion、parameter / type-parameter の declaration が返却範囲外にある narrow excerpt の検証、および indexed-source reconstruction を line / character 上限内に保ちながら手前の token によって file 後半の excerpt が空にならないことを保証する `RunExcerpt_JsonCSharpSemanticTokensUseVisibleRangeBudget_Issue4852` を維持してください。`HandleMessage_SemanticTokens_FallsBackToIndexedCSharpSymbolsWhenSourceReadFails_Issue4852` は bounded source loading が失敗しても indexed declaration token を利用できる状態を維持します。
constructor-navigation coverage は、0 引数と 1 引数の明示 constructor を別々の partial file に置き、0 引数 declaration と同じ file の caller、暗黙 partial constructor、positional record、通常の partial-type reference と同じ fixture にまとめます。adversarial fixture には static constructor、finalizer、primary / secondary constructor の併存、同名 type の generic arity 違い、同一 arity overload、明示 constructor と併存する value type の default construction、enum、delegate も含めます。source-site identity、invocation-arity filtering、暗黙 constructor の安定した代表 declaration、constructor だけの ambiguity、class 以外の construction の維持、意図的な type-family expansion が混在しないよう、definition と declaration の両 request を assert してください。
document-symbol hierarchy coverage は positional member と body member を同一行の record fixture にまとめ、root から member までの完全な形状と決定的な sibling 順序を assert します。順序非依存の親解決が無関係な symbol を過剰に nest しないよう、same-range top-level の control は独立したまま維持してください。selection column による曖昧性解消で前の member が行内で後にある同名 container の配下へ移動しないよう、異なる positional member を持つ同一行の duplicate-container fixture も維持してください。
Expand Down
22 changes: 22 additions & 0 deletions changelog.d/unreleased/4869.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
category: fixed
issues:
- 4869
affected:
- src/CodeIndex/Lsp/LspServer.PositionValidation.cs
- src/CodeIndex/Lsp/LspServer.cs
- src/CodeIndex/Lsp/LspServer.Protocol.cs
- src/CodeIndex/Lsp/LspServer.PositionResolution.cs
- src/CodeIndex/Lsp/LspServer.NavigationFeatures.cs
- tests/CodeIndex.Tests/LspServerTests.cs
- DEVELOPER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **LSP requests now reject invalid positions and ranges as InvalidParams (#4869)** — position-based providers and inlay hints now validate non-negative integer coordinates, overflow, and range ordering before document or index access, while invalid ranged document changes cannot mutate live text and valid UTF-16/EOF behavior remains unchanged.

## 日本語

- **LSP request が不正な position / range を InvalidParams として拒否するようになりました (#4869)** — position-based provider と inlay hint は document / index へアクセスする前に、非負整数の coordinate、overflow、range 順序を検証します。不正な range 付き document change は live text を変更できず、正当な UTF-16 / EOF 挙動は維持されます。
56 changes: 12 additions & 44 deletions src/CodeIndex/Lsp/LspServer.NavigationFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,19 +182,19 @@ private List<SemanticToken> BuildIndexedSemanticTokens(IndexedDocumentContext do

private JsonArray InlayHint(JsonElement root)
{
var requestedRange = ReadRequiredLspRange(root, "params", "range");
if (!TryResolveIndexedDocument(root, out var document))
return [];

var array = new JsonArray();
var lineCache = new Dictionary<int, string?>();
var hasRange = TryReadInlayHintRange(root, out var startLine, out _, out var endLine, out _);
foreach (var symbol in GetDocumentSymbols(
document.IndexedPath,
MaxDocumentSymbols,
hasRange ? startLine + 1 : null,
hasRange ? endLine + 1 : null)
ToOneBasedLspLine(requestedRange.Start.Line),
ToOneBasedLspLine(requestedRange.End.Line))
.Where(symbol => !string.IsNullOrWhiteSpace(symbol.ReturnType))
.Where(symbol => IsInlayHintInRequestedRange(root, document, symbol, lineCache))
.Where(symbol => IsInlayHintInRequestedRange(requestedRange, document, symbol, lineCache))
.Where(symbol => !HasExplicitTypeBeforeSymbol(document, symbol, lineCache))
.Take(MaxInlayHintItems))
{
Expand All @@ -204,17 +204,20 @@ private JsonArray InlayHint(JsonElement root)
}

private bool IsInlayHintInRequestedRange(
JsonElement root,
LspRange requestedRange,
IndexedDocumentContext document,
SymbolResult symbol,
Dictionary<int, string?> lineCache)
{
if (!TryReadInlayHintRange(root, out var startLine, out var startCharacter, out var endLine, out var endCharacter))
return true;

var line = Math.Max(symbol.Line, 1) - 1;
var character = FindSymbolStartCharacter(document.ResolvedPath, symbol, lineCache) + symbol.Name.Length;
return IsPositionInRange(line, character, startLine, startCharacter, endLine, endCharacter);
return IsPositionInRange(
line,
character,
requestedRange.Start.Line,
requestedRange.Start.Character,
requestedRange.End.Line,
requestedRange.End.Character);
}

private bool HasExplicitTypeBeforeSymbol(
Expand Down Expand Up @@ -242,38 +245,6 @@ private bool HasExplicitTypeBeforeSymbol(
return typeEnd <= symbolStart && sourceLine.AsSpan(typeEnd, symbolStart - typeEnd).Trim().IsEmpty;
}

private static bool TryReadLspPosition(JsonElement range, string propertyName, out int line, out int character)
{
line = 0;
character = 0;
return range.ValueKind == JsonValueKind.Object &&
range.TryGetProperty(propertyName, out var position) &&
position.ValueKind == JsonValueKind.Object &&
position.TryGetProperty("line", out var lineElement) &&
lineElement.TryGetInt32(out line) &&
line >= 0 &&
position.TryGetProperty("character", out var characterElement) &&
characterElement.TryGetInt32(out character) &&
character >= 0;
}

private static bool TryReadInlayHintRange(
JsonElement root,
out int startLine,
out int startCharacter,
out int endLine,
out int endCharacter)
{
startLine = 0;
startCharacter = 0;
endLine = 0;
endCharacter = 0;
return root.TryGetProperty("params", out var paramsElement) &&
paramsElement.TryGetProperty("range", out var range) &&
TryReadLspPosition(range, "start", out startLine, out startCharacter) &&
TryReadLspPosition(range, "end", out endLine, out endCharacter);
}

private static bool IsPositionInRange(
int line,
int character,
Expand All @@ -284,9 +255,6 @@ private static bool IsPositionInRange(
=> ComparePosition(line, character, startLine, startCharacter) >= 0 &&
ComparePosition(line, character, endLine, endCharacter) < 0;

private static int ComparePosition(int leftLine, int leftCharacter, int rightLine, int rightCharacter)
=> leftLine != rightLine ? leftLine.CompareTo(rightLine) : leftCharacter.CompareTo(rightCharacter);

private static JsonObject CompletionList(JsonArray items) => new()
{
["isIncomplete"] = false,
Expand Down
10 changes: 3 additions & 7 deletions src/CodeIndex/Lsp/LspServer.PositionResolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,9 @@ private bool TryExtractPositionToken(JsonElement root, out PositionTokenContext
context = default;
failureReason = null;
var path = GetDocumentPath(root);
var line = GetInt32(root, "params", "position", "line");
var character = GetInt32(root, "params", "position", "character");
if (line < 0 || character < 0)
{
failureReason = FailureInvalidPosition;
return false;
}
var position = ReadRequiredLspPosition(root, "params", "position");
var line = position.Line;
var character = position.Character;

if (!TryResolveDocumentPath(path, out var resolvedPath, out var projectRelativePath, out var workspaceRoot, out failureReason))
return false;
Expand Down
Loading
Loading