Port SelectionTree from clangd to clice (#166)

This commit is contained in:
ykiko
2025-08-05 22:10:08 +08:00
committed by GitHub
parent 139eaa44f7
commit 072ddb3a59
25 changed files with 2419 additions and 1182 deletions

View File

@@ -1,5 +1,6 @@
#pragma once
#include "AST/SourceCode.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/StringMap.h"
@@ -7,40 +8,137 @@ namespace clice::testing {
struct AnnotatedSource {
std::string content;
/// All named offsets
llvm::StringMap<std::uint32_t> offsets;
llvm::StringMap<LocalSourceRange> ranges;
std::vector<std::uint32_t> nameless_offsets;
/// Point Annotation:
/// - $(key): Marks a single point.
///
/// Range Annotation:
/// - @key[...content...]: Marks a range.
///
/// A range annotation for 'key' creates both a `ranges["key"]` and an `offsets["key"]`
/// (pointing to the start).
static AnnotatedSource from(llvm::StringRef content) {
std::string source;
llvm::StringMap<std::uint32_t> offsets;
source.reserve(content.size());
llvm::StringMap<std::uint32_t> offsets;
llvm::StringMap<LocalSourceRange> ranges;
std::vector<std::uint32_t> nameless_offsets;
std::uint32_t offset = 0;
for(uint32_t i = 0; i < content.size();) {
auto c = content[i];
std::uint32_t i = 0;
// Helper lambda to parse a point annotation $(key).
// It captures all necessary variables by reference.
// Returns true if a point was successfully parsed, false otherwise.
auto try_parse_point_annotation = [&]() -> bool {
if(content[i] != '$') {
return false;
}
// Peek ahead to see if it's "$(key)" or just "$"
if(i + 1 < content.size() && content[i + 1] == '(') {
// It's the full "$(key)" syntax
uint32_t key_start = i + 2;
size_t key_end = content.find(')', key_start);
if(key_end == llvm::StringRef::npos) {
return false;
} // Malformed
llvm::StringRef key = content.slice(key_start, key_end);
/// empty key is regarded as a nameless, and `()` is not consumed.
if(key.empty()) {
// It's the shorthand "$" syntax for an nameless key
nameless_offsets.emplace_back(offset);
i += 1; // Advance cursor past the single '$'
} else {
offsets.try_emplace(key, offset);
i = key_end + 1; // Advance cursor past the entire "$(key)"
}
return true;
} else {
// It's the shorthand "$" syntax for an nameless key
nameless_offsets.emplace_back(offset);
i += 1; // Advance cursor past the single '$'
return true;
}
};
while(i < content.size()) {
// Check for a point annotation first.
if(try_parse_point_annotation()) {
continue;
}
char c = content[i];
// Handle Range: @key[...]
if(c == '@') {
// Skip '@'
i += 1;
auto key = content.substr(i).take_until([](char c) { return c == ' '; });
offsets.try_emplace(key, offset);
const char open_bracket = '[';
const char close_bracket = ']';
llvm::StringRef key = content.substr(i).take_until(
[&](char c) { return isspace(c) || c == open_bracket; });
i += key.size();
while(i < content.size() && isspace(content[i])) {
i++;
}
assert(i < content.size() && content[i] == open_bracket &&
"Expect @key[...] for ranges.");
i += 1; // Skip '['
uint32_t begin_offset = offset;
int bracket_level = 1;
while(i < content.size() && bracket_level > 0) {
// Inside a range, we can still have nested point annotations.
if(try_parse_point_annotation()) {
continue;
}
char inner_c = content[i];
if(inner_c == open_bracket)
bracket_level++;
else if(inner_c == close_bracket)
bracket_level--;
if(bracket_level > 0) {
source += inner_c;
offset += 1;
i += 1;
} else {
i += 1; // Skip the final ']'
}
}
ranges.try_emplace(key, LocalSourceRange{begin_offset, offset});
continue;
}
if(c == '$') {
assert(i + 1 < content.size() && content[i + 1] == '(' && "expect $(name)");
i += 2;
auto key = content.substr(i).take_until([](char c) { return c == ')'; });
i += key.size() + 1;
offsets.try_emplace(key, offset);
continue;
}
i += 1;
offset += 1;
// If nothing else matched, it's a regular character.
source += c;
offset += 1;
i += 1;
}
return AnnotatedSource{std::move(source), std::move(offsets)};
return AnnotatedSource{
std::move(source),
std::move(offsets),
std::move(ranges),
std::move(nameless_offsets),
};
}
};
@@ -81,7 +179,7 @@ struct AnnotatedSources {
while(!content.empty()) {
llvm::StringRef line = content.take_front(content.find_first_of("\r\n"));
content = content.drop_front(line.size());
if(content.starts_with("\n")) {
if(content.starts_with("\r\n")) {
content = content.drop_front(2);
} else if(content.starts_with("\n")) {
content = content.drop_front(1);

View File

@@ -16,10 +16,12 @@ struct LocationChain {
LocationChain(LocationChain& outer,
std::source_location current = std::source_location::current()) :
locations{std::move(outer.locations)} {
locations{outer.locations} {
locations.emplace_back(current);
}
LocationChain(const LocationChain&) = delete;
/// Dump all locations.
void backtrace() {
for(auto location: locations) {

View File

@@ -47,6 +47,7 @@ inline void EXPECT_FAILURE(std::string message, LocationChain chain = LocationCh
inline void ASSERT_FAILURE(std::string message, LocationChain chain = LocationChain()) {
chain.backtrace();
GTEST_MESSAGE_AT_("", 0, message.c_str(), ::testing::TestPartResult::kFatalFailure);
std::abort();
}
inline void EXPECT_TRUE(auto&& value, LocationChain chain = LocationChain()) {

View File

@@ -26,20 +26,34 @@ struct Tester {
sources.add_source(name, content);
}
Tester& compile(llvm::StringRef standard = "-std=c++20") {
void add_files(llvm::StringRef main_file, llvm::StringRef content) {
src_path = main_file;
sources.add_sources(content);
}
bool compile(llvm::StringRef standard = "-std=c++20") {
auto command = std::format("clang++ {} {} -fms-extensions", standard, src_path);
database.update_command("fake", src_path, command);
params.arguments = database.get_command(src_path).arguments;
params.arguments = database.get_command(src_path, true, true).arguments;
for(auto& [file, source]: sources.all_files) {
params.add_remapped_file(file, source.content);
if(file == src_path) {
params.add_remapped_file(file, source.content);
} else {
/// FIXME: This is a workaround.
std::string path = path::is_absolute(file) ? file.str() : path::join(".", file);
params.add_remapped_file(path, source.content);
}
}
auto info = clice::compile(params);
ASSERT_TRUE(info);
if(!info) {
return false;
}
this->unit.emplace(std::move(*info));
return *this;
return true;
}
bool compile_with_pch(llvm::StringRef standard = "-std=c++20") {
@@ -93,6 +107,44 @@ struct Tester {
std::uint32_t operator[] (llvm::StringRef file, llvm::StringRef pos) {
return sources.all_files.lookup(file).offsets.lookup(pos);
}
std::uint32_t point(llvm::StringRef name = "", llvm::StringRef file = "") {
if(file.empty()) {
file = src_path;
}
auto& offsets = sources.all_files[file].offsets;
if(name.empty()) {
assert(offsets.size() == 1);
return offsets.begin()->second;
} else {
assert(offsets.contains(name));
return offsets.lookup(name);
}
}
llvm::ArrayRef<std::uint32_t> nameless_points(llvm::StringRef file = "") {
if(file.empty()) {
file = src_path;
}
return sources.all_files[file].nameless_offsets;
}
LocalSourceRange range(llvm::StringRef name = "", llvm::StringRef file = "") {
if(file.empty()) {
file = src_path;
}
auto& ranges = sources.all_files[file].ranges;
if(name.empty()) {
assert(ranges.size() == 1);
return ranges.begin()->second;
} else {
assert(ranges.contains(name));
return ranges.lookup(name);
}
}
};
struct TestFixture : ::testing::Test, Tester {};