fix(extraction): preload the Objective-C grammar for C-family headers (#1628)

A .h file is classified from its path as C, so the preload set that the parse
workers receive contains c (and cpp, added explicitly for exactly this
reason). Parse-time detection then reads the source and can return objc — a
grammar no worker holds, so the file fails outright with 'Failed to get
parser for language: objc' and nothing in it is indexed. A project with any
.m file happened to work, because that pulled objc into the set by path.

Preload objc alongside cpp whenever c is present: both are readings that
content-aware .h detection can produce, so both have to be available before
the workers start.

The same three lines existed twice (full index and changed-file reindex), and
only one of them would have been easy to remember to change, so this extracts
preloadLanguagesForFiles() and uses it in both places.

Verified end to end against the reporter's reproduction — a directory holding
only an Objective-C .h file:
  before: 1 parser initialization failures / 'Failed to get parser for
          language: objc' in .codegraph/errors.log
  after:  Indexed 1 files, 2 nodes, 1 edges

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Max Hsu
2026-08-28 13:48:56 +08:00
parent 43271f3cd3
commit 8d398a92e3
3 changed files with 69 additions and 11 deletions
+2
View File
@@ -242,6 +242,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Fixed a long-running `codegraph ui` session serving a symbol that a sync had already deleted. The viewer keeps one connection to your index open, and its in-memory lookup didn't notice when another process — your agent's sync, or `codegraph sync` — rewrote the file underneath it, so a symbol screen could keep showing a body with no callers while search correctly reported it had moved. Because a symbol's identity includes the line it starts on, this happened after almost any edit above it.
- Objective-C headers now index in a project that has no `.m` file. A `.h` file is read as C from its name alone, and only later — once its contents are read — recognized as Objective-C; the grammar for that was never loaded up front, so the file failed with a parser error and nothing in it reached the index. Adding any `.m` file used to make the same header work, which is what made this look arbitrary. Thanks @Juddd. (#1628)
## [1.6.0] - 2026-08-26
### Highlights
+41
View File
@@ -0,0 +1,41 @@
/**
* Grammar preload set for a file list (#1628).
*
* Path-only detection calls every `.h` file C, but parse-time detection reads
* the source and can reclassify it as C++ or Objective-C. Workers only ever
* receive the grammars named by this set, so a header that turns out to be
* Objective-C in a project with no `.m` file had no parser to go to and the
* file failed outright with `Failed to get parser for language: objc`.
*/
import { describe, it, expect } from 'vitest';
import { preloadLanguagesForFiles } from '../src/extraction';
describe('grammar preload set (#1628)', () => {
it('covers both ambiguous readings of a .h file, C++ and Objective-C', () => {
const langs = preloadLanguagesForFiles(['repro.h']);
// Path-only detection says C…
expect(langs).toContain('c');
// …and parse-time detection may say either of these instead.
expect(langs).toContain('cpp');
expect(langs).toContain('objc');
});
it('adds nothing for a project with no C-family headers', () => {
const langs = preloadLanguagesForFiles(['a.ts', 'b.py']);
expect(langs).not.toContain('c');
expect(langs).not.toContain('cpp');
expect(langs).not.toContain('objc');
});
it('does not duplicate a language the files already need', () => {
const langs = preloadLanguagesForFiles(['repro.h', 'seed.m', 'other.cpp']);
expect(langs.filter((l) => l === 'objc')).toHaveLength(1);
expect(langs.filter((l) => l === 'cpp')).toHaveLength(1);
});
it('honors extension overrides when detecting the base set', () => {
const langs = preloadLanguagesForFiles(['weird.frob'], { '.frob': 'python' });
expect(langs).toContain('python');
});
});
+26 -11
View File
@@ -742,6 +742,30 @@ function findNestedGitRepos(absDir: string, relPrefix: string): string[] {
* scope cannot diverge from each other or from `git ls-files --exclude-standard`
* (#1728).
*/
/**
* The grammars to preload for a file set.
*
* Path-only detection calls every `.h` file C, but parse-time detection reads
* the source and can reclassify it as C++ or Objective-C (`detectLanguage`
* with a `source` argument). Workers only ever get the grammars named here, so
* a header that turns out to be Objective-C in a project with no `.m` file
* found no parser and failed with `Failed to get parser for language: objc`
* (#1628). C++ was already covered; Objective-C was not.
*/
export function preloadLanguagesForFiles(
files: string[],
overrides?: Record<string, Language>
): Language[] {
const languages = [...new Set(files.map((f) => detectLanguage(f, undefined, overrides)))];
if (languages.includes('c')) {
for (const ambiguous of ['cpp', 'objc'] as const) {
if (!languages.includes(ambiguous)) languages.push(ambiguous);
}
}
return languages;
}
export class ScopeIgnore {
private embedded: Array<{ root: string; matcher: Ignore }>;
private defaults: Ignore = defaultsOnlyIgnore();
@@ -1798,11 +1822,7 @@ export class ExtractionOrchestrator {
await new Promise(resolve => setImmediate(resolve));
// Detect needed languages and load grammars in the parse worker
const neededLanguages = [...new Set(files.map((f) => detectLanguage(f, undefined, overrides)))];
// .h files default to 'c' but may be C++ — ensure cpp grammar is loaded when c is needed
if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
neededLanguages.push('cpp');
}
const neededLanguages = preloadLanguagesForFiles(files, overrides);
// Parse files on a pool of worker threads (keeps the main thread free for UI
// and uses every core). Falls back to in-process parsing when the compiled
@@ -3020,12 +3040,7 @@ export class ExtractionOrchestrator {
// Load only grammars needed for changed files
if (filesToIndex.length > 0) {
const overrides = loadExtensionOverrides(this.rootDir);
const neededLanguages = [...new Set(filesToIndex.map((f) => detectLanguage(f, undefined, overrides)))];
// .h files default to 'c' but may be C++ — ensure cpp grammar is loaded
if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
neededLanguages.push('cpp');
}
await loadGrammarsForLanguages(neededLanguages);
await loadGrammarsForLanguages(preloadLanguagesForFiles(filesToIndex, overrides));
}
// Index changed files