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
4 changes: 4 additions & 0 deletions tsc/internal/checker/exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ func IsTupleType(t *Type) bool {
return isTupleType(t)
}

func GetEndElementCount(t *TupleType, flags ElementFlags) int {
return getEndElementCount(t, flags)
}

func IsTupleTypeTarget(t *Type) bool {
return isTupleType(t) && t.Target() == t
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/TypeScript/tsc/internal/fourslash"
"github.com/microsoft/TypeScript/tsc/internal/ls/lsutil"
"github.com/microsoft/TypeScript/tsc/internal/testutil"
)

func TestInlayHintsOptionalTrailingTupleAfterSpread(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `declare function optional<T extends unknown[]>(...args: [...head: T, first?: number, second?: string]): void;
function forward<T extends unknown[]>(x: T) {
optional<T>(...x, 1);
optional<T>(...x, 1, "two");
}`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyNoErrors(t)
f.VerifyBaselineInlayHints(t, nil /*span*/, &lsutil.UserPreferences{InlayHints: lsutil.InlayHintsPreferences{IncludeInlayParameterNameHints: lsutil.IncludeInlayParameterNameHintsAll}})
}

func TestInlayHintsSingleRestWithSpreadAndSuffix(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `declare function f(...args: [...head: string[], tail: string]): void;
declare const xs: string[];
f(...xs, "middle", "end");`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyNoErrors(t)
f.VerifyBaselineInlayHints(t, nil /*span*/, &lsutil.UserPreferences{InlayHints: lsutil.InlayHintsPreferences{IncludeInlayParameterNameHints: lsutil.IncludeInlayParameterNameHintsAll}})
}

func TestInlayHintsOptionalTrailingAfterTupleSpread(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `declare function optional<T extends unknown[]>(...args: [...head: T, first?: number, second?: string]): void;
function forward<T extends unknown[]>(x: [...T]) {
optional<T>(...x, 1);
}`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyNoErrors(t)
f.VerifyBaselineInlayHints(t, nil /*span*/, &lsutil.UserPreferences{InlayHints: lsutil.InlayHintsPreferences{IncludeInlayParameterNameHints: lsutil.IncludeInlayParameterNameHintsAll}})
}

func TestInlayHintsOptionalParameterAfterTupleSpread(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `declare function g(a: number, b?: number, ...rest: string[]): void;
declare const x: [number, number?];
g(...x, "end");`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyNoErrors(t)
f.VerifyBaselineInlayHints(t, nil /*span*/, &lsutil.UserPreferences{InlayHints: lsutil.InlayHintsPreferences{IncludeInlayParameterNameHints: lsutil.IncludeInlayParameterNameHintsAll}})
}
32 changes: 32 additions & 0 deletions tsc/internal/fourslash/tests/inlayHintsTupleRestTrailing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/TypeScript/tsc/internal/fourslash"
"github.com/microsoft/TypeScript/tsc/internal/ls/lsutil"
"github.com/microsoft/TypeScript/tsc/internal/testutil"
)

func TestInlayHintsTupleRestTrailing(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `declare function fn(prefix: boolean, ...args: [first: number, ...middle: string[], last: string]): void;
fn(true, 1, "a", "b", "end");
fn(true, 1, "end");
declare function onlyTrailing(...args: [...head: number[], tail: string]): void;
onlyTrailing(1, 2, "end");
onlyTrailing("end");
declare function noTrailing(...args: [first: number, ...rest: string[]]): void;
noTrailing(1, "a", "b");
declare function multi<T extends string[], U extends string[]>(...args: [first: number, ...left: T, ...right: U, penultimate: string, last: boolean]): void;
function combined<T extends string[], U extends string[]>(middle: [...T, ...U]) {
multi<T, U>(1, ...middle, "end", true);
}
declare function skip({ x }: { x: number }, next: number): void;
skip({ x: 1 }, 2);`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyNoErrors(t)
f.VerifyBaselineInlayHints(t, nil /*span*/, &lsutil.UserPreferences{InlayHints: lsutil.InlayHintsPreferences{IncludeInlayParameterNameHints: lsutil.IncludeInlayParameterNameHintsAll}})
}
96 changes: 67 additions & 29 deletions tsc/internal/ls/inlay_hints.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package ls

