Store definitionLocation in ClassType (#1313)

Store the location of an `AstStatDeclareClass` in the `ClassType` as
`definitionLocation`.

This is useful for documentation comments and Go To Definition on a
class type

https://github.com/luau-lang/luau/issues/1137#issuecomment-2212413633
This commit is contained in:
JohnnyMorganz 2024-07-17 16:43:31 +02:00 committed by GitHub
parent 4f917420d7
commit 623e1e30db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 50 additions and 46 deletions

View File

@ -499,10 +499,11 @@ struct ClassType
Tags tags;
std::shared_ptr<ClassUserData> userData;
ModuleName definitionModuleName;
std::optional<Location> definitionLocation;
std::optional<TableIndexer> indexer;
ClassType(Name name, Props props, std::optional<TypeId> parent, std::optional<TypeId> metatable, Tags tags,
std::shared_ptr<ClassUserData> userData, ModuleName definitionModuleName)
std::shared_ptr<ClassUserData> userData, ModuleName definitionModuleName, std::optional<Location> definitionLocation)
: name(name)
, props(props)
, parent(parent)
@ -510,11 +511,13 @@ struct ClassType
, tags(tags)
, userData(userData)
, definitionModuleName(definitionModuleName)
, definitionLocation(definitionLocation)
{
}
ClassType(Name name, Props props, std::optional<TypeId> parent, std::optional<TypeId> metatable, Tags tags,
std::shared_ptr<ClassUserData> userData, ModuleName definitionModuleName, std::optional<TableIndexer> indexer)
std::shared_ptr<ClassUserData> userData, ModuleName definitionModuleName, std::optional<Location> definitionLocation,
std::optional<TableIndexer> indexer)
: name(name)
, props(props)
, parent(parent)
@ -522,6 +525,7 @@ struct ClassType
, tags(tags)
, userData(userData)
, definitionModuleName(definitionModuleName)
, definitionLocation(definitionLocation)
, indexer(indexer)
{
}

View File

@ -1349,7 +1349,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareClas
Name className(declaredClass->name.value);
TypeId classTy = arena->addType(ClassType(className, {}, superTy, std::nullopt, {}, {}, module->name));
TypeId classTy = arena->addType(ClassType(className, {}, superTy, std::nullopt, {}, {}, module->name, declaredClass->location));
ClassType* ctv = getMutable<ClassType>(classTy);
TypeId metaTy = arena->addType(TableType{TableState::Sealed, scope->level, scope.get()});

View File

