From 7947d0b230a11be64edd891fca7d2b9a07a9fbc7 Mon Sep 17 00:00:00 2001 From: Sravan1011 Date: Mon, 21 Sep 2026 21:13:48 +0530 Subject: [PATCH 1/2] fix(#2965): system headers must not resolve to in-repo files C and C++ use two syntactically distinct include forms: #include -- angle-bracket: search system include paths only #include "x.h" -- quoted: search relative to the including file first The old suffix-match fallback in resolveCImportTarget had no awareness of this distinction, so a repo containing its own stdio.h would capture every #include and resolve it to the local file. Fix: - Add isSystem?: boolean to the wildcard variant of ParsedImportSyntax - interpretCImport / interpretCppImport now set isSystem from the @import.system tree-sitter capture (present for angle-bracket form) - resolveImportTarget in both cScopeResolver and cppScopeResolver short-circuits to null when context.parsedImport.isSystem is true, refusing to suffix-match system headers against workspace files - Remove C and C++ from KNOWN_GAPS in the conformance test; add proper test cases with a parsedImport factory that distinguishes angle-bracket (isSystem:true) from quoted (isSystem:false) includes Test: all 36 external-import-conformance cases pass, including the two new c/cpp arms that were previously in KNOWN_GAPS. --- gitnexus-shared/src/scope-resolution/types.ts | 5 ++++ .../core/ingestion/languages/c/interpret.ts | 9 +++--- .../ingestion/languages/c/scope-resolver.ts | 11 +++++++- .../core/ingestion/languages/cpp/interpret.ts | 6 ++-- .../ingestion/languages/cpp/scope-resolver.ts | 11 +++++++- .../external-import-conformance.test.ts | 28 +++++++++++++++---- 6 files changed, 54 insertions(+), 16 deletions(-) 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/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`'], ]); /** From c11e66663e929edef8c4c6e727a31c2fc990ba5c Mon Sep 17 00:00:00 2001 From: Sravan1011 Date: Mon, 21 Sep 2026 21:25:04 +0530 Subject: [PATCH 2/2] fix(#2965): update cpp-imports unit tests for isSystem field Two test assertions were broken by the interpreter change: 1. Local include: the wildcard ParsedImport now always includes isSystem (false for quoted includes). Updated expected object to include isSystem:false. 2. System header: the old test asserted interpretCppImport returned null for system headers. The refactored design moves the null decision to the resolver layer (cppScopeResolver.resolveImportTarget) so the call graph and resolution stay separate concerns. The interpreter now returns { kind:'wildcard', isSystem:true } and the test name/assertion are updated to reflect this. --- .../test/unit/scope-resolution/cpp/cpp-imports.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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)', () => {