Skip to content

Signature help implementation #861

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

Open
wants to merge 28 commits into
base: main
Choose a base branch
from

Conversation

navya9singh
Copy link
Member

This pr covers mostly everything that is needed for signature help except for signature help in .js files. Some tests that I haven't yet ported from Strada are tests for:

  1. Tagged templates
  2. Verify no signature help
  3. Filtered triggers
  4. Tests with JSDoc comments and other comments. It needs some changes in the test setup

@Copilot Copilot AI review requested due to automatic review settings May 12, 2025 21:09
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements support for signature help in the language server and makes extensive naming refactors throughout the checker and utility modules. Key changes include adding a new handler for signature help in the server, updating configuration for the SignatureHelpProvider, and renaming many internal API functions (e.g. getSignaturesOfType → GetSignaturesOfType) to improve consistency and clarity.

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated no comments.

Show a summary per file
File Description
internal/lsp/server.go Adds new case handling for SignatureHelpParams and configures the signature help provider.
internal/ls/utilities.go Updates comments and refactors type argument info functions without altering behavior.
internal/checker/utilities.go Renames internal helper functions (e.g. CanHaveSymbol, IsCallOrNewExpression) for consistency.
internal/checker/types.go Adds accessor methods on Signature and TupleType for better API exposure.
internal/checker/services.go Consistently replaces lower-case API functions with their exported versions.
internal/checker/relater.go Refactors signature comparison and tuple handling to use new exported functions.
internal/checker/printer.go Adjusts printer logic to use the new exported APIs and updates variadic parameter expansion.
internal/checker/jsx.go Updates JSX-related checks to use renamed functions.
internal/checker/inference.go Updates inference utilities with new function names.
internal/checker/flow.go Refactors flow analysis to call updated API names.
internal/checker/checker.go Widespread updates to use capitalized exported API functions for type retrievals and checks.
internal/astnav/tokens.go Adds a new helper function, HasQuestionToken, for token analysis.
internal/ast/utilities.go Adds helpers for handling template literal tokens and string text nodes.
_submodules/TypeScript Updates the submodule commit to a new revision.

@@ -2073,3 +2084,211 @@ func (c *Checker) checkNotCanceled() {
panic("Checker was previously cancelled")
}
}

Copy link
Member Author

Choose a reason for hiding this comment

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

These are some functions I added temporarily to make Signature help work. They will be removed once the node builder pr is merged. Also, these do not give the best result for generics, and returns object instead. This is why some of the tests have a comments //returns object because this part returns object.

Comment on lines +104 to +105
// Converting signatureHelpParameter to *lsproto.ParameterInformation
var parameters []*lsproto.ParameterInformation
Copy link
Member Author

Choose a reason for hiding this comment

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

Some of these functions are converting from local types to lsproto type because they needed some extra parameters like isRest, isOptional or isVariadic to be able to correctly calculate the parameter count and the parameter index that would be highlighted.

@@ -602,3 +602,13 @@ func getNodeVisitor(
},
})
}

