Files
tsl-devkit/lsp-server/test/test_semantic/test_semantic.cppm
T
csh e5782c76fa ♻️ refactor(lsp_server): move entrypoints out of named modules
- export Run() from launcher module and add non-module main TU
- apply same pattern to module-based test executables
- migrate utils/string implementation fully into string.cppm and drop string.cpp
- point clangd to linux build compilation database
2025-12-13 19:55:45 +08:00

502 lines
20 KiB
C++

module;
export module lsp.test.semantic.main;
import std;
import tree_sitter;
import lsp.language.ast;
import lsp.language.semantic;
import lsp.language.symbol;
import lsp.utils.string;
import lsp.protocol;
extern "C" const TSLanguage* tree_sitter_tsf(void);
namespace
{
using namespace lsp;
struct Options
{
std::filesystem::path workspace;
std::filesystem::path target_file;
bool show_calls = true;
bool show_inheritance = true;
bool show_references = true;
bool quiet = false;
bool help = false;
};
void PrintUsage(const char* program_name)
{
std::cout << "\n╭─────────────────────────────────────╮\n";
std::cout << "│ Semantic Graph Inspector │\n";
std::cout << "╰─────────────────────────────────────╯\n\n";
std::cout << "Usage:\n";
std::cout << " " << program_name << " <tsf_directory> <tsf_file> [options]\n\n";
std::cout << "Options:\n";
std::cout << " -h, --help Show this help message\n";
std::cout << " --no-calls Skip printing call graph\n";
std::cout << " --no-inheritance Skip printing inheritance relationships\n";
std::cout << " --no-references Skip printing reference summary\n";
std::cout << " -q, --quiet Reduce informational output\n\n";
std::cout << "Example:\n";
std::cout << " " << program_name << " ./tsl_workspace ./tsl_workspace/sample.tsf\n\n";
}
bool ParseArguments(int argc, char** argv, Options& options)
{
for (int i = 1; i < argc; ++i)
{
std::string arg = argv[i];
if (arg == "-h" || arg == "--help")
{
options.help = true;
return true;
}
else if (arg == "--no-calls")
{
options.show_calls = false;
}
else if (arg == "--no-inheritance")
{
options.show_inheritance = false;
}
else if (arg == "--no-references")
{
options.show_references = false;
}
else if (arg == "-q" || arg == "--quiet")
{
options.quiet = true;
}
else if (arg.starts_with("-"))
{
std::cerr << "Unknown option: " << arg << std::endl;
return false;
}
else if (options.workspace.empty())
{
options.workspace = arg;
}
else if (options.target_file.empty())
{
options.target_file = arg;
}
else
{
std::cerr << "Unexpected argument: " << arg << std::endl;
return false;
}
}
return !options.workspace.empty() && !options.target_file.empty();
}
void Expect(bool condition, const std::string& message)
{
if (!condition)
throw std::runtime_error(message);
}
std::string ReadFile(const std::filesystem::path& path)
{
std::ifstream file(path, std::ios::binary);
Expect(file.is_open(), "Failed to open fixture: " + path.string());
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
return content;
}
bool IsTsfFile(const std::filesystem::path& path)
{
if (!path.has_extension())
return false;
auto ext = utils::ToLower(path.extension().string());
return ext == ".tsf";
}
void BuildSymbolsForFile(const std::filesystem::path& path, language::symbol::SymbolTable& table)
{
const std::string source = ReadFile(path);
std::unique_ptr<TSParser, decltype(&ts_parser_delete)> parser(ts_parser_new(), &ts_parser_delete);
Expect(parser != nullptr, "Failed to create tree-sitter parser");
Expect(ts_parser_set_language(parser.get(), tree_sitter_tsf()), "Failed to set TSL language");
std::unique_ptr<TSTree, decltype(&ts_tree_delete)> tree(
ts_parser_parse_string(parser.get(), nullptr, source.c_str(), source.size()),
&ts_tree_delete);
Expect(tree != nullptr, "Failed to parse " + path.string());
language::ast::Deserializer deserializer;
auto ast_result = deserializer.Parse(ts_tree_root_node(tree.get()), source);
Expect(ast_result.IsSuccess(), "AST parse failed for " + path.string());
Expect(ast_result.root != nullptr, "AST root missing for " + path.string());
language::symbol::Builder builder(table);
builder.Build(*ast_result.root);
}
struct WorkspaceSymbols
{
std::unordered_map<std::string, std::vector<language::symbol::Symbol>, utils::IHasher, utils::IEqualTo> by_name;
};
void CollectSymbolsFromFile(const std::filesystem::path& path, WorkspaceSymbols& registry)
{
language::symbol::SymbolTable table;
BuildSymbolsForFile(path, table);
for (const auto& symbol_ref : table.all_definitions())
{
const auto& symbol = symbol_ref.get();
// 调试:打印某些类的 kind
if (symbol.name() == "Document" || symbol.name() == "Properties" ||
symbol.name() == "Endnotes" || symbol.name() == "Theme")
{
std::cerr << "CollectSymbols: " << symbol.name()
<< " from " << path.filename()
<< " kind=" << static_cast<int>(symbol.kind()) << std::endl;
}
registry.by_name[symbol.name()].push_back(symbol);
}
}
void BuildWorkspaceSymbols(const std::filesystem::path& workspace,
const std::filesystem::path& target_file,
WorkspaceSymbols& registry)
{
auto options = std::filesystem::directory_options::follow_directory_symlink |
std::filesystem::directory_options::skip_permission_denied;
size_t total_files = 0;
size_t loaded_files = 0;
size_t failed_files = 0;
for (const auto& entry : std::filesystem::recursive_directory_iterator(workspace, options))
{
if (!entry.is_regular_file())
continue;
if (!IsTsfFile(entry.path()))
continue;
total_files++;
std::error_code ec;
if (std::filesystem::equivalent(entry.path(), target_file, ec) && !ec)
{
continue;
}
try
{
CollectSymbolsFromFile(entry.path(), registry);
loaded_files++;
}
catch (const std::exception& e)
{
failed_files++;
std::cerr << "⚠ Failed to load " << entry.path().filename() << ": " << e.what() << std::endl;
}
}
std::cout << "\n📊 Summary: " << loaded_files << "/" << total_files
<< " files loaded successfully";
if (failed_files > 0)
{
std::cout << " (" << failed_files << " failed)";
}
std::cout << "\n";
}
struct FileAnalysis
{
std::filesystem::path path;
std::unique_ptr<language::symbol::SymbolTable> symbol_table;
std::unique_ptr<language::semantic::SemanticModel> semantic_model;
};
FileAnalysis AnalyzeFile(const std::filesystem::path& path,
std::unique_ptr<language::symbol::SymbolTable> table,
WorkspaceSymbols& registry)
{
Expect(table != nullptr, "Symbol table is null");
const std::string source = ReadFile(path);
std::unique_ptr<TSParser, decltype(&ts_parser_delete)> parser(ts_parser_new(), &ts_parser_delete);
Expect(parser != nullptr, "Failed to create tree-sitter parser");
Expect(ts_parser_set_language(parser.get(), tree_sitter_tsf()), "Failed to set TSL language");
std::unique_ptr<TSTree, decltype(&ts_tree_delete)> tree(
ts_parser_parse_string(parser.get(), nullptr, source.c_str(), source.size()),
&ts_tree_delete);
Expect(tree != nullptr, "Failed to parse fixture into tree");
language::ast::Deserializer deserializer;
auto ast_result = deserializer.Parse(ts_tree_root_node(tree.get()), source);
Expect(ast_result.IsSuccess(), "AST parse failed for " + path.string());
Expect(ast_result.root != nullptr, "AST root missing for " + path.string());
language::symbol::Builder builder(*table);
builder.Build(*ast_result.root);
auto semantic_model = std::make_unique<language::semantic::SemanticModel>(*table);
language::semantic::Analyzer analyzer(*table, *semantic_model);
analyzer.SetExternalSymbolProvider(
[&registry](const std::string& name) -> std::optional<language::symbol::Symbol> {
auto it = registry.by_name.find(name);
if (it == registry.by_name.end())
{
return std::nullopt;
}
// 如果有多个同名符号,优先选择 Class
if (it->second.size() > 1)
{
[[maybe_unused]] bool found_class = false;
for (const auto& symbol : it->second)
{
if (symbol.kind() == protocol::SymbolKind::Class)
{
if (name == "Document" || name == "Endnotes")
{
std::cerr << "ExternalProvider: Selecting Class for " << name << std::endl;
}
found_class = true;
return symbol;
}
}
if (name == "Document" || name == "Endnotes")
{
std::cerr << "ExternalProvider: No Class found for " << name
<< ", size=" << it->second.size() << std::endl;
for (const auto& s : it->second)
{
std::cerr << " - kind=" << static_cast<int>(s.kind()) << std::endl;
}
}
// 如果没有 Class,返回第一个
}
return it->second.front();
});
analyzer.Analyze(*ast_result.root);
FileAnalysis analysis;
analysis.path = path;
analysis.symbol_table = std::move(table);
analysis.semantic_model = std::move(semantic_model);
return analysis;
}
std::string SymbolName(const language::symbol::SymbolTable& table, language::symbol::SymbolId id)
{
const auto* symbol = table.definition(id);
if (!symbol)
return "<unknown>";
return symbol->name();
}
void PrintCallGraph(const FileAnalysis& analysis)
{
std::cout << "\n╭─────────────────────────────────────╮\n";
std::cout << "│ 📞 Call Graph │\n";
std::cout << "╰─────────────────────────────────────╯\n";
bool any = false;
for (const auto& symbol_ref : analysis.symbol_table->all_definitions())
{
const auto& symbol = symbol_ref.get();
const auto kind = symbol.kind();
if (kind != protocol::SymbolKind::Function && kind != protocol::SymbolKind::Method)
continue;
const auto& callees = analysis.semantic_model->calls().callees(symbol.id());
if (callees.empty())
continue;
any = true;
std::cout << " ▸ " << symbol.name() << " ⟶ ";
for (size_t i = 0; i < callees.size(); ++i)
{
auto callee_symbol = analysis.symbol_table->definition(callees[i].callee);
if (callee_symbol)
{
std::cout << callee_symbol->name();
// 如果被调用的是类(对象创建),添加标记
if (callee_symbol->kind() == protocol::SymbolKind::Class)
{
std::cout << " 🏗";
}
else
{
// 调试:显示符号类型
std::cout << " [kind=" << static_cast<int>(callee_symbol->kind()) << "]";
}
}
else
{
std::cout << "<unknown>";
}
if (i + 1 < callees.size())
std::cout << " • ";
}
std::cout << '\n';
}
if (!any)
std::cout << " ⚬ No call relationships detected\n";
}
void PrintInheritanceGraph(const FileAnalysis& analysis)
{
std::cout << "\n╭─────────────────────────────────────╮\n";
std::cout << "│ 🔗 Inheritance Graph │\n";
std::cout << "╰─────────────────────────────────────╯\n";
bool any = false;
for (const auto& symbol_ref : analysis.symbol_table->all_definitions())
{
const auto& symbol = symbol_ref.get();
if (symbol.kind() != protocol::SymbolKind::Class)
continue;
const auto& bases = analysis.semantic_model->inheritance().base_classes(symbol.id());
if (bases.empty())
continue;
any = true;
std::cout << " ▸ " << symbol.name() << " ⇐ ";
for (size_t i = 0; i < bases.size(); ++i)
{
std::cout << SymbolName(*analysis.symbol_table, bases[i]);
if (i + 1 < bases.size())
std::cout << " • ";
}
std::cout << '\n';
}
if (!any)
std::cout << " ⚬ No inheritance relationships detected\n";
}
void PrintReferenceSummary(const FileAnalysis& analysis)
{
std::cout << "\n╭─────────────────────────────────────────────────────────────────╮\n";
std::cout << "│ 📊 Reference Summary │\n";
std::cout << "╰─────────────────────────────────────────────────────────────────╯\n";
bool any = false;
for (const auto& symbol_ref : analysis.symbol_table->all_definitions())
{
const auto& symbol = symbol_ref.get();
const auto& refs = analysis.semantic_model->references().references(symbol.id());
if (refs.empty())
continue;
any = true;
size_t writes = 0;
size_t reads = 0;
for (const auto& ref : refs)
{
if (ref.is_write)
++writes;
else
++reads;
}
// 打印符号名和统计信息
std::cout << "\n ┌─ " << symbol.name()
<< " (" << refs.size() << " reference" << (refs.size() > 1 ? "s" : "") << ")\n";
std::cout << " │ ├─ 📖 Reads: " << reads << '\n';
std::cout << " │ └─ ✏️ Writes: " << writes << '\n';
// 打印每个引用的位置
std::cout << " │\n";
for (const auto& ref : refs)
{
const char* icon = ref.is_write ? "✏️ " : "👁 ";
const char* type = ref.is_write ? "write" : "read ";
std::cout << " │ " << icon << " Line "
<< ref.location.start_line << ":" << ref.location.start_column
<< " - " << ref.location.end_line << ":" << ref.location.end_column
<< " [" << type << "]";
if (ref.is_definition)
{
std::cout << " 🔖";
}
std::cout << '\n';
}
}
if (!any)
std::cout << " ⚬ No tracked references\n";
else
std::cout << '\n';
}
}
export int Run(int argc, char** argv)
{
try
{
Options options;
if (!ParseArguments(argc, argv, options))
{
PrintUsage(argv[0]);
return options.help ? 0 : 1;
}
if (options.help)
{
PrintUsage(argv[0]);
return 0;
}
const auto workspace_dir = std::filesystem::absolute(options.workspace);
const auto target_file = std::filesystem::absolute(options.target_file);
Expect(std::filesystem::exists(workspace_dir) && std::filesystem::is_directory(workspace_dir),
"Workspace path is invalid: " + workspace_dir.string());
Expect(std::filesystem::exists(target_file) && std::filesystem::is_regular_file(target_file),
"Target file is invalid: " + target_file.string());
if (!options.quiet)
{
std::cout << "\n╭─────────────────────────────────────╮\n";
std::cout << "│ 🔍 Analysis Configuration │\n";
std::cout << "╰─────────────────────────────────────╯\n";
std::cout << " 📁 Workspace: " << workspace_dir << '\n';
std::cout << " 📄 Target : " << target_file << '\n';
}
WorkspaceSymbols workspace_symbols;
BuildWorkspaceSymbols(workspace_dir, target_file, workspace_symbols);
auto symbol_table = std::make_unique<language::symbol::SymbolTable>();
const auto analysis = AnalyzeFile(target_file, std::move(symbol_table), workspace_symbols);
if (options.show_calls)
PrintCallGraph(analysis);
if (options.show_inheritance)
PrintInheritanceGraph(analysis);
if (options.show_references)
PrintReferenceSummary(analysis);
if (!options.quiet)
{
std::cout << "\n╭─────────────────────────────────────╮\n";
std::cout << "│ ✓ Analysis Completed │\n";
std::cout << "╰─────────────────────────────────────╯\n";
}
return 0;
}
catch (const std::exception& e)
{
std::cerr << "\n╭─────────────────────────────────────╮\n";
std::cerr << "│ ✗ Analysis Failed │\n";
std::cerr << "╰─────────────────────────────────────╯\n";
std::cerr << " Error: " << e.what() << std::endl;
return 1;
}
}