Fix: language composition percentages diluted by unrecognised files

determine_programming_language_composition computed percentages over the total
file count, so files belonging to no supported language (images, plain text,
licenses, lock files) diluted the numbers — a repository with 2 Python files
and 23 assets reported Python at 8%. Percentages are now relative to files
matching at least one supported language, and the counting runs in a single
pass over the file list instead of one pass per language.

The only consumer (ProjectConfig.autogenerate) sorts languages by percentage,
which is unaffected by the uniform rescaling.
This commit is contained in:
bymebyu authored and Dr. Dominik Jain committed 2026-07-02 22:24:32 +02:00
1 parent 3c241bab0d
commit a7501b1c69
3 files changed
+75 -23

No files matched your search

+4 -1
View File
@@ -26,7 +26,10 @@ Status of the `main` branch. Changes prior to the next official version change w
(which does not correctly unpack structured output) #1042
- Fix: Project-specific filtering of files for source files ignored the language backend.
The check is really only possible for LSP.
- Fix: File system permission errors during gitignore scanning were not caught #1624
- Fix: File system permission errors during gitignore scanning were not caught #1624
- During project creation, language composition percentages are now computed relative to the total number
of recognised source files instead of all files, i.e. unrecognised files are ignored in the percentage
computation.
* CLI:
- Fix `--project-from-cwd` hijacking git worktrees nested under a Serena project. `find_project_root`
+26 -22
View File
@@ -31,36 +31,40 @@ def determine_programming_language_composition(repo_path: str) -> dict[Language,
"""
Determine the programming language composition of a repository.
:param repo_path: Path to the repository to analyze
Percentages are computed relative to the number of files that match at least
one supported language, not the total file count. This prevents files that
belong to no supported language (images, plain text, licenses, lock files, etc.)
from diluting language percentages in repositories where such files dominate.
:return: Dictionary mapping languages to percentages of files matching each language
:param repo_path: Path to the repository to analyze
:return: Dictionary mapping languages to percentages of recognised source files
matching each language (denominator = files matched by at least one language)
"""
all_files = find_all_non_ignored_files(repo_path)
if not all_files:
return {}
# Count files for each language
# collect all language matchers once
all_languages = list(Language.iter_all(include_experimental=False))
matchers = {lang: lang.get_source_fn_matcher() for lang in all_languages}
# count files per language in a single pass over the files
language_counts: dict[Language, int] = {}
total_files = len(all_files)
for language in Language.iter_all(include_experimental=False):
matcher = language.get_source_fn_matcher()
count = 0
for file_path in all_files:
# Use just the filename for matching, not the full path
filename = os.path.basename(file_path)
recognised_files = 0
for file_path in all_files:
# Use just the filename for matching, not the full path
filename = os.path.basename(file_path)
matched_any = False
for lang, matcher in matchers.items():
if matcher.is_relevant_filename(filename):
count += 1
language_counts[lang] = language_counts.get(lang, 0) + 1
matched_any = True
if matched_any:
recognised_files += 1
if count > 0:
language_counts[language] = count
if recognised_files == 0:
return {}
# Convert counts to percentages
language_percentages: dict[Language, float] = {}
for language, count in language_counts.items():
percentage = (count / total_files) * 100
language_percentages[language] = round(percentage, 2)
return language_percentages
# convert to percentages relative to recognised source files only
return {lang: round(count / recognised_files * 100, 2) for lang, count in language_counts.items()}
+45
View File
@@ -0,0 +1,45 @@
"""Unit tests for :func:`serena.util.inspection.determine_programming_language_composition`."""
from pathlib import Path
from serena.util.inspection import determine_programming_language_composition
from solidlsp.ls_config import Language
def _touch(directory: Path, *names: str) -> None:
for name in names:
(directory / name).write_text("content", encoding="utf-8")
class TestDetermineProgrammingLanguageComposition:
def test_single_language_repo(self, tmp_path: Path) -> None:
_touch(tmp_path, "a.py", "b.py", "c.py")
composition = determine_programming_language_composition(str(tmp_path))
assert composition[Language.PYTHON] == 100.0
def test_unrecognised_files_do_not_dilute_percentages(self, tmp_path: Path) -> None:
# 2 source files vs many files that belong to no supported language
_touch(tmp_path, "main.py", "util.py")
_touch(tmp_path, *[f"note_{i}.txt" for i in range(20)])
_touch(tmp_path, "logo.png", "LICENSE", "data.csv")
composition = determine_programming_language_composition(str(tmp_path))
# previously: 2 / 25 files = 8% — now the denominator is recognised source files only
assert composition[Language.PYTHON] == 100.0
def test_mixed_language_percentages_relative_to_recognised_files(self, tmp_path: Path) -> None:
_touch(tmp_path, "a.py", "b.py", "c.py", "d.go")
_touch(tmp_path, *[f"asset_{i}.dat" for i in range(50)])
composition = determine_programming_language_composition(str(tmp_path))
assert composition[Language.PYTHON] == 75.0
assert composition[Language.GO] == 25.0
def test_repo_without_recognised_files(self, tmp_path: Path) -> None:
_touch(tmp_path, "readme.txt", "logo.png")
assert determine_programming_language_composition(str(tmp_path)) == {}
def test_empty_repo(self, tmp_path: Path) -> None:
assert determine_programming_language_composition(str(tmp_path)) == {}