Skip to content

Fix SourceText.GetChanges exception on merging certain changes#39258

Merged
RikkiGibson merged 4 commits intodotnet:masterfrom
ashmind:sourcetext-getchanges-merge-fix
Oct 20, 2019
Merged

Fix SourceText.GetChanges exception on merging certain changes#39258
RikkiGibson merged 4 commits intodotnet:masterfrom
ashmind:sourcetext-getchanges-merge-fix

Conversation

@ashmind
Copy link
Contributor

@ashmind ashmind commented Oct 13, 2019

Closes #22289
Closes #26305

See #22289 for full details on the bug.

Summary

After certain WithChanges calls, SourceText.GetChanges is no longer able to handle the SourceText state and throws ArgumentOutOfRangeException when called.

Notes

  • Three of the new tests are failing without my change, other tests added just in case.
  • I didn't follow "Hello World" pattern in tests, sorry. I found it a bit hard to remember the offsets when using actual words.
  • Not fully confident in the change even though the tests pass. The flow is quite complicated and I feel the current coverage might be insufficient.

@ashmind ashmind requested a review from a team as a code owner October 13, 2019 00:22
@dnfclas
Copy link

dnfclas commented Oct 13, 2019

CLA assistant check
All CLA requirements met. #Closed

AddRange(list, new TextChangeRange(oldChange.Span, oldChange.NewLength + newChange.NewLength - newChange.Span.Length));
oldDelta = oldDelta - oldChange.Span.Length + oldChange.NewLength - newChange.Span.Length;
oldIndex++;
newIndex++;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not certain why this is safe/ok. if newChange and oldChange have different contents, then what's going on here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually merges the list of TextChangeRange objects, not TextChange objects.
Those don't have contents, just old length and new length, so there is no difference in contents.

The ranges are mapped to the final text later, in GetTextChanges.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this will clarify things or not, but here's the way I understand this method:

  • We have an original text t1 and call t1.WithTextChanges(oldChanges). This produces a ChangedText t2, storing the oldChanges and t1 in fields.
  • When we call t2.WithTextChanges(newChanges) it produces a new ChangedText t3, storing the newChanges and t2.
  • When we call t3.GetTextChanges(t1), we have to rewrite the newChanges to make them occur w.r.t. t1 instead of w.r.t. t2:
    • we have to split individual changes apart when old and new changes overlap with each other
    • we merge adjacent changes together as we go to simplify them.

In the end we get a TextChangeRange[] which describes the difference between the original text and the final text.

@RikkiGibson RikkiGibson added Area-Compilers Community The pull request was submitted by a contributor who is not a Microsoft employee. labels Oct 13, 2019
@RikkiGibson RikkiGibson added this to the 16.4.P3 milestone Oct 13, 2019
@RikkiGibson RikkiGibson self-requested a review October 14, 2019 20:20
oldDelta = oldDelta - oldChange.Span.Length + oldChangeLeadingInsertion;
oldChange = new TextChangeRange(new TextSpan(oldChange.Span.Start, 0), oldChange.NewLength - oldChangeLeadingInsertion);
newChange = new TextChangeRange(new TextSpan(oldChange.Span.Start + oldDelta, newChange.Span.Length), newChange.NewLength);
var oldChangeLeadingDeletion = Math.Min(oldChange.Span.Length, oldChangeLeadingInsertion);
Copy link
Member

@RikkiGibson RikkiGibson Oct 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'm talking myself through why this makes sense.

In this line we are figuring out how many characters of the old change occur before the new change. We assume that if the old change deletes characters then it will delete from the left at most as many as it is inserting on the left. When the old change's deletion is longer than the old change's leading insertion, the rest of its deletion has to be shifted over to remove the characters after the new change's insertion. #Resolved

Assert.Equal("0aa4", change1.ToString());
Assert.Equal("0bba4", change2.ToString());
Assert.Equal(new[] { new TextChange(new TextSpan(1, 3), "bba") }, changes);
}
Copy link
Member

@RikkiGibson RikkiGibson Oct 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these all cases that would have thrown before your change?

BTW, this new form for the tests is significantly easier to reason about, thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only 3 of them would throw, but I no longer remember which ones. TestMergeChanges_Overlapping_NewInsideOld_AndOldHasDeletion for sure. We can revert the class itself on local and run tests to see.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up pulling down the changes after I commented to see what would break, and it was pretty much as you describe :)

I actually needed to pull down the changes and run the whole test class with the debugger with breakpoints on areas you changed to be sure I understood what the code was doing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your time! Yep it was a bit hard to follow for me as well, existing tests were extremely useful.

Copy link
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just had a few questions and suggestions

@RikkiGibson RikkiGibson self-assigned this Oct 14, 2019
@RikkiGibson RikkiGibson requested a review from a team October 14, 2019 22:49
ashmind and others added 2 commits October 15, 2019 14:39
Co-Authored-By: Rikki Gibson <rikkigibson@gmail.com>
Co-Authored-By: Rikki Gibson <rikkigibson@gmail.com>
@RikkiGibson
Copy link
Member

Could we please get a senior review on this? @dotnet/roslyn-compiler

@RikkiGibson
Copy link
Member

Ping @dotnet/roslyn-compiler

Assert.Equal("Cool ", changes[0].NewText);
}

[Fact]
Copy link
Contributor

@cston cston Oct 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fact [](start = 9, length = 4)

Please add a WorkItem attribute to each of these three tests and the last test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the link might have been auto-formatted, did you mean something like:

[WorkItem(22289, "https://github.com/dotnet/roslyn/issues/22289")]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, it's a bit unclear which 3 we are talking about -- I'll add to all the ones that were failing before this fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Assert.Equal("0aa4", change1.ToString());
Assert.Equal("0bba4", change2.ToString());
Assert.Equal(new[] { new TextChange(new TextSpan(1, 3), "bba") }, changes);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} [](start = 8, length = 1)

Do we also need to handle the case where the new deletion is larger than the old? It looks like the following fails with or without this PR:

var original = SourceText.From("01234");
var change1 = original.WithChanges(new TextChange(new TextSpan(1, 3), "aa"));
var change2 = change1.WithChanges(new TextChange(new TextSpan(1, 3), "bb"));

var changes = change2.GetTextChanges(original);
Assert.Equal("0aa4", change1.ToString());
Assert.Equal("0bb", change2.ToString());

We can log a bug for this case and handle it separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I'll take a look in a few moments -- if it's easy to fix then maybe just fix it here, if hard (needs a lot of review) then I can do another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems similar to what I am already doing in newChange.Span.Start > oldChange.Span.Start + oldDelta, but that means it's complicated enough for it's own review.

So I would prefer to do as a separate bug/PR just to make review easier (also maybe there is some reuse that can be done between those two cases).

@RikkiGibson
Copy link
Member

Thanks so much for your contribution @ashmind!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers Community The pull request was submitted by a contributor who is not a Microsoft employee.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ChangedText.GetTextChanges should not assert ArgumentOutOfRangeException in CompletionService.GetCompletionsAsync

5 participants