From dcaa5a7188cf1732a2fc4fd6aeb6e173291e5d7b Mon Sep 17 00:00:00 2001 From: ThanhNguyxn Date: Sun, 25 Jan 2026 17:09:53 +0700 Subject: [PATCH 1/2] Fix inlay hints for rest tuple tail mapping --- src/services/inlayHints.ts | 95 ++++++++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 18 deletions(-) diff --git a/src/services/inlayHints.ts b/src/services/inlayHints.ts index 43dad7ea1793d..dbaa79da2018c 100644 --- a/src/services/inlayHints.ts +++ b/src/services/inlayHints.ts @@ -109,7 +109,9 @@ import { PrefixUnaryExpression, PropertyDeclaration, QuotePreference, + Signature, SignatureDeclarationBase, + signatureHasRestParameter, skipParentheses, some, Symbol, @@ -301,46 +303,103 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] { const signature = checker.getResolvedSignature(expr); if (signature === undefined) return; + const argumentSpans = args.map(arg => getArgumentSpan(skipParentheses(arg))); + let totalArgumentPositions = 0; + for (const span of argumentSpans) { + totalArgumentPositions += span; + } + + const nonRestParamCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); + const restTupleInfo = getRestTupleInfo(signature, nonRestParamCount, totalArgumentPositions); + let signatureParamPos = 0; - for (const originalArg of args) { + for (let argIndex = 0; argIndex < args.length; argIndex++) { + const originalArg = args[argIndex]; const arg = skipParentheses(originalArg); + const spreadArgs = argumentSpans[argIndex]; + if (spreadArgs === 0) { + continue; + } if (shouldShowLiteralParameterNameHintsOnly(preferences) && !isHintableLiteral(arg)) { - signatureParamPos++; + signatureParamPos += spreadArgs; continue; } - let spreadArgs = 0; + const parameterPos = getAdjustedParameterPosition(signatureParamPos, restTupleInfo); + const identifierInfo = checker.getParameterIdentifierInfoAtPosition(signature, parameterPos); + signatureParamPos = signatureParamPos + (spreadArgs || 1); + if (identifierInfo) { + const { parameter, parameterName, isRestParameter: isFirstVariadicArgument } = identifierInfo; + const isParameterNameNotSameAsArgument = preferences.includeInlayParameterNameHintsWhenArgumentMatchesName || !identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName); + if (!isParameterNameNotSameAsArgument && !isFirstVariadicArgument) { + continue; + } + + const name = unescapeLeadingUnderscores(parameterName); + if (leadingCommentsContainsParameterName(arg, name)) { + continue; + } + + addParameterHints(name, parameter, originalArg.getStart(), isFirstVariadicArgument); + } + } + + function getArgumentSpan(arg: Expression) { if (isSpreadElement(arg)) { const spreadType = checker.getTypeAtLocation(arg.expression); if (checker.isTupleType(spreadType)) { const { elementFlags, fixedLength } = (spreadType as TupleTypeReference).target; if (fixedLength === 0) { - continue; + return 0; } const firstOptionalIndex = findIndex(elementFlags, f => !(f & ElementFlags.Required)); const requiredArgs = firstOptionalIndex < 0 ? fixedLength : firstOptionalIndex; if (requiredArgs > 0) { - spreadArgs = firstOptionalIndex < 0 ? fixedLength : firstOptionalIndex; + return requiredArgs; } } } + return 1; + } - const identifierInfo = checker.getParameterIdentifierInfoAtPosition(signature, signatureParamPos); - signatureParamPos = signatureParamPos + (spreadArgs || 1); - if (identifierInfo) { - const { parameter, parameterName, isRestParameter: isFirstVariadicArgument } = identifierInfo; - const isParameterNameNotSameAsArgument = preferences.includeInlayParameterNameHintsWhenArgumentMatchesName || !identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName); - if (!isParameterNameNotSameAsArgument && !isFirstVariadicArgument) { - continue; - } + function getRestTupleInfo(signature: Signature, paramCount: number, totalPositions: number) { + if (!signatureHasRestParameter(signature)) { + return undefined; + } + const restParameter = signature.parameters[paramCount]; + if (!restParameter) { + return undefined; + } + const restType = checker.getTypeOfSymbol(restParameter); + if (!checker.isTupleType(restType)) { + return undefined; + } + const elementFlags = (restType as TupleTypeReference).target.elementFlags; + const restStartIndex = findIndex(elementFlags, f => !!(f & ElementFlags.Variable)); + if (restStartIndex < 0) { + return undefined; + } + const restTailCount = elementFlags.length - restStartIndex - 1; + const restPositionsTotal = Math.max(0, totalPositions - paramCount); + return { restStartIndex, restTailCount, restPositionsTotal, paramCount }; + } - const name = unescapeLeadingUnderscores(parameterName); - if (leadingCommentsContainsParameterName(arg, name)) { - continue; + function getAdjustedParameterPosition(signatureParamPos: number, restInfo?: { restStartIndex: number; restTailCount: number; restPositionsTotal: number; paramCount: number; }) { + if (!restInfo || signatureParamPos < restInfo.paramCount) { + return signatureParamPos; + } + const restPosition = signatureParamPos - restInfo.paramCount; + if (restPosition < restInfo.restStartIndex) { + return signatureParamPos; + } + if (restInfo.restTailCount > 0 && restInfo.restPositionsTotal >= restInfo.restTailCount) { + const tailStart = restInfo.restPositionsTotal - restInfo.restTailCount; + if (restPosition >= tailStart) { + const tailIndex = restPosition - tailStart; + return restInfo.paramCount + restInfo.restStartIndex + 1 + tailIndex; } - - addParameterHints(name, parameter, originalArg.getStart(), isFirstVariadicArgument); } + return restInfo.paramCount + restInfo.restStartIndex; } } From 18192c905529dad64a5c0e051d98a500ec4e52a9 Mon Sep 17 00:00:00 2001 From: ThanhNguyxn Date: Sun, 25 Jan 2026 17:10:12 +0700 Subject: [PATCH 2/2] Add inlay hints test for rest tuple tail --- .../inlayHintsRestTupleTail.baseline | 63 +++++++++++++++++++ .../fourslash/inlayHintsRestTupleTail.ts | 8 +++ 2 files changed, 71 insertions(+) create mode 100644 tests/baselines/reference/inlayHintsRestTupleTail.baseline create mode 100644 tests/cases/fourslash/inlayHintsRestTupleTail.ts diff --git a/tests/baselines/reference/inlayHintsRestTupleTail.baseline b/tests/baselines/reference/inlayHintsRestTupleTail.baseline new file mode 100644 index 0000000000000..d37587619a821 --- /dev/null +++ b/tests/baselines/reference/inlayHintsRestTupleTail.baseline @@ -0,0 +1,63 @@ +// === Inlay Hints === +test(10, 'a', 'b', 'c') + ^ +{ + "text": "first:", + "position": 83, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'b', 'c') + ^ +{ + "text": "...middle:", + "position": 87, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'b', 'c') + ^ +{ + "text": "...middle:", + "position": 92, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'b', 'c') + ^ +{ + "text": "last:", + "position": 97, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'c') + ^ +{ + "text": "first:", + "position": 107, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'c') + ^ +{ + "text": "...middle:", + "position": 111, + "kind": "Parameter", + "whitespaceAfter": true +} + +test(10, 'a', 'c') + ^ +{ + "text": "last:", + "position": 116, + "kind": "Parameter", + "whitespaceAfter": true +} \ No newline at end of file diff --git a/tests/cases/fourslash/inlayHintsRestTupleTail.ts b/tests/cases/fourslash/inlayHintsRestTupleTail.ts new file mode 100644 index 0000000000000..8899e8df9666b --- /dev/null +++ b/tests/cases/fourslash/inlayHintsRestTupleTail.ts @@ -0,0 +1,8 @@ +//// function test(...rest: [first: number, ...middle: string[], last: string]) {} +//// test(10, 'a', 'b', 'c') +//// test(10, 'a', 'c') + +verify.baselineInlayHints(undefined, { + includeInlayParameterNameHints: "all", + includeInlayFunctionParameterTypeHints: true, +});