IntelliJ-Luanalysis icon indicating copy to clipboard operation
IntelliJ-Luanalysis copied to clipboard

Extending @class definitions with functions outside of the class scope

Open pouwelsjochem opened this issue 3 years ago • 5 comments

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

pouwelsjochem avatar Mar 14 '21 12:03 pouwelsjochem

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

adriweb avatar Mar 14 '21 12:03 adriweb

@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.

Benjamin-Dobell avatar Mar 14 '21 12:03 Benjamin-Dobell

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.

pouwelsjochem avatar Mar 14 '21 13:03 pouwelsjochem

I think the right terminology for this strategy would be "Multiple inheritance" or "Interfaces with default implementations".

Being able implement multiple interfaces (or rather @shapes) 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.

Benjamin-Dobell avatar Mar 14 '21 13:03 Benjamin-Dobell

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 @shapes) is something I'd like to add.

Something like that would be awesome and would allow me to manually define my class structures.

pouwelsjochem avatar Mar 14 '21 14:03 pouwelsjochem