Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 50 additions & 18 deletions desloppify/languages/typescript/fixers/syntax_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

from __future__ import annotations

from desloppify.languages.typescript.detectors.smells.helpers import scan_code
from desloppify.languages.typescript.detectors.smells.helpers import (
_strip_ts_comments,
scan_code,
)

_CHAR_DEPTH_DELTA: dict[str, tuple[str, int]] = {
"(": ("parens", 1),
Expand All @@ -14,33 +17,62 @@
}


def _iter_code_chars(
lines: list[str], start: int, stop: int
) -> tuple[int, str, bool]:
in_block_comment = False
for idx in range(start, stop):
line = lines[idx]
if in_block_comment:
close = line.find("*/")
if close == -1:
continue
line = line[close + 2 :]
in_block_comment = False

while True:
block_start = line.find("/*")
if block_start == -1:
break
block_end = line.find("*/", block_start + 2)
if block_end == -1:
line = line[:block_start]
in_block_comment = True
break
line = line[:block_start] + line[block_end + 2 :]

for _, ch, in_s in scan_code(_strip_ts_comments(line)):
yield idx, ch, in_s


def find_balanced_end(
lines: list[str], start: int, *, track: str = "parens", max_lines: int = 80
) -> int | None:
"""Find the line where brackets opened at *start* balance to zero."""
depths = {"parens": 0, "braces": 0, "brackets": 0}
for idx in range(start, min(start + max_lines, len(lines))):
for _, ch, in_s in scan_code(lines[idx]):
if in_s:
continue
delta_spec = _CHAR_DEPTH_DELTA.get(ch)
if delta_spec is None:
continue
key, delta = delta_spec
depths[key] += delta
if delta > 0:
continue
if track == "parens" and key == "parens" and depths["parens"] <= 0:
return idx
if track == "braces" and key == "braces" and depths["braces"] <= 0:
return idx
if track == "all" and key == "parens" and depths["parens"] <= 0:
return idx
stop = min(start + max_lines, len(lines))
for idx, ch, in_s in _iter_code_chars(lines, start, stop):
if in_s:
continue
delta_spec = _CHAR_DEPTH_DELTA.get(ch)
if delta_spec is None:
continue
key, delta = delta_spec
depths[key] += delta
if delta > 0:
continue
if track == "parens" and key == "parens" and depths["parens"] <= 0:
return idx
if track == "braces" and key == "braces" and depths["braces"] <= 0:
return idx
if track == "all" and key == "parens" and depths["parens"] <= 0:
return idx
return None


def extract_body_between_braces(text: str, search_after: str = "") -> str | None:
"""Extract content between the first ``{`` and its matching ``}``."""
text = _strip_ts_comments(text)
start_pos = 0
if search_after:
pos = text.find(search_after)
Expand Down
52 changes: 51 additions & 1 deletion desloppify/languages/typescript/tests/test_ts_fixers.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,25 @@ def test_returns_none_when_unbalanced(self):
lines = ["foo(\n", " bar\n"]
assert find_balanced_end(lines, 0, track="parens") is None

def test_ignores_closing_parens_in_line_comment(self):
"""Line comments must not terminate a multiline call early."""
lines = [
"console.log( // ))\n",
" '[DEBUG] value',\n",
" someVar,\n",
");\n",
]
assert find_balanced_end(lines, 0, track="parens") == 3

def test_ignores_closing_braces_in_block_comment(self):
"""Block comments must not terminate brace tracking early."""
lines = [
"if (x) { /* }} */\n",
" return 1;\n",
"}\n",
]
assert find_balanced_end(lines, 0, track="braces") == 2


class TestCommonExtractBody:
"""Tests for extract_body_between_braces()."""
Expand Down Expand Up @@ -127,6 +146,13 @@ def test_search_after_not_found_returns_none(self):
"""Returns None if search_after marker not found."""
assert extract_body_between_braces("no marker", search_after="=>") is None

def test_ignores_comment_braces_when_extracting_body(self):
"""Comment braces must not truncate the extracted body."""
text = "const f = () => { /* } */ return 42; }"
body = extract_body_between_braces(text, search_after="=>")
assert body is not None
assert "return 42;" in body


class TestCommonCollapseBlankLines:
"""Tests for collapse_blank_lines()."""
Expand Down Expand Up @@ -495,6 +521,31 @@ def test_remove_multiline_log(self, tmp_path):
assert "console.log" not in content
assert "return 1;" in content

def test_remove_multiline_log_ignores_comment_delimiters(self, tmp_path):
"""Comment delimiters on the opening line must not truncate removal."""
ts_file = tmp_path / "app.ts"
ts_file.write_text(
textwrap.dedent("""\
function foo() {
console.log( // ))
'[DEBUG] multi',
someVar
);
return 1;
}
""")
)
entries = [
{"file": str(ts_file), "line": 2, "tag": "DEBUG", "content": "console.log("}
]
result = fix_debug_logs(entries, dry_run=False)
assert len(result.entries) == 1
content = ts_file.read_text()
assert "console.log" not in content
assert "'[DEBUG] multi'" not in content
assert "someVar" not in content
assert "return 1;" in content

def test_removes_orphaned_debug_comment(self, tmp_path):
"""A preceding // DEBUG comment is removed along with the log."""
ts_file = tmp_path / "app.ts"
Expand Down Expand Up @@ -719,4 +770,3 @@ def test_dry_run(self, tmp_path):
_ = fix_unused_params(entries, dry_run=True)
assert ts_file.read_text() == original