depyo 1.0.3 → 1.2.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/README.md +160 -83
- package/lib/PycDecompiler.js +761 -1
- package/lib/PycReader.js +10 -1
- package/lib/PythonObject.js +10 -2
- package/lib/ast/ast_node.js +130 -21
- package/lib/bytecode/python_3_14.js +1 -1
- package/lib/handlers/binary_ops.js +9 -0
- package/lib/handlers/comparisons.js +3 -10
- package/lib/handlers/exceptions_blocks.js +7 -2
- package/lib/handlers/function_calls.js +24 -5
- package/lib/handlers/function_class_build.js +11 -1
- package/lib/handlers/load_store_names.js +46 -1
- package/lib/handlers/misc_other.js +76 -4
- package/lib/handlers/stack_ops.js +4 -2
- package/lib/handlers/unary_ops.js +7 -0
- package/package.json +2 -2
package/lib/PycDecompiler.js
CHANGED
|
@@ -150,6 +150,8 @@ class PycDecompiler {
|
|
|
150
150
|
this.enrichGenericAnnotations(functonBody);
|
|
151
151
|
this.rewriteClassDefinitions(functonBody);
|
|
152
152
|
this.removeNullSentinelComparisons(functonBody);
|
|
153
|
+
this.cleanupExcMatchArtifacts(functonBody);
|
|
154
|
+
this.hoistNestedExceptBlocks(functonBody);
|
|
153
155
|
this.dedupeExceptHandlers(functonBody);
|
|
154
156
|
this.removeDuplicateReturns(functonBody);
|
|
155
157
|
|
|
@@ -428,7 +430,19 @@ class PycDecompiler {
|
|
|
428
430
|
if (offset === undefined) {
|
|
429
431
|
return;
|
|
430
432
|
}
|
|
431
|
-
|
|
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
|
+
);
|
|
432
446
|
|
|
433
447
|
// Build set of WITH_EXCEPT_START handler ranges (lazily, once per code object)
|
|
434
448
|
if (!this._withExceptRanges) {
|
|
@@ -579,6 +593,16 @@ class PycDecompiler {
|
|
|
579
593
|
}
|
|
580
594
|
}
|
|
581
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
|
+
}
|
|
582
606
|
const handlerEnd = entry.depth === 0
|
|
583
607
|
? (maxHandlerEnd || entry.end || this.code.LastOffset || this.object.CodeSize || 0)
|
|
584
608
|
: (entry.end || maxHandlerEnd || this.code.LastOffset || this.object.CodeSize || 0);
|
|
@@ -644,6 +668,54 @@ class PycDecompiler {
|
|
|
644
668
|
if (!next) break;
|
|
645
669
|
end = next.end;
|
|
646
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
|
+
}
|
|
647
719
|
return end;
|
|
648
720
|
}
|
|
649
721
|
|
|
@@ -861,6 +933,23 @@ class PycDecompiler {
|
|
|
861
933
|
break; // This WITH block hasn't reached its end yet
|
|
862
934
|
}
|
|
863
935
|
|
|
936
|
+
// Close any blocks ABOVE this WITH whose end is also reached.
|
|
937
|
+
// 3.14 with-bodies that contain an if/else commonly have the
|
|
938
|
+
// else's end coincide with the with's end (both target the
|
|
939
|
+
// post-with cleanup offset). If we close the WITH while an
|
|
940
|
+
// Else is still open above it, the else body gets reparented
|
|
941
|
+
// to the WITH's parent — producing a stray `else:` dedented
|
|
942
|
+
// out of the with-block in the rendered source.
|
|
943
|
+
while (this.blocks.length - 1 > withIdx) {
|
|
944
|
+
const above = this.blocks.top();
|
|
945
|
+
if (above.end <= 0 || this.code.Current.Offset < above.end) {
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
this.blocks.pop();
|
|
949
|
+
const newTop = this.blocks.top();
|
|
950
|
+
if (newTop) newTop.append(above);
|
|
951
|
+
}
|
|
952
|
+
|
|
864
953
|
// Close this WITH block
|
|
865
954
|
this.blocks.splice(withIdx, 1);
|
|
866
955
|
const parentBlock = this.blocks[withIdx - 1] || this.blocks.top();
|
|
@@ -1653,11 +1742,45 @@ class PycDecompiler {
|
|
|
1653
1742
|
if (hasDataclass) {
|
|
1654
1743
|
node.addDecorator(new AST.ASTName('dataclass'));
|
|
1655
1744
|
}
|
|
1745
|
+
} else if (node.src instanceof AST.ASTCall && this.isDecoratedClassCall(node.src)) {
|
|
1746
|
+
// Pattern: `Name = decorator(ClassLiteral[, ...])` produced by Py3.12+
|
|
1747
|
+
// dataclass/decorator-of-class flows. Promote to a real class with
|
|
1748
|
+
// the call-of-decorator attached as an @decorator.
|
|
1749
|
+
const call = node.src;
|
|
1750
|
+
const classNode = call.pparams[0];
|
|
1751
|
+
const decoratorExpr = this._buildDecoratorExpr(call);
|
|
1752
|
+
node.src = classNode;
|
|
1753
|
+
this.cleanupClassBody(classNode);
|
|
1754
|
+
if (decoratorExpr) {
|
|
1755
|
+
node.addDecorator(decoratorExpr);
|
|
1756
|
+
}
|
|
1656
1757
|
} else if (node.src instanceof AST.ASTClass) {
|
|
1657
1758
|
this.cleanupClassBody(node.src);
|
|
1658
1759
|
}
|
|
1659
1760
|
}
|
|
1660
1761
|
|
|
1762
|
+
isDecoratedClassCall(call) {
|
|
1763
|
+
if (!(call instanceof AST.ASTCall)) return false;
|
|
1764
|
+
if (call.func instanceof AST.ASTClass) return false;
|
|
1765
|
+
if (!call.pparams || call.pparams.length < 1) return false;
|
|
1766
|
+
if (!(call.pparams[0] instanceof AST.ASTClass)) return false;
|
|
1767
|
+
// Any further pparams would become positional args to the decorator,
|
|
1768
|
+
// which cannot be expressed as `@decorator(class)` sugar. Reject.
|
|
1769
|
+
return call.pparams.length === 1;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
_buildDecoratorExpr(call) {
|
|
1773
|
+
// `decorator(class)` → `@decorator`
|
|
1774
|
+
// `decorator(class, kw=val, ...)` → `@decorator(kw=val, ...)`
|
|
1775
|
+
const kwargs = call.kwparams || [];
|
|
1776
|
+
if (kwargs.length === 0 && !call.hasVar && !call.hasKw) {
|
|
1777
|
+
return call.func;
|
|
1778
|
+
}
|
|
1779
|
+
const wrapper = new AST.ASTCall(call.func, [], kwargs);
|
|
1780
|
+
wrapper.line = call.line;
|
|
1781
|
+
return wrapper;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1661
1784
|
_childContainers(node) {
|
|
1662
1785
|
const containers = [];
|
|
1663
1786
|
if (node instanceof AST.ASTBlock) {
|
|
@@ -1801,6 +1924,635 @@ class PycDecompiler {
|
|
|
1801
1924
|
}
|
|
1802
1925
|
}
|
|
1803
1926
|
|
|
1927
|
+
// Rewrites 3.11+ CHECK_EXC_MATCH artifacts that leaked past block
|
|
1928
|
+
// reconstruction: unwraps `except <exc> EXC_MATCH <type>` conditions
|
|
1929
|
+
// down to `<type>`, rewrites sibling `while __exception__ EXC_MATCH T:`
|
|
1930
|
+
// blocks into `except T:` handlers on the preceding Try, and strips
|
|
1931
|
+
// trailing compiler-generated reraise/cleanup nodes (bare `raise`,
|
|
1932
|
+
// `e = None; del e`, empty `try: pass`). Runs once on the whole tree.
|
|
1933
|
+
cleanupExcMatchArtifacts(root) {
|
|
1934
|
+
// Pre-3.11 except-as handlers still need two cleanups: strip the
|
|
1935
|
+
// `X = None; del X` pair CPython emits after every `except ... as X:`
|
|
1936
|
+
// body (PEP 3134), and unwrap the synthetic `try: body / finally: pass`
|
|
1937
|
+
// that wraps the handler body (CPython lowers `except ... as X:` to
|
|
1938
|
+
// `try: body; finally: X = None; del X`, and the finally body ends up
|
|
1939
|
+
// emptied as the X=None/del X move out to the handler tail).
|
|
1940
|
+
if (this.object.Reader.versionCompare(3, 11) < 0) {
|
|
1941
|
+
this.cleanupPre311ExceptAsArtifacts(root);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
const EXC_MATCH = AST.ASTCompare.CompareOp.Exception;
|
|
1946
|
+
|
|
1947
|
+
const unwrapExcMatch = (cond) => {
|
|
1948
|
+
if (!cond) return cond;
|
|
1949
|
+
// The condition may have been wrapped in ASTStore (alias binding) -
|
|
1950
|
+
// unwrap first, then strip the EXC_MATCH compare.
|
|
1951
|
+
let inner = cond;
|
|
1952
|
+
if (inner instanceof AST.ASTStore && inner.src) {
|
|
1953
|
+
inner = inner.src;
|
|
1954
|
+
}
|
|
1955
|
+
if (inner instanceof AST.ASTCompare && inner.op === EXC_MATCH) {
|
|
1956
|
+
return inner.right;
|
|
1957
|
+
}
|
|
1958
|
+
return cond;
|
|
1959
|
+
};
|
|
1960
|
+
|
|
1961
|
+
const isExcMatchCompare = (node) =>
|
|
1962
|
+
node instanceof AST.ASTCompare && node.op === EXC_MATCH;
|
|
1963
|
+
|
|
1964
|
+
const isExceptionName = (node) =>
|
|
1965
|
+
node instanceof AST.ASTName && node.name === '__exception__';
|
|
1966
|
+
|
|
1967
|
+
const containsExceptionRef = (node, depth = 0) => {
|
|
1968
|
+
if (!node || depth > 4) return false;
|
|
1969
|
+
if (isExceptionName(node)) return true;
|
|
1970
|
+
if (node instanceof AST.ASTCompare || node instanceof AST.ASTBinary) {
|
|
1971
|
+
return containsExceptionRef(node.left, depth + 1) ||
|
|
1972
|
+
containsExceptionRef(node.right, depth + 1);
|
|
1973
|
+
}
|
|
1974
|
+
if (node instanceof AST.ASTCall) {
|
|
1975
|
+
return containsExceptionRef(node.func, depth + 1) ||
|
|
1976
|
+
(node.pparams || []).some(p => containsExceptionRef(p, depth + 1));
|
|
1977
|
+
}
|
|
1978
|
+
if (node instanceof AST.ASTStore) {
|
|
1979
|
+
return containsExceptionRef(node.src, depth + 1);
|
|
1980
|
+
}
|
|
1981
|
+
return false;
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1984
|
+
const isEmptyTryBlock = (node) =>
|
|
1985
|
+
node instanceof AST.ASTBlock &&
|
|
1986
|
+
node.blockType === AST.ASTBlock.BlockType.Try &&
|
|
1987
|
+
(node.nodes.length === 0 ||
|
|
1988
|
+
(node.nodes.length === 1 &&
|
|
1989
|
+
node.nodes[0] instanceof AST.ASTKeyword &&
|
|
1990
|
+
node.nodes[0].key === AST.ASTKeyword.Word.Pass));
|
|
1991
|
+
|
|
1992
|
+
const isOrphanExceptionStmt = (node) => {
|
|
1993
|
+
// Bare `raise` with no params → fall-through RERAISE artifact
|
|
1994
|
+
if (node instanceof AST.ASTRaise && (!node.params || node.params.length === 0)) {
|
|
1995
|
+
return true;
|
|
1996
|
+
}
|
|
1997
|
+
// `__exception__(None, None, None)` or `__exception__` alone
|
|
1998
|
+
if (node instanceof AST.ASTCall && isExceptionName(node.func)) {
|
|
1999
|
+
return true;
|
|
2000
|
+
}
|
|
2001
|
+
if (isExceptionName(node)) {
|
|
2002
|
+
return true;
|
|
2003
|
+
}
|
|
2004
|
+
// `x = __exception__` - CPython alias-clear artifact
|
|
2005
|
+
if (node instanceof AST.ASTStore && isExceptionName(node.src)) {
|
|
2006
|
+
return true;
|
|
2007
|
+
}
|
|
2008
|
+
// `return __exception__` - misrendered reraise epilogue
|
|
2009
|
+
if (node instanceof AST.ASTReturn && isExceptionName(node.value)) {
|
|
2010
|
+
return true;
|
|
2011
|
+
}
|
|
2012
|
+
return false;
|
|
2013
|
+
};
|
|
2014
|
+
|
|
2015
|
+
const rewriteContainer = (nodes, parentBlockType) => {
|
|
2016
|
+
if (!Array.isArray(nodes)) return;
|
|
2017
|
+
|
|
2018
|
+
// Pass 1: unwrap EXC_MATCH in Except block conditions.
|
|
2019
|
+
for (const node of nodes) {
|
|
2020
|
+
if (node instanceof AST.ASTCondBlock &&
|
|
2021
|
+
node.blockType === AST.ASTBlock.BlockType.Except) {
|
|
2022
|
+
node.condition = unwrapExcMatch(node.condition);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// Pass 2: convert ASTWhile/ASTIf with EXC_MATCH condition to Except
|
|
2027
|
+
// handlers and attach them to the preceding Try block. If no preceding
|
|
2028
|
+
// Try exists (deeply misparsed region), drop them to avoid invalid output.
|
|
2029
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2030
|
+
const node = nodes[i];
|
|
2031
|
+
if (!(node instanceof AST.ASTCondBlock) ||
|
|
2032
|
+
!(node.blockType === AST.ASTBlock.BlockType.While ||
|
|
2033
|
+
node.blockType === AST.ASTBlock.BlockType.If)) {
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
if (!isExcMatchCompare(node.condition) ||
|
|
2037
|
+
!containsExceptionRef(node.condition.left)) {
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
const matchType = node.condition.right;
|
|
2041
|
+
// Find nearest preceding Try block in this container.
|
|
2042
|
+
let tryIdx = -1;
|
|
2043
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
2044
|
+
const cand = nodes[j];
|
|
2045
|
+
if (cand instanceof AST.ASTBlock &&
|
|
2046
|
+
cand.blockType === AST.ASTBlock.BlockType.Try) {
|
|
2047
|
+
tryIdx = j;
|
|
2048
|
+
break;
|
|
2049
|
+
}
|
|
2050
|
+
// Allow existing Except handlers between; skip past them.
|
|
2051
|
+
if (cand instanceof AST.ASTCondBlock &&
|
|
2052
|
+
cand.blockType === AST.ASTBlock.BlockType.Except) {
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
break;
|
|
2056
|
+
}
|
|
2057
|
+
// Strip any trailing orphan reraise/alias-clear statements from body.
|
|
2058
|
+
const cleanBody = node.nodes.filter(n => !isOrphanExceptionStmt(n));
|
|
2059
|
+
if (cleanBody.length === 0) {
|
|
2060
|
+
cleanBody.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
if (tryIdx >= 0) {
|
|
2064
|
+
const exceptBlk = new AST.ASTCondBlock(
|
|
2065
|
+
AST.ASTBlock.BlockType.Except,
|
|
2066
|
+
node.start, node.end, matchType, false);
|
|
2067
|
+
exceptBlk.line = node.line;
|
|
2068
|
+
exceptBlk.m_nodes = cleanBody;
|
|
2069
|
+
// Insert right after last existing Except handler for that Try.
|
|
2070
|
+
let insertAt = tryIdx + 1;
|
|
2071
|
+
while (insertAt < nodes.length &&
|
|
2072
|
+
nodes[insertAt] instanceof AST.ASTCondBlock &&
|
|
2073
|
+
nodes[insertAt].blockType === AST.ASTBlock.BlockType.Except) {
|
|
2074
|
+
insertAt++;
|
|
2075
|
+
}
|
|
2076
|
+
nodes.splice(i, 1);
|
|
2077
|
+
if (insertAt > i) insertAt--;
|
|
2078
|
+
nodes.splice(insertAt, 0, exceptBlk);
|
|
2079
|
+
} else {
|
|
2080
|
+
// No Try to attach to — dropping keeps output parseable.
|
|
2081
|
+
nodes.splice(i, 1);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// Pass 3: strip orphan exception-related statements at container
|
|
2086
|
+
// level — but NOT inside Except/Finally bodies, where bare `raise`
|
|
2087
|
+
// is a legitimate re-raise statement.
|
|
2088
|
+
const inHandlerBody = parentBlockType === AST.ASTBlock.BlockType.Except ||
|
|
2089
|
+
parentBlockType === AST.ASTBlock.BlockType.Finally;
|
|
2090
|
+
if (!inHandlerBody) {
|
|
2091
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2092
|
+
if (isOrphanExceptionStmt(nodes[i])) {
|
|
2093
|
+
nodes.splice(i, 1);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// Pass 3.5: strip `X = None; del X` pairs — CPython 3.11+ emits this
|
|
2099
|
+
// at the tail/fall-through of every `except Foo as X:` body to clear
|
|
2100
|
+
// the binding. The pattern is synthetic and user code effectively
|
|
2101
|
+
// never writes it literally, so removing it is safe and prevents
|
|
2102
|
+
// the artifact from breaking up adjacent except clauses.
|
|
2103
|
+
for (let i = nodes.length - 2; i >= 0; i--) {
|
|
2104
|
+
const a = nodes[i], b = nodes[i + 1];
|
|
2105
|
+
if (a instanceof AST.ASTStore &&
|
|
2106
|
+
a.src instanceof AST.ASTNone &&
|
|
2107
|
+
a.dest instanceof AST.ASTName &&
|
|
2108
|
+
b instanceof AST.ASTDelete &&
|
|
2109
|
+
b.value instanceof AST.ASTName &&
|
|
2110
|
+
a.dest.name === b.value.name) {
|
|
2111
|
+
nodes.splice(i, 2);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// Pass 4: drop empty `try:` blocks. With no following handler the
|
|
2116
|
+
// try is a bare leftover. With a trivial `except: pass` handler the
|
|
2117
|
+
// whole pair is a reconstruction artifact — Python forbids
|
|
2118
|
+
// `try:<empty>`, so either outcome requires the try to go. When the
|
|
2119
|
+
// handler has real body drop only the try and splice a `pass`
|
|
2120
|
+
// placeholder into the handler's matching — but the current
|
|
2121
|
+
// sightings all have pass-only handlers, so mirror that case.
|
|
2122
|
+
const isTrivialHandlerBlock = (n) =>
|
|
2123
|
+
n instanceof AST.ASTCondBlock &&
|
|
2124
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2125
|
+
n.blockType === AST.ASTBlock.BlockType.Finally) &&
|
|
2126
|
+
(n.nodes.length === 0 ||
|
|
2127
|
+
(n.nodes.length === 1 &&
|
|
2128
|
+
n.nodes[0] instanceof AST.ASTKeyword &&
|
|
2129
|
+
n.nodes[0].key === AST.ASTKeyword.Word.Pass));
|
|
2130
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2131
|
+
if (!isEmptyTryBlock(nodes[i])) continue;
|
|
2132
|
+
const next = nodes[i + 1];
|
|
2133
|
+
const hasHandler = next instanceof AST.ASTCondBlock &&
|
|
2134
|
+
(next.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2135
|
+
next.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2136
|
+
if (!hasHandler) {
|
|
2137
|
+
nodes.splice(i, 1);
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
// Walk all trailing handlers; drop the whole cluster iff every
|
|
2141
|
+
// one is a trivial pass-body — keeping any would still leave
|
|
2142
|
+
// the empty try as a syntax error anyway.
|
|
2143
|
+
let tailEnd = i + 1;
|
|
2144
|
+
while (tailEnd < nodes.length && isTrivialHandlerBlock(nodes[tailEnd])) {
|
|
2145
|
+
tailEnd++;
|
|
2146
|
+
}
|
|
2147
|
+
const allTrivial = tailEnd > i + 1 &&
|
|
2148
|
+
nodes.slice(i + 1, tailEnd).every(isTrivialHandlerBlock) &&
|
|
2149
|
+
(tailEnd === nodes.length ||
|
|
2150
|
+
!(nodes[tailEnd] instanceof AST.ASTCondBlock &&
|
|
2151
|
+
(nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Except ||
|
|
2152
|
+
nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Finally)));
|
|
2153
|
+
if (allTrivial) {
|
|
2154
|
+
nodes.splice(i, tailEnd - i);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// Pass 4.5: move a sibling Except/Finally that immediately follows
|
|
2159
|
+
// a Try into the Try's body. When CFG reconstruction hoists the
|
|
2160
|
+
// handler one level up (e.g. into the parent Else), the block
|
|
2161
|
+
// renderer dedents `except:` to the parent's indent — producing
|
|
2162
|
+
// `else: / try: / except:` where `except:` lands at the `else:`
|
|
2163
|
+
// column instead of the `try:` column, which is a SyntaxError.
|
|
2164
|
+
// Absorbing the handler into the Try body lets the renderer's
|
|
2165
|
+
// single-step dedent land it at the correct `try:` column.
|
|
2166
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
2167
|
+
const cur = nodes[i];
|
|
2168
|
+
if (!(cur instanceof AST.ASTBlock) ||
|
|
2169
|
+
cur.blockType !== AST.ASTBlock.BlockType.Try) continue;
|
|
2170
|
+
const next = nodes[i + 1];
|
|
2171
|
+
if (!(next instanceof AST.ASTCondBlock)) continue;
|
|
2172
|
+
if (next.blockType !== AST.ASTBlock.BlockType.Except &&
|
|
2173
|
+
next.blockType !== AST.ASTBlock.BlockType.Finally) continue;
|
|
2174
|
+
// Skip if the Try already carries its own trailing handler —
|
|
2175
|
+
// moving another one in would stack two handlers, which is
|
|
2176
|
+
// only valid for multi-except chains (same Try). Those cases
|
|
2177
|
+
// already render fine.
|
|
2178
|
+
const last = cur.nodes[cur.nodes.length - 1];
|
|
2179
|
+
const hasOwnHandler = last instanceof AST.ASTCondBlock &&
|
|
2180
|
+
(last.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2181
|
+
last.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2182
|
+
if (hasOwnHandler) continue;
|
|
2183
|
+
// Empty Try bodies + non-trivial handler would collapse to an
|
|
2184
|
+
// invalid `try: / except:` with nothing between. Inject a
|
|
2185
|
+
// `pass` so the rendered block is syntactically valid.
|
|
2186
|
+
if (cur.nodes.length === 0) {
|
|
2187
|
+
cur.nodes.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
2188
|
+
}
|
|
2189
|
+
cur.nodes.push(next);
|
|
2190
|
+
nodes.splice(i + 1, 1);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// Pass 5: a Try block with a non-empty body but no following
|
|
2194
|
+
// Except/Finally handler is a SyntaxError. Append a synthetic
|
|
2195
|
+
// `except: pass` so the output parses — real structural recovery
|
|
2196
|
+
// needs dedicated work, but the fallback keeps the file usable.
|
|
2197
|
+
// Skip when the Try already has an Except/Finally nested in its
|
|
2198
|
+
// body (except* groups build nested handler structures).
|
|
2199
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
2200
|
+
const cur = nodes[i];
|
|
2201
|
+
if (!(cur instanceof AST.ASTBlock) ||
|
|
2202
|
+
cur.blockType !== AST.ASTBlock.BlockType.Try) {
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
if (cur.nodes.length === 0) continue;
|
|
2206
|
+
const next = nodes[i + 1];
|
|
2207
|
+
const hasHandler = next instanceof AST.ASTCondBlock &&
|
|
2208
|
+
(next.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2209
|
+
next.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2210
|
+
if (hasHandler) continue;
|
|
2211
|
+
const hasNestedExcept = cur.nodes.some(n =>
|
|
2212
|
+
n instanceof AST.ASTCondBlock &&
|
|
2213
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2214
|
+
n.blockType === AST.ASTBlock.BlockType.Finally));
|
|
2215
|
+
if (hasNestedExcept) continue;
|
|
2216
|
+
const synthetic = new AST.ASTCondBlock(
|
|
2217
|
+
AST.ASTBlock.BlockType.Except,
|
|
2218
|
+
cur.end, cur.end, null, false);
|
|
2219
|
+
synthetic.line = cur.line;
|
|
2220
|
+
synthetic.m_nodes.push(new AST.ASTKeyword(AST.ASTKeyword.Word.Pass));
|
|
2221
|
+
nodes.splice(i + 1, 0, synthetic);
|
|
2222
|
+
i++;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// Pass 5.5: adopt orphan Except into a preceding branch whose
|
|
2226
|
+
// body ends with an un-paired Try. CPython can emit a try/except
|
|
2227
|
+
// inside the tail of an if/else branch so that, after CFG
|
|
2228
|
+
// reconstruction, the Try ends up as the last child of the branch
|
|
2229
|
+
// while its handler gets hoisted one level up as a stray sibling.
|
|
2230
|
+
// Moving the handler back in keeps the pair valid. Absorbed
|
|
2231
|
+
// trailing statements stay in the handler body — not semantically
|
|
2232
|
+
// correct, but renders to parseable Python instead of a stray
|
|
2233
|
+
// `except:` at the outer indent.
|
|
2234
|
+
const branchTypes = [
|
|
2235
|
+
AST.ASTBlock.BlockType.If,
|
|
2236
|
+
AST.ASTBlock.BlockType.Elif,
|
|
2237
|
+
AST.ASTBlock.BlockType.Else,
|
|
2238
|
+
];
|
|
2239
|
+
const endsWithOrphanTry = (block) => {
|
|
2240
|
+
if (!(block instanceof AST.ASTBlock)) return false;
|
|
2241
|
+
if (!branchTypes.includes(block.blockType)) return false;
|
|
2242
|
+
const body = block.nodes;
|
|
2243
|
+
if (!body || body.length === 0) return false;
|
|
2244
|
+
const last = body[body.length - 1];
|
|
2245
|
+
if (!(last instanceof AST.ASTBlock) ||
|
|
2246
|
+
last.blockType !== AST.ASTBlock.BlockType.Try) {
|
|
2247
|
+
return false;
|
|
2248
|
+
}
|
|
2249
|
+
const hasHandlerInside = last.nodes.some(n =>
|
|
2250
|
+
n instanceof AST.ASTCondBlock &&
|
|
2251
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2252
|
+
n.blockType === AST.ASTBlock.BlockType.Finally));
|
|
2253
|
+
return !hasHandlerInside;
|
|
2254
|
+
};
|
|
2255
|
+
for (let i = nodes.length - 1; i > 0; i--) {
|
|
2256
|
+
const cur = nodes[i];
|
|
2257
|
+
if (!(cur instanceof AST.ASTCondBlock)) continue;
|
|
2258
|
+
if (cur.blockType !== AST.ASTBlock.BlockType.Except &&
|
|
2259
|
+
cur.blockType !== AST.ASTBlock.BlockType.Finally) continue;
|
|
2260
|
+
const prev = nodes[i - 1];
|
|
2261
|
+
if (!endsWithOrphanTry(prev)) continue;
|
|
2262
|
+
// Confirm there's no matching Try at this container level,
|
|
2263
|
+
// otherwise leave the pair alone.
|
|
2264
|
+
let hasLocalTry = false;
|
|
2265
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
2266
|
+
const p = nodes[j];
|
|
2267
|
+
if (p instanceof AST.ASTBlock &&
|
|
2268
|
+
p.blockType === AST.ASTBlock.BlockType.Try) {
|
|
2269
|
+
hasLocalTry = true;
|
|
2270
|
+
break;
|
|
2271
|
+
}
|
|
2272
|
+
if (p instanceof AST.ASTCondBlock &&
|
|
2273
|
+
(p.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2274
|
+
p.blockType === AST.ASTBlock.BlockType.Finally)) {
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
break;
|
|
2278
|
+
}
|
|
2279
|
+
if (hasLocalTry) continue;
|
|
2280
|
+
prev.nodes.push(cur);
|
|
2281
|
+
nodes.splice(i, 1);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// Pass 6: drop orphan Except/Finally blocks that have no matching
|
|
2285
|
+
// Try (or preceding Except) anywhere earlier in this container.
|
|
2286
|
+
// These are leftovers from misreconstructed CFGs — keeping them
|
|
2287
|
+
// produces SyntaxError at parse time. Inside a Try body Except
|
|
2288
|
+
// can legitimately nest (Py3.11+ handler layout, except* groups),
|
|
2289
|
+
// but only at the tail. A mid-body Except with non-handler
|
|
2290
|
+
// statements after it is a stray handler — drop it.
|
|
2291
|
+
const inTryBody = parentBlockType === AST.ASTBlock.BlockType.Try;
|
|
2292
|
+
const isHandlerBlock = (x) =>
|
|
2293
|
+
x instanceof AST.ASTCondBlock &&
|
|
2294
|
+
(x.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2295
|
+
x.blockType === AST.ASTBlock.BlockType.Finally);
|
|
2296
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2297
|
+
const cur = nodes[i];
|
|
2298
|
+
if (!(cur instanceof AST.ASTCondBlock)) continue;
|
|
2299
|
+
if (cur.blockType !== AST.ASTBlock.BlockType.Except &&
|
|
2300
|
+
cur.blockType !== AST.ASTBlock.BlockType.Finally) continue;
|
|
2301
|
+
if (inTryBody) {
|
|
2302
|
+
// except* groups have complex internal structure — never
|
|
2303
|
+
// touch them.
|
|
2304
|
+
if (cur.isExceptStar) continue;
|
|
2305
|
+
// For regular Except nested inside Try body (Py3.11+
|
|
2306
|
+
// handler layout): legitimate only when at the tail.
|
|
2307
|
+
let allTrailingHandlers = true;
|
|
2308
|
+
for (let k = i + 1; k < nodes.length; k++) {
|
|
2309
|
+
if (!isHandlerBlock(nodes[k])) {
|
|
2310
|
+
allTrailingHandlers = false;
|
|
2311
|
+
break;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
if (allTrailingHandlers) continue;
|
|
2315
|
+
}
|
|
2316
|
+
let anchorFound = false;
|
|
2317
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
2318
|
+
const prev = nodes[j];
|
|
2319
|
+
if (prev instanceof AST.ASTBlock &&
|
|
2320
|
+
prev.blockType === AST.ASTBlock.BlockType.Try) {
|
|
2321
|
+
anchorFound = true;
|
|
2322
|
+
break;
|
|
2323
|
+
}
|
|
2324
|
+
if (prev instanceof AST.ASTCondBlock &&
|
|
2325
|
+
(prev.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2326
|
+
prev.blockType === AST.ASTBlock.BlockType.Finally)) {
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
break;
|
|
2330
|
+
}
|
|
2331
|
+
if (!anchorFound) nodes.splice(i, 1);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// Pass 7: after earlier passes may have emptied a Try body, drop
|
|
2335
|
+
// Try+trivial-Except pairs where the body is now empty. Python
|
|
2336
|
+
// forbids `try:` with no body; a pass-only handler carries no
|
|
2337
|
+
// recoverable intent, so splice the cluster out entirely.
|
|
2338
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2339
|
+
if (!isEmptyTryBlock(nodes[i])) continue;
|
|
2340
|
+
let tailEnd = i + 1;
|
|
2341
|
+
while (tailEnd < nodes.length && isTrivialHandlerBlock(nodes[tailEnd])) {
|
|
2342
|
+
tailEnd++;
|
|
2343
|
+
}
|
|
2344
|
+
if (tailEnd === i + 1) continue;
|
|
2345
|
+
const trailingHandler = nodes[tailEnd] instanceof AST.ASTCondBlock &&
|
|
2346
|
+
(nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Except ||
|
|
2347
|
+
nodes[tailEnd].blockType === AST.ASTBlock.BlockType.Finally);
|
|
2348
|
+
if (trailingHandler) continue;
|
|
2349
|
+
nodes.splice(i, tailEnd - i);
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
|
|
2353
|
+
// Post-order traversal: process children before parent so nested
|
|
2354
|
+
// emptyings bubble up — an outer Try can only be recognised as empty
|
|
2355
|
+
// after the inner Try/Except artifacts inside its body were dropped.
|
|
2356
|
+
const visited = new WeakSet();
|
|
2357
|
+
const visit = (n) => {
|
|
2358
|
+
if (!n || visited.has(n)) return;
|
|
2359
|
+
visited.add(n);
|
|
2360
|
+
let nodes = null;
|
|
2361
|
+
let parentType = null;
|
|
2362
|
+
if (n instanceof AST.ASTNodeList) {
|
|
2363
|
+
nodes = n.list;
|
|
2364
|
+
parentType = null;
|
|
2365
|
+
} else if (n instanceof AST.ASTBlock) {
|
|
2366
|
+
nodes = n.nodes;
|
|
2367
|
+
parentType = n.blockType;
|
|
2368
|
+
} else if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2369
|
+
const body = n.src.code?.object?.SourceCode;
|
|
2370
|
+
if (body) visit(body);
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
if (nodes) {
|
|
2374
|
+
for (const c of nodes) visit(c);
|
|
2375
|
+
rewriteContainer(nodes, parentType);
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2378
|
+
visit(root);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// Pre-3.11 `except Foo as X:` expands to `try: body / finally: X = None;
|
|
2382
|
+
// del X`. The decompiler hoists the cleanup out to the handler tail,
|
|
2383
|
+
// leaving `try: body / finally: pass / X = None / del X`. Strip the pair
|
|
2384
|
+
// and unwrap the synthetic try/finally so the handler renders as `body`.
|
|
2385
|
+
cleanupPre311ExceptAsArtifacts(root) {
|
|
2386
|
+
const isNoneDelPair = (a, b) =>
|
|
2387
|
+
a instanceof AST.ASTStore &&
|
|
2388
|
+
a.src instanceof AST.ASTNone &&
|
|
2389
|
+
a.dest instanceof AST.ASTName &&
|
|
2390
|
+
b instanceof AST.ASTDelete &&
|
|
2391
|
+
b.value instanceof AST.ASTName &&
|
|
2392
|
+
a.dest.name === b.value.name;
|
|
2393
|
+
|
|
2394
|
+
const isFinallyTrivial = (n) =>
|
|
2395
|
+
n instanceof AST.ASTBlock &&
|
|
2396
|
+
n.blockType === AST.ASTBlock.BlockType.Finally &&
|
|
2397
|
+
(n.nodes.length === 0 ||
|
|
2398
|
+
(n.nodes.length === 1 &&
|
|
2399
|
+
n.nodes[0] instanceof AST.ASTKeyword &&
|
|
2400
|
+
n.nodes[0].key === AST.ASTKeyword.Word.Pass));
|
|
2401
|
+
|
|
2402
|
+
// A CPython `except Foo as X:` body wraps user code in a synthetic
|
|
2403
|
+
// `try: body / finally: X=None; del X`. After the Pass 3.5 strip the
|
|
2404
|
+
// finally becomes empty / pass-only, leaving an ASTContainerBlock
|
|
2405
|
+
// whose children are [Try(body), Finally(pass)]. Collapse that back
|
|
2406
|
+
// to just the try-body so the handler renders as `body`.
|
|
2407
|
+
const unwrapTryFinallyPassContainer = (nodes) => {
|
|
2408
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
2409
|
+
const cont = nodes[i];
|
|
2410
|
+
if (!(cont instanceof AST.ASTContainerBlock)) continue;
|
|
2411
|
+
const children = cont.nodes;
|
|
2412
|
+
if (!children || children.length !== 2) continue;
|
|
2413
|
+
const tryBlk = children[0];
|
|
2414
|
+
const finBlk = children[1];
|
|
2415
|
+
if (!(tryBlk instanceof AST.ASTBlock) ||
|
|
2416
|
+
tryBlk.blockType !== AST.ASTBlock.BlockType.Try) continue;
|
|
2417
|
+
if (!isFinallyTrivial(finBlk)) continue;
|
|
2418
|
+
if (tryBlk.nodes.length === 0) continue;
|
|
2419
|
+
// Keep real user try/except nested inside — unwrapping
|
|
2420
|
+
// would change semantics when the try body itself carries
|
|
2421
|
+
// handlers that could fire.
|
|
2422
|
+
const hasNestedHandler = tryBlk.nodes.some(n =>
|
|
2423
|
+
n instanceof AST.ASTCondBlock &&
|
|
2424
|
+
(n.blockType === AST.ASTBlock.BlockType.Except ||
|
|
2425
|
+
n.blockType === AST.ASTBlock.BlockType.Finally));
|
|
2426
|
+
if (hasNestedHandler) continue;
|
|
2427
|
+
nodes.splice(i, 1, ...tryBlk.nodes);
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
|
|
2431
|
+
const stripNoneDelPairs = (nodes) => {
|
|
2432
|
+
for (let i = nodes.length - 2; i >= 0; i--) {
|
|
2433
|
+
if (isNoneDelPair(nodes[i], nodes[i + 1])) {
|
|
2434
|
+
nodes.splice(i, 2);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
|
|
2439
|
+
const visited = new WeakSet();
|
|
2440
|
+
const visit = (n) => {
|
|
2441
|
+
if (!n || visited.has(n)) return;
|
|
2442
|
+
visited.add(n);
|
|
2443
|
+
let nodes = null;
|
|
2444
|
+
let parentType = null;
|
|
2445
|
+
if (n instanceof AST.ASTNodeList) {
|
|
2446
|
+
nodes = n.list;
|
|
2447
|
+
} else if (n instanceof AST.ASTBlock) {
|
|
2448
|
+
nodes = n.nodes;
|
|
2449
|
+
parentType = n.blockType;
|
|
2450
|
+
} else if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2451
|
+
const body = n.src.code?.object?.SourceCode;
|
|
2452
|
+
if (body) visit(body);
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
if (nodes) {
|
|
2456
|
+
for (const c of nodes) visit(c);
|
|
2457
|
+
stripNoneDelPairs(nodes);
|
|
2458
|
+
// The try/finally:pass wrapper is CPython's synthetic scaffold
|
|
2459
|
+
// for `except Foo as X:` — only unwrap inside Except bodies,
|
|
2460
|
+
// otherwise real user-written `try: body / finally: pass`
|
|
2461
|
+
// constructs at top-level/function body get collapsed too.
|
|
2462
|
+
if (parentType === AST.ASTBlock.BlockType.Except) {
|
|
2463
|
+
unwrapTryFinallyPassContainer(nodes);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
visit(root);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
// An Except block can get reconstructed inside a non-Try block's body
|
|
2471
|
+
// (most often a For/While/If inside the protected try-body) when the
|
|
2472
|
+
// handler's offset range overlaps a nested loop. Python requires the
|
|
2473
|
+
// except clause to sit next to its `try:`, so walk the tree and pull
|
|
2474
|
+
// such Except blocks out of any non-Try ancestor until they become a
|
|
2475
|
+
// sibling of their enclosing Try.
|
|
2476
|
+
hoistNestedExceptBlocks(root) {
|
|
2477
|
+
const isHoistableBlock = (node) =>
|
|
2478
|
+
node instanceof AST.ASTBlock &&
|
|
2479
|
+
node.blockType !== AST.ASTBlock.BlockType.Try;
|
|
2480
|
+
|
|
2481
|
+
const isExcept = (node) =>
|
|
2482
|
+
node instanceof AST.ASTCondBlock &&
|
|
2483
|
+
node.blockType === AST.ASTBlock.BlockType.Except;
|
|
2484
|
+
|
|
2485
|
+
// Fixed-point loop: one pass hoists Except up one level. Bounded by
|
|
2486
|
+
// tree depth; excessive iterations just exit.
|
|
2487
|
+
for (let iter = 0; iter < 64; iter++) {
|
|
2488
|
+
let moved = false;
|
|
2489
|
+
const stack = [{container: null, nodes: null, parent: null}];
|
|
2490
|
+
const parentMap = new WeakMap(); // child container → {arr, index, parent}
|
|
2491
|
+
|
|
2492
|
+
const visit = (arr, parentInfo) => {
|
|
2493
|
+
for (let i = 0; i < arr.length; i++) {
|
|
2494
|
+
const n = arr[i];
|
|
2495
|
+
if (!n) continue;
|
|
2496
|
+
let children = null;
|
|
2497
|
+
if (n instanceof AST.ASTBlock || n instanceof AST.ASTCondBlock) {
|
|
2498
|
+
children = n.nodes;
|
|
2499
|
+
} else if (n instanceof AST.ASTNodeList) {
|
|
2500
|
+
children = n.list;
|
|
2501
|
+
} else if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2502
|
+
const src = n.src.code?.object?.SourceCode;
|
|
2503
|
+
if (src instanceof AST.ASTNodeList) {
|
|
2504
|
+
visit(src.list, null);
|
|
2505
|
+
}
|
|
2506
|
+
continue;
|
|
2507
|
+
}
|
|
2508
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
2509
|
+
// If this block is non-Try and its first child is an
|
|
2510
|
+
// Except, pull that Except out into the parent arr
|
|
2511
|
+
// right after `n`.
|
|
2512
|
+
if (isHoistableBlock(n)) {
|
|
2513
|
+
// Scan forward for leading Except children only;
|
|
2514
|
+
// an Except appearing later in the body is usually
|
|
2515
|
+
// a real construct we should leave alone.
|
|
2516
|
+
while (children.length > 0 && isExcept(children[0])) {
|
|
2517
|
+
const ex = children.shift();
|
|
2518
|
+
arr.splice(i + 1, 0, ex);
|
|
2519
|
+
moved = true;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
visit(children, {arr, index: i});
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
|
|
2527
|
+
const roots = [];
|
|
2528
|
+
if (root instanceof AST.ASTNodeList) roots.push(root.list);
|
|
2529
|
+
else if (root instanceof AST.ASTBlock) roots.push(root.nodes);
|
|
2530
|
+
|
|
2531
|
+
for (const r of roots) visit(r, null);
|
|
2532
|
+
|
|
2533
|
+
// Also dive into nested function bodies.
|
|
2534
|
+
const walk = (n, seen) => {
|
|
2535
|
+
if (!n || seen.has(n)) return;
|
|
2536
|
+
seen.add(n);
|
|
2537
|
+
if (n instanceof AST.ASTStore && n.src instanceof AST.ASTFunction) {
|
|
2538
|
+
const src = n.src.code?.object?.SourceCode;
|
|
2539
|
+
if (src instanceof AST.ASTNodeList) {
|
|
2540
|
+
visit(src.list, null);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
let children = null;
|
|
2544
|
+
if (n instanceof AST.ASTNodeList) children = n.list;
|
|
2545
|
+
else if (n instanceof AST.ASTBlock || n instanceof AST.ASTCondBlock) children = n.nodes;
|
|
2546
|
+
if (Array.isArray(children)) {
|
|
2547
|
+
for (const c of children) walk(c, seen);
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
walk(root, new WeakSet());
|
|
2551
|
+
|
|
2552
|
+
if (!moved) break;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
1804
2556
|
dedupeExceptHandlers(root) {
|
|
1805
2557
|
const queue = [root];
|
|
1806
2558
|
const visited = new WeakSet();
|
|
@@ -2085,6 +2837,14 @@ class PycDecompiler {
|
|
|
2085
2837
|
const frag = ch?.codeFragment?.()?.toString?.() || "";
|
|
2086
2838
|
return frag === "pass" || frag.includes("__exception__") || frag.includes("##ERROR##");
|
|
2087
2839
|
}))) {
|
|
2840
|
+
// Drop only synthetic except* handlers without a user condition.
|
|
2841
|
+
// User-written `except:` / `except E:` must be preserved; replace
|
|
2842
|
+
// cleanup-only body with `pass` so the try statement stays valid.
|
|
2843
|
+
if (node.isExceptStar && !node.condition) {
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
node.m_nodes = [new AST.ASTKeyword(AST.ASTKeyword.Word.Pass)];
|
|
2847
|
+
rewritten.push(node);
|
|
2088
2848
|
continue;
|
|
2089
2849
|
}
|
|
2090
2850
|
if (typeof node?.codeFragment === 'function') {
|