import (
"context"
"slices"
"strings"
"unicode"

Expand Down Expand Up @@ -157,37 +156,48 @@ func (s *inlayHintState) visitCallOrNewExpression(expr *ast.CallOrNewExpression)
return
}

// A spread makes the effective argument count unknown, but explicit arguments after the last
// spread still have a known offset from the end.
argumentCount := len(args)
lastSpreadIndex := -1
for i, arg := range args {
if ast.IsSpreadElement(ast.SkipParentheses(arg)) {
argumentCount = -1
lastSpreadIndex = i
}
}
signatureParamPos := 0
for _, originalArg := range args {
hasUncertainTupleSpread := false
for i, originalArg := range args {
arg := ast.SkipParentheses(originalArg)
if shouldShowLiteralParameterNameHintsOnly(s.preferences) && !isHintableLiteral(arg) {
signatureParamPos++
continue
}

spreadArgs := 0
skipSpreadHint := false
if ast.IsSpreadElement(arg) {
spreadType := s.checker.GetTypeAtLocation(arg.Expression())
if spreadType.IsTupleType() {
elementFlags := spreadType.Target().AsTupleType().ElementFlags()
fixedLength := spreadType.Target().AsTupleType().FixedLength()
if fixedLength == 0 {
continue
}
firstOptionalIndex := slices.IndexFunc(elementFlags, func(f checker.ElementFlags) bool {
return f&checker.ElementFlagsRequired == 0
})
requiredArgs := core.IfElse(firstOptionalIndex < 0, fixedLength, firstOptionalIndex)
if requiredArgs > 0 {
spreadArgs = requiredArgs
}
tupleType := spreadType.Target().AsTupleType()
spreadArgs = getRequiredTupleElementCount(tupleType)
// Optional or variable tuple elements make subsequent positional hints ambiguous.
hasUncertainTupleSpread = hasUncertainTupleSpread || spreadArgs < len(tupleType.ElementInfos())
skipSpreadHint = tupleType.FixedLength() == 0
}
}
if shouldShowLiteralParameterNameHintsOnly(s.preferences) && !isHintableLiteral(arg) {
signatureParamPos++
continue
}
if skipSpreadHint {
continue
}

identifierInfo := s.getParameterIdentifierInfoAtPosition(signature, signatureParamPos)
offsetFromEnd := -1
if lastSpreadIndex >= 0 && i > lastSpreadIndex {
offsetFromEnd = len(args) - i
}
identifierInfo := s.getParameterIdentifierInfoAtPosition(signature, signatureParamPos, argumentCount, offsetFromEnd, hasUncertainTupleSpread && !ast.IsSpreadElement(arg))
signatureParamPos = signatureParamPos + core.IfElse(spreadArgs > 0, spreadArgs, 1)
if identifierInfo == nil {
return
continue
}

parameter := identifierInfo.parameter
Expand Down Expand Up @@ -831,10 +841,13 @@ type parameterInfo struct {
isRestParameter bool
}

func (s *inlayHintState) getParameterIdentifierInfoAtPosition(signature *checker.Signature, pos int) *parameterInfo {
func (s *inlayHintState) getParameterIdentifierInfoAtPosition(signature *checker.Signature, pos int, argumentCount int, offsetFromEnd int, uncertainTupleSpread bool) *parameterInfo {
parameters := signature.Parameters()
paramCount := len(parameters) - core.IfElse(signature.HasRestParameter(), 1, 0)
if pos < paramCount {
if uncertainTupleSpread {
return nil
}
param := parameters[pos]
paramId := getParameterDeclarationIdentifier(param)
if paramId == nil {
Expand All @@ -859,14 +872,36 @@ func (s *inlayHintState) getParameterIdentifierInfoAtPosition(signature *checker

restType := s.checker.GetTypeOfSymbol(restParameter)
if restType.IsTupleType() {
associatedNames := make([]*ast.Node, 0, len(restType.Target().AsTupleType().ElementInfos()))
for _, elementInfo := range restType.Target().AsTupleType().ElementInfos() {
labeledElement := elementInfo.LabeledDeclaration()
associatedNames = append(associatedNames, labeledElement)
}
tupleType := restType.Target().AsTupleType()
elementInfos := tupleType.ElementInfos()
index := pos - paramCount
if index < len(associatedNames) {
associatedName := associatedNames[index]
restArgumentCount := argumentCount - paramCount
firstVariableIndex := tupleType.FixedLength()
trailingCount := checker.GetEndElementCount(tupleType, checker.ElementFlagsFixed)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe trailingFixedCount

// Optional trailing elements may be omitted, so only required ones can be aligned from the end.
requiredTrailingCount := checker.GetEndElementCount(tupleType, checker.ElementFlagsRequired)
if uncertainTupleSpread && (offsetFromEnd <= 0 || offsetFromEnd > requiredTrailingCount) {
return nil
}
variableCount := len(elementInfos) - firstVariableIndex - trailingCount
if trailingCount > 0 && variableCount > 0 {
switch {
case offsetFromEnd > 0 && offsetFromEnd <= requiredTrailingCount:
index = len(elementInfos) - offsetFromEnd
case offsetFromEnd > trailingCount:
return nil
case argumentCount >= 0 && restArgumentCount >= firstVariableIndex+trailingCount:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have mostly been able to track this stuff, but I'm starting to find it hard to keep track of in this branch. Maybe you could add some comments at each branch to show concrete situations that we're trying to handle?

trailingStart := restArgumentCount - trailingCount
switch {
case index >= trailingStart:
index = len(elementInfos) - (restArgumentCount - index)
case index >= firstVariableIndex && (variableCount > 1 || index > firstVariableIndex):
return nil
}
}
}
if index < len(elementInfos) {
associatedName := elementInfos[index].LabeledDeclaration()
if associatedName != nil {
debug.Assert(ast.IsIdentifier(associatedName.Name()))
var isRestTupleElement bool
Expand All @@ -886,6 +921,9 @@ func (s *inlayHintState) getParameterIdentifierInfoAtPosition(signature *checker
return nil
}

if uncertainTupleSpread {
return nil
}
if pos == paramCount {
return &parameterInfo{
parameter: restId,
Expand Down
22 changes: 9 additions & 13 deletions tsc/internal/ls/signaturehelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -1078,23 +1078,19 @@ func getSpreadElementCount(node *ast.SpreadElement, c *checker.Checker) int {
if tupleType == nil {
return 0
}
elementFlags := tupleType.ElementFlags()
fixedLength := tupleType.FixedLength()
if fixedLength == 0 {
return 0
}

firstOptionalIndex := core.FindIndex(elementFlags, func(f checker.ElementFlags) bool {
return (f&checker.ElementFlagsRequired == 0)
})
if firstOptionalIndex < 0 {
return fixedLength
}
return firstOptionalIndex
return getRequiredTupleElementCount(tupleType)
}
return 0
}

func getRequiredTupleElementCount(tupleType *checker.TupleType) int {
fixedLength := tupleType.FixedLength()
firstOptionalIndex := core.FindIndex(tupleType.ElementInfos()[:fixedLength], func(info checker.TupleElementInfo) bool {
return info.TupleElementFlags()&checker.ElementFlagsRequired == 0
})
return core.IfElse(firstOptionalIndex < 0, fixedLength, firstOptionalIndex)
}

func getArgumentIndex(node *ast.Node, arguments *ast.NodeList, sourceFile *ast.SourceFile, c *checker.Checker) int {
return getArgumentIndexOrCount(getTokenFromNodeList(arguments, node.Parent, sourceFile), node, c)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,38 +255,6 @@
"paddingRight": true
}

foo(...x, 3);
^
{
"position": {
"line": 11,
"character": 14
},
"label": [
{
"value": "b",
"location": {
"uri": "file:///inlayHintsInteractiveRestParameters2.ts",
"range": {
"start": {
"line": 0,
"character": 25
},
"end": {
"line": 0,
"character": 26
}
}
}
},
{
"value": ":"
}
],
"kind": 2,
"paddingRight": true
}

foo(...x, 3);
^
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// === Inlay Hints ===
g(...x, "end");
^
{
"position": {
"line": 2,
"character": 2
},
"label": [
{
"value": "a",
"location": {
"uri": "file:///inlayHintsOptionalParameterAfterTupleSpread.ts",
"range": {
"start": {
"line": 0,
"character": 19
},
"end": {
"line": 0,
"character": 20
}
}
}
},
{
"value": ":"
}
],
"kind": 2,
"paddingRight": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// === Inlay Hints ===
=== No inlay hints ===
Loading
Loading