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';
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|