#include "test/test.h" #include "test/tester.h" #include "index/merged_index.h" #include "index/project_index.h" #include "index/tu_index.h" namespace clice::testing { namespace { TEST_SUITE(IndexQuery, Tester) { index::ProjectIndex project_index; llvm::DenseMap merged_indices; /// Build TUIndex from code and merge into ProjectIndex + MergedIndex shards. void build_and_merge(llvm::StringRef code, std::source_location location = std::source_location::current()) { add_main("main.cpp", code); ASSERT_TRUE(compile()); auto tu_index = index::TUIndex::build(*unit); auto file_ids_map = project_index.merge(tu_index); // Merge main file index as compilation context. auto main_tu_path_id = static_cast(tu_index.graph.paths.size() - 1); auto main_global_id = file_ids_map[main_tu_path_id]; std::vector include_locs; for(auto& loc: tu_index.graph.locations) { index::IncludeLocation remapped = loc; remapped.path_id = file_ids_map[loc.path_id]; include_locs.push_back(remapped); } merged_indices[main_global_id].merge(main_global_id, tu_index.built_at, std::move(include_locs), tu_index.main_file_index, {}); // Merge header file indices. for(auto& [fid, file_idx]: tu_index.file_indices) { auto tu_pid = tu_index.graph.path_id(fid); auto global_pid = file_ids_map[tu_pid]; auto include_id = tu_index.graph.include_location_id(fid); merged_indices[global_pid].merge(global_pid, include_id, file_idx, {}); } } /// Reset index state between test cases. void reset() { project_index = index::ProjectIndex(); merged_indices.clear(); clear(); } /// Lookup the symbol hash at a given annotation offset in any merged index. index::SymbolHash lookup_symbol(llvm::StringRef pos) { auto offset = point(pos); index::SymbolHash result = 0; for(auto& [path_id, merged]: merged_indices) { merged.lookup(offset, [&](const index::Occurrence& o) { if(o.range.contains(offset)) { result = o.target; return false; } return true; }); if(result != 0) break; } return result; } /// Find all relations of a given kind for a symbol across all merged indices. std::vector find_relations(index::SymbolHash symbol, RelationKind kind) { std::vector results; auto sym_it = project_index.symbols.find(symbol); if(sym_it == project_index.symbols.end()) return results; // Search every shard that references this symbol. for(auto file_id: sym_it->second.reference_files) { auto it = merged_indices.find(file_id); if(it == merged_indices.end()) continue; it->second.lookup(symbol, kind, [&](const index::Relation& r) { results.push_back(r); return true; }); } // Also search all shards (symbol may appear in files not tracked by reference_files). if(results.empty()) { for(auto& [pid, merged]: merged_indices) { merged.lookup(symbol, kind, [&](const index::Relation& r) { results.push_back(r); return true; }); } } return results; } // ============================================================ // Test cases // ============================================================ TEST_CASE(GoToDefinition) { reset(); build_and_merge(R"( int $(decl)foo(); int @def[$(def)foo]() { return 42; } int main() { return $(use)foo(); } )"); auto hash = lookup_symbol("use"); ASSERT_NE(hash, 0UL); auto defs = find_relations(hash, RelationKind::Definition); ASSERT_FALSE(defs.empty()); auto expected = range("def"); ASSERT_EQ(dump(defs.front().range), dump(expected)); } TEST_CASE(FindReferences) { reset(); build_and_merge(R"( int $(decl)foo(); int $(def)foo() { return 42; } int bar() { return $(ref1)foo() + $(ref2)foo(); } )"); auto hash = lookup_symbol("decl"); ASSERT_NE(hash, 0UL); auto refs = find_relations(hash, RelationKind::Reference); ASSERT_GE(refs.size(), 2U); } TEST_CASE(DeclAndDef) { reset(); build_and_merge(R"( int $(decl)foo(); int @def[$(def)foo]() { return 42; } )"); auto hash = lookup_symbol("decl"); ASSERT_NE(hash, 0UL); auto decls = find_relations(hash, RelationKind::Declaration); ASSERT_FALSE(decls.empty()); auto defs = find_relations(hash, RelationKind::Definition); ASSERT_FALSE(defs.empty()); auto expected_def = range("def"); ASSERT_EQ(dump(defs.front().range), dump(expected_def)); } TEST_CASE(CallerCallee) { reset(); build_and_merge(R"( void $(callee_def)callee() {} void $(caller_def)caller() { $(call_site)callee(); } )"); auto caller_hash = lookup_symbol("caller_def"); ASSERT_NE(caller_hash, 0UL); auto callees = find_relations(caller_hash, RelationKind::Callee); ASSERT_FALSE(callees.empty()); auto callee_hash = lookup_symbol("callee_def"); ASSERT_NE(callee_hash, 0UL); auto callers = find_relations(callee_hash, RelationKind::Caller); ASSERT_FALSE(callers.empty()); } TEST_CASE(OverrideRelation) { reset(); build_and_merge(R"( struct Base { virtual void $(base_method)method() {} }; struct Derived : Base { void $(derived_method)method() override {} }; )"); // Derived::method should have Interface relation to Base::method. auto derived_hash = lookup_symbol("derived_method"); ASSERT_NE(derived_hash, 0UL); auto interfaces = find_relations(derived_hash, RelationKind::Interface); ASSERT_FALSE(interfaces.empty()); // Base::method should have Implementation relation. auto base_hash = lookup_symbol("base_method"); ASSERT_NE(base_hash, 0UL); auto impls = find_relations(base_hash, RelationKind::Implementation); ASSERT_FALSE(impls.empty()); } TEST_CASE(BaseAndDerived) { reset(); build_and_merge(R"( struct $(base_cls)Animal { virtual void speak() {} }; struct $(derived_cls)Dog : $(base_ref)Animal { void speak() override {} }; )"); auto derived_hash = lookup_symbol("derived_cls"); ASSERT_NE(derived_hash, 0UL); // Look for any Base relation in any shard. bool found_base = false; for(auto& [pid, merged]: merged_indices) { merged.lookup(derived_hash, RelationKind::Base, [&](const index::Relation& r) { found_base = true; return false; }); } ASSERT_TRUE(found_base); } TEST_CASE(ClassTemplate) { reset(); build_and_merge(R"( template struct @primary[$(primary)foo] {}; $(use)foo x; )"); auto hash = lookup_symbol("use"); ASSERT_NE(hash, 0UL); auto defs = find_relations(hash, RelationKind::Definition); ASSERT_FALSE(defs.empty()); } TEST_CASE(SymbolKinds) { reset(); build_and_merge(R"( struct $(cls)MyClass {}; void $(func)myFunc() {} int $(var)myVar = 0; )"); auto cls_hash = lookup_symbol("cls"); ASSERT_NE(cls_hash, 0UL); ASSERT_TRUE(project_index.symbols.contains(cls_hash)); ASSERT_EQ(project_index.symbols[cls_hash].kind.value(), SymbolKind(SymbolKind::Struct).value()); auto func_hash = lookup_symbol("func"); ASSERT_NE(func_hash, 0UL); ASSERT_TRUE(project_index.symbols.contains(func_hash)); ASSERT_EQ(project_index.symbols[func_hash].kind.value(), SymbolKind(SymbolKind::Function).value()); auto var_hash = lookup_symbol("var"); ASSERT_NE(var_hash, 0UL); ASSERT_TRUE(project_index.symbols.contains(var_hash)); ASSERT_EQ(project_index.symbols[var_hash].kind.value(), SymbolKind(SymbolKind::Variable).value()); } TEST_CASE(ReferenceFiles) { reset(); build_and_merge(R"( int $(target)target = 42; int a = $(ref)target + 1; )"); auto hash = lookup_symbol("target"); ASSERT_NE(hash, 0UL); auto sym_it = project_index.symbols.find(hash); ASSERT_TRUE(sym_it != project_index.symbols.end()); // reference_files should contain at least the main file. ASSERT_FALSE(sym_it->second.reference_files.isEmpty()); } TEST_CASE(CrossFileQuery) { reset(); add_file("header.h", R"( #pragma once int $(hdr_decl)helper(); )"); add_main("main.cpp", R"( #include "header.h" int main() { return $(use_helper)helper(); } )"); ASSERT_TRUE(compile()); auto tu_index = index::TUIndex::build(*unit); auto file_ids_map = project_index.merge(tu_index); // Merge main file. auto main_tu_path_id = static_cast(tu_index.graph.paths.size() - 1); auto main_global_id = file_ids_map[main_tu_path_id]; std::vector include_locs; for(auto& loc: tu_index.graph.locations) { index::IncludeLocation remapped = loc; remapped.path_id = file_ids_map[loc.path_id]; include_locs.push_back(remapped); } merged_indices[main_global_id].merge(main_global_id, tu_index.built_at, std::move(include_locs), tu_index.main_file_index, {}); // Merge header file indices. for(auto& [fid, file_idx]: tu_index.file_indices) { auto tu_pid = tu_index.graph.path_id(fid); auto global_pid = file_ids_map[tu_pid]; auto include_id = tu_index.graph.include_location_id(fid); merged_indices[global_pid].merge(global_pid, include_id, file_idx, {}); } // Query: from usage in main.cpp, find the symbol via merged index. auto use_offset = point("use_helper"); index::SymbolHash helper_hash = 0; merged_indices[main_global_id].lookup(use_offset, [&](const index::Occurrence& o) { if(o.range.contains(use_offset)) { helper_hash = o.target; return false; } return true; }); ASSERT_NE(helper_hash, 0UL); // Find declaration across all shards -- should find it in header shard. auto decls = find_relations(helper_hash, RelationKind::Declaration); ASSERT_FALSE(decls.empty()); } }; // TEST_SUITE(IndexQuery) } // namespace } // namespace clice::testing