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
76 changes: 76 additions & 0 deletions src/sentry/issues/auto_source_code_config/directory_prefixes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from __future__ import annotations

from collections import Counter
from collections.abc import Sequence
from typing import NamedTuple

DEFAULT_PREFIX_CAP = 200
_SLASH = "/"
_BACKSLASH = "\\"


class RankedPrefix(NamedTuple):
path: str
file_count: int


def directory_prefixes(path: str) -> list[str]:
scheme, remainder = _peel_scheme(path)
sep = _detect_separator(remainder)
prefixes = _prefix_slices(remainder, sep)

if scheme:
# Emit the bare scheme first; prepend it to each later prefix so each
# result is a literal slice of the original frame path.
return [scheme] + [scheme + p for p in prefixes]
return prefixes


def rank_directory_prefixes(
paths: Sequence[str], *, cap: int = DEFAULT_PREFIX_CAP
) -> list[RankedPrefix]:
counts: Counter[str] = Counter()
seen: set[str] = set()
for path in paths:
# src/foo.py and src\foo.py are different stack roots; treat as distinct.
if path in seen:
continue
seen.add(path)
counts.update(directory_prefixes(path))

ranked = sorted(counts.items(), key=_rank_key)
return [RankedPrefix(path, count) for path, count in ranked[:cap]]


def _peel_scheme(path: str) -> tuple[str, str]:
# Cuts inside scheme:/// (e.g. app:/, webpack:/) are not valid prefix options.
idx = path.find(":///")
if idx > 0:
scheme = path[: idx + 4]
return scheme, path[idx + 4 :]
return "", path


def _detect_separator(path: str) -> str:
# Keep the path's own separator so each prefix is a literal slice of the
# original frame, which is what startswith checks in code_mapping.py.
return _SLASH if _SLASH in path else _BACKSLASH


def _prefix_slices(path: str, sep: str) -> list[str]:
prefixes = []
for i, ch in enumerate(path):
if ch != sep:
continue
prefix = path[: i + 1]
# Skip a prefix made entirely of separators — no directory content to suggest.
if prefix.strip(sep):
prefixes.append(prefix)
return prefixes


def _rank_key(item: tuple[str, int]) -> tuple[int, int, str]:
path, count = item
# Count both separators so src\foo\ ranks deeper than src\ when counts tie.
depth = path.count(_SLASH) + path.count(_BACKSLASH)
return (-count, depth, path)
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from sentry.issues.auto_source_code_config.directory_prefixes import (
DEFAULT_PREFIX_CAP,
directory_prefixes,
rank_directory_prefixes,
)


class TestDirectoryPrefixes:
def test_relative_path(self) -> None:
assert directory_prefixes("src/sentry/web/frontend/views.py") == [
"src/",
"src/sentry/",
"src/sentry/web/",
"src/sentry/web/frontend/",
]

def test_absolute_path(self) -> None:
assert directory_prefixes("/usr/src/app/foo.py") == [
"/usr/",
"/usr/src/",
"/usr/src/app/",
]

def test_filename_only_and_empty(self) -> None:
assert directory_prefixes("views.py") == []
assert directory_prefixes("") == []

def test_windows_separators(self) -> None:
result = directory_prefixes("src\\foo\\bar.py")
assert result == ["src\\", "src\\foo\\"]
assert "src\\foo\\bar.py".startswith(result[0])
assert "src\\foo\\bar.py".startswith(result[1])

def test_root_file_has_no_prefix(self) -> None:
assert directory_prefixes("/foo.py") == []

def test_app_scheme_preserves_triple_slash(self) -> None:
result = directory_prefixes("app:///src/index.tsx")
assert result == ["app:///", "app:///src/"]
assert "app:/" not in result

def test_app_scheme_file_at_root(self) -> None:
assert directory_prefixes("app:///index.tsx") == ["app:///"]

def test_webpack_scheme(self) -> None:
result = directory_prefixes("webpack:///src/index.js")
assert result == ["webpack:///", "webpack:///src/"]
assert all("webpack:///src/index.js".startswith(p) for p in result)

def test_double_slash_path(self) -> None:
result = directory_prefixes("//usr/src/foo.py")
assert result == ["//usr/", "//usr/src/"]
assert all("//usr/src/foo.py".startswith(p) for p in result)

def test_unc_path(self) -> None:
result = directory_prefixes("\\\\server\\share\\file.py")
assert result == ["\\\\server\\", "\\\\server\\share\\"]
assert all("\\\\server\\share\\file.py".startswith(p) for p in result)


class TestRankDirectoryPrefixes:
def test_ranks_by_file_count(self) -> None:
ranked = rank_directory_prefixes(
[
"src/sentry/web/views.py",
"src/sentry/utils/foo.py",
"tests/sentry/test_web.py",
]
)
by_path = {item.path: item.file_count for item in ranked}

assert by_path["src/"] == 2
assert by_path["src/sentry/"] == 2
assert by_path["tests/"] == 1
assert by_path["src/sentry/web/"] == 1

top = ranked[:2]
assert {item.path for item in top} == {"src/", "src/sentry/"}
assert all(item.file_count == 2 for item in top)
assert all(item.file_count == 1 for item in ranked[2:])

def test_duplicate_files_count_once(self) -> None:
# a/b.py deduped to 1 instance; a\b.py is a distinct path, not a duplicate
ranked = rank_directory_prefixes(["a/b.py", "a/b.py", "a\\b.py"])
by_path = {item.path: item.file_count for item in ranked}
assert by_path["a/"] == 1
assert by_path["a\\"] == 1

def test_cap_keeps_highest_counts(self) -> None:
paths = [f"common/dir{index}/file.py" for index in range(DEFAULT_PREFIX_CAP + 50)]
ranked = rank_directory_prefixes(paths)

assert len(ranked) == DEFAULT_PREFIX_CAP
assert ranked[0].path == "common/"
assert ranked[0].file_count == DEFAULT_PREFIX_CAP + 50
assert min(item.file_count for item in ranked) >= 1
Loading