fix(extract): don't force-parse MATLAB .m through the Objective-C grammar (#1702)

`.m` is shared by Objective-C implementation files and MATLAB, but the extractor
dispatch routed every `.m` to extract_objc unconditionally. Feeding real MATLAB
to the Objective-C tree-sitter grammar yields root.has_error and garbage
nodes/edges — worse than skipping, because it pollutes the graph with wrong data.

_get_extractor now content-sniffs `.m` the same way `.h` already is: a genuine
Objective-C `.m` carries an ObjC directive (@implementation/@interface/@import/
#import) and still routes to extract_objc; a `.m` without one (MATLAB, Octave)
gets no extractor, so it is surfaced by the no-AST-extractor warning (#1689)
instead of mis-parsed. `.mm` is unambiguously Objective-C++ and is left untouched.

This stops the wrong-by-omission garbage; wiring a real tree-sitter-matlab
extractor (the issue's primary ask) remains a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-07-07 11:46:51 +01:00
co-authored by Claude Opus 4.8
parent ac6bb274a1
commit 733ad08852
2 changed files with 52 additions and 0 deletions
+21
View File
@@ -16063,6 +16063,20 @@ _CPP_HEADER_MARKERS = (
)
def _is_objc_source(path: Path) -> bool:
"""Whether a `.m` file is Objective-C rather than MATLAB/Octave (#1702).
`.m` is shared by Objective-C implementation files and MATLAB (also Octave).
The suffix map routes `.m` to extract_objc unconditionally, which force-parses
MATLAB through the Objective-C tree-sitter grammar and emits garbage nodes/edges
(worse than skipping). A genuine ObjC `.m` always carries an ObjC directive
(@implementation/@interface/@import/#import); MATLAB has none of them. Reuses
the same marker set as the `.h` sniff. `.mm` is unambiguously Objective-C++ and
is not sniffed.
"""
return _is_objc_header(path)
def _is_cpp_header(path: Path) -> bool:
"""Whether a `.h` file is C++ rather than plain C (#1547).
@@ -16106,6 +16120,13 @@ def _get_extractor(path: Path) -> Any | None:
# grammar has no class_specifier). Reroute to extract_cpp (#1547).
if _is_cpp_header(path):
return extract_cpp
# `.m` is Objective-C OR MATLAB. extract_objc unconditionally would force-parse
# MATLAB through the ObjC grammar into garbage (#1702). Route to extract_objc
# only when the file actually looks like Objective-C; otherwise leave it without
# an extractor (surfaced by the no-AST-extractor warning, #1689) rather than
# mis-parsed. `.mm` is unambiguously Objective-C++ and stays on extract_objc.
if suffix == ".m" and not _is_objc_source(path):
return None
# Extensionless files: resolve by shebang, mirroring detect.classify_file.
# Without this, detect labels e.g. `#!/usr/bin/env bash` CLIs as code but
# extraction returns no extractor and the file silently contributes nothing.
+31
View File
@@ -1813,3 +1813,34 @@ def test_extract_progress_final_line_uses_consistent_denominator(tmp_path, capsy
# final progress line reports the uncached count (100), not the total (105)
assert "100/100 uncached files (100%)" in out
assert "105/105 files" not in out, "final line must not switch to total_files (#1693)"
def test_get_extractor_routes_matlab_m_away_from_objc(tmp_path):
# #1702: .m is shared by Objective-C and MATLAB. A real ObjC .m still routes to
# extract_objc, but a MATLAB .m must NOT be force-parsed by the ObjC grammar
# (which produces garbage) — it gets no extractor instead.
from graphify.extract import _get_extractor, extract_objc
objc = tmp_path / "Foo.m"
objc.write_text('#import "Foo.h"\n@implementation Foo\n- (void)bar {}\n@end\n')
matlab_fn = tmp_path / "solver.m"
matlab_fn.write_text("function y = solver(x)\n y = x + 1;\nend\n")
matlab_cls = tmp_path / "Model.m"
matlab_cls.write_text("classdef Model\n methods\n function run(obj); end\n end\nend\n")
mm = tmp_path / "x.mm"
mm.write_text("#import <F/F.h>\n@implementation X\n@end\n")
assert _get_extractor(objc) is extract_objc # real ObjC .m -> objc
assert _get_extractor(matlab_fn) is None # MATLAB function -> no garbage
assert _get_extractor(matlab_cls) is None # MATLAB classdef -> no garbage
assert _get_extractor(mm) is extract_objc # .mm is unambiguously ObjC++
def test_matlab_m_not_extracted_as_garbage(tmp_path, capsys):
# End to end: a MATLAB .m produces no (garbage) nodes and is surfaced by the
# no-AST-extractor warning (#1702 + #1689), rather than mis-parsed as ObjC.
m = tmp_path / "controller.m"
m.write_text("function u = controller(x)\n u = -x;\nend\n")
result = extract([m], cache_root=tmp_path)
assert result["nodes"] == [] # no garbage ObjC nodes
assert "no AST extractor" in capsys.readouterr().err # surfaced, not silent