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
}

{
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 */ {
[P in keyof T as ToGet<P>]: () => T[P]
};

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

type T50 = Getters<Y>; // { getFoo: () => string, getBar: () => number }

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 ")
}
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.
/**
* 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 NOT displayed.
/*1*/getName: () => "Jake Carter",
// ❌ When hovering here, the documentation is NOT displayed.
/*2*/getAge: () => 35,
// ❌ When hovering here, the documentation is NOT displayed.
/*3*/getLocation: () => "United States",
};

// ❌ When hovering here, the documentation is NOT displayed.
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 */
[P in keyof T as ToGet<P>]: () => T[P]

};

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

type T50 = Getters<Y>; // { getFoo: () => string, getBar: () => number }

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, synthesize documentation
// from 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()
hasJSDoc := p.jsdocScannerInfo()
p.parseExpected(ast.KindOpenBraceToken)
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, hasJSDoc)
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