223 lines
8.3 KiB
Python
223 lines
8.3 KiB
Python
import ast
|
|
import textwrap
|
|
import tokenize
|
|
import io
|
|
from typing import Dict, List, Any, Optional
|
|
|
|
from .imports import handle_import, handle_import_from
|
|
from .context import enter_class, exit_class, enter_function, exit_function
|
|
from .calls import handle_method_call
|
|
from .widget_creation import is_widget_creation, extract_widget_info, analyze_widget_creation_commands
|
|
from .extractors import extract_call_parameters, extract_parent_container
|
|
from .values import extract_value, get_variable_name, analyze_lambda_complexity, extract_lambda_body, get_operator_symbol
|
|
from .placements import update_widget_placement
|
|
from .events import analyze_bind_event, analyze_config_command, analyze_callback, is_interactive_widget
|
|
from .connections import create_widget_handler_connections
|
|
|
|
|
|
class TkinterAnalyzer(ast.NodeVisitor):
|
|
|
|
def __init__(self, source_code: str = ""):
|
|
self.source_code = source_code
|
|
self.line_offsets = [0]
|
|
current = 0
|
|
if source_code:
|
|
for line in source_code.splitlines(keepends=True):
|
|
current += len(line)
|
|
self.line_offsets.append(current)
|
|
|
|
self.widgets: List[Dict[str, Any]] = []
|
|
self.window_config = {'title': 'App', 'width': 800, 'height': 600}
|
|
self.imports: Dict[str, str] = {}
|
|
self.variables: Dict[str, Any] = {}
|
|
self.current_class: Optional[str] = None
|
|
self.current_method: Optional[str] = None
|
|
self.event_handlers: List[Dict[str, Any]] = []
|
|
self.command_callbacks: List[Dict[str, Any]] = []
|
|
self.bind_events: List[Dict[str, Any]] = []
|
|
self.methods: Dict[str, str] = {}
|
|
|
|
def visit_Import(self, node: ast.Import):
|
|
handle_import(self, node)
|
|
self.generic_visit(node)
|
|
|
|
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
handle_import_from(self, node)
|
|
self.generic_visit(node)
|
|
|
|
def visit_ClassDef(self, node: ast.ClassDef):
|
|
prev = self.current_class
|
|
enter_class(self, node)
|
|
|
|
is_tk_class = False
|
|
for base in node.bases:
|
|
if isinstance(base, ast.Attribute) and base.attr == 'Tk':
|
|
is_tk_class = True
|
|
elif isinstance(base, ast.Name) and base.id == 'Tk':
|
|
is_tk_class = True
|
|
|
|
if node.name == 'Application':
|
|
self.window_config['className'] = node.name
|
|
elif is_tk_class and self.window_config.get('className') != 'Application':
|
|
self.window_config['className'] = node.name
|
|
elif not self.window_config.get('className'):
|
|
self.window_config['className'] = node.name
|
|
|
|
self.generic_visit(node)
|
|
exit_class(self, prev)
|
|
|
|
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
prev = self.current_method
|
|
enter_function(self, node)
|
|
|
|
if self.current_class and node.name not in ['__init__', 'create_widgets', 'run'] and self.source_code and node.body:
|
|
try:
|
|
|
|
|
|
|
|
|
|
start_line = node.lineno
|
|
end_line = getattr(node, 'end_lineno', None)
|
|
|
|
if end_line is None:
|
|
|
|
end_line = node.body[-1].lineno
|
|
|
|
lines = self.source_code.splitlines(keepends=True)
|
|
|
|
func_lines = lines[start_line-1 : end_line]
|
|
func_text = "".join(func_lines)
|
|
|
|
|
|
|
|
|
|
|
|
body_start_idx = -1
|
|
|
|
try:
|
|
tokens = list(tokenize.tokenize(io.BytesIO(func_text.encode('utf-8')).readline))
|
|
|
|
nesting = 0
|
|
colon_token = None
|
|
|
|
for tok in tokens:
|
|
if tok.type == tokenize.OP:
|
|
if tok.string in '([{':
|
|
nesting += 1
|
|
elif tok.string in ')]}':
|
|
nesting -= 1
|
|
elif tok.string == ':' and nesting == 0:
|
|
colon_token = tok
|
|
|
|
break
|
|
|
|
if colon_token:
|
|
|
|
|
|
|
|
|
|
colon_line_idx = colon_token.end[0] - 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
body_lines = func_lines[colon_line_idx + 1:]
|
|
|
|
|
|
colon_line = func_lines[colon_line_idx]
|
|
after_colon = colon_line[colon_token.end[1]:]
|
|
|
|
if after_colon.strip():
|
|
body_lines.insert(0, after_colon)
|
|
|
|
body_text = "".join(body_lines)
|
|
|
|
|
|
dedented_body = textwrap.dedent(body_text)
|
|
|
|
|
|
|
|
|
|
sig_lines = func_lines[:colon_line_idx]
|
|
sig_lines.append(colon_line[:colon_token.end[1]])
|
|
|
|
sig_raw = "".join(sig_lines).strip()
|
|
if sig_raw.endswith(':'):
|
|
sig_raw = sig_raw[:-1].strip()
|
|
|
|
self.methods[node.name] = {
|
|
'body': dedented_body.strip(),
|
|
'signature': sig_raw
|
|
}
|
|
|
|
except tokenize.TokenError:
|
|
pass
|
|
|
|
except Exception as e:
|
|
pass
|
|
|
|
self.generic_visit(node)
|
|
exit_function(self, prev)
|
|
|
|
def visit_Assign(self, node: ast.Assign):
|
|
if is_widget_creation(self, node):
|
|
info = extract_widget_info(self, node)
|
|
if info:
|
|
self.widgets.append(info)
|
|
analyze_widget_creation_commands(self, node)
|
|
|
|
for target in node.targets:
|
|
if isinstance(target, ast.Name):
|
|
self.variables[target.id] = node.value
|
|
elif isinstance(target, ast.Attribute):
|
|
if isinstance(target.value, ast.Name) and target.value.id == 'self':
|
|
self.variables[target.attr] = node.value
|
|
|
|
self.generic_visit(node)
|
|
|
|
def visit_Call(self, node: ast.Call):
|
|
if isinstance(node.func, ast.Attribute):
|
|
handle_method_call(self, node)
|
|
self.generic_visit(node)
|
|
|
|
def extract_call_parameters(self, call_node: ast.Call) -> Dict[str, Any]:
|
|
return extract_call_parameters(self, call_node)
|
|
|
|
def extract_value(self, node: ast.AST) -> Any:
|
|
return extract_value(node)
|
|
|
|
def extract_parent_container(self, call_node: ast.Call) -> str:
|
|
return extract_parent_container(self, call_node)
|
|
|
|
def get_variable_name(self, node: ast.AST) -> str:
|
|
return get_variable_name(node)
|
|
|
|
def update_widget_placement(self, target_var: str, call_node: ast.Call, method: str):
|
|
return update_widget_placement(self, target_var, call_node, method)
|
|
|
|
def analyze_bind_event(self, target_var: str, call_node: ast.Call):
|
|
return analyze_bind_event(self, target_var, call_node)
|
|
|
|
def analyze_config_command(self, target_var: str, call_node: ast.Call):
|
|
return analyze_config_command(self, target_var, call_node)
|
|
|
|
def analyze_callback(self, callback_node: ast.AST) -> Dict[str, Any]:
|
|
return analyze_callback(self, callback_node)
|
|
|
|
def analyze_lambda_complexity(self, lambda_node: ast.Lambda) -> str:
|
|
return analyze_lambda_complexity(lambda_node)
|
|
|
|
def extract_lambda_body(self, body_node: ast.AST) -> str:
|
|
return extract_lambda_body(body_node)
|
|
|
|
def get_operator_symbol(self, op_node: ast.AST) -> str:
|
|
return get_operator_symbol(op_node)
|
|
|
|
def analyze_widget_creation_commands(self, node: ast.Assign):
|
|
return analyze_widget_creation_commands(self, node)
|