Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 41 additions & 0 deletions __tests__/preload-languages.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
37 changes: 26 additions & 11 deletions src/extraction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down