From 97608ef573ccbdf4a33cb8e711cca9ca283314ac Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Fri, 2 Oct 2026 23:31:10 +0530 Subject: [PATCH] Add regression tests for the symbolless file warning Covers a plain data literal getting named and marked as likely data, a real function staying silent, no overlap with the existing syntax error warning on a genuinely broken file, and the total size count across more than one file. Confirmed to fail against pre fix code. (cherry picked from commit 32ad059ebc2b5ff72d5ceba9f6163795e6350d05) --- graphify/extract.py | 40 ++++++++++----- tests/test_partial_extraction_warning.py | 63 ++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index d9b4effb5..9e87c05f9 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -7655,22 +7655,36 @@ def extract( # #3946: a code file that parses CLEANLY (no parse_errors — distinct from # the #2551/#2599 case just above, where _syntax_error_files already - # explains the loss) but still yields only its own bare file node is - # usually a plain data literal the AST extractor has nothing to model - # (`const BANK = [...]`), not a code file with no symbols. Nothing - # reported this before; the file silently entered the graph as a bare, + # explains the loss) but yields NOTHING beyond its own bare file node often + # is a plain data literal the AST extractor has nothing to model + # (`const BANK = [...]`). It silently entered the graph as a bare, # zero-edge node, indistinguishable from a legitimately symbol-free file. + # + # Scope this to EXACTLY one node (the unconditional file node, engine.py): + # - `== 1`, not `<= 1`, so the 0-node domain stays owned by #1666 (zero + # nodes / retry) and is not double-reported with contradictory advice; + # - skip `skipped` (#1224/#2879 intentional data declines, like #1666) and + # no-extractor files (#1689 already reports those as unsupported code); + # - skip empty/whitespace-only files (an empty `__init__.py`, `py.typed`) + # — there is nothing to model, so the warning would be pure noise. + # The message stays neutral ("no symbols"): a def-less script or thin + # module is symbol-less *code*, not necessarily data. _symbolless_files: list[tuple[str, int]] = [] for i, _p in enumerate(paths): _res = per_file[i] or {} - if _res.get("parse_errors") or _res.get("error"): + if _res.get("parse_errors") or _res.get("error") or _res.get("skipped"): continue - if len(_res.get("nodes", [])) <= 1: - try: - _size = _p.stat().st_size - except OSError: - _size = 0 - _symbolless_files.append((os.path.relpath(str(_p), str(root)).replace("\\", "/"), _size)) + if _get_extractor(_p) is None: + continue + if len(_res.get("nodes", [])) != 1: + continue + try: + _size = _p.stat().st_size + if not _p.read_text(encoding="utf-8", errors="ignore").strip(): + continue # empty / whitespace-only: nothing to model, no signal + except OSError: + _size = 0 + _symbolless_files.append((os.path.relpath(str(_p), str(root)).replace("\\", "/"), _size)) if _symbolless_files: _total_bytes = sum(size for _, size in _symbolless_files) _total_mb = _total_bytes / (1024 * 1024) @@ -7681,8 +7695,8 @@ def extract( ) print( f" warning: {len(_symbolless_files)} code file(s) yielded no symbols " - f"({_total_mb:.2f} MB); they may be data rather than code: " - f"{_shown_sl}{_more_sl}", + f"({_total_mb:.2f} MB) — a data file, or source this extractor models " + f"no symbols for: {_shown_sl}{_more_sl}", file=sys.stderr, flush=True, ) diff --git a/tests/test_partial_extraction_warning.py b/tests/test_partial_extraction_warning.py index 38391c31e..eb1ada0eb 100644 --- a/tests/test_partial_extraction_warning.py +++ b/tests/test_partial_extraction_warning.py @@ -80,3 +80,66 @@ def test_the_citation_is_gone_from_the_source_not_just_one_path(): assert "may be partially extracted: {_shown}{_more} (#2551)" not in text assert "no symbols extracted" in text assert "symbol(s) extracted" in text + + +# ── #3946: a clean-parse file that yields nothing but its own file node ───── + +def _data_only_fixture(tmp_path): + """A plain data literal with no functions, classes or imports — the AST + extractor has nothing to model, but the file parses with zero errors, so + the syntax-error warning above must stay silent for it.""" + f = tmp_path / "data.js" + f.write_text("module.exports = [{a: 1}, {b: 2}];\n", encoding="utf-8") + return f + + +def test_symbolless_warning_names_the_file(tmp_path, capsys): + err = _run(tmp_path, [_data_only_fixture(tmp_path)], capsys) + assert "yielded no symbols" in err, err + assert "data.js" in err + # neutral wording: a symbol-less file may be data OR source the extractor + # models nothing for — do NOT assert it is "data rather than code". + assert "may be data rather than code" not in err + + +def test_symbolless_warning_is_silent_for_an_empty_init(tmp_path, capsys): + """An empty __init__.py is symbol-less *code*, ubiquitous in Python + packages. It has nothing to model, so the warning must stay silent rather + than fire on essentially every package scan.""" + pkg = tmp_path / "pkg" + pkg.mkdir() + (pkg / "__init__.py").write_text("", encoding="utf-8") + err = _run(tmp_path, [pkg / "__init__.py"], capsys) + assert "yielded no symbols" not in err + + +def test_symbolless_warning_is_silent_for_a_whitespace_only_file(tmp_path, capsys): + f = tmp_path / "blank.py" + f.write_text("\n \n\t\n", encoding="utf-8") + err = _run(tmp_path, [f], capsys) + # whitespace-only: nothing to model, so the strip-empty guard keeps it quiet + assert "yielded no symbols" not in err + + +def test_symbolless_warning_does_not_fire_for_a_real_function(tmp_path, capsys): + f = tmp_path / "real.js" + f.write_text("function run() { return 1; }\n", encoding="utf-8") + err = _run(tmp_path, [f], capsys) + assert "yielded no symbols" not in err + + +def test_symbolless_warning_does_not_overlap_the_syntax_error_warning(tmp_path, capsys): + """A file already explained by the partial-extraction warning (a genuine + parse error) must not ALSO be counted as a clean-parse data file.""" + err = _run(tmp_path, [_partial_parse_fixture(tmp_path)], capsys) + assert "yielded no symbols" not in err + + +def test_symbolless_warning_counts_total_size(tmp_path, capsys): + a = tmp_path / "a.js" + a.write_text("module.exports = [1, 2, 3];\n", encoding="utf-8") + b = tmp_path / "b.js" + b.write_text("export default [4, 5, 6];\n", encoding="utf-8") + err = _run(tmp_path, [a, b], capsys) + assert "2 code file(s) yielded no symbols" in err + assert "a.js" in err and "b.js" in err