IntelliJ-Luanalysis
IntelliJ-Luanalysis copied to clipboard
Extending @class definitions with functions outside of the class scope
Environment
name | version |
---|---|
IDEA version | IC2020.3 |
Luanalysis version | 1.2.2 |
OS | MacOS Big Sur |
What are the steps to reproduce this issue?
---@param _myObject MyObject
local function outerFunction1(_myObject)
_myObject.variableE = true -- No such member 'variableE' found on type 'MyObject'
end
---@param _myObject any
local function outerFunction2(_myObject)
_myObject.variableF = true
end
local function createMyObject()
local myObject = {} ---@class MyObject
myObject.variableA = true
local function localFunction()
myObject.variableB = true
end
function myObject.dotFunction()
myObject.variableC = true
end
function myObject:colonFunction()
self.variableD = true
end
outerFunction1(myObject)
outerFunction2(myObject)
return myObject
end
local myObject = createMyObject()
print(myObject.variableA)
print(myObject.variableB)
print(myObject.variableC)
print(myObject.variableD)
print(myObject.variableE) -- No such member 'variableE' found on type 'MyObject'
print(myObject.variableF) -- No such member 'variableF' found on type 'MyObject'
What happens?
There seems to be no way to extend a @class
definition by calling external functions. In my codebase these kind of structures are used a lot to implement interface-like functionality.
As for implementation, here's two quick thoughts.
The first idea would consist of following the function calls inside the @class
scope where the class instance is given as parameter. In the example above this would mean treating the outerFunction1
body the same as createMyObject
. (although it might need an additional flag on the outerFunction1
definition to prevent the No such member 'variableE' found on type 'MyObject'
error
Second idea would be 'sibling' class support, working similar to the current super classes. Could be defined something like ---@class PotionItem : Item & SellableItem & BuyableItem & HealingItem
. Ideally this would come with an additional flag on the outer function as well to define a parameter as a new class.
Below is an example of what I mean by adding an interface structure.
An instance of a specific item, called CAKE_RECOVER_ENERGY_1
local t = {}
function t:new()
local item = abstractRecoverEnergyCakeItem:new() ---@class CAKE_RECOVER_ENERGY_1 : abstractRecoverEnergyCakeItem
buyableShopItemPlugin:install(item)
sellableShopItemPlugin:install(item)
function item:getGoldCost()
return 100
end
function item:getGoldSellPrice()
return 50
end
function item:getRecoverEnergyValue(_monster)
return 20
end
return item
end
return t
An outer function which adds certain functionality to all kinds of abstractItems, for example sellableShopItemPlugin
local t = {}
---@param item abstractItem
function t:install(item)
item.classes["sellableShopItemPlugin"] = true
---@return number
function item:getGoldSellPrice() -- To be overridden
return 0
end
function item:onActionButton(_screenParams, _mode, _onItemUpdate)
shopItemOverlayBuilder:new(self, _mode, function(_amount)
local itemPrice = self:getGoldSellPrice()
-- some logic to remove currency from the player (_amount * itemPrice)
end)
end
end
return t
Wouldn't you simply be able to declare it (which also nicely gives it a type) ?
---@class MyObject
---@field variableE boolean
local myObject = {}
function myObject:foobar() ... end
@pouwelsjochem, there may be cases where this doesn't work. However, what @adriweb has described is the preferred workflow. The motivation being that to benefit fully from static type checking you don't want to be able to easily/accidentally extend types.
Your second proposal is better, as it's explicit, however I don't believe it's necessary in this particular case.
Wouldn't you simply be able to declare it (which also nicely gives it a type) ?
---@class MyObject ---@field variableE boolean local myObject = {} function myObject:foobar() ... end
The interfaces/plugins I use are installed on many classes. I use this method since 'super inheritance' does not always suffice when coming up with some types of classes. It's a common practice. Some of my interfaces/plugins add 10+ method, so defining them all with fields wouldn't be viable.
These implementations are not much different than defining them within the class scope as seen in the example. But in a re-usable way.
myObject.variableA = true
local function localFunction()
myObject.variableB = true
end
function myObject.dotFunction()
myObject.variableC = true
end
function myObject:colonFunction()
self.variableD = true
end
@pouwelsjochem, there may be cases where this doesn't work. However, what @adriweb has described is the preferred workflow. The motivation being that to benefit fully from static type checking you don't want to be able to easily/accidentally extend types.
Your second proposal is better, as it's explicit, however I don't believe it's necessary in this particular case.
The reply to @adriweb's quote also is applies to this quote. The issue is that the interfaces/plugins are widely installed on all kinds of classes, so just defining them on 1 class as fields would not work. I think the right terminology for this strategy would be "Multiple inheritance" or "Interfaces with default implementations". I think its a valid programming paradigm to eventually be supported in Luanalysis.
I think the right terminology for this strategy would be "Multiple inheritance" or "Interfaces with default implementations".
Being able implement multiple interfaces (or rather @shape
s) is something I'd like to add.
However, I'm not sure I understand what you mean by multiple inheritance (default implementations) given that Luanalysis is a bolt-on type system (to a dynamic runtime). It sounds more like you're after mixins, which can be expressed as implementing multiple interfaces. How those interfaces come to be implemented is out of scope for Luanalysis, as there's no compilation nor runtime component.
Ah I see, a mixin does indeed sounds like something which describes the same (I'm used to different terminology from other languages like C# and Java)
I was seeing it within the scope of Luanalysis since the myObject
instance does actually have all the properties, but the ones added outside of the direct scope are not detected. Luanalysis being able to auto detect the properties within the scope is very powerful, and my suggestion was to extend the auto detection to follow the class instance as parameter to additional scopes. Either with or without the requirement to explicitly annotate which function calls to follow. (i.e. buyableShopItemPlugin:install(item) ---@extend item
Being able implement multiple interfaces (or rather
@shape
s) is something I'd like to add.
Something like that would be awesome and would allow me to manually define my class structures.