Skip to content

Improve go-to-definition and implement go-to-type-definition #1405

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 17, 2025
Merged
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 internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -2785,6 +2785,10 @@ func (node *SwitchStatement) computeSubtreeFacts() SubtreeFacts {
propagateSubtreeFacts(node.CaseBlock)
}

func IsSwitchStatement(node *Node) bool {
return node.Kind == KindSwitchStatement
}

// CaseBlock

type CaseBlock struct {
Expand Down
82 changes: 1 addition & 81 deletions internal/binder/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2758,7 +2758,7 @@ func (b *Binder) errorOrSuggestionOnRange(isError bool, startNode *ast.Node, end
// If so, the node _must_ be in the current file (as that's the only way anything could have traversed to it to yield it as the error node)
// This version of `createDiagnosticForNode` uses the binder's context to account for this, and always yields correct diagnostics even in these situations.
func (b *Binder) createDiagnosticForNode(node *ast.Node, message *diagnostics.Message, args ...any) *ast.Diagnostic {
return ast.NewDiagnostic(b.file, GetErrorRangeForNode(b.file, node), message, args...)
return ast.NewDiagnostic(b.file, scanner.GetErrorRangeForNode(b.file, node), message, args...)
}

func (b *Binder) addDiagnostic(diagnostic *ast.Diagnostic) {
Expand Down Expand Up @@ -2855,83 +2855,3 @@ func isAssignmentDeclaration(decl *ast.Node) bool {
func isEffectiveModuleDeclaration(node *ast.Node) bool {
return ast.IsModuleDeclaration(node) || ast.IsIdentifier(node)
}

func getErrorRangeForArrowFunction(sourceFile *ast.SourceFile, node *ast.Node) core.TextRange {
pos := scanner.SkipTrivia(sourceFile.Text(), node.Pos())
body := node.AsArrowFunction().Body
if body != nil && body.Kind == ast.KindBlock {
startLine, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, body.Pos())
endLine, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, body.End())
if startLine < endLine {
// The arrow function spans multiple lines,
// make the error span be the first line, inclusive.
return core.NewTextRange(pos, scanner.GetEndLinePosition(sourceFile, startLine))
}
}
return core.NewTextRange(pos, node.End())
}

func GetErrorRangeForNode(sourceFile *ast.SourceFile, node *ast.Node) core.TextRange {
errorNode := node
switch node.Kind {
case ast.KindSourceFile:
pos := scanner.SkipTrivia(sourceFile.Text(), 0)
if pos == len(sourceFile.Text()) {
return core.NewTextRange(0, 0)
}
return scanner.GetRangeOfTokenAtPosition(sourceFile, pos)
// This list is a work in progress. Add missing node kinds to improve their error spans
case ast.KindFunctionDeclaration, ast.KindMethodDeclaration:
if node.Flags&ast.NodeFlagsReparsed != 0 {
errorNode = node
break
}
fallthrough
case ast.KindVariableDeclaration, ast.KindBindingElement, ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration,
ast.KindModuleDeclaration, ast.KindEnumDeclaration, ast.KindEnumMember, ast.KindFunctionExpression,
ast.KindGetAccessor, ast.KindSetAccessor, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration, ast.KindPropertyDeclaration,
ast.KindPropertySignature, ast.KindNamespaceImport:
errorNode = ast.GetNameOfDeclaration(node)
case ast.KindArrowFunction:
return getErrorRangeForArrowFunction(sourceFile, node)
case ast.KindCaseClause, ast.KindDefaultClause:
start := scanner.SkipTrivia(sourceFile.Text(), node.Pos())
end := node.End()
statements := node.AsCaseOrDefaultClause().Statements.Nodes
if len(statements) != 0 {
end = statements[0].Pos()
}
return core.NewTextRange(start, end)
case ast.KindReturnStatement, ast.KindYieldExpression:
pos := scanner.SkipTrivia(sourceFile.Text(), node.Pos())
return scanner.GetRangeOfTokenAtPosition(sourceFile, pos)
case ast.KindSatisfiesExpression:
pos := scanner.SkipTrivia(sourceFile.Text(), node.AsSatisfiesExpression().Expression.End())
return scanner.GetRangeOfTokenAtPosition(sourceFile, pos)
case ast.KindConstructor:
if node.Flags&ast.NodeFlagsReparsed != 0 {
errorNode = node
break
}
scanner := scanner.GetScannerForSourceFile(sourceFile, node.Pos())
start := scanner.TokenStart()
for scanner.Token() != ast.KindConstructorKeyword && scanner.Token() != ast.KindStringLiteral && scanner.Token() != ast.KindEndOfFile {
scanner.Scan()
}
return core.NewTextRange(start, scanner.TokenEnd())
// !!!
// case KindJSDocSatisfiesTag:
// pos := scanner.SkipTrivia(sourceFile.Text(), node.tagName.pos)
// return scanner.GetRangeOfTokenAtPosition(sourceFile, pos)
}
if errorNode == nil {
// If we don't have a better node, then just set the error on the first token of
// construct.
return scanner.GetRangeOfTokenAtPosition(sourceFile, node.Pos())
}
pos := errorNode.Pos()
if !ast.NodeIsMissing(errorNode) && !ast.IsJsxText(errorNode) {
pos = scanner.SkipTrivia(sourceFile.Text(), pos)
}
return core.NewTextRange(pos, errorNode.End())
}
16 changes: 16 additions & 0 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -30263,6 +30263,22 @@ func (c *Checker) getSymbolAtLocation(node *ast.Node, ignoreErrors bool) *ast.Sy
}
}

