mirror of
https://github.com/MG1937/ASC.git
synced 2026-10-02 00:25:28 +08:00
461 lines
17 KiB
Python
461 lines
17 KiB
Python
import unittest
|
|
from types import SimpleNamespace
|
|
|
|
from droidasc.asc_client.gui.app import AscGuiApp
|
|
from droidasc.asc_client.gui.runtime import build_find_query
|
|
from droidasc.asc_client.gui.text_utils import decode_java_unicode_escapes_with_ranges
|
|
from droidasc.asc_client.gui.widgets import EditorTab
|
|
from droidasc.asc_client.gui.source_edit import (
|
|
find_member_declaration,
|
|
linkable_member_spans,
|
|
member_reference_at_offset,
|
|
remap_ranges_after_replacements,
|
|
)
|
|
from droidasc.asc_core.findrefs.findrefs_manager import FindRefManager
|
|
from droidasc.asc_core.utils.decompiler import _source_with_member_references
|
|
from droidasc.asc_core.utils.tinydex import DEX
|
|
|
|
|
|
class _Value:
|
|
def __init__(self, value=None):
|
|
self.value = value
|
|
|
|
def get(self):
|
|
return self.value
|
|
|
|
def set(self, value):
|
|
self.value = value
|
|
|
|
|
|
class _NavigationText:
|
|
def __init__(self, click_index="1.0"):
|
|
self.click_index = click_index
|
|
self.insert = "1.0"
|
|
self.seen = []
|
|
self.tags = []
|
|
|
|
def index(self, value):
|
|
if str(value).startswith("@"):
|
|
return self.click_index
|
|
if value == "insert":
|
|
return self.insert
|
|
return value
|
|
|
|
def mark_set(self, _mark, value):
|
|
self.insert = value
|
|
|
|
def see(self, value):
|
|
self.seen.append(value)
|
|
|
|
def yview(self):
|
|
return (0.5, 0.75)
|
|
|
|
def tag_remove(self, name, start, end):
|
|
self.tags.append(("remove", name, start, end))
|
|
|
|
def tag_raise(self, name):
|
|
self.tags.append(("raise", name))
|
|
|
|
|
|
class _FindWidget:
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
def grid(self):
|
|
self.calls.append("grid")
|
|
|
|
def focus_set(self):
|
|
self.calls.append("focus")
|
|
|
|
def selection_range(self, start, end):
|
|
self.calls.append(("selection", start, end))
|
|
|
|
|
|
def _text_index(text, offset):
|
|
line = text.count("\n", 0, offset) + 1
|
|
line_start = text.rfind("\n", 0, offset) + 1
|
|
return f"{line}.{offset - line_start}"
|
|
|
|
|
|
def _make_app(source, references, offset):
|
|
tab = EditorTab(
|
|
dalvik_class="Lexample/Test;",
|
|
title="Test",
|
|
source=source,
|
|
loading=False,
|
|
member_references=references,
|
|
)
|
|
started = []
|
|
app = SimpleNamespace(
|
|
_active_tab=lambda: tab,
|
|
source_text=SimpleNamespace(index=lambda _mark: _text_index(source, offset)),
|
|
search_type_var=_Value(),
|
|
search_value_var=_Value(),
|
|
search_class_var=_Value(),
|
|
fuzzy_class_var=_Value(True),
|
|
status_var=_Value(),
|
|
_on_search_type_changed=lambda: None,
|
|
_start_search=lambda **kwargs: started.append(kwargs),
|
|
)
|
|
app._member_at_index = lambda index: AscGuiApp._member_at_index(app, index)
|
|
return app, started
|
|
|
|
|
|
class SourceEditTests(unittest.TestCase):
|
|
def test_ctrl_f_clears_member_underlines_before_moving_focus(self):
|
|
calls = []
|
|
frame = _FindWidget()
|
|
entry = _FindWidget()
|
|
app = SimpleNamespace(
|
|
_hide_member_links=lambda: calls.append("hide-links"),
|
|
editor_find_frame=frame,
|
|
editor_find_entry=entry,
|
|
editor_find_var=_Value("needle"),
|
|
_last_editor_find_text="",
|
|
_refresh_editor_find_marks=lambda reset_cursor: calls.append(("refresh", reset_cursor)),
|
|
)
|
|
|
|
result = AscGuiApp._show_editor_find(app)
|
|
|
|
self.assertEqual(result, "break")
|
|
self.assertEqual(calls[0], "hide-links")
|
|
self.assertEqual(frame.calls, ["grid"] )
|
|
self.assertEqual(entry.calls[0], "focus")
|
|
|
|
def test_rendered_member_ranges_follow_inserted_line_comments(self):
|
|
source = "void first() {}\nvoid second() {}\n"
|
|
first = source.index("first")
|
|
second = source.index("second")
|
|
tab = EditorTab(
|
|
dalvik_class="Lexample/Test;", title="Test", source=source, loading=False,
|
|
comments={1: "note"},
|
|
member_references=[
|
|
(first, first + 5, "method", "Lexample/Test;", "first", "()V", True),
|
|
(second, second + 6, "method", "Lexample/Test;", "second", "()V", True),
|
|
],
|
|
)
|
|
|
|
rendered, _comment_spans = AscGuiApp._render_tab_source(SimpleNamespace(), tab)
|
|
|
|
self.assertEqual(
|
|
rendered[tab.rendered_member_references[0][0]:tab.rendered_member_references[0][1]],
|
|
"first",
|
|
)
|
|
self.assertEqual(
|
|
rendered[tab.rendered_member_references[1][0]:tab.rendered_member_references[1][1]],
|
|
"second",
|
|
)
|
|
|
|
def test_ctrl_click_passes_exact_target_to_navigation(self):
|
|
target = (4, 8, "method", "Lexample/Target;", "call", "(I)V", False)
|
|
opened = []
|
|
completed = []
|
|
app = SimpleNamespace(
|
|
source_text=_NavigationText("1.5"),
|
|
_member_at_index=lambda _index: target,
|
|
store=SimpleNamespace(class_to_dex={"Lexample/Target;": "classes.dex"}),
|
|
status_var=_Value(),
|
|
_pending_member_navigation=None,
|
|
open_class=lambda class_name: opened.append(class_name),
|
|
_complete_pending_member_navigation=lambda class_name: completed.append(class_name),
|
|
)
|
|
|
|
result = AscGuiApp._open_member_from_click(app, SimpleNamespace(x=10, y=20))
|
|
|
|
self.assertEqual(result, "break")
|
|
self.assertEqual(
|
|
app._pending_member_navigation,
|
|
("Lexample/Target;", "method", "call", "(I)V"),
|
|
)
|
|
self.assertEqual(opened, ["Lexample/Target;"] )
|
|
self.assertEqual(completed, ["Lexample/Target;"] )
|
|
|
|
def test_pending_navigation_moves_to_exact_declaration(self):
|
|
wrong = (12, 15, "method", "Lexample/Target;", "run", "()V", True)
|
|
target = (40, 43, "method", "Lexample/Target;", "run", "(I)V", True)
|
|
tab = EditorTab(
|
|
dalvik_class="Lexample/Target;", title="Target", source="x" * 80,
|
|
loading=False, rendered_member_references=[wrong, target],
|
|
)
|
|
text = _NavigationText()
|
|
app = SimpleNamespace(
|
|
_pending_member_navigation=("Lexample/Target;", "method", "run", "(I)V"),
|
|
editor_tabs=[tab],
|
|
_find_tab_index=lambda _class_name: 0,
|
|
source_text=text,
|
|
_highlight_active_line=lambda: None,
|
|
_highlight_related_identifier=lambda _index: None,
|
|
status_var=_Value(),
|
|
)
|
|
|
|
AscGuiApp._complete_pending_member_navigation(app, "Lexample/Target;")
|
|
|
|
self.assertIsNone(app._pending_member_navigation)
|
|
self.assertEqual(text.insert, "1.0+40c")
|
|
self.assertEqual(text.seen, ["1.0+40c"] )
|
|
self.assertEqual(tab.insert_index, "1.0+40c")
|
|
|
|
def test_ctrl_underline_marks_only_linkable_member_ranges(self):
|
|
references = [
|
|
(1, 4, "method", "Llocal/Target;", "run", "()V", False),
|
|
(8, 13, "field", "Ljava/lang/System;", "value", "I", False),
|
|
]
|
|
tab = EditorTab(
|
|
dalvik_class="Llocal/Source;", title="Source", source="x" * 20,
|
|
loading=False, rendered_member_references=references,
|
|
)
|
|
applied = []
|
|
text = _NavigationText()
|
|
app = SimpleNamespace(
|
|
_ctrl_member_links_visible=False,
|
|
source_text=text,
|
|
_active_tab=lambda: tab,
|
|
store=SimpleNamespace(class_to_dex={"Llocal/Target;": "classes.dex"}),
|
|
_tag_ranges=lambda name, spans: applied.append((name, spans)),
|
|
)
|
|
|
|
AscGuiApp._show_member_links(app)
|
|
|
|
self.assertTrue(app._ctrl_member_links_visible)
|
|
self.assertEqual(applied, [("member_link", [(1, 4)])])
|
|
self.assertIn(("raise", "member_link"), text.tags)
|
|
|
|
def test_member_hit_requires_cursor_on_member_name(self):
|
|
reference = (10, 14, "method", "LTarget;", "call", "()V", False)
|
|
self.assertEqual(member_reference_at_offset([reference], 10), reference)
|
|
self.assertEqual(member_reference_at_offset([reference], 13), reference)
|
|
self.assertIsNone(member_reference_at_offset([reference], 9))
|
|
self.assertIsNone(member_reference_at_offset([reference], 14))
|
|
|
|
def test_navigation_selects_exact_method_overload(self):
|
|
declarations = [
|
|
(10, 13, "method", "LTarget;", "run", "()V", True),
|
|
(30, 33, "method", "LTarget;", "run", "(I)V", True),
|
|
]
|
|
self.assertEqual(
|
|
find_member_declaration(declarations, "method", "run", "(I)V"),
|
|
declarations[1],
|
|
)
|
|
|
|
def test_navigation_selects_exact_field_type(self):
|
|
declarations = [
|
|
(10, 15, "field", "LTarget;", "value", "I", True),
|
|
]
|
|
self.assertEqual(
|
|
find_member_declaration(declarations, "field", "value", "I"),
|
|
declarations[0],
|
|
)
|
|
self.assertIsNone(find_member_declaration(
|
|
declarations, "field", "value", "Ljava/lang/String;"
|
|
))
|
|
|
|
def test_ctrl_underline_only_marks_members_in_apk(self):
|
|
references = [
|
|
(1, 4, "method", "Llocal/Target;", "run", "()V", False),
|
|
(8, 13, "field", "Ljava/lang/System;", "value", "I", False),
|
|
]
|
|
self.assertEqual(
|
|
linkable_member_spans(references, {"Llocal/Target;": "classes.dex"}),
|
|
[(1, 4)],
|
|
)
|
|
|
|
def test_real_this_field_method_chain_maps_both_members(self):
|
|
from dex_fixture import make_field_invoke_chain_dex
|
|
from droidasc.asc_client.asc_handler import AscHandler
|
|
|
|
source, references = AscHandler().getclass_with_metadata(
|
|
make_field_invoke_chain_dex(), "Lexample/Test;"
|
|
)
|
|
field = next(item for item in references if item[2:5] == (
|
|
"field", "Lexample/Test;", "a"
|
|
))
|
|
method = next(item for item in references if item[2:5] == (
|
|
"method", "Lexample/Target;", "callToMethod"
|
|
))
|
|
self.assertEqual(source[field[0]:field[1]], "a")
|
|
self.assertEqual(source[method[0]:method[1]], "callToMethod")
|
|
|
|
def test_rename_keeps_later_member_reference_offsets_aligned(self):
|
|
source = "int v1 = 0; target.call();"
|
|
call_start = source.index("call")
|
|
references = [(call_start, call_start + 4, "method", "LTarget;", "call", "()V", False)]
|
|
remapped = remap_ranges_after_replacements(
|
|
references, [(source.index("v1"), source.index("v1") + 2)], len("value")
|
|
)
|
|
new_source = source.replace("v1", "value")
|
|
self.assertEqual(new_source[remapped[0][0]:remapped[0][1]], "call")
|
|
|
|
def test_unicode_decoding_keeps_method_reference_offsets_aligned(self):
|
|
source = '"\\u4f60"; void run() {}'
|
|
start = source.index("run")
|
|
decoded, references = decode_java_unicode_escapes_with_ranges(
|
|
source, [(start, start + 3, "method", "Lexample/Test;", "run", "()V", True)]
|
|
)
|
|
ref_start, ref_end, *_metadata = references[0]
|
|
self.assertEqual(decoded[ref_start:ref_end], "run")
|
|
|
|
def test_real_dex_invocation_metadata_has_target_class(self):
|
|
from dex_fixture import make_invoke_dex
|
|
from droidasc.asc_client.asc_handler import AscHandler
|
|
|
|
source, references = AscHandler().getclass_with_metadata(
|
|
make_invoke_dex(), "Lexample/Test;"
|
|
)
|
|
invocation = next(item for item in references if item[4] == "callToMethod")
|
|
self.assertEqual(source[invocation[0]:invocation[1]], "callToMethod")
|
|
self.assertEqual(
|
|
invocation[2:], ("method", "Lexample/Target;", "callToMethod", "()V", False)
|
|
)
|
|
|
|
def test_real_dex_static_field_metadata_has_declaring_class(self):
|
|
from dex_fixture import make_static_field_dex
|
|
from droidasc.asc_client.asc_handler import AscHandler
|
|
|
|
source, references = AscHandler().getclass_with_metadata(
|
|
make_static_field_dex(), "Lexample/Statics;"
|
|
)
|
|
matches = [item for item in references if item[4] == "SECOND"]
|
|
self.assertEqual(len(matches), 2)
|
|
self.assertTrue(all(source[item[0]:item[1]] == "SECOND" for item in matches))
|
|
self.assertTrue(all(item[2:6] == (
|
|
"field", "Lexample/Statics;", "SECOND", "I"
|
|
) for item in matches))
|
|
|
|
def test_real_dex_instance_field_metadata_has_declaring_class(self):
|
|
from dex_fixture import make_instance_field_dex
|
|
from droidasc.asc_client.asc_handler import AscHandler
|
|
|
|
source, references = AscHandler().getclass_with_metadata(
|
|
make_instance_field_dex(), "Lexample/Test;"
|
|
)
|
|
matches = [item for item in references if item[2] == "field" and item[4] == "a"]
|
|
self.assertEqual(len(matches), 2)
|
|
self.assertTrue(all(source[item[0]:item[1]] == "a" for item in matches))
|
|
self.assertTrue(all(item[3] == "Lexample/Test;" for item in matches))
|
|
|
|
def test_source_metadata_uses_declaration_and_invoke_classes(self):
|
|
declared = SimpleNamespace(
|
|
cls_name="Lexample/Test;", name="run",
|
|
triple=("example/Test", "run", "()V"),
|
|
)
|
|
invoked = SimpleNamespace(
|
|
cls="example.Target",
|
|
name="callToMethod",
|
|
triple=("example/Target", "callToMethod", "()V"),
|
|
)
|
|
source, references = _source_with_member_references([
|
|
("METHOD", [
|
|
("NAME_METHOD_PROTOTYPE", "run", declared),
|
|
("TEXT", " { this.a."),
|
|
("NAME_METHOD_INVOKE", "callToMethod", "ignored", None, None, None, invoked),
|
|
("TEXT", "(); }"),
|
|
]),
|
|
])
|
|
|
|
self.assertEqual(source, "run { this.a.callToMethod(); }")
|
|
self.assertEqual(references, [
|
|
(0, 3, "method", "Lexample/Test;", "run", "()V", True),
|
|
(13, 25, "method", "Lexample/Target;", "callToMethod", "()V", False),
|
|
])
|
|
|
|
def test_x_on_method_declaration_searches_current_class(self):
|
|
source = "public void run()\n{\n return;\n}\n"
|
|
start = source.index("run")
|
|
app, started = _make_app(
|
|
source, [(start, start + 3, "method", "Lexample/Test;", "run", "()V", True)], start + 1
|
|
)
|
|
|
|
result = AscGuiApp._find_current_member_references(app)
|
|
|
|
self.assertEqual(result, "break")
|
|
self.assertEqual(app.search_value_var.get(), "run")
|
|
self.assertEqual(app.search_class_var.get(), "Lexample/Test;")
|
|
self.assertEqual(started, [{"exact_member": True}])
|
|
|
|
def test_x_on_invocation_uses_invoked_method_class(self):
|
|
source = "public void run() { this.a.callToMethod(); }"
|
|
start = source.index("callToMethod")
|
|
app, started = _make_app(
|
|
source,
|
|
[(start, start + len("callToMethod"), "method", "Lexample/Target;", "callToMethod", "()V", False)],
|
|
start + 2,
|
|
)
|
|
|
|
AscGuiApp._find_current_member_references(app)
|
|
|
|
self.assertEqual(app.search_value_var.get(), "callToMethod")
|
|
self.assertEqual(app.search_class_var.get(), "Lexample/Target;")
|
|
self.assertEqual(started, [{"exact_member": True}])
|
|
|
|
def test_x_on_field_access_uses_declaring_field_class(self):
|
|
source = "public void run() { this.a = target.value; }"
|
|
start = source.index("value")
|
|
app, started = _make_app(
|
|
source,
|
|
[(start, start + len("value"), "field", "Lexample/Target;", "value", "I", False)],
|
|
start + 2,
|
|
)
|
|
|
|
AscGuiApp._find_current_member_references(app)
|
|
|
|
self.assertEqual(app.search_type_var.get(), "field refs")
|
|
self.assertEqual(app.search_value_var.get(), "value")
|
|
self.assertEqual(app.search_class_var.get(), "Lexample/Target;")
|
|
self.assertEqual(started, [{"exact_member": True}])
|
|
|
|
def test_x_inside_method_but_not_on_method_name_does_not_search(self):
|
|
source = "public void run()\n{\n return;\n}\n"
|
|
start = source.index("run")
|
|
app, started = _make_app(
|
|
source, [(start, start + 3, "method", "Lexample/Test;", "run", "()V", True)], source.index("return")
|
|
)
|
|
|
|
AscGuiApp._find_current_member_references(app)
|
|
|
|
self.assertEqual(started, [])
|
|
self.assertEqual(
|
|
app.status_var.get(),
|
|
"Place the cursor on a method or field name to find its references",
|
|
)
|
|
|
|
def test_x_uses_exact_method_name_query(self):
|
|
find_type, query = build_find_query(
|
|
"method refs", "foo", "Lexample/Test;", exact_member=True
|
|
)
|
|
self.assertEqual(find_type, "method")
|
|
self.assertEqual(query, {
|
|
"method": {"class": ["Lexample/Test;", True], "method": ["foo", True]}
|
|
})
|
|
|
|
def test_exact_method_locator_does_not_use_substring_matching(self):
|
|
from dex_fixture import make_dex
|
|
|
|
dex = DEX.parse(memoryview(make_dex()), "fixture.dex")
|
|
locator = FindRefManager(dex)._get_method_locator(True)
|
|
self.assertEqual(locator.locate({
|
|
"class": ["Lexample/Test;", True],
|
|
"method": ["fir", True],
|
|
}), set())
|
|
self.assertEqual(locator.locate({
|
|
"class": ["Lexample/Test;", True],
|
|
"method": ["first", True],
|
|
}), {0})
|
|
|
|
def test_exact_field_locator_does_not_use_substring_matching(self):
|
|
from dex_fixture import make_static_field_dex
|
|
|
|
dex = DEX.parse(memoryview(make_static_field_dex()), "fixture.dex")
|
|
locator = FindRefManager(dex)._get_field_locator(True)
|
|
self.assertEqual(locator.locate({
|
|
"class": ["Lexample/Statics;", True],
|
|
"field": ["SEC", True],
|
|
}), set())
|
|
self.assertEqual(locator.locate({
|
|
"class": ["Lexample/Statics;", True],
|
|
"field": ["SECOND", True],
|
|
}), {1})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|