Skip to content

Commit 733ad08

Browse files
safishamsiclaude
andcommitted
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>
1 parent ac6bb27 commit 733ad08

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

graphify/extract.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16063,6 +16063,20 @@ def _is_objc_header(path: Path) -> bool:
1606316063
)
1606416064

1606516065

16066+
def _is_objc_source(path: Path) -> bool:
16067+
"""Whether a `.m` file is Objective-C rather than MATLAB/Octave (#1702).
16068+
16069+
`.m` is shared by Objective-C implementation files and MATLAB (also Octave).
16070+
The suffix map routes `.m` to extract_objc unconditionally, which force-parses
16071+
MATLAB through the Objective-C tree-sitter grammar and emits garbage nodes/edges
16072+
(worse than skipping). A genuine ObjC `.m` always carries an ObjC directive
16073+
(@implementation/@interface/@import/#import); MATLAB has none of them. Reuses
16074+
the same marker set as the `.h` sniff. `.mm` is unambiguously Objective-C++ and
16075+
is not sniffed.
16076+
"""
16077+
return _is_objc_header(path)
16078+
16079+
1606616080
def _is_cpp_header(path: Path) -> bool:
1606716081
"""Whether a `.h` file is C++ rather than plain C (#1547).
1606816082

@@ -16106,6 +16120,13 @@ def _get_extractor(path: Path) -> Any | None:
1610616120
# grammar has no class_specifier). Reroute to extract_cpp (#1547).
1610716121
if _is_cpp_header(path):
1610816122
return extract_cpp
16123+
# `.m` is Objective-C OR MATLAB. extract_objc unconditionally would force-parse
16124+
# MATLAB through the ObjC grammar into garbage (#1702). Route to extract_objc
16125+
# only when the file actually looks like Objective-C; otherwise leave it without
16126+
# an extractor (surfaced by the no-AST-extractor warning, #1689) rather than
16127+
# mis-parsed. `.mm` is unambiguously Objective-C++ and stays on extract_objc.
16128+
if suffix == ".m" and not _is_objc_source(path):
16129+
return None
1610916130
# Extensionless files: resolve by shebang, mirroring detect.classify_file.
1611016131
# Without this, detect labels e.g. `#!/usr/bin/env bash` CLIs as code but
1611116132
# extraction returns no extractor and the file silently contributes nothing.

tests/test_extract.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,3 +1813,34 @@ def test_extract_progress_final_line_uses_consistent_denominator(tmp_path, capsy
18131813
# final progress line reports the uncached count (100), not the total (105)
18141814
assert "100/100 uncached files (100%)" in out
18151815
assert "105/105 files" not in out, "final line must not switch to total_files (#1693)"
1816+
1817+
1818+
def test_get_extractor_routes_matlab_m_away_from_objc(tmp_path):
1819+
# #1702: .m is shared by Objective-C and MATLAB. A real ObjC .m still routes to
1820+
# extract_objc, but a MATLAB .m must NOT be force-parsed by the ObjC grammar
1821+
# (which produces garbage) — it gets no extractor instead.
1822+
from graphify.extract import _get_extractor, extract_objc
1823+
1824+
objc = tmp_path / "Foo.m"
1825+
objc.write_text('#import "Foo.h"\n@implementation Foo\n- (void)bar {}\n@end\n')
1826+
matlab_fn = tmp_path / "solver.m"
1827+
matlab_fn.write_text("function y = solver(x)\n y = x + 1;\nend\n")
1828+
matlab_cls = tmp_path / "Model.m"
1829+
matlab_cls.write_text("classdef Model\n methods\n function run(obj); end\n end\nend\n")
1830+
mm = tmp_path / "x.mm"
1831+
mm.write_text("#import <F/F.h>\n@implementation X\n@end\n")
1832+
1833+
assert _get_extractor(objc) is extract_objc # real ObjC .m -> objc
1834+
assert _get_extractor(matlab_fn) is None # MATLAB function -> no garbage
1835+
assert _get_extractor(matlab_cls) is None # MATLAB classdef -> no garbage
1836+
assert _get_extractor(mm) is extract_objc # .mm is unambiguously ObjC++
1837+
1838+
1839+
def test_matlab_m_not_extracted_as_garbage(tmp_path, capsys):
1840+
# End to end: a MATLAB .m produces no (garbage) nodes and is surfaced by the
1841+
# no-AST-extractor warning (#1702 + #1689), rather than mis-parsed as ObjC.
1842+
m = tmp_path / "controller.m"
1843+
m.write_text("function u = controller(x)\n u = -x;\nend\n")
1844+
result = extract([m], cache_root=tmp_path)
1845+
assert result["nodes"] == [] # no garbage ObjC nodes
1846+
assert "no AST extractor" in capsys.readouterr().err # surfaced, not silent

0 commit comments

Comments
 (0)