Skip to content

Commit ed7cd02

Browse files
JohnnyMorganzclaude
andcommitted
Auto-detect undefined types in dumpRobloxTypes.py
Instead of manually hardcoding `type X = any` stubs in START_BASE and POST_DATATYPES_BASE, automatically detect types that are referenced in the API dump but never defined, and emit stubs for them. This means new undefined types like CollectionHandle are picked up automatically without manual intervention. Also fixes a bug where types with real definitions in DataTypes.json (FloatCurveKey, RotationCurveKey, etc.) were incorrectly overridden with `any`, and fixes an identifier validation bug in registerDeclaredInType where the [A-z] range matched extra characters. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b25a8d3 commit ed7cd02

File tree

1 file changed

+84
-65
lines changed

1 file changed

+84
-65
lines changed

scripts/dumpRobloxTypes.py

Lines changed: 84 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
LUAU_TYPES_URL = "https://raw.githubusercontent.com/MaximumADHD/Roblox-Client-Tracker/roblox/LuauTypes.d.luau"
1515
BRICK_COLORS_URL = "https://gist.githubusercontent.com/Anaminus/49ac255a68e7a7bc3cdd72b602d5071f/raw/f1534dcae312dbfda716b7677f8ac338b565afc3/BrickColor.json"
1616

17-
# Whether to include deprecated members that cannot be assigned the @deprecated attribute (i.e. deprecated non-functions).
17+
# Whether to include deprecated members that cannot be assigned the @deprecated attribute (i.e. deprecated non-functions).
1818
# Deprecated functions will always be defined, with their corresponding @deprecated attribute.
1919
INCLUDE_DEPRECATED_MEMBERS = False
2020