func (c *Checker) getIndexSignaturesAtLocation(node *ast.Node) []*ast.Node {
var signatures []*ast.Node
if ast.IsIdentifier(node) && ast.IsPropertyAccessExpression(node.Parent) && node.Parent.Name() == node {
keyType := c.getLiteralTypeFromPropertyName(node)
objectType := c.getTypeOfExpression(node.Parent.Expression())
for _, t := range objectType.Distributed() {
for _, info := range c.getApplicableIndexInfos(t, keyType) {
if info.declaration != nil {
signatures = core.AppendIfUnique(signatures, info.declaration)
}
}
}
}
return signatures
}

func (c *Checker) getSymbolOfNameOrPropertyAccessExpression(name *ast.Node) *ast.Symbol {
if ast.IsDeclarationName(name) {
return c.getSymbolOfNode(name.Parent)
Expand Down
8 changes: 8 additions & 0 deletions internal/checker/exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,11 @@ func (c *Checker) GetTypeOfPropertyOfType(t *Type, name string) *Type {
func (c *Checker) GetContextualTypeForArgumentAtIndex(node *ast.Node, argIndex int) *Type {
return c.getContextualTypeForArgumentAtIndex(node, argIndex)
}

func (c *Checker) GetIndexSignaturesAtLocation(node *ast.Node) []*ast.Node {
return c.getIndexSignaturesAtLocation(node)
}

func (c *Checker) GetResolvedSymbol(node *ast.Node) *ast.Symbol {
return c.getResolvedSymbol(node)
}
65 changes: 65 additions & 0 deletions internal/checker/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,3 +618,68 @@ func (c *Checker) GetTypeParameterAtPosition(s *Signature, pos int) *Type {
}
return t
}

func (c *Checker) GetContextualDeclarationsForObjectLiteralElement(objectLiteral *ast.Node, name string) []*ast.Node {
var result []*ast.Node
if t := c.getApparentTypeOfContextualType(objectLiteral, ContextFlagsNone); t != nil {
for _, t := range t.Distributed() {
prop := c.getPropertyOfType(t, name)
if prop != nil {
for _, declaration := range prop.Declarations {
result = core.AppendIfUnique(result, declaration)
}
} else {
for _, info := range c.getApplicableIndexInfos(t, c.getStringLiteralType(name)) {
if info.declaration != nil {
result = core.AppendIfUnique(result, info.declaration)
}
}
}
}
}
return result
}

var knownGenericTypeNames = map[string]struct{}{
"Array": {},
"ArrayLike": {},
"ReadonlyArray": {},
"Promise": {},
"PromiseLike": {},
"Iterable": {},
"IterableIterator": {},
"AsyncIterable": {},
"Set": {},
"WeakSet": {},
"ReadonlySet": {},
"Map": {},
"WeakMap": {},
"ReadonlyMap": {},
"Partial": {},
"Required": {},
"Readonly": {},
"Pick": {},
"Omit": {},
Copy link
Member

@RyanCavanaugh RyanCavanaugh Jul 15, 2025

Choose a reason for hiding this comment

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

Suggested change
"Omit": {},
"Omit": {},
"Exclude": {},

Copy link
Member Author

Choose a reason for hiding this comment

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

The types in this list generally operate on object types for which we record source locations. Exclude and Extract are almost always used with (unions of) literal types, and we don't record source locations for those.

Copy link
Member Author

Choose a reason for hiding this comment

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

However, we should include NonNullable<T>.

"NonNullable": {},
}

func isKnownGenericTypeName(name string) bool {
_, exists := knownGenericTypeNames[name]
return exists
}

func (c *Checker) GetFirstTypeArgumentFromKnownType(t *Type) *Type {
if t.objectFlags&ObjectFlagsReference != 0 && isKnownGenericTypeName(t.symbol.Name) {
symbol := c.getGlobalSymbol(t.symbol.Name, ast.SymbolFlagsType, nil)
if symbol != nil && symbol == t.Target().symbol {
return core.FirstOrNil(c.getTypeArguments(t))
}
}
if t.alias != nil && isKnownGenericTypeName(t.alias.symbol.Name) {
symbol := c.getGlobalSymbol(t.alias.symbol.Name, ast.SymbolFlagsType, nil)
if symbol != nil && symbol == t.alias.symbol {
return core.FirstOrNil(t.alias.typeArguments)
}
}
return nil
}
2 changes: 1 addition & 1 deletion internal/checker/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func NewDiagnosticForNode(node *ast.Node, message *diagnostics.Message, args ...
var loc core.TextRange
if node != nil {
file = ast.GetSourceFileOfNode(node)
loc = binder.GetErrorRangeForNode(file, node)
loc = scanner.GetErrorRangeForNode(file, node)
}
return ast.NewDiagnostic(file, loc, message, args...)
}
Expand Down
Loading
Loading