diff --git a/bex/tag_preprocessor/analyze.py b/bex/tag_preprocessor/analyze.py index 231808d..631faa5 100644 --- a/bex/tag_preprocessor/analyze.py +++ b/bex/tag_preprocessor/analyze.py @@ -58,6 +58,48 @@ def _match_glob(filepath, pattern): return spec.match_file(filepath) +def _file_to_package(fp, ext): + """Infer the package/directory from a file path. + + For Kotlin/Java: derives from `src/main/kotlin/` or `src/test/kotlin/` tree. + For other: shows the relative directory path. + """ + p = PurePath(fp) + try: + idx = p.parts.index("kotlin") + return "/".join(p.parts[idx + 1:-1]) + except ValueError: + pass + try: + idx = p.parts.index("java") + return "/".join(p.parts[idx + 1:-1]) + except ValueError: + pass + try: + idx = p.parts.index("python") + return "/".join(p.parts[idx + 1:-1]) + except ValueError: + pass + # Fallback: show parent dirs relative to first language-agnostic src + parts = p.parts + for keyword in ("src", "lib", "app"): + try: + idx = parts.index(keyword) + return "/".join(parts[idx:-1]) + except ValueError: + continue + return str(p.parent) + + +def _top_packages(file_paths, ext, top_n=3): + """Return the most common packages among a set of files.""" + pkg_counts = Counter() + for fp in file_paths: + pkg = _file_to_package(fp, ext) + pkg_counts[pkg] += 1 + return [pkg for pkg, _ in pkg_counts.most_common(top_n)] + + def _build_arg_patterns(file_paths): """Extract merged argument patterns across a set of files.""" merged = {} @@ -245,7 +287,8 @@ def analyze_clusters(file_paths, extension, min_coverage=0.2, prefer=None, kmax= arg_patterns = _build_arg_patterns(cluster_fps) symbol_seqs = [[text for _, text, _ in seq] for seq in cluster_seqs] result = infer_ensemble(symbol_seqs, kmax=kmax, N=N, prefer=prefer) - meta = {"files": cluster_fps, "imports": imports, "arg_patterns": arg_patterns} + packages = _top_packages(cluster_fps, extension) + meta = {"files": cluster_fps, "imports": imports, "arg_patterns": arg_patterns, "packages": packages} results.append((label, result, len(cluster_seqs), meta)) return results diff --git a/tests/test_analyze.py b/tests/test_analyze.py index 99135a5..724fea4 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -1,6 +1,6 @@ """Tests for tag-preprocessor orchestrator (analyze.py).""" -from pathlib import Path +from pathlib import Path, PurePath import tempfile import sys @@ -8,7 +8,9 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from bex.tag_preprocessor.analyze import ( scan_directory, frequency_filter, infer, analyze_directory, _match_glob, + _file_to_package, _top_packages, _extract_imports, ) +from bex.tag_preprocessor.code import _summarize_arg_info, _classify_arg_node def test_scan_directory_empty(): @@ -161,6 +163,104 @@ def test_infer_low_coverage_filters_noise(): print(" PASS test_infer_low_coverage_filters_noise") +def test_file_to_package(): + assert _file_to_package( + "/repo/src/main/kotlin/com/example/app/Foo.kt", ".kt" + ) == "com/example/app" + assert _file_to_package( + "/repo/src/test/java/com/example/FooTest.java", ".java" + ) == "com/example" + assert _file_to_package( + "/repo/src/main/python/mypackage/module.py", ".py" + ) == "mypackage" + assert _file_to_package("/repo/lib/foo.py", ".py") == "lib" + print(" PASS test_file_to_package") + + +def test_top_packages(): + fps = { + "/repo/src/main/kotlin/com/example/a/Foo.kt", + "/repo/src/main/kotlin/com/example/a/Bar.kt", + "/repo/src/main/kotlin/com/example/b/Baz.kt", + } + pkgs = _top_packages(fps, ".kt") + assert pkgs[0] == "com/example/a" + assert pkgs[1] == "com/example/b" + print(" PASS test_top_packages") + + +def test_extract_imports(): + with tempfile.TemporaryDirectory() as td: + d = Path(td) + (d / "a.kt").write_text( + "package com.example\nimport io.mockk.every\nimport io.mockk.verify\n\nclass Foo" + ) + (d / "b.kt").write_text( + "package com.example\nimport org.junit.Test\n\nclass Bar" + ) + result = _extract_imports({str(d / "a.kt"), str(d / "b.kt")}) + assert "import io.mockk.every" in result + assert "import io.mockk.verify" in result + assert "import org.junit.Test" in result + assert len(result) == 3 + print(" PASS test_extract_imports") + + +def test_extract_imports_empty(): + assert _extract_imports(set()) == [] + print(" PASS test_extract_imports_empty") + + +def test_extract_imports_no_imports(): + with tempfile.TemporaryDirectory() as td: + d = Path(td) + (d / "a.py").write_text("x = 1\ny = 2") + result = _extract_imports({str(d / "a.py")}) + assert result == [] + print(" PASS test_extract_imports_no_imports") + + +def test_extract_arg_info_python(): + with tempfile.TemporaryDirectory() as td: + d = Path(td) + (d / "a.py").write_text( + "def test_foo():\n" + " result = compute(42)\n" + " assert result == 0\n" + " run(mock, times=1)\n" + ) + from bex.tag_preprocessor.code import extract_arg_info + fp = str(d / "a.py") + code = Path(fp).read_text() + info = extract_arg_info(fp, code) + # compute(42) -> 1 arg: literal + assert "compute" in info + obs = info["compute"] + assert any(n == 1 and ts == ("lit",) for n, ts in obs) + # run(mock, times=1) -> 2 args: var + kwarg + assert "run" in info + obs2 = info["run"] + assert any(n == 2 for n, ts in obs2) + assert any(ts == ("var", "kwarg") for n, ts in obs2) + print(" PASS test_extract_arg_info_python") + + +def test_summarize_arg_info(): + info = { + "foo": [(2, ("var", "lit")), (2, ("var", "lit")), (3, ("var", "lit", "lit"))], + "bar": [(0, ()), (1, ("lambda",))], + } + summary = _summarize_arg_info(info) + assert summary["foo"]["occurrences"] == 3 + assert summary["foo"]["arg_count"]["min"] == 2 + assert summary["foo"]["arg_count"]["max"] == 3 + assert summary["foo"]["arg_count"]["common"] == 2 + assert len(summary["foo"]["patterns"]) == 2 + assert summary["bar"]["occurrences"] == 2 + assert summary["bar"]["arg_count"]["min"] == 0 + print(" PASS test_summarize_arg_info") + + def run_all(): tests = [ test_scan_directory_empty, @@ -175,6 +275,13 @@ def run_all(): test_frequency_filter_edge_empty_sequences, test_infer_returns_ensemble_dict, test_infer_low_coverage_filters_noise, + test_file_to_package, + test_top_packages, + test_extract_imports, + test_extract_imports_empty, + test_extract_imports_no_imports, + test_extract_arg_info_python, + test_summarize_arg_info, ] passed = 0 failed = 0