tsl-devkit/lsp-server/test/test_semantic/test_semantic.cppm

502 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
}
}
int main(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;
}
}