meltygui 0.1.0__py3-none-any.whl
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.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,2684 @@
|
|
|
1
|
+
"""melty_scan — the standalone front half of core_syntax: text → syntax dict.
|
|
2
|
+
|
|
3
|
+
STANDALONE ON PURPOSE: stdlib only, no Melty / libcst imports, so it runs in a
|
|
4
|
+
3.12 SUBINTERPRETER (its own GIL — the parse of a big file never stalls the
|
|
5
|
+
render thread) and its output crosses back as pickled plain data. Three parts:
|
|
6
|
+
|
|
7
|
+
scan(text) the "cst-lite" parser: `tokenize` (C) → statements →
|
|
8
|
+
ast-SHAPED nodes (same class names / fields as `ast`,
|
|
9
|
+
columns in CHARACTERS) for exactly the subset the dict
|
|
10
|
+
surfaces; anything else is an `Opaque` node with a
|
|
11
|
+
span. Purely functional over the token list.
|
|
12
|
+
Extractor the dict builder (moved here from core_syntax) — runs on
|
|
13
|
+
real `ast` nodes or scanner nodes alike (kind = class
|
|
14
|
+
name), emitting either the studio's parse classes
|
|
15
|
+
(in-process) or the neutral `N*` classes below (worker),
|
|
16
|
+
plus the Origin tables.
|
|
17
|
+
scan_extract(text) the worker entry: scan → extract (neutral) → validate
|
|
18
|
+
with ast.parse → picklable result.
|
|
19
|
+
|
|
20
|
+
The `ast` front end stays (core_syntax.parse_to_dict(frontend="ast")) as the
|
|
21
|
+
oracle: tests/test_core_syntax.py compares both on every file under src/.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import ast
|
|
27
|
+
import bisect
|
|
28
|
+
import codecs
|
|
29
|
+
import io
|
|
30
|
+
import keyword
|
|
31
|
+
import tokenize
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
_SKIP_PARAMS = {"self", "cls"}
|
|
35
|
+
_SIMPLE_LITERAL_TYPES = (str, int, float, bool, type(None))
|
|
36
|
+
_DEF_KINDS = ("ClassDef", "FunctionDef", "AsyncFunctionDef")
|
|
37
|
+
_BLOCK_KINDS = ("If", "For", "AsyncFor", "Try", "TryStar")
|
|
38
|
+
UNRESOLVED = object()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _k(node):
|
|
42
|
+
return type(node).__name__
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
46
|
+
# ║ Neutral data objects (what the worker emits; materialized on the main thread) ║
|
|
47
|
+
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
48
|
+
|
|
49
|
+
class NParse(dict):
|
|
50
|
+
def __init__(self, *args, source="", **kwargs):
|
|
51
|
+
super().__init__(*args, **kwargs)
|
|
52
|
+
self.source = source
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NGeneralParse(NParse):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NClassParse(NParse):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class NEnumParse(NClassParse):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class NFunctionParse(NParse):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class NCallParse(NParse):
|
|
72
|
+
def __init__(self, *args, func_name=None, **kwargs):
|
|
73
|
+
super().__init__(*args, **kwargs)
|
|
74
|
+
self.func_name = func_name
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class NDecorationParse(NCallParse):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class NConditional(dict):
|
|
82
|
+
def __init__(self, *args, condition=None, **kwargs):
|
|
83
|
+
super().__init__(*args, **kwargs)
|
|
84
|
+
self.condition = condition
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class NLoop(dict):
|
|
88
|
+
def __init__(self, *args, target=None, iter=None, **kwargs):
|
|
89
|
+
super().__init__(*args, **kwargs)
|
|
90
|
+
self.target = target
|
|
91
|
+
self.iter = iter
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class NTry(dict):
|
|
95
|
+
def __init__(self, *args, header=None, **kwargs):
|
|
96
|
+
super().__init__(*args, **kwargs)
|
|
97
|
+
self.header = header
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class NExcept(dict):
|
|
101
|
+
def __init__(self, *args, header=None, **kwargs):
|
|
102
|
+
super().__init__(*args, **kwargs)
|
|
103
|
+
self.header = header
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class NComment(str):
|
|
107
|
+
def __new__(cls, text, inline=None):
|
|
108
|
+
instance = super().__new__(cls, text)
|
|
109
|
+
instance.inline = inline
|
|
110
|
+
return instance
|
|
111
|
+
|
|
112
|
+
def __eq__(self, other):
|
|
113
|
+
return isinstance(other, NComment) and str(self) == str(other) and self.inline == other.inline
|
|
114
|
+
|
|
115
|
+
def __ne__(self, other):
|
|
116
|
+
return not self.__eq__(other)
|
|
117
|
+
|
|
118
|
+
def __hash__(self):
|
|
119
|
+
return hash(("__comment__", str(self), self.inline))
|
|
120
|
+
|
|
121
|
+
def __reduce__(self):
|
|
122
|
+
return (NComment, (str(self), self.inline))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class NCodeLine(str):
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class NameRef(str):
|
|
130
|
+
"""A dotted name the worker can't resolve (that needs the live src scope):
|
|
131
|
+
materialize turns it into the callable / enum member or a CodeLine."""
|
|
132
|
+
def __new__(cls, text, parts):
|
|
133
|
+
instance = super().__new__(cls, text)
|
|
134
|
+
instance.parts = list(parts)
|
|
135
|
+
return instance
|
|
136
|
+
|
|
137
|
+
def __reduce__(self):
|
|
138
|
+
return (NameRef, (str(self), self.parts))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class NNoDefault:
|
|
142
|
+
def __repr__(self):
|
|
143
|
+
return "NO_DEFAULT"
|
|
144
|
+
|
|
145
|
+
def __reduce__(self):
|
|
146
|
+
return (NNoDefault, ()) # constructor form: the main side maps it to the NO_DEFAULT singleton
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
N_NO_DEFAULT = NNoDefault()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class NSpan:
|
|
153
|
+
"""A span whose LINES are relative to a Base cell (the enclosing top-level
|
|
154
|
+
statement) — the main side maps it to core_syntax.RelSpan."""
|
|
155
|
+
__slots__ = ("base", "rel_start_line", "start_col", "rel_end_line", "end_col")
|
|
156
|
+
|
|
157
|
+
def __init__(self, base, rel_start_line, start_col, rel_end_line, end_col):
|
|
158
|
+
self.base = base
|
|
159
|
+
self.rel_start_line = rel_start_line
|
|
160
|
+
self.start_col = start_col
|
|
161
|
+
self.rel_end_line = rel_end_line
|
|
162
|
+
self.end_col = end_col
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def start_line(self):
|
|
166
|
+
return self.rel_start_line + self.base.line
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def end_line(self):
|
|
170
|
+
return self.rel_end_line + self.base.line
|
|
171
|
+
|
|
172
|
+
def __reduce__(self):
|
|
173
|
+
return (NSpan, (self.base, self.rel_start_line, self.start_col, self.rel_end_line, self.end_col))
|
|
174
|
+
|
|
175
|
+
def __repr__(self):
|
|
176
|
+
return f"Span({self.start_line}:{self.start_col}–{self.end_line}:{self.end_col})"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Types:
|
|
180
|
+
"""The output vocabulary of the Extractor. `resolve(parts)` → live object or
|
|
181
|
+
UNRESOLVED (None = defer as NameRef); `positional_names(parts)` → the
|
|
182
|
+
callee's positional parameter names or None (None hook = defer)."""
|
|
183
|
+
|
|
184
|
+
def __init__(self, *, GeneralParse, ClassParse, EnumParse, FunctionParse, CallParse,
|
|
185
|
+
DecorationParse, Comment, CodeLine, Conditional, Loop, Try, Except,
|
|
186
|
+
NO_DEFAULT, Span, resolve=None, positional_names=None):
|
|
187
|
+
self.GeneralParse = GeneralParse
|
|
188
|
+
self.ClassParse = ClassParse
|
|
189
|
+
self.EnumParse = EnumParse
|
|
190
|
+
self.FunctionParse = FunctionParse
|
|
191
|
+
self.CallParse = CallParse
|
|
192
|
+
self.DecorationParse = DecorationParse
|
|
193
|
+
self.Comment = Comment
|
|
194
|
+
self.CodeLine = CodeLine
|
|
195
|
+
self.Conditional = Conditional
|
|
196
|
+
self.Loop = Loop
|
|
197
|
+
self.Try = Try
|
|
198
|
+
self.Except = Except
|
|
199
|
+
self.NO_DEFAULT = NO_DEFAULT
|
|
200
|
+
self.Span = Span
|
|
201
|
+
self.resolve = resolve
|
|
202
|
+
self.positional_names = positional_names
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
NEUTRAL_TYPES = Types(
|
|
206
|
+
GeneralParse=NGeneralParse, ClassParse=NClassParse, EnumParse=NEnumParse,
|
|
207
|
+
FunctionParse=NFunctionParse, CallParse=NCallParse, DecorationParse=NDecorationParse,
|
|
208
|
+
Comment=NComment, CodeLine=NCodeLine, Conditional=NConditional, Loop=NLoop, Try=NTry,
|
|
209
|
+
Except=NExcept, NO_DEFAULT=N_NO_DEFAULT, Span=NSpan)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
213
|
+
# ║ Override comments & block keys (copies of libcst_conversion's helpers) ║
|
|
214
|
+
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
215
|
+
|
|
216
|
+
def parse_override_comment(text):
|
|
217
|
+
"""'# [k=v, ...]' → dict, else None. Same rules as libcst_conversion."""
|
|
218
|
+
if not isinstance(text, str):
|
|
219
|
+
return None
|
|
220
|
+
body = " ".join(ln.strip().lstrip("#").strip() for ln in text.split("\n")).strip()
|
|
221
|
+
if not (body.startswith("[") and body.endswith("]")):
|
|
222
|
+
return None
|
|
223
|
+
inner = body[1:-1].strip()
|
|
224
|
+
if not inner:
|
|
225
|
+
return None
|
|
226
|
+
try:
|
|
227
|
+
call = ast.parse(f"dict({inner})", mode="eval").body
|
|
228
|
+
if not isinstance(call, ast.Call) or call.args:
|
|
229
|
+
return None
|
|
230
|
+
parsed = {}
|
|
231
|
+
for kw in call.keywords:
|
|
232
|
+
if kw.arg is None:
|
|
233
|
+
return None
|
|
234
|
+
if kw.arg == "view_func" and isinstance(kw.value, (ast.Name, ast.Attribute)):
|
|
235
|
+
reference = ast.unparse(kw.value)
|
|
236
|
+
if not all(part.isidentifier() for part in reference.split(".")):
|
|
237
|
+
return None
|
|
238
|
+
parsed[kw.arg] = reference
|
|
239
|
+
else:
|
|
240
|
+
parsed[kw.arg] = ast.literal_eval(kw.value)
|
|
241
|
+
return parsed or None
|
|
242
|
+
except (SyntaxError, ValueError, TypeError):
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def occ_key(base, occ_counter):
|
|
247
|
+
n = occ_counter.get(base, 0)
|
|
248
|
+
occ_counter[base] = n + 1
|
|
249
|
+
return base if n == 0 else f"{base}##{n}"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
253
|
+
# ║ Source text ║
|
|
254
|
+
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
255
|
+
|
|
256
|
+
class _Src:
|
|
257
|
+
"""The parsed text with a line-start table. `ast` reports columns in UTF-8
|
|
258
|
+
BYTES; scanner nodes carry CHARACTER columns (`_charcols`); every span here
|
|
259
|
+
is in characters."""
|
|
260
|
+
|
|
261
|
+
__slots__ = ("text", "line_starts", "newline")
|
|
262
|
+
|
|
263
|
+
def __init__(self, text):
|
|
264
|
+
self.text = text
|
|
265
|
+
starts = [0]
|
|
266
|
+
find = text.find
|
|
267
|
+
i = find("\n")
|
|
268
|
+
while i != -1:
|
|
269
|
+
starts.append(i + 1)
|
|
270
|
+
i = find("\n", i + 1)
|
|
271
|
+
self.line_starts = starts
|
|
272
|
+
crlf = text.count("\r\n")
|
|
273
|
+
self.newline = "\r\n" if crlf and crlf * 2 > text.count("\n") else "\n"
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def spliced(cls, old, new_text, rs, re_, delta, region_src):
|
|
277
|
+
"""The line table of `new_text` from `old`'s, when only [rs, re_) was
|
|
278
|
+
replaced (by a region whose own table is `region_src`): lines before
|
|
279
|
+
stay, the region's are re-based, lines after shift by `delta`."""
|
|
280
|
+
self = cls.__new__(cls)
|
|
281
|
+
self.text = new_text
|
|
282
|
+
starts = old.line_starts
|
|
283
|
+
lo = bisect.bisect_right(starts, rs) # lines starting at or before rs stay (rs is a line start)
|
|
284
|
+
hi = bisect.bisect_left(starts, re_) # first line start at/after re_
|
|
285
|
+
region = [rs + x for x in region_src.line_starts[1:]]
|
|
286
|
+
self.line_starts = starts[:lo] + region + [x + delta for x in starts[hi:]]
|
|
287
|
+
self.newline = old.newline
|
|
288
|
+
return self
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def line_count(self):
|
|
292
|
+
return len(self.line_starts)
|
|
293
|
+
|
|
294
|
+
def line_start(self, lineno):
|
|
295
|
+
return self.line_starts[lineno - 1]
|
|
296
|
+
|
|
297
|
+
def next_line_start(self, lineno):
|
|
298
|
+
if lineno <= 0:
|
|
299
|
+
return 0
|
|
300
|
+
if lineno < len(self.line_starts):
|
|
301
|
+
return self.line_starts[lineno]
|
|
302
|
+
return len(self.text)
|
|
303
|
+
|
|
304
|
+
def line_end(self, lineno):
|
|
305
|
+
if lineno < len(self.line_starts):
|
|
306
|
+
e = self.line_starts[lineno] - 1
|
|
307
|
+
if e > 0 and self.text[e - 1] == "\r":
|
|
308
|
+
e -= 1
|
|
309
|
+
return e
|
|
310
|
+
return len(self.text)
|
|
311
|
+
|
|
312
|
+
def line_text(self, lineno):
|
|
313
|
+
return self.text[self.line_start(lineno):self.line_end(lineno)]
|
|
314
|
+
|
|
315
|
+
def offset(self, lineno, col_bytes):
|
|
316
|
+
start = self.line_start(lineno)
|
|
317
|
+
line = self.text[start:self.next_line_start(lineno)]
|
|
318
|
+
if line.isascii():
|
|
319
|
+
return start + col_bytes
|
|
320
|
+
return start + len(line.encode("utf-8")[:col_bytes].decode("utf-8", "ignore"))
|
|
321
|
+
|
|
322
|
+
def node_span(self, node):
|
|
323
|
+
if getattr(node, "_charcols", False):
|
|
324
|
+
return (self.line_starts[node.lineno - 1] + node.col_offset,
|
|
325
|
+
self.line_starts[node.end_lineno - 1] + node.end_col_offset)
|
|
326
|
+
return (self.offset(node.lineno, node.col_offset),
|
|
327
|
+
self.offset(node.end_lineno, node.end_col_offset))
|
|
328
|
+
|
|
329
|
+
def linecol(self, offset):
|
|
330
|
+
lineno = bisect.bisect_right(self.line_starts, offset)
|
|
331
|
+
return lineno, offset - self.line_starts[lineno - 1]
|
|
332
|
+
|
|
333
|
+
def indent_of_line(self, lineno):
|
|
334
|
+
line = self.line_text(lineno)
|
|
335
|
+
return line[:len(line) - len(line.lstrip())]
|
|
336
|
+
|
|
337
|
+
def is_blank(self, lineno):
|
|
338
|
+
return not self.line_text(lineno).strip()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def scan_comments(text):
|
|
342
|
+
"""{lineno: (col, text)} for standalone comment lines and for trailing
|
|
343
|
+
(same-line-as-code) comments — the `ast` front end's comment source."""
|
|
344
|
+
standalone, trailing = {}, {}
|
|
345
|
+
try:
|
|
346
|
+
for tok in tokenize.generate_tokens(io.StringIO(text).readline):
|
|
347
|
+
if tok.type != tokenize.COMMENT:
|
|
348
|
+
continue
|
|
349
|
+
line, col = tok.start
|
|
350
|
+
if tok.line[:col].strip() == "":
|
|
351
|
+
standalone[line] = (col, tok.string)
|
|
352
|
+
else:
|
|
353
|
+
trailing[line] = (col, tok.string)
|
|
354
|
+
except (tokenize.TokenError, IndentationError, SyntaxError):
|
|
355
|
+
pass
|
|
356
|
+
return standalone, trailing
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
360
|
+
# ║ Origin tables ║
|
|
361
|
+
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
362
|
+
|
|
363
|
+
class Base:
|
|
364
|
+
"""The anchor of one TOP-LEVEL statement: the char offset and 1-based line
|
|
365
|
+
where its extent starts. Every Item / Seq inside the statement stores its
|
|
366
|
+
positions RELATIVE to its Base, so an edit above the statement moves it
|
|
367
|
+
by touching this one cell — the incremental reparse shifts ~hundreds of
|
|
368
|
+
Bases instead of ~ten thousand items."""
|
|
369
|
+
__slots__ = ("offset", "line")
|
|
370
|
+
|
|
371
|
+
def __init__(self, offset=0, line=1):
|
|
372
|
+
self.offset = offset
|
|
373
|
+
self.line = line
|
|
374
|
+
|
|
375
|
+
def __reduce__(self):
|
|
376
|
+
return (Base, (self.offset, self.line))
|
|
377
|
+
|
|
378
|
+
def __repr__(self):
|
|
379
|
+
return f"Base({self.offset}@{self.line})"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
ZERO_BASE = Base(0, 1)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class Item:
|
|
386
|
+
"""One surfaced site. `extent` is what a delete removes (leading comment
|
|
387
|
+
lines and blank lines above, the statement, its trailing comment and
|
|
388
|
+
newline); `core` is what a reorder moves (extent minus the blank lines
|
|
389
|
+
above it); `value_span` is the editable expression, None when there is
|
|
390
|
+
none (a def, a block, a parameter without a default — `slot` then says
|
|
391
|
+
where a value would be inserted). Positions read and write ABSOLUTE
|
|
392
|
+
offsets; they are stored relative to `base`."""
|
|
393
|
+
__slots__ = ("base", "path", "key", "kind", "_extent", "_core", "_value_span", "orig", "indent",
|
|
394
|
+
"_slot", "_code_end", "seq", "comment_key", "shadowed")
|
|
395
|
+
|
|
396
|
+
def __init__(self, base, path, key, kind, extent, core, value_span, orig, indent="", slot=None,
|
|
397
|
+
code_end=None, seq=None, comment_key=None, shadowed=False):
|
|
398
|
+
self.base = base
|
|
399
|
+
self.path = path
|
|
400
|
+
self.key = key
|
|
401
|
+
self.kind = kind # value | param | kwarg | element | pair | def | block | header | comment | trailing | override | decorator | pseudo
|
|
402
|
+
self.extent = extent
|
|
403
|
+
self.core = core
|
|
404
|
+
self.value_span = value_span
|
|
405
|
+
self.orig = orig
|
|
406
|
+
self.indent = indent
|
|
407
|
+
self.slot = slot
|
|
408
|
+
self.code_end = code_end # end of the statement's code (trailing comments append here)
|
|
409
|
+
self.seq = seq
|
|
410
|
+
self.comment_key = comment_key # override items: the comment key that shares their span
|
|
411
|
+
self.shadowed = shadowed # a later statement re-bound this key: fixed in place, never diffed
|
|
412
|
+
|
|
413
|
+
@property
|
|
414
|
+
def extent(self):
|
|
415
|
+
o = self.base.offset
|
|
416
|
+
return (self._extent[0] + o, self._extent[1] + o)
|
|
417
|
+
|
|
418
|
+
@extent.setter
|
|
419
|
+
def extent(self, v):
|
|
420
|
+
o = self.base.offset
|
|
421
|
+
self._extent = (v[0] - o, v[1] - o)
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def core(self):
|
|
425
|
+
o = self.base.offset
|
|
426
|
+
return (self._core[0] + o, self._core[1] + o)
|
|
427
|
+
|
|
428
|
+
@core.setter
|
|
429
|
+
def core(self, v):
|
|
430
|
+
o = self.base.offset
|
|
431
|
+
self._core = (v[0] - o, v[1] - o)
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def value_span(self):
|
|
435
|
+
if self._value_span is None:
|
|
436
|
+
return None
|
|
437
|
+
o = self.base.offset
|
|
438
|
+
return (self._value_span[0] + o, self._value_span[1] + o)
|
|
439
|
+
|
|
440
|
+
@value_span.setter
|
|
441
|
+
def value_span(self, v):
|
|
442
|
+
if v is None:
|
|
443
|
+
self._value_span = None
|
|
444
|
+
else:
|
|
445
|
+
o = self.base.offset
|
|
446
|
+
self._value_span = (v[0] - o, v[1] - o)
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def slot(self):
|
|
450
|
+
return None if self._slot is None else self._slot + self.base.offset
|
|
451
|
+
|
|
452
|
+
@slot.setter
|
|
453
|
+
def slot(self, v):
|
|
454
|
+
self._slot = None if v is None else v - self.base.offset
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def code_end(self):
|
|
458
|
+
return None if self._code_end is None else self._code_end + self.base.offset
|
|
459
|
+
|
|
460
|
+
@code_end.setter
|
|
461
|
+
def code_end(self, v):
|
|
462
|
+
self._code_end = None if v is None else v - self.base.offset
|
|
463
|
+
|
|
464
|
+
def __reduce__(self):
|
|
465
|
+
return (Item, (self.base, self.path, self.key, self.kind, self.extent, self.core, self.value_span,
|
|
466
|
+
self.orig, self.indent, self.slot, self.code_end, self.seq, self.comment_key,
|
|
467
|
+
self.shadowed))
|
|
468
|
+
|
|
469
|
+
def __repr__(self):
|
|
470
|
+
return (f"Item({self.path!r}, {self.kind}, extent={self.extent}, value_span={self.value_span}"
|
|
471
|
+
f"{', shadowed' if self.shadowed else ''})")
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class Seq:
|
|
475
|
+
"""An ordered container of items in the source. `region` = [first core
|
|
476
|
+
start, last core end) (or the insertion point when empty); `insert_at` is
|
|
477
|
+
stored relative to `base`."""
|
|
478
|
+
__slots__ = ("id", "owner", "kind", "items", "base", "_insert_at", "indent", "sep")
|
|
479
|
+
|
|
480
|
+
def __init__(self, id, owner, kind, items=None, base=ZERO_BASE, insert_at=0, indent="", sep=", "):
|
|
481
|
+
self.id = id
|
|
482
|
+
self.owner = owner # node path whose keys this Seq holds
|
|
483
|
+
self.kind = kind # body | params | args | elements | pairs | decorators
|
|
484
|
+
self.items = items if items is not None else []
|
|
485
|
+
self.base = base
|
|
486
|
+
self.insert_at = insert_at
|
|
487
|
+
self.indent = indent
|
|
488
|
+
self.sep = sep
|
|
489
|
+
|
|
490
|
+
@property
|
|
491
|
+
def insert_at(self):
|
|
492
|
+
return self._insert_at + self.base.offset
|
|
493
|
+
|
|
494
|
+
@insert_at.setter
|
|
495
|
+
def insert_at(self, v):
|
|
496
|
+
self._insert_at = v - self.base.offset
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def region(self):
|
|
500
|
+
if self.items:
|
|
501
|
+
return (self.items[0].core[0], self.items[-1].core[1])
|
|
502
|
+
return (self.insert_at, self.insert_at)
|
|
503
|
+
|
|
504
|
+
def __reduce__(self):
|
|
505
|
+
return (Seq, (self.id, self.owner, self.kind, self.items, self.base, self.insert_at, self.indent, self.sep))
|
|
506
|
+
|
|
507
|
+
def __repr__(self):
|
|
508
|
+
return f"Seq(#{self.id} {self.kind} owner={self.owner!r} keys={[it.key for it in self.items]})"
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class Origin:
|
|
512
|
+
"""The residual: the parsed text plus the flat site tables."""
|
|
513
|
+
|
|
514
|
+
def __init__(self, text):
|
|
515
|
+
self.text = text
|
|
516
|
+
self.src = _Src(text)
|
|
517
|
+
self.items: dict[tuple, Item] = {}
|
|
518
|
+
self.seqs: dict[int, Seq] = {}
|
|
519
|
+
self.default_seq: dict[tuple, int] = {} # node path → seq new items of that node join
|
|
520
|
+
self.owned: dict[tuple, list] = {} # node path → seq ids whose deletes it answers
|
|
521
|
+
self.loose: dict[tuple, list] = {} # node path → items outside any Seq (comments, etc)
|
|
522
|
+
# Every TOP-LEVEL statement, surfaced or not, as (Base, relative extent
|
|
523
|
+
# end, key-or-None) in source order - the extents within the module body -
|
|
524
|
+
# the incremental reparse re-scans exactly the statements an edit touches
|
|
525
|
+
# and re-bases the rest. `top_extents()` gives these absolute.
|
|
526
|
+
self.top_stmts: list = []
|
|
527
|
+
self.generation = 0
|
|
528
|
+
self.file_path = None
|
|
529
|
+
self.line_offset = 0
|
|
530
|
+
self._next_seq_id = 0
|
|
531
|
+
|
|
532
|
+
def top_extents(self):
|
|
533
|
+
return [(b.offset, b.offset + rel_end, key) for b, rel_end, key in self.top_stmts]
|
|
534
|
+
|
|
535
|
+
def new_seq(self, owner, kind, *, base=ZERO_BASE, default=True, indent="", insert_at=0, sep=", "):
|
|
536
|
+
seq = Seq(self._next_seq_id, owner, kind, base=base, indent=indent, insert_at=insert_at, sep=sep)
|
|
537
|
+
self._next_seq_id += 1 # never len(seqs): ids must survive drop_seq
|
|
538
|
+
self.seqs[seq.id] = seq
|
|
539
|
+
self.owned.setdefault(owner, []).append(seq.id)
|
|
540
|
+
if default:
|
|
541
|
+
self.default_seq[owner] = seq.id
|
|
542
|
+
return seq
|
|
543
|
+
|
|
544
|
+
def drop_seq(self, seq):
|
|
545
|
+
self.seqs.pop(seq.id, None)
|
|
546
|
+
owned = self.owned.get(seq.owner)
|
|
547
|
+
if owned and seq.id in owned:
|
|
548
|
+
owned.remove(seq.id)
|
|
549
|
+
if owned is not None and not owned:
|
|
550
|
+
del self.owned[seq.owner]
|
|
551
|
+
if self.default_seq.get(seq.owner) == seq.id:
|
|
552
|
+
del self.default_seq[seq.owner]
|
|
553
|
+
|
|
554
|
+
def add(self, item, seq=None):
|
|
555
|
+
if seq is not None:
|
|
556
|
+
item.seq = seq.id
|
|
557
|
+
seq.items.append(item)
|
|
558
|
+
else:
|
|
559
|
+
self.loose.setdefault(item.path[:-1], []).append(item)
|
|
560
|
+
prev = self.items.get(item.path)
|
|
561
|
+
if prev is not None:
|
|
562
|
+
prev.shadowed = True
|
|
563
|
+
self.items[item.path] = item
|
|
564
|
+
return item
|
|
565
|
+
|
|
566
|
+
def shadow(self, path):
|
|
567
|
+
"""A key is being re-bound: the earlier binding stays a fixed slot in its
|
|
568
|
+
Seq (never diffed) and everything nested under it is unreachable from
|
|
569
|
+
the dict, so its tables go. Call BEFORE extracting the new value."""
|
|
570
|
+
prev = self.items.get(path)
|
|
571
|
+
if prev is None:
|
|
572
|
+
return
|
|
573
|
+
prev.shadowed = True
|
|
574
|
+
n = len(path)
|
|
575
|
+
for p in [p for p in self.items if len(p) > n and p[:n] == path]:
|
|
576
|
+
del self.items[p]
|
|
577
|
+
for sid in [sid for sid, sq in self.seqs.items() if len(sq.owner) >= n and sq.owner[:n] == path]:
|
|
578
|
+
self.drop_seq(self.seqs[sid])
|
|
579
|
+
for p in [p for p in self.loose if len(p) >= n and p[:n] == path]:
|
|
580
|
+
del self.loose[p]
|
|
581
|
+
|
|
582
|
+
def seal_seq(self, seq):
|
|
583
|
+
pass # `region` derives from the items / insert point
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
587
|
+
# ║ Scanner: tokenize → ast-shaped nodes ║
|
|
588
|
+
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
589
|
+
|
|
590
|
+
class _Node:
|
|
591
|
+
"""Base of the scanner's nodes: the `ast` field names, CHARACTER columns."""
|
|
592
|
+
__slots__ = ("lineno", "col_offset", "end_lineno", "end_col_offset")
|
|
593
|
+
_charcols = True
|
|
594
|
+
|
|
595
|
+
def _pos(self, first, last):
|
|
596
|
+
self.lineno, self.col_offset = first.start
|
|
597
|
+
self.end_lineno, self.end_col_offset = last.end
|
|
598
|
+
return self
|
|
599
|
+
|
|
600
|
+
def __repr__(self):
|
|
601
|
+
return f"<{_k(self)} {self.lineno}:{self.col_offset}-{self.end_lineno}:{self.end_col_offset}>"
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
class Module(_Node):
|
|
605
|
+
__slots__ = ("body",)
|
|
606
|
+
|
|
607
|
+
def __init__(self, body):
|
|
608
|
+
self.body = body
|
|
609
|
+
self.lineno = self.col_offset = 0
|
|
610
|
+
self.end_lineno = self.end_col_offset = 0
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
class Opaque(_Node):
|
|
614
|
+
"""A statement / expression the dict doesn't model. `body` (compound
|
|
615
|
+
statements) is consumed but never surfaced."""
|
|
616
|
+
__slots__ = ("body",)
|
|
617
|
+
|
|
618
|
+
def __init__(self, body=None):
|
|
619
|
+
self.body = body or []
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class alias(_Node):
|
|
623
|
+
"""`name [as asname]` of an import statement (ast.alias shape)."""
|
|
624
|
+
__slots__ = ("name", "asname")
|
|
625
|
+
|
|
626
|
+
def __init__(self, name, asname=None):
|
|
627
|
+
self.name = name
|
|
628
|
+
self.asname = asname
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class Import(_Node):
|
|
632
|
+
"""`import a.b [as c], d` — ast.Import shape. Surfaced for the file
|
|
633
|
+
import graph (view/playground/file_graph.py); the dict ignores it."""
|
|
634
|
+
__slots__ = ("names",)
|
|
635
|
+
|
|
636
|
+
def __init__(self, names):
|
|
637
|
+
self.names = names
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
class ImportFrom(_Node):
|
|
641
|
+
"""`from [..]mod import x [as y], (…)` / `import *` — ast.ImportFrom
|
|
642
|
+
shape: `module` None for a bare relative import, `level` = leading dots."""
|
|
643
|
+
__slots__ = ("module", "names", "level")
|
|
644
|
+
|
|
645
|
+
def __init__(self, module, names, level):
|
|
646
|
+
self.module = module
|
|
647
|
+
self.names = names
|
|
648
|
+
self.level = level
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class ClassDef(_Node):
|
|
652
|
+
__slots__ = ("name", "bases", "keywords", "body", "decorator_list")
|
|
653
|
+
|
|
654
|
+
def __init__(self, name, bases, keywords, body, decorator_list):
|
|
655
|
+
self.name, self.bases, self.keywords = name, bases, keywords
|
|
656
|
+
self.body, self.decorator_list = body, decorator_list
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class FunctionDef(_Node):
|
|
660
|
+
__slots__ = ("name", "args", "body", "decorator_list", "returns")
|
|
661
|
+
|
|
662
|
+
def __init__(self, name, args, body, decorator_list, returns=None):
|
|
663
|
+
self.name, self.args, self.body = name, args, body
|
|
664
|
+
self.decorator_list, self.returns = decorator_list, returns
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class AsyncFunctionDef(FunctionDef):
|
|
668
|
+
__slots__ = ()
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class arguments:
|
|
672
|
+
__slots__ = ("posonlyargs", "args", "vararg", "kwonlyargs", "kw_defaults", "kwarg", "defaults")
|
|
673
|
+
|
|
674
|
+
def __init__(self):
|
|
675
|
+
self.posonlyargs, self.args, self.kwonlyargs = [], [], []
|
|
676
|
+
self.kw_defaults, self.defaults = [], []
|
|
677
|
+
self.vararg = self.kwarg = None
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
class arg(_Node):
|
|
681
|
+
__slots__ = ("arg", "annotation")
|
|
682
|
+
|
|
683
|
+
def __init__(self, name, annotation=None):
|
|
684
|
+
self.arg, self.annotation = name, annotation
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
class Assign(_Node):
|
|
688
|
+
__slots__ = ("targets", "value")
|
|
689
|
+
|
|
690
|
+
def __init__(self, targets, value):
|
|
691
|
+
self.targets, self.value = targets, value
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class AnnAssign(_Node):
|
|
695
|
+
__slots__ = ("target", "annotation", "value")
|
|
696
|
+
|
|
697
|
+
def __init__(self, target, annotation, value):
|
|
698
|
+
self.target, self.annotation, self.value = target, annotation, value
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class Expr(_Node):
|
|
702
|
+
__slots__ = ("value",)
|
|
703
|
+
|
|
704
|
+
def __init__(self, value):
|
|
705
|
+
self.value = value
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
class If(_Node):
|
|
709
|
+
__slots__ = ("test", "body", "orelse")
|
|
710
|
+
|
|
711
|
+
def __init__(self, test, body, orelse):
|
|
712
|
+
self.test, self.body, self.orelse = test, body, orelse
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class For(_Node):
|
|
716
|
+
__slots__ = ("target", "iter", "body", "orelse")
|
|
717
|
+
|
|
718
|
+
def __init__(self, target, iter, body, orelse):
|
|
719
|
+
self.target, self.iter, self.body, self.orelse = target, iter, body, orelse
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
class AsyncFor(For):
|
|
723
|
+
__slots__ = ()
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
class Try(_Node):
|
|
727
|
+
__slots__ = ("body", "handlers", "orelse", "finalbody")
|
|
728
|
+
|
|
729
|
+
def __init__(self, body, handlers, orelse, finalbody):
|
|
730
|
+
self.body, self.handlers, self.orelse, self.finalbody = body, handlers, orelse, finalbody
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
class TryStar(Try):
|
|
734
|
+
__slots__ = ()
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
class ExceptHandler(_Node):
|
|
738
|
+
__slots__ = ("type", "name", "body")
|
|
739
|
+
|
|
740
|
+
def __init__(self, type, name, body):
|
|
741
|
+
self.type, self.name, self.body = type, name, body
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
# expressions
|
|
745
|
+
class Name(_Node):
|
|
746
|
+
__slots__ = ("id",)
|
|
747
|
+
|
|
748
|
+
def __init__(self, id):
|
|
749
|
+
self.id = id
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
class Attribute(_Node):
|
|
753
|
+
__slots__ = ("value", "attr")
|
|
754
|
+
|
|
755
|
+
def __init__(self, value, attr):
|
|
756
|
+
self.value, self.attr = value, attr
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class Constant(_Node):
|
|
760
|
+
__slots__ = ("value",)
|
|
761
|
+
|
|
762
|
+
def __init__(self, value):
|
|
763
|
+
self.value = value
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
class USub:
|
|
767
|
+
pass
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
class UAdd:
|
|
771
|
+
pass
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
class UnaryOp(_Node):
|
|
775
|
+
__slots__ = ("op", "operand")
|
|
776
|
+
|
|
777
|
+
def __init__(self, op, operand):
|
|
778
|
+
self.op, self.operand = op, operand
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
class Call(_Node):
|
|
782
|
+
__slots__ = ("func", "args", "keywords")
|
|
783
|
+
|
|
784
|
+
def __init__(self, func, args, keywords):
|
|
785
|
+
self.func, self.args, self.keywords = func, args, keywords
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
class keyword_(_Node):
|
|
789
|
+
__slots__ = ("arg", "value")
|
|
790
|
+
|
|
791
|
+
def __init__(self, arg, value):
|
|
792
|
+
self.arg, self.value = arg, value
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
keyword_.__name__ = "keyword"
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
class Starred(_Node):
|
|
799
|
+
__slots__ = ("value",)
|
|
800
|
+
|
|
801
|
+
def __init__(self, value):
|
|
802
|
+
self.value = value
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
class Tuple(_Node):
|
|
806
|
+
__slots__ = ("elts",)
|
|
807
|
+
|
|
808
|
+
def __init__(self, elts):
|
|
809
|
+
self.elts = elts
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
class List(_Node):
|
|
813
|
+
__slots__ = ("elts",)
|
|
814
|
+
|
|
815
|
+
def __init__(self, elts):
|
|
816
|
+
self.elts = elts
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
class Set(_Node):
|
|
820
|
+
__slots__ = ("elts",)
|
|
821
|
+
|
|
822
|
+
def __init__(self, elts):
|
|
823
|
+
self.elts = elts
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
class Dict(_Node):
|
|
827
|
+
__slots__ = ("keys", "values")
|
|
828
|
+
|
|
829
|
+
def __init__(self, keys, values):
|
|
830
|
+
self.keys, self.values = keys, values
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
class Subscript(_Node):
|
|
834
|
+
__slots__ = ("value", "slice")
|
|
835
|
+
|
|
836
|
+
def __init__(self, value, slice):
|
|
837
|
+
self.value, self.slice = value, slice
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
_OP, _NAME, _NUMBER, _STRING = tokenize.OP, tokenize.NAME, tokenize.NUMBER, tokenize.STRING
|
|
841
|
+
_NEWLINE, _INDENT, _DEDENT, _ENDMARKER = tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT, tokenize.ENDMARKER
|
|
842
|
+
_FSTRING_START = getattr(tokenize, "FSTRING_START", -1)
|
|
843
|
+
_FSTRING_END = getattr(tokenize, "FSTRING_END", -2)
|
|
844
|
+
_OPEN = {"(": ")", "[": "]", "{": "}"}
|
|
845
|
+
_CLOSE = {")", "]", "}"}
|
|
846
|
+
# The keywords that start a statement the dict never does (`return`,
|
|
847
|
+
# `import`, ...). Soft keywords (`match`, `case`, `type`, `_`) are allowed
|
|
848
|
+
# unless the header rule in compound_or_simple says otherwise.
|
|
849
|
+
_STMT_KEYWORDS = set(keyword.kwlist) - {"True", "False", "None", "lambda", "not", "await"}
|
|
850
|
+
_UNARY_NUM = {"-": USub, "+": UAdd}
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
class ScanError(SyntaxError):
|
|
854
|
+
pass
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def _number(s):
|
|
858
|
+
try:
|
|
859
|
+
if s[-1] in "jJ":
|
|
860
|
+
return complex(s)
|
|
861
|
+
return int(s, 0) if not any(c in s for c in ".eE") or s[:2].lower() == "0x" else float(s)
|
|
862
|
+
except ValueError:
|
|
863
|
+
try:
|
|
864
|
+
return float(s)
|
|
865
|
+
except ValueError:
|
|
866
|
+
return UNRESOLVED
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _string(tok):
|
|
870
|
+
"""The value of one STRING token (no f-strings here: those are FSTRING_*
|
|
871
|
+
tokens). Raw strings pass through; bytes decode their own escapes."""
|
|
872
|
+
i = 0
|
|
873
|
+
while i < len(tok) and tok[i] in "rRbBuU":
|
|
874
|
+
i += 1
|
|
875
|
+
prefix = tok[:i].lower()
|
|
876
|
+
body = tok[i:]
|
|
877
|
+
q = body[:3] if body[:3] in ('"""', "'''") else body[:1]
|
|
878
|
+
inner = body[len(q):-len(q)]
|
|
879
|
+
raw = "r" in prefix
|
|
880
|
+
if "b" in prefix:
|
|
881
|
+
if raw:
|
|
882
|
+
return inner.encode("latin-1", "backslashreplace")
|
|
883
|
+
return codecs.escape_decode(inner.encode("latin-1", "backslashreplace"))[0]
|
|
884
|
+
if raw or "\\" not in inner:
|
|
885
|
+
return inner
|
|
886
|
+
# unicode_escape reads latin-1 bytes; non-latin-1 chars are round-tripped as
|
|
887
|
+
# \\uXXXX, so they survive the trip.
|
|
888
|
+
return inner.encode("latin-1", "backslashreplace").decode("unicode_escape")
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _is_whole_fstring(tok):
|
|
892
|
+
"""Tokenizers before Python 3.12 give a whole f-string as ONE STRING token
|
|
893
|
+
(no FSTRING_* tokens): it is opaque code, never a constant."""
|
|
894
|
+
return tok.type == _STRING and "f" in tok.string[:tok.string.index(tok.string[-1])].lower()
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def _has_depth0(toks, string, tok_type=_OP):
|
|
898
|
+
"""Index of the first `string` token at bracket depth 0 (lambda parameter
|
|
899
|
+
lists excluded, their `=` / `:` are not the statement's), else None."""
|
|
900
|
+
depth = 0
|
|
901
|
+
lam = 0
|
|
902
|
+
for i, t in enumerate(toks):
|
|
903
|
+
if t.type == _OP:
|
|
904
|
+
if t.string in _OPEN:
|
|
905
|
+
depth += 1
|
|
906
|
+
elif t.string in _CLOSE:
|
|
907
|
+
depth -= 1
|
|
908
|
+
elif depth == 0:
|
|
909
|
+
if lam and t.string == ":":
|
|
910
|
+
lam -= 1
|
|
911
|
+
continue
|
|
912
|
+
if not lam and t.type == tok_type and t.string == string:
|
|
913
|
+
return i
|
|
914
|
+
elif t.type == _NAME and t.string == "lambda" and depth == 0:
|
|
915
|
+
lam += 1
|
|
916
|
+
elif depth == 0 and not lam and t.type == tok_type and t.string == string:
|
|
917
|
+
return i
|
|
918
|
+
return None
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _split_depth0(toks, string):
|
|
922
|
+
"""Split at depth-0 `string` tokens. A depth-0 `lambda … :` is one unit:
|
|
923
|
+
its parameter commas / `=` defaults never split (`f(lambda a, b=1: a)`)."""
|
|
924
|
+
parts, cur, depth, lam = [], [], 0, 0
|
|
925
|
+
for t in toks:
|
|
926
|
+
if t.type == _OP and t.string in _OPEN:
|
|
927
|
+
depth += 1
|
|
928
|
+
elif t.type == _OP and t.string in _CLOSE:
|
|
929
|
+
depth -= 1
|
|
930
|
+
elif depth == 0 and t.type == _NAME and t.string == "lambda":
|
|
931
|
+
lam += 1
|
|
932
|
+
elif depth == 0 and lam and t.type == _OP and t.string == ":":
|
|
933
|
+
lam -= 1
|
|
934
|
+
cur.append(t)
|
|
935
|
+
continue
|
|
936
|
+
if depth == 0 and not lam and t.type == _OP and t.string == string:
|
|
937
|
+
parts.append(cur)
|
|
938
|
+
cur = []
|
|
939
|
+
else:
|
|
940
|
+
cur.append(t)
|
|
941
|
+
parts.append(cur)
|
|
942
|
+
return parts
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _match(toks, i):
|
|
946
|
+
"""Index of the bracket closing toks[i], else None."""
|
|
947
|
+
depth = 0
|
|
948
|
+
for j in range(i, len(toks)):
|
|
949
|
+
t = toks[j]
|
|
950
|
+
if t.type == _OP:
|
|
951
|
+
if t.string in _OPEN:
|
|
952
|
+
depth += 1
|
|
953
|
+
elif t.string in _CLOSE:
|
|
954
|
+
depth -= 1
|
|
955
|
+
if depth == 0:
|
|
956
|
+
return j
|
|
957
|
+
return None
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
class _Parser:
|
|
961
|
+
def __init__(self, toks):
|
|
962
|
+
self.toks = toks
|
|
963
|
+
self.i = 0
|
|
964
|
+
|
|
965
|
+
def peek(self):
|
|
966
|
+
return self.toks[self.i]
|
|
967
|
+
|
|
968
|
+
def logical_line(self):
|
|
969
|
+
"""Tokens of the next logical line (NEWLINE consumed, not returned)."""
|
|
970
|
+
out = []
|
|
971
|
+
toks = self.toks
|
|
972
|
+
n = len(toks)
|
|
973
|
+
while self.i < n:
|
|
974
|
+
t = toks[self.i]
|
|
975
|
+
self.i += 1
|
|
976
|
+
if t.type == _NEWLINE:
|
|
977
|
+
break
|
|
978
|
+
if t.type in (_INDENT, _DEDENT, _ENDMARKER):
|
|
979
|
+
self.i -= 1
|
|
980
|
+
break
|
|
981
|
+
out.append(t)
|
|
982
|
+
return out
|
|
983
|
+
|
|
984
|
+
# ── blocks ───────────────────────────────────────────────────────────────
|
|
985
|
+
|
|
986
|
+
def block(self):
|
|
987
|
+
stmts = []
|
|
988
|
+
while True:
|
|
989
|
+
t = self.peek()
|
|
990
|
+
if t.type in (_DEDENT, _ENDMARKER):
|
|
991
|
+
return stmts
|
|
992
|
+
if t.type == _INDENT:
|
|
993
|
+
raise ScanError("unexpected indent", ("<text>", t.start[0], t.start[1] + 1, t.line))
|
|
994
|
+
stmts.extend(self.statement())
|
|
995
|
+
|
|
996
|
+
def indented_block(self, after_tok):
|
|
997
|
+
t = self.peek()
|
|
998
|
+
if t.type != _INDENT:
|
|
999
|
+
raise ScanError("expected an indented block", ("<text>", after_tok.end[0], after_tok.end[1] + 1, after_tok.line))
|
|
1000
|
+
self.i += 1
|
|
1001
|
+
stmts = self.block()
|
|
1002
|
+
if self.peek().type == _DEDENT:
|
|
1003
|
+
self.i += 1
|
|
1004
|
+
return stmts
|
|
1005
|
+
|
|
1006
|
+
def body_after(self, toks, colon):
|
|
1007
|
+
"""The body of a compound header: inline statements after the colon, or
|
|
1008
|
+
the indented block that follows."""
|
|
1009
|
+
rest = toks[colon + 1:]
|
|
1010
|
+
if rest:
|
|
1011
|
+
return self.simple_statements(rest)
|
|
1012
|
+
return self.indented_block(toks[colon])
|
|
1013
|
+
|
|
1014
|
+
# ── statements ───────────────────────────────────────────────────────────
|
|
1015
|
+
|
|
1016
|
+
def statement(self):
|
|
1017
|
+
toks = self.logical_line()
|
|
1018
|
+
if not toks:
|
|
1019
|
+
return []
|
|
1020
|
+
t0 = toks[0]
|
|
1021
|
+
if t0.type == _OP and t0.string == "@":
|
|
1022
|
+
decorators = [self.expr(toks[1:], t0)]
|
|
1023
|
+
while True:
|
|
1024
|
+
toks = self.logical_line()
|
|
1025
|
+
if toks and toks[0].type == _OP and toks[0].string == "@":
|
|
1026
|
+
decorators.append(self.expr(toks[1:], toks[0]))
|
|
1027
|
+
continue
|
|
1028
|
+
break
|
|
1029
|
+
stmts = self.compound_or_simple(toks)
|
|
1030
|
+
if stmts and _k(stmts[0]) in _DEF_KINDS:
|
|
1031
|
+
stmts[0].decorator_list = decorators
|
|
1032
|
+
return stmts
|
|
1033
|
+
return self.compound_or_simple(toks)
|
|
1034
|
+
|
|
1035
|
+
def compound_or_simple(self, toks):
|
|
1036
|
+
t0 = toks[0]
|
|
1037
|
+
if t0.type != _NAME:
|
|
1038
|
+
return self.simple_statements(toks)
|
|
1039
|
+
kw = t0.string
|
|
1040
|
+
if kw == "async" and len(toks) > 1 and toks[1].type == _NAME and toks[1].string in ("def", "for", "with"):
|
|
1041
|
+
kw = "async " + toks[1].string
|
|
1042
|
+
if kw in ("def", "async def"):
|
|
1043
|
+
return [self.funcdef(toks, kw == "async def")]
|
|
1044
|
+
if kw == "class":
|
|
1045
|
+
return [self.classdef(toks)]
|
|
1046
|
+
if kw == "if":
|
|
1047
|
+
return [self.if_stmt(toks)]
|
|
1048
|
+
if kw in ("for", "async for"):
|
|
1049
|
+
return [self.for_stmt(toks, kw == "async for")]
|
|
1050
|
+
if kw == "try":
|
|
1051
|
+
return [self.try_stmt(toks)]
|
|
1052
|
+
if kw in ("while", "with", "async with"):
|
|
1053
|
+
return [self.opaque_compound(toks)]
|
|
1054
|
+
if kw in ("match", "case") and toks[-1].type == _OP and toks[-1].string == ":" and self.peek().type == _INDENT:
|
|
1055
|
+
return [self.opaque_compound(toks)]
|
|
1056
|
+
return self.simple_statements(toks)
|
|
1057
|
+
|
|
1058
|
+
def header_colon(self, toks, start=1):
|
|
1059
|
+
c = _has_depth0(toks[start:], ":")
|
|
1060
|
+
if c is None:
|
|
1061
|
+
raise ScanError("expected ':'", ("<text>", toks[0].start[0], toks[0].start[1] + 1, toks[0].line))
|
|
1062
|
+
return c + start
|
|
1063
|
+
|
|
1064
|
+
def opaque_compound(self, toks):
|
|
1065
|
+
colon = self.header_colon(toks, 0)
|
|
1066
|
+
body = self.body_after(toks, colon)
|
|
1067
|
+
node = Opaque(body)
|
|
1068
|
+
last = self._last_tok(body) or toks[-1]
|
|
1069
|
+
node._pos(toks[0], last)
|
|
1070
|
+
# while/for-else on an opaque loop: consume the else block too.
|
|
1071
|
+
if toks[0].string == "while" and self.peek().type == _NAME and self.peek().string == "else":
|
|
1072
|
+
etoks = self.logical_line()
|
|
1073
|
+
ebody = self.body_after(etoks, self.header_colon(etoks, 0))
|
|
1074
|
+
last = self._last_tok(ebody) or etoks[-1]
|
|
1075
|
+
node.end_lineno, node.end_col_offset = last.end
|
|
1076
|
+
return node
|
|
1077
|
+
|
|
1078
|
+
@staticmethod
|
|
1079
|
+
def _last_tok(stmts):
|
|
1080
|
+
"""A pseudo token for the end of the last statement of a body."""
|
|
1081
|
+
if not stmts:
|
|
1082
|
+
return None
|
|
1083
|
+
s = stmts[-1]
|
|
1084
|
+
return _EndTok(s.end_lineno, s.end_col_offset)
|
|
1085
|
+
|
|
1086
|
+
def funcdef(self, toks, is_async):
|
|
1087
|
+
start = toks[0]
|
|
1088
|
+
j = 1 if not is_async else 2
|
|
1089
|
+
name_tok = toks[j]
|
|
1090
|
+
open_idx = j + 1
|
|
1091
|
+
close = _match(toks, open_idx)
|
|
1092
|
+
if close is None or toks[open_idx].string != "(":
|
|
1093
|
+
raise ScanError("bad def", ("<text>", start.start[0], start.start[1] + 1, start.line))
|
|
1094
|
+
args = self.params(toks[open_idx + 1:close])
|
|
1095
|
+
colon = self.header_colon(toks, close + 1)
|
|
1096
|
+
returns = self.expr(toks[close + 2:colon], toks[close]) if colon > close + 2 else None
|
|
1097
|
+
body = self.body_after(toks, colon)
|
|
1098
|
+
node = (AsyncFunctionDef if is_async else FunctionDef)(name_tok.string, args, body, [], returns)
|
|
1099
|
+
node._pos(start, self._last_tok(body) or toks[colon])
|
|
1100
|
+
return node
|
|
1101
|
+
|
|
1102
|
+
def classdef(self, toks):
|
|
1103
|
+
start = toks[0]
|
|
1104
|
+
name_tok = toks[1]
|
|
1105
|
+
bases, keywords = [], []
|
|
1106
|
+
colon = self.header_colon(toks, 2)
|
|
1107
|
+
if colon > 2 and toks[2].type == _OP and toks[2].string == "(":
|
|
1108
|
+
close = _match(toks, 2)
|
|
1109
|
+
for part in _split_depth0(toks[3:close], ","):
|
|
1110
|
+
if not part:
|
|
1111
|
+
continue
|
|
1112
|
+
eq = _has_depth0(part, "=")
|
|
1113
|
+
if eq is not None and eq == 1 and part[0].type == _NAME:
|
|
1114
|
+
keywords.append(keyword_(part[0].string, self.expr(part[2:], part[1]))._pos(part[0], part[-1]))
|
|
1115
|
+
else:
|
|
1116
|
+
bases.append(self.expr(part, part[0]))
|
|
1117
|
+
body = self.body_after(toks, colon)
|
|
1118
|
+
node = ClassDef(name_tok.string, bases, keywords, body, [])
|
|
1119
|
+
node._pos(start, self._last_tok(body) or toks[colon])
|
|
1120
|
+
return node
|
|
1121
|
+
|
|
1122
|
+
def if_stmt(self, toks):
|
|
1123
|
+
start = toks[0]
|
|
1124
|
+
colon = self.header_colon(toks)
|
|
1125
|
+
test = self.expr(toks[1:colon], toks[0])
|
|
1126
|
+
body = self.body_after(toks, colon)
|
|
1127
|
+
node = If(test, body, [])
|
|
1128
|
+
node._pos(start, self._last_tok(body) or toks[colon])
|
|
1129
|
+
nxt = self.peek()
|
|
1130
|
+
if nxt.type == _NAME and nxt.string == "elif":
|
|
1131
|
+
etoks = self.logical_line()
|
|
1132
|
+
sub = self.if_stmt(etoks)
|
|
1133
|
+
node.orelse = [sub]
|
|
1134
|
+
node.end_lineno, node.end_col_offset = sub.end_lineno, sub.end_col_offset
|
|
1135
|
+
elif nxt.type == _NAME and nxt.string == "else":
|
|
1136
|
+
etoks = self.logical_line()
|
|
1137
|
+
node.orelse = self.body_after(etoks, self.header_colon(etoks, 0))
|
|
1138
|
+
last = self._last_tok(node.orelse) or etoks[-1]
|
|
1139
|
+
node.end_lineno, node.end_col_offset = last.end
|
|
1140
|
+
return node
|
|
1141
|
+
|
|
1142
|
+
def for_stmt(self, toks, is_async):
|
|
1143
|
+
start = toks[0]
|
|
1144
|
+
j = 2 if is_async else 1
|
|
1145
|
+
colon = self.header_colon(toks, j)
|
|
1146
|
+
in_idx = _has_depth0(toks[j:colon], "in", _NAME)
|
|
1147
|
+
if in_idx is None:
|
|
1148
|
+
raise ScanError("bad for", ("<text>", start.start[0], start.start[1] + 1, start.line))
|
|
1149
|
+
in_idx += j
|
|
1150
|
+
target = self.expr(toks[j:in_idx], toks[j - 1])
|
|
1151
|
+
it = self.expr(toks[in_idx + 1:colon], toks[in_idx])
|
|
1152
|
+
body = self.body_after(toks, colon)
|
|
1153
|
+
node = (AsyncFor if is_async else For)(target, it, body, [])
|
|
1154
|
+
node._pos(start, self._last_tok(body) or toks[colon])
|
|
1155
|
+
nxt = self.peek()
|
|
1156
|
+
if nxt.type == _NAME and nxt.string == "else":
|
|
1157
|
+
etoks = self.logical_line()
|
|
1158
|
+
node.orelse = self.body_after(etoks, self.header_colon(etoks, 0))
|
|
1159
|
+
last = self._last_tok(node.orelse) or etoks[-1]
|
|
1160
|
+
node.end_lineno, node.end_col_offset = last.end
|
|
1161
|
+
return node
|
|
1162
|
+
|
|
1163
|
+
def try_stmt(self, toks):
|
|
1164
|
+
start = toks[0]
|
|
1165
|
+
body = self.body_after(toks, self.header_colon(toks, 0))
|
|
1166
|
+
handlers, orelse, finalbody = [], [], []
|
|
1167
|
+
star = False
|
|
1168
|
+
last = self._last_tok(body) or toks[-1]
|
|
1169
|
+
while True:
|
|
1170
|
+
nxt = self.peek()
|
|
1171
|
+
if nxt.type != _NAME or nxt.string not in ("except", "else", "finally"):
|
|
1172
|
+
break
|
|
1173
|
+
htoks = self.logical_line()
|
|
1174
|
+
colon = self.header_colon(htoks, 0)
|
|
1175
|
+
if htoks[0].string == "except":
|
|
1176
|
+
j = 1
|
|
1177
|
+
if j < colon and htoks[j].type == _OP and htoks[j].string == "*":
|
|
1178
|
+
star = True
|
|
1179
|
+
j += 1
|
|
1180
|
+
as_idx = _has_depth0(htoks[j:colon], "as", _NAME)
|
|
1181
|
+
typ = name = None
|
|
1182
|
+
if as_idx is not None:
|
|
1183
|
+
as_idx += j
|
|
1184
|
+
typ = self.expr(htoks[j:as_idx], htoks[j - 1]) if as_idx > j else None
|
|
1185
|
+
name = htoks[as_idx + 1].string
|
|
1186
|
+
elif colon > j:
|
|
1187
|
+
typ = self.expr(htoks[j:colon], htoks[j - 1])
|
|
1188
|
+
hbody = self.body_after(htoks, colon)
|
|
1189
|
+
h = ExceptHandler(typ, name, hbody)
|
|
1190
|
+
h._pos(htoks[0], self._last_tok(hbody) or htoks[colon])
|
|
1191
|
+
handlers.append(h)
|
|
1192
|
+
last = self._last_tok(hbody) or htoks[colon]
|
|
1193
|
+
elif htoks[0].string == "else":
|
|
1194
|
+
orelse = self.body_after(htoks, colon)
|
|
1195
|
+
last = self._last_tok(orelse) or htoks[colon]
|
|
1196
|
+
else:
|
|
1197
|
+
finalbody = self.body_after(htoks, colon)
|
|
1198
|
+
last = self._last_tok(finalbody) or htoks[colon]
|
|
1199
|
+
node = (TryStar if star else Try)(body, handlers, orelse, finalbody)
|
|
1200
|
+
node._pos(start, last)
|
|
1201
|
+
return node
|
|
1202
|
+
|
|
1203
|
+
def params(self, toks):
|
|
1204
|
+
a = arguments()
|
|
1205
|
+
posonly_seen = False
|
|
1206
|
+
kwonly = False
|
|
1207
|
+
for part in _split_depth0(toks, ","):
|
|
1208
|
+
if not part:
|
|
1209
|
+
continue
|
|
1210
|
+
t0 = part[0]
|
|
1211
|
+
if t0.type == _OP and t0.string == "/":
|
|
1212
|
+
a.posonlyargs, a.args = a.args, []
|
|
1213
|
+
posonly_seen = True
|
|
1214
|
+
continue
|
|
1215
|
+
if t0.type == _OP and t0.string == "*":
|
|
1216
|
+
if len(part) == 1:
|
|
1217
|
+
kwonly = True
|
|
1218
|
+
else:
|
|
1219
|
+
a.vararg = self._arg(part[1:])
|
|
1220
|
+
kwonly = True
|
|
1221
|
+
continue
|
|
1222
|
+
if t0.type == _OP and t0.string == "**":
|
|
1223
|
+
a.kwarg = self._arg(part[1:])
|
|
1224
|
+
continue
|
|
1225
|
+
eq = _has_depth0(part, "=")
|
|
1226
|
+
if eq is None:
|
|
1227
|
+
p = self._arg(part)
|
|
1228
|
+
default = None
|
|
1229
|
+
else:
|
|
1230
|
+
p = self._arg(part[:eq])
|
|
1231
|
+
default = self.expr(part[eq + 1:], part[eq])
|
|
1232
|
+
if kwonly:
|
|
1233
|
+
a.kwonlyargs.append(p)
|
|
1234
|
+
a.kw_defaults.append(default)
|
|
1235
|
+
else:
|
|
1236
|
+
a.args.append(p)
|
|
1237
|
+
if default is not None:
|
|
1238
|
+
a.defaults.append(default)
|
|
1239
|
+
return a
|
|
1240
|
+
|
|
1241
|
+
def _arg(self, toks):
|
|
1242
|
+
name = toks[0]
|
|
1243
|
+
colon = _has_depth0(toks, ":")
|
|
1244
|
+
ann = self.expr(toks[colon + 1:], toks[colon]) if colon is not None and colon + 1 < len(toks) else None
|
|
1245
|
+
node = arg(name.string, ann)
|
|
1246
|
+
node._pos(name, toks[-1] if ann is not None else name)
|
|
1247
|
+
return node
|
|
1248
|
+
|
|
1249
|
+
def simple_statements(self, toks):
|
|
1250
|
+
out = []
|
|
1251
|
+
for part in _split_depth0(toks, ";"):
|
|
1252
|
+
if part:
|
|
1253
|
+
out.append(self.simple(part))
|
|
1254
|
+
return out
|
|
1255
|
+
|
|
1256
|
+
def simple(self, toks):
|
|
1257
|
+
t0 = toks[0]
|
|
1258
|
+
if t0.type == _NAME and t0.string in ("import", "from"):
|
|
1259
|
+
node = self.import_stmt(toks)
|
|
1260
|
+
if node is not None:
|
|
1261
|
+
return node
|
|
1262
|
+
if t0.type == _NAME and t0.string in _STMT_KEYWORDS:
|
|
1263
|
+
return Opaque()._pos(t0, toks[-1])
|
|
1264
|
+
eq = _has_depth0(toks, "=")
|
|
1265
|
+
if eq is None:
|
|
1266
|
+
colon = _has_depth0(toks, ":")
|
|
1267
|
+
if colon == 1 and t0.type == _NAME:
|
|
1268
|
+
node = AnnAssign(Name(t0.string)._pos(t0, t0), self.expr(toks[2:], toks[1]), None)
|
|
1269
|
+
return node._pos(t0, toks[-1])
|
|
1270
|
+
return Expr(self.expr(toks, t0))._pos(t0, toks[-1])
|
|
1271
|
+
lhs, rhs = toks[:eq], toks[eq + 1:]
|
|
1272
|
+
if not lhs or not rhs:
|
|
1273
|
+
return Opaque()._pos(t0, toks[-1])
|
|
1274
|
+
eq2 = _has_depth0(rhs, "=")
|
|
1275
|
+
if eq2 is not None:
|
|
1276
|
+
# chained assignment: two targets → never surfaced, like ast's Assign with 2+ targets
|
|
1277
|
+
value = self.expr(rhs[eq2 + 1:], rhs[eq2])
|
|
1278
|
+
node = Assign([self.target(lhs), self.target(rhs[:eq2])], value)
|
|
1279
|
+
return node._pos(t0, toks[-1])
|
|
1280
|
+
colon = _has_depth0(lhs, ":")
|
|
1281
|
+
if colon is not None:
|
|
1282
|
+
node = AnnAssign(self.target(lhs[:colon]), self.expr(lhs[colon + 1:], lhs[colon]), self.expr(rhs, toks[eq]))
|
|
1283
|
+
return node._pos(t0, toks[-1])
|
|
1284
|
+
node = Assign([self.target(lhs)], self.expr(rhs, toks[eq]))
|
|
1285
|
+
return node._pos(t0, toks[-1])
|
|
1286
|
+
|
|
1287
|
+
# ── imports ──────────────────────────────────────────────────────────────
|
|
1288
|
+
|
|
1289
|
+
def import_stmt(self, toks):
|
|
1290
|
+
return parse_import(toks)
|
|
1291
|
+
|
|
1292
|
+
@staticmethod
|
|
1293
|
+
def _import_aliases(toks):
|
|
1294
|
+
return _import_aliases(toks)
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def target(self, toks):
|
|
1298
|
+
if len(toks) == 1 and toks[0].type == _NAME:
|
|
1299
|
+
return Name(toks[0].string)._pos(toks[0], toks[0])
|
|
1300
|
+
chain = self.postfix(toks)
|
|
1301
|
+
if chain is not None and _k(chain) in ("Attribute", "Subscript"):
|
|
1302
|
+
return chain
|
|
1303
|
+
return Opaque()._pos(toks[0], toks[-1])
|
|
1304
|
+
|
|
1305
|
+
# ── expressions ──────────────────────────────────────────────────────────
|
|
1306
|
+
|
|
1307
|
+
def expr(self, toks, anchor):
|
|
1308
|
+
if not toks:
|
|
1309
|
+
return Opaque()._pos(anchor, anchor)
|
|
1310
|
+
t0, tl = toks[0], toks[-1]
|
|
1311
|
+
n = len(toks)
|
|
1312
|
+
if n == 1:
|
|
1313
|
+
if t0.type == _NUMBER:
|
|
1314
|
+
v = _number(t0.string)
|
|
1315
|
+
return (Constant(v) if v is not UNRESOLVED else Opaque())._pos(t0, t0)
|
|
1316
|
+
if t0.type == _STRING:
|
|
1317
|
+
if _is_whole_fstring(t0):
|
|
1318
|
+
return Opaque()._pos(t0, t0)
|
|
1319
|
+
return Constant(_string(t0.string))._pos(t0, t0)
|
|
1320
|
+
if t0.type == _NAME:
|
|
1321
|
+
if t0.string in ("True", "False", "None"):
|
|
1322
|
+
return Constant({"True": True, "False": False, "None": None}[t0.string])._pos(t0, t0)
|
|
1323
|
+
if t0.string not in _STMT_KEYWORDS and t0.string not in ("lambda", "not", "await"):
|
|
1324
|
+
return Name(t0.string)._pos(t0, t0)
|
|
1325
|
+
return Opaque()._pos(t0, t0)
|
|
1326
|
+
if t0.type == _OP and t0.string == "...":
|
|
1327
|
+
return Constant(Ellipsis)._pos(t0, t0)
|
|
1328
|
+
return Opaque()._pos(t0, t0)
|
|
1329
|
+
if n == 2 and t0.type == _OP and t0.string in _UNARY_NUM and tl.type == _NUMBER:
|
|
1330
|
+
v = _number(tl.string)
|
|
1331
|
+
if v is UNRESOLVED:
|
|
1332
|
+
return Opaque()._pos(t0, tl)
|
|
1333
|
+
return UnaryOp(_UNARY_NUM[t0.string](), Constant(v)._pos(tl, tl))._pos(t0, tl)
|
|
1334
|
+
if all(t.type == _STRING for t in toks):
|
|
1335
|
+
if any(_is_whole_fstring(t) for t in toks):
|
|
1336
|
+
return Opaque()._pos(t0, tl)
|
|
1337
|
+
try:
|
|
1338
|
+
parts = [_string(t.string) for t in toks]
|
|
1339
|
+
if all(isinstance(p, str) for p in parts) or all(isinstance(p, bytes) for p in parts):
|
|
1340
|
+
return Constant(parts[0][:0].join(parts))._pos(t0, tl)
|
|
1341
|
+
except (UnicodeDecodeError, ValueError):
|
|
1342
|
+
pass
|
|
1343
|
+
return Opaque()._pos(t0, tl)
|
|
1344
|
+
if _has_depth0(toks, ",") is not None and not (t0.type == _OP and t0.string in _OPEN and _match(toks, 0) == n - 1):
|
|
1345
|
+
elts = self.elements(_split_depth0(toks, ","))
|
|
1346
|
+
if elts is None:
|
|
1347
|
+
return Opaque()._pos(t0, tl)
|
|
1348
|
+
return Tuple(elts)._pos(t0, tl)
|
|
1349
|
+
chain = self.postfix(toks)
|
|
1350
|
+
if chain is not None:
|
|
1351
|
+
return chain
|
|
1352
|
+
return Opaque()._pos(t0, tl)
|
|
1353
|
+
|
|
1354
|
+
def elements(self, parts):
|
|
1355
|
+
"""Element nodes of a display; None when a part is a comprehension /
|
|
1356
|
+
yield (the whole display then stays opaque)."""
|
|
1357
|
+
out = []
|
|
1358
|
+
for part in parts:
|
|
1359
|
+
if not part:
|
|
1360
|
+
continue
|
|
1361
|
+
if any(t.type == _NAME and t.string in ("for", "yield") for t in part
|
|
1362
|
+
if _has_depth0([t], t.string, _NAME) is not None):
|
|
1363
|
+
pass
|
|
1364
|
+
if _has_depth0(part, "for", _NAME) is not None or _has_depth0(part, "yield", _NAME) is not None:
|
|
1365
|
+
return None
|
|
1366
|
+
if part[0].type == _OP and part[0].string == "*":
|
|
1367
|
+
out.append(Starred(self.expr(part[1:], part[0]))._pos(part[0], part[-1]))
|
|
1368
|
+
else:
|
|
1369
|
+
out.append(self.expr(part, part[0]))
|
|
1370
|
+
return out
|
|
1371
|
+
|
|
1372
|
+
def atom(self, toks):
|
|
1373
|
+
"""(node, next index) for the atom at toks[0], or None."""
|
|
1374
|
+
t0 = toks[0]
|
|
1375
|
+
if t0.type == _OP and t0.string in _OPEN:
|
|
1376
|
+
j = _match(toks, 0)
|
|
1377
|
+
if j is None:
|
|
1378
|
+
return None
|
|
1379
|
+
inner = toks[1:j]
|
|
1380
|
+
node = self.display(t0.string, inner, t0, toks[j])
|
|
1381
|
+
return (node, j + 1) if node is not None else None
|
|
1382
|
+
if t0.type == _NAME:
|
|
1383
|
+
if t0.string in ("True", "False", "None"):
|
|
1384
|
+
return Constant({"True": True, "False": False, "None": None}[t0.string])._pos(t0, t0), 1
|
|
1385
|
+
if t0.string in _STMT_KEYWORDS or t0.string in ("lambda", "not", "await"):
|
|
1386
|
+
return None
|
|
1387
|
+
return Name(t0.string)._pos(t0, t0), 1
|
|
1388
|
+
if t0.type == _NUMBER:
|
|
1389
|
+
v = _number(t0.string)
|
|
1390
|
+
return (Constant(v)._pos(t0, t0), 1) if v is not UNRESOLVED else None
|
|
1391
|
+
if t0.type == _STRING or t0.type == _FSTRING_START:
|
|
1392
|
+
# A run of string parts: plain STRING tokens and/or whole f-strings
|
|
1393
|
+
# (FSTRING_START ... FSTRING_END, which nest). All plain → Constant;
|
|
1394
|
+
# any f-string → an opaque JoinedStr with the run's span.
|
|
1395
|
+
k, depth, fstr = 0, 0, False
|
|
1396
|
+
n = len(toks)
|
|
1397
|
+
while k < n:
|
|
1398
|
+
t = toks[k]
|
|
1399
|
+
if t.type == _FSTRING_START:
|
|
1400
|
+
depth += 1
|
|
1401
|
+
fstr = True
|
|
1402
|
+
elif t.type == _FSTRING_END:
|
|
1403
|
+
depth -= 1
|
|
1404
|
+
elif depth == 0 and t.type != _STRING:
|
|
1405
|
+
break
|
|
1406
|
+
elif depth == 0 and _is_whole_fstring(t):
|
|
1407
|
+
fstr = True
|
|
1408
|
+
k += 1
|
|
1409
|
+
if depth != 0:
|
|
1410
|
+
return None
|
|
1411
|
+
if fstr:
|
|
1412
|
+
return Opaque()._pos(t0, toks[k - 1]), k
|
|
1413
|
+
node = self.expr(toks[:k], t0)
|
|
1414
|
+
return (node, k) if _k(node) == "Constant" else None
|
|
1415
|
+
return None
|
|
1416
|
+
|
|
1417
|
+
def display(self, opener, inner, open_tok, close_tok):
|
|
1418
|
+
if opener == "(":
|
|
1419
|
+
if not inner:
|
|
1420
|
+
return Tuple([])._pos(open_tok, close_tok)
|
|
1421
|
+
if _has_depth0(inner, ",") is not None:
|
|
1422
|
+
if _has_depth0(inner, "for", _NAME) is not None or _has_depth0(inner, "yield", _NAME) is not None:
|
|
1423
|
+
return Opaque()._pos(open_tok, close_tok)
|
|
1424
|
+
elts = self.elements(_split_depth0(inner, ","))
|
|
1425
|
+
if elts is None:
|
|
1426
|
+
return Opaque()._pos(open_tok, close_tok)
|
|
1427
|
+
return Tuple(elts)._pos(open_tok, close_tok)
|
|
1428
|
+
if _has_depth0(inner, "for", _NAME) is not None or _has_depth0(inner, "yield", _NAME) is not None:
|
|
1429
|
+
return Opaque()._pos(open_tok, close_tok)
|
|
1430
|
+
return self.expr(inner, open_tok) # (x): the inner node, its original span
|
|
1431
|
+
if opener == "[":
|
|
1432
|
+
if _has_depth0(inner, "for", _NAME) is not None:
|
|
1433
|
+
return Opaque()._pos(open_tok, close_tok)
|
|
1434
|
+
elts = self.elements(_split_depth0(inner, ",")) if inner else []
|
|
1435
|
+
if elts is None:
|
|
1436
|
+
return Opaque()._pos(open_tok, close_tok)
|
|
1437
|
+
return List(elts)._pos(open_tok, close_tok)
|
|
1438
|
+
# {
|
|
1439
|
+
if not inner:
|
|
1440
|
+
return Dict([], [])._pos(open_tok, close_tok)
|
|
1441
|
+
if _has_depth0(inner, "for", _NAME) is not None:
|
|
1442
|
+
return Opaque()._pos(open_tok, close_tok)
|
|
1443
|
+
parts = [p for p in _split_depth0(inner, ",") if p]
|
|
1444
|
+
is_dict = any((p[0].type == _OP and p[0].string == "**") or _has_depth0(p, ":") is not None for p in parts)
|
|
1445
|
+
if is_dict:
|
|
1446
|
+
keys, values = [], []
|
|
1447
|
+
for p in parts:
|
|
1448
|
+
if p[0].type == _OP and p[0].string == "**":
|
|
1449
|
+
keys.append(None)
|
|
1450
|
+
values.append(self.expr(p[1:], p[0]))
|
|
1451
|
+
continue
|
|
1452
|
+
c = _has_depth0(p, ":")
|
|
1453
|
+
if c is None:
|
|
1454
|
+
return Opaque()._pos(open_tok, close_tok)
|
|
1455
|
+
keys.append(self.expr(p[:c], p[0]))
|
|
1456
|
+
values.append(self.expr(p[c + 1:], p[c]))
|
|
1457
|
+
return Dict(keys, values)._pos(open_tok, close_tok)
|
|
1458
|
+
elts = self.elements(parts)
|
|
1459
|
+
if elts is None:
|
|
1460
|
+
return Opaque()._pos(open_tok, close_tok)
|
|
1461
|
+
return Set(elts)._pos(open_tok, close_tok)
|
|
1462
|
+
|
|
1463
|
+
def postfix(self, toks):
|
|
1464
|
+
"""atom ('.' NAME | '(' args ')' | '[' ... ']')* covering ALL of toks, else None."""
|
|
1465
|
+
got = self.atom(toks)
|
|
1466
|
+
if got is None:
|
|
1467
|
+
return None
|
|
1468
|
+
node, i = got
|
|
1469
|
+
n = len(toks)
|
|
1470
|
+
while i < n:
|
|
1471
|
+
t = toks[i]
|
|
1472
|
+
if t.type == _OP and t.string == "." and i + 1 < n and toks[i + 1].type == _NAME:
|
|
1473
|
+
node = Attribute(node, toks[i + 1].string)._pos(toks[0], toks[i + 1])
|
|
1474
|
+
i += 2
|
|
1475
|
+
elif t.type == _OP and t.string == "(":
|
|
1476
|
+
j = _match(toks, i)
|
|
1477
|
+
if j is None:
|
|
1478
|
+
return None
|
|
1479
|
+
args, keywords = self.call_args(toks[i + 1:j], toks[i], toks[j])
|
|
1480
|
+
node = Call(node, args, keywords)._pos(toks[0], toks[j])
|
|
1481
|
+
i = j + 1
|
|
1482
|
+
elif t.type == _OP and t.string == "[":
|
|
1483
|
+
j = _match(toks, i)
|
|
1484
|
+
if j is None:
|
|
1485
|
+
return None
|
|
1486
|
+
node = Subscript(node, Opaque()._pos(toks[i], toks[j]))._pos(toks[0], toks[j])
|
|
1487
|
+
i = j + 1
|
|
1488
|
+
else:
|
|
1489
|
+
return None
|
|
1490
|
+
return node
|
|
1491
|
+
|
|
1492
|
+
def call_args(self, toks, open_tok, close_tok):
|
|
1493
|
+
args, keywords = [], []
|
|
1494
|
+
if toks and _has_depth0(toks, "for", _NAME) is not None:
|
|
1495
|
+
# A depth-0 `for` inside call parentheses can only be a SOLE generator
|
|
1496
|
+
# argument (`f(x for x in y)`, `"".join(t for _, t in run)`; its own
|
|
1497
|
+
# commas do not split it); ast spans it from `(` to `)`.
|
|
1498
|
+
return [Opaque()._pos(open_tok, close_tok)], []
|
|
1499
|
+
parts = _split_depth0(toks, ",")
|
|
1500
|
+
for part in parts:
|
|
1501
|
+
if not part:
|
|
1502
|
+
continue
|
|
1503
|
+
p0 = part[0]
|
|
1504
|
+
if p0.type == _OP and p0.string == "*":
|
|
1505
|
+
args.append(Starred(self.expr(part[1:], p0))._pos(p0, part[-1]))
|
|
1506
|
+
continue
|
|
1507
|
+
if p0.type == _OP and p0.string == "**":
|
|
1508
|
+
keywords.append(keyword_(None, self.expr(part[1:], p0))._pos(p0, part[-1]))
|
|
1509
|
+
continue
|
|
1510
|
+
if len(part) > 2 and p0.type == _NAME and part[1].type == _OP and part[1].string == "=":
|
|
1511
|
+
keywords.append(keyword_(p0.string, self.expr(part[2:], part[1]))._pos(p0, part[-1]))
|
|
1512
|
+
continue
|
|
1513
|
+
if _has_depth0(part, "for", _NAME) is not None or _has_depth0(part, ":=") is not None:
|
|
1514
|
+
args.append(Opaque()._pos(p0, part[-1]))
|
|
1515
|
+
continue
|
|
1516
|
+
args.append(self.expr(part, p0))
|
|
1517
|
+
return args, keywords
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
class _EndTok:
|
|
1521
|
+
"""A position-only stand-in for 'the end of a nested body'."""
|
|
1522
|
+
__slots__ = ("start", "end", "line", "type", "string")
|
|
1523
|
+
|
|
1524
|
+
def __init__(self, line, col):
|
|
1525
|
+
self.start = self.end = (line, col)
|
|
1526
|
+
self.line = ""
|
|
1527
|
+
self.type = -1
|
|
1528
|
+
self.string = ""
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
def parse_import(toks):
|
|
1532
|
+
"""`import …` / `from … import …` → Import / ImportFrom, or None when
|
|
1533
|
+
the tokens don't read as one (the caller falls back to Opaque). A
|
|
1534
|
+
module-level function so scan_imports can run it straight off the
|
|
1535
|
+
token stream without building a tree."""
|
|
1536
|
+
t0 = toks[0]
|
|
1537
|
+
if t0.string == "import":
|
|
1538
|
+
names = _import_aliases(toks[1:])
|
|
1539
|
+
return Import(names)._pos(t0, toks[-1]) if names else None
|
|
1540
|
+
# from [dots] [module] import names
|
|
1541
|
+
level, i = 0, 1
|
|
1542
|
+
while i < len(toks) and toks[i].type == _OP and toks[i].string in (".", "..."):
|
|
1543
|
+
level += len(toks[i].string)
|
|
1544
|
+
i += 1
|
|
1545
|
+
module_parts = []
|
|
1546
|
+
while i < len(toks) and not (toks[i].type == _NAME and toks[i].string == "import"):
|
|
1547
|
+
t = toks[i]
|
|
1548
|
+
if t.type == _NAME or (t.type == _OP and t.string == "."):
|
|
1549
|
+
module_parts.append(t.string)
|
|
1550
|
+
else:
|
|
1551
|
+
return None
|
|
1552
|
+
i += 1
|
|
1553
|
+
if i >= len(toks) or (not module_parts and level == 0):
|
|
1554
|
+
return None
|
|
1555
|
+
rest = toks[i + 1:]
|
|
1556
|
+
if rest and rest[0].type == _OP and rest[0].string == "(":
|
|
1557
|
+
close = _match(rest, 0)
|
|
1558
|
+
rest = rest[1:close] if close is not None else rest[1:]
|
|
1559
|
+
if len(rest) == 1 and rest[0].type == _OP and rest[0].string == "*":
|
|
1560
|
+
names = [alias("*")._pos(rest[0], rest[0])]
|
|
1561
|
+
else:
|
|
1562
|
+
names = _import_aliases(rest)
|
|
1563
|
+
if not names:
|
|
1564
|
+
return None
|
|
1565
|
+
module = "".join(module_parts) or None
|
|
1566
|
+
return ImportFrom(module, names, level)._pos(t0, toks[-1])
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def _import_aliases(toks):
|
|
1570
|
+
"""`a.b [as c], d [as e]` → [alias]; None on anything unexpected."""
|
|
1571
|
+
out = []
|
|
1572
|
+
for part in _split_depth0(toks, ","):
|
|
1573
|
+
if not part:
|
|
1574
|
+
continue
|
|
1575
|
+
parts, asname, j = [], None, 0
|
|
1576
|
+
while j < len(part):
|
|
1577
|
+
t = part[j]
|
|
1578
|
+
if t.type == _NAME and t.string == "as":
|
|
1579
|
+
if j + 1 != len(part) - 1 or part[j + 1].type != _NAME:
|
|
1580
|
+
return None
|
|
1581
|
+
asname = part[j + 1].string
|
|
1582
|
+
break
|
|
1583
|
+
if t.type == _NAME or (t.type == _OP and t.string == "."):
|
|
1584
|
+
parts.append(t.string)
|
|
1585
|
+
else:
|
|
1586
|
+
return None
|
|
1587
|
+
j += 1
|
|
1588
|
+
if not parts:
|
|
1589
|
+
return None
|
|
1590
|
+
out.append(alias("".join(parts), asname)._pos(part[0], part[-1]))
|
|
1591
|
+
return out
|
|
1592
|
+
|
|
1593
|
+
|
|
1594
|
+
_BODY_FIELDS = ("body", "orelse", "handlers", "finalbody")
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
def _token_scan_error(text, error):
|
|
1598
|
+
"""Tokenizers before Python 3.12 report an unclosed bracket or string at the
|
|
1599
|
+
END of the text; the compiler names the line that opened it."""
|
|
1600
|
+
msg, (line, col) = error.args[0], error.args[1] if len(error.args) > 1 else (0, 0)
|
|
1601
|
+
try:
|
|
1602
|
+
compile(text, "<text>", "exec")
|
|
1603
|
+
except SyntaxError as located:
|
|
1604
|
+
return ScanError(located.msg, ("<text>", located.lineno or line, located.offset or col + 1, ""))
|
|
1605
|
+
except ValueError:
|
|
1606
|
+
pass
|
|
1607
|
+
return ScanError(msg, ("<text>", line, col + 1, ""))
|
|
1608
|
+
|
|
1609
|
+
|
|
1610
|
+
def scan_imports(text):
|
|
1611
|
+
"""Every Import / ImportFrom of `text`, in source order, WITHOUT building
|
|
1612
|
+
the tree — the file import graph's path (file_graph.py). The token
|
|
1613
|
+
stream alone decides what is a statement start (after NEWLINE / INDENT
|
|
1614
|
+
/ DEDENT / a depth-0 `;`), so imports inside strings and comments are
|
|
1615
|
+
never seen, exactly like scan(); each `import` / `from` statement's
|
|
1616
|
+
tokens go through the same parse_import. ~6× cheaper than scan() on
|
|
1617
|
+
the src tree (tokenize is the whole cost). Raises ScanError like scan()."""
|
|
1618
|
+
out = []
|
|
1619
|
+
stmt = None # tokens of the import statement being collected
|
|
1620
|
+
at_start = True
|
|
1621
|
+
try:
|
|
1622
|
+
for t in tokenize.generate_tokens(io.StringIO(text).readline):
|
|
1623
|
+
tt = t.type
|
|
1624
|
+
if tt in (tokenize.COMMENT, tokenize.NL, tokenize.ENCODING):
|
|
1625
|
+
continue
|
|
1626
|
+
if stmt is not None:
|
|
1627
|
+
if tt == _NEWLINE or tt == _ENDMARKER or (tt == _OP and t.string == ";"):
|
|
1628
|
+
node = parse_import(stmt)
|
|
1629
|
+
if node is not None:
|
|
1630
|
+
out.append(node)
|
|
1631
|
+
stmt = None
|
|
1632
|
+
at_start = True
|
|
1633
|
+
else:
|
|
1634
|
+
stmt.append(t)
|
|
1635
|
+
continue
|
|
1636
|
+
if tt in (_NEWLINE, _INDENT, _DEDENT):
|
|
1637
|
+
at_start = True
|
|
1638
|
+
continue
|
|
1639
|
+
if tt == _OP and t.string == ";":
|
|
1640
|
+
at_start = True
|
|
1641
|
+
continue
|
|
1642
|
+
if at_start and tt == _NAME and t.string in ("import", "from"):
|
|
1643
|
+
stmt = [t]
|
|
1644
|
+
at_start = False
|
|
1645
|
+
except tokenize.TokenError as e:
|
|
1646
|
+
raise _token_scan_error(text, e) from None
|
|
1647
|
+
except (IndentationError, SyntaxError) as e:
|
|
1648
|
+
raise ScanError(str(e), ("<text>", getattr(e, "lineno", 0) or 0, getattr(e, "offset", 0) or 0, "")) from None
|
|
1649
|
+
if stmt is not None:
|
|
1650
|
+
node = parse_import(stmt)
|
|
1651
|
+
if node is not None:
|
|
1652
|
+
out.append(node)
|
|
1653
|
+
return out
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
def iter_imports(node):
|
|
1657
|
+
"""Every Import / ImportFrom node under `node` (a scan() Module or any
|
|
1658
|
+
scanner node), in source order, at any nesting — function bodies,
|
|
1659
|
+
if/try blocks and opaque compounds included."""
|
|
1660
|
+
stack = [node]
|
|
1661
|
+
while stack:
|
|
1662
|
+
n = stack.pop()
|
|
1663
|
+
k = _k(n)
|
|
1664
|
+
if k in ("Import", "ImportFrom"):
|
|
1665
|
+
yield n
|
|
1666
|
+
continue
|
|
1667
|
+
children = []
|
|
1668
|
+
for field in _BODY_FIELDS:
|
|
1669
|
+
body = getattr(n, field, None)
|
|
1670
|
+
if isinstance(body, list):
|
|
1671
|
+
children.extend(body)
|
|
1672
|
+
stack.extend(reversed(children))
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def scan(text):
|
|
1676
|
+
"""text → (Module, standalone comments, trailing comments). Raises
|
|
1677
|
+
SyntaxError (a ScanError) for what the tokenizer / block structure can't
|
|
1678
|
+
take; anything else parses — validation against `ast` is the caller's."""
|
|
1679
|
+
standalone, trailing = {}, {}
|
|
1680
|
+
sig = []
|
|
1681
|
+
try:
|
|
1682
|
+
for t in tokenize.generate_tokens(io.StringIO(text).readline):
|
|
1683
|
+
tt = t.type
|
|
1684
|
+
if tt == tokenize.COMMENT:
|
|
1685
|
+
line, col = t.start
|
|
1686
|
+
if t.line[:col].strip() == "":
|
|
1687
|
+
standalone[line] = (col, t.string)
|
|
1688
|
+
else:
|
|
1689
|
+
trailing[line] = (col, t.string)
|
|
1690
|
+
elif tt in (tokenize.NL, tokenize.ENCODING):
|
|
1691
|
+
continue
|
|
1692
|
+
else:
|
|
1693
|
+
sig.append(t)
|
|
1694
|
+
except tokenize.TokenError as e:
|
|
1695
|
+
raise _token_scan_error(text, e) from None
|
|
1696
|
+
except (IndentationError, SyntaxError) as e:
|
|
1697
|
+
raise ScanError(str(e), ("<text>", getattr(e, "lineno", 0) or 0, getattr(e, "offset", 0) or 0, "")) from None
|
|
1698
|
+
if not sig or sig[-1].type != _ENDMARKER:
|
|
1699
|
+
sig.append(_EndTok(text.count("\n") + 2, 0))
|
|
1700
|
+
sig[-1].type = _ENDMARKER
|
|
1701
|
+
p = _Parser(sig)
|
|
1702
|
+
body = p.block()
|
|
1703
|
+
return Module(body), standalone, trailing
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
1707
|
+
# ║ Node helpers used by both front ends ║
|
|
1708
|
+
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
1709
|
+
|
|
1710
|
+
def _dotted_parts(node):
|
|
1711
|
+
parts = []
|
|
1712
|
+
while _k(node) == "Attribute":
|
|
1713
|
+
parts.append(node.attr)
|
|
1714
|
+
node = node.value
|
|
1715
|
+
if _k(node) == "Name":
|
|
1716
|
+
parts.append(node.id)
|
|
1717
|
+
parts.reverse()
|
|
1718
|
+
return parts
|
|
1719
|
+
return None
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
def _call_func_name(call):
|
|
1723
|
+
func = call.func
|
|
1724
|
+
k = _k(func)
|
|
1725
|
+
if k == "Name":
|
|
1726
|
+
return func.id
|
|
1727
|
+
if k == "Attribute":
|
|
1728
|
+
parts = _dotted_parts(func)
|
|
1729
|
+
return ".".join(parts) if parts is not None else func.attr
|
|
1730
|
+
return None
|
|
1731
|
+
|
|
1732
|
+
|
|
1733
|
+
def _assign_target_name(stmt):
|
|
1734
|
+
k = _k(stmt)
|
|
1735
|
+
if k == "Assign" and len(stmt.targets) == 1 and _k(stmt.targets[0]) == "Name":
|
|
1736
|
+
return stmt.targets[0].id
|
|
1737
|
+
if k == "AnnAssign" and _k(stmt.target) == "Name" and stmt.value is not None:
|
|
1738
|
+
return stmt.target.id
|
|
1739
|
+
return None
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
def _stmt_call(stmt, nonname_targets):
|
|
1743
|
+
k = _k(stmt)
|
|
1744
|
+
if k == "Expr" and _k(stmt.value) == "Call":
|
|
1745
|
+
return stmt.value
|
|
1746
|
+
if not nonname_targets:
|
|
1747
|
+
return None
|
|
1748
|
+
if k == "Assign" and _k(stmt.value) == "Call":
|
|
1749
|
+
if not (len(stmt.targets) == 1 and _k(stmt.targets[0]) == "Name"):
|
|
1750
|
+
return stmt.value
|
|
1751
|
+
if k == "AnnAssign" and stmt.value is not None and _k(stmt.value) == "Call":
|
|
1752
|
+
if _k(stmt.target) != "Name":
|
|
1753
|
+
return stmt.value
|
|
1754
|
+
return None
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
def _is_enum_classdef(node):
|
|
1758
|
+
for base in list(node.bases) + [kw.value for kw in node.keywords if kw.arg == "metaclass"]:
|
|
1759
|
+
parts = _dotted_parts(base)
|
|
1760
|
+
last = parts[-1] if parts else None
|
|
1761
|
+
if last and (last.endswith("Enum") or last.endswith("Flag")):
|
|
1762
|
+
return True
|
|
1763
|
+
return False
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
def _param_names(funcdef):
|
|
1767
|
+
a = funcdef.args
|
|
1768
|
+
names = []
|
|
1769
|
+
for p in list(a.posonlyargs) + list(a.args):
|
|
1770
|
+
if not names and p.arg in _SKIP_PARAMS:
|
|
1771
|
+
continue
|
|
1772
|
+
names.append(p.arg)
|
|
1773
|
+
return names
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
def _local_signatures(stmts):
|
|
1777
|
+
return {s.name: _param_names(s) for s in stmts if _k(s) in ("FunctionDef", "AsyncFunctionDef")}
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
def _const_value(node):
|
|
1781
|
+
"""The value of a literal-only expression (dict keys), else raise ValueError."""
|
|
1782
|
+
k = _k(node)
|
|
1783
|
+
if k == "Constant":
|
|
1784
|
+
return node.value
|
|
1785
|
+
if k == "UnaryOp" and _k(node.op) in ("USub", "UAdd") and _k(node.operand) == "Constant" \
|
|
1786
|
+
and isinstance(node.operand.value, (int, float, complex)) and not isinstance(node.operand.value, bool):
|
|
1787
|
+
return -node.operand.value if _k(node.op) == "USub" else node.operand.value
|
|
1788
|
+
if k == "Tuple":
|
|
1789
|
+
return tuple(_const_value(e) for e in node.elts)
|
|
1790
|
+
raise ValueError("not a literal")
|
|
1791
|
+
|
|
1792
|
+
|
|
1793
|
+
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
1794
|
+
# ║ Extractor: nodes → dict + Origin ║
|
|
1795
|
+
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
1796
|
+
|
|
1797
|
+
class Extractor:
|
|
1798
|
+
def __init__(self, origin, types, comments, module_header=True):
|
|
1799
|
+
self.origin = origin
|
|
1800
|
+
self.src = origin.src
|
|
1801
|
+
self.T = types
|
|
1802
|
+
self.standalone, self.trailing = comments
|
|
1803
|
+
# False for a REGION parse (an incremental reparse of statements that
|
|
1804
|
+
# don't start the file): an override comment above its first
|
|
1805
|
+
# statement is that statement's, not a module-header override.
|
|
1806
|
+
self.module_header = module_header
|
|
1807
|
+
self._base = ZERO_BASE # the current TOP-LEVEL statement's Base cell
|
|
1808
|
+
self.cursor = 0 # last consumed line (1-based); 0 = nothing yet
|
|
1809
|
+
# >0 while extracting call arguments / container elements: a CallParse
|
|
1810
|
+
# statement there gets no .span (libcst's span map only covers a
|
|
1811
|
+
# statement's direct value call), so LineMap depth matches.
|
|
1812
|
+
self._nested = 0
|
|
1813
|
+
|
|
1814
|
+
# ── spans on the dict (the consumers' view: Span / _child_spans) ─────────
|
|
1815
|
+
|
|
1816
|
+
def span_obj(self, start, end):
|
|
1817
|
+
(sl, sc), (el, ec) = self.src.linecol(start), self.src.linecol(end)
|
|
1818
|
+
b = self._base
|
|
1819
|
+
return self.T.Span(b, sl - b.line, sc, el - b.line, ec)
|
|
1820
|
+
|
|
1821
|
+
def _stamp(self, obj, start, end):
|
|
1822
|
+
try:
|
|
1823
|
+
obj.span = self.span_obj(start, end)
|
|
1824
|
+
except AttributeError:
|
|
1825
|
+
pass
|
|
1826
|
+
return obj
|
|
1827
|
+
|
|
1828
|
+
def _record_child(self, container, key, value, start, end):
|
|
1829
|
+
if isinstance(value, dict) and getattr(value, "span", None) is not None:
|
|
1830
|
+
return
|
|
1831
|
+
cs = getattr(container, "_child_spans", None)
|
|
1832
|
+
if cs is None:
|
|
1833
|
+
cs = {}
|
|
1834
|
+
try:
|
|
1835
|
+
container._child_spans = cs
|
|
1836
|
+
except AttributeError:
|
|
1837
|
+
return
|
|
1838
|
+
cs[key] = self.span_obj(start, end)
|
|
1839
|
+
|
|
1840
|
+
# ── comments ─────────────────────────────────────────────────────────────
|
|
1841
|
+
|
|
1842
|
+
def _leading_groups(self, upto_line):
|
|
1843
|
+
runs, run = [], []
|
|
1844
|
+
for ln in range(self.cursor + 1, upto_line):
|
|
1845
|
+
c = self.standalone.get(ln)
|
|
1846
|
+
if c is None:
|
|
1847
|
+
if run:
|
|
1848
|
+
runs.append(run)
|
|
1849
|
+
run = []
|
|
1850
|
+
continue
|
|
1851
|
+
run.append((ln, c[1]))
|
|
1852
|
+
if run:
|
|
1853
|
+
runs.append(run)
|
|
1854
|
+
groups = []
|
|
1855
|
+
for run in runs:
|
|
1856
|
+
k = plain = 0
|
|
1857
|
+
while k < len(run):
|
|
1858
|
+
ov_end = self._override_run_end(run, k)
|
|
1859
|
+
if ov_end is None:
|
|
1860
|
+
k += 1
|
|
1861
|
+
continue
|
|
1862
|
+
if plain < k:
|
|
1863
|
+
groups.append(self._group(run[plain:k], False))
|
|
1864
|
+
groups.append(self._group(run[k:ov_end], True))
|
|
1865
|
+
k = plain = ov_end
|
|
1866
|
+
if plain < len(run):
|
|
1867
|
+
groups.append(self._group(run[plain:], False))
|
|
1868
|
+
return groups
|
|
1869
|
+
|
|
1870
|
+
@staticmethod
|
|
1871
|
+
def _group(run, is_override):
|
|
1872
|
+
return (run[0][0], run[-1][0], [t for _, t in run], is_override)
|
|
1873
|
+
|
|
1874
|
+
@staticmethod
|
|
1875
|
+
def _override_run_end(run, i):
|
|
1876
|
+
if not run[i][1].lstrip("#").strip().startswith("["):
|
|
1877
|
+
return None
|
|
1878
|
+
for j in range(i, len(run)):
|
|
1879
|
+
if run[j][1].rstrip().endswith("]"):
|
|
1880
|
+
joined = "\n".join(t for _, t in run[i:j + 1])
|
|
1881
|
+
if parse_override_comment(joined) is not None:
|
|
1882
|
+
return j + 1
|
|
1883
|
+
return None
|
|
1884
|
+
|
|
1885
|
+
def _comment_extent(self, first_line, last_line):
|
|
1886
|
+
return self.src.line_start(first_line), self.src.next_line_start(last_line)
|
|
1887
|
+
|
|
1888
|
+
def _comment_span(self, first_line, last_line):
|
|
1889
|
+
col = self.standalone[first_line][0]
|
|
1890
|
+
return self.src.line_start(first_line) + col, self.src.line_end(last_line)
|
|
1891
|
+
|
|
1892
|
+
def _surface_comment(self, out, path, group):
|
|
1893
|
+
first, last, texts, _ = group
|
|
1894
|
+
text = "\n".join(texts)
|
|
1895
|
+
c = self.T.Comment(text)
|
|
1896
|
+
out[c] = c
|
|
1897
|
+
extent = self._comment_extent(first, last)
|
|
1898
|
+
self.origin.add(Item(self._base, path + (c,), c, "comment", extent, extent,
|
|
1899
|
+
self._comment_span(first, last), text,
|
|
1900
|
+
indent=self.src.indent_of_line(first)))
|
|
1901
|
+
return c
|
|
1902
|
+
|
|
1903
|
+
def _merge_override(self, comment, out, path, span, indent):
|
|
1904
|
+
if isinstance(out.get("__overrides__"), dict):
|
|
1905
|
+
return
|
|
1906
|
+
parsed = parse_override_comment(str(comment))
|
|
1907
|
+
if parsed:
|
|
1908
|
+
out["__overrides__"] = parsed
|
|
1909
|
+
self.origin.add(Item(self._base, path + ("__overrides__",), "__overrides__", "override",
|
|
1910
|
+
span, span, span, dict(parsed), indent=indent,
|
|
1911
|
+
comment_key=comment))
|
|
1912
|
+
|
|
1913
|
+
def _trailing_comment(self, stmt, out, path, key):
|
|
1914
|
+
tc = self.trailing.get(stmt.end_lineno)
|
|
1915
|
+
if tc is None:
|
|
1916
|
+
return
|
|
1917
|
+
col, text = tc
|
|
1918
|
+
code_end = self.src.node_span(stmt)[1]
|
|
1919
|
+
c = self.T.Comment(text, inline=key)
|
|
1920
|
+
out[c] = c
|
|
1921
|
+
start = self.src.line_start(stmt.end_lineno) + col
|
|
1922
|
+
end = self.src.line_end(stmt.end_lineno)
|
|
1923
|
+
self.origin.add(Item(self._base, path + (c,), c, "trailing", (code_end, end), (code_end, end),
|
|
1924
|
+
(start, end), text, indent=self.src.indent_of_line(stmt.end_lineno)))
|
|
1925
|
+
self._merge_override(c, out, path, (start, end), self.src.indent_of_line(stmt.end_lineno))
|
|
1926
|
+
|
|
1927
|
+
def _consume_footer(self, body_indent_len):
|
|
1928
|
+
ln = self.cursor + 1
|
|
1929
|
+
last_taken = self.cursor
|
|
1930
|
+
while ln <= self.src.line_count:
|
|
1931
|
+
c = self.standalone.get(ln)
|
|
1932
|
+
if c is not None:
|
|
1933
|
+
if c[0] < body_indent_len:
|
|
1934
|
+
break
|
|
1935
|
+
last_taken = ln
|
|
1936
|
+
elif not self.src.is_blank(ln):
|
|
1937
|
+
break
|
|
1938
|
+
ln += 1
|
|
1939
|
+
self.cursor = last_taken
|
|
1940
|
+
|
|
1941
|
+
# ── statement extents ────────────────────────────────────────────────────
|
|
1942
|
+
|
|
1943
|
+
def _stmt_first_line(self, stmt):
|
|
1944
|
+
decs = getattr(stmt, "decorator_list", None)
|
|
1945
|
+
if decs:
|
|
1946
|
+
return min(stmt.lineno, decs[0].lineno)
|
|
1947
|
+
return stmt.lineno
|
|
1948
|
+
|
|
1949
|
+
def _header_end_line(self, stmt):
|
|
1950
|
+
body = stmt.body
|
|
1951
|
+
ln = self._stmt_first_line(body[0]) - 1
|
|
1952
|
+
first = self._stmt_first_line(stmt)
|
|
1953
|
+
while ln > first and (self.src.is_blank(ln) or ln in self.standalone):
|
|
1954
|
+
ln -= 1
|
|
1955
|
+
return ln
|
|
1956
|
+
|
|
1957
|
+
def _keyword_line(self, body_stmts, low):
|
|
1958
|
+
ln = self._stmt_first_line(body_stmts[0]) - 1
|
|
1959
|
+
while ln > low and (self.src.is_blank(ln) or ln in self.standalone):
|
|
1960
|
+
ln -= 1
|
|
1961
|
+
return ln
|
|
1962
|
+
|
|
1963
|
+
# ── module ───────────────────────────────────────────────────────────────
|
|
1964
|
+
|
|
1965
|
+
def module(self, tree):
|
|
1966
|
+
gp = self.T.GeneralParse(source=self.origin.text)
|
|
1967
|
+
seq = self.origin.new_seq((), "body", base=ZERO_BASE, indent="", insert_at=0)
|
|
1968
|
+
self._body(tree.body, gp, (), seq, scope="module")
|
|
1969
|
+
self.origin.seal_seq(seq)
|
|
1970
|
+
return gp
|
|
1971
|
+
|
|
1972
|
+
# ── bodies ───────────────────────────────────────────────────────────────
|
|
1973
|
+
|
|
1974
|
+
def _body(self, stmts, out, path, seq, *, scope, block_state=None):
|
|
1975
|
+
T = self.T
|
|
1976
|
+
is_function = scope == "function"
|
|
1977
|
+
nonname_calls = scope != "class"
|
|
1978
|
+
local_sigs = _local_signatures(stmts)
|
|
1979
|
+
call_seen: dict[str, int] = {}
|
|
1980
|
+
counts: dict[str, int] = {}
|
|
1981
|
+
seen: dict[str, int] = {}
|
|
1982
|
+
if is_function:
|
|
1983
|
+
for s in stmts:
|
|
1984
|
+
n = _assign_target_name(s)
|
|
1985
|
+
if n is not None:
|
|
1986
|
+
counts[n] = counts.get(n, 0) + 1
|
|
1987
|
+
if block_state is None:
|
|
1988
|
+
block_state = ({"if": 0, "elif": 0, "else": 0}, {})
|
|
1989
|
+
cond_counters, block_occ = block_state
|
|
1990
|
+
|
|
1991
|
+
for stmt in stmts:
|
|
1992
|
+
kind = _k(stmt)
|
|
1993
|
+
first_line = self._stmt_first_line(stmt)
|
|
1994
|
+
same_line = first_line <= self.cursor # `a = 1; b = 2`
|
|
1995
|
+
gap_start = self.src.next_line_start(self.cursor) if not same_line else self.src.node_span(stmt)[0]
|
|
1996
|
+
if scope == "module":
|
|
1997
|
+
self._base = Base(gap_start, self.src.linecol(gap_start)[0])
|
|
1998
|
+
groups = [] if same_line else self._leading_groups(first_line)
|
|
1999
|
+
core_start = self.src.line_start(groups[0][0]) if groups else (
|
|
2000
|
+
self.src.line_start(first_line) if not same_line else gap_start)
|
|
2001
|
+
indent = self.src.indent_of_line(first_line)
|
|
2002
|
+
is_def = kind in _DEF_KINDS
|
|
2003
|
+
is_block = kind in _BLOCK_KINDS
|
|
2004
|
+
field_override = child_override = None
|
|
2005
|
+
|
|
2006
|
+
for g in groups:
|
|
2007
|
+
if g[3]: # override comment
|
|
2008
|
+
if is_def:
|
|
2009
|
+
if child_override is None:
|
|
2010
|
+
child_override = g
|
|
2011
|
+
elif is_block and is_function:
|
|
2012
|
+
c = self._surface_comment(out, path, g)
|
|
2013
|
+
self._merge_override(c, out, path, self._comment_span(g[0], g[1]),
|
|
2014
|
+
self.src.indent_of_line(g[0]))
|
|
2015
|
+
elif scope == "module" and self.module_header and self.cursor == 0 and not out:
|
|
2016
|
+
c = self._surface_comment(out, path, g)
|
|
2017
|
+
self._merge_override(c, out, path, self._comment_span(g[0], g[1]),
|
|
2018
|
+
self.src.indent_of_line(g[0]))
|
|
2019
|
+
elif field_override is None:
|
|
2020
|
+
field_override = g
|
|
2021
|
+
else:
|
|
2022
|
+
self._surface_comment(out, path, g)
|
|
2023
|
+
|
|
2024
|
+
key = None
|
|
2025
|
+
if is_def:
|
|
2026
|
+
key = self._def(stmt, out, path, seq, scope, child_override, gap_start, core_start)
|
|
2027
|
+
elif kind in ("Assign", "AnnAssign") and _assign_target_name(stmt) is not None:
|
|
2028
|
+
name = _assign_target_name(stmt)
|
|
2029
|
+
if is_function:
|
|
2030
|
+
occ = seen.get(name, 0)
|
|
2031
|
+
seen[name] = occ + 1
|
|
2032
|
+
key = name if (counts[name] == 1 or occ == 0) else f"{name}#{occ}"
|
|
2033
|
+
else:
|
|
2034
|
+
key = name
|
|
2035
|
+
self.origin.shadow(path + (key,))
|
|
2036
|
+
value = self._value(stmt.value, path + (key,))
|
|
2037
|
+
out[key] = value
|
|
2038
|
+
vs = self.src.node_span(stmt.value)
|
|
2039
|
+
self._record_child(out, key, value, *vs)
|
|
2040
|
+
self.origin.add(Item(self._base, path + (key,), key, "value", (gap_start, 0), (core_start, 0), vs, value,
|
|
2041
|
+
indent=indent, code_end=self.src.node_span(stmt)[1]), seq)
|
|
2042
|
+
elif _stmt_call(stmt, nonname_calls) is not None:
|
|
2043
|
+
call = _stmt_call(stmt, nonname_calls)
|
|
2044
|
+
fname = _call_func_name(call) or "call"
|
|
2045
|
+
occ = call_seen.get(fname, 0)
|
|
2046
|
+
call_seen[fname] = occ + 1
|
|
2047
|
+
key = f"{fname}()" if occ == 0 else f"{fname}()#{occ}"
|
|
2048
|
+
self.origin.shadow(path + (key,))
|
|
2049
|
+
parsed = self._call(call, path + (key,), T.CallParse, local_sigs.get(fname), allow_empty=True)
|
|
2050
|
+
if parsed is None:
|
|
2051
|
+
key = None
|
|
2052
|
+
else:
|
|
2053
|
+
out[key] = parsed
|
|
2054
|
+
cs = self.src.node_span(call)
|
|
2055
|
+
self.origin.add(Item(self._base, path + (key,), key, "value", (gap_start, 0), (core_start, 0), cs, parsed,
|
|
2056
|
+
indent=indent, code_end=self.src.node_span(stmt)[1]), seq)
|
|
2057
|
+
elif is_function and kind == "If":
|
|
2058
|
+
self._if_chain(stmt, out, path, seq, cond_counters, block_occ, gap_start, core_start, indent)
|
|
2059
|
+
elif is_function and kind in ("For", "AsyncFor"):
|
|
2060
|
+
self._for_loop(stmt, out, path, seq, block_occ, gap_start, core_start, indent)
|
|
2061
|
+
elif is_function and kind in ("Try", "TryStar"):
|
|
2062
|
+
self._try_block(stmt, out, path, seq, block_occ, gap_start, core_start, indent)
|
|
2063
|
+
|
|
2064
|
+
if key is not None and not is_def and not same_line:
|
|
2065
|
+
self._trailing_comment(stmt, out, path, key)
|
|
2066
|
+
if field_override is not None:
|
|
2067
|
+
self._attach_field_override(field_override, out, path, key)
|
|
2068
|
+
|
|
2069
|
+
if not is_def and not is_block:
|
|
2070
|
+
self.cursor = max(self.cursor, stmt.end_lineno)
|
|
2071
|
+
end = self.src.next_line_start(self.cursor) if not same_line else self.src.node_span(stmt)[1]
|
|
2072
|
+
if key is not None:
|
|
2073
|
+
item = self.origin.items[path + (key,)]
|
|
2074
|
+
item.extent = (item.extent[0], end)
|
|
2075
|
+
item.core = (item.core[0], end)
|
|
2076
|
+
if scope == "module":
|
|
2077
|
+
self.origin.top_stmts.append((self._base, end - gap_start, key))
|
|
2078
|
+
|
|
2079
|
+
def _attach_field_override(self, group, out, path, key):
|
|
2080
|
+
first, last, texts, _ = group
|
|
2081
|
+
parsed = parse_override_comment("\n".join(texts))
|
|
2082
|
+
if not parsed:
|
|
2083
|
+
return
|
|
2084
|
+
overrides = out.get("__overrides__")
|
|
2085
|
+
if not isinstance(overrides, dict):
|
|
2086
|
+
overrides = {}
|
|
2087
|
+
out["__overrides__"] = overrides
|
|
2088
|
+
slot = f"__{key}__"
|
|
2089
|
+
if slot in overrides:
|
|
2090
|
+
return
|
|
2091
|
+
overrides[slot] = parsed
|
|
2092
|
+
span = self._comment_span(first, last)
|
|
2093
|
+
self.origin.add(Item(self._base, path + ("__overrides__", slot), slot, "override",
|
|
2094
|
+
self._comment_extent(first, last), span, span, dict(parsed),
|
|
2095
|
+
indent=self.src.indent_of_line(first)))
|
|
2096
|
+
|
|
2097
|
+
# ── definitions ─────────────────────────────────────────────────────────────────
|
|
2098
|
+
|
|
2099
|
+
def _def(self, stmt, out, path, seq, scope, child_override, gap_start, core_start):
|
|
2100
|
+
name = stmt.name
|
|
2101
|
+
child_path = path + (name,)
|
|
2102
|
+
first_line = self._stmt_first_line(stmt)
|
|
2103
|
+
indent = self.src.indent_of_line(first_line)
|
|
2104
|
+
is_init = (scope == "class" and _k(stmt) in ("FunctionDef", "AsyncFunctionDef") and name == "__init__")
|
|
2105
|
+
if is_init:
|
|
2106
|
+
self.cursor = max(self.cursor, stmt.end_lineno)
|
|
2107
|
+
return None
|
|
2108
|
+
code_span = (self.src.line_start(first_line) + len(indent), self.src.node_span(stmt)[1])
|
|
2109
|
+
self.origin.shadow(child_path)
|
|
2110
|
+
if _k(stmt) == "ClassDef":
|
|
2111
|
+
child = self._class(stmt, child_path)
|
|
2112
|
+
else:
|
|
2113
|
+
child = self._function(stmt, child_path)
|
|
2114
|
+
out[name] = child
|
|
2115
|
+
self.origin.add(Item(self._base, child_path, name, "def", (gap_start, 0), (core_start, 0), None, None,
|
|
2116
|
+
indent=indent, code_end=code_span[1]), seq)
|
|
2117
|
+
if child_override is not None:
|
|
2118
|
+
parsed = parse_override_comment("\n".join(child_override[2]))
|
|
2119
|
+
if parsed:
|
|
2120
|
+
existing = child.get("__overrides__")
|
|
2121
|
+
if isinstance(existing, dict):
|
|
2122
|
+
for k, v in parsed.items():
|
|
2123
|
+
existing.setdefault(k, v)
|
|
2124
|
+
else:
|
|
2125
|
+
child["__overrides__"] = parsed
|
|
2126
|
+
span = self._comment_span(child_override[0], child_override[1])
|
|
2127
|
+
self.origin.add(Item(self._base, child_path + ("__overrides__",), "__overrides__", "override",
|
|
2128
|
+
self._comment_extent(child_override[0], child_override[1]), span, span,
|
|
2129
|
+
dict(parsed), indent=self.src.indent_of_line(child_override[0])))
|
|
2130
|
+
return name
|
|
2131
|
+
|
|
2132
|
+
def _class(self, node, path):
|
|
2133
|
+
T = self.T
|
|
2134
|
+
cls = T.EnumParse if _is_enum_classdef(node) else T.ClassParse
|
|
2135
|
+
first_line = self._stmt_first_line(node)
|
|
2136
|
+
start = self.src.line_start(first_line) + len(self.src.indent_of_line(first_line))
|
|
2137
|
+
end = self.src.node_span(node)[1]
|
|
2138
|
+
readable = cls(source=self.origin.text[start:end])
|
|
2139
|
+
readable.def_name = node.name
|
|
2140
|
+
self._stamp(readable, self.src.node_span(node)[0], end)
|
|
2141
|
+
decorators = self._decorators(node, path)
|
|
2142
|
+
if decorators:
|
|
2143
|
+
readable["decorators"] = decorators
|
|
2144
|
+
self.cursor = self._header_end_line(node)
|
|
2145
|
+
body_indent = self.src.indent_of_line(node.body[0].lineno)
|
|
2146
|
+
seq = self.origin.new_seq(path, "body", base=self._base, indent=body_indent,
|
|
2147
|
+
insert_at=self.src.line_start(node.body[0].lineno))
|
|
2148
|
+
self._body(node.body, readable, path, seq, scope="class")
|
|
2149
|
+
self._consume_footer(len(body_indent))
|
|
2150
|
+
self.origin.seal_seq(seq)
|
|
2151
|
+
self._init_fields(node, readable, path)
|
|
2152
|
+
return readable
|
|
2153
|
+
|
|
2154
|
+
def _init_fields(self, node, readable, path):
|
|
2155
|
+
init = next((s for s in node.body if _k(s) in ("FunctionDef", "AsyncFunctionDef")
|
|
2156
|
+
and s.name == "__init__"), None)
|
|
2157
|
+
if init is None:
|
|
2158
|
+
return
|
|
2159
|
+
body_indent = self.src.indent_of_line(init.body[0].lineno)
|
|
2160
|
+
seq = self.origin.new_seq(path, "body", base=self._base, default=False, indent=body_indent,
|
|
2161
|
+
insert_at=self.src.line_start(init.body[0].lineno))
|
|
2162
|
+
for stmt in init.body:
|
|
2163
|
+
target = value = None
|
|
2164
|
+
k = _k(stmt)
|
|
2165
|
+
if k == "Assign" and len(stmt.targets) == 1:
|
|
2166
|
+
target, value = stmt.targets[0], stmt.value
|
|
2167
|
+
elif k == "AnnAssign":
|
|
2168
|
+
target, value = stmt.target, stmt.value
|
|
2169
|
+
if (target is None or value is None or _k(target) != "Attribute"
|
|
2170
|
+
or _k(target.value) != "Name" or target.value.id != "self"):
|
|
2171
|
+
continue
|
|
2172
|
+
attr = target.attr
|
|
2173
|
+
if attr in readable:
|
|
2174
|
+
continue
|
|
2175
|
+
py = self._value(value, path + (attr,))
|
|
2176
|
+
readable[attr] = py
|
|
2177
|
+
vs = self.src.node_span(value)
|
|
2178
|
+
self._record_child(readable, attr, py, *vs)
|
|
2179
|
+
extent = (self.src.line_start(stmt.lineno), self.src.next_line_start(stmt.end_lineno))
|
|
2180
|
+
self.origin.add(Item(self._base, path + (attr,), attr, "value", extent, extent, vs, py,
|
|
2181
|
+
indent=self.src.indent_of_line(stmt.lineno),
|
|
2182
|
+
code_end=self.src.node_span(stmt)[1]), seq)
|
|
2183
|
+
self.origin.seal_seq(seq)
|
|
2184
|
+
|
|
2185
|
+
def _function(self, node, path):
|
|
2186
|
+
T = self.T
|
|
2187
|
+
first_line = self._stmt_first_line(node)
|
|
2188
|
+
start = self.src.line_start(first_line) + len(self.src.indent_of_line(first_line))
|
|
2189
|
+
end = self.src.node_span(node)[1]
|
|
2190
|
+
readable = T.FunctionParse(source=self.origin.text[start:end])
|
|
2191
|
+
readable.def_name = node.name
|
|
2192
|
+
self._stamp(readable, self.src.node_span(node)[0], end)
|
|
2193
|
+
decorators = self._decorators(node, path)
|
|
2194
|
+
if decorators:
|
|
2195
|
+
readable["decorators"] = decorators
|
|
2196
|
+
params = self._params(node, path + ("parameters",))
|
|
2197
|
+
if params:
|
|
2198
|
+
readable["parameters"] = params
|
|
2199
|
+
self.origin.add(Item(ZERO_BASE, path + ("parameters",), "parameters", "pseudo", (0, 0), (0, 0), None, params))
|
|
2200
|
+
self.cursor = self._header_end_line(node)
|
|
2201
|
+
body_indent = self.src.indent_of_line(node.body[0].lineno)
|
|
2202
|
+
locals_ = T.GeneralParse(source="")
|
|
2203
|
+
seq = self.origin.new_seq(path + ("locals",), "body", base=self._base, indent=body_indent,
|
|
2204
|
+
insert_at=self.src.line_start(node.body[0].lineno))
|
|
2205
|
+
self._body(node.body, locals_, path + ("locals",), seq, scope="function")
|
|
2206
|
+
self._consume_footer(len(body_indent))
|
|
2207
|
+
self.origin.seal_seq(seq)
|
|
2208
|
+
if locals_:
|
|
2209
|
+
bs, be = self.src.node_span(node.body[0])[0], self.src.node_span(node.body[-1])[1]
|
|
2210
|
+
self._stamp(locals_, bs, be)
|
|
2211
|
+
readable["locals"] = locals_
|
|
2212
|
+
self.origin.add(Item(ZERO_BASE, path + ("locals",), "locals", "pseudo", (0, 0), (0, 0), None, locals_))
|
|
2213
|
+
else:
|
|
2214
|
+
self.origin.drop_seq(seq)
|
|
2215
|
+
return readable
|
|
2216
|
+
|
|
2217
|
+
def _decorators(self, node, path):
|
|
2218
|
+
T = self.T
|
|
2219
|
+
if not node.decorator_list:
|
|
2220
|
+
return {}
|
|
2221
|
+
result = {}
|
|
2222
|
+
dpath = path + ("decorators",)
|
|
2223
|
+
seq = self.origin.new_seq(dpath, "decorators", base=self._base,
|
|
2224
|
+
indent=self.src.indent_of_line(node.decorator_list[0].lineno),
|
|
2225
|
+
insert_at=self.src.line_start(node.decorator_list[0].lineno))
|
|
2226
|
+
for dec in node.decorator_list:
|
|
2227
|
+
extent = (self.src.line_start(dec.lineno), self.src.next_line_start(dec.end_lineno))
|
|
2228
|
+
if _k(dec) == "Call":
|
|
2229
|
+
name = _call_func_name(dec)
|
|
2230
|
+
if name is None:
|
|
2231
|
+
continue
|
|
2232
|
+
self.origin.shadow(dpath + (name,))
|
|
2233
|
+
self._nested += 1
|
|
2234
|
+
try:
|
|
2235
|
+
parsed = self._call(dec, dpath + (name,), T.DecorationParse, None, allow_empty=True)
|
|
2236
|
+
finally:
|
|
2237
|
+
self._nested -= 1
|
|
2238
|
+
if parsed is None:
|
|
2239
|
+
parsed = T.CodeLine(self._text(dec))
|
|
2240
|
+
result[name] = parsed
|
|
2241
|
+
self.origin.add(Item(self._base, dpath + (name,), name, "decorator", extent, extent,
|
|
2242
|
+
self.src.node_span(dec), parsed,
|
|
2243
|
+
indent=self.src.indent_of_line(dec.lineno)), seq)
|
|
2244
|
+
else:
|
|
2245
|
+
code = self._text(dec)
|
|
2246
|
+
result[code] = code
|
|
2247
|
+
self.origin.add(Item(self._base, dpath + (code,), code, "decorator", extent, extent,
|
|
2248
|
+
self.src.node_span(dec), code,
|
|
2249
|
+
indent=self.src.indent_of_line(dec.lineno)), seq)
|
|
2250
|
+
self.origin.seal_seq(seq)
|
|
2251
|
+
if result:
|
|
2252
|
+
self.origin.add(Item(ZERO_BASE, dpath, "decorators", "pseudo", (0, 0), (0, 0), None, result))
|
|
2253
|
+
else:
|
|
2254
|
+
self.origin.drop_seq(seq)
|
|
2255
|
+
return result
|
|
2256
|
+
|
|
2257
|
+
def _params(self, node, path):
|
|
2258
|
+
T = self.T
|
|
2259
|
+
a = node.args
|
|
2260
|
+
n_pos = len(a.posonlyargs) + len(a.args)
|
|
2261
|
+
defaults = [None] * (n_pos - len(a.defaults)) + list(a.defaults)
|
|
2262
|
+
regular = [(p, defaults[len(a.posonlyargs) + i]) for i, p in enumerate(a.args)]
|
|
2263
|
+
posonly = [(p, defaults[i]) for i, p in enumerate(a.posonlyargs)]
|
|
2264
|
+
kwonly = [(p, a.kw_defaults[i]) for i, p in enumerate(a.kwonlyargs)]
|
|
2265
|
+
ordered = regular + posonly + kwonly
|
|
2266
|
+
if not ordered:
|
|
2267
|
+
return None
|
|
2268
|
+
result = T.GeneralParse(source="")
|
|
2269
|
+
all_params = [p for p, _ in ordered]
|
|
2270
|
+
seq = self.origin.new_seq(path, "params", base=self._base, sep=", ")
|
|
2271
|
+
first = True
|
|
2272
|
+
for p, default in ordered:
|
|
2273
|
+
if p.arg in _SKIP_PARAMS:
|
|
2274
|
+
continue
|
|
2275
|
+
ps = self.src.node_span(p)
|
|
2276
|
+
if first:
|
|
2277
|
+
seq.insert_at = ps[0]
|
|
2278
|
+
first = False
|
|
2279
|
+
if default is not None:
|
|
2280
|
+
value = self._value(default, path + (p.arg,))
|
|
2281
|
+
vs = self.src.node_span(default)
|
|
2282
|
+
extent = (ps[0], vs[1])
|
|
2283
|
+
else:
|
|
2284
|
+
value = T.NO_DEFAULT
|
|
2285
|
+
vs = None
|
|
2286
|
+
extent = ps
|
|
2287
|
+
result[p.arg] = value
|
|
2288
|
+
self.origin.add(Item(self._base, path + (p.arg,), p.arg, "param", extent, extent, vs, value,
|
|
2289
|
+
slot=ps[1]), seq)
|
|
2290
|
+
if all_params:
|
|
2291
|
+
self._stamp(result, self.src.node_span(all_params[0])[0],
|
|
2292
|
+
max(self.src.node_span(p)[1] for p in all_params))
|
|
2293
|
+
if len(seq.items) >= 2:
|
|
2294
|
+
seq.sep = self.origin.text[seq.items[0].extent[1]:seq.items[1].extent[0]]
|
|
2295
|
+
self.origin.seal_seq(seq)
|
|
2296
|
+
if not result:
|
|
2297
|
+
self.origin.drop_seq(seq)
|
|
2298
|
+
return None
|
|
2299
|
+
return result
|
|
2300
|
+
|
|
2301
|
+
# ── blocks (function body) ─────────────────────────────────────────────
|
|
2302
|
+
|
|
2303
|
+
def _block_body(self, stmts, out, path, header_line, block_state):
|
|
2304
|
+
self.cursor = header_line
|
|
2305
|
+
body_indent = self.src.indent_of_line(stmts[0].lineno)
|
|
2306
|
+
seq = self.origin.new_seq(path, "body", base=self._base, indent=body_indent,
|
|
2307
|
+
insert_at=self.src.line_start(stmts[0].lineno))
|
|
2308
|
+
self._body(stmts, out, path, seq, scope="function")
|
|
2309
|
+
self._consume_footer(len(body_indent))
|
|
2310
|
+
self.origin.seal_seq(seq)
|
|
2311
|
+
return seq
|
|
2312
|
+
|
|
2313
|
+
def _finish_block_item(self, key, path, seq, gap_start, core_start, indent, kind="block"):
|
|
2314
|
+
end = self.src.next_line_start(self.cursor)
|
|
2315
|
+
self.origin.add(Item(self._base, path + (key,), key, kind, (gap_start, end), (core_start, end), None, None,
|
|
2316
|
+
indent=indent), seq)
|
|
2317
|
+
|
|
2318
|
+
def _condition(self, test, branch_path, keyword):
|
|
2319
|
+
T = self.T
|
|
2320
|
+
sub_call = _k(test) == "Subscript" and _k(test.value) == "Call"
|
|
2321
|
+
call = test.value if sub_call else test
|
|
2322
|
+
if _k(call) == "Call":
|
|
2323
|
+
cond_key = f"{_call_func_name(call) or 'call'}()##{keyword}"
|
|
2324
|
+
else:
|
|
2325
|
+
cond_key = f"##{keyword}"
|
|
2326
|
+
if sub_call:
|
|
2327
|
+
inner = self._call(test.value, branch_path + (cond_key,), T.CallParse, None)
|
|
2328
|
+
value = inner if inner is not None else T.CodeLine(self._text(test))
|
|
2329
|
+
else:
|
|
2330
|
+
value = self._value(test, branch_path + (cond_key,))
|
|
2331
|
+
return cond_key, value
|
|
2332
|
+
|
|
2333
|
+
def _if_chain(self, node, out, path, seq, cond_counters, block_occ, gap_start, core_start, indent):
|
|
2334
|
+
T = self.T
|
|
2335
|
+
first = True
|
|
2336
|
+
while True:
|
|
2337
|
+
keyword = "if" if first else "elif"
|
|
2338
|
+
idx = cond_counters[keyword]
|
|
2339
|
+
cond_counters[keyword] += 1
|
|
2340
|
+
key = f"{keyword}##{idx}"
|
|
2341
|
+
branch_path = path + (key,)
|
|
2342
|
+
branch = T.Conditional(condition=key)
|
|
2343
|
+
cond_key, cond_value = self._condition(node.test, branch_path, keyword)
|
|
2344
|
+
branch[cond_key] = cond_value
|
|
2345
|
+
ts = self.src.node_span(node.test)
|
|
2346
|
+
self._record_child(branch, cond_key, cond_value, *ts)
|
|
2347
|
+
self.origin.add(Item(self._base, branch_path + (cond_key,), cond_key, "header", ts, ts, ts, cond_value))
|
|
2348
|
+
self._block_body(node.body, branch, branch_path, node.test.end_lineno, (cond_counters, block_occ))
|
|
2349
|
+
self._stamp(branch, ts[0], self.src.node_span(node.body[-1])[1])
|
|
2350
|
+
out[key] = branch
|
|
2351
|
+
self._finish_block_item(key, path, seq, gap_start, core_start, indent)
|
|
2352
|
+
first = False
|
|
2353
|
+
orelse = node.orelse
|
|
2354
|
+
if not orelse:
|
|
2355
|
+
return
|
|
2356
|
+
if (len(orelse) == 1 and _k(orelse[0]) == "If"
|
|
2357
|
+
and self.src.line_text(orelse[0].lineno).lstrip().startswith("elif")):
|
|
2358
|
+
node = orelse[0]
|
|
2359
|
+
gap_start = core_start = self.src.line_start(node.lineno)
|
|
2360
|
+
indent = self.src.indent_of_line(node.lineno)
|
|
2361
|
+
continue
|
|
2362
|
+
idx = cond_counters["else"]
|
|
2363
|
+
cond_counters["else"] += 1
|
|
2364
|
+
key = f"else##{idx}"
|
|
2365
|
+
branch_path = path + (key,)
|
|
2366
|
+
branch = T.Conditional(condition=key)
|
|
2367
|
+
kw_line = self._keyword_line(orelse, self.cursor)
|
|
2368
|
+
else_seq = self._block_body(orelse, branch, branch_path, kw_line, (cond_counters, block_occ))
|
|
2369
|
+
if branch:
|
|
2370
|
+
self._stamp(branch, self.src.node_span(orelse[0])[0], self.src.node_span(orelse[-1])[1])
|
|
2371
|
+
out[key] = branch
|
|
2372
|
+
gs = self.src.line_start(kw_line)
|
|
2373
|
+
self._finish_block_item(key, path, seq, gs, gs, self.src.indent_of_line(kw_line))
|
|
2374
|
+
else:
|
|
2375
|
+
self.origin.drop_seq(else_seq)
|
|
2376
|
+
return
|
|
2377
|
+
|
|
2378
|
+
def _for_loop(self, node, out, path, seq, block_occ, gap_start, core_start, indent):
|
|
2379
|
+
T = self.T
|
|
2380
|
+
target = self._text(node.target)
|
|
2381
|
+
it = self._text(node.iter)
|
|
2382
|
+
key = occ_key(f"for {target} in {it}", block_occ)
|
|
2383
|
+
loop_path = path + (key,)
|
|
2384
|
+
loop = T.Loop(target=target, iter=it)
|
|
2385
|
+
self._block_body(node.body, loop, loop_path, node.iter.end_lineno, None)
|
|
2386
|
+
range_args = self._range_args(node.iter, loop_path + ("range",))
|
|
2387
|
+
if range_args is not None:
|
|
2388
|
+
loop["range"] = range_args
|
|
2389
|
+
rs = (self.src.node_span(node.iter.args[0])[0], self.src.node_span(node.iter.args[-1])[1])
|
|
2390
|
+
self._record_child(loop, "range", range_args, *rs)
|
|
2391
|
+
self.origin.add(Item(self._base, loop_path + ("range",), "range", "value", rs, rs, rs, range_args))
|
|
2392
|
+
self._stamp(loop, *self.src.node_span(node))
|
|
2393
|
+
out[key] = loop
|
|
2394
|
+
self._finish_block_item(key, path, seq, gap_start, core_start, indent)
|
|
2395
|
+
if node.orelse:
|
|
2396
|
+
branch = T.Conditional(condition="else")
|
|
2397
|
+
kw_line = self._keyword_line(node.orelse, self.cursor)
|
|
2398
|
+
else_seq = self._block_body(node.orelse, branch, path + (f"{key} else",), kw_line, None)
|
|
2399
|
+
if branch:
|
|
2400
|
+
self._stamp(branch, self.src.node_span(node.orelse[0])[0], self.src.node_span(node.orelse[-1])[1])
|
|
2401
|
+
out[f"{key} else"] = branch
|
|
2402
|
+
gs = self.src.line_start(kw_line)
|
|
2403
|
+
self._finish_block_item(f"{key} else", path, seq, gs, gs, self.src.indent_of_line(kw_line))
|
|
2404
|
+
else:
|
|
2405
|
+
self.origin.drop_seq(else_seq)
|
|
2406
|
+
|
|
2407
|
+
def _range_args(self, iter_node, path):
|
|
2408
|
+
if not (_k(iter_node) == "Call" and _k(iter_node.func) == "Name"
|
|
2409
|
+
and iter_node.func.id == "range" and iter_node.args and not iter_node.keywords):
|
|
2410
|
+
return None
|
|
2411
|
+
args = []
|
|
2412
|
+
seq = self.origin.new_seq(path, "elements", base=self._base, sep=", ",
|
|
2413
|
+
insert_at=self.src.node_span(iter_node.args[0])[0])
|
|
2414
|
+
for i, a in enumerate(iter_node.args):
|
|
2415
|
+
v = self._literal(a)
|
|
2416
|
+
if v is UNRESOLVED:
|
|
2417
|
+
self.origin.drop_seq(seq)
|
|
2418
|
+
for j in range(i):
|
|
2419
|
+
self.origin.items.pop(path + (j,), None)
|
|
2420
|
+
return None
|
|
2421
|
+
args.append(v)
|
|
2422
|
+
sp = self.src.node_span(a)
|
|
2423
|
+
self.origin.add(Item(self._base, path + (i,), i, "element", sp, sp, sp, v), seq)
|
|
2424
|
+
self.origin.seal_seq(seq)
|
|
2425
|
+
return args
|
|
2426
|
+
|
|
2427
|
+
def _try_block(self, node, out, path, seq, block_occ, gap_start, core_start, indent):
|
|
2428
|
+
T = self.T
|
|
2429
|
+
try_key = occ_key("try", block_occ)
|
|
2430
|
+
body = T.Try(header="try")
|
|
2431
|
+
tseq = self._block_body(node.body, body, path + (try_key,), node.lineno, None)
|
|
2432
|
+
if body:
|
|
2433
|
+
self._stamp(body, self.src.node_span(node.body[0])[0], self.src.node_span(node.body[-1])[1])
|
|
2434
|
+
out[try_key] = body
|
|
2435
|
+
self._finish_block_item(try_key, path, seq, gap_start, core_start, indent)
|
|
2436
|
+
else:
|
|
2437
|
+
self.origin.drop_seq(tseq)
|
|
2438
|
+
star = _k(node) == "TryStar"
|
|
2439
|
+
for handler in node.handlers:
|
|
2440
|
+
parts = ["except*" if star else "except"]
|
|
2441
|
+
if handler.type is not None:
|
|
2442
|
+
parts.append(self._text(handler.type))
|
|
2443
|
+
if handler.name is not None:
|
|
2444
|
+
parts += ["as", handler.name]
|
|
2445
|
+
header = " ".join(parts)
|
|
2446
|
+
hkey = occ_key(header, block_occ)
|
|
2447
|
+
hbody = T.Except(header=header)
|
|
2448
|
+
hline = handler.type.end_lineno if handler.type is not None else handler.lineno
|
|
2449
|
+
hseq = self._block_body(handler.body, hbody, path + (hkey,), hline, None)
|
|
2450
|
+
if hbody:
|
|
2451
|
+
self._stamp(hbody, *self.src.node_span(handler))
|
|
2452
|
+
out[hkey] = hbody
|
|
2453
|
+
gs = self.src.line_start(handler.lineno)
|
|
2454
|
+
self._finish_block_item(hkey, path, seq, gs, gs, self.src.indent_of_line(handler.lineno))
|
|
2455
|
+
else:
|
|
2456
|
+
self.origin.drop_seq(hseq)
|
|
2457
|
+
if node.orelse:
|
|
2458
|
+
ekey = occ_key("try else", block_occ)
|
|
2459
|
+
ebody = T.Try(header="try else")
|
|
2460
|
+
kw_line = self._keyword_line(node.orelse, self.cursor)
|
|
2461
|
+
eseq = self._block_body(node.orelse, ebody, path + (ekey,), kw_line, None)
|
|
2462
|
+
if ebody:
|
|
2463
|
+
self._stamp(ebody, self.src.node_span(node.orelse[0])[0], self.src.node_span(node.orelse[-1])[1])
|
|
2464
|
+
out[ekey] = ebody
|
|
2465
|
+
gs = self.src.line_start(kw_line)
|
|
2466
|
+
self._finish_block_item(ekey, path, seq, gs, gs, self.src.indent_of_line(kw_line))
|
|
2467
|
+
else:
|
|
2468
|
+
self.origin.drop_seq(eseq)
|
|
2469
|
+
if node.finalbody:
|
|
2470
|
+
fkey = occ_key("finally", block_occ)
|
|
2471
|
+
fbody = T.Try(header="finally")
|
|
2472
|
+
kw_line = self._keyword_line(node.finalbody, self.cursor)
|
|
2473
|
+
fseq = self._block_body(node.finalbody, fbody, path + (fkey,), kw_line, None)
|
|
2474
|
+
if fbody:
|
|
2475
|
+
self._stamp(fbody, self.src.node_span(node.finalbody[0])[0], self.src.node_span(node.finalbody[-1])[1])
|
|
2476
|
+
out[fkey] = fbody
|
|
2477
|
+
gs = self.src.line_start(kw_line)
|
|
2478
|
+
self._finish_block_item(fkey, path, seq, gs, gs, self.src.indent_of_line(kw_line))
|
|
2479
|
+
else:
|
|
2480
|
+
self.origin.drop_seq(fseq)
|
|
2481
|
+
|
|
2482
|
+
# ── values ───────────────────────────────────────────────────────────────
|
|
2483
|
+
|
|
2484
|
+
def _text(self, node):
|
|
2485
|
+
s, e = self.src.node_span(node)
|
|
2486
|
+
return self.origin.text[s:e]
|
|
2487
|
+
|
|
2488
|
+
def _literal(self, node):
|
|
2489
|
+
k = _k(node)
|
|
2490
|
+
if k == "Constant" and isinstance(node.value, _SIMPLE_LITERAL_TYPES):
|
|
2491
|
+
return node.value
|
|
2492
|
+
if (k == "UnaryOp" and _k(node.op) in ("USub", "UAdd") and _k(node.operand) == "Constant"
|
|
2493
|
+
and isinstance(node.operand.value, (int, float)) and not isinstance(node.operand.value, bool)):
|
|
2494
|
+
return -node.operand.value if _k(node.op) == "USub" else node.operand.value
|
|
2495
|
+
return UNRESOLVED
|
|
2496
|
+
|
|
2497
|
+
def _value(self, node, path):
|
|
2498
|
+
T = self.T
|
|
2499
|
+
lit = self._literal(node)
|
|
2500
|
+
if lit is not UNRESOLVED:
|
|
2501
|
+
return lit
|
|
2502
|
+
k = _k(node)
|
|
2503
|
+
if k == "Subscript" and _k(node.value) == "Call":
|
|
2504
|
+
inner = self._call(node.value, path, T.CallParse, None)
|
|
2505
|
+
if inner is not None:
|
|
2506
|
+
return inner
|
|
2507
|
+
return T.CodeLine(self._text(node))
|
|
2508
|
+
if k == "Call":
|
|
2509
|
+
parsed = self._call(node, path, T.CallParse, None)
|
|
2510
|
+
return parsed if parsed is not None else T.CodeLine(self._text(node))
|
|
2511
|
+
if k in ("Tuple", "List"):
|
|
2512
|
+
if any(_k(e) == "Starred" for e in node.elts):
|
|
2513
|
+
return T.CodeLine(self._text(node))
|
|
2514
|
+
cstart = self.src.node_span(node)[0]
|
|
2515
|
+
seq = self.origin.new_seq(path, "elements", base=self._base, sep=", ",
|
|
2516
|
+
insert_at=cstart + (1 if self.origin.text[cstart] in "([{" else 0))
|
|
2517
|
+
values = []
|
|
2518
|
+
self._nested += 1
|
|
2519
|
+
for i, e in enumerate(node.elts):
|
|
2520
|
+
v = self._value(e, path + (i,))
|
|
2521
|
+
values.append(v)
|
|
2522
|
+
sp = self.src.node_span(e)
|
|
2523
|
+
self.origin.add(Item(self._base, path + (i,), i, "element", sp, sp, sp, v), seq)
|
|
2524
|
+
self._nested -= 1
|
|
2525
|
+
if len(seq.items) >= 2:
|
|
2526
|
+
seq.sep = self.origin.text[seq.items[0].extent[1]:seq.items[1].extent[0]]
|
|
2527
|
+
if seq.items:
|
|
2528
|
+
self.origin.seal_seq(seq)
|
|
2529
|
+
else:
|
|
2530
|
+
self.origin.drop_seq(seq)
|
|
2531
|
+
return tuple(values) if k == "Tuple" else values
|
|
2532
|
+
if k == "Dict":
|
|
2533
|
+
if any(kn is None for kn in node.keys):
|
|
2534
|
+
return T.CodeLine(self._text(node))
|
|
2535
|
+
keys = []
|
|
2536
|
+
for kn in node.keys:
|
|
2537
|
+
try:
|
|
2538
|
+
kv = _const_value(kn)
|
|
2539
|
+
hash(kv)
|
|
2540
|
+
except Exception:
|
|
2541
|
+
# Mode dictionaries use type expressions as keys. Keep
|
|
2542
|
+
# those expressions unevaluated while exposing their
|
|
2543
|
+
# nested kwargs to the same editable source tree.
|
|
2544
|
+
kv = T.CodeLine(self._text(kn))
|
|
2545
|
+
keys.append(kv)
|
|
2546
|
+
seq = self.origin.new_seq(path, "pairs", base=self._base, sep=", ",
|
|
2547
|
+
insert_at=self.src.node_span(node)[0] + 1)
|
|
2548
|
+
result = {}
|
|
2549
|
+
self._nested += 1
|
|
2550
|
+
for kv, knode, vnode in zip(keys, node.keys, node.values):
|
|
2551
|
+
self.origin.shadow(path + (kv,))
|
|
2552
|
+
v = self._value(vnode, path + (kv,))
|
|
2553
|
+
result[kv] = v
|
|
2554
|
+
ks, ke = self.src.node_span(knode)
|
|
2555
|
+
vs = self.src.node_span(vnode)
|
|
2556
|
+
self.origin.add(Item(self._base, path + (kv,), kv, "pair", (ks, vs[1]), (ks, vs[1]), vs, v), seq)
|
|
2557
|
+
self._nested -= 1
|
|
2558
|
+
if len(seq.items) >= 2:
|
|
2559
|
+
seq.sep = self.origin.text[seq.items[0].extent[1]:seq.items[1].extent[0]]
|
|
2560
|
+
if seq.items:
|
|
2561
|
+
self.origin.seal_seq(seq)
|
|
2562
|
+
else:
|
|
2563
|
+
self.origin.drop_seq(seq)
|
|
2564
|
+
return result
|
|
2565
|
+
if k == "Set":
|
|
2566
|
+
elements = []
|
|
2567
|
+
for e in node.elts:
|
|
2568
|
+
v = self._literal(e)
|
|
2569
|
+
if v is UNRESOLVED:
|
|
2570
|
+
return T.CodeLine(self._text(node))
|
|
2571
|
+
elements.append(v)
|
|
2572
|
+
return set(elements)
|
|
2573
|
+
if k in ("Name", "Attribute"):
|
|
2574
|
+
parts = _dotted_parts(node)
|
|
2575
|
+
if parts is not None:
|
|
2576
|
+
if T.resolve is None:
|
|
2577
|
+
return NameRef(self._text(node), parts) # worker: the main side resolves
|
|
2578
|
+
resolved = T.resolve(parts)
|
|
2579
|
+
if resolved is not UNRESOLVED:
|
|
2580
|
+
return resolved
|
|
2581
|
+
return T.CodeLine(self._text(node))
|
|
2582
|
+
|
|
2583
|
+
def _call(self, call, path, result_cls, pos_names_override, allow_empty=False):
|
|
2584
|
+
T = self.T
|
|
2585
|
+
readable = result_cls(source=self._text(call), func_name=_call_func_name(call))
|
|
2586
|
+
positional = [a for a in call.args if _k(a) != "Starred"]
|
|
2587
|
+
pending = None
|
|
2588
|
+
if pos_names_override is not None:
|
|
2589
|
+
pos_names = pos_names_override
|
|
2590
|
+
elif positional:
|
|
2591
|
+
parts = _dotted_parts(call.func)
|
|
2592
|
+
pos_names = None
|
|
2593
|
+
if parts is not None:
|
|
2594
|
+
if T.positional_names is None:
|
|
2595
|
+
pending = parts # worker: the main side binds the runtime signature
|
|
2596
|
+
else:
|
|
2597
|
+
pos_names = T.positional_names(parts)
|
|
2598
|
+
if pos_names is None:
|
|
2599
|
+
pos_names = [f"arg{i}" for i in range(len(positional))]
|
|
2600
|
+
if any(n in {kw.arg for kw in call.keywords} for n in pos_names):
|
|
2601
|
+
pos_names = None
|
|
2602
|
+
pending = None
|
|
2603
|
+
else:
|
|
2604
|
+
pos_names = None
|
|
2605
|
+
seq = self.origin.new_seq(path, "args", base=self._base, sep=", ")
|
|
2606
|
+
pos_idx = 0
|
|
2607
|
+
nested = self._nested > 0
|
|
2608
|
+
self._nested += 1
|
|
2609
|
+
for a in call.args:
|
|
2610
|
+
if _k(a) == "Starred":
|
|
2611
|
+
continue
|
|
2612
|
+
if pos_names is not None and pos_idx < len(pos_names):
|
|
2613
|
+
key = pos_names[pos_idx]
|
|
2614
|
+
v = self._value(a, path + (key,))
|
|
2615
|
+
readable[key] = v
|
|
2616
|
+
sp = self.src.node_span(a)
|
|
2617
|
+
self.origin.add(Item(self._base, path + (key,), key, "element", sp, sp, sp, v), seq)
|
|
2618
|
+
pos_idx += 1
|
|
2619
|
+
for kw in call.keywords:
|
|
2620
|
+
if kw.arg is None:
|
|
2621
|
+
continue
|
|
2622
|
+
v = self._value(kw.value, path + (kw.arg,))
|
|
2623
|
+
readable[kw.arg] = v
|
|
2624
|
+
vs = self.src.node_span(kw.value)
|
|
2625
|
+
ks = self.src.node_span(kw)[0]
|
|
2626
|
+
self.origin.add(Item(self._base, path + (kw.arg,), kw.arg, "kwarg", (ks, vs[1]), (ks, vs[1]), vs, v), seq)
|
|
2627
|
+
self._nested -= 1
|
|
2628
|
+
if len(seq.items) >= 2:
|
|
2629
|
+
seq.sep = self.origin.text[seq.items[0].extent[1]:seq.items[1].extent[0]]
|
|
2630
|
+
# Always anchored just inside the closing paren: the main side may drop
|
|
2631
|
+
# positionals a runtime signature doesn't name (`print(x)`) and re-seal.
|
|
2632
|
+
seq.insert_at = self.src.node_span(call)[1] - 1
|
|
2633
|
+
self.origin.seal_seq(seq)
|
|
2634
|
+
if not readable and not allow_empty:
|
|
2635
|
+
self.origin.drop_seq(seq)
|
|
2636
|
+
return None
|
|
2637
|
+
callee_span = self.src.node_span(call.func)
|
|
2638
|
+
self.origin.add(Item(self._base, path + ("__callee__",), "__callee__", "callee",
|
|
2639
|
+
callee_span, callee_span, callee_span, self._text(call.func)))
|
|
2640
|
+
if pos_names:
|
|
2641
|
+
readable["__pos_names__"] = list(pos_names)
|
|
2642
|
+
if pending is not None:
|
|
2643
|
+
readable._pos_pending = pending
|
|
2644
|
+
if not nested:
|
|
2645
|
+
self._stamp(readable, *self.src.node_span(call))
|
|
2646
|
+
return readable
|
|
2647
|
+
|
|
2648
|
+
|
|
2649
|
+
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
2650
|
+
# ║ Worker entry ║
|
|
2651
|
+
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
2652
|
+
|
|
2653
|
+
def extract(text, *, frontend, types, file_path=None, line_offset=0, module_header=True):
|
|
2654
|
+
"""(gp, origin) for `text` through one front end: "scan" (the tokenizer
|
|
2655
|
+
parser) or "ast" (Python's parser, the oracle)."""
|
|
2656
|
+
if frontend == "ast":
|
|
2657
|
+
tree = ast.parse(text)
|
|
2658
|
+
comments = scan_comments(text)
|
|
2659
|
+
else:
|
|
2660
|
+
tree, standalone, trailing = scan(text)
|
|
2661
|
+
comments = (standalone, trailing)
|
|
2662
|
+
origin = Origin(text)
|
|
2663
|
+
origin.file_path = file_path
|
|
2664
|
+
origin.line_offset = line_offset
|
|
2665
|
+
gp = Extractor(origin, types, comments, module_header=module_header).module(tree)
|
|
2666
|
+
return gp, origin
|
|
2667
|
+
|
|
2668
|
+
|
|
2669
|
+
def scan_extract(text, *, validate=False):
|
|
2670
|
+
"""The worker's job: scanner front end, neutral types, validated by
|
|
2671
|
+
ast.parse (whose SyntaxError is reported as data — nothing raises across
|
|
2672
|
+
the interpreter boundary). Returns ("ok", gp, origin) or
|
|
2673
|
+
("error", message, lineno, offset)."""
|
|
2674
|
+
if validate:
|
|
2675
|
+
try:
|
|
2676
|
+
ast.parse(text)
|
|
2677
|
+
except SyntaxError as e:
|
|
2678
|
+
return ("error", e.msg, e.lineno or 0, e.offset or 0)
|
|
2679
|
+
try:
|
|
2680
|
+
gp, origin = extract(text, frontend="scan", types=NEUTRAL_TYPES)
|
|
2681
|
+
except SyntaxError as e:
|
|
2682
|
+
return ("error", str(e.msg if hasattr(e, "msg") else e), getattr(e, "lineno", 0) or 0,
|
|
2683
|
+
getattr(e, "offset", 0) or 0)
|
|
2684
|
+
return ("ok", gp, origin)
|