Skip to content

Commit 5aa1d24

Browse files
swernerxclaude
andcommitted
refactor(transformer): modularize into handler modules
Extract transformer code from monolithic index.ts (7352 lines) into focused handler modules for better maintainability: - handlers/classes.ts: Class, dataclass, enum, TypedDict, Protocol, NamedTuple - handlers/functions.ts: Functions, lambda, async/await, yield - handlers/comprehensions.ts: List/dict/set comprehensions, generators - handlers/imports.ts: Import statement handling - handlers/assignments.ts: Assignments, destructuring - handlers/exceptions.ts: Try/except/finally, raise - handlers/control-flow.ts: If/while/for/match statements - handlers/docstrings.ts: Docstring parsing, JSDoc generation - handlers/literals.ts: Numbers, strings, f-strings, booleans - handlers/expressions.ts: Binary/unary operators, conditionals - builtins/call-handlers.ts: Builtin function registry - types.ts: Shared type definitions - context.ts: Transform context and scope management Result: index.ts reduced from 7352 to 2589 lines (65% reduction) All 2297 tests passing, ESLint clean. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6782579 commit 5aa1d24

14 files changed

Lines changed: 6718 additions & 6339 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import type { SyntaxNode } from "@lezer/common"
2+
import { getChildren } from "../../parser/index.js"
3+
import type { TransformContext, NodeTransformer } from "../types.js"
4+
5+
/**
6+
* Handler configuration for builtin functions.
7+
* Simple handlers just need runtime and output pattern.
8+
*/
9+
interface SimpleHandler {
10+
runtime: string
11+
output: string // Use {args} as placeholder
12+
isAsync?: boolean
13+
needsNew?: boolean
14+
}
15+
16+
/**
17+
* Simple builtin handlers that follow the pattern:
18+
* ctx.usesRuntime.add(runtime); return output.replace("{args}", args)
19+
*/
20+
const SIMPLE_BUILTINS: Record<string, SimpleHandler> = {
21+
// Core builtins
22+
len: { runtime: "len", output: "len({args})" },
23+
range: { runtime: "range", output: "range({args})" },
24+
int: { runtime: "int", output: "int({args})" },
25+
float: { runtime: "float", output: "float({args})" },
26+
str: { runtime: "str", output: "str({args})" },
27+
bool: { runtime: "bool", output: "bool({args})" },
28+
abs: { runtime: "abs", output: "abs({args})" },
29+
min: { runtime: "min", output: "min({args})" },
30+
max: { runtime: "max", output: "max({args})" },
31+
sum: { runtime: "sum", output: "sum({args})" },
32+
list: { runtime: "list", output: "list({args})" },
33+
dict: { runtime: "dict", output: "dict({args})" },
34+
set: { runtime: "set", output: "set({args})" },
35+
tuple: { runtime: "tuple", output: "tuple({args})" },
36+
enumerate: { runtime: "enumerate", output: "enumerate({args})" },
37+
zip: { runtime: "zip", output: "zip({args})" },
38+
sorted: { runtime: "sorted", output: "sorted({args})" },
39+
reversed: { runtime: "reversed", output: "reversed({args})" },
40+
type: { runtime: "type", output: "type({args})" },
41+
input: { runtime: "input", output: "input({args})" },
42+
ord: { runtime: "ord", output: "ord({args})" },
43+
chr: { runtime: "chr", output: "chr({args})" },
44+
all: { runtime: "all", output: "all({args})" },
45+
any: { runtime: "any", output: "any({args})" },
46+
map: { runtime: "map", output: "map({args})" },
47+
filter: { runtime: "filter", output: "filter({args})" },
48+
repr: { runtime: "repr", output: "repr({args})" },
49+
round: { runtime: "round", output: "round({args})" },
50+
divmod: { runtime: "divmod", output: "divmod({args})" },
51+
hex: { runtime: "hex", output: "hex({args})" },
52+
oct: { runtime: "oct", output: "oct({args})" },
53+
bin: { runtime: "bin", output: "bin({args})" },
54+
getattr: { runtime: "getattr", output: "getattr({args})" },
55+
hasattr: { runtime: "hasattr", output: "hasattr({args})" },
56+
setattr: { runtime: "setattr", output: "setattr({args})" },
57+
58+
// itertools
59+
chain: { runtime: "itertools/chain", output: "chain({args})" },
60+
combinations: { runtime: "itertools/combinations", output: "combinations({args})" },
61+
permutations: { runtime: "itertools/permutations", output: "permutations({args})" },
62+
product: { runtime: "itertools/product", output: "product({args})" },
63+
cycle: { runtime: "itertools/cycle", output: "cycle({args})" },
64+
repeat: { runtime: "itertools/repeat", output: "repeat({args})" },
65+
islice: { runtime: "itertools/islice", output: "islice({args})" },
66+
takewhile: { runtime: "itertools/takeWhile", output: "takeWhile({args})" },
67+
dropwhile: { runtime: "itertools/dropWhile", output: "dropWhile({args})" },
68+
zip_longest: { runtime: "itertools/zipLongest", output: "zipLongest({args})" },
69+
compress: { runtime: "itertools/compress", output: "compress({args})" },
70+
filterfalse: { runtime: "itertools/filterFalse", output: "filterFalse({args})" },
71+
accumulate: { runtime: "itertools/accumulate", output: "accumulate({args})" },
72+
groupby: { runtime: "itertools/groupby", output: "groupby({args})" },
73+
count: { runtime: "itertools/count", output: "count({args})" },
74+
tee: { runtime: "itertools/tee", output: "tee({args})" },
75+
pairwise: { runtime: "itertools/pairwise", output: "pairwise({args})" },
76+
combinations_with_replacement: {
77+
runtime: "itertools/combinationsWithReplacement",
78+
output: "combinationsWithReplacement({args})"
79+
},
80+
81+
// collections
82+
Counter: { runtime: "collections/Counter", output: "new Counter({args})", needsNew: true },
83+
defaultdict: { runtime: "collections/defaultdict", output: "defaultdict({args})" },
84+
deque: { runtime: "collections/deque", output: "new deque({args})", needsNew: true },
85+
86+
// functools
87+
partial: { runtime: "functools/partial", output: "partial({args})" },
88+
reduce: { runtime: "functools/reduce", output: "reduce({args})" },
89+
lru_cache: { runtime: "functools/lruCache", output: "lruCache({args})" },
90+
cache: { runtime: "functools/cache", output: "cache({args})" },
91+
wraps: { runtime: "functools/wraps", output: "wraps({args})" },
92+
cmp_to_key: { runtime: "functools/cmpToKey", output: "cmpToKey({args})" },
93+
total_ordering: { runtime: "functools/totalOrdering", output: "totalOrdering({args})" },
94+
95+
// json
96+
dumps: { runtime: "json/dumps", output: "dumps({args})" },
97+
loads: { runtime: "json/loads", output: "loads({args})" },
98+
dump: { runtime: "json/dump", output: "dump({args})" },
99+
load: { runtime: "json/load", output: "load({args})" },
100+
101+
// datetime
102+
datetime: { runtime: "datetime/datetime", output: "new datetime({args})", needsNew: true },
103+
date: { runtime: "datetime/date", output: "new date({args})", needsNew: true },
104+
time: { runtime: "datetime/time", output: "new time({args})", needsNew: true },
105+
timedelta: { runtime: "datetime/timedelta", output: "new timedelta({args})", needsNew: true },
106+
107+
// string module
108+
Template: { runtime: "string/Template", output: "new Template({args})", needsNew: true },
109+
capwords: { runtime: "string/capWords", output: "capWords({args})" },
110+
111+
// glob module - async
112+
glob: { runtime: "glob/glob", output: "await glob({args})", isAsync: true },
113+
iglob: { runtime: "glob/iglob", output: "await iglob({args})", isAsync: true },
114+
rglob: { runtime: "glob/rglob", output: "await rglob({args})", isAsync: true },
115+
116+
// shutil module - async
117+
copy: { runtime: "shutil/copy", output: "await copy({args})", isAsync: true },
118+
copy2: { runtime: "shutil/copy2", output: "await copy2({args})", isAsync: true },
119+
copytree: { runtime: "shutil/copytree", output: "await copytree({args})", isAsync: true },
120+
move: { runtime: "shutil/move", output: "await move({args})", isAsync: true },
121+
rmtree: { runtime: "shutil/rmtree", output: "await rmtree({args})", isAsync: true },
122+
which: { runtime: "shutil/which", output: "await which({args})", isAsync: true },
123+
124+
// tempfile module - async
125+
mkstemp: { runtime: "tempfile/mkstemp", output: "await mkstemp({args})", isAsync: true },
126+
mkdtemp: { runtime: "tempfile/mkdtemp", output: "await mkdtemp({args})", isAsync: true },
127+
NamedTemporaryFile: {
128+
runtime: "tempfile/NamedTemporaryFile",
129+
output: "await NamedTemporaryFile.create({args})",
130+
isAsync: true
131+
},
132+
TemporaryDirectory: {
133+
runtime: "tempfile/TemporaryDirectory",
134+
output: "await TemporaryDirectory.create({args})",
135+
isAsync: true
136+
},
137+
138+
// pathlib
139+
Path: { runtime: "pathlib/Path", output: "new Path({args})", needsNew: true }
140+
}
141+
142+
/**
143+
* Handle isinstance() call with special tuple-to-array conversion
144+
*/
145+
function handleIsinstance(
146+
args: string,
147+
argList: SyntaxNode | undefined,
148+
ctx: TransformContext,
149+
transformNode: NodeTransformer
150+
): string {
151+
ctx.usesRuntime.add("isinstance")
152+
153+
// Special handling: if second arg is a tuple, convert to array for multiple type check
154+
if (argList) {
155+
const argChildren = getChildren(argList).filter(
156+
(c) => c.name !== "(" && c.name !== ")" && c.name !== ","
157+
)
158+
if (argChildren.length >= 2) {
159+
const firstArg = argChildren[0]
160+
const secondArg = argChildren[1]
161+
if (firstArg && secondArg?.name === "TupleExpression") {
162+
// Convert tuple to array literal
163+
const tupleChildren = getChildren(secondArg).filter(
164+
(c) => c.name !== "(" && c.name !== ")" && c.name !== ","
165+
)
166+
const typesCodes = tupleChildren.map((el) => transformNode(el, ctx))
167+
return `isinstance(${transformNode(firstArg, ctx)}, [${typesCodes.join(", ")}])`
168+
}
169+
}
170+
}
171+
return `isinstance(${args})`
172+
}
173+
174+
/**
175+
* Handle builtin function call.
176+
* Returns the transformed code, or null if not a builtin.
177+
*/
178+
export function handleBuiltinCall(
179+
calleeName: string,
180+
args: string,
181+
argList: SyntaxNode | undefined,
182+
ctx: TransformContext,
183+
transformNode: NodeTransformer
184+
): string | null {
185+
// Special case: print -> console.log (no runtime needed)
186+
if (calleeName === "print") {
187+
return `console.log(${args})`
188+
}
189+
190+
// Special case: isinstance needs complex handling
191+
if (calleeName === "isinstance") {
192+
return handleIsinstance(args, argList, ctx, transformNode)
193+
}
194+
195+
// Check simple builtins registry
196+
const handler = SIMPLE_BUILTINS[calleeName]
197+
if (handler) {
198+
ctx.usesRuntime.add(handler.runtime)
199+
return handler.output.replace("{args}", args)
200+
}
201+
202+
return null
203+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import type { TransformContext } from "./types.js"
2+
3+
/**
4+
* JavaScript/TypeScript reserved keywords that cannot be used as variable names.
5+
* Python allows these as identifiers, so we need to rename them.
6+
*/
7+
export const JS_RESERVED_KEYWORDS = new Set([
8+
// ECMAScript reserved words
9+
"break",
10+
"case",
11+
"catch",
12+
"continue",
13+
"debugger",
14+
"default",
15+
"delete",
16+
"do",
17+
"else",
18+
"finally",
19+
"for",
20+
"function",
21+
"if",
22+
"in",
23+
"instanceof",
24+
"new",
25+
"return",
26+
"switch",
27+
"this",
28+
"throw",
29+
"try",
30+
"typeof",
31+
"var",
32+
"void",
33+
"while",
34+
"with",
35+
// ECMAScript 6+ reserved words
36+
"class",
37+
"const",
38+
"enum",
39+
"export",
40+
"extends",
41+
"import",
42+
// Note: 'super' is intentionally NOT in this list as it's valid in JS class contexts
43+
// Strict mode reserved words
44+
"implements",
45+
"interface",
46+
"let",
47+
"package",
48+
"private",
49+
"protected",
50+
"public",
51+
"static",
52+
"yield",
53+
// TypeScript reserved words
54+
"abstract",
55+
"as",
56+
"async",
57+
"await",
58+
"declare",
59+
"is",
60+
"module",
61+
"namespace",
62+
"require",
63+
"type"
64+
// Note: 'get', 'set', 'from', 'of' are contextual keywords and valid as identifiers
65+
])
66+
67+
/**
68+
* Escape a variable name if it's a reserved keyword.
69+
* Adds underscore prefix to reserved keywords.
70+
*/
71+
export function escapeReservedKeyword(name: string): string {
72+
return JS_RESERVED_KEYWORDS.has(name) ? `_${name}` : name
73+
}
74+
75+
/**
76+
* Create a new transform context for processing Python source.
77+
*/
78+
export function createContext(source: string): TransformContext {
79+
return {
80+
source,
81+
indentLevel: 0,
82+
usesRuntime: new Set(),
83+
scopeStack: [new Set()], // Start with one global scope
84+
definedClasses: new Set(),
85+
insideFunctionBody: 0,
86+
insideTryBlock: 0,
87+
hoistedImports: []
88+
}
89+
}
90+
91+
/** Push a new scope onto the stack */
92+
export function pushScope(ctx: TransformContext): void {
93+
ctx.scopeStack.push(new Set())
94+
}
95+
96+
/** Pop the current scope from the stack */
97+
export function popScope(ctx: TransformContext): void {
98+
if (ctx.scopeStack.length > 1) {
99+
ctx.scopeStack.pop()
100+
}
101+
}
102+
103+
/** Check if a variable is declared in any accessible scope */
104+
export function isVariableDeclared(ctx: TransformContext, name: string): boolean {
105+
for (const scope of ctx.scopeStack) {
106+
if (scope.has(name)) return true
107+
}
108+
return false
109+
}
110+
111+
/** Declare a variable in the current (top) scope */
112+
export function declareVariable(ctx: TransformContext, name: string): void {
113+
const currentScope = ctx.scopeStack[ctx.scopeStack.length - 1]
114+
if (currentScope) {
115+
currentScope.add(name)
116+
}
117+
}
118+
119+
/**
120+
* Strip outer parentheses from an expression if they're redundant.
121+
* Used to avoid double-parentheses in conditions like `if ((x === y))`.
122+
* Does NOT strip if the expression contains an assignment (walrus operator needs double parens).
123+
*/
124+
export function stripOuterParens(code: string): string {
125+
const trimmed = code.trim()
126+
if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
127+
// Don't strip if this contains an assignment expression (walrus operator)
128+
// Assignment in condition requires double parens: if ((x = getValue()))
129+
const inner = trimmed.slice(1, -1)
130+
// Check for assignment: = not preceded by !, =, <, > (to avoid ==, !=, <=, >=)
131+
if (/(?<![!=<>])=(?!=)/.test(inner)) {
132+
return code
133+
}
134+
135+
// Check if the parens are balanced (not just opening and closing separately)
136+
let depth = 0
137+
for (let i = 0; i < trimmed.length; i++) {
138+
if (trimmed[i] === "(") depth++
139+
else if (trimmed[i] === ")") depth--
140+
// If depth reaches 0 before the end, the outer parens aren't matched
141+
if (depth === 0 && i < trimmed.length - 1) return code
142+
}
143+
// Safe to strip the outer parens
144+
return inner
145+
}
146+
return code
147+
}
148+
149+
/** Map of Python built-in types to TypeScript types */
150+
export const PYTHON_TO_TS_TYPES: Record<string, string> = {
151+
str: "string",
152+
int: "number",
153+
float: "number",
154+
bool: "boolean",
155+
bytes: "Uint8Array",
156+
None: "null",
157+
Any: "any",
158+
object: "object"
159+
}

0 commit comments

Comments
 (0)