From 64dc06c461c41722ecff63dff15d4fb4c5ecaa15 Mon Sep 17 00:00:00 2001 From: Max Hsu Date: Fri, 28 Aug 2026 13:48:56 +0800 Subject: [PATCH] fix(extraction): preload the Objective-C grammar for C-family headers (#1628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 4 +++ __tests__/preload-languages.test.ts | 41 +++++++++++++++++++++++++++++ src/extraction/index.ts | 37 ++++++++++++++++++-------- 3 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 __tests__/preload-languages.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca5b9ff1..fee893a1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixes + +- 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 diff --git a/__tests__/preload-languages.test.ts b/__tests__/preload-languages.test.ts new file mode 100644 index 000000000..5f8e5ec02 --- /dev/null +++ b/__tests__/preload-languages.test.ts @@ -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'); + }); +}); diff --git a/src/extraction/index.ts b/src/extraction/index.ts index 93be48352..c63e46ec4 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -639,6 +639,30 @@ function findNestedGitRepos(absDir: string, relPrefix: string): string[] { * * Single source of truth for indexer and watcher scope — they must not diverge. */ + +/** + * 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 +): 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(); @@ -1662,11 +1686,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 @@ -2874,12 +2894,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