Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
([PR #19724](https://github.com/dotnet/fsharp/pull/19724))
* Emit debug points at a stack-empty position ([PR #19877](https://github.com/dotnet/fsharp/pull/19877))
* Fix spurious XmlDoc warnings (unknown parameter / no documentation for parameter) under `--warnon:3390` when a get/set property documents the full parameter set across both accessors. ([Issue #13684](https://github.com/dotnet/fsharp/issues/13684), [PR #19884](https://github.com/dotnet/fsharp/pull/19884))
* Fix dot-completion after indexed expressions (`a.[0].Data.`, `a[0].Data.`, `[1;2].Length.`) returning unrelated global completions instead of expression-typings members. ([Issue #4966](https://github.com/dotnet/fsharp/issues/4966), [PR #19934](https://github.com/dotnet/fsharp/pull/19934))

### Added

Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/Service/QuickParse.fs
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ module QuickParse =
AtStartOfIdentifier(pos + 1, "" :: current, throwAwayNext, Some pos)
elif not (pos > 0 && (IsIdentifierPartCharacter(pos - 1) || IsWhitespace(pos - 1))) then
// it's not dots as part.of.a.long.ident, it's e.g. the range operator (..), or some other multi-char operator ending in dot
if lineStr[pos - 1] = ')' then
if lineStr[pos - 1] = ')' || lineStr[pos - 1] = ']' then
// one very problematic case is someCall(args).Name
// without special logic, we will decide that ). is an operator and parse Name as the plid
// but in fact this is an expression tail, and we don't want a plid, rather we need to use expression typings at that location
Expand Down
57 changes: 57 additions & 0 deletions tests/FSharp.Compiler.Service.Tests/QuickParseTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,60 @@ let ``QuickParse does not treat question mark as identifier in other contexts``(
| None ->
// Or we might get None, which is also acceptable
()

// --- Issue #4966 regression tests -----------------------------------------
// QuickParse.GetPartialLongNameEx had a special case for ')' before a trigger
// dot (so 'f(x).Name.' did not feed 'Name' back as a qualifying ident).
// The equivalent case for ']' was missing, so 'a.[0].Data.' wrongly returned
// QualifyingIdents = ["Data"] and the completion engine then resolved 'Data'
// as a module/type prefix. After the fix, ']' must behave like ')': the
// returned PartialLongName must be empty so the completer falls back to
// expression-typings.

[<Theory>]
[<InlineData("a.[0].Data.")>] // legacy explicit-dot indexer
[<InlineData("a[0].Data.")>] // modern indexer syntax
[<InlineData("[1;2].Length.")>] // list literal indexer/tail
[<InlineData("xs.[0].[1].Value.")>] // nested explicit-dot indexer
[<InlineData("xs[0][1].Value.")>] // nested modern indexer
let ``GetPartialLongNameEx returns empty for dot after closing bracket`` (lineStr: string) =
// cursor is right after the trailing '.', so index is the position of that dot
let index = lineStr.Length - 1
Assert.Equal('.', lineStr[index])

let pln = QuickParse.GetPartialLongNameEx(lineStr, index)

Assert.Equal<string list>([], pln.QualifyingIdents)
Assert.Equal("", pln.PartialIdent)

// Regression guard: the pre-existing ')' special case must keep working
// exactly as it does on `main`. These cases pass today; they must keep passing
// after the Sprint-02 fix.
[<Theory>]
[<InlineData("f(x).Name.")>]
[<InlineData("(1).ToString.")>]
[<InlineData("(f x).Length.")>]
let ``GetPartialLongNameEx returns empty for dot after closing paren`` (lineStr: string) =
let index = lineStr.Length - 1
Assert.Equal('.', lineStr[index])

let pln = QuickParse.GetPartialLongNameEx(lineStr, index)

Assert.Equal<string list>([], pln.QualifyingIdents)
Assert.Equal("", pln.PartialIdent)

// Negative cases: simple long-ident with no bracket/paren tail must STILL be
// returned as a qualifying ident. The fix must not over-throw-away.
[<Theory>]
[<InlineData("Foo.", "Foo")>]
[<InlineData("System.IO.", "IO")>]
[<InlineData("xs.Length.", "Length")>]
let ``GetPartialLongNameEx preserves plain long identifiers`` (lineStr: string, lastQualifier: string) =
let index = lineStr.Length - 1
Assert.Equal('.', lineStr[index])

let pln = QuickParse.GetPartialLongNameEx(lineStr, index)

Assert.NotEmpty pln.QualifyingIdents
Assert.Equal(lastQualifier, List.last pln.QualifyingIdents)
Assert.Equal("", pln.PartialIdent)
Loading