Просмотр исходного кода

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

Land upstream PR #1634 by @maxmilian, commit
8d398a92e30c9df27196bc90833efd674384569d.

Path-only detection classifies .h files as c, so preloading previously
covered c and cpp but missed objc selected by content-aware detection.
Preload objc alongside cpp whenever c is present. Full indexing and
changed-file reindexing now share preloadLanguagesForFiles().

Retain all four upstream unit tests and place the existing #1628 changelog
entry under Unreleased / Fixes / Symbols, tests and the viewer.

Verified fail -> pass on Linux with the reporter's repro.h as the only
source file, without a .m or .mm grammar seed. Before: Objective-C parser
initialization failure, 0 nodes, index state failed. After: Indexed 1 files,
2 nodes, 1 edge, index state complete; node CGRepro finds the class and
errors.log is absent.

Validation: npx tsc && npm run copy-assets passed; rebuilt CLI is executable.
npx vitest run __tests__/preload-languages.test.ts: 4 tests passed.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: Max Hsu <maxmilian@gmail.com>
Colby Mchenry 4 часов назад
Родитель
Сommit
e720f6ca53
3 измененных файлов с 69 добавлено и 11 удалено
  1. 2 0
      CHANGELOG.md
  2. 41 0
      __tests__/preload-languages.test.ts
  3. 26 11
      src/extraction/index.ts

+ 2 - 0
CHANGELOG.md

@@ -213,6 +213,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
 #### Symbols, tests and the viewer
 #### Symbols, tests and the viewer
 
 
+- 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)
+
 - TypeScript interface methods and properties are now indexed, so `node`, `callers` and impact can find platform `.d.ts` APIs while declaration-only files keep their lower ranking on flow queries; re-index TypeScript projects after upgrading. (#1638)
 - TypeScript interface methods and properties are now indexed, so `node`, `callers` and impact can find platform `.d.ts` APIs while declaration-only files keep their lower ranking on flow queries; re-index TypeScript projects after upgrading. (#1638)
 - Lua and Luau function expressions assigned to locals, table members, or keyed table fields are now indexed as callable nodes. Calls from `local f = function() ... end`, `M.f = function() ... end`, and callback tables such as `M.handlers = { onClick = function() ... end }` are attributed to the named function or method instead of collapsing onto the file node, so callers and impact no longer omit these handlers. Re-index after upgrading. (#1616, #1650)
 - Lua and Luau function expressions assigned to locals, table members, or keyed table fields are now indexed as callable nodes. Calls from `local f = function() ... end`, `M.f = function() ... end`, and callback tables such as `M.handlers = { onClick = function() ... end }` are attributed to the named function or method instead of collapsing onto the file node, so callers and impact no longer omit these handlers. Re-index after upgrading. (#1616, #1650)
 - **Functions bound with `const` inside another function are symbols now.** `const handleClear = () => {…}` inside a React component — every handler that skips `useCallback` — was invisible to `callers`, `callees` and impact, answering "Symbol not found" exactly the way a function with no callers would. It is indexed like its module-level twin, contained by the enclosing function, with its own calls. Re-index after upgrading. (#1669)
 - **Functions bound with `const` inside another function are symbols now.** `const handleClear = () => {…}` inside a React component — every handler that skips `useCallback` — was invisible to `callers`, `callees` and impact, answering "Symbol not found" exactly the way a function with no callers would. It is indexed like its module-level twin, contained by the enclosing function, with its own calls. Re-index after upgrading. (#1669)

+ 41 - 0
__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');
+  });
+});

+ 26 - 11
src/extraction/index.ts

@@ -742,6 +742,30 @@ function findNestedGitRepos(absDir: string, relPrefix: string): string[] {
  * scope cannot diverge from each other or from `git ls-files --exclude-standard`
  * scope cannot diverge from each other or from `git ls-files --exclude-standard`
  * (#1728).
  * (#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 {
 export class ScopeIgnore {
   private embedded: Array<{ root: string; matcher: Ignore }>;
   private embedded: Array<{ root: string; matcher: Ignore }>;
   private defaults: Ignore = defaultsOnlyIgnore();
   private defaults: Ignore = defaultsOnlyIgnore();
@@ -1798,11 +1822,7 @@ export class ExtractionOrchestrator {
     await new Promise(resolve => setImmediate(resolve));
     await new Promise(resolve => setImmediate(resolve));
 
 
     // Detect needed languages and load grammars in the parse worker
     // 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
     // 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
     // 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
     // Load only grammars needed for changed files
     if (filesToIndex.length > 0) {
     if (filesToIndex.length > 0) {
       const overrides = loadExtensionOverrides(this.rootDir);
       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
     // Index changed files