luau-obfuscator 1.0.0
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/.github/workflows/release.yml +57 -0
- package/dist/index.cjs +1097 -0
- package/dist/index.d.cts +79 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +1068 -0
- package/generated/final.luau +1029 -0
- package/package.json +38 -0
- package/scripts/example.luau +1174 -0
- package/scripts/test.js +8 -0
- package/src/config.ts +80 -0
- package/src/index.ts +22 -0
- package/src/passes/ConstantArray.ts +90 -0
- package/src/passes/EncryptNumbers.ts +66 -0
- package/src/passes/EncryptStrings.ts +83 -0
- package/src/passes/GlobalMapping.ts +194 -0
- package/src/passes/InsertJunk.ts +185 -0
- package/src/passes/Minify.ts +6 -0
- package/src/passes/NumbersToExpressions.ts +86 -0
- package/src/passes/RenameVariables.ts +32 -0
- package/src/passes/StringsToExpressions.ts +56 -0
- package/src/passes/StripTypes.ts +192 -0
- package/src/passes/WrapInFunction.ts +32 -0
- package/src/passes/nodeFactory.ts +143 -0
- package/src/passes/walk.ts +173 -0
- package/src/pipeline.ts +58 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Program, Block, Statement, Expression,
|
|
3
|
+
} from "luau-parser"
|
|
4
|
+
import {
|
|
5
|
+
identifier, numberLiteral, binary, localStatement, doStatement, block,
|
|
6
|
+
ifStatement, ifClause,
|
|
7
|
+
} from "./nodeFactory"
|
|
8
|
+
|
|
9
|
+
export interface InsertJunkOptions {
|
|
10
|
+
/** 기존 statement 하나 앞에 junk를 끼워넣을 확률 (0~1) */
|
|
11
|
+
probability: number
|
|
12
|
+
/** 블록 하나당 최대 삽입 개수 */
|
|
13
|
+
maxPerBlock: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function randomInt(min: number, max: number): number {
|
|
17
|
+
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function randomName(): string {
|
|
21
|
+
return "_" + globalThis.crypto.randomUUID().replace(/-/g, "")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function junkNumberExpr(): Expression {
|
|
25
|
+
const op = Math.random() < 0.5 ? "+" : "*"
|
|
26
|
+
return binary(op, numberLiteral(randomInt(1, 999)), numberLiteral(randomInt(1, 999)))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 실행돼도 관찰 가능한 부작용이 전혀 없는 더미 statement 하나를 만듦.
|
|
31
|
+
* - 매번 새 랜덤 이름을 쓰므로 다른 변수와 충돌하지 않음
|
|
32
|
+
* - if 분기 조건이 실제로 참/거짓 어느 쪽이어도 몸통이 쓸모없는 local 선언뿐이라
|
|
33
|
+
* 상관없음 (opaque predicate를 엄밀하게 항상 거짓으로 만들 필요가 없음)
|
|
34
|
+
*/
|
|
35
|
+
function buildJunkStatement(): Statement {
|
|
36
|
+
const variants: Array<() => Statement> = [
|
|
37
|
+
() => localStatement(randomName(), junkNumberExpr()),
|
|
38
|
+
() => doStatement(block([localStatement(randomName(), junkNumberExpr())])),
|
|
39
|
+
() => ifStatement([
|
|
40
|
+
ifClause(
|
|
41
|
+
binary("==", numberLiteral(randomInt(1, 999)), numberLiteral(randomInt(1, 999))),
|
|
42
|
+
block([localStatement(randomName(), junkNumberExpr())]),
|
|
43
|
+
),
|
|
44
|
+
]),
|
|
45
|
+
]
|
|
46
|
+
return variants[randomInt(0, variants.length - 1)]()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function runInsertJunk(program: Program, options: InsertJunkOptions): void {
|
|
50
|
+
function processExpr(expr: Expression): void {
|
|
51
|
+
switch (expr.type) {
|
|
52
|
+
case "InterpolatedStringExpression":
|
|
53
|
+
for (const part of expr.parts) {
|
|
54
|
+
if (part.kind === "expression") processExpr(part.expression)
|
|
55
|
+
}
|
|
56
|
+
return
|
|
57
|
+
case "FunctionExpression":
|
|
58
|
+
processBlock(expr.func.body)
|
|
59
|
+
return
|
|
60
|
+
case "TableExpression":
|
|
61
|
+
for (const field of expr.fields) {
|
|
62
|
+
if (field.type === "TableFieldPositional") processExpr(field.value)
|
|
63
|
+
else if (field.type === "TableFieldNamed") processExpr(field.value)
|
|
64
|
+
else { processExpr(field.key); processExpr(field.value) }
|
|
65
|
+
}
|
|
66
|
+
return
|
|
67
|
+
case "BinaryExpression":
|
|
68
|
+
processExpr(expr.left)
|
|
69
|
+
processExpr(expr.right)
|
|
70
|
+
return
|
|
71
|
+
case "UnaryExpression":
|
|
72
|
+
processExpr(expr.argument)
|
|
73
|
+
return
|
|
74
|
+
case "MemberExpression":
|
|
75
|
+
processExpr(expr.object)
|
|
76
|
+
return
|
|
77
|
+
case "IndexExpression":
|
|
78
|
+
processExpr(expr.object)
|
|
79
|
+
processExpr(expr.index)
|
|
80
|
+
return
|
|
81
|
+
case "CallExpression":
|
|
82
|
+
processExpr(expr.callee)
|
|
83
|
+
expr.arguments.forEach(processExpr)
|
|
84
|
+
return
|
|
85
|
+
case "MethodCallExpression":
|
|
86
|
+
processExpr(expr.object)
|
|
87
|
+
expr.arguments.forEach(processExpr)
|
|
88
|
+
return
|
|
89
|
+
case "ParenthesizedExpression":
|
|
90
|
+
processExpr(expr.expression)
|
|
91
|
+
return
|
|
92
|
+
case "TypeAssertionExpression":
|
|
93
|
+
processExpr(expr.expression)
|
|
94
|
+
return
|
|
95
|
+
case "IfElseExpression":
|
|
96
|
+
for (const clause of expr.clauses) {
|
|
97
|
+
processExpr(clause.condition)
|
|
98
|
+
processExpr(clause.body)
|
|
99
|
+
}
|
|
100
|
+
processExpr(expr.alternate)
|
|
101
|
+
return
|
|
102
|
+
default:
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function processStatement(stmt: Statement): void {
|
|
108
|
+
switch (stmt.type) {
|
|
109
|
+
case "LocalStatement":
|
|
110
|
+
stmt.init.forEach(processExpr)
|
|
111
|
+
return
|
|
112
|
+
case "LocalFunctionStatement":
|
|
113
|
+
processBlock(stmt.func.body)
|
|
114
|
+
return
|
|
115
|
+
case "FunctionDeclarationStatement":
|
|
116
|
+
processBlock(stmt.func.body)
|
|
117
|
+
return
|
|
118
|
+
case "AssignmentStatement":
|
|
119
|
+
stmt.targets.forEach(processExpr)
|
|
120
|
+
stmt.values.forEach(processExpr)
|
|
121
|
+
return
|
|
122
|
+
case "CompoundAssignmentStatement":
|
|
123
|
+
processExpr(stmt.target)
|
|
124
|
+
processExpr(stmt.value)
|
|
125
|
+
return
|
|
126
|
+
case "CallStatement":
|
|
127
|
+
processExpr(stmt.expression)
|
|
128
|
+
return
|
|
129
|
+
case "DoStatement":
|
|
130
|
+
processBlock(stmt.body)
|
|
131
|
+
return
|
|
132
|
+
case "WhileStatement":
|
|
133
|
+
processExpr(stmt.condition)
|
|
134
|
+
processBlock(stmt.body)
|
|
135
|
+
return
|
|
136
|
+
case "RepeatStatement":
|
|
137
|
+
processBlock(stmt.body)
|
|
138
|
+
processExpr(stmt.condition)
|
|
139
|
+
return
|
|
140
|
+
case "IfStatement":
|
|
141
|
+
for (const clause of stmt.clauses) {
|
|
142
|
+
processExpr(clause.condition)
|
|
143
|
+
processBlock(clause.body)
|
|
144
|
+
}
|
|
145
|
+
if (stmt.alternate) processBlock(stmt.alternate)
|
|
146
|
+
return
|
|
147
|
+
case "NumericForStatement":
|
|
148
|
+
processExpr(stmt.start)
|
|
149
|
+
processExpr(stmt.end)
|
|
150
|
+
if (stmt.step) processExpr(stmt.step)
|
|
151
|
+
processBlock(stmt.body)
|
|
152
|
+
return
|
|
153
|
+
case "GenericForStatement":
|
|
154
|
+
stmt.iterators.forEach(processExpr)
|
|
155
|
+
processBlock(stmt.body)
|
|
156
|
+
return
|
|
157
|
+
case "ReturnStatement":
|
|
158
|
+
stmt.arguments.forEach(processExpr)
|
|
159
|
+
return
|
|
160
|
+
default:
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 자식 블록들부터 먼저 처리한 뒤(재귀 무한 방지 — 새로 끼워넣는 junk는
|
|
166
|
+
* 다시 처리하지 않음), 현재 블록에 junk를 끼워넣음. junk는 기존 statement
|
|
167
|
+
* "앞"에만 넣으므로 return/break/continue(항상 블록 맨 끝에만 올 수 있음)
|
|
168
|
+
* 뒤에 잘못 삽입될 일이 없음. */
|
|
169
|
+
function processBlock(blk: Block): void {
|
|
170
|
+
for (const stmt of blk.statements) processStatement(stmt)
|
|
171
|
+
|
|
172
|
+
const result: Statement[] = []
|
|
173
|
+
let inserted = 0
|
|
174
|
+
for (const stmt of blk.statements) {
|
|
175
|
+
if (inserted < options.maxPerBlock && Math.random() < options.probability) {
|
|
176
|
+
result.push(buildJunkStatement())
|
|
177
|
+
inserted++
|
|
178
|
+
}
|
|
179
|
+
result.push(stmt)
|
|
180
|
+
}
|
|
181
|
+
blk.statements = result
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
processBlock(program.body)
|
|
185
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Program, Expression } from "luau-parser"
|
|
2
|
+
import { transformExpressions } from "./walk"
|
|
3
|
+
import { numberLiteral, binary, paren } from "./nodeFactory"
|
|
4
|
+
|
|
5
|
+
export interface NumbersToExpressionsOptions {
|
|
6
|
+
/** 랜덤 보조항의 최소 절댓값 */
|
|
7
|
+
min: number
|
|
8
|
+
/** 랜덤 보조항의 최대 절댓값 */
|
|
9
|
+
max: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MAX_DEPTH = 1
|
|
13
|
+
const NEST_PROBABILITY = 0.35
|
|
14
|
+
|
|
15
|
+
function randomInt(min: number, max: number): number {
|
|
16
|
+
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function randSign(): 1 | -1 {
|
|
20
|
+
return Math.random() < 0.5 ? 1 : -1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Strategy = "add" | "sub" | "mul"
|
|
24
|
+
|
|
25
|
+
/** value를 min~max 범위 정수로 나눌 수 있으면 mul도 후보에 추가 */
|
|
26
|
+
function pickStrategy(value: number, min: number, max: number): Strategy {
|
|
27
|
+
const candidates: Strategy[] = ["add", "sub"]
|
|
28
|
+
if (value !== 0) {
|
|
29
|
+
for (let a = min; a <= max; a++) {
|
|
30
|
+
if (a !== 0 && value % a === 0) {
|
|
31
|
+
candidates.push("mul")
|
|
32
|
+
break
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return candidates[randomInt(0, candidates.length - 1)]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 확률적으로 피연산자를 리터럴 대신 한 단계 더 중첩된 식으로 치환 */
|
|
40
|
+
function operand(n: number, min: number, max: number, depth: number): Expression {
|
|
41
|
+
if (depth < MAX_DEPTH && Math.random() < NEST_PROBABILITY) {
|
|
42
|
+
return buildNumberExpr(n, min, max, depth + 1)
|
|
43
|
+
}
|
|
44
|
+
return numberLiteral(n)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildNumberExpr(value: number, min: number, max: number, depth = 0): Expression {
|
|
48
|
+
const strategy = pickStrategy(value, min, max)
|
|
49
|
+
let inner: Expression
|
|
50
|
+
|
|
51
|
+
switch (strategy) {
|
|
52
|
+
case "add": {
|
|
53
|
+
const a = randomInt(min, max) * randSign()
|
|
54
|
+
const b = value - a
|
|
55
|
+
inner = binary("+", operand(a, min, max, depth), operand(b, min, max, depth))
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
case "sub": {
|
|
59
|
+
const a = randomInt(min, max) * randSign()
|
|
60
|
+
const b = a - value
|
|
61
|
+
inner = binary("-", operand(a, min, max, depth), operand(b, min, max, depth))
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
case "mul": {
|
|
65
|
+
let a = 1
|
|
66
|
+
for (let cand = min; cand <= max; cand++) {
|
|
67
|
+
if (cand !== 0 && value % cand === 0) {
|
|
68
|
+
a = cand
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const b = value / a
|
|
73
|
+
inner = binary("*", operand(a, min, max, depth), operand(b, min, max, depth))
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return paren(inner)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function runNumbersToExpressions(program: Program, options: NumbersToExpressionsOptions): void {
|
|
82
|
+
transformExpressions(program, (expr) => {
|
|
83
|
+
if (expr.type !== "NumberLiteral") return
|
|
84
|
+
return buildNumberExpr(expr.value, options.min, options.max)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Program } from "luau-parser"
|
|
2
|
+
import { analyzeScopes, isGlobal } from "luau-parser"
|
|
3
|
+
|
|
4
|
+
export interface RenameVariablesOptions {
|
|
5
|
+
random?: () => string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let counter = 0
|
|
9
|
+
function defaultRandomName(): string {
|
|
10
|
+
counter += 1
|
|
11
|
+
return `_l${counter.toString(36)}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function runRenameVariables(program: Program, options: RenameVariablesOptions): void {
|
|
15
|
+
const analysis = analyzeScopes(program)
|
|
16
|
+
const makeName = options.random ?? defaultRandomName
|
|
17
|
+
|
|
18
|
+
for (const binding of analysis.bindings.values()) {
|
|
19
|
+
if (isGlobal(binding)) continue
|
|
20
|
+
if (binding.kind === "self") continue
|
|
21
|
+
|
|
22
|
+
const newName = makeName()
|
|
23
|
+
binding.name = newName
|
|
24
|
+
|
|
25
|
+
if (binding.declarationNode) {
|
|
26
|
+
binding.declarationNode.name = newName
|
|
27
|
+
}
|
|
28
|
+
for (const ref of binding.references) {
|
|
29
|
+
ref.name = newName
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Program, Expression } from "luau-parser"
|
|
2
|
+
import { transformExpressions } from "./walk"
|
|
3
|
+
import { numberLiteral, binary, call, member, identifier } from "./nodeFactory"
|
|
4
|
+
|
|
5
|
+
export interface StringsToExpressionsOptions {
|
|
6
|
+
/** 청크 하나에 들어가는 바이트 개수 최소값 */
|
|
7
|
+
min: number
|
|
8
|
+
/** 청크 하나에 들어가는 바이트 개수 최대값 */
|
|
9
|
+
max: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function randomInt(min: number, max: number): number {
|
|
13
|
+
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Luau 소스는 UTF-8 바이트 스트림이므로 charCodeAt이 아니라 실제 UTF-8 바이트로 변환.
|
|
17
|
+
* Buffer(Node 전용) 대신 표준 TextEncoder 사용 — 브라우저/번들러 환경에서도 동작. */
|
|
18
|
+
function toUtf8Bytes(value: string): number[] {
|
|
19
|
+
return Array.from(new TextEncoder().encode(value))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function splitByteChunks(bytes: number[], min: number, max: number): number[][] {
|
|
23
|
+
const chunks: number[][] = []
|
|
24
|
+
let i = 0
|
|
25
|
+
while (i < bytes.length) {
|
|
26
|
+
const size = Math.max(1, randomInt(min, max))
|
|
27
|
+
chunks.push(bytes.slice(i, i + size))
|
|
28
|
+
i += size
|
|
29
|
+
}
|
|
30
|
+
return chunks.length > 0 ? chunks : [bytes]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function stringCharCall(bytes: number[]): Expression {
|
|
34
|
+
// string.char(b1, b2, ...) — 원문 텍스트가 소스에 전혀 남지 않음
|
|
35
|
+
return call(member(identifier("string"), "char"), bytes.map(numberLiteral))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildConcatChain(chunks: number[][]): Expression {
|
|
39
|
+
let expr: Expression = stringCharCall(chunks[0])
|
|
40
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
41
|
+
expr = binary("..", expr, stringCharCall(chunks[i]))
|
|
42
|
+
}
|
|
43
|
+
return expr
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function runStringsToExpressions(program: Program, options: StringsToExpressionsOptions): void {
|
|
47
|
+
transformExpressions(program, (expr) => {
|
|
48
|
+
if (expr.type !== "StringLiteral") return
|
|
49
|
+
if (expr.value.length === 0) return
|
|
50
|
+
|
|
51
|
+
const bytes = toUtf8Bytes(expr.value)
|
|
52
|
+
const chunks = splitByteChunks(bytes, options.min, options.max)
|
|
53
|
+
|
|
54
|
+
return buildConcatChain(chunks)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Program, Block, Statement, Expression, FunctionBody,
|
|
3
|
+
} from "luau-parser"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 파이프라인 맨 앞에서 실행되어야 하는 패스.
|
|
7
|
+
*
|
|
8
|
+
* 이후 패스들(GlobalMapping, RenameVariables 등)은 값 트리만 순회하고
|
|
9
|
+
* `typeof(x)`, `x :: T` 같은 타입 노드 내부의 expression은 건드리지 않는다.
|
|
10
|
+
* 타입을 나중까지 들고 있으면 값 쪽 식별자는 바뀌는데 타입 쪽 참조는 원래
|
|
11
|
+
* 이름 그대로 남아 출력물이 타입체크 불가능한 상태가 된다
|
|
12
|
+
* (`typeof(runtimeValue)`가 obfuscate 이후에도 원본 이름을 참조하는 것이 그 예).
|
|
13
|
+
*
|
|
14
|
+
* 그래서 타입 정보 자체를 파이프라인 시작 시점에 전부 지워서 이 클래스의
|
|
15
|
+
* 버그를 원천 차단한다:
|
|
16
|
+
* - TypeAliasStatement / ExportTypeAliasStatement 문 자체를 블록에서 제거
|
|
17
|
+
* - TypedIdentifier / FunctionParameter 의 typeAnnotation 제거
|
|
18
|
+
* - FunctionBody 의 generics / varargTypeAnnotation / returnType 제거
|
|
19
|
+
* - `expr :: T` (TypeAssertionExpression) 은 T를 버리고 expr로 치환
|
|
20
|
+
*/
|
|
21
|
+
export function runStripTypes(program: Program): void {
|
|
22
|
+
stripBlock(program.body)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function stripBlock(block: Block): void {
|
|
26
|
+
const kept: Statement[] = []
|
|
27
|
+
for (const stmt of block.statements) {
|
|
28
|
+
if (stmt.type === "TypeAliasStatement" || stmt.type === "ExportTypeAliasStatement") {
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
stripStatement(stmt)
|
|
32
|
+
kept.push(stmt)
|
|
33
|
+
}
|
|
34
|
+
block.statements = kept
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stripFunctionBody(func: FunctionBody): void {
|
|
38
|
+
func.generics = []
|
|
39
|
+
for (const param of func.params) delete param.typeAnnotation
|
|
40
|
+
delete func.varargTypeAnnotation
|
|
41
|
+
delete func.returnType
|
|
42
|
+
stripBlock(func.body)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stripStatement(stmt: Statement): void {
|
|
46
|
+
switch (stmt.type) {
|
|
47
|
+
case "LocalStatement":
|
|
48
|
+
for (const name of stmt.names) delete name.typeAnnotation
|
|
49
|
+
for (let i = 0; i < stmt.init.length; i++) stmt.init[i] = stripExpr(stmt.init[i])
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
case "LocalFunctionStatement":
|
|
53
|
+
stripFunctionBody(stmt.func)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
case "FunctionDeclarationStatement":
|
|
57
|
+
stripFunctionBody(stmt.func)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
case "AssignmentStatement":
|
|
61
|
+
for (let i = 0; i < stmt.targets.length; i++) stmt.targets[i] = stripExpr(stmt.targets[i])
|
|
62
|
+
for (let i = 0; i < stmt.values.length; i++) stmt.values[i] = stripExpr(stmt.values[i])
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
case "CompoundAssignmentStatement":
|
|
66
|
+
stmt.target = stripExpr(stmt.target)
|
|
67
|
+
stmt.value = stripExpr(stmt.value)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
case "CallStatement":
|
|
71
|
+
stmt.expression = stripExpr(stmt.expression) as typeof stmt.expression
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
case "DoStatement":
|
|
75
|
+
stripBlock(stmt.body)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
case "WhileStatement":
|
|
79
|
+
stmt.condition = stripExpr(stmt.condition)
|
|
80
|
+
stripBlock(stmt.body)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
case "RepeatStatement":
|
|
84
|
+
stripBlock(stmt.body)
|
|
85
|
+
stmt.condition = stripExpr(stmt.condition)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
case "IfStatement":
|
|
89
|
+
for (const clause of stmt.clauses) {
|
|
90
|
+
clause.condition = stripExpr(clause.condition)
|
|
91
|
+
stripBlock(clause.body)
|
|
92
|
+
}
|
|
93
|
+
if (stmt.alternate) stripBlock(stmt.alternate)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
case "NumericForStatement":
|
|
97
|
+
delete stmt.variable.typeAnnotation
|
|
98
|
+
stmt.start = stripExpr(stmt.start)
|
|
99
|
+
stmt.end = stripExpr(stmt.end)
|
|
100
|
+
if (stmt.step) stmt.step = stripExpr(stmt.step)
|
|
101
|
+
stripBlock(stmt.body)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
case "GenericForStatement":
|
|
105
|
+
for (const v of stmt.variables) delete v.typeAnnotation
|
|
106
|
+
for (let i = 0; i < stmt.iterators.length; i++) stmt.iterators[i] = stripExpr(stmt.iterators[i])
|
|
107
|
+
stripBlock(stmt.body)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
case "ReturnStatement":
|
|
111
|
+
for (let i = 0; i < stmt.arguments.length; i++) stmt.arguments[i] = stripExpr(stmt.arguments[i])
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
case "BreakStatement":
|
|
115
|
+
case "ContinueStatement":
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stripExpr(expr: Expression): Expression {
|
|
121
|
+
switch (expr.type) {
|
|
122
|
+
case "InterpolatedStringExpression":
|
|
123
|
+
for (const part of expr.parts) {
|
|
124
|
+
if (part.kind === "expression") part.expression = stripExpr(part.expression)
|
|
125
|
+
}
|
|
126
|
+
return expr
|
|
127
|
+
|
|
128
|
+
case "FunctionExpression":
|
|
129
|
+
stripFunctionBody(expr.func)
|
|
130
|
+
return expr
|
|
131
|
+
|
|
132
|
+
case "TableExpression":
|
|
133
|
+
for (const field of expr.fields) {
|
|
134
|
+
if (field.type === "TableFieldPositional") {
|
|
135
|
+
field.value = stripExpr(field.value)
|
|
136
|
+
} else if (field.type === "TableFieldNamed") {
|
|
137
|
+
field.value = stripExpr(field.value)
|
|
138
|
+
} else if (field.type === "TableFieldComputed") {
|
|
139
|
+
field.key = stripExpr(field.key)
|
|
140
|
+
field.value = stripExpr(field.value)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return expr
|
|
144
|
+
|
|
145
|
+
case "BinaryExpression":
|
|
146
|
+
expr.left = stripExpr(expr.left)
|
|
147
|
+
expr.right = stripExpr(expr.right)
|
|
148
|
+
return expr
|
|
149
|
+
|
|
150
|
+
case "UnaryExpression":
|
|
151
|
+
expr.argument = stripExpr(expr.argument)
|
|
152
|
+
return expr
|
|
153
|
+
|
|
154
|
+
case "MemberExpression":
|
|
155
|
+
expr.object = stripExpr(expr.object)
|
|
156
|
+
return expr
|
|
157
|
+
|
|
158
|
+
case "IndexExpression":
|
|
159
|
+
expr.object = stripExpr(expr.object)
|
|
160
|
+
expr.index = stripExpr(expr.index)
|
|
161
|
+
return expr
|
|
162
|
+
|
|
163
|
+
case "CallExpression":
|
|
164
|
+
expr.callee = stripExpr(expr.callee)
|
|
165
|
+
for (let i = 0; i < expr.arguments.length; i++) expr.arguments[i] = stripExpr(expr.arguments[i])
|
|
166
|
+
return expr
|
|
167
|
+
|
|
168
|
+
case "MethodCallExpression":
|
|
169
|
+
expr.object = stripExpr(expr.object)
|
|
170
|
+
for (let i = 0; i < expr.arguments.length; i++) expr.arguments[i] = stripExpr(expr.arguments[i])
|
|
171
|
+
return expr
|
|
172
|
+
|
|
173
|
+
case "ParenthesizedExpression":
|
|
174
|
+
expr.expression = stripExpr(expr.expression)
|
|
175
|
+
return expr
|
|
176
|
+
|
|
177
|
+
case "TypeAssertionExpression":
|
|
178
|
+
// `expr :: T` -> T를 버리고 expr만 남김 (괄호로 감싸서 우선순위 보존)
|
|
179
|
+
return stripExpr(expr.expression)
|
|
180
|
+
|
|
181
|
+
case "IfElseExpression":
|
|
182
|
+
for (const clause of expr.clauses) {
|
|
183
|
+
clause.condition = stripExpr(clause.condition)
|
|
184
|
+
clause.body = stripExpr(clause.body)
|
|
185
|
+
}
|
|
186
|
+
expr.alternate = stripExpr(expr.alternate)
|
|
187
|
+
return expr
|
|
188
|
+
|
|
189
|
+
default:
|
|
190
|
+
return expr
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Program } from "luau-parser"
|
|
2
|
+
import {
|
|
3
|
+
block, functionBody, functionExpression, paren, call, vararg, returnStatement,
|
|
4
|
+
} from "./nodeFactory"
|
|
5
|
+
|
|
6
|
+
export interface WrapInFunctionOptions {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 전체 프로그램을:
|
|
10
|
+
* return (function(...)
|
|
11
|
+
* <원래 코드>
|
|
12
|
+
* end)(...)
|
|
13
|
+
* 로 감쌈.
|
|
14
|
+
*
|
|
15
|
+
* - `return`을 쓰는 이유: Script/LocalScript에서는 top-level return이 그냥 청크를
|
|
16
|
+
* 조기 종료시킬 뿐 무해하고, ModuleScript라면 IIFE의 결과값이 그대로 require()
|
|
17
|
+
* 호출자에게 전달돼야 하므로 필요함. 즉 스크립트 종류를 가리지 않고 안전.
|
|
18
|
+
* - `...`을 파라미터로 받아서 다시 그대로 넘겨주는 이유: 원본 청크가 최상위에서
|
|
19
|
+
* `...`(스크립트 인자)을 참조하는 경우를 대비. 그냥 지워버리면 그런 코드가
|
|
20
|
+
* 깨짐.
|
|
21
|
+
*
|
|
22
|
+
* 반드시 파이프라인 맨 마지막 근처에서 실행돼야 함 — 이 패스 이후에 실행되는
|
|
23
|
+
* 다른 패스가 top-level 스코프를 순회/변형한다면 이미 감싸인 함수 내부까지
|
|
24
|
+
* 안 보고 지나칠 수 있음.
|
|
25
|
+
*/
|
|
26
|
+
export function runWrapInFunction(program: Program, _options: WrapInFunctionOptions): void {
|
|
27
|
+
const innerBody = program.body
|
|
28
|
+
const wrapper = functionExpression(functionBody([], innerBody, true))
|
|
29
|
+
const iife = call(paren(wrapper), [vararg()])
|
|
30
|
+
|
|
31
|
+
program.body = block([returnStatement([iife])])
|
|
32
|
+
}
|