mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* feat(cpp): resolve inheritance-lattice member lookup * fix(cpp): harden inheritance-lattice lookup --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
172 lines
6.8 KiB
TypeScript
172 lines
6.8 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'),
|
|
});
|
|
expect(result).toEqual({ kind: 'wildcard', targetRaw: 'header.hpp' });
|
|
});
|
|
|
|
it('returns null for system headers', () => {
|
|
const result = interpretCppImport({
|
|
'@import.kind': capt('@import.kind', 'wildcard'),
|
|
'@import.source': capt('@import.source', 'iostream'),
|
|
'@import.system': capt('@import.system', 'true'),
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|