@@ -41,41 +41,41 @@
4141
LUAU_SNIPPET_PATCHES = {
4242
"declare function delay(delay: number?, callback: () -> ())":
4343
"@[deprecated{ use = \"task.delay\" }]\ndeclare function delay(delay: number?, callback: (dt: number, gt: number) -> ())",
44-
44+
4545
"declare function collectgarbage(mode: string): number":
4646
"@[deprecated{ use = \"gcinfo\" }]\ndeclare function collectgarbage(mode: \"count\"): number",
47-
47+
4848
"declare function stats()":
4949
"@[deprecated{ use = 'game:GetService(\"Stats\")' }]\ndeclare function stats(): Stats",
50-
51-
"declare function wait(delay: number?): (number, number)":
50+
51+
"declare function wait(delay: number?): (number, number)":
5252
"@[deprecated{ use = \"task.wait\" }]\ndeclare function wait(delay: number?): (number, number)",
53-
53+
5454
"declare function printidentity(prefix: string?)":
5555
"@deprecated declare function printidentity(prefix: string?)",
5656

5757
"declare function version(): string":
5858
"@deprecated declare function version(): string",
59-
59+
6060
"declare game: any": "declare game: DataModel",
6161
"declare workspace: any": "declare workspace: Workspace",
6262
"declare script: any": "declare script: LuaSourceContainer",
6363

64-
"declare Delay: typeof(delay)":
64+
"declare Delay: typeof(delay)":
6565
"@[deprecated{ use = \"task.delay\" }]\ndeclare function Delay(delay: number?, callback: (dt: number, gt: number) -> ())",
66-
67-
"declare Wait: typeof(wait)":
66+
67+
"declare Wait: typeof(wait)":
6868
"@[deprecated{ use = \"task.wait\" }]\ndeclare function Wait(delay: number?): (number, number)",
69-
70-
"declare ElapsedTime: typeof(elapsedTime)":
69+
70+
"declare ElapsedTime: typeof(elapsedTime)":
7171
"@[deprecated{ use = \"elapsedTime\" }]\ndeclare function ElapsedTime(): number",
72-
72+
7373
"declare Stats: typeof(stats)":
7474
"@[deprecated{ use = 'game:GetService(\"Stats\")' }]\ndeclare function Stats(): Stats",
75-
76-
"declare Version: typeof(version)":
75+
76+
"declare Version: typeof(version)":
7777
"@[deprecated{ use = \"version\" }]\ndeclare function Version(): string",
78-
78+
7979
"declare Workspace: any": "",
8080
"declare Game: any": "",
8181
}
@@ -430,15 +430,7 @@
430430
type BinaryString = string
431431
type QDir = string
432432
type QFont = string
433-
type FloatCurveKey = any
434-
type RotationCurveKey = any
435-
type Secret = any
436-
type Path2DControlPoint = any
437-
type UniqueId = any
438-
type SecurityCapabilities = any
439433
type TeleportData = boolean | buffer | number | string | {[number]: TeleportData} | {[string]: TeleportData}
440-
type SystemAddress = any
441-
type AdReward = any
442434
443435
declare class Enum
444436
function GetEnumItems(self): { any }
@@ -487,9 +479,6 @@
487479
function __iter(self): (any, number) -> (number, any)
488480
end
489481
490-
export type OpenCloudModel = any
491-
export type ClipEvaluator = any
492-
493482
export type RBXScriptSignal<T... = ...any> = {
494483
Wait: (self: RBXScriptSignal<T...>) -> T...,
495484
Connect: (self: RBXScriptSignal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
@@ -668,7 +657,7 @@
668657
)
669658

670659
ApiPropertySecurityLevel = TypedDict(
671-
"ApiPropertySecurityLevel",
660+
"ApiPropertySecurityLevel",
672661
{
673662
"Read": ApiSecurityLevel,
674663
"Write": ApiSecurityLevel
@@ -788,19 +777,43 @@
788777
# Cache for looking up members by name when resolving deprecations
789778
classesWithMemberName: dict[str, List[ApiClass]] = {}
790779

791-
# Keep track of declared Luau types as a failsafe if we need to declare them.
792-
# This needs to be kept in-sync with any Luau type declarations
793-
# added in EXTRA_MEMBERS
794-
795-
declaredLuauTypes: Set[str] = {
780+
# Types referenced in the API dump / EXTRA_MEMBERS / Corrections that we need to track for auto-declaration.
781+
# Seeded with types from EXTRA_MEMBERS that bypass resolveType().
782+
referenced_types: Set[str] = {
796783
"ReviewableContentEvent",
797784
"AutoSetupParams",
798785
"CaptureParams",
799786
"VideoSample",
800787
}
801788

789+
LUAU_PRIMITIVES = {"string", "number", "boolean", "nil", "any", "unknown", "never", "buffer", "thread", "vector"}
790+
791+
# All types that are defined (populated incrementally during generation).
792+
# Seeded with static sources; classes, datatypes, and enums are added during printing.
793+
def extractDefinedNames(text: str) -> Set[str]:
794+
"""Extract type/class/declare names from Luau definition text."""
795+
names: Set[str] = set()
796+
for m in re.finditer(r'(?:export\s+)?type\s+(\w+)', text):
797+
names.add(m.group(1))
798+
for m in re.finditer(r'declare\s+class\s+(\w+)', text):
799+
names.add(m.group(1))
800+
for m in re.finditer(r'declare\s+(\w+)\s*:', text):
801+
names.add(m.group(1))
802+
return names
803+
804+
def _seed_defined_types() -> Set[str]:
805+
types: Set[str] = set()
806+
types.update(LUAU_PRIMITIVES)
807+
types.update(TYPE_INDEX.values())
808+
types.update(IGNORED_INSTANCES)
809+
for base in [START_BASE, POST_DATATYPES_BASE, END_BASE]:
810+
types.update(extractDefinedNames(base))
811+
return types
812+
813+
defined_types: Set[str] = _seed_defined_types()
814+
802815
def isIdentifier(name: str):
803-
return re.match(r"^[a-zA-Z_]+[a-zA-Z_0-9]*$", name) # TODO: 'function'
816+
return re.match(r"^[A-Za-z_]\w*$", name)
804817

805818
def escapeName(name: str):
806819
"""Escape a name string to be property-compatible"""
@@ -845,7 +858,10 @@ def resolveType(type: Union[ApiValueType, CorrectionsValueType]) -> str:
845858
if category == "Enum":
846859
return "Enum" + name
847860
else:
848-
return TYPE_INDEX[name] if name in TYPE_INDEX else name
861+
resolved = TYPE_INDEX[name] if name in TYPE_INDEX else name
862+
if resolved not in LUAU_PRIMITIVES and isIdentifier(resolved):
863+
referenced_types.add(resolved)
864+
return resolved
849865

850866

851867
def resolveParameter(param: ApiParameter):
@@ -874,15 +890,15 @@ def resolveReturnType(member: Union[ApiFunction, ApiCallback]) -> str:
874890
return "(" + ", ".join(types) + ")"
875891
elif member["ReturnType"] is not None:
876892
return resolveType(member["ReturnType"])
877-
893+
878894
return "nil"
879895

880896
def resolveDeprecation(member: ApiMember, klass: ApiClass | DataType) -> str:
881897
tags: Optional[List[Union[str, ApiDeprecatedInfo]]] = None
882-
898+
883899
if "Tags" in member:
884900
tags = member["Tags"]
885-
901+
886902
result = ""
887903

888904
if tags is not None:
@@ -915,7 +931,7 @@ def resolveDeprecation(member: ApiMember, klass: ApiClass | DataType) -> str:
915931
if member["Name"] == preferred:
916932
bestMember = member
917933
break
918-
934+
919935
if bestMember is not None:
920936
# Use the classname and member name, we found a different class to point to!
921937
result = f"@[deprecated {{use = \"{bestClass['Name']}{':' if bestMember['MemberType'] == 'Function' else '.'}{preferred}\"}}]\n\t\t"
@@ -951,15 +967,15 @@ def filterMember(klassName: str, member: ApiMember):
951967
and member["MemberType"] != "Function"
952968
):
953969
return False
954-
970+
955971
if ("Tags" in member and
956972
member["Tags"] is not None
957973
and "NotScriptable" in member["Tags"]):
958974
return False
959-
975+
960976
if member["Name"] in classIgnoredMembers(klassName):
961977
return False
962-
978+
963979

964980
if "Security" in member:
965981
if isinstance(member["Security"], str):
@@ -981,6 +997,8 @@ def declareClass(klass: Union[ApiClass, DataType]) -> str:
981997
if klass["Name"] in IGNORED_INSTANCES:
982998
return ""
983999

1000+
defined_types.add(klass["Name"])
1001+
9841002
out = "declare class " + klass["Name"]
9851003
if "Superclass" in klass and klass["Superclass"] != "<<<ROOT>>>":
9861004
out += " extends " + klass["Superclass"]
@@ -1028,6 +1046,7 @@ def printEnums(dump: ApiDump):
10281046
# Declare each enum individually
10291047
out = ""
10301048
for enum, items in enums.items():
1049+
defined_types.add("Enum" + enum)
10311050
# Declare an atom for the enum
10321051
out += f"declare class Enum{enum} extends EnumItem end\n"
10331052
out += f"declare class Enum{enum}_INTERNAL extends Enum\n"
@@ -1071,6 +1090,7 @@ def printDataTypeConstructors(types: DataTypesDump):
10711090
if klass["Name"] in IGNORED_INSTANCES:
10721091
continue
10731092
name = klass["Name"]
1093+
defined_types.add(name)
10741094
members = klass["Members"]
10751095

10761096
isBrickColorNew = False
@@ -1127,6 +1147,14 @@ def printDataTypeConstructors(types: DataTypesDump):
11271147
print()
11281148

11291149

1150+
def printUndefinedTypeStubs():
1151+
undefined = sorted(referenced_types - defined_types)
1152+
if undefined:
1153+
for name in undefined:
1154+
print(f"type {name} = any")
1155+
print()
1156+
1157+
11301158
def applyCorrections(dump: ApiDump, corrections: CorrectionsDump):
11311159
for klass in corrections["Classes"]:
11321160
for otherClass in dump["Classes"]:
@@ -1194,7 +1222,7 @@ def loadMembersIntoStructures(klass: ApiClass):
11941222
classesWithMemberName[name] = [klass]
11951223
else:
11961224
classesWithMemberName[name].append(klass)
1197-
1225+
11981226

11991227
def loadClassesIntoStructures(dump: ApiDump):
12001228
for klass in dump["Classes"]:
@@ -1216,22 +1244,22 @@ def loadClassesIntoStructures(dump: ApiDump):
12161244
def registerDeclaredInType(type: CorrectionsValueType | None):
12171245
if type is not None:
12181246
if "Declared" in type and type["Declared"] is not None:
1219-
if type["Declared"] not in declaredLuauTypes:
1247+
if type["Declared"] not in referenced_types:
12201248
declaredType = type["Declared"]
12211249

12221250
if declaredType.endswith("?"):
12231251
declaredType = declaredType[:-1]
12241252

1225-
if re.match("^[A-z0-9_]+$", declaredType):
1226-
declaredLuauTypes.add(declaredType)
1253+
if isIdentifier(declaredType):
1254+
referenced_types.add(declaredType)
12271255

12281256
def registerDeclared(dump: CorrectionsDump):
12291257
for klass in dump["Classes"]:
12301258
for member in klass["Members"]:
12311259
if "TupleReturns" in member and member["TupleReturns"] is not None:
12321260
for ret in member["TupleReturns"]:
12331261
registerDeclaredInType(ret)
1234-
1262+
12351263
if "ReturnType" in member:
12361264
if isinstance(member["ReturnType"], list):
12371265
for ret in member["ReturnType"]:
@@ -1243,7 +1271,7 @@ def registerDeclared(dump: CorrectionsDump):
12431271
if "ValueType" in member:
12441272
value = member["ValueType"]
12451273
registerDeclaredInType(value)
1246-
1274+
12471275
if "Parameters" in member and member["Parameters"] is not None:
12481276
for param in member["Parameters"]:
12491277
if "Type" in param:
@@ -1268,7 +1296,7 @@ def printLuauTypes():
12681296

12691297
while not luauLines[0].startswith("-- SECTION BEGIN:"):
12701298
luauLines.pop(0)
1271-
1299+
12721300
# Begin capturing sections from SECTION BEGIN to SECTION END
12731301
luauSections: dict[str, list[str]] = dict()
12741302
sectionNames: list[str] = []
@@ -1283,12 +1311,12 @@ def printLuauTypes():
12831311
if currentSection is not None:
12841312
luauSections[sectionName] = currentSection
12851313
currentSection = None
1286-
1314+
12871315
sectionNames.append(sectionName)
12881316
elif currentSection is not None:
12891317
if line in LUAU_SNIPPET_PATCHES.keys():
12901318
line = LUAU_SNIPPET_PATCHES[line]
1291-
1319+
12921320
line = line.replace("Enum.", "Enum")
12931321
currentSection.append(line)
12941322

@@ -1302,27 +1330,17 @@ def printLuauTypes():
13021330

13031331
for line in sectionLines:
13041332
writtenLines.add(line)
1305-
1333+
13061334
luauTypes += "\n".join(sectionLines) + "\n"
1307-
1335+
13081336
# Fail-safe: Append any patches that were not written
13091337
for patch in LUAU_SNIPPET_PATCHES.values():
13101338
if patch not in writtenLines:
13111339
luauTypes += patch + "\n"
13121340

1313-
# Fail-safe: Declare any missing types that were marked as declared
1314-
for declaredType in declaredLuauTypes:
1315-
declaration = f"type {declaredType} = any"
1316-
found = False
1317-
1318-
for line in writtenLines:
1319-
if line.find(declaredType) != -1:
1320-
found = True
1321-
break
1341+
# Extract type/class names defined in LuauTypes for use by printUndefinedTypeStubs
1342+
defined_types.update(extractDefinedNames(luauTypes))
13221343

1323-
if not found:
1324-
luauTypes += declaration + "\n"
1325-
13261344
print(luauTypes)
13271345

13281346
# Load BrickColors
@@ -1349,4 +1367,5 @@ def printLuauTypes():
13491367
printLuauTypes()
13501368
printClasses(dump)
13511369
printDataTypeConstructors(dataTypes)
1370+
printUndefinedTypeStubs()
13521371
print(END_BASE)

0 commit comments

Comments
 (0)