lua-language-server icon indicating copy to clipboard operation
lua-language-server copied to clipboard

Go to definition does not go to definition

Open Real-Gecko opened this issue 5 months ago • 8 comments

How are you using the lua-language-server?

Visual Studio Code Extension (sumneko.lua)

Which OS are you using?

Linux

What is the issue affecting?

Libraries

Expected Behaviour

Following https://github.com/LuaLS/lua-language-server/discussions/3219 I've figured out that Go to definition does not act properly even if variable is set as local explicitly, see the video for details

Actual Behaviour

https://github.com/user-attachments/assets/cd98b077-bd11-461b-aa2c-78b375087dbf

Reproduction steps

I've created a repo with the code used in a video so you can clone and test it yourself https://github.com/Real-Gecko/lua-ls-test

Additional Notes

No response

Log File

No response

Real-Gecko avatar Jul 04 '25 09:07 Real-Gecko

I am not sure but I guess assigning value to a field (setfield operation) is a kind of definition of that field? 😕 So this is the normal behavior? (at least I used to this behavior LOL)

this is also true for local var, a minimal example

local A
A = 1

print(A) --< use goto definition on `A` here
--> will list both lines of `local A` and `A = 1`

tomlau10 avatar Jul 04 '25 12:07 tomlau10

Hmm, I guess you're confusing Go to definition from class instance and Go to definition from actual class definition. I've recorded a video of working with Python "version" of the same scripts. But I can say for sure that this behavior is the same for any PL in VSCode.

https://github.com/user-attachments/assets/eeea6261-24e7-40b1-be43-47a94c5b8061

Real-Gecko avatar Jul 07 '25 05:07 Real-Gecko

Same for Java

https://github.com/user-attachments/assets/f6798ec4-f9e1-4130-9a26-4af4f6a75e6f

Real-Gecko avatar Jul 07 '25 05:07 Real-Gecko

Hmm, I guess you're confusing Go to definition from class instance and Go to definition from actual class definition.

Yeah, maybe you are right 😕 I am actually quite confused now after viewing your screen recording 🙈 .

In particular, I see that you demoed both goto definition and goto declaration. 🤔 Just my point of view, seems what you want is a goto declaration => where the variable / field is first declared ?

However, I duno if LuaLS support this or not 😕 In my own usage experience, goto definition in LLS has long been like this: showing all setter as definitions.

I tried to ask an AI and here is how sonnet 4 replies

  • prompt

In vscode's lua-language-server extension, when I use "goto definition" on a variable, it shows results including all assignment statement of this var as well. Is this reasonable?

  • sonnet 4's reply
Click to show

This behavior is actually reasonable and typical for the Lua Language Server, though it might feel different if you're coming from statically-typed languages.

Why this happens in Lua:

  1. Dynamic typing: In Lua, variables don't have fixed types, so any assignment could potentially change the "definition" of what a variable represents.

  2. No explicit declarations: Unlike languages with var, let, or type declarations, Lua variables are implicitly created on first assignment.

  3. Scope complexity: Lua's scoping rules mean that what looks like the "same" variable might actually be different variables in different scopes.

Example why this is useful:

local myVar = "initial string"    -- Assignment 1
-- ... some code ...
myVar = {data = "table"}          -- Assignment 2 - completely different type
-- ... more code ...
myVar = function() end            -- Assignment 3 - now it's a function

When you "goto definition" on a usage of myVar, seeing all assignments helps you understand:

  • What type the variable might be at that point
  • How the variable's purpose has evolved
  • Which assignment is most relevant to your current context

Many Lua developers find seeing all assignments helpful for understanding the code flow, especially in a dynamically-typed language where variable "meaning" can change throughout execution.


PS: I'm not maintainer of LLS, maybe this issue should be answered by authors of this extension 🤔 🤔

tomlau10 avatar Jul 07 '25 09:07 tomlau10

In particular, I see that you demoed both goto definition and goto declaration. 🤔 Just my point of view, seems what you want is a goto declaration => where the variable / field is first declared ?

Sorry, my screen capture software is dumb and sometimes skips frames, so after rewatching videos I've noticed the source of confusion: sometimes it seems as if I'm clicking Go to definition but in fact I was pressing Go to declaration(also F12 and Ctrl+Click).

  • Dynamic typing: In Lua, variables don't have fixed types, so any assignment could potentially change the "definition" of what a variable represents.

  • No explicit declarations: Unlike languages with var, let, or type declarations, Lua variables are implicitly created on first assignment.

  • Scope complexity: Lua's scoping rules mean that what looks like the "same" variable might actually be different variables in different scopes.

