Fix go-to-definition for binding elements in ObjectBindingPattern#2842
Fix go-to-definition for binding elements in ObjectBindingPattern#2842
Conversation
Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com>
| ) | ||
|
|
||
| func TestGotoDefinitionInObjectBindingPattern1(t *testing.T) { | ||
| fourslash.SkipIfFailing(t) |
There was a problem hiding this comment.
@copilot Instead of just deleting these, you need to modify the failingtests.txt file
There was a problem hiding this comment.
Restored SkipIfFailing(t) to all three gen test files in f55d461. The tests are now enabled by keeping them out of failingTests.txt (rather than removing the SkipIfFailing calls).
Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes go-to-definition functionality for binding elements in object binding patterns, enabling developers to navigate from destructured parameters to their type definitions. The issue was reported in the context of Playwright fixtures where go-to-definition failed to navigate to the fixture definition.
Changes:
- Added new handling logic in
getDeclarationsFromLocationfor ObjectBindingPattern binding elements that navigates to the property declaration in the contextual type - Created a new test file with comprehensive test cases for the feature
- Updated 4 existing test baselines that now correctly match TypeScript's reference behavior
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| internal/ls/definition.go | Added logic to detect binding elements in object binding patterns and navigate to their property declarations in the contextual type using GetTypeAtLocation and GetPropertyOfType |
| internal/fourslash/tests/goToDefinitionObjectBindingPattern_test.go | New test file with test cases covering function parameters, variable declarations, and object literals with destructuring |
| testdata/baselines/reference/fourslash/goToDefinition/goToDefinitionObjectBindingPattern.baseline.jsonc | New baseline file for the custom test, showing correct navigation to interface properties |
| testdata/baselines/reference/submodule/fourslash/goToDefinition/gotoDefinitionInObjectBindingPattern1.baseline.jsonc | Updated baseline - now correctly navigates to interface property instead of binding element |
| testdata/baselines/reference/submodule/fourslash/goToDefinition/gotoDefinitionInObjectBindingPattern1.baseline.jsonc.diff | Removed - test now matches TypeScript reference |
| testdata/baselines/reference/submodule/fourslash/goToDefinition/gotoDefinitionInObjectBindingPattern2.baseline.jsonc | Updated baseline - now correctly navigates to interface properties |
| testdata/baselines/reference/submodule/fourslash/goToDefinition/gotoDefinitionInObjectBindingPattern2.baseline.jsonc.diff | Removed - test now matches TypeScript reference |
| testdata/baselines/reference/submodule/fourslash/goToDefinition/goToDefinitionDynamicImport3.baseline.jsonc | Updated baseline - now correctly navigates to export declarations in dynamic imports |
| testdata/baselines/reference/submodule/fourslash/goToDefinition/goToDefinitionDynamicImport3.baseline.jsonc.diff | Removed - test now matches TypeScript reference |
| testdata/baselines/reference/submodule/fourslash/goToDefinition/goToDefinitionDynamicImport4.baseline.jsonc | Updated baseline - now correctly navigates to export declarations in dynamic imports |
| testdata/baselines/reference/submodule/fourslash/goToDefinition/goToDefinitionDynamicImport4.baseline.jsonc.diff | Removed - test now matches TypeScript reference |
| if ast.IsPropertyName(node) && ast.IsBindingElement(node.Parent) && ast.IsObjectBindingPattern(node.Parent.Parent) { | ||
| bindingEl := node.Parent.AsBindingElement() | ||
| if node == core.OrElse(bindingEl.PropertyName, node.Parent.Name()) { | ||
| if name, ok := ast.TryGetTextOfPropertyName(node); ok { | ||
| t := c.GetTypeAtLocation(node.Parent.Parent) | ||
| types := []*checker.Type{t} | ||
| if t.IsUnion() { | ||
| types = t.Types() | ||
| } | ||
| var result []*ast.Node | ||
| for _, unionType := range types { | ||
| if prop := c.GetPropertyOfType(unionType, name); prop != nil { | ||
| result = append(result, prop.Declarations...) | ||
| } | ||
| } | ||
| return result | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The implementation should check if the BindingElement is a rest element (has DotDotDotToken) before attempting to navigate to property declarations. Rest elements like const { ...rest } = obj capture all remaining properties and should not navigate to a specific property in the type definition. Add a check: if bindingEl.DotDotDotToken != nil { return nil } or similar early in the block.
There was a problem hiding this comment.
Done in d4ec7c7 — added bindingEl.DotDotDotToken == nil guard so rest elements like { ...rest } skip the property lookup.
| const content = `interface SomeType { | ||
| [|/*targetPropertyDef*/targetProperty|]: number; |
There was a problem hiding this comment.
| const content = `interface SomeType { | |
| [|/*targetPropertyDef*/targetProperty|]: number; | |
| const content = ` | |
| interface SomeType { | |
| targetProperty: number; |
There was a problem hiding this comment.
Done in d4ec7c7 — added newline after backtick and semicolons in the interface.
internal/ls/definition.go
Outdated
| // If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the | ||
| // declaration of the symbol (which is itself), we should try to get to the original type of the | ||
| // ObjectBindingPattern and return the property declaration for the referenced property. | ||
| // For example: | ||
| // import('./foo').then(({ bar }) => undefined); => should navigate to the declaration in file "./foo" | ||
| // | ||
| // function bar<T>(onfulfilled: (value: T) => void) { } | ||
| // interface Test { prop1: number } | ||
| // bar<Test>(({ prop1 }) => {}); => should navigate to prop1 in Test |
There was a problem hiding this comment.
Add a newline, move the comment into the if check as is done above.
There was a problem hiding this comment.
Done in d4ec7c7 — added blank line before the if block and moved the comment inside it.
| } | ||
| node = getDeclarationNameForKeyword(node) |
There was a problem hiding this comment.
| } | |
| node = getDeclarationNameForKeyword(node) | |
| } | |
| node = getDeclarationNameForKeyword(node) |
internal/ls/definition.go
Outdated
| // bar<Test>(({ prop1 }) => {}); => should navigate to prop1 in Test | ||
| if ast.IsPropertyName(node) && ast.IsBindingElement(node.Parent) && ast.IsObjectBindingPattern(node.Parent.Parent) { | ||
| bindingEl := node.Parent.AsBindingElement() | ||
| if node == core.OrElse(bindingEl.PropertyName, node.Parent.Name()) { |
There was a problem hiding this comment.
I don't think I see any tests where destructuring re-names the property. Add the following to the newly-added test
let { /*5*/targetProperty: /*6*/alias_1 }: SomeType = { targetProperty: 42 };
let { x: { /*7*/targetProperty: /*8*/{} }: { x: SomeType } = { x: { targetProperty: 42 } };I'd expect marker 8 not to go anywhere.
There was a problem hiding this comment.
Done in d4ec7c7 — added markers 5–8. Results in baseline: 5 → SomeType.targetProperty, 6 → binding element declaration for alias_1, 7 → SomeType.targetProperty, 8 → no definition (empty ObjectBindingPattern).
| let { [|/*2*/targetProperty|] }: SomeType = { /*3*/targetProperty: 42 };` | ||
| f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) | ||
| defer done() | ||
| f.VerifyBaselineGoToDefinition(t, true, "1", "2", "3", "4") |
There was a problem hiding this comment.
Don't hard-code these in - look at how other tests baseline with a call to Markers() or whatever it is.
There was a problem hiding this comment.
Done in d4ec7c7 — using f.MarkerNames()... now.
Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com>
| // interface Test { prop1: number } | ||
| // bar<Test>(({ prop1 }) => {}); => should navigate to prop1 in Test | ||
| bindingEl := node.Parent.AsBindingElement() | ||
| if bindingEl.DotDotDotToken == nil && node == core.OrElse(bindingEl.PropertyName, node.Parent.Name()) { |
There was a problem hiding this comment.
If we're checking for ...bindings, we should add another test. The newly-added one is getting a bit long, so add a separate test in the same file.
There was a problem hiding this comment.
Done in 9e89742 — added TestGoToDefinitionObjectBindingPatternRest as a separate test in the same file. It verifies that let { ...rest }: SomeType = {...} navigates to the rest binding element declaration (not a type property), confirming the DotDotDotToken guard works correctly.
Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com>
getDeclarationsFromLocationininternal/ls/definition.gogoToDefinitionObjectBindingPattern_test.go)SkipIfFailingin gen test files; keep tests out offailingTests.txtto enable themifblock, add blank lines for readabilityDotDotDotToken == nilguard to skip rest elementsf.MarkerNames()...TestGoToDefinitionObjectBindingPatternResttest covering...restspread binding (verifies rest elements navigate to the binding element itself, not a type property)Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.