LuaLS / lua-language-server

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

fix: improve function type narrow by checking params' literal identical #2822

Closed tomlau10 closed 2 months ago

tomlau10 commented 2 months ago

The following related issues are not fixed by this PR, but I tested and they already work as of v3.10.5: ~#2509~, #1343, #1146

edit: I originally thought #2509 is working, but NO. Just that the reproduction code is not complete, and I didn't reproduce it and think it is solved. I confirmed that it is still unsolved with a more complete example code.

The Problem

Originated from an example provided by @carsakiller: https://github.com/LuaLS/lua-language-server/issues/1456#issuecomment-2303627852, where the base signature contains a param type that is a superset of the other overload, type narrow feature will always includes the baseline one and cannot perform a more accurate type narrow.

A minimal example:

---@overload fun(a: string): string
---@overload fun(a: 'test'): integer
local function test(...) end

local a = test('')      -- string, good
local b = test('test')  -- string|integer, bad
local s = 'test'
local c = test(s)       -- string|integer, bad

Proposed Solution

I introduced a match score when trying to do a further type narrow. After the 1st pass checking isAllParamMatched in the existing logic, I tried to check if their arguments' literal have exact match:

This is just an extra logic and should be no worse than current logic. Because when there are no identical view param, all functions just have the same score 0 => all will be kept as in the existing logic.

After the fix:

---@overload fun(a: string): string
---@overload fun(a: 'test'): integer
local function test(...) end

local a = test('')      -- string, good
local b = test('test')  -- integer, good
local s = 'test'
local c = test(s)       -- integer, good

中文版

長話短說,我嘗試在 vm.getExactMatchedFunctions 引入1個 計分制 在滿足原有的 isAllParamMatched 前題下

我不確定有沒有更好改法 這個需要 @sumneko 來 review (希望我沒有像上次一樣,fix 了1個問題卻又引起新 bug ... 🤦‍♂️ )

tomlau10 commented 2 months ago

Seems my latest strategy of checking param's literals map contains arg's literal is not working for carsakiller's example... Because the base function signatures uses an @alias type, and the literals map of it will contains all enum literals value🤦‍♂️

So the logic will add bonus score for both the base function and the overload one => they have same bonus score => cannot do further type narrow ☹️


由於 @aliasliterals map 實際上是包含其定義中的所有 literal 所以我最新 logic 下的 檢查 function param literals map 包含 arg's liteval 值 對 carsakiller 原有例子沒效果。。。

---@alias ccTweaked.os.event
---| "alarm"
---| "char"
---| string

---@async
---@param event? ccTweaked.os.event
---@return any ...
---@overload fun(event: "alarm"): "alarm", integer
---@overload fun(event: "char"): "char", string
function os.pullEvent(event) end

local event, alarmID = os.pullEvent("alarm")
  --> any, any (expected: "alarm", integer)
local event, character = os.pullEvent("char")
  --> any, any (expected: "char", string)

要再想想先。。。

tomlau10 commented 2 months ago

想到了 🙂 計分制中不直接做 + 1,而是 + 1/{literals map 的 entry 總數}

這樣就傾向選擇 param literal type 定義為較 narrow 的 function 了

---@alias ccTweaked.os.event
---| "alarm"
---| "char"
---| string

---@async
---@param event? ccTweaked.os.event
---@return any ...
---@overload fun(event: "alarm"): "alarm", integer
---@overload fun(event: "char"): "char", string
function os.pullEvent(event) end

local event, alarmID = os.pullEvent("alarm")
  --> "alarm", integer (good)
local event, character = os.pullEvent("char")
  --> "char", string (good)
tomlau10 commented 2 months ago

先 hold 一下,我突然又有些想法估計可以 fix https://github.com/LuaLS/lua-language-server/issues/2509 🤔

要些時間驗證一下


edit: 還是分開另外處理吧 不然出 bug 了都不知是跟哪個改動有關 😅

sumneko commented 2 months ago

我没有看代码,但是看描述我觉得无论什么时候都不应该用计分。如果无法确定如何narrow,那就不做narrow。

tomlau10 commented 2 months ago

无法确定如何narrow,那就不做narrow

我這裡的計分,最初其實是 計算 exact literal match 的 param count (count 是數量,也就可理解成 1個 score) 並且是在 肯定的時候加分,否則就當 0分 🤔

讓我具體舉一些例子

例子1

---@overload fun(a: string): string
---@overload fun(a: 'test'): integer
local function f(...) end

例子2

---@alias ccTweaked.os.event
---| "alarm"
---| "char"
---| string

---@async
---@param event? ccTweaked.os.event
---@return any ...
---@overload fun(event: "alarm"): "alarm", integer
---@overload fun(event: "char"): "char", string
function os.pullEvent(event) end

我大概理解你的擔心,是以為這計分制是 (胡亂) 計算什麼 similarity 之類? 但這裡不是,是在 肯定 (出現 exact literal value match) 的情況下做加分,其餘 case 都是 0 分 沒有 literal match 的情況下,不會給任何分數的 🤔 (而與期說分數,更像是 count 有多少 literal value match) @sumneko

sumneko commented 2 months ago

明白了,这里的计分指的是narrow程度,那应该没有问题

tomlau10 commented 2 months ago

这里的计分指的是narrow程度

確實你這個說法更準確 👍