Search code examples
lualuacheck

How to avoid luacheck warning "setting undefined field of global"


When writing Lua code, I felt that the built-in methods for string were a bit lacking. So I added this function to the string class.

---@param self string
function string:firstToUpper() -- setting undefined field 'firstToUpper' of global 'string'string
    return (self:gsub("^%l", string.upper))
end

However, when running luacheck, it reported warning: (W142) setting undefined field firstToUpper of global string. I don't want to add an ignore rule in my .luacheckrc file. What changes should I make to my code to prevent this error?

I have try to write in this way

local mt = getmetatable("")
mt.__index.firstToUpper = function(self)
    return (self:gsub("^%l", string.upper))
end

it does not report this warning, but I have not seen this kind of writing before, my question is:

is "" a table when handle it in Lua interpreter?

and how can I improve my code


Solution

  • is "" a table when handle it in Lua interpreter?

    No, it's a string, just like you would expect. Every value in Lua can have a metatable. This includes strings, hence why getmetatable() will work on them. Types other than tables and full userdata have one metatable per type. This also includes strings, that's why you can modify metatable of "" and it propagates to all strings.

    See 2.4 - Metatables and Metamethods for more detailed explanation.

    and how can I improve my code

    Do you need to? Linters are just tools. You don't need to listen to it. Of course, modifying built-in modules is usually considered bad practice in any language, but does it matter in your case?

    If you want to listen to it, you could create another module that "inherits" from built-in string module and then use debug.setmetatable() to modify metatable of strings:

    local ext = {
        firstToUpper = function (str)
            return (str:gsub("^%l", string.upper))
        end,
    }
    setmetatable(ext, {__index=string})
    debug.setmetatable("", {__index=ext})
    print(("hello"):firstToUpper())  --> Hello
    

    This way you end up using debug module and you still modify part of built-in state. Is it any better in your case? If not you can use ext.firstToUpper("hello") but this way you lose convenience.

    Alternatively, you can define read_globals in configuration file:

    read_globals = {
        string = {
            fields = {
                firstToUpper = {
                    read_only=false,
                },
            },
        },
    }
    

    But at this point I'd consider it being a very targeted ignore which you are trying to avoid. Remember that this would disable detection of any unintentional changes to firstToUpper (or anything else defined similarly).

    For me, currently, the best option would be to modify and define all needed globals in a file(s) that wouldn't be checked or would be checked with loose rules and then define strict luacheck configuration file using globals and read_globals for all other files that would use the modified global configuration.