cluaupp 1.5.2 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -0
- package/README.md +27 -40
- package/api/generated/bench-latest.json +43 -21
- package/api/generated/lsp-index.json +1 -1
- package/api/manifests/roblox-target.manifest.json +5 -5
- package/api/normalized/roblox-target.profile.json +8729 -267
- package/api/roblox/luau-builtins.d.luau +185 -0
- package/api/roblox/surface.json +1 -0
- package/api/roblox-api.lock.json +11 -2
- package/bin/cluaupp.js +38 -0
- package/editors/vscode/syntaxes/clpp.tmLanguage.json +211 -0
- package/examples/README.md +9 -9
- package/examples/game/cluaupp.config.json +2 -2
- package/examples/game/default.project.json +4 -15
- package/examples/game/out/ReplicatedStorage/Shared/GameInfo.luau +8 -0
- package/examples/game/out/ReplicatedStorage/Shared/GameInfo.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Leaderstats.server.luau +26 -0
- package/examples/game/out/ServerScriptService/Leaderstats.server.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Main.server.luau +15 -0
- package/examples/game/out/ServerScriptService/Main.server.luau.map.json +13 -0
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Main.client.luau +10 -0
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Main.client.luau.map.json +13 -0
- package/examples/game/src/ReplicatedStorage/Shared/GameInfo.clp +1 -0
- package/examples/game/src/ServerScriptService/Leaderstats.server.clp +18 -0
- package/examples/game/src/ServerScriptService/Main.server.clp +9 -0
- package/examples/game/src/StarterPlayer/StarterPlayerScripts/Main.client.clp +4 -0
- package/generated/api/build-profile.js +1 -1
- package/generated/api/loader.js +2 -2
- package/generated/api/surface/build.js +328 -0
- package/generated/api/surface/luau-defs.js +595 -0
- package/generated/api/surface/model.js +8 -0
- package/generated/clpp/contract.js +6 -3
- package/generated/clpp/handshake.js +36 -0
- package/generated/clpp/modules.js +20 -9
- package/generated/clpp/runner.js +62 -31
- package/generated/compile.js +18 -1
- package/generated/compiler/ast.js +3 -0
- package/generated/compiler/checker/calls.js +999 -0
- package/generated/compiler/checker/checker.js +1215 -0
- package/generated/compiler/checker/exprs.js +791 -0
- package/generated/compiler/checker/flow.js +168 -0
- package/generated/compiler/checker/members.js +763 -0
- package/generated/compiler/checker/statements.js +792 -0
- package/generated/compiler/checker/symbols.js +90 -0
- package/generated/compiler/checker/typeops.js +348 -0
- package/generated/compiler/checker/yields.js +115 -0
- package/generated/compiler/diagnostics.js +138 -0
- package/generated/compiler/emit/emitter.js +2356 -0
- package/generated/compiler/emit/luau-types.js +137 -0
- package/generated/compiler/emit/names.js +130 -0
- package/generated/compiler/emit/runtime.js +432 -0
- package/generated/compiler/lexer.js +403 -0
- package/generated/compiler/parser.js +1881 -0
- package/generated/compiler/project.js +332 -0
- package/generated/compiler/roblox/api.js +388 -0
- package/generated/compiler/roblox/context.js +100 -0
- package/generated/compiler/source.js +59 -0
- package/generated/compiler/stdlib.js +232 -0
- package/generated/compiler/types.js +250 -0
- package/generated/target/build-cache.js +4 -4
- package/generated/target/gen-rojo-tree.js +15 -0
- package/generated/target/source-map.js +61 -8
- package/generated/target/studio-bridge.js +1 -0
- package/generated/utils/project.js +83 -25
- package/native/bin/.gitkeep +1 -0
- package/package.json +17 -6
- package/plugins/CluauppNav/Plugin.luau +28 -4
- package/rust/cluaupp/Cargo.lock +795 -0
- package/rust/cluaupp/Cargo.toml +17 -0
- package/rust/cluaupp/src/main.rs +717 -0
- package/scripts/postinstall-native.js +56 -0
- package/skills/README.md +3 -3
- package/skills/clpp-hive-flare/SKILL.md +30 -26
- package/skills/clpp-language/SKILL.md +67 -74
- package/skills/clpp-language/compiler.md +24 -36
- package/skills/clpp-style/SKILL.md +31 -44
- package/src/api/build-profile.ts +1 -1
- package/src/api/loader.ts +2 -2
- package/src/api/surface/build.ts +374 -0
- package/src/api/surface/luau-defs.ts +635 -0
- package/src/api/surface/model.ts +96 -0
- package/src/clpp/contract.ts +20 -3
- package/src/clpp/handshake.ts +25 -0
- package/src/clpp/modules.ts +17 -10
- package/src/clpp/runner.ts +52 -34
- package/src/compile.ts +20 -2
- package/src/compiler/ast.ts +336 -0
- package/src/compiler/checker/calls.ts +1025 -0
- package/src/compiler/checker/checker.ts +1352 -0
- package/src/compiler/checker/exprs.ts +805 -0
- package/src/compiler/checker/flow.ts +174 -0
- package/src/compiler/checker/members.ts +793 -0
- package/src/compiler/checker/statements.ts +802 -0
- package/src/compiler/checker/symbols.ts +219 -0
- package/src/compiler/checker/typeops.ts +355 -0
- package/src/compiler/checker/yields.ts +120 -0
- package/src/compiler/diagnostics.ts +163 -0
- package/src/compiler/emit/emitter.ts +2486 -0
- package/src/compiler/emit/luau-types.ts +143 -0
- package/src/compiler/emit/names.ts +130 -0
- package/src/compiler/emit/runtime.ts +438 -0
- package/src/compiler/lexer.ts +444 -0
- package/src/compiler/parser.ts +1943 -0
- package/src/compiler/project.ts +354 -0
- package/src/compiler/roblox/api.ts +408 -0
- package/src/compiler/roblox/context.ts +108 -0
- package/src/compiler/source.ts +67 -0
- package/src/compiler/stdlib.ts +256 -0
- package/src/compiler/types.ts +313 -0
- package/src/target/build-cache.ts +5 -8
- package/src/target/gen-rojo-tree.ts +15 -0
- package/src/target/source-map.ts +65 -27
- package/src/target/studio-bridge.ts +3 -0
- package/src/types.ts +3 -0
- package/src/utils/project.ts +79 -31
- package/templates/README.md +7 -0
- package/templates/{game → game-modern}/cluaupp.config.json +2 -2
- package/templates/{game → game-modern}/package.json +1 -2
- package/templates/game-modern/rokit.toml +2 -0
- package/templates/game-modern/src/ReplicatedStorage/Shared/GameInfo.clp +1 -0
- package/templates/game-modern/src/ServerScriptService/Handlers/Guard.server.clp +4 -0
- package/templates/game-modern/src/ServerScriptService/Main.server.clp +8 -0
- package/templates/game-modern/src/StarterPlayer/StarterPlayerScripts/Main.client.clp +4 -0
- package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau +0 -29
- package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau.map.json +0 -13
- package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau +0 -83
- package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau.map.json +0 -13
- package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau +0 -18
- package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau.map.json +0 -13
- package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau +0 -14
- package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau.map.json +0 -13
- package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau +0 -15
- package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau.map.json +0 -13
- package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau +0 -50
- package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau.map.json +0 -13
- package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau +0 -12
- package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau.map.json +0 -13
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau +0 -16
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau.map.json +0 -13
- package/examples/game/src/Client/Controllers/DataController.client.clpp +0 -7
- package/examples/game/src/Client/Main.client.clpp +0 -4
- package/examples/game/src/Declarations/Net.flare +0 -5
- package/examples/game/src/Include/GameTypes.clh +0 -40
- package/examples/game/src/Modules/Core/Guard.server.clpp +0 -7
- package/examples/game/src/Modules/Core/PlayerHandler.server.clpp +0 -23
- package/examples/game/src/Server/Boot/DataBoot.server.clpp +0 -13
- package/examples/game/src/Server/Main.server.clpp +0 -4
- package/templates/game/.clangd +0 -25
- package/templates/game/.vscode/extensions.json +0 -8
- package/templates/game/.vscode/settings.json +0 -17
- package/templates/game/Packages/.gitkeep +0 -0
- package/templates/game/compile_flags.txt +0 -6
- package/templates/game/default.project.json +0 -41
- package/templates/game/include/clpp/datatypes.clh +0 -575
- package/templates/game/include/clpp/generated/enums.clh +0 -4920
- package/templates/game/include/clpp/generated/instances.clh +0 -87182
- package/templates/game/include/clpp/libs/arrayindexer.clh +0 -12
- package/templates/game/include/clpp/libs/axiom.clh +0 -154
- package/templates/game/include/clpp/libs/bloom.clh +0 -58
- package/templates/game/include/clpp/libs/coil.clh +0 -37
- package/templates/game/include/clpp/libs/crest.clh +0 -22
- package/templates/game/include/clpp/libs/echo.clh +0 -52
- package/templates/game/include/clpp/libs/ember.clh +0 -17
- package/templates/game/include/clpp/libs/flare.clh +0 -65
- package/templates/game/include/clpp/libs/gleam.clh +0 -111
- package/templates/game/include/clpp/libs/guide.clh +0 -42
- package/templates/game/include/clpp/libs/helm.clh +0 -31
- package/templates/game/include/clpp/libs/hive.clh +0 -20
- package/templates/game/include/clpp/libs/keep.clh +0 -158
- package/templates/game/include/clpp/libs/lens.clh +0 -24
- package/templates/game/include/clpp/libs/mint.clh +0 -28
- package/templates/game/include/clpp/libs/net.clh +0 -31
- package/templates/game/include/clpp/libs/occlude.clh +0 -15
- package/templates/game/include/clpp/libs/pin.clh +0 -28
- package/templates/game/include/clpp/libs/promise.clh +0 -24
- package/templates/game/include/clpp/libs/roster.clh +0 -123
- package/templates/game/include/clpp/libs/shift.clh +0 -22
- package/templates/game/include/clpp/libs/spark.clh +0 -28
- package/templates/game/include/clpp/libs/stage.clh +0 -23
- package/templates/game/include/clpp/libs/sweep.clh +0 -23
- package/templates/game/include/clpp/libs/trace.clh +0 -18
- package/templates/game/include/clpp/libs/ward.clh +0 -19
- package/templates/game/include/clpp/libs.clh +0 -31
- package/templates/game/include/clpp/roblox.clh +0 -41
- package/templates/game/rokit.toml +0 -5
- package/templates/game/src/Client/Controllers/DataController.client.clpp +0 -7
- package/templates/game/src/Client/Main.client.clpp +0 -4
- package/templates/game/src/Declarations/Net.flare +0 -5
- package/templates/game/src/Include/GameTypes.clh +0 -40
- package/templates/game/src/Modules/Core/Guard.server.clpp +0 -7
- package/templates/game/src/Modules/Core/PlayerHandler.server.clpp +0 -24
- package/templates/game/src/Server/Boot/DataBoot.server.clpp +0 -13
- package/templates/game/src/Server/Main.server.clpp +0 -4
- package/templates/game/stylua.toml +0 -9
- package/templates/game/wally.toml +0 -12
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@clpp.roblox` — the typed Roblox API surface (classes, datatypes, enums, services, globals,
|
|
3
|
+
* libraries) as the checker sees it. Loaded once from `api/roblox/surface.json`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import type { LFn, LType, RobloxSurface, SurfaceClass, SurfaceDatatype, SurfaceMember, SurfaceStatic } from "../../api/surface/model.js";
|
|
9
|
+
import { projectRoot } from "../../package-info.js";
|
|
10
|
+
import { T, type FnParam, type Type } from "../types.js";
|
|
11
|
+
|
|
12
|
+
/** Library functions whose `number` results are always integers on Roblox. */
|
|
13
|
+
const INT_RESULTS = new Set([
|
|
14
|
+
"math.floor", "math.ceil", "math.round", "math.sign", "string.len", "string.byte", "utf8.len", "utf8.codepoint",
|
|
15
|
+
"os.time", "buffer.len", "buffer.readi8", "buffer.readu8", "buffer.readi16", "buffer.readu16", "buffer.readi32",
|
|
16
|
+
"buffer.readu32", "buffer.readbits", "table.find", "table.maxn", "bit32.band", "bit32.bor", "bit32.bxor", "bit32.bnot",
|
|
17
|
+
"bit32.lshift", "bit32.rshift", "bit32.arshift", "bit32.lrotate", "bit32.rrotate", "bit32.extract", "bit32.replace",
|
|
18
|
+
"bit32.countlz", "bit32.countrz", "bit32.byteswap", "rawlen",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export interface MemberHit {
|
|
22
|
+
member: SurfaceMember;
|
|
23
|
+
owner: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class RobloxApi {
|
|
27
|
+
readonly classes: Record<string, SurfaceClass>;
|
|
28
|
+
readonly datatypes: Record<string, SurfaceDatatype>;
|
|
29
|
+
readonly enums: RobloxSurface["enums"];
|
|
30
|
+
readonly globals: RobloxSurface["globals"];
|
|
31
|
+
readonly libraries: RobloxSurface["libraries"];
|
|
32
|
+
readonly aliases: RobloxSurface["aliases"];
|
|
33
|
+
readonly services: string[];
|
|
34
|
+
private readonly memberCache = new Map<string, Map<string, MemberHit>>();
|
|
35
|
+
private readonly convertCache = new WeakMap<object, Type>();
|
|
36
|
+
|
|
37
|
+
constructor(readonly surface: RobloxSurface) {
|
|
38
|
+
this.classes = surface.classes;
|
|
39
|
+
this.datatypes = surface.datatypes;
|
|
40
|
+
this.enums = surface.enums;
|
|
41
|
+
this.globals = surface.globals;
|
|
42
|
+
this.libraries = surface.libraries;
|
|
43
|
+
this.aliases = surface.aliases;
|
|
44
|
+
this.services = Object.values(surface.classes)
|
|
45
|
+
.filter((c) => c.service)
|
|
46
|
+
.map((c) => c.name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
hasClass(name: string): boolean {
|
|
50
|
+
return Object.prototype.hasOwnProperty.call(this.classes, name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
hasDatatype(name: string): boolean {
|
|
54
|
+
return Object.prototype.hasOwnProperty.call(this.datatypes, name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
hasEnum(name: string): boolean {
|
|
58
|
+
return Object.prototype.hasOwnProperty.call(this.enums, name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
isService(name: string): boolean {
|
|
62
|
+
return this.hasClass(name) && this.classes[name].service;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
isA(cls: string, base: string): boolean {
|
|
66
|
+
let cur: string | null = cls;
|
|
67
|
+
let guard = 0;
|
|
68
|
+
while (cur && guard++ < 64) {
|
|
69
|
+
if (cur === base) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
cur = this.classes[cur]?.superclass ?? null;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** All members visible on a class (own first, then inherited). */
|
|
78
|
+
members(cls: string): Map<string, MemberHit> {
|
|
79
|
+
const cached = this.memberCache.get(cls);
|
|
80
|
+
if (cached) {
|
|
81
|
+
return cached;
|
|
82
|
+
}
|
|
83
|
+
const out = new Map<string, MemberHit>();
|
|
84
|
+
let cur: string | null = cls;
|
|
85
|
+
let guard = 0;
|
|
86
|
+
while (cur && guard++ < 64) {
|
|
87
|
+
const c: SurfaceClass | undefined = this.classes[cur];
|
|
88
|
+
if (!c) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
for (const [name, member] of Object.entries(c.members)) {
|
|
92
|
+
if (!out.has(name)) {
|
|
93
|
+
out.set(name, { member, owner: cur });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
cur = c.superclass;
|
|
97
|
+
}
|
|
98
|
+
this.memberCache.set(cls, out);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
member(cls: string, name: string): MemberHit | null {
|
|
103
|
+
return this.members(cls).get(name) ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
datatypeMember(name: string, member: string): SurfaceMember | null {
|
|
107
|
+
return this.datatypes[name]?.members[member] ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
datatypeStatic(name: string, member: string): SurfaceStatic | null {
|
|
111
|
+
return this.datatypes[name]?.statics[member] ?? null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
library(name: string): Record<string, SurfaceStatic> | null {
|
|
115
|
+
return this.libraries[name] ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------- LType → Type ----------
|
|
119
|
+
|
|
120
|
+
convert(lt: LType, context?: string): Type {
|
|
121
|
+
const cached = this.convertCache.get(lt);
|
|
122
|
+
if (cached && !context) {
|
|
123
|
+
return cached;
|
|
124
|
+
}
|
|
125
|
+
const out = this.convertUncached(lt, context);
|
|
126
|
+
if (!context) {
|
|
127
|
+
this.convertCache.set(lt, out);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private convertUncached(lt: LType, context?: string): Type {
|
|
133
|
+
switch (lt.k) {
|
|
134
|
+
case "name":
|
|
135
|
+
return this.convertName(lt.name, lt.args);
|
|
136
|
+
case "opt":
|
|
137
|
+
return T.option(this.convert(lt.t));
|
|
138
|
+
case "union": {
|
|
139
|
+
const hasNil = lt.ts.some((t) => t.k === "name" && t.name === "nil");
|
|
140
|
+
const rest = lt.ts.filter((t) => !(t.k === "name" && t.name === "nil")).map((t) => this.convert(t));
|
|
141
|
+
const allLit = rest.every((r) => r.k === "lit");
|
|
142
|
+
let base: Type;
|
|
143
|
+
if (rest.length === 1) {
|
|
144
|
+
base = rest[0];
|
|
145
|
+
} else if (!allLit && rest.every((r) => r.k === "instance")) {
|
|
146
|
+
base = T.instance("Instance");
|
|
147
|
+
} else if (!allLit && rest.some((r) => r.k === "any")) {
|
|
148
|
+
base = T.any;
|
|
149
|
+
} else {
|
|
150
|
+
base = { k: "union", members: rest };
|
|
151
|
+
}
|
|
152
|
+
return hasNil ? T.option(base) : base;
|
|
153
|
+
}
|
|
154
|
+
case "inter": {
|
|
155
|
+
const fns = lt.ts.filter((t) => t.k === "fn") as LFn[];
|
|
156
|
+
if (fns.length === lt.ts.length && fns.length > 0) {
|
|
157
|
+
const converted = fns.map((f) => this.convertFn(f));
|
|
158
|
+
const first = converted[0] as Extract<Type, { k: "func" }>;
|
|
159
|
+
return { ...first, overloads: converted };
|
|
160
|
+
}
|
|
161
|
+
const fields: Record<string, Type> = {};
|
|
162
|
+
for (const t of lt.ts) {
|
|
163
|
+
if (t.k === "table") {
|
|
164
|
+
for (const [k, v] of Object.entries(t.fields)) {
|
|
165
|
+
fields[k] = this.convert(v);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { k: "table", fields };
|
|
170
|
+
}
|
|
171
|
+
case "array":
|
|
172
|
+
return T.list(this.convert(lt.t));
|
|
173
|
+
case "map":
|
|
174
|
+
return T.dict(this.convert(lt.key), this.convert(lt.value));
|
|
175
|
+
case "table": {
|
|
176
|
+
const fields: Record<string, Type> = {};
|
|
177
|
+
for (const [k, v] of Object.entries(lt.fields)) {
|
|
178
|
+
fields[k] = this.convert(v);
|
|
179
|
+
}
|
|
180
|
+
return { k: "table", fields };
|
|
181
|
+
}
|
|
182
|
+
case "fn":
|
|
183
|
+
return this.convertFn(lt, context);
|
|
184
|
+
case "lit":
|
|
185
|
+
return { k: "lit", value: lt.value };
|
|
186
|
+
case "pack":
|
|
187
|
+
return T.any;
|
|
188
|
+
default:
|
|
189
|
+
return T.any;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
convertFn(fn: LFn, qualifiedName?: string): Type {
|
|
194
|
+
const params: FnParam[] = fn.params.map((p, i) => {
|
|
195
|
+
const t = this.convert(p.t);
|
|
196
|
+
return { name: p.name || `arg${i + 1}`, type: t.k === "option" ? t.inner : t, optional: t.k === "option" || (t.k === "any" && false) };
|
|
197
|
+
});
|
|
198
|
+
let ret: Type;
|
|
199
|
+
const rets = fn.returns.map((r) => this.convert(r));
|
|
200
|
+
if (rets.length === 0) {
|
|
201
|
+
ret = fn.returnsVariadic ? T.any : T.void;
|
|
202
|
+
} else if (rets.length === 1) {
|
|
203
|
+
ret = rets[0];
|
|
204
|
+
} else {
|
|
205
|
+
ret = { k: "tuple", elems: rets };
|
|
206
|
+
}
|
|
207
|
+
if (qualifiedName && INT_RESULTS.has(qualifiedName)) {
|
|
208
|
+
ret = ret.k === "option" ? T.option(T.int) : ret.k === "float" ? T.int : ret;
|
|
209
|
+
}
|
|
210
|
+
const out: Type = { k: "func", params, ret, variadic: fn.variadic ? this.convert(fn.variadic) : null };
|
|
211
|
+
if (fn.generics?.length) {
|
|
212
|
+
out.generics = fn.generics;
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private convertName(name: string, args?: LType[]): Type {
|
|
218
|
+
switch (name) {
|
|
219
|
+
case "number":
|
|
220
|
+
case "double":
|
|
221
|
+
case "float":
|
|
222
|
+
return T.float;
|
|
223
|
+
case "int":
|
|
224
|
+
case "int64":
|
|
225
|
+
return T.int;
|
|
226
|
+
case "string":
|
|
227
|
+
return T.string;
|
|
228
|
+
case "boolean":
|
|
229
|
+
case "bool":
|
|
230
|
+
return T.bool;
|
|
231
|
+
case "nil":
|
|
232
|
+
case "()":
|
|
233
|
+
return T.void;
|
|
234
|
+
case "any":
|
|
235
|
+
case "unknown":
|
|
236
|
+
case "userdata":
|
|
237
|
+
case "Object":
|
|
238
|
+
return name === "Object" ? T.instance("Object") : T.any;
|
|
239
|
+
case "never":
|
|
240
|
+
return T.never;
|
|
241
|
+
case "thread":
|
|
242
|
+
return T.thread;
|
|
243
|
+
case "buffer":
|
|
244
|
+
return T.buffer;
|
|
245
|
+
case "vector":
|
|
246
|
+
return T.datatype("Vector3");
|
|
247
|
+
case "RBXScriptSignal":
|
|
248
|
+
return { k: "rbxSignal", args: (args || []).filter((a) => !(a.k === "name" && a.name === "()")).map((a) => this.convert(a)) };
|
|
249
|
+
case "Instances":
|
|
250
|
+
return T.list(T.instance("Instance"));
|
|
251
|
+
case "Objects":
|
|
252
|
+
return T.list(T.instance("Instance"));
|
|
253
|
+
case "Enum":
|
|
254
|
+
return { k: "rbxEnum", enumName: "*" };
|
|
255
|
+
case "EnumItem":
|
|
256
|
+
return { k: "rbxEnumItem", enumName: "*" };
|
|
257
|
+
default:
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
if (this.hasClass(name)) {
|
|
261
|
+
return T.instance(name);
|
|
262
|
+
}
|
|
263
|
+
if (this.hasDatatype(name)) {
|
|
264
|
+
return T.datatype(name);
|
|
265
|
+
}
|
|
266
|
+
if (name.startsWith("Enum") && this.hasEnum(name.slice(4))) {
|
|
267
|
+
return { k: "rbxEnumItem", enumName: name.slice(4) };
|
|
268
|
+
}
|
|
269
|
+
if (name.startsWith("Enum.") && this.hasEnum(name.slice(5))) {
|
|
270
|
+
return { k: "rbxEnumItem", enumName: name.slice(5) };
|
|
271
|
+
}
|
|
272
|
+
const alias = this.aliases[name];
|
|
273
|
+
if (alias) {
|
|
274
|
+
return this.convert(alias);
|
|
275
|
+
}
|
|
276
|
+
return T.any;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Function type of a library member (`math.floor`), with integer results applied. */
|
|
280
|
+
libraryFunction(lib: string, member: string): Type | null {
|
|
281
|
+
const st = this.libraries[lib]?.[member];
|
|
282
|
+
if (!st) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
if (st.kind === "value") {
|
|
286
|
+
return st.type ? this.convert(st.type) : T.any;
|
|
287
|
+
}
|
|
288
|
+
const fns = (st.overloads || []).map((f) => this.convertFn(f, `${lib}.${member}`));
|
|
289
|
+
if (fns.length === 0) {
|
|
290
|
+
return T.any;
|
|
291
|
+
}
|
|
292
|
+
const first = fns[0] as Extract<Type, { k: "func" }>;
|
|
293
|
+
return fns.length > 1 ? { ...first, overloads: fns } : first;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
globalFunction(name: string): Type | null {
|
|
297
|
+
const g = this.globals[name];
|
|
298
|
+
if (!g) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
if (g.overloads) {
|
|
302
|
+
const fns = g.overloads.map((f) => this.convertFn(f, name));
|
|
303
|
+
const first = fns[0] as Extract<Type, { k: "func" }>;
|
|
304
|
+
return fns.length > 1 ? { ...first, overloads: fns } : first;
|
|
305
|
+
}
|
|
306
|
+
return g.type ? this.convert(g.type) : T.any;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
methodType(member: SurfaceMember): Type {
|
|
310
|
+
const fns = (member.overloads || []).map((f) => this.convertFn(f));
|
|
311
|
+
if (fns.length === 0) {
|
|
312
|
+
return T.func([], T.void);
|
|
313
|
+
}
|
|
314
|
+
const first = fns[0] as Extract<Type, { k: "func" }>;
|
|
315
|
+
return fns.length > 1 ? { ...first, overloads: fns } : first;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
staticFunctionType(st: SurfaceStatic): Type {
|
|
319
|
+
if (st.kind === "value") {
|
|
320
|
+
return st.type ? this.convert(st.type) : T.any;
|
|
321
|
+
}
|
|
322
|
+
const fns = (st.overloads || []).map((f) => this.convertFn(f));
|
|
323
|
+
const first = fns[0] as Extract<Type, { k: "func" }>;
|
|
324
|
+
return fns.length > 1 ? { ...first, overloads: fns } : first;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Result type of `left <op> right` for datatypes (Vector3 + Vector3, CFrame * Vector3, 2 * v). */
|
|
328
|
+
operator(op: string, left: Type, right: Type): Type | null {
|
|
329
|
+
const meta: Record<string, string> = { "+": "__add", "-": "__sub", "*": "__mul", "/": "__div", "%": "__mod" };
|
|
330
|
+
const name = meta[op];
|
|
331
|
+
if (!name) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const tryOn = (dt: string, other: Type): Type | null => {
|
|
335
|
+
for (const fn of this.datatypes[dt]?.operators[name] || []) {
|
|
336
|
+
const param = fn.params[0] ? this.convert(fn.params[0].t) : null;
|
|
337
|
+
if (!param || this.acceptsOperand(param, other)) {
|
|
338
|
+
return fn.returns[0] ? this.convert(fn.returns[0]) : null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
};
|
|
343
|
+
if (left.k === "datatype") {
|
|
344
|
+
const r = tryOn(left.name, right);
|
|
345
|
+
if (r) {
|
|
346
|
+
return r;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (right.k === "datatype" && (op === "*" || op === "+") && (left.k === "int" || left.k === "float" || left.k === "lit")) {
|
|
350
|
+
return tryOn(right.name, left);
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private acceptsOperand(param: Type, arg: Type): boolean {
|
|
356
|
+
if (param.k === "union") {
|
|
357
|
+
return param.members.some((m) => this.acceptsOperand(m, arg));
|
|
358
|
+
}
|
|
359
|
+
if (param.k === "float" || param.k === "int") {
|
|
360
|
+
return arg.k === "int" || arg.k === "float" || (arg.k === "lit" && typeof arg.value === "number");
|
|
361
|
+
}
|
|
362
|
+
if (param.k === "datatype") {
|
|
363
|
+
return arg.k === "datatype" && arg.name === param.name;
|
|
364
|
+
}
|
|
365
|
+
return param.k === "any";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
unaryMinus(t: Type): boolean {
|
|
369
|
+
return t.k === "datatype" && Boolean(this.datatypes[t.name]?.operators.__unm);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Creation policy for `Instance.new("X")` / `X()`. */
|
|
373
|
+
creationError(cls: string): string | null {
|
|
374
|
+
const c = this.classes[cls];
|
|
375
|
+
if (!c) {
|
|
376
|
+
return `'${cls}' is not a Roblox class`;
|
|
377
|
+
}
|
|
378
|
+
if (c.service) {
|
|
379
|
+
return `${cls} is a service; get it with game.GetService("${cls}") instead of creating it`;
|
|
380
|
+
}
|
|
381
|
+
if (!c.creatable) {
|
|
382
|
+
return `${cls} cannot be created with Instance.new (it is ${c.abstract ? "abstract" : "not creatable"})`;
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let shared: RobloxApi | null = null;
|
|
389
|
+
|
|
390
|
+
export function surfacePath(): string {
|
|
391
|
+
return path.join(projectRoot, "api", "roblox", "surface.json");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function robloxApi(): RobloxApi {
|
|
395
|
+
if (!shared) {
|
|
396
|
+
const file = surfacePath();
|
|
397
|
+
if (!fs.existsSync(file)) {
|
|
398
|
+
throw new Error(`cluaupp: missing Roblox surface ${file}. Run \`cluaupp api surface\` to rebuild it.`);
|
|
399
|
+
}
|
|
400
|
+
shared = new RobloxApi(JSON.parse(fs.readFileSync(file, "utf8")) as RobloxSurface);
|
|
401
|
+
}
|
|
402
|
+
return shared;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Test hook. */
|
|
406
|
+
export function setRobloxApi(api: RobloxApi | null): void {
|
|
407
|
+
shared = api;
|
|
408
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server/client authority rules. Each rule names a service, a class member or a service member,
|
|
3
|
+
* and the only run context where it works. Members apply to subclasses (RemoteEvent rules also
|
|
4
|
+
* cover UnreliableRemoteEvent through the explicit entries).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type RunContext = "server" | "client" | "shared" | "plugin";
|
|
8
|
+
|
|
9
|
+
export interface ContextRule {
|
|
10
|
+
only: "server" | "client";
|
|
11
|
+
why: string;
|
|
12
|
+
severity?: "error" | "warning";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** `service:Name` → rule for touching the service at all. */
|
|
16
|
+
export const SERVICE_RULES: Record<string, ContextRule> = {
|
|
17
|
+
ServerStorage: { only: "server", why: "ServerStorage is never replicated to clients" },
|
|
18
|
+
ServerScriptService: { only: "server", why: "ServerScriptService is never replicated to clients" },
|
|
19
|
+
DataStoreService: { only: "server", why: "DataStores can only be accessed by the server" },
|
|
20
|
+
MessagingService: { only: "server", why: "MessagingService only works on the server" },
|
|
21
|
+
MemoryStoreService: { only: "server", why: "MemoryStoreService only works on the server" },
|
|
22
|
+
AnalyticsService: { only: "server", why: "AnalyticsService events are logged from the server" },
|
|
23
|
+
OpenCloudService: { only: "server", why: "OpenCloudService only works on the server" },
|
|
24
|
+
UserInputService: { only: "client", why: "UserInputService reads the local device; on the server it has no input" },
|
|
25
|
+
ContextActionService: { only: "client", why: "ContextActionService binds local input" },
|
|
26
|
+
HapticService: { only: "client", why: "HapticService drives local devices" },
|
|
27
|
+
VRService: { only: "client", why: "VRService reads the local headset" },
|
|
28
|
+
GuiService: { only: "client", why: "GuiService controls the local player's GUI", severity: "warning" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** `Class.Member` → rule (checked against the member's owner chain). */
|
|
32
|
+
export const MEMBER_RULES: Record<string, ContextRule> = {
|
|
33
|
+
"Players.LocalPlayer": { only: "client", why: "Players.LocalPlayer is nil on the server" },
|
|
34
|
+
"Player.GetMouse": { only: "client", why: "GetMouse only works for the local player" },
|
|
35
|
+
"RunService.RenderStepped": { only: "client", why: "RenderStepped only fires on clients" },
|
|
36
|
+
"RunService.BindToRenderStep": { only: "client", why: "BindToRenderStep only works on clients" },
|
|
37
|
+
"RunService.UnbindFromRenderStep": { only: "client", why: "UnbindFromRenderStep only works on clients" },
|
|
38
|
+
"StarterGui.SetCore": { only: "client", why: "SetCore only works from a LocalScript" },
|
|
39
|
+
"StarterGui.GetCore": { only: "client", why: "GetCore only works from a LocalScript" },
|
|
40
|
+
"StarterGui.SetCoreGuiEnabled": { only: "client", why: "SetCoreGuiEnabled only works from a LocalScript" },
|
|
41
|
+
"RemoteEvent.FireServer": { only: "client", why: "FireServer sends from a client to the server" },
|
|
42
|
+
"RemoteEvent.OnClientEvent": { only: "client", why: "OnClientEvent fires on clients" },
|
|
43
|
+
"RemoteEvent.FireClient": { only: "server", why: "FireClient sends from the server to one client" },
|
|
44
|
+
"RemoteEvent.FireAllClients": { only: "server", why: "FireAllClients sends from the server" },
|
|
45
|
+
"RemoteEvent.OnServerEvent": { only: "server", why: "OnServerEvent fires on the server" },
|
|
46
|
+
"UnreliableRemoteEvent.FireServer": { only: "client", why: "FireServer sends from a client to the server" },
|
|
47
|
+
"UnreliableRemoteEvent.OnClientEvent": { only: "client", why: "OnClientEvent fires on clients" },
|
|
48
|
+
"UnreliableRemoteEvent.FireClient": { only: "server", why: "FireClient sends from the server to one client" },
|
|
49
|
+
"UnreliableRemoteEvent.FireAllClients": { only: "server", why: "FireAllClients sends from the server" },
|
|
50
|
+
"UnreliableRemoteEvent.OnServerEvent": { only: "server", why: "OnServerEvent fires on the server" },
|
|
51
|
+
"RemoteFunction.InvokeServer": { only: "client", why: "InvokeServer calls the server from a client" },
|
|
52
|
+
"RemoteFunction.OnClientInvoke": { only: "client", why: "OnClientInvoke runs on clients" },
|
|
53
|
+
"RemoteFunction.InvokeClient": { only: "server", why: "InvokeClient calls a client from the server" },
|
|
54
|
+
"RemoteFunction.OnServerInvoke": { only: "server", why: "OnServerInvoke runs on the server" },
|
|
55
|
+
"DataModel.BindToClose": { only: "server", why: "BindToClose runs when the server shuts down" },
|
|
56
|
+
"Players.BanAsync": { only: "server", why: "bans are issued by the server" },
|
|
57
|
+
"Players.UnbanAsync": { only: "server", why: "bans are issued by the server" },
|
|
58
|
+
"Players.CreateHumanoidModelFromUserId": { only: "server", why: "only the server can build character models" },
|
|
59
|
+
"Players.CreateHumanoidModelFromDescription": { only: "server", why: "only the server can build character models" },
|
|
60
|
+
"Player.LoadCharacter": { only: "server", why: "characters are spawned by the server" },
|
|
61
|
+
"Player.LoadCharacterAsync": { only: "server", why: "characters are spawned by the server" },
|
|
62
|
+
"BasePart.SetNetworkOwner": { only: "server", why: "network ownership is assigned by the server" },
|
|
63
|
+
"BasePart.SetNetworkOwnershipAuto": { only: "server", why: "network ownership is assigned by the server" },
|
|
64
|
+
"MarketplaceService.ProcessReceipt": { only: "server", why: "developer product receipts are processed by the server" },
|
|
65
|
+
"BadgeService.AwardBadge": { only: "server", why: "badges are awarded by the server" },
|
|
66
|
+
"BadgeService.AwardBadgeAsync": { only: "server", why: "badges are awarded by the server" },
|
|
67
|
+
"TextService.FilterStringAsync": { only: "server", why: "text filtering runs on the server" },
|
|
68
|
+
"HttpService.GetAsync": { only: "server", why: "HTTP requests can only be made by the server" },
|
|
69
|
+
"HttpService.PostAsync": { only: "server", why: "HTTP requests can only be made by the server" },
|
|
70
|
+
"HttpService.RequestAsync": { only: "server", why: "HTTP requests can only be made by the server" },
|
|
71
|
+
"TeleportService.TeleportAsync": { only: "server", why: "TeleportAsync is server-only" },
|
|
72
|
+
"TeleportService.ReserveServer": { only: "server", why: "ReserveServer is server-only" },
|
|
73
|
+
"TeleportService.ReserveServerAsync": { only: "server", why: "ReserveServerAsync is server-only" },
|
|
74
|
+
"InsertService.LoadAsset": { only: "server", why: "LoadAsset is server-only" },
|
|
75
|
+
"PhysicsService.RegisterCollisionGroup": { only: "server", why: "collision groups are registered by the server" },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Members that work but are a known exploit or freeze vector. */
|
|
79
|
+
export const RISKY_MEMBERS: Record<string, string> = {
|
|
80
|
+
"RemoteFunction.InvokeClient": "the server yields until the client answers; a malicious client can hang it forever — prefer a RemoteEvent",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function contextLabel(ctx: RunContext): string {
|
|
84
|
+
switch (ctx) {
|
|
85
|
+
case "server":
|
|
86
|
+
return "a server Script";
|
|
87
|
+
case "client":
|
|
88
|
+
return "a LocalScript";
|
|
89
|
+
case "plugin":
|
|
90
|
+
return "a plugin";
|
|
91
|
+
default:
|
|
92
|
+
return "a shared module";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function runContextOf(fileName: string): RunContext {
|
|
97
|
+
const base = fileName.replace(/\\/g, "/").split("/").pop()!.toLowerCase();
|
|
98
|
+
if (/\.server\.clp$/.test(base)) {
|
|
99
|
+
return "server";
|
|
100
|
+
}
|
|
101
|
+
if (/\.client\.clp$/.test(base)) {
|
|
102
|
+
return "client";
|
|
103
|
+
}
|
|
104
|
+
if (/\.plugin\.clp$/.test(base)) {
|
|
105
|
+
return "plugin";
|
|
106
|
+
}
|
|
107
|
+
return "shared";
|
|
108
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Source text with fast offset → line/column mapping. Columns are 1-based UTF-16 units. */
|
|
2
|
+
|
|
3
|
+
export interface Span {
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Position {
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SourceFile {
|
|
14
|
+
readonly lineStarts: number[];
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
readonly path: string,
|
|
18
|
+
readonly text: string,
|
|
19
|
+
) {
|
|
20
|
+
const starts = [0];
|
|
21
|
+
for (let i = 0; i < text.length; i++) {
|
|
22
|
+
const c = text.charCodeAt(i);
|
|
23
|
+
if (c === 10) {
|
|
24
|
+
starts.push(i + 1);
|
|
25
|
+
} else if (c === 13) {
|
|
26
|
+
if (text.charCodeAt(i + 1) === 10) {
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
starts.push(i + 1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
this.lineStarts = starts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
position(offset: number): Position {
|
|
36
|
+
const starts = this.lineStarts;
|
|
37
|
+
let lo = 0;
|
|
38
|
+
let hi = starts.length - 1;
|
|
39
|
+
while (lo < hi) {
|
|
40
|
+
const mid = (lo + hi + 1) >> 1;
|
|
41
|
+
if (starts[mid] <= offset) {
|
|
42
|
+
lo = mid;
|
|
43
|
+
} else {
|
|
44
|
+
hi = mid - 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { line: lo + 1, column: offset - starts[lo] + 1 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
lineText(line: number): string {
|
|
51
|
+
const start = this.lineStarts[line - 1] ?? 0;
|
|
52
|
+
const next = this.lineStarts[line] ?? this.text.length;
|
|
53
|
+
return this.text.slice(start, next).replace(/\r?\n$|\r$/, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
slice(span: Span): string {
|
|
57
|
+
return this.text.slice(span.start, span.end);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function spanOf(start: number, end: number): Span {
|
|
62
|
+
return { start, end };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function joinSpans(a: Span, b: Span): Span {
|
|
66
|
+
return { start: Math.min(a.start, b.start), end: Math.max(a.end, b.end) };
|
|
67
|
+
}
|