LuaLS / lua-language-server

A language server that offers Lua language support - programmed in Lua
https://luals.github.io
MIT License
3.3k stars 306 forks source link

"array-like" table can't be natively annotated as enum #2435

Open alexbalandi opened 10 months ago

alexbalandi commented 10 months ago

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?

Annotations, Hover

Expected Behaviour

On hover over action param in my function I'd like to get annotation consistent with already existing annotation that i get if i hover over all in actions.all :

(field) actions.all: {
    [10]: string = "supportAttack",
    [11]: string = "supportDefence",
    [12]: string = "supportBoth",
    [13]: string = "supportAttackPike",
    [14]: string = "supportDefencePike",
    [15]: string = "stanceShieldwall",
    [16]: string = "stanceLastman",
    [17]: string = "stanceBarrage",
    [18]: string = "encamp",
    [19]: string = "uncamp",
    [1]: string = "retreat",
    [20]: string = "destroyCamp",
    [21]: string = "buildFort",
    [22]: string = "demountFort",
    [23]: string = "destroyFort",
    [24]: string = "rest",
    [25]: string = "respire",
    [2]: string = "move",
    [3]: string = "swap",
    [4]: string = "forcedMove",
    [5]: string = "melee",
    [6]: string = "shoot",
    [7]: string = "shoot2",
    [8]: string = "breakthrough",
    [9]: string = "enclosing",
}

Essentially i'd like enum for this array to give the same behavioir as alias like this

---@alias Action
---| '"retreat"'
---| '"move"
---| '"swap"' 
---| '"forcedMove"'
---| '"melee"'
---| '"shoot"'
---| '"shoot2"'
---| '"breakthrough"
---| '"enclosing"'
---| '"supportAttack"'
---| '"supportDefence"'
---| '"supportBoth"'
---| '"supportAttackPike"'
---| '"supportDefencePike"'
---| '"stanceShieldwall"'
---| '"stanceLastman"'
---| '"stanceBarrage"'
---| '"encamp"'
---| '"uncamp"'
---| '"destroyCamp"'
---| '"buildFort"'
---| '"demountFort"'
---| '"destroyFort"'
---| '"rest"'
---| '"respire"'

Actual Behaviour

On hover over action param in my function I get this annotation:

(parameter) action: Action
{
}

Reproduction steps

Consider this minimal code snippet:

local actions = {}

---@enum Action
actions.all = {
  "retreat",
  "move",
  "swap",
  "forcedMove",
  "melee",
  "shoot",
  "shoot2",
  "breakthrough",
  "enclosing",
  "supportAttack",
  "supportDefence",
  "supportBoth",
  "supportAttackPike",
  "supportDefencePike",
  "stanceShieldwall",
  "stanceLastman",
  "stanceBarrage",
  "encamp",
  "uncamp",
  "destroyCamp",
  "buildFort",
  "demountFort",
  "destroyFort",
  "rest",
  "respire",
}

actions.isStance = {stanceShieldwall = true, stanceLastman = true, stanceBarrage = true}

--- @param action Action
--- @return boolean
function actions.IsStance(action)
  return actions.isStance[action]
end

Additional Notes

No response

Log File

No response

carsakiller commented 9 months ago

Hello :wave:

This appears to be functioning as I would expect. When you annotate actions.all as an @enum, it is remaining a simple table, the annotation has no effect on how the table should be indexed at runtime. It is exactly like a normal table, where the keys are used to index the values contained within. In order to retrieve a value, say "rest", from the actions.all table, you would have to use its index (24).

I think you are getting the usages of @enum and @alias confused. The decision to use either comes down to how you are designing your code. Here are some summaries and examples to clarify:

Alias

---@alias colors
---| "red"
---| "green"

---@param color colors
local function setColor(color)
    if color == "red" then
        print("red!")
    elseif color == "green" then
        print("green!")
    else
        print("Well that isn't an option")
    end
end

image

Enum

Excuse the heinous 0-indexing below, I've been using other languages recently 😊. 1-indexing should be used instead.


---@enum colors
local myEnum = {
red = 0,
green = 1
}

---@param color colors local function setColor(color) if color == myEnum.red then print("red!") elseif color == myEnum.green then print("green!") else print("Neither!") end end


![image](https://github.com/LuaLS/lua-language-server/assets/61925890/bb266f65-0898-4f6a-ae3d-60c3fde54d31)

Note how `0` and `1` are also suggested, as they are valid values, they just don't index `myEnum` to get the value. It is recommended to still index `myEnum` to get the value, however, as it allows you to change the value in one place and have it "update" everywhere!

---
Notice how the two code examples given are for a very similar use case. It is often the case that you can choose to use either, it comes down to how you are designing your code.

And to summarize their similarities:
**An `@enum` and `@alias` are similar in the fact that they exist purely for the user and to make their life easier. An enum "actually" exists though 🙂**
alexbalandi commented 9 months ago

Thank you very much for your answer, @carsakiller !

I have refactored the code quite a bit since then, now this part of code looks like this:

---@enum (key) ActionName
local enumed_action_names = {
  retreat = 1,
  move = 2,
  swap = 3,
  forcedMove = 4,
  melee = 5,
  shoot = 6,
  shoot2 = 7,
  breakthrough = 8,
  enclosing = 9,
  supportAttack = 10,
  supportDefence = 11,
  supportBoth = 12,
  supportAttackPike = 13,
  supportDefencePike = 14,
  stanceShieldwall = 15,
  stanceLastman = 16,
  stanceBarrage = 17,
  encamp = 18,
  uncamp = 19,
  destroyCamp = 20,
  buildFort = 21,
  demountFort = 22,
  destroyFort = 23,
  rest = 24,
  respire = 25,
  jointAttack = 26,
}

---@type ActionName[]
actions.all = {}

---@param actionName ActionName
for actionName, _ in pairs(enumed_action_names) do
  table.insert(actions.all, actionName)
end

With this I get exactly the annotation I want on hover:

(enum) ActionName
"retreat" | "move" | "swap" | "forcedMove" | "melee" | "shoot" | "shoot2" | "breakthrough" | "enclosing" | "supportAttack" | "supportDefence" | "supportBoth" | "supportAttackPike" | "supportDefencePike" | "stanceShieldwall" | "stanceLastman" | "stanceBarrage" | "encamp" | "uncamp" | "destroyCamp" | "buildFort" | "demountFort" | "destroyFort" | "rest" | "respire" | "jointAttack"

But the downside here is that right now the actual structure that I use in my code aka actions.all has to be created in a cycle after I create an object enumed_action_names as the workaround to let me have both data structure and alias created dynamically. Not too heavy on resources, but a bit clunky.

I think you're right in pointing out that what I really need here is not enum but alias set from an object defined at runtime. I just looked at enum because it did almost what I wanted (and as you see - well, I can actually use enum in current way to get essentially a dynamically created alias :sweat_smile: , but at the cost of some extra work)

So that I would use a notation like

---@alias (table) ActionName
actions.all = {
  "retreat",
  "move"
}

To get the behavior I'm getting right now.

This would create an alias from the table values at the moment of their definition in code (so this way alias remains what it already is - static definition).

I could take it upon myself to implement this and suggest as PR, I probably would need some pointers to contribution guidelines (I lurked a bit, but didn't quite find those, although obv I can just look at current PRs/tests/etc).