// !!!
func HasQuestionToken(node *ast.Node) bool {
Copy link
Member

Choose a reason for hiding this comment

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

Should this go into the ast package?

@@ -17392,7 +17392,7 @@ func (c *Checker) getTypeOfPropertyOfType(t *Type, name string) *Type {
return nil
}

func (c *Checker) getSignaturesOfType(t *Type, kind SignatureKind) []*Signature {
func (c *Checker) GetSignaturesOfType(t *Type, kind SignatureKind) []*Signature {
Copy link
Member

Choose a reason for hiding this comment

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

Don't rename this method; just declare GetSignaturesOfType in exports.go and then forward to this unexported method.

I think the same should go for all of the other unexported functions; we're trying to consolidate these into a clearer API and also avoid huge diffs due to renames (which this PR does at the moment).

Copy link
Member

@DanielRosenwasser DanielRosenwasser left a comment

Choose a reason for hiding this comment

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

Mostly stylistic feedback. I haven't fully reviewed but I haven't seen anything glaringly different from the original so far!

@@ -25962,16 +25962,23 @@ func (c *Checker) markPropertyAsReferenced(prop *ast.Symbol, nodeForCheckWriteOn
c.symbolReferenceLinks.Get(target).referenceKinds |= ast.SymbolFlagsAll
}

func (c *Checker) GetExpandedParameters(signature *Signature /* !!! skipUnionExpanding */) []*ast.Symbol {
func (c *Checker) getExpandedParameters(signature *Signature, skipUnionExpanding *bool) [][]*ast.Symbol {
Copy link
Member

Choose a reason for hiding this comment

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

I would avoid making this a *bool. It implies one of two things to me:

  1. You intend to mutate the state after the fact (which is why it's a pointer)
  2. You need nil to be treated distinctly from false.

The only reason the old codebase had to account for undefined was so it could support an optional parameter which Go doesn't have.

Comment on lines +487 to +489
ancestorNode := ast.FindAncestor(node, func(n *ast.Node) bool {
return ast.IsCallLikeOrFunctionLikeExpression(n)
})
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
ancestorNode := ast.FindAncestor(node, func(n *ast.Node) bool {
return ast.IsCallLikeOrFunctionLikeExpression(n)
})
ancestorNode := ast.FindAncestor(node, ast.IsCallLikeOrFunctionLikeExpression)

return ast.IsCallLikeOrFunctionLikeExpression(n)
})
if ancestorNode != nil {
cachedResolvedSignatures := make(map[*SignatureLinks]*Signature)
Copy link
Member

Choose a reason for hiding this comment

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

I might just do two slices here instead of a map, but this might be cleaner.

Either way, I would just add a comment like

Suggested change
cachedResolvedSignatures := make(map[*SignatureLinks]*Signature)
// We're going to temporarily reset resolved signatures for all surrounding calls.
cachedResolvedSignatures := make(map[*SignatureLinks]*Signature)

Comment on lines +505 to +506
result := fn()
for signatureLinks, resolvedSignature := range cachedResolvedSignatures {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
result := fn()
for signatureLinks, resolvedSignature := range cachedResolvedSignatures {
result := fn()
// Once our work is over, we'll restore any signatures we previously resolved.
for signatureLinks, resolvedSignature := range cachedResolvedSignatures {

// if tokenIn != nil {
// searchPosition = scanner.GetTokenPosOfNode(tokenIn, sourceFile, false /*includeJSDoc*/)
// }
if strings.LastIndex(sourceFile.Text(), "<") == -1 {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if strings.LastIndex(sourceFile.Text(), "<") == -1 {
if strings.LastIndexByte(sourceFile.Text(), '<') == -1 {

Comment on lines +261 to +265
// This function determines if the node could be type argument position
// Since during editing, when type argument list is not complete,
// the tree could be of any shape depending on the tokens parsed before current node,
// scanning of the previous identifier followed by "<" before current node would give us better result
// Note that we also balance out the already provided type arguments, arrays, object literals while doing so
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// This function determines if the node could be type argument position
// Since during editing, when type argument list is not complete,
// the tree could be of any shape depending on the tokens parsed before current node,
// scanning of the previous identifier followed by "<" before current node would give us better result
// Note that we also balance out the already provided type arguments, arrays, object literals while doing so
// This function determines if the node could be a type argument position
// When editing, it is common to have an incomplete type argument list (e.g. missing ">"),
// so the tree can have any shape depending on the tokens before the current node.
// Instead, scanning for an identifier followed by a "<" before current node
// will typically give us better results than inspecting the tree.
// Note that we also balance out the already provided type arguments, arrays, object literals while doing so.

if token == nil || !ast.IsIdentifier(token) {
return nil
}
if remainingLessThanTokens <= 0 {
Copy link
Member

Choose a reason for hiding this comment

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

No need to check less-than, right?

@@ -245,13 +245,99 @@ type PossibleTypeArgumentInfo struct {
nTypeArguments int
}

// !!! signature help
// Get info for an expression like `f <` that may be the start of type arguments.
func getPossibleTypeArgumentsInfo(tokenIn *ast.Node, sourceFile *ast.SourceFile) *PossibleTypeArgumentInfo {
Copy link
Member

Choose a reason for hiding this comment

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

Minor preference for this to just return an bool, PossibleTypeArgumentInfo - it doesn't need to allocate. Not sure what others think.

Copy link
Member

Choose a reason for hiding this comment

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

I think completions uses the detailed return info, so we'll probably need to return a struct here.

}
tokenKind := token.Kind
remainingMatchingTokens := 0
for true {
Copy link
Member

Choose a reason for hiding this comment

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

I think the test of the codebases uses

Suggested change
for true {
for {

Copy link
Member

@gabritto gabritto left a comment

Choose a reason for hiding this comment

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

Some minor comments, but in general I think this is looking good.

switch node.Kind {
case KindParameter, KindMethodDeclaration, KindMethodSignature, KindShorthandPropertyAssignment,
KindPropertyAssignment, KindPropertyDeclaration, KindPropertySignature:
return node.AsParameterDeclaration().QuestionToken != nil
Copy link
Member

Choose a reason for hiding this comment

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

That doesn't look right: if the node kind is, say, method declaration, node.AsParameterDeclaration() will fail. So I'd suggest either adding a separate cast for each node kind here, or maybe creating a QuestionToken() method in ast.go, in the style of the other getters we have there already.

@@ -29422,7 +29429,7 @@ func (c *Checker) GetSymbolAtLocation(node *ast.Node) *ast.Symbol {
// be used only by the language service and external tools. The semantics of the function are deliberately "fuzzy"
// and aim to just return *some* symbol for the node. To obtain the symbol associated with a node for type checking
// purposes, use appropriate function for the context, e.g. `getResolvedSymbol` for an expression identifier,
// `getSymbolOfDeclaration` for a declaration, etc.
Copy link
Member

Choose a reason for hiding this comment

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

These are probably leftover from the old rename.

return c.getReturnTypeOfSignature(sig)
}

func IsCallOrNewExpression(node *ast.Node) bool {
Copy link
Member

Choose a reason for hiding this comment

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

I would just move those node predicates to ast/utilities.go, like this one, GetInvokedExpression, IsJsxOpeningLikeElement, etc.

@@ -1235,7 +1235,7 @@ func (c *Checker) inferFromIntraExpressionSites(n *InferenceContext) {
if ast.IsMethodDeclaration(site.node) {
contextualType = c.getContextualTypeForObjectLiteralMethod(site.node, ContextFlagsNoConstraints)
} else {
contextualType = c.getContextualType(site.node, ContextFlagsNoConstraints)
Copy link
Member

Choose a reason for hiding this comment

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

Also looks like leftover from a rename?

@@ -25962,16 +25962,23 @@ func (c *Checker) markPropertyAsReferenced(prop *ast.Symbol, nodeForCheckWriteOn
c.symbolReferenceLinks.Get(target).referenceKinds |= ast.SymbolFlagsAll
}

func (c *Checker) GetExpandedParameters(signature *Signature /* !!! skipUnionExpanding */) []*ast.Symbol {
func (c *Checker) getExpandedParameters(signature *Signature, skipUnionExpanding *bool) [][]*ast.Symbol {
Copy link
Member

Choose a reason for hiding this comment

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

skipUnionExpanding should just be bool, since we can't avoid passing arguments in Go like we can in TS with optional parameters.

}
// This is a rare case, but one that saves on a _lot_ of work if true - if the source file has _no_ `<` character,
// then there obviously can't be any type arguments - no expensive brace-matching backwards scanning required
// searchPosition := len(sourceFile.Text())
Copy link
Member

Choose a reason for hiding this comment

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

Is this missing a // !!!, or are we getting rid of this code?

}

// Only need to be careful if the user typed a character and signature help wasn't showing.
onlyUseSyntacticOwners := context.TriggerKind == 2
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
onlyUseSyntacticOwners := context.TriggerKind == 2
onlyUseSyntacticOwners := context.TriggerKind == lsproto.SignatureHelpTriggerKindTriggerCharacter

@@ -245,13 +245,99 @@ type PossibleTypeArgumentInfo struct {
nTypeArguments int
}

// !!! signature help
// Get info for an expression like `f <` that may be the start of type arguments.
func getPossibleTypeArgumentsInfo(tokenIn *ast.Node, sourceFile *ast.SourceFile) *PossibleTypeArgumentInfo {
Copy link
Member

Choose a reason for hiding this comment

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

I think completions uses the detailed return info, so we'll probably need to return a struct here.

printer := printer.NewPrinter(printer.PrinterOptions{NewLine: core.NewLineKindLF}, printer.PrintHandlers{}, nil)

var getTypeParameters []signatureHelpParameter = []signatureHelpParameter{}
if candidateSignature.TypeParameters() != nil && len(candidateSignature.TypeParameters()) != 0 {
Copy link
Member

Choose a reason for hiding this comment

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

This here is what I mentioned elsewhere that could become just len(candidateSignature.TypeParameters()) != 0.

return containsNode(invocationChildren, startingToken)
case ast.KindCommaToken:
return containsNode(invocationChildren, startingToken)
// const containingList = findContainingList(startingToken);
Copy link
Member

Choose a reason for hiding this comment

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

Should this have a // !!!?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants