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 internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -20397,6 +20397,7 @@ func (c *Checker) resolveMappedTypeMembers(t *Type) {
}
prop := c.newSymbol(ast.SymbolFlagsProperty|core.IfElse(isOptional, ast.SymbolFlagsOptional, 0), propName)
prop.CheckFlags = lateFlag | ast.CheckFlagsMapped | core.IfElse(isReadonly, ast.CheckFlagsReadonly, 0) | core.IfElse(stripOptional, ast.CheckFlagsStripOptional, 0)
prop.Parent = mappedType.symbol
valueLinks := c.valueSymbolLinks.Get(prop)
valueLinks.containingType = t
valueLinks.nameType = propNameType
Expand Down
8 changes: 8 additions & 0 deletions internal/checker/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,14 @@ func (c *Checker) GetMappedTypeSymbolOfProperty(symbol *ast.Symbol) *ast.Symbol
return nil
}

// GetMappedSyntheticOrigin returns the syntheticOrigin of a mapped symbol (the original property it was derived from).
func (c *Checker) GetMappedSyntheticOrigin(symbol *ast.Symbol) *ast.Symbol {
if symbol.CheckFlags&ast.CheckFlagsMapped != 0 && c.mappedSymbolLinks.Has(symbol) {
return c.mappedSymbolLinks.Get(symbol).syntheticOrigin
}
return nil
}

