- 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
502 lines
20 KiB
C++
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(
|
|
[®istry](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;
|
|
}
|
|
}
|