depyo 1.0.2 → 1.1.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/depyo.js +43 -2
- package/lib/OpCodes.js +22 -5
- package/lib/PycDecompiler.js +1050 -40
- package/lib/PycDisassembler.js +1 -1
- package/lib/PycReader.js +22 -3
- package/lib/PythonObject.js +42 -6
- package/lib/ast/ast_node.js +381 -88
- package/lib/bytecode/python_3_0.js +1 -1
- package/lib/bytecode/python_3_12.js +1 -1
- package/lib/bytecode/python_3_13.js +13 -13
- package/lib/bytecode/python_3_14.js +13 -13
- package/lib/bytecode/python_3_15.js +183 -0
- package/lib/code_reader.js +107 -146
- package/lib/handlers/collections_update.js +50 -1
- package/lib/handlers/comparisons.js +3 -10
- package/lib/handlers/context_managers.js +202 -13
- package/lib/handlers/control_flow_jumps.js +516 -23
- package/lib/handlers/exceptions_blocks.js +92 -24
- package/lib/handlers/formatting.js +60 -17
- package/lib/handlers/function_calls.js +474 -58
- package/lib/handlers/function_class_build.js +170 -65
- package/lib/handlers/generators_async.js +67 -0
- package/lib/handlers/load_store_names.js +190 -57
- package/lib/handlers/loop_iterator.js +162 -6
- package/lib/handlers/misc_other.js +253 -44
- package/lib/handlers/stack_ops.js +81 -19
- package/lib/handlers/subscript_slice.js +103 -1
- package/lib/handlers/unpack.js +18 -16
- package/package.json +2 -2
package/lib/PycDecompiler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const AST = require('./ast/ast_node');
|
|
4
|
+
const { resolveBoolChainFrames } = require('./handlers/control_flow_jumps');
|
|
4
5
|
|
|
5
6
|
Array.prototype.top = function ArrayTop (pos = 0) {
|
|
6
7
|
return this[this.length - pos - 1];
|
|
@@ -13,6 +14,7 @@ Array.prototype.empty = function ArrayIsEmpty () {
|
|
|
13
14
|
class PycDecompiler {
|
|
14
15
|
static opCodeHandlers = {};
|
|
15
16
|
cleanBuild = false;
|
|
17
|
+
errors = []; // Per-opcode exceptions caught by main loop. Non-empty → cleanBuild=false.
|
|
16
18
|
object = null;
|
|
17
19
|
code = null;
|
|
18
20
|
|
|
@@ -97,21 +99,32 @@ class PycDecompiler {
|
|
|
97
99
|
typeof fileExports[handlerName] === 'function' &&
|
|
98
100
|
handlerName.startsWith("handle")) {
|
|
99
101
|
|
|
100
|
-
// Convert handler name (e.g., "handleJumpForwardA") to opcode name ("JUMP_FORWARD_A")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
// Convert handler name (e.g., "handleJumpForwardA") to opcode name ("JUMP_FORWARD_A").
|
|
103
|
+
// Primary rule: camelCase → SNAKE_CASE with explicit runs-of-caps handling
|
|
104
|
+
// (HandleABCDef → ABC_DEF). Some legacy handlers like handleCallIntrinsic1A
|
|
105
|
+
// still map via the looser regex — OpCodes.js provides both _1A and _1_A aliases.
|
|
106
|
+
const body = handlerName.replace(/^handle/, '');
|
|
107
|
+
let opCodeName = body
|
|
108
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
109
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
|
|
110
|
+
.toUpperCase();
|
|
111
|
+
if (!(opCodeName in OpCodesMap)) {
|
|
112
|
+
const legacy = body
|
|
113
|
+
.replaceAll(/([A-Z][a-z]+)/g, m => m.toUpperCase() + '_')
|
|
114
|
+
.replace(/_$/, '');
|
|
115
|
+
if (legacy in OpCodesMap) opCodeName = legacy;
|
|
116
|
+
}
|
|
104
117
|
|
|
105
118
|
if (opCodeName in OpCodesMap) {
|
|
106
119
|
const opCodeId = OpCodesMap[opCodeName];
|
|
107
120
|
const handlerFunc = fileExports[handlerName];
|
|
108
121
|
|
|
109
122
|
if (PycDecompiler.opCodeHandlers[opCodeId]) {
|
|
110
|
-
|
|
123
|
+
throw new Error(`Static handler collision: OpCode ${opCodeName} (${opCodeId}) already bound; refusing silent overwrite from ${entry.name}:${handlerName}.`);
|
|
111
124
|
}
|
|
112
125
|
PycDecompiler.opCodeHandlers[opCodeId] = handlerFunc;
|
|
113
126
|
} else {
|
|
114
|
-
|
|
127
|
+
throw new Error(`Static handler mapping failed: "${opCodeName}" (from ${entry.name}:${handlerName}) is not in OpCodes. Rename the handler to match a known opcode, or add the opcode to lib/OpCodes.js.`);
|
|
115
128
|
}
|
|
116
129
|
}
|
|
117
130
|
}
|
|
@@ -137,9 +150,15 @@ class PycDecompiler {
|
|
|
137
150
|
this.enrichGenericAnnotations(functonBody);
|
|
138
151
|
this.rewriteClassDefinitions(functonBody);
|
|
139
152
|
this.removeNullSentinelComparisons(functonBody);
|
|
153
|
+
this.cleanupExcMatchArtifacts(functonBody);
|
|
154
|
+
this.hoistNestedExceptBlocks(functonBody);
|
|
140
155
|
this.dedupeExceptHandlers(functonBody);
|
|
141
156
|
this.removeDuplicateReturns(functonBody);
|
|
142
157
|
|
|
158
|
+
if (this.object.Name == "<lambda>") {
|
|
159
|
+
this.foldLambdaConditional(functonBody);
|
|
160
|
+
}
|
|
161
|
+
|
|
143
162
|
if (this.object.Name != "<lambda>" && functonBody.last instanceof AST.ASTReturn && functonBody.last.value instanceof AST.ASTNone) {
|
|
144
163
|
functonBody.list.pop();
|
|
145
164
|
}
|
|
@@ -152,6 +171,81 @@ class PycDecompiler {
|
|
|
152
171
|
return functonBody;
|
|
153
172
|
}
|
|
154
173
|
|
|
174
|
+
foldLambdaConditional(body) {
|
|
175
|
+
// Lambda bodies must be single expressions. CPython often compiles a
|
|
176
|
+
// conditional expression `A if cond else B` as:
|
|
177
|
+
// POP_JUMP_IF_FALSE X; LOAD A; RETURN_VALUE; X: LOAD B; RETURN_VALUE
|
|
178
|
+
// The decompiler's default path leaves this as two statements which is
|
|
179
|
+
// invalid Python inside a lambda. Detect the patterns and fold into a
|
|
180
|
+
// single ASTReturn(ASTTernary).
|
|
181
|
+
const list = body?.list;
|
|
182
|
+
if (!Array.isArray(list) || list.length !== 2) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const first = list[0];
|
|
186
|
+
const second = list[1];
|
|
187
|
+
if (!(second instanceof AST.ASTReturn)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const elseVal = second.value;
|
|
191
|
+
|
|
192
|
+
const makeCondBlock = (condExpr, negative) => {
|
|
193
|
+
const cb = new AST.ASTCondBlock(AST.ASTBlock.BlockType.If, 0, 0, condExpr, !!negative);
|
|
194
|
+
return cb;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
let tern = null;
|
|
198
|
+
|
|
199
|
+
// Case 1: first is `ASTReturn(ASTBinary(cond AND/OR val))` — the line-900
|
|
200
|
+
// collapse already ran. Recover cond/val/direction from the binary op.
|
|
201
|
+
if (first instanceof AST.ASTReturn && first.value instanceof AST.ASTBinary) {
|
|
202
|
+
const bin = first.value;
|
|
203
|
+
const isAnd = bin.op === AST.ASTBinary.BinOp.LogicalAnd;
|
|
204
|
+
const isOr = bin.op === AST.ASTBinary.BinOp.LogicalOr;
|
|
205
|
+
if (isAnd || isOr) {
|
|
206
|
+
const cond = bin.left;
|
|
207
|
+
const thenVal = bin.right;
|
|
208
|
+
const cb = makeCondBlock(cond, isOr);
|
|
209
|
+
tern = new AST.ASTTernary(cb, thenVal, elseVal);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Case 2: first is an ASTCondBlock (if-return not collapsed by line 900).
|
|
214
|
+
if (!tern && first instanceof AST.ASTCondBlock &&
|
|
215
|
+
first.blockType === AST.ASTBlock.BlockType.If &&
|
|
216
|
+
Array.isArray(first.nodes) && first.nodes.length === 1 &&
|
|
217
|
+
first.nodes[0] instanceof AST.ASTReturn) {
|
|
218
|
+
const thenVal = first.nodes[0].value;
|
|
219
|
+
const cb = makeCondBlock(first.condition, first.negative);
|
|
220
|
+
tern = new AST.ASTTernary(cb, thenVal, elseVal);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!tern) {
|
|
224
|
+
// Two plain-return statements with no folded condition = optimizer
|
|
225
|
+
// eliminated the `if X else Y` when X was a constant, or the first
|
|
226
|
+
// return short-circuits the whole lambda (e.g. chained compare).
|
|
227
|
+
// The second return is unreachable; drop it so the lambda renders
|
|
228
|
+
// as a valid expression. Also covers generator lambdas where the
|
|
229
|
+
// yield expression is followed by the implicit `return None`.
|
|
230
|
+
const secondIsImplicitNone =
|
|
231
|
+
second instanceof AST.ASTReturn &&
|
|
232
|
+
second.rettype === AST.ASTReturn.RetType.Return &&
|
|
233
|
+
(!second.value || second.value instanceof AST.ASTNone);
|
|
234
|
+
if (first instanceof AST.ASTReturn &&
|
|
235
|
+
(first.rettype === AST.ASTReturn.RetType.Return || secondIsImplicitNone)) {
|
|
236
|
+
body.list.length = 1;
|
|
237
|
+
first.inLambda = true;
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const folded = new AST.ASTReturn(tern);
|
|
243
|
+
folded.inLambda = true;
|
|
244
|
+
folded.line = second.line ?? first.line;
|
|
245
|
+
body.list.length = 0;
|
|
246
|
+
body.list.push(folded);
|
|
247
|
+
}
|
|
248
|
+
|
|
155
249
|
append_to_chain_store(chainStore, item)
|
|
156
250
|
{
|
|
157
251
|
if (this.dataStack.top() == item) {
|
|
@@ -243,6 +337,23 @@ class PycDecompiler {
|
|
|
243
337
|
if (!(rit instanceof AST.ASTBlock) ||
|
|
244
338
|
rit.blockType != AST.ASTBlock.BlockType.If)
|
|
245
339
|
return;
|
|
340
|
+
// Peek before mutating: a real ternary needs both branches as expressions
|
|
341
|
+
// on the data stack. Otherwise the if/else are real statements (the bytecode
|
|
342
|
+
// path covers a control-flow if, not an if-expr) and rewriting them produces
|
|
343
|
+
// a ##ERROR## ternary that masks the real source.
|
|
344
|
+
const else_candidate = this.dataStack.top();
|
|
345
|
+
if (else_candidate == null) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const ifBlockCandidate = this.curBlock.nodes[this.curBlock.nodes.length - 2];
|
|
349
|
+
const stackDepth = this.dataStack.length;
|
|
350
|
+
const ifExprOnStack = stackDepth >= 2 ? this.dataStack[stackDepth - 2] : null;
|
|
351
|
+
const ifExprFromBody = ifExprOnStack == null && ifBlockCandidate.nodes.length == 1
|
|
352
|
+
? ifBlockCandidate.nodes[0]
|
|
353
|
+
: ifExprOnStack;
|
|
354
|
+
if (ifExprFromBody == null) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
246
357
|
let else_expr = this.dataStack.pop();
|
|
247
358
|
this.curBlock.removeLast();
|
|
248
359
|
let if_block = this.curBlock.nodes.top();
|
|
@@ -319,7 +430,19 @@ class PycDecompiler {
|
|
|
319
430
|
if (offset === undefined) {
|
|
320
431
|
return;
|
|
321
432
|
}
|
|
322
|
-
|
|
433
|
+
// Exclude cleanup-only handler entries (depth>0 whose start is a RERAISE site,
|
|
434
|
+
// not a PUSH_EXC_INFO) from the try-block span. Those entries protect CPython's
|
|
435
|
+
// implicit re-raise tail, not a user-written except, and must not be treated
|
|
436
|
+
// as handler bodies when sizing the enclosing try.
|
|
437
|
+
const isUserHandlerEntry = (e) => {
|
|
438
|
+
if (!e || e.depth <= 0) return false;
|
|
439
|
+
const startInstr = this.code.PeekInstructionAtOffset(e.start);
|
|
440
|
+
return startInstr && startInstr.OpCodeID === this.OpCodes.PUSH_EXC_INFO;
|
|
441
|
+
};
|
|
442
|
+
const maxHandlerEnd = this.maxExceptionHandlerEnd || Math.max(
|
|
443
|
+
...entries.filter(e => e.depth > 0 && isUserHandlerEntry(e)).map(e => e.end || 0),
|
|
444
|
+
0
|
|
445
|
+
);
|
|
323
446
|
|
|
324
447
|
// Build set of WITH_EXCEPT_START handler ranges (lazily, once per code object)
|
|
325
448
|
if (!this._withExceptRanges) {
|
|
@@ -470,6 +593,16 @@ class PycDecompiler {
|
|
|
470
593
|
}
|
|
471
594
|
}
|
|
472
595
|
}
|
|
596
|
+
// Skip depth>0 entries whose start is a RERAISE cleanup site rather than
|
|
597
|
+
// a user-level PUSH_EXC_INFO handler. These appear in CPython 3.11+ as
|
|
598
|
+
// protected tails around `RERAISE 0` or `COPY n; POP_EXCEPT; RERAISE 1`
|
|
599
|
+
// and have no source-level except counterpart.
|
|
600
|
+
if (entry.depth > 0 && !isUserHandlerEntry(entry)) {
|
|
601
|
+
if (g_cliArgs?.debug) {
|
|
602
|
+
console.log(`[EnsureExcBlocks] Skipping cleanup-only handler entry at ${entry.start} (not PUSH_EXC_INFO)`);
|
|
603
|
+
}
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
473
606
|
const handlerEnd = entry.depth === 0
|
|
474
607
|
? (maxHandlerEnd || entry.end || this.code.LastOffset || this.object.CodeSize || 0)
|
|
475
608
|
: (entry.end || maxHandlerEnd || this.code.LastOffset || this.object.CodeSize || 0);
|
|
@@ -520,7 +653,70 @@ class PycDecompiler {
|
|
|
520
653
|
if (!matches.length) {
|
|
521
654
|
return this.maxExceptionHandlerEnd || null;
|
|
522
655
|
}
|
|
523
|
-
|
|
656
|
+
let end = Math.max(...matches.map(e => e.end));
|
|
657
|
+
// CPython 3.11+ splits a single source-level except handler into
|
|
658
|
+
// contiguous depth>0 sub-regions (alias-binding, body, cleanup).
|
|
659
|
+
// Walk forward through adjacent depth>0 entries so the Except block
|
|
660
|
+
// covers the full handler body, not just the alias-binding region.
|
|
661
|
+
let guard = 0;
|
|
662
|
+
while (guard++ < 32) {
|
|
663
|
+
const next = entries.find(e =>
|
|
664
|
+
e.depth > 0 &&
|
|
665
|
+
e.start >= end &&
|
|
666
|
+
e.start <= end + 2 &&
|
|
667
|
+
e.end > end);
|
|
668
|
+
if (!next) break;
|
|
669
|
+
end = next.end;
|
|
670
|
+
}
|
|
671
|
+
// The exception-table entry only covers the prologue (the protected
|
|
672
|
+
// region around CHECK_EXC_MATCH). The actual handler body — user
|
|
673
|
+
// statements, POP_EXCEPT, and the terminating exit — lives past the
|
|
674
|
+
// entry end. Walk forward to include POP_EXCEPT and the following
|
|
675
|
+
// terminator (RETURN/JUMP_FORWARD/RERAISE) so the Except block
|
|
676
|
+
// captures the body that renders as user code.
|
|
677
|
+
if (this.code && this.OpCodes) {
|
|
678
|
+
let cursor = end;
|
|
679
|
+
let steps = 0;
|
|
680
|
+
let sawHandlerExit = false;
|
|
681
|
+
while (cursor >= 0 && steps++ < 500) {
|
|
682
|
+
const instr = this.code.PeekInstructionAtOffset(cursor);
|
|
683
|
+
if (!instr) break;
|
|
684
|
+
const op = instr.OpCodeID;
|
|
685
|
+
// Stop if we enter another handler's prologue.
|
|
686
|
+
if (op === this.OpCodes.PUSH_EXC_INFO) break;
|
|
687
|
+
if (op === this.OpCodes.POP_EXCEPT) {
|
|
688
|
+
sawHandlerExit = true;
|
|
689
|
+
end = instr.Offset + (instr.Size || 2);
|
|
690
|
+
cursor = end;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (sawHandlerExit && (
|
|
694
|
+
op === this.OpCodes.RETURN_VALUE ||
|
|
695
|
+
op === this.OpCodes.RETURN_VALUE_A ||
|
|
696
|
+
op === this.OpCodes.RETURN_CONST_A ||
|
|
697
|
+
op === this.OpCodes.JUMP_FORWARD ||
|
|
698
|
+
op === this.OpCodes.JUMP_FORWARD_A ||
|
|
699
|
+
op === this.OpCodes.JUMP_BACKWARD ||
|
|
700
|
+
op === this.OpCodes.JUMP_BACKWARD_A ||
|
|
701
|
+
op === this.OpCodes.RERAISE ||
|
|
702
|
+
op === this.OpCodes.RERAISE_A)) {
|
|
703
|
+
end = instr.Offset + (instr.Size || 2);
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
if (sawHandlerExit) {
|
|
707
|
+
// Skip trailing CACHE/POP_TOP cleanup that sometimes sits
|
|
708
|
+
// between POP_EXCEPT and the terminator on 3.11+.
|
|
709
|
+
if (op === this.OpCodes.CACHE ||
|
|
710
|
+
op === this.OpCodes.POP_TOP) {
|
|
711
|
+
cursor = instr.Offset + (instr.Size || 2);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
cursor = instr.Offset + (instr.Size || 2);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return end;
|
|
524
720
|
}
|
|
525
721
|
|
|
526
722
|
statements () {
|
|
@@ -538,6 +734,83 @@ class PycDecompiler {
|
|
|
538
734
|
// Open blocks based on 3.11+ exception table (no SETUP_EXCEPT opcodes)
|
|
539
735
|
this.ensureExceptionTableBlocks();
|
|
540
736
|
|
|
737
|
+
// Early-return/raise in try body: CPython omits POP_BLOCK+JUMP_FORWARD,
|
|
738
|
+
// so the Try block is still open when we reach the except/finally handler.
|
|
739
|
+
// Close it and open Except/Finally here so handler prologue is consumed
|
|
740
|
+
// in the right block and __exception__ doesn't leak into Try.
|
|
741
|
+
if (this.code.Current.Offset > 0 && this.blocks.length > 1) {
|
|
742
|
+
let tryIdx = -1, containerIdx = -1, isFinally = false;
|
|
743
|
+
for (let i = this.blocks.length - 1; i >= 0; i--) {
|
|
744
|
+
const blk = this.blocks[i];
|
|
745
|
+
if (blk.blockType === AST.ASTBlock.BlockType.Container) {
|
|
746
|
+
if (blk.hasExcept && blk.except === this.code.Current.Offset) {
|
|
747
|
+
containerIdx = i;
|
|
748
|
+
isFinally = false;
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
if (blk.hasFinally && !blk.hasExcept
|
|
752
|
+
&& blk.finally === this.code.Current.Offset) {
|
|
753
|
+
containerIdx = i;
|
|
754
|
+
isFinally = true;
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (blk.blockType === AST.ASTBlock.BlockType.Try && tryIdx === -1) {
|
|
759
|
+
tryIdx = i;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (containerIdx >= 0 && tryIdx > containerIdx
|
|
763
|
+
&& this.curBlock.blockType !== AST.ASTBlock.BlockType.Except
|
|
764
|
+
&& this.curBlock.blockType !== AST.ASTBlock.BlockType.Finally) {
|
|
765
|
+
while (this.blocks.length - 1 > containerIdx) {
|
|
766
|
+
const top = this.blocks[this.blocks.length - 1];
|
|
767
|
+
this.blocks.pop();
|
|
768
|
+
this.curBlock = this.blocks.top();
|
|
769
|
+
this.curBlock.append(top);
|
|
770
|
+
}
|
|
771
|
+
const cont = this.curBlock;
|
|
772
|
+
const handlerStart = isFinally ? cont.finally : cont.except;
|
|
773
|
+
let handlerEnd = handlerStart;
|
|
774
|
+
let cursor = handlerStart;
|
|
775
|
+
for (let k = 0; k < 500; k++) {
|
|
776
|
+
const instr = this.code.PeekInstructionAtOffset(cursor);
|
|
777
|
+
if (!instr) break;
|
|
778
|
+
const op = instr.OpCodeID;
|
|
779
|
+
if (op === this.OpCodes.END_FINALLY
|
|
780
|
+
|| op === this.OpCodes.POP_EXCEPT
|
|
781
|
+
|| op === this.OpCodes.RERAISE_A
|
|
782
|
+
|| op === this.OpCodes.RERAISE) {
|
|
783
|
+
handlerEnd = instr.Offset;
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
if (op === this.OpCodes.RETURN_VALUE
|
|
787
|
+
|| op === this.OpCodes.RETURN_VALUE_A
|
|
788
|
+
|| op === this.OpCodes.RETURN_CONST_A
|
|
789
|
+
|| op === this.OpCodes.RAISE_VARARGS_A) {
|
|
790
|
+
handlerEnd = instr.Offset + (instr.Size || 2);
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
cursor = instr.Offset + (instr.Size || 2);
|
|
794
|
+
}
|
|
795
|
+
if (isFinally) {
|
|
796
|
+
let finallyBlk = new AST.ASTBlock(AST.ASTBlock.BlockType.Finally, this.code.Current.Offset, handlerEnd, true);
|
|
797
|
+
this.blocks.push(finallyBlk);
|
|
798
|
+
this.curBlock = this.blocks.top();
|
|
799
|
+
if (global.g_cliArgs?.debug) {
|
|
800
|
+
console.log(`[EarlyReturnFinally] Opened Finally at ${this.code.Current.Offset}, handlerEnd=${handlerEnd}`);
|
|
801
|
+
}
|
|
802
|
+
} else {
|
|
803
|
+
let except = new AST.ASTCondBlock(AST.ASTBlock.BlockType.Except, this.code.Current.Offset, handlerEnd, null, false);
|
|
804
|
+
except.init();
|
|
805
|
+
this.blocks.push(except);
|
|
806
|
+
this.curBlock = this.blocks.top();
|
|
807
|
+
if (global.g_cliArgs?.debug) {
|
|
808
|
+
console.log(`[EarlyReturnExcept] Opened Except at ${this.code.Current.Offset}, handlerEnd=${handlerEnd}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
541
814
|
// Python 3.8: When entering exception handler, push exception instance to stack
|
|
542
815
|
if (this.exceptionHandlerOffsets.has(this.code.Current.Offset)) {
|
|
543
816
|
// Create synthetic exception instance placeholder
|
|
@@ -708,10 +981,33 @@ class PycDecompiler {
|
|
|
708
981
|
}
|
|
709
982
|
}
|
|
710
983
|
|
|
984
|
+
// Resolve any open boolean-expression chain frames whose target
|
|
985
|
+
// offset matches the current instruction. This folds pending
|
|
986
|
+
// JUMP_IF_FALSE/TRUE lhs+rhs pairs into ASTBinary AND/OR nodes
|
|
987
|
+
// and pushes the result onto dataStack before the consumer
|
|
988
|
+
// (STORE_*, RETURN_*, CALL_*, or a nested JUMP_IF_*) fires.
|
|
989
|
+
if (this.boolChainStack && this.boolChainStack.length > 0) {
|
|
990
|
+
resolveBoolChainFrames(this, this.code.Current.Offset);
|
|
991
|
+
}
|
|
992
|
+
|
|
711
993
|
if (this.code.Current.OpCodeID in PycDecompiler.opCodeHandlers)
|
|
712
994
|
{
|
|
713
995
|
PycDecompiler.opCodeHandlers[this.code.Current.OpCodeID].call(this);
|
|
714
996
|
} else {
|
|
997
|
+
// Unsupported opcode: bail out of the loop with what we have.
|
|
998
|
+
// Record so `--strict` and the dirty-files report can surface it.
|
|
999
|
+
this.errors.push({
|
|
1000
|
+
opcode: this.code.Current.InstructionName,
|
|
1001
|
+
argument: this.code.Current.Argument,
|
|
1002
|
+
offset: this.code.Current.Offset,
|
|
1003
|
+
fileOffset: this.object.codeOffset + this.code.Current.Offset,
|
|
1004
|
+
codeObject: this.object.Name,
|
|
1005
|
+
message: `Unsupported opcode ${this.code.Current.InstructionName}`,
|
|
1006
|
+
unsupported: true
|
|
1007
|
+
});
|
|
1008
|
+
if (global.g_cliArgs?.strict) {
|
|
1009
|
+
throw new Error(`Unsupported opcode ${this.code.Current.InstructionName} at offset ${this.code.Current.Offset}`);
|
|
1010
|
+
}
|
|
715
1011
|
if (!g_cliArgs?.silent) {
|
|
716
1012
|
console.error(`Unsupported opcode ${this.code.Current.InstructionName} at pos ${this.code.Current.Offset}\n`);
|
|
717
1013
|
}
|
|
@@ -727,6 +1023,21 @@ class PycDecompiler {
|
|
|
727
1023
|
&& (this.curBlock.end == this.code.Next?.Offset);
|
|
728
1024
|
|
|
729
1025
|
} catch (ex) {
|
|
1026
|
+
// Record the failure so callers can detect partial output. Without this
|
|
1027
|
+
// accumulator the main loop swallows handler exceptions silently and
|
|
1028
|
+
// cleanBuild gets reset to true at end-of-loop, masking corruption.
|
|
1029
|
+
this.errors.push({
|
|
1030
|
+
opcode: this.code.Current.InstructionName,
|
|
1031
|
+
argument: this.code.Current.Argument,
|
|
1032
|
+
offset: this.code.Current.Offset,
|
|
1033
|
+
fileOffset: this.object.codeOffset + this.code.Current.Offset,
|
|
1034
|
+
codeObject: this.object.Name,
|
|
1035
|
+
message: ex.message,
|
|
1036
|
+
stack: ex.stack
|
|
1037
|
+
});
|
|
1038
|
+
if (global.g_cliArgs?.strict) {
|
|
1039
|
+
throw ex;
|
|
1040
|
+
}
|
|
730
1041
|
if (!g_cliArgs?.silent) {
|
|
731
1042
|
console.error(`EXCEPTION for OpCode ${this.code.Current.InstructionName} (${this.code.Current.Argument}) at offset ${this.code.Current.Offset} in code object '${this.object.Name}', file offset ${this.object.codeOffset + this.code.Current.Offset} : ${ex.message}\n\n`);
|
|
732
1043
|
if (global.g_cliArgs?.debug) {
|
|
@@ -764,7 +1075,7 @@ class PycDecompiler {
|
|
|
764
1075
|
}
|
|
765
1076
|
}
|
|
766
1077
|
|
|
767
|
-
this.cleanBuild =
|
|
1078
|
+
this.cleanBuild = this.errors.length === 0;
|
|
768
1079
|
let mainNode = new AST.ASTNodeList(this.defBlock.nodes);
|
|
769
1080
|
return mainNode;
|
|
770
1081
|
}
|
|
@@ -1123,10 +1434,19 @@ class PycDecompiler {
|
|
|
1123
1434
|
return true;
|
|
1124
1435
|
});
|
|
1125
1436
|
if (aliasName) {
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1437
|
+
const existing = blk.condition;
|
|
1438
|
+
if (existing instanceof AST.ASTStore) {
|
|
1439
|
+
// Condition already encodes `Type as alias` (set by misc_other.js
|
|
1440
|
+
// POP_TOP→STORE_FAST→POP_TOP path). Don't double-wrap.
|
|
1441
|
+
if (matchType) {
|
|
1442
|
+
blk.isExceptStar = true;
|
|
1443
|
+
}
|
|
1444
|
+
} else {
|
|
1445
|
+
const cond = matchType || existing || new AST.ASTName("Exception");
|
|
1446
|
+
blk.condition = new AST.ASTStore(cond, aliasName);
|
|
1447
|
+
if (matchType) {
|
|
1448
|
+
blk.isExceptStar = true;
|
|
1449
|
+
}
|
|
1130
1450
|
}
|
|
1131
1451
|
}
|
|
1132
1452
|
if (blk.m_nodes.length > 3) {
|
|
@@ -1256,14 +1576,15 @@ class PycDecompiler {
|
|
|
1256
1576
|
const alias = node.condition instanceof AST.ASTStore ? node.condition.dest : null;
|
|
1257
1577
|
const filtered = (node.nodes || []).filter(n => !isCleanupNode(n, alias));
|
|
1258
1578
|
|
|
1259
|
-
//
|
|
1260
|
-
//
|
|
1261
|
-
|
|
1579
|
+
// Only synthetic except* handlers without a condition AND no real body
|
|
1580
|
+
// should be dropped; user-written `except:` and `except E:` blocks must
|
|
1581
|
+
// be preserved (with `pass` if the body was nothing but cleanup).
|
|
1582
|
+
if (filtered.length === 0) {
|
|
1262
1583
|
node.m_nodes = [new AST.ASTKeyword(AST.ASTKeyword.Word.Pass)];
|
|
1263
1584
|
} else if (filtered.length !== (node.nodes || []).length) {
|
|
1264
1585
|
node.m_nodes = filtered;
|
|
1265
1586
|
}
|
|
1266
|
-
if (Array.isArray(parentArr) &&
|
|
1587
|
+
if (Array.isArray(parentArr) && node.isExceptStar && !node.condition && filtered.length === 0) {
|
|
1267
1588
|
parentArr.splice(idx, 1);
|
|
1268
1589
|
continue;
|
|
1269
1590
|
}
|
|
@@ -1361,28 +1682,104 @@ class PycDecompiler {
|
|
|
1361
1682
|
}
|
|
1362
1683
|
|
|
1363
1684
|
rewriteClassDefinitions(root) {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1685
|
+
const hasDataclass = (root instanceof AST.ASTNodeList) && this.astHasDataclassImport(root);
|
|
1686
|
+
this._rewriteClassDefsInNodes(this._collectStatementNodes(root), hasDataclass, new Set());
|
|
1687
|
+
}
|
|
1367
1688
|
|
|
1368
|
-
|
|
1689
|
+
_collectStatementNodes(container) {
|
|
1690
|
+
if (!container) return [];
|
|
1691
|
+
if (container instanceof AST.ASTNodeList) return container.list;
|
|
1692
|
+
if (container instanceof AST.ASTBlock) return container.nodes;
|
|
1693
|
+
return [];
|
|
1694
|
+
}
|
|
1369
1695
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1696
|
+
_rewriteClassDefsInNodes(nodes, hasDataclass, visited) {
|
|
1697
|
+
if (!Array.isArray(nodes) || visited.has(nodes)) return;
|
|
1698
|
+
visited.add(nodes);
|
|
1699
|
+
|
|
1700
|
+
for (const node of nodes) {
|
|
1701
|
+
if (node instanceof AST.ASTStore) {
|
|
1702
|
+
this._maybeRewriteClassStore(node, hasDataclass);
|
|
1703
|
+
}
|
|
1704
|
+
for (const child of this._childContainers(node)) {
|
|
1705
|
+
this._rewriteClassDefsInNodes(child, hasDataclass, visited);
|
|
1373
1706
|
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1374
1709
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1710
|
+
_maybeRewriteClassStore(node, hasDataclass) {
|
|
1711
|
+
if (node.src instanceof AST.ASTCall && node.src.func instanceof AST.ASTClass &&
|
|
1712
|
+
this.isPlainClassCall(node.src)) {
|
|
1713
|
+
node.src = node.src.func;
|
|
1714
|
+
this.cleanupClassBody(node.src);
|
|
1715
|
+
if (hasDataclass) {
|
|
1716
|
+
node.addDecorator(new AST.ASTName('dataclass'));
|
|
1717
|
+
}
|
|
1718
|
+
} else if (node.src instanceof AST.ASTCall && node.src.func instanceof AST.ASTClass &&
|
|
1719
|
+
this.isClassCallWithOnlyKwargs(node.src)) {
|
|
1720
|
+
const callNode = node.src;
|
|
1721
|
+
const classNode = callNode.func;
|
|
1722
|
+
classNode.kwargs = callNode.kwparams || [];
|
|
1723
|
+
node.src = classNode;
|
|
1724
|
+
this.cleanupClassBody(classNode);
|
|
1725
|
+
if (hasDataclass) {
|
|
1726
|
+
node.addDecorator(new AST.ASTName('dataclass'));
|
|
1727
|
+
}
|
|
1728
|
+
} else if (node.src instanceof AST.ASTCall && this.isDecoratedClassCall(node.src)) {
|
|
1729
|
+
// Pattern: `Name = decorator(ClassLiteral[, ...])` produced by Py3.12+
|
|
1730
|
+
// dataclass/decorator-of-class flows. Promote to a real class with
|
|
1731
|
+
// the call-of-decorator attached as an @decorator.
|
|
1732
|
+
const call = node.src;
|
|
1733
|
+
const classNode = call.pparams[0];
|
|
1734
|
+
const decoratorExpr = this._buildDecoratorExpr(call);
|
|
1735
|
+
node.src = classNode;
|
|
1736
|
+
this.cleanupClassBody(classNode);
|
|
1737
|
+
if (decoratorExpr) {
|
|
1738
|
+
node.addDecorator(decoratorExpr);
|
|
1739
|
+
}
|
|
1740
|
+
} else if (node.src instanceof AST.ASTClass) {
|
|
1741
|
+
this.cleanupClassBody(node.src);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
isDecoratedClassCall(call) {
|
|
1746
|
+
if (!(call instanceof AST.ASTCall)) return false;
|
|
1747
|
+
if (call.func instanceof AST.ASTClass) return false;
|
|
1748
|
+
if (!call.pparams || call.pparams.length < 1) return false;
|
|
1749
|
+
if (!(call.pparams[0] instanceof AST.ASTClass)) return false;
|
|
1750
|
+
// Any further pparams would become positional args to the decorator,
|
|
1751
|
+
// which cannot be expressed as `@decorator(class)` sugar. Reject.
|
|
1752
|
+
return call.pparams.length === 1;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
_buildDecoratorExpr(call) {
|
|
1756
|
+
// `decorator(class)` → `@decorator`
|
|
1757
|
+
// `decorator(class, kw=val, ...)` → `@decorator(kw=val, ...)`
|
|
1758
|
+
const kwargs = call.kwparams || [];
|
|
1759
|
+
if (kwargs.length === 0 && !call.hasVar && !call.hasKw) {
|
|
1760
|
+
return call.func;
|
|
1761
|
+
}
|
|
1762
|
+
const wrapper = new AST.ASTCall(call.func, [], kwargs);
|
|
1763
|
+
wrapper.line = call.line;
|
|
1764
|
+
return wrapper;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
_childContainers(node) {
|
|
1768
|
+
const containers = [];
|
|
1769
|
+
if (node instanceof AST.ASTBlock) {
|
|
1770
|
+
containers.push(node.nodes);
|
|
1771
|
+
}
|
|
1772
|
+
if (node?.body && (node.body instanceof AST.ASTBlock || node.body instanceof AST.ASTNodeList)) {
|
|
1773
|
+
containers.push(this._collectStatementNodes(node.body));
|
|
1774
|
+
}
|
|
1775
|
+
// Generic walk for nested blocks/lists in object properties (e.g. ifBlock.elseBlock).
|
|
1776
|
+
for (const key of ['elseBlock', 'finallyBlock', 'tryBlock', 'thenBlock']) {
|
|
1777
|
+
const val = node?.[key];
|
|
1778
|
+
if (val instanceof AST.ASTBlock || val instanceof AST.ASTNodeList) {
|
|
1779
|
+
containers.push(this._collectStatementNodes(val));
|
|
1384
1780
|
}
|
|
1385
1781
|
}
|
|
1782
|
+
return containers;
|
|
1386
1783
|
}
|
|
1387
1784
|
|
|
1388
1785
|
isPlainClassCall(call) {
|
|
@@ -1395,6 +1792,15 @@ class PycDecompiler {
|
|
|
1395
1792
|
return !hasParams;
|
|
1396
1793
|
}
|
|
1397
1794
|
|
|
1795
|
+
isClassCallWithOnlyKwargs(call) {
|
|
1796
|
+
if (!(call.func instanceof AST.ASTClass)) {
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
const hasPparams = call.pparams && call.pparams.length > 0;
|
|
1800
|
+
const hasKwparams = call.kwparams && call.kwparams.length > 0;
|
|
1801
|
+
return !hasPparams && hasKwparams && !call.hasVar && !call.hasKw;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1398
1804
|
astHasDataclassImport(root) {
|
|
1399
1805
|
if (!(root instanceof AST.ASTNodeList)) {
|
|
1400
1806
|
return false;
|
|
@@ -1501,6 +1907,539 @@ class PycDecompiler {
|
|
|
1501
1907
|
}
|
|
1502
1908
|
}
|
|
1503
1909
|
|
|
1910
|
+
// Rewrites 3.11+ CHECK_EXC_MATCH artifacts that leaked past block
|
|
1911
|
+
// reconstruction: unwraps `except <exc> EXC_MATCH <type>` conditions
|
|
1912
|
+
// down to `<type>`, rewrites sibling `while __exception__ EXC_MATCH T:`
|
|
1913
|
+
// blocks into `except T:` handlers on the preceding Try, and strips
|
|
1914
|
+
// trailing compiler-generated reraise/cleanup nodes (bare `raise`,
|
|
1915
|
+
// `e = None; del e`, empty `try: pass`). Runs once on the whole tree.
|
|
1916
|
+
cleanupExcMatchArtifacts(root) {
|
|
1917
|
+
// CHECK_EXC_MATCH only exists in Py3.11+; earlier versions never produce
|
|
1918
|
+
// the EXC_MATCH compare op or the __exception__ sentinel, so skip.
|
|
1919
|
+
if (this.object.Reader.versionCompare(3, 11) < 0) return;
|
|
1920
|
+
|
|
1921
|
+
const EXC_MATCH = AST.ASTCompare.CompareOp.Exception;
|
|
1922
|
+
|
|
1923
|
+
const unwrapExcMatch = (cond) => {
|
|
1924
|
+
if (!cond) return cond;
|
|
1925
|
+
// The condition may have been wrapped in ASTStore (alias binding) -
|
|
1926
|
+
// unwrap first, then strip the EXC_MATCH compare.
|
|
1927
|
+
let inner = cond;
|
|
1928
|
+
if (inner instanceof AST.ASTStore && inner.src) {
|
|
1929
|
+
inner = inner.src;
|
|
1930
|
+
}
|
|
1931
|
+
if (inner instanceof AST.ASTCompare && inner.op === EXC_MATCH) {
|
|
1932
|
+
return inner.right;
|
|
1933
|
+
}
|
|
1934
|
+
return cond;
|
|
1935
|
+
};
|
|
1936
|
+
|
|
1937
|
+
const isExcMatchCompare = (node) =>
|
|
1938
|
+
node instanceof AST.ASTCompare && node.op === EXC_MATCH;
|
|
1939
|
+
|
|
1940
|
+
const isExceptionName = (node) =>
|
|
1941
|
+
node instanceof AST.ASTName && node.name === '__exception__';
|
|
1942
|
+
|
|
1943
|
+
const containsExceptionRef = (node, depth = 0) => {
|
|
1944
|
+
if (!node || depth > 4) return false;
|
|
1945
|
+
if (isExceptionName(node)) return true;
|
|
1946
|
+
if (node instanceof AST.ASTCompare || node instanceof AST.ASTBinary) {
|
|
1947
|
+
return containsExceptionRef(node.left, depth + 1) ||
|
|
1948
|
+
containsExceptionRef(node.right, depth + 1);
|
|
1949
|
+
}
|
|
1950
|
+
if (node instanceof AST.ASTCall) {
|
|
1951
|
+
return containsExceptionRef(node.func, depth + 1) ||
|
|
1952
|
+
(node.pparams || []).some(p => containsExceptionRef(p, depth + 1));
|
|
1953
|
+
}
|
|
1954
|
+
if (node instanceof AST.ASTStore) {
|
|
1955
|
+
return containsExceptionRef(node.src, depth + 1);
|
|
1956
|
+
}
|
|
1957
|
+
return false;
|
|
1958
|
+
};
|
|
1959
|
+
|
|
1960
|
+
const isEmptyTryBlock = (node) =>
|
|
1961
|
+
node instanceof AST.ASTBlock &&
|
|
1962
|
+
node.blockType === AST.ASTBlock.BlockType.Try &&
|
|
1963
|
+
(node.nodes.length === 0 ||
|
|
1964
|
+
(node.nodes.length === 1 &&
|
|
1965
|
+
node.nodes[0] instanceof AST.ASTKeyword &&
|
|
1966
|
+
node.nodes[0].key === AST.ASTKeyword.Word.Pass));
|
|
1967
|
+
|
|
1968
|
+
const isOrphanExceptionStmt = (node) => {
|
|
1969
|
+
// Bare `raise` with no params → fall-through RERAISE artifact
|
|
1970
|
+
if (node instanceof AST.ASTRaise && (!node.params || node.params.length === 0)) {
|
|
1971
|
+
return true;
|
|
1972
|
+
}
|
|
1973
|
+
// `__exception__(None, None, None)` or `__exception__` alone
|
|
1974
|
+
if (node instanceof AST.ASTCall && isExceptionName(node.func)) {
|
|
1975
|
+
return true;
|
|
1976
|
+
}
|
|
1977
|
+
if (isExceptionName(node)) {
|
|
1978
|
+
return true;
|
|
1979
|
+
}
|
|
1980
|
+
// `x = __exception__` - CPython alias-clear artifact
|
|
1981
|
+
if (node instanceof AST.ASTStore && isExceptionName(node.src)) {
|
|
1982
|
+
return true;
|
|
1983
|
+
}
|
|
1984
|
+
// `return __exception__` - misrendered reraise epilogue
|
|
1985
|
+
if (node instanceof AST.ASTReturn && isExceptionName(node.value)) {
|
|
1986
|
+
return true;
|
|
1987
|
+
}
|
|
1988
|
+
return false;
|
|
1989
|
+
};
|
|
1990
|
+
|
|
1991
|
+
const rewriteContainer = (nodes, parentBlockType) => {
|
|
1992
|
+
if (!Array.isArray(nodes)) return;
|
|
1993
|
+
|
|
1994
|
+
// Pass 1: unwrap EXC_MATCH in Except block conditions.
|
|
1995
|
+
for (const node of nodes) {
|
|
1996
|
+
if (node instanceof AST.ASTCondBlock &&
|
|
1997
|
+
node.blockType === AST.ASTBlock.BlockType.Except) {
|
|
1998
|
+
node.condition = unwrapExcMatch(node.condition);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// Pass 2: convert ASTWhile/ASTIf with EXC_MATCH condition to Except
|
|
2003
|
+
// handlers and attach them to the preceding Try block. If no preceding
|
|
2004
|
+
// Try exists (deeply misparsed region), drop them to avoid invalid output.
|
|
2005
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2006
|
+
const node = nodes[i];
|
|
2007
|
+
if (!(node instanceof AST.ASTCondBlock) ||
|
|
2008
|
+
!(node.blockType === AST.ASTBlock.BlockType.While ||
|
|
2009
|
+
node.blockType === AST.ASTBlock.BlockType.If)) {
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
if (!isExcMatchCompare(node.condition) ||
|
|
2013
|
+
!containsExceptionRef(node.condition.left)) {
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
const matchType = node.condition.right;
|
|
2017
|
+
// Find nearest preceding Try block in this container.
|
|
2018
|
+
let tryIdx = -1;
|
|
2019
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
2020
|
+
const cand = nodes[j];
|
|
2021
|
+
if (cand instanceof AST.ASTBlock &&
|
|
2022
|
+
cand.blockType === AST.ASTBlock.BlockType.Try) {
|
|
2023
|
+
tryIdx = j;
|
|
2024
|
+
break;
|
|
2025
|
+
}
|
|
2026
|
+
// Allow existing Except handlers between; skip past them.
|
|
2027
|
+
if (cand instanceof AST.ASTCondBlock &&
|
|
2028
|
+
cand.blockType === AST.ASTBlock.BlockType.Except) {
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
// Strip any trailing orphan reraise/alias-clear statements from body.
|
|
2034
|
+
const cleanBody = node.nodes.filter(n => !isOrphanExceptionStmt(n));
|
|
2035
|
+
if (cleanBody.length === 0) {
|
|
2036
|
+
cleanBody.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
if (tryIdx >= 0) {
|
|
2040
|
+
const exceptBlk = new AST.ASTCondBlock(
|
|
2041
|
+
AST.ASTBlock.BlockType.Except,
|
|
2042
|
+
node.start, node.end, matchType, false);
|
|
2043
|
+
exceptBlk.line = node.line;
|
|
2044
|
+
exceptBlk.m_nodes = cleanBody;
|
|
2045
|
+
// Insert right after last existing Except handler for that Try.
|
|
2046
|
+
let insertAt = tryIdx + 1;
|
|
2047
|
+
while (insertAt < nodes.length &&
|
|
2048
|
+
nodes[insertAt] instanceof AST.ASTCondBlock &&
|
|
2049
|
+
nodes[insertAt].blockType === AST.ASTBlock.BlockType.Except) {
|
|
2050
|
+
insertAt++;
|
|
2051
|
+
}
|
|
2052
|
+
nodes.splice(i, 1);
|
|
2053
|
+
if (insertAt > i) insertAt--;
|
|
2054
|
+
nodes.splice(insertAt, 0, exceptBlk);
|
|
2055
|
+
} else {
|
|
2056
|
+
// No Try to attach to — dropping keeps output parseable.
|
|
2057
|
+
nodes.splice(i, 1);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// Pass 3: strip orphan exception-related statements at container
|
|
2062
|
+
// level — but NOT inside Except/Finally bodies, where bare `raise`
|
|
2063
|
+
// is a legitimate re-raise statement.
|
|
2064
|
+
const inHandlerBody = parentBlockType === AST.ASTBlock.BlockType.Except ||
|
|
2065
|
+
parentBlockType === AST.ASTBlock.BlockType.Finally;
|
|
2066
|
+
if (!inHandlerBody) {
|
|
2067
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2068
|
+
if (isOrphanExceptionStmt(nodes[i])) {
|
|
2069
|
+
nodes.splice(i, 1);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// Pass 3.5: strip `X = None; del X` pairs — CPython 3.11+ emits this
|
|
2075
|
+
// at the tail/fall-through of every `except Foo as X:` body to clear
|
|
2076
|
+
// the binding. The pattern is synthetic and user code effectively
|
|
2077
|
+
// never writes it literally, so removing it is safe and prevents
|
|
2078
|
+
// the artifact from breaking up adjacent except clauses.
|
|
2079
|
+
for (let i = nodes.length - 2; i >= 0; i--) {
|
|
2080
|
+
const a = nodes[i], b = nodes[i + 1];
|
|
2081
|
+
if (a instanceof AST.ASTStore &&
|
|
2082
|
+
a.src instanceof AST.ASTNone &&
|
|
2083
|
+
a.dest instanceof AST.ASTName &&
|
|
2084
|
+
b instanceof AST.ASTDelete &&
|
|
2085
|
+
b.value instanceof AST.ASTName &&
|
|
2086
|
+
a.dest.name === b.value.name) {
|
|
2087
|
+
nodes.splice(i, 2);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// Pass 4: drop empty `try:` blocks. With no following handler the
|
|
2092
|
+
// try is a bare leftover. With a trivial `except: pass` handler the
|
|
2093
|
+
// whole pair is a reconstruction artifact — Python forbids
|
|
2094
|
+
// `try:<empty>`, so either outcome requires the try to go. When the
|
|
2095
|
+
// handler has real body drop only the try and splice a `pass`
|
|
2096
|
+
// placeholder into the handler's matching — but the current
|
|
2097
|
+
// sightings all have pass-only handlers, so mirror that case.
|
|
2098
|
+
const isTrivialHandlerBlock = (n) =>
|
|
2099
|
+
n instanceof AST.ASTCondBlock &&
|
|
2100
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2101
|
+
n.blockType === AST.ASTBlock.BlockType.Finally) &&
|
|
2102
|
+
(n.nodes.length === 0 ||
|
|
2103
|
+
(n.nodes.length === 1 &&
|
|
2104
|
+
n.nodes[0] instanceof AST.ASTKeyword &&
|
|
2105
|
+
n.nodes[0].key === AST.ASTKeyword.Word.Pass));
|
|
2106
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2107
|
+
if (!isEmptyTryBlock(nodes[i])) continue;
|
|
2108
|
+
const next = nodes[i + 1];
|
|
2109
|
+
const hasHandler = next instanceof AST.ASTCondBlock &&
|
|
2110
|
+
(next.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2111
|
+
next.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2112
|
+
if (!hasHandler) {
|
|
2113
|
+
nodes.splice(i, 1);
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
// Walk all trailing handlers; drop the whole cluster iff every
|
|
2117
|
+
// one is a trivial pass-body — keeping any would still leave
|
|
2118
|
+
// the empty try as a syntax error anyway.
|
|
2119
|
+
let tailEnd = i + 1;
|
|
2120
|
+
while (tailEnd < nodes.length && isTrivialHandlerBlock(nodes[tailEnd])) {
|
|
2121
|
+
tailEnd++;
|
|
2122
|
+
}
|
|
2123
|
+
const allTrivial = tailEnd > i + 1 &&
|
|
2124
|
+
nodes.slice(i + 1, tailEnd).every(isTrivialHandlerBlock) &&
|
|
2125
|
+
(tailEnd === nodes.length ||
|
|
2126
|
+
!(nodes[tailEnd] instanceof AST.ASTCondBlock &&
|
|
2127
|
+
(nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Except ||
|
|
2128
|
+
nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Finally)));
|
|
2129
|
+
if (allTrivial) {
|
|
2130
|
+
nodes.splice(i, tailEnd - i);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// Pass 4.5: move a sibling Except/Finally that immediately follows
|
|
2135
|
+
// a Try into the Try's body. When CFG reconstruction hoists the
|
|
2136
|
+
// handler one level up (e.g. into the parent Else), the block
|
|
2137
|
+
// renderer dedents `except:` to the parent's indent — producing
|
|
2138
|
+
// `else: / try: / except:` where `except:` lands at the `else:`
|
|
2139
|
+
// column instead of the `try:` column, which is a SyntaxError.
|
|
2140
|
+
// Absorbing the handler into the Try body lets the renderer's
|
|
2141
|
+
// single-step dedent land it at the correct `try:` column.
|
|
2142
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
2143
|
+
const cur = nodes[i];
|
|
2144
|
+
if (!(cur instanceof AST.ASTBlock) ||
|
|
2145
|
+
cur.blockType !== AST.ASTBlock.BlockType.Try) continue;
|
|
2146
|
+
const next = nodes[i + 1];
|
|
2147
|
+
if (!(next instanceof AST.ASTCondBlock)) continue;
|
|
2148
|
+
if (next.blockType !== AST.ASTBlock.BlockType.Except &&
|
|
2149
|
+
next.blockType !== AST.ASTBlock.BlockType.Finally) continue;
|
|
2150
|
+
// Skip if the Try already carries its own trailing handler —
|
|
2151
|
+
// moving another one in would stack two handlers, which is
|
|
2152
|
+
// only valid for multi-except chains (same Try). Those cases
|
|
2153
|
+
// already render fine.
|
|
2154
|
+
const last = cur.nodes[cur.nodes.length - 1];
|
|
2155
|
+
const hasOwnHandler = last instanceof AST.ASTCondBlock &&
|
|
2156
|
+
(last.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2157
|
+
last.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2158
|
+
if (hasOwnHandler) continue;
|
|
2159
|
+
// Empty Try bodies + non-trivial handler would collapse to an
|
|
2160
|
+
// invalid `try: / except:` with nothing between. Inject a
|
|
2161
|
+
// `pass` so the rendered block is syntactically valid.
|
|
2162
|
+
if (cur.nodes.length === 0) {
|
|
2163
|
+
cur.nodes.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
2164
|
+
}
|
|
2165
|
+
cur.nodes.push(next);
|
|
2166
|
+
nodes.splice(i + 1, 1);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// Pass 5: a Try block with a non-empty body but no following
|
|
2170
|
+
// Except/Finally handler is a SyntaxError. Append a synthetic
|
|
2171
|
+
// `except: pass` so the output parses — real structural recovery
|
|
2172
|
+
// needs dedicated work, but the fallback keeps the file usable.
|
|
2173
|
+
// Skip when the Try already has an Except/Finally nested in its
|
|
2174
|
+
// body (except* groups build nested handler structures).
|
|
2175
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
2176
|
+
const cur = nodes[i];
|
|
2177
|
+
if (!(cur instanceof AST.ASTBlock) ||
|
|
2178
|
+
cur.blockType !== AST.ASTBlock.BlockType.Try) {
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
if (cur.nodes.length === 0) continue;
|
|
2182
|
+
const next = nodes[i + 1];
|
|
2183
|
+
const hasHandler = next instanceof AST.ASTCondBlock &&
|
|
2184
|
+
(next.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2185
|
+
next.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2186
|
+
if (hasHandler) continue;
|
|
2187
|
+
const hasNestedExcept = cur.nodes.some(n =>
|
|
2188
|
+
n instanceof AST.ASTCondBlock &&
|
|
2189
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2190
|
+
n.blockType === AST.ASTBlock.BlockType.Finally));
|
|
2191
|
+
if (hasNestedExcept) continue;
|
|
2192
|
+
const synthetic = new AST.ASTCondBlock(
|
|
2193
|
+
AST.ASTBlock.BlockType.Except,
|
|
2194
|
+
cur.end, cur.end, null, false);
|
|
2195
|
+
synthetic.line = cur.line;
|
|
2196
|
+
synthetic.m_nodes.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
2197
|
+
nodes.splice(i + 1, 0, synthetic);
|
|
2198
|
+
i++;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// Pass 5.5: adopt orphan Except into a preceding branch whose
|
|
2202
|
+
// body ends with an un-paired Try. CPython can emit a try/except
|
|
2203
|
+
// inside the tail of an if/else branch so that, after CFG
|
|
2204
|
+
// reconstruction, the Try ends up as the last child of the branch
|
|
2205
|
+
// while its handler gets hoisted one level up as a stray sibling.
|
|
2206
|
+
// Moving the handler back in keeps the pair valid. Absorbed
|
|
2207
|
+
// trailing statements stay in the handler body — not semantically
|
|
2208
|
+
// correct, but renders to parseable Python instead of a stray
|
|
2209
|
+
// `except:` at the outer indent.
|
|
2210
|
+
const branchTypes = [
|
|
2211
|
+
AST.ASTBlock.BlockType.If,
|
|
2212
|
+
AST.ASTBlock.BlockType.Elif,
|
|
2213
|
+
AST.ASTBlock.BlockType.Else,
|
|
2214
|
+
];
|
|
2215
|
+
const endsWithOrphanTry = (block) => {
|
|
2216
|
+
if (!(block instanceof AST.ASTBlock)) return false;
|
|
2217
|
+
if (!branchTypes.includes(block.blockType)) return false;
|
|
2218
|
+
const body = block.nodes;
|
|
2219
|
+
if (!body || body.length === 0) return false;
|
|
2220
|
+
const last = body[body.length - 1];
|
|
2221
|
+
if (!(last instanceof AST.ASTBlock) ||
|
|
2222
|
+
last.blockType !== AST.ASTBlock.BlockType.Try) {
|
|
2223
|
+
return false;
|
|
2224
|
+
}
|
|
2225
|
+
const hasHandlerInside = last.nodes.some(n =>
|
|
2226
|
+
n instanceof AST.ASTCondBlock &&
|
|
2227
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2228
|
+
n.blockType === AST.ASTBlock.BlockType.Finally));
|
|
2229
|
+
return !hasHandlerInside;
|
|
2230
|
+
};
|
|
2231
|
+
for (let i = nodes.length - 1; i > 0; i--) {
|
|
2232
|
+
const cur = nodes[i];
|
|
2233
|
+
if (!(cur instanceof AST.ASTCondBlock)) continue;
|
|
2234
|
+
if (cur.blockType !== AST.ASTBlock.BlockType.Except &&
|
|
2235
|
+
cur.blockType !== AST.ASTBlock.BlockType.Finally) continue;
|
|
2236
|
+
const prev = nodes[i - 1];
|
|
2237
|
+
if (!endsWithOrphanTry(prev)) continue;
|
|
2238
|
+
// Confirm there's no matching Try at this container level,
|
|
2239
|
+
// otherwise leave the pair alone.
|
|
2240
|
+
let hasLocalTry = false;
|
|
2241
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
2242
|
+
const p = nodes[j];
|
|
2243
|
+
if (p instanceof AST.ASTBlock &&
|
|
2244
|
+
p.blockType === AST.ASTBlock.BlockType.Try) {
|
|
2245
|
+
hasLocalTry = true;
|
|
2246
|
+
break;
|
|
2247
|
+
}
|
|
2248
|
+
if (p instanceof AST.ASTCondBlock &&
|
|
2249
|
+
(p.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2250
|
+
p.blockType === AST.ASTBlock.BlockType.Finally)) {
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
if (hasLocalTry) continue;
|
|
2256
|
+
prev.nodes.push(cur);
|
|
2257
|
+
nodes.splice(i, 1);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// Pass 6: drop orphan Except/Finally blocks that have no matching
|
|
2261
|
+
// Try (or preceding Except) anywhere earlier in this container.
|
|
2262
|
+
// These are leftovers from misreconstructed CFGs — keeping them
|
|
2263
|
+
// produces SyntaxError at parse time. Inside a Try body Except
|
|
2264
|
+
// can legitimately nest (Py3.11+ handler layout, except* groups),
|
|
2265
|
+
// but only at the tail. A mid-body Except with non-handler
|
|
2266
|
+
// statements after it is a stray handler — drop it.
|
|
2267
|
+
const inTryBody = parentBlockType === AST.ASTBlock.BlockType.Try;
|
|
2268
|
+
const isHandlerBlock = (x) =>
|
|
2269
|
+
x instanceof AST.ASTCondBlock &&
|
|
2270
|
+
(x.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2271
|
+
x.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2272
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2273
|
+
const cur = nodes[i];
|
|
2274
|
+
if (!(cur instanceof AST.ASTCondBlock)) continue;
|
|
2275
|
+
if (cur.blockType !== AST.ASTBlock.BlockType.Except &&
|
|
2276
|
+
cur.blockType !== AST.ASTBlock.BlockType.Finally) continue;
|
|
2277
|
+
if (inTryBody) {
|
|
2278
|
+
// except* groups have complex internal structure — never
|
|
2279
|
+
// touch them.
|
|
2280
|
+
if (cur.isExceptStar) continue;
|
|
2281
|
+
// For regular Except nested inside Try body (Py3.11+
|
|
2282
|
+
// handler layout): legitimate only when at the tail.
|
|
2283
|
+
let allTrailingHandlers = true;
|
|
2284
|
+
for (let k = i + 1; k < nodes.length; k++) {
|
|
2285
|
+
if (!isHandlerBlock(nodes[k])) {
|
|
2286
|
+
allTrailingHandlers = false;
|
|
2287
|
+
break;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if (allTrailingHandlers) continue;
|
|
2291
|
+
}
|
|
2292
|
+
let anchorFound = false;
|
|
2293
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
2294
|
+
const prev = nodes[j];
|
|
2295
|
+
if (prev instanceof AST.ASTBlock &&
|
|
2296
|
+
prev.blockType === AST.ASTBlock.BlockType.Try) {
|
|
2297
|
+
anchorFound = true;
|
|
2298
|
+
break;
|
|
2299
|
+
}
|
|
2300
|
+
if (prev instanceof AST.ASTCondBlock &&
|
|
2301
|
+
(prev.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2302
|
+
prev.blockType === AST.ASTBlock.BlockType.Finally)) {
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2307
|
+
if (!anchorFound) nodes.splice(i, 1);
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// Pass 7: after earlier passes may have emptied a Try body, drop
|
|
2311
|
+
// Try+trivial-Except pairs where the body is now empty. Python
|
|
2312
|
+
// forbids `try:` with no body; a pass-only handler carries no
|
|
2313
|
+
// recoverable intent, so splice the cluster out entirely.
|
|
2314
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2315
|
+
if (!isEmptyTryBlock(nodes[i])) continue;
|
|
2316
|
+
let tailEnd = i + 1;
|
|
2317
|
+
while (tailEnd < nodes.length && isTrivialHandlerBlock(nodes[tailEnd])) {
|
|
2318
|
+
tailEnd++;
|
|
2319
|
+
}
|
|
2320
|
+
if (tailEnd === i + 1) continue;
|
|
2321
|
+
const trailingHandler = nodes[tailEnd] instanceof AST.ASTCondBlock &&
|
|
2322
|
+
(nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Except ||
|
|
2323
|
+
nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Finally);
|
|
2324
|
+
if (trailingHandler) continue;
|
|
2325
|
+
nodes.splice(i, tailEnd - i);
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2329
|
+
// Post-order traversal: process children before parent so nested
|
|
2330
|
+
// emptyings bubble up — an outer Try can only be recognised as empty
|
|
2331
|
+
// after the inner Try/Except artifacts inside its body were dropped.
|
|
2332
|
+
const visited = new WeakSet();
|
|
2333
|
+
const visit = (n) => {
|
|
2334
|
+
if (!n || visited.has(n)) return;
|
|
2335
|
+
visited.add(n);
|
|
2336
|
+
let nodes = null;
|
|
2337
|
+
let parentType = null;
|
|
2338
|
+
if (n instanceof AST.ASTNodeList) {
|
|
2339
|
+
nodes = n.list;
|
|
2340
|
+
parentType = null;
|
|
2341
|
+
} else if (n instanceof AST.ASTBlock) {
|
|
2342
|
+
nodes = n.nodes;
|
|
2343
|
+
parentType = n.blockType;
|
|
2344
|
+
} else if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2345
|
+
const body = n.src.code?.object?.SourceCode;
|
|
2346
|
+
if (body) visit(body);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
if (nodes) {
|
|
2350
|
+
for (const c of nodes) visit(c);
|
|
2351
|
+
rewriteContainer(nodes, parentType);
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
visit(root);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// An Except block can get reconstructed inside a non-Try block's body
|
|
2358
|
+
// (most often a For/While/If inside the protected try-body) when the
|
|
2359
|
+
// handler's offset range overlaps a nested loop. Python requires the
|
|
2360
|
+
// except clause to sit next to its `try:`, so walk the tree and pull
|
|
2361
|
+
// such Except blocks out of any non-Try ancestor until they become a
|
|
2362
|
+
// sibling of their enclosing Try.
|
|
2363
|
+
hoistNestedExceptBlocks(root) {
|
|
2364
|
+
const isHoistableBlock = (node) =>
|
|
2365
|
+
node instanceof AST.ASTBlock &&
|
|
2366
|
+
node.blockType !== AST.ASTBlock.BlockType.Try;
|
|
2367
|
+
|
|
2368
|
+
const isExcept = (node) =>
|
|
2369
|
+
node instanceof AST.ASTCondBlock &&
|
|
2370
|
+
node.blockType === AST.ASTBlock.BlockType.Except;
|
|
2371
|
+
|
|
2372
|
+
// Fixed-point loop: one pass hoists Except up one level. Bounded by
|
|
2373
|
+
// tree depth; excessive iterations just exit.
|
|
2374
|
+
for (let iter = 0; iter < 64; iter++) {
|
|
2375
|
+
let moved = false;
|
|
2376
|
+
const stack = [{container: null, nodes: null, parent: null}];
|
|
2377
|
+
const parentMap = new WeakMap(); // child container → {arr, index, parent}
|
|
2378
|
+
|
|
2379
|
+
const visit = (arr, parentInfo) => {
|
|
2380
|
+
for (let i = 0; i < arr.length; i++) {
|
|
2381
|
+
const n = arr[i];
|
|
2382
|
+
if (!n) continue;
|
|
2383
|
+
let children = null;
|
|
2384
|
+
if (n instanceof AST.ASTBlock || n instanceof AST.ASTCondBlock) {
|
|
2385
|
+
children = n.nodes;
|
|
2386
|
+
} else if (n instanceof AST.ASTNodeList) {
|
|
2387
|
+
children = n.list;
|
|
2388
|
+
} else if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2389
|
+
const src = n.src.code?.object?.SourceCode;
|
|
2390
|
+
if (src instanceof AST.ASTNodeList) {
|
|
2391
|
+
visit(src.list, null);
|
|
2392
|
+
}
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
2396
|
+
// If this block is non-Try and its first child is an
|
|
2397
|
+
// Except, pull that Except out into the parent arr
|
|
2398
|
+
// right after `n`.
|
|
2399
|
+
if (isHoistableBlock(n)) {
|
|
2400
|
+
// Scan forward for leading Except children only;
|
|
2401
|
+
// an Except appearing later in the body is usually
|
|
2402
|
+
// a real construct we should leave alone.
|
|
2403
|
+
while (children.length > 0 && isExcept(children[0])) {
|
|
2404
|
+
const ex = children.shift();
|
|
2405
|
+
arr.splice(i + 1, 0, ex);
|
|
2406
|
+
moved = true;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
visit(children, {arr, index: i});
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2414
|
+
const roots = [];
|
|
2415
|
+
if (root instanceof AST.ASTNodeList) roots.push(root.list);
|
|
2416
|
+
else if (root instanceof AST.ASTBlock) roots.push(root.nodes);
|
|
2417
|
+
|
|
2418
|
+
for (const r of roots) visit(r, null);
|
|
2419
|
+
|
|
2420
|
+
// Also dive into nested function bodies.
|
|
2421
|
+
const walk = (n, seen) => {
|
|
2422
|
+
if (!n || seen.has(n)) return;
|
|
2423
|
+
seen.add(n);
|
|
2424
|
+
if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2425
|
+
const src = n.src.code?.object?.SourceCode;
|
|
2426
|
+
if (src instanceof AST.ASTNodeList) {
|
|
2427
|
+
visit(src.list, null);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
let children = null;
|
|
2431
|
+
if (n instanceof AST.ASTNodeList) children = n.list;
|
|
2432
|
+
else if (n instanceof AST.ASTBlock || n instanceof AST.ASTCondBlock) children = n.nodes;
|
|
2433
|
+
if (Array.isArray(children)) {
|
|
2434
|
+
for (const c of children) walk(c, seen);
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
walk(root, new WeakSet());
|
|
2438
|
+
|
|
2439
|
+
if (!moved) break;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
1504
2443
|
dedupeExceptHandlers(root) {
|
|
1505
2444
|
const queue = [root];
|
|
1506
2445
|
const visited = new WeakSet();
|
|
@@ -1520,12 +2459,23 @@ class PycDecompiler {
|
|
|
1520
2459
|
}
|
|
1521
2460
|
}
|
|
1522
2461
|
|
|
1523
|
-
// Drop trivial generic except handlers that only contain pass/cleanup.
|
|
2462
|
+
// Drop trivial generic except* handlers (exception-group artifacts) that only contain pass/cleanup.
|
|
2463
|
+
// User-written `except:` and `except Exception:` must be preserved.
|
|
2464
|
+
// Python forbids mixing `except` and `except*` inside the same try, so a
|
|
2465
|
+
// non-star trivial `except Exception [as X]: pass` in a try that has any
|
|
2466
|
+
// `except*` sibling is always a decompiler artifact.
|
|
2467
|
+
const hasExceptStarSibling = nodes.some(n =>
|
|
2468
|
+
n instanceof AST.ASTCondBlock &&
|
|
2469
|
+
n.blockType === AST.ASTBlock.BlockType.Except &&
|
|
2470
|
+
n.isExceptStar);
|
|
1524
2471
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
1525
2472
|
const blk = nodes[i];
|
|
1526
2473
|
if (!(blk instanceof AST.ASTCondBlock) || blk.blockType !== AST.ASTBlock.BlockType.Except) {
|
|
1527
2474
|
continue;
|
|
1528
2475
|
}
|
|
2476
|
+
if (!blk.isExceptStar && !hasExceptStarSibling) {
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
1529
2479
|
const condNode = blk.condition instanceof AST.ASTStore ? blk.condition.src : blk.condition;
|
|
1530
2480
|
const condText = (typeof condNode?.codeFragment === 'function' ? condNode.codeFragment()?.toString?.() : null) || condNode?.name;
|
|
1531
2481
|
const body = blk.nodes || [];
|
|
@@ -1603,6 +2553,11 @@ class PycDecompiler {
|
|
|
1603
2553
|
if (!(node instanceof AST.ASTCondBlock)) {
|
|
1604
2554
|
return false;
|
|
1605
2555
|
}
|
|
2556
|
+
// `while True: pass` / `while 1:` has no explicit condition but is a legitimate
|
|
2557
|
+
// user construct; only If blocks are null-sentinel artifacts from with-cleanup.
|
|
2558
|
+
if (node.blockType !== AST.ASTBlock.BlockType.If) {
|
|
2559
|
+
return false;
|
|
2560
|
+
}
|
|
1606
2561
|
const condition = node.condition;
|
|
1607
2562
|
|
|
1608
2563
|
// Drop degenerate IF blocks with no condition and empty body (often leftover from with cleanup tests)
|
|
@@ -1769,6 +2724,14 @@ class PycDecompiler {
|
|
|
1769
2724
|
const frag = ch?.codeFragment?.()?.toString?.() || "";
|
|
1770
2725
|
return frag === "pass" || frag.includes("__exception__") || frag.includes("##ERROR##");
|
|
1771
2726
|
}))) {
|
|
2727
|
+
// Drop only synthetic except* handlers without a user condition.
|
|
2728
|
+
// User-written `except:` / `except E:` must be preserved; replace
|
|
2729
|
+
// cleanup-only body with `pass` so the try statement stays valid.
|
|
2730
|
+
if (node.isExceptStar && !node.condition) {
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
node.m_nodes = [new AST.ASTKeyword(AST.ASTKeyword.Word.Pass)];
|
|
2734
|
+
rewritten.push(node);
|
|
1772
2735
|
continue;
|
|
1773
2736
|
}
|
|
1774
2737
|
if (typeof node?.codeFragment === 'function') {
|
|
@@ -1843,25 +2806,72 @@ class PycDecompiler {
|
|
|
1843
2806
|
}
|
|
1844
2807
|
|
|
1845
2808
|
const targetName = node.dest?.name || node.dest?.codeFragment?.();
|
|
2809
|
+
const isClassLike = targetName && targetName[0] === targetName[0]?.toUpperCase?.();
|
|
2810
|
+
|
|
2811
|
+
// Preferred path (3.13+): the wrapper's own SourceCode ends with
|
|
2812
|
+
// `return <ASTFunction>` where the function already has correct
|
|
2813
|
+
// annotations (via SET_FUNCTION_ATTRIBUTE) and typeParams with
|
|
2814
|
+
// PEP 696 defaults (via CALL_INTRINSIC_2 intrinsics 5 + 4).
|
|
2815
|
+
const wrapperBody = func.code?.object?.SourceCode?.list || [];
|
|
2816
|
+
let returnedNode = null;
|
|
2817
|
+
for (let j = wrapperBody.length - 1; j >= 0; j--) {
|
|
2818
|
+
const stmt = wrapperBody[j];
|
|
2819
|
+
if (stmt instanceof AST.ASTReturn && stmt.value) {
|
|
2820
|
+
returnedNode = stmt.value;
|
|
2821
|
+
break;
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
if (!isClassLike && returnedNode instanceof AST.ASTFunction) {
|
|
2825
|
+
node.m_src = returnedNode;
|
|
2826
|
+
continue;
|
|
2827
|
+
}
|
|
2828
|
+
// Classes: the wrapper's direct return is an ASTClass whose base is
|
|
2829
|
+
// an internal CPython temp (".generic_base" for SUBSCRIPT_GENERIC).
|
|
2830
|
+
// Fall through to the Consts-based rebuild which infers clean
|
|
2831
|
+
// typeParams from string names instead.
|
|
2832
|
+
|
|
2833
|
+
// Fallback (pre-3.13 or irregular wrappers): rebuild from Consts.
|
|
1846
2834
|
const codeConsts = func.code?.object?.Consts?.Value || [];
|
|
1847
|
-
const
|
|
2835
|
+
const allCodeObjects = codeConsts.filter(c => c?.ClassName === 'Py_CodeObject');
|
|
2836
|
+
let innerCodeObj = allCodeObjects.find(c => c.Name?.toString?.() === targetName);
|
|
2837
|
+
if (!innerCodeObj) {
|
|
2838
|
+
innerCodeObj = allCodeObjects[0];
|
|
2839
|
+
}
|
|
1848
2840
|
if (!innerCodeObj) {
|
|
1849
2841
|
continue;
|
|
1850
2842
|
}
|
|
1851
|
-
|
|
1852
|
-
const typeParams = codeConsts
|
|
2843
|
+
const typeParamNames = codeConsts
|
|
1853
2844
|
.filter(c => c?.ClassName === 'Py_String')
|
|
1854
2845
|
.map(c => c.Value)
|
|
1855
2846
|
.filter(v => /^[A-Z][A-Za-z0-9_]*$/.test(v || '') && v !== targetName);
|
|
1856
2847
|
|
|
1857
|
-
|
|
2848
|
+
const typeParams = typeParamNames.map(name => {
|
|
2849
|
+
const defaultCode = allCodeObjects.find(c =>
|
|
2850
|
+
c !== innerCodeObj &&
|
|
2851
|
+
(c.ArgCount || 0) === 0 &&
|
|
2852
|
+
c.Name?.toString?.() === name
|
|
2853
|
+
);
|
|
2854
|
+
if (!defaultCode) return name;
|
|
2855
|
+
try {
|
|
2856
|
+
const dd = new PycDecompiler(defaultCode);
|
|
2857
|
+
const body = dd.decompile();
|
|
2858
|
+
defaultCode.SourceCode = body;
|
|
2859
|
+
if (dd.errors.length) this.errors.push(...dd.errors);
|
|
2860
|
+
const list = body?.list || [];
|
|
2861
|
+
const top = list.top?.() || list[list.length - 1] || list[0];
|
|
2862
|
+
if (top instanceof AST.ASTReturn && top.value) {
|
|
2863
|
+
return { name, default: top.value };
|
|
2864
|
+
}
|
|
2865
|
+
} catch (_) { /* fall through */ }
|
|
2866
|
+
return name;
|
|
2867
|
+
});
|
|
2868
|
+
|
|
1858
2869
|
const innerDecompiler = new PycDecompiler(innerCodeObj);
|
|
1859
2870
|
const innerBody = innerDecompiler.decompile();
|
|
1860
2871
|
innerCodeObj.SourceCode = innerBody;
|
|
2872
|
+
if (innerDecompiler.errors.length) this.errors.push(...innerDecompiler.errors);
|
|
1861
2873
|
const astObj = new AST.ASTObject(innerCodeObj);
|
|
1862
2874
|
|
|
1863
|
-
const isClassLike = targetName && targetName[0] === targetName[0]?.toUpperCase?.();
|
|
1864
|
-
|
|
1865
2875
|
if (!isClassLike) {
|
|
1866
2876
|
const fn = new AST.ASTFunction(astObj);
|
|
1867
2877
|
fn.annotations = innerDecompiler.funcAnnotations || fn.annotations;
|