func (c *Checker) getImmediateRootSymbols(symbol *ast.Symbol) []*ast.Symbol {
if symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 {
return core.MapNonNil(
Expand Down
32 changes: 32 additions & 0 deletions internal/fourslash/tests/goToDefinitionMappedType2_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-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestGoToDefinitionMappedType2(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `interface Foo {
/*def*/property: string
}

type JustMapIt<T> = {[P in keyof T]: 0}
type MapItWithRemap<T> = {[P in keyof T as P extends string ? ` + "`mapped_${P}`" + ` : never]: 0}

{
let gotoDef!: JustMapIt<Foo>
gotoDef.property

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.

Did you mean to also test the simpler case here?

}

{
let gotoDef!: MapItWithRemap<Foo>
gotoDef.[|/*ref*/mapped_property|]
}`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyBaselineGoToDefinition(t, true, "ref")
}
41 changes: 41 additions & 0 deletions internal/fourslash/tests/goToDefinitionMappedType3_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestGoToDefinitionMappedType3(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `interface Source {
/*def*/alpha: number;
beta: string;
}

// Transforming interface field names with a suffix
type Transformed<T> = {
[K in keyof T as ` + "`${K & string}Suffix`" + `]: () => T[K];
};

type Result = Transformed<Source>;
/*
Expected:
{
alphaSuffix: () => number;
betaSuffix: () => string;
}
*/

const obj: Result = {
alphaSuffix: () => 42,
betaSuffix: () => "hello",
};

obj.[|/*ref*/alphaSuffix|]();`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyBaselineGoToDefinition(t, true, "ref")
}
34 changes: 34 additions & 0 deletions internal/fourslash/tests/quickInfoMappedType2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestQuickInfoMappedType2(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
// Tests that @inheritDoc on a MappedTypeNode causes hover to show documentation.
// TODO: once @inheritDoc resolution is fully implemented, the expected documentation
// should be "desc on Getters\nhello" (combined from the mapped type and the source property).
const content = `type ToGet<T> = T extends string ? ` + "`get${Capitalize<T>}`" + ` : never;
type Getters<T> = /** @inheritDoc desc on Getters */ {

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.

Could you also add a quick info case without property remapping?

[P in keyof T as ToGet<P>]: () => T[P]
};

type Y = {
/** hello */
d: string;
}

type T50 = Getters<Y>;

declare let y: T50;
y.get/*3*/D;`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
// Current Go behavior: shows the raw @inheritDoc tag text from the mapped type declaration.
f.VerifyQuickInfoAt(t, "3", "(property) getD: () => string", "\n\n*@inheritDoc* \u2014 desc on Getters ")

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.

So the default behavior will be to not inherit the jsdoc from e.g. Y, unless @inheritDoc is present in the mapped type?
Also, this shouldn't affect "go to definition", right? Only documentation.

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'm wondering if it makes sense to also port @inheritDoc support in this PR then, since otherwise you can't inherit the docs.

}
58 changes: 58 additions & 0 deletions internal/fourslash/tests/quickInfoMappedType3_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestQuickInfoMappedType3(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `type Getters<Type> = /** @inheritDoc desc on Getters */ {
[Property in keyof Type as ` + "`" + `get${Capitalize<
string & Property
>}` + "`" + `]: () => Type[Property];
};

interface Person {
// ✅ When hovering here, the documentation is displayed, as it should.

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

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

The embedded TS test source still includes the "✅ When hovering here..." narration (and the ✅ symbol). Per earlier reviewer guidance, please remove these explanatory comments from the content string (and the other similar occurrences in this test) to avoid lossy/noisy artifacts in baselines and keep the test focused on markers/baseline output.

Copilot uses AI. Check for mistakes.
/**
* Person's name.
* @example "John Doe"
*/
name: string;

// ✅ When hovering here, the documentation is displayed, as it should.
/**
* Person's Age.
* @example 30
*/
age: number;

// ✅ When hovering here, the documentation is displayed, as it should.
/**
* Person's Location.
* @example "Brazil"
*/
location: string;
}

type LazyPerson = Getters<Person>;

const me: LazyPerson = {
// ✅ When hovering here, the documentation is displayed, as it should.
/*1*/getName: () => "Jake Carter",
// ✅ When hovering here, the documentation is displayed, as it should.
/*2*/getAge: () => 35,
// ✅ When hovering here, the documentation is displayed, as it should.
/*3*/getLocation: () => "United States",
};

// ✅ When hovering here, the documentation is displayed, as it should.
me./*4*/getName();`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyBaselineHover(t)
}
32 changes: 32 additions & 0 deletions internal/fourslash/tests/quickInfoMappedType4_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-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestQuickInfoMappedType4(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `type ToGet<T> = T extends string ? ` + "`get${Capitalize<T>}`" + ` : never;
type Getters<T> = {
/** @inheritDoc desc on Getters */

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.

Why doesn't this work but adding the comment outside the mapped type work?

[P in keyof T as ToGet<P>]: () => T[P]

};

type Y = {
/** hello */
d: string;
}

type T50 = Getters<Y>;

declare let y: T50;
y.get/*3*/D;`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyQuickInfoAt(t, "3", "(property) getD: () => string", "")
}
6 changes: 6 additions & 0 deletions internal/ls/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ func getDeclarationsFromLocation(c *checker.Checker, node *ast.Node) []*ast.Node
if len(symbol.Declarations) > 0 {
return symbol.Declarations
}
// For mapped type properties with no declarations, fall back to syntheticOrigin declarations
if symbol.CheckFlags&ast.CheckFlagsMapped != 0 {
if syntheticOrigin := c.GetMappedSyntheticOrigin(symbol); syntheticOrigin != nil && len(syntheticOrigin.Declarations) > 0 {
return syntheticOrigin.Declarations
}
}
}
if indexInfos := c.GetIndexSignaturesAtLocation(node); len(indexInfos) != 0 {
return indexInfos
Expand Down
42 changes: 42 additions & 0 deletions internal/ls/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Check

func (l *LanguageService) getDocumentationFromDeclaration(c *checker.Checker, symbol *ast.Symbol, declaration *ast.Node, location *ast.Node, contentFormat lsproto.MarkupKind, commentOnly bool) string {
if declaration == nil {
// For mapped type symbols with @inheritDoc on the mapped type, collect documentation
// by considering the mapped type's declarations and the syntheticOrigin's declarations.
if symbol != nil && symbol.CheckFlags&ast.CheckFlagsMapped != 0 &&
symbol.Parent != nil && len(symbol.Parent.Declarations) > 0 &&
core.Some(symbol.Parent.Declarations, hasJSDocInheritDocTag) {
syntheticOrigin := c.GetMappedSyntheticOrigin(symbol)
var decls []*ast.Node
decls = append(decls, symbol.Parent.Declarations...)
if syntheticOrigin != nil {
decls = append(decls, syntheticOrigin.Declarations...)
}
return l.getDocumentationFromDeclarations(c, decls, location, contentFormat, commentOnly)
}
return ""
}

Expand Down Expand Up @@ -187,6 +200,35 @@ func (l *LanguageService) getDocumentationFromDeclaration(c *checker.Checker, sy
return b.String()
}

// hasJSDocInheritDocTag reports whether the given node has a @inheritDoc JSDoc tag.
func hasJSDocInheritDocTag(node *ast.Node) bool {
for _, jsdoc := range node.JSDoc(nil) {
if jsdoc.Kind != ast.KindJSDoc {
continue
}
tags := jsdoc.AsJSDoc().Tags
if tags == nil {
continue
}
for _, tag := range tags.Nodes {
if tag.Kind == ast.KindJSDocTag && tag.TagName().Text() == "inheritDoc" {
return true
}
}
}
return false
}

// getDocumentationFromDeclarations gets documentation from the first declaration in decls that has JSDoc.
func (l *LanguageService) getDocumentationFromDeclarations(c *checker.Checker, decls []*ast.Node, location *ast.Node, contentFormat lsproto.MarkupKind, commentOnly bool) string {
for _, decl := range decls {
if s := l.getDocumentationFromDeclaration(c, nil, decl, location, contentFormat, commentOnly); s != "" {
return s
}
}
return ""
}

func getCommentText(comments []*ast.Node) string {
var b strings.Builder
for _, comment := range comments {
Expand Down
5 changes: 4 additions & 1 deletion internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3120,6 +3120,7 @@ func (p *Parser) nextIsStartOfMappedType() bool {

func (p *Parser) parseMappedType() *ast.Node {
pos := p.nodePos()
jsdocInfo := p.jsdocScannerInfo()
p.parseExpected(ast.KindOpenBraceToken)
Comment thread
gabritto marked this conversation as resolved.
var readonlyToken *ast.Node // ReadonlyKeyword | PlusToken | MinusToken
if p.token == ast.KindReadonlyKeyword || p.token == ast.KindPlusToken || p.token == ast.KindMinusToken {
Expand All @@ -3146,7 +3147,9 @@ func (p *Parser) parseMappedType() *ast.Node {
p.parseSemicolon()
members := p.parseList(PCTypeMembers, (*Parser).parseTypeMember)
p.parseExpected(ast.KindCloseBraceToken)
return p.finishNode(p.factory.NewMappedTypeNode(readonlyToken, typeParameter, nameType, questionToken, typeNode, members), pos)
result := p.finishNode(p.factory.NewMappedTypeNode(readonlyToken, typeParameter, nameType, questionToken, typeNode, members), pos)
p.withJSDoc(result, jsdocInfo)
return result
}

func (p *Parser) parseMappedTypeParameter() *ast.Node {
Expand Down
2 changes: 1 addition & 1 deletion internal/parser/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func tokenIsIdentifierOrKeywordOrGreaterThan(token ast.Kind) bool {

func GetJSDocCommentRanges(f *ast.NodeFactory, commentRanges []ast.CommentRange, node *ast.Node, text string) []ast.CommentRange {
switch node.Kind {
case ast.KindParameter, ast.KindTypeParameter, ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindParenthesizedExpression, ast.KindVariableDeclaration, ast.KindExportSpecifier:
case ast.KindParameter, ast.KindTypeParameter, ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindParenthesizedExpression, ast.KindVariableDeclaration, ast.KindExportSpecifier, ast.KindMappedType:
for commentRange := range scanner.GetTrailingCommentRanges(f, text, node.Pos()) {
commentRanges = append(commentRanges, commentRange)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// === goToDefinition ===
// === /goToDefinitionMappedType2.ts ===
// interface Foo {
// <|[|property|]: string|>
// }
//
// type JustMapIt<T> = {[P in keyof T]: 0}
// --- (line: 6) skipped ---

// --- (line: 11) skipped ---
//
// {
// let gotoDef!: MapItWithRemap<Foo>
// gotoDef./*GOTO DEF*/mapped_property
// }
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// === goToDefinition ===
// === /goToDefinitionMappedType3.ts ===
// interface Source {
// <|[|alpha|]: number;|>
// beta: string;
// }
//
// --- (line: 6) skipped ---

// --- (line: 21) skipped ---
// betaSuffix: () => "hello",
// };
//
// obj./*GOTO DEF*/alphaSuffix();
Loading
Loading