Setmetatable's Type-Checking bugs out when using {@ metatable t, m}

Description

The Type-Checking for setmetatable bugs out when you try to use another table with a metatable attached ({@ metatable t, m}), preventing the autocompleter from working properly, especially when using the __index metamethod.

Repro

--!strict

local Foo = {}
Foo.__index = Foo
Foo.A = 1

function new1()
	return setmetatable({}, Foo)
end


local Bar = {}
Bar.__index = Bar
Bar.B = 2

setmetatable(Bar, Foo)

function new2()
	return setmetatable(new1(), Bar) -- 'setmetatable should take a table'
end

print(new2().B) -- Bugged

Additional Info

This is the specific bug that prevents writing Strictly-Typed OOP as it prevents the SubClass type from reading methods from itself in the below examples.

SuperClass (Not Bugged)
--!strict

local SuperClass = {}
SuperClass.__index = SuperClass


type self = {
	SuperValue: number
}

export type SuperClass = typeof(setmetatable({} :: self, SuperClass))


function SuperClass.new(): SuperClass
	local self: SuperClass = setmetatable({} :: self, SuperClass)
	
	self.SuperValue = 1
	
	return self
end

function SuperClass:Foo(): ()
	local self: SuperClass = self
	
	print("Foo")
end


return SuperClass
SubClass
--!strict

local SuperClass = require(SuperClass)

local SubClass = {}
SubClass.__index = SubClass

setmetatable(SubClass, SuperClass)


type self = SuperClass.SuperClass & {
	SubValue: number
}

export type SubClass = typeof(setmetatable({} :: self, SubClass))


function SubClass.new(): SubClass
	local self = setmetatable(SuperClass.new() :: self, SubClass)
	
	self.SuperValue = 1
	self.SubValue = 1
	
	self:Bar() -- Nope
	
	return self
end

function SubClass:Bar(): ()
	local self: SubClass = self
	
	print("Bar")
end

return SubClass
3 Likes

We’ve filed a ticket to our internal database for this issue, and will update you as soon as we have news!

Thanks for flagging!

5 Likes

Checking on this two and a half months later. This bug is still present and interfering with my workflow.

The problem is most likely caused by normal tables not being able to have the type of a table with a metatable casted onto it. This would explain why setmetatable doesn’t like the forementioned type.

--!nonstrict
local Table = {} :: typeof(setmetatable({}, {}))
2 Likes

Hey everyone, wanted to circle back here, is this still an issue?

1 Like

Extremely sorry for the long wait, but yes, the t parameter still doesn’t like having a table with a metatable attached.

I’ve found ways to “trick” analysis into ignoring it, i.e. putting setmetatable at the very bottom of the file, but they are a little unstable.

yes, it is still an issue

asdasdasdasd

Sorry the reviving the topic and notifying you all, but did any of you guys find a solution to the problem? This seems to be the only explanation to the issue I’m experiencing right now. Do you guys have a work-around to circumvent this issue, or should I just stop using type checking in a script that encounters this bug?

The new type solver seems to fix the first half of the problem but not the second. It looks like something is messing up with the __index metamethod:

local foo = {}
foo.__index = foo
foo.a = 1

function f()
	return setmetatable({}, foo)
end

local bar = {}
bar.__index = bar
bar.b = 2

function g()
	return setmetatable(f(), bar)
end

local a = f().a
local b = g().b -- autocompletes, but warns

Just cast to any for every false positive. I wouldn’t recommend turning off typechecking.

When I read your example script, I was in a bit of disbelief, so I decided to check it out for myself. For whatever reason, when I put the script in studio the issue is still there, unless I’m not understanding what you meant by “the first half of the problem”.

I’m still experiencing the same “setmetatable should take a table” warning. And although Bar objects are able to autocomplete the values of Foo, they’re unable to autocomplete the Values of bar.

Was your “first half of the problem” related to Bar being unable to autocomplete Foo?

--!strict
local foo = {}
foo.__index = foo
foo.a = 1
foo.b = 2

function f()
	return setmetatable({}, foo)
end

local bar = {}
bar.__index = bar
bar.c = 3

function g()
	return setmetatable(f(), bar) --Warns "setmetatable should take a table"
end

local a = f().a --autocompletes without warning
local b = g().b --autocompletes without warning
local b = g().c --doesn't autocomplete, warns "Type '{ @metatable foo, {|  |} }' does not have key 'c'"

I really appreciate your response 9 months later. As you’ve said, asserting the type as “any”, or “{any}” would definitely be a suitable workaround, easily replaceable once the Type Checker no longer has this issue. Thanks for your help.