Skip to content

Fix go-to-definition for binding elements in ObjectBindingPattern#2842

Open
Copilot wants to merge 5 commits intomainfrom
copilot/fix-go-to-definition-issue-again
Open

Fix go-to-definition for binding elements in ObjectBindingPattern#2842
Copilot wants to merge 5 commits intomainfrom
copilot/fix-go-to-definition-issue-again

Conversation

Copy link
Contributor

Copilot AI commented Feb 19, 2026

  • Add handling for BindingElement within ObjectBindingPattern in getDeclarationsFromLocation in internal/ls/definition.go
  • Create a test file for the new behavior (goToDefinitionObjectBindingPattern_test.go)
  • Generate and accept the baseline file for the new test
  • Update submodule test baselines that are now correctly fixed (4 tests now match TypeScript reference)
  • Restore SkipIfFailing in gen test files; keep tests out of failingTests.txt to enable them
  • Address reviewer feedback:
    • Move block comment inside the if block, add blank lines for readability
    • Add DotDotDotToken == nil guard to skip rest elements
    • Add newline after backtick, add semicolons to interface property in test
    • Add renaming test cases (markers 5-8): renamed property, local alias, nested destructuring, empty binding pattern
    • Replace hardcoded marker names with f.MarkerNames()...
    • Regenerate baseline with all 8 markers
  • Add separate TestGoToDefinitionObjectBindingPatternRest test covering ...rest spread binding (verifies rest elements navigate to the binding element itself, not a type property)
Original prompt

This section details on the original issue you should resolve

<issue_title>Cannot "go to definition" of a playwright fixture</issue_title>
<issue_description>## Steps to reproduce

  1. Clone the repository at https://github.com/david-guevara-gorillalogic/tsgo-playwright-issue.git

    This an standard playwright repository with a custom fixture:

    export const test = base.extend<{
      helper: () => Promise<void>;
    }>({
      helper: async ({ page }, use) => {
        await use(async () => {
          await page.goto("https://playwright.dev/");
          await expect(page).toHaveTitle(/Playwright/);
        });
      },
    });
  2. Open a test file that uses that custom fixture:

    test("has title", async ({ helper }) => {
      await helper();
    });
  3. Move your cursor over the helper fixture and try to "go to definition".

  4. Your editor won't go to the definition inside the fixtures file, at most it will move to the parameter { helper }.

Behavior with typescript@6.0

The editor will go to the definition inside the fixture file.

Behavior with tsgo

The editor won't go to the definition inside the fixtures file, at most it will move to the parameter { helper }.

Notes

I tested this with neovim and VSCode and was able to reproduce the issue in both.</issue_description>

Comments on the Issue (you are @copilot in this section)

@DanielRosenwasser I do get references in VS Code, but only because of the default `editor.gotoLocation.alternativeDefinitionCommand`. Image

Basically I think tsgo sees this as a definition, and the editor instead decides to retry as find-all-references.

Here's a more minimal repro:

interface SomeType {
    targetProperty: number
}

function foo(callback: (p: SomeType) => void) {
}

foo(({ /*1*/targetProperty }) => {
    /*4*/targetProperty
})

let { /*2*/targetProperty }: SomeType = { /*3*/targetProperty: 42 }

A test can just baseline go-to-definition on all markers. A fix should witness a change in go-to-definition on targets /*1*/ and /*2*/.</comment_new>


💡 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.

Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix go to definition issue for playwright fixture Fix go-to-definition for binding elements in ObjectBindingPattern Feb 19, 2026
)

func TestGotoDefinitionInObjectBindingPattern1(t *testing.T) {
fourslash.SkipIfFailing(t)
Copy link
Member

Choose a reason for hiding this comment

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

@copilot Instead of just deleting these, you need to modify the failingtests.txt file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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>
@DanielRosenwasser DanielRosenwasser marked this pull request as ready for review February 20, 2026 00:35
Copilot AI review requested due to automatic review settings February 20, 2026 00:35
Copy link
Contributor

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 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 getDeclarationsFromLocation for 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

Comment on lines 227 to 245
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
}
}
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in d4ec7c7 — added bindingEl.DotDotDotToken == nil guard so rest elements like { ...rest } skip the property lookup.

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.

Comment on lines 13 to 14
const content = `interface SomeType {
[|/*targetPropertyDef*/targetProperty|]: number;
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
const content = `interface SomeType {
[|/*targetPropertyDef*/targetProperty|]: number;
const content = `
interface SomeType {
targetProperty: number;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in d4ec7c7 — added newline after backtick and semicolons in the interface.

Comment on lines 218 to 226
// 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
Copy link
Member

Choose a reason for hiding this comment

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

Add a newline, move the comment into the if check as is done above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in d4ec7c7 — added blank line before the if block and moved the comment inside it.

Comment on lines 245 to 246
}
node = getDeclarationNameForKeyword(node)
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
}
node = getDeclarationNameForKeyword(node)
}
node = getDeclarationNameForKeyword(node)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in d4ec7c7.

// 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()) {
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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")
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 hard-code these in - look at how other tests baseline with a call to Markers() or whatever it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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()) {
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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>
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.

Cannot "go to definition" of a playwright fixture

2 participants

Comments