Skip to content
Merged
5 changes: 3 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
* `CHG` Improve performance of multithreaded `--check` and `undefined-field` diagnostic
* `FIX` Addons can now self-recommend as expected. Fixed by correcting the `wholeMatch` function
* `FIX` Now correctly evaluates the visibility of fields in a class when they are defined directly in the object. use for completion and invisible dianostic. [#2752](https://github.com/LuaLS/lua-language-server/issues/2752)
* `NEW` added lua regular expression support for Lua.doc.<scope>Name [#2753](https://github.com/LuaLS/lua-language-server/pull/2753)
* `NEW` Added lua regular expression support for Lua.doc.\<scope\>Name [#2753](https://github.com/LuaLS/lua-language-server/pull/2753)
* `FIX` Bad triggering of the `inject-field` diagnostic, when the fields are declared at the creation of the object [#2746](https://github.com/LuaLS/lua-language-server/issues/2746)
* `CHG` Change spacing of parameter inlay hints to match other LSPs, like `rust-analyzer`
* `FIX` Inconsistent type narrow behavior of function call args [#2758](https://github.com/LuaLS/lua-language-server/issues/2758)

* `FIX` Improve the `missing-fields` logic to be able to correctly handle classes defined several times [#22770](https://github.com/LuaLS/lua-language-server/pull/2770)
*
## 3.9.3
`2024-6-11`
* `FIX` Sometimes providing incorrect autocompletion when chaining calls
Expand Down
75 changes: 44 additions & 31 deletions script/core/diagnostics/missing-fields.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,61 +15,74 @@ return function (uri, callback)
guide.eachSourceType(state.ast, 'table', function (src)
await.delay()

vm.removeNode(src) -- the node is not updated correctly, reason still unknown
local defs = vm.getDefs(src)
local sortedDefs = {}
for _, def in ipairs(defs) do
if def.type == 'doc.class' and def.bindSource then
if guide.isInRange(def.bindSource, src.start) then
if def.type == 'doc.class' then
if def.bindSource and guide.isInRange(def.bindSource, src.start) then
return
end
local className = def.class[1]
if not sortedDefs[className] then
sortedDefs[className] = {}
end
local samedefs = sortedDefs[className]
samedefs[#samedefs+1] = def
end
if def.type == 'doc.type.array'
or def.type == 'doc.type.table' then
return
end
end
local warnings = {}
for _, def in ipairs(defs) do
if def.type == 'doc.class' then
if not def.fields then
return
end

local requiresKeys = {}
for _, field in ipairs(def.fields) do
if not field.optional
and not vm.compileNode(field):isNullable() then
local key = vm.getKeyName(field)
if key and not requiresKeys[key] then
requiresKeys[key] = true
requiresKeys[#requiresKeys+1] = key
end
end
local warnings = {}
for _, samedefs in pairs(sortedDefs) do
local missedKeys = {}
local className
for _, def in ipairs(samedefs) do
className = def.class[1]
if not def.fields or #def.fields == 0 then
goto continue
end

if #requiresKeys == 0 then
return
end
local myKeys = {}
for _, field in ipairs(src) do
local key = vm.getKeyName(field)
local key = vm.getKeyName(field) or field.tindex
if key then
myKeys[key] = true
Copy link
Contributor

Choose a reason for hiding this comment

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

The computation of myKeys seems independent with the current nested forloop. It only depends on src, which is a function argument in the callback.

I suppose this can be optimized to execute only once using lazy initialization 😄

  • first define local myKeys outside of current nested forloop
  • then inside the loop, only compute it if myKeys == nil
local myKeys
for _, samedefs in pairs(sortedDefs) do
  ...
  for _, def in ipairs(samedefs) do
    ...
    -- build myKeys lookup table once
    if myKeys == nil then
      myKeys = {}
      for _, field in ipairs(src) do
        -- same logic
      end
    end

end
end

local missedKeys = {}
for _, key in ipairs(requiresKeys) do
if not myKeys[key] then
missedKeys[#missedKeys+1] = ('`%s`'):format(key)
end
end
for _, field in ipairs(def.fields) do
if not field.optional
and not vm.compileNode(field):isNullable() then
local key = vm.getKeyName(field)
if not key then
local fieldnode = vm.compileNode(field.field)[1]
if fieldnode and fieldnode.type == 'doc.type.integer' then
---@cast fieldnode parser.object
key = vm.getKeyName(fieldnode)
end
end

if #missedKeys == 0 then
return
if key and not myKeys[key] then
if type(key) == "number" then
missedKeys[#missedKeys+1] = ('`[%s]`'):format(key)
else
missedKeys[#missedKeys+1] = ('`%s`'):format(key)
end
end
end
end
::continue::
end

warnings[#warnings+1] = lang.script('DIAG_MISSING_FIELDS', def.class[1], table.concat(missedKeys, ', '))
if #missedKeys == 0 then
return
end

warnings[#warnings+1] = lang.script('DIAG_MISSING_FIELDS', className, table.concat(missedKeys, ', '))
end

if #warnings == 0 then
Expand Down
122 changes: 122 additions & 0 deletions test/diagnostics/missing-fields.lua
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,125 @@ local t = {
y = 1,
}
]]

TEST [[
---@diagnostic disable: unused-local

---@class Foo
---@field a number
---@field b number
---@field c number

---@class Foo

---@class Bar
---@field ba number
---@field bb number
---@field bc number

---@class Bar
---@field bd number

---@type Foo|Bar
local x = {
ba = 1,
bb = 2,
bc = 3,
bd = 4,
}
]]

TEST [[
---@diagnostic disable: unused-local

---@class Foo
---@field a number
---@field b number
---@field c number

---@class Foo

---@class Bar
---@field ba number
---@field bb number
---@field bc number

---@class Bar
---@field bd number

---@type Foo|Bar
local x = {
a = 1,
b = 2,
c = 3,
}
]]

TEST [[
---@diagnostic disable: unused-local

---@class Foo
---@field a number
---@field b number
---@field c number

---@class Foo

---@class Bar
---@field ba number
---@field bb number
---@field bc number

---@class Bar
---@field bd number

---@type Foo|Bar
local x = <!{
a = 1,
b = 2,
}!>
]]

TEST [[
---@diagnostic disable: unused-local

---@class Foo
---@field a number
---@field b number
---@field c number

---@class Foo

---@class Bar
---@field ba number
---@field bb number
---@field bc number

---@class Bar
---@field bd number

---@type Foo|Bar
local x = <!{
ba = 1,
bb = 2,
bd = 4,
}!>
]]

TEST[[
---@class A
---@field [1] string
---@field x number

---@type A
local t = {x = 1, ""}
]]

TEST[[
---@class A
---@field [1] string
---@field x number

---@type A
local t = <!{x = 1}!>
]]