OmniSharp / OmniSharp/csharp-language-server-protocol

Bug in SemanticTokensDocument.GetSemanticTokenEdits

Open
#782 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
638
Forks
109
Avg merge
1m
Merged PRs (30d)
2

Description

This bit of code in GetSemanticTokenEdits (lines 162 to 178) throws an ArgumentOutOfRangeException in the call to ImmutableArray.Create when you delete code in a document (but maybe in other cases too):

while (startIndex < dataLength && startIndex < prevDataLength && prevData[startIndex] ==
    Data[startIndex])
{
    startIndex++;
}

if (startIndex < dataLength && startIndex < prevDataLength)
{
    // Find end index
    var endIndex = 0;
    while (endIndex < dataLength && endIndex < prevDataLength &&
           prevData[prevDataLength - 1 - endIndex] == Data[dataLength - 1 - endIndex])
    {
        endIndex++;
    }

    var newData = ImmutableArray.Create(Data, startIndex, dataLength - endIndex - startIndex);

It looks like the intent here is to find both the longest common prefix and the longest common suffix between prevData and Data. Then it will return a SematicTokensEdit with a DeleteCount = prevDataLength - endIndex - startIndex to delete the tokens in the middle, and Data = newData to insert the replacement tokens.

Logically:

prevData = [... common prefix ...] [... stuff to delete ...] [... common suffix ...]
    Data = [... common prefix ...] [... stuff to insert ...] [... common suffix ...]

Where "stuff to insert" can have a different length than "stuff to delete", including a length of zero.

So it's trying to diff them like this:

prevData = [a a a b b b c c c]
    Data = [a a a       c c c] // extra spaces added for alignment, Data.Length = 6
                  ^ ^ ^ snip!

startIndex = 3
endIndex = 3

newData = Create(Data, 3, 6 - 3 - 3) // length = 0, this is fine
DeleteCount = 9 - 3 - 3 // = 3, all good

But this isn't reliable. The common prefix can overlap with the common suffix, just by chance, as in this example:

prevData = [a a a a b a a a a]
    Data = [a a a       a a a]
                  ^ ^ ^ snip!

startIndex = 4
endIndex = 4

newData = Create(Data, 3, 6 - 4 - 4) // length = -2, oops!!!!
DeleteCount = 9 - 3 - 3 // = 3

The search for endIndex needs to be limited to not eat into the common prefix. In this example, we should get startIndex = 4, endIndex = 2, DeleteCount = 3, newData.Length = 0.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading SemanticTokensDocument.GetSemanticTokenEdits around lines 162-178 and trace the prefix and suffix scans for the deletion example. Reproduce the overlapping-prefix case, then verify the resulting SemanticTokensEdit has valid replacement and delete lengths without an ArgumentOutOfRangeException.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
devtools, tooling
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.