diff --git a/gitnexus-shared/src/scope-resolution/types.ts b/gitnexus-shared/src/scope-resolution/types.ts index 50a968146..a085e441b 100644 --- a/gitnexus-shared/src/scope-resolution/types.ts +++ b/gitnexus-shared/src/scope-resolution/types.ts @@ -379,6 +379,11 @@ type ParsedImportSyntax = * deferred — `use` does not execute * (`LanguageProvider.importsExecuteWhereWritten`). */ readonly runsOnlyWhenCalled?: boolean; + /** + * C/C++ specific. Whether the import is a system header `#include <...>` vs `#include "..."`. + * Used during resolution to gate suffix-match fallback. + */ + readonly isSystem?: boolean; } /** * Runtime-computed target — the import path is not a static literal at diff --git a/gitnexus/src/core/ingestion/languages/c/interpret.ts b/gitnexus/src/core/ingestion/languages/c/interpret.ts index e5a15f2fe..e4b126839 100644 --- a/gitnexus/src/core/ingestion/languages/c/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/c/interpret.ts @@ -8,10 +8,11 @@ export function interpretCImport(captures: CaptureMatch): ParsedImport | null { const source = captures['@import.source']?.text; if (source === undefined) return null; - // System headers (e.g. ) are not resolved to local files - if (captures['@import.system'] !== undefined) return null; - - return { kind: 'wildcard', targetRaw: source }; + return { + kind: 'wildcard', + targetRaw: source, + isSystem: captures['@import.system'] !== undefined + }; } /** diff --git a/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts index c3f9fb36f..82c898f26 100644 --- a/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/c/scope-resolver.ts @@ -81,7 +81,16 @@ export const cScopeResolver: ScopeResolver = { // this process. Runs BEFORE `populateOwners`. applyCaptureSideChannel: applyCStaticLinkageSideChannel, - resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => { + resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig, context) => { + // A gate on the angle/quote form alone closes the reported case (#2965). + // System headers (#include ) search only the implementation-defined + // system paths and the configured include path. We do not parse CMake/Make + // include paths today, so we refuse them rather than guessing across the + // whole repo. + if (context?.parsedImport?.kind === 'wildcard' && context.parsedImport.isSystem) { + return null; + } + // Augment allFilePaths with .h files discovered via loadResolutionConfig // since the phase only passes .c files to the C resolver but #include // targets .h files classified as C++ in language detection. diff --git a/gitnexus/src/core/ingestion/languages/cpp/interpret.ts b/gitnexus/src/core/ingestion/languages/cpp/interpret.ts index e5a3635ca..aadeba8ea 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/interpret.ts @@ -14,9 +14,7 @@ export function interpretCppImport(captures: CaptureMatch): ParsedImport | null const source = captures['@import.source']?.text; if (source === undefined) return null; - // System headers are not resolved to local files - if (captures['@import.system'] !== undefined) return null; - + const isSystem = captures['@import.system'] !== undefined; const kind = captures['@import.kind']?.text; if (kind === 'named') { @@ -27,7 +25,7 @@ export function interpretCppImport(captures: CaptureMatch): ParsedImport | null } // #include or using namespace — wildcard import - return { kind: 'wildcard', targetRaw: source }; + return { kind: 'wildcard', targetRaw: source, isSystem }; } /** diff --git a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts index 9421975ea..ee70b1ba7 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts @@ -113,7 +113,16 @@ export const cppScopeResolver: ScopeResolver = { return scanCppHeaderFiles(repoPath); }, - resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => { + resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig, context) => { + // A gate on the angle/quote form alone closes the reported case (#2965). + // System headers (#include ) search only the implementation-defined + // system paths and the configured include path. We do not parse CMake/Make + // include paths today, so we refuse them rather than guessing across the + // whole repo. + if (context?.parsedImport?.kind === 'wildcard' && context.parsedImport.isSystem) { + return null; + } + // Augment allFilePaths with header files discovered via loadResolutionConfig. // C++ .h/.hpp/.hxx/.hh files may be classified differently by language // detection but are importable from .cpp files via #include. diff --git a/gitnexus/test/unit/scope-resolution/cpp/cpp-imports.test.ts b/gitnexus/test/unit/scope-resolution/cpp/cpp-imports.test.ts index 3f2761d50..aa980622f 100644 --- a/gitnexus/test/unit/scope-resolution/cpp/cpp-imports.test.ts +++ b/gitnexus/test/unit/scope-resolution/cpp/cpp-imports.test.ts @@ -108,16 +108,20 @@ describe('C++ import interpretation (interpretCppImport)', () => { '@import.kind': capt('@import.kind', 'wildcard'), '@import.source': capt('@import.source', 'header.hpp'), }); - expect(result).toEqual({ kind: 'wildcard', targetRaw: 'header.hpp' }); + // isSystem is false for quoted #include "..." (no @import.system capture). + expect(result).toEqual({ kind: 'wildcard', targetRaw: 'header.hpp', isSystem: false }); }); - it('returns null for system headers', () => { + it('interprets system header as wildcard with isSystem:true', () => { + // The interpreter no longer returns null for system headers — it returns a + // ParsedImport with isSystem:true. The resolver (cppScopeResolver) is the + // layer that refuses to suffix-match system headers against workspace files. const result = interpretCppImport({ '@import.kind': capt('@import.kind', 'wildcard'), '@import.source': capt('@import.source', 'iostream'), '@import.system': capt('@import.system', 'true'), }); - expect(result).toBeNull(); + expect(result).toEqual({ kind: 'wildcard', targetRaw: 'iostream', isSystem: true }); }); it('interprets named import (using std::vector)', () => { diff --git a/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts b/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts index 3e56563f1..b755698bd 100644 --- a/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts +++ b/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts @@ -342,25 +342,43 @@ const CASES: ReadonlyMap = new Map([ [ SupportedLanguages.C, { + // Workspace has a local `src/stdio.h` that shadows the system header. + // `#include ` (angle-bracket, isSystem:true) must NOT resolve to + // it. `#include "./stdio.h"` (quoted relative form, isSystem:false) from + // `src/main.c` DOES reach it via sibling lookup — the decoy-reachability + // proof. Using './stdio.h' for reachesDecoy vs 'stdio.h' for external lets + // the parsedImport factory distinguish the two arms. files: ['src/stdio.h', 'include/util.h', 'src/main.c'], fromFile: 'src/main.c', resolutionConfig: undefined, external: 'stdio.h', decoy: 'src/stdio.h', - reachesDecoy: 'util.h', + reachesDecoy: './stdio.h', + parsedImport: (targetRaw) => ({ + kind: 'wildcard', + targetRaw, + // Bare name → angle-bracket system include; relative path → quoted local. + isSystem: !targetRaw.startsWith('.'), + }), }, ], [ SupportedLanguages.CPlusPlus, { + // Same shape as C: a local `src/cstdio.h` that shadows the C++ system + // header. `#include ` (isSystem:true) must NOT resolve to it; + // `#include "./cstdio.h"` (isSystem:false) from `src/main.cpp` DOES. files: ['src/cstdio.h', 'include/util.hpp', 'src/main.cpp'], fromFile: 'src/main.cpp', resolutionConfig: undefined, - // `cstdio` with no extension would miss the decoy on spelling alone and - // post a pass that measures nothing; the header spelling is the real test. external: 'cstdio.h', decoy: 'src/cstdio.h', - reachesDecoy: 'util.hpp', + reachesDecoy: './cstdio.h', + parsedImport: (targetRaw) => ({ + kind: 'wildcard', + targetRaw, + isSystem: !targetRaw.startsWith('.'), + }), }, ], [ @@ -417,8 +435,6 @@ const CASES: ReadonlyMap = new Map([ */ const KNOWN_GAPS: ReadonlyMap = new Map([ [SupportedLanguages.Dart, '`package:http/http.dart` -> `lib/http.dart`'], - [SupportedLanguages.C, '`stdio.h` -> `src/stdio.h`'], - [SupportedLanguages.CPlusPlus, '`cstdio.h` -> `src/cstdio.h`'], ]); /**