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