Yeah, all the same concepts both apply to Python and Javascript, but both Python LS and JS LS go to actual definition of the field if I Ctrl+Click on a field of class instance. And yes, if you Ctrl+Click on field declaration(like in place where your class is defined) it shows you all the places this var is used in, which can be really convenient when trying to understand where this particular field is used.

Real-Gecko avatar Jul 07 '25 09:07 Real-Gecko

Here's JS example of the same code, note absence of type definitions

https://github.com/user-attachments/assets/022e68fe-114e-4d9d-9eb1-755b5e6337cc

Real-Gecko avatar Jul 07 '25 09:07 Real-Gecko

I can understand your standpoint, that you are trying to use the behavior of LS of other languages to justify that the current behavior of LuaLS is wrong/incorrect. I have no experience in implementing a LS, so I cannot give comment on that 😕

Then I resorted to chatting with sonnet4 🙈

Here is the summary of my conversation with it

Details

Summary: LSP "Go to Definition" vs "Go to Declaration" Implementation

Core Question: How should a Lua language server handle "go to definition" and "go to declaration" for variables?

Key Learnings:

  1. Basic Distinction:

    • Declaration: Where variable enters scope (local a)
    • Definition: Where variable gets its (initial) value (a = 1)
  2. Lua-Specific Behavior:

    • When declaration includes initialization (local a = 1): both commands → same location
    • When separate (local a then a = 1): declaration → local a, definition → a = 1
    • When no initial value: definition falls back to declaration
  3. Multiple Definitions:

    • lua-language-server shows multiple locations (e.g., both local a = 1 and a = 2)
    • This is not wrong - it's sophisticated data-flow analysis
    • Provides more context for understanding variable's value chain
  4. Implementation Options:

    • Simple: Both commands go to declaration
    • Standard: Declaration → local, Definition → first assignment
    • Advanced: Show multiple relevant locations (like lua-language-server)

Takeaway: The LSP spec is flexible, and showing multiple definitions can be more helpful than forcing arbitrary choices.

Here is the full conversation if you are interested 👀

Click to show

LSP "Goto" Feature in Lua

User:

i am trying to implement a language-server following the lsp defined by vscode. but i am confused about the "goto definition" and "goto declaration" feature. what should be the behavior of the above, when performed on the variable a on line3 in the following given lua snippet?

local a = 1
a = 2
print(a) --< use goto xxx on `a` on this line
Assistant:

Great question! The distinction between "goto definition" and "goto declaration" can be confusing, especially in languages like Lua. Let me clarify the expected behavior for your example:

local a = 1  -- Line 1: Declaration AND definition
a = 2        -- Line 2: Assignment/reassignment  
print(a)     -- Line 3: Usage (where you're triggering goto)

Expected Behavior:

Both "Go to Definition" and "Go to Declaration" should go to Line 1 (local a = 1)

Why?

In Lua (and most dynamically typed languages), the distinction between declaration and definition is less meaningful than in languages like C/C++:

  • Declaration: Where the variable is first introduced/declared in scope
  • Definition: Where the variable gets its value/implementation

In your example:

  • Line 1 is both the declaration (local a) AND the initial definition (= 1)
  • Line 2 is just a reassignment, not a new declaration or definition

Language-Specific Considerations:

For C/C++, you might have:

extern int a;        // Declaration
int a = 1;          // Definition

But in Lua, variables are typically declared and defined simultaneously with local.

Implementation Recommendation:

For your Lua language server, I'd suggest:

  1. Both commands should navigate to the first occurrence where the variable enters the current scope
  2. For local variables, this is the local statement
  3. For global variables, this might be the first assignment in the file/project

This approach aligns with how most LSP implementations handle dynamically typed languages and provides the most useful behavior for developers.

User:

you said "Definition: Where the variable gets its value/implementation" and on line 2 of my snippet, the variable also gets its value through reassignment why isn't this line considered as a kind of definition?

further more, consider the following slightly modified snippet:

local a
a = 1
a = 2
print(a) --< use goto xxx on this line

what should be the behavior now?

Assistant:

Excellent point! You're absolutely right to question this. Let me clarify the nuanced distinction:

Terminology Clarification:

You're correct that technically, line 2 (a = 2) is also giving the variable a value. The key distinction in LSP context is:

  • Declaration: Where the variable is first introduced into scope
  • Definition: Where the variable gets its initial value/implementation
  • Assignment: Subsequent value changes

For Your Modified Example:

local a      -- Line 1: Declaration only
a = 1        -- Line 2: First assignment (initial definition)  
a = 2        -- Line 3: Reassignment
print(a)     -- Line 4: Usage

Expected behavior:

  • Go to Declaration → Line 1 (local a)
  • Go to Definition → Line 2 (a = 1) - the first assignment that gives it a meaningful value

Why This Distinction Matters:

In your modified example, there's now a meaningful difference:

  • Line 1 introduces the variable name into scope but doesn't give it a usable value
  • Line 2 is where the variable first becomes "defined" with an actual value
  • Line 3 is just another assignment

Real-World LSP Behavior:

Different language servers handle this differently:

  • TypeScript/JavaScript: Often treats the first assignment as definition
  • Python: Similar behavior with uninitialized variables
  • Some LSPs: Make both commands go to declaration for simplicity

My Recommendation:

For your Lua LSP implementation:

  1. Go to Declaration: Always go to the local statement
  2. Go to Definition: Go to the first assignment that gives it a value (if declaration doesn't include initialization)

This provides maximum utility - developers can choose whether they want to see where the variable was introduced or where it first got a meaningful value.

User:

more questions:

  • if the variable doesn't get initial value, what would be the behavior of "goto definition" ?
local a
print(a) --< trigger goto definition of 'a'
  • I used lua-language-server vscode's extension, where goto definition of a will shows both locations local a = 1 and a = 2 does it implemented this feature wrongly?
Assistant:

Great questions! These highlight the complexity and varying interpretations of LSP behavior.

Question 1: Variable without initial value

local a
print(a) --< trigger goto definition of 'a'

Expected behavior: "Go to Definition" should fall back to the declaration (local a) since there's no separate definition.

Rationale: The variable is declared but never explicitly assigned, so the declaration IS the only definition available.

Question 2: lua-language-server showing multiple locations

The lua-language-server showing both local a = 1 and a = 2 is NOT wrong - it's actually a more sophisticated approach!

Why Multiple Results Make Sense:

local a = 1  -- Original definition
a = 2        -- Redefinition that changes the value
print(a)     -- Which definition is relevant?

Arguments for showing multiple locations:

  1. Data flow analysis: Both assignments affect the value that print(a) will see
  2. Code understanding: Developers often want to see all places where a variable gets defined
  3. Debugging: Understanding the variable's "definition chain" is valuable

LSP Specification Flexibility:

The LSP spec doesn't strictly define what constitutes "the definition" - it allows for:

  • Single location (most common)
  • Multiple locations (like lua-language-server)
  • Contextual decisions based on data flow

Industry Examples:

  • Rust Analyzer: Sometimes shows multiple definitions for trait implementations
  • TypeScript: May show multiple locations for overloaded functions
  • Python: Some LSPs show all assignments in certain contexts

Conclusion:

lua-language-server's behavior is actually quite sophisticated and user-friendly. It's providing more information rather than forcing the LSP to make an arbitrary choice about which definition is "the one."

For your implementation, you could:

  1. Simple approach: Always go to declaration
  2. Advanced approach: Implement data-flow analysis like lua-language-server
  3. Hybrid: Provide configuration options for user preference

In short, the LSP spec doesn't strictly define what constitutes "the definition", so it says LuaLS returning multiple locations, including those assign/reassign statements, is NOT wrong 🤔

Moreover, it distinguishes definition as assignment to that variable, while declaration as introducing variable into scope. Thus your expected goto definition behavior is more like a goto declaration (location where this member field is declared) to me 🧐 .

Either way, as a LuaLS user I used to the current goto definition behavior. If this is going to be changed (or "fixed" if you wish to say it this way), I hope that there can be an option to toggle this behavior:

  • the default one is including all assignment locations
  • the other one is your expected behavior, which only shows the first declaration

That will be a win-win 😄

tomlau10 avatar Jul 07 '25 12:07 tomlau10

Moreover, it distinguishes definition as assignment to that variable, while declaration as introducing variable into scope. Thus your expected goto definition behavior is more like a goto declaration (location where this member field is declared) to me 🧐 .

Totally makes sense 🤔 , however other LS work the way I showed, that's why I'm confused, initially I thought it's due to variable being global, but turns out it's not the case.

Real-Gecko avatar Jul 07 '25 13:07 Real-Gecko