@ -117,7 +117,7 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a
{
if (alwaysClone)
{
ClassType clone{a.name, a.props, a.parent, a.metatable, a.tags, a.userData, a.definitionModuleName, a.indexer};
ClassType clone{a.name, a.props, a.parent, a.metatable, a.tags, a.userData, a.definitionModuleName, a.definitionLocation, a.indexer};
return dest.addType(std::move(clone));
}
else

View File

@ -969,7 +969,7 @@ BuiltinTypes::BuiltinTypes()
, threadType(arena->addType(Type{PrimitiveType{PrimitiveType::Thread}, /*persistent*/ true}))
, bufferType(arena->addType(Type{PrimitiveType{PrimitiveType::Buffer}, /*persistent*/ true}))
, functionType(arena->addType(Type{PrimitiveType{PrimitiveType::Function}, /*persistent*/ true}))
, classType(arena->addType(Type{ClassType{"class", {}, std::nullopt, std::nullopt, {}, {}, {}}, /*persistent*/ true}))
, classType(arena->addType(Type{ClassType{"class", {}, std::nullopt, std::nullopt, {}, {}, {}, {}}, /*persistent*/ true}))
, tableType(arena->addType(Type{PrimitiveType{PrimitiveType::Table}, /*persistent*/ true}))
, emptyTableType(arena->addType(Type{TableType{TableState::Sealed, TypeLevel{}, nullptr}, /*persistent*/ true}))
, trueType(arena->addType(Type{SingletonType{BooleanSingleton{true}}, /*persistent*/ true}))

View File

@ -1733,7 +1733,7 @@ void TypeChecker::prototype(const ScopePtr& scope, const AstStatDeclareClass& de
Name className(declaredClass.name.value);
TypeId classTy = addType(ClassType(className, {}, superTy, std::nullopt, {}, {}, currentModule->name));
TypeId classTy = addType(ClassType(className, {}, superTy, std::nullopt, {}, {}, currentModule->name, declaredClass.location));
ClassType* ctv = getMutable<ClassType>(classTy);
TypeId metaTy = addType(TableType{TableState::Sealed, scope->level});

View File

@ -124,7 +124,7 @@ int registerTypes(Luau::Frontend& frontend, Luau::GlobalTypes& globals, bool for
// Vector3 stub
TypeId vector3MetaType = arena.addType(TableType{});
TypeId vector3InstanceType = arena.addType(ClassType{"Vector3", {}, nullopt, vector3MetaType, {}, {}, "Test"});
TypeId vector3InstanceType = arena.addType(ClassType{"Vector3", {}, nullopt, vector3MetaType, {}, {}, "Test", {}});
getMutable<ClassType>(vector3InstanceType)->props = {
{"X", {builtinTypes.numberType}},
{"Y", {builtinTypes.numberType}},
@ -138,7 +138,7 @@ int registerTypes(Luau::Frontend& frontend, Luau::GlobalTypes& globals, bool for
globals.globalScope->exportedTypeBindings["Vector3"] = TypeFun{{}, vector3InstanceType};
// Instance stub
TypeId instanceType = arena.addType(ClassType{"Instance", {}, nullopt, nullopt, {}, {}, "Test"});
TypeId instanceType = arena.addType(ClassType{"Instance", {}, nullopt, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(instanceType)->props = {
{"Name", {builtinTypes.stringType}},
};
@ -146,7 +146,7 @@ int registerTypes(Luau::Frontend& frontend, Luau::GlobalTypes& globals, bool for
globals.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, instanceType};
// Part stub
TypeId partType = arena.addType(ClassType{"Part", {}, instanceType, nullopt, {}, {}, "Test"});
TypeId partType = arena.addType(ClassType{"Part", {}, instanceType, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(partType)->props = {
{"Position", {vector3InstanceType}},
};

View File

@ -18,9 +18,9 @@ ClassFixture::ClassFixture()
unfreeze(arena);
TypeId connectionType = arena.addType(ClassType{"Connection", {}, nullopt, nullopt, {}, {}, "Connection"});
TypeId connectionType = arena.addType(ClassType{"Connection", {}, nullopt, nullopt, {}, {}, "Connection", {}});
TypeId baseClassInstanceType = arena.addType(ClassType{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"});
TypeId baseClassInstanceType = arena.addType(ClassType{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(baseClassInstanceType)->props = {
{"BaseMethod", Property::readonly(makeFunction(arena, baseClassInstanceType, {numberType}, {}))},
{"BaseField", {numberType}},
@ -31,7 +31,7 @@ ClassFixture::ClassFixture()
getMutable<ClassType>(connectionType)->props = {
{"Connect", {makeFunction(arena, connectionType, {makeFunction(arena, nullopt, {baseClassInstanceType}, {})}, {})}}};
TypeId baseClassType = arena.addType(ClassType{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"});
TypeId baseClassType = arena.addType(ClassType{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(baseClassType)->props = {
{"StaticMethod", {makeFunction(arena, nullopt, {}, {numberType})}},
{"Clone", {makeFunction(arena, nullopt, {baseClassInstanceType}, {baseClassInstanceType})}},
@ -40,48 +40,48 @@ ClassFixture::ClassFixture()
globals.globalScope->exportedTypeBindings["BaseClass"] = TypeFun{{}, baseClassInstanceType};
addGlobalBinding(globals, "BaseClass", baseClassType, "@test");
TypeId childClassInstanceType = arena.addType(ClassType{"ChildClass", {}, baseClassInstanceType, nullopt, {}, {}, "Test"});
TypeId childClassInstanceType = arena.addType(ClassType{"ChildClass", {}, baseClassInstanceType, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(childClassInstanceType)->props = {
{"Method", {makeFunction(arena, childClassInstanceType, {}, {stringType})}},
};
TypeId childClassType = arena.addType(ClassType{"ChildClass", {}, baseClassType, nullopt, {}, {}, "Test"});
TypeId childClassType = arena.addType(ClassType{"ChildClass", {}, baseClassType, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(childClassType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {childClassInstanceType})}},
};
globals.globalScope->exportedTypeBindings["ChildClass"] = TypeFun{{}, childClassInstanceType};
addGlobalBinding(globals, "ChildClass", childClassType, "@test");
TypeId grandChildInstanceType = arena.addType(ClassType{"GrandChild", {}, childClassInstanceType, nullopt, {}, {}, "Test"});
TypeId grandChildInstanceType = arena.addType(ClassType{"GrandChild", {}, childClassInstanceType, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(grandChildInstanceType)->props = {
{"Method", {makeFunction(arena, grandChildInstanceType, {}, {stringType})}},
};
TypeId grandChildType = arena.addType(ClassType{"GrandChild", {}, baseClassType, nullopt, {}, {}, "Test"});
TypeId grandChildType = arena.addType(ClassType{"GrandChild", {}, baseClassType, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(grandChildType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {grandChildInstanceType})}},
};
globals.globalScope->exportedTypeBindings["GrandChild"] = TypeFun{{}, grandChildInstanceType};
addGlobalBinding(globals, "GrandChild", childClassType, "@test");
TypeId anotherChildInstanceType = arena.addType(ClassType{"AnotherChild", {}, baseClassInstanceType, nullopt, {}, {}, "Test"});
TypeId anotherChildInstanceType = arena.addType(ClassType{"AnotherChild", {}, baseClassInstanceType, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(anotherChildInstanceType)->props = {
{"Method", {makeFunction(arena, anotherChildInstanceType, {}, {stringType})}},
};
TypeId anotherChildType = arena.addType(ClassType{"AnotherChild", {}, baseClassType, nullopt, {}, {}, "Test"});
TypeId anotherChildType = arena.addType(ClassType{"AnotherChild", {}, baseClassType, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(anotherChildType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {anotherChildInstanceType})}},
};
globals.globalScope->exportedTypeBindings["AnotherChild"] = TypeFun{{}, anotherChildInstanceType};
addGlobalBinding(globals, "AnotherChild", childClassType, "@test");
TypeId unrelatedClassInstanceType = arena.addType(ClassType{"UnrelatedClass", {}, nullopt, nullopt, {}, {}, "Test"});
TypeId unrelatedClassInstanceType = arena.addType(ClassType{"UnrelatedClass", {}, nullopt, nullopt, {}, {}, "Test", {}});
TypeId unrelatedClassType = arena.addType(ClassType{"UnrelatedClass", {}, nullopt, nullopt, {}, {}, "Test"});
TypeId unrelatedClassType = arena.addType(ClassType{"UnrelatedClass", {}, nullopt, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(unrelatedClassType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {unrelatedClassInstanceType})}},
};
@ -90,13 +90,13 @@ ClassFixture::ClassFixture()
TypeId vector2MetaType = arena.addType(TableType{});
vector2InstanceType = arena.addType(ClassType{"Vector2", {}, nullopt, vector2MetaType, {}, {}, "Test"});
vector2InstanceType = arena.addType(ClassType{"Vector2", {}, nullopt, vector2MetaType, {}, {}, "Test", {}});
getMutable<ClassType>(vector2InstanceType)->props = {
{"X", {numberType}},
{"Y", {numberType}},
};
vector2Type = arena.addType(ClassType{"Vector2", {}, nullopt, nullopt, {}, {}, "Test"});
vector2Type = arena.addType(ClassType{"Vector2", {}, nullopt, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(vector2Type)->props = {
{"New", {makeFunction(arena, nullopt, {numberType, numberType}, {vector2InstanceType})}},
};
@ -110,7 +110,7 @@ ClassFixture::ClassFixture()
addGlobalBinding(globals, "Vector2", vector2Type, "@test");
TypeId callableClassMetaType = arena.addType(TableType{});
TypeId callableClassType = arena.addType(ClassType{"CallableClass", {}, nullopt, callableClassMetaType, {}, {}, "Test"});
TypeId callableClassType = arena.addType(ClassType{"CallableClass", {}, nullopt, callableClassMetaType, {}, {}, "Test", {}});
getMutable<TableType>(callableClassMetaType)->props = {
{"__call", {makeFunction(arena, nullopt, {callableClassType, stringType}, {numberType})}},
};
@ -119,7 +119,7 @@ ClassFixture::ClassFixture()
auto addIndexableClass = [&arena, &globals](const char* className, TypeId keyType, TypeId returnType) {
TypeId indexableClassMetaType = arena.addType(TableType{});
TypeId indexableClassType =
arena.addType(ClassType{className, {}, nullopt, indexableClassMetaType, {}, {}, "Test", TableIndexer{keyType, returnType}});
arena.addType(ClassType{className, {}, nullopt, indexableClassMetaType, {}, {}, "Test", {}, TableIndexer{keyType, returnType}});
globals.globalScope->exportedTypeBindings[className] = TypeFun{{}, indexableClassType};
};

View File

@ -658,7 +658,7 @@ void createSomeClasses(Frontend* frontend)
ScopePtr moduleScope = globals.globalScope;
TypeId parentType = arena.addType(ClassType{"Parent", {}, frontend->builtinTypes->classType, std::nullopt, {}, nullptr, "Test"});
TypeId parentType = arena.addType(ClassType{"Parent", {}, frontend->builtinTypes->classType, std::nullopt, {}, nullptr, "Test", {}});
ClassType* parentClass = getMutable<ClassType>(parentType);
parentClass->props["method"] = {makeFunction(arena, parentType, {}, {})};
@ -668,17 +668,17 @@ void createSomeClasses(Frontend* frontend)
addGlobalBinding(globals, "Parent", {parentType});
moduleScope->exportedTypeBindings["Parent"] = TypeFun{{}, parentType};
TypeId childType = arena.addType(ClassType{"Child", {}, parentType, std::nullopt, {}, nullptr, "Test"});
TypeId childType = arena.addType(ClassType{"Child", {}, parentType, std::nullopt, {}, nullptr, "Test", {}});
addGlobalBinding(globals, "Child", {childType});
moduleScope->exportedTypeBindings["Child"] = TypeFun{{}, childType};
TypeId anotherChildType = arena.addType(ClassType{"AnotherChild", {}, parentType, std::nullopt, {}, nullptr, "Test"});
TypeId anotherChildType = arena.addType(ClassType{"AnotherChild", {}, parentType, std::nullopt, {}, nullptr, "Test", {}});
addGlobalBinding(globals, "AnotherChild", {anotherChildType});
moduleScope->exportedTypeBindings["AnotherChild"] = TypeFun{{}, anotherChildType};
TypeId unrelatedType = arena.addType(ClassType{"Unrelated", {}, frontend->builtinTypes->classType, std::nullopt, {}, nullptr, "Test"});
TypeId unrelatedType = arena.addType(ClassType{"Unrelated", {}, frontend->builtinTypes->classType, std::nullopt, {}, nullptr, "Test", {}});
addGlobalBinding(globals, "Unrelated", {unrelatedType});
moduleScope->exportedTypeBindings["Unrelated"] = TypeFun{{}, unrelatedType};

View File

@ -112,7 +112,7 @@ TEST_CASE_FIXTURE(GeneralizationFixture, "dont_traverse_into_class_types_when_ge
{
auto [propTy, _] = freshType();
TypeId cursedClass = arena.addType(ClassType{"Cursed", {{"oh_no", Property::readonly(propTy)}}, std::nullopt, std::nullopt, {}, {}, ""});
TypeId cursedClass = arena.addType(ClassType{"Cursed", {{"oh_no", Property::readonly(propTy)}}, std::nullopt, std::nullopt, {}, {}, "", {}});
auto genClass = generalize(cursedClass);
REQUIRE(genClass);

View File

@ -1485,7 +1485,7 @@ TEST_CASE_FIXTURE(Fixture, "LintHygieneUAF")
TEST_CASE_FIXTURE(BuiltinsFixture, "DeprecatedApiTyped")
{
unfreeze(frontend.globals.globalTypes);
TypeId instanceType = frontend.globals.globalTypes.addType(ClassType{"Instance", {}, std::nullopt, std::nullopt, {}, {}, "Test"});
TypeId instanceType = frontend.globals.globalTypes.addType(ClassType{"Instance", {}, std::nullopt, std::nullopt, {}, {}, "Test", {}});
persist(instanceType);
frontend.globals.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, instanceType};

View File

@ -244,13 +244,13 @@ TEST_CASE_FIXTURE(Fixture, "clone_class")
{
{"__add", {builtinTypes->anyType}},
},
std::nullopt, std::nullopt, {}, {}, "Test"}};
std::nullopt, std::nullopt, {}, {}, "Test", {}}};
Type exampleClass{ClassType{"ExampleClass",
{
{"PropOne", {builtinTypes->numberType}},
{"PropTwo", {builtinTypes->stringType}},
},
std::nullopt, &exampleMetaClass, {}, {}, "Test"}};
std::nullopt, &exampleMetaClass, {}, {}, "Test", {}}};
TypeArena dest;
CloneState cloneState{builtinTypes};

View File

@ -146,7 +146,7 @@ struct SubtypeFixture : Fixture
TypeId cls(const std::string& name, std::optional<TypeId> parent = std::nullopt)
{
return arena.addType(ClassType{name, {}, parent.value_or(builtinTypes->classType), {}, {}, nullptr, ""});
return arena.addType(ClassType{name, {}, parent.value_or(builtinTypes->classType), {}, {}, nullptr, "", {}});
}
TypeId cls(const std::string& name, ClassType::Props&& props)

View File

@ -21,13 +21,13 @@ struct ToDotClassFixture : Fixture
TypeId baseClassMetaType = arena.addType(TableType{});
TypeId baseClassInstanceType = arena.addType(ClassType{"BaseClass", {}, std::nullopt, baseClassMetaType, {}, {}, "Test"});
TypeId baseClassInstanceType = arena.addType(ClassType{"BaseClass", {}, std::nullopt, baseClassMetaType, {}, {}, "Test", {}});
getMutable<ClassType>(baseClassInstanceType)->props = {
{"BaseField", {builtinTypes->numberType}},
};
frontend.globals.globalScope->exportedTypeBindings["BaseClass"] = TypeFun{{}, baseClassInstanceType};
TypeId childClassInstanceType = arena.addType(ClassType{"ChildClass", {}, baseClassInstanceType, std::nullopt, {}, {}, "Test"});
TypeId childClassInstanceType = arena.addType(ClassType{"ChildClass", {}, baseClassInstanceType, std::nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(childClassInstanceType)->props = {
{"ChildField", {builtinTypes->stringType}},
};

View File

@ -706,19 +706,19 @@ TEST_CASE_FIXTURE(Fixture, "read_write_class_properties")
unfreeze(arena);
TypeId instanceType = arena.addType(ClassType{"Instance", {}, nullopt, nullopt, {}, {}, "Test"});
TypeId instanceType = arena.addType(ClassType{"Instance", {}, nullopt, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(instanceType)->props = {{"Parent", Property::rw(instanceType)}};
//
TypeId workspaceType = arena.addType(ClassType{"Workspace", {}, nullopt, nullopt, {}, {}, "Test"});
TypeId workspaceType = arena.addType(ClassType{"Workspace", {}, nullopt, nullopt, {}, {}, "Test", {}});
TypeId scriptType =
arena.addType(ClassType{"Script", {{"Parent", Property::rw(workspaceType, instanceType)}}, instanceType, nullopt, {}, {}, "Test"});
arena.addType(ClassType{"Script", {{"Parent", Property::rw(workspaceType, instanceType)}}, instanceType, nullopt, {}, {}, "Test", {}});
TypeId partType = arena.addType(
ClassType{"Part", {{"BrickColor", Property::rw(builtinTypes->stringType)}, {"Parent", Property::rw(workspaceType, instanceType)}},
instanceType, nullopt, {}, {}, "Test"});
instanceType, nullopt, {}, {}, "Test", {}});
getMutable<ClassType>(workspaceType)->props = {{"Script", Property::readonly(scriptType)}, {"Part", Property::readonly(partType)}};

View File

@ -66,14 +66,14 @@ struct RefinementClassFixture : BuiltinsFixture
std::optional<TypeId> rootSuper = std::make_optional(builtinTypes->classType);
unfreeze(arena);
TypeId vec3 = arena.addType(ClassType{"Vector3", {}, rootSuper, std::nullopt, {}, nullptr, "Test"});
TypeId vec3 = arena.addType(ClassType{"Vector3", {}, rootSuper, std::nullopt, {}, nullptr, "Test", {}});
getMutable<ClassType>(vec3)->props = {
{"X", Property{builtinTypes->numberType}},
{"Y", Property{builtinTypes->numberType}},
{"Z", Property{builtinTypes->numberType}},
};
TypeId inst = arena.addType(ClassType{"Instance", {}, rootSuper, std::nullopt, {}, nullptr, "Test"});
TypeId inst = arena.addType(ClassType{"Instance", {}, rootSuper, std::nullopt, {}, nullptr, "Test", {}});
TypePackId isAParams = arena.addTypePack({inst, builtinTypes->stringType});
TypePackId isARets = arena.addTypePack({builtinTypes->booleanType});
@ -86,8 +86,8 @@ struct RefinementClassFixture : BuiltinsFixture
{"IsA", Property{isA}},
};
TypeId folder = frontend.globals.globalTypes.addType(ClassType{"Folder", {}, inst, std::nullopt, {}, nullptr, "Test"});
TypeId part = frontend.globals.globalTypes.addType(ClassType{"Part", {}, inst, std::nullopt, {}, nullptr, "Test"});
TypeId folder = frontend.globals.globalTypes.addType(ClassType{"Folder", {}, inst, std::nullopt, {}, nullptr, "Test", {}});
TypeId part = frontend.globals.globalTypes.addType(ClassType{"Part", {}, inst, std::nullopt, {}, nullptr, "Test", {}});
getMutable<ClassType>(part)->props = {
{"Position", Property{vec3}},
};

View File

@ -314,7 +314,7 @@ TEST_CASE("tagging_tables")
TEST_CASE("tagging_classes")
{
Type base{ClassType{"Base", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"}};
Type base{ClassType{"Base", {}, std::nullopt, std::nullopt, {}, nullptr, "Test", {}}};
CHECK(!Luau::hasTag(&base, "foo"));
Luau::attachTag(&base, "foo");
CHECK(Luau::hasTag(&base, "foo"));
@ -322,8 +322,8 @@ TEST_CASE("tagging_classes")
TEST_CASE("tagging_subclasses")
{
Type base{ClassType{"Base", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"}};
Type derived{ClassType{"Derived", {}, &base, std::nullopt, {}, nullptr, "Test"}};
Type base{ClassType{"Base", {}, std::nullopt, std::nullopt, {}, nullptr, "Test", {}}};
Type derived{ClassType{"Derived", {}, &base, std::nullopt, {}, nullptr, "Test", {}}};
CHECK(!Luau::hasTag(&base, "foo"));
CHECK(!Luau::hasTag(&derived, "foo"));