From 733ad08852267c02a08ffadc82f6de31d09096f1 Mon Sep 17 00:00:00 2001 From: safishamsi Date: Tue, 7 Jul 2026 11:46:51 +0100 Subject: [PATCH] fix(extract): don't force-parse MATLAB .m through the Objective-C grammar (#1702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.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) --- graphify/extract.py | 21 +++++++++++++++++++++ tests/test_extract.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/graphify/extract.py b/graphify/extract.py index dea5bd0b3..652261ba6 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -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. diff --git a/tests/test_extract.py b/tests/test_extract.py index 7cae30776..04baf3926 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -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 \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