mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
* fix(#2965): system headers must not resolve to in-repo files C and C++ use two syntactically distinct include forms: #include <x.h> -- 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 <stdio.h> 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. * 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. * fix(#2965): resolve C and C++ includes on search paths Angle includes follow each translation unit's include roots, so a local stdio.h no longer captures system headers or another file's -I list. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3341) - Accept in-repo include roots whose names start with `..` while still rejecting parent escapes. - Correct the import-target bench comments so CONTEXT_LANGS and newPass match C/C++ header passes. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(c,cpp): read include config per directory for monorepos (#2965) Angle includes now resolve only against declared search roots, so config read only at the repo root left monorepo sub-projects with nothing declared. - compile_commands.json, compile_flags.txt, .ccls, .clangd and c_cpp_properties.json are read in every directory; a file takes the nearest one, and a nearer database's entry beats a shallower one (clangd). - CMake include_directories / target_include_directories are read: directory-scoped and PRIVATE roots reach their subtree, PUBLIC and INTERFACE roots reach every file. ${CMAKE_CURRENT_SOURCE_DIR} and friends expand; unresolvable variables and generator expressions are dropped. - Declared roots win: implicit include/Headers/inc roots apply only when no config speaks for the file, and never to a database-listed file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(c,cpp): give CMake PUBLIC includes only to linked targets (#3341) target_link_libraries now decides who sees PUBLIC and INTERFACE roots, so an unrelated target no longer resolves another package's headers. --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
176 lines
7.3 KiB
TypeScript
176 lines
7.3 KiB
TypeScript
/**
|
|
* Unit tests for C++ import decomposition, interpretation, and target resolution.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { getCppParser } from '../../../../src/core/ingestion/languages/cpp/query.js';
|
|
import {
|
|
splitCppInclude,
|
|
splitCppUsingDecl,
|
|
} from '../../../../src/core/ingestion/languages/cpp/import-decomposer.js';
|
|
import { interpretCppImport } from '../../../../src/core/ingestion/languages/cpp/interpret.js';
|
|
import { resolveCppImportTarget } from '../../../../src/core/ingestion/languages/cpp/import-target.js';
|
|
import type { SyntaxNode } from '../../../../src/core/ingestion/utils/ast-helpers.js';
|
|
|
|
function parseNode(src: string, type: string): SyntaxNode | null {
|
|
const tree = getCppParser().parse(src);
|
|
const stack: SyntaxNode[] = [tree.rootNode as SyntaxNode];
|
|
while (stack.length > 0) {
|
|
const node = stack.pop()!;
|
|
if (node.type === type) return node;
|
|
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
const child = node.namedChild(i);
|
|
if (child !== null) stack.push(child as SyntaxNode);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function capt(name: string, text: string) {
|
|
return { name, text, range: { startLine: 1, startCol: 1, endLine: 1, endCol: 1 } };
|
|
}
|
|
|
|
// ── #include decomposition ──────────────────────────────────────────────────
|
|
|
|
describe('C++ include decomposition (splitCppInclude)', () => {
|
|
it('decomposes local include "#include \\"foo.h\\""', () => {
|
|
const node = parseNode('#include "foo.h"', 'preproc_include');
|
|
expect(node).not.toBeNull();
|
|
const match = splitCppInclude(node!);
|
|
expect(match).not.toBeNull();
|
|
expect(match!['@import.source'].text).toBe('foo.h');
|
|
expect(match!['@import.kind'].text).toBe('wildcard');
|
|
expect(match!['@import.system']).toBeUndefined();
|
|
});
|
|
|
|
it('decomposes system include "#include <iostream>"', () => {
|
|
const node = parseNode('#include <iostream>', 'preproc_include');
|
|
expect(node).not.toBeNull();
|
|
const match = splitCppInclude(node!);
|
|
expect(match).not.toBeNull();
|
|
expect(match!['@import.source'].text).toBe('iostream');
|
|
expect(match!['@import.system']).toBeDefined();
|
|
});
|
|
|
|
it('decomposes C++ header include "#include \\"utils/helpers.hpp\\""', () => {
|
|
const node = parseNode('#include "utils/helpers.hpp"', 'preproc_include');
|
|
expect(node).not.toBeNull();
|
|
const match = splitCppInclude(node!);
|
|
expect(match).not.toBeNull();
|
|
expect(match!['@import.source'].text).toBe('utils/helpers.hpp');
|
|
});
|
|
});
|
|
|
|
// ── using declaration decomposition ─────────────────────────────────────────
|
|
|
|
describe('C++ using declaration decomposition (splitCppUsingDecl)', () => {
|
|
it('does not treat a class-scope member using-declaration as an import', () => {
|
|
const node = parseNode('struct Derived : Base { using Base::run; };', 'using_declaration');
|
|
expect(node).not.toBeNull();
|
|
expect(splitCppUsingDecl(node!)).toBeNull();
|
|
});
|
|
|
|
it('decomposes "using namespace std;" as wildcard import', () => {
|
|
const node = parseNode('using namespace std;', 'using_declaration');
|
|
expect(node).not.toBeNull();
|
|
const match = splitCppUsingDecl(node!);
|
|
expect(match).not.toBeNull();
|
|
expect(match!['@import.kind'].text).toBe('wildcard');
|
|
expect(match!['@import.source'].text).toBe('std');
|
|
expect(match!['@import.using-namespace']).toBeDefined();
|
|
});
|
|
|
|
it('decomposes "using std::vector;" as named import', () => {
|
|
const node = parseNode('using std::vector;', 'using_declaration');
|
|
expect(node).not.toBeNull();
|
|
const match = splitCppUsingDecl(node!);
|
|
expect(match).not.toBeNull();
|
|
expect(match!['@import.kind'].text).toBe('named');
|
|
expect(match!['@import.source'].text).toBe('std');
|
|
expect(match!['@import.name'].text).toBe('vector');
|
|
});
|
|
|
|
it('decomposes nested namespace "using namespace foo::bar;"', () => {
|
|
const node = parseNode('using namespace foo::bar;', 'using_declaration');
|
|
expect(node).not.toBeNull();
|
|
const match = splitCppUsingDecl(node!);
|
|
expect(match).not.toBeNull();
|
|
expect(match!['@import.kind'].text).toBe('wildcard');
|
|
expect(match!['@import.source'].text).toBe('foo::bar');
|
|
});
|
|
});
|
|
|
|
// ── Import interpretation ───────────────────────────────────────────────────
|
|
|
|
describe('C++ import interpretation (interpretCppImport)', () => {
|
|
it('interprets local include as wildcard import', () => {
|
|
const result = interpretCppImport({
|
|
'@import.kind': capt('@import.kind', 'wildcard'),
|
|
'@import.source': capt('@import.source', 'header.hpp'),
|
|
});
|
|
// isSystem is false for quoted #include "..." (no @import.system capture).
|
|
expect(result).toEqual({ kind: 'wildcard', targetRaw: 'header.hpp', isSystem: false });
|
|
});
|
|
|
|
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).toEqual({ kind: 'wildcard', targetRaw: 'iostream', isSystem: true });
|
|
});
|
|
|
|
it('interprets named import (using std::vector)', () => {
|
|
const result = interpretCppImport({
|
|
'@import.kind': capt('@import.kind', 'named'),
|
|
'@import.source': capt('@import.source', 'std'),
|
|
'@import.name': capt('@import.name', 'vector'),
|
|
});
|
|
expect(result).not.toBeNull();
|
|
expect(result!.kind).toBe('named');
|
|
expect(result!.targetRaw).toBe('std');
|
|
});
|
|
|
|
it('returns null when @import.source is missing', () => {
|
|
const result = interpretCppImport({
|
|
'@import.kind': capt('@import.kind', 'wildcard'),
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ── Import target resolution ────────────────────────────────────────────────
|
|
|
|
describe('C++ import target resolution (resolveCppImportTarget)', () => {
|
|
it('resolves .hpp header', () => {
|
|
const result = resolveCppImportTarget('foo.hpp', 'main.cpp', new Set(['foo.hpp', 'bar.cpp']));
|
|
expect(result).toBe('foo.hpp');
|
|
});
|
|
|
|
it('resolves .hxx header', () => {
|
|
const result = resolveCppImportTarget('foo.hxx', 'main.cpp', new Set(['foo.hxx']));
|
|
expect(result).toBe('foo.hxx');
|
|
});
|
|
|
|
it('prefers same-directory sibling', () => {
|
|
const result = resolveCppImportTarget(
|
|
'bar.hpp',
|
|
'src/foo.cpp',
|
|
new Set(['include/bar.hpp', 'src/bar.hpp']),
|
|
);
|
|
expect(result).toBe('src/bar.hpp');
|
|
});
|
|
|
|
it('resolves suffix match with depth tiebreak', () => {
|
|
const result = resolveCppImportTarget('foo.h', 'main.cpp', new Set(['a/b/c/foo.h', 'z/foo.h']));
|
|
expect(result).toBe('z/foo.h');
|
|
});
|
|
|
|
it('returns null for no match', () => {
|
|
expect(resolveCppImportTarget('missing.hpp', 'main.cpp', new Set(['foo.h']))).toBeNull();
|
|
});
|